Skip to content

Commit

Permalink
Merge pull request #17790 from mozilla/FXA-7830-Page-Processing-actions
Browse files Browse the repository at this point in the history
feat(next): Set up cart status polling
  • Loading branch information
david1alvarez authored Oct 11, 2024
2 parents 9514993 + c910017 commit 496ecb0
Show file tree
Hide file tree
Showing 34 changed files with 559 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import Link from 'next/link';
import { DEFAULT_LOCALE } from '@fxa/shared/l10n';

import errorIcon from '@fxa/shared/assets/images/error.svg';
import { SupportedPages, getApp } from '@fxa/payments/ui/server';
import {
getApp,
CheckoutParams,
SupportedPages,
} from '@fxa/payments/ui/server';
import {
getCartOrRedirectAction,
recordEmitterEventAction,
Expand Down Expand Up @@ -40,13 +44,6 @@ const getErrorReason = (reason: CartErrorReasonId | null) => {
}
};

interface CheckoutParams {
cartId: string;
locale: string;
interval: string;
offeringId: string;
}

export default async function CheckoutError({
params,
searchParams,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
PriceInterval,
SubscriptionTitle,
TermsAndPrivacy,
CheckoutParams,
} from '@fxa/payments/ui/server';
import { DEFAULT_LOCALE } from '@fxa/shared/l10n';
import { config } from 'apps/payments/next/config';
Expand All @@ -22,13 +23,6 @@ export const metadata = {
description: 'Mozilla accounts',
};

export interface CheckoutParams {
cartId: string;
locale: string;
interval: string;
offeringId: string;
}

export interface CheckoutSearchParams {
experiment?: string;
promotion_code?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,28 @@
* 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/. */

import { headers } from 'next/headers';
import { LoadingSpinner } from '@fxa/payments/ui';
import {
CheckoutParams,
LoadingSpinner,
PollingSection,
} from '@fxa/payments/ui';
import { getApp } from '@fxa/payments/ui/server';
import { headers } from 'next/headers';
import { DEFAULT_LOCALE } from '@fxa/shared/l10n';

export default async function ProcessingPage() {
export default function ProcessingPage({ params }: { params: CheckoutParams }) {
const locale = headers().get('accept-language') || DEFAULT_LOCALE;
const l10n = getApp().getL10n(locale);

return (
<section
className="flex flex-col text-center text-sm"
data-testid="payment-processing"
>
<LoadingSpinner className="w-10 h-10" />
<PollingSection cartId={params.cartId} />
{l10n.getString(
'payment-processing-message',
'Please wait while we process your payment…'
'next-payment-processing-message',
`Please wait while we process your payment…`
)}
</section>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@
import { revalidatePath } from 'next/cache';
import { headers } from 'next/headers';
import { BaseButton, ButtonVariant, PaymentSection } from '@fxa/payments/ui';
import { getApp, SupportedPages } from '@fxa/payments/ui/server';
import { getApp, SupportedPages, CheckoutParams, } from '@fxa/payments/ui/server';
import { getCartOrRedirectAction } from '@fxa/payments/ui/actions';
import { DEFAULT_LOCALE } from '@fxa/shared/l10n';
import {
getFakeCartData,
getCMSContent,
} from 'apps/payments/next/app/_lib/apiClient';
import { auth, signIn } from 'apps/payments/next/auth';
import { CheckoutParams } from '../layout';

export const dynamic = 'force-dynamic';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getCartOrRedirectAction,
recordEmitterEventAction,
} from '@fxa/payments/ui/actions';
import { CheckoutParams } from '@fxa/payments/ui/server';

export const dynamic = 'force-dynamic';

Expand All @@ -41,13 +42,6 @@ const ConfirmationDetail = ({
);
};

interface CheckoutParams {
cartId: string;
locale: string;
interval: string;
offeringId: string;
}

export default async function CheckoutSuccess({
params,
searchParams,
Expand Down
3 changes: 1 addition & 2 deletions apps/payments/next/app/[locale]/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
import { useEffect, useState } from 'react';
import * as Sentry from '@sentry/nextjs';
import { useRouter, useParams } from 'next/navigation';
import { CheckoutParams } from './[offeringId]/checkout/[interval]/[cartId]/layout';
import Image from 'next/image';
import errorIcon from '@fxa/shared/assets/images/error.svg';
import { LoadingSpinner } from '@fxa/payments/ui';
import { CheckoutParams, LoadingSpinner } from '@fxa/payments/ui';
import Link from 'next/link';
import { Localized } from '@fluent/react';
import { restartCartAction, getCartAction } from '@fxa/payments/ui/actions';
Expand Down
8 changes: 8 additions & 0 deletions libs/payments/cart/src/lib/cart.error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,11 @@ export class CartInvalidCurrencyError extends CartError {
});
}
}

export class CartSubscriptionNotFoundError extends CartError {
constructor(cartId: string) {
super('Cart subscription not found', {
cartId,
});
}
}
18 changes: 17 additions & 1 deletion libs/payments/cart/src/lib/cart.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@ import type { AccountDatabase } from '@fxa/shared/db/mysql/account';
// For an action to be executed, the cart state needs to be in one of
// valid states listed in the array of CartStates below
const ACTIONS_VALID_STATE = {
updateFreshCart: [CartState.START],
updateFreshCart: [CartState.START, CartState.PROCESSING],
finishCart: [CartState.PROCESSING],
finishErrorCart: [CartState.START, CartState.PROCESSING],
deleteCart: [CartState.START, CartState.PROCESSING],
restartCart: [CartState.START, CartState.PROCESSING, CartState.FAIL],
setProcessingCart: [CartState.START],
};

// Type guard to check if action is valid key in ACTIONS_VALID_STATE
Expand Down Expand Up @@ -180,6 +181,21 @@ export class CartManager {
}
}

public async setProcessingCart(cartId: string) {
const cart = await this.fetchCartById(cartId);

this.checkActionForValidCartState(cart, 'setProcessingCart');

try {
await updateCart(this.db, Buffer.from(cartId, 'hex'), cart.version, {
state: CartState.PROCESSING,
});
} catch (error) {
const cause = error instanceof CartNotUpdatedError ? undefined : error;
throw new CartNotUpdatedError(cartId, cause);
}
}

public async deleteCart(cart: ResultCart) {
this.checkActionForValidCartState(cart, 'deleteCart');

Expand Down
18 changes: 16 additions & 2 deletions libs/payments/cart/src/lib/cart.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
StripeResponseFactory,
MockStripeConfigProvider,
AccountCustomerManager,
StripePaymentIntentFactory,
} from '@fxa/payments/stripe';
import {
MockProfileClientConfigProvider,
Expand Down Expand Up @@ -214,6 +215,7 @@ describe('CartService', () => {
.spyOn(currencyManager, 'getCurrencyForCountry')
.mockReturnValue(mockResolvedCurrency);
jest.spyOn(cartManager, 'createCart').mockResolvedValue(mockResultCart);
jest.spyOn(accountManager, 'getAccounts').mockResolvedValue([]);

const result = await cartService.setupCart(args);

Expand Down Expand Up @@ -330,9 +332,15 @@ describe('CartService', () => {
it('accepts payment with stripe', async () => {
const mockCart = ResultCartFactory();
const mockPaymentMethodId = faker.string.uuid();
const mockPaymentIntent = StripePaymentIntentFactory({
payment_method: mockPaymentMethodId,
status: 'succeeded',
});

jest.spyOn(cartManager, 'fetchCartById').mockResolvedValue(mockCart);
jest.spyOn(checkoutService, 'payWithStripe').mockResolvedValue();
jest
.spyOn(checkoutService, 'payWithStripe')
.mockResolvedValue(mockPaymentIntent);
jest.spyOn(cartManager, 'finishCart').mockResolvedValue();
jest.spyOn(cartManager, 'finishErrorCart').mockResolvedValue();

Expand All @@ -359,9 +367,15 @@ describe('CartService', () => {
it('calls cartManager.finishErrorCart when error occurs during checkout', async () => {
const mockCart = ResultCartFactory();
const mockPaymentMethodId = faker.string.uuid();
const mockPaymentIntent = StripePaymentIntentFactory({
payment_method: mockPaymentMethodId,
status: 'succeeded',
});

jest.spyOn(cartManager, 'fetchCartById').mockResolvedValue(mockCart);
jest.spyOn(checkoutService, 'payWithStripe').mockResolvedValue();
jest
.spyOn(checkoutService, 'payWithStripe')
.mockResolvedValue(mockPaymentIntent);
jest.spyOn(cartManager, 'finishCart').mockRejectedValue(undefined);
jest.spyOn(cartManager, 'finishErrorCart').mockResolvedValue();

Expand Down
Loading

0 comments on commit 496ecb0

Please sign in to comment.