@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/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.2";
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
@@ -227,10 +244,14 @@ function createIframe(src) {
227
244
  top: "0",
228
245
  left: "0",
229
246
  zIndex: "9999",
230
- background: "transparent",
231
- 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.
232
251
  opacity: "0",
233
252
  visibility: "hidden",
253
+ background: "transparent",
254
+ backgroundColor: "transparent",
234
255
  borderRadius: d ? "0" : "",
235
256
  boxShadow: d ? "none" : "",
236
257
  colorScheme: "normal"
@@ -265,15 +286,15 @@ function adjustZIndex(loader, iframe) {
265
286
  function transitionLoaderToIframe(loader, iframe, onDone) {
266
287
  const d = isDesktop();
267
288
  const dur = d ? 300 : 500;
268
- setStyles(iframe, { opacity: "0", visibility: "hidden" });
269
289
  const card = loader.querySelector("div");
270
290
  if (card) {
271
291
  setStyles(card, {
292
+ transition: "transform 300ms ease-out, opacity 300ms ease-out",
272
293
  transform: d ? "translateY(-10px) scale(0.98)" : "translateY(-15px) scale(0.96)",
273
294
  opacity: "0"
274
295
  });
275
296
  }
276
- loader.style.opacity = "0";
297
+ setStyles(loader, { transition: "opacity 300ms ease-out", opacity: "0" });
277
298
  setTimeout(() => {
278
299
  onDone();
279
300
  setStyles(iframe, {
@@ -284,6 +305,160 @@ function transitionLoaderToIframe(loader, iframe, onDone) {
284
305
  }, dur);
285
306
  }
286
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
+
287
462
  // src/browser/loader.ts
288
463
  var LOADER_I18N = {
289
464
  en: { title: "Initializing...", subtitle: "Loading KYC widget", errorTitle: "Something went wrong", close: "Close" },
@@ -533,8 +708,81 @@ var Loader = class {
533
708
  this.cleanup = null;
534
709
  }
535
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
+ }
536
773
 
537
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
+ }
538
786
  var MessageHandler = class {
539
787
  constructor(iframeSrc, iframe, callbacks) {
540
788
  this.iframeSrc = iframeSrc;
@@ -567,6 +815,7 @@ var MessageHandler = class {
567
815
  this.completionHandled = true;
568
816
  this.callbacks.onError?.(data.error || "Unknown error");
569
817
  } else if (type === WIDGET_EVENTS.READY) {
818
+ checkProtocol(data);
570
819
  this.callbacks.onReady?.();
571
820
  }
572
821
  };
@@ -718,14 +967,55 @@ async function showLoaderWithEarlyColor(tokensPromise, lang) {
718
967
  new Promise((resolve) => setTimeout(() => resolve(null), EARLY_COLOR_BUDGET_MS))
719
968
  ]);
720
969
  const primaryColor = primaryFrom(earlyTokens);
721
- const loader = new Loader();
970
+ const loader = makeLoader();
722
971
  loader.show(primaryColor, lang);
723
972
  return { loader, primaryColor };
724
973
  }
725
974
  var FlonkKYC = class {
726
975
  constructor(options = {}) {
976
+ this.disposeDiagnostics = null;
727
977
  this.widgetUrl = (options.widgetUrl || DEFAULT_WIDGET_URL).replace(/\/$/, "");
728
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
+ });
729
1019
  }
730
1020
  /**
731
1021
  * Warm the project's branding (colors) ahead of time so the widget paints the
@@ -1004,12 +1294,14 @@ var FlonkKYC = class {
1004
1294
  const filtered = Object.fromEntries(
1005
1295
  Object.entries(params).filter(([, v]) => v != null)
1006
1296
  );
1297
+ filtered[WIDGET_PARAMS.PROTOCOL_VERSION] = String(PROTOCOL_VERSION);
1007
1298
  const search = new URLSearchParams(filtered);
1008
1299
  const src = `${this.widgetUrl}/?${search.toString()}`;
1009
1300
  const iframe = createIframe(src);
1301
+ emitDiagnostic("IFRAME_FRESH", "info", "Built a fresh widget iframe");
1010
1302
  const mountTarget = opts.mount || document.body;
1011
1303
  const loader = opts.loader ?? (() => {
1012
- const l = new Loader();
1304
+ const l = makeLoader();
1013
1305
  l.show(opts.primaryColor, opts.lang);
1014
1306
  return l;
1015
1307
  })();
@@ -1045,11 +1337,18 @@ var FlonkKYC = class {
1045
1337
  onReady: opts.onReady
1046
1338
  });
1047
1339
  handler.listen();
1048
- handler.onReadyOnce(() => {
1049
- if (loader.element) {
1050
- transitionLoaderToIframe(loader.element, iframe, () => loader.destroy());
1051
- }
1052
- });
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);
1053
1352
  return {
1054
1353
  iframe,
1055
1354
  destroy: cleanupAll
@@ -1137,6 +1436,6 @@ function FlonkKYCBrandingPreloader({
1137
1436
  return null;
1138
1437
  }
1139
1438
 
1140
- export { FlonkError, FlonkKYC, FlonkKYCBrandingPreloader, FlonkKYCWidget, FlonkValidationError };
1439
+ export { API_VERSION, FlonkError, FlonkKYC, FlonkKYCBrandingPreloader, FlonkKYCWidget, FlonkValidationError, PROTOCOL_VERSION, SDK_VERSION, WIDGET_EVENTS, WIDGET_PARAMS, addDiagnosticHandler };
1141
1440
  //# sourceMappingURL=index.js.map
1142
1441
  //# sourceMappingURL=index.js.map