Skip to content

Commit efe5936

Browse files
authored
Merge pull request #19895 from mozilla/pay-3460-submanage-error-fix
fix(next): show relevant stripe error msg
2 parents 13712ad + 478c2c0 commit efe5936

10 files changed

Lines changed: 131 additions & 57 deletions

File tree

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,8 @@ import { getStripeClientSession } from '@fxa/payments/ui/actions';
1717

1818
export default async function StripePaymentManagementPage({
1919
params,
20-
searchParams,
2120
}: {
2221
params: ManageParams;
23-
searchParams: Record<string, string | string[]> | undefined;
2422
}) {
2523
const session = await auth();
2624
const { locale } = params;
@@ -44,6 +42,9 @@ export default async function StripePaymentManagementPage({
4442
const { clientSecret, defaultPaymentMethod, currency } =
4543
stripeClientSession;
4644

45+
// Only change the instanceKey, thereby remounting the StripeManagementWrapper, when the defaultPaymentMethod changes.
46+
const instanceKey = defaultPaymentMethod ? `${defaultPaymentMethod.type}-${defaultPaymentMethod.id}` : uuidv4();
47+
4748
return (
4849
<section
4950
className="w-full tablet:px-8 desktop:max-w-[1024px]"
@@ -54,7 +55,7 @@ export default async function StripePaymentManagementPage({
5455
locale={locale}
5556
currency={currency}
5657
sessionSecret={clientSecret}
57-
instanceKey={uuidv4()}
58+
instanceKey={instanceKey}
5859
>
5960
<PaymentMethodManagement
6061
uid={session?.user?.id}

libs/payments/customer/src/lib/setupIntent.manager.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ describe('SetupIntentManager', () => {
5555
enabled: true,
5656
allow_redirects: 'never',
5757
},
58+
use_stripe_sdk: true,
5859
});
5960
expect(result).toEqual(mockResponse);
6061
});

libs/payments/customer/src/lib/setupIntent.manager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export class SetupIntentManager {
1919
enabled: true,
2020
allow_redirects: 'never',
2121
},
22+
use_stripe_sdk: true,
2223
});
2324
}
2425

