Просмотр исходного кода

Add new Link sharing upgrade prompt in project page (#8775)

GitOrigin-RevId: 984ab0770b3b920daa945ed8b190e7debf9e4a1d
M Fahru 4 лет назад
Родитель
Сommit
a2e2dcccda

+ 15 - 0
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,
         err,

+ 2 - 0
services/web/frontend/extracted-translations.json

@@ -347,6 +347,7 @@
   "please_set_main_file": "",
   "please_set_main_file": "",
   "plus_upgraded_accounts_receive": "",
   "plus_upgraded_accounts_receive": "",
   "premium_feature": "",
   "premium_feature": "",
+  "premium_makes_collab_easier_with_features": "",
   "press_shortcut_to_open_advanced_reference_search": "",
   "press_shortcut_to_open_advanced_reference_search": "",
   "private": "",
   "private": "",
   "processing": "",
   "processing": "",
@@ -485,6 +486,7 @@
   "too_recently_compiled": "",
   "too_recently_compiled": "",
   "total_words": "",
   "total_words": "",
   "try_again": "",
   "try_again": "",
+  "try_for_free": "",
   "try_it_for_free": "",
   "try_it_for_free": "",
   "try_premium_for_free": "",
   "try_premium_for_free": "",
   "try_recompile_project_or_troubleshoot": "",
   "try_recompile_project_or_troubleshoot": "",

+ 60 - 3
services/web/frontend/js/features/share-project-modal/components/link-sharing.js

@@ -1,7 +1,7 @@
 import { useCallback, useState } from 'react'
 import { useCallback, useState } from 'react'
 import PropTypes from 'prop-types'
 import PropTypes from 'prop-types'
 import { Button, Col, Row } from 'react-bootstrap'
 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 Tooltip from '../../../shared/components/tooltip'
 import Icon from '../../../shared/components/icon'
 import Icon from '../../../shared/components/icon'
 import { useShareProjectContext } from './share-project-modal'
 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 { useProjectContext } from '../../../shared/context/project-context'
 import * as eventTracking from '../../../infrastructure/event-tracking'
 import * as eventTracking from '../../../infrastructure/event-tracking'
 import { useUserContext } from '../../../shared/context/user-context'
 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 [inflight, setInflight] = useState(false)
 
 
   const { monitorRequest } = useShareProjectContext()
   const { monitorRequest } = useShareProjectContext()
@@ -51,6 +53,7 @@ export default function LinkSharing() {
         <TokenBasedSharing
         <TokenBasedSharing
           setAccessLevel={setAccessLevel}
           setAccessLevel={setAccessLevel}
           inflight={inflight}
           inflight={inflight}
+          canAddCollaborators={canAddCollaborators}
         />
         />
       )
       )
 
 
@@ -70,6 +73,10 @@ export default function LinkSharing() {
   }
   }
 }
 }
 
 
