@morefin/cashier-bootstrapper 0.1.9 → 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 +15 -7
- package/dist/index.d.ts +8 -1
- package/dist/index.js +128 -12
- package/dist/types.d.ts +8 -0
- package/package.json +1 -1
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
|
|
292
|
-
const
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 '
|
|
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, '&')
|
|
515
|
+
.replace(/</g, '<')
|
|
516
|
+
.replace(/>/g, '>')
|
|
517
|
+
.replace(/"/g, '"')
|
|
518
|
+
.replace(/'/g, ''');
|
|
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;
|