@meshconnect/web-link-sdk 3.12.0 → 3.14.1
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/Link.js +183 -69
- package/README.md +67 -11
- package/backup-bundle/generated.d.ts +2 -0
- package/backup-bundle/generated.js +7 -0
- package/backup-bundle/index.d.ts +21 -0
- package/backup-bundle/index.js +13 -0
- package/backup-bundle/theme.d.ts +9 -0
- package/backup-bundle/theme.js +13 -0
- package/backupFlow.d.ts +72 -0
- package/backupFlow.js +536 -0
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +3 -32
- package/utils/backup.d.ts +93 -0
- package/utils/backup.js +96 -0
- package/utils/backupTier.d.ts +55 -0
- package/utils/backupTier.js +84 -0
- package/utils/event-types.d.ts +23 -2
- package/utils/event-types.js +3 -1
- package/utils/types.d.ts +137 -2
- package/utils/version.d.ts +1 -1
- package/utils/version.js +1 -1
package/Link.js
CHANGED
|
@@ -49,6 +49,8 @@ import { addPopup, buildIframeAllowPolicy, iframeId, removePopup } from './utils
|
|
|
49
49
|
import { isLinkEventTypeKey } from './utils/event-types';
|
|
50
50
|
import { sdkSpecs } from './utils/sdk-specs';
|
|
51
51
|
import { appendQueryParam } from './utils/url';
|
|
52
|
+
import { JIT_REQUEST_MESSAGE_TYPE, SESSION_NONCE_PARAM } from './utils/backup';
|
|
53
|
+
import { createBackupFlow } from './backupFlow';
|
|
52
54
|
import { BridgeParent } from '@meshconnect/uwc-bridge-parent';
|
|
53
55
|
import { createPrewarmIframe, removePrewarmIframe } from './utils/prewarm';
|
|
54
56
|
var currentOptions;
|
|
@@ -66,85 +68,147 @@ function sendMessageToIframe(message) {
|
|
|
66
68
|
console.warn("Mesh SDK: Failed to deliver ".concat(message.type, " message to the iframe - no iframe element found"));
|
|
67
69
|
return;
|
|
68
70
|
}
|
|
69
|
-
|
|
71
|
+
// Tier 2 is a sandboxed, opaque-origin iframe (no pinnable origin) — post with a
|
|
72
|
+
// '*' target; the widget is our own bundled content and authenticates us in turn.
|
|
73
|
+
var target = backup.isTier2() ? '*' : linkTokenOrigin;
|
|
74
|
+
if (!target) {
|
|
70
75
|
console.warn("Mesh SDK: Failed to deliver ".concat(message.type, " message to the iframe - no link token origin found"));
|
|
71
76
|
return;
|
|
72
77
|
}
|
|
73
78
|
try {
|
|
74
|
-
(_a = iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.postMessage(message,
|
|
79
|
+
(_a = iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.postMessage(message, target);
|
|
75
80
|
}
|
|
76
81
|
catch (e) {
|
|
77
82
|
console.error("Mesh SDK: Failed to deliver ".concat(message.type, " message to the iframe"));
|
|
78
83
|
console.error(e);
|
|
79
84
|
}
|
|
80
85
|
}
|
|
86
|
+
// The SDK-backup (deposit-only fallback) flow lives in its own module. It reads and
|
|
87
|
+
// writes the shared Link state through this host bridge; the message handlers below
|
|
88
|
+
// query it for backup state (`isTier2`/`getActiveFlow`/`isFromActiveWidget`) and
|
|
89
|
+
// delegate the backup branches to it (`handleJitRequest`/`completeReadyHandshake`).
|
|
90
|
+
var backupHost = {
|
|
91
|
+
getOptions: function () { return currentOptions; },
|
|
92
|
+
setOptions: function (options) {
|
|
93
|
+
currentOptions = options;
|
|
94
|
+
},
|
|
95
|
+
getLinkTokenOrigin: function () { return linkTokenOrigin; },
|
|
96
|
+
setLinkTokenOrigin: function (origin) {
|
|
97
|
+
linkTokenOrigin = origin;
|
|
98
|
+
},
|
|
99
|
+
setTargetOrigin: function (origin) {
|
|
100
|
+
targetOrigin = origin;
|
|
101
|
+
},
|
|
102
|
+
iframeElement: iframeElement,
|
|
103
|
+
setCurrentIframeId: function (id) {
|
|
104
|
+
currentIframeId = id;
|
|
105
|
+
},
|
|
106
|
+
getBridgeParent: function () { return bridgeParent; },
|
|
107
|
+
setBridgeParent: function (bridge) {
|
|
108
|
+
bridgeParent = bridge;
|
|
109
|
+
},
|
|
110
|
+
addMessageListener: function () { return window.addEventListener('message', eventsListener); },
|
|
111
|
+
removeMessageListener: function () {
|
|
112
|
+
return window.removeEventListener('message', eventsListener);
|
|
113
|
+
},
|
|
114
|
+
sendMessageToIframe: sendMessageToIframe
|
|
115
|
+
};
|
|
116
|
+
var backup = createBackupFlow(backupHost);
|
|
81
117
|
function handleLinkEvent(event) {
|
|
82
118
|
return __awaiter(this, void 0, void 0, function () {
|
|
83
|
-
var payload, payload, payload, payload;
|
|
84
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j
|
|
85
|
-
return __generator(this, function (
|
|
86
|
-
switch (
|
|
87
|
-
case
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
payload:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
119
|
+
var payload, payload, payload, payload, onExit, linkEvent;
|
|
120
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
121
|
+
return __generator(this, function (_k) {
|
|
122
|
+
switch (_k.label) {
|
|
123
|
+
case 0:
|
|
124
|
+
if (!(event.data.type === JIT_REQUEST_MESSAGE_TYPE)) return [3 /*break*/, 2];
|
|
125
|
+
return [4 /*yield*/, backup.handleJitRequest(event)];
|
|
126
|
+
case 1:
|
|
127
|
+
_k.sent();
|
|
128
|
+
return [2 /*return*/];
|
|
129
|
+
case 2:
|
|
130
|
+
switch (event.data.type) {
|
|
131
|
+
case 'brokerageAccountAccessToken': {
|
|
132
|
+
payload = {
|
|
133
|
+
accessToken: event.data.payload
|
|
134
|
+
};
|
|
135
|
+
(_a = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onEvent) === null || _a === void 0 ? void 0 : _a.call(currentOptions, {
|
|
136
|
+
type: 'integrationConnected',
|
|
137
|
+
payload: payload
|
|
138
|
+
});
|
|
139
|
+
(_b = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onIntegrationConnected) === null || _b === void 0 ? void 0 : _b.call(currentOptions, payload);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
case 'delayedAuthentication': {
|
|
143
|
+
payload = {
|
|
144
|
+
delayedAuth: event.data.payload
|
|
145
|
+
};
|
|
146
|
+
(_c = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onEvent) === null || _c === void 0 ? void 0 : _c.call(currentOptions, {
|
|
147
|
+
type: 'integrationConnected',
|
|
148
|
+
payload: payload
|
|
149
|
+
});
|
|
150
|
+
(_d = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onIntegrationConnected) === null || _d === void 0 ? void 0 : _d.call(currentOptions, payload);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case 'transferFinished': {
|
|
154
|
+
payload = event.data.payload;
|
|
155
|
+
(_e = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onEvent) === null || _e === void 0 ? void 0 : _e.call(currentOptions, {
|
|
156
|
+
type: 'transferCompleted',
|
|
157
|
+
payload: payload
|
|
158
|
+
});
|
|
159
|
+
(_f = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onTransferFinished) === null || _f === void 0 ? void 0 : _f.call(currentOptions, payload);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case 'close':
|
|
163
|
+
case 'done': {
|
|
164
|
+
payload = (_g = event.data) === null || _g === void 0 ? void 0 : _g.payload;
|
|
165
|
+
onExit = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onExit;
|
|
166
|
+
window.removeEventListener('message', eventsListener);
|
|
167
|
+
bridgeParent === null || bridgeParent === void 0 ? void 0 : bridgeParent.destroy();
|
|
168
|
+
removePopup();
|
|
169
|
+
backup.reset();
|
|
170
|
+
onExit === null || onExit === void 0 ? void 0 : onExit(payload === null || payload === void 0 ? void 0 : payload.errorMessage, payload);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case 'loaded': {
|
|
174
|
+
sendMessageToIframe({
|
|
175
|
+
type: 'meshSDKSpecs',
|
|
176
|
+
payload: __assign({}, sdkSpecs)
|
|
177
|
+
});
|
|
178
|
+
// Never forward integration access tokens to the backup widget: it is a
|
|
179
|
+
// deposit-only flow served from an independent origin (no shared failure
|
|
180
|
+
// domain with Mesh) and has no use for them — forwarding would leak the
|
|
181
|
+
// user's credentials cross-origin. Gate on the POSITIVE `activeFlow ===
|
|
182
|
+
// 'primary'` (not `!backupSession`): after an aborted re-open the stale
|
|
183
|
+
// backup iframe can still fire `loaded` with the session cleared, and only
|
|
184
|
+
// this positive check withholds the tokens in that dead window.
|
|
185
|
+
if ((currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.accessTokens) &&
|
|
186
|
+
backup.getActiveFlow() === 'primary') {
|
|
187
|
+
sendMessageToIframe({
|
|
188
|
+
type: 'frontAccessTokens',
|
|
189
|
+
payload: currentOptions.accessTokens
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// The backup flow completes its ready handshake + config delivery here, but
|
|
193
|
+
// only for a `loaded` from its own widget window (no-op otherwise).
|
|
194
|
+
backup.completeReadyHandshake(event);
|
|
195
|
+
(_h = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onEvent) === null || _h === void 0 ? void 0 : _h.call(currentOptions, { type: 'pageLoaded' });
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
default: {
|
|
199
|
+
// `backupTierChanged` is generated by the SDK itself (backupFlow) — never
|
|
200
|
+
// accept it from a widget, which could otherwise forge tier transitions.
|
|
201
|
+
if (isLinkEventTypeKey(event.data.type) &&
|
|
202
|
+
event.data.type !== 'backupTierChanged') {
|
|
203
|
+
linkEvent = __assign({}, event.data);
|
|
204
|
+
delete linkEvent[SESSION_NONCE_PARAM];
|
|
205
|
+
(_j = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onEvent) === null || _j === void 0 ? void 0 : _j.call(currentOptions, linkEvent);
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
143
209
|
}
|
|
144
|
-
|
|
145
|
-
}
|
|
210
|
+
return [2 /*return*/];
|
|
146
211
|
}
|
|
147
|
-
return [2 /*return*/];
|
|
148
212
|
});
|
|
149
213
|
});
|
|
150
214
|
}
|
|
@@ -153,14 +217,48 @@ function eventsListener(event) {
|
|
|
153
217
|
return __generator(this, function (_a) {
|
|
154
218
|
switch (_a.label) {
|
|
155
219
|
case 0:
|
|
156
|
-
if (!(
|
|
157
|
-
|
|
220
|
+
if (!backup.isTier2()) return [3 /*break*/, 4];
|
|
221
|
+
if (!(backup.isFromActiveWidget(event) && event.origin === 'null')) return [3 /*break*/, 2];
|
|
222
|
+
return [4 /*yield*/, handleLinkEvent(event)];
|
|
223
|
+
case 1:
|
|
224
|
+
_a.sent();
|
|
158
225
|
return [3 /*break*/, 3];
|
|
159
|
-
case 1: return [4 /*yield*/, handleLinkEvent(event)];
|
|
160
226
|
case 2:
|
|
161
|
-
|
|
227
|
+
console.warn('Ignored backup Tier-2 message from an unexpected source');
|
|
162
228
|
_a.label = 3;
|
|
163
229
|
case 3: return [2 /*return*/];
|
|
230
|
+
case 4:
|
|
231
|
+
if (event.origin !== targetOrigin && event.origin !== linkTokenOrigin) {
|
|
232
|
+
console.warn('Received message from untrusted origin:', event.origin);
|
|
233
|
+
return [2 /*return*/];
|
|
234
|
+
}
|
|
235
|
+
// No flow is live. An aborted re-open (`openLink` with a bad token, or
|
|
236
|
+
// `openLinkBackup` with a bad origin) calls `backup.reset()` — clearing the
|
|
237
|
+
// active-flow marker — but returns early WITHOUT removing the previous iframe or
|
|
238
|
+
// this listener. A late `close`/`transferFinished` from that stale iframe still
|
|
239
|
+
// passes the retained origin check, so gate on the POSITIVE active flow: drop
|
|
240
|
+
// everything while no flow is active.
|
|
241
|
+
if (backup.getActiveFlow() === null) {
|
|
242
|
+
console.warn('Ignored message: no active Link flow');
|
|
243
|
+
return [2 /*return*/];
|
|
244
|
+
}
|
|
245
|
+
// During a BACKUP flow, additionally require the message to come from the active
|
|
246
|
+
// widget window — not just the backup ORIGIN. In embedded mode a reopen into a
|
|
247
|
+
// different iframe leaves the previous one alive on the SAME backup origin, so an
|
|
248
|
+
// origin-only check would let a late `close`/`done`/`transferFinished` from the
|
|
249
|
+
// stale iframe tear down or report a transfer for the current session. A reopen
|
|
250
|
+
// into the SAME embedded iframe keeps its window AND origin, so the message must
|
|
251
|
+
// also echo this open's session nonce (`isFromActiveWidget`). (Primary flow has
|
|
252
|
+
// no backup widget window, so it is gated by origin only, as before.)
|
|
253
|
+
if (backup.getActiveFlow() === 'backup' &&
|
|
254
|
+
!backup.isFromActiveWidget(event)) {
|
|
255
|
+
console.warn('Ignored backup message from an unexpected source');
|
|
256
|
+
return [2 /*return*/];
|
|
257
|
+
}
|
|
258
|
+
return [4 /*yield*/, handleLinkEvent(event)];
|
|
259
|
+
case 5:
|
|
260
|
+
_a.sent();
|
|
261
|
+
return [2 /*return*/];
|
|
164
262
|
}
|
|
165
263
|
});
|
|
166
264
|
});
|
|
@@ -169,6 +267,9 @@ export var createLink = function (options) {
|
|
|
169
267
|
var openLink = function (linkToken, customIframeId) {
|
|
170
268
|
var _a, _b, _c;
|
|
171
269
|
removePrewarmIframe();
|
|
270
|
+
// Clear any prior backup session so its config can't leak into the token
|
|
271
|
+
// path and its pending tier timer can't fire against this iframe.
|
|
272
|
+
backup.reset();
|
|
172
273
|
if (!linkToken) {
|
|
173
274
|
(_a = options === null || options === void 0 ? void 0 : options.onExit) === null || _a === void 0 ? void 0 : _a.call(options, 'Invalid link token!');
|
|
174
275
|
return;
|
|
@@ -186,6 +287,9 @@ export var createLink = function (options) {
|
|
|
186
287
|
(_c = options === null || options === void 0 ? void 0 : options.onExit) === null || _c === void 0 ? void 0 : _c.call(options, 'Invalid link token!');
|
|
187
288
|
return;
|
|
188
289
|
}
|
|
290
|
+
// Committed to the primary (token) flow — safe to forward access tokens on the
|
|
291
|
+
// `loaded` handshake. (`backup.reset()` above cleared any prior flow.)
|
|
292
|
+
backup.setPrimaryFlow();
|
|
189
293
|
linkUrl = addLanguage(linkUrl, currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.language);
|
|
190
294
|
linkUrl = addDisplayFiatCurrency(linkUrl, currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.displayFiatCurrency);
|
|
191
295
|
linkUrl = addTheme(linkUrl, currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.theme);
|
|
@@ -196,6 +300,11 @@ export var createLink = function (options) {
|
|
|
196
300
|
var iframe_1 = document.getElementById(customIframeId);
|
|
197
301
|
if (iframe_1) {
|
|
198
302
|
iframe_1.allow = buildIframeAllowPolicy(linkTokenOrigin);
|
|
303
|
+
// Clear any leftover Tier-2 state from a prior backup session on this
|
|
304
|
+
// reused iframe: `srcdoc` (takes precedence over `src`) and the `sandbox`
|
|
305
|
+
// attribute (would otherwise keep it opaque-origin).
|
|
306
|
+
iframe_1.removeAttribute('srcdoc');
|
|
307
|
+
iframe_1.removeAttribute('sandbox');
|
|
199
308
|
iframe_1.src = linkUrl;
|
|
200
309
|
currentIframeId = customIframeId;
|
|
201
310
|
}
|
|
@@ -214,11 +323,15 @@ export var createLink = function (options) {
|
|
|
214
323
|
bridgeParent = new BridgeParent(iframe);
|
|
215
324
|
}
|
|
216
325
|
};
|
|
326
|
+
var openLinkBackup = function (session, backupOptions) {
|
|
327
|
+
backup.open(options, session, backupOptions);
|
|
328
|
+
};
|
|
217
329
|
var closeLink = function () {
|
|
218
330
|
var _a;
|
|
219
331
|
bridgeParent === null || bridgeParent === void 0 ? void 0 : bridgeParent.destroy();
|
|
220
332
|
removePopup();
|
|
221
333
|
window.removeEventListener('message', eventsListener);
|
|
334
|
+
backup.reset();
|
|
222
335
|
(_a = options.onExit) === null || _a === void 0 ? void 0 : _a.call(options);
|
|
223
336
|
};
|
|
224
337
|
var closeLinkRequested = function () {
|
|
@@ -231,6 +344,7 @@ export var createLink = function (options) {
|
|
|
231
344
|
};
|
|
232
345
|
return {
|
|
233
346
|
openLink: openLink,
|
|
347
|
+
openLinkBackup: openLinkBackup,
|
|
234
348
|
closeLink: closeLink,
|
|
235
349
|
closeLinkRequested: closeLinkRequested
|
|
236
350
|
};
|
package/README.md
CHANGED
|
@@ -94,26 +94,79 @@ After successfull authentication on the Link session, the popup will be closed a
|
|
|
94
94
|
|
|
95
95
|
#### `createLink` arguments
|
|
96
96
|
|
|
97
|
-
| key | type
|
|
98
|
-
| ------------------------ |
|
|
99
|
-
| `onIntegrationConnected` | `(payload: LinkPayload) => void`
|
|
100
|
-
| `onExit` | `((error?: string \| undefined) => void) \| undefined`
|
|
101
|
-
| `onTransferFinished` | `(payload: TransferFinishedPayload) => void`
|
|
102
|
-
| `onEvent` | `(payload: LinkEventType) => void`
|
|
103
|
-
| `accessTokens` | `IntegrationAccessToken[]`
|
|
104
|
-
| `language` | `'en' \| undefined`
|
|
105
|
-
| `displayFiatCurrency` | `'USD' \| undefined`
|
|
106
|
-
| `theme` | `'dark' \| 'light' \| 'system' \| undefined`
|
|
107
|
-
| `renderType` | `'overlay' \| 'embedded' \| undefined`
|
|
97
|
+
| key | type | description |
|
|
98
|
+
| ------------------------ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
99
|
+
| `onIntegrationConnected` | `((payload: LinkPayload) => void) \| undefined` | Callback called when users connects their accounts |
|
|
100
|
+
| `onExit` | `((error?: string \| undefined) => void) \| undefined` | Called if connection not happened |
|
|
101
|
+
| `onTransferFinished` | `((payload: TransferFinishedPayload) => void) \| undefined` | Callback called when a crypto transfer is executed |
|
|
102
|
+
| `onEvent` | `((payload: LinkEventType) => void) \| undefined` | A callback function that is called when various events occur within the Front iframe |
|
|
103
|
+
| `accessTokens` | `IntegrationAccessToken[]` | An array of integration access tokens |
|
|
104
|
+
| `language` | `'en' \| undefined` | Link UI language |
|
|
105
|
+
| `displayFiatCurrency` | `'USD' \| undefined` | A fiat currency to display fiat equivalent of a crypto amount |
|
|
106
|
+
| `theme` | `'dark' \| 'light' \| 'system' \| undefined` | Color theme of Link UI interface |
|
|
107
|
+
| `renderType` | `'overlay' \| 'embedded' \| undefined` | `'overlay'` (default) renders a full-screen popup; `'embedded'` renders inside a client-supplied iframe (requires `customIframeId` in `openLink`) |
|
|
108
|
+
|
|
109
|
+
All callbacks are optional.
|
|
110
|
+
|
|
111
|
+
#### Withdrawal events
|
|
112
|
+
|
|
113
|
+
When a user confirms a withdrawal, `onEvent` receives a `withdrawalRequested` event, then Link closes and calls `onExit`.
|
|
114
|
+
With `renderType: 'embedded'`, the SDK does not remove your iframe: hide it in `onExit`.
|
|
115
|
+
Keep the `transferId` and continue the withdrawal once Link has closed, for example with your own 2FA prompt.
|
|
116
|
+
Treat the event, not `onExit`, as confirmation of the withdrawal.
|
|
117
|
+
The payload carries no address or amount: read the transfer details from the webhook or the transfer API.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
onEvent: event => {
|
|
121
|
+
if (event.type === 'withdrawalRequested') {
|
|
122
|
+
const { transferId, status } = event.payload // 'pending' or 'success'; treat any other value as pending
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
108
126
|
|
|
109
127
|
#### `createLink` return value
|
|
110
128
|
|
|
111
129
|
| key | type | description |
|
|
112
130
|
| -------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
113
131
|
| `openLink` | `(linkToken: string, customIframeId?: string) => void` | Opens the Link UI popup. Optionally targets an existing iframe by ID instead of creating a new popup |
|
|
132
|
+
| `openLinkBackup` | `(backupConfig: MeshBackupConfig, options?: MeshBackupOptions) => void` | Opens the deposit-only **backup flow** when the Mesh API is unavailable (see below) |
|
|
114
133
|
| `closeLink` | `() => void` | Closes the Link UI popup immediately |
|
|
115
134
|
| `closeLinkRequested` | `() => void` | Requests graceful close in `embedded` mode (sends `closeRequested` to iframe); closes immediately otherwise |
|
|
116
135
|
|
|
136
|
+
### Backup / outage flow
|
|
137
|
+
|
|
138
|
+
If a `linktoken` request fails in a way that indicates a Mesh outage (network/DNS error, timeout, HTTP 5xx or 429), you can open a **deposit-only backup flow** instead of the normal flow. It runs select-token → select-network → QR / copy-address from Mesh infrastructure on a separate domain, and resolves deposit addresses either from static config or through callbacks you provide.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const link = createLink({
|
|
142
|
+
onIntegrationConnected, onTransferFinished, onEvent, onExit,
|
|
143
|
+
|
|
144
|
+
// JIT callbacks — required only if any destination omits `address`. They run in
|
|
145
|
+
// YOUR app with YOUR session; only (symbol, networkId) and the resolved address
|
|
146
|
+
// cross the bridge. `fetch` resolves on 4xx/5xx, so throw on a non-OK response to
|
|
147
|
+
// fail closed.
|
|
148
|
+
onAddressInit: async (symbol, networkId) => {
|
|
149
|
+
const res = await api.post('/wallets/addresses/assign', { currency: symbol, network_id: networkId })
|
|
150
|
+
if (!res.ok) throw new Error('address init failed')
|
|
151
|
+
},
|
|
152
|
+
onStatusPoll: async (symbol, networkId) => {
|
|
153
|
+
const res = await api.get('/wallets/deposit_addresses', { currency: symbol })
|
|
154
|
+
if (!res.ok) throw new Error('status poll failed')
|
|
155
|
+
const m = (await res.json()).deposit_addresses.find(a => a.network_id === networkId && a.address)
|
|
156
|
+
return m ? { status: 'ready', address: m.address, addressTag: m.address_tag } : { status: 'pending' }
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// Normal path:
|
|
161
|
+
link.openLink(linkToken)
|
|
162
|
+
// Outage path — same handlers, no link token:
|
|
163
|
+
link.openLinkBackup(backupConfig)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Callbacks (`onAddressInit`, `onStatusPoll`) are passed to `createLink`. `onAddressInit(symbol, networkId)` kicks off generation (its return is ignored; a throw/reject is a failure). `onStatusPoll(symbol, networkId)` returns `Promise<{ status: 'pending' | 'ready' | 'failed'; address?; addressTag? }>` and is polled until `ready`/`failed`; it must be idempotent per `(symbol, networkId)`. Supply both only if any `backupConfig.destinations[]` entry omits `address`.
|
|
167
|
+
|
|
168
|
+
> **⚠️ Host CSP requirements (web only).** Serve the embedding page over **https**. Your Content-Security-Policy's `frame-src`/`child-src` (or `default-src`) must allow **the backup widget origin** (`https://backup.meshconnect.com` by default) and **`blob:`** — the bundled last-resort fallback renders as a sandboxed `blob:` iframe, so a CSP that pins `frame-src` to the primary Mesh origin will block it and the fallback will fail. (React Native is not affected — it uses a native WebView, not an iframe.)
|
|
169
|
+
|
|
117
170
|
### Using tokens
|
|
118
171
|
|
|
119
172
|
You can use broker tokens to perform requests to get current balance, assets and execute transactions. Full API reference can be found [here](https://integration-api.meshconnect.com/apireference).
|
|
@@ -134,6 +187,9 @@ TypeScript definitions for `@meshconnect/web-link-sdk` are built into the npm pa
|
|
|
134
187
|
| `BrokerType` | Union of supported broker/integration type strings (re-exported from `@meshconnect/node-api`) |
|
|
135
188
|
| `LinkOptions` | Full options object passed to `createLink` |
|
|
136
189
|
| `Link` | Return type of `createLink` |
|
|
190
|
+
| `MeshBackupConfig` | Config passed to `openLinkBackup` (clientId, userId, destinations, preselectedSymbol) |
|
|
191
|
+
| `MeshBackupOptions` | Options for `openLinkBackup` (e.g. `customIframeId`, `widgetOrigin`) |
|
|
192
|
+
| `MeshBackupStatusResult` | Return type of the `onStatusPoll` callback |
|
|
137
193
|
| `AccountToken` | Account token within `AccessTokenPayload` |
|
|
138
194
|
| `Account` | Account details within `AccountToken` |
|
|
139
195
|
| `BrandInfo` | Integration brand/logo info |
|