@flonkid/kyc 1.6.0 → 1.7.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.cjs CHANGED
@@ -4,7 +4,7 @@ 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.6.0";
7
+ var SDK_VERSION = "1.7.0";
8
8
  var DEFAULT_WIDGET_URL = "https://widget.flonk.id";
9
9
  var DEFAULT_API_BASE = "https://api.flonk.id/v1";
10
10
  var WIDGET_EVENTS = {
@@ -35,7 +35,7 @@ function getOrigin(url) {
35
35
  try {
36
36
  return new URL(url).origin;
37
37
  } catch {
38
- return window.location.origin;
38
+ return "null";
39
39
  }
40
40
  }
41
41
  function isDesktop() {
@@ -75,8 +75,23 @@ function generateSecondaryColor(hex) {
75
75
  return "#93c5fd";
76
76
  }
77
77
  }
78
+ var DEFAULT_FETCH_TIMEOUT_MS = 2e4;
79
+ async function fetchWithTimeout(url, init = {}, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
80
+ const controller = new AbortController();
81
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
82
+ try {
83
+ return await fetch(url, { ...init, signal: controller.signal });
84
+ } catch (err) {
85
+ if (err?.name === "AbortError") {
86
+ throw new Error(`Request timed out after ${timeoutMs}ms: ${url}`);
87
+ }
88
+ throw err;
89
+ } finally {
90
+ clearTimeout(timer);
91
+ }
92
+ }
78
93
  async function fetchWidgetToken(pk, apiBase) {
79
- const res = await fetch(`${apiBase}/public/widget-token`, {
94
+ const res = await fetchWithTimeout(`${apiBase}/public/widget-token`, {
80
95
  headers: { "x-kyc-pk": pk },
81
96
  credentials: "include"
82
97
  });
@@ -97,10 +112,11 @@ async function fetchDesignTokens(apiBase, opts) {
97
112
  else if (opts.clientId) params.push(`clientId=${encodeURIComponent(opts.clientId)}`);
98
113
  const url = `${apiBase}/public/design-tokens${params.length ? "?" + params.join("&") : ""}`;
99
114
  try {
100
- const res = await fetch(url, {
101
- headers: opts.pk ? { "x-kyc-pk": opts.pk } : {},
102
- credentials: "omit"
103
- });
115
+ const res = await fetchWithTimeout(
116
+ url,
117
+ { headers: opts.pk ? { "x-kyc-pk": opts.pk } : {}, credentials: "omit" },
118
+ 8e3
119
+ );
104
120
  return res.ok ? res.json() : null;
105
121
  } catch {
106
122
  return null;
@@ -123,7 +139,7 @@ function validateServerUrl(url) {
123
139
  }
124
140
  async function fetchSessionFromServer(serverUrl, clientMetadata, requestHeaders) {
125
141
  validateServerUrl(serverUrl);
126
- const res = await fetch(serverUrl, {
142
+ const res = await fetchWithTimeout(serverUrl, {
127
143
  method: "POST",
128
144
  headers: { "Content-Type": "application/json", ...requestHeaders },
129
145
  credentials: "include",
@@ -142,7 +158,7 @@ async function fetchSessionFromServer(serverUrl, clientMetadata, requestHeaders)
142
158
  return res.json();
143
159
  }
144
160
  async function fetchPublicSession(apiBase, sessionId, embedToken) {
145
- const res = await fetch(`${apiBase}/public/session/${sessionId}`, {
161
+ const res = await fetchWithTimeout(`${apiBase}/public/session/${sessionId}`, {
146
162
  headers: {
147
163
  "Content-Type": "application/json",
148
164
  "Authorization": `Bearer ${embedToken}`
@@ -160,7 +176,7 @@ async function fetchPublicSession(apiBase, sessionId, embedToken) {
160
176
  return res.json();
161
177
  }
162
178
  async function exchangeSessionForToken(apiBase, sessionId) {
163
- const res = await fetch(`${apiBase}/public/session/${sessionId}/token`, {
179
+ const res = await fetchWithTimeout(`${apiBase}/public/session/${sessionId}/token`, {
164
180
  method: "POST",
165
181
  headers: { "Content-Type": "application/json" }
166
182
  });
@@ -383,6 +399,18 @@ var Loader = class {
383
399
  get element() {
384
400
  return this.overlay;
385
401
  }
402
+ updateColor(primaryColor) {
403
+ if (!this.overlay) return;
404
+ const color = primaryColor || "#15BA68";
405
+ const bgCircle = this.overlay.querySelector("circle:first-child");
406
+ const fgCircle = this.overlay.querySelector("circle:last-child");
407
+ const bar = this.overlay.querySelector('[style*="kycprogress"]');
408
+ if (bgCircle) bgCircle.setAttribute("stroke", color + "33");
409
+ if (fgCircle) fgCircle.setAttribute("stroke", color);
410
+ if (bar) bar.style.backgroundColor = color;
411
+ const track = bar?.parentElement;
412
+ if (track) track.style.backgroundColor = color + "1A";
413
+ }
386
414
  showError(message, lang) {
387
415
  if (!this.overlay) return;
388
416
  const strings = LOADER_I18N[lang || ""] || LOADER_I18N.en;
@@ -504,23 +532,20 @@ var MessageHandler = class {
504
532
  if (e.source !== this.iframe.contentWindow) return;
505
533
  const data = e.data || {};
506
534
  const type = data.type;
507
- if (type === WIDGET_EVENTS.COMPLETE && this.callbacks.onSuccess) {
535
+ if (type === WIDGET_EVENTS.COMPLETE) {
508
536
  if (this.completionHandled) return;
509
537
  this.completionHandled = true;
510
- this.callbacks.onSuccess(data.result);
511
- setTimeout(() => this.iframe.remove(), 1e3);
512
- } else if (type === WIDGET_EVENTS.CANCEL && this.callbacks.onCancel) {
538
+ this.callbacks.onSuccess?.(data.result);
539
+ } else if (type === WIDGET_EVENTS.CANCEL) {
513
540
  if (this.completionHandled) return;
514
541
  this.completionHandled = true;
515
- this.callbacks.onCancel();
516
- setTimeout(() => this.iframe.remove(), 500);
517
- } else if (type === WIDGET_EVENTS.ERROR && this.callbacks.onError) {
542
+ this.callbacks.onCancel?.();
543
+ } else if (type === WIDGET_EVENTS.ERROR) {
518
544
  if (this.completionHandled) return;
519
545
  this.completionHandled = true;
520
- this.callbacks.onError(data.error || "Unknown error");
521
- setTimeout(() => this.iframe.remove(), 500);
522
- } else if (type === WIDGET_EVENTS.READY && this.callbacks.onReady) {
523
- this.callbacks.onReady();
546
+ this.callbacks.onError?.(data.error || "Unknown error");
547
+ } else if (type === WIDGET_EVENTS.READY) {
548
+ this.callbacks.onReady?.();
524
549
  }
525
550
  };
526
551
  window.addEventListener("message", this.listener);
@@ -531,7 +556,9 @@ var MessageHandler = class {
531
556
  onReadyOnce(callback) {
532
557
  const origin = getOrigin(this.iframeSrc);
533
558
  this.readyListener = (e) => {
534
- if (e.origin !== origin || e.data?.type !== WIDGET_EVENTS.READY) return;
559
+ if (e.origin !== origin || e.source !== this.iframe.contentWindow || e.data?.type !== WIDGET_EVENTS.READY) {
560
+ return;
561
+ }
535
562
  window.removeEventListener("message", this.readyListener);
536
563
  this.readyListener = null;
537
564
  callback();
@@ -660,6 +687,19 @@ function setupViewportSizing(overlay, iframe) {
660
687
  }
661
688
 
662
689
  // src/browser/index.ts
690
+ var FALLBACK_PRIMARY = "#15BA68";
691
+ var EARLY_COLOR_BUDGET_MS = 800;
692
+ var primaryFrom = (tokens) => tokens?.colors?.primary?.cannabis || FALLBACK_PRIMARY;
693
+ async function showLoaderWithEarlyColor(tokensPromise, lang) {
694
+ const earlyTokens = await Promise.race([
695
+ tokensPromise,
696
+ new Promise((resolve) => setTimeout(() => resolve(null), EARLY_COLOR_BUDGET_MS))
697
+ ]);
698
+ const primaryColor = primaryFrom(earlyTokens);
699
+ const loader = new Loader();
700
+ loader.show(primaryColor, lang);
701
+ return { loader, primaryColor };
702
+ }
663
703
  var FlonkKYC = class {
664
704
  constructor(options = {}) {
665
705
  this.widgetUrl = (options.widgetUrl || DEFAULT_WIDGET_URL).replace(/\/$/, "");
@@ -669,11 +709,13 @@ var FlonkKYC = class {
669
709
  /**
670
710
  * Open the KYC verification widget.
671
711
  *
672
- * Flows (pick one):
673
- * 1. `{ serverUrl, publishableKey }` — auto-create session via your backend (recommended).
674
- * `publishableKey` enables instant branded loader (~200-500ms faster).
675
- * 2. `{ sessionId, embedToken }` — server-to-server with pre-created session
676
- * 3. `{ publishableKey }` client-side only (legacy)
712
+ * Flows (pick one; add `publishableKey` to any for an instant branded loader):
713
+ * 1. `{ serverUrl }` — SDK auto-creates the session via your backend (recommended).
714
+ * 2. `{ sessionId, embedToken }` you created the session; pass its credentials.
715
+ * 3. `{ sessionId }` — **deprecated**: exchanges the sessionId for an embedToken
716
+ * via an extra round-trip. Prefer flow 2 by returning `embedToken` from your
717
+ * backend alongside `sessionId`.
718
+ * 4. `{ publishableKey }` — client-only; SDK mints a short-lived widget token.
677
719
  */
678
720
  async init(config) {
679
721
  if (!config) throw new FlonkValidationError("config is required");
@@ -778,27 +820,20 @@ var FlonkKYC = class {
778
820
  async initWithServerUrl(config) {
779
821
  const pk = config.publishableKey;
780
822
  const designTokensPromise = pk ? fetchDesignTokens(this.apiBase, { pk }) : Promise.resolve(null);
781
- let loader;
782
- if (pk) {
783
- const earlyTokens = await Promise.race([
784
- designTokensPromise,
785
- new Promise((resolve) => setTimeout(() => resolve(null), 150))
786
- ]);
787
- const primaryColor = earlyTokens?.colors?.primary?.cannabis || "#15BA68";
788
- loader = new Loader();
789
- loader.show(primaryColor, config.lang);
790
- }
823
+ const sessionPromise = fetchSessionFromServer(
824
+ config.serverUrl,
825
+ config.clientMetadata,
826
+ config.requestHeaders
827
+ );
828
+ const { loader, primaryColor } = await showLoaderWithEarlyColor(designTokensPromise, config.lang);
791
829
  try {
792
830
  const [{ sessionId, embedToken }, designTokens] = await Promise.all([
793
- fetchSessionFromServer(config.serverUrl, config.clientMetadata, config.requestHeaders),
831
+ sessionPromise,
794
832
  designTokensPromise
795
833
  ]);
796
834
  const finalTokens = designTokens ?? await fetchDesignTokens(this.apiBase, { sessionId });
797
- const primaryColor = finalTokens?.colors?.primary?.cannabis || "#15BA68";
798
- if (!loader) {
799
- loader = new Loader();
800
- loader.show(primaryColor, config.lang);
801
- }
835
+ const finalColor = primaryFrom(finalTokens);
836
+ if (finalColor !== primaryColor) loader.updateColor(finalColor);
802
837
  const sessionData = await fetchPublicSession(this.apiBase, sessionId, embedToken);
803
838
  const session = {
804
839
  id: sessionData.id,
@@ -807,15 +842,14 @@ var FlonkKYC = class {
807
842
  qrCodeUrl: sessionData.qrCodeUrl,
808
843
  testMode: sessionData.testMode || false,
809
844
  poaEnabled: sessionData.poaEnabled || false,
810
- poaRequired: sessionData.poaRequired || false
845
+ poaRequired: sessionData.poaRequired || false,
846
+ mlAutoCaptureEnabled: sessionData.mlAutoCaptureEnabled || false,
847
+ mlCropEnabled: sessionData.mlCropEnabled ?? true,
848
+ mlVerifyEnabled: sessionData.mlVerifyEnabled || false
811
849
  };
812
850
  return this.buildWidget(embedToken, session, config, loader, finalTokens);
813
851
  } catch (err) {
814
852
  const msg = err.message || "Failed to create session";
815
- if (!loader) {
816
- loader = new Loader();
817
- loader.show("#15BA68", config.lang);
818
- }
819
853
  loader.showError(msg, config.lang);
820
854
  config.onError?.(msg);
821
855
  throw err;
@@ -825,18 +859,21 @@ var FlonkKYC = class {
825
859
  * Flow 2: sessionId + embedToken — fetch session data, open widget.
826
860
  */
827
861
  async initWithEmbedToken(config) {
828
- const designTokens = await fetchDesignTokens(this.apiBase, {
829
- sessionId: config.sessionId
830
- });
831
- const primaryColor = designTokens?.colors?.primary?.cannabis || "#15BA68";
832
- const loader = new Loader();
833
- loader.show(primaryColor, config.lang);
862
+ const pk = config.publishableKey;
863
+ const designTokensPromise = pk ? fetchDesignTokens(this.apiBase, { pk }) : fetchDesignTokens(this.apiBase, { sessionId: config.sessionId });
864
+ const sessionPromise = fetchPublicSession(
865
+ this.apiBase,
866
+ config.sessionId,
867
+ config.embedToken
868
+ );
869
+ const { loader, primaryColor } = await showLoaderWithEarlyColor(designTokensPromise, config.lang);
834
870
  try {
835
- const sessionData = await fetchPublicSession(
836
- this.apiBase,
837
- config.sessionId,
838
- config.embedToken
839
- );
871
+ const [sessionData, designTokens] = await Promise.all([
872
+ sessionPromise,
873
+ designTokensPromise
874
+ ]);
875
+ const finalColor = primaryFrom(designTokens);
876
+ if (finalColor !== primaryColor) loader.updateColor(finalColor);
840
877
  const session = {
841
878
  id: sessionData.id,
842
879
  allowManualUpload: sessionData.allowManualUpload ?? config.allowManualUpload ?? true,
@@ -844,7 +881,10 @@ var FlonkKYC = class {
844
881
  qrCodeUrl: sessionData.qrCodeUrl,
845
882
  testMode: sessionData.testMode || false,
846
883
  poaEnabled: sessionData.poaEnabled || false,
847
- poaRequired: sessionData.poaRequired || false
884
+ poaRequired: sessionData.poaRequired || false,
885
+ mlAutoCaptureEnabled: sessionData.mlAutoCaptureEnabled || false,
886
+ mlCropEnabled: sessionData.mlCropEnabled ?? true,
887
+ mlVerifyEnabled: sessionData.mlVerifyEnabled || false
848
888
  };
849
889
  return this.buildWidget(config.embedToken, session, config, loader, designTokens);
850
890
  } catch (err) {
@@ -856,19 +896,22 @@ var FlonkKYC = class {
856
896
  }
857
897
  /**
858
898
  * Flow 3: sessionId only — exchange for embedToken, then init.
899
+ *
900
+ * @deprecated Prefer flow 2 (`sessionId` + `embedToken`). Return the
901
+ * `embedToken` from your backend together with the `sessionId` to skip this
902
+ * extra token-exchange round-trip.
859
903
  */
860
904
  async initWithSession(config) {
861
- const designTokens = await fetchDesignTokens(this.apiBase, {
905
+ const designTokensPromise = fetchDesignTokens(this.apiBase, {
862
906
  sessionId: config.sessionId
863
907
  });
864
- const primaryColor = designTokens?.colors?.primary?.cannabis || "#15BA68";
865
- const loader = new Loader();
866
- loader.show(primaryColor, config.lang);
908
+ const exchangePromise = exchangeSessionForToken(this.apiBase, config.sessionId);
909
+ const { loader } = await showLoaderWithEarlyColor(designTokensPromise, config.lang);
867
910
  try {
868
- const { embedToken, session } = await exchangeSessionForToken(
869
- this.apiBase,
870
- config.sessionId
871
- );
911
+ const [{ embedToken, session }, designTokens] = await Promise.all([
912
+ exchangePromise,
913
+ designTokensPromise
914
+ ]);
872
915
  return this.buildWidget(embedToken, session, config, loader, designTokens);
873
916
  } catch (err) {
874
917
  const msg = err.message || "Failed to initialize verification";
@@ -882,12 +925,11 @@ var FlonkKYC = class {
882
925
  */
883
926
  async initWithPublishableKey(config) {
884
927
  const pk = config.publishableKey;
885
- const designTokens = await fetchDesignTokens(this.apiBase, { pk });
886
- const primaryColor = designTokens?.colors?.primary?.cannabis || "#15BA68";
887
- const loader = new Loader();
888
- loader.show(primaryColor, config.lang);
928
+ const designTokensPromise = fetchDesignTokens(this.apiBase, { pk });
929
+ const widgetTokenPromise = fetchWidgetToken(pk, this.apiBase);
930
+ const { loader, primaryColor } = await showLoaderWithEarlyColor(designTokensPromise, config.lang);
889
931
  try {
890
- const data = await fetchWidgetToken(pk, this.apiBase);
932
+ const data = await widgetTokenPromise;
891
933
  const params = {
892
934
  mode: "embedded",
893
935
  publishableKey: pk,
@@ -927,6 +969,9 @@ var FlonkKYC = class {
927
969
  if (session.testMode) params.testMode = "true";
928
970
  if (session.poaEnabled) params.poaEnabled = "true";
929
971
  if (session.poaRequired) params.poaRequired = "true";
972
+ if (session.mlAutoCaptureEnabled) params.mlAutoCaptureEnabled = "true";
973
+ if (session.mlCropEnabled !== false) params.mlCropEnabled = "true";
974
+ if (session.mlVerifyEnabled) params.mlVerifyEnabled = "true";
930
975
  if (designTokens?.colors) {
931
976
  params.designTokens = JSON.stringify(designTokens);
932
977
  }
@@ -939,7 +984,7 @@ var FlonkKYC = class {
939
984
  if (config.lang) params.lang = config.lang;
940
985
  if (config.overlayColor) params.overlayColor = config.overlayColor;
941
986
  return this.openWidget(params, {
942
- primaryColor: designTokens?.colors?.primary?.cannabis || "#15BA68",
987
+ primaryColor: primaryFrom(designTokens),
943
988
  lang: config.lang,
944
989
  loader,
945
990
  onSuccess: config.onSuccess,
@@ -980,25 +1025,27 @@ var FlonkKYC = class {
980
1025
  } catch {
981
1026
  }
982
1027
  };
983
- const afterCleanup = (delayMs) => () => setTimeout(cleanupAll, delayMs);
1028
+ const afterCleanup = (delayMs) => setTimeout(cleanupAll, delayMs);
984
1029
  const handler = new MessageHandler(src, iframe, {
985
- onSuccess: opts.onSuccess ? (r) => {
1030
+ onSuccess: (r) => {
986
1031
  opts.onSuccess?.(r);
987
- afterCleanup(1e3)();
988
- } : void 0,
989
- onError: opts.onError ? (e) => {
1032
+ afterCleanup(1e3);
1033
+ },
1034
+ onError: (e) => {
990
1035
  opts.onError?.(e);
991
- afterCleanup(500)();
992
- } : void 0,
993
- onCancel: opts.onCancel ? () => {
1036
+ afterCleanup(500);
1037
+ },
1038
+ onCancel: () => {
994
1039
  opts.onCancel?.();
995
- afterCleanup(500)();
996
- } : void 0,
1040
+ afterCleanup(500);
1041
+ },
997
1042
  onReady: opts.onReady
998
1043
  });
999
1044
  handler.listen();
1000
1045
  handler.onReadyOnce(() => {
1001
- transitionLoaderToIframe(loader.element, iframe, () => loader.destroy());
1046
+ if (loader.element) {
1047
+ transitionLoaderToIframe(loader.element, iframe, () => loader.destroy());
1048
+ }
1002
1049
  });
1003
1050
  return {
1004
1051
  iframe,
@@ -1069,7 +1116,7 @@ function FlonkKYCWidget({
1069
1116
  widgetRef.current?.destroy();
1070
1117
  widgetRef.current = null;
1071
1118
  };
1072
- }, [sdk, publishableKey, serverUrl, sessionId, autoOpen]);
1119
+ }, [sdk, publishableKey, serverUrl, sessionId, embedToken, lang, overlayColor, allowManualUpload, autoOpen]);
1073
1120
  return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: mountRef });
1074
1121
  }
1075
1122