Skip to content

Commit c6a88c8

Browse files
fix(payments-next): Location can't be set to Denmark, Poland and Czech Republic on the "Set up your subscription" page for Mozilla VPN monthly subscription (Stage only)
Because: * When a user changes their location to use a currency that is not supported by a price set in Stripe, they were being shown a non-informative error message. * The call to stripe.invoices.retrieveUpcoming throws an error saying “Error: The price specified only supports `eur` or `usd`. This doesn't match the invoice's currency: `pln`." (in the case of dev VPN daily). * The user's cart was being transitioned into an error state, and user was taken to the “Error confirming subscription…” page. * Even though the cart was transitioned into an error state and error thrown, the error being thrown was caught by the try catch in the SelectTaxLocation component’s handleFormSubmit, so the user wasn't taken to the error page right away. This commit: * Updates the SelectTaxLocation component to show the customer a more informative error message in this situation * Does not transition cart into an error state Closes #[PAY-3566](https://mozilla-hub.atlassian.net/browse/PAY-3566)
1 parent 82aa181 commit c6a88c8

10 files changed

Lines changed: 71 additions & 28 deletions

File tree

apps/payments/next/app/[locale]/[offeringId]/[interval]/checkout/[cartId]/layout.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ export default async function CheckoutLayout({
130130
countryCode,
131131
postalCode,
132132
},
133-
session?.user?.id
133+
session?.user?.id,
134+
params.interval
134135
);
135136

