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