@morefin/cashier-bootstrapper 0.1.7 → 0.1.9

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
@@ -10,10 +10,10 @@ npm install @morefin/cashier-bootstrapper
10
10
 
11
11
  ## Usage
12
12
 
13
- Throughout the examples below, set a host variable once and reuse it:
13
+ Throughout the examples below, set an environment once and reuse it:
14
14
 
15
15
  ```typescript
16
- const CASHIER_HOST = 'http://uat-api.morefin.com'; // change per environment
16
+ const CASHIER_ENVIRONMENT = 'uat' as const; // 'production' | 'uat'
17
17
  ```
18
18
 
19
19
  ### Basic Embed
@@ -22,7 +22,7 @@ const CASHIER_HOST = 'http://uat-api.morefin.com'; // change per environment
22
22
  import { CashierBootstrapper } from '@morefin/cashier-bootstrapper';
23
23
 
24
24
  new CashierBootstrapper('#cashier-root', {
25
- properties: { host: CASHIER_HOST }
25
+ properties: { environment: CASHIER_ENVIRONMENT }
26
26
  });
27
27
  ```
28
28
 
@@ -39,11 +39,10 @@ new CashierBootstrapper('#cashier-root', {
39
39
  sessionId: 'session-abc',
40
40
  predefinedAmounts: [100, 200, 300],
41
41
  layout: 'default',
42
- transactionType: ('deosit'|'withdrawal')
42
+ transactionType: ('deposit'|'withdrawal')
43
43
  },
44
44
  properties: {
45
- host: CASHIER_HOST,
46
- cashierPath: '/cashier',
45
+ environment: CASHIER_ENVIRONMENT,
47
46
  iframe: {
48
47
  height: '720px',
49
48
  minHeight: '640px',
@@ -62,7 +61,7 @@ import { CashierBootstrapper } from '@morefin/cashier-bootstrapper';
62
61
 
63
62
  new CashierBootstrapper(null, {
64
63
  properties: {
65
- host: CASHIER_HOST,
64
+ environment: CASHIER_ENVIRONMENT,
66
65
  container: '#cashier-root'
67
66
  }
68
67
  });
@@ -74,7 +73,7 @@ new CashierBootstrapper(null, {
74
73
  import { CashierBootstrapper } from '@morefin/cashier-bootstrapper';
75
74
 
76
75
  new CashierBootstrapper('#cashier-root', {
77
- properties: { host: CASHIER_HOST }
76
+ properties: { environment: CASHIER_ENVIRONMENT }
78
77
  }, api => {
79
78
  api.setCss(`
