Skip to content

Commit

Permalink
composite-checkout: Throw early in useProcessPayment if processor not…
Browse files Browse the repository at this point in the history
… found (#96737)

* Throw early in useProcessPayment if processor not found

useProcessPayment creates the callback sent to every payment method
inside a `CheckoutProvider` when rendering `CheckoutSubmitButton`. It
uses the payment processor ID of the currently selected payment method
to find the payment processor function and then call it. If the function
cannot be found, it throws an error.

However, the error it throws is inside the callback, which is an async
function, and errors thrown inside async functions do not trigger React
Error boundaries (see facebook/react#14981).
Therefore, any errors thrown here will be displayed in the JS console
but never shown to the user or logged.

In this change, we modify useProcessPayment to call usePaymentProcessor
to find its processor function at render time instead of during the
async callback. If the function is not found, this will cause an error
to be thrown during render, allowing Error Boundaries to catch it.

* Make sure tests have paymentProcessors during render

* Make sure existing-credit-card test also has paymentProcessors set
  • Loading branch information
sirbrillig authored Nov 25, 2024
1 parent ff4ed83 commit 634dcfe
Show file tree
Hide file tree
Showing 4 changed files with 35 additions and 14 deletions.
4 changes: 2 additions & 2 deletions client/my-sites/checkout/src/test/credit-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,14 @@ function CompleteCreditCardFields() {

describe( 'Credit card payment method', () => {
it( 'renders a credit card option', async () => {
render( <TestWrapper /> );
render( <TestWrapper paymentProcessors={ { card: () => makeSuccessResponse( 'ok' ) } } /> );
await waitFor( () => {
expect( screen.queryByText( 'Credit or debit card' ) ).toBeInTheDocument();
} );
} );

it( 'renders submit button when credit card is selected', async () => {
render( <TestWrapper /> );
render( <TestWrapper paymentProcessors={ { card: () => makeSuccessResponse( 'ok' ) } } /> );
await waitFor( () => {
expect( screen.queryByText( activePayButtonText ) ).toBeInTheDocument();
} );
Expand Down
18 changes: 16 additions & 2 deletions client/my-sites/checkout/src/test/existing-credit-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,14 @@ describe( 'Existing credit card payment methods', () => {
it( 'renders an existing card option for a stored card', async () => {
mockTaxLocationEndpoint();
const existingCard = getExistingCardPaymentMethod();
render( <TestWrapper paymentMethods={ [ existingCard ] }></TestWrapper> );
render(
<TestWrapper
paymentMethods={ [ existingCard ] }
paymentProcessors={ {
[ existingCard.paymentProcessorId ]: () => makeSuccessResponse( 'ok' ),
} }
></TestWrapper>
);
await waitFor( () => {
expect( screen.queryByText( cardholderName ) ).toBeInTheDocument();
} );
Expand All @@ -93,7 +100,14 @@ describe( 'Existing credit card payment methods', () => {
it( 'renders an existing card button when an existing card is selected', async () => {
mockTaxLocationEndpoint();
const existingCard = getExistingCardPaymentMethod();
render( <TestWrapper paymentMethods={ [ existingCard ] }></TestWrapper> );
render(
<TestWrapper
paymentMethods={ [ existingCard ] }
paymentProcessors={ {
[ existingCard.paymentProcessorId ]: () => makeSuccessResponse( 'ok' ),
} }
></TestWrapper>
);
await waitFor( () => {
expect( screen.queryByText( activePayButtonText ) ).toBeInTheDocument();
} );
Expand Down
14 changes: 12 additions & 2 deletions client/my-sites/checkout/src/test/netbanking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,25 @@ function ResetNetbankingStoreFields() {
describe( 'Netbanking payment method', () => {
it( 'renders a netbanking option', async () => {
const paymentMethod = getPaymentMethod();
render( <TestWrapper paymentMethods={ [ paymentMethod ] }></TestWrapper> );
render(
<TestWrapper
paymentMethods={ [ paymentMethod ] }
paymentProcessors={ { netbanking: () => makeSuccessResponse( 'ok' ) } }
></TestWrapper>
);
await waitFor( () => {
expect( screen.queryByText( 'Net Banking' ) ).toBeInTheDocument();
} );
} );

it( 'renders submit button when netbanking is selected', async () => {
const paymentMethod = getPaymentMethod();
render( <TestWrapper paymentMethods={ [ paymentMethod ] }></TestWrapper> );
render(
<TestWrapper
paymentMethods={ [ paymentMethod ] }
paymentProcessors={ { netbanking: () => makeSuccessResponse( 'ok' ) } }
></TestWrapper>
);
await waitFor( () => {
expect( screen.queryByText( activePayButtonText ) ).toBeInTheDocument();
} );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useI18n } from '@wordpress/react-i18n';
import debugFactory from 'debug';
import { useCallback, useMemo, useState } from 'react';
import InvalidPaymentProcessorResponseError from '../lib/invalid-payment-processor-response-error';
import { usePaymentProcessors, useTransactionStatus } from '../public-api';
import { usePaymentProcessor, useTransactionStatus } from '../public-api';
import {
PaymentProcessorResponse,
PaymentProcessorResponseType,
Expand All @@ -12,25 +12,22 @@ import {
SetTransactionError,
} from '../types';

const debug = debugFactory( 'composite-checkout:use-create-payment-processor-on-click' );
const debug = debugFactory( 'composite-checkout:use-process-payment' );

export default function useProcessPayment( paymentProcessorId: string ): ProcessPayment {
const paymentProcessors = usePaymentProcessors();
const { setTransactionPending } = useTransactionStatus();
const handlePaymentProcessorPromise = useHandlePaymentProcessorResponse();
const processor = usePaymentProcessor( paymentProcessorId );

return useCallback(
async ( submitData ) => {
debug( 'beginning payment processor onClick handler' );
if ( ! paymentProcessors[ paymentProcessorId ] ) {
throw new Error( `No payment processor found with key: ${ paymentProcessorId }` );
}
setTransactionPending();
debug( 'calling payment processor function', paymentProcessorId );
const response = paymentProcessors[ paymentProcessorId ]( submitData );
const response = processor( submitData );
return handlePaymentProcessorPromise( paymentProcessorId, response );
},
[ paymentProcessorId, handlePaymentProcessorPromise, paymentProcessors, setTransactionPending ]
[ paymentProcessorId, handlePaymentProcessorPromise, processor, setTransactionPending ]
);
}

Expand Down

0 comments on commit 634dcfe

Please sign in to comment.