// Must use `import *` or named imports for React's types import {FunctionComponent} from 'react'; import * as stripeJs from '@stripe/stripe-js'; import React from 'react'; import PropTypes from 'prop-types'; import {useAttachEvent} from '../utils/useAttachEvent'; import {ElementProps} from '../types'; import {usePrevious} from '../utils/usePrevious'; import { extractAllowedOptionsUpdates, UnknownOptions, } from '../utils/extractAllowedOptionsUpdates'; import {useElementsOrCheckoutContextWithUseCase} from '../checkout/components/CheckoutProvider'; type UnknownCallback = (...args: unknown[]) => any; interface PrivateElementProps { id?: string; className?: string; onChange?: UnknownCallback; onBlur?: UnknownCallback; onFocus?: UnknownCallback; onEscape?: UnknownCallback; onReady?: UnknownCallback; onClick?: UnknownCallback; onLoadError?: UnknownCallback; onLoaderStart?: UnknownCallback; onNetworksChange?: UnknownCallback; onConfirm?: UnknownCallback; onCancel?: UnknownCallback; onShippingAddressChange?: UnknownCallback; onShippingRateChange?: UnknownCallback; onSavedPaymentMethodRemove?: UnknownCallback; onSavedPaymentMethodUpdate?: UnknownCallback; options?: UnknownOptions; } const capitalized = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); const createElementComponent = ( type: stripeJs.StripeElementType, isServer: boolean ): FunctionComponent => { const displayName = `${capitalized(type)}Element`; const ClientElement: FunctionComponent = ({ id, className, options = {}, onBlur, onFocus, onReady, onChange, onEscape, onClick, onLoadError, onLoaderStart, onNetworksChange, onConfirm, onCancel, onShippingAddressChange, onShippingRateChange, onSavedPaymentMethodRemove, onSavedPaymentMethodUpdate, }) => { const ctx = useElementsOrCheckoutContextWithUseCase( `mounts <${displayName}>` ); const elements = 'elements' in ctx ? ctx.elements : null; const checkoutState = 'checkoutState' in ctx ? ctx.checkoutState : null; const checkoutSdk = checkoutState?.type === 'success' || checkoutState?.type === 'loading' ? checkoutState.sdk : null; const [element, setElement] = React.useState( null ); const elementRef = React.useRef(null); const domNode = React.useRef(null); // For every event where the merchant provides a callback, call element.on // with that callback. If the merchant ever changes the callback, removes // the old callback with element.off and then call element.on with the new one. useAttachEvent(element, 'blur', onBlur); useAttachEvent(element, 'focus', onFocus); useAttachEvent(element, 'escape', onEscape); useAttachEvent(element, 'click', onClick); useAttachEvent(element, 'loaderror', onLoadError); useAttachEvent(element, 'loaderstart', onLoaderStart); useAttachEvent(element, 'networkschange', onNetworksChange); useAttachEvent(element, 'confirm', onConfirm); useAttachEvent(element, 'cancel', onCancel); useAttachEvent(element, 'shippingaddresschange', onShippingAddressChange); useAttachEvent(element, 'shippingratechange', onShippingRateChange); useAttachEvent( element, 'savedpaymentmethodremove', onSavedPaymentMethodRemove ); useAttachEvent( element, 'savedpaymentmethodupdate', onSavedPaymentMethodUpdate ); useAttachEvent(element, 'change', onChange); let readyCallback: UnknownCallback | undefined; if (onReady) { if (type === 'expressCheckout') { // Passes through the event, which includes visible PM types readyCallback = onReady; } else { // For other Elements, pass through the Element itself. readyCallback = () => { onReady(element); }; } } useAttachEvent(element, 'ready', readyCallback); React.useLayoutEffect(() => { if ( elementRef.current === null && domNode.current !== null && (elements || checkoutSdk) ) { let newElement: stripeJs.StripeElement | null = null; if (checkoutSdk) { switch (type) { case 'paymentForm': newElement = checkoutSdk.createPaymentFormElement(); break; case 'payment': newElement = checkoutSdk.createPaymentElement(options); break; case 'address': if ('mode' in options) { const {mode, ...restOptions} = options; if (mode === 'shipping') { newElement = checkoutSdk.createShippingAddressElement( restOptions ); } else if (mode === 'billing') { newElement = checkoutSdk.createBillingAddressElement( restOptions ); } else { throw new Error( "Invalid options.mode. mode must be 'billing' or 'shipping'." ); } } else { throw new Error( "You must supply options.mode. mode must be 'billing' or 'shipping'." ); } break; case 'expressCheckout': newElement = checkoutSdk.createExpressCheckoutElement( options as any ) as stripeJs.StripeExpressCheckoutElement; break; case 'currencySelector': newElement = checkoutSdk.createCurrencySelectorElement(); break; case 'taxId': newElement = checkoutSdk.createTaxIdElement(options); break; default: throw new Error( `Invalid Element type ${displayName}. You must use either the , , , or .` ); } } else if (elements) { newElement = elements.create(type as any, options); } // Store element in a ref to ensure it's _immediately_ available in cleanup hooks in StrictMode elementRef.current = newElement; // Store element in state to facilitate event listener attachment setElement(newElement); if (newElement) { newElement.mount(domNode.current); } } }, [elements, checkoutSdk, options]); const prevOptions = usePrevious(options); React.useEffect(() => { if (!elementRef.current) { return; } const updates = extractAllowedOptionsUpdates(options, prevOptions, [ 'paymentRequest', ]); if (updates && 'update' in elementRef.current) { elementRef.current.update(updates); } }, [options, prevOptions]); React.useLayoutEffect(() => { return () => { if ( elementRef.current && typeof elementRef.current.destroy === 'function' ) { try { elementRef.current.destroy(); elementRef.current = null; } catch (error) { // Do nothing } } }; }, []); return
; }; // Only render the Element wrapper in a server environment. const ServerElement: FunctionComponent = (props) => { useElementsOrCheckoutContextWithUseCase(`mounts <${displayName}>`); const {id, className} = props; return
; }; const Element = isServer ? ServerElement : ClientElement; Element.propTypes = { id: PropTypes.string, className: PropTypes.string, onChange: PropTypes.func, onBlur: PropTypes.func, onFocus: PropTypes.func, onReady: PropTypes.func, onEscape: PropTypes.func, onClick: PropTypes.func, onLoadError: PropTypes.func, onLoaderStart: PropTypes.func, onNetworksChange: PropTypes.func, onConfirm: PropTypes.func, onCancel: PropTypes.func, onShippingAddressChange: PropTypes.func, onShippingRateChange: PropTypes.func, onSavedPaymentMethodRemove: PropTypes.func, onSavedPaymentMethodUpdate: PropTypes.func, options: PropTypes.object as any, }; Element.displayName = displayName; (Element as any).__elementType = type; return Element as FunctionComponent; }; export default createElementComponent;