diff --git a/package.json b/package.json index adeb263..1106128 100644 --- a/package.json +++ b/package.json @@ -48,5 +48,6 @@ }, "alias": { "jose": false - } + }, + "packageManager": "pnpm@11.16.0+sha512.b767e9a98fc87aec0f42daefcd0e84941c2cdb45d73c4e1e68860fb2bdfdbe032ab592105479e5e05c237f740c8a5ffdbbb52385797ddc2296745a1bbb883e8d" } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5eafda9..fbb070d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,8 @@ +allowBuilds: + '@swc/core': true + lmdb: true + msgpackr-extract: true + puppeteer: true onlyBuiltDependencies: - '@swc/core' - lmdb diff --git a/src/TecsafeWidgetSDK.ts b/src/TecsafeWidgetSDK.ts index f34f685..a3d4c6d 100644 --- a/src/TecsafeWidgetSDK.ts +++ b/src/TecsafeWidgetSDK.ts @@ -11,11 +11,7 @@ import { CustomerTokenCallback } from './types/CustomerTokenCallback' import { EventBus } from './util/EventBus' import { Logger } from './util/Logger' -import { - AddToCartHandler, - BulkAddToCartHandler, - SingleAddToCartHandler, -} from './types/AddToCardHandler' +import { AddToCartHandler } from './types/AddToCartHandler' import { parseCustomerJwt } from './util/ParseCustomerJwt' @@ -59,40 +55,18 @@ export class TecsafeWidgetManager extends EventBus { document.createElement('div'), this ) - // To don't make it to obvious thats a "browserID" - // We shorten it to "bid" - this.browserId = localStorage.getItem('tecsafe-bid') - if (!this.browserId) { - this.browserId = Math.random().toString(36).slice(2) - localStorage.setItem('tecsafe-bid', this.browserId) - } - const params = readUrlParams() - if (!params.browserId) return - if (this.browserId !== params.browserId) { - clearUrlParams() - Logger.getInstance().warn('Browser ID mismatch, clearing URL params') - return - } - try { - this.openFullScreen(params.url) - } catch (e) { - Logger.getInstance().error('Failed to open full screen:', String(e)) - } - this.on(IN_MESSAGES.InMessageAddToCart, async (e) => { + const handler = this.addToCartCallback const positions = e.event.positions - if ('bulk' in this.addToCartCallback) { - const results = await ( - this.addToCartCallback as BulkAddToCartHandler - ).bulk(positions) + if ('bulk' in handler) { + const results = await handler.bulk(positions) results.forEach((result) => { e.respond(OUT_MESSAGES.OutMessageAddedToCart.create(result)) }) - } else if ('single' in this.addToCartCallback) { - const singleCb = this.addToCartCallback as SingleAddToCartHandler + } else { await Promise.all( positions.map(async (pos) => { - const success = await singleCb.single( + const success = await handler.single( pos.articleNumber, pos.quantity, pos.configurationId @@ -107,13 +81,32 @@ export class TecsafeWidgetManager extends EventBus { ) } }) + // To don't make it to obvious thats a "browserID" + // We shorten it to "bid" + this.browserId = + localStorage.getItem('tecsafe-bid') ?? Math.random().toString(36).slice(2) + if (!localStorage.getItem('tecsafe-bid')) { + localStorage.setItem('tecsafe-bid', this.browserId) + } + const params = readUrlParams() + if (!params.browserId) return + if (this.browserId !== params.browserId) { + clearUrlParams() + Logger.getInstance().warn('Browser ID mismatch, clearing URL params') + return + } + try { + this.openFullScreen(params.url) + } catch (e) { + Logger.getInstance().error('Failed to open full screen:', String(e)) + } } - private browserId: string + private browserId!: string private widgets: BaseWidget[] = [] - private appWidget: AppWidget - private token: string - private tokenTimeout: number + private appWidget!: AppWidget + private token!: string + private tokenTimeout!: number private tokenPromise: Promise | null = null private refreshTimeoutId: number | null = null private fullScreenData: any @@ -151,10 +144,6 @@ export class TecsafeWidgetManager extends EventBus { this.sendToAllWidgets(OUT_MESSAGES.OutMessageFullScreenClosed.create()) } - /** - * Sends a message to all widgets - * @param message The message to send - */ /** * Sends a message to all widgets * @param message The message to send diff --git a/src/index.ts b/src/index.ts index c95dd3a..d500606 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,10 @@ import { TecsafeWidgetManager } from './TecsafeWidgetSDK' export * from './TecsafeWidgetSDK' +export * from './types/AddToCartHandler' export * from './types/BaseWidget' +export * from './types/Context' +export * from './types/CustomerTokenCallback' export * from './types/WidgetManagerConfig' export * from './types/MessageEnvelope' export * from './util/UrlParamRW' diff --git a/src/types/AddToCardHandler.ts b/src/types/AddToCartHandler.ts similarity index 100% rename from src/types/AddToCardHandler.ts rename to src/types/AddToCartHandler.ts diff --git a/src/types/WidgetManagerConfig.ts b/src/types/WidgetManagerConfig.ts index e17bd99..58aa3a8 100644 --- a/src/types/WidgetManagerConfig.ts +++ b/src/types/WidgetManagerConfig.ts @@ -7,19 +7,19 @@ export class RequiredWidgetManagerConfig { * Will activate customer tracking. Make sure to request tracking consent from your customer and * set accordingly. */ - public trackingAllowed: boolean + public trackingAllowed!: boolean /** * The RFC 4647 language tag representing the user's preferred language, e.g. "en-US" */ - public languageRFC4647: string + public languageRFC4647!: string /** * The ISO 4217 currency code, e.g. "USD" */ - public currencyCodeISO4217: string + public currencyCodeISO4217!: string /** * Whether tax is included in the prices. Required to display correct pricing info - gross/net. */ - public taxIncluded: boolean + public taxIncluded!: boolean } /** @@ -42,12 +42,21 @@ export class WidgetManagerConfig extends RequiredWidgetManagerConfig { /** * The base URL for the widget UIs */ - public widgetBaseURL: string = 'https://tecsafe.github.io/app-ui/iframe' // TODO: update to live page + public widgetBaseURL: string = 'https://app-ui.tecsafe.de/iframe' /** * A list of allowed origins for the SDK to communicate with */ - public allowedOrigins: string[] = ['https://tecsafe.github.io'] // TODO: update to live page + public allowedOrigins: string[] = [ + 'app-ui.tecsafe.de', + 'tecsafe.github.io', + 'tecsafe.de', + 'editor.tecsafe.de', + 'ofcp-editor.stage.tecsafe.de', + 'ofcp-editor.testing.tecsafe.de', + 'ofcp-editor.tecsafe-local.de', + 'example.com', + ] // TODO: update to live page /** * Iframe styles.transition property diff --git a/src/widget/AppWidget.ts b/src/widget/AppWidget.ts index 3baebb8..9421c8e 100644 --- a/src/widget/AppWidget.ts +++ b/src/widget/AppWidget.ts @@ -15,7 +15,7 @@ export class AppWidget extends BaseWidget implements IAppWidget { * The app widget allows any origin defined in the config, so it does not have a uiPath * @see {@link AppWidget.setUrl} {@link AppWidget.getUrl} */ - protected url: string + protected url!: string /** * Sets the url of the widget @@ -73,7 +73,9 @@ export class AppWidget extends BaseWidget implements IAppWidget { * This ensures if a user refreshes the page, they will be taken back to the same place. */ protected handlePageLoad(): void { - const url = this.iframe.contentWindow.location.href + const iframe = this.iframe + if (!iframe?.contentWindow) return + const url = iframe.contentWindow.location.href const { origin } = new URL(url) if (!this.config.allowedOrigins.includes(origin)) { this.destroy() @@ -90,6 +92,7 @@ export class AppWidget extends BaseWidget implements IAppWidget { * @inheritdoc */ protected postShow(): void { + if (!this.iframe) return if (!this.url) { this.destroy() throw new Error(`[TECSAFE] Widget ${this.el} cannot show without a url`) diff --git a/test/TecsafeWidgetSDK.spec.ts b/test/TecsafeWidgetSDK.spec.ts index a0f64dd..5efec46 100644 --- a/test/TecsafeWidgetSDK.spec.ts +++ b/test/TecsafeWidgetSDK.spec.ts @@ -8,6 +8,13 @@ import { } from '@jest/globals' import { TecsafeWidgetManager } from '../src/TecsafeWidgetSDK' import { WidgetManagerConfig } from '../src/types/WidgetManagerConfig' +import { IN_MESSAGES } from '../src/messages/Messages' +import type { + SingleAddToCartHandler, + BulkAddToCartHandler, + AddToCartHandler, + CustomerTokenCallback, +} from '../src' describe('TecsafeWidgetManager', () => { let mockTokenCallback: any @@ -209,4 +216,116 @@ describe('TecsafeWidgetManager', () => { manager.emit({ type: 'test', create: () => ({}) } as any, {}) expect(spy).toHaveBeenCalled() }) + + describe('add-to-cart handling', () => { + const positions = [ + { + linePosition: 1, + articleNumber: 'ART-1', + quantity: 2, + configurationId: 'cfg-1', + }, + { linePosition: 2, articleNumber: 'ART-2', quantity: 1 }, + ] + + const flushAsync = () => new Promise((resolve) => setTimeout(resolve, 0)) + + it('registers the add-to-cart listener even without tecsafe url params', () => { + const manager = new TecsafeWidgetManager( + mockTokenCallback, + mockAddToCartCallback, + mockConfig + ) + expect(manager.getMessageListeners()).toContain('add-to-cart') + }) + + it('calls the single handler per position and responds per position', async () => { + const single = jest + .fn< + ( + articleNumber: string, + quantity: number, + configurationId?: string + ) => Promise + >() + .mockResolvedValue(true) + const manager = new TecsafeWidgetManager( + mockTokenCallback, + { single }, + mockConfig + ) + const widget = { sendMessage: jest.fn() } as any + + manager._triggerListeners( + 'add-to-cart', + IN_MESSAGES.InMessageAddToCart.create({ positions }), + widget + ) + await flushAsync() + + expect(single).toHaveBeenCalledTimes(2) + expect(single).toHaveBeenNthCalledWith(1, 'ART-1', 2, 'cfg-1') + expect(single).toHaveBeenNthCalledWith(2, 'ART-2', 1, undefined) + expect(widget.sendMessage).toHaveBeenCalledWith({ + type: 'added-to-cart', + payload: { linePosition: 1, success: true }, + }) + expect(widget.sendMessage).toHaveBeenCalledWith({ + type: 'added-to-cart', + payload: { linePosition: 2, success: true }, + }) + }) + + it('calls the bulk handler once with all positions and responds per result', async () => { + const bulk = jest + .fn< + ( + items: { + linePosition: number + articleNumber: string + quantity: number + configurationId?: string + }[] + ) => Promise<{ linePosition: number; success: boolean }[]> + >() + .mockResolvedValue([ + { linePosition: 1, success: true }, + { linePosition: 2, success: false }, + ]) + const manager = new TecsafeWidgetManager( + mockTokenCallback, + { bulk }, + mockConfig + ) + const widget = { sendMessage: jest.fn() } as any + + manager._triggerListeners( + 'add-to-cart', + IN_MESSAGES.InMessageAddToCart.create({ positions }), + widget + ) + await flushAsync() + + expect(bulk).toHaveBeenCalledTimes(1) + expect(bulk).toHaveBeenCalledWith(positions) + expect(widget.sendMessage).toHaveBeenCalledWith({ + type: 'added-to-cart', + payload: { linePosition: 1, success: true }, + }) + expect(widget.sendMessage).toHaveBeenCalledWith({ + type: 'added-to-cart', + payload: { linePosition: 2, success: false }, + }) + }) + + it('exposes the handler types via the package entrypoint', () => { + const single: SingleAddToCartHandler = { single: async () => true } + const bulk: BulkAddToCartHandler = { bulk: async () => [] } + const either: AddToCartHandler = single + const tokenCb: CustomerTokenCallback = async () => 'token' + expect('single' in either).toBe(true) + expect(typeof bulk.bulk).toBe('function') + expect(typeof tokenCb).toBe('function') + }) + }) }) diff --git a/tsconfig.json b/tsconfig.json index 17ab3e7..003f18b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,8 @@ "target": "ESNext", "module": "ESNext", "moduleResolution": "Node", - "baseUrl": "." + "baseUrl": ".", + "ignoreDeprecations": "6.0" }, "exclude": [ "dist",