+LinkSharing.propTypes = {
+  canAddCollaborators: PropTypes.bool,
+}
+
 function PrivateSharing({ setAccessLevel, inflight }) {
 function PrivateSharing({ setAccessLevel, inflight }) {
   return (
   return (
     <Row className="public-access-level">
     <Row className="public-access-level">
@@ -100,7 +107,7 @@ PrivateSharing.propTypes = {
   inflight: PropTypes.bool,
   inflight: PropTypes.bool,
 }
 }
 
 
-function TokenBasedSharing({ setAccessLevel, inflight }) {
+function TokenBasedSharing({ setAccessLevel, inflight, canAddCollaborators }) {
   const { tokens } = useProjectContext()
   const { tokens } = useProjectContext()
 
 
   return (
   return (
@@ -121,6 +128,7 @@ function TokenBasedSharing({ setAccessLevel, inflight }) {
         <span>&nbsp;&nbsp;</span>
         <span>&nbsp;&nbsp;</span>
         <LinkSharingInfo />
         <LinkSharingInfo />
       </Col>
       </Col>
+      <LinkSharingUpgradePrompt canAddCollaborators={canAddCollaborators} />
       <Col xs={12} className="access-token-display-area">
       <Col xs={12} className="access-token-display-area">
         <div className="access-token-wrapper">
         <div className="access-token-wrapper">
           <strong>
           <strong>
@@ -150,6 +158,7 @@ function TokenBasedSharing({ setAccessLevel, inflight }) {
 TokenBasedSharing.propTypes = {
 TokenBasedSharing.propTypes = {
   setAccessLevel: PropTypes.func.isRequired,
   setAccessLevel: PropTypes.func.isRequired,
   inflight: PropTypes.bool,
   inflight: PropTypes.bool,
+  canAddCollaborators: PropTypes.bool,
 }
 }
 
 
 function LegacySharing({ accessLevel, setAccessLevel, inflight }) {
 function LegacySharing({ accessLevel, setAccessLevel, inflight }) {
@@ -257,3 +266,51 @@ function LinkSharingInfo() {
     </Tooltip>
     </Tooltip>
   )
   )
 }
 }
+
+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 (
+    <>
+      <Col xs={12} className="link-sharing-upgrade-prompt">
+        <p>
+          <Trans
+            i18nKey="premium_makes_collab_easier_with_features"
+            components={[<b />, <b />, <b />]} // eslint-disable-line react/jsx-key
+          />
+        </p>
+        <StartFreeTrialButton
+          buttonStyle="success"
+          setStartedFreeTrial={setStartedFreeTrial}
+          source="link-sharing"
+        />
+      </Col>
+      {startedFreeTrial && (
+        <p className="small teaser-refresh-label">
+          {t('refresh_page_after_starting_free_trial')}
+        </p>
+      )}
+    </>
+  )
+}
+
+LinkSharingUpgradePrompt.propTypes = {
+  canAddCollaborators: PropTypes.bool,
+}

+ 6 - 19
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 { Row } from 'react-bootstrap'
 import AddCollaborators from './add-collaborators'
 import AddCollaborators from './add-collaborators'
 import AddCollaboratorsUpgrade from './add-collaborators-upgrade'
 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 (
   return (
     <Row className="invite-controls">
     <Row className="invite-controls">
       {canAddCollaborators ? <AddCollaborators /> : <AddCollaboratorsUpgrade />}
       {canAddCollaborators ? <AddCollaborators /> : <AddCollaboratorsUpgrade />}
     </Row>
     </Row>
   )
   )
 }
 }
+
+SendInvites.propTypes = {
+  canAddCollaborators: PropTypes.bool,
+}

+ 32 - 7
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 { useEditorContext } from '../../../shared/context/editor-context'
 import { useProjectContext } from '../../../shared/context/project-context'
 import { useProjectContext } from '../../../shared/context/project-context'
 import { useSplitTestContext } from '../../../shared/context/split-test-context'
 import { useSplitTestContext } from '../../../shared/context/split-test-context'
+import { useMemo } from 'react'
 import { Row } from 'react-bootstrap'
 import { Row } from 'react-bootstrap'
 import PropTypes from 'prop-types'
 import PropTypes from 'prop-types'
 import RecaptchaConditions from '../../../shared/components/recaptcha-conditions'
 import RecaptchaConditions from '../../../shared/components/recaptcha-conditions'
@@ -17,8 +18,22 @@ export default function ShareModalBody() {
     splitTestVariants: PropTypes.object,
     splitTestVariants: PropTypes.object,
   })
   })
 
 
+  const { members, invites, features } = useProjectContext()
   const { isProjectOwner } = useEditorContext()
   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']) {
   switch (splitTestVariants['project-share-modal-paywall']) {
     case 'new-copy-top':
     case 'new-copy-top':
@@ -26,7 +41,7 @@ export default function ShareModalBody() {
         <>
         <>
           {isProjectOwner ? (
           {isProjectOwner ? (
             <>
             <>
-              <SendInvites />
+              <SendInvites canAddCollaborators={canAddCollaborators} />
               <Row className="public-access-level" />
               <Row className="public-access-level" />
             </>
             </>
           ) : (
           ) : (
@@ -54,7 +69,7 @@ export default function ShareModalBody() {
           {isProjectOwner && (
           {isProjectOwner && (
             <>
             <>
               <br />
               <br />
-              <LinkSharing />
+              <LinkSharing canAddCollaborators={canAddCollaborators} />
             </>
             </>
           )}
           )}
 
 
@@ -84,12 +99,16 @@ export default function ShareModalBody() {
             />
             />
           ))}
           ))}
 
 
-          {isProjectOwner ? <SendInvites /> : <SendInvitesNotice />}
+          {isProjectOwner ? (
+            <SendInvites canAddCollaborators={canAddCollaborators} />
+          ) : (
+            <SendInvitesNotice />
+          )}
 
 
           {isProjectOwner && (
           {isProjectOwner && (
             <>
             <>
               <br />
               <br />
-              <LinkSharing />
+              <LinkSharing canAddCollaborators={canAddCollaborators} />
             </>
             </>
           )}
           )}
 
 
@@ -103,7 +122,9 @@ export default function ShareModalBody() {
     default:
     default:
       return (
       return (
         <>
         <>
-          {isProjectOwner && <LinkSharing />}
+          {isProjectOwner && (
+            <LinkSharing canAddCollaborators={canAddCollaborators} />
+          )}
 
 
           <OwnerInfo />
           <OwnerInfo />
 
 
@@ -123,7 +144,11 @@ export default function ShareModalBody() {
             />
             />
           ))}
           ))}
 
 
-          {isProjectOwner ? <SendInvites /> : <SendInvitesNotice />}
+          {isProjectOwner ? (
+            <SendInvites canAddCollaborators={canAddCollaborators} />
+          ) : (
+            <SendInvitesNotice />
+          )}
 
 
           {!window.ExposedSettings.recaptchaDisabled?.invite && (
           {!window.ExposedSettings.recaptchaDisabled?.invite && (
             <RecaptchaConditions />
             <RecaptchaConditions />

+ 16 - 0
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 {
   .public-access-level.public-access-level--notice {

+ 1 - 0
services/web/locales/en.json

@@ -1790,6 +1790,7 @@
   "trial_last_day": "This is the last day of your <b>Overleaf Premium</b> trial",
   "trial_last_day": "This is the last day of your <b>Overleaf Premium</b> trial",
   "trial_remaining_days": "__days__ more days on your <b>Overleaf Premium</b> trial",
   "trial_remaining_days": "__days__ more days on your <b>Overleaf Premium</b> 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</0>.",
   "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</0>.",
+  "premium_makes_collab_easier_with_features": "<0>Overleaf Premium</0> makes collaboration with others easier with features such as <1>track changes</1> and <2>full document history.</2>",
   "would_you_like_to_see_a_university_subscription": "Would you like to see a university-wide __appName__ subscription at your university?",
   "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.",
   "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",
   "show_your_support": "Show your support",