export enum MessageTypes { V1_Ready = 'v1.ready', V1_AddToCart = 'v1.add-to-cart', V1_PriceChanged = 'v1.price-changed', V1_SaveCurrentConfiguration = 'v1.save-current-config', V1_BeforeImageUpload = 'v1.before-image-upload', } type MessageEventHandler = (eventData: any) => boolean | void; type ExpiviEventHandler = (eventData: any) => Promise | any; type XpvMessage = { xpv: true; type: MessageTypes; state: 'pending' | 'failed' | 'finished'; payload: any; uuid: string; }; export default class ExpiviIFrameService { private static expiviReady = false; private readonly frame: Window; /** * _listeners is for all internal listeners to make the service work */ private _listeners: Record = {}; /** * client's event listeners */ private extListeners: Record = {}; constructor(expiviFrame: Window) { this.frame = expiviFrame; window.addEventListener('message', event => void this.handleIncomingMessage(event)); } private async handleIncomingMessage(event: MessageEvent): Promise { if (!event.data.xpv || !Object.values(MessageTypes).includes(event.data.type)) { return; // if we are not an expivi message, then expivi shouldn't handle us. } this.extListeners['*']?.(event.data); const eventType = event.data.type; // these listeners are defined by the current frame. if (this.extListeners[eventType] === undefined) { if (event.data.state === 'pending') { // Return false if no listeners are attached to the event void this.sendMessage(eventType, false, event.data.uuid); } } else { const result = await this.extListeners[eventType](event.data.payload); if (event.data.state === 'pending') { void this.sendMessage(eventType, result, event.data.uuid); } } // These listeners are created when sending a message from this frame to the target frame const listeners = this._listeners[eventType]; if (listeners?.length > 0) { for (let i = listeners.length - 1; i >= 0; i--) { if (typeof listeners[i](event.data) === 'boolean') { this._listeners[eventType].splice(i, 1); } } } } public async sendMessage(messageType: MessageTypes, payload: any = undefined, uuid: string | undefined = undefined): Promise { return new Promise((resolve, reject) => { const message = ExpiviIFrameService.generateMessage(messageType, payload, uuid); if (message.state === 'pending') { this.addEventListener(messageType, (data: XpvMessage) => { if (message.uuid !== data.uuid) { return; } if (data.state === 'finished') { resolve(data.payload); return true; } reject(data); return false; }); } this.frame.postMessage(message, '*'); }); } private addEventListener(type: MessageTypes, listener: MessageEventHandler) { if (this._listeners[type] === undefined) { this._listeners[type] = []; } this._listeners[type].push(listener); } protected static uuidV4(): string { return window.crypto?.randomUUID?.() ?? 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } private static generateMessage(type: MessageTypes, payload: any = undefined, uuid: string | undefined = undefined): XpvMessage { return { xpv: true, type, state: uuid ? 'finished' : 'pending', payload, uuid: uuid ? uuid : ExpiviIFrameService.uuidV4(), }; } public setEventListener(type: MessageTypes | string, handler: ExpiviEventHandler): void { this.extListeners[type] = handler; } public async isReady(): Promise { if (ExpiviIFrameService.expiviReady) { return true; } return new Promise((resolve, reject) => { this.addEventListener(MessageTypes.V1_Ready, (data: XpvMessage) => { if (data.state !== 'finished') { reject('[Expivi] Unable to initialise.'); } ExpiviIFrameService.expiviReady = true; resolve(true); }); }); } } (window as any).ExpiviIFrameService = ExpiviIFrameService;