From f95bf4182437bf3ba5386941e2020c76e9bee456 Mon Sep 17 00:00:00 2001 From: Jimmy Domagala-Tang Date: Tue, 22 Apr 2025 09:13:26 -0400 Subject: [PATCH] Merge pull request #24697 from overleaf/jdt-show-addons-via-wf Display When Ai Assist Is Granted Via Writefull GitOrigin-RevId: 91f6e1843e2e1d1f7b3a49d95f31603e838c5545 --- .../Subscription/SubscriptionController.js | 8 +- .../views/subscriptions/dashboard-react.pug | 49 +++++----- .../web/frontend/extracted-translations.json | 4 + .../dashboard/states/active/active-new.tsx | 4 + .../dashboard/states/active/add-ons.tsx | 93 +++++++++++++++---- .../writefull-bundle-management-modal.tsx | 51 ++++++++++ services/web/frontend/js/utils/meta.ts | 1 + services/web/locales/en.json | 4 + .../SubscriptionControllerTests.js | 6 +- .../types/subscription/dashboard/modal-ids.ts | 1 + 10 files changed, 177 insertions(+), 44 deletions(-) create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/writefull-bundle-management-modal.tsx diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.js b/services/web/app/src/Features/Subscription/SubscriptionController.js index 07a5454be6..838b580273 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionController.js +++ b/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' ) diff --git a/services/web/app/views/subscriptions/dashboard-react.pug b/services/web/app/views/subscriptions/dashboard-react.pug index 6fac08dd53..dab505e4e5 100644 --- a/services/web/app/views/subscriptions/dashboard-react.pug +++ b/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 diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 0b09d21471..cec5c60ebb 100644 --- a/services/web/frontend/extracted-translations.json +++ b/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": "", diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/active-new.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/active-new.tsx index 1fc21a0c3c..22291f499e 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/active-new.tsx +++ b/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} /> @@ -255,6 +258,7 @@ export function ActiveSubscriptionNew({ + ) diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/add-ons.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/add-ons.tsx index 6fe79df1e3..bd23fabad3 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/add-ons.tsx +++ b/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 ( +
+
+ +
+
+
{ADD_ON_NAME}
+
+ {t('included_as_part_of_your_writefull_subscription')} +
+
+ +
+ + + + + + + {t('manage_subscription')} + + + +
+
+ ) +} + 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 ( <>

{t('add_ons')}

- {addOnsToDisplay && addOnsToDisplay.length > 0 ? ( - addOnsToDisplay.map(addOn => ( - pendingAddOn.code !== addOn.addOnCode - ) - } - displayPrice={addOnsDisplayPrices[addOn.addOnCode]} - nextBillingDate={subscription.payment.nextPaymentDueDate} - /> - )) + {hasAddons ? ( + <> + {addOnsToDisplay?.map(addOn => ( + pendingAddOn.code !== addOn.addOnCode + ) + } + displayPrice={addOnsDisplayPrices[addOn.addOnCode]} + nextBillingDate={subscription.payment.nextPaymentDueDate} + /> + ))} + {hasAiAssistViaWritefull && ( + + )} + ) : (

{t('you_dont_have_any_add_ons_on_your_account')}

)} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/writefull-bundle-management-modal.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/writefull-bundle-management-modal.tsx new file mode 100644 index 0000000000..ef512ddec5 --- /dev/null +++ b/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 ( + + + {t('manage_your_ai_assist_add_on')} + + + +

{t('ai_assist_in_overleaf_is_included_via_writefull')}

+
+ + + + {t('back')} + + + {t('go_to_writefull')} + + +
+ ) +} diff --git a/services/web/frontend/js/utils/meta.ts b/services/web/frontend/js/utils/meta.ts index c45a17948f..15ff6db6a7 100644 --- a/services/web/frontend/js/utils/meta.ts +++ b/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 diff --git a/services/web/locales/en.json b/services/web/locales/en.json index ffe692c8c3..10b399e6e6 100644 --- a/services/web/locales/en.json +++ b/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__", diff --git a/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js b/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js index ef4b055dc3..27ba5bf85b 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js +++ b/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 = { diff --git a/services/web/types/subscription/dashboard/modal-ids.ts b/services/web/types/subscription/dashboard/modal-ids.ts index bbc13614c2..d878c356d7 100644 --- a/services/web/types/subscription/dashboard/modal-ids.ts +++ b/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'