import { ExtensionConfigMessage, ExtensionMessage, ExtensionReadyMessage, ExtensionRequestMessage, ExtensionResizeMessage, ExtensionResponseMessage, createConfigMessage, createSuccessfulResponseMessage, createUnsuccessfulResponseMessage, } from '@stack-spot/portal-extension-commons' import { URLChangeMessage } from '../../commons/dist' import { CURRENT_VIEW_ATTRIBUTE } from './constants' import { buildNetworkConfig } from './network-config' import { handleOperation } from './operation-handler' interface ConstructorProps { slug: string, version?: string, apiUrl: string, pathToParent?: string, iframe: HTMLIFrameElement, shouldAdaptHeight?: boolean, onSubmit?: (data: Record) => void, onCancel?: () => void, } export class MessageHandler { private iframe: HTMLIFrameElement private version: string | undefined private apiUrl: string private pathToParent?: string private slug: string private listener: ((event: MessageEvent) => void) | undefined private shouldAdaptHeight: boolean private onSubmit?: (data: Record) => void private onCancel?: () => void constructor({ apiUrl, iframe, slug, pathToParent, version, shouldAdaptHeight, onCancel, onSubmit }: ConstructorProps) { this.iframe = iframe this.version = version this.apiUrl = apiUrl this.slug = slug this.setupListeners() this.pathToParent = pathToParent this.shouldAdaptHeight = shouldAdaptHeight ?? false this.onCancel = onCancel this.onSubmit = onSubmit } private postMessage(message: ExtensionMessage) { this.iframe.contentWindow?.postMessage(message, '*') } private getMessage(data: any) { return ( !data || typeof data != 'object' || data.type != 'stackspot-extension-message' || (data.signal != 'ready' && data.signal != 'resize' && data.signal != 'request' && data.signal !== 'url-change') ) ? undefined : data as ExtensionReadyMessage | ExtensionResizeMessage | ExtensionRequestMessage | URLChangeMessage } private setupListeners() { this.listener = (event) => { if (event.source != this.iframe.contentWindow) return const message = this.getMessage(event.data) if (message?.signal === 'ready') this.sendConfig(createConfigMessage({ network: buildNetworkConfig() })) else if (message?.signal === 'request') this.handleRequest(message) else if (message?.signal === 'resize') this.handleResizeMessage(message) else if (message?.signal === 'url-change') this.handleURLChangeMessage(message) } window.addEventListener('message', this.listener) } private handleResizeMessage(message: ExtensionResizeMessage) { if (this.shouldAdaptHeight) this.iframe.height = `${message.height}px` } private handleURLChangeMessage(message: URLChangeMessage) { if (!this.pathToParent) return const iframeBaseRegex = new RegExp(`^${this.apiUrl}(/#)?`) const current = message.current.replace(iframeBaseRegex, '') const next = `${this.pathToParent}/${current}`.replace('#', '/').replace(/\/+/g, '/').replace(/\/$/, '') if (next !== location.pathname.replace(/\/$/, '')) window.history.replaceState(null, '', next || '/') this.iframe.setAttribute(CURRENT_VIEW_ATTRIBUTE, current) } private async handleRequest(message: ExtensionRequestMessage) { let response: ExtensionResponseMessage try { const result = await handleOperation( { apiUrl: this.apiUrl, operation: message.operation, slug: this.slug, variables: message.variables, version: this.version, }, this.iframe.getAttribute('src') ?? '', this.onSubmit, this.onCancel, ) response = createSuccessfulResponseMessage(message.operation, message.id, result) } catch (error: any) { let errorMessage: string | undefined = error.message if (!errorMessage && typeof errorMessage === 'object') { try { errorMessage = JSON.stringify(error) } catch { /* empty */ } } errorMessage ||= `${error}` response = createUnsuccessfulResponseMessage(message.operation, message.id, errorMessage, error.status) } this.postMessage(response) } sendConfig(config: ExtensionConfigMessage) { this.postMessage(config) } destroy() { if (this.listener) window.removeEventListener('message', this.listener) } }