Skip to content

Commit bb6c1d8

Browse files
committed
fix(payments-next): Fix update button for Link as saved method
Because: * When the user had Link as their saved payment method, the Manage Payment Methods page would show the option to update/save their payment method even when no changes had been made This commit: * Fixes the logic that compares the selected vs default payment method states Closes #PAY-3422
1 parent ba48315 commit bb6c1d8

File tree

5 files changed

+96
-50
lines changed

5 files changed

+96
-50
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export default async function StripePaymentManagementPage({
4141
throw error;
4242
}
4343
}
44-
const { clientSecret, defaultPaymentMethodId, currency } =
44+
const { clientSecret, defaultPaymentMethod, currency } =
4545
stripeClientSession;
4646

4747
return (
@@ -58,7 +58,7 @@ export default async function StripePaymentManagementPage({
5858
>
5959
<PaymentMethodManagement
6060
uid={session?.user?.id}
61-
defaultPaymentMethodId={defaultPaymentMethodId}
61+
defaultPaymentMethod={defaultPaymentMethod}
6262
sessionEmail={session?.user?.email ?? undefined}
6363
/>
6464
</StripeManagementWrapper>

libs/payments/management/src/lib/subscriptionManagement.service.spec.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,8 @@ describe('SubscriptionManagementService', () => {
192192
provide: ChurnInterventionService,
193193
useValue: {
194194
determineStaySubscribedEligibility: jest.fn(),
195-
}
196-
}
195+
},
196+
},
197197
],
198198
}).compile();
199199

