@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 +60 -18
- package/dist/example-usage.js +6 -7
- package/dist/index.d.ts +20 -1
- package/dist/index.js +276 -29
- package/dist/types.d.ts +17 -6
- package/package.json +1 -1
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
|
|
13
|
+
Throughout the examples below, set an environment once and reuse it:
|
|
14
14
|
|
|
15
15
|
```typescript
|
|
16
|
-
const
|
|
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: {
|
|
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: ('
|
|
42
|
+
transactionType: ('deposit'|'withdrawal')
|
|
43
43
|
},
|
|
44
44
|
properties: {
|
|
45
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
175
|
+
interface CashierResultCallbackPayload {
|
|
176
|
+
status?: string;
|
|
177
|
+
transactionId?: string;
|
|
178
|
+
message?: string;
|
|
179
|
+
}
|
|
146
180
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
+
```
|
package/dist/example-usage.js
CHANGED
|
@@ -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
|
|
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: {
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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: {
|
|
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 (
|
|
66
|
-
query.append('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
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
+
}
|