@morefin/cashier-bootstrapper 0.1.9 → 0.3.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 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
@@ -115,11 +127,6 @@ Callback mapping:
115
127
  - `onCancel`: result status `CANCELLED`
116
128
  - `onValidationFailed`: `/validate` request failed; `message` is forwarded from cashier
117
129
 
118
- ## Examples Folder
119
-
120
- - Repository examples live in `../examples/cashier-bootstrapper` (outside this package directory), including `../examples/cashier-bootstrapper/npm`.
121
- - `src/example-usage.ts` contains additional snippets for reference.
122
-
123
130
  ## API
124
131
 
125
132
  ### `new CashierBootstrapper(container, config?, onReady?)`
@@ -146,6 +153,7 @@ interface CashierRequestParams {
146
153
  predefinedAmounts?: number[];
147
154
  layout?: string;
148
155
  transactionType?: string;
156
+ attributes?: Record<string, unknown>;
149
157
  }
150
158
 
151
159
  type CashierEnvironment = 'production' | 'uat';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CashierIframeApi, CashierIframeConfig } from './types';
2
- export type { CashierCallbacks, CashierEnvironment, CashierRequestParams, CashierIframeApi, CashierIframeConfig, CashierIframeProperties, CashierResultCallbackPayload, CashierValidationFailedPayload } 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<{
@@ -39,6 +39,13 @@ export declare class CashierBootstrapper {
39
39
  private closeProviderRedirectOverlay;
40
40
  private syncProviderRedirectOverlayToCashierIframe;
41
41
  private normalizeRedirectTarget;
42
+ private normalizeRedirectMethod;
43
+ private normalizeRedirectParameters;
44
+ private openPopupWithPost;
45
+ private writePostFormToWindow;
46
+ private submitPostForm;
47
+ private buildPostHtml;
48
+ private escapeHtml;
42
49
  private normalizeResultStatus;
43
50
  private asString;
44
51
  private invokeCallback;
package/dist/index.js CHANGED
@@ -92,6 +92,9 @@ function buildQueryString(requestParams, fingerprint) {
92
92
  if (requestParams.transactionType) {
93
93
  query.append('transactionType', requestParams.transactionType);
94
94
  }
95
+ if (requestParams.attributes) {
96
+ query.append('attributes', JSON.stringify(requestParams.attributes));
97
+ }
95
98
  const qs = query.toString();
96
99
  return qs ? `?${qs}` : '';
97
100
  }
@@ -288,31 +291,53 @@ export class CashierBootstrapper {
288
291
  if (!payload || typeof payload !== 'object') {
289
292
  return;
290
293
  }
291
- const redirectUrl = payload.url;
292
- const redirectTarget = this.normalizeRedirectTarget(payload.target);
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);
293
299
  if (typeof redirectUrl !== 'string' || redirectUrl.trim() === '') {
294
300
  return;
295
301
  }
296
302
  switch (redirectTarget) {
297
303
  case 'iframe':
298
- console.log('[CashierBootstrapper] Handling redirect target "iframe".', { redirectUrl });
299
- if (!this.openProviderRedirectOverlay(redirectUrl) && this.iframe) {
304
+ console.log('[CashierBootstrapper] Handling redirect target "iframe".', { redirectUrl, redirectMethod, redirectParameters });
305
+ if (!this.openProviderRedirectOverlay(redirectUrl, redirectMethod, redirectParameters) && this.iframe) {
300
306
  console.warn('[CashierBootstrapper] Could not open provider overlay; falling back to redirect inside cashier iframe.');
301
- this.iframe.src = redirectUrl;
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
+ }
302
313
  }
303
314
  break;
304
315
  case 'tab':
305
- console.log('[CashierBootstrapper] Handling redirect target "tab".', { redirectUrl });
306
- window.open(redirectUrl, '_blank', 'noopener,noreferrer');
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
+ }
307
325
  break;
308
326
  case 'window':
309
327
  default:
310
- console.log('[CashierBootstrapper] Handling redirect target "window".', { redirectUrl });
311
- window.location.assign(redirectUrl);
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
+ }
312
337
  break;
313
338
  }
314
339
  }
315
- openProviderRedirectOverlay(redirectUrl) {
340
+ openProviderRedirectOverlay(redirectUrl, redirectMethod, redirectParameters) {
316
341
  if (typeof document === 'undefined' || typeof window === 'undefined') {
317
342
  return false;
318
343
  }
@@ -343,7 +368,11 @@ export class CashierBootstrapper {
343
368
  panel.style.flexDirection = 'column';
344
369
  panel.style.pointerEvents = 'auto';
345
370
  const iframe = document.createElement('iframe');
346
- iframe.src = redirectUrl;
371
+ const iframeName = `provider-redirect-iframe-${Date.now()}`;
372
+ iframe.name = iframeName;
373
+ if (redirectMethod === 'GET') {
374
+ iframe.src = redirectUrl;
375
+ }
347
376
  iframe.style.border = 'none';
348
377
  iframe.style.width = '100%';
349
378
  iframe.style.height = '100%';
@@ -360,6 +389,13 @@ export class CashierBootstrapper {
360
389
  this.overlayPositionSyncHandler = sync;
361
390
  window.addEventListener('resize', sync);
362
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
+ }
363
399
  return true;
364
400
  }
365
401
  catch (err) {
@@ -399,7 +435,87 @@ export class CashierBootstrapper {
399
435
  if (target === 'iframe' || target === 'tab' || target === 'window') {
400
436
  return target;
401
437
  }
402
- return 'window';
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;');
403
519
  }
404
520
  normalizeResultStatus(status) {
405
521
  if (typeof status !== 'string') {
package/dist/types.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface CashierRequestParams {
9
9
  predefinedAmounts?: number[];
10
10
  layout?: string;
11
11
  transactionType?: string;
12
+ attributes?: Record<string, unknown>;
12
13
  }
13
14
  export type CashierEnvironment = 'production' | 'uat' | 'local' | 'local4200';
14
15
  /**
@@ -101,6 +102,13 @@ export interface CashierResultCallbackPayload {
101
102
  export interface CashierValidationFailedPayload {
102
103
  message: string;
103
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
+ }
104
112
  export interface CashierCallbacks {
105
113
  onSuccess?: (payload: CashierResultCallbackPayload) => void;
106
114
  onFailure?: (payload: CashierResultCallbackPayload) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morefin/cashier-bootstrapper",
3
- "version": "0.1.9",
3
+ "version": "0.3.0",
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",