136137
if (result.ok) {

apps/payments/next/app/[locale]/[offeringId]/[interval]/location/page.tsx

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export default async function Location({
5757
try {
5858
const [cmsData, validateLocationResults] = await Promise.all([
5959
fetchCMSData(params.offeringId, acceptLanguage, params.locale),
60-
validateLocationAction(params.offeringId, taxAddress, fxaUid),
60+
validateLocationAction(params.offeringId, taxAddress, fxaUid, params.interval),
6161
]);
6262
cms = cmsData;
6363
locationStatus = validateLocationResults.status;
@@ -117,7 +117,8 @@ export default async function Location({
117117
)}
118118
</Banner>
119119
) : locationStatus === LocationStatus.ProductNotAvailable ||
120-
locationStatus === TaxChangeAllowedStatus.CurrencyNotFound ? (
120+
locationStatus === TaxChangeAllowedStatus.CurrencyNotFound ||
121+
locationStatus === TaxChangeAllowedStatus.PriceCurrencyNotAvailable ? (
121122
<Banner variant={BannerVariant.Error} showCloseButton={true}>
122123
{l10n.getString(
123124
'select-tax-location-product-not-available',
@@ -160,20 +161,18 @@ export default async function Location({
160161
saveAction={async (countryCode: string, postalCode: string) => {
161162
'use server';
162163

163-
if (fxaUid) {
164-
// call server Action here to validate if tax location change is allowed
165-
const result = await validateLocationAction(
166-
params.offeringId,
167-
{ countryCode, postalCode },
168-
fxaUid
169-
);
164+
const result = await validateLocationAction(
165+
params.offeringId,
166+
{ countryCode, postalCode },
167+
fxaUid,
168+
params.interval
169+
);
170170

171-
if (!result.isValid) {
172-
return {
173-
ok: false,
174-
error: result.status,
175-
};
176-
}
171+
if (!result.isValid) {
172+
return {
173+
ok: false,
174+
error: result.status,
175+
};
177176
}
178177

179178
searchParams['countryCode'] = countryCode;

apps/payments/next/app/[locale]/[offeringId]/[interval]/new/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ export default async function New({
7979
const { isValid: locationIsValid } = await validateLocationAction(
8080
offeringId,
8181
taxAddress,
82-
fxaUid
82+
fxaUid,
83+
interval
8384
);
8485

8586
if (!taxAddress || !locationIsValid) {

libs/payments/cart/src/lib/tax.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
export enum TaxChangeAllowedStatus {
66
CurrencyNotFound = 'currency_not_found',
77
CurrencyChange = 'currency_change',
8+
PriceCurrencyNotAvailable = 'price_currency_not_available',
89
Allowed = 'allowed',
910
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ export const updateTaxAddressAction = async (
1414
version: number,
1515
offeringId: string,
1616
taxAddress: TaxAddress,
17-
uid?: string
17+
uid?: string,
18+
interval?: string
1819
) => {
1920
const actionsService = getApp().getActionsService();
2021

@@ -24,6 +25,7 @@ export const updateTaxAddressAction = async (
2425
offeringId,
2526
taxAddress,
2627
uid,
28+
interval,
2729
});
2830

2931
revalidatePath(

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import { getApp } from '../nestapp/app';
99
export const validateLocationAction = async (
1010
offeringId: string,
1111
taxAddress?: TaxAddress,
12-
uid?: string
12+
uid?: string,
13+
interval?: string
1314
) => {
1415
return await getApp()
1516
.getActionsService()
16-
.validateLocation({ offeringId, taxAddress, uid });
17+
.validateLocation({ offeringId, taxAddress, uid, interval });
1718
};

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import spinnerWhiteImage from '@fxa/shared/assets/images/spinnerwhite.svg';
1818

1919
enum SaveActionErrors {
2020
CURRENCY_CHANGE_NOT_ALLOWED = 'currency_change', //TaxChangeAllowedStatus.CurrencyChange
21+
PRICE_CURRENCY_NOT_AVAILABLE = 'price_currency_not_available', //TaxChangeAllowedStatus.PriceCurrencyNotAvailable
2122
}
2223

2324
type SaveActionSignature = (
@@ -220,6 +221,13 @@ const Expanded = ({
220221
...prev,
221222
invalidCurrencyChange: true,
222223
}));
224+
} else if (
225+
result.error === SaveActionErrors.PRICE_CURRENCY_NOT_AVAILABLE
226+
) {
227+
setServerErrors((prev) => ({
228+
...prev,
229+
productNotAvailable: true,
230+
}));
223231
} else {
224232
setServerErrors((prev) => ({
225233
...prev,

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

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -941,6 +941,7 @@ export class NextJSActionsService {
941941
offeringId: string;
942942
taxAddress: TaxAddress;
943943
uid?: string;
944+
interval?: string;
944945
}): Promise<
945946
| {
946947
ok: true;
@@ -951,13 +952,14 @@ export class NextJSActionsService {
951952
error: string;
952953
}
953954
> {
954-
const { cartId, version, offeringId, taxAddress, uid } = args;
955+
const { cartId, version, offeringId, taxAddress, uid, interval } = args;
955956

956957
// Validate Tax Address before updating
957958
const { isValid, status } = await this.validateLocation({
958959
offeringId,
959960
taxAddress,
960961
uid,
962+
interval,
961963
});
962964
if (!isValid) {
963965
return {
@@ -1009,6 +1011,7 @@ export class NextJSActionsService {
10091011
offeringId: string;
10101012
taxAddress?: TaxAddress;
10111013
uid?: string;
1014+
interval?: string;
10121015
}) {
10131016
const { status: locationStatus } =
10141017
await this.eligibilityService.getProductAvailabilityForLocation(
@@ -1020,17 +1023,36 @@ export class NextJSActionsService {
10201023
if (args.uid && args.taxAddress) {
10211024
const { status: taxChangeStatus, currentCurrency } =
10221025
await this.taxService.getTaxChangeStatus(args.uid, args.taxAddress);
1023-
return {
1024-
isValid: taxChangeStatus === TaxChangeAllowedStatus.Allowed,
1025-
status: taxChangeStatus,
1026-
currentCurrency,
1027-
};
1028-
} else {
1026+
if (taxChangeStatus !== TaxChangeAllowedStatus.Allowed) {
1027+
return {
1028+
isValid: false,
1029+
status: taxChangeStatus,
1030+
currentCurrency,
1031+
};
1032+
}
1033+
}
1034+
if (args.interval && args.taxAddress?.countryCode) {
1035+
const currency = this.currencyManager.getCurrencyForCountry(
1036+
args.taxAddress.countryCode
1037+
);
1038+
if (currency) {
1039+
const price =
1040+
await this.productConfigurationManager.retrieveStripePrice(
1041+
args.offeringId,
1042+
args.interval as SubplatInterval
1043+
);
1044+
if (price && !price.currency_options?.[currency]) {
1045+
return {
1046+
isValid: false,
1047+
status: TaxChangeAllowedStatus.PriceCurrencyNotAvailable,
1048+
};
1049+
}
1050+
}
1051+
}
10291052
return {
10301053
isValid: true,
10311054
status: locationStatus,
10321055
};
1033-
}
10341056
} else {
10351057
return {
10361058
isValid: false,

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,8 @@ export class UpdateTaxAddressActionArgs {
2828
@IsOptional()
2929
@IsString()
3030
uid?: string;
31+
32+
@IsOptional()
33+
@IsString()
34+
interval?: string;
3135
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,8 @@ export class ValidateLocationActionArgs {
1515
@IsString()
1616
@IsOptional()
1717
uid?: string;
18+
19+
@IsString()
20+
@IsOptional()
21+
interval?: string;
1822
}

0 commit comments

Comments
 (0)