Skip to content

Commit c27b5c5

Browse files
committed
fix(payments-next): Update copy and Cancel action
1 parent e3da348 commit c27b5c5

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)