import React from 'react'; import { EventSubscription } from 'react-native'; import NativeStripeSdk from '../specs/NativeStripeSdkModule'; import type { CustomerSheetInitParams, CustomerSheetPresentParams, CustomerSheetResult, CustomerAdapter, StripeError, CustomerSheetError, ClientSecretProvider, } from '../types'; import { addListener } from '../events'; let fetchPaymentMethodsCallback: EventSubscription | null = null; let attachPaymentMethodCallback: EventSubscription | null = null; let detachPaymentMethodCallback: EventSubscription | null = null; let setSelectedPaymentOptionCallback: EventSubscription | null = null; let fetchSelectedPaymentOptionCallback: EventSubscription | null = null; let setupIntentClientSecretForCustomerAttachCallback: EventSubscription | null = null; let setupIntentClientSecretProviderCallback: EventSubscription | null = null; let customerSessionClientSecretProviderCallback: EventSubscription | null = null; /** Initialize an instance of Customer Sheet with your desired configuration. */ const initialize = async ( params: CustomerSheetInitParams ): Promise<{ error?: StripeError; }> => { let customerAdapterOverrides = {}; if (params.customerAdapter) { customerAdapterOverrides = configureCustomerAdapterEventListeners( params.customerAdapter ); } if (params.clientSecretProvider) { configureClientSecretProviderEventListeners(params.clientSecretProvider); } try { const { error } = await NativeStripeSdk.initCustomerSheet( params, customerAdapterOverrides ); if (error) { return { error }; } return {}; } catch (error: any) { return { error, }; } }; const configureCustomerAdapterEventListeners = ( customerAdapter: CustomerAdapter ): { [Property in keyof CustomerAdapter]: boolean } => { if (customerAdapter.fetchPaymentMethods) { fetchPaymentMethodsCallback?.remove(); fetchPaymentMethodsCallback = addListener( 'onCustomerAdapterFetchPaymentMethodsCallback', async () => { if (customerAdapter.fetchPaymentMethods) { const paymentMethods = await customerAdapter.fetchPaymentMethods(); await NativeStripeSdk.customerAdapterFetchPaymentMethodsCallback( paymentMethods ); } else { throw new Error( '[@stripe/stripe-react-native] Tried to call `fetchPaymentMethods` on your CustomerAdapter, but no matching method was found.' ); } } ); } if (customerAdapter.attachPaymentMethod) { attachPaymentMethodCallback?.remove(); attachPaymentMethodCallback = addListener( 'onCustomerAdapterAttachPaymentMethodCallback', async ({ paymentMethodId }) => { if (customerAdapter.attachPaymentMethod) { const paymentMethod = await customerAdapter.attachPaymentMethod(paymentMethodId); await NativeStripeSdk.customerAdapterAttachPaymentMethodCallback( paymentMethod ); } else { throw new Error( '[@stripe/stripe-react-native] Tried to call `attachPaymentMethod` on your CustomerAdapter, but no matching method was found.' ); } } ); } if (customerAdapter.detachPaymentMethod) { detachPaymentMethodCallback?.remove(); detachPaymentMethodCallback = addListener( 'onCustomerAdapterDetachPaymentMethodCallback', async ({ paymentMethodId }) => { if (customerAdapter.detachPaymentMethod) { const paymentMethod = await customerAdapter.detachPaymentMethod(paymentMethodId); await NativeStripeSdk.customerAdapterDetachPaymentMethodCallback( paymentMethod ); } else { throw new Error( '[@stripe/stripe-react-native] Tried to call `detachPaymentMethod` on your CustomerAdapter, but no matching method was found.' ); } } ); } if (customerAdapter.setSelectedPaymentOption) { setSelectedPaymentOptionCallback?.remove(); setSelectedPaymentOptionCallback = addListener( 'onCustomerAdapterSetSelectedPaymentOptionCallback', async ({ paymentOption }) => { if (customerAdapter.setSelectedPaymentOption) { await customerAdapter.setSelectedPaymentOption(paymentOption); await NativeStripeSdk.customerAdapterSetSelectedPaymentOptionCallback(); } else { throw new Error( '[@stripe/stripe-react-native] Tried to call `setSelectedPaymentOption` on your CustomerAdapter, but no matching method was found.' ); } } ); } if (customerAdapter.fetchSelectedPaymentOption) { fetchSelectedPaymentOptionCallback?.remove(); fetchSelectedPaymentOptionCallback = addListener( 'onCustomerAdapterFetchSelectedPaymentOptionCallback', async () => { if (customerAdapter.fetchSelectedPaymentOption) { const paymentOption = await customerAdapter.fetchSelectedPaymentOption(); await NativeStripeSdk.customerAdapterFetchSelectedPaymentOptionCallback( paymentOption ); } else { throw new Error( '[@stripe/stripe-react-native] Tried to call `fetchSelectedPaymentOption` on your CustomerAdapter, but no matching method was found.' ); } } ); } if (customerAdapter.setupIntentClientSecretForCustomerAttach) { setupIntentClientSecretForCustomerAttachCallback?.remove(); setupIntentClientSecretForCustomerAttachCallback = addListener( 'onCustomerAdapterSetupIntentClientSecretForCustomerAttachCallback', async () => { if (customerAdapter.setupIntentClientSecretForCustomerAttach) { const clientSecret = await customerAdapter.setupIntentClientSecretForCustomerAttach(); await NativeStripeSdk.customerAdapterSetupIntentClientSecretForCustomerAttachCallback( clientSecret ); } else { throw new Error( '[@stripe/stripe-react-native] Tried to call `setupIntentClientSecretForCustomerAttach` on your CustomerAdapter, but no matching method was found.' ); } } ); } return { fetchPaymentMethods: !!customerAdapter.fetchPaymentMethods, attachPaymentMethod: !!customerAdapter.attachPaymentMethod, detachPaymentMethod: !!customerAdapter.detachPaymentMethod, setSelectedPaymentOption: !!customerAdapter.setSelectedPaymentOption, fetchSelectedPaymentOption: !!customerAdapter.fetchSelectedPaymentOption, setupIntentClientSecretForCustomerAttach: !!customerAdapter.setupIntentClientSecretForCustomerAttach, }; }; function configureClientSecretProviderEventListeners( clientSecretProvider: ClientSecretProvider ): void { setupIntentClientSecretProviderCallback?.remove(); setupIntentClientSecretProviderCallback = addListener( 'onCustomerSessionProviderSetupIntentClientSecret', async () => { const setupIntentClientSecret = await clientSecretProvider.provideSetupIntentClientSecret(); await NativeStripeSdk.clientSecretProviderSetupIntentClientSecretCallback( setupIntentClientSecret ); } ); customerSessionClientSecretProviderCallback?.remove(); customerSessionClientSecretProviderCallback = addListener( 'onCustomerSessionProviderCustomerSessionClientSecret', async () => { const customerSessionClientSecret = await clientSecretProvider.provideCustomerSessionClientSecret(); await NativeStripeSdk.clientSecretProviderCustomerSessionClientSecretCallback( customerSessionClientSecret ); } ); } /** Launches the Customer Sheet UI. */ const present = async ( params: CustomerSheetPresentParams = {} ): Promise => { try { return await NativeStripeSdk.presentCustomerSheet(params); } catch (error: any) { return { error, }; } }; /** * You can use this to obtain the selected payment method without presenting the CustomerSheet. * This will return an error if you have not called `.initialize` */ const retrievePaymentOptionSelection = async (): Promise => { try { return await NativeStripeSdk.retrieveCustomerSheetPaymentOptionSelection(); } catch (error: any) { return { error, }; } }; /** * Props */ export type Props = { /** Whether the sheet is visible. Defaults to false. */ visible: boolean; /** Called when the user submits, dismisses, or cancels the sheet, or when an error occurs. */ onResult: (result: CustomerSheetResult) => void; } & CustomerSheetInitParams & CustomerSheetPresentParams; /** * A component wrapper around the Customer Sheet functions. Upon passing `true` to the `visible` prop, * Customer Sheet will call `initialize` and `present`, and the result(s) will be passed through to the * onResult callback. * * @example * ```ts * const [selectedPaymentOption, setSelectedPaymentOption] = React.useState(null); * const [customerSheetVisible, setCustomerSheetVisible] = React.useState(false); * * return ( * { * setCustomerSheetVisible(false); * if (error) { * Alert.alert(error.code, error.localizedMessage); * } * if (paymentOption) { * setSelectedPaymentOption(paymentOption); * console.log(JSON.stringify(paymentOption, null, 2)); * } * if (paymentMethod) { * console.log(JSON.stringify(paymentMethod, null, 2)); * } * }} * /> * ); * ``` * @param __namedParameters Props * @returns JSX.Element * @category ReactComponents */ function Component({ visible, presentationStyle, animationStyle, style, appearance, merchantDisplayName, headerTextForSelectionScreen, defaultBillingDetails, billingDetailsCollectionConfiguration, returnURL, removeSavedPaymentMethodMessage, applePayEnabled, googlePayEnabled, timeout, onResult, // CustomerAdapter Init Params setupIntentClientSecret, customerId, customerEphemeralKeySecret, customerAdapter, // CustomerSession Init Params intentConfiguration, clientSecretProvider, }: Props) { React.useEffect(() => { if (visible) { const optionalParams = { style, appearance, merchantDisplayName, headerTextForSelectionScreen, defaultBillingDetails, billingDetailsCollectionConfiguration, returnURL, removeSavedPaymentMethodMessage, applePayEnabled, googlePayEnabled, }; const requiredParams = intentConfiguration != null && clientSecretProvider != null ? { intentConfiguration: intentConfiguration, clientSecretProvider: clientSecretProvider, } : { customerId: customerId, setupIntentClientSecret, customerEphemeralKeySecret, customerAdapter, }; const params: CustomerSheetInitParams = { ...optionalParams, ...requiredParams, }; initialize(params).then((initResult) => { if (initResult.error) { onResult(initResult); } else { present({ timeout, presentationStyle, animationStyle, }).then((presentResult) => { onResult(presentResult); }); } }); } // Only run this hook when visible prop changes // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible]); return null; } /** * The Customer Sheet is a prebuilt UI component that lets your customers manage their saved payment methods. */ export const CustomerSheet = { Component, initialize, present, retrievePaymentOptionSelection, };