@flonkid/kyc 1.8.2 → 1.9.0
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 +108 -2
- package/dist/index.cjs +318 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +128 -4
- package/dist/index.d.ts +128 -4
- package/dist/index.js +313 -14
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +102 -32
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +938 -36
- package/dist/server.d.ts +938 -36
- package/dist/server.js +103 -33
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +950 -34
- package/package.json +78 -49
package/README.md
CHANGED
|
@@ -124,9 +124,16 @@ widget.destroy();
|
|
|
124
124
|
```typescript
|
|
125
125
|
import { FlonkKYCServer } from '@flonkid/kyc/server';
|
|
126
126
|
|
|
127
|
-
const flonk = new FlonkKYCServer({
|
|
127
|
+
const flonk = new FlonkKYCServer({
|
|
128
|
+
secretKey: 'sk_live_...',
|
|
129
|
+
// Transient failures (429 / 5xx / network) are retried with jittered
|
|
130
|
+
// exponential backoff. GETs always retry; writes retry only when idempotent.
|
|
131
|
+
maxRetries: 2, // default; set 0 to disable
|
|
132
|
+
});
|
|
128
133
|
|
|
129
|
-
// Create session
|
|
134
|
+
// Create session — idempotent: a retry (same key) returns the original session,
|
|
135
|
+
// never a duplicate. A key is auto-generated per call; pass `idempotencyKey` to
|
|
136
|
+
// make a specific create idempotent across process restarts.
|
|
130
137
|
const session = await flonk.createSession({
|
|
131
138
|
clientMetadata: { email: 'user@example.com', userId: 'user_123' },
|
|
132
139
|
expiryMinutes: 30,
|
|
@@ -162,6 +169,105 @@ switch (event.type) {
|
|
|
162
169
|
}
|
|
163
170
|
```
|
|
164
171
|
|
|
172
|
+
### Signature formats & replay protection
|
|
173
|
+
|
|
174
|
+
Flonk sends **two** signature headers, both HMAC-SHA256 with your webhook
|
|
175
|
+
secret; `constructEvent` verifies whichever you pass. Both prove authenticity +
|
|
176
|
+
integrity — the only difference is replay protection:
|
|
177
|
+
|
|
178
|
+
| Header | Format | Replay-protected? |
|
|
179
|
+
|--------|--------|-------------------|
|
|
180
|
+
| `X-Signature` | `t=<unix>, v1=<hex>` | ✅ Timestamp is signed; requests outside the skew window (default 300s) are rejected. |
|
|
181
|
+
| `X-Signature-256` | `sha256=<hex>` | ❌ No timestamp — a captured request stays valid (close the gap with event-id dedup, below). |
|
|
182
|
+
|
|
183
|
+
Both are fully valid signatures; prefer `X-Signature` when you want replay
|
|
184
|
+
protection at the signature layer. Verification is constant-time.
|
|
185
|
+
|
|
186
|
+
Since delivery is **at-least-once** (Flonk retries on non-200), also dedupe by
|
|
187
|
+
`event.id` so a retry is processed once. Do this in your own store — a unique
|
|
188
|
+
DB index, or Redis `SET id 1 NX EX <ttl>` — exactly as you would for Stripe:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
app.post('/webhooks/flonk', async (req, res) => {
|
|
192
|
+
const event = flonk.webhooks.constructEvent(req.rawBody, req.headers['x-signature'], secret);
|
|
193
|
+
|
|
194
|
+
// Idempotency: SET NX returns null if the key already existed → it's a retry.
|
|
195
|
+
const fresh = await redis.set(`whk:${event.id}`, '1', 'PX', 6 * 60_000, 'NX');
|
|
196
|
+
if (fresh === null) return res.status(200).end(); // already handled
|
|
197
|
+
|
|
198
|
+
await process(event);
|
|
199
|
+
res.status(200).end();
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Content Security Policy & CORS
|
|
204
|
+
|
|
205
|
+
The widget runs in an iframe on `widget.flonk.id` and loads a small branded
|
|
206
|
+
loader script from the API. If your site sends a `Content-Security-Policy`
|
|
207
|
+
header, allow our origins:
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
Content-Security-Policy:
|
|
211
|
+
frame-src https://widget.flonk.id;
|
|
212
|
+
script-src https://widget.flonk.id https://api.flonk.id;
|
|
213
|
+
connect-src https://api.flonk.id https://widget.flonk.id;
|
|
214
|
+
img-src https://widget.flonk.id data:;
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
- `frame-src` — the widget iframe (`widget.flonk.id`).
|
|
218
|
+
- `script-src` — `widget.js` (`widget.flonk.id`) and the server-hosted loader
|
|
219
|
+
(`api.flonk.id/v1/public/loader.js`). If the loader script is blocked, the SDK
|
|
220
|
+
**falls back to its bundled loader** — it still works, just without
|
|
221
|
+
dashboard-driven branding/fixes.
|
|
222
|
+
- `connect-src` — session/token/design-token fetches.
|
|
223
|
+
|
|
224
|
+
You do **not** need any CORS or `Cross-Origin-Resource-Policy` configuration on
|
|
225
|
+
your side. Our public assets (`loader.js`, `loader-config`, `design-tokens`)
|
|
226
|
+
already send `Cross-Origin-Resource-Policy: cross-origin`, and the API handles
|
|
227
|
+
CORS for the SDK's `fetch`/`POST` calls.
|
|
228
|
+
|
|
229
|
+
### Debugging blocked resources
|
|
230
|
+
|
|
231
|
+
Every degradation is observable — no silent failures. Pass `onDiagnostic`, or
|
|
232
|
+
flip the global debug flag to mirror events to the console:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
const kyc = new FlonkKYC({
|
|
236
|
+
onDiagnostic: (e) => console.log(`[flonk:${e.code}] ${e.message}`, e.detail),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Or, without code — anywhere before the widget opens:
|
|
240
|
+
window.__FLONK_DEBUG__ = true; // SDK + widget.js both honor this
|
|
241
|
+
// <script ... data-debug> also enables it for widget.js
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Codes you may see when something is blocked or degraded:
|
|
245
|
+
|
|
246
|
+
| Code | Meaning |
|
|
247
|
+
|------|---------|
|
|
248
|
+
| `LOADER_SCRIPT_BLOCKED` | Server loader script failed to load (CSP `script-src`, CORP, offline). Bundled loader used. |
|
|
249
|
+
| `LOADER_FALLBACK_BUNDLED` | Server loader wasn't ready at show time; bundled loader used. |
|
|
250
|
+
| `PREWARM_SKIPPED` | Prewarm disabled (`prewarm: 'none'`) or no DOM (SSR). |
|
|
251
|
+
| `READY_TIMEOUT_REVEAL` | No `READY` from the iframe within 4s; revealed on safety timeout (check `frame-src`). |
|
|
252
|
+
| `IFRAME_FRESH` | A fresh widget iframe was built. |
|
|
253
|
+
|
|
254
|
+
Console output is silent unless `onDiagnostic` is set or `__FLONK_DEBUG__` is
|
|
255
|
+
truthy — zero noise in production by default.
|
|
256
|
+
|
|
257
|
+
## Versioning
|
|
258
|
+
|
|
259
|
+
Three versions, deliberately separate:
|
|
260
|
+
|
|
261
|
+
- **Package version** (npm) — changes every release.
|
|
262
|
+
- **SDK↔iframe wire protocol** — the widget iframe and SDK deploy independently,
|
|
263
|
+
so the wire is additive-only; a `PROTOCOL_VERSION_MISMATCH` diagnostic surfaces
|
|
264
|
+
a stale-cached peer. Details: [`CONTRACT.md`](./CONTRACT.md).
|
|
265
|
+
- **REST API version** — date-pinned, sent as the `Flonk-Version` header on every
|
|
266
|
+
server request. The API is additive-only within a version; a breaking change
|
|
267
|
+
would mint a new date and serve the old shape to SDKs pinned to the old one.
|
|
268
|
+
Override with `new FlonkKYCServer({ apiVersion: '2026-06-01' })`; the server
|
|
269
|
+
echoes the resolved version back in the `Flonk-Version` response header.
|
|
270
|
+
|
|
165
271
|
## Links
|
|
166
272
|
|
|
167
273
|
- [Full Documentation](https://docs.flonk.id)
|
package/dist/index.cjs
CHANGED
|
@@ -4,14 +4,31 @@ var react = require('react');
|
|
|
4
4
|
var jsxRuntime = require('react/jsx-runtime');
|
|
5
5
|
|
|
6
6
|
// src/shared/constants.ts
|
|
7
|
-
var SDK_VERSION = "1.
|
|
7
|
+
var SDK_VERSION = "1.9.0";
|
|
8
8
|
var DEFAULT_WIDGET_URL = "https://widget.flonk.id";
|
|
9
9
|
var DEFAULT_API_BASE = "https://api.flonk.id/v1";
|
|
10
|
+
var API_VERSION = "2026-06-01";
|
|
11
|
+
var PROTOCOL_VERSION = 1;
|
|
10
12
|
var WIDGET_EVENTS = {
|
|
11
13
|
READY: "KYC_WIDGET_READY",
|
|
12
14
|
COMPLETE: "KYC_COMPLETE",
|
|
13
15
|
CANCEL: "KYC_CANCEL",
|
|
14
|
-
ERROR: "KYC_ERROR"
|
|
16
|
+
ERROR: "KYC_ERROR",
|
|
17
|
+
CONFIG: "KYC_WIDGET_CONFIG"
|
|
18
|
+
};
|
|
19
|
+
var WIDGET_PARAMS = {
|
|
20
|
+
PROTOCOL_VERSION: "pv",
|
|
21
|
+
SESSION_ID: "sessionId",
|
|
22
|
+
EMBED_TOKEN: "embedToken",
|
|
23
|
+
TOKEN: "token",
|
|
24
|
+
PUBLISHABLE_KEY: "publishableKey",
|
|
25
|
+
CLIENT_ID: "clientId",
|
|
26
|
+
CLIENT_METADATA: "clientMetadata",
|
|
27
|
+
DESIGN_TOKENS: "designTokens",
|
|
28
|
+
ALLOW_MANUAL_UPLOAD: "allowManualUpload",
|
|
29
|
+
LANG: "lang",
|
|
30
|
+
OVERLAY_COLOR: "overlayColor",
|
|
31
|
+
MODE: "mode"
|
|
15
32
|
};
|
|
16
33
|
|
|
17
34
|
// src/shared/errors.ts
|
|
@@ -229,10 +246,14 @@ function createIframe(src) {
|
|
|
229
246
|
top: "0",
|
|
230
247
|
left: "0",
|
|
231
248
|
zIndex: "9999",
|
|
232
|
-
|
|
233
|
-
|
|
249
|
+
// Hidden until reveal so the loader overlay doesn't show the iframe's own
|
|
250
|
+
// loading state through its translucent backdrop (double loader). Reveal
|
|
251
|
+
// is gated on READY-OR-a-timeout (see index.ts), so a missing READY can't
|
|
252
|
+
// leave it permanently hidden — that timeout is what removed the deadlock.
|
|
234
253
|
opacity: "0",
|
|
235
254
|
visibility: "hidden",
|
|
255
|
+
background: "transparent",
|
|
256
|
+
backgroundColor: "transparent",
|
|
236
257
|
borderRadius: d ? "0" : "",
|
|
237
258
|
boxShadow: d ? "none" : "",
|
|
238
259
|
colorScheme: "normal"
|
|
@@ -267,15 +288,15 @@ function adjustZIndex(loader, iframe) {
|
|
|
267
288
|
function transitionLoaderToIframe(loader, iframe, onDone) {
|
|
268
289
|
const d = isDesktop();
|
|
269
290
|
const dur = d ? 300 : 500;
|
|
270
|
-
setStyles(iframe, { opacity: "0", visibility: "hidden" });
|
|
271
291
|
const card = loader.querySelector("div");
|
|
272
292
|
if (card) {
|
|
273
293
|
setStyles(card, {
|
|
294
|
+
transition: "transform 300ms ease-out, opacity 300ms ease-out",
|
|
274
295
|
transform: d ? "translateY(-10px) scale(0.98)" : "translateY(-15px) scale(0.96)",
|
|
275
296
|
opacity: "0"
|
|
276
297
|
});
|
|
277
298
|
}
|
|
278
|
-
loader
|
|
299
|
+
setStyles(loader, { transition: "opacity 300ms ease-out", opacity: "0" });
|
|
279
300
|
setTimeout(() => {
|
|
280
301
|
onDone();
|
|
281
302
|
setStyles(iframe, {
|
|
@@ -286,6 +307,160 @@ function transitionLoaderToIframe(loader, iframe, onDone) {
|
|
|
286
307
|
}, dur);
|
|
287
308
|
}
|
|
288
309
|
|
|
310
|
+
// src/browser/diagnostics.ts
|
|
311
|
+
var handlers = /* @__PURE__ */ new Set();
|
|
312
|
+
function addDiagnosticHandler(handler) {
|
|
313
|
+
handlers.add(handler);
|
|
314
|
+
return () => {
|
|
315
|
+
handlers.delete(handler);
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function debugEnabled() {
|
|
319
|
+
try {
|
|
320
|
+
return Boolean(globalThis.__FLONK_DEBUG__);
|
|
321
|
+
} catch {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function emitDiagnostic(code, level, message, detail) {
|
|
326
|
+
const event = { code, level, message, detail };
|
|
327
|
+
if (debugEnabled()) {
|
|
328
|
+
try {
|
|
329
|
+
const fn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
|
|
330
|
+
fn(`[flonk:${code}] ${message}`, detail ?? "");
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
for (const handler of handlers) {
|
|
335
|
+
try {
|
|
336
|
+
handler(event);
|
|
337
|
+
} catch {
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/browser/prewarm.ts
|
|
343
|
+
var noop = () => {
|
|
344
|
+
};
|
|
345
|
+
function originOf(url) {
|
|
346
|
+
try {
|
|
347
|
+
return new URL(url).origin;
|
|
348
|
+
} catch {
|
|
349
|
+
return url.replace(/\/+$/, "");
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function addLinkHint(doc, rel, href, attrs) {
|
|
353
|
+
try {
|
|
354
|
+
const existing = doc.querySelector(`link[rel="${rel}"][href="${href}"]`);
|
|
355
|
+
if (existing) return;
|
|
356
|
+
const link = doc.createElement("link");
|
|
357
|
+
link.rel = rel;
|
|
358
|
+
link.href = href;
|
|
359
|
+
if (attrs) for (const k of Object.keys(attrs)) link.setAttribute(k, attrs[k]);
|
|
360
|
+
(doc.head || doc.documentElement).appendChild(link);
|
|
361
|
+
} catch {
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function preconnect(widgetUrl, doc = document) {
|
|
365
|
+
const origin = originOf(widgetUrl);
|
|
366
|
+
addLinkHint(doc, "preconnect", origin, { crossorigin: "" });
|
|
367
|
+
addLinkHint(doc, "dns-prefetch", origin);
|
|
368
|
+
}
|
|
369
|
+
function defaultScheduleIdle(fn) {
|
|
370
|
+
if (typeof window === "undefined") {
|
|
371
|
+
fn();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const w = window;
|
|
375
|
+
const run = () => {
|
|
376
|
+
if (typeof w.requestIdleCallback === "function") {
|
|
377
|
+
w.requestIdleCallback(fn, { timeout: 2e3 });
|
|
378
|
+
} else {
|
|
379
|
+
setTimeout(fn, 200);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
if (document.readyState === "complete") run();
|
|
383
|
+
else window.addEventListener("load", run, { once: true });
|
|
384
|
+
}
|
|
385
|
+
function prewarm(options) {
|
|
386
|
+
const level = options.level ?? "connect";
|
|
387
|
+
const doc = options.doc ?? (typeof document !== "undefined" ? document : void 0);
|
|
388
|
+
if (level === "none" || !doc) {
|
|
389
|
+
emitDiagnostic("PREWARM_SKIPPED", "info", `Prewarm skipped (level=${level}${doc ? "" : ", no document"}).`);
|
|
390
|
+
return noop;
|
|
391
|
+
}
|
|
392
|
+
const scheduleIdle = options.scheduleIdle ?? defaultScheduleIdle;
|
|
393
|
+
const origin = originOf(options.widgetUrl);
|
|
394
|
+
preconnect(options.widgetUrl, doc);
|
|
395
|
+
const warmAssets = () => {
|
|
396
|
+
if (options.apiBase) {
|
|
397
|
+
const q = options.publishableKey ? `pk=${encodeURIComponent(options.publishableKey)}` : options.sessionId ? `sessionId=${encodeURIComponent(options.sessionId)}` : "";
|
|
398
|
+
addLinkHint(doc, "prefetch", `${options.apiBase}/v1/public/design-tokens${q ? `?${q}` : ""}`);
|
|
399
|
+
}
|
|
400
|
+
addLinkHint(doc, "prefetch", `${origin}/`, { as: "document" });
|
|
401
|
+
};
|
|
402
|
+
if (level === "intent") {
|
|
403
|
+
return attachIntent(doc, options.trigger ?? null, () => {
|
|
404
|
+
warmAssets();
|
|
405
|
+
mountHiddenIframe(doc, origin);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
scheduleIdle(() => {
|
|
409
|
+
warmAssets();
|
|
410
|
+
if (level === "eager") mountHiddenIframe(doc, origin);
|
|
411
|
+
});
|
|
412
|
+
return noop;
|
|
413
|
+
}
|
|
414
|
+
function attachIntent(doc, trigger, warm) {
|
|
415
|
+
let fired = false;
|
|
416
|
+
const fire = () => {
|
|
417
|
+
if (fired) return;
|
|
418
|
+
fired = true;
|
|
419
|
+
cleanup();
|
|
420
|
+
warm();
|
|
421
|
+
};
|
|
422
|
+
const events = [
|
|
423
|
+
["mouseenter", fire],
|
|
424
|
+
["focusin", fire],
|
|
425
|
+
["touchstart", fire]
|
|
426
|
+
];
|
|
427
|
+
let io = null;
|
|
428
|
+
const cleanup = () => {
|
|
429
|
+
if (trigger) for (const [type, fn] of events) trigger.removeEventListener(type, fn);
|
|
430
|
+
if (io) {
|
|
431
|
+
io.disconnect();
|
|
432
|
+
io = null;
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
if (trigger) {
|
|
436
|
+
for (const [type, fn] of events) trigger.addEventListener(type, fn, { passive: true });
|
|
437
|
+
const w = doc.defaultView || (typeof window !== "undefined" ? window : void 0);
|
|
438
|
+
if (w && typeof w.IntersectionObserver === "function") {
|
|
439
|
+
const observer = new w.IntersectionObserver((entries) => {
|
|
440
|
+
if (entries.some((e) => e.isIntersecting)) fire();
|
|
441
|
+
});
|
|
442
|
+
observer.observe(trigger);
|
|
443
|
+
io = observer;
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
defaultScheduleIdle(fire);
|
|
447
|
+
}
|
|
448
|
+
return cleanup;
|
|
449
|
+
}
|
|
450
|
+
function mountHiddenIframe(doc, origin) {
|
|
451
|
+
try {
|
|
452
|
+
if (doc.querySelector("iframe[data-flonk-prewarm]")) return;
|
|
453
|
+
const iframe = doc.createElement("iframe");
|
|
454
|
+
iframe.src = `${origin}/?prewarm=1`;
|
|
455
|
+
iframe.setAttribute("data-flonk-prewarm", "1");
|
|
456
|
+
iframe.setAttribute("aria-hidden", "true");
|
|
457
|
+
iframe.tabIndex = -1;
|
|
458
|
+
iframe.style.cssText = "position:absolute;width:1px;height:1px;left:-9999px;top:-9999px;border:0;opacity:0;pointer-events:none";
|
|
459
|
+
(doc.body || doc.documentElement).appendChild(iframe);
|
|
460
|
+
} catch {
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
289
464
|
// src/browser/loader.ts
|
|
290
465
|
var LOADER_I18N = {
|
|
291
466
|
en: { title: "Initializing...", subtitle: "Loading KYC widget", errorTitle: "Something went wrong", close: "Close" },
|
|
@@ -535,8 +710,81 @@ var Loader = class {
|
|
|
535
710
|
this.cleanup = null;
|
|
536
711
|
}
|
|
537
712
|
};
|
|
713
|
+
function isServerLoaderReady() {
|
|
714
|
+
return typeof window !== "undefined" && !!window.FlonkWidgetLoader?.show;
|
|
715
|
+
}
|
|
716
|
+
var serverScriptRequested = false;
|
|
717
|
+
function loadServerLoaderScript(apiBase, pk, sessionId) {
|
|
718
|
+
if (typeof document === "undefined" || serverScriptRequested || isServerLoaderReady()) return;
|
|
719
|
+
serverScriptRequested = true;
|
|
720
|
+
try {
|
|
721
|
+
const q = pk ? `?publishableKey=${encodeURIComponent(pk)}` : sessionId ? `?sessionId=${encodeURIComponent(sessionId)}` : "";
|
|
722
|
+
const s = document.createElement("script");
|
|
723
|
+
s.src = `${apiBase}/public/loader.js${q}`;
|
|
724
|
+
s.async = true;
|
|
725
|
+
s.onerror = () => {
|
|
726
|
+
serverScriptRequested = false;
|
|
727
|
+
emitDiagnostic(
|
|
728
|
+
"LOADER_SCRIPT_BLOCKED",
|
|
729
|
+
"warn",
|
|
730
|
+
`Failed to load the server loader (${s.src}). Likely a CSP script-src or CORP block \u2014 allow the API origin in script-src. Falling back to the bundled loader.`,
|
|
731
|
+
{ src: s.src }
|
|
732
|
+
);
|
|
733
|
+
};
|
|
734
|
+
(document.head || document.documentElement).appendChild(s);
|
|
735
|
+
} catch {
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
var ServerLoader = class {
|
|
739
|
+
constructor() {
|
|
740
|
+
this.overlay = null;
|
|
741
|
+
}
|
|
742
|
+
show(primaryColor, lang) {
|
|
743
|
+
this.overlay = window.FlonkWidgetLoader.show({ primaryColor, lang });
|
|
744
|
+
return this.overlay;
|
|
745
|
+
}
|
|
746
|
+
get element() {
|
|
747
|
+
return this.overlay;
|
|
748
|
+
}
|
|
749
|
+
updateColor(primaryColor) {
|
|
750
|
+
this.overlay?.updateColor?.(primaryColor);
|
|
751
|
+
}
|
|
752
|
+
showError(message, lang) {
|
|
753
|
+
this.overlay?.showError?.(message, lang);
|
|
754
|
+
}
|
|
755
|
+
fadeOut() {
|
|
756
|
+
this.overlay?.fadeOut?.();
|
|
757
|
+
}
|
|
758
|
+
destroy() {
|
|
759
|
+
try {
|
|
760
|
+
this.overlay?.remove();
|
|
761
|
+
} catch {
|
|
762
|
+
}
|
|
763
|
+
this.overlay = null;
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
function makeLoader() {
|
|
767
|
+
if (isServerLoaderReady()) return new ServerLoader();
|
|
768
|
+
emitDiagnostic(
|
|
769
|
+
"LOADER_FALLBACK_BUNDLED",
|
|
770
|
+
"info",
|
|
771
|
+
"Server loader not ready at show time \u2014 using the bundled loader."
|
|
772
|
+
);
|
|
773
|
+
return new Loader();
|
|
774
|
+
}
|
|
538
775
|
|
|
539
776
|
// src/browser/message-handler.ts
|
|
777
|
+
function checkProtocol(data) {
|
|
778
|
+
const remote = data.protocolVersion;
|
|
779
|
+
if (typeof remote === "number" && remote !== PROTOCOL_VERSION) {
|
|
780
|
+
emitDiagnostic(
|
|
781
|
+
"PROTOCOL_VERSION_MISMATCH",
|
|
782
|
+
"warn",
|
|
783
|
+
`SDK speaks protocol ${PROTOCOL_VERSION}, iframe speaks ${remote}. Additive-compatible, but check for a stale-cached widget if behavior is off.`,
|
|
784
|
+
{ sdk: PROTOCOL_VERSION, iframe: remote }
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
540
788
|
var MessageHandler = class {
|
|
541
789
|
constructor(iframeSrc, iframe, callbacks) {
|
|
542
790
|
this.iframeSrc = iframeSrc;
|
|
@@ -569,6 +817,7 @@ var MessageHandler = class {
|
|
|
569
817
|
this.completionHandled = true;
|
|
570
818
|
this.callbacks.onError?.(data.error || "Unknown error");
|
|
571
819
|
} else if (type === WIDGET_EVENTS.READY) {
|
|
820
|
+
checkProtocol(data);
|
|
572
821
|
this.callbacks.onReady?.();
|
|
573
822
|
}
|
|
574
823
|
};
|
|
@@ -720,14 +969,55 @@ async function showLoaderWithEarlyColor(tokensPromise, lang) {
|
|
|
720
969
|
new Promise((resolve) => setTimeout(() => resolve(null), EARLY_COLOR_BUDGET_MS))
|
|
721
970
|
]);
|
|
722
971
|
const primaryColor = primaryFrom(earlyTokens);
|
|
723
|
-
const loader =
|
|
972
|
+
const loader = makeLoader();
|
|
724
973
|
loader.show(primaryColor, lang);
|
|
725
974
|
return { loader, primaryColor };
|
|
726
975
|
}
|
|
727
976
|
var FlonkKYC = class {
|
|
728
977
|
constructor(options = {}) {
|
|
978
|
+
this.disposeDiagnostics = null;
|
|
729
979
|
this.widgetUrl = (options.widgetUrl || DEFAULT_WIDGET_URL).replace(/\/$/, "");
|
|
730
980
|
this.apiBase = (options.apiBase || DEFAULT_API_BASE).replace(/\/$/, "");
|
|
981
|
+
if (options.onDiagnostic) this.disposeDiagnostics = addDiagnosticHandler(options.onDiagnostic);
|
|
982
|
+
if (typeof document !== "undefined") {
|
|
983
|
+
try {
|
|
984
|
+
preconnect(this.widgetUrl);
|
|
985
|
+
} catch {
|
|
986
|
+
}
|
|
987
|
+
try {
|
|
988
|
+
loadServerLoaderScript(this.apiBase);
|
|
989
|
+
} catch {
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
/** Unregister this instance's `onDiagnostic` handler. */
|
|
994
|
+
dispose() {
|
|
995
|
+
this.disposeDiagnostics?.();
|
|
996
|
+
this.disposeDiagnostics = null;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Prewarm the widget ahead of the user's click — preconnect + idle prefetch
|
|
1000
|
+
* of branding/assets, and (with `level:'eager'`) a hidden background iframe so
|
|
1001
|
+
* the full bundle is loaded before the click. Call on page mount / route
|
|
1002
|
+
* enter. Returns a cleanup (removes `intent` listeners). Never pre-creates a
|
|
1003
|
+
* session. SSR-safe.
|
|
1004
|
+
*
|
|
1005
|
+
* @example
|
|
1006
|
+
* FlonkKYC.prewarm({ publishableKey: 'pk_live_…', level: 'eager' });
|
|
1007
|
+
* // or, warm only when the user shows intent:
|
|
1008
|
+
* FlonkKYC.prewarm({ publishableKey: 'pk_live_…', level: 'intent', trigger: btn });
|
|
1009
|
+
*/
|
|
1010
|
+
static prewarm(opts = {}) {
|
|
1011
|
+
if (typeof document === "undefined") return () => {
|
|
1012
|
+
};
|
|
1013
|
+
return prewarm({
|
|
1014
|
+
widgetUrl: (opts.widgetUrl || DEFAULT_WIDGET_URL).replace(/\/$/, ""),
|
|
1015
|
+
apiBase: (opts.apiBase || DEFAULT_API_BASE).replace(/\/$/, ""),
|
|
1016
|
+
publishableKey: opts.publishableKey,
|
|
1017
|
+
sessionId: opts.sessionId,
|
|
1018
|
+
level: opts.level,
|
|
1019
|
+
trigger: opts.trigger ?? null
|
|
1020
|
+
});
|
|
731
1021
|
}
|
|
732
1022
|
/**
|
|
733
1023
|
* Warm the project's branding (colors) ahead of time so the widget paints the
|
|
@@ -1006,12 +1296,14 @@ var FlonkKYC = class {
|
|
|
1006
1296
|
const filtered = Object.fromEntries(
|
|
1007
1297
|
Object.entries(params).filter(([, v]) => v != null)
|
|
1008
1298
|
);
|
|
1299
|
+
filtered[WIDGET_PARAMS.PROTOCOL_VERSION] = String(PROTOCOL_VERSION);
|
|
1009
1300
|
const search = new URLSearchParams(filtered);
|
|
1010
1301
|
const src = `${this.widgetUrl}/?${search.toString()}`;
|
|
1011
1302
|
const iframe = createIframe(src);
|
|
1303
|
+
emitDiagnostic("IFRAME_FRESH", "info", "Built a fresh widget iframe");
|
|
1012
1304
|
const mountTarget = opts.mount || document.body;
|
|
1013
1305
|
const loader = opts.loader ?? (() => {
|
|
1014
|
-
const l =
|
|
1306
|
+
const l = makeLoader();
|
|
1015
1307
|
l.show(opts.primaryColor, opts.lang);
|
|
1016
1308
|
return l;
|
|
1017
1309
|
})();
|
|
@@ -1047,11 +1339,18 @@ var FlonkKYC = class {
|
|
|
1047
1339
|
onReady: opts.onReady
|
|
1048
1340
|
});
|
|
1049
1341
|
handler.listen();
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1342
|
+
let revealed = false;
|
|
1343
|
+
const revealOnce = () => {
|
|
1344
|
+
if (revealed) return;
|
|
1345
|
+
revealed = true;
|
|
1346
|
+
clearTimeout(revealTimer);
|
|
1347
|
+
if (loader.element) transitionLoaderToIframe(loader.element, iframe, () => loader.destroy());
|
|
1348
|
+
};
|
|
1349
|
+
const revealTimer = setTimeout(() => {
|
|
1350
|
+
emitDiagnostic("READY_TIMEOUT_REVEAL", "warn", "Widget did not signal READY in time; revealing anyway");
|
|
1351
|
+
revealOnce();
|
|
1352
|
+
}, 4e3);
|
|
1353
|
+
handler.onReadyOnce(revealOnce);
|
|
1055
1354
|
return {
|
|
1056
1355
|
iframe,
|
|
1057
1356
|
destroy: cleanupAll
|
|
@@ -1139,10 +1438,16 @@ function FlonkKYCBrandingPreloader({
|
|
|
1139
1438
|
return null;
|
|
1140
1439
|
}
|
|
1141
1440
|
|
|
1441
|
+
exports.API_VERSION = API_VERSION;
|
|
1142
1442
|
exports.FlonkError = FlonkError;
|
|
1143
1443
|
exports.FlonkKYC = FlonkKYC;
|
|
1144
1444
|
exports.FlonkKYCBrandingPreloader = FlonkKYCBrandingPreloader;
|
|
1145
1445
|
exports.FlonkKYCWidget = FlonkKYCWidget;
|
|
1146
1446
|
exports.FlonkValidationError = FlonkValidationError;
|
|
1447
|
+
exports.PROTOCOL_VERSION = PROTOCOL_VERSION;
|
|
1448
|
+
exports.SDK_VERSION = SDK_VERSION;
|
|
1449
|
+
exports.WIDGET_EVENTS = WIDGET_EVENTS;
|
|
1450
|
+
exports.WIDGET_PARAMS = WIDGET_PARAMS;
|
|
1451
|
+
exports.addDiagnosticHandler = addDiagnosticHandler;
|
|
1147
1452
|
//# sourceMappingURL=index.cjs.map
|
|
1148
1453
|
//# sourceMappingURL=index.cjs.map
|