80
79
  .payment-layout-root .payment-container {
@@ -86,9 +85,39 @@ new CashierBootstrapper('#cashier-root', {
86
85
  });
87
86
  ```
88
87
 
88
+ ### Transaction Result Callbacks
89
+
90
+ ```typescript
91
+ import { CashierBootstrapper } from '@morefin/cashier-bootstrapper';
92
+
93
+ new CashierBootstrapper('#cashier-root', {
94
+ properties: { environment: CASHIER_ENVIRONMENT },
95
+ callbacks: {
96
+ onSuccess: ({ status, transactionId }) => {
97
+ console.log('Cashier success callback', status, transactionId);
98
+ },
99
+ onFailure: ({ status, message, transactionId }) => {
100
+ console.log('Cashier failure callback', status, message, transactionId);
101
+ },
102
+ onCancel: ({ status, transactionId }) => {
103
+ console.log('Cashier cancel callback', status, transactionId);
104
+ },
105
+ onValidationFailed: ({ message }) => {
106
+ console.log('Cashier validation failed callback', message);
107
+ }
108
+ }
109
+ });
110
+ ```
111
+
112
+ Callback mapping:
113
+ - `onSuccess`: result status `APPROVED` or `DECLINED`
114
+ - `onFailure`: result status `INVALID` (or explicit cashier error event)
115
+ - `onCancel`: result status `CANCELLED`
116
+ - `onValidationFailed`: `/validate` request failed; `message` is forwarded from cashier
117
+
89
118
  ## Examples Folder
90
119
 
91
- - `examples/npm` contains a minimal app that installs the package from npm and bundles it with esbuild.
120
+ - Repository examples live in `../examples/cashier-bootstrapper` (outside this package directory), including `../examples/cashier-bootstrapper/npm`.
92
121
  - `src/example-usage.ts` contains additional snippets for reference.
93
122
 
94
123
  ## API
@@ -99,7 +128,8 @@ Iframe-based embed that loads the cashier URL and exposes runtime controls once
99
128
 
100
129
  **Parameters:**
101
130
  - `container: string | HTMLElement | null | undefined` - Where the iframe is appended. If omitted, `config.properties.container` is used (falling back to `document.body`).
102
- - `config?: CashierIframeConfig` - Request params and iframe properties. `properties.host` is required.
131
+ - `config?: CashierIframeConfig` - Request params and iframe properties. `properties.environment` is required.
132
+ - `config.callbacks?: CashierCallbacks` - Host callback handlers for cashier result events.
103
133
  - `onReady?: (api: CashierIframeApi) => void` - Called when the iframe loads; exposes helpers:
104
134
  - `api.setCss(css: string)` – inject CSS inside the cashier iframe
105
135
  - `api.updateData(data: object)` – post updated `APP_DATA` to the cashier
@@ -115,10 +145,11 @@ interface CashierRequestParams {
115
145
  sessionId?: string;
116
146
  predefinedAmounts?: number[];
117
147
  layout?: string;
118
- fingerprint?: string;
119
148
  transactionType?: string;
120
149
  }
121
150
 
151
+ type CashierEnvironment = 'production' | 'uat';
152
+
122
153
  interface CashierIframeOptions {
123
154
  width?: string;
124
155
  height?: string;
@@ -130,20 +161,31 @@ interface CashierIframeOptions {
130
161
  }
131
162
 
132
163
  interface CashierIframeProperties {
133
- host: string;
164
+ environment: CashierEnvironment;
134
165
  container?: string | HTMLElement;
135
- cashierPath?: string;
136
166
  iframe?: CashierIframeOptions;
137
167
  }
138
168
 
139
169
  interface CashierIframeConfig {
140
170
  requestParams?: CashierRequestParams;
141
171
  properties?: CashierIframeProperties;
172
+ callbacks?: CashierCallbacks;
142
173
  }
143
- ```
144
174
 
145
- ## Defaults
175
+ interface CashierResultCallbackPayload {
176
+ status?: string;
177
+ transactionId?: string;
178
+ message?: string;
179
+ }
146
180
 
147
- - `cashierPath`: `/cashier`
148
- - `iframe.width`/`iframe.height`: `100%`
149
- - `iframe.allow`: `geolocation *;camera *;payment *;clipboard-read *;clipboard-write *;autoplay *;microphone *;fullscreen *;accelerometer *;magnetometer *;gyroscope *;picture-in-picture *;otp-credentials *;`
181
+ interface CashierValidationFailedPayload {
182
+ message: string;
183
+ }
184
+
185
+ interface CashierCallbacks {
186
+ onSuccess?: (payload: CashierResultCallbackPayload) => void;
187
+ onFailure?: (payload: CashierResultCallbackPayload) => void;
188
+ onCancel?: (payload: CashierResultCallbackPayload) => void;
189
+ onValidationFailed?: (payload: CashierValidationFailedPayload) => void;
190
+ }
191
+ ```
@@ -3,11 +3,11 @@
3
3
  * This file is not included in the npm package, it's just for reference.
4
4
  */
5
5
  import { CashierBootstrapper } from './index';
6
- const CASHIER_HOST = 'http://localhost:7082'; // change per environment
6
+ const CASHIER_ENVIRONMENT = 'uat';
7
7
  // Example 1: Minimal configuration with selector
8
8
  export function example1() {
9
9
  new CashierBootstrapper('#cashier-root', {
10
- properties: { host: CASHIER_HOST }
10
+ properties: { environment: CASHIER_ENVIRONMENT }
11
11
  });
12
12
  }
13
13
  // Example 2: Custom request params + iframe options
@@ -22,8 +22,7 @@ export function example2(container) {
22
22
  layout: 'default'
23
23
  },
24
24
  properties: {
25
- host: CASHIER_HOST,
26
- cashierPath: '/cashier',
25
+ environment: CASHIER_ENVIRONMENT,
27
26
  iframe: {
28
27
  height: '720px',
29
28
  minHeight: '640px',
@@ -37,7 +36,7 @@ export function example2(container) {
37
36
  // Example 3: Provide container via config instead of constructor
38
37
  export function example3() {
39
38
  new CashierBootstrapper(null, {
40
- properties: { host: CASHIER_HOST, container: '#cashier-root' }
39
+ properties: { environment: CASHIER_ENVIRONMENT, container: '#cashier-root' }
41
40
  });
42
41
  }
43
42
  // Example 4: Update CSS at runtime after iframe is ready
@@ -51,7 +50,7 @@ export function example4(container) {
51
50
  layout: 'default'
52
51
  },
53
52
  properties: {
54
- host: CASHIER_HOST
53
+ environment: CASHIER_ENVIRONMENT
55
54
  }
56
55
  }, api => {
57
56
  api.setCss(`
@@ -64,7 +63,7 @@ export function example4(container) {
64
63
  // Example 5: Updating data via the postMessage API
65
64
  export function example5(container) {
66
65
  new CashierBootstrapper(container, {
67
- properties: { host: CASHIER_HOST }
66
+ properties: { environment: CASHIER_ENVIRONMENT }
68
67
  }, api => {
69
68
  api.updateData({
70
69
  userId: 'updated-user'
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CashierIframeApi, CashierIframeConfig } from './types';
2
- export type { CashierRequestParams, CashierIframeApi, CashierIframeConfig, CashierIframeProperties } from './types';
2
+ export type { CashierCallbacks, CashierEnvironment, CashierRequestParams, CashierIframeApi, CashierIframeConfig, CashierIframeProperties, CashierResultCallbackPayload, CashierValidationFailedPayload } from './types';
3
3
  type FingerprintJSGlobal = {
4
4
  load: () => Promise<{
5
5
  get: () => Promise<{
@@ -14,15 +14,34 @@ declare global {
14
14
  }
15
15
  export declare class CashierBootstrapper {
16
16
  private iframe?;
17
+ private redirectOverlayElement?;
18
+ private redirectOverlayPanel?;
19
+ private redirectOverlayIframe?;
20
+ private overlayPositionSyncHandler?;
17
21
  private origin;
18
22
  private ready;
19
23
  private readonly fullConfig;
24
+ private readonly host;
25
+ private readonly allowedHostnames;
20
26
  private readonly onReady?;
27
+ private readonly callbacks?;
28
+ private readonly messageListener;
21
29
  constructor(container: string | HTMLElement | null | undefined, config?: CashierIframeConfig, onReady?: (api: CashierIframeApi) => void);
22
30
  private bootstrapIframe;
23
31
  private createIframeShell;
24
32
  private handleLoad;
25
33
  private postMessage;
34
+ private handleIframeMessage;
35
+ private handleCashierResultEvent;
36
+ private handleCashierValidationFailedEvent;
37
+ private handleCashierRedirect;
38
+ private openProviderRedirectOverlay;
39
+ private closeProviderRedirectOverlay;
40
+ private syncProviderRedirectOverlayToCashierIframe;
41
+ private normalizeRedirectTarget;
42
+ private normalizeResultStatus;
43
+ private asString;
44
+ private invokeCallback;
26
45
  /**
27
46
  * API exposed to host pages for runtime control.
28
47
  */
package/dist/index.js CHANGED
@@ -1,5 +1,29 @@
1
1
  const DEFAULT_IFRAME_ALLOW = 'geolocation *;camera *;payment *;clipboard-read *;clipboard-write *;autoplay *;microphone *;fullscreen *;accelerometer *;magnetometer *;gyroscope *;picture-in-picture *;otp-credentials *;';
2
+ const CASHIER_PATH = '/cashier';
2
3
  const FINGERPRINT_CDN = 'https://cdn.jsdelivr.net/npm/@fingerprintjs/fingerprintjs@4/dist/fp.min.js';
4
+ const CASHIER_REDIRECT_EVENT = 'CASHIER_REDIRECT';
5
+ const TOP_URL_REPLACE_EVENT = 'TOP_URL_REPLACE';
6
+ const CASHIER_IFRAME_OVERLAY_CLOSE_EVENT = 'CASHIER_IFRAME_OVERLAY_CLOSE';
7
+ const CASHIER_RESULT_EVENT = 'CASHIER_RESULT';
8
+ const CASHIER_VALIDATION_FAILED_EVENT = 'CASHIER_VALIDATION_FAILED';
9
+ const CASHIER_ENVIRONMENT_CONFIG = {
10
+ production: {
11
+ host: 'https://api.morefin.com',
12
+ allowedHostnames: ['api.morefin.com']
13
+ },
14
+ uat: {
15
+ host: 'https://uat-api.morefin.com',
16
+ allowedHostnames: ['uat-api.morefin.com']
17
+ },
18
+ local: {
19
+ host: 'http://localhost:7082',
20
+ allowedHostnames: ['localhost', '127.0.0.1']
21
+ },
22
+ local4200: {
23
+ host: 'http://localhost:4200',
24
+ allowedHostnames: ['localhost', '127.0.0.1']
25
+ }
26
+ };
3
27
  let fpLoaderPromise;
4
28
  function loadFingerprintLibrary() {
5
29
  if (typeof window === 'undefined') {
@@ -40,7 +64,7 @@ async function generateFingerprint() {
40
64
  return undefined;
41
65
  }
42
66
  }
43
- function buildQueryString(requestParams) {
67
+ function buildQueryString(requestParams, fingerprint) {
44
68
  const query = new URLSearchParams();
45
69
  if (requestParams.merchantId != null) {
46
70
  query.append('merchantId', String(requestParams.merchantId));
@@ -62,8 +86,8 @@ function buildQueryString(requestParams) {
62
86
  if (requestParams.layout != null && requestParams.layout !== '' && requestParams.layout !== 'undefined') {
63
87
  query.append('layout', requestParams.layout);
64
88
  }
65
- if (requestParams.fingerprint) {
66
- query.append('fingerprint', requestParams.fingerprint);
89
+ if (fingerprint) {
90
+ query.append('fingerprint', fingerprint);
67
91
  }
68
92
  if (requestParams.transactionType) {
69
93
  query.append('transactionType', requestParams.transactionType);
@@ -71,12 +95,6 @@ function buildQueryString(requestParams) {
71
95
  const qs = query.toString();
72
96
  return qs ? `?${qs}` : '';
73
97
  }
74
- function normalizePath(path) {
75
- if (!path) {
76
- return '/cashier';
77
- }
78
- return path.startsWith('/') ? path : `/${path}`;
79
- }
80
98
  function resolveContainer(container) {
81
99
  if (container instanceof HTMLElement) {
82
100
  return container;
@@ -90,34 +108,48 @@ function resolveContainer(container) {
90
108
  }
91
109
  return document.body;
92
110
  }
93
- function buildIframeUrl(host, cashierPath, requestParams) {
94
- const trimmedHost = host.endsWith('/') ? host.slice(0, -1) : host;
95
- const path = normalizePath(cashierPath);
96
- return `${trimmedHost}${path}${buildQueryString(requestParams)}`;
97
- }
98
- function getOriginFromUrl(url) {
99
- try {
100
- return new URL(url).origin;
111
+ function resolveEnvironmentConfig(environment) {
112
+ const config = CASHIER_ENVIRONMENT_CONFIG[environment];
113
+ if (!config) {
114
+ throw new Error(`Unsupported cashier environment "${environment}"`);
101
115
  }
102
- catch {
103
- return '*';
116
+ return config;
117
+ }
118
+ function parseAndVerifyCashierUrl(url, allowedHostnames) {
119
+ const parsedUrl = new URL(url);
120
+ if (!allowedHostnames.includes(parsedUrl.hostname)) {
121
+ throw new Error(`Cashier domain "${parsedUrl.hostname}" is not allowed`);
104
122
  }
123
+ return parsedUrl;
124
+ }
125
+ function buildIframeUrl(host, requestParams, allowedHostnames, fingerprint) {
126
+ const trimmedHost = host.endsWith('/') ? host.slice(0, -1) : host;
127
+ const url = `${trimmedHost}${CASHIER_PATH}${buildQueryString(requestParams, fingerprint)}`;
128
+ parseAndVerifyCashierUrl(url, allowedHostnames);
129
+ return url;
130
+ }
131
+ function getOriginFromUrl(url, allowedHostnames) {
132
+ return parseAndVerifyCashierUrl(url, allowedHostnames).origin;
105
133
  }
106
134
  export class CashierBootstrapper {
107
135
  constructor(container, config = {}, onReady) {
108
136
  this.origin = '*';
109
137
  this.ready = false;
138
+ this.messageListener = (event) => this.handleIframeMessage(event);
110
139
  this.onReady = onReady;
111
- const host = config.properties?.host;
112
- if (!host) {
113
- throw new Error('Cashier host is required to bootstrap the iframe');
140
+ this.callbacks = config.callbacks;
141
+ const environment = config.properties?.environment;
142
+ if (!environment) {
143
+ throw new Error('Cashier environment is required to bootstrap the iframe');
114
144
  }
145
+ const environmentConfig = resolveEnvironmentConfig(environment);
146
+ this.host = environmentConfig.host;
147
+ this.allowedHostnames = environmentConfig.allowedHostnames;
115
148
  this.fullConfig = {
116
149
  requestParams: { ...config.requestParams },
117
150
  properties: {
118
151
  ...config.properties,
119
- host,
120
- cashierPath: normalizePath(config.properties?.cashierPath),
152
+ environment,
121
153
  iframe: {
122
154
  width: '100%',
123
155
  height: '100%',
@@ -128,15 +160,15 @@ export class CashierBootstrapper {
128
160
  };
129
161
  const target = resolveContainer(container ?? this.fullConfig.properties.container);
130
162
  this.iframe = this.createIframeShell(target);
163
+ if (typeof window !== 'undefined') {
164
+ window.addEventListener('message', this.messageListener);
165
+ }
131
166
  void this.bootstrapIframe();
132
167
  }
133
168
  async bootstrapIframe() {
134
169
  const fp = await generateFingerprint();
135
- if (fp) {
136
- this.fullConfig.requestParams.fingerprint = fp;
137
- }
138
- const src = buildIframeUrl(this.fullConfig.properties.host, this.fullConfig.properties.cashierPath ?? '/cashier', this.fullConfig.requestParams);
139
- this.origin = getOriginFromUrl(src);
170
+ const src = buildIframeUrl(this.host, this.fullConfig.requestParams, this.allowedHostnames, fp);
171
+ this.origin = getOriginFromUrl(src, this.allowedHostnames);
140
172
  if (this.iframe) {
141
173
  this.iframe.src = src;
142
174
  this.iframe.onload = () => this.handleLoad();
@@ -179,6 +211,221 @@ export class CashierBootstrapper {
179
211
  }
180
212
  this.iframe.contentWindow.postMessage({ eventType, payload }, this.origin);
181
213
  }
214
+ handleIframeMessage(event) {
215
+ const sourceIsMainCashierIframe = !!this.iframe?.contentWindow && event.source === this.iframe.contentWindow;
216
+ const sourceIsOverlayIframe = !!this.redirectOverlayIframe?.contentWindow && event.source === this.redirectOverlayIframe.contentWindow;
217
+ if (!sourceIsMainCashierIframe && !sourceIsOverlayIframe) {
218
+ return;
219
+ }
220
+ if (event.origin !== this.origin) {
221
+ return;
222
+ }
223
+ if (!event.data || typeof event.data !== 'object') {
224
+ return;
225
+ }
226
+ const { eventType, payload } = event.data;
227
+ if (eventType === CASHIER_IFRAME_OVERLAY_CLOSE_EVENT) {
228
+ if (sourceIsOverlayIframe) {
229
+ this.closeProviderRedirectOverlay();
230
+ }
231
+ return;
232
+ }
233
+ if (!sourceIsMainCashierIframe) {
234
+ return;
235
+ }
236
+ if (eventType === TOP_URL_REPLACE_EVENT) {
237
+ this.handleCashierRedirect({ url: payload?.url, target: 'window' });
238
+ return;
239
+ }
240
+ if (eventType === CASHIER_RESULT_EVENT) {
241
+ this.handleCashierResultEvent(payload);
242
+ return;
243
+ }
244
+ if (eventType === CASHIER_VALIDATION_FAILED_EVENT) {
245
+ this.handleCashierValidationFailedEvent(payload);
246
+ return;
247
+ }
248
+ if (eventType === CASHIER_REDIRECT_EVENT) {
249
+ this.handleCashierRedirect(payload);
250
+ }
251
+ }
252
+ handleCashierResultEvent(payload) {
253
+ if (!payload || typeof payload !== 'object') {
254
+ return;
255
+ }
256
+ const resultPayload = payload;
257
+ const status = this.normalizeResultStatus(resultPayload['status']);
258
+ const callbackPayload = {
259
+ status,
260
+ transactionId: this.asString(resultPayload['transactionId']),
261
+ message: this.asString(resultPayload['message'])
262
+ };
263
+ if (!status) {
264
+ return;
265
+ }
266
+ if (status === 'approved' || status === 'declined') {
267
+ this.invokeCallback(this.callbacks?.onSuccess, callbackPayload);
268
+ return;
269
+ }
270
+ if (status === 'cancelled') {
271
+ this.invokeCallback(this.callbacks?.onCancel, callbackPayload);
272
+ return;
273
+ }
274
+ if (status === 'invalid' || status === 'error') {
275
+ this.invokeCallback(this.callbacks?.onFailure, callbackPayload);
276
+ }
277
+ }
278
+ handleCashierValidationFailedEvent(payload) {
279
+ if (!payload || typeof payload !== 'object') {
280
+ return;
281
+ }
282
+ const validationPayload = payload;
283
+ const message = this.asString(validationPayload['message']) ?? 'Validation failed';
284
+ const callbackPayload = { message };
285
+ this.invokeCallback(this.callbacks?.onValidationFailed, callbackPayload);
286
+ }
287
+ handleCashierRedirect(payload) {
288
+ if (!payload || typeof payload !== 'object') {
289
+ return;
290
+ }
291
+ const redirectUrl = payload.url;
292
+ const redirectTarget = this.normalizeRedirectTarget(payload.target);
293
+ if (typeof redirectUrl !== 'string' || redirectUrl.trim() === '') {
294
+ return;
295
+ }
296
+ switch (redirectTarget) {
297
+ case 'iframe':
298
+ console.log('[CashierBootstrapper] Handling redirect target "iframe".', { redirectUrl });
299
+ if (!this.openProviderRedirectOverlay(redirectUrl) && this.iframe) {
300
+ console.warn('[CashierBootstrapper] Could not open provider overlay; falling back to redirect inside cashier iframe.');
301
+ this.iframe.src = redirectUrl;
302
+ }
303
+ break;
304
+ case 'tab':
305
+ console.log('[CashierBootstrapper] Handling redirect target "tab".', { redirectUrl });
306
+ window.open(redirectUrl, '_blank', 'noopener,noreferrer');
307
+ break;
308
+ case 'window':
309
+ default:
310
+ console.log('[CashierBootstrapper] Handling redirect target "window".', { redirectUrl });
311
+ window.location.assign(redirectUrl);
312
+ break;
313
+ }
314
+ }
315
+ openProviderRedirectOverlay(redirectUrl) {
316
+ if (typeof document === 'undefined' || typeof window === 'undefined') {
317
+ return false;
318
+ }
319
+ if (!this.iframe) {
320
+ return false;
321
+ }
322
+ this.closeProviderRedirectOverlay();
323
+ try {
324
+ const overlay = document.createElement('div');
325
+ overlay.setAttribute('data-cashier-provider-overlay', 'true');
326
+ overlay.style.position = 'fixed';
327
+ overlay.style.top = '0';
328
+ overlay.style.left = '0';
329
+ overlay.style.right = '0';
330
+ overlay.style.bottom = '0';
331
+ overlay.style.zIndex = '2147483000';
332
+ overlay.style.background = 'transparent';
333
+ overlay.style.pointerEvents = 'none';
334
+ const panel = document.createElement('div');
335
+ panel.style.position = 'fixed';
336
+ panel.style.top = '0';
337
+ panel.style.left = '0';
338
+ panel.style.width = '0';
339
+ panel.style.height = '0';
340
+ panel.style.background = '#fff';
341
+ panel.style.overflow = 'hidden';
342
+ panel.style.display = 'flex';
343
+ panel.style.flexDirection = 'column';
344
+ panel.style.pointerEvents = 'auto';
345
+ const iframe = document.createElement('iframe');
346
+ iframe.src = redirectUrl;
347
+ iframe.style.border = 'none';
348
+ iframe.style.width = '100%';
349
+ iframe.style.height = '100%';
350
+ iframe.allow = DEFAULT_IFRAME_ALLOW;
351
+ iframe.setAttribute('data-provider-redirect-iframe', 'true');
352
+ panel.appendChild(iframe);
353
+ overlay.appendChild(panel);
354
+ document.body.appendChild(overlay);
355
+ this.redirectOverlayElement = overlay;
356
+ this.redirectOverlayPanel = panel;
357
+ this.redirectOverlayIframe = iframe;
358
+ this.syncProviderRedirectOverlayToCashierIframe();
359
+ const sync = () => this.syncProviderRedirectOverlayToCashierIframe();
360
+ this.overlayPositionSyncHandler = sync;
361
+ window.addEventListener('resize', sync);
362
+ window.addEventListener('scroll', sync, true);
363
+ return true;
364
+ }
365
+ catch (err) {
366
+ console.warn('Failed to mount provider redirect iframe overlay.', err);
367
+ this.closeProviderRedirectOverlay();
368
+ return false;
369
+ }
370
+ }
371
+ closeProviderRedirectOverlay() {
372
+ if (typeof window !== 'undefined' && this.overlayPositionSyncHandler) {
373
+ window.removeEventListener('resize', this.overlayPositionSyncHandler);
374
+ window.removeEventListener('scroll', this.overlayPositionSyncHandler, true);
375
+ }
376
+ this.overlayPositionSyncHandler = undefined;
377
+ const overlay = this.redirectOverlayElement;
378
+ if (overlay && overlay.parentNode) {
379
+ overlay.parentNode.removeChild(overlay);
380
+ }
381
+ this.redirectOverlayElement = undefined;
382
+ this.redirectOverlayPanel = undefined;
383
+ this.redirectOverlayIframe = undefined;
384
+ }
385
+ syncProviderRedirectOverlayToCashierIframe() {
386
+ const panel = this.redirectOverlayPanel;
387
+ const iframe = this.iframe;
388
+ if (!panel || !iframe) {
389
+ return;
390
+ }
391
+ const rect = iframe.getBoundingClientRect();
392
+ panel.style.left = `${rect.left}px`;
393
+ panel.style.top = `${rect.top}px`;
394
+ panel.style.width = `${rect.width}px`;
395
+ panel.style.height = `${rect.height}px`;
396
+ panel.style.borderRadius = window.getComputedStyle(iframe).borderRadius || '0';
397
+ }
398
+ normalizeRedirectTarget(target) {
399
+ if (target === 'iframe' || target === 'tab' || target === 'window') {
400
+ return target;
401
+ }
402
+ return 'window';
403
+ }
404
+ normalizeResultStatus(status) {
405
+ if (typeof status !== 'string') {
406
+ return undefined;
407
+ }
408
+ const normalized = status.trim().toLowerCase();
409
+ return normalized || undefined;
410
+ }
411
+ asString(value) {
412
+ if (typeof value !== 'string') {
413
+ return undefined;
414
+ }
415
+ const normalized = value.trim();
416
+ return normalized || undefined;
417
+ }
418
+ invokeCallback(callback, payload) {
419
+ if (!callback) {
420
+ return;
421
+ }
422
+ try {
423
+ callback(payload);
424
+ }
425
+ catch (err) {
426
+ console.error('[CashierBootstrapper] Host callback threw an error.', err);
427
+ }
428
+ }
182
429
  /**
183
430
  * API exposed to host pages for runtime control.
184
431
  */
package/dist/types.d.ts CHANGED
@@ -8,14 +8,14 @@ export interface CashierRequestParams {
8
8
  sessionId?: string;
9
9
  predefinedAmounts?: number[];
10
10
  layout?: string;
11
- fingerprint?: string;
12
11
  transactionType?: string;
13
12
  }
13
+ export type CashierEnvironment = 'production' | 'uat' | 'local' | 'local4200';
14
14
  /**
15
15
  * Bootstrap properties configuration
16
16
  */
17
17
  export interface BootstrapProperties {
18
- host: string;
18
+ environment: CashierEnvironment;
19
19
  /**
20
20
  * Selector or element where HTML should be rendered
21
21
  * Default: 'body' (replaces entire body)
@@ -70,10 +70,6 @@ export interface CashierIframeProperties extends BootstrapProperties {
70
70
  * Optional DOM container (selector or element) where the iframe is appended.
71
71
  */
72
72
  container?: string | HTMLElement;
73
- /**
74
- * Path to the cashier page. Default: '/cashier'.
75
- */
76
- cashierPath?: string;
77
73
  /**
78
74
  * Iframe tuning options (dimensions, attributes).
79
75
  */
@@ -85,6 +81,7 @@ export interface CashierIframeProperties extends BootstrapProperties {
85
81
  export interface CashierIframeConfig {
86
82
  requestParams?: CashierRequestParams;
87
83
  properties?: CashierIframeProperties;
84
+ callbacks?: CashierCallbacks;
88
85
  }
89
86
  /**
90
87
  * Minimal API surface returned to hosts after iframe creation.
@@ -96,3 +93,17 @@ export interface CashierIframeApi {
96
93
  pause: () => void;
97
94
  resume: () => void;
98
95
  }
96
+ export interface CashierResultCallbackPayload {
97
+ status?: string;
98
+ transactionId?: string;
99
+ message?: string;
100
+ }
101
+ export interface CashierValidationFailedPayload {
102
+ message: string;
103
+ }
104
+ export interface CashierCallbacks {
105
+ onSuccess?: (payload: CashierResultCallbackPayload) => void;
106
+ onFailure?: (payload: CashierResultCallbackPayload) => void;
107
+ onCancel?: (payload: CashierResultCallbackPayload) => void;
108
+ onValidationFailed?: (payload: CashierValidationFailedPayload) => void;
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morefin/cashier-bootstrapper",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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",