UNPKG

@stripe/stripe-js

Version:

Stripe.js loading utility

212 lines (167 loc) 5.94 kB
import {Stripe, StripeConstructor} from '../types'; export type LoadStripe = ( ...args: Parameters<StripeConstructor> ) => Promise<Stripe | null>; export interface LoadParams { advancedFraudSignals: boolean; } // `_VERSION` will be rewritten by `@rollup/plugin-replace` as a string literal // containing the package.json version declare const _VERSION: string; export const RELEASE_TRAIN = 'clover'; const runtimeVersionToUrlVersion = (version: string | number) => version === 3 ? 'v3' : version; const ORIGIN = 'https://js.stripe.com'; const STRIPE_JS_URL = `${ORIGIN}/${RELEASE_TRAIN}/stripe.js`; const V3_URL_REGEX = /^https:\/\/js\.stripe\.com\/v3\/?(\?.*)?$/; const STRIPE_JS_URL_REGEX = /^https:\/\/js\.stripe\.com\/(v3|[a-z]+)\/stripe\.js(\?.*)?$/; const EXISTING_SCRIPT_MESSAGE = 'loadStripe.setLoadParameters was called but an existing Stripe.js script already exists in the document; existing script parameters will be used'; const isStripeJSURL = (url: string): boolean => V3_URL_REGEX.test(url) || STRIPE_JS_URL_REGEX.test(url); export const findScript = (): HTMLScriptElement | null => { const scripts = document.querySelectorAll<HTMLScriptElement>( `script[src^="${ORIGIN}"]` ); for (let i = 0; i < scripts.length; i++) { const script = scripts[i]; if (!isStripeJSURL(script.src)) { continue; } return script; } return null; }; const injectScript = (params: null | LoadParams): HTMLScriptElement => { const queryString = params && !params.advancedFraudSignals ? '?advancedFraudSignals=false' : ''; const script = document.createElement('script'); script.src = `${STRIPE_JS_URL}${queryString}`; const headOrBody = document.head || document.body; if (!headOrBody) { throw new Error( 'Expected document.body not to be null. Stripe.js requires a <body> element.' ); } headOrBody.appendChild(script); return script; }; const registerWrapper = (stripe: any, startTime: number): void => { if (!stripe || !stripe._registerWrapper) { return; } stripe._registerWrapper({name: 'stripe-js', version: _VERSION, startTime}); }; let stripePromise: Promise<StripeConstructor | null> | null = null; let onErrorListener: ((cause?: unknown) => void) | null = null; let onLoadListener: (() => void) | null = null; const onError = (reject: (reason?: any) => void) => (cause?: unknown) => { reject(new Error('Failed to load Stripe.js', {cause})); }; const onLoad = ( resolve: ( value: StripeConstructor | PromiseLike<StripeConstructor | null> | null ) => void, reject: (reason?: any) => void ) => () => { if (window.Stripe) { resolve(window.Stripe); } else { reject(new Error('Stripe.js not available')); } }; export const loadScript = ( params: null | LoadParams ): Promise<StripeConstructor | null> => { // Ensure that we only attempt to load Stripe.js at most once if (stripePromise !== null) { return stripePromise; } stripePromise = new Promise((resolve, reject) => { if (typeof window === 'undefined' || typeof document === 'undefined') { // Resolve to null when imported server side. This makes the module // safe to import in an isomorphic code base. resolve(null); return; } if (window.Stripe && params) { console.warn(EXISTING_SCRIPT_MESSAGE); } if (window.Stripe) { resolve(window.Stripe); return; } try { let script = findScript(); if (script && params) { console.warn(EXISTING_SCRIPT_MESSAGE); } else if (!script) { script = injectScript(params); } else if ( script && onLoadListener !== null && onErrorListener !== null ) { // remove event listeners script.removeEventListener('load', onLoadListener); script.removeEventListener('error', onErrorListener); // if script exists, but we are reloading due to an error, // reload script to trigger 'load' event script.parentNode?.removeChild(script); script = injectScript(params); } onLoadListener = onLoad(resolve, reject); onErrorListener = onError(reject); script.addEventListener('load', onLoadListener); script.addEventListener('error', onErrorListener); } catch (error) { reject(error); return; } }); // Resets stripePromise on error return stripePromise.catch((error) => { stripePromise = null; return Promise.reject(error); }); }; export const initStripe = ( maybeStripe: StripeConstructor | null, args: Parameters<StripeConstructor>, startTime: number ): Stripe | null => { if (maybeStripe === null) { return null; } const pk = args[0]; const isTestKey = pk.match(/^pk_test/); // @ts-expect-error this is not publicly typed const version = runtimeVersionToUrlVersion(maybeStripe.version); const expectedVersion = RELEASE_TRAIN; if (isTestKey && version !== expectedVersion) { console.warn( `Stripe.js@${version} was loaded on the page, but @stripe/stripe-js@${_VERSION} expected Stripe.js@${expectedVersion}. This may result in unexpected behavior. For more information, see https://docs.stripe.com/sdks/stripejs-versioning` ); } const stripe = maybeStripe.apply(undefined, args); registerWrapper(stripe, startTime); return stripe; }; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export const validateLoadParams = (params: any): LoadParams => { const errorMessage = `invalid load parameters; expected object of shape {advancedFraudSignals: boolean} but received ${JSON.stringify(params)} `; if (params === null || typeof params !== 'object') { throw new Error(errorMessage); } if ( Object.keys(params).length === 1 && typeof params.advancedFraudSignals === 'boolean' ) { return params; } throw new Error(errorMessage); };