Skip to content

Commit

Permalink
WT-2017 Split checkout bundle by widget (#1441)
Browse files Browse the repository at this point in the history
Co-authored-by: Pano Skylakis <[email protected]>
Co-authored-by: Zach Couchman <[email protected]>
Co-authored-by: Charlie McKenzie <[email protected]>
  • Loading branch information
4 people authored Feb 19, 2024
1 parent 4235517 commit e47396c
Show file tree
Hide file tree
Showing 27 changed files with 252 additions and 128 deletions.
108 changes: 90 additions & 18 deletions packages/checkout/sdk/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,20 @@ 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 { getWidgetsEsmUrl, loadUnresolvedBundle } from './widgets/load';
import { WidgetsInit } from './types/widgets';
import { HttpClient } from './api/http';
import { isMatchingAddress } from './utils/utils';
import { WidgetConfiguration } from './widgets/definitions/configurations';
import { SemanticVersion } from './widgets/definitions/types';
import { validateAndBuildVersion } from './widgets/version';

const SANDBOX_CONFIGURATION = {
baseConfig: {
environment: Environment.SANDBOX,
},
passport: undefined,
};
const WIDGETS_SCRIPT_TIMEOUT = 100;

// Checkout SDK
export class Checkout {
Expand Down Expand Up @@ -119,25 +121,71 @@ export class Checkout {
// Preload the configurations
await checkout.config.remote.getConfig();

const factory = new Promise<ImmutableCheckoutWidgets.WidgetsFactory>((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 factory = await this.loadEsModules(init.config, init.version);
return factory;
} catch (err: any) {
throw new CheckoutError(
'Failed to load widgets script',
CheckoutErrorType.WIDGETS_SCRIPT_LOAD_ERROR,
{ error: err },
);
}
}

private async loadUmdBundle(
config: WidgetConfiguration,
version?: SemanticVersion,
) {
const checkout = this;

const factory = new Promise<ImmutableCheckoutWidgets.WidgetsFactory>((resolve, reject) => {
try {
const script = loadUnresolved(init.version);
if (script.loaded && typeof ImmutableCheckoutWidgets !== 'undefined') {
resolve(new ImmutableCheckoutWidgets.WidgetsFactory(checkout, init.config));
} else {
checkForWidgetsBundleLoaded();
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) {
if (typeof ImmutableCheckoutWidgets !== 'undefined') {
resolve(new ImmutableCheckoutWidgets.WidgetsFactory(checkout, config));
} else {
reject(
new CheckoutError(
'Failed to find ImmutableCheckoutWidgets script',
CheckoutErrorType.WIDGETS_SCRIPT_LOAD_ERROR,
),
);
}
}

const tag = document.createElement('script');

tag.addEventListener('load', () => {
if (typeof ImmutableCheckoutWidgets !== 'undefined') {
resolve(new ImmutableCheckoutWidgets.WidgetsFactory(checkout, config));
} else {
reject(
new CheckoutError(
'Failed to find ImmutableCheckoutWidgets script',
CheckoutErrorType.WIDGETS_SCRIPT_LOAD_ERROR,
),
);
}
});

tag.addEventListener('error', (err) => {
reject(
new CheckoutError(
'Failed to load widgets script',
CheckoutErrorType.WIDGETS_SCRIPT_LOAD_ERROR,
{ error: err },
),
);
});

loadUnresolvedBundle(tag, scriptId, validVersion);
} catch (err: any) {
reject(
new CheckoutError(
Expand All @@ -152,6 +200,30 @@ export class Checkout {
return factory;
}

private async loadEsModules(
config: WidgetConfiguration,
version?: SemanticVersion,
) {
const checkout = this;
try {
const cdnUrl = getWidgetsEsmUrl(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);

if (checkoutWidgetsModule && checkoutWidgetsModule.WidgetsFactory) {
return new checkoutWidgetsModule.WidgetsFactory(checkout, config);
}
} catch (err: any) {
// eslint-disable-next-line no-console
console.warn(`Failed to resolve checkout widgets module, falling back to UMD bundle. Error: ${err.message}`);
}

// Fallback to UMD bundle if esm bundle fails to load
return await checkout.loadUmdBundle(config, version);
}

/**
* Creates a provider using the given parameters.
* @param {CreateProviderParams} params - The parameters for creating the provider.
Expand Down
4 changes: 2 additions & 2 deletions packages/checkout/sdk/src/types/widgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ 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).
*/
export type WidgetsInit = {
config: WidgetConfiguration;
version?: SemanticVersion
version?: SemanticVersion;
};
32 changes: 14 additions & 18 deletions packages/checkout/sdk/src/widgets/load.test.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,41 @@
/** @jest-environment jsdom */
import { SDK_VERSION_MARKER } from '../env';
import { loadUnresolved } from './load';
import { getWidgetsEsmUrl, loadUnresolvedBundle } from './load';

describe('load', () => {
const SDK_VERSION = SDK_VERSION_MARKER;
const scriptId = 'immutable-checkout-widgets-bundle';

beforeEach(() => {
jest.spyOn(console, 'warn').mockImplementation(() => {});
});

describe('Config', () => {
describe('load unresolved bundle', () => {
it('should validate the versioning', () => {
loadUnresolved();
const tag = document.createElement('script');
loadUnresolvedBundle(tag, scriptId, SDK_VERSION);
expect(document.head.innerHTML).toBe(
'<script id="immutable-checkout-widgets-bundle" '
+ 'data-version="__SDK_VERSION__" '
+ `src="https://cdn.jsdelivr.net/npm/@imtbl/sdk@${SDK_VERSION}/dist/browser/checkout/widgets.js"></script>`,
);
});
});

it('should not re-add script', () => {
loadUnresolved();
loadUnresolved();
loadUnresolved();
expect(document.head.innerHTML).toBe(
'<script id="immutable-checkout-widgets-bundle" '
+ 'data-version="__SDK_VERSION__" '
+ `src="https://cdn.jsdelivr.net/npm/@imtbl/sdk@${SDK_VERSION}/dist/browser/checkout/widgets.js"></script>`,
describe('get widgets esm url', () => {
it('should validate the versioning', () => {
expect(getWidgetsEsmUrl()).toEqual(
`https://cdn.jsdelivr.net/npm/@imtbl/sdk@${SDK_VERSION}/dist/browser/checkout/widgets-esm.js`,
);
});

it('should change version', () => {
loadUnresolved();
loadUnresolved({
expect(getWidgetsEsmUrl({
major: 1,
minor: 2,
});
expect(document.head.innerHTML).toBe(
'<script id="immutable-checkout-widgets-bundle" '
+ 'data-version="__SDK_VERSION__" '
+ `src="https://cdn.jsdelivr.net/npm/@imtbl/sdk@${SDK_VERSION}/dist/browser/checkout/widgets.js"></script>`,
patch: 3,
})).toEqual(
'https://cdn.jsdelivr.net/npm/@imtbl/[email protected]/dist/browser/checkout/widgets-esm.js',
);
});
});
Expand Down
29 changes: 15 additions & 14 deletions packages/checkout/sdk/src/widgets/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { useLocalBundle } from '../env';
import { SemanticVersion } from './definitions/types';
import { validateAndBuildVersion } from './version';

export function loadUnresolved(
version?: SemanticVersion,
): { loaded: boolean, element: HTMLScriptElement } {
// Loads the checkout widgets bundle from the CDN and appends the script to the document head
export function loadUnresolvedBundle(
tag: HTMLScriptElement,
scriptId: string,
validVersion: string,
) {
if (window === undefined) {
throw new Error('missing window object: please run Checkout client side');
}
Expand All @@ -13,16 +16,6 @@ export function loadUnresolved(
throw new Error('missing document object: please run Checkout client side');
}

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 tag = document.createElement('script');

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`;

Expand All @@ -31,6 +24,14 @@ export function loadUnresolved(
tag.setAttribute('src', cdnUrl);

document.head.appendChild(tag);
}

return { loaded: false, element: tag };
// Gets the CDN url for the split checkout widgets bundle
export function getWidgetsEsmUrl(
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;
}
2 changes: 1 addition & 1 deletion packages/checkout/widgets-lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"private": true,
"scripts": {
"build": "rollup --config rollup.config.js",
"build:local": "yarn build && mkdir -p ../widgets-sample-app/public/lib/js && cp dist/widgets.js ../widgets-sample-app/public/lib/js/widgets.js",
"build:local": "yarn build && mkdir -p ../widgets-sample-app/public/lib/js && cp dist/*.js ../widgets-sample-app/public/lib/js/",
"lint": "eslint ./src --ext .ts,.jsx,.tsx --max-warnings=0",
"lint:fix": "eslint ./src --ext .ts,.jsx,.tsx --max-warnings=0 --fix",
"start": "rollup --config rollup.config.js --watch",
Expand Down
22 changes: 12 additions & 10 deletions packages/checkout/widgets-lib/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 [
Expand All @@ -23,7 +30,9 @@ export default [
dir: 'dist',
format: 'es'
},
plugins: [...defaultPlugin ],
plugins: [
...defaultPlugin,
]
},
{
watch: false,
Expand All @@ -36,14 +45,7 @@ export default [
},
context: 'window',
plugins: [
resolve({
browser: true,
dedupe: ['react', 'react-dom'],
}),
nodePolyfills(),
commonjs(),
...defaultPlugin,
terser(),
...defaultPlugin,,
]
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
initialConnectLoaderState,
} from '../../context/connect-loader-context/ConnectLoaderContext';
import { LoadingView } from '../../views/loading/LoadingView';
import { ConnectWidget } from '../../widgets/connect/ConnectWidget';
import ConnectWidget from '../../widgets/connect/ConnectWidget';
import { ConnectWidgetViews } from '../../context/view-context/ConnectViewContextTypes';
import { StrongCheckoutWidgetsConfig } from '../../lib/withDefaultWidgetConfig';
import { useAnalytics } from '../../context/analytics-provider/SegmentAnalyticsProvider';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { BigNumber } from 'ethers';
import { TokenBridge } from '@imtbl/bridge-sdk';
import { ViewContextTestComponent } from 'context/view-context/test-components/ViewContextTestComponent';
import { Transaction } from 'lib/clients';
import { BridgeWidget } from './BridgeWidget';
import BridgeWidget from './BridgeWidget';
import mockTransactionPending from './test-components/BridgeTransactionWithdrawalPending.json';
import mockTransactionInProgress from './test-components/BridgeTransactionInProgress.json';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export type BridgeWidgetInputs = BridgeWidgetParams & {
web3Provider?: Web3Provider;
};

export function BridgeWidget({
export default function BridgeWidget({
checkout,
web3Provider,
config,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { Suspense } from 'react';
import {
BridgeWidgetParams,
IMTBLWidgetEvents,
Expand All @@ -9,9 +9,12 @@ import {
} from '@imtbl/checkout-sdk';
import { Base } from 'widgets/BaseWidgetRoot';
import { isValidWalletProvider, isValidAmount, isValidAddress } from 'lib/validations/widgetValidators';
import { BridgeWidget } from 'widgets/bridge/BridgeWidget';
import { ThemeProvider } from 'components/ThemeProvider/ThemeProvider';
import { CustomAnalyticsProvider } from 'context/analytics-provider/CustomAnalyticsProvider';
import { LoadingView } from 'views/loading/LoadingView';
import i18n from '../../i18n';

const BridgeWidget = React.lazy(() => import('./BridgeWidget'));

export class Bridge extends Base<WidgetType.BRIDGE> {
protected eventTopic: IMTBLWidgetEvents = IMTBLWidgetEvents.IMTBL_BRIDGE_WIDGET_EVENT;
Expand Down Expand Up @@ -57,18 +60,22 @@ export class Bridge extends Base<WidgetType.BRIDGE> {

protected render() {
if (!this.reactRoot) return;
const { t } = i18n;

this.reactRoot.render(
<React.StrictMode>
<CustomAnalyticsProvider checkout={this.checkout}>
<ThemeProvider id="bridge-container" config={this.strongConfig()}>
<BridgeWidget
checkout={this.checkout}
config={this.strongConfig()}
web3Provider={this.web3Provider}
tokenAddress={this.parameters.tokenAddress}
amount={this.parameters.amount}
walletProviderName={this.parameters.walletProviderName}
/>
<Suspense fallback={<LoadingView loadingText={t('views.LOADING_VIEW.text')} />}>
<BridgeWidget
checkout={this.checkout}
config={this.strongConfig()}
web3Provider={this.web3Provider}
tokenAddress={this.parameters.tokenAddress}
amount={this.parameters.amount}
walletProviderName={this.parameters.walletProviderName}
/>
</Suspense>
</ThemeProvider>
</CustomAnalyticsProvider>
</React.StrictMode>,
Expand Down
Loading

0 comments on commit e47396c

Please sign in to comment.