@stripe/stripe-js
Version:
Stripe.js loading utility
212 lines (167 loc) • 5.94 kB
text/typescript
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);
};