mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 04:43:51 -05:00
Add StartFreeTrialButton
GitOrigin-RevId: dd9ab4bb12e8b9071b2a869e2d452945b49d9cd7
This commit is contained in:
parent
e6acfda6e3
commit
e832c9ed70
11 changed files with 142 additions and 41 deletions
|
@ -89,4 +89,9 @@ export const decorators = [withTheme]
|
||||||
window.ExposedSettings = {
|
window.ExposedSettings = {
|
||||||
maxEntitiesPerProject: 10,
|
maxEntitiesPerProject: 10,
|
||||||
maxUploadSize: 5 * 1024 * 1024,
|
maxUploadSize: 5 * 1024 * 1024,
|
||||||
|
enableSubscriptions: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
window.user = {
|
||||||
|
id: 'storybook',
|
||||||
}
|
}
|
||||||
|
|
|
@ -244,6 +244,7 @@
|
||||||
"unlimited_projects": "",
|
"unlimited_projects": "",
|
||||||
"unlink_github_repository": "",
|
"unlink_github_repository": "",
|
||||||
"unlinking": "",
|
"unlinking": "",
|
||||||
|
"upgrade": "",
|
||||||
"upgrade_for_longer_compiles": "",
|
"upgrade_for_longer_compiles": "",
|
||||||
"upload": "",
|
"upload": "",
|
||||||
"use_your_own_machine": "",
|
"use_your_own_machine": "",
|
||||||
|
|
|
@ -5,7 +5,7 @@ import PreviewLogsPaneEntry from './preview-logs-pane-entry'
|
||||||
import Icon from '../../../shared/components/icon'
|
import Icon from '../../../shared/components/icon'
|
||||||
import { useApplicationContext } from '../../../shared/context/application-context'
|
import { useApplicationContext } from '../../../shared/context/application-context'
|
||||||
import { useEditorContext } from '../../../shared/context/editor-context'
|
import { useEditorContext } from '../../../shared/context/editor-context'
|
||||||
import { startFreeTrial } from '../../../main/account-upgrade'
|
import StartFreeTrialButton from '../../../shared/components/start-free-trial-button'
|
||||||
|
|
||||||
function PreviewError({ name }) {
|
function PreviewError({ name }) {
|
||||||
const { isProjectOwner } = useEditorContext({
|
const { isProjectOwner } = useEditorContext({
|
||||||
|
@ -84,10 +84,6 @@ function PreviewError({ name }) {
|
||||||
function TimeoutUpgradePrompt({ isProjectOwner }) {
|
function TimeoutUpgradePrompt({ isProjectOwner }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
function handleStartFreeTrialClick() {
|
|
||||||
startFreeTrial('compile-timeout')
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeoutUpgradePromptContent = (
|
const timeoutUpgradePromptContent = (
|
||||||
<>
|
<>
|
||||||
<p>{t('free_accounts_have_timeout_upgrade_to_increase')}</p>
|
<p>{t('free_accounts_have_timeout_upgrade_to_increase')}</p>
|
||||||
|
@ -128,12 +124,11 @@ function TimeoutUpgradePrompt({ isProjectOwner }) {
|
||||||
</div>
|
</div>
|
||||||
{isProjectOwner ? (
|
{isProjectOwner ? (
|
||||||
<p className="text-center">
|
<p className="text-center">
|
||||||
<button
|
<StartFreeTrialButton
|
||||||
className="btn btn-success row-spaced-small"
|
source="compile-timeout"
|
||||||
onClick={handleStartFreeTrialClick}
|
buttonStyle="success"
|
||||||
>
|
classes={{ button: 'row-spaced-small' }}
|
||||||
{t('start_free_trial')}
|
/>
|
||||||
</button>
|
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -2,12 +2,10 @@ import React, { useState } from 'react'
|
||||||
import { Trans } from 'react-i18next'
|
import { Trans } from 'react-i18next'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import Icon from '../../../shared/components/icon'
|
import Icon from '../../../shared/components/icon'
|
||||||
import { startFreeTrial, upgradePlan } from '../../../main/account-upgrade'
|
import { upgradePlan } from '../../../main/account-upgrade'
|
||||||
import { useShareProjectContext } from './share-project-modal'
|
import StartFreeTrialButton from '../../../shared/components/start-free-trial-button'
|
||||||
|
|
||||||
export default function AddCollaboratorsUpgrade() {
|
export default function AddCollaboratorsUpgrade() {
|
||||||
const { eventTracking } = useShareProjectContext()
|
|
||||||
|
|
||||||
const [startedFreeTrial, setStartedFreeTrial] = useState(false)
|
const [startedFreeTrial, setStartedFreeTrial] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -54,20 +52,11 @@ export default function AddCollaboratorsUpgrade() {
|
||||||
|
|
||||||
<p className="text-center row-spaced-thin">
|
<p className="text-center row-spaced-thin">
|
||||||
{window.user.allowedFreeTrial ? (
|
{window.user.allowedFreeTrial ? (
|
||||||
<Button
|
<StartFreeTrialButton
|
||||||
bsStyle="success"
|
buttonStyle="success"
|
||||||
onClick={() => {
|
setStartedFreeTrial={setStartedFreeTrial}
|
||||||
startFreeTrial(
|
source="projectMembers"
|
||||||
'projectMembers',
|
/>
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
eventTracking
|
|
||||||
)
|
|
||||||
setStartedFreeTrial(true)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trans i18nKey="start_free_trial" />
|
|
||||||
</Button>
|
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
bsStyle="success"
|
bsStyle="success"
|
||||||
|
@ -76,7 +65,7 @@ export default function AddCollaboratorsUpgrade() {
|
||||||
setStartedFreeTrial(true)
|
setStartedFreeTrial(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trans i18nKey="start_free_trial" />
|
<Trans i18nKey="upgrade" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -16,9 +16,6 @@ ShareProjectContext.Provider.propTypes = {
|
||||||
isAdmin: PropTypes.bool.isRequired,
|
isAdmin: PropTypes.bool.isRequired,
|
||||||
updateProject: PropTypes.func.isRequired,
|
updateProject: PropTypes.func.isRequired,
|
||||||
monitorRequest: PropTypes.func.isRequired,
|
monitorRequest: PropTypes.func.isRequired,
|
||||||
eventTracking: PropTypes.shape({
|
|
||||||
sendMB: PropTypes.func.isRequired,
|
|
||||||
}),
|
|
||||||
inFlight: PropTypes.bool,
|
inFlight: PropTypes.bool,
|
||||||
setInFlight: PropTypes.func,
|
setInFlight: PropTypes.func,
|
||||||
error: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
|
error: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
|
||||||
|
@ -87,7 +84,6 @@ export default function ShareProjectModal({
|
||||||
show,
|
show,
|
||||||
animation = true,
|
animation = true,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
eventTracking,
|
|
||||||
ide,
|
ide,
|
||||||
}) {
|
}) {
|
||||||
const [inFlight, setInFlight] = useState(false)
|
const [inFlight, setInFlight] = useState(false)
|
||||||
|
@ -148,7 +144,6 @@ export default function ShareProjectModal({
|
||||||
value={{
|
value={{
|
||||||
isAdmin,
|
isAdmin,
|
||||||
updateProject,
|
updateProject,
|
||||||
eventTracking,
|
|
||||||
monitorRequest,
|
monitorRequest,
|
||||||
inFlight,
|
inFlight,
|
||||||
setInFlight,
|
setInFlight,
|
||||||
|
@ -176,7 +171,4 @@ ShareProjectModal.propTypes = {
|
||||||
$scope: PropTypes.object.isRequired,
|
$scope: PropTypes.object.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
show: PropTypes.bool.isRequired,
|
show: PropTypes.bool.isRequired,
|
||||||
eventTracking: PropTypes.shape({
|
|
||||||
sendMB: PropTypes.func.isRequired,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
|
13
services/web/frontend/js/infrastructure/event-tracking.js
Normal file
13
services/web/frontend/js/infrastructure/event-tracking.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { postJSON } from './fetch-json'
|
||||||
|
|
||||||
|
export function send(category, action, label, value) {
|
||||||
|
if (typeof window.ga === 'function') {
|
||||||
|
window.ga('send', 'event', category, action, label, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendMB(key, body = {}) {
|
||||||
|
postJSON(`/event/${key}`, { body }).catch(() => {
|
||||||
|
// ignore errors
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
import React, { useCallback } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Button } from 'react-bootstrap'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import * as eventTracking from '../../infrastructure/event-tracking'
|
||||||
|
|
||||||
|
export default function StartFreeTrialButton({
|
||||||
|
buttonStyle = 'info',
|
||||||
|
children,
|
||||||
|
classes = {},
|
||||||
|
setStartedFreeTrial,
|
||||||
|
source,
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const handleClick = useCallback(
|
||||||
|
event => {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
eventTracking.send('subscription-funnel', 'upgraded-free-trial', source)
|
||||||
|
|
||||||
|
const plan = 'collaborator_free_trial_7_days'
|
||||||
|
|
||||||
|
eventTracking.sendMB('subscription-start-trial', { source, plan })
|
||||||
|
|
||||||
|
if (setStartedFreeTrial) {
|
||||||
|
setStartedFreeTrial(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
planCode: plan,
|
||||||
|
ssp: 'true',
|
||||||
|
itm_campaign: source,
|
||||||
|
})
|
||||||
|
|
||||||
|
window.open(`/user/subscription/new?${params}`)
|
||||||
|
},
|
||||||
|
[setStartedFreeTrial, source]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
bsStyle={buttonStyle}
|
||||||
|
onClick={handleClick}
|
||||||
|
className={classes.button}
|
||||||
|
>
|
||||||
|
{children || t('start_free_trial')}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
StartFreeTrialButton.propTypes = {
|
||||||
|
buttonStyle: PropTypes.string,
|
||||||
|
children: PropTypes.any,
|
||||||
|
classes: PropTypes.shape({
|
||||||
|
button: PropTypes.string.isRequired,
|
||||||
|
}),
|
||||||
|
setStartedFreeTrial: PropTypes.func,
|
||||||
|
source: PropTypes.string.isRequired,
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ export function setupContext() {
|
||||||
window.project_id = '1234'
|
window.project_id = '1234'
|
||||||
window.user = {
|
window.user = {
|
||||||
id: 'fake_user',
|
id: 'fake_user',
|
||||||
|
allowedFreeTrial: true,
|
||||||
}
|
}
|
||||||
let $scope = {}
|
let $scope = {}
|
||||||
if (window._ide) {
|
if (window._ide) {
|
||||||
|
|
|
@ -100,7 +100,7 @@ function SampleHumanReadableHintComponent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'PreviewLogsPaneEntry',
|
title: 'Preview Logs / Entry',
|
||||||
component: PreviewLogsPaneEntry,
|
component: PreviewLogsPaneEntry,
|
||||||
args: {
|
args: {
|
||||||
sourceLocation: {
|
sourceLocation: {
|
||||||
|
@ -113,7 +113,7 @@ export default {
|
||||||
The LaTeX compiler output
|
The LaTeX compiler output
|
||||||
* With a lot of details
|
* With a lot of details
|
||||||
|
|
||||||
Wrapped in an HTML <pre> element with
|
Wrapped in an HTML <pre> element with
|
||||||
preformatted text which is to be presented exactly
|
preformatted text which is to be presented exactly
|
||||||
as written in the HTML file
|
as written in the HTML file
|
||||||
|
|
||||||
|
@ -129,7 +129,7 @@ LaTeX Font Info: External font \`cmex10' loaded for size
|
||||||
<recently read> \\Zlpha
|
<recently read> \\Zlpha
|
||||||
|
|
||||||
main.tex, line 23
|
main.tex, line 23
|
||||||
|
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
44
services/web/frontend/stories/preview-logs-pane.stories.js
Normal file
44
services/web/frontend/stories/preview-logs-pane.stories.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PreviewLogsPane from '../js/features/preview/components/preview-logs-pane'
|
||||||
|
import { EditorProvider } from '../js/shared/context/editor-context'
|
||||||
|
import { ApplicationProvider } from '../js/shared/context/application-context'
|
||||||
|
import useFetchMock from './hooks/use-fetch-mock'
|
||||||
|
|
||||||
|
export const TimedOutError = args => {
|
||||||
|
useFetchMock(fetchMock => {
|
||||||
|
fetchMock.post('express:/event/:key', 202)
|
||||||
|
})
|
||||||
|
|
||||||
|
const ide = {
|
||||||
|
$scope: {
|
||||||
|
$watch: () => () => null,
|
||||||
|
project: {
|
||||||
|
owner: {
|
||||||
|
_id: window.user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ApplicationProvider>
|
||||||
|
<EditorProvider ide={ide} settings={{}}>
|
||||||
|
<PreviewLogsPane {...args} />
|
||||||
|
</EditorProvider>
|
||||||
|
</ApplicationProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TimedOutError.args = {
|
||||||
|
errors: {
|
||||||
|
timedout: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Preview Logs / Pane',
|
||||||
|
component: PreviewLogsPane,
|
||||||
|
argTypes: {
|
||||||
|
onLogEntryLocationClick: { action: 'log entry location' },
|
||||||
|
onClearCache: { action: 'clear cache' },
|
||||||
|
},
|
||||||
|
}
|
|
@ -71,6 +71,8 @@ const setupFetchMock = () => {
|
||||||
.post('express:/project/:projectId/invite/:inviteId/resend', 200, {
|
.post('express:/project/:projectId/invite/:inviteId/resend', 200, {
|
||||||
delay,
|
delay,
|
||||||
})
|
})
|
||||||
|
// send analytics event
|
||||||
|
.post('express:/event/:key', 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ideWithProject = project => {
|
const ideWithProject = project => {
|
||||||
|
|
Loading…
Reference in a new issue