@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 +61 -13
- package/dist/index.d.ts +26 -1
- package/dist/index.js +374 -18
- package/dist/types.d.ts +24 -1
- 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
|
|
@@ -85,10 +97,35 @@ new CashierBootstrapper('#cashier-root', {
|
|
|
85
97
|
});
|
|
86
98
|
```
|
|
87
99
|
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
- `
|
|
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
|
-
|
|
183
|
+
interface CashierResultCallbackPayload {
|
|
184
|
+
status?: string;
|
|
185
|
+
transactionId?: string;
|
|
186
|
+
message?: string;
|
|
187
|
+
}
|
|
145
188
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
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('
|
|
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
|
|
94
|
-
const
|
|
95
|
-
if (!
|
|
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
|
|
119
|
+
return config;
|
|
99
120
|
}
|
|
100
|
-
function parseAndVerifyCashierUrl(url) {
|
|
121
|
+
function parseAndVerifyCashierUrl(url, allowedHostnames) {
|
|
101
122
|
const parsedUrl = new URL(url);
|
|
102
|
-
if (!
|
|
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
|
-
|
|
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, '&')
|
|
515
|
+
.replace(/</g, '<')
|
|
516
|
+
.replace(/>/g, '>')
|
|
517
|
+
.replace(/"/g, '"')
|
|
518
|
+
.replace(/'/g, ''');
|
|
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
|
+
}
|