@morefin/cashier-bootstrapper 0.1.8 → 0.2.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
@@ -39,14 +39,22 @@ new CashierBootstrapper('#cashier-root', {
39
39
  sessionId: 'session-abc',
40
40
  predefinedAmounts: [100, 200, 300],
41
41
  layout: 'default',
42
- transactionType: ('deposit'|'withdrawal')
42
+ transactionType: ('deposit'|'withdrawal'),
43
+ attributes: {
44
+ channel: 'web',
45
+ campaign: 'spring-launch',
46
+ isVip: true
47
+ }
43
48
  },
44
49
  properties: {
45
50
  environment: CASHIER_ENVIRONMENT,
46
51
  iframe: {
47
52
  height: '720px',
48
53
  minHeight: '640px',
49
- title: 'Morefin Cashier'
54
+ title: 'Morefin Cashier',
55
+ attributes: {
56
+ 'data-testid': 'cashier-iframe'
57
+ }
50
58
  }
51
59
  }
52
60
  }, api => {
@@ -54,6 +62,10 @@ new CashierBootstrapper('#cashier-root', {
54
62
  });
55
63
  ```
56
64
 
65
+ `requestParams.attributes` sends custom cashier data as the `attributes` query parameter. Use it for values the cashier should receive as part of the request payload.
66
+
67
+ `properties.iframe.attributes` applies plain HTML attributes directly to the rendered `<iframe>`. Use it for DOM concerns such as `data-*`, `loading`, or other iframe element attributes.
68
+
57
69
  ### Provide Container via Config
58
70
 
59
71
  ```typescript
@@ -85,10 +97,35 @@ new CashierBootstrapper('#cashier-root', {
85
97
  });
86
98
  ```
87
99
 
88
- ## Examples Folder
100
+ ### Transaction Result Callbacks
101
+
102
+ ```typescript
103
+ import { CashierBootstrapper } from '@morefin/cashier-bootstrapper';
104
+
105
+ new CashierBootstrapper('#cashier-root', {
106
+ properties: { environment: CASHIER_ENVIRONMENT },
107
+ callbacks: {
108
+ onSuccess: ({ status, transactionId }) => {
109
+ console.log('Cashier success callback', status, transactionId);
110
+ },
111
+ onFailure: ({ status, message, transactionId }) => {
112
+ console.log('Cashier failure callback', status, message, transactionId);
113
+ },
114
+ onCancel: ({ status, transactionId }) => {
115
+ console.log('Cashier cancel callback', status, transactionId);
116
+ },
117
+ onValidationFailed: ({ message }) => {
118
+ console.log('Cashier validation failed callback', message);
119
+ }
120
+ }
121
+ });
122
+ ```
89
123
 
90
- - `examples/npm` contains a minimal app that installs the package from npm and bundles it with esbuild.
91
- - `src/example-usage.ts` contains additional snippets for reference.
124
+ Callback mapping:
125
+ - `onSuccess`: result status `APPROVED` or `DECLINED`
126
+ - `onFailure`: result status `INVALID` (or explicit cashier error event)
127
+ - `onCancel`: result status `CANCELLED`
128
+ - `onValidationFailed`: `/validate` request failed; `message` is forwarded from cashier
92
129
 
93
130
  ## API
94
131
 
@@ -99,6 +136,7 @@ Iframe-based embed that loads the cashier URL and exposes runtime controls once
99
136
  **Parameters:**
100
137
  - `container: string | HTMLElement | null | undefined` - Where the iframe is appended. If omitted, `config.properties.container` is used (falling back to `document.body`).
101
138
  - `config?: CashierIframeConfig` - Request params and iframe properties. `properties.environment` is required.
139
+ - `config.callbacks?: CashierCallbacks` - Host callback handlers for cashier result events.
102
140
  - `onReady?: (api: CashierIframeApi) => void` - Called when the iframe loads; exposes helpers:
103
141
  - `api.setCss(css: string)` – inject CSS inside the cashier iframe
104
142
  - `api.updateData(data: object)` – post updated `APP_DATA` to the cashier
@@ -115,6 +153,7 @@ interface CashierRequestParams {
115
153
  predefinedAmounts?: number[];
116
154
  layout?: string;
117
155
  transactionType?: string;
156
+ attributes?: Record<string, unknown>;
118
157
  }
119
158
 
120
159
  type CashierEnvironment = 'production' | 'uat';
@@ -138,14 +177,23 @@ interface CashierIframeProperties {
138
177
  interface CashierIframeConfig {
139
178
  requestParams?: CashierRequestParams;
140
179
  properties?: CashierIframeProperties;
180
+ callbacks?: CashierCallbacks;
141
181
  }
142
- ```
143
182
 
144
- ## Defaults
183
+ interface CashierResultCallbackPayload {
184
+ status?: string;
185
+ transactionId?: string;
186
+ message?: string;
187
+ }
145
188
 
146
- - Cashier path is fixed to `/cashier`
147
- - `environment: 'production'` resolves to `https://api.morefin.com`
148
- - `environment: 'uat'` resolves to `https://uat-api.morefin.com`
149
- - Iframe URL origin is validated to allow only `api.morefin.com` and `uat-api.morefin.com`
150
- - `iframe.width`/`iframe.height`: `100%`
151
- - `iframe.allow`: `geolocation *;camera *;payment *;clipboard-read *;clipboard-write *;autoplay *;microphone *;fullscreen *;accelerometer *;magnetometer *;gyroscope *;picture-in-picture *;otp-credentials *;`
189
+ interface CashierValidationFailedPayload {
190
+ message: string;
191
+ }
192
+
193
+ interface CashierCallbacks {
194
+ onSuccess?: (payload: CashierResultCallbackPayload) => void;
195
+ onFailure?: (payload: CashierResultCallbackPayload) => void;
196
+ onCancel?: (payload: CashierResultCallbackPayload) => void;
197
+ onValidationFailed?: (payload: CashierValidationFailedPayload) => void;
198
+ }
199
+ ```
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CashierIframeApi, CashierIframeConfig } from './types';
2
- export type { CashierEnvironment, CashierRequestParams, CashierIframeApi, CashierIframeConfig, CashierIframeProperties } from './types';
2
+ export type { CashierCallbacks, CashierEnvironment, CashierRequestParams, CashierIframeApi, CashierIframeConfig, CashierIframeProperties, CashierResultCallbackPayload, CashierRedirectPayload, CashierValidationFailedPayload } from './types';
3
3
  type FingerprintJSGlobal = {
4
4
  load: () => Promise<{
5
5
  get: () => Promise<{
@@ -14,16 +14,41 @@ 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;
20
24
  private readonly host;
25
+ private readonly allowedHostnames;
21
26
  private readonly onReady?;
27
+ private readonly callbacks?;
28
+ private readonly messageListener;
22
29
  constructor(container: string | HTMLElement | null | undefined, config?: CashierIframeConfig, onReady?: (api: CashierIframeApi) => void);
23
30
  private bootstrapIframe;
24
31
  private createIframeShell;
25
32
  private handleLoad;
26
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 normalizeRedirectMethod;
43
+ private normalizeRedirectParameters;
44
+ private openPopupWithPost;
45
+ private writePostFormToWindow;
46
+ private submitPostForm;
47
+ private buildPostHtml;
48
+ private escapeHtml;
49
+ private normalizeResultStatus;
50
+ private asString;
51
+ private invokeCallback;
27
52
  /**
28
53
  * API exposed to host pages for runtime control.
29
54
  */
package/dist/index.js CHANGED
@@ -1,11 +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
2
  const CASHIER_PATH = '/cashier';
3
3
  const FINGERPRINT_CDN = 'https://cdn.jsdelivr.net/npm/@fingerprintjs/fingerprintjs@4/dist/fp.min.js';
4
- const CASHIER_HOSTS = {
5
- production: 'https://api.morefin.com',
6
- uat: 'https://uat-api.morefin.com'
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
+ }
7
26
  };
8
- const ALLOWED_CASHIER_DOMAINS = new Set(['api.morefin.com', 'uat-api.morefin.com']);
9
27
  let fpLoaderPromise;
10
28
  function loadFingerprintLibrary() {
11
29
  if (typeof window === 'undefined') {
@@ -62,7 +80,7 @@ function buildQueryString(requestParams, fingerprint) {
62
80
  }
63
81
  if (Array.isArray(requestParams.predefinedAmounts)) {
64
82
  requestParams.predefinedAmounts.forEach(amount => {
65
- query.append('predefined_amounts', String(amount));
83
+ query.append('predefinedAmounts', String(amount));
66
84
  });
67
85
  }
68
86
  if (requestParams.layout != null && requestParams.layout !== '' && requestParams.layout !== 'undefined') {
@@ -74,6 +92,9 @@ function buildQueryString(requestParams, fingerprint) {
74
92
  if (requestParams.transactionType) {
75
93
  query.append('transactionType', requestParams.transactionType);
76
94
  }
95
+ if (requestParams.attributes) {
96
+ query.append('attributes', JSON.stringify(requestParams.attributes));
97
+ }
77
98
  const qs = query.toString();
78
99
  return qs ? `?${qs}` : '';
79
100
  }
@@ -90,39 +111,43 @@ function resolveContainer(container) {
90
111
  }
91
112
  return document.body;
92
113
  }
93
- function resolveHost(environment) {
94
- const host = CASHIER_HOSTS[environment];
95
- if (!host) {
114
+ function resolveEnvironmentConfig(environment) {
115
+ const config = CASHIER_ENVIRONMENT_CONFIG[environment];
116
+ if (!config) {
96
117
  throw new Error(`Unsupported cashier environment "${environment}"`);
97
118
  }
98
- return host;
119
+ return config;
99
120
  }
100
- function parseAndVerifyCashierUrl(url) {
121
+ function parseAndVerifyCashierUrl(url, allowedHostnames) {
101
122
  const parsedUrl = new URL(url);
102
- if (!ALLOWED_CASHIER_DOMAINS.has(parsedUrl.hostname)) {
123
+ if (!allowedHostnames.includes(parsedUrl.hostname)) {
103
124
  throw new Error(`Cashier domain "${parsedUrl.hostname}" is not allowed`);
104
125
  }
105
126
  return parsedUrl;
106
127
  }
107
- function buildIframeUrl(host, requestParams, fingerprint) {
128
+ function buildIframeUrl(host, requestParams, allowedHostnames, fingerprint) {
108
129
  const trimmedHost = host.endsWith('/') ? host.slice(0, -1) : host;
109
130
  const url = `${trimmedHost}${CASHIER_PATH}${buildQueryString(requestParams, fingerprint)}`;
110
- parseAndVerifyCashierUrl(url);
131
+ parseAndVerifyCashierUrl(url, allowedHostnames);
111
132
  return url;
112
133
  }
113
- function getOriginFromUrl(url) {
114
- return parseAndVerifyCashierUrl(url).origin;
134
+ function getOriginFromUrl(url, allowedHostnames) {
135
+ return parseAndVerifyCashierUrl(url, allowedHostnames).origin;
115
136
  }
116
137
  export class CashierBootstrapper {
117
138
  constructor(container, config = {}, onReady) {
118
139
  this.origin = '*';
119
140
  this.ready = false;
141
+ this.messageListener = (event) => this.handleIframeMessage(event);
120
142
  this.onReady = onReady;
143
+ this.callbacks = config.callbacks;
121
144
  const environment = config.properties?.environment;
122
145
  if (!environment) {
123
146
  throw new Error('Cashier environment is required to bootstrap the iframe');
124
147
  }
125
- this.host = resolveHost(environment);
148
+ const environmentConfig = resolveEnvironmentConfig(environment);
149
+ this.host = environmentConfig.host;
150
+ this.allowedHostnames = environmentConfig.allowedHostnames;
126
151
  this.fullConfig = {
127
152
  requestParams: { ...config.requestParams },
128
153
  properties: {
@@ -138,12 +163,15 @@ export class CashierBootstrapper {
138
163
  };
139
164
  const target = resolveContainer(container ?? this.fullConfig.properties.container);
140
165
  this.iframe = this.createIframeShell(target);
166
+ if (typeof window !== 'undefined') {
167
+ window.addEventListener('message', this.messageListener);
168
+ }
141
169
  void this.bootstrapIframe();
142
170
  }
143
171
  async bootstrapIframe() {
144
172
  const fp = await generateFingerprint();
145
- const src = buildIframeUrl(this.host, this.fullConfig.requestParams, fp);
146
- this.origin = getOriginFromUrl(src);
173
+ const src = buildIframeUrl(this.host, this.fullConfig.requestParams, this.allowedHostnames, fp);
174
+ this.origin = getOriginFromUrl(src, this.allowedHostnames);
147
175
  if (this.iframe) {
148
176
  this.iframe.src = src;
149
177
  this.iframe.onload = () => this.handleLoad();
@@ -186,6 +214,334 @@ export class CashierBootstrapper {
186
214
  }
187
215
  this.iframe.contentWindow.postMessage({ eventType, payload }, this.origin);
188
216
  }
217
+ handleIframeMessage(event) {
218
+ const sourceIsMainCashierIframe = !!this.iframe?.contentWindow && event.source === this.iframe.contentWindow;
219
+ const sourceIsOverlayIframe = !!this.redirectOverlayIframe?.contentWindow && event.source === this.redirectOverlayIframe.contentWindow;
220
+ if (!sourceIsMainCashierIframe && !sourceIsOverlayIframe) {
221
+ return;
222
+ }
223
+ if (event.origin !== this.origin) {
224
+ return;
225
+ }
226
+ if (!event.data || typeof event.data !== 'object') {
227
+ return;
228
+ }
229
+ const { eventType, payload } = event.data;
230
+ if (eventType === CASHIER_IFRAME_OVERLAY_CLOSE_EVENT) {
231
+ if (sourceIsOverlayIframe) {
232
+ this.closeProviderRedirectOverlay();
233
+ }
234
+ return;
235
+ }
236
+ if (!sourceIsMainCashierIframe) {
237
+ return;
238
+ }
239
+ if (eventType === TOP_URL_REPLACE_EVENT) {
240
+ this.handleCashierRedirect({ url: payload?.url, target: 'window' });
241
+ return;
242
+ }
243
+ if (eventType === CASHIER_RESULT_EVENT) {
244
+ this.handleCashierResultEvent(payload);
245
+ return;
246
+ }
247
+ if (eventType === CASHIER_VALIDATION_FAILED_EVENT) {
248
+ this.handleCashierValidationFailedEvent(payload);
249
+ return;
250
+ }
251
+ if (eventType === CASHIER_REDIRECT_EVENT) {
252
+ this.handleCashierRedirect(payload);
253
+ }
254
+ }
255
+ handleCashierResultEvent(payload) {
256
+ if (!payload || typeof payload !== 'object') {
257
+ return;
258
+ }
259
+ const resultPayload = payload;
260
+ const status = this.normalizeResultStatus(resultPayload['status']);
261
+ const callbackPayload = {
262
+ status,
263
+ transactionId: this.asString(resultPayload['transactionId']),
264
+ message: this.asString(resultPayload['message'])
265
+ };
266
+ if (!status) {
267
+ return;
268
+ }
269
+ if (status === 'approved' || status === 'declined') {
270
+ this.invokeCallback(this.callbacks?.onSuccess, callbackPayload);
271
+ return;
272
+ }
273
+ if (status === 'cancelled') {
274
+ this.invokeCallback(this.callbacks?.onCancel, callbackPayload);
275
+ return;
276
+ }
277
+ if (status === 'invalid' || status === 'error') {
278
+ this.invokeCallback(this.callbacks?.onFailure, callbackPayload);
279
+ }
280
+ }
281
+ handleCashierValidationFailedEvent(payload) {
282
+ if (!payload || typeof payload !== 'object') {
283
+ return;
284
+ }
285
+ const validationPayload = payload;
286
+ const message = this.asString(validationPayload['message']) ?? 'Validation failed';
287
+ const callbackPayload = { message };
288
+ this.invokeCallback(this.callbacks?.onValidationFailed, callbackPayload);
289
+ }
290
+ handleCashierRedirect(payload) {
291
+ if (!payload || typeof payload !== 'object') {
292
+ return;
293
+ }
294
+ const redirectPayload = payload;
295
+ const redirectUrl = redirectPayload.url;
296
+ const redirectTarget = this.normalizeRedirectTarget(redirectPayload.target);
297
+ const redirectMethod = this.normalizeRedirectMethod(redirectPayload.method);
298
+ const redirectParameters = this.normalizeRedirectParameters(redirectPayload.parameters);
299
+ if (typeof redirectUrl !== 'string' || redirectUrl.trim() === '') {
300
+ return;
301
+ }
302
+ switch (redirectTarget) {
303
+ case 'iframe':
304
+ console.log('[CashierBootstrapper] Handling redirect target "iframe".', { redirectUrl, redirectMethod, redirectParameters });
305
+ if (!this.openProviderRedirectOverlay(redirectUrl, redirectMethod, redirectParameters) && this.iframe) {
306
+ console.warn('[CashierBootstrapper] Could not open provider overlay; falling back to redirect inside cashier iframe.');
307
+ if (redirectMethod === 'POST' && !this.submitPostForm(redirectUrl, redirectParameters, '_self')) {
308
+ this.iframe.src = redirectUrl;
309
+ }
310
+ else if (redirectMethod === 'GET') {
311
+ this.iframe.src = redirectUrl;
312
+ }
313
+ }
314
+ break;
315
+ case 'tab':
316
+ console.log('[CashierBootstrapper] Handling redirect target "tab".', { redirectUrl, redirectMethod, redirectParameters });
317
+ if (redirectMethod === 'POST') {
318
+ if (!this.openPopupWithPost(redirectUrl, redirectParameters)) {
319
+ this.submitPostForm(redirectUrl, redirectParameters, '_blank');
320
+ }
321
+ }
322
+ else {
323
+ window.open(redirectUrl, '_blank', 'noopener,noreferrer');
324
+ }
325
+ break;
326
+ case 'window':
327
+ default:
328
+ console.log('[CashierBootstrapper] Handling redirect target "window".', { redirectUrl, redirectMethod, redirectParameters });
329
+ if (redirectMethod === 'POST') {
330
+ if (!this.openPopupWithPost(redirectUrl, redirectParameters)) {
331
+ this.submitPostForm(redirectUrl, redirectParameters, '_self');
332
+ }
333
+ }
334
+ else {
335
+ window.location.assign(redirectUrl);
336
+ }
337
+ break;
338
+ }
339
+ }
340
+ openProviderRedirectOverlay(redirectUrl, redirectMethod, redirectParameters) {
341
+ if (typeof document === 'undefined' || typeof window === 'undefined') {
342
+ return false;
343
+ }
344
+ if (!this.iframe) {
345
+ return false;
346
+ }
347
+ this.closeProviderRedirectOverlay();
348
+ try {
349
+ const overlay = document.createElement('div');
350
+ overlay.setAttribute('data-cashier-provider-overlay', 'true');
351
+ overlay.style.position = 'fixed';
352
+ overlay.style.top = '0';
353
+ overlay.style.left = '0';
354
+ overlay.style.right = '0';
355
+ overlay.style.bottom = '0';
356
+ overlay.style.zIndex = '2147483000';
357
+ overlay.style.background = 'transparent';
358
+ overlay.style.pointerEvents = 'none';
359
+ const panel = document.createElement('div');
360
+ panel.style.position = 'fixed';
361
+ panel.style.top = '0';
362
+ panel.style.left = '0';
363
+ panel.style.width = '0';
364
+ panel.style.height = '0';
365
+ panel.style.background = '#fff';
366
+ panel.style.overflow = 'hidden';
367
+ panel.style.display = 'flex';
368
+ panel.style.flexDirection = 'column';
369
+ panel.style.pointerEvents = 'auto';
370
+ const iframe = document.createElement('iframe');
371
+ const iframeName = `provider-redirect-iframe-${Date.now()}`;
372
+ iframe.name = iframeName;
373
+ if (redirectMethod === 'GET') {
374
+ iframe.src = redirectUrl;
375
+ }
376
+ iframe.style.border = 'none';
377
+ iframe.style.width = '100%';
378
+ iframe.style.height = '100%';
379
+ iframe.allow = DEFAULT_IFRAME_ALLOW;
380
+ iframe.setAttribute('data-provider-redirect-iframe', 'true');
381
+ panel.appendChild(iframe);
382
+ overlay.appendChild(panel);
383
+ document.body.appendChild(overlay);
384
+ this.redirectOverlayElement = overlay;
385
+ this.redirectOverlayPanel = panel;
386
+ this.redirectOverlayIframe = iframe;
387
+ this.syncProviderRedirectOverlayToCashierIframe();
388
+ const sync = () => this.syncProviderRedirectOverlayToCashierIframe();
389
+ this.overlayPositionSyncHandler = sync;
390
+ window.addEventListener('resize', sync);
391
+ window.addEventListener('scroll', sync, true);
392
+ if (redirectMethod === 'POST') {
393
+ const submitted = this.submitPostForm(redirectUrl, redirectParameters, iframeName, overlay);
394
+ if (!submitted) {
395
+ this.closeProviderRedirectOverlay();
396
+ return false;
397
+ }
398
+ }
399
+ return true;
400
+ }
401
+ catch (err) {
402
+ console.warn('Failed to mount provider redirect iframe overlay.', err);
403
+ this.closeProviderRedirectOverlay();
404
+ return false;
405
+ }
406
+ }
407
+ closeProviderRedirectOverlay() {
408
+ if (typeof window !== 'undefined' && this.overlayPositionSyncHandler) {
409
+ window.removeEventListener('resize', this.overlayPositionSyncHandler);
410
+ window.removeEventListener('scroll', this.overlayPositionSyncHandler, true);
411
+ }
412
+ this.overlayPositionSyncHandler = undefined;
413
+ const overlay = this.redirectOverlayElement;
414
+ if (overlay && overlay.parentNode) {
415
+ overlay.parentNode.removeChild(overlay);
416
+ }
417
+ this.redirectOverlayElement = undefined;
418
+ this.redirectOverlayPanel = undefined;
419
+ this.redirectOverlayIframe = undefined;
420
+ }
421
+ syncProviderRedirectOverlayToCashierIframe() {
422
+ const panel = this.redirectOverlayPanel;
423
+ const iframe = this.iframe;
424
+ if (!panel || !iframe) {
425
+ return;
426
+ }
427
+ const rect = iframe.getBoundingClientRect();
428
+ panel.style.left = `${rect.left}px`;
429
+ panel.style.top = `${rect.top}px`;
430
+ panel.style.width = `${rect.width}px`;
431
+ panel.style.height = `${rect.height}px`;
432
+ panel.style.borderRadius = window.getComputedStyle(iframe).borderRadius || '0';
433
+ }
434
+ normalizeRedirectTarget(target) {
435
+ if (target === 'iframe' || target === 'tab' || target === 'window') {
436
+ return target;
437
+ }
438
+ return 'tab';
439
+ }
440
+ normalizeRedirectMethod(method) {
441
+ if (typeof method !== 'string') {
442
+ return 'GET';
443
+ }
444
+ const normalized = method.trim().toUpperCase();
445
+ return normalized === 'POST' ? 'POST' : 'GET';
446
+ }
447
+ normalizeRedirectParameters(parameters) {
448
+ if (!parameters || typeof parameters !== 'object') {
449
+ return {};
450
+ }
451
+ return Object.entries(parameters).reduce((acc, [key, value]) => {
452
+ acc[key] = String(value ?? '');
453
+ return acc;
454
+ }, {});
455
+ }
456
+ openPopupWithPost(url, parameters) {
457
+ if (typeof window === 'undefined') {
458
+ return false;
459
+ }
460
+ const popup = window.open('', '_blank');
461
+ if (!popup) {
462
+ return false;
463
+ }
464
+ return this.writePostFormToWindow(popup, url, parameters);
465
+ }
466
+ writePostFormToWindow(targetWindow, url, parameters) {
467
+ try {
468
+ targetWindow.document.open();
469
+ targetWindow.document.write(this.buildPostHtml(url, parameters, 'provider-redirect-form-window'));
470
+ targetWindow.document.close();
471
+ return true;
472
+ }
473
+ catch {
474
+ return false;
475
+ }
476
+ }
477
+ submitPostForm(url, parameters, target, parent = document) {
478
+ try {
479
+ const doc = parent instanceof Document ? parent : parent.ownerDocument;
480
+ if (!doc || !doc.body) {
481
+ return false;
482
+ }
483
+ const form = doc.createElement('form');
484
+ form.method = 'post';
485
+ form.action = url;
486
+ form.target = target;
487
+ form.style.display = 'none';
488
+ Object.entries(parameters).forEach(([key, value]) => {
489
+ const input = doc.createElement('input');
490
+ input.type = 'hidden';
491
+ input.name = key;
492
+ input.value = value;
493
+ form.appendChild(input);
494
+ });
495
+ doc.body.appendChild(form);
496
+ form.submit();
497
+ form.remove();
498
+ return true;
499
+ }
500
+ catch {
501
+ return false;
502
+ }
503
+ }
504
+ buildPostHtml(url, parameters, formName) {
505
+ const safeAction = this.escapeHtml(url);
506
+ const safeFormName = this.escapeHtml(formName);
507
+ const inputs = Object.entries(parameters)
508
+ .map(([key, value]) => `<input type="hidden" name="${this.escapeHtml(key)}" value="${this.escapeHtml(value)}">`)
509
+ .join('');
510
+ return `<!doctype html><html><head><meta charset="utf-8"></head><body><form id="${safeFormName}" name="${safeFormName}" method="post" action="${safeAction}">${inputs}</form><script>document.getElementById('${safeFormName}')?.submit();</script></body></html>`;
511
+ }
512
+ escapeHtml(value) {
513
+ return value
514
+ .replace(/&/g, '&amp;')
515
+ .replace(/</g, '&lt;')
516
+ .replace(/>/g, '&gt;')
517
+ .replace(/"/g, '&quot;')
518
+ .replace(/'/g, '&#39;');
519
+ }
520
+ normalizeResultStatus(status) {
521
+ if (typeof status !== 'string') {
522
+ return undefined;
523
+ }
524
+ const normalized = status.trim().toLowerCase();
525
+ return normalized || undefined;
526
+ }
527
+ asString(value) {
528
+ if (typeof value !== 'string') {
529
+ return undefined;
530
+ }
531
+ const normalized = value.trim();
532
+ return normalized || undefined;
533
+ }
534
+ invokeCallback(callback, payload) {
535
+ if (!callback) {
536
+ return;
537
+ }
538
+ try {
539
+ callback(payload);
540
+ }
541
+ catch (err) {
542
+ console.error('[CashierBootstrapper] Host callback threw an error.', err);
543
+ }
544
+ }
189
545
  /**
190
546
  * API exposed to host pages for runtime control.
191
547
  */
package/dist/types.d.ts CHANGED
@@ -9,8 +9,9 @@ export interface CashierRequestParams {
9
9
  predefinedAmounts?: number[];
10
10
  layout?: string;
11
11
  transactionType?: string;
12
+ attributes?: Record<string, unknown>;
12
13
  }
13
- export type CashierEnvironment = 'production' | 'uat';
14
+ export type CashierEnvironment = 'production' | 'uat' | 'local' | 'local4200';
14
15
  /**
15
16
  * Bootstrap properties configuration
16
17
  */
@@ -81,6 +82,7 @@ export interface CashierIframeProperties extends BootstrapProperties {
81
82
  export interface CashierIframeConfig {
82
83
  requestParams?: CashierRequestParams;
83
84
  properties?: CashierIframeProperties;
85
+ callbacks?: CashierCallbacks;
84
86
  }
85
87
  /**
86
88
  * Minimal API surface returned to hosts after iframe creation.
@@ -92,3 +94,24 @@ export interface CashierIframeApi {
92
94
  pause: () => void;
93
95
  resume: () => void;
94
96
  }
97
+ export interface CashierResultCallbackPayload {
98
+ status?: string;
99
+ transactionId?: string;
100
+ message?: string;
101
+ }
102
+ export interface CashierValidationFailedPayload {
103
+ message: string;
104
+ }
105
+ export interface CashierRedirectPayload {
106
+ url: string;
107
+ target?: 'iframe' | 'window' | 'tab';
108
+ method?: 'GET' | 'POST';
109
+ parameters?: Record<string, string>;
110
+ transactionId?: string;
111
+ }
112
+ export interface CashierCallbacks {
113
+ onSuccess?: (payload: CashierResultCallbackPayload) => void;
114
+ onFailure?: (payload: CashierResultCallbackPayload) => void;
115
+ onCancel?: (payload: CashierResultCallbackPayload) => void;
116
+ onValidationFailed?: (payload: CashierValidationFailedPayload) => void;
117
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morefin/cashier-bootstrapper",
3
- "version": "0.1.8",
3
+ "version": "0.2.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",