Skip to content
Merged
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@
},
"alias": {
"jose": false
}
},
"packageManager": "pnpm@11.16.0+sha512.b767e9a98fc87aec0f42daefcd0e84941c2cdb45d73c4e1e68860fb2bdfdbe032ab592105479e5e05c237f740c8a5ffdbbb52385797ddc2296745a1bbb883e8d"
}
5 changes: 5 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
allowBuilds:
'@swc/core': true
lmdb: true
msgpackr-extract: true
puppeteer: true
onlyBuiltDependencies:
- '@swc/core'
- lmdb
Expand Down
69 changes: 29 additions & 40 deletions src/TecsafeWidgetSDK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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
Expand All @@ -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<string> | null = null
private refreshTimeoutId: number | null = null
private fullScreenData: any
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
File renamed without changes.
21 changes: 15 additions & 6 deletions src/types/WidgetManagerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand All @@ -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
Expand Down
7 changes: 5 additions & 2 deletions src/widget/AppWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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`)
Expand Down
119 changes: 119 additions & 0 deletions test/TecsafeWidgetSDK.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<boolean>
>()
.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')
})
})
})
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node",
"baseUrl": "."
"baseUrl": ".",
"ignoreDeprecations": "6.0"
},
"exclude": [
"dist",
Expand Down
Loading