@wetransform/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # @wetransform/core
2
+
3
+ Headless browser SDK to open WeTransform in your app (modal or inline), authenticate, and interact through a typed event API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @wetransform/core
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { createWeTransform } from '@wetransform/core'
15
+
16
+ const sdk = createWeTransform({
17
+ organizationHandle: 'acme',
18
+ locale: 'en',
19
+ displayAsModal: true,
20
+ initialLocation: 'transformations',
21
+ authentication: {
22
+ customerId: 'ext-123',
23
+ signature: 'signed-payload',
24
+ templateHandle: 'demo-template',
25
+ sourceId: 'newsletter',
26
+ },
27
+ })
28
+
29
+ // Subscribing to the successSubmit event
30
+ sdk.on('successSubmit', (payload) => console.log('File successfully submitted', payload))
31
+
32
+ // Opening WeTransform
33
+ await sdk.open()
34
+ // Close WeTransform
35
+ await sdk.close()
36
+ // Destroy WeTransform session
37
+ await sdk.destroy()
38
+ ```
39
+
40
+ ## API
41
+
42
+ ### `createWeTransform(config)`
43
+
44
+ Returns a `WeTransformInstance` with:
45
+
46
+ - `open(): Promise<void>` - initializes auth and mounts WeTransform
47
+ - `close(): Promise<void>` - closes SDK UI and unmounts WeTransform
48
+ - `destroy(): Promise<void>` - closes and tears down SDK resources
49
+ - `on(event, handler): () => void` - subscribes to SDK events, returns unsubscribe function
50
+
51
+ ### `WeTransformConfig`
52
+
53
+ - `organizationHandle: string`
54
+ - `locale?: 'en' | 'fr'`
55
+ - `authentication: WeTransformAuthenticationConfig`
56
+ - `initialLocation?: 'transformations' | 'uploader' | 'mapper' | 'finalize' | 'sourcePreview' | 'sourceUpdate' | 'scheduler'`
57
+ - `displayAsModal?: boolean` (default: `true`)
58
+ - `mountElement?: string | HTMLElement` (default id: `weTransform_iframeContainer`, used when `displayAsModal` is `false`)
59
+
60
+ Some `initialLocation` values require additional auth fields:
61
+
62
+ - `uploader`: requires `authentication.templateHandle`
63
+ - `mapper`, `finalize`, `sourcePreview`, `sourceUpdate`, `scheduler`: require both `authentication.templateHandle` and `authentication.sourceId`
64
+
65
+ ### `WeTransformAuthenticationConfig`
66
+
67
+ - `customerId: string`
68
+ - `signature: string`
69
+ - `templateHandle?: string`
70
+ - `sourceId?: string`
71
+
72
+ ## Events
73
+
74
+ Supported `sdk.on(...)` events:
75
+
76
+ - `open`
77
+ - `close`
78
+ - `destroy`
79
+ - `onReady`
80
+ - `successSubmit` with payload
81
+
82
+ ```ts
83
+ {
84
+ customerId: string
85
+ templateHandle: string
86
+ fileUrl: string
87
+ signature: string
88
+ // Deprecated
89
+ redirectUrl: string
90
+ }
91
+ ```
92
+
93
+ - `error` with payload `{ code: string; message: string }`
@@ -0,0 +1,64 @@
1
+ //#region src/types.d.ts
2
+ type WeTransformSdkConfigBase = {
3
+ organizationHandle: string;
4
+ locale?: SupportedLocales;
5
+ displayAsModal?: boolean;
6
+ mountElement?: string | HTMLElement;
7
+ };
8
+ type WeTransformSdkConfigWithoutInitialLocation = WeTransformSdkConfigBase & {
9
+ initialLocation?: undefined;
10
+ authentication: WeTransformAuthenticationConfig;
11
+ };
12
+ type WeTransformConfig = WeTransformSdkConfigWithoutInitialLocation | { [L in WeTransformInitialLocations]: {
13
+ organizationHandle: WeTransformSdkConfigBase['organizationHandle'];
14
+ initialLocation: L;
15
+ authentication: AuthenticationConfigForLocation<L>;
16
+ } & WeTransformSdkConfigBase }[WeTransformInitialLocations];
17
+ type WeTransformAuthenticationConfig = {
18
+ customerId: string;
19
+ signature: string;
20
+ templateHandle?: string;
21
+ sourceId?: string;
22
+ };
23
+ type AuthenticationConfigRequiringTemplateHandle = WeTransformAuthenticationConfig & {
24
+ templateHandle: string;
25
+ };
26
+ type AuthenticationConfigRequiringTemplateAndSource = AuthenticationConfigRequiringTemplateHandle & {
27
+ sourceId: string;
28
+ };
29
+ type WeTransformInitialLocations = 'transformations' | 'uploader' | 'mapper' | 'finalize' | 'sourcePreview' | 'sourceUpdate' | 'scheduler';
30
+ type LocationsRequiringTemplateHandle = 'uploader';
31
+ type LocationsRequiringTemplateAndSource = 'mapper' | 'finalize' | 'sourcePreview' | 'sourceUpdate' | 'scheduler';
32
+ type AuthenticationConfigForLocation<L extends WeTransformInitialLocations> = L extends LocationsRequiringTemplateAndSource ? AuthenticationConfigRequiringTemplateAndSource : L extends LocationsRequiringTemplateHandle ? AuthenticationConfigRequiringTemplateHandle : WeTransformAuthenticationConfig;
33
+ type WeTransformEventMap = {
34
+ open: () => void;
35
+ close: () => void;
36
+ destroy: () => void;
37
+ onReady: () => void;
38
+ successSubmit: (payload: {
39
+ customerId: string;
40
+ templateHandle: string;
41
+ fileUrl: string;
42
+ signature: string;
43
+ /**
44
+ * Deprecated. Prefer handling the resulting navigation flow in the host app.
45
+ */
46
+ redirectUrl: string;
47
+ }) => void;
48
+ error: (payload: {
49
+ code: string;
50
+ message: string;
51
+ }) => void;
52
+ };
53
+ type WeTransformInstance = {
54
+ open: () => Promise<void>;
55
+ close: () => Promise<void>;
56
+ destroy: () => Promise<void>;
57
+ on: <K extends keyof WeTransformEventMap>(event: K, handler: WeTransformEventMap[K]) => () => void;
58
+ };
59
+ type SupportedLocales = 'en' | 'fr';
60
+ //#endregion
61
+ //#region src/index.d.ts
62
+ declare function createWeTransform(config: WeTransformConfig): WeTransformInstance;
63
+ //#endregion
64
+ export { type WeTransformAuthenticationConfig, type WeTransformConfig, type WeTransformEventMap, type WeTransformInstance, createWeTransform };
package/dist/index.mjs ADDED
@@ -0,0 +1,850 @@
1
+ import { WindowMessenger, connect } from "penpal";
2
+ import { createFocusTrap } from "focus-trap";
3
+ //#region src/events/EventEmitter.ts
4
+ var EventEmitter = class {
5
+ listeners = {};
6
+ on(event, handler) {
7
+ const existing = this.listeners[event] ?? /* @__PURE__ */ new Set();
8
+ existing.add(handler);
9
+ this.listeners[event] = existing;
10
+ return () => this.off(event, handler);
11
+ }
12
+ off(event, handler) {
13
+ const existing = this.listeners[event];
14
+ if (!existing) return;
15
+ existing.delete(handler);
16
+ if (existing.size === 0) delete this.listeners[event];
17
+ }
18
+ emit(event, ...args) {
19
+ const existing = this.listeners[event];
20
+ if (!existing) return;
21
+ for (const handler of existing) handler(...args);
22
+ }
23
+ clear() {
24
+ this.listeners = {};
25
+ }
26
+ };
27
+ //#endregion
28
+ //#region src/utils/abort.ts
29
+ function createAbortError() {
30
+ return new DOMException("The operation was aborted.", "AbortError");
31
+ }
32
+ function isAbortError(error) {
33
+ return error instanceof DOMException && error.name === "AbortError";
34
+ }
35
+ function throwIfAborted(signal) {
36
+ if (signal.aborted) throw createAbortError();
37
+ }
38
+ //#endregion
39
+ //#region src/auth/AuthBridge.ts
40
+ var AuthBridge = class {
41
+ accessToken = null;
42
+ refreshToken = null;
43
+ initPromise = null;
44
+ refreshPromise = null;
45
+ revokePromise = null;
46
+ constructor(apiUrl, authentication, emitError) {
47
+ this.apiUrl = apiUrl;
48
+ this.authentication = authentication;
49
+ this.emitError = emitError;
50
+ }
51
+ async getAccessToken(signal) {
52
+ if (this.accessToken) return this.accessToken;
53
+ return this.initAuth(signal);
54
+ }
55
+ async refreshAccessToken(signal) {
56
+ if (this.refreshPromise) return this.refreshPromise;
57
+ if (!this.refreshToken) {
58
+ this.emitError({
59
+ code: "auth_refresh_missing",
60
+ message: "No refresh token available for refresh."
61
+ });
62
+ return null;
63
+ }
64
+ this.refreshPromise = this.post("/connect/refresh", { refresh_token: this.refreshToken }, void 0, signal).then((response) => {
65
+ if (response.success) {
66
+ this.setTokens(response.payload);
67
+ return this.accessToken;
68
+ } else {
69
+ this.emitError({
70
+ code: "auth_refresh_failed",
71
+ message: "Auth refresh failed."
72
+ });
73
+ return null;
74
+ }
75
+ }).catch((error) => {
76
+ if (isAbortError(error)) return null;
77
+ this.emitError({
78
+ code: "auth_refresh_failed",
79
+ message: error instanceof Error ? error.message : "Refresh failed."
80
+ });
81
+ return null;
82
+ }).finally(() => {
83
+ this.refreshPromise = null;
84
+ });
85
+ return this.refreshPromise;
86
+ }
87
+ clear() {
88
+ this.accessToken = null;
89
+ this.refreshToken = null;
90
+ this.initPromise = null;
91
+ this.refreshPromise = null;
92
+ this.revokePromise = null;
93
+ }
94
+ revokeSession(options, signal) {
95
+ if (this.revokePromise) return this.revokePromise;
96
+ const refreshToken = this.refreshToken;
97
+ this.accessToken = null;
98
+ this.refreshToken = null;
99
+ if (!refreshToken) return Promise.resolve();
100
+ this.revokePromise = this.post("/connect/revoke", { refresh_token: refreshToken }, { keepalive: options?.bestEffort === true }, signal).then(() => void 0).catch((error) => {
101
+ if (isAbortError(error)) return;
102
+ this.emitError({
103
+ code: "auth_revoke_failed",
104
+ message: error instanceof Error ? error.message : "Session revoke failed."
105
+ });
106
+ }).finally(() => {
107
+ this.revokePromise = null;
108
+ });
109
+ return this.revokePromise;
110
+ }
111
+ async initAuth(signal) {
112
+ if (this.initPromise) return this.initPromise;
113
+ this.initPromise = this.post("/connect/embed", {
114
+ external_id: this.authentication.customerId,
115
+ signature: this.authentication.signature,
116
+ template_handle: this.authentication.templateHandle,
117
+ source_id: this.authentication.sourceId
118
+ }, void 0, signal).then((response) => {
119
+ if (response.success) {
120
+ this.setTokens(response.payload);
121
+ return this.accessToken;
122
+ } else {
123
+ this.emitError({
124
+ code: "auth_init_failed",
125
+ message: "Auth init failed."
126
+ });
127
+ return null;
128
+ }
129
+ }).catch((error) => {
130
+ if (isAbortError(error)) return null;
131
+ this.emitError({
132
+ code: "auth_init_failed",
133
+ message: error instanceof Error ? error.message : "Auth init failed."
134
+ });
135
+ return null;
136
+ }).finally(() => {
137
+ this.initPromise = null;
138
+ });
139
+ return this.initPromise;
140
+ }
141
+ setTokens(tokens) {
142
+ this.accessToken = tokens.access_token;
143
+ this.refreshToken = tokens.refresh_token;
144
+ }
145
+ async post(path, body, options, signal) {
146
+ const url = this.apiUrl + path;
147
+ const response = await fetch(url, {
148
+ method: "POST",
149
+ headers: { "Content-Type": "application/json" },
150
+ body: JSON.stringify(body),
151
+ keepalive: !!options?.keepalive,
152
+ signal
153
+ });
154
+ if (!response.ok) throw new Error(`Request failed with status ${response.status}.`);
155
+ return await response.json();
156
+ }
157
+ };
158
+ //#endregion
159
+ //#region src/iframe/urlHelper.ts
160
+ const getInitialLocationPath = (location, templateHandle, sourceId) => {
161
+ if (location) switch (location) {
162
+ case "transformations": return "/app/workflows";
163
+ case "uploader": return templateHandle ? `/${templateHandle}` : "/app/workflows";
164
+ case "mapper": return templateHandle && sourceId ? `/${templateHandle}/${sourceId}/map/columns/match` : "/app/workflows";
165
+ case "finalize": return templateHandle && sourceId ? `/${templateHandle}/${sourceId}/review` : "/app/workflows";
166
+ case "sourcePreview": return templateHandle && sourceId ? `/${templateHandle}/source/preview/${sourceId}` : "/app/workflows";
167
+ case "sourceUpdate": return templateHandle && sourceId ? `/${templateHandle}/source/update-config/${sourceId}` : "/app/workflows";
168
+ case "scheduler": return templateHandle && sourceId ? `/${templateHandle}/${sourceId}/embedded-schedules/edit` : "/app/workflows";
169
+ default: return "/app/workflows";
170
+ }
171
+ if (templateHandle && sourceId) return `/${templateHandle}/${sourceId}/map/columns/match`;
172
+ if (templateHandle) return `/${templateHandle}`;
173
+ return "/app/workflows";
174
+ };
175
+ //#endregion
176
+ //#region src/iframe/IframeManager.ts
177
+ const EMBED_ROUTE_STORAGE_KEY = "wetransform-sdk-embed-route";
178
+ const EMBED_ROUTE_PENDING_STORAGE_KEY = "wetransform-sdk-embed-route-pending";
179
+ var IframeManager = class {
180
+ iframe = null;
181
+ connection = null;
182
+ childApi = null;
183
+ activeSignal = null;
184
+ origin;
185
+ onPopState = (e) => {
186
+ this.syncChildToHostUrl(e);
187
+ };
188
+ constructor(baseUrl, events, authBridge, locationInfo) {
189
+ this.baseUrl = baseUrl;
190
+ this.events = events;
191
+ this.authBridge = authBridge;
192
+ this.locationInfo = locationInfo;
193
+ this.origin = new URL(baseUrl).origin;
194
+ }
195
+ mount(container, bootstrapToken, signal) {
196
+ this.unmount();
197
+ this.activeSignal = signal ?? null;
198
+ const initialPath = this.resolveInitialPath();
199
+ const iframe = document.createElement("iframe");
200
+ iframe.src = this.buildIframeUrl(initialPath, bootstrapToken);
201
+ iframe.title = "WeTransform";
202
+ iframe.setAttribute("allow", "clipboard-read; clipboard-write");
203
+ iframe.setAttribute("tabindex", "0");
204
+ iframe.style.width = "100%";
205
+ iframe.style.height = "100%";
206
+ iframe.style.border = "0";
207
+ iframe.style.display = "block";
208
+ container.appendChild(iframe);
209
+ this.iframe = iframe;
210
+ window.addEventListener("popstate", this.onPopState);
211
+ this.connect(signal);
212
+ }
213
+ unmount() {
214
+ window.removeEventListener("popstate", this.onPopState);
215
+ this.activeSignal = null;
216
+ if (this.connection) {
217
+ this.connection.destroy();
218
+ this.connection = null;
219
+ this.childApi = null;
220
+ }
221
+ if (this.iframe?.parentElement) this.iframe.parentElement.removeChild(this.iframe);
222
+ this.iframe = null;
223
+ }
224
+ async requestLogout(signal) {
225
+ await this.notifyLogout(signal ?? this.activeSignal ?? void 0);
226
+ }
227
+ clearPersistedRoute() {
228
+ sessionStorage.removeItem(EMBED_ROUTE_STORAGE_KEY);
229
+ sessionStorage.removeItem(EMBED_ROUTE_PENDING_STORAGE_KEY);
230
+ }
231
+ persistRouteForPageLeave(event) {
232
+ if (event.persisted) return;
233
+ const currentRoute = this.getPersistedRoute();
234
+ if (!currentRoute) return;
235
+ sessionStorage.setItem(EMBED_ROUTE_PENDING_STORAGE_KEY, currentRoute);
236
+ sessionStorage.removeItem(EMBED_ROUTE_STORAGE_KEY);
237
+ }
238
+ connect(signal) {
239
+ if (!this.iframe) return;
240
+ if (!this.iframe.contentWindow) {
241
+ this.events.emit("error", {
242
+ code: "iframe_unavailable",
243
+ message: "Iframe contentWindow is not available."
244
+ });
245
+ return;
246
+ }
247
+ this.connection = connect({
248
+ messenger: new WindowMessenger({
249
+ remoteWindow: this.iframe.contentWindow,
250
+ allowedOrigins: [this.origin]
251
+ }),
252
+ methods: this.createParentMethods()
253
+ });
254
+ this.childApi = this.withAbort(this.connection.promise, signal).catch((error) => {
255
+ if (isAbortError(error)) return null;
256
+ throw error;
257
+ });
258
+ }
259
+ createParentMethods() {
260
+ return {
261
+ onReady: async () => {
262
+ this.events.emit("onReady");
263
+ try {
264
+ await this.sendAuthToChild(void 0, this.activeSignal ?? void 0);
265
+ } catch (error) {
266
+ if (!isAbortError(error)) throw error;
267
+ }
268
+ },
269
+ needRefresh: async () => {
270
+ const accessToken = await this.authBridge.refreshAccessToken(this.activeSignal ?? void 0);
271
+ if (!accessToken) return false;
272
+ await this.sendAuthToChild(accessToken, this.activeSignal ?? void 0);
273
+ return true;
274
+ },
275
+ currentUrl: (payload) => {
276
+ window.history.pushState({ path: payload.url }, "", "");
277
+ this.setPersistedRoute(payload.url);
278
+ },
279
+ successSubmit: (payload) => {
280
+ this.clearPersistedRoute();
281
+ this.events.emit("successSubmit", payload);
282
+ },
283
+ error: (payload) => {
284
+ this.events.emit("error", payload);
285
+ }
286
+ };
287
+ }
288
+ async sendAuthToChild(overrideToken, signal) {
289
+ const child = this.childApi ? await this.withAbort(this.childApi, signal) : null;
290
+ if (!child) return;
291
+ const accessToken = overrideToken ?? await this.authBridge.getAccessToken(signal ?? this.activeSignal ?? void 0);
292
+ if (!accessToken) return;
293
+ await this.withAbort(child.setAuth({ access_token: accessToken }), signal);
294
+ }
295
+ async notifyLogout(signal) {
296
+ const logout = (this.childApi ? await this.withAbort(this.childApi, signal) : null)?.logout;
297
+ if (!logout) return;
298
+ try {
299
+ await this.withAbort(logout(), signal);
300
+ } catch {}
301
+ }
302
+ async syncChildToHostUrl(e) {
303
+ const signal = this.activeSignal ?? void 0;
304
+ let child = null;
305
+ try {
306
+ child = this.childApi ? await this.withAbort(this.childApi, signal) : null;
307
+ } catch (error) {
308
+ if (isAbortError(error)) return;
309
+ throw error;
310
+ }
311
+ const navigate = child?.navigate;
312
+ if (!navigate) return;
313
+ const url = e.state?.path;
314
+ if (typeof url !== "string") return;
315
+ try {
316
+ await this.withAbort(navigate({ route: url }), signal);
317
+ } catch {}
318
+ }
319
+ withAbort(promise, signal) {
320
+ if (!signal) return promise;
321
+ if (signal.aborted) return Promise.reject(createAbortError());
322
+ return new Promise((resolve, reject) => {
323
+ const onAbort = () => {
324
+ signal.removeEventListener("abort", onAbort);
325
+ reject(createAbortError());
326
+ };
327
+ signal.addEventListener("abort", onAbort, { once: true });
328
+ promise.then((value) => {
329
+ signal.removeEventListener("abort", onAbort);
330
+ resolve(value);
331
+ }).catch((error) => {
332
+ signal.removeEventListener("abort", onAbort);
333
+ reject(error);
334
+ });
335
+ });
336
+ }
337
+ buildIframeUrl(path, bootstrapToken) {
338
+ const url = new URL(path, this.baseUrl);
339
+ url.searchParams.set("sdk", "true");
340
+ if (bootstrapToken) url.searchParams.set("token", bootstrapToken);
341
+ return url.toString();
342
+ }
343
+ resolveInitialPath() {
344
+ const persistedRoute = this.getPersistedRoute();
345
+ if (persistedRoute) return persistedRoute;
346
+ const pendingRoute = sessionStorage.getItem(EMBED_ROUTE_PENDING_STORAGE_KEY);
347
+ if (!pendingRoute) return getInitialLocationPath(this.locationInfo.initialLocation, this.locationInfo.templateHandle, this.locationInfo.sourceId);
348
+ if (this.isReloadNavigation()) {
349
+ this.setPersistedRoute(pendingRoute);
350
+ sessionStorage.removeItem(EMBED_ROUTE_PENDING_STORAGE_KEY);
351
+ return pendingRoute;
352
+ }
353
+ sessionStorage.removeItem(EMBED_ROUTE_PENDING_STORAGE_KEY);
354
+ return getInitialLocationPath(this.locationInfo.initialLocation, this.locationInfo.templateHandle, this.locationInfo.sourceId);
355
+ }
356
+ getPersistedRoute() {
357
+ return sessionStorage.getItem(EMBED_ROUTE_STORAGE_KEY);
358
+ }
359
+ setPersistedRoute(route) {
360
+ sessionStorage.setItem(EMBED_ROUTE_STORAGE_KEY, route);
361
+ sessionStorage.removeItem(EMBED_ROUTE_PENDING_STORAGE_KEY);
362
+ }
363
+ isReloadNavigation() {
364
+ const [entry] = performance.getEntriesByType("navigation");
365
+ return entry instanceof PerformanceNavigationTiming ? entry.type === "reload" : false;
366
+ }
367
+ };
368
+ //#endregion
369
+ //#region src/ui/ModalHost.ts
370
+ var ModalHost = class {
371
+ host = null;
372
+ shadowRoot = null;
373
+ iframeContainer = null;
374
+ focusTrap = null;
375
+ inertSnapshot = [];
376
+ previousOverflow = null;
377
+ constructor(onCloseRequest) {
378
+ this.onCloseRequest = onCloseRequest;
379
+ }
380
+ open() {
381
+ if (this.host && this.iframeContainer) return this.iframeContainer;
382
+ this.host = document.createElement("div");
383
+ this.host.setAttribute("data-WeTransform-sdk", "");
384
+ this.shadowRoot = this.host.attachShadow({ mode: "open" });
385
+ const style = document.createElement("style");
386
+ style.textContent = this.getStyles();
387
+ const backdrop = document.createElement("div");
388
+ backdrop.className = "sdk-backdrop";
389
+ const shell = document.createElement("div");
390
+ shell.className = "sdk-shell";
391
+ const dialog = document.createElement("div");
392
+ dialog.className = "sdk-dialog";
393
+ dialog.setAttribute("role", "dialog");
394
+ dialog.setAttribute("aria-modal", "true");
395
+ dialog.setAttribute("aria-label", "WeTransform");
396
+ dialog.tabIndex = -1;
397
+ const closeBtn = document.createElement("button");
398
+ closeBtn.className = "sdk-close-btn";
399
+ closeBtn.setAttribute("aria-label", "Close");
400
+ closeBtn.type = "button";
401
+ closeBtn.innerHTML = "&times;";
402
+ closeBtn.addEventListener("click", (e) => {
403
+ e.stopPropagation();
404
+ this.onCloseRequest();
405
+ });
406
+ const frameContainer = document.createElement("div");
407
+ frameContainer.className = "sdk-frame-container";
408
+ dialog.appendChild(frameContainer);
409
+ shell.appendChild(dialog);
410
+ shell.appendChild(closeBtn);
411
+ backdrop.appendChild(shell);
412
+ this.shadowRoot.appendChild(style);
413
+ this.shadowRoot.appendChild(backdrop);
414
+ document.body.appendChild(this.host);
415
+ this.iframeContainer = frameContainer;
416
+ this.lockPage();
417
+ this.focusTrap = createFocusTrap(shell, {
418
+ escapeDeactivates: false,
419
+ allowOutsideClick: true,
420
+ fallbackFocus: dialog
421
+ });
422
+ this.focusTrap.activate();
423
+ dialog.focus();
424
+ return frameContainer;
425
+ }
426
+ close() {
427
+ this.focusTrap?.deactivate();
428
+ this.focusTrap = null;
429
+ this.unlockPage();
430
+ if (this.host?.parentElement) this.host.parentElement.removeChild(this.host);
431
+ this.host = null;
432
+ this.shadowRoot = null;
433
+ this.iframeContainer = null;
434
+ }
435
+ lockPage() {
436
+ const bodyChildren = Array.from(document.body.children);
437
+ this.inertSnapshot = [];
438
+ for (const child of bodyChildren) {
439
+ if (child === this.host) continue;
440
+ const element = child;
441
+ const inertElement = this.getInertCapableElement(element);
442
+ this.inertSnapshot.push({
443
+ element,
444
+ ariaHidden: element.getAttribute("aria-hidden"),
445
+ inert: inertElement ? Boolean(inertElement.inert) : false
446
+ });
447
+ element.setAttribute("aria-hidden", "true");
448
+ if (inertElement) inertElement.inert = true;
449
+ }
450
+ this.previousOverflow = document.documentElement.style.overflow || null;
451
+ document.documentElement.style.overflow = "hidden";
452
+ }
453
+ unlockPage() {
454
+ for (const snapshot of this.inertSnapshot) {
455
+ if (snapshot.ariaHidden === null) snapshot.element.removeAttribute("aria-hidden");
456
+ else snapshot.element.setAttribute("aria-hidden", snapshot.ariaHidden);
457
+ const inertElement = this.getInertCapableElement(snapshot.element);
458
+ if (inertElement) inertElement.inert = snapshot.inert;
459
+ }
460
+ this.inertSnapshot = [];
461
+ if (this.previousOverflow === null) document.documentElement.style.removeProperty("overflow");
462
+ else document.documentElement.style.overflow = this.previousOverflow;
463
+ this.previousOverflow = null;
464
+ }
465
+ getInertCapableElement(element) {
466
+ return "inert" in element ? element : null;
467
+ }
468
+ getStyles() {
469
+ return `
470
+ :host {
471
+ all: initial;
472
+ }
473
+ .sdk-backdrop {
474
+ position: fixed;
475
+ inset: 0;
476
+ background: rgba(0, 0, 0, 0.5);
477
+ display: flex;
478
+ align-items: center;
479
+ justify-content: center;
480
+ z-index: 2147483647;
481
+ }
482
+ .sdk-dialog {
483
+ width: 98vw;
484
+ height: 98vh;
485
+ overflow: hidden;
486
+ background: #ffffff;
487
+ border-radius: 12px;
488
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
489
+ display: flex;
490
+ flex-direction: column;
491
+ }
492
+ .sdk-shell {
493
+ position: relative;
494
+ }
495
+ .sdk-close-btn {
496
+ position: absolute;
497
+ top: -8px;
498
+ right: -8px;
499
+ width: 20px;
500
+ height: 20px;
501
+ padding: 0;
502
+ border: none;
503
+ background: rgb(0,0,0);
504
+ border-radius: 100%;
505
+ display: flex;
506
+ align-items: center;
507
+ justify-content: center;
508
+ font-size: 20px;
509
+ line-height: 1;
510
+ cursor: pointer;
511
+ color: #fff;
512
+ box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.5);
513
+ }
514
+ .sdk-close-btn:hover {
515
+ background: rgba(0,0,0,0.6);
516
+ }
517
+ .sdk-frame-container {
518
+ flex: 1;
519
+ min-height: 0;
520
+ }
521
+ `;
522
+ }
523
+ };
524
+ //#endregion
525
+ //#region src/ui/UiHost.ts
526
+ const DEFAULT_IFRAME_CONTAINER_STYLE_ID = "wetransform-sdk-inline-container-defaults";
527
+ const OPENING_LOADER_ATTRIBUTE = "data-sender-sdk-loader";
528
+ const OPENING_LOADER_FADE_OUT_DURATION_MS = 400;
529
+ const OPENING_LOADER_SVG = "<svg class=\"wetransform-iframeLoader\" width=\"46px\" height=\"46px\" viewBox=\"25 25 50 50\" style=\"animation: wetransform-spin-animation 2s linear infinite; transform-origin: center center;\" ><circle cx=\"50\" cy=\"50\" r=\"20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"5\" stroke-miterlimit=\"10\" style=\"stroke-dasharray: 1,200; stroke-dashoffset: 0; animation: wetransform-spin-dash 1.5s ease-in-out infinite;\" ></circle></svg>";
530
+ function ensureDefaultInlineContainerStyles() {
531
+ if (document.getElementById(DEFAULT_IFRAME_CONTAINER_STYLE_ID)) return;
532
+ const style = document.createElement("style");
533
+ style.id = DEFAULT_IFRAME_CONTAINER_STYLE_ID;
534
+ style.textContent = `
535
+ :where(#weTransform_iframeContainer) {
536
+ width: 1080px;
537
+ height: 720px;
538
+ border: 0;
539
+ position: relative;
540
+ }
541
+
542
+ :where(.wetransform-iframeLoader) {
543
+ color: var(--wetransform-primary-color, #008df7);
544
+ }
545
+
546
+ @keyframes wetransform-spin-animation {
547
+ 0% {
548
+ transform:rotate3d(0,0,1,0);
549
+ }
550
+ 25% {
551
+ transform:rotate3d(0,0,1,90deg);
552
+ }
553
+ 50% {
554
+ transform:rotate3d(0,0,1,180deg);
555
+ }
556
+ 75% {
557
+ transform:rotate3d(0,0,1,270deg);
558
+ }
559
+ to {
560
+ transform:rotate3d(0,0,1,359deg);
561
+ }
562
+ }
563
+
564
+ @keyframes wetransform-spin-dash {
565
+ 0% {
566
+ stroke-dasharray:1,200;
567
+ stroke-dashoffset:0
568
+ }
569
+ 50% {
570
+ stroke-dasharray:89,200;
571
+ stroke-dashoffset:-35px
572
+ }
573
+ to {
574
+ stroke-dasharray:89,200;
575
+ stroke-dashoffset:-124px
576
+ }
577
+ }`;
578
+ document.head.prepend(style);
579
+ }
580
+ var UiHost = class {
581
+ modalHost = null;
582
+ inlineContainer = null;
583
+ ownsInlineContainer = false;
584
+ openingContainer = null;
585
+ clearOpeningLoader = null;
586
+ constructor(config) {
587
+ this.config = config;
588
+ }
589
+ open(onCloseRequest) {
590
+ if (this.config.displayAsModal) {
591
+ this.modalHost ??= new ModalHost(onCloseRequest);
592
+ return this.modalHost.open();
593
+ }
594
+ return this.ensureInlineContainer();
595
+ }
596
+ startOpening(onCloseRequest) {
597
+ const container = this.open(onCloseRequest);
598
+ this.openingContainer = container;
599
+ this.clearOpeningLoader = this.showOpeningLoader(container);
600
+ return container;
601
+ }
602
+ finishOpening() {
603
+ this.clearOpeningLoader?.();
604
+ this.clearOpeningLoader = null;
605
+ this.openingContainer = null;
606
+ }
607
+ hasOpeningUi() {
608
+ return this.openingContainer !== null;
609
+ }
610
+ cancelOpening() {
611
+ if (!this.openingContainer) return;
612
+ this.finishOpening();
613
+ this.close();
614
+ }
615
+ close() {
616
+ this.finishOpening();
617
+ if (this.config.displayAsModal) {
618
+ this.modalHost?.close();
619
+ return;
620
+ }
621
+ this.clearInlineContainer();
622
+ }
623
+ dispose() {
624
+ this.finishOpening();
625
+ this.modalHost = null;
626
+ this.inlineContainer = null;
627
+ this.ownsInlineContainer = false;
628
+ this.openingContainer = null;
629
+ this.clearOpeningLoader = null;
630
+ }
631
+ showOpeningLoader(container) {
632
+ const existingLoader = container.querySelector(`[${OPENING_LOADER_ATTRIBUTE}]`);
633
+ if (existingLoader?.parentElement) existingLoader.parentElement.removeChild(existingLoader);
634
+ const loader = document.createElement("div");
635
+ loader.setAttribute(OPENING_LOADER_ATTRIBUTE, "");
636
+ loader.setAttribute("role", "status");
637
+ loader.setAttribute("aria-live", "polite");
638
+ loader.setAttribute("aria-label", "Loading");
639
+ loader.innerHTML = OPENING_LOADER_SVG;
640
+ loader.style.display = "flex";
641
+ loader.style.alignItems = "center";
642
+ loader.style.justifyContent = "center";
643
+ loader.style.width = "100%";
644
+ loader.style.height = "100%";
645
+ loader.style.minHeight = "120px";
646
+ loader.style.color = "#1f2937";
647
+ loader.style.fontFamily = "system-ui, -apple-system, Segoe UI, sans-serif";
648
+ loader.style.fontSize = "14px";
649
+ loader.style.backgroundColor = "#fff";
650
+ loader.style.position = "absolute";
651
+ loader.style.top = "0";
652
+ loader.style.left = "0";
653
+ loader.style.opacity = "1";
654
+ loader.style.transition = `opacity ${OPENING_LOADER_FADE_OUT_DURATION_MS}ms`;
655
+ container.appendChild(loader);
656
+ let isCleared = false;
657
+ let fallbackTimer = null;
658
+ return () => {
659
+ if (isCleared) return;
660
+ isCleared = true;
661
+ const removeLoader = () => {
662
+ loader.removeEventListener("transitionend", removeLoader);
663
+ if (fallbackTimer !== null) {
664
+ window.clearTimeout(fallbackTimer);
665
+ fallbackTimer = null;
666
+ }
667
+ if (loader.parentElement) loader.parentElement.removeChild(loader);
668
+ };
669
+ loader.addEventListener("transitionend", removeLoader);
670
+ fallbackTimer = window.setTimeout(removeLoader, OPENING_LOADER_FADE_OUT_DURATION_MS + 50);
671
+ loader.style.opacity = "0";
672
+ };
673
+ }
674
+ ensureInlineContainer() {
675
+ if (this.inlineContainer) return this.inlineContainer;
676
+ if (this.config.mountElement instanceof HTMLElement) {
677
+ this.inlineContainer = this.config.mountElement;
678
+ this.ownsInlineContainer = false;
679
+ return this.config.mountElement;
680
+ }
681
+ const existingMountElement = document.getElementById(this.config.mountElement);
682
+ if (existingMountElement) {
683
+ this.inlineContainer = existingMountElement;
684
+ this.ownsInlineContainer = false;
685
+ return existingMountElement;
686
+ }
687
+ const container = document.createElement("div");
688
+ container.id = this.config.mountElement;
689
+ document.body.appendChild(container);
690
+ this.inlineContainer = container;
691
+ this.ownsInlineContainer = true;
692
+ return container;
693
+ }
694
+ clearInlineContainer() {
695
+ if (this.ownsInlineContainer && this.inlineContainer?.parentElement) this.inlineContainer.parentElement.removeChild(this.inlineContainer);
696
+ this.inlineContainer = null;
697
+ this.ownsInlineContainer = false;
698
+ }
699
+ };
700
+ //#endregion
701
+ //#region src/index.ts
702
+ function resolveBaseUrl(organizationHandle) {
703
+ const envBaseUrl = import.meta.env.VITE_SENDER_BASE_URL;
704
+ if (envBaseUrl) return envBaseUrl.replace("{organizationHandle}", organizationHandle);
705
+ return window.location.origin;
706
+ }
707
+ function resolveApiUrl(organizationHandle, locale) {
708
+ const envApiUrl = import.meta.env.VITE_SENDER_API_URL;
709
+ if (!envApiUrl) throw new Error("SENDER_API_URL environment variable is not defined.");
710
+ return `${envApiUrl}/${locale ?? "en"}/send/${organizationHandle}`;
711
+ }
712
+ function createWeTransform(config) {
713
+ if (typeof window === "undefined") throw new Error("WeTransform SDK must run in a browser context.");
714
+ ensureDefaultInlineContainerStyles();
715
+ const baseUrl = resolveBaseUrl(config.organizationHandle);
716
+ const apiUrl = resolveApiUrl(config.organizationHandle, config.locale);
717
+ const events = new EventEmitter();
718
+ const uiHost = new UiHost({
719
+ displayAsModal: config.displayAsModal ?? true,
720
+ mountElement: config.mountElement ?? "weTransform_iframeContainer"
721
+ });
722
+ const authBridge = new AuthBridge(apiUrl, config.authentication, (payload) => {
723
+ events.emit("error", payload);
724
+ });
725
+ const iframeManager = new IframeManager(baseUrl, events, authBridge, {
726
+ initialLocation: config.initialLocation,
727
+ templateHandle: config.authentication.templateHandle,
728
+ sourceId: config.authentication.sourceId
729
+ });
730
+ const revokeOnPageUnload = () => {
731
+ authBridge.revokeSession({ bestEffort: true });
732
+ };
733
+ const onPageHide = (event) => {
734
+ iframeManager.persistRouteForPageLeave(event);
735
+ revokeOnPageUnload();
736
+ };
737
+ window.addEventListener("pagehide", onPageHide);
738
+ window.addEventListener("beforeunload", revokeOnPageUnload);
739
+ let status = "idle";
740
+ let openingPromise = null;
741
+ let currentOpenAbortController = null;
742
+ let stopWaitingForReady = null;
743
+ const open = async () => {
744
+ if (status !== "idle") return;
745
+ status = "opening";
746
+ const container = uiHost.startOpening(close);
747
+ events.emit("open");
748
+ const openAbortController = new AbortController();
749
+ currentOpenAbortController = openAbortController;
750
+ const onReadyPromise = new Promise((resolve) => {
751
+ const unsubscribeOnReady = events.on("onReady", () => {
752
+ unsubscribeOnReady();
753
+ stopWaitingForReady = null;
754
+ resolve();
755
+ });
756
+ const onAbort = () => {
757
+ unsubscribeOnReady();
758
+ stopWaitingForReady = null;
759
+ resolve();
760
+ };
761
+ stopWaitingForReady = () => {
762
+ openAbortController.signal.removeEventListener("abort", onAbort);
763
+ onAbort();
764
+ };
765
+ openAbortController.signal.addEventListener("abort", onAbort, { once: true });
766
+ });
767
+ openingPromise = (async () => {
768
+ try {
769
+ const accessToken = await authBridge.getAccessToken(openAbortController.signal);
770
+ if (!accessToken) return;
771
+ throwIfAborted(openAbortController.signal);
772
+ iframeManager.mount(container, accessToken, openAbortController.signal);
773
+ await onReadyPromise;
774
+ throwIfAborted(openAbortController.signal);
775
+ if (status !== "opening") return;
776
+ uiHost.finishOpening();
777
+ status = "open";
778
+ } catch (error) {
779
+ stopWaitingForReady?.();
780
+ if (isAbortError(error)) return;
781
+ events.emit("error", {
782
+ code: "open_failed",
783
+ message: error instanceof Error ? error.message : "Failed to open WeTransform SDK."
784
+ });
785
+ }
786
+ })();
787
+ try {
788
+ await openingPromise;
789
+ } finally {
790
+ const shouldAutoClose = status === "opening" && uiHost.hasOpeningUi();
791
+ if (currentOpenAbortController === openAbortController) currentOpenAbortController = null;
792
+ if (shouldAutoClose) {
793
+ status = "idle";
794
+ uiHost.cancelOpening();
795
+ events.emit("close");
796
+ }
797
+ openingPromise = null;
798
+ }
799
+ };
800
+ const close = async () => {
801
+ if (status === "destroyed" || status === "idle" || status === "closing") return;
802
+ const wasOpen = status === "open";
803
+ const wasOpening = status === "opening";
804
+ status = "closing";
805
+ currentOpenAbortController?.abort();
806
+ if (wasOpening) {
807
+ stopWaitingForReady?.();
808
+ await iframeManager.requestLogout();
809
+ iframeManager.unmount();
810
+ iframeManager.clearPersistedRoute();
811
+ uiHost.cancelOpening();
812
+ status = "idle";
813
+ events.emit("close");
814
+ return;
815
+ }
816
+ if (wasOpen) {
817
+ await iframeManager.requestLogout();
818
+ iframeManager.unmount();
819
+ uiHost.close();
820
+ iframeManager.clearPersistedRoute();
821
+ status = "idle";
822
+ events.emit("close");
823
+ }
824
+ };
825
+ const destroy = async () => {
826
+ if (status === "destroyed") return;
827
+ currentOpenAbortController?.abort();
828
+ await close();
829
+ uiHost.dispose();
830
+ authBridge.revokeSession().finally(() => {
831
+ authBridge.clear();
832
+ });
833
+ window.removeEventListener("pagehide", onPageHide);
834
+ window.removeEventListener("beforeunload", revokeOnPageUnload);
835
+ events.emit("destroy");
836
+ events.clear();
837
+ status = "destroyed";
838
+ };
839
+ const on = (event, handler) => {
840
+ return events.on(event, handler);
841
+ };
842
+ return {
843
+ open,
844
+ close,
845
+ destroy,
846
+ on
847
+ };
848
+ }
849
+ //#endregion
850
+ export { createWeTransform };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@wetransform/core",
3
+ "version": "0.1.0",
4
+ "license": "ISC",
5
+ "files": [
6
+ "dist/**"
7
+ ],
8
+ "type": "module",
9
+ "exports": {
10
+ ".": "./dist/index.mjs"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "scripts": {
16
+ "build": "vp pack",
17
+ "dev": "vp pack --watch",
18
+ "lint": "vp lint --type-aware --type-check .",
19
+ "test": "vp test run"
20
+ },
21
+ "dependencies": {
22
+ "focus-trap": "^8.0.1",
23
+ "penpal": "^7.0.6"
24
+ },
25
+ "devDependencies": {
26
+ "jsdom": "^29.0.1",
27
+ "typescript": "catalog:",
28
+ "vite": "catalog:",
29
+ "vite-plugin-css-injected-by-js": "^4.0.1",
30
+ "vite-plus": "catalog:",
31
+ "vitest": "catalog:"
32
+ }
33
+ }