Skip to content

Commit

Permalink
feat(checkout): CHECKOUT-7595 Add client extension service (#2056)
Browse files Browse the repository at this point in the history
  • Loading branch information
animesh1987 authored Jul 19, 2023
1 parent b74f725 commit c04106a
Show file tree
Hide file tree
Showing 10 changed files with 286 additions and 34 deletions.
1 change: 1 addition & 0 deletions packages/core/src/bundles/extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { initializeExtensionService } from '../extension';
54 changes: 54 additions & 0 deletions packages/core/src/extension/extension-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
export interface InitializeExtensionServiceOptions {
extensionId: string;
parentOrigin: string;
}

export enum ExtensionEventType {
CheckoutLoaded = 'CHECKOUT_LOADED',
ShippingCountryChange = 'SHIPPING_COUNTRY_CHANGE',
}

export interface CheckoutLoadedEvent {
type: ExtensionEventType.CheckoutLoaded;
}

export interface ShippingCountryChangeEvent {
type: ExtensionEventType.ShippingCountryChange;
payload: {
countryCode: string;
};
}

export interface ExtensionEventMap {
[ExtensionEventType.CheckoutLoaded]: CheckoutLoadedEvent;
[ExtensionEventType.ShippingCountryChange]: ShippingCountryChangeEvent;
}

export enum ExtensionCommandType {
FRAME_LOADED = 'FRAME_LOADED',
RELOAD_CHECKOUT = 'RELOAD_CHECKOUT',
SHOW_LOADING_INDICATOR = 'SHOW_LOADING_INDICATOR',
}

export interface BaseEventPayload {
payload: {
extensionId?: string;
};
}

export interface ExtensionReloadCommand extends BaseEventPayload {
type: ExtensionCommandType.RELOAD_CHECKOUT;
}

export interface ExtensionShowLoadingIndicatorCommand extends BaseEventPayload {
type: ExtensionCommandType.SHOW_LOADING_INDICATOR;
}

export interface ExtensionFrameLoadedCommand extends BaseEventPayload {
type: ExtensionCommandType.FRAME_LOADED;
}

export type ExtensionCommand =
| ExtensionReloadCommand
| ExtensionShowLoadingIndicatorCommand
| ExtensionFrameLoadedCommand;
4 changes: 2 additions & 2 deletions packages/core/src/extension/extension-origin-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ export interface ReloadCheckoutEvent {
}

export interface ShowLoadingIndicatorEvent {
type: ExtensionCommand.ReloadCheckout;
type: ExtensionCommand.ShowLoadingIndicator;
payload: {
extensionId: string;
show: boolean;
};
}

export interface SetIframeStylePayload {
type: ExtensionCommand.ReloadCheckout;
type: ExtensionCommand.SetIframeStyle;
payload: {
extensionId: string;
style: {
Expand Down
102 changes: 102 additions & 0 deletions packages/core/src/extension/extension-service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { noop } from 'lodash';

import { IframeEventListener, IframeEventPoster } from '../common/iframe';

import {
ExtensionCommand,
ExtensionCommandType,
ExtensionEventMap,
ExtensionEventType,
} from './extension-client';
import ExtensionService from './extension-service';

describe('ExtensionService', () => {
let extensionService: ExtensionService;
let eventListener: IframeEventListener<ExtensionEventMap>;
let eventPoster: IframeEventPoster<ExtensionCommand>;

beforeEach(() => {
eventListener = new IframeEventListener('https://mybigcommerce.com');
eventPoster = new IframeEventPoster('https://mybigcommerce.com');

jest.spyOn(eventListener, 'listen');
jest.spyOn(eventListener, 'addListener');
jest.spyOn(eventPoster, 'post');
jest.spyOn(eventPoster, 'post');

extensionService = new ExtensionService(eventListener, eventPoster);
});

it('#initializes success fully', () => {
extensionService.initialize('test');

expect(eventListener.listen).toHaveBeenCalled();
});

it('#initialize throws error if no extension Id is passed', () => {
expect(() => extensionService.initialize(undefined as unknown as string)).toThrow(
new Error('Extension Id not found.'),
);
});

it('#post throws error if extension id is not set', () => {
extensionService.initialize('test');

const event: ExtensionCommand = {
type: ExtensionCommandType.FRAME_LOADED,
payload: {},
};

extensionService.post(event);

expect(eventPoster.post).toHaveBeenCalledWith({
type: ExtensionCommandType.FRAME_LOADED,
payload: {
extensionId: 'test',
},
});
});

it('#post throws error if event name is not passed correctly', () => {
extensionService.initialize('test');

const event: ExtensionCommand = {
type: 'some-event' as ExtensionCommandType,
payload: {},
};

expect(() => extensionService.post(event)).toThrow('some-event is not supported.');
});

it('#addListener adds callback as noop if no callback method is passed', () => {
extensionService.initialize('test');

extensionService.addListener(ExtensionEventType.CheckoutLoaded);

expect(eventListener.addListener).toHaveBeenCalledWith(
ExtensionEventType.CheckoutLoaded,
noop,
);
});

it('#addListener is not called if event name is not correct', () => {
extensionService.initialize('test');

expect(() => extensionService.addListener('someevent' as ExtensionEventType)).toThrow(
'someevent is not supported.',
);
});

it('#addListener is called correctly with params', () => {
extensionService.initialize('test');

const callbackFn = jest.fn();

extensionService.addListener(ExtensionEventType.CheckoutLoaded, callbackFn);

expect(eventListener.addListener).toHaveBeenCalledWith(
ExtensionEventType.CheckoutLoaded,
callbackFn,
);
});
});
58 changes: 58 additions & 0 deletions packages/core/src/extension/extension-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { noop } from 'lodash';

import { IframeEventListener, IframeEventPoster } from '../common/iframe';

import {
ExtensionCommand,
ExtensionCommandType,
ExtensionEventMap,
ExtensionEventType,
} from './extension-client';

export default class ExtensionService {
private _extensionId?: string;

constructor(
private _eventListener: IframeEventListener<ExtensionEventMap>,
private _eventPoster: IframeEventPoster<ExtensionCommand>,
) {
this._eventPoster.setTarget(window.parent);
}

initialize(extensionId: string): void {
if (!extensionId) {
throw new Error('Extension Id not found.');
}

this._extensionId = extensionId;

this._eventListener.listen();
}

post(event: ExtensionCommand): void {
if (!this._extensionId) {
return;
}

if (!Object.values(ExtensionCommandType).includes(event.type)) {
throw new Error(`${event.type} is not supported.`);
}

const payload = {
...event.payload,
extensionId: this._extensionId,
};

this._eventPoster.post({ ...event, payload });
}

addListener(eventType: ExtensionEventType, callback: () => void = noop): () => void {
if (!Object.values(ExtensionEventType).includes(eventType)) {
throw new Error(`${eventType} is not supported.`);
}

this._eventListener.addListener(eventType, callback);

return () => this._eventListener.removeListener(eventType, callback);
}
}
1 change: 1 addition & 0 deletions packages/core/src/extension/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ export {
ExtensionSelectorFactory,
createExtensionSelectorFactory,
} from './extension-selector';
export { default as initializeExtensionService } from './initialize-extension-service';
export { ExtensionState } from './extension-state';
export { HostOriginEvent } from './host-origin-event';
16 changes: 16 additions & 0 deletions packages/core/src/extension/initialize-extension-service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { InitializeExtensionServiceOptions } from './extension-client';
import ExtensionService from './extension-service';
import initializeExtensionService from './initialize-extension-service';

describe('initializeExtensionService', () => {
it('initializes extension service correctly', () => {
const options: InitializeExtensionServiceOptions = {
extensionId: 'test',
parentOrigin: 'https://test.com',
};

const extensionService = initializeExtensionService(options);

expect(extensionService).toBeInstanceOf(ExtensionService);
});
});
29 changes: 29 additions & 0 deletions packages/core/src/extension/initialize-extension-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
IframeEventListener,
IframeEventPoster,
setupContentWindowForIframeResizer,
} from '../common/iframe';

import {
ExtensionCommand,
ExtensionCommandType,
ExtensionEventMap,
InitializeExtensionServiceOptions,
} from './extension-client';
import ExtensionService from './extension-service';

export default function initializeExtensionService(options: InitializeExtensionServiceOptions) {
const { extensionId, parentOrigin } = options;

setupContentWindowForIframeResizer();

const extension = new ExtensionService(
new IframeEventListener<ExtensionEventMap>(parentOrigin),
new IframeEventPoster<ExtensionCommand>(parentOrigin),
);

extension.initialize(extensionId);
extension.post({ type: ExtensionCommandType.FRAME_LOADED, payload: { extensionId } });

return extension;
}
34 changes: 14 additions & 20 deletions webpack-common.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
const path = require('path');
const { DefinePlugin } = require('webpack');

const { getNextVersion, packageLoaderRules : { aliasMap: alias, tsSrcPackages } } = require('./scripts/webpack');
const {
getNextVersion,
packageLoaderRules: { aliasMap: alias, tsSrcPackages },
} = require('./scripts/webpack');

const libraryName = 'checkoutKit';

Expand All @@ -11,6 +14,7 @@ const libraryEntries = {
'checkout-sdk': path.join(coreSrcPath, 'bundles', 'checkout-sdk.ts'),
'checkout-button': path.join(coreSrcPath, 'bundles', 'checkout-button.ts'),
'embedded-checkout': path.join(coreSrcPath, 'bundles', 'embedded-checkout.ts'),
extension: path.join(coreSrcPath, 'bundles', 'extension.ts'),
'hosted-form': path.join(coreSrcPath, 'bundles', 'hosted-form.ts'),
'internal-mappers': path.join(coreSrcPath, 'bundles', 'internal-mappers.ts'),
};
Expand All @@ -19,7 +23,7 @@ async function getBaseConfig() {
return {
stats: {
errorDetails: true,
logging: 'verbose'
logging: 'verbose',
},
devtool: 'source-map',
mode: 'production',
Expand All @@ -39,25 +43,22 @@ async function getBaseConfig() {
enforce: 'pre',
loader: require.resolve('source-map-loader'),
},
...tsSrcPackages
...tsSrcPackages,
],
},
plugins: [
new DefinePlugin({
'LIBRARY_VERSION': JSON.stringify(await getNextVersion()),
LIBRARY_VERSION: JSON.stringify(await getNextVersion()),
}),
],
};
};
}

const babelEnvPreset = [
'@babel/preset-env',
{
corejs: 3,
targets: [
'defaults',
'ie 11',
],
targets: ['defaults', 'ie 11'],
useBuiltIns: 'usage',
},
];
Expand All @@ -68,25 +69,18 @@ const babelLoaderRules = [
loader: 'babel-loader',
include: coreSrcPath,
options: {
presets: [
babelEnvPreset,
],
presets: [babelEnvPreset],
},
},
{
test: /\.js$/,
loader: 'babel-loader',
include: path.join(__dirname, 'node_modules'),
exclude: [
/\/node_modules\/core-js\//,
/\/node_modules\/webpack\//,
],
exclude: [/\/node_modules\/core-js\//, /\/node_modules\/webpack\//],
options: {
presets: [
babelEnvPreset,
],
presets: [babelEnvPreset],
sourceType: 'unambiguous',
}
},
},
];

Expand Down
Loading

0 comments on commit c04106a

Please sign in to comment.