Skip to content

Commit 67ee6f2

Browse files
authored
Merge pull request #20128 from mozilla/PAY-3559
fix(payments-next): Update copy and Cancel action Because Per legal feedback, we need to add an After that sentence on our churn flows cancel subscription instead of continue to cancel This pull request Standard Cancel Updated "Stay subscribed" button label to "Keep subscription" as the button navigates the customer to Subscription Management and not Stay subscribed page. Churn Cancel Added "After that..." sentence Updated "Continue to cancel" button to "Cancel subscription" Added success message for when subscription is successfully canceled Added "You can turn your subscription back on..." sentence Added message for already_canceling_at_period_end upon refresh Moved out error message for already_canceling_at_period_end and into its own component as it is reused three times (CancelChurn, Churn Error, InterstitialOffer) Churn Stay Add "After that..." sentence Offer Created InterstitialOffer component Updated "Continue to cancel" button to "Cancel subscription" Added success message for when subscription is successfully canceled Added "You can turn your subscription back on..." sentence Added message for already_canceling_at_period_end upon refresh
2 parents 6a88348 + c27b5c5 commit 67ee6f2

22 files changed

Lines changed: 811 additions & 273 deletions

File tree

apps/payments/next/app/[locale]/subscriptions/[subscriptionId]/loyalty-discount/cancel/error/en.ftl

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,5 @@ churn-cancel-flow-error-offer-expired-title = This offer has expired
44
churn-cancel-flow-error-offer-expired-message = There are currently no discounts available for this subscription. You can continue with cancellation if you’d like.
55
churn-cancel-flow-error-button-continue-to-cancel = Continue to cancel
66
churn-cancel-flow-error-page-button-back-to-subscriptions = Back to subscriptions
7-
churn-cancel-flow-error-already-canceling-title = Your subscription is set to end
8-
# $productName (String) - The name of the product to create subscription, e.g. Mozilla VPN
9-
# $currentPeriodEnd (Date) - The end date of the subscription's current billing period (e.g., September, 8, 2025)
10-
churn-cancel-flow-error-already-canceling-message = You’ll continue to have access to { $productName } until { $currentPeriodEnd }.
11-
churn-cancel-flow-error-page-button-keep-subscription = Keep subscription
127
138
##

apps/payments/next/app/[locale]/subscriptions/[subscriptionId]/loyalty-discount/cancel/error/page.tsx

Lines changed: 1 addition & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ import { redirect } from 'next/navigation';
99

1010
import { SubscriptionParams } from '@fxa/payments/ui';
1111
import { determineChurnCancelEligibilityAction } from '@fxa/payments/ui/actions';
12-
import { ChurnError, getApp } from '@fxa/payments/ui/server';
13-
import { getLocalizedDateString } from '@fxa/shared/l10n';
12+
import { getApp, ChurnError } from '@fxa/payments/ui/server';
1413
import { auth } from 'apps/payments/next/auth';
1514
import { config } from 'apps/payments/next/config';
1615