@@ -313,7 +313,8 @@ describe('SubscriptionManagementService', () => {
313313
const mockPaymentMethod = StripeResponseFactory(
314314
StripePaymentMethodFactory({})
315315
);
316-
const mockStaySubscribedCmsChurnEntry = ChurnInterventionByProductIdResultFactory();
316+
const mockStaySubscribedCmsChurnEntry =
317+
ChurnInterventionByProductIdResultFactory();
317318
const mockSubscriptionContent = SubscriptionContentFactory();
318319
const mockPaymentMethodInformation = {
319320
type: SubPlatPaymentMethodType.Card,
@@ -1000,7 +1001,10 @@ describe('SubscriptionManagementService', () => {
10001001
expect(result).toEqual({
10011002
clientSecret: mockCustomerSession.client_secret,
10021003
customer: mockCustomer.id,
1003-
defaultPaymentMethodId: mockPaymentMethod.id,
1004+
defaultPaymentMethod: {
1005+
id: mockPaymentMethod.id,
1006+
type: mockPaymentMethod.type,
1007+
},
10041008
currency: mockCustomer.currency,
10051009
});
10061010
});
@@ -1070,7 +1074,10 @@ describe('SubscriptionManagementService', () => {
10701074
expect(result).toEqual({
10711075
clientSecret: mockCustomerSession.client_secret,
10721076
customer: mockCustomer.id,
1073-
defaultPaymentMethodId: mockPaymentMethod.id,
1077+
defaultPaymentMethod: {
1078+
id: mockPaymentMethod.id,
1079+
type: mockPaymentMethod.type,
1080+
},
10741081
currency: mockCurrency,
10751082
});
10761083
});
@@ -1130,7 +1137,10 @@ describe('SubscriptionManagementService', () => {
11301137
expect(result).toEqual({
11311138
clientSecret: mockCustomerSession.client_secret,
11321139
customer: mockCustomer.id,
1133-
defaultPaymentMethodId: mockPaymentMethod.id,
1140+
defaultPaymentMethod: {
1141+
id: mockPaymentMethod.id,
1142+
type: mockPaymentMethod.type,
1143+
},
11341144
currency: mockCurrency,
11351145
});
11361146
});
@@ -1527,7 +1537,7 @@ describe('SubscriptionManagementService', () => {
15271537
await expect(
15281538
subscriptionManagementService.setDefaultStripePaymentDetails(
15291539
mockAccountCustomer.uid,
1530-
'pm_12345',
1540+
'pm_12345'
15311541
)
15321542
).rejects.toBeInstanceOf(SetDefaultPaymentAccountCustomerMissingStripeId);
15331543
});

libs/payments/management/src/lib/subscriptionManagement.service.ts

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ import {
2323
AccountCustomerManager,
2424
AccountCustomerNotFoundError,
2525
} from '@fxa/payments/stripe';
26-
import {
27-
throwStripeUpdatePaymentFailedError
28-
} from './throwStripeUpdatePaymentFailedError';
26+
import { throwStripeUpdatePaymentFailedError } from './throwStripeUpdatePaymentFailedError';
2927
import type {
3028
ResultAccountCustomer,
3129
StripeCustomer,
@@ -278,7 +276,7 @@ export class SubscriptionManagementService {
278276
uid,
279277
accountCustomer?.stripeCustomerId ?? stripeCustomer.id,
280278
acceptLanguage,
281-
selectedLanguage,
279+
selectedLanguage
282280
);
283281

284282
if (content) {
@@ -477,13 +475,14 @@ export class SubscriptionManagementService {
477475
.filter((tax) => !tax.inclusive)
478476
.reduce((sum, tax) => sum + tax.amount, 0);
479477

480-
const staySubscribedResult = await this.churnInterventionService.determineStaySubscribedEligibility(
481-
uid,
482-
customerId,
483-
subscription.id,
484-
acceptLanguage,
485-
selectedLanguage,
486-
);
478+
const staySubscribedResult =
479+
await this.churnInterventionService.determineStaySubscribedEligibility(
480+
uid,
481+
customerId,
482+
subscription.id,
483+
acceptLanguage,
484+
selectedLanguage
485+
);
487486

488487
return {
489488
id: subscription.id,
@@ -516,7 +515,8 @@ export class SubscriptionManagementService {
516515
promotionName,
517516
cancelAtPeriodEnd: subscription.cancel_at_period_end,
518517
isEligibleForChurnStaySubscribed: staySubscribedResult.isEligible,
519-
churnStaySubscribedCtaMessage: staySubscribedResult.cmsChurnInterventionEntry?.ctaMessage,
518+
churnStaySubscribedCtaMessage:
519+
staySubscribedResult.cmsChurnInterventionEntry?.ctaMessage,
520520
};
521521
}
522522

@@ -808,8 +808,15 @@ export class SubscriptionManagementService {
808808
return {
809809
clientSecret: customerSession.client_secret,
810810
customer: customerSession.customer,
811-
defaultPaymentMethodId: defaultPaymentMethod?.id,
812811
currency,
812+
...(defaultPaymentMethod
813+
? {
814+
defaultPaymentMethod: {
815+
type: defaultPaymentMethod?.type,
816+
id: defaultPaymentMethod?.id,
817+
},
818+
}
819+
: {}),
813820
};
814821
}
815822

@@ -928,10 +935,7 @@ export class SubscriptionManagementService {
928935
}
929936

930937
@SanitizeExceptions()
931-
async setDefaultStripePaymentDetails(
932-
uid: string,
933-
paymentMethodId: string,
934-
) {
938+
async setDefaultStripePaymentDetails(uid: string, paymentMethodId: string) {
935939
const accountCustomer =
936940
await this.accountCustomerManager.getAccountCustomerByUid(uid);
937941

@@ -942,7 +946,7 @@ export class SubscriptionManagementService {
942946
await this.customerManager.update(accountCustomer.stripeCustomerId, {
943947
invoice_settings: {
944948
default_payment_method: paymentMethodId,
945-
}
949+
},
946950
});
947951
}
948952

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

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,15 @@ import {
2828

2929
export function PaymentMethodManagement({
3030
uid,
31-
defaultPaymentMethodId,
31+
defaultPaymentMethod,
3232
sessionEmail,
3333
}: {
3434
uid?: string;
35-
defaultPaymentMethodId?: string;
35+
defaultPaymentMethod?: {
36+
id: string;
37+
email?: string;
38+
type?: string;
39+
};
3640
sessionEmail?: string;
3741
}) {
3842
const { l10n } = useLocalization();
@@ -65,17 +69,36 @@ export function PaymentMethodManagement({
6569
) => {
6670
setIsComplete(event.complete);
6771
setError(null);
68-
setHasPaymentMethod(!!event.value.payment_method);
72+
setHasPaymentMethod(
73+
!!event.value.payment_method ||
74+
(event.value.type === 'link' && defaultPaymentMethod?.type === 'link')
75+
);
6976

7077
if (event.value.type !== 'card') {
7178
setIsNonCardSelected(true);
7279
setIsInputNewCardDetails(false);
73-
if (!!event.value.payment_method) {
74-
if (event.value.payment_method.id !== defaultPaymentMethodId) {
80+
if (
81+
event.value.payment_method?.type === 'link' &&
82+
defaultPaymentMethod?.type === 'link'
83+
) {
84+
/**
85+
* Users can add Link to their account twice if a Link account was not associated with their Stripe
86+
* account email when starting the checkout flow (i.e. this was the customers first time using Link).
87+
* The Payment Element does not currently handle accounts with multiple Link payment methods,
88+
* but the user can still modify their Link payment method settings in-component.
89+
*/
90+
setIsNonDefaultCardSelected(false);
91+
} else if (
92+
!!event.value.payment_method &&
93+
event.value.payment_method.id
94+
) {
95+
if (event.value.payment_method.id !== defaultPaymentMethod?.id) {
7596
setIsNonDefaultCardSelected(true);
7697
} else {
7798
setIsNonDefaultCardSelected(false);
7899
}
100+
} else {
101+
setIsNonDefaultCardSelected(false);
79102
}
80103
return;
81104
}
@@ -86,8 +109,12 @@ export function PaymentMethodManagement({
86109
} else if (event.value.type === 'card' && !!event.value.payment_method) {
87110
setIsInputNewCardDetails(false);
88111

89-
if (event.value.payment_method.id !== defaultPaymentMethodId) {
90-
setIsNonDefaultCardSelected(true);
112+
if (event.value.payment_method.id) {
113+
if (event.value.payment_method.id !== defaultPaymentMethod?.id) {
114+
setIsNonDefaultCardSelected(true);
115+
} else {
116+
setIsNonDefaultCardSelected(false);
117+
}
91118
} else {
92119
setIsNonDefaultCardSelected(false);
93120
}
@@ -156,18 +183,16 @@ export function PaymentMethodManagement({
156183
}
157184

158185
let response;
159-
try {
160-
response = await updateStripePaymentDetails(
186+
try {
187+
response = await updateStripePaymentDetails(
161188
uid ?? '',
162189
confirmationToken.id
163190
);
164191
} catch (error) {
165192
const errorReason = getManagePaymentMethodErrorFtlInfo(error.message);
166-
setError(l10n.getString(
167-
errorReason.messageFtl,
168-
{},
169-
errorReason.message
170-
));
193+
setError(
194+
l10n.getString(errorReason.messageFtl, {}, errorReason.message)
195+
);
171196
return;
172197
}
173198

@@ -247,12 +272,8 @@ export function PaymentMethodManagement({
247272
className="h-10 mt-10 w-full"
248273
type="submit"
249274
variant={ButtonVariant.Primary}
250-
aria-disabled={
251-
!stripe || !isComplete || isLoading
252-
}
253-
disabled={
254-
!stripe || !isComplete || isLoading
255-
}
275+
aria-disabled={!stripe || !isComplete || isLoading}
276+
disabled={!stripe || !isComplete || isLoading}
256277
>
257278
{isLoading ? (
258279
<Image

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,17 @@
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+
8+
export class PaymentMethodDetails {
9+
@IsString()
10+
id!: string;
11+
12+
@IsString()
13+
@IsOptional()
14+
type?: string;
15+
}
616

717
export class GetStripePaymentManagementDetailsResult {
818
@IsString()
@@ -11,9 +21,10 @@ export class GetStripePaymentManagementDetailsResult {
1121
@IsString()
1222
customer!: string;
1323

14-
@IsString()
24+
@ValidateNested({ each: true })
25+
@Type(() => PaymentMethodDetails)
1526
@IsOptional()
16-
defaultPaymentMethodId?: string;
27+
defaultPaymentMethod?: PaymentMethodDetails;
1728

1829
@IsString()
1930
currency!: string;

0 commit comments

Comments
 (0)