@melio-eng/web-sdk 1.1.0-pr.93.ae7fb3c → 1.1.0-pr.94.438ff96
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 +27 -21
- package/dist/flows/Flow.d.ts +3 -19
- package/dist/flows/Flow.js +72 -212
- package/dist/flows/InitFlow.d.ts +0 -20
- package/dist/flows/InitFlow.js +36 -213
- package/dist/flows/utils.js +0 -2
- package/dist/index.d.ts +0 -6
- package/dist/index.js +34 -57
- package/dist/types.d.ts +21 -12
- package/package.json +1 -1
- package/dist/flows/logger.d.ts +0 -61
- package/dist/flows/logger.js +0 -153
package/README.md
CHANGED
|
@@ -28,7 +28,6 @@ const init = melioSDK.init("AUTH_CODE_FROM_YOUR_BACKEND", {
|
|
|
28
28
|
partnerName: "your-partner-name", // required
|
|
29
29
|
environment: "production", // optional — defaults to 'production'
|
|
30
30
|
keepAlive: true, // optional — keeps the session warm
|
|
31
|
-
debug: true, // optional — verbose console logging
|
|
32
31
|
});
|
|
33
32
|
|
|
34
33
|
init.on("authenticationSucceeded", () => {
|
|
@@ -66,29 +65,9 @@ melioSDK.init(authenticationCode: string, options: InitOptions): InitFlowInstanc
|
|
|
66
65
|
- `keepAlive` (boolean): If true, the session will be kept alive in the background via an invisible iframe
|
|
67
66
|
- `environment` (Environment): The environment to use for API endpoints (defaults to `'production'`)
|
|
68
67
|
- `branchOverride` (string): Advanced/testing only
|
|
69
|
-
- `debug` (boolean): If true, prints debug-level logs to the console on top of the warnings and errors the SDK always prints (defaults to `false`)
|
|
70
68
|
|
|
71
69
|
`init` returns an `InitFlowInstance` you can listen on for `authenticationSucceeded` / `authenticationFailed`.
|
|
72
70
|
|
|
73
|
-
### Debug Logging
|
|
74
|
-
|
|
75
|
-
The SDK always prints warnings and errors to the browser console, prefixed with `[melio-sdk]`. Pass `debug: true` to `init()` to add debug-level output:
|
|
76
|
-
|
|
77
|
-
```typescript
|
|
78
|
-
melioSDK.init("auth_code", { partnerName: "your-partner-name", debug: true });
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Every line carries `msSinceInit`, the elapsed time since `init()` was called — which is what makes a flow that *hangs* (rather than fails) visible in a console capture.
|
|
82
|
-
|
|
83
|
-
Debug output covers:
|
|
84
|
-
|
|
85
|
-
- host environment at init (user agent, `readyState`, whether the SDK is running inside a frame)
|
|
86
|
-
- iframe lifecycle for the hidden auth iframe and every visible flow, with `loadMs` / `authMs` / `readyMs` timings
|
|
87
|
-
- every `postMessage` exchanged with the embedded app, including message types the SDK does not handle
|
|
88
|
-
- a keep-alive heartbeat (when `keepAlive: true`), which doubles as proof the page is still executing
|
|
89
|
-
|
|
90
|
-
Regardless of the flag, a flow that neither loads nor becomes interactive is reported as an error every 15 seconds, up to 8 times. Auth codes and customer details are redacted from all output.
|
|
91
|
-
|
|
92
71
|
### Environment Configuration
|
|
93
72
|
|
|
94
73
|
The SDK supports multiple environments to facilitate development and testing:
|
|
@@ -199,8 +178,35 @@ flow.on("navigated", (p) => console.log("Navigated to:", p.target));
|
|
|
199
178
|
- `error`: A flow error occurred (e.g. `billsSyncFailed`)
|
|
200
179
|
- `exit`: The user exited the iframe
|
|
201
180
|
- `navigated`: Navigation occurred inside the iframe
|
|
181
|
+
- `partnerActionRequired`: Melio needs your page to navigate somewhere it cannot reach itself (see below)
|
|
202
182
|
- `authenticationSucceeded` / `authenticationFailed`: emitted on the `init` instance
|
|
203
183
|
|
|
184
|
+
#### Partner actions (`partnerActionRequired`)
|
|
185
|
+
|
|
186
|
+
Some Melio screens contain buttons whose destination lives in **your** product, not in Melio —
|
|
187
|
+
today the "+ Quick payment" button on the payments dashboard, which should take the user to
|
|
188
|
+
your own supplier selection page. Melio cannot navigate the host page, so it emits an event
|
|
189
|
+
and waits for you:
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
const dashboard = melioSDK.openPaymentsDashboard({
|
|
193
|
+
containerId: "melio-dashboard-container",
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
dashboard.on("partnerActionRequired", (action) => {
|
|
197
|
+
if (action.requiredAction === "NAVIGATE_TO_NEW_PAYMENT") {
|
|
198
|
+
window.location.assign("/suppliers/select");
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
| `requiredAction` | Emitted when | What you should do |
|
|
204
|
+
|---|---|---|
|
|
205
|
+
| `NAVIGATE_TO_NEW_PAYMENT` | The user clicks the new payment button on the payments dashboard | Take them to your supplier selection page, then open a pay flow |
|
|
206
|
+
|
|
207
|
+
Without a listener the button does nothing at all. Further actions ship in minor releases, so
|
|
208
|
+
switch on `requiredAction` and treat an unrecognised value as a no-op.
|
|
209
|
+
|
|
204
210
|
## 💡 Examples
|
|
205
211
|
|
|
206
212
|
### Pay Flow
|
package/dist/flows/Flow.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FlowInstance, FlowEventType, FlowEventCallback, FlowCompletionData, NavigationData, BaseFlowConfig, Environment, ErrorData } from '../types.js';
|
|
1
|
+
import { FlowInstance, FlowEventType, FlowEventCallback, FlowCompletionData, NavigationData, PartnerActionData, BaseFlowConfig, Environment, ErrorData } from '../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Flow class implementation for handling iframe flows and events
|
|
4
4
|
*/
|
|
@@ -13,28 +13,11 @@ export declare class Flow implements FlowInstance {
|
|
|
13
13
|
private eventListeners;
|
|
14
14
|
protected keepAliveInterval: number | null;
|
|
15
15
|
private messageHandler;
|
|
16
|
-
private loadWatchdog;
|
|
17
|
-
private watchdogReports;
|
|
18
|
-
private iframeLoaded;
|
|
19
|
-
private readyForInteraction;
|
|
20
|
-
private injectedAt;
|
|
21
16
|
constructor(containerId: string, config: BaseFlowConfig, partnerName: string, environment: Environment, branchOverride?: string | undefined);
|
|
22
17
|
/**
|
|
23
18
|
* Initialize the flow by creating and injecting the iframe
|
|
24
19
|
*/
|
|
25
20
|
initialize(): Promise<void>;
|
|
26
|
-
/**
|
|
27
|
-
* Report a flow that has neither loaded nor become interactive within the budget.
|
|
28
|
-
*
|
|
29
|
-
* Diagnostic only: this logs and does nothing else. It separates "the iframe
|
|
30
|
-
* never loaded" from "it loaded but never reported READY_FOR_INTERACTION",
|
|
31
|
-
* which is the difference between a transport problem and the embedded app
|
|
32
|
-
* stalling after its document arrived.
|
|
33
|
-
*/
|
|
34
|
-
private startLoadWatchdog;
|
|
35
|
-
/** Milliseconds since the iframe was injected, for load/ready timings. */
|
|
36
|
-
private sinceInject;
|
|
37
|
-
private clearLoadWatchdog;
|
|
38
21
|
/**
|
|
39
22
|
* Construct the specific flow URL - can be overridden by subclasses
|
|
40
23
|
*/
|
|
@@ -44,13 +27,13 @@ export declare class Flow implements FlowInstance {
|
|
|
44
27
|
*/
|
|
45
28
|
protected createFlowUrl(): string;
|
|
46
29
|
private setupEventListeners;
|
|
47
|
-
private handleMessage;
|
|
48
30
|
/**
|
|
49
31
|
* Emit events to registered listeners
|
|
50
32
|
*/
|
|
51
33
|
protected emit(event: 'completed', data: FlowCompletionData): void;
|
|
52
34
|
protected emit(event: 'loaded'): void;
|
|
53
35
|
protected emit(event: 'exit'): void;
|
|
36
|
+
protected emit(event: 'partnerActionRequired', data: PartnerActionData): void;
|
|
54
37
|
protected emit(event: 'navigated', data: NavigationData): void;
|
|
55
38
|
protected emit(event: 'authenticationSucceeded'): void;
|
|
56
39
|
protected emit(event: 'authenticationFailed'): void;
|
|
@@ -62,6 +45,7 @@ export declare class Flow implements FlowInstance {
|
|
|
62
45
|
*/
|
|
63
46
|
on(event: 'completed', callback: (data: FlowCompletionData) => void): void;
|
|
64
47
|
on(event: 'exit', callback: () => void): void;
|
|
48
|
+
on(event: 'partnerActionRequired', callback: (data: PartnerActionData) => void): void;
|
|
65
49
|
on(event: 'navigated', callback: (payload: NavigationData) => void): void;
|
|
66
50
|
on(event: 'authenticationSucceeded', callback: () => void): void;
|
|
67
51
|
on(event: 'authenticationFailed', callback: () => void): void;
|
package/dist/flows/Flow.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { getBaseUrl } from './utils.js';
|
|
2
|
-
import { logger, redactUrl, describeError, describeConfig, LOAD_WATCHDOG_MS, WATCHDOG_MAX_REPORTS, } from './logger.js';
|
|
3
2
|
/**
|
|
4
3
|
* Flow class implementation for handling iframe flows and events
|
|
5
4
|
*/
|
|
@@ -15,11 +14,6 @@ export class Flow {
|
|
|
15
14
|
this.eventListeners = new Map();
|
|
16
15
|
this.keepAliveInterval = null;
|
|
17
16
|
this.messageHandler = null;
|
|
18
|
-
this.loadWatchdog = null;
|
|
19
|
-
this.watchdogReports = 0;
|
|
20
|
-
this.iframeLoaded = false;
|
|
21
|
-
this.readyForInteraction = false;
|
|
22
|
-
this.injectedAt = null;
|
|
23
17
|
this.setupEventListeners();
|
|
24
18
|
}
|
|
25
19
|
/**
|
|
@@ -28,95 +22,15 @@ export class Flow {
|
|
|
28
22
|
async initialize() {
|
|
29
23
|
this.container = document.getElementById(this.containerId);
|
|
30
24
|
if (!this.container) {
|
|
31
|
-
logger.error('Flow container not found in the DOM', {
|
|
32
|
-
containerId: this.containerId,
|
|
33
|
-
partnerName: this.partnerName,
|
|
34
|
-
});
|
|
35
25
|
throw new Error(`Container with ID "${this.containerId}" not found`);
|
|
36
26
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
this.iframe.style.height = '1000px';
|
|
45
|
-
this.iframe.style.border = 'none';
|
|
46
|
-
this.iframe.style.display = 'block';
|
|
47
|
-
this.iframe.addEventListener('load', () => {
|
|
48
|
-
this.iframeLoaded = true;
|
|
49
|
-
logger.debug('Flow iframe fired `load`', {
|
|
50
|
-
url: safeUrl,
|
|
51
|
-
loadMs: this.sinceInject(),
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
// Fires for network-level failures. A document that loads but then stalls
|
|
55
|
-
// internally will NOT fire this — that case is covered by the watchdog.
|
|
56
|
-
this.iframe.addEventListener('error', () => {
|
|
57
|
-
this.clearLoadWatchdog();
|
|
58
|
-
logger.error('Flow iframe failed to load', { url: safeUrl });
|
|
59
|
-
});
|
|
60
|
-
this.startLoadWatchdog(safeUrl);
|
|
61
|
-
logger.debug('Injecting flow iframe', {
|
|
62
|
-
url: safeUrl,
|
|
63
|
-
containerId: this.containerId,
|
|
64
|
-
});
|
|
65
|
-
this.injectedAt = Date.now();
|
|
66
|
-
this.container.appendChild(this.iframe);
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
this.clearLoadWatchdog();
|
|
70
|
-
logger.error('Failed to inject flow iframe', {
|
|
71
|
-
url: safeUrl,
|
|
72
|
-
...describeError(error),
|
|
73
|
-
});
|
|
74
|
-
throw error;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Report a flow that has neither loaded nor become interactive within the budget.
|
|
79
|
-
*
|
|
80
|
-
* Diagnostic only: this logs and does nothing else. It separates "the iframe
|
|
81
|
-
* never loaded" from "it loaded but never reported READY_FOR_INTERACTION",
|
|
82
|
-
* which is the difference between a transport problem and the embedded app
|
|
83
|
-
* stalling after its document arrived.
|
|
84
|
-
*/
|
|
85
|
-
startLoadWatchdog(safeUrl) {
|
|
86
|
-
this.loadWatchdog = window.setInterval(() => {
|
|
87
|
-
if (this.iframeLoaded && this.readyForInteraction) {
|
|
88
|
-
this.clearLoadWatchdog();
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
this.watchdogReports += 1;
|
|
92
|
-
const context = {
|
|
93
|
-
url: safeUrl,
|
|
94
|
-
waitedMs: this.watchdogReports * LOAD_WATCHDOG_MS,
|
|
95
|
-
report: `${this.watchdogReports}/${WATCHDOG_MAX_REPORTS}`,
|
|
96
|
-
containerId: this.containerId,
|
|
97
|
-
environment: this.environment,
|
|
98
|
-
};
|
|
99
|
-
if (!this.iframeLoaded) {
|
|
100
|
-
logger.error('Flow iframe has not fired `load` — the flow document appears to be stuck', context);
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
logger.error('Flow iframe loaded but never sent READY_FOR_INTERACTION — the embedded app is stuck after its document loaded', context);
|
|
104
|
-
}
|
|
105
|
-
if (this.watchdogReports >= WATCHDOG_MAX_REPORTS) {
|
|
106
|
-
logger.error('Giving up on stuck-flow reporting — the flow never became interactive. If this is the last SDK line in the console, the page stopped executing here', context);
|
|
107
|
-
this.clearLoadWatchdog();
|
|
108
|
-
}
|
|
109
|
-
}, LOAD_WATCHDOG_MS);
|
|
110
|
-
}
|
|
111
|
-
/** Milliseconds since the iframe was injected, for load/ready timings. */
|
|
112
|
-
sinceInject() {
|
|
113
|
-
return this.injectedAt === null ? null : Date.now() - this.injectedAt;
|
|
114
|
-
}
|
|
115
|
-
clearLoadWatchdog() {
|
|
116
|
-
if (this.loadWatchdog !== null) {
|
|
117
|
-
clearInterval(this.loadWatchdog);
|
|
118
|
-
this.loadWatchdog = null;
|
|
119
|
-
}
|
|
27
|
+
this.iframe = document.createElement('iframe');
|
|
28
|
+
this.iframe.src = this.createFlowUrl();
|
|
29
|
+
this.iframe.style.width = '100%';
|
|
30
|
+
this.iframe.style.height = '1000px';
|
|
31
|
+
this.iframe.style.border = 'none';
|
|
32
|
+
this.iframe.style.display = 'block';
|
|
33
|
+
this.container.appendChild(this.iframe);
|
|
120
34
|
}
|
|
121
35
|
/**
|
|
122
36
|
* Construct the specific flow URL - can be overridden by subclasses
|
|
@@ -128,6 +42,11 @@ export class Flow {
|
|
|
128
42
|
* Create flow URL using partner name and environment
|
|
129
43
|
*/
|
|
130
44
|
createFlowUrl() {
|
|
45
|
+
console.log('🔧 Creating flow URL...');
|
|
46
|
+
console.log('📝 Config:', this.config);
|
|
47
|
+
console.log('🏢 Partner:', this.partnerName);
|
|
48
|
+
console.log('🌍 Environment:', this.environment);
|
|
49
|
+
console.log('🔝 Branch Override:', this.branchOverride);
|
|
131
50
|
const baseUrl = getBaseUrl(this.environment);
|
|
132
51
|
let finalUrl = this.constructFlowUrl(baseUrl);
|
|
133
52
|
// Add cdn_branch_override parameter for non-production environments
|
|
@@ -135,125 +54,79 @@ export class Flow {
|
|
|
135
54
|
const separator = finalUrl.includes('?') ? '&' : '?';
|
|
136
55
|
finalUrl += `${separator}cdn_branch_override=${this.branchOverride}`;
|
|
137
56
|
}
|
|
138
|
-
|
|
139
|
-
url: redactUrl(finalUrl),
|
|
140
|
-
partnerName: this.partnerName,
|
|
141
|
-
environment: this.environment,
|
|
142
|
-
branchOverride: this.branchOverride,
|
|
143
|
-
config: describeConfig(this.config),
|
|
144
|
-
});
|
|
57
|
+
console.log('🌐 Final URL:', finalUrl);
|
|
145
58
|
return finalUrl;
|
|
146
59
|
}
|
|
147
60
|
setupEventListeners() {
|
|
148
61
|
// Add post message handlers for internal events - setHeight, scroll etc
|
|
149
62
|
// Also need to implement callbacks for flow completed exit etc in the platform-app
|
|
150
63
|
this.messageHandler = (event) => {
|
|
151
|
-
|
|
152
|
-
|
|
64
|
+
if (!/melio\.com|melioservices\.com/.test(event.origin)) {
|
|
65
|
+
return;
|
|
153
66
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
67
|
+
const { type, ...data } = event.data;
|
|
68
|
+
console.log('📬 Received message from iframe:', {
|
|
69
|
+
type,
|
|
70
|
+
data,
|
|
71
|
+
origin: event.origin,
|
|
72
|
+
});
|
|
73
|
+
switch (type) {
|
|
74
|
+
case 'FLOW_COMPLETED':
|
|
75
|
+
this.emit('completed', data);
|
|
76
|
+
break;
|
|
77
|
+
case 'FLOW_EXIT':
|
|
78
|
+
this.emit('exit');
|
|
79
|
+
break;
|
|
80
|
+
case 'PARTNER_ACTION_REQUIRED':
|
|
81
|
+
// Only the new payment request is routed today; every other
|
|
82
|
+
// requiredAction is ignored until the SDK declares it.
|
|
83
|
+
if (data.data?.requiredAction === 'NAVIGATE_TO_NEW_PAYMENT') {
|
|
84
|
+
this.emit('partnerActionRequired', {
|
|
85
|
+
requiredAction: 'NAVIGATE_TO_NEW_PAYMENT',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
case 'NAVIGATED_TO_TARGET':
|
|
90
|
+
this.emit('navigated', data);
|
|
91
|
+
break;
|
|
92
|
+
case 'ONBOARDING_FORM_COMPLETED':
|
|
93
|
+
this.emit('onboardingCompleted');
|
|
94
|
+
break;
|
|
95
|
+
case 'ONBOARDING_REQUIRED':
|
|
96
|
+
this.emit('onboardingRequired');
|
|
97
|
+
break;
|
|
98
|
+
case 'READY_FOR_INTERACTION':
|
|
99
|
+
this.emit('loaded');
|
|
100
|
+
break;
|
|
101
|
+
case 'PAYMENT_SCHEDULED':
|
|
102
|
+
this.emit('completed', { flowName: 'payment', ...data });
|
|
103
|
+
break;
|
|
104
|
+
case 'MELIO_ERROR':
|
|
105
|
+
if (data.code === 'failed_to_sync_bills') {
|
|
106
|
+
this.emit('error', { errorCode: 'billsSyncFailed' });
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
case 'HEIGHT_CHANGE':
|
|
110
|
+
if (this.iframe) {
|
|
111
|
+
this.iframe.style.height = `${data.height}px`;
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
161
114
|
}
|
|
162
115
|
};
|
|
163
116
|
window.addEventListener('message', this.messageHandler);
|
|
164
117
|
}
|
|
165
|
-
handleMessage(event) {
|
|
166
|
-
if (!/melio\.com|melioservices\.com/.test(event.origin)) {
|
|
167
|
-
// Only surface near-misses. Host pages receive plenty of unrelated
|
|
168
|
-
// postMessage traffic and logging all of it would bury the signal.
|
|
169
|
-
if (/melio/i.test(event.origin)) {
|
|
170
|
-
logger.warn('Ignored message from an unrecognised Melio origin — check the origin allowlist', { origin: event.origin });
|
|
171
|
-
}
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
// Destructuring a null or primitive payload throws, and this handler runs
|
|
175
|
-
// for every message that clears the origin check.
|
|
176
|
-
if (event.data === null || typeof event.data !== 'object') {
|
|
177
|
-
logger.warn('Ignored non-object message from a Melio origin', {
|
|
178
|
-
origin: event.origin,
|
|
179
|
-
dataType: event.data === null ? 'null' : typeof event.data,
|
|
180
|
-
});
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
const { type, ...data } = event.data;
|
|
184
|
-
logger.debug('Received message from flow iframe', {
|
|
185
|
-
type,
|
|
186
|
-
data,
|
|
187
|
-
origin: event.origin,
|
|
188
|
-
});
|
|
189
|
-
switch (type) {
|
|
190
|
-
case 'FLOW_COMPLETED':
|
|
191
|
-
this.emit('completed', data);
|
|
192
|
-
break;
|
|
193
|
-
case 'FLOW_EXIT':
|
|
194
|
-
this.emit('exit');
|
|
195
|
-
break;
|
|
196
|
-
case 'NAVIGATED_TO_TARGET':
|
|
197
|
-
this.emit('navigated', data);
|
|
198
|
-
break;
|
|
199
|
-
case 'ONBOARDING_FORM_COMPLETED':
|
|
200
|
-
this.emit('onboardingCompleted');
|
|
201
|
-
break;
|
|
202
|
-
case 'ONBOARDING_REQUIRED':
|
|
203
|
-
this.emit('onboardingRequired');
|
|
204
|
-
break;
|
|
205
|
-
case 'READY_FOR_INTERACTION':
|
|
206
|
-
this.readyForInteraction = true;
|
|
207
|
-
this.clearLoadWatchdog();
|
|
208
|
-
logger.debug('Flow reported READY_FOR_INTERACTION', {
|
|
209
|
-
readyMs: this.sinceInject(),
|
|
210
|
-
});
|
|
211
|
-
this.emit('loaded');
|
|
212
|
-
break;
|
|
213
|
-
case 'PAYMENT_SCHEDULED':
|
|
214
|
-
this.emit('completed', { flowName: 'payment', ...data });
|
|
215
|
-
break;
|
|
216
|
-
case 'MELIO_ERROR':
|
|
217
|
-
logger.error('Embedded app reported MELIO_ERROR', { ...data });
|
|
218
|
-
if (data.code === 'failed_to_sync_bills') {
|
|
219
|
-
this.emit('error', { errorCode: 'billsSyncFailed' });
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
// Only failed_to_sync_bills maps to a public ErrorData code, so any
|
|
223
|
-
// other code is not forwarded to the host at all. Log it loudly
|
|
224
|
-
// rather than dropping it silently.
|
|
225
|
-
logger.error('MELIO_ERROR code has no mapping and was not forwarded to the host', { code: data.code });
|
|
226
|
-
}
|
|
227
|
-
break;
|
|
228
|
-
case 'HEIGHT_CHANGE':
|
|
229
|
-
if (this.iframe) {
|
|
230
|
-
this.iframe.style.height = `${data.height}px`;
|
|
231
|
-
}
|
|
232
|
-
else {
|
|
233
|
-
logger.warn('Received HEIGHT_CHANGE but the flow iframe no longer exists', { height: data.height });
|
|
234
|
-
}
|
|
235
|
-
break;
|
|
236
|
-
default:
|
|
237
|
-
logger.debug('Unhandled message type from flow iframe', { type });
|
|
238
|
-
break;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
118
|
emit(event, data) {
|
|
242
119
|
const listeners = this.eventListeners.get(event);
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
120
|
+
if (listeners) {
|
|
121
|
+
listeners.forEach((callback) => {
|
|
122
|
+
try {
|
|
123
|
+
callback(data);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
console.error(`Error in ${event} event listener:`, error);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
248
129
|
}
|
|
249
|
-
listeners.forEach((callback) => {
|
|
250
|
-
try {
|
|
251
|
-
callback(data);
|
|
252
|
-
}
|
|
253
|
-
catch (error) {
|
|
254
|
-
logger.error(`Listener for '${event}' threw`, describeError(error));
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
130
|
}
|
|
258
131
|
on(event, callback) {
|
|
259
132
|
if (!this.eventListeners.has(event)) {
|
|
@@ -275,25 +148,12 @@ export class Flow {
|
|
|
275
148
|
* Close the flow and clean up resources
|
|
276
149
|
*/
|
|
277
150
|
close() {
|
|
278
|
-
logger.debug('Closing flow', {
|
|
279
|
-
containerId: this.containerId,
|
|
280
|
-
iframeLoaded: this.iframeLoaded,
|
|
281
|
-
readyForInteraction: this.readyForInteraction,
|
|
282
|
-
});
|
|
283
|
-
this.clearLoadWatchdog();
|
|
284
151
|
if (this.keepAliveInterval) {
|
|
285
152
|
clearInterval(this.keepAliveInterval);
|
|
286
153
|
this.keepAliveInterval = null;
|
|
287
154
|
}
|
|
288
155
|
if (this.iframe && this.container) {
|
|
289
|
-
|
|
290
|
-
this.container.removeChild(this.iframe);
|
|
291
|
-
}
|
|
292
|
-
catch (error) {
|
|
293
|
-
// Throwing here would abandon the rest of teardown, leaving the message
|
|
294
|
-
// listener attached and the keep-alive running.
|
|
295
|
-
logger.error('Failed to remove the flow iframe during close — continuing teardown', describeError(error));
|
|
296
|
-
}
|
|
156
|
+
this.container.removeChild(this.iframe);
|
|
297
157
|
this.iframe = null;
|
|
298
158
|
}
|
|
299
159
|
if (this.messageHandler) {
|
package/dist/flows/InitFlow.d.ts
CHANGED
|
@@ -13,30 +13,11 @@ export declare class InitFlow implements InitFlowInstance {
|
|
|
13
13
|
private partnerName;
|
|
14
14
|
private environment;
|
|
15
15
|
private branchOverride?;
|
|
16
|
-
private loadWatchdog;
|
|
17
|
-
private watchdogReports;
|
|
18
|
-
private iframeLoaded;
|
|
19
|
-
private authResolved;
|
|
20
|
-
private keepAliveTargetLost;
|
|
21
|
-
private injectedAt;
|
|
22
|
-
private keepAlivePings;
|
|
23
16
|
constructor(config: InitConfig, partnerName: string, environment: Environment, branchOverride?: string);
|
|
24
17
|
/**
|
|
25
18
|
* Initialize the init flow by creating and injecting a hidden iframe
|
|
26
19
|
*/
|
|
27
20
|
initialize(): Promise<void>;
|
|
28
|
-
/**
|
|
29
|
-
* Report a flow that has neither loaded nor authenticated within the budget.
|
|
30
|
-
*
|
|
31
|
-
* Diagnostic only: this logs and does nothing else. It deliberately separates
|
|
32
|
-
* "the auth document never loaded" from "it loaded but never authenticated",
|
|
33
|
-
* because those point at different layers and the distinction is invisible
|
|
34
|
-
* from the host page otherwise.
|
|
35
|
-
*/
|
|
36
|
-
private startLoadWatchdog;
|
|
37
|
-
/** Milliseconds since the iframe was injected, for load/auth timings. */
|
|
38
|
-
private sinceInject;
|
|
39
|
-
private clearLoadWatchdog;
|
|
40
21
|
/**
|
|
41
22
|
* Construct the specific flow URL for initialization
|
|
42
23
|
*/
|
|
@@ -49,7 +30,6 @@ export declare class InitFlow implements InitFlowInstance {
|
|
|
49
30
|
* Setup event listeners to only handle authentication events
|
|
50
31
|
*/
|
|
51
32
|
private setupEventListeners;
|
|
52
|
-
private handleMessage;
|
|
53
33
|
/**
|
|
54
34
|
* Emit events to registered listeners
|
|
55
35
|
*/
|
package/dist/flows/InitFlow.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isEmptyString, getBaseUrl } from './utils.js';
|
|
2
|
-
import { logger, redactUrl, describeError, describeConfig, LOAD_WATCHDOG_MS, WATCHDOG_MAX_REPORTS, KEEP_ALIVE_INTERVAL_MS, } from './logger.js';
|
|
3
2
|
/**
|
|
4
3
|
* Standalone InitFlow class for handling initialization with callbacks
|
|
5
4
|
*/
|
|
@@ -10,18 +9,7 @@ export class InitFlow {
|
|
|
10
9
|
this.eventListeners = new Map();
|
|
11
10
|
this.keepAliveInterval = null;
|
|
12
11
|
this.messageHandler = null;
|
|
13
|
-
this.loadWatchdog = null;
|
|
14
|
-
this.watchdogReports = 0;
|
|
15
|
-
this.iframeLoaded = false;
|
|
16
|
-
this.authResolved = false;
|
|
17
|
-
this.keepAliveTargetLost = false;
|
|
18
|
-
this.injectedAt = null;
|
|
19
|
-
this.keepAlivePings = 0;
|
|
20
12
|
if (isEmptyString(config.authCode)) {
|
|
21
|
-
logger.error('Cannot create init flow: authorization code is missing', {
|
|
22
|
-
partnerName,
|
|
23
|
-
environment,
|
|
24
|
-
});
|
|
25
13
|
throw new Error('Authorization code is required for init flow');
|
|
26
14
|
}
|
|
27
15
|
this.authorizationCode = config.authCode;
|
|
@@ -35,101 +23,15 @@ export class InitFlow {
|
|
|
35
23
|
* Initialize the init flow by creating and injecting a hidden iframe
|
|
36
24
|
*/
|
|
37
25
|
async initialize() {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
throw new Error('document.body is not available for iframe injection');
|
|
48
|
-
}
|
|
49
|
-
this.container = document.body;
|
|
50
|
-
this.iframe = document.createElement('iframe');
|
|
51
|
-
this.iframe.src = url;
|
|
52
|
-
this.iframe.style.width = '1px';
|
|
53
|
-
this.iframe.style.height = '1px';
|
|
54
|
-
this.iframe.style.position = 'absolute';
|
|
55
|
-
this.iframe.style.left = '-9999px';
|
|
56
|
-
this.iframe.style.top = '-9999px';
|
|
57
|
-
this.iframe.addEventListener('load', () => {
|
|
58
|
-
this.iframeLoaded = true;
|
|
59
|
-
logger.debug('Auth iframe fired `load`', {
|
|
60
|
-
url: safeUrl,
|
|
61
|
-
loadMs: this.sinceInject(),
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
// Fires for network-level failures. A document that loads but then stalls
|
|
65
|
-
// internally will NOT fire this — that case is covered by the watchdog.
|
|
66
|
-
this.iframe.addEventListener('error', () => {
|
|
67
|
-
this.clearLoadWatchdog();
|
|
68
|
-
logger.error('Auth iframe failed to load', { url: safeUrl });
|
|
69
|
-
});
|
|
70
|
-
this.startLoadWatchdog(safeUrl);
|
|
71
|
-
logger.debug('Injecting hidden auth iframe', {
|
|
72
|
-
url: safeUrl,
|
|
73
|
-
partnerName: this.partnerName,
|
|
74
|
-
environment: this.environment,
|
|
75
|
-
config: describeConfig(this.config),
|
|
76
|
-
});
|
|
77
|
-
this.injectedAt = Date.now();
|
|
78
|
-
this.container.appendChild(this.iframe);
|
|
79
|
-
}
|
|
80
|
-
catch (error) {
|
|
81
|
-
this.clearLoadWatchdog();
|
|
82
|
-
logger.error('Failed to inject auth iframe', {
|
|
83
|
-
url: safeUrl,
|
|
84
|
-
...describeError(error),
|
|
85
|
-
});
|
|
86
|
-
throw error;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Report a flow that has neither loaded nor authenticated within the budget.
|
|
91
|
-
*
|
|
92
|
-
* Diagnostic only: this logs and does nothing else. It deliberately separates
|
|
93
|
-
* "the auth document never loaded" from "it loaded but never authenticated",
|
|
94
|
-
* because those point at different layers and the distinction is invisible
|
|
95
|
-
* from the host page otherwise.
|
|
96
|
-
*/
|
|
97
|
-
startLoadWatchdog(safeUrl) {
|
|
98
|
-
this.loadWatchdog = window.setInterval(() => {
|
|
99
|
-
if (this.iframeLoaded && this.authResolved) {
|
|
100
|
-
this.clearLoadWatchdog();
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
this.watchdogReports += 1;
|
|
104
|
-
const waitedMs = this.watchdogReports * LOAD_WATCHDOG_MS;
|
|
105
|
-
const context = {
|
|
106
|
-
url: safeUrl,
|
|
107
|
-
waitedMs,
|
|
108
|
-
report: `${this.watchdogReports}/${WATCHDOG_MAX_REPORTS}`,
|
|
109
|
-
partnerName: this.partnerName,
|
|
110
|
-
environment: this.environment,
|
|
111
|
-
};
|
|
112
|
-
if (!this.iframeLoaded) {
|
|
113
|
-
logger.error('Auth iframe has not fired `load` — the auth document appears to be stuck', context);
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
logger.error('Auth iframe loaded but never reported AUTHENTICATION_SUCCESS or AUTHENTICATION_ERROR — auth is stuck inside the iframe', context);
|
|
117
|
-
}
|
|
118
|
-
if (this.watchdogReports >= WATCHDOG_MAX_REPORTS) {
|
|
119
|
-
logger.error('Giving up on stuck-flow reporting — the auth flow never resolved. If this is the last SDK line in the console, the page stopped executing here', context);
|
|
120
|
-
this.clearLoadWatchdog();
|
|
121
|
-
}
|
|
122
|
-
}, LOAD_WATCHDOG_MS);
|
|
123
|
-
}
|
|
124
|
-
/** Milliseconds since the iframe was injected, for load/auth timings. */
|
|
125
|
-
sinceInject() {
|
|
126
|
-
return this.injectedAt === null ? null : Date.now() - this.injectedAt;
|
|
127
|
-
}
|
|
128
|
-
clearLoadWatchdog() {
|
|
129
|
-
if (this.loadWatchdog !== null) {
|
|
130
|
-
clearInterval(this.loadWatchdog);
|
|
131
|
-
this.loadWatchdog = null;
|
|
132
|
-
}
|
|
26
|
+
this.container = document.body;
|
|
27
|
+
this.iframe = document.createElement('iframe');
|
|
28
|
+
this.iframe.src = this.createFlowUrl();
|
|
29
|
+
this.iframe.style.width = '1px';
|
|
30
|
+
this.iframe.style.height = '1px';
|
|
31
|
+
this.iframe.style.position = 'absolute';
|
|
32
|
+
this.iframe.style.left = '-9999px';
|
|
33
|
+
this.iframe.style.top = '-9999px';
|
|
34
|
+
this.container.appendChild(this.iframe);
|
|
133
35
|
}
|
|
134
36
|
/**
|
|
135
37
|
* Construct the specific flow URL for initialization
|
|
@@ -159,80 +61,42 @@ export class InitFlow {
|
|
|
159
61
|
*/
|
|
160
62
|
setupEventListeners() {
|
|
161
63
|
this.messageHandler = (event) => {
|
|
162
|
-
|
|
163
|
-
|
|
64
|
+
if (!/melio\.com|melioservices\.com/.test(event.origin)) {
|
|
65
|
+
return;
|
|
164
66
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
67
|
+
const { type, ...data } = event.data;
|
|
68
|
+
console.log('📬 Received message from iframe:', {
|
|
69
|
+
type,
|
|
70
|
+
data,
|
|
71
|
+
origin: event.origin,
|
|
72
|
+
});
|
|
73
|
+
switch (type) {
|
|
74
|
+
case 'AUTHENTICATION_SUCCESS':
|
|
75
|
+
this.emit('authenticationSucceeded');
|
|
76
|
+
break;
|
|
77
|
+
case 'AUTHENTICATION_ERROR':
|
|
78
|
+
this.emit('authenticationFailed');
|
|
79
|
+
break;
|
|
172
80
|
}
|
|
173
81
|
};
|
|
174
82
|
window.addEventListener('message', this.messageHandler);
|
|
175
83
|
}
|
|
176
|
-
handleMessage(event) {
|
|
177
|
-
if (!/melio\.com|melioservices\.com/.test(event.origin)) {
|
|
178
|
-
// Only surface near-misses. Host pages receive plenty of unrelated
|
|
179
|
-
// postMessage traffic and logging all of it would bury the signal.
|
|
180
|
-
if (/melio/i.test(event.origin)) {
|
|
181
|
-
logger.warn('Ignored message from an unrecognised Melio origin — check the origin allowlist', { origin: event.origin });
|
|
182
|
-
}
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
// Destructuring a null or primitive payload throws, and this handler runs
|
|
186
|
-
// for every message that clears the origin check.
|
|
187
|
-
if (event.data === null || typeof event.data !== 'object') {
|
|
188
|
-
logger.warn('Ignored non-object message from a Melio origin', {
|
|
189
|
-
origin: event.origin,
|
|
190
|
-
dataType: event.data === null ? 'null' : typeof event.data,
|
|
191
|
-
});
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
const { type, ...data } = event.data;
|
|
195
|
-
logger.debug('Received message from auth iframe', {
|
|
196
|
-
type,
|
|
197
|
-
data,
|
|
198
|
-
origin: event.origin,
|
|
199
|
-
});
|
|
200
|
-
switch (type) {
|
|
201
|
-
case 'AUTHENTICATION_SUCCESS':
|
|
202
|
-
this.authResolved = true;
|
|
203
|
-
this.clearLoadWatchdog();
|
|
204
|
-
logger.debug('Authentication succeeded');
|
|
205
|
-
this.emit('authenticationSucceeded');
|
|
206
|
-
break;
|
|
207
|
-
case 'AUTHENTICATION_ERROR':
|
|
208
|
-
this.authResolved = true;
|
|
209
|
-
this.clearLoadWatchdog();
|
|
210
|
-
logger.error('Authentication failed', { ...data });
|
|
211
|
-
this.emit('authenticationFailed', data);
|
|
212
|
-
break;
|
|
213
|
-
default:
|
|
214
|
-
logger.debug('Unhandled message type from auth iframe', { type });
|
|
215
|
-
break;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
84
|
/**
|
|
219
85
|
* Emit events to registered listeners
|
|
220
86
|
*/
|
|
221
87
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
222
88
|
emit(event, data) {
|
|
223
89
|
const listeners = this.eventListeners.get(event);
|
|
224
|
-
if (
|
|
225
|
-
|
|
226
|
-
|
|
90
|
+
if (listeners) {
|
|
91
|
+
listeners.forEach((callback) => {
|
|
92
|
+
try {
|
|
93
|
+
callback(data);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
console.error(`Error in ${event} event listener:`, error);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
227
99
|
}
|
|
228
|
-
listeners.forEach((callback) => {
|
|
229
|
-
try {
|
|
230
|
-
callback(data);
|
|
231
|
-
}
|
|
232
|
-
catch (error) {
|
|
233
|
-
logger.error(`Listener for '${event}' threw`, describeError(error));
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
100
|
}
|
|
237
101
|
/**
|
|
238
102
|
* Register an event listener
|
|
@@ -257,24 +121,12 @@ export class InitFlow {
|
|
|
257
121
|
* Close the flow and clean up resources
|
|
258
122
|
*/
|
|
259
123
|
close() {
|
|
260
|
-
logger.debug('Closing init flow', {
|
|
261
|
-
iframeLoaded: this.iframeLoaded,
|
|
262
|
-
authResolved: this.authResolved,
|
|
263
|
-
});
|
|
264
|
-
this.clearLoadWatchdog();
|
|
265
124
|
if (this.keepAliveInterval) {
|
|
266
125
|
clearInterval(this.keepAliveInterval);
|
|
267
126
|
this.keepAliveInterval = null;
|
|
268
127
|
}
|
|
269
128
|
if (this.iframe && this.container) {
|
|
270
|
-
|
|
271
|
-
this.container.removeChild(this.iframe);
|
|
272
|
-
}
|
|
273
|
-
catch (error) {
|
|
274
|
-
// Throwing here would abandon the rest of teardown, leaving the message
|
|
275
|
-
// listener attached and the keep-alive running.
|
|
276
|
-
logger.error('Failed to remove the auth iframe during close — continuing teardown', describeError(error));
|
|
277
|
-
}
|
|
129
|
+
this.container.removeChild(this.iframe);
|
|
278
130
|
this.iframe = null;
|
|
279
131
|
}
|
|
280
132
|
if (this.messageHandler) {
|
|
@@ -284,39 +136,10 @@ export class InitFlow {
|
|
|
284
136
|
this.eventListeners.clear();
|
|
285
137
|
}
|
|
286
138
|
setupKeepAlive() {
|
|
287
|
-
if (this.keepAliveInterval !== null) {
|
|
288
|
-
logger.warn('setupKeepAlive called while a keep-alive is already running — clearing the previous interval to avoid a leak');
|
|
289
|
-
clearInterval(this.keepAliveInterval);
|
|
290
|
-
this.keepAliveInterval = null;
|
|
291
|
-
}
|
|
292
139
|
this.keepAliveInterval = window.setInterval(() => {
|
|
293
|
-
if (
|
|
294
|
-
// Logged once: the interval keeps running, so an unguarded log here
|
|
295
|
-
// would repeat every 30s for the life of the page.
|
|
296
|
-
if (!this.keepAliveTargetLost) {
|
|
297
|
-
this.keepAliveTargetLost = true;
|
|
298
|
-
logger.error('Keep-alive ping has no target — the auth iframe or its content window is gone, every subsequent ping is a no-op', {
|
|
299
|
-
hasIframe: Boolean(this.iframe),
|
|
300
|
-
iframeLoaded: this.iframeLoaded,
|
|
301
|
-
authResolved: this.authResolved,
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
if (this.keepAliveTargetLost) {
|
|
307
|
-
this.keepAliveTargetLost = false;
|
|
308
|
-
logger.warn('Keep-alive target recovered');
|
|
309
|
-
}
|
|
310
|
-
try {
|
|
140
|
+
if (this.iframe && this.iframe.contentWindow) {
|
|
311
141
|
this.iframe.contentWindow.postMessage({ type: 'USER_ACTIVE_PING' }, '*');
|
|
312
|
-
this.keepAlivePings += 1;
|
|
313
|
-
// Doubles as a liveness heartbeat: if these stop while the flow is
|
|
314
|
-
// still open, the page stopped executing rather than merely stalling.
|
|
315
|
-
logger.debug('Keep-alive ping sent', { ping: this.keepAlivePings });
|
|
316
|
-
}
|
|
317
|
-
catch (error) {
|
|
318
|
-
logger.error('Keep-alive ping threw', describeError(error));
|
|
319
142
|
}
|
|
320
|
-
},
|
|
143
|
+
}, 30000); // Send ping every 30 seconds
|
|
321
144
|
}
|
|
322
145
|
}
|
package/dist/flows/utils.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { logger } from './logger.js';
|
|
2
1
|
/**
|
|
3
2
|
* Utility function to get the base URL for a given environment
|
|
4
3
|
*/
|
|
@@ -17,7 +16,6 @@ export function getBaseUrl(environment) {
|
|
|
17
16
|
case 'localhost':
|
|
18
17
|
return 'http://localhost:3005';
|
|
19
18
|
default:
|
|
20
|
-
logger.warn('Unknown environment — falling back to the default base URL, which may not serve this partner', { environment, fallback: 'https://partnerships.melioservices.com' });
|
|
21
19
|
return 'https://partnerships.melioservices.com';
|
|
22
20
|
}
|
|
23
21
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,12 +14,6 @@ export declare class MelioSDK implements MelioSDK {
|
|
|
14
14
|
* Initialize the SDK - triggers auth event without creating iframe
|
|
15
15
|
*/
|
|
16
16
|
init(authenticationCode: string, options: InitOptions): InitFlowInstance;
|
|
17
|
-
/**
|
|
18
|
-
* Shared launch path for every open* flow: log the attempt, report a
|
|
19
|
-
* constructor failure, and make sure a rejected initialize() surfaces rather
|
|
20
|
-
* than becoming an unhandled promise.
|
|
21
|
-
*/
|
|
22
|
-
private startFlow;
|
|
23
17
|
/**
|
|
24
18
|
* Launch the onboarding flow
|
|
25
19
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { InitFlow, OnboardingFlow, PayFlow, JustPayFlow, SettingsFlow, PaymentsDashboardFlow, isEmptyString, } from './flows/index.js';
|
|
2
|
-
import { logger, markInitStart, describeError, describeConfig, describeHost, setDebugEnabled, } from './flows/logger.js';
|
|
3
2
|
/**
|
|
4
3
|
* Main SDK implementation - now partner agnostic
|
|
5
4
|
*/
|
|
@@ -17,101 +16,79 @@ export class MelioSDK {
|
|
|
17
16
|
*/
|
|
18
17
|
init(authenticationCode, options) {
|
|
19
18
|
if (this.initFlow) {
|
|
20
|
-
|
|
19
|
+
console.log('SDK already initialized. Returning existing init flow instance.');
|
|
21
20
|
return this.initFlow;
|
|
22
21
|
}
|
|
23
|
-
// Set before the first log so the init banner honours the flag.
|
|
24
|
-
setDebugEnabled(Boolean(options.debug));
|
|
25
|
-
markInitStart();
|
|
26
22
|
this.partnerName = options.partnerName;
|
|
27
23
|
this.environment = options.environment || 'production';
|
|
28
24
|
this.branchOverride = options.branchOverride;
|
|
29
25
|
this.authCode = authenticationCode;
|
|
30
|
-
|
|
26
|
+
console.log('starting init flow', {
|
|
31
27
|
partnerName: this.partnerName,
|
|
32
28
|
environment: this.environment,
|
|
33
|
-
keepAlive: Boolean(options.keepAlive),
|
|
34
|
-
branchOverride: this.branchOverride,
|
|
35
|
-
...describeHost(),
|
|
36
|
-
});
|
|
37
|
-
let initFlow;
|
|
38
|
-
try {
|
|
39
|
-
initFlow = new InitFlow({ authCode: authenticationCode, containerId: '' }, this.partnerName, this.environment, this.branchOverride);
|
|
40
|
-
}
|
|
41
|
-
catch (error) {
|
|
42
|
-
logger.error('Failed to create the init flow', describeError(error));
|
|
43
|
-
throw error;
|
|
44
|
-
}
|
|
45
|
-
// `initialize()` is intentionally not awaited — `init()` is synchronous by
|
|
46
|
-
// contract — but its rejection must not become an unhandled promise, which
|
|
47
|
-
// is invisible to partners debugging from the console.
|
|
48
|
-
initFlow.initialize().catch((error) => {
|
|
49
|
-
logger.error('Init flow failed to initialize', describeError(error));
|
|
50
29
|
});
|
|
30
|
+
const initFlow = new InitFlow({ authCode: authenticationCode, containerId: '' }, this.partnerName, this.environment, this.branchOverride);
|
|
31
|
+
initFlow.initialize();
|
|
32
|
+
console.log('InitFlow initialized successfully');
|
|
51
33
|
if (options.keepAlive)
|
|
52
34
|
initFlow.setupKeepAlive();
|
|
53
35
|
this.initFlow = initFlow;
|
|
54
36
|
return this.initFlow;
|
|
55
37
|
}
|
|
56
38
|
/**
|
|
57
|
-
*
|
|
58
|
-
* constructor failure, and make sure a rejected initialize() surfaces rather
|
|
59
|
-
* than becoming an unhandled promise.
|
|
39
|
+
* Launch the onboarding flow
|
|
60
40
|
*/
|
|
61
|
-
|
|
41
|
+
openOnboarding(config) {
|
|
62
42
|
if (isEmptyString(this.authCode)) {
|
|
63
|
-
|
|
64
|
-
logger.error(message);
|
|
65
|
-
throw new Error(message);
|
|
66
|
-
}
|
|
67
|
-
logger.debug(`Opening ${flowName}`, {
|
|
68
|
-
partnerName: this.partnerName,
|
|
69
|
-
environment: this.environment,
|
|
70
|
-
config: describeConfig(config),
|
|
71
|
-
});
|
|
72
|
-
let flow;
|
|
73
|
-
try {
|
|
74
|
-
flow = create();
|
|
43
|
+
throw new Error('SDK not initialized. Please call init() before opening onboarding flow.');
|
|
75
44
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
throw error;
|
|
79
|
-
}
|
|
80
|
-
// Not awaited by contract, but a rejection must not vanish.
|
|
81
|
-
flow.initialize().catch((error) => {
|
|
82
|
-
logger.error(`${flowName} failed to initialize`, describeError(error));
|
|
83
|
-
});
|
|
45
|
+
const flow = new OnboardingFlow(config.containerId, config, this.partnerName, this.environment, this.authCode, this.branchOverride);
|
|
46
|
+
flow.initialize();
|
|
84
47
|
return flow;
|
|
85
48
|
}
|
|
86
|
-
/**
|
|
87
|
-
* Launch the onboarding flow
|
|
88
|
-
*/
|
|
89
|
-
openOnboarding(config) {
|
|
90
|
-
return this.startFlow('onboarding flow', config, () => new OnboardingFlow(config.containerId, config, this.partnerName, this.environment, this.authCode, this.branchOverride));
|
|
91
|
-
}
|
|
92
49
|
/**
|
|
93
50
|
* Launch the pay flow
|
|
94
51
|
*/
|
|
95
52
|
openPayFlow(config) {
|
|
96
|
-
|
|
53
|
+
if (isEmptyString(this.authCode)) {
|
|
54
|
+
throw new Error('SDK not initialized. Please call init() before opening pay flow.');
|
|
55
|
+
}
|
|
56
|
+
const flow = new PayFlow(config.containerId, config, this.partnerName, this.environment, this.authCode, this.branchOverride);
|
|
57
|
+
flow.initialize();
|
|
58
|
+
return flow;
|
|
97
59
|
}
|
|
98
60
|
/**
|
|
99
61
|
* Launch the just pay flow (vendor-based payment)
|
|
100
62
|
*/
|
|
101
63
|
openJustPayFlow(config) {
|
|
102
|
-
|
|
64
|
+
if (isEmptyString(this.authCode)) {
|
|
65
|
+
throw new Error('SDK not initialized. Please call init() before opening just pay flow.');
|
|
66
|
+
}
|
|
67
|
+
const flow = new JustPayFlow(config.containerId, config, this.partnerName, this.environment, this.authCode, this.branchOverride);
|
|
68
|
+
flow.initialize();
|
|
69
|
+
return flow;
|
|
103
70
|
}
|
|
104
71
|
/**
|
|
105
72
|
* Launch the settings flow
|
|
106
73
|
*/
|
|
107
74
|
openSettings(config) {
|
|
108
|
-
|
|
75
|
+
if (isEmptyString(this.authCode)) {
|
|
76
|
+
throw new Error('SDK not initialized. Please call init() before opening settings flow.');
|
|
77
|
+
}
|
|
78
|
+
const flow = new SettingsFlow(config.containerId, config, this.partnerName, this.environment, this.authCode, this.branchOverride);
|
|
79
|
+
flow.initialize();
|
|
80
|
+
return flow;
|
|
109
81
|
}
|
|
110
82
|
/**
|
|
111
83
|
* Launch the payments dashboard flow
|
|
112
84
|
*/
|
|
113
85
|
openPaymentsDashboard(config) {
|
|
114
|
-
|
|
86
|
+
if (isEmptyString(this.authCode)) {
|
|
87
|
+
throw new Error('SDK not initialized. Please call init() before opening payments dashboard flow.');
|
|
88
|
+
}
|
|
89
|
+
const flow = new PaymentsDashboardFlow(config.containerId, config, this.partnerName, this.environment, this.authCode, this.branchOverride);
|
|
90
|
+
flow.initialize();
|
|
91
|
+
return flow;
|
|
115
92
|
}
|
|
116
93
|
}
|
|
117
94
|
/**
|
package/dist/types.d.ts
CHANGED
|
@@ -14,16 +14,6 @@ export interface InitOptions {
|
|
|
14
14
|
environment?: Environment;
|
|
15
15
|
/** The branch to use for the melio platform. Defaults to 'main' */
|
|
16
16
|
branchOverride?: string;
|
|
17
|
-
/**
|
|
18
|
-
* If true, the SDK prints debug-level logs to the console on top of the
|
|
19
|
-
* warnings and errors it always prints. Defaults to false.
|
|
20
|
-
*
|
|
21
|
-
* Turn this on to trace a flow that loads but never becomes usable: debug
|
|
22
|
-
* output covers iframe lifecycle, load and auth timings, every postMessage
|
|
23
|
-
* exchanged with the embedded app, and a keep-alive heartbeat proving the
|
|
24
|
-
* page is still executing.
|
|
25
|
-
*/
|
|
26
|
-
debug?: boolean;
|
|
27
17
|
}
|
|
28
18
|
/**
|
|
29
19
|
* Base configuration for all flows
|
|
@@ -139,6 +129,24 @@ export interface NavigationData {
|
|
|
139
129
|
target: string;
|
|
140
130
|
targetAction: 'schedulePayment' | 'scheduleBatchPayments' | 'viewSubscriptionPlans' | 'viewSettingsCollaborators' | 'viewPayment' | 'viewPaidPayment' | 'viewBill' | 'addNewBill' | 'viewVendors' | 'addVendor' | 'viewSettings' | 'viewArInvoices' | 'redirect';
|
|
141
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* An action the embedded Melio app cannot perform itself and is asking your page to
|
|
134
|
+
* perform. Delivered by the `partnerActionRequired` event.
|
|
135
|
+
*
|
|
136
|
+
* `'NAVIGATE_TO_NEW_PAYMENT'` is emitted when the user clicks the new payment button on
|
|
137
|
+
* the payments dashboard: take them to your own supplier selection page, from which you
|
|
138
|
+
* open a payment flow.
|
|
139
|
+
*
|
|
140
|
+
* @remarks
|
|
141
|
+
* Melio cannot navigate your page, so an action with no listener registered is a button
|
|
142
|
+
* that visibly does nothing when clicked.
|
|
143
|
+
*
|
|
144
|
+
* Further actions are added in minor releases, so switch on `requiredAction` and treat an
|
|
145
|
+
* unrecognised value as a no-op rather than an error.
|
|
146
|
+
*/
|
|
147
|
+
export interface PartnerActionData {
|
|
148
|
+
requiredAction: 'NAVIGATE_TO_NEW_PAYMENT';
|
|
149
|
+
}
|
|
142
150
|
/**
|
|
143
151
|
* Flow completion data
|
|
144
152
|
*/
|
|
@@ -160,11 +168,11 @@ export interface ErrorData {
|
|
|
160
168
|
/**
|
|
161
169
|
* Event types that can be listened to
|
|
162
170
|
*/
|
|
163
|
-
export type FlowEventType = 'completed' | 'exit' | 'navigated' | 'authenticationSucceeded' | 'authenticationFailed' | 'error' | 'loaded' | 'onboardingCompleted' | 'onboardingRequired';
|
|
171
|
+
export type FlowEventType = 'completed' | 'exit' | 'partnerActionRequired' | 'navigated' | 'authenticationSucceeded' | 'authenticationFailed' | 'error' | 'loaded' | 'onboardingCompleted' | 'onboardingRequired';
|
|
164
172
|
/**
|
|
165
173
|
* Event callback function types
|
|
166
174
|
*/
|
|
167
|
-
export type FlowEventCallback = ((data: FlowCompletionData | NavigationData | ErrorData) => void) | (() => void);
|
|
175
|
+
export type FlowEventCallback = ((data: FlowCompletionData | NavigationData | ErrorData | PartnerActionData) => void) | (() => void);
|
|
168
176
|
/**
|
|
169
177
|
* Flow instance interface for event handling
|
|
170
178
|
*/
|
|
@@ -176,6 +184,7 @@ export interface FlowInstance {
|
|
|
176
184
|
*/
|
|
177
185
|
on(event: 'completed', callback: (data: FlowCompletionData) => void): void;
|
|
178
186
|
on(event: 'exit', callback: () => void): void;
|
|
187
|
+
on(event: 'partnerActionRequired', callback: (data: PartnerActionData) => void): void;
|
|
179
188
|
on(event: 'navigated', callback: (payload: NavigationData) => void): void;
|
|
180
189
|
on(event: 'authenticationSucceeded', callback: () => void): void;
|
|
181
190
|
on(event: 'authenticationFailed', callback: () => void): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@melio-eng/web-sdk",
|
|
3
|
-
"version": "1.1.0-pr.
|
|
3
|
+
"version": "1.1.0-pr.94.438ff96",
|
|
4
4
|
"description": "Melio Web SDK - Embed core Melio workflows directly into partner UI with minimal effort",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
package/dist/flows/logger.d.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lightweight console logger for the SDK.
|
|
3
|
-
*
|
|
4
|
-
* Every line is prefixed with `[melio-sdk]` so partners can filter SDK output out
|
|
5
|
-
* of a busy host page, and carries `msSinceInit` — elapsed time since `init()` was
|
|
6
|
-
* called. The elapsed time is what makes a *hang* legible in a console capture: a
|
|
7
|
-
* hard failure shows up as an error either way, but a flow that simply never
|
|
8
|
-
* finishes is only visible as a gap on the clock.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* Severity levels, lowest first. `debug` is opt-in via `init({ debug: true })`;
|
|
12
|
-
* `warn` and `error` always print, because a partner debugging a broken flow
|
|
13
|
-
* should not have to enable anything to see that something failed.
|
|
14
|
-
*/
|
|
15
|
-
export type LogLevel = 'debug' | 'warn' | 'error';
|
|
16
|
-
/** Enable or disable debug-level output. Called from `init({ debug })`. */
|
|
17
|
-
export declare const setDebugEnabled: (enabled: boolean) => void;
|
|
18
|
-
/** Whether debug-level output is currently enabled. */
|
|
19
|
-
export declare const isDebugEnabled: () => boolean;
|
|
20
|
-
/**
|
|
21
|
-
* How long a flow may stay unloaded before the SDK reports it as stuck, and how
|
|
22
|
-
* often it keeps repeating that report afterwards.
|
|
23
|
-
*
|
|
24
|
-
* Diagnostic only — reaching it logs, it does not tear the flow down.
|
|
25
|
-
*
|
|
26
|
-
* The repeat matters for diagnosing a page that dies rather than recovers: a
|
|
27
|
-
* stuck flow keeps announcing itself every 15s, so the timestamp of the *last*
|
|
28
|
-
* report marks the moment the tab stopped executing. A one-shot log cannot
|
|
29
|
-
* distinguish "still hanging" from "the renderer was killed".
|
|
30
|
-
*/
|
|
31
|
-
export declare const LOAD_WATCHDOG_MS = 15000;
|
|
32
|
-
/** Stop repeating the stuck-flow report after this many rounds (~2 minutes). */
|
|
33
|
-
export declare const WATCHDOG_MAX_REPORTS = 8;
|
|
34
|
-
/** Interval between keep-alive pings to an initialized flow. */
|
|
35
|
-
export declare const KEEP_ALIVE_INTERVAL_MS = 30000;
|
|
36
|
-
/** Start the SDK clock. Called once from `init()`. */
|
|
37
|
-
export declare const markInitStart: () => void;
|
|
38
|
-
/**
|
|
39
|
-
* Strip credential-bearing query params so a flow URL can be logged safely.
|
|
40
|
-
* On a URL we cannot parse we fall back to the path, never the raw string.
|
|
41
|
-
*/
|
|
42
|
-
export declare const redactUrl: (url: string) => string;
|
|
43
|
-
/** Flatten an unknown throwable into something that survives a console capture. */
|
|
44
|
-
export declare const describeError: (error: unknown) => Record<string, unknown>;
|
|
45
|
-
/**
|
|
46
|
-
* Shallow-copy a flow config with PII redacted, so a config can be logged
|
|
47
|
-
* without printing customer details into the partner's console.
|
|
48
|
-
*/
|
|
49
|
-
export declare const describeConfig: (config: object | undefined) => Record<string, unknown>;
|
|
50
|
-
/**
|
|
51
|
-
* Describe the host environment once at init. The Safari 26.5 regression on
|
|
52
|
-
* Xero's Bills page was only ever reproducible on specific browser builds, and
|
|
53
|
-
* whether we are running inside a frame decides which failure modes are even
|
|
54
|
-
* possible — so both belong in any console capture we are asked to read.
|
|
55
|
-
*/
|
|
56
|
-
export declare const describeHost: () => Record<string, unknown>;
|
|
57
|
-
export declare const logger: {
|
|
58
|
-
debug(message: string, context?: Record<string, unknown>): void;
|
|
59
|
-
warn(message: string, context?: Record<string, unknown>): void;
|
|
60
|
-
error(message: string, context?: Record<string, unknown>): void;
|
|
61
|
-
};
|
package/dist/flows/logger.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lightweight console logger for the SDK.
|
|
3
|
-
*
|
|
4
|
-
* Every line is prefixed with `[melio-sdk]` so partners can filter SDK output out
|
|
5
|
-
* of a busy host page, and carries `msSinceInit` — elapsed time since `init()` was
|
|
6
|
-
* called. The elapsed time is what makes a *hang* legible in a console capture: a
|
|
7
|
-
* hard failure shows up as an error either way, but a flow that simply never
|
|
8
|
-
* finishes is only visible as a gap on the clock.
|
|
9
|
-
*/
|
|
10
|
-
const PREFIX = '[melio-sdk]';
|
|
11
|
-
/** Query params that may carry a credential and must never reach the console. */
|
|
12
|
-
const REDACTED_PARAMS = ['token', 'authCode', 'code'];
|
|
13
|
-
/**
|
|
14
|
-
* Flow-config keys holding a credential or customer PII. Their presence is worth
|
|
15
|
-
* logging, their contents are not — this SDK runs inside a partner's page.
|
|
16
|
-
*
|
|
17
|
-
* `authCode` matters most: it is on both `InitConfig` and `BaseFlowConfig`, so
|
|
18
|
-
* logging a config verbatim prints a live auth token to the console.
|
|
19
|
-
*/
|
|
20
|
-
const REDACTED_CONFIG_KEYS = [
|
|
21
|
-
'authCode',
|
|
22
|
-
'token',
|
|
23
|
-
'code',
|
|
24
|
-
'userDetails',
|
|
25
|
-
'organizationDetails',
|
|
26
|
-
];
|
|
27
|
-
const LEVEL_WEIGHT = {
|
|
28
|
-
debug: 10,
|
|
29
|
-
warn: 20,
|
|
30
|
-
error: 30,
|
|
31
|
-
};
|
|
32
|
-
let minimumWeight = LEVEL_WEIGHT.warn;
|
|
33
|
-
/** Enable or disable debug-level output. Called from `init({ debug })`. */
|
|
34
|
-
export const setDebugEnabled = (enabled) => {
|
|
35
|
-
minimumWeight = enabled ? LEVEL_WEIGHT.debug : LEVEL_WEIGHT.warn;
|
|
36
|
-
};
|
|
37
|
-
/** Whether debug-level output is currently enabled. */
|
|
38
|
-
export const isDebugEnabled = () => minimumWeight <= LEVEL_WEIGHT.debug;
|
|
39
|
-
const shouldLog = (level) => LEVEL_WEIGHT[level] >= minimumWeight;
|
|
40
|
-
/**
|
|
41
|
-
* How long a flow may stay unloaded before the SDK reports it as stuck, and how
|
|
42
|
-
* often it keeps repeating that report afterwards.
|
|
43
|
-
*
|
|
44
|
-
* Diagnostic only — reaching it logs, it does not tear the flow down.
|
|
45
|
-
*
|
|
46
|
-
* The repeat matters for diagnosing a page that dies rather than recovers: a
|
|
47
|
-
* stuck flow keeps announcing itself every 15s, so the timestamp of the *last*
|
|
48
|
-
* report marks the moment the tab stopped executing. A one-shot log cannot
|
|
49
|
-
* distinguish "still hanging" from "the renderer was killed".
|
|
50
|
-
*/
|
|
51
|
-
export const LOAD_WATCHDOG_MS = 15000;
|
|
52
|
-
/** Stop repeating the stuck-flow report after this many rounds (~2 minutes). */
|
|
53
|
-
export const WATCHDOG_MAX_REPORTS = 8;
|
|
54
|
-
/** Interval between keep-alive pings to an initialized flow. */
|
|
55
|
-
export const KEEP_ALIVE_INTERVAL_MS = 30000;
|
|
56
|
-
let initTimestamp = null;
|
|
57
|
-
/** Start the SDK clock. Called once from `init()`. */
|
|
58
|
-
export const markInitStart = () => {
|
|
59
|
-
initTimestamp = Date.now();
|
|
60
|
-
};
|
|
61
|
-
const buildContext = (context) => ({
|
|
62
|
-
...(initTimestamp === null
|
|
63
|
-
? {}
|
|
64
|
-
: { msSinceInit: Date.now() - initTimestamp }),
|
|
65
|
-
...context,
|
|
66
|
-
});
|
|
67
|
-
/**
|
|
68
|
-
* Strip credential-bearing query params so a flow URL can be logged safely.
|
|
69
|
-
* On a URL we cannot parse we fall back to the path, never the raw string.
|
|
70
|
-
*/
|
|
71
|
-
export const redactUrl = (url) => {
|
|
72
|
-
try {
|
|
73
|
-
const parsed = new URL(url);
|
|
74
|
-
REDACTED_PARAMS.forEach((param) => {
|
|
75
|
-
if (parsed.searchParams.has(param)) {
|
|
76
|
-
parsed.searchParams.set(param, 'redacted');
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
return parsed.toString();
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
return `${url.split('?')[0]}?[unparsed-query-redacted]`;
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
/** Flatten an unknown throwable into something that survives a console capture. */
|
|
86
|
-
export const describeError = (error) => {
|
|
87
|
-
if (error instanceof Error) {
|
|
88
|
-
return { name: error.name, message: error.message, stack: error.stack };
|
|
89
|
-
}
|
|
90
|
-
return { thrown: String(error) };
|
|
91
|
-
};
|
|
92
|
-
/**
|
|
93
|
-
* Shallow-copy a flow config with PII redacted, so a config can be logged
|
|
94
|
-
* without printing customer details into the partner's console.
|
|
95
|
-
*/
|
|
96
|
-
export const describeConfig = (config) => {
|
|
97
|
-
if (!config) {
|
|
98
|
-
return {};
|
|
99
|
-
}
|
|
100
|
-
return Object.fromEntries(Object.entries(config).map(([key, value]) => [
|
|
101
|
-
key,
|
|
102
|
-
REDACTED_CONFIG_KEYS.includes(key) && value !== undefined
|
|
103
|
-
? '[redacted]'
|
|
104
|
-
: value,
|
|
105
|
-
]));
|
|
106
|
-
};
|
|
107
|
-
/**
|
|
108
|
-
* Describe the host environment once at init. The Safari 26.5 regression on
|
|
109
|
-
* Xero's Bills page was only ever reproducible on specific browser builds, and
|
|
110
|
-
* whether we are running inside a frame decides which failure modes are even
|
|
111
|
-
* possible — so both belong in any console capture we are asked to read.
|
|
112
|
-
*/
|
|
113
|
-
export const describeHost = () => {
|
|
114
|
-
const host = {};
|
|
115
|
-
if (typeof navigator !== 'undefined') {
|
|
116
|
-
host.userAgent = navigator.userAgent;
|
|
117
|
-
}
|
|
118
|
-
if (typeof document !== 'undefined') {
|
|
119
|
-
host.readyState = document.readyState;
|
|
120
|
-
host.visibilityState = document.visibilityState;
|
|
121
|
-
}
|
|
122
|
-
if (typeof window !== 'undefined') {
|
|
123
|
-
try {
|
|
124
|
-
host.isFramed = window.top !== window.self;
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
127
|
-
// Cross-origin parents throw on access, which itself proves we are framed.
|
|
128
|
-
host.isFramed = true;
|
|
129
|
-
}
|
|
130
|
-
host.origin = window.location?.origin;
|
|
131
|
-
}
|
|
132
|
-
return host;
|
|
133
|
-
};
|
|
134
|
-
export const logger = {
|
|
135
|
-
debug(message, context) {
|
|
136
|
-
if (!shouldLog('debug')) {
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
console.log(`${PREFIX} ${message}`, buildContext(context));
|
|
140
|
-
},
|
|
141
|
-
warn(message, context) {
|
|
142
|
-
if (!shouldLog('warn')) {
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
console.warn(`${PREFIX} ${message}`, buildContext(context));
|
|
146
|
-
},
|
|
147
|
-
error(message, context) {
|
|
148
|
-
if (!shouldLog('error')) {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
console.error(`${PREFIX} ${message}`, buildContext(context));
|
|
152
|
-
},
|
|
153
|
-
};
|