@flonkid/kyc 1.8.1 → 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/dist/index.js CHANGED
@@ -2,14 +2,31 @@ import { useRef, useMemo, useEffect } from 'react';
2
2
  import { jsx } from 'react/jsx-runtime';
3
3
 
4
4
  // src/shared/constants.ts
5
- var SDK_VERSION = "1.8.1";
5
+ var SDK_VERSION = "1.9.0";
6
6
  var DEFAULT_WIDGET_URL = "https://widget.flonk.id";
7
7
  var DEFAULT_API_BASE = "https://api.flonk.id/v1";
8
+ var API_VERSION = "2026-06-01";
9
+ var PROTOCOL_VERSION = 1;
8
10
  var WIDGET_EVENTS = {
9
11
  READY: "KYC_WIDGET_READY",
10
12
  COMPLETE: "KYC_COMPLETE",
11
13
  CANCEL: "KYC_CANCEL",
12
- ERROR: "KYC_ERROR"
14
+ ERROR: "KYC_ERROR",
15
+ CONFIG: "KYC_WIDGET_CONFIG"
16
+ };
17
+ var WIDGET_PARAMS = {
18
+ PROTOCOL_VERSION: "pv",
19
+ SESSION_ID: "sessionId",
20
+ EMBED_TOKEN: "embedToken",
21
+ TOKEN: "token",
22
+ PUBLISHABLE_KEY: "publishableKey",
23
+ CLIENT_ID: "clientId",
24
+ CLIENT_METADATA: "clientMetadata",
25
+ DESIGN_TOKENS: "designTokens",
26
+ ALLOW_MANUAL_UPLOAD: "allowManualUpload",
27
+ LANG: "lang",
28
+ OVERLAY_COLOR: "overlayColor",
29
+ MODE: "mode"
13
30
  };
14
31
 
15
32
  // src/shared/errors.ts
@@ -197,24 +214,6 @@ async function fetchSessionFromServer(serverUrl, clientMetadata, requestHeaders)
197
214
  sessionCreateInflight.set(key, promise);
198
215
  return promise;
199
216
  }
