diff --git a/client/my-sites/checkout/composite-checkout/hooks/use-create-payment-complete-callback.tsx b/client/my-sites/checkout/composite-checkout/hooks/use-create-payment-complete-callback.tsx index a0a9135c7b346..6db3778065f54 100644 --- a/client/my-sites/checkout/composite-checkout/hooks/use-create-payment-complete-callback.tsx +++ b/client/my-sites/checkout/composite-checkout/hooks/use-create-payment-complete-callback.tsx @@ -104,7 +104,7 @@ export default function useCreatePaymentCompleteCallback( { productAliasFromUrl, isEligibleForSignupDestinationResult, shouldShowOneClickTreatment, - hideNudge: !! isComingFromUpsell, + hideNudge: isComingFromUpsell, isInEditor, previousRoute, }; diff --git a/packages/calypso-stripe/src/index.tsx b/packages/calypso-stripe/src/index.tsx index 35ce49c52a801..92af16a3bdaef 100644 --- a/packages/calypso-stripe/src/index.tsx +++ b/packages/calypso-stripe/src/index.tsx @@ -538,20 +538,10 @@ export function StripeHookProvider( { */ export function useStripe(): StripeData { const stripeData = useContext( StripeContext ); - return ( - stripeData || { - stripe: null, - stripeConfiguration: null, - isStripeLoading: false, - stripeLoadingError: null, - reloadStripeConfiguration: () => { - // eslint-disable-next-line no-console - console.error( - `You cannot use reloadStripeConfiguration until stripe has been initialized.` - ); - }, - } - ); + if ( ! stripeData ) { + throw new Error( 'useStripe can only be used inside a StripeHookProvider' ); + } + return stripeData; } /** diff --git a/packages/composite-checkout/README.md b/packages/composite-checkout/README.md index 3a165b196f897..b256fd9337dd5 100644 --- a/packages/composite-checkout/README.md +++ b/packages/composite-checkout/README.md @@ -107,7 +107,7 @@ If not using the `onClick` function, when the `submitButton` component has been 1. Call `setTransactionPending()` from [useTransactionStatus](#useTransactionStatus). This will change the [form status](#useFormStatus) to [`.SUBMITTING`](#FormStatus) and disable the form. 2. Call the payment processor function returned from [usePaymentProcessor](#usePaymentProcessor]), passing whatever data that function requires. Each payment processor will be different, so you'll need to know the API of that function explicitly. -3. Payment processor functions return a `Promise`. When the `Promise` resolves, call `setTransactionComplete()` from [useTransactionStatus](#useTransactionStatus) if the transaction was a success. Depending on the payment processor, some transactions might require additional actions before they are complete. If the transaction requires a redirect, call `setTransactionRedirecting(url: string)` instead. +3. Payment processor functions return a `Promise`. When the `Promise` resolves, check its value (it will be one of [makeManualResponse](#makeManualResponse), [makeRedirectResponse](#makeRedirectResponse), or [makeSuccessResponse](#makeSuccessResponse)). Then call `setTransactionComplete(responseData: unknown)` from [useTransactionStatus](#useTransactionStatus) if the transaction was a success. If the transaction requires a redirect, call `setTransactionRedirecting(url: string)` instead. 4. If the `Promise` rejects, call `setTransactionError(message: string)`. 5. At this point the [CheckoutProvider](#CheckoutProvider) will automatically take action if the transaction status is [`.COMPLETE`](#TransactionStatus) (call [onPaymentComplete](#CheckoutProvider)), [`.ERROR`](#TransactionStatus) (display the error and re-enable the form), or [`.REDIRECTING`](#TransactionStatus) (redirect to the url). If for some reason the transaction should be cancelled, call `resetTransaction()`. @@ -506,6 +506,10 @@ A React Hook that returns a payment processor function as passed to the `payment A React Hook that returns all the payment processor functions in a Record. +### useProcessPayment + +A React Hook that will return the function passed to each [payment method's submitButton component](#payment-methods). Call it with a payment method ID and data for the payment processor and it will handle the transaction. + ### useRegisterStore A React Hook that can be used to create a @wordpress/data store. This is the same as calling `registerStore()` but is easier to use within functional components because it will only create the store once. Only works within [CheckoutProvider](#CheckoutProvider). @@ -534,9 +538,9 @@ A React Hook that returns an object with the following properties to be used by - `previousTransactionStatus: `[`TransactionStatus.`](#TransactionStatus). The last status of the transaction. - `transactionError: string | null`. The most recent error message encountered by the transaction if its status is [`.ERROR`](#TransactionStatus). - `transactionRedirectUrl: string | null`. The redirect url to use if the transaction status is [`.REDIRECTING`](#TransactionStatus). -- `transactionLastResponse: object | null`. The most recent full response object as returned by the transaction's endpoint and passed to `setTransactionComplete`. +- `transactionLastResponse: unknown | null`. The most recent full response object as returned by the transaction's endpoint and passed to `setTransactionComplete`. - `resetTransaction: () => void`. Function to change the transaction status to [`.NOT_STARTED`](#TransactionStatus). -- `setTransactionComplete: ( object ) => void`. Function to change the transaction status to [`.COMPLETE`](#TransactionStatus) and save the response object in `transactionLastResponse`. +- `setTransactionComplete: ( transactionResponse: unknown ) => void`. Function to change the transaction status to [`.COMPLETE`](#TransactionStatus) and save the response object in `transactionLastResponse`. - `setTransactionError: ( string ) => void`. Function to change the transaction status to [`.ERROR`](#TransactionStatus) and save the error in `transactionError`. - `setTransactionPending: () => void`. Function to change the transaction status to [`.PENDING`](#TransactionStatus). - `setTransactionRedirecting: ( string ) => void`. Function to change the transaction status to [`.REDIRECTING`](#TransactionStatus) and save the redirect URL in `transactionRedirectUrl`. diff --git a/packages/composite-checkout/src/components/checkout-submit-button.tsx b/packages/composite-checkout/src/components/checkout-submit-button.tsx index c24fe56a63e5f..8fefd2de50948 100644 --- a/packages/composite-checkout/src/components/checkout-submit-button.tsx +++ b/packages/composite-checkout/src/components/checkout-submit-button.tsx @@ -1,25 +1,16 @@ /** * External dependencies */ -import React, { useCallback } from 'react'; -import debugFactory from 'debug'; +import React from 'react'; import { useI18n } from '@automattic/react-i18n'; /** * Internal dependencies */ import joinClasses from '../lib/join-classes'; -import { - usePaymentMethod, - usePaymentProcessors, - useTransactionStatus, - useFormStatus, - FormStatus, -} from '../public-api'; -import { PaymentProcessorResponse, PaymentProcessorResponseType } from '../types'; +import { useFormStatus, FormStatus } from '../public-api'; import CheckoutErrorBoundary from './checkout-error-boundary'; - -const debug = debugFactory( 'composite-checkout:checkout-submit-button' ); +import { usePaymentMethod, useProcessPayment } from '../public-api'; export default function CheckoutSubmitButton( { className, @@ -30,63 +21,10 @@ export default function CheckoutSubmitButton( { disabled?: boolean; onLoadError?: ( error: string ) => void; } ): JSX.Element | null { - const { - setTransactionComplete, - setTransactionRedirecting, - setTransactionError, - setTransactionPending, - } = useTransactionStatus(); - const paymentProcessors = usePaymentProcessors(); const { formStatus } = useFormStatus(); const { __ } = useI18n(); - const redirectErrorMessage = __( - 'An error occurred while redirecting to the payment partner. Please try again or contact support.' - ); const isDisabled = disabled || formStatus !== FormStatus.READY; - - const onClick = useCallback( - async ( paymentProcessorId: string, submitData: unknown ) => { - 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 ); - return paymentProcessors[ paymentProcessorId ]( submitData ) - .then( ( processorResponse: PaymentProcessorResponse ) => { - debug( 'payment processor function response', processorResponse ); - if ( processorResponse.type === PaymentProcessorResponseType.REDIRECT ) { - if ( ! processorResponse.payload ) { - throw new Error( redirectErrorMessage ); - } - setTransactionRedirecting( processorResponse.payload ); - return processorResponse; - } - if ( processorResponse.type === PaymentProcessorResponseType.SUCCESS ) { - setTransactionComplete( processorResponse.payload ); - return processorResponse; - } - if ( processorResponse.type === PaymentProcessorResponseType.MANUAL ) { - return processorResponse; - } - throw new Error( - `Unknown payment processor response for processor "${ paymentProcessorId }"` - ); - } ) - .catch( ( error: Error ) => { - debug( 'payment processor function error', error ); - setTransactionError( error.message ); - } ); - }, - [ - redirectErrorMessage, - paymentProcessors, - setTransactionComplete, - setTransactionPending, - setTransactionRedirecting, - setTransactionError, - ] - ); + const onClick = useProcessPayment(); const paymentMethod = usePaymentMethod(); if ( ! paymentMethod ) { diff --git a/packages/composite-checkout/src/components/use-process-payment.ts b/packages/composite-checkout/src/components/use-process-payment.ts new file mode 100644 index 0000000000000..901011f534bde --- /dev/null +++ b/packages/composite-checkout/src/components/use-process-payment.ts @@ -0,0 +1,108 @@ +/** + * External dependencies + */ +import { useCallback, useMemo } from 'react'; +import debugFactory from 'debug'; +import { useI18n } from '@automattic/react-i18n'; + +/** + * Internal dependencies + */ +import { usePaymentProcessors, useTransactionStatus } from '../public-api'; +import { + PaymentProcessorResponse, + PaymentProcessorResponseType, + SetTransactionComplete, + SetTransactionRedirecting, + ProcessPayment, +} from '../types'; + +const debug = debugFactory( 'composite-checkout:use-create-payment-processor-on-click' ); + +export default function useProcessPayment(): ProcessPayment { + const paymentProcessors = usePaymentProcessors(); + const { setTransactionPending } = useTransactionStatus(); + const handlePaymentProcessorPromise = useHandlePaymentProcessorResponse(); + + return useCallback( + async ( paymentProcessorId, 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 ); + return handlePaymentProcessorPromise( + paymentProcessorId, + paymentProcessors[ paymentProcessorId ]( submitData ) + ); + }, + [ handlePaymentProcessorPromise, paymentProcessors, setTransactionPending ] + ); +} + +function useHandlePaymentProcessorResponse() { + const { __ } = useI18n(); + const redirectErrorMessage = useMemo( + () => + __( + 'An error occurred while redirecting to the payment partner. Please try again or contact support.' + ), + [ __ ] + ); + const { + setTransactionComplete, + setTransactionRedirecting, + setTransactionError, + } = useTransactionStatus(); + + return useCallback( + async ( + paymentProcessorId: string, + processorPromise: Promise< PaymentProcessorResponse > + ): Promise< PaymentProcessorResponse > => { + return processorPromise + .then( ( response ) => + handlePaymentProcessorResponse( response, paymentProcessorId, redirectErrorMessage, { + setTransactionRedirecting, + setTransactionComplete, + } ) + ) + .catch( ( error: Error ) => { + setTransactionError( error.message ); + throw error; + } ); + }, + [ redirectErrorMessage, setTransactionError, setTransactionComplete, setTransactionRedirecting ] + ); +} + +async function handlePaymentProcessorResponse( + processorResponse: PaymentProcessorResponse, + paymentProcessorId: string, + redirectErrorMessage: string, + { + setTransactionRedirecting, + setTransactionComplete, + }: { + setTransactionRedirecting: SetTransactionRedirecting; + setTransactionComplete: SetTransactionComplete; + } +): Promise< PaymentProcessorResponse > { + debug( 'payment processor function response', processorResponse ); + if ( processorResponse.type === PaymentProcessorResponseType.REDIRECT ) { + if ( ! processorResponse.payload ) { + throw new Error( redirectErrorMessage ); + } + setTransactionRedirecting( processorResponse.payload ); + return processorResponse; + } + if ( processorResponse.type === PaymentProcessorResponseType.SUCCESS ) { + setTransactionComplete( processorResponse.payload ); + return processorResponse; + } + if ( processorResponse.type === PaymentProcessorResponseType.MANUAL ) { + return processorResponse; + } + throw new Error( `Unknown payment processor response for processor "${ paymentProcessorId }"` ); +} diff --git a/packages/composite-checkout/src/public-api.ts b/packages/composite-checkout/src/public-api.ts index ce8bd39854b42..0677860e15084 100644 --- a/packages/composite-checkout/src/public-api.ts +++ b/packages/composite-checkout/src/public-api.ts @@ -81,6 +81,7 @@ import { makeSuccessResponse, makeRedirectResponse, } from './lib/payment-processors'; +import useProcessPayment from './components/use-process-payment'; import RadioButton from './components/radio-button'; import checkoutTheme from './lib/theme'; export * from './types'; @@ -155,6 +156,7 @@ export { usePaymentMethodId, usePaymentProcessor, usePaymentProcessors, + useProcessPayment, useRegisterStore, useRegistry, useSelect, diff --git a/packages/composite-checkout/src/types.ts b/packages/composite-checkout/src/types.ts index 4f13117cb707b..dc58b86b3f1ed 100644 --- a/packages/composite-checkout/src/types.ts +++ b/packages/composite-checkout/src/types.ts @@ -101,10 +101,7 @@ export interface PaymentProcessorProp { [ key: string ]: PaymentProcessorFunction; } -export type PaymentCompleteCallback = ( { - paymentMethodId, - transactionLastResponse, -}: PaymentCompleteCallbackArguments ) => void; +export type PaymentCompleteCallback = ( args: PaymentCompleteCallbackArguments ) => void; export type PaymentCompleteCallbackArguments = { paymentMethodId: string | null; @@ -131,8 +128,10 @@ export type PaymentProcessorResponse = | PaymentProcessorRedirect | PaymentProcessorManual; +export type PaymentProcessorSubmitData = unknown; + export type PaymentProcessorFunction = ( - submitData: unknown + submitData: PaymentProcessorSubmitData ) => Promise< PaymentProcessorResponse >; export enum PaymentProcessorResponseType { @@ -199,13 +198,28 @@ export type TransactionStatusPayload = export type TransactionStatusAction = ReactStandardAction< 'STATUS_SET', TransactionStatusPayload >; export interface TransactionStatusManager extends TransactionStatusState { - resetTransaction: () => void; - setTransactionError: ( message: string ) => void; - setTransactionComplete: ( response: PaymentProcessorResponseData ) => void; - setTransactionPending: () => void; - setTransactionRedirecting: ( url: string ) => void; + resetTransaction: ResetTransaction; + setTransactionError: SetTransactionError; + setTransactionComplete: SetTransactionComplete; + setTransactionPending: SetTransactionPending; + setTransactionRedirecting: SetTransactionRedirecting; } +export type ProcessPayment = ( + paymentProcessorId: string, + processorData: PaymentProcessorSubmitData +) => Promise< PaymentProcessorResponse >; + +export type SetTransactionRedirecting = ( url: string ) => void; + +export type SetTransactionPending = () => void; + +export type SetTransactionComplete = ( response: PaymentProcessorResponseData ) => void; + +export type SetTransactionError = ( message: string ) => void; + +export type ResetTransaction = () => void; + export interface LineItemsState { items: LineItem[]; total: LineItem;