Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import Link from 'next/link';

import errorIcon from '@fxa/shared/assets/images/error.svg';
import checkIcon from '@fxa/shared/assets/images/check.svg';
import { redirect } from 'next/navigation';
import { URLSearchParams } from 'url';
import {
getApp,
CheckoutParams,
Expand Down Expand Up @@ -51,6 +53,20 @@ export default async function CheckoutError({
params: CheckoutParams;
searchParams: Record<string, string | string[]>;
}) {
if (
searchParams?.payment_intent ||
searchParams?.payment_intent_client_secret ||
searchParams?.redirect_status
) {
const cleanedParams = new URLSearchParams(searchParams);
cleanedParams.delete('payment_intent');
cleanedParams.delete('payment_intent_client_secret');
cleanedParams.delete('redirect_status');
const queryParamString = `?${cleanedParams.toString()}`;
redirect(
`/${params.locale}/${params.offeringId}/${params.interval}/checkout/${params.cartId}/error${queryParamString}`
);
}
const { locale } = params;
const acceptLanguage = headers().get('accept-language');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ import {
buildPageMetadata,
} from '@fxa/payments/ui/server';
import { headers } from 'next/headers';
import { validateCartStateAndRedirectAction } from '@fxa/payments/ui/actions';
import {
validateCartStateAndRedirectAction,
handleStripeRedirect
} from '@fxa/payments/ui/actions';
import type { Metadata } from 'next';
import { config } from 'apps/payments/next/config';
import { revalidatePath } from 'next/cache';

export async function generateMetadata({
params,
Expand All @@ -41,9 +45,22 @@ export default async function ProcessingPage({
params: CheckoutParams;
searchParams: Record<string, string | string[]> | undefined;
}) {
const { locale } = params;
const { locale, cartId, offeringId, interval } = params;
const acceptLanguage = headers().get('accept-language');
const l10n = getApp().getL10n(acceptLanguage, locale);

const paymentIntentId = typeof searchParams?.payment_intent === 'string' ? searchParams.payment_intent : null;
const clientSecret = typeof searchParams?.payment_intent_client_secret === 'string' ? searchParams.payment_intent_client_secret : null;
const redirectStatus = typeof searchParams?.redirect_status === 'string' ? searchParams.redirect_status : null;

if (paymentIntentId && clientSecret && redirectStatus) {
try {
await handleStripeRedirect(cartId, paymentIntentId, redirectStatus);
} catch {
revalidatePath(`/${locale}/${offeringId}/${interval}/checkout/${cartId}/processing`);
}
}

await validateCartStateAndRedirectAction(
params.cartId,
SupportedPages.PROCESSING,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import type { Metadata } from 'next';
import { headers } from 'next/headers';
import { URLSearchParams } from 'url';
import Image from 'next/image';
import { auth } from 'apps/payments/next/auth';
import { getCardIcon } from '@fxa/payments/ui';
Expand All @@ -19,6 +20,7 @@ import {
buildPageMetadata,
} from '@fxa/payments/ui/server';
import { config } from 'apps/payments/next/config';
import { redirect } from 'next/navigation';

export const dynamic = 'force-dynamic';

Expand Down Expand Up @@ -46,6 +48,20 @@ export default async function CheckoutSuccess({
params: CheckoutParams;
searchParams: Record<string, string | string[]>;
}) {
if (
searchParams?.payment_intent ||
searchParams?.payment_intent_client_secret ||
searchParams?.redirect_status
) {
const cleanedParams = new URLSearchParams(searchParams);
cleanedParams.delete('payment_intent');
cleanedParams.delete('payment_intent_client_secret');
cleanedParams.delete('redirect_status');
const queryParamString = `?${cleanedParams.toString()}`;
redirect(
`/${params.locale}/${params.offeringId}/${params.interval}/checkout/${params.cartId}/success${queryParamString}`
);
}
const { locale } = params;
const acceptLanguage = headers().get('accept-language');

Expand Down Expand Up @@ -162,6 +178,13 @@ export default async function CheckoutSuccess({
width={70}
height={24}
/>
) : cart.paymentInfo.type === 'amazon_pay' ? (
<Image
src={getCardIcon('amazon_pay', l10n).img}
alt={l10n.getString('amazon-pay-logo-alt-text', 'Amazon Pay logo')}
width={70}
height={24}
/>
) : (
<span className="flex items-center gap-2">
{cart.paymentInfo.brand && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import { headers } from 'next/headers';
import Image from 'next/image';
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { URLSearchParams } from 'url';

import errorIcon from '@fxa/shared/assets/images/error.svg';
import {
Expand Down Expand Up @@ -49,6 +51,20 @@ export default async function UpgradeError({
params: CheckoutParams;
searchParams: Record<string, string | string[]>;
}) {
if (
searchParams?.payment_intent ||
searchParams?.payment_intent_client_secret ||
searchParams?.redirect_status
) {
const cleanedParams = new URLSearchParams(searchParams);
cleanedParams.delete('payment_intent');
cleanedParams.delete('payment_intent_client_secret');
cleanedParams.delete('redirect_status');
const queryParamString = `?${cleanedParams.toString()}`;
redirect(
`/${params.locale}/${params.offeringId}/${params.interval}/upgrade/${params.cartId}/error${queryParamString}`
);
}
const { locale } = params;
const acceptLanguage = headers().get('accept-language');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { CheckoutParams } from '@fxa/payments/ui/server';
import Image from 'next/image';
import { Metadata } from 'next';
import { config } from 'apps/payments/next/config';
import { redirect } from 'next/navigation';
import { URLSearchParams } from 'url';

export const dynamic = 'force-dynamic';

Expand Down Expand Up @@ -46,6 +48,20 @@ export default async function UpgradeSuccess({
params: CheckoutParams;
searchParams: Record<string, string | string[]>;
}) {
if (
searchParams?.payment_intent ||
searchParams?.payment_intent_client_secret ||
searchParams?.redirect_status
) {
const cleanedParams = new URLSearchParams(searchParams);
cleanedParams.delete('payment_intent');
cleanedParams.delete('payment_intent_client_secret');
cleanedParams.delete('redirect_status');
const queryParamString = `?${cleanedParams.toString()}`;
redirect(
`/${params.locale}/${params.offeringId}/${params.interval}/upgrade/${params.cartId}/success${queryParamString}`
);
}
const { locale } = params;
const acceptLanguage = headers().get('accept-language');

Expand Down Expand Up @@ -163,7 +179,14 @@ export default async function UpgradeSuccess({
width={70}
height={24}
/>
) :(
) : cart.paymentInfo.type === 'amazon_pay' ? (
<Image
src={getCardIcon('amazon_pay', l10n).img}
alt={l10n.getString('amazon-pay-logo-alt-text', 'Amazon Pay logo')}
width={70}
height={24}
/>
) : (
<span className="flex items-center gap-2">
{cart.paymentInfo.brand && (
<Image
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ export default async function Upgrade({
width={70}
height={24}
/>
) : cart.paymentInfo.type === 'amazon_pay' ? (
<Image
src={getCardIcon('amazon_pay', l10n).img}
alt={l10n.getString('amazon-pay-logo-alt-text', 'Amazon Pay logo')}
width={70}
height={24}
/>
) : (
<span className="flex items-center gap-2">
{cart.paymentInfo.brand && (
Expand Down
1 change: 1 addition & 0 deletions apps/payments/next/app/[locale]/en.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ visa-logo-alt-text = { -brand-visa } logo
# Alt text for generic payment card logo
unbranded-logo-alt-text = Unbranded logo
link-logo-alt-text = { -brand-link } logo
amazon-pay-logo-alt-text = { -brand-amazon-pay } logo

## Error pages - /checkout and /upgrade
## Common strings used in multiple pages
Expand Down
26 changes: 26 additions & 0 deletions apps/payments/next/app/[locale]/subscriptions/manage/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,32 @@ export default async function Manage({
</div>
)}

{type === 'amazon_pay' && (
<div className="flex items-center justify-between">
<div className="leading-5 text-sm">
<Image
src={getCardIcon('amazon_pay', l10n).img}
alt={l10n.getString('amazon-pay-logo-alt-text', 'Amazon Pay logo')}
width={70}
height={24}
/>
</div>
<Link
className={CSS_SECONDARY_LINK}
href={`${config.paymentsNextHostedUrl}/${locale}/subscriptions/payments/stripe`}
aria-label={l10n.getString(
'subscription-management-button-change-payment-method-aria',
'Change payment method'
)}
>
{l10n.getString(
'subscription-management-button-change-payment-method',
'Change'
)}
</Link>
</div>
)}

{type === 'external_paypal' && brand && (
<div className="flex items-center justify-between">
<div className="leading-5 text-sm">
Expand Down
1 change: 1 addition & 0 deletions libs/payments/cart/src/lib/cart.factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export const PaymentInfoFactory = (
'apple_iap',
'external_paypal',
'link',
'amazon_pay',
]),
...override,
});
Expand Down
51 changes: 51 additions & 0 deletions libs/payments/cart/src/lib/cart.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1083,4 +1083,55 @@ export class CartService {
}
});
}

@SanitizeExceptions()
async handleStripeRedirect(
cartId: string,
paymentIntentId: string,
redirectStatus: string
) {
return this.wrapWithCartCatch(cartId, async () => {
const cart = await this.cartManager.fetchCartById(cartId);

if (cart.stripeIntentId !== paymentIntentId) {
await this.cartManager.updateFreshCart(cartId, cart.version, {
stripeIntentId: paymentIntentId,
});
}

const isPayIntentId = isPaymentIntentId(paymentIntentId);

for (let i = 0; i < 10; i++) {
const intent = isPayIntentId
? await this.paymentIntentManager.retrieve(paymentIntentId)
: await this.setupIntentManager.retrieve(paymentIntentId);

if (redirectStatus === 'succeeded'|| intent.status === 'succeeded') {
await this.submitNeedsInput(cartId);
return;
}

if (redirectStatus === 'failed' || intent.status === 'requires_payment_method') {
const errorCode = isPaymentIntent(intent)
? intent.last_payment_error?.code
: intent.last_setup_error?.code;
const declineCode = isPaymentIntent(intent)
? intent.last_payment_error?.decline_code
: intent.last_setup_error?.decline_code;

throwIntentFailedError(
errorCode,
declineCode,
cart.id,
intent.id,
isPaymentIntentId(intent.id) ? 'PaymentIntent' : 'SetupIntent'
);
}

await new Promise((r) => setTimeout(r, 500));
}

throw new SubmitNeedsInputFailedError(cartId);
});
}
}
13 changes: 13 additions & 0 deletions libs/payments/cart/src/lib/checkout.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import { throwIntentFailedError } from './util/throwIntentFailedError';
import type { AsyncLocalStorage } from 'async_hooks';
import { AsyncLocalStorageCart } from './cart-als.provider';
import type { CartStore } from './cart-als.types';
import { CartEligibilityStatus } from '@fxa/shared/db/mysql/account';

@Injectable()
export class CheckoutService {
Expand Down Expand Up @@ -397,11 +398,23 @@ export class CheckoutService {
);

if (invoice.payment_intent && invoice.amount_due !== 0) {
const pageType = cart.eligibilityStatus === CartEligibilityStatus.UPGRADE
? 'upgrade'
: 'checkout';

const baseUrl = process.env['PAYMENTS_NEXT_HOSTED_URL'];

intent = await this.paymentIntentManager.confirm(
invoice.payment_intent,
{
confirmation_token: confirmationTokenId,
off_session: false,
...(baseUrl && {
return_url:
`${baseUrl}` +
`/${customerData.locale}/${cart.offeringConfigId}/${cart.interval}` +
`/${pageType}/${cart.id}/processing`,
}),
}
);
} else {
Expand Down
1 change: 1 addition & 0 deletions libs/payments/metrics/src/lib/glean/glean.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const PaymentProvidersTypePartial = [
'apple_iap',
'external_paypal',
'link',
'amazon_pay',
] as const;
export type PaymentProvidersType =
| Stripe.PaymentMethod.Type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const StripePaymentIntentFactory = (
persistent_token: null,
},
},
payment_method_types: ['card', 'link'],
payment_method_types: ['card', 'link', 'amazon_pay'],
processing: null,
receipt_email: null,
review: null,
Expand Down
19 changes: 19 additions & 0 deletions libs/payments/ui/src/lib/actions/handleStripeRedirect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

'use server';

import { getApp } from '../nestapp/app';

export async function handleStripeRedirect(
cartId: string,
paymentIntentId: string,
redirectStatus: string,
) {
await getApp().getActionsService().handleStripeRedirect({
cartId,
paymentIntentId,
redirectStatus,
});
}
1 change: 1 addition & 0 deletions libs/payments/ui/src/lib/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ export { serverLogAction } from './serverLog';
export { getStripeClientSession } from './getStripeClientSession';
export { updateStripePaymentDetails } from './updateStripePaymentDetails';
export { setDefaultStripePaymentDetails } from './setDefaultStripePaymentDetails';
export { handleStripeRedirect } from './handleStripeRedirect';
Loading