@morefin/cashier-bootstrapper 0.3.5 → 0.3.7

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 CHANGED
@@ -100,6 +100,33 @@ new CashierBootstrapper('#cashier-root', {
100
100
  });
101
101
  ```
102
102
 
103
+ The bootstrapper calls `onReady` after the cashier iframe has registered its runtime message listener. If the cashier build does not support that ready signal, the bootstrapper falls back to iframe load after 3 seconds to preserve existing integrations.
104
+
105
+ For asynchronous merchant configuration, return or await the async work from `onReady`. The cashier stays in its host-configuration loading state until the returned promise completes.
106
+
107
+ ```typescript
108
+ import { CashierBootstrapper } from '@morefin/cashier-bootstrapper';
109
+
110
+ new CashierBootstrapper('#cashier-root', {
111
+ properties: { environment: CASHIER_ENVIRONMENT }
112
+ }, async api => {
113
+ const css = await fetch('/merchant-cashier-theme.css').then(response => response.text());
114
+ api.setCss(css);
115
+ });
116
+ ```
117
+
118
+ Avoid starting async setup without returning it. In this example, the bootstrapper cannot wait for the timer, so the cashier may become visible before the CSS is applied:
119
+
120
+ ```typescript
121
+ new CashierBootstrapper('#cashier-root', {
122
+ properties: { environment: CASHIER_ENVIRONMENT }
123
+ }, api => {
124
+ setTimeout(() => {
125
+ api.setCss(css);
126
+ }, 1000);
127
+ });
128
+ ```
129
+
103
130
  ### Transaction Result Callbacks
104
131
 
105
132
  ```typescript
@@ -140,11 +167,13 @@ Iframe-based embed that loads the cashier URL and exposes runtime controls once
140
167
  - `container: string | HTMLElement | null | undefined` - Where the iframe is appended. If omitted, `config.properties.container` is used (falling back to `document.body`).
141
168
  - `config?: CashierIframeConfig` - Request params and iframe properties. `properties.environment` is required.
142
169
  - `config.callbacks?: CashierCallbacks` - Host callback handlers for cashier result events.
143
- - `onReady?: (api: CashierIframeApi) => void` - Called when the iframe loads; exposes helpers:
170
+ - `onReady?: (api: CashierIframeApi) => void | Promise<void>` - Called when the cashier iframe is ready for runtime messages; exposes helpers:
144
171
  - `api.setCss(css: string)` – inject CSS inside the cashier iframe
145
172
  - `api.updateData(data: object)` – post updated `APP_DATA` to the cashier
146
173
  - `api.pause()` / `api.resume()` – forward pause/resume signals
147
174
 
175
+ If `onReady` returns a promise, the bootstrapper waits for it before notifying the cashier that host configuration is completed. Return or await asynchronous CSS/data setup so the cashier does not render before merchant configuration is applied.
176
+
148
177
  ## Types
149
178
 