libs/payments/ui/src/lib/actions/updateStripePaymentDetails.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const updateStripePaymentDetails = async (
1212
) => {
1313
const actionsService = getApp().getActionsService();
1414

15-
return await actionsService.updateStripePaymentDetails({
15+
return actionsService.updateStripePaymentDetails({
1616
uid,
1717
confirmationTokenId,
1818
});

libs/payments/ui/src/lib/client/components/PaymentMethodManagement/index.tsx

Lines changed: 36 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export function PaymentMethodManagement({
7070
setError(null);
7171
setHasPaymentMethod(
7272
!!event.value.payment_method ||
73-
(event.value.type === 'link' && defaultPaymentMethod?.type === 'link')
73+
(event.value.type === 'link' && defaultPaymentMethod?.type === 'link')
7474
);
7575

7676
if (event.value.type !== 'card') {
@@ -181,25 +181,21 @@ export function PaymentMethodManagement({
181181
throw confirmationTokenError;
182182
}
183183

184-
let response;
185-
try {
186-
response = await updateStripePaymentDetails(
187-
uid ?? '',
188-
confirmationToken.id
189-
);
190-
} catch (error) {
191-
const errorReason = getManagePaymentMethodErrorFtlInfo(error.message);
184+
const { ok, result, errorMessage } = await updateStripePaymentDetails(
185+
uid ?? '',
186+
confirmationToken.id
187+
);
188+
if (!ok || !result) {
189+
const errorReason = getManagePaymentMethodErrorFtlInfo(errorMessage);
192190
setError(
193191
l10n.getString(errorReason.messageFtl, {}, errorReason.message)
194192
);
195-
return;
196-
}
197-
198-
if (response.status === 'requires_action' && response.clientSecret) {
199-
await handleNextAction(response.clientSecret);
193+
} else {
194+
if (result.status === 'requires_action' && result.clientSecret) {
195+
await handleNextAction(result.clientSecret);
196+
}
197+
router.refresh();
200198
}
201-
202-
router.refresh();
203199
} catch (error) {
204200
console.error(error);
205201
setError(error.message || 'An unexpected error occurred.');
@@ -265,30 +261,30 @@ export function PaymentMethodManagement({
265261
</Form.Field>
266262
{(isInputNewCardDetails ||
267263
(isNonCardSelected && !hasPaymentMethod)) && (
268-
<div className="flex flex-row justify-center pt-4">
269-
<Form.Submit asChild>
270-
<BaseButton
271-
className="h-10 mt-10 w-full"
272-
type="submit"
273-
variant={ButtonVariant.Primary}
274-
aria-disabled={!stripe || !isComplete || isLoading}
275-
disabled={!stripe || !isComplete || isLoading}
276-
>
277-
{isLoading ? (
278-
<Image
279-
src={spinnerWhiteImage}
280-
alt=""
281-
className="absolute animate-spin h-8 w-8"
282-
/>
283-
) : (
284-
<Localized id="payment-method-management-save-method">
285-
Save payment method
286-
</Localized>
287-
)}
288-
</BaseButton>
289-
</Form.Submit>
290-
</div>
291-
)}
264+
<div className="flex flex-row justify-center pt-4">
265+
<Form.Submit asChild>
266+
<BaseButton
267+
className="h-10 mt-10 w-full"
268+
type="submit"
269+
variant={ButtonVariant.Primary}
270+
aria-disabled={!stripe || !isComplete || isLoading}
271+
disabled={!stripe || !isComplete || isLoading}
272+
>
273+
{isLoading ? (
274+
<Image
275+
src={spinnerWhiteImage}
276+
alt=""
277+
className="absolute animate-spin h-8 w-8"
278+
/>
279+
) : (
280+
<Localized id="payment-method-management-save-method">
281+
Save payment method
282+
</Localized>
283+
)}
284+
</BaseButton>
285+
</Form.Submit>
286+
</div>
287+
)}
292288
{isNonDefaultCardSelected && !isInputNewCardDetails && (
293289
<div className="flex flex-row justify-center pt-4">
294290
<Form.Submit asChild>

libs/payments/ui/src/lib/client/components/StripeWrapper.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,10 @@ interface StripeWrapperProps {
108108
cart: {
109109
paymentInfo?: {
110110
type:
111-
| Stripe.PaymentMethod.Type
112-
| 'google_iap'
113-
| 'apple_iap'
114-
| 'external_paypal';
111+
| Stripe.PaymentMethod.Type
112+
| 'google_iap'
113+
| 'apple_iap'
114+
| 'external_paypal';
115115
last4?: string;
116116
brand?: string;
117117
customerSessionClientSecret?: string;

libs/payments/ui/src/lib/nestapp/nextjs-actions.service.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -915,10 +915,24 @@ export class NextJSActionsService {
915915
uid: string;
916916
confirmationTokenId: string;
917917
}) {
918-
return await this.subscriptionManagementService.updateStripePaymentDetails(
919-
args.uid,
920-
args.confirmationTokenId
921-
);
918+
try {
919+
const result =
920+
await this.subscriptionManagementService.updateStripePaymentDetails(
921+
args.uid,
922+
args.confirmationTokenId
923+
);
924+
return {
925+
ok: true,
926+
result,
927+
errorMessage: null,
928+
};
929+
} catch (error) {
930+
return {
931+
ok: false,
932+
result: null,
933+
errorMessage: error.message,
934+
};
935+
}
922936
}
923937

924938
@SanitizeExceptions()
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import { Type } from 'class-transformer';
6+
import {
7+
IsBoolean,
8+
IsOptional,
9+
IsString,
10+
Validate,
11+
ValidateNested,
12+
ValidatorConstraint,
13+
type ValidationArguments,
14+
type ValidatorConstraintInterface,
15+
} from 'class-validator';
16+
17+
@ValidatorConstraint({ name: 'ResultErrorConsistency', async: false })
18+
class ResultErrorConsistency implements ValidatorConstraintInterface {
19+
validate(_: any, args: ValidationArguments) {
20+
const obj = args.object as TemplateResult;
21+
22+
if (obj.ok) {
23+
return obj.result != null && obj.errorMessage == null;
24+
}
25+
26+
return obj.result == null && typeof obj.errorMessage === 'string';
27+
}
28+
29+
defaultMessage() {
30+
return 'Invalid combination of ok, result, and errorMessage';
31+
}
32+
}
33+
34+
class TemplateSuccessResponse {
35+
@IsString()
36+
id!: string;
37+
}
38+
39+
export class TemplateResult {
40+
@IsBoolean()
41+
ok!: boolean;
42+
43+
@ValidateNested()
44+
@Type(() => TemplateSuccessResponse)
45+
@IsOptional()
46+
result!: TemplateSuccessResponse | null;
47+
48+
@IsString()
49+
@IsOptional()
50+
errorMessage!: string | null;
51+
52+
@Validate(ResultErrorConsistency)
53+
_consistencyCheck!: boolean;
54+
}

libs/payments/ui/src/lib/nestapp/validators/UpdateStripePaymentDetailsActionResult.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
import { IsOptional, IsString } from 'class-validator';
5+
import { Type } from 'class-transformer';
6+
import { IsOptional, IsString, ValidateNested } from 'class-validator';
7+
import { TemplateResult } from './TemplateResult';
68

7-
export class UpdateStripePaymentDetailsResult {
9+
class UpdateStripePaymentDetailsSuccessResponse {
810
@IsString()
911
id!: string;
1012

@@ -15,3 +17,10 @@ export class UpdateStripePaymentDetailsResult {
1517
@IsOptional()
1618
clientSecret?: string;
1719
}
20+
21+
export class UpdateStripePaymentDetailsResult extends TemplateResult {
22+
@ValidateNested()
23+
@Type(() => UpdateStripePaymentDetailsSuccessResponse)
24+
@IsOptional()
25+
result!: UpdateStripePaymentDetailsSuccessResponse | null;
26+
}

libs/payments/ui/src/lib/utils/getManagePaymentMethodErrorFtlInfo.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
export function getManagePaymentMethodErrorFtlInfo(
6-
errorCode: string,
7-
) {
5+
export function getManagePaymentMethodErrorFtlInfo(errorCode?: string) {
86
switch (errorCode) {
97
case 'intent_failed_card_declined':
108
return {

0 commit comments

Comments
 (0)