/* eslint-disable react-native/no-inline-styles */ import { LayoutAnimation, Platform, findNodeHandle, EventSubscription, HostComponent, } from 'react-native'; import type { BillingDetails, AddressDetails, UserInterfaceStyle, CardBrand, } from './Common'; import type { PaymentMethod } from '.'; import type * as ConfirmationToken from './ConfirmationToken'; import * as PaymentSheetTypes from './PaymentSheet'; import NativeStripeSdkModule from '../specs/NativeStripeSdkModule'; import { ReactElement, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import React from 'react'; import { addListener } from '../events'; import NativeEmbeddedPaymentElement, { Commands, NativeProps, } from '../specs/NativeEmbeddedPaymentElement'; // ----------------------------------------------------------------------------- // Types // ----------------------------------------------------------------------------- /** * The final result of a confirm call. * Typically: payment succeeded (“completed”), canceled, or failed with error. */ export type EmbeddedPaymentElementResult = | { status: 'completed' } | { status: 'canceled' } | { status: 'failed'; error: Error }; /** * Contains details about a payment method that can be displayed to the customer in the embedded payment element UI. */ export interface PaymentOptionDisplayData { /** * A user-facing label for the payment method, like "Apple Pay" or "•••• 4242" for a card. */ label: string; /** * A base64 encoded string representing the image for the payment option. */ image: string; /** * Optional billing details associated with the payment method, such as name, email, or address. */ billingDetails?: BillingDetails; /** * A string identifier for the type of payment method. * Stripe values: https://stripe.com/docs/api/payment_methods/object#payment_method_object-type * External methods: https://stripe.com/docs/payments/external-payment-methods?platform=ios#available-external-payment-methods * Apple Pay: "apple_pay" */ paymentMethodType: string; /** * If you set `configuration.embeddedViewDisplaysMandateText = false`, this HTML text must be displayed to the customer near your "Buy" button to comply with regulations. * This text may contain formatting, colors, and links that should be preserved when rendering. */ mandateHTML?: string; } /** * Describes the action performed when the bottom button in the embedded payment form sheet is tapped. * The embedded view may show payment method options such as "Card". When selected, a form sheet appears * for customers to input their payment details. At the bottom of that form sheet is a button. * Defaults to 'continue'. * This type determines what tapping that button does: * - In the `confirm` case, the button says “Pay” or “Set up” and triggers confirmation of the payment or setup intent inside the sheet. * - In the `continue` case, the button says “Continue” and simply dismisses the sheet. The payment or setup is then confirmed outside the sheet, typically in your app. */ export type EmbeddedFormSheetAction = | { /** * The button says “Pay” or “Set up”. When tapped, it confirms the payment or setup directly within the form sheet. * @param result - Callback invoked with the result of the confirmation. You can use this to show a success message or handle errors. */ type: 'confirm'; onFormSheetConfirmComplete?: ( result: EmbeddedPaymentElementResult ) => void; } | { /** * The button says “Continue”. When tapped, the form sheet closes without confirming anything. * Use this when you want to handle confirmation elsewhere in your app after the customer has filled in their details. */ type: 'continue'; }; /** * Describes how the EmbeddedPaymentElement handles payment method row selections: * - In the `default` case, the payment method option row enters a selected state. * - In the `immediateAction` case, `onSelectPaymentOption` is called. */ export type EmbeddedRowSelectionBehavior = | { /** * When a payment method option is selected, the customer taps a button to continue or confirm payment. * This is the default recommended integration. */ type: 'default'; } | { /** * When a payment method option is selected, `onSelectPaymentOption` is triggered. * You can implement this function to immediately perform an action such as going back to the checkout screen or confirming the payment. * Note that certain payment options like Apple Pay and saved payment methods are disabled in this mode if you set * `EmbeddedPaymentElementConfiguration.formSheetAction` to `continue` */ type: 'immediateAction'; onSelectPaymentOption?: () => void; }; /** * Configuration object (subset of EmbeddedPaymentElement.Configuration). */ export interface EmbeddedPaymentElementConfiguration { /** Your customer-facing business name. On Android, this is required and cannot be an empty string. */ merchantDisplayName: string; /** The identifier of the Stripe Customer object. See https://stripe.com/docs/api/customers/object#customer_object-id */ customerId?: string; /** A short-lived token that allows the SDK to access a Customer's payment methods. */ customerEphemeralKeySecret?: string; /** The client secret of this Customer Session. Used on the client to set up secure access to the given customer. */ customerSessionClientSecret?: string; /** iOS only. Enable Apple Pay in the Payment Sheet by passing an ApplePayParams object. */ applePay?: PaymentSheetTypes.ApplePayParams; /** Android only. Enable Google Pay in the Payment Sheet by passing a GooglePayParams object. */ googlePay?: PaymentSheetTypes.GooglePayParams; /** Configuration for Link */ link?: PaymentSheetTypes.LinkParams; /** The color styling to use for PaymentSheet UI. Defaults to 'automatic'. */ style?: UserInterfaceStyle; /** A URL that redirects back to your app that EmbeddedPaymentElement can use to auto-dismiss web views used for additional authentication, e.g. 3DS2 */ returnURL?: string; /** Configuration for how billing details are collected during checkout. */ billingDetailsCollectionConfiguration?: PaymentSheetTypes.BillingDetailsCollectionConfiguration; /** PaymentSheet pre-populates the billing fields that are displayed in the Payment Sheet (only country and postal code, as of this version) with these values, if provided. */ defaultBillingDetails?: BillingDetails; /** * The shipping information for the customer. If set, EmbeddedPaymentElement will pre-populate the form fields with the values provided. * This is used to display a "Billing address is same as shipping" checkbox if `defaultBillingDetails` is not provided. * If `name` and `line1` are populated, it's also [attached to the PaymentIntent](https://stripe.com/docs/api/payment_intents/object#payment_intent_object-shipping) during payment. */ defaultShippingDetails?: AddressDetails; /** If true, allows payment methods that do not move money at the end of the checkout. Defaults to false. * * Some payment methods can’t guarantee you will receive funds from your customer at the end of the checkout * because they take time to settle (eg. most bank debits, like SEPA or ACH) or require customer action to * complete (e.g. OXXO, Konbini, Boleto). If this is set to true, make sure your integration listens to webhooks * for notifications on whether a payment has succeeded or not. */ allowsDelayedPaymentMethods?: boolean; /** Customizes the appearance of EmbeddedPaymentElement */ appearance?: PaymentSheetTypes.AppearanceParams; /** The label to use for the primary button. If not set, Payment Sheet will display suitable default labels for payment and setup intents. */ primaryButtonLabel?: string; /** Optional configuration to display a custom message when a saved payment method is removed. iOS only. */ removeSavedPaymentMethodMessage?: string; /** The list of preferred networks that should be used to process payments made with a co-branded card. * This value will only be used if your user hasn't selected a network themselves. */ preferredNetworks?: Array; /** By default, EmbeddedPaymentElement will use a dynamic ordering that optimizes payment method display for the customer. * You can override the default order in which payment methods are displayed in EmbeddedPaymentElement with a list of payment method types. * See https://stripe.com/docs/api/payment_methods/object#payment_method_object-type for the list of valid types. You may also pass external payment methods. * - Example: ["card", "external_paypal", "klarna"] * - Note: If you omit payment methods from this list, they’ll be automatically ordered by Stripe after the ones you provide. Invalid payment methods are ignored. */ paymentMethodOrder?: Array; /** This is an experimental feature that may be removed at any time. * Defaults to true. If true, the customer can delete all saved payment methods. * If false, the customer can't delete if they only have one saved payment method remaining. */ allowsRemovalOfLastSavedPaymentMethod?: boolean; /** * By default, EmbeddedPaymentElement will accept all supported cards by Stripe. * You can specify card brands EmbeddedPaymentElement should block or allow payment for by providing an array of those card brands. * Note: This is only a client-side solution. * Note: Card brand filtering is not currently supported in Link. */ cardBrandAcceptance?: PaymentSheetTypes.CardBrandAcceptance; /** * Configuration for filtering cards by funding type. * @note This is a private preview API and will have no effect unless your Stripe account is enrolled in the private preview. */ cardFundingFiltering?: PaymentSheetTypes.CardFundingFiltering; /** The view can display payment methods like "Card" that, when tapped, open a sheet where customers enter their payment method details. * The sheet has a button at the bottom. `formSheetAction` controls the action the button performs. Defaults to 'continue'. */ formSheetAction?: EmbeddedFormSheetAction; /** Configuration for custom payment methods in EmbeddedPaymentElement. */ customPaymentMethodConfiguration?: PaymentSheetTypes.CustomPaymentMethodConfiguration; /** Describes how the EmbeddedPaymentElement handles payment method row selections. */ rowSelectionBehavior?: EmbeddedRowSelectionBehavior; /** * Controls whether the view displays mandate text at the bottom for payment methods that require it. * If set to `false`, your integration must display `PaymentOptionDisplayData.mandateHTML` to the customer near your "Buy" button to comply with regulations. * Note: This doesn't affect mandates displayed in the form sheet. * Defaults to `true`. */ embeddedViewDisplaysMandateText?: boolean; /** By default, EmbeddedPaymentElement offers a card scan button within the new card entry form. * When opensCardScannerAutomatically is set to true, * the card entry form will initialize with the card scanner already open. * Defaults to false. */ opensCardScannerAutomatically?: boolean; } // ----------------------------------------------------------------------------- // Embedded API // ----------------------------------------------------------------------------- class EmbeddedPaymentElement { /** * Call this when the intent configuration changes (e.g., amount or currency). * Cancels any in-progress update. Ensures the correct payment methods are shown and fields are collected. * If the selected payment option becomes invalid, it may be cleared. * Returns the final result of the update; earlier in-flight updates will return `{ status: 'canceled' }`. */ async update(intentConfig: PaymentSheetTypes.IntentConfiguration) { const result = await NativeStripeSdkModule.updateEmbeddedPaymentElement(intentConfig); return result; } /** * Confirm the payment or setup intent. * Waits for any in-progress `update()` call to finish before proceeding. * May present authentication flows (e.g., 3DS) if required. * Requires the most recent `update()` call to have succeeded. * Returns the final result: success, failure, or cancellation. */ async confirm(): Promise { const result = await NativeStripeSdkModule.confirmEmbeddedPaymentElement(-1); return result; } /** Clear the currently selected payment option (reset to null). */ clearPaymentOption(): void { NativeStripeSdkModule.clearEmbeddedPaymentOption(-1); } } // ----------------------------------------------------------------------------- // JS Factory: createEmbeddedPaymentElement // ----------------------------------------------------------------------------- let confirmHandlerCallback: EventSubscription | null = null; let confirmationTokenHandlerCallback: EventSubscription | null = null; let formSheetActionConfirmCallback: EventSubscription | null = null; let customPaymentMethodConfirmCallback: EventSubscription | null = null; let rowSelectionCallback: EventSubscription | null = null; async function createEmbeddedPaymentElement( intentConfig: PaymentSheetTypes.IntentConfiguration, configuration: EmbeddedPaymentElementConfiguration ): Promise { setupConfirmAndSelectionHandlers(intentConfig, configuration); await NativeStripeSdkModule.createEmbeddedPaymentElement( intentConfig, configuration ); return new EmbeddedPaymentElement(); } function setupConfirmAndSelectionHandlers( intentConfig: PaymentSheetTypes.IntentConfiguration, configuration: EmbeddedPaymentElementConfiguration ) { const confirmHandler = intentConfig.confirmHandler; if (confirmHandler) { confirmHandlerCallback?.remove(); confirmHandlerCallback = addListener( 'onConfirmHandlerCallback', ({ paymentMethod, shouldSavePaymentMethod, }: { paymentMethod: PaymentMethod.Result; shouldSavePaymentMethod: boolean; }) => { confirmHandler( paymentMethod, shouldSavePaymentMethod, NativeStripeSdkModule.intentCreationCallback ); } ); } const confirmationTokenConfirmHandler = intentConfig.confirmationTokenConfirmHandler; if (confirmationTokenConfirmHandler) { confirmationTokenHandlerCallback?.remove(); confirmationTokenHandlerCallback = addListener( 'onConfirmationTokenHandlerCallback', ({ confirmationToken, }: { confirmationToken: ConfirmationToken.Result; }) => { confirmationTokenConfirmHandler( confirmationToken, NativeStripeSdkModule.confirmationTokenCreationCallback ); } ); } if (configuration.formSheetAction?.type === 'confirm') { const confirmFormSheetHandler = configuration.formSheetAction.onFormSheetConfirmComplete; if (confirmFormSheetHandler) { formSheetActionConfirmCallback?.remove(); formSheetActionConfirmCallback = addListener( 'embeddedPaymentElementFormSheetConfirmComplete', (result: EmbeddedPaymentElementResult) => { // Pass the result back to the formSheetAction handler confirmFormSheetHandler(result); } ); } } // Setup custom payment method confirmation handler if (configuration.customPaymentMethodConfiguration) { const customPaymentMethodHandler = configuration.customPaymentMethodConfiguration .confirmCustomPaymentMethodCallback; if (customPaymentMethodHandler) { customPaymentMethodConfirmCallback?.remove(); customPaymentMethodConfirmCallback = addListener( 'onCustomPaymentMethodConfirmHandlerCallback', ({ customPaymentMethod, billingDetails, }: { customPaymentMethod: PaymentSheetTypes.CustomPaymentMethod; billingDetails: BillingDetails | null; }) => { // Call the user's handler with a result handler callback customPaymentMethodHandler( customPaymentMethod, billingDetails, (result: PaymentSheetTypes.CustomPaymentMethodResult) => { // Send the result back to the native side NativeStripeSdkModule.customPaymentMethodResultCallback(result); } ); } ); } } if (configuration.rowSelectionBehavior?.type === 'immediateAction') { const rowSelectionHandler = configuration.rowSelectionBehavior.onSelectPaymentOption; if (rowSelectionHandler) { rowSelectionCallback?.remove(); rowSelectionCallback = addListener( 'embeddedPaymentElementRowSelectionImmediateAction', () => { rowSelectionHandler(); } ); } } } // ----------------------------------------------------------------------------- // Hook: useEmbeddedPaymentElement // ----------------------------------------------------------------------------- export interface UseEmbeddedPaymentElementResult { // A view that displays payment methods. It can present a sheet to collect more details or display saved payment methods. embeddedPaymentElementView: ReactElement | null; /** * Contains information about the customer's selected payment option. * Use this to display the payment option in your own UI */ paymentOption: PaymentOptionDisplayData | null; /** * Completes the payment or setup. * @returns {Promise} The result of the payment after any presented view controllers are dismissed. * @note This method requires that the last call to `update` succeeded. If the last `update` call failed, this call will fail. If this method is called while a call to `update` is in progress, it waits until the `update` call completes. */ confirm: () => Promise; /** * Call this method when the IntentConfiguration values you used to initialize `EmbeddedPaymentElement` (amount, currency, etc.) change. * This ensures the appropriate payment methods are displayed, collect the right fields, etc. * @param {Object} intentConfiguration - An updated IntentConfiguration. * @throws {Error} Sets loadingError if the update fails. * @note Upon completion, `paymentOption` may become null if it's no longer available. * @note If you call `update` while a previous call to `update` is still in progress, the previous call is canceled. */ update: (intentConfig: PaymentSheetTypes.IntentConfiguration) => void; // Sets the currently selected payment option to null clearPaymentOption: () => void; // Any error encountered during creation/update, or null loadingError: Error | null; // Whether the embedded payment element has loaded (height > 1) isLoaded: boolean; } /** * An asynchronous failable initializer * Loads the Customer's payment methods, their default payment method, etc. * @param {Object} intentConfiguration - Information about the PaymentIntent or SetupIntent you will create later to complete the confirmation. * @param {Object} configuration - Configuration for the PaymentSheet. e.g. your business name, customer details, etc. * @returns {EmbeddedPaymentElement|null} A valid EmbeddedPaymentElement instance if successful, null otherwise. * @description If loading fails, this function sets the loadingError state variable instead of throwing an error. */ export function useEmbeddedPaymentElement( intentConfig: PaymentSheetTypes.IntentConfiguration, configuration: EmbeddedPaymentElementConfiguration ): UseEmbeddedPaymentElementResult { const isAndroid = Platform.OS === 'android'; const elementRef = useRef(null); const [element, setElement] = useState(null); const [paymentOption, setPaymentOption] = useState(null); const [height, setHeight] = useState(); const viewRef = useRef>>(null); const [loadingError, setLoadingError] = useState(null); const isLoaded = useMemo(() => { return height !== undefined && height > 1; }, [height]); function getElementOrThrow(ref: { current: EmbeddedPaymentElement | null; }): EmbeddedPaymentElement { if (!ref.current) { throw new Error( 'EmbeddedPaymentElement is not ready yet – wait until it finishes loading before calling this API.' ); } return ref.current; } // Create embedded payment element useEffect(() => { let active = true; (async () => { const el = await createEmbeddedPaymentElement( intentConfig, configuration ); if (!active) return; elementRef.current = el; setElement(el); })(); return () => { active = false; elementRef.current = null; setElement(null); }; }, [intentConfig, configuration, viewRef, isAndroid]); useEffect(() => { const sub = addListener( 'embeddedPaymentElementDidUpdatePaymentOption', ({ paymentOption: opt }) => setPaymentOption(opt ?? null) ); return () => sub.remove(); }); // Listen for height changes useEffect(() => { const sub = addListener( 'embeddedPaymentElementDidUpdateHeight', ({ height: h }) => { // ignore zero if (h > 0 || (isAndroid && h === 0)) { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setHeight(h); } } ); return () => sub.remove(); }, [isAndroid]); // Listen for loading failures useEffect(() => { const sub = addListener( 'embeddedPaymentElementLoadingFailed', (nativeError: { message: string }) => { setLoadingError(new Error(nativeError.message)); } ); return () => sub.remove(); }, []); // Render the embedded view const embeddedPaymentElementView = useMemo(() => { if (isAndroid && configuration && intentConfig) { return ( ); } if (!element) return null; return ( ); }, [configuration, element, height, intentConfig, isAndroid]); // Other APIs const confirm = useCallback((): Promise => { const currentRef = viewRef.current; if (isAndroid) { if (currentRef) { const promise = new Promise((resolve) => { const sub = addListener( 'embeddedPaymentElementFormSheetConfirmComplete', (result: EmbeddedPaymentElementResult) => { sub.remove(); resolve(result); } ); }); Commands.confirm(currentRef); return promise; } else { return Promise.reject( new Error('Unable to find Android embedded payment element view!') ); } } // iOS: just proxy to the native hook return getElementOrThrow(elementRef).confirm(); }, [isAndroid]); const update = useCallback( (cfg: PaymentSheetTypes.IntentConfiguration) => { if (isAndroid) { const currentRef = viewRef.current; if (currentRef) { return new Promise<{ status: string } | null>((resolve) => { const sub = addListener( 'embeddedPaymentElementUpdateComplete', (result: { status: string } | null) => { sub.remove(); resolve(result); } ); Commands.update(currentRef, JSON.stringify(cfg)); }); } return Promise.reject( new Error('Unable to find Android embedded payment element view!') ); } // iOS: use native module directly return getElementOrThrow(elementRef).update(cfg); }, [isAndroid] ); const clearPaymentOption = useCallback((): Promise => { if (isAndroid) { const tag = findNodeHandle(viewRef.current); if (tag == null) { return Promise.reject(new Error('Unable to find Android view handle')); } return NativeStripeSdkModule.clearEmbeddedPaymentOption(tag); } // iOS: clear on the element instance getElementOrThrow(elementRef).clearPaymentOption(); return Promise.resolve(); }, [isAndroid]); return { embeddedPaymentElementView, paymentOption, confirm, update, clearPaymentOption, loadingError, isLoaded, }; }