150
179
  ```typescript
package/dist/index.d.ts CHANGED
@@ -18,6 +18,10 @@ export declare class CashierBootstrapper {
18
18
  private redirectOverlayPanel?;
19
19
  private redirectOverlayIframe?;
20
20
  private overlayPositionSyncHandler?;
21
+ private managedRedirectTab?;
22
+ private managedRedirectTabTransactionId?;
23
+ private managedRedirectTabCheckIntervalId?;
24
+ private iframeLoadFallbackTimeoutId?;
21
25
  private origin;
22
26
  private ready;
23
27
  private readonly fullConfig;
@@ -26,10 +30,12 @@ export declare class CashierBootstrapper {
26
30
  private readonly onReady?;
27
31
  private readonly callbacks?;
28
32
  private readonly messageListener;
29
- constructor(container: string | HTMLElement | null | undefined, config?: CashierIframeConfig, onReady?: (api: CashierIframeApi) => void);
33
+ constructor(container: string | HTMLElement | null | undefined, config?: CashierIframeConfig, onReady?: (api: CashierIframeApi) => void | Promise<void>);
30
34
  private bootstrapIframe;
31
35
  private createIframeShell;
32
36
  private handleLoad;
37
+ private completeReady;
38
+ private runHostReadyCallback;
33
39
  private postMessage;
34
40
  private handleIframeMessage;
35
41
  private handleCashierResultEvent;
@@ -41,6 +47,10 @@ export declare class CashierBootstrapper {
41
47
  private normalizeRedirectTarget;
42
48
  private normalizeRedirectMethod;
43
49
  private normalizeRedirectParameters;
50
+ private openManagedRedirectTab;
51
+ private openManagedRedirectTabWithPost;
52
+ private trackManagedRedirectTab;
53
+ private clearManagedRedirectTabWatcher;
44
54
  private openPopupWithPost;
45
55
  private writePostFormToWindow;
46
56
  private submitPostForm;
package/dist/index.js CHANGED
@@ -2,6 +2,9 @@ const DEFAULT_IFRAME_ALLOW = 'geolocation *;camera *;payment *;clipboard-read *;
2
2
  const CASHIER_PATH = '/cashier';
3
3
  const FINGERPRINT_CDN = 'https://cdn.jsdelivr.net/npm/@fingerprintjs/fingerprintjs@4/dist/fp.min.js';
4
4
  const CASHIER_REDIRECT_EVENT = 'CASHIER_REDIRECT';
5
+ const CASHIER_REDIRECT_TAB_CLOSED_EVENT = 'CASHIER_REDIRECT_TAB_CLOSED';
6
+ const CASHIER_READY_EVENT = 'CASHIER_READY';
7
+ const CASHIER_HOST_CONFIG_DONE_EVENT = 'CASHIER_HOST_CONFIG_DONE';
5
8
  const TOP_URL_REPLACE_EVENT = 'TOP_URL_REPLACE';
6
9
  const CASHIER_IFRAME_OVERLAY_CLOSE_EVENT = 'CASHIER_IFRAME_OVERLAY_CLOSE';
7
10
  const CASHIER_RESULT_EVENT = 'CASHIER_RESULT';
@@ -212,8 +215,8 @@ export class CashierBootstrapper {
212
215
  const src = buildIframeUrl(this.host, this.fullConfig.requestParams, this.allowedHostnames, fp);
213
216
  this.origin = getOriginFromUrl(src, this.allowedHostnames);
214
217
  if (this.iframe) {
215
- this.iframe.src = src;
216
218
  this.iframe.onload = () => this.handleLoad();
219
+ this.iframe.src = src;
217
220
  }
218
221
  }
219
222
  createIframeShell(container) {
@@ -240,11 +243,33 @@ export class CashierBootstrapper {
240
243
  return iframe;
241
244
  }
242
245
  handleLoad() {
246
+ if (this.ready)
247
+ return;
248
+ if (this.iframeLoadFallbackTimeoutId !== undefined)
249
+ return;
250
+ this.iframeLoadFallbackTimeoutId = setTimeout(() => this.completeReady(), 3000);
251
+ }
252
+ completeReady() {
243
253
  if (this.ready)
244
254
  return;
245
255
  this.ready = true;
246
- if (this.onReady) {
247
- this.onReady(this.api());
256
+ if (this.iframeLoadFallbackTimeoutId !== undefined) {
257
+ clearTimeout(this.iframeLoadFallbackTimeoutId);
258
+ this.iframeLoadFallbackTimeoutId = undefined;
259
+ }
260
+ void this.runHostReadyCallback();
261
+ }
262
+ async runHostReadyCallback() {
263
+ try {
264
+ if (this.onReady) {
265
+ await this.onReady(this.api());
266
+ }
267
+ }
268
+ catch (err) {
269
+ console.error('Host ready callback threw an error.', err);
270
+ }
271
+ finally {
272
+ this.postMessage(CASHIER_HOST_CONFIG_DONE_EVENT, {});
248
273
  }
249
274
  }
250
275
  postMessage(eventType, payload) {
@@ -266,6 +291,12 @@ export class CashierBootstrapper {
266
291
  return;
267
292
  }
268
293
  const { eventType, payload } = event.data;
294
+ if (eventType === CASHIER_READY_EVENT) {
295
+ if (sourceIsMainCashierIframe) {
296
+ this.completeReady();
297
+ }
298
+ return;
299
+ }
269
300
  if (eventType === CASHIER_IFRAME_OVERLAY_CLOSE_EVENT) {
270
301
  if (sourceIsOverlayIframe) {
271
302
  this.closeProviderRedirectOverlay();
@@ -335,6 +366,7 @@ export class CashierBootstrapper {
335
366
  const redirectTarget = this.normalizeRedirectTarget(redirectPayload.target);
336
367
  const redirectMethod = this.normalizeRedirectMethod(redirectPayload.method);
337
368
  const redirectParameters = this.normalizeRedirectParameters(redirectPayload.parameters);
369
+ const transactionId = this.asString(redirectPayload.transactionId);
338
370
  if (typeof redirectUrl !== 'string' || redirectUrl.trim() === '') {
339
371
  return;
340
372
  }
@@ -354,12 +386,12 @@ export class CashierBootstrapper {
354
386
  case 'tab':
355
387
  console.log('[CashierBootstrapper] Handling redirect target "tab".', { redirectUrl, redirectMethod, redirectParameters });
356
388
  if (redirectMethod === 'POST') {
357
- if (!this.openPopupWithPost(redirectUrl, redirectParameters)) {
389
+ if (!this.openManagedRedirectTabWithPost(redirectUrl, redirectParameters, transactionId)) {
358
390
  this.submitPostForm(redirectUrl, redirectParameters, '_blank');
359
391
  }
360
392
  }
361
393
  else {
362
- window.open(redirectUrl, '_blank', 'noopener,noreferrer');
394
+ this.openManagedRedirectTab(redirectUrl, transactionId);
363
395
  }
364
396
  break;
365
397
  case 'window':
@@ -492,6 +524,55 @@ export class CashierBootstrapper {
492
524
  return acc;
493
525
  }, {});
494
526
  }
527
+ openManagedRedirectTab(url, transactionId) {
528
+ if (typeof window === 'undefined') {
529
+ return false;
530
+ }
531
+ const popup = window.open(url, '_blank');
532
+ if (!popup) {
533
+ return false;
534
+ }
535
+ this.trackManagedRedirectTab(popup, transactionId);
536
+ return true;
537
+ }
538
+ openManagedRedirectTabWithPost(url, parameters, transactionId) {
539
+ if (typeof window === 'undefined') {
540
+ return false;
541
+ }
542
+ const popup = window.open('', '_blank');
543
+ if (!popup) {
544
+ return false;
545
+ }
546
+ if (!this.writePostFormToWindow(popup, url, parameters)) {
547
+ return false;
548
+ }
549
+ this.trackManagedRedirectTab(popup, transactionId);
550
+ return true;
551
+ }
552
+ trackManagedRedirectTab(tab, transactionId) {
553
+ this.clearManagedRedirectTabWatcher();
554
+ this.managedRedirectTab = tab;
555
+ this.managedRedirectTabTransactionId = transactionId;
556
+ this.managedRedirectTabCheckIntervalId = setInterval(() => {
557
+ if (!this.managedRedirectTab?.closed) {
558
+ return;
559
+ }
560
+ const closedTransactionId = this.managedRedirectTabTransactionId;
561
+ this.managedRedirectTab = null;
562
+ this.managedRedirectTabTransactionId = undefined;
563
+ this.clearManagedRedirectTabWatcher();
564
+ this.postMessage(CASHIER_REDIRECT_TAB_CLOSED_EVENT, {
565
+ transactionId: closedTransactionId
566
+ });
567
+ }, 500);
568
+ }
569
+ clearManagedRedirectTabWatcher() {
570
+ if (this.managedRedirectTabCheckIntervalId === undefined) {
571
+ return;
572
+ }
573
+ clearInterval(this.managedRedirectTabCheckIntervalId);
574
+ this.managedRedirectTabCheckIntervalId = undefined;
575
+ }
495
576
  openPopupWithPost(url, parameters) {
496
577
  if (typeof window === 'undefined') {
497
578
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morefin/cashier-bootstrapper",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Bootstrap service for initializing cashier payment page data from API",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",