@@ -76,7 +75,6 @@ export default async function LoyaltyDiscountCancelErrorPage({
7675
}
7776

7877
const { cmsOfferingContent, reason } = churnCancelContentEligibility;
79-
8078
const cancelContent = pageContent.cancelContent;
8179

8280
if (cancelContent.flowType !== 'cancel' || !cmsOfferingContent) {
@@ -145,70 +143,6 @@ export default async function LoyaltyDiscountCancelErrorPage({
145143
);
146144
}
147145

148-
if (reason === 'already_canceling_at_period_end') {
149-
const { productName, webIcon } = cmsOfferingContent;
150-
const { currentPeriodEnd } = cancelContent;
151-
const currentPeriodEndLongFallback = getLocalizedDateString(
152-
currentPeriodEnd,
153-
false,
154-
locale
155-
);
156-
return (
157-
<section
158-
className="flex justify-center min-h-[calc(100vh_-_4rem)] tablet:items-center tablet:min-h-[calc(100vh_-_5rem)]"
159-
aria-labelledby="error-already-canceling-heading"
160-
>
161-
<div className="max-w-[480px] p-10 text-grey-600 tablet:bg-white tablet:rounded-xl tablet:border tablet:border-grey-200 tablet:shadow-[0_0_16px_0_rgba(0,0,0,0.08)]">
162-
<div className="flex flex-col items-center justify-center gap-4 text-center">
163-
<Image src={webIcon} alt={productName} height={64} width={64} />
164-
165-
<h1
166-
id="error-already-canceling-heading"
167-
className="font-bold leading-7 text-center text-xl"
168-
>
169-
{l10n.getString(
170-
'churn-cancel-flow-error-already-canceling-title',
171-
'Your subscription is set to end'
172-
)}
173-
</h1>
174-
<div className="leading-6">
175-
<p className="my-2">
176-
{l10n.getString(
177-
'churn-cancel-flow-error-already-canceling-message',
178-
{
179-
productName,
180-
currentPeriodEnd: currentPeriodEndLongFallback,
181-
},
182-
`You’ll continue to have access to ${productName} until ${currentPeriodEndLongFallback}.`
183-
)}
184-
</p>
185-
</div>
186-
<div className="flex flex-col gap-3 w-full">
187-
<Link
188-
href={`/${locale}/subscriptions/landing`}
189-
className="border box-border flex font-bold font-header h-12 items-center justify-center rounded text-center py-2 px-5 bg-blue-500 border-blue-600 hover:bg-blue-700 text-white"
190-
>
191-
{l10n.getString(
192-
'churn-cancel-flow-error-page-button-back-to-subscriptions',
193-
'Back to subscriptions'
194-
)}
195-
</Link>
196-
<Link
197-
href={`/${locale}/subscriptions/${subscriptionId}/stay-subscribed`}
198-
className="border box-border flex font-bold font-header h-12 items-center justify-center rounded text-center py-2 px-5 bg-grey-10 border-grey-200 hover:bg-grey-50"
199-
>
200-
{l10n.getString(
201-
'churn-cancel-flow-error-page-button-keep-subscription',
202-
'Keep subscription'
203-
)}
204-
</Link>
205-
</div>
206-
</div>
207-
</div>
208-
</section>
209-
);
210-
}
211-
212146
return (
213147
<ChurnError
214148
cmsOfferingContent={cmsOfferingContent}

apps/payments/next/app/[locale]/subscriptions/[subscriptionId]/loyalty-discount/cancel/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ export default async function LoyaltyDiscountCancelPage({
6464
churnCancelContentEligibility;
6565
const reasonStr = typeof reason === 'string' ? reason : undefined;
6666
const isAllowedCancelReason =
67-
reasonStr === 'eligible' || reasonStr === 'discount_already_applied';
67+
reasonStr === 'eligible' ||
68+
reasonStr === 'discount_already_applied' ||
69+
reasonStr === 'already_canceling_at_period_end';
6870

6971
if (!isAllowedCancelReason) {
7072
redirect(

apps/payments/next/app/[locale]/subscriptions/[subscriptionId]/offer/en.ftl

Lines changed: 0 additions & 8 deletions
This file was deleted.

apps/payments/next/app/[locale]/subscriptions/[subscriptionId]/offer/error/page.tsx

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -44,36 +44,50 @@ export default async function InterstitialOfferErrorPage({
4444

4545
const uid = session.user.id;
4646

47-
let interstitialOfferContent;
48-
let reason = 'general_error';
47+
let interstitialOfferContent = null;
48+
4949
try {
5050
interstitialOfferContent = await getInterstitialOfferContentAction(
5151
uid,
5252
subscriptionId,
5353
acceptLanguage,
5454
locale
5555
);
56-
reason = interstitialOfferContent?.reason ?? 'general_error';
57-
} catch (error) {
56+
} catch {
5857
interstitialOfferContent = null;
59-
reason = 'general_error';
6058
}
6159

62-
const webIcon = interstitialOfferContent?.webIcon;
63-
const productName = interstitialOfferContent?.productName;
60+
const cancelContent = interstitialOfferContent?.cancelContent;
61+
let reason =
62+
typeof interstitialOfferContent?.reason === 'string'
63+
? interstitialOfferContent.reason
64+
: 'general_error';
65+
66+
const canRenderOfferPage =
67+
reason === 'eligible' || reason === 'already_canceling_at_period_end';
68+
69+
if (cancelContent?.flowType === 'cancel' && canRenderOfferPage) {
70+
redirect(`/${locale}/subscriptions/${subscriptionId}/offer`);
71+
}
72+
73+
const pageContent = interstitialOfferContent?.pageContent;
74+
75+
const productName =
76+
pageContent?.productName ??
77+
(cancelContent?.flowType === 'cancel' ? cancelContent.productName : null);
6478

65-
if (webIcon && !productName) {
66-
console.error('Missing productName for interstitial offer icon');
67-
reason = 'general_error';
68-
}
79+
const supportUrl =
80+
pageContent?.supportUrl ??
81+
(cancelContent?.flowType === 'cancel' ? cancelContent.supportUrl : null) ??
82+
'https://support.mozilla.org';
6983

70-
if (
71-
interstitialOfferContent?.isEligible &&
72-
interstitialOfferContent?.pageContent
73-
) {
74-
redirect(`/${locale}/subscriptions/${subscriptionId}/offer`);
75-
}
84+
const webIcon =
85+
pageContent?.webIcon ??
86+
(cancelContent?.flowType === 'cancel' ? cancelContent.webIcon : null);
7687

88+
if (webIcon && !productName) {
89+
reason = 'general_error';
90+
}
7791
const l10n = getApp().getL10n(acceptLanguage, locale);
7892

7993
const getErrorContent = (reason: string) => {
@@ -120,7 +134,7 @@ export default async function InterstitialOfferErrorPage({
120134
'interstitial-offer-error-button-contact-support',
121135
'Contact Support'
122136
),
123-
href: 'https://support.mozilla.org/',
137+
href: supportUrl,
124138
isExternal: true,
125139
},
126140
};
@@ -153,7 +167,8 @@ export default async function InterstitialOfferErrorPage({
153167
}
154168
};
155169

156-
const { heading, message, primaryButton, secondaryButton } = getErrorContent(reason);
170+
const { heading, message, primaryButton, secondaryButton } =
171+
getErrorContent(reason);
157172

158173
return (
159174
<section
@@ -187,8 +202,8 @@ export default async function InterstitialOfferErrorPage({
187202
>
188203
{primaryButton.label}
189204
</Link>
190-
{secondaryButton && (
191-
secondaryButton.isExternal ? (
205+
{secondaryButton &&
206+
(secondaryButton.isExternal ? (
192207
<LinkExternal
193208
className="border box-border font-header h-14 items-center justify-center rounded-md text-center font-bold py-4 px-6 bg-grey-10 border-grey-200 hover:bg-grey-50 flex w-full"
194209
href={secondaryButton.href}
@@ -202,8 +217,7 @@ export default async function InterstitialOfferErrorPage({
202217
>
203218
<span>{secondaryButton.label}</span>
204219
</Link>
205-
)
206-
)}
220+
))}
207221
</div>
208222
</div>
209223
</section>

apps/payments/next/app/[locale]/subscriptions/[subscriptionId]/offer/page.tsx

Lines changed: 18 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@
44

55
import { headers } from 'next/headers';
66
import { redirect } from 'next/navigation';
7-
import { getApp } from '@fxa/payments/ui/server';
7+
8+
import { InterstitialOffer } from '@fxa/payments/ui';
89
import { getInterstitialOfferContentAction } from '@fxa/payments/ui/actions';
910
import { auth } from 'apps/payments/next/auth';
1011
import { config } from 'apps/payments/next/config';
11-
import Image from 'next/image';
12-
import Link from 'next/link';
1312

1413
export default async function InterstitialOfferPage({
1514
params,
@@ -28,7 +27,6 @@ export default async function InterstitialOfferPage({
2827
}
2928

3029
const acceptLanguage = headers().get('accept-language');
31-
const l10n = getApp().getL10n(acceptLanguage, locale);
3230
const session = await auth();
3331
if (!session?.user?.id) {
3432
const redirectToUrl = new URL(
@@ -56,100 +54,29 @@ export default async function InterstitialOfferPage({
5654
redirect(`/${locale}/subscriptions/${subscriptionId}/offer/error`);
5755
}
5856

59-
if (!interstitialOfferContent.isEligible || !interstitialOfferContent.pageContent) {
57+
if (interstitialOfferContent.cancelContent.flowType === 'not_found') {
6058
redirect(`/${locale}/subscriptions/${subscriptionId}/offer/error`);
6159
}
6260

63-
const {
64-
currentInterval,
65-
modalHeading1,
66-
modalMessage,
67-
upgradeButtonLabel,
68-
upgradeButtonUrl,
69-
webIcon,
70-
productName,
71-
} = interstitialOfferContent.pageContent;
61+
const hasOffer = !!interstitialOfferContent.pageContent;
62+
const cancelAtPeriodEnd =
63+
!!interstitialOfferContent.cancelContent.cancelAtPeriodEnd;
7264

73-
const getKeepCurrentSubscriptionFtlIds = (interval: string) => {
74-
switch (interval) {
75-
case 'daily':
76-
return {
77-
ftlId: 'interstitial-offer-button-keep-current-interval-daily',
78-
fallbackText: 'Keep daily subscription',
79-
};
80-
case 'weekly':
81-
return {
82-
ftlId: 'interstitial-offer-button-keep-current-interval-weekly',
83-
fallbackText: 'Keep weekly subscription',
84-
};
85-
case 'halfyearly':
86-
return {
87-
ftlId: 'interstitial-offer-button-keep-current-interval-halfyearly',
88-
fallbackText: 'Keep six-month subscription',
89-
};
90-
case 'monthly':
91-
default:
92-
return {
93-
ftlId: 'interstitial-offer-button-keep-current-interval-monthly',
94-
fallbackText: 'Keep monthly subscription',
95-
};
96-
}
97-
};
65+
if (!hasOffer && !cancelAtPeriodEnd) {
66+
redirect(`/${locale}/subscriptions/${subscriptionId}/offer/error`);
67+
}
9868

99-
const { ftlId, fallbackText } = getKeepCurrentSubscriptionFtlIds(currentInterval);
100-
const keepCurrentSubscriptionButtonText = l10n.getString(ftlId, fallbackText);
10169
const searchParamsObj = new URLSearchParams(searchParams);
102-
searchParamsObj.append('entrypoint', 'subscription-management');
70+
searchParamsObj.set('entrypoint', 'subscription-management');
10371

10472
return (
105-
<section
106-
className="flex justify-center min-h-[calc(100vh_-_4rem)] tablet:items-center tablet:min-h-[calc(100vh_-_5rem)]"
107-
>
108-
<div className="w-full max-w-[480px] flex flex-col justify-center items-center p-10 tablet:bg-white tablet:rounded-xl tablet:border tablet:border-grey-200 tablet:shadow-[0_0_16px_0_rgba(0,0,0,0.08)]">
109-
<div className="w-full flex flex-col items-center gap-6 text-center">
110-
<Image
111-
src={webIcon}
112-
alt={productName}
113-
height={64}
114-
width={64}
115-
/>
116-
<h1 className="font-bold self-stretch text-center font-header text-xl leading-8 ">
117-
{modalHeading1}
118-
</h1>
119-
</div>
120-
<p className="w-full self-stretch leading-7 text-lg text-grey-900">
121-
{modalMessage &&
122-
modalMessage.map((line, i) => (
123-
<p className="my-2" key={i}>
124-
{line}
125-
</p>
126-
))}
127-
</p>
128-
129-
<div className="w-full flex flex-col gap-3 mt-12">
130-
<Link
131-
className="border box-border font-header h-14 items-center justify-center rounded-md text-white text-center font-bold py-4 px-6 bg-blue-500 hover:bg-blue-700 flex w-full"
132-
href={`${upgradeButtonUrl}?${searchParamsObj.toString()}`}
133-
>
134-
{upgradeButtonLabel}
135-
</Link>
136-
<Link
137-
className="border box-border font-header h-14 items-center justify-center rounded-md text-center font-bold py-4 px-6 bg-grey-10 border-grey-200 hover:bg-grey-50 flex w-full"
138-
href={`/${locale}/subscriptions/landing`}
139-
>
140-
<span>{keepCurrentSubscriptionButtonText}</span>
141-
</Link>
142-
<Link
143-
className="border box-border font-header h-14 items-center justify-center rounded-md text-center font-bold py-4 px-6 bg-grey-10 border-grey-200 hover:bg-grey-50 flex w-full"
144-
href={`/${locale}/subscriptions/${subscriptionId}/cancel`}
145-
>
146-
<span>{l10n.getString(
147-
'interstitial-offer-button-cancel-subscription',
148-
'Continue to cancel'
149-
)}</span>
150-
</Link>
151-
</div>
152-
</div>
153-
</section>
73+
<InterstitialOffer
74+
uid={uid}
75+
locale={locale}
76+
subscriptionId={subscriptionId}
77+
pageContent={interstitialOfferContent.pageContent}
78+
cancelContent={interstitialOfferContent.cancelContent}
79+
searchParams={searchParamsObj}
80+
/>
15481
);
15582
}

0 commit comments

Comments
 (0)