-
Notifications
You must be signed in to change notification settings - Fork 215
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(checkout): CHECKOUT-7595 Add client extension service (#2056)
- Loading branch information
1 parent
b74f725
commit c04106a
Showing
10 changed files
with
286 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { initializeExtensionService } from '../extension'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
packages/core/src/extension/initialize-extension-service.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
29
packages/core/src/extension/initialize-extension-service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.