Ver Fonte

Merge pull request #24697 from overleaf/jdt-show-addons-via-wf

Display When Ai Assist Is Granted Via Writefull

GitOrigin-RevId: 91f6e1843e2e1d1f7b3a49d95f31603e838c5545
Jimmy Domagala-Tang há 1 ano atrás
pai
commit
f95bf41824

+ 6 - 2
services/web/app/src/Features/Subscription/SubscriptionController.js

@@ -154,6 +154,9 @@ async function userSubscriptionPage(req, res) {
     )
   }
 
+  const hasAiAssistViaWritefull =
+    await FeaturesUpdater.promises.hasFeaturesViaWritefull(user._id)
+
   const data = {
     title: 'your_subscription',
     plans: plansData?.plans,
@@ -176,6 +179,7 @@ async function userSubscriptionPage(req, res) {
     groupSettingsEnabledFor,
     isManagedAccount: !!req.managedBy,
     userRestrictions: Array.from(req.userRestrictions || []),
+    hasAiAssistViaWritefull,
   }
   res.render('subscriptions/dashboard-react', data)
 }
@@ -322,13 +326,13 @@ async function previewAddonPurchase(req, res) {
     subscriptionChange =
       await SubscriptionHandler.promises.previewAddonPurchase(userId, addOnCode)
 
-    const hasBundleViaWritefull =
+    const hasAiAssistViaWritefull =
       await FeaturesUpdater.promises.hasFeaturesViaWritefull(userId)
     const isAiUpgrade =
       PaymentProviderEntities.subscriptionChangeIsAiAssistUpgrade(
         subscriptionChange
       )
-    if (hasBundleViaWritefull && isAiUpgrade) {
+    if (hasAiAssistViaWritefull && isAiUpgrade) {
       return res.redirect(
         '/user/subscription?redirect-reason=writefull-entitled'
       )

+ 25 - 24
services/web/app/views/subscriptions/dashboard-react.pug

@@ -1,33 +1,34 @@
 extends ../layout-react
 
 block entrypointVar
-	- entrypoint = 'pages/user/subscription/dashboard'
+  - entrypoint = 'pages/user/subscription/dashboard'
 
 block head-scripts
-	script(type="text/javascript", nonce=scriptNonce, src="https://js.recurly.com/v4/recurly.js")
+  script(type="text/javascript", nonce=scriptNonce, src="https://js.recurly.com/v4/recurly.js")
 
 block append meta
-	meta(name="ol-subscription" data-type="json" content=personalSubscription)
-	meta(name="ol-userCanExtendTrial" data-type="boolean" content=userCanExtendTrial)
-	meta(name="ol-managedGroupSubscriptions" data-type="json" content=managedGroupSubscriptions)
-	meta(name="ol-memberGroupSubscriptions" data-type="json" content=memberGroupSubscriptions)
-	meta(name="ol-managedInstitutions" data-type="json" content=managedInstitutions)
-	meta(name="ol-managedPublishers" data-type="json" content=managedPublishers)
-	meta(name="ol-planCodesChangingAtTermEnd" data-type="json", content=planCodesChangingAtTermEnd)
-	meta(name="ol-currentInstitutionsWithLicence" data-type="json" content=currentInstitutionsWithLicence)
-	meta(name="ol-hasSubscription" data-type="boolean" content=hasSubscription)
-	meta(name="ol-fromPlansPage" data-type="boolean" content=fromPlansPage)
-	meta(name="ol-plans", data-type="json" content=plans)
-	meta(name="ol-groupSettingsAdvertisedFor", data-type="json" content=groupSettingsAdvertisedFor)
-	meta(name="ol-canUseFlexibleLicensing", data-type="boolean", content=canUseFlexibleLicensing)
-	meta(name="ol-showGroupDiscount", data-type="boolean", content=showGroupDiscount)
-	meta(name="ol-groupSettingsEnabledFor", data-type="json" content=groupSettingsEnabledFor)
-	meta(name="ol-user" data-type="json" content=user)
-	if (personalSubscription && personalSubscription.payment)
-		meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey)
-		meta(name="ol-recommendedCurrency" content=personalSubscription.payment.currency)
-		meta(name="ol-groupPlans" data-type="json" content=groupPlans)
+  meta(name="ol-subscription" data-type="json" content=personalSubscription)
+  meta(name="ol-userCanExtendTrial" data-type="boolean" content=userCanExtendTrial)
+  meta(name="ol-managedGroupSubscriptions" data-type="json" content=managedGroupSubscriptions)
+  meta(name="ol-memberGroupSubscriptions" data-type="json" content=memberGroupSubscriptions)
+  meta(name="ol-managedInstitutions" data-type="json" content=managedInstitutions)
+  meta(name="ol-managedPublishers" data-type="json" content=managedPublishers)
+  meta(name="ol-planCodesChangingAtTermEnd" data-type="json", content=planCodesChangingAtTermEnd)
+  meta(name="ol-currentInstitutionsWithLicence" data-type="json" content=currentInstitutionsWithLicence)
+  meta(name="ol-hasSubscription" data-type="boolean" content=hasSubscription)
+  meta(name="ol-fromPlansPage" data-type="boolean" content=fromPlansPage)
+  meta(name="ol-plans" data-type="json" content=plans)
+  meta(name="ol-groupSettingsAdvertisedFor" data-type="json" content=groupSettingsAdvertisedFor)
+  meta(name="ol-canUseFlexibleLicensing" data-type="boolean", content=canUseFlexibleLicensing)
+  meta(name="ol-showGroupDiscount" data-type="boolean", content=showGroupDiscount)
+  meta(name="ol-groupSettingsEnabledFor" data-type="json" content=groupSettingsEnabledFor)
+  meta(name="ol-hasAiAssistViaWritefull" data-type="boolean", content=hasAiAssistViaWritefull)
+  meta(name="ol-user" data-type="json" content=user)
+  if (personalSubscription && personalSubscription.payment)
+    meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey)
+    meta(name="ol-recommendedCurrency" content=personalSubscription.payment.currency)
+    meta(name="ol-groupPlans" data-type="json" content=groupPlans)
 
 block content
-	main.content.content-alt#main-content
-		#subscription-dashboard-root
+  main.content.content-alt#main-content
+    #subscription-dashboard-root

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

@@ -103,6 +103,7 @@
   "aggregate_changed": "",
   "aggregate_to": "",
   "agree_with_the_terms": "",
+  "ai_assist_in_overleaf_is_included_via_writefull": "",
   "ai_assistance_to_help_you": "",
   "ai_based_language_tools": "",
   "ai_can_make_mistakes": "",
@@ -671,6 +672,7 @@
   "go_to_pdf_location_in_code": "",
   "go_to_settings": "",
   "go_to_subscriptions": "",
+  "go_to_writefull": "",
   "good_news_you_already_purchased_this_add_on": "",
   "good_news_you_are_already_receiving_this_add_on_via_writefull": "",
   "group_admin": "",
@@ -789,6 +791,7 @@
   "include_results_from_your_reference_manager": "",
   "include_results_from_your_x_account": "",
   "include_the_error_message_and_ai_response": "",
+  "included_as_part_of_your_writefull_subscription": "",
   "increase_indent": "",
   "increased_compile_timeout": "",
   "inline": "",
@@ -979,6 +982,7 @@
   "manage_publisher_managers": "",
   "manage_sessions": "",
   "manage_subscription": "",
+  "manage_your_ai_assist_add_on": "",
   "managed": "",
   "managed_user_accounts": "",
   "managed_user_invite_has_been_sent_to_email": "",

+ 4 - 0
services/web/frontend/js/features/subscription/components/dashboard/states/active/active-new.tsx

@@ -10,6 +10,7 @@ import { ConfirmChangePlanModal } from './change-plan/modals/confirm-change-plan
 import { KeepCurrentPlanModal } from './change-plan/modals/keep-current-plan-modal'
 import { ChangeToGroupModal } from './change-plan/modals/change-to-group-modal'
 import { CancelAiAddOnModal } from '@/features/subscription/components/dashboard/states/active/change-plan/modals/cancel-ai-add-on-modal'
+import { WritefullBundleManagementModal } from '@/features/subscription/components/dashboard/states/active/change-plan/modals/writefull-bundle-management-modal'
 import OLButton from '@/features/ui/components/ol/ol-button'
 import isInFreeTrial from '../../../../util/is-in-free-trial'
 import AddOns from '@/features/subscription/components/dashboard/states/active/add-ons'
@@ -71,6 +72,7 @@ export function ActiveSubscriptionNew({
   }
 
   const handlePlanChange = () => setModalIdShown('change-plan')
+  const handleManageOnWritefull = () => setModalIdShown('manage-on-writefull')
   const handleCancelClick = (addOnCode: string) => {
     if ([AI_STANDALONE_PLAN_CODE, AI_ADD_ON_CODE].includes(addOnCode)) {
       setModalIdShown('cancel-ai-add-on')
@@ -248,6 +250,7 @@ export function ActiveSubscriptionNew({
         subscription={subscription}
         onStandalonePlan={onStandalonePlan}
         handleCancelClick={handleCancelClick}
+        handleManageOnWritefull={handleManageOnWritefull}
       />
 
       <ChangePlanModal />
@@ -255,6 +258,7 @@ export function ActiveSubscriptionNew({
       <KeepCurrentPlanModal />
       <ChangeToGroupModal />
       <CancelAiAddOnModal />
+      <WritefullBundleManagementModal />
       <PauseSubscriptionModal />
     </>
   )

+ 76 - 17
services/web/frontend/js/features/subscription/components/dashboard/states/active/add-ons.tsx

@@ -1,4 +1,5 @@
 import { useTranslation } from 'react-i18next'
+import getMeta from '@/utils/meta'
 import { Dropdown, DropdownMenu, DropdownToggle } from 'react-bootstrap-5'
 import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
 import MaterialIcon from '@/shared/components/material-icon'
@@ -16,6 +17,7 @@ type AddOnsProps = {
   subscription: PaidSubscription
   onStandalonePlan: boolean
   handleCancelClick: (code: string) => void
+  handleManageOnWritefull: () => void
 }
 
 type AddOnProps = {
@@ -98,12 +100,60 @@ function AddOn({
   )
 }
 
+function WritefullGrantedAddOn({
+  handleManageOnWritefull,
+}: {
+  handleManageOnWritefull: () => void
+}) {
+  const { t } = useTranslation()
+  return (
+    <div className="add-on-card">
+      <div>
+        <img
+          alt="sparkle"
+          className="add-on-card-icon"
+          src={sparkle}
+          aria-hidden="true"
+        />
+      </div>
+      <div className="add-on-card-content">
+        <div className="heading">{ADD_ON_NAME}</div>
+        <div className="description small mt-1">
+          {t('included_as_part_of_your_writefull_subscription')}
+        </div>
+      </div>
+
+      <div className="ms-auto">
+        <Dropdown align="end">
+          <DropdownToggle
+            id="add-on-dropdown-toggle"
+            className="add-on-options-toggle"
+            variant="secondary"
+          >
+            <MaterialIcon
+              type="more_vert"
+              accessibilityLabel={t('more_options')}
+            />
+          </DropdownToggle>
+          <DropdownMenu flip={false}>
+            <OLDropdownMenuItem tabIndex={-1} onClick={handleManageOnWritefull}>
+              {t('manage_subscription')}
+            </OLDropdownMenuItem>
+          </DropdownMenu>
+        </Dropdown>
+      </div>
+    </div>
+  )
+}
+
 function AddOns({
   subscription,
   onStandalonePlan,
   handleCancelClick,
+  handleManageOnWritefull,
 }: AddOnsProps) {
   const { t } = useTranslation()
+  const hasAiAssistViaWritefull = getMeta('ol-hasAiAssistViaWritefull')
   const addOnsDisplayPrices = onStandalonePlan
     ? {
         [AI_STANDALONE_PLAN_CODE]: subscription.payment.displayPrice,
@@ -113,26 +163,35 @@ function AddOns({
     ? [{ addOnCode: AI_STANDALONE_PLAN_CODE }]
     : subscription.addOns?.filter(addOn => addOn.addOnCode !== LICENSE_ADD_ON)
 
+  const hasAddons =
+    (addOnsToDisplay && addOnsToDisplay.length > 0) || hasAiAssistViaWritefull
   return (
     <>
       <h2 className="h3 fw-bold">{t('add_ons')}</h2>
-      {addOnsToDisplay && addOnsToDisplay.length > 0 ? (
-        addOnsToDisplay.map(addOn => (
-          <AddOn
-            addOnCode={addOn.addOnCode}
-            key={addOn.addOnCode}
-            isAnnual={Boolean(subscription.plan.annual)}
-            handleCancelClick={handleCancelClick}
-            pendingCancellation={
-              subscription.pendingPlan !== undefined &&
-              (subscription.pendingPlan.addOns ?? []).every(
-                pendingAddOn => pendingAddOn.code !== addOn.addOnCode
-              )
-            }
-            displayPrice={addOnsDisplayPrices[addOn.addOnCode]}
-            nextBillingDate={subscription.payment.nextPaymentDueDate}
-          />
-        ))
+      {hasAddons ? (
+        <>
+          {addOnsToDisplay?.map(addOn => (
+            <AddOn
+              addOnCode={addOn.addOnCode}
+              key={addOn.addOnCode}
+              isAnnual={Boolean(subscription.plan.annual)}
+              handleCancelClick={handleCancelClick}
+              pendingCancellation={
+                subscription.pendingPlan !== undefined &&
+                (subscription.pendingPlan.addOns ?? []).every(
+                  pendingAddOn => pendingAddOn.code !== addOn.addOnCode
+                )
+              }
+              displayPrice={addOnsDisplayPrices[addOn.addOnCode]}
+              nextBillingDate={subscription.payment.nextPaymentDueDate}
+            />
+          ))}
+          {hasAiAssistViaWritefull && (
+            <WritefullGrantedAddOn
+              handleManageOnWritefull={handleManageOnWritefull}
+            />
+          )}
+        </>
       ) : (
         <p>{t('you_dont_have_any_add_ons_on_your_account')}</p>
       )}

+ 51 - 0
services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/writefull-bundle-management-modal.tsx

@@ -0,0 +1,51 @@
+import { useTranslation } from 'react-i18next'
+import { SubscriptionDashModalIds } from '../../../../../../../../../../types/subscription/dashboard/modal-ids'
+import { useSubscriptionDashboardContext } from '../../../../../../context/subscription-dashboard-context'
+import OLModal, {
+  OLModalBody,
+  OLModalFooter,
+  OLModalHeader,
+  OLModalTitle,
+} from '@/features/ui/components/ol/ol-modal'
+import OLButton from '@/features/ui/components/ol/ol-button'
+
+export function WritefullBundleManagementModal() {
+  const modalId: SubscriptionDashModalIds = 'manage-on-writefull'
+  const { t } = useTranslation()
+  const { handleCloseModal, modalIdShown } = useSubscriptionDashboardContext()
+
+  if (modalIdShown !== modalId) return null
+
+  return (
+    <OLModal
+      id={modalId}
+      show
+      animation
+      onHide={handleCloseModal}
+      backdrop="static"
+    >
+      <OLModalHeader>
+        <OLModalTitle>{t('manage_your_ai_assist_add_on')}</OLModalTitle>
+      </OLModalHeader>
+
+      <OLModalBody>
+        <p>{t('ai_assist_in_overleaf_is_included_via_writefull')}</p>
+      </OLModalBody>
+
+      <OLModalFooter>
+        <OLButton variant="secondary" onClick={handleCloseModal}>
+          {t('back')}
+        </OLButton>
+        <OLButton
+          variant="primary"
+          onClick={handleCloseModal}
+          href="https://my.writefull.com/account"
+          target="_blank"
+          rel="noreferrer"
+        >
+          {t('go_to_writefull')}
+        </OLButton>
+      </OLModalFooter>
+    </OLModal>
+  )
+}

+ 1 - 0
services/web/frontend/js/utils/meta.ts

@@ -115,6 +115,7 @@ export interface Meta {
   'ol-groupSsoSetupSuccess': boolean
   'ol-groupSubscriptionsPendingEnrollment': PendingGroupSubscriptionEnrollment[]
   'ol-groupsAndEnterpriseBannerVariant': GroupsAndEnterpriseBannerVariant
+  'ol-hasAiAssistViaWritefull': boolean
   'ol-hasGroupSSOFeature': boolean
   'ol-hasIndividualRecurlySubscription': boolean
   'ol-hasManagedUsersFeature': boolean

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

@@ -125,6 +125,7 @@
   "aggregate_changed": "Changed",
   "aggregate_to": "to",
   "agree_with_the_terms": "I agree with the Overleaf terms",
+  "ai_assist_in_overleaf_is_included_via_writefull": "AI Assist in Overleaf is included as part of your Writefull subscription. You can cancel or manage your access to AI Assist in your Writefull subscription settings.",
   "ai_assistance_to_help_you": "AI assistance to help you fix LaTeX errors",
   "ai_based_language_tools": "AI-based language tools tailored to research writing",
   "ai_can_make_mistakes": "AI can make mistakes. Review fixes before you apply them.",
@@ -886,6 +887,7 @@
   "go_to_previous_page": "Go to previous page",
   "go_to_settings": "Go to settings",
   "go_to_subscriptions": "Go to Subscriptions",
+  "go_to_writefull": "Go to Writefull",
   "good_news_you_already_purchased_this_add_on": "Good news! You already have this add-on, so no need to pay again.",
   "good_news_you_are_already_receiving_this_add_on_via_writefull": "Good news! You already have this add-on via your Writefull subscription. No need to pay again.",
   "great_for_getting_started": "Great for getting started",
@@ -1018,6 +1020,7 @@
   "include_results_from_your_reference_manager": "Include results from your reference manager",
   "include_results_from_your_x_account": "Include results from your __provider__ account",
   "include_the_error_message_and_ai_response": "Include the error message and AI response",
+  "included_as_part_of_your_writefull_subscription": "Included as part of your Writefull subscription",
   "increase_indent": "Increase indentation",
   "increased_compile_timeout": "Increased compile timeout",
   "individuals": "Individuals",
@@ -1288,6 +1291,7 @@
   "manage_publisher_managers": "Manage publisher managers",
   "manage_sessions": "Manage Your Sessions",
   "manage_subscription": "Manage subscription",
+  "manage_your_ai_assist_add_on": "Manage your AI Assist add-on",
   "managed": "Managed",
   "managed_user_accounts": "Managed user accounts",
   "managed_user_invite_has_been_sent_to_email": "Managed User invite has been sent to <0>__email__</0>",

+ 5 - 1
services/web/test/unit/src/Subscription/SubscriptionControllerTests.js

@@ -155,7 +155,11 @@ describe('SubscriptionController', function () {
         './RecurlyEventHandler': {
           sendRecurlyAnalyticsEvent: sinon.stub().resolves(),
         },
-        './FeaturesUpdater': (this.FeaturesUpdater = {}),
+        './FeaturesUpdater': (this.FeaturesUpdater = {
+          promises: {
+            hasFeaturesViaWritefull: sinon.stub().resolves(false),
+          },
+        }),
         './GroupPlansData': (this.GroupPlansData = {}),
         './V1SubscriptionManager': (this.V1SubscriptionManager = {}),
         '../Errors/HttpErrorHandler': (this.HttpErrorHandler = {

+ 1 - 0
services/web/types/subscription/dashboard/modal-ids.ts

@@ -5,5 +5,6 @@ export type SubscriptionDashModalIds =
   | 'leave-group'
   | 'change-plan'
   | 'cancel-ai-add-on'
+  | 'manage-on-writefull'
   | 'pause-subscription'
   | 'unpause-subscription'