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

Display When Ai Assist Is Granted Via Writefull

GitOrigin-RevId: 91f6e1843e2e1d1f7b3a49d95f31603e838c5545
This commit is contained in:
Jimmy Domagala-Tang
2025-04-22 09:13:26 -04:00
committed by Copybot
parent d492512d9e
commit f95bf41824
10 changed files with 177 additions and 44 deletions

View File

@@ -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'
)

View File

@@ -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

View File

@@ -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": "",

View File

@@ -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 />
</>
)

View File

@@ -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>
)}

View File

@@ -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>
)
}

View File

@@ -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

View File

@@ -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>",

View File

@@ -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 = {

View File

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