diff --git a/packages/checkout/sdk/src/sdk.ts b/packages/checkout/sdk/src/sdk.ts index bb983ec76c..462a05844f 100644 --- a/packages/checkout/sdk/src/sdk.ts +++ b/packages/checkout/sdk/src/sdk.ts @@ -65,7 +65,7 @@ import { FiatRampService, FiatRampWidgetParams } from './fiatRamp'; import { getItemRequirementsFromRequirements } from './smartCheckout/itemRequirements'; import { CheckoutError, CheckoutErrorType } from './errors'; import { AvailabilityService, availabilityService } from './availability'; -import { loadUnresolved } from './widgets/load'; +import { getCdnUrl, loadUnresolvedBundle } from './widgets/load'; import { WidgetsInit } from './types/widgets'; import { HttpClient } from './api/http'; import { isMatchingAddress } from './utils/utils'; @@ -76,6 +76,7 @@ const SANDBOX_CONFIGURATION = { }, passport: undefined, }; +const WIDGETS_SCRIPT_TIMEOUT = 100; // Checkout SDK export class Checkout { @@ -118,18 +119,55 @@ export class Checkout { // Preload the configurations await checkout.config.remote.getConfig(); - const load = loadUnresolved(init.version); - const checkoutWidgetsModule = await import(/* webpackIgnore: true */load.cdnUrl); + // Resolves the factory for the esm build of the widgets + if (init.useEsModules) { + const cdnUrl = getCdnUrl(init.version); + // WebpackIgnore comment required to prevent webpack modifying the import statement and + // breaking the dynamic import in certain applications integrating checkout + const checkoutWidgetsModule = await import(/* webpackIgnore: true */ cdnUrl); - const factory = new Promise((resolve, reject) => { - try { + const factory = new Promise((resolve, reject) => { if (checkoutWidgetsModule && checkoutWidgetsModule.WidgetsFactory) { resolve(new checkoutWidgetsModule.WidgetsFactory(checkout, init.config)); } else { - throw new Error('WidgetsFactory is not found in the imported module.'); + reject(new CheckoutError( + 'Unable to resolve the WidgetsFactory from the checkout widgets module', + CheckoutErrorType.WIDGETS_SCRIPT_LOAD_ERROR, + )); + } + }); + + return factory; + } + + // Resolves the factory for the umd build of the widgets + const factory = new Promise((resolve, reject) => { + function checkForWidgetsBundleLoaded() { + if (typeof ImmutableCheckoutWidgets !== 'undefined') { + resolve(new ImmutableCheckoutWidgets.WidgetsFactory(checkout, init.config)); + } else { + // If ImmutableCheckoutWidgets is not defined, wait for set amount of time. + // When time has elapsed, check again if ImmutableCheckoutWidgets is defined. + // Once it's defined, the promise will resolve and setTimeout won't be called again. + setTimeout(checkForWidgetsBundleLoaded, WIDGETS_SCRIPT_TIMEOUT); + } + } + + try { + const script = loadUnresolvedBundle(init.version); + if (script.loaded && typeof ImmutableCheckoutWidgets !== 'undefined') { + resolve(new ImmutableCheckoutWidgets.WidgetsFactory(checkout, init.config)); + } else { + checkForWidgetsBundleLoaded(); } - } catch (error) { - reject(error); + } catch (err: any) { + reject( + new CheckoutError( + 'Failed to load widgets script', + CheckoutErrorType.WIDGETS_SCRIPT_LOAD_ERROR, + { error: err }, + ), + ); } }); diff --git a/packages/checkout/sdk/src/types/widgets.ts b/packages/checkout/sdk/src/types/widgets.ts index 626796fc5b..02e38ef327 100644 --- a/packages/checkout/sdk/src/types/widgets.ts +++ b/packages/checkout/sdk/src/types/widgets.ts @@ -4,9 +4,12 @@ import { SemanticVersion } from '../widgets/definitions/types'; /** * Represents the configuration options for instantiating the Checkout Widgets factory. * @property {WidgetConfiguration} config - global configuration options for the widgets. - * @property {SemanticVersion | undefined} version - version of the Checkout widgets bundle (default latest version will be used). + * @property {SemanticVersion | undefined} version - version of the Checkout widgets bundle(default latest version will be used). + * @property {boolean | undefined} useEsModules - Set to true to use the esm build of the widgets. */ export type WidgetsInit = { config: WidgetConfiguration; - version?: SemanticVersion + version?: SemanticVersion; + /** Loads separate parts of the widget script as required for performance improvements. */ + useEsModules?: boolean; }; diff --git a/packages/checkout/sdk/src/widgets/load.ts b/packages/checkout/sdk/src/widgets/load.ts index 9535ee96d7..82622d68a8 100644 --- a/packages/checkout/sdk/src/widgets/load.ts +++ b/packages/checkout/sdk/src/widgets/load.ts @@ -1,10 +1,11 @@ -// import { useLocalBundle } from '../env'; +import { useLocalBundle } from '../env'; import { SemanticVersion } from './definitions/types'; import { validateAndBuildVersion } from './version'; -export function loadUnresolved( +// Loads the checkout widgets bundle from the CDN and appends the script to the document head +export function loadUnresolvedBundle( version?: SemanticVersion, -): { loaded: boolean, cdnUrl: string } { +): { loaded: boolean, element: HTMLScriptElement } { if (window === undefined) { throw new Error('missing window object: please run Checkout client side'); } @@ -13,25 +14,34 @@ export function loadUnresolved( throw new Error('missing document object: please run Checkout client side'); } - // const scriptId = 'immutable-checkout-widgets-bundle'; + const scriptId = 'immutable-checkout-widgets-bundle'; const validVersion = validateAndBuildVersion(version); // Prevent the script to be loaded more than once // by checking the presence of the script and its version. - // const initScript = document.getElementById(scriptId) as HTMLScriptElement; - // if (initScript) return { loaded: true, element: initScript }; + const initScript = document.getElementById(scriptId) as HTMLScriptElement; + if (initScript) return { loaded: true, element: initScript }; - // const tag = document.createElement('script'); + const tag = document.createElement('script'); - const cdnUrl = `https://cdn.jsdelivr.net/npm/@imtbl/sdk@${validVersion}/dist/browser/checkout/widgets.js`; - // if (useLocalBundle()) cdnUrl = `http://${window.location.host}/lib/js/widgets.js`; + let cdnUrl = `https://cdn.jsdelivr.net/npm/@imtbl/sdk@${validVersion}/dist/browser/checkout/widgets.js`; + if (useLocalBundle()) cdnUrl = `http://${window.location.host}/lib/js/widgets.js`; - // tag.setAttribute('id', scriptId); - // tag.setAttribute('data-version', validVersion); - // tag.setAttribute('type', 'module'); - // tag.setAttribute('src', cdnUrl); + tag.setAttribute('id', scriptId); + tag.setAttribute('data-version', validVersion); + tag.setAttribute('src', cdnUrl); - // document.head.appendChild(tag); + document.head.appendChild(tag); - return { loaded: false, cdnUrl }; + return { loaded: false, element: tag }; +} + +// Gets the CDN url for the split checkout widgets bundle +export function getCdnUrl( + version?: SemanticVersion, +): string { + const validVersion = validateAndBuildVersion(version); + let cdnUrl = `https://cdn.jsdelivr.net/npm/@imtbl/sdk@${validVersion}/dist/browser/checkout/widgets-esm.js`; + if (useLocalBundle()) cdnUrl = `http://${window.location.host}/lib/js/index.js`; + return cdnUrl; } diff --git a/packages/checkout/widgets-lib/rollup.config.js b/packages/checkout/widgets-lib/rollup.config.js index b1941a02b4..19ddd39faa 100644 --- a/packages/checkout/widgets-lib/rollup.config.js +++ b/packages/checkout/widgets-lib/rollup.config.js @@ -7,12 +7,19 @@ import replace from '@rollup/plugin-replace'; import nodePolyfills from 'rollup-plugin-polyfill-node'; const defaultPlugin = [ + resolve({ + browser: true, + dedupe: ['react', 'react-dom'], + }), + nodePolyfills(), + commonjs(), json(), replace({ preventAssignment: true, 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), }), - typescript() + typescript(), + terser() ] export default [ @@ -24,14 +31,21 @@ export default [ format: 'es' }, plugins: [ - resolve({ - browser: true, - dedupe: ['react', 'react-dom'], - }), - nodePolyfills(), - commonjs(), ...defaultPlugin, - terser() + ] + }, + { + watch: false, + input: 'src/index.ts', + output: { + file: 'dist/widgets.js', + format: 'umd', + name: 'ImmutableCheckoutWidgets', + inlineDynamicImports: true + }, + context: 'window', + plugins: [ + ...defaultPlugin,, ] } ] diff --git a/sdk/module-release.json b/sdk/module-release.json index a315542800..198fc230e8 100644 --- a/sdk/module-release.json +++ b/sdk/module-release.json @@ -17,9 +17,9 @@ "fileCopy": [ { "src": "../packages/checkout/widgets-lib/dist/index.js", - "dest": "./dist/browser/checkout/widgets.js", + "dest": "./dist/browser/checkout/widgets-esm.js", "stage": "prod", - "splitBundle": true + "copyAllJsFiles": true }, { "src": "../packages/checkout/sdk/dist/browser.js", diff --git a/sdk/scripts/copyBrowserBundles.js b/sdk/scripts/copyBrowserBundles.js index 2df435b4bf..3e326cf316 100755 --- a/sdk/scripts/copyBrowserBundles.js +++ b/sdk/scripts/copyBrowserBundles.js @@ -54,17 +54,17 @@ const main = () => { fs.writeFileSync(destPath, data); - // Copy over all chunks when the splitBundle flag is set - if (item.splitBundle) { + // Copy over all js files when the copyAllJsFiles flag is set + if (item.copyAllJsFiles) { const srcDirectory = path.dirname(sourceFile); - const chunkFiles = fs.readdirSync(srcDirectory); - chunkFiles.forEach((chunkFile) => { + const jsFiles = fs.readdirSync(srcDirectory); + jsFiles.forEach((jsFile) => { // Check if the file is a .js file - if (path.extname(chunkFile) === '.js') { + if (path.extname(jsFile) === '.js') { // Skip copying the original file and only copy .js chunks - if (chunkFile !== path.basename(sourceFile)) { - const chunkSrcPath = path.join(srcDirectory, chunkFile); - const chunkDestPath = path.join(directoryPath, chunkFile); + if (jsFile !== path.basename(sourceFile)) { + const chunkSrcPath = path.join(srcDirectory, jsFile); + const chunkDestPath = path.join(directoryPath, jsFile); fs.copyFileSync(chunkSrcPath, chunkDestPath); } }