200
- async function fetchPublicSession(apiBase, sessionId, embedToken) {
201
- const res = await fetchWithTimeout(`${apiBase}/public/session/${sessionId}`, {
202
- headers: {
203
- "Content-Type": "application/json",
204
- "Authorization": `Bearer ${embedToken}`
205
- }
206
- });
207
- if (!res.ok) {
208
- let message = `Failed to fetch session (${res.status})`;
209
- try {
210
- const b = await res.json();
211
- message = b.error || b.message || message;
212
- } catch {
213
- }
214
- throw new Error(message);
215
- }
216
- return res.json();
217
- }
218
217
  async function exchangeSessionForToken(apiBase, sessionId) {
219
218
  const res = await fetchWithTimeout(`${apiBase}/public/session/${sessionId}/token`, {
220
219
  method: "POST",
@@ -245,10 +244,14 @@ function createIframe(src) {
245
244
  top: "0",
246
245
  left: "0",
247
246
  zIndex: "9999",
248
- background: "transparent",
249
- backgroundColor: "transparent",
247
+ // Hidden until reveal so the loader overlay doesn't show the iframe's own
248
+ // loading state through its translucent backdrop (double loader). Reveal
249
+ // is gated on READY-OR-a-timeout (see index.ts), so a missing READY can't
250
+ // leave it permanently hidden — that timeout is what removed the deadlock.
250
251
  opacity: "0",
251
252
  visibility: "hidden",
253
+ background: "transparent",
254
+ backgroundColor: "transparent",
252
255
  borderRadius: d ? "0" : "",
253
256
  boxShadow: d ? "none" : "",
254
257
  colorScheme: "normal"
@@ -283,15 +286,15 @@ function adjustZIndex(loader, iframe) {
283
286
  function transitionLoaderToIframe(loader, iframe, onDone) {
284
287
  const d = isDesktop();
285
288
  const dur = d ? 300 : 500;
286
- setStyles(iframe, { opacity: "0", visibility: "hidden" });
287
289
  const card = loader.querySelector("div");
288
290
  if (card) {
289
291
  setStyles(card, {
292
+ transition: "transform 300ms ease-out, opacity 300ms ease-out",
290
293
  transform: d ? "translateY(-10px) scale(0.98)" : "translateY(-15px) scale(0.96)",
291
294
  opacity: "0"
292
295
  });
293
296
  }
294
- loader.style.opacity = "0";
297
+ setStyles(loader, { transition: "opacity 300ms ease-out", opacity: "0" });
295
298
  setTimeout(() => {
296
299
  onDone();
297
300
  setStyles(iframe, {
@@ -302,6 +305,160 @@ function transitionLoaderToIframe(loader, iframe, onDone) {
302
305
  }, dur);
303
306
  }
304
307
 
308
+ // src/browser/diagnostics.ts
309
+ var handlers = /* @__PURE__ */ new Set();
310
+ function addDiagnosticHandler(handler) {
311
+ handlers.add(handler);
312
+ return () => {
313
+ handlers.delete(handler);
314
+ };
315
+ }
316
+ function debugEnabled() {
317
+ try {
318
+ return Boolean(globalThis.__FLONK_DEBUG__);
319
+ } catch {
320
+ return false;
321
+ }
322
+ }
323
+ function emitDiagnostic(code, level, message, detail) {
324
+ const event = { code, level, message, detail };
325
+ if (debugEnabled()) {
326
+ try {
327
+ const fn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
328
+ fn(`[flonk:${code}] ${message}`, detail ?? "");
329
+ } catch {
330
+ }
331
+ }
332
+ for (const handler of handlers) {
333
+ try {
334
+ handler(event);
335
+ } catch {
336
+ }
337
+ }
338
+ }
339
+
340
+ // src/browser/prewarm.ts
341
+ var noop = () => {
342
+ };
343
+ function originOf(url) {
344
+ try {
345
+ return new URL(url).origin;
346
+ } catch {
347
+ return url.replace(/\/+$/, "");
348
+ }
349
+ }
350
+ function addLinkHint(doc, rel, href, attrs) {
351
+ try {
352
+ const existing = doc.querySelector(`link[rel="${rel}"][href="${href}"]`);
353
+ if (existing) return;
354
+ const link = doc.createElement("link");
355
+ link.rel = rel;
356
+ link.href = href;
357
+ if (attrs) for (const k of Object.keys(attrs)) link.setAttribute(k, attrs[k]);
358
+ (doc.head || doc.documentElement).appendChild(link);
359
+ } catch {
360
+ }
361
+ }
362
+ function preconnect(widgetUrl, doc = document) {
363
+ const origin = originOf(widgetUrl);
364
+ addLinkHint(doc, "preconnect", origin, { crossorigin: "" });
365
+ addLinkHint(doc, "dns-prefetch", origin);
366
+ }
367
+ function defaultScheduleIdle(fn) {
368
+ if (typeof window === "undefined") {
369
+ fn();
370
+ return;
371
+ }
372
+ const w = window;
373
+ const run = () => {
374
+ if (typeof w.requestIdleCallback === "function") {
375
+ w.requestIdleCallback(fn, { timeout: 2e3 });
376
+ } else {
377
+ setTimeout(fn, 200);
378
+ }
379
+ };
380
+ if (document.readyState === "complete") run();
381
+ else window.addEventListener("load", run, { once: true });
382
+ }
383
+ function prewarm(options) {
384
+ const level = options.level ?? "connect";
385
+ const doc = options.doc ?? (typeof document !== "undefined" ? document : void 0);
386
+ if (level === "none" || !doc) {
387
+ emitDiagnostic("PREWARM_SKIPPED", "info", `Prewarm skipped (level=${level}${doc ? "" : ", no document"}).`);
388
+ return noop;
389
+ }
390
+ const scheduleIdle = options.scheduleIdle ?? defaultScheduleIdle;
391
+ const origin = originOf(options.widgetUrl);
392
+ preconnect(options.widgetUrl, doc);
393
+ const warmAssets = () => {
394
+ if (options.apiBase) {
395
+ const q = options.publishableKey ? `pk=${encodeURIComponent(options.publishableKey)}` : options.sessionId ? `sessionId=${encodeURIComponent(options.sessionId)}` : "";
396
+ addLinkHint(doc, "prefetch", `${options.apiBase}/v1/public/design-tokens${q ? `?${q}` : ""}`);
397
+ }
398
+ addLinkHint(doc, "prefetch", `${origin}/`, { as: "document" });
399
+ };
400
+ if (level === "intent") {
401
+ return attachIntent(doc, options.trigger ?? null, () => {
402
+ warmAssets();
403
+ mountHiddenIframe(doc, origin);
404
+ });
405
+ }
406
+ scheduleIdle(() => {
407
+ warmAssets();
408
+ if (level === "eager") mountHiddenIframe(doc, origin);
409
+ });
410
+ return noop;
411
+ }
412
+ function attachIntent(doc, trigger, warm) {
413
+ let fired = false;
414
+ const fire = () => {
415
+ if (fired) return;
416
+ fired = true;
417
+ cleanup();
418
+ warm();
419
+ };
420
+ const events = [
421
+ ["mouseenter", fire],
422
+ ["focusin", fire],
423
+ ["touchstart", fire]
424
+ ];
425
+ let io = null;
426
+ const cleanup = () => {
427
+ if (trigger) for (const [type, fn] of events) trigger.removeEventListener(type, fn);
428
+ if (io) {
429
+ io.disconnect();
430
+ io = null;
431
+ }
432
+ };
433
+ if (trigger) {
434
+ for (const [type, fn] of events) trigger.addEventListener(type, fn, { passive: true });
435
+ const w = doc.defaultView || (typeof window !== "undefined" ? window : void 0);
436
+ if (w && typeof w.IntersectionObserver === "function") {
437
+ const observer = new w.IntersectionObserver((entries) => {
438
+ if (entries.some((e) => e.isIntersecting)) fire();
439
+ });
440
+ observer.observe(trigger);
441
+ io = observer;
442
+ }
443
+ } else {
444
+ defaultScheduleIdle(fire);
445
+ }
446
+ return cleanup;
447
+ }
448
+ function mountHiddenIframe(doc, origin) {
449
+ try {
450
+ if (doc.querySelector("iframe[data-flonk-prewarm]")) return;
451
+ const iframe = doc.createElement("iframe");
452
+ iframe.src = `${origin}/?prewarm=1`;
453
+ iframe.setAttribute("data-flonk-prewarm", "1");
454
+ iframe.setAttribute("aria-hidden", "true");
455
+ iframe.tabIndex = -1;
456
+ iframe.style.cssText = "position:absolute;width:1px;height:1px;left:-9999px;top:-9999px;border:0;opacity:0;pointer-events:none";
457
+ (doc.body || doc.documentElement).appendChild(iframe);
458
+ } catch {
459
+ }
460
+ }
461
+
305
462
  // src/browser/loader.ts
306
463
  var LOADER_I18N = {
307
464
  en: { title: "Initializing...", subtitle: "Loading KYC widget", errorTitle: "Something went wrong", close: "Close" },
@@ -551,8 +708,81 @@ var Loader = class {
551
708
  this.cleanup = null;
552
709
  }
553
710
  };
711
+ function isServerLoaderReady() {
712
+ return typeof window !== "undefined" && !!window.FlonkWidgetLoader?.show;
713
+ }
714
+ var serverScriptRequested = false;
715
+ function loadServerLoaderScript(apiBase, pk, sessionId) {
716
+ if (typeof document === "undefined" || serverScriptRequested || isServerLoaderReady()) return;
717
+ serverScriptRequested = true;
718
+ try {
719
+ const q = pk ? `?publishableKey=${encodeURIComponent(pk)}` : sessionId ? `?sessionId=${encodeURIComponent(sessionId)}` : "";
720
+ const s = document.createElement("script");
721
+ s.src = `${apiBase}/public/loader.js${q}`;
722
+ s.async = true;
723
+ s.onerror = () => {
724
+ serverScriptRequested = false;
725
+ emitDiagnostic(
726
+ "LOADER_SCRIPT_BLOCKED",
727
+ "warn",
728
+ `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.`,
729
+ { src: s.src }
730
+ );
731
+ };
732
+ (document.head || document.documentElement).appendChild(s);
733
+ } catch {
734
+ }
735
+ }
736
+ var ServerLoader = class {
737
+ constructor() {
738
+ this.overlay = null;
739
+ }
740
+ show(primaryColor, lang) {
741
+ this.overlay = window.FlonkWidgetLoader.show({ primaryColor, lang });
742
+ return this.overlay;
743
+ }
744
+ get element() {
745
+ return this.overlay;
746
+ }
747
+ updateColor(primaryColor) {
748
+ this.overlay?.updateColor?.(primaryColor);
749
+ }
750
+ showError(message, lang) {
751
+ this.overlay?.showError?.(message, lang);
752
+ }
753
+ fadeOut() {
754
+ this.overlay?.fadeOut?.();
755
+ }
756
+ destroy() {
757
+ try {
758
+ this.overlay?.remove();
759
+ } catch {
760
+ }
761
+ this.overlay = null;
762
+ }
763
+ };
764
+ function makeLoader() {
765
+ if (isServerLoaderReady()) return new ServerLoader();
766
+ emitDiagnostic(
767
+ "LOADER_FALLBACK_BUNDLED",
768
+ "info",
769
+ "Server loader not ready at show time \u2014 using the bundled loader."
770
+ );
771
+ return new Loader();
772
+ }
554
773
 
555
774
  // src/browser/message-handler.ts
775
+ function checkProtocol(data) {
776
+ const remote = data.protocolVersion;
777
+ if (typeof remote === "number" && remote !== PROTOCOL_VERSION) {
778
+ emitDiagnostic(
779
+ "PROTOCOL_VERSION_MISMATCH",
780
+ "warn",
781
+ `SDK speaks protocol ${PROTOCOL_VERSION}, iframe speaks ${remote}. Additive-compatible, but check for a stale-cached widget if behavior is off.`,
782
+ { sdk: PROTOCOL_VERSION, iframe: remote }
783
+ );
784
+ }
785
+ }
556
786
  var MessageHandler = class {
557
787
  constructor(iframeSrc, iframe, callbacks) {
558
788
  this.iframeSrc = iframeSrc;
@@ -585,6 +815,7 @@ var MessageHandler = class {
585
815
  this.completionHandled = true;
586
816
  this.callbacks.onError?.(data.error || "Unknown error");
587
817
  } else if (type === WIDGET_EVENTS.READY) {
818
+ checkProtocol(data);
588
819
  this.callbacks.onReady?.();
589
820
  }
590
821
  };
@@ -736,14 +967,55 @@ async function showLoaderWithEarlyColor(tokensPromise, lang) {
736
967
  new Promise((resolve) => setTimeout(() => resolve(null), EARLY_COLOR_BUDGET_MS))
737
968
  ]);
738
969
  const primaryColor = primaryFrom(earlyTokens);
739
- const loader = new Loader();
970
+ const loader = makeLoader();
740
971
  loader.show(primaryColor, lang);
741
972
  return { loader, primaryColor };
742
973
  }
743
974
  var FlonkKYC = class {
744
975
  constructor(options = {}) {
976
+ this.disposeDiagnostics = null;
745
977
  this.widgetUrl = (options.widgetUrl || DEFAULT_WIDGET_URL).replace(/\/$/, "");
746
978
  this.apiBase = (options.apiBase || DEFAULT_API_BASE).replace(/\/$/, "");
979
+ if (options.onDiagnostic) this.disposeDiagnostics = addDiagnosticHandler(options.onDiagnostic);
980
+ if (typeof document !== "undefined") {
981
+ try {
982
+ preconnect(this.widgetUrl);
983
+ } catch {
984
+ }
985
+ try {
986
+ loadServerLoaderScript(this.apiBase);
987
+ } catch {
988
+ }
989
+ }
990
+ }
991
+ /** Unregister this instance's `onDiagnostic` handler. */
992
+ dispose() {
993
+ this.disposeDiagnostics?.();
994
+ this.disposeDiagnostics = null;
995
+ }
996
+ /**
997
+ * Prewarm the widget ahead of the user's click — preconnect + idle prefetch
998
+ * of branding/assets, and (with `level:'eager'`) a hidden background iframe so
999
+ * the full bundle is loaded before the click. Call on page mount / route
1000
+ * enter. Returns a cleanup (removes `intent` listeners). Never pre-creates a
1001
+ * session. SSR-safe.
1002
+ *
1003
+ * @example
1004
+ * FlonkKYC.prewarm({ publishableKey: 'pk_live_…', level: 'eager' });
1005
+ * // or, warm only when the user shows intent:
1006
+ * FlonkKYC.prewarm({ publishableKey: 'pk_live_…', level: 'intent', trigger: btn });
1007
+ */
1008
+ static prewarm(opts = {}) {
1009
+ if (typeof document === "undefined") return () => {
1010
+ };
1011
+ return prewarm({
1012
+ widgetUrl: (opts.widgetUrl || DEFAULT_WIDGET_URL).replace(/\/$/, ""),
1013
+ apiBase: (opts.apiBase || DEFAULT_API_BASE).replace(/\/$/, ""),
1014
+ publishableKey: opts.publishableKey,
1015
+ sessionId: opts.sessionId,
1016
+ level: opts.level,
1017
+ trigger: opts.trigger ?? null
1018
+ });
747
1019
  }
748
1020
  /**
749
1021
  * Warm the project's branding (colors) ahead of time so the widget paints the
@@ -894,20 +1166,7 @@ var FlonkKYC = class {
894
1166
  const finalTokens = designTokens ?? await fetchDesignTokens(this.apiBase, { sessionId });
895
1167
  const finalColor = primaryFrom(finalTokens);
896
1168
  if (finalColor !== primaryColor) loader.updateColor(finalColor);
897
- const sessionData = await fetchPublicSession(this.apiBase, sessionId, embedToken);
898
- const session = {
899
- id: sessionData.id,
900
- allowManualUpload: sessionData.allowManualUpload ?? config.allowManualUpload ?? true,
901
- clientMetadata: sessionData.clientMetadata || config.clientMetadata,
902
- qrCodeUrl: sessionData.qrCodeUrl,
903
- testMode: sessionData.testMode || false,
904
- poaEnabled: sessionData.poaEnabled || false,
905
- poaRequired: sessionData.poaRequired || false,
906
- mlAutoCaptureEnabled: sessionData.mlAutoCaptureEnabled || false,
907
- mlCropEnabled: sessionData.mlCropEnabled ?? true,
908
- mlVerifyEnabled: sessionData.mlVerifyEnabled || false
909
- };
910
- return this.buildWidget(embedToken, session, config, loader, finalTokens);
1169
+ return this.buildWidget(embedToken, sessionId, config, loader, finalTokens);
911
1170
  } catch (err) {
912
1171
  const msg = err.message || "Failed to create session";
913
1172
  loader.showError(msg, config.lang);
@@ -921,32 +1180,12 @@ var FlonkKYC = class {
921
1180
  async initWithEmbedToken(config) {
922
1181
  const pk = config.publishableKey;
923
1182
  const designTokensPromise = pk ? fetchDesignTokens(this.apiBase, { pk }) : fetchDesignTokens(this.apiBase, { sessionId: config.sessionId });
924
- const sessionPromise = fetchPublicSession(
925
- this.apiBase,
926
- config.sessionId,
927
- config.embedToken
928
- );
929
1183
  const { loader, primaryColor } = await showLoaderWithEarlyColor(designTokensPromise, config.lang);
930
1184
  try {
931
- const [sessionData, designTokens] = await Promise.all([
932
- sessionPromise,
933
- designTokensPromise
934
- ]);
1185
+ const designTokens = await designTokensPromise;
935
1186
  const finalColor = primaryFrom(designTokens);
936
1187
  if (finalColor !== primaryColor) loader.updateColor(finalColor);
937
- const session = {
938
- id: sessionData.id,
939
- allowManualUpload: sessionData.allowManualUpload ?? config.allowManualUpload ?? true,
940
- clientMetadata: sessionData.clientMetadata || config.clientMetadata,
941
- qrCodeUrl: sessionData.qrCodeUrl,
942
- testMode: sessionData.testMode || false,
943
- poaEnabled: sessionData.poaEnabled || false,
944
- poaRequired: sessionData.poaRequired || false,
945
- mlAutoCaptureEnabled: sessionData.mlAutoCaptureEnabled || false,
946
- mlCropEnabled: sessionData.mlCropEnabled ?? true,
947
- mlVerifyEnabled: sessionData.mlVerifyEnabled || false
948
- };
949
- return this.buildWidget(config.embedToken, session, config, loader, designTokens);
1188
+ return this.buildWidget(config.embedToken, config.sessionId, config, loader, designTokens);
950
1189
  } catch (err) {
951
1190
  const msg = err.message || "Failed to initialize verification";
952
1191
  loader.showError(msg, config.lang);
@@ -972,7 +1211,7 @@ var FlonkKYC = class {
972
1211
  exchangePromise,
973
1212
  designTokensPromise
974
1213
  ]);
975
- return this.buildWidget(embedToken, session, config, loader, designTokens);
1214
+ return this.buildWidget(embedToken, config.sessionId || session.id, config, loader, designTokens);
976
1215
  } catch (err) {
977
1216
  const msg = err.message || "Failed to initialize verification";
978
1217
  loader.showError(msg, config.lang);
@@ -1019,32 +1258,21 @@ var FlonkKYC = class {
1019
1258
  }
1020
1259
  }
1021
1260
  // ── Core widget builder ──────────────────────────────
1022
- buildWidget(token, session, config, loader, designTokens) {
1261
+ buildWidget(token, sessionId, config, loader, designTokens) {
1023
1262
  const params = {
1024
1263
  mode: "embedded",
1025
- sessionId: config.sessionId || session.id,
1026
- token,
1027
- allowManualUpload: String(session.allowManualUpload !== false)
1264
+ sessionId,
1265
+ token
1028
1266
  };
1029
- if (session.testMode) params.testMode = "true";
1030
- if (session.poaEnabled) params.poaEnabled = "true";
1031
- if (session.poaRequired) params.poaRequired = "true";
1032
- if (session.mlAutoCaptureEnabled) params.mlAutoCaptureEnabled = "true";
1033
- if (session.mlCropEnabled !== false) params.mlCropEnabled = "true";
1034
- if (session.mlVerifyEnabled) params.mlVerifyEnabled = "true";
1035
- if (session.vaultReuseEnabled) params.vaultReuseEnabled = "true";
1036
- if (session.vaultReuseChallenge) params.vaultReuseChallenge = session.vaultReuseChallenge;
1037
- if (session.faceAutoCapture) params.faceAutoCapture = JSON.stringify(session.faceAutoCapture);
1038
- if (session.project) params.project = JSON.stringify(session.project);
1039
- params.policyReady = "1";
1267
+ if (config.allowManualUpload !== void 0) {
1268
+ params.allowManualUpload = String(config.allowManualUpload !== false);
1269
+ }
1040
1270
  if (designTokens?.colors) {
1041
1271
  params.designTokens = JSON.stringify(designTokens);
1042
1272
  }
1043
1273
  if (config.embedToken) params.embedToken = config.embedToken;
1044
- if (session.qrCodeUrl) params.qrCodeUrl = session.qrCodeUrl;
1045
- const clientMetadata = session.clientMetadata || config.clientMetadata;
1046
- if (clientMetadata) {
1047
- params.clientMetadata = JSON.stringify(clientMetadata);
1274
+ if (config.clientMetadata) {
1275
+ params.clientMetadata = JSON.stringify(config.clientMetadata);
1048
1276
  }
1049
1277
  if (config.lang) params.lang = config.lang;
1050
1278
  if (config.overlayColor) params.overlayColor = config.overlayColor;
@@ -1066,12 +1294,14 @@ var FlonkKYC = class {
1066
1294
  const filtered = Object.fromEntries(
1067
1295
  Object.entries(params).filter(([, v]) => v != null)
1068
1296
  );
1297
+ filtered[WIDGET_PARAMS.PROTOCOL_VERSION] = String(PROTOCOL_VERSION);
1069
1298
  const search = new URLSearchParams(filtered);
1070
1299
  const src = `${this.widgetUrl}/?${search.toString()}`;
1071
1300
  const iframe = createIframe(src);
1301
+ emitDiagnostic("IFRAME_FRESH", "info", "Built a fresh widget iframe");
1072
1302
  const mountTarget = opts.mount || document.body;
1073
1303
  const loader = opts.loader ?? (() => {
1074
- const l = new Loader();
1304
+ const l = makeLoader();
1075
1305
  l.show(opts.primaryColor, opts.lang);
1076
1306
  return l;
1077
1307
  })();
@@ -1107,11 +1337,18 @@ var FlonkKYC = class {
1107
1337
  onReady: opts.onReady
1108
1338
  });
1109
1339
  handler.listen();
1110
- handler.onReadyOnce(() => {
1111
- if (loader.element) {
1112
- transitionLoaderToIframe(loader.element, iframe, () => loader.destroy());
1113
- }
1114
- });
1340
+ let revealed = false;
1341
+ const revealOnce = () => {
1342
+ if (revealed) return;
1343
+ revealed = true;
1344
+ clearTimeout(revealTimer);
1345
+ if (loader.element) transitionLoaderToIframe(loader.element, iframe, () => loader.destroy());
1346
+ };
1347
+ const revealTimer = setTimeout(() => {
1348
+ emitDiagnostic("READY_TIMEOUT_REVEAL", "warn", "Widget did not signal READY in time; revealing anyway");
1349
+ revealOnce();
1350
+ }, 4e3);
1351
+ handler.onReadyOnce(revealOnce);
1115
1352
  return {
1116
1353
  iframe,
1117
1354
  destroy: cleanupAll
@@ -1199,6 +1436,6 @@ function FlonkKYCBrandingPreloader({
1199
1436
  return null;
1200
1437
  }
1201
1438
 
1202
- export { FlonkError, FlonkKYC, FlonkKYCBrandingPreloader, FlonkKYCWidget, FlonkValidationError };
1439
+ export { API_VERSION, FlonkError, FlonkKYC, FlonkKYCBrandingPreloader, FlonkKYCWidget, FlonkValidationError, PROTOCOL_VERSION, SDK_VERSION, WIDGET_EVENTS, WIDGET_PARAMS, addDiagnosticHandler };
1203
1440
  //# sourceMappingURL=index.js.map
1204
1441
  //# sourceMappingURL=index.js.map