diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index 0195926c44..c50c41353c 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -968,6 +968,21 @@ const ProjectController = { } }, ], + linkSharingUpgradePromptAssignment(cb) { + SplitTestHandler.getAssignment( + req, + res, + 'link-sharing-upgrade-prompt', + (error, assignment) => { + // do not fail editor load if assignment fails + if (error) { + cb(null, { variant: 'default' }) + } else { + cb(null, assignment) + } + } + ) + }, }, ( err, diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 11fc720261..97ae4e8f4c 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -347,6 +347,7 @@ "please_set_main_file": "", "plus_upgraded_accounts_receive": "", "premium_feature": "", + "premium_makes_collab_easier_with_features": "", "press_shortcut_to_open_advanced_reference_search": "", "private": "", "processing": "", @@ -485,6 +486,7 @@ "too_recently_compiled": "", "total_words": "", "try_again": "", + "try_for_free": "", "try_it_for_free": "", "try_premium_for_free": "", "try_recompile_project_or_troubleshoot": "", diff --git a/services/web/frontend/js/features/share-project-modal/components/link-sharing.js b/services/web/frontend/js/features/share-project-modal/components/link-sharing.js index 2c4fdc0ef9..a4912076d2 100644 --- a/services/web/frontend/js/features/share-project-modal/components/link-sharing.js +++ b/services/web/frontend/js/features/share-project-modal/components/link-sharing.js @@ -1,7 +1,7 @@ import { useCallback, useState } from 'react' import PropTypes from 'prop-types' import { Button, Col, Row } from 'react-bootstrap' -import { Trans } from 'react-i18next' +import { Trans, useTranslation } from 'react-i18next' import Tooltip from '../../../shared/components/tooltip' import Icon from '../../../shared/components/icon' import { useShareProjectContext } from './share-project-modal' @@ -10,8 +10,10 @@ import CopyLink from '../../../shared/components/copy-link' import { useProjectContext } from '../../../shared/context/project-context' import * as eventTracking from '../../../infrastructure/event-tracking' import { useUserContext } from '../../../shared/context/user-context' +import StartFreeTrialButton from '../../../shared/components/start-free-trial-button' +import { useSplitTestContext } from '../../../shared/context/split-test-context' -export default function LinkSharing() { +export default function LinkSharing({ canAddCollaborators }) { const [inflight, setInflight] = useState(false) const { monitorRequest } = useShareProjectContext() @@ -51,6 +53,7 @@ export default function LinkSharing() { ) @@ -70,6 +73,10 @@ export default function LinkSharing() { } } +LinkSharing.propTypes = { + canAddCollaborators: PropTypes.bool, +} + function PrivateSharing({ setAccessLevel, inflight }) { return ( @@ -100,7 +107,7 @@ PrivateSharing.propTypes = { inflight: PropTypes.bool, } -function TokenBasedSharing({ setAccessLevel, inflight }) { +function TokenBasedSharing({ setAccessLevel, inflight, canAddCollaborators }) { const { tokens } = useProjectContext() return ( @@ -121,6 +128,7 @@ function TokenBasedSharing({ setAccessLevel, inflight }) {    +
@@ -150,6 +158,7 @@ function TokenBasedSharing({ setAccessLevel, inflight }) { TokenBasedSharing.propTypes = { setAccessLevel: PropTypes.func.isRequired, inflight: PropTypes.bool, + canAddCollaborators: PropTypes.bool, } function LegacySharing({ accessLevel, setAccessLevel, inflight }) { @@ -257,3 +266,51 @@ function LinkSharingInfo() { ) } + +function LinkSharingUpgradePrompt({ canAddCollaborators }) { + const [startedFreeTrial, setStartedFreeTrial] = useState(false) + const { t } = useTranslation() + const { splitTestVariants } = useSplitTestContext() + const linkSharingUpgradePromptActive = + splitTestVariants['link-sharing-upgrade-prompt'] === 'active' + + const user = useUserContext({ + allowedFreeTrial: PropTypes.bool, + }) + + const showLinkSharingUpgradePrompt = + linkSharingUpgradePromptActive && + user.allowedFreeTrial && + canAddCollaborators + + if (!showLinkSharingUpgradePrompt) { + return null + } + + return ( + <> + +

+ , , ]} // eslint-disable-line react/jsx-key + /> +

+ + + {startedFreeTrial && ( +

+ {t('refresh_page_after_starting_free_trial')} +

+ )} + + ) +} + +LinkSharingUpgradePrompt.propTypes = { + canAddCollaborators: PropTypes.bool, +} diff --git a/services/web/frontend/js/features/share-project-modal/components/send-invites.js b/services/web/frontend/js/features/share-project-modal/components/send-invites.js index d538dfc202..97f8d8f652 100644 --- a/services/web/frontend/js/features/share-project-modal/components/send-invites.js +++ b/services/web/frontend/js/features/share-project-modal/components/send-invites.js @@ -1,29 +1,16 @@ -import { useMemo } from 'react' import { Row } from 'react-bootstrap' import AddCollaborators from './add-collaborators' import AddCollaboratorsUpgrade from './add-collaborators-upgrade' -import { useProjectContext } from '../../../shared/context/project-context' - -export default function SendInvites() { - const { members, invites, features } = useProjectContext() - - // whether the project has not reached the collaborator limit - const canAddCollaborators = useMemo(() => { - if (!features) { - return false - } - - if (features.collaborators === -1) { - // infinite collaborators - return true - } - - return members.length + invites.length < features.collaborators - }, [members, invites, features]) +import PropTypes from 'prop-types' +export default function SendInvites({ canAddCollaborators }) { return ( {canAddCollaborators ? : } ) } + +SendInvites.propTypes = { + canAddCollaborators: PropTypes.bool, +} diff --git a/services/web/frontend/js/features/share-project-modal/components/share-modal-body.js b/services/web/frontend/js/features/share-project-modal/components/share-modal-body.js index bc7f37bf5e..85a36650d3 100644 --- a/services/web/frontend/js/features/share-project-modal/components/share-modal-body.js +++ b/services/web/frontend/js/features/share-project-modal/components/share-modal-body.js @@ -8,6 +8,7 @@ import SendInvitesNotice from './send-invites-notice' import { useEditorContext } from '../../../shared/context/editor-context' import { useProjectContext } from '../../../shared/context/project-context' import { useSplitTestContext } from '../../../shared/context/split-test-context' +import { useMemo } from 'react' import { Row } from 'react-bootstrap' import PropTypes from 'prop-types' import RecaptchaConditions from '../../../shared/components/recaptcha-conditions' @@ -17,8 +18,22 @@ export default function ShareModalBody() { splitTestVariants: PropTypes.object, }) + const { members, invites, features } = useProjectContext() const { isProjectOwner } = useEditorContext() - const { invites, members } = useProjectContext() + + // whether the project has not reached the collaborator limit + const canAddCollaborators = useMemo(() => { + if (!isProjectOwner || !features) { + return false + } + + if (features.collaborators === -1) { + // infinite collaborators + return true + } + + return members.length + invites.length < features.collaborators + }, [members, invites, features, isProjectOwner]) switch (splitTestVariants['project-share-modal-paywall']) { case 'new-copy-top': @@ -26,7 +41,7 @@ export default function ShareModalBody() { <> {isProjectOwner ? ( <> - + ) : ( @@ -54,7 +69,7 @@ export default function ShareModalBody() { {isProjectOwner && ( <>
- + )} @@ -84,12 +99,16 @@ export default function ShareModalBody() { /> ))} - {isProjectOwner ? : } + {isProjectOwner ? ( + + ) : ( + + )} {isProjectOwner && ( <>
- + )} @@ -103,7 +122,9 @@ export default function ShareModalBody() { default: return ( <> - {isProjectOwner && } + {isProjectOwner && ( + + )} @@ -123,7 +144,11 @@ export default function ShareModalBody() { /> ))} - {isProjectOwner ? : } + {isProjectOwner ? ( + + ) : ( + + )} {!window.ExposedSettings.recaptchaDisabled?.invite && ( diff --git a/services/web/frontend/stylesheets/app/editor/share.less b/services/web/frontend/stylesheets/app/editor/share.less index d8b35959d4..304eb7c149 100644 --- a/services/web/frontend/stylesheets/app/editor/share.less +++ b/services/web/frontend/stylesheets/app/editor/share.less @@ -45,6 +45,22 @@ } } } + .link-sharing-upgrade-prompt { + background-color: @ol-blue-gray-0; + margin-top: @margin-sm; + padding: @padding-sm 15px; + display: flex; + align-items: center; + border-radius: 3px; + + p { + margin-bottom: 0; + margin-right: 15px; + color: @ol-blue-gray-3; + font-size: 14px; + line-height: 23px; + } + } } .public-access-level.public-access-level--notice { diff --git a/services/web/locales/en.json b/services/web/locales/en.json index df1f2282b1..407fa4ae1b 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1790,6 +1790,7 @@ "trial_last_day": "This is the last day of your Overleaf Premium trial", "trial_remaining_days": "__days__ more days on your Overleaf Premium trial", "get_most_subscription_by_checking_premium_features": "Get the most out of your __appName__ subscription by checking out the list of <0>__appName__’s premium features.", + "premium_makes_collab_easier_with_features": "<0>Overleaf Premium makes collaboration with others easier with features such as <1>track changes and <2>full document history.", "would_you_like_to_see_a_university_subscription": "Would you like to see a university-wide __appName__ subscription at your university?", "student_and_faculty_support_make_difference": "Student and faculty support make a difference! We can share this information with our contacts at your university when discussing an Overleaf institutional account.", "show_your_support": "Show your support",