@flonkid/kyc 1.9.2 → 1.9.3

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/core.cjs ADDED
@@ -0,0 +1,1371 @@
1
+ 'use strict';
2
+
3
+ // src/shared/constants.ts
4
+ var SDK_VERSION = "1.9.3";
5
+ var DEFAULT_WIDGET_URL = "https://widget.flonk.id";
6
+ var DEFAULT_API_BASE = "https://api.flonk.id/v1";
7
+ var API_VERSION = "2026-06-01";
8
+ var PROTOCOL_VERSION = 1;
9
+ var WIDGET_EVENTS = {
10
+ READY: "KYC_WIDGET_READY",
11
+ COMPLETE: "KYC_COMPLETE",
12
+ CANCEL: "KYC_CANCEL",
13
+ ERROR: "KYC_ERROR",
14
+ CONFIG: "KYC_WIDGET_CONFIG"
15
+ };
16
+ var WIDGET_PARAMS = {
17
+ PROTOCOL_VERSION: "pv",
18
+ SESSION_ID: "sessionId",
19
+ EMBED_TOKEN: "embedToken",
20
+ TOKEN: "token",
21
+ PUBLISHABLE_KEY: "publishableKey",
22
+ CLIENT_ID: "clientId",
23
+ CLIENT_METADATA: "clientMetadata",
24
+ DESIGN_TOKENS: "designTokens",
25
+ ALLOW_MANUAL_UPLOAD: "allowManualUpload",
26
+ LANG: "lang",
27
+ OVERLAY_COLOR: "overlayColor",
28
+ MODE: "mode"
29
+ };
30
+
31
+ // src/shared/errors.ts
32
+ var FlonkError = class extends Error {
33
+ constructor(message, code, statusCode) {
34
+ super(message);
35
+ this.code = code;
36
+ this.statusCode = statusCode;
37
+ this.name = "FlonkError";
38
+ }
39
+ };
40
+ var FlonkValidationError = class extends FlonkError {
41
+ constructor(message) {
42
+ super(message, "validation_error", 400);
43
+ this.name = "FlonkValidationError";
44
+ }
45
+ };
46
+
47
+ // src/browser/utils.ts
48
+ function getOrigin(url) {
49
+ try {
50
+ return new URL(url).origin;
51
+ } catch {
52
+ return "null";
53
+ }
54
+ }
55
+ function isDesktop() {
56
+ return window.innerWidth >= 1024 && !("ontouchstart" in window || navigator.maxTouchPoints > 0);
57
+ }
58
+ function setStyles(el, styles) {
59
+ Object.assign(el.style, styles);
60
+ }
61
+ function createEl(tag, styles, attrs) {
62
+ const el = document.createElement(tag);
63
+ if (styles) setStyles(el, styles);
64
+ if (attrs) {
65
+ for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
66
+ }
67
+ return el;
68
+ }
69
+ function debounce(fn, ms) {
70
+ let timer;
71
+ return ((...args) => {
72
+ clearTimeout(timer);
73
+ timer = setTimeout(() => fn(...args), ms);
74
+ });
75
+ }
76
+ function generateSecondaryColor(hex) {
77
+ try {
78
+ const h = hex.replace("#", "");
79
+ const r = parseInt(h.substring(0, 2), 16);
80
+ const g = parseInt(h.substring(2, 4), 16);
81
+ const b = parseInt(h.substring(4, 6), 16);
82
+ const f = 0.6;
83
+ const toHex = (n) => {
84
+ const s = n.toString(16);
85
+ return s.length === 1 ? "0" + s : s;
86
+ };
87
+ return "#" + toHex(Math.round(r + (255 - r) * f)) + toHex(Math.round(g + (255 - g) * f)) + toHex(Math.round(b + (255 - b) * f));
88
+ } catch {
89
+ return "#93c5fd";
90
+ }
91
+ }
92
+ var DEFAULT_FETCH_TIMEOUT_MS = 2e4;
93
+ async function fetchWithTimeout(url, init = {}, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
94
+ const controller = new AbortController();
95
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
96
+ try {
97
+ return await fetch(url, { ...init, signal: controller.signal });
98
+ } catch (err) {
99
+ if (err?.name === "AbortError") {
100
+ throw new Error(`Request timed out after ${timeoutMs}ms: ${url}`);
101
+ }
102
+ throw err;
103
+ } finally {
104
+ clearTimeout(timer);
105
+ }
106
+ }
107
+ var widgetTokenInflight = /* @__PURE__ */ new Map();
108
+ async function fetchWidgetToken(pk, apiBase) {
109
+ const key = `${apiBase}|${pk}`;
110
+ const existing = widgetTokenInflight.get(key);
111
+ if (existing) return existing;
112
+ const promise = (async () => {
113
+ const res = await fetchWithTimeout(`${apiBase}/public/widget-token`, {
114
+ headers: { "x-kyc-pk": pk },
115
+ credentials: "include"
116
+ });
117
+ if (!res.ok) {
118
+ let message = `Widget token request failed (${res.status})`;
119
+ try {
120
+ const b = await res.json();
121
+ message = b.error || b.message || message;
122
+ } catch {
123
+ }
124
+ throw new Error(message);
125
+ }
126
+ return res.json();
127
+ })().finally(() => {
128
+ widgetTokenInflight.delete(key);
129
+ });
130
+ widgetTokenInflight.set(key, promise);
131
+ return promise;
132
+ }
133
+ var BRANDING_CACHE_TTL_MS = 5 * 60 * 1e3;
134
+ var brandingCache = /* @__PURE__ */ new Map();
135
+ function brandingCacheKey(opts) {
136
+ if (opts.pk) return `pk:${opts.pk}`;
137
+ if (opts.sessionId) return `sid:${opts.sessionId}`;
138
+ if (opts.clientId) return `cid:${opts.clientId}`;
139
+ return null;
140
+ }
141
+ async function requestDesignTokens(apiBase, opts) {
142
+ const params = [];
143
+ if (opts.sessionId) params.push(`sessionId=${encodeURIComponent(opts.sessionId)}`);
144
+ else if (opts.clientId) params.push(`clientId=${encodeURIComponent(opts.clientId)}`);
145
+ const url = `${apiBase}/public/design-tokens${params.length ? "?" + params.join("&") : ""}`;
146
+ const res = await fetchWithTimeout(
147
+ url,
148
+ { headers: opts.pk ? { "x-kyc-pk": opts.pk } : {}, credentials: "omit" },
149
+ 8e3
150
+ );
151
+ return res.ok ? res.json() : null;
152
+ }
153
+ async function fetchDesignTokens(apiBase, opts) {
154
+ const key = brandingCacheKey(opts);
155
+ if (!key) return null;
156
+ const now = Date.now();
157
+ const cached = brandingCache.get(key);
158
+ if (cached && cached.expiresAt > now) {
159
+ return cached.promise;
160
+ }
161
+ const promise = requestDesignTokens(apiBase, opts).catch(() => null);
162
+ brandingCache.set(key, { promise, expiresAt: now + BRANDING_CACHE_TTL_MS });
163
+ promise.then((tokens) => {
164
+ if (tokens == null) brandingCache.delete(key);
165
+ });
166
+ return promise;
167
+ }
168
+ function preloadDesignTokens(apiBase, opts) {
169
+ return fetchDesignTokens(apiBase, opts);
170
+ }
171
+ function validateServerUrl(url) {
172
+ if (url.startsWith("/")) return;
173
+ try {
174
+ const parsed = new URL(url);
175
+ const isLocalhost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
176
+ if (parsed.protocol !== "https:" && !isLocalhost) {
177
+ throw new Error(
178
+ `serverUrl must use HTTPS in production. Got: ${parsed.protocol}//. Use HTTPS ('https://api.myapp.com/...') or a relative path ('/api/...')`
179
+ );
180
+ }
181
+ } catch (e) {
182
+ if (e.message.includes("serverUrl must use HTTPS")) throw e;
183
+ throw new Error(`Invalid serverUrl: ${url}`);
184
+ }
185
+ }
186
+ var sessionCreateInflight = /* @__PURE__ */ new Map();
187
+ async function fetchSessionFromServer(serverUrl, clientMetadata, requestHeaders) {
188
+ validateServerUrl(serverUrl);
189
+ const key = `${serverUrl}|${JSON.stringify(clientMetadata ?? null)}`;
190
+ const existing = sessionCreateInflight.get(key);
191
+ if (existing) return existing;
192
+ const promise = (async () => {
193
+ const res = await fetchWithTimeout(serverUrl, {
194
+ method: "POST",
195
+ headers: { "Content-Type": "application/json", ...requestHeaders },
196
+ credentials: "include",
197
+ body: JSON.stringify({ clientMetadata })
198
+ });
199
+ if (!res.ok) {
200
+ let message = `Session request failed (${res.status})`;
201
+ try {
202
+ const body = await res.json();
203
+ if (body.error) message = body.error;
204
+ else if (body.message) message = body.message;
205
+ } catch {
206
+ }
207
+ throw new Error(message);
208
+ }
209
+ return res.json();
210
+ })().finally(() => {
211
+ sessionCreateInflight.delete(key);
212
+ });
213
+ sessionCreateInflight.set(key, promise);
214
+ return promise;
215
+ }
216
+ async function exchangeSessionForToken(apiBase, sessionId) {
217
+ const res = await fetchWithTimeout(`${apiBase}/public/session/${sessionId}/token`, {
218
+ method: "POST",
219
+ headers: { "Content-Type": "application/json" }
220
+ });
221
+ if (!res.ok) {
222
+ let message = `Token exchange failed (${res.status})`;
223
+ try {
224
+ const b = await res.json();
225
+ message = b.error || b.message || message;
226
+ } catch {
227
+ }
228
+ throw new Error(message);
229
+ }
230
+ return res.json();
231
+ }
232
+
233
+ // src/browser/iframe-manager.ts
234
+ function createIframe(src) {
235
+ const d = isDesktop();
236
+ const iframe = createEl(
237
+ "iframe",
238
+ {
239
+ border: "0",
240
+ width: window.innerWidth + "px",
241
+ height: window.innerHeight + "px",
242
+ position: "fixed",
243
+ top: "0",
244
+ left: "0",
245
+ zIndex: "9999",
246
+ // Hidden until reveal so the loader overlay doesn't show the iframe's own
247
+ // loading state through its translucent backdrop (double loader). Reveal
248
+ // is gated on READY-OR-a-timeout (see index.ts), so a missing READY can't
249
+ // leave it permanently hidden — that timeout is what removed the deadlock.
250
+ opacity: "0",
251
+ visibility: "hidden",
252
+ background: "transparent",
253
+ backgroundColor: "transparent",
254
+ borderRadius: d ? "0" : "",
255
+ boxShadow: d ? "none" : "",
256
+ colorScheme: "normal"
257
+ },
258
+ {
259
+ src,
260
+ allow: "camera;microphone;clipboard-read;clipboard-write",
261
+ sandbox: "allow-scripts allow-forms allow-same-origin allow-popups",
262
+ "aria-label": "KYC Verification",
263
+ allowtransparency: "true"
264
+ }
265
+ );
266
+ try {
267
+ iframe.style.setProperty("background", "transparent", "important");
268
+ iframe.style.setProperty("background-color", "transparent", "important");
269
+ iframe.style.setProperty("color-scheme", "normal", "important");
270
+ } catch {
271
+ }
272
+ return iframe;
273
+ }
274
+ function adjustZIndex(loader, iframe) {
275
+ if (!isDesktop()) return;
276
+ const all = Array.from(document.querySelectorAll("*"));
277
+ const maxZ = Math.max(
278
+ ...all.map((el) => parseInt(getComputedStyle(el).zIndex) || 0).filter((z) => z < 999999)
279
+ );
280
+ if (maxZ > 9998) {
281
+ loader.style.zIndex = String(maxZ + 1);
282
+ iframe.style.zIndex = String(maxZ + 2);
283
+ }
284
+ }
285
+ function transitionLoaderToIframe(loader, iframe, onDone) {
286
+ const d = isDesktop();
287
+ const dur = d ? 300 : 500;
288
+ const card = loader.querySelector("div");
289
+ if (card) {
290
+ setStyles(card, {
291
+ transition: "transform 300ms ease-out, opacity 300ms ease-out",
292
+ transform: d ? "translateY(-10px) scale(0.98)" : "translateY(-15px) scale(0.96)",
293
+ opacity: "0"
294
+ });
295
+ }
296
+ setStyles(loader, { transition: "opacity 300ms ease-out", opacity: "0" });
297
+ setTimeout(() => {
298
+ onDone();
299
+ setStyles(iframe, {
300
+ transition: d ? "opacity 300ms cubic-bezier(0.4,0,0.2,1),visibility 0ms" : "opacity 400ms ease-out,visibility 0ms",
301
+ opacity: "1",
302
+ visibility: "visible"
303
+ });
304
+ }, dur);
305
+ }
306
+
307
+ // src/browser/diagnostics.ts
308
+ var handlers = /* @__PURE__ */ new Set();
309
+ function addDiagnosticHandler(handler) {
310
+ handlers.add(handler);
311
+ return () => {
312
+ handlers.delete(handler);
313
+ };
314
+ }
315
+ function debugEnabled() {
316
+ try {
317
+ return Boolean(globalThis.__FLONK_DEBUG__);
318
+ } catch {
319
+ return false;
320
+ }
321
+ }
322
+ function emitDiagnostic(code, level, message, detail) {
323
+ const event = { code, level, message, detail };
324
+ if (debugEnabled()) {
325
+ try {
326
+ const fn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
327
+ fn(`[flonk:${code}] ${message}`, detail ?? "");
328
+ } catch {
329
+ }
330
+ }
331
+ for (const handler of handlers) {
332
+ try {
333
+ handler(event);
334
+ } catch {
335
+ }
336
+ }
337
+ }
338
+
339
+ // src/browser/prewarm.ts
340
+ var noop = () => {
341
+ };
342
+ function originOf(url) {
343
+ try {
344
+ return new URL(url).origin;
345
+ } catch {
346
+ return url.replace(/\/+$/, "");
347
+ }
348
+ }
349
+ function addLinkHint(doc, rel, href, attrs) {
350
+ try {
351
+ const existing = doc.querySelector(`link[rel="${rel}"][href="${href}"]`);
352
+ if (existing) return;
353
+ const link = doc.createElement("link");
354
+ link.rel = rel;
355
+ link.href = href;
356
+ if (attrs) for (const k of Object.keys(attrs)) link.setAttribute(k, attrs[k]);
357
+ (doc.head || doc.documentElement).appendChild(link);
358
+ } catch {
359
+ }
360
+ }
361
+ function preconnect(widgetUrl, doc = document) {
362
+ const origin = originOf(widgetUrl);
363
+ addLinkHint(doc, "preconnect", origin, { crossorigin: "" });
364
+ addLinkHint(doc, "dns-prefetch", origin);
365
+ }
366
+ function defaultScheduleIdle(fn) {
367
+ if (typeof window === "undefined") {
368
+ fn();
369
+ return;
370
+ }
371
+ const w = window;
372
+ const run = () => {
373
+ if (typeof w.requestIdleCallback === "function") {
374
+ w.requestIdleCallback(fn, { timeout: 2e3 });
375
+ } else {
376
+ setTimeout(fn, 200);
377
+ }
378
+ };
379
+ if (document.readyState === "complete") run();
380
+ else window.addEventListener("load", run, { once: true });
381
+ }
382
+ function prewarm(options) {
383
+ const level = options.level ?? "connect";
384
+ const doc = options.doc ?? (typeof document !== "undefined" ? document : void 0);
385
+ if (level === "none" || !doc) {
386
+ emitDiagnostic("PREWARM_SKIPPED", "info", `Prewarm skipped (level=${level}${doc ? "" : ", no document"}).`);
387
+ return noop;
388
+ }
389
+ const scheduleIdle = options.scheduleIdle ?? defaultScheduleIdle;
390
+ const origin = originOf(options.widgetUrl);
391
+ preconnect(options.widgetUrl, doc);
392
+ const warmAssets = () => {
393
+ if (options.apiBase) {
394
+ const q = options.publishableKey ? `pk=${encodeURIComponent(options.publishableKey)}` : options.sessionId ? `sessionId=${encodeURIComponent(options.sessionId)}` : "";
395
+ addLinkHint(doc, "prefetch", `${options.apiBase}/v1/public/design-tokens${q ? `?${q}` : ""}`);
396
+ }
397
+ addLinkHint(doc, "prefetch", `${origin}/`, { as: "document" });
398
+ };
399
+ if (level === "intent") {
400
+ return attachIntent(doc, options.trigger ?? null, () => {
401
+ warmAssets();
402
+ mountHiddenIframe(doc, origin);
403
+ });
404
+ }
405
+ scheduleIdle(() => {
406
+ warmAssets();
407
+ if (level === "eager") mountHiddenIframe(doc, origin);
408
+ });
409
+ return noop;
410
+ }
411
+ function attachIntent(doc, trigger, warm) {
412
+ let fired = false;
413
+ const fire = () => {
414
+ if (fired) return;
415
+ fired = true;
416
+ cleanup();
417
+ warm();
418
+ };
419
+ const events = [
420
+ ["mouseenter", fire],
421
+ ["focusin", fire],
422
+ ["touchstart", fire]
423
+ ];
424
+ let io = null;
425
+ const cleanup = () => {
426
+ if (trigger) for (const [type, fn] of events) trigger.removeEventListener(type, fn);
427
+ if (io) {
428
+ io.disconnect();
429
+ io = null;
430
+ }
431
+ };
432
+ if (trigger) {
433
+ for (const [type, fn] of events) trigger.addEventListener(type, fn, { passive: true });
434
+ const w = doc.defaultView || (typeof window !== "undefined" ? window : void 0);
435
+ if (w && typeof w.IntersectionObserver === "function") {
436
+ const observer = new w.IntersectionObserver((entries) => {
437
+ if (entries.some((e) => e.isIntersecting)) fire();
438
+ });
439
+ observer.observe(trigger);
440
+ io = observer;
441
+ }
442
+ } else {
443
+ defaultScheduleIdle(fire);
444
+ }
445
+ return cleanup;
446
+ }
447
+ function mountHiddenIframe(doc, origin) {
448
+ try {
449
+ if (doc.querySelector("iframe[data-flonk-prewarm]")) return;
450
+ const iframe = doc.createElement("iframe");
451
+ iframe.src = `${origin}/?prewarm=1`;
452
+ iframe.setAttribute("data-flonk-prewarm", "1");
453
+ iframe.setAttribute("aria-hidden", "true");
454
+ iframe.tabIndex = -1;
455
+ iframe.style.cssText = "position:absolute;width:1px;height:1px;left:-9999px;top:-9999px;border:0;opacity:0;pointer-events:none";
456
+ (doc.body || doc.documentElement).appendChild(iframe);
457
+ } catch {
458
+ }
459
+ }
460
+
461
+ // src/browser/loader.ts
462
+ var LOADER_I18N = {
463
+ en: { title: "Initializing...", subtitle: "Loading KYC widget", errorTitle: "Something went wrong", close: "Close" },
464
+ de: { title: "Initialisierung...", subtitle: "KYC-Widget wird geladen", errorTitle: "Ein Fehler ist aufgetreten", close: "Schlie\xDFen" },
465
+ uk: { title: "\u0406\u043D\u0456\u0446\u0456\u0430\u043B\u0456\u0437\u0430\u0446\u0456\u044F...", subtitle: "\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F KYC-\u0432\u0456\u0434\u0436\u0435\u0442\u0430", errorTitle: "\u0429\u043E\u0441\u044C \u043F\u0456\u0448\u043B\u043E \u043D\u0435 \u0442\u0430\u043A", close: "\u0417\u0430\u043A\u0440\u0438\u0442\u0438" }
466
+ };
467
+ var KEYFRAMES = `
468
+ @keyframes kycspin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}
469
+ @keyframes kycdash{0%{stroke-dasharray:1,150;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-35}100%{stroke-dasharray:90,150;stroke-dashoffset:-124}}
470
+ @keyframes kycprogress{0%{transform:translateX(-100%)}50%{transform:translateX(0%)}100%{transform:translateX(250%)}}
471
+ `.trim();
472
+ var styleInjected = false;
473
+ var Loader = class {
474
+ constructor() {
475
+ this.overlay = null;
476
+ this.cleanup = null;
477
+ this.origBodyStyles = null;
478
+ }
479
+ show(primaryColor, lang) {
480
+ const color = primaryColor || "#15BA68";
481
+ const strings = LOADER_I18N[lang || ""] || LOADER_I18N.en;
482
+ const d = isDesktop();
483
+ if (!styleInjected) {
484
+ const style = document.createElement("style");
485
+ style.textContent = KEYFRAMES;
486
+ document.head.appendChild(style);
487
+ styleInjected = true;
488
+ }
489
+ const overlay = createEl("div", {
490
+ position: "fixed",
491
+ top: "0",
492
+ left: "0",
493
+ right: "0",
494
+ bottom: "0",
495
+ width: window.innerWidth + "px",
496
+ height: window.innerHeight + "px",
497
+ background: d ? "rgba(0,0,0,0.05)" : "transparent",
498
+ backdropFilter: d ? "blur(2px)" : "none",
499
+ display: "flex",
500
+ alignItems: "center",
501
+ justifyContent: "center",
502
+ zIndex: "9998",
503
+ transition: "opacity 600ms ease-out,transform 400ms ease-out",
504
+ opacity: "1",
505
+ overflow: "hidden",
506
+ margin: "0",
507
+ padding: "0"
508
+ });
509
+ const card = createEl("div", {
510
+ width: d ? "min(400px, 35vw)" : "min(360px, 85vw)",
511
+ backgroundColor: "#FFF",
512
+ borderRadius: d ? "32px" : "24px",
513
+ boxShadow: d ? "0 20px 64px rgba(17,17,17,0.12),0 4px 16px rgba(17,17,17,0.08)" : "0 8px 32px rgba(17,17,17,0.08)",
514
+ display: "flex",
515
+ flexDirection: "column",
516
+ alignItems: "center",
517
+ justifyContent: "center",
518
+ padding: d ? "48px 36px" : "36px 24px",
519
+ gap: d ? "24px" : "20px",
520
+ transition: "transform 400ms ease-out,opacity 400ms ease-out"
521
+ });
522
+ const wrap = createEl("div", {
523
+ width: "48px",
524
+ height: "48px",
525
+ display: "flex",
526
+ alignItems: "center",
527
+ justifyContent: "center"
528
+ });
529
+ const NS = "http://www.w3.org/2000/svg";
530
+ const svg = document.createElementNS(NS, "svg");
531
+ svg.setAttribute("width", "48");
532
+ svg.setAttribute("height", "48");
533
+ svg.setAttribute("viewBox", "0 0 48 48");
534
+ svg.style.animation = "kycspin 1.2s cubic-bezier(0.4,0,0.6,1) infinite";
535
+ const bg = document.createElementNS(NS, "circle");
536
+ for (const [k, v] of Object.entries({
537
+ cx: "24",
538
+ cy: "24",
539
+ r: "20",
540
+ "stroke-width": "3",
541
+ fill: "none",
542
+ stroke: color + "33"
543
+ })) bg.setAttribute(k, v);
544
+ const fg = document.createElementNS(NS, "circle");
545
+ for (const [k, v] of Object.entries({
546
+ cx: "24",
547
+ cy: "24",
548
+ r: "20",
549
+ "stroke-width": "3",
550
+ fill: "none",
551
+ stroke: color,
552
+ "stroke-linecap": "round",
553
+ "stroke-dasharray": "62.8",
554
+ "stroke-dashoffset": "15.7"
555
+ })) fg.setAttribute(k, v);
556
+ setStyles(fg, { transformOrigin: "center", animation: "kycdash 1.5s ease-in-out infinite" });
557
+ svg.append(bg, fg);
558
+ wrap.appendChild(svg);
559
+ const font = 'Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif';
560
+ const textBox = createEl("div", { textAlign: "center" });
561
+ const title = createEl("h3", { fontFamily: font, fontWeight: "600", fontSize: "18px", lineHeight: "1.3", color: "#1F2937", margin: "0 0 4px 0" });
562
+ title.textContent = strings.title;
563
+ const subtitle = createEl("p", { fontFamily: font, fontWeight: "400", fontSize: "13px", lineHeight: "1.4", color: "rgba(31,41,55,0.7)", margin: "0" });
564
+ subtitle.textContent = strings.subtitle;
565
+ textBox.append(title, subtitle);
566
+ const track = createEl("div", { width: "100%", maxWidth: "240px", height: "3px", backgroundColor: color + "1A", borderRadius: "2px", overflow: "hidden" });
567
+ const bar = createEl("div", { width: "40%", height: "100%", backgroundColor: color, borderRadius: "2px", transform: "translateX(-100%)", animation: "kycprogress 2000ms ease-in-out infinite" });
568
+ track.appendChild(bar);
569
+ card.append(wrap, textBox, track);
570
+ overlay.appendChild(card);
571
+ this.origBodyStyles = { overflow: document.body.style.overflow, position: document.body.style.position };
572
+ setStyles(document.body, { overflow: "hidden", position: "fixed", width: "100%", height: "100%" });
573
+ document.body.appendChild(overlay);
574
+ let prevW = window.innerWidth;
575
+ let prevH = window.innerHeight;
576
+ const onResize = debounce(() => {
577
+ const w = window.innerWidth;
578
+ const h = window.innerHeight;
579
+ if (Math.abs(w - prevW) > 1 || Math.abs(h - prevH) > 1) {
580
+ setStyles(overlay, { width: w + "px", height: h + "px" });
581
+ prevW = w;
582
+ prevH = h;
583
+ }
584
+ }, d ? 50 : 150);
585
+ window.addEventListener("resize", onResize);
586
+ this.overlay = overlay;
587
+ this.cleanup = () => {
588
+ window.removeEventListener("resize", onResize);
589
+ if (this.origBodyStyles) {
590
+ setStyles(document.body, { ...this.origBodyStyles, width: "", height: "" });
591
+ }
592
+ };
593
+ return overlay;
594
+ }
595
+ get element() {
596
+ return this.overlay;
597
+ }
598
+ updateColor(primaryColor) {
599
+ if (!this.overlay) return;
600
+ const color = primaryColor || "#15BA68";
601
+ const bgCircle = this.overlay.querySelector("circle:first-child");
602
+ const fgCircle = this.overlay.querySelector("circle:last-child");
603
+ const bar = this.overlay.querySelector('[style*="kycprogress"]');
604
+ if (bgCircle) bgCircle.setAttribute("stroke", color + "33");
605
+ if (fgCircle) fgCircle.setAttribute("stroke", color);
606
+ if (bar) bar.style.backgroundColor = color;
607
+ const track = bar?.parentElement;
608
+ if (track) track.style.backgroundColor = color + "1A";
609
+ }
610
+ showError(message, lang) {
611
+ if (!this.overlay) return;
612
+ const strings = LOADER_I18N[lang || ""] || LOADER_I18N.en;
613
+ const card = this.overlay.querySelector("div");
614
+ if (!card) return;
615
+ card.innerHTML = "";
616
+ const font = 'Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif';
617
+ const iconWrap = createEl("div", {
618
+ width: "48px",
619
+ height: "48px",
620
+ borderRadius: "50%",
621
+ backgroundColor: "rgba(239, 68, 68, 0.1)",
622
+ display: "flex",
623
+ alignItems: "center",
624
+ justifyContent: "center"
625
+ });
626
+ const NS = "http://www.w3.org/2000/svg";
627
+ const svg = document.createElementNS(NS, "svg");
628
+ svg.setAttribute("width", "24");
629
+ svg.setAttribute("height", "24");
630
+ svg.setAttribute("viewBox", "0 0 24 24");
631
+ svg.setAttribute("fill", "none");
632
+ svg.setAttribute("stroke", "#ef4444");
633
+ svg.setAttribute("stroke-width", "2");
634
+ svg.setAttribute("stroke-linecap", "round");
635
+ const line1 = document.createElementNS(NS, "line");
636
+ line1.setAttribute("x1", "18");
637
+ line1.setAttribute("y1", "6");
638
+ line1.setAttribute("x2", "6");
639
+ line1.setAttribute("y2", "18");
640
+ const line2 = document.createElementNS(NS, "line");
641
+ line2.setAttribute("x1", "6");
642
+ line2.setAttribute("y1", "6");
643
+ line2.setAttribute("x2", "18");
644
+ line2.setAttribute("y2", "18");
645
+ svg.append(line1, line2);
646
+ iconWrap.appendChild(svg);
647
+ const title = createEl("h3", {
648
+ fontFamily: font,
649
+ fontWeight: "600",
650
+ fontSize: "18px",
651
+ lineHeight: "1.3",
652
+ color: "#1F2937",
653
+ margin: "0",
654
+ textAlign: "center"
655
+ });
656
+ title.textContent = strings.errorTitle;
657
+ const msg = createEl("p", {
658
+ fontFamily: font,
659
+ fontWeight: "400",
660
+ fontSize: "13px",
661
+ lineHeight: "1.5",
662
+ color: "rgba(31,41,55,0.7)",
663
+ margin: "0",
664
+ textAlign: "center",
665
+ wordBreak: "break-word",
666
+ maxWidth: "300px"
667
+ });
668
+ msg.textContent = message;
669
+ const btn = createEl("button", {
670
+ fontFamily: font,
671
+ fontWeight: "600",
672
+ fontSize: "14px",
673
+ padding: "10px 32px",
674
+ borderRadius: "12px",
675
+ border: "none",
676
+ backgroundColor: "#1F2937",
677
+ color: "#fff",
678
+ cursor: "pointer",
679
+ transition: "opacity 150ms"
680
+ });
681
+ btn.textContent = strings.close;
682
+ btn.onmouseenter = () => {
683
+ btn.style.opacity = "0.85";
684
+ };
685
+ btn.onmouseleave = () => {
686
+ btn.style.opacity = "1";
687
+ };
688
+ btn.onclick = () => this.destroy();
689
+ card.append(iconWrap, title, msg, btn);
690
+ }
691
+ fadeOut() {
692
+ if (!this.overlay) return;
693
+ const d = isDesktop();
694
+ const card = this.overlay.querySelector("div");
695
+ if (card) {
696
+ setStyles(card, {
697
+ transform: d ? "translateY(-10px) scale(0.98)" : "translateY(-15px) scale(0.96)",
698
+ opacity: "0"
699
+ });
700
+ }
701
+ this.overlay.style.opacity = "0";
702
+ }
703
+ destroy() {
704
+ this.cleanup?.();
705
+ if (this.overlay?.parentNode) this.overlay.remove();
706
+ this.overlay = null;
707
+ this.cleanup = null;
708
+ }
709
+ };
710
+ function isServerLoaderReady() {
711
+ return typeof window !== "undefined" && !!window.FlonkWidgetLoader?.show;
712
+ }
713
+ var serverScriptRequested = false;
714
+ function loadServerLoaderScript(apiBase, pk, sessionId) {
715
+ if (typeof document === "undefined" || serverScriptRequested || isServerLoaderReady()) return;
716
+ serverScriptRequested = true;
717
+ try {
718
+ const q = pk ? `?publishableKey=${encodeURIComponent(pk)}` : sessionId ? `?sessionId=${encodeURIComponent(sessionId)}` : "";
719
+ const s = document.createElement("script");
720
+ s.src = `${apiBase}/public/loader.js${q}`;
721
+ s.async = true;
722
+ s.onerror = () => {
723
+ serverScriptRequested = false;
724
+ emitDiagnostic(
725
+ "LOADER_SCRIPT_BLOCKED",
726
+ "warn",
727
+ `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.`,
728
+ { src: s.src }
729
+ );
730
+ };
731
+ (document.head || document.documentElement).appendChild(s);
732
+ } catch {
733
+ }
734
+ }
735
+ var ServerLoader = class {
736
+ constructor() {
737
+ this.overlay = null;
738
+ }
739
+ show(primaryColor, lang) {
740
+ this.overlay = window.FlonkWidgetLoader.show({ primaryColor, lang });
741
+ return this.overlay;
742
+ }
743
+ get element() {
744
+ return this.overlay;
745
+ }
746
+ updateColor(primaryColor) {
747
+ this.overlay?.updateColor?.(primaryColor);
748
+ }
749
+ showError(message, lang) {
750
+ this.overlay?.showError?.(message, lang);
751
+ }
752
+ fadeOut() {
753
+ this.overlay?.fadeOut?.();
754
+ }
755
+ destroy() {
756
+ try {
757
+ this.overlay?.remove();
758
+ } catch {
759
+ }
760
+ this.overlay = null;
761
+ }
762
+ };
763
+ function makeLoader() {
764
+ if (isServerLoaderReady()) return new ServerLoader();
765
+ emitDiagnostic(
766
+ "LOADER_FALLBACK_BUNDLED",
767
+ "info",
768
+ "Server loader not ready at show time \u2014 using the bundled loader."
769
+ );
770
+ return new Loader();
771
+ }
772
+
773
+ // src/browser/message-handler.ts
774
+ function checkProtocol(data) {
775
+ const remote = data.protocolVersion;
776
+ if (typeof remote === "number" && remote !== PROTOCOL_VERSION) {
777
+ emitDiagnostic(
778
+ "PROTOCOL_VERSION_MISMATCH",
779
+ "warn",
780
+ `SDK speaks protocol ${PROTOCOL_VERSION}, iframe speaks ${remote}. Additive-compatible, but check for a stale-cached widget if behavior is off.`,
781
+ { sdk: PROTOCOL_VERSION, iframe: remote }
782
+ );
783
+ }
784
+ }
785
+ var MessageHandler = class {
786
+ constructor(iframeSrc, iframe, callbacks) {
787
+ this.iframeSrc = iframeSrc;
788
+ this.iframe = iframe;
789
+ this.callbacks = callbacks;
790
+ this.listener = null;
791
+ this.readyListener = null;
792
+ this.completionHandled = false;
793
+ }
794
+ /**
795
+ * Start listening for postMessage events from the widget iframe.
796
+ */
797
+ listen() {
798
+ const origin = getOrigin(this.iframeSrc);
799
+ this.listener = (e) => {
800
+ if (e.origin !== origin) return;
801
+ if (e.source !== this.iframe.contentWindow) return;
802
+ const data = e.data || {};
803
+ const type = data.type;
804
+ if (type === WIDGET_EVENTS.COMPLETE) {
805
+ if (this.completionHandled) return;
806
+ this.completionHandled = true;
807
+ this.callbacks.onSuccess?.(data.result);
808
+ } else if (type === WIDGET_EVENTS.CANCEL) {
809
+ if (this.completionHandled) return;
810
+ this.completionHandled = true;
811
+ this.callbacks.onCancel?.();
812
+ } else if (type === WIDGET_EVENTS.ERROR) {
813
+ if (this.completionHandled) return;
814
+ this.completionHandled = true;
815
+ this.callbacks.onError?.(data.error || "Unknown error");
816
+ } else if (type === WIDGET_EVENTS.READY) {
817
+ checkProtocol(data);
818
+ this.callbacks.onReady?.();
819
+ }
820
+ };
821
+ window.addEventListener("message", this.listener);
822
+ }
823
+ /**
824
+ * Listen for the first READY event, then call the callback once.
825
+ */
826
+ onReadyOnce(callback) {
827
+ const origin = getOrigin(this.iframeSrc);
828
+ this.readyListener = (e) => {
829
+ if (e.origin !== origin || e.source !== this.iframe.contentWindow || e.data?.type !== WIDGET_EVENTS.READY) {
830
+ return;
831
+ }
832
+ window.removeEventListener("message", this.readyListener);
833
+ this.readyListener = null;
834
+ callback();
835
+ };
836
+ window.addEventListener("message", this.readyListener);
837
+ }
838
+ destroy() {
839
+ if (this.listener) {
840
+ window.removeEventListener("message", this.listener);
841
+ this.listener = null;
842
+ }
843
+ if (this.readyListener) {
844
+ window.removeEventListener("message", this.readyListener);
845
+ this.readyListener = null;
846
+ }
847
+ }
848
+ };
849
+
850
+ // src/browser/viewport.ts
851
+ function getVV() {
852
+ if (window.visualViewport) {
853
+ return {
854
+ width: window.visualViewport.width,
855
+ height: window.visualViewport.height,
856
+ offsetTop: window.visualViewport.offsetTop || 0,
857
+ offsetLeft: window.visualViewport.offsetLeft || 0
858
+ };
859
+ }
860
+ return { width: window.innerWidth, height: window.innerHeight, offsetTop: 0, offsetLeft: 0 };
861
+ }
862
+ function ensureViewportMeta() {
863
+ try {
864
+ const meta = document.querySelector('meta[name="viewport"]');
865
+ if (!meta) {
866
+ const m = document.createElement("meta");
867
+ m.setAttribute("name", "viewport");
868
+ m.setAttribute("content", "width=device-width, initial-scale=1.0, viewport-fit=cover");
869
+ document.head.appendChild(m);
870
+ } else {
871
+ const c = meta.getAttribute("content") || "";
872
+ if (!c.includes("viewport-fit=cover")) {
873
+ meta.setAttribute("content", c + ", viewport-fit=cover");
874
+ }
875
+ }
876
+ } catch {
877
+ }
878
+ }
879
+ function setupViewportSizing(overlay, iframe) {
880
+ ensureViewportMeta();
881
+ let baselineHeight = 0;
882
+ let desktop = isDesktop();
883
+ let rafId = null;
884
+ const initBaseline = () => {
885
+ const vv = getVV();
886
+ baselineHeight = window.innerHeight || vv.height || document.documentElement.clientHeight || 0;
887
+ desktop = isDesktop();
888
+ if (rafId) cancelAnimationFrame(rafId);
889
+ rafId = requestAnimationFrame(applySize);
890
+ };
891
+ const applySize = () => {
892
+ try {
893
+ const vv = getVV();
894
+ const inner = window.innerHeight || vv.height;
895
+ const kbThreshold = desktop ? 200 : 150;
896
+ const isKeyboard = inner - vv.height > kbThreshold;
897
+ const height = isKeyboard ? vv.height : baselineHeight || inner;
898
+ const dims = desktop ? { top: "0", left: "0", width: window.innerWidth + "px", height: window.innerHeight + "px" } : { top: vv.offsetTop + "px", left: vv.offsetLeft + "px", width: vv.width + "px", height: height + "px" };
899
+ if (overlay) setStyles(overlay, dims);
900
+ if (iframe) setStyles(iframe, dims);
901
+ } catch {
902
+ }
903
+ };
904
+ let prevW = window.innerWidth;
905
+ let prevH = window.innerHeight;
906
+ let prevX = 0;
907
+ let prevY = 0;
908
+ const debouncedApply = debounce(() => {
909
+ if (rafId) cancelAnimationFrame(rafId);
910
+ rafId = requestAnimationFrame(applySize);
911
+ }, desktop ? 50 : 150);
912
+ const handleResize = () => {
913
+ const vv = getVV();
914
+ const w = vv.width;
915
+ const h = vv.height;
916
+ const x = vv.offsetLeft;
917
+ const y = vv.offsetTop;
918
+ const changed = Math.abs(w - prevW) > 1 || Math.abs(h - prevH) > 1 || Math.abs(x - prevX) > 1 || Math.abs(y - prevY) > 1;
919
+ if (!changed) return;
920
+ if (Math.abs(h - prevH) > 1) initBaseline();
921
+ prevW = w;
922
+ prevH = h;
923
+ prevX = x;
924
+ prevY = y;
925
+ if (desktop) {
926
+ if (rafId) cancelAnimationFrame(rafId);
927
+ rafId = requestAnimationFrame(applySize);
928
+ } else {
929
+ debouncedApply();
930
+ }
931
+ };
932
+ const handleOrientation = () => {
933
+ initBaseline();
934
+ if (rafId) cancelAnimationFrame(rafId);
935
+ rafId = requestAnimationFrame(applySize);
936
+ };
937
+ initBaseline();
938
+ applySize();
939
+ window.addEventListener("resize", handleResize);
940
+ window.addEventListener("orientationchange", handleOrientation);
941
+ if (window.visualViewport) {
942
+ window.visualViewport.addEventListener("resize", handleResize);
943
+ window.visualViewport.addEventListener("scroll", handleResize);
944
+ }
945
+ return () => {
946
+ try {
947
+ if (rafId) cancelAnimationFrame(rafId);
948
+ window.removeEventListener("resize", handleResize);
949
+ window.removeEventListener("orientationchange", handleOrientation);
950
+ if (window.visualViewport) {
951
+ window.visualViewport.removeEventListener("resize", handleResize);
952
+ window.visualViewport.removeEventListener("scroll", handleResize);
953
+ }
954
+ } catch {
955
+ }
956
+ };
957
+ }
958
+
959
+ // src/browser/index.ts
960
+ var FALLBACK_PRIMARY = "#15BA68";
961
+ var EARLY_COLOR_BUDGET_MS = 800;
962
+ var primaryFrom = (tokens) => tokens?.colors?.primary?.cannabis || FALLBACK_PRIMARY;
963
+ async function showLoaderWithEarlyColor(tokensPromise, lang) {
964
+ const earlyTokens = await Promise.race([
965
+ tokensPromise,
966
+ new Promise((resolve) => setTimeout(() => resolve(null), EARLY_COLOR_BUDGET_MS))
967
+ ]);
968
+ const primaryColor = primaryFrom(earlyTokens);
969
+ const loader = makeLoader();
970
+ loader.show(primaryColor, lang);
971
+ return { loader, primaryColor };
972
+ }
973
+ var FlonkKYC = class {
974
+ constructor(options = {}) {
975
+ this.disposeDiagnostics = null;
976
+ this.widgetUrl = (options.widgetUrl || DEFAULT_WIDGET_URL).replace(/\/$/, "");
977
+ this.apiBase = (options.apiBase || DEFAULT_API_BASE).replace(/\/$/, "");
978
+ if (options.onDiagnostic) this.disposeDiagnostics = addDiagnosticHandler(options.onDiagnostic);
979
+ if (typeof document !== "undefined") {
980
+ try {
981
+ preconnect(this.widgetUrl);
982
+ } catch {
983
+ }
984
+ try {
985
+ loadServerLoaderScript(this.apiBase);
986
+ } catch {
987
+ }
988
+ }
989
+ }
990
+ /** Unregister this instance's `onDiagnostic` handler. */
991
+ dispose() {
992
+ this.disposeDiagnostics?.();
993
+ this.disposeDiagnostics = null;
994
+ }
995
+ /**
996
+ * Prewarm the widget ahead of the user's click — preconnect + idle prefetch
997
+ * of branding/assets, and (with `level:'eager'`) a hidden background iframe so
998
+ * the full bundle is loaded before the click. Call on page mount / route
999
+ * enter. Returns a cleanup (removes `intent` listeners). Never pre-creates a
1000
+ * session. SSR-safe.
1001
+ *
1002
+ * @example
1003
+ * FlonkKYC.prewarm({ publishableKey: 'pk_live_…', level: 'eager' });
1004
+ * // or, warm only when the user shows intent:
1005
+ * FlonkKYC.prewarm({ publishableKey: 'pk_live_…', level: 'intent', trigger: btn });
1006
+ */
1007
+ static prewarm(opts = {}) {
1008
+ if (typeof document === "undefined") return () => {
1009
+ };
1010
+ return prewarm({
1011
+ widgetUrl: (opts.widgetUrl || DEFAULT_WIDGET_URL).replace(/\/$/, ""),
1012
+ apiBase: (opts.apiBase || DEFAULT_API_BASE).replace(/\/$/, ""),
1013
+ publishableKey: opts.publishableKey,
1014
+ sessionId: opts.sessionId,
1015
+ level: opts.level,
1016
+ trigger: opts.trigger ?? null
1017
+ });
1018
+ }
1019
+ /**
1020
+ * Warm the project's branding (colors) ahead of time so the widget paints the
1021
+ * brand color on the first frame, with no branding round-trip at click time.
1022
+ *
1023
+ * Call it early — on page mount, route enter, or hover of the "verify" button
1024
+ * — well before `init()`. The result is cached (module-level, 5-min TTL) and
1025
+ * every subsequent `init()`/`open()` for the same key reads from it. Safe to
1026
+ * call repeatedly; concurrent calls dedupe. Never throws.
1027
+ *
1028
+ * @example
1029
+ * // in a layout effect, long before the user clicks "Verify"
1030
+ * FlonkKYC.preloadBranding({ publishableKey: 'pk_live_...' });
1031
+ */
1032
+ static preloadBranding(opts) {
1033
+ const apiBase = (opts.apiBase || DEFAULT_API_BASE).replace(/\/$/, "");
1034
+ return preloadDesignTokens(apiBase, {
1035
+ pk: opts.publishableKey,
1036
+ sessionId: opts.sessionId
1037
+ }).then(() => void 0);
1038
+ }
1039
+ // ── Public API ───────────────────────────────────────
1040
+ /**
1041
+ * Open the KYC verification widget.
1042
+ *
1043
+ * Flows (pick one; add `publishableKey` to any for an instant branded loader):
1044
+ * 1. `{ serverUrl }` — SDK auto-creates the session via your backend (recommended).
1045
+ * 2. `{ sessionId, embedToken }` — you created the session; pass its credentials.
1046
+ * 3. `{ sessionId }` — **deprecated**: exchanges the sessionId for an embedToken
1047
+ * via an extra round-trip. Prefer flow 2 by returning `embedToken` from your
1048
+ * backend alongside `sessionId`.
1049
+ * 4. `{ publishableKey }` — client-only; SDK mints a short-lived widget token.
1050
+ */
1051
+ async init(config) {
1052
+ if (!config) throw new FlonkValidationError("config is required");
1053
+ if (config.serverUrl) {
1054
+ return this.initWithServerUrl(config);
1055
+ }
1056
+ if (config.sessionId && config.embedToken) {
1057
+ return this.initWithEmbedToken(config);
1058
+ }
1059
+ if (config.sessionId) {
1060
+ return this.initWithSession(config);
1061
+ }
1062
+ const pk = config.publishableKey;
1063
+ if (!pk || !/^pk_/.test(pk)) {
1064
+ throw new FlonkValidationError(
1065
+ "Provide one of: serverUrl, sessionId + embedToken, or publishableKey (pk_*)"
1066
+ );
1067
+ }
1068
+ return this.initWithPublishableKey(config);
1069
+ }
1070
+ /**
1071
+ * Preview mode — no API calls, mock data.
1072
+ */
1073
+ preview(config = {}) {
1074
+ const colors = config.colors || { primaryColor: "#3b82f6", secondaryColor: "#93c5fd" };
1075
+ return this.openWidget({
1076
+ mode: "preview",
1077
+ isPreview: "true",
1078
+ previewColors: JSON.stringify(colors),
1079
+ allowManualUpload: "true",
1080
+ documentType: config.documentType || "id_card",
1081
+ lang: config.lang || "de",
1082
+ overlayColor: config.overlayColor
1083
+ }, {
1084
+ primaryColor: colors.primaryColor || "#3b82f6",
1085
+ lang: config.lang,
1086
+ onSuccess: config.onSuccess,
1087
+ onError: config.onError,
1088
+ onCancel: config.onCancel
1089
+ });
1090
+ }
1091
+ /**
1092
+ * Embed inline preview in a container (for dashboards).
1093
+ */
1094
+ embed(config) {
1095
+ if (!config?.container) throw new FlonkValidationError("container is required");
1096
+ const container = typeof config.container === "string" ? document.querySelector(config.container) : config.container;
1097
+ if (!container) throw new FlonkValidationError("Container element not found");
1098
+ let colors = config.colors || { primaryColor: "#3b82f6", secondaryColor: "#93c5fd" };
1099
+ let device = config.device || "mobile";
1100
+ let scale = config.scale ?? 1;
1101
+ const lang = config.lang || "de";
1102
+ const buildSrc = (c, d) => {
1103
+ const p = new URLSearchParams({
1104
+ mode: "preview",
1105
+ isPreview: "true",
1106
+ previewColors: JSON.stringify(c),
1107
+ allowManualUpload: "true",
1108
+ documentType: config.documentType || "id_card",
1109
+ device: d,
1110
+ lang
1111
+ });
1112
+ return `${this.widgetUrl}/?${p.toString()}`;
1113
+ };
1114
+ const applyScale = () => {
1115
+ iframe.style.transform = scale !== 1 ? `scale(${scale})` : "";
1116
+ iframe.style.transformOrigin = scale !== 1 ? "top left" : "";
1117
+ };
1118
+ const iframe = document.createElement("iframe");
1119
+ iframe.style.cssText = "border:none;width:100%;height:100%;display:block";
1120
+ iframe.src = buildSrc(colors, device);
1121
+ iframe.title = "KYC Widget Preview";
1122
+ iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
1123
+ applyScale();
1124
+ container.innerHTML = "";
1125
+ container.appendChild(iframe);
1126
+ return {
1127
+ iframe,
1128
+ setColors(newColors) {
1129
+ colors = { ...colors, ...newColors };
1130
+ if (newColors.primaryColor && !newColors.secondaryColor) {
1131
+ colors.secondaryColor = generateSecondaryColor(newColors.primaryColor);
1132
+ }
1133
+ iframe.src = buildSrc(colors, device);
1134
+ },
1135
+ setDevice(d) {
1136
+ device = d;
1137
+ iframe.src = buildSrc(colors, device);
1138
+ },
1139
+ getColors: () => ({ ...colors }),
1140
+ destroy: () => iframe.remove()
1141
+ };
1142
+ }
1143
+ // ── Private flows ────────────────────────────────────
1144
+ /**
1145
+ * Flow 1: serverUrl — POST to client's backend, get sessionId + embedToken.
1146
+ *
1147
+ * When `publishableKey` is provided, design tokens are fetched in parallel
1148
+ * with the session creation request. This shows the branded loader ~200-500ms
1149
+ * faster because we don't have to wait for the session to be created first.
1150
+ */
1151
+ async initWithServerUrl(config) {
1152
+ const pk = config.publishableKey;
1153
+ const designTokensPromise = pk ? fetchDesignTokens(this.apiBase, { pk }) : Promise.resolve(null);
1154
+ const sessionPromise = fetchSessionFromServer(
1155
+ config.serverUrl,
1156
+ config.clientMetadata,
1157
+ config.requestHeaders
1158
+ );
1159
+ const { loader, primaryColor } = await showLoaderWithEarlyColor(designTokensPromise, config.lang);
1160
+ try {
1161
+ const [{ sessionId, embedToken, qrCodeUrl }, designTokens] = await Promise.all([
1162
+ sessionPromise,
1163
+ designTokensPromise
1164
+ ]);
1165
+ const finalTokens = designTokens ?? await fetchDesignTokens(this.apiBase, { sessionId });
1166
+ const finalColor = primaryFrom(finalTokens);
1167
+ if (finalColor !== primaryColor) loader.updateColor(finalColor);
1168
+ return this.buildWidget(embedToken, sessionId, config, loader, finalTokens, qrCodeUrl);
1169
+ } catch (err) {
1170
+ const msg = err.message || "Failed to create session";
1171
+ loader.showError(msg, config.lang);
1172
+ config.onError?.(msg);
1173
+ throw err;
1174
+ }
1175
+ }
1176
+ /**
1177
+ * Flow 2: sessionId + embedToken — fetch session data, open widget.
1178
+ */
1179
+ async initWithEmbedToken(config) {
1180
+ const pk = config.publishableKey;
1181
+ const designTokensPromise = pk ? fetchDesignTokens(this.apiBase, { pk }) : fetchDesignTokens(this.apiBase, { sessionId: config.sessionId });
1182
+ const { loader, primaryColor } = await showLoaderWithEarlyColor(designTokensPromise, config.lang);
1183
+ try {
1184
+ const designTokens = await designTokensPromise;
1185
+ const finalColor = primaryFrom(designTokens);
1186
+ if (finalColor !== primaryColor) loader.updateColor(finalColor);
1187
+ return this.buildWidget(config.embedToken, config.sessionId, config, loader, designTokens);
1188
+ } catch (err) {
1189
+ const msg = err.message || "Failed to initialize verification";
1190
+ loader.showError(msg, config.lang);
1191
+ config.onError?.(msg);
1192
+ throw err;
1193
+ }
1194
+ }
1195
+ /**
1196
+ * Flow 3: sessionId only — exchange for embedToken, then init.
1197
+ *
1198
+ * @deprecated Prefer flow 2 (`sessionId` + `embedToken`). Return the
1199
+ * `embedToken` from your backend together with the `sessionId` to skip this
1200
+ * extra token-exchange round-trip.
1201
+ */
1202
+ async initWithSession(config) {
1203
+ const designTokensPromise = fetchDesignTokens(this.apiBase, {
1204
+ sessionId: config.sessionId
1205
+ });
1206
+ const exchangePromise = exchangeSessionForToken(this.apiBase, config.sessionId);
1207
+ const { loader } = await showLoaderWithEarlyColor(designTokensPromise, config.lang);
1208
+ try {
1209
+ const [{ embedToken, session }, designTokens] = await Promise.all([
1210
+ exchangePromise,
1211
+ designTokensPromise
1212
+ ]);
1213
+ return this.buildWidget(embedToken, config.sessionId || session.id, config, loader, designTokens);
1214
+ } catch (err) {
1215
+ const msg = err.message || "Failed to initialize verification";
1216
+ loader.showError(msg, config.lang);
1217
+ config.onError?.(msg);
1218
+ throw err;
1219
+ }
1220
+ }
1221
+ /**
1222
+ * Flow 4: publishableKey — fetch widget token, open widget.
1223
+ */
1224
+ async initWithPublishableKey(config) {
1225
+ const pk = config.publishableKey;
1226
+ const designTokensPromise = fetchDesignTokens(this.apiBase, { pk });
1227
+ const widgetTokenPromise = fetchWidgetToken(pk, this.apiBase);
1228
+ const { loader, primaryColor } = await showLoaderWithEarlyColor(designTokensPromise, config.lang);
1229
+ try {
1230
+ const data = await widgetTokenPromise;
1231
+ const params = {
1232
+ mode: "embedded",
1233
+ publishableKey: pk,
1234
+ allowManualUpload: String(data.allowManualUpload !== false)
1235
+ };
1236
+ if (data.token) params.token = data.token;
1237
+ if (config.clientMetadata) {
1238
+ params.clientMetadata = JSON.stringify(config.clientMetadata);
1239
+ }
1240
+ if (config.lang) params.lang = config.lang;
1241
+ if (config.overlayColor) params.overlayColor = config.overlayColor;
1242
+ return this.openWidget(params, {
1243
+ primaryColor,
1244
+ lang: config.lang,
1245
+ loader,
1246
+ onSuccess: config.onSuccess,
1247
+ onError: config.onError,
1248
+ onCancel: config.onCancel,
1249
+ onReady: config.onReady,
1250
+ mount: config.mount
1251
+ });
1252
+ } catch (err) {
1253
+ const msg = err.message || "Failed to initialize verification";
1254
+ loader.showError(msg, config.lang);
1255
+ config.onError?.(msg);
1256
+ throw err;
1257
+ }
1258
+ }
1259
+ // ── Core widget builder ──────────────────────────────
1260
+ buildWidget(token, sessionId, config, loader, designTokens, qrCodeUrl) {
1261
+ const params = {
1262
+ mode: "embedded",
1263
+ sessionId,
1264
+ token
1265
+ };
1266
+ const qr = qrCodeUrl ?? config.qrCodeUrl;
1267
+ if (qr) params.qrCodeUrl = encodeURIComponent(qr);
1268
+ if (config.allowManualUpload !== void 0) {
1269
+ params.allowManualUpload = String(config.allowManualUpload !== false);
1270
+ }
1271
+ if (designTokens?.colors) {
1272
+ params.designTokens = JSON.stringify(designTokens);
1273
+ }
1274
+ if (config.embedToken) params.embedToken = config.embedToken;
1275
+ if (config.clientMetadata) {
1276
+ params.clientMetadata = JSON.stringify(config.clientMetadata);
1277
+ }
1278
+ if (config.lang) params.lang = config.lang;
1279
+ if (config.overlayColor) params.overlayColor = config.overlayColor;
1280
+ return this.openWidget(params, {
1281
+ primaryColor: primaryFrom(designTokens),
1282
+ lang: config.lang,
1283
+ loader,
1284
+ onSuccess: config.onSuccess,
1285
+ onError: config.onError,
1286
+ onCancel: config.onCancel,
1287
+ onReady: config.onReady,
1288
+ mount: config.mount
1289
+ });
1290
+ }
1291
+ /**
1292
+ * Core: create iframe, attach listeners, return WidgetInstance.
1293
+ */
1294
+ openWidget(params, opts) {
1295
+ const filtered = Object.fromEntries(
1296
+ Object.entries(params).filter(([, v]) => v != null)
1297
+ );
1298
+ filtered[WIDGET_PARAMS.PROTOCOL_VERSION] = String(PROTOCOL_VERSION);
1299
+ const search = new URLSearchParams(filtered);
1300
+ const src = `${this.widgetUrl}/?${search.toString()}`;
1301
+ const iframe = createIframe(src);
1302
+ emitDiagnostic("IFRAME_FRESH", "info", "Built a fresh widget iframe");
1303
+ const mountTarget = opts.mount || document.body;
1304
+ const loader = opts.loader ?? (() => {
1305
+ const l = makeLoader();
1306
+ l.show(opts.primaryColor, opts.lang);
1307
+ return l;
1308
+ })();
1309
+ if (loader.element) adjustZIndex(loader.element, iframe);
1310
+ mountTarget.appendChild(iframe);
1311
+ const cleanupViewport = setupViewportSizing(loader.element, iframe);
1312
+ let cleaned = false;
1313
+ const cleanupAll = () => {
1314
+ if (cleaned) return;
1315
+ cleaned = true;
1316
+ try {
1317
+ handler.destroy();
1318
+ cleanupViewport();
1319
+ loader.destroy();
1320
+ if (iframe.parentNode) iframe.remove();
1321
+ } catch {
1322
+ }
1323
+ };
1324
+ const afterCleanup = (delayMs) => setTimeout(cleanupAll, delayMs);
1325
+ const handler = new MessageHandler(src, iframe, {
1326
+ onSuccess: (r) => {
1327
+ opts.onSuccess?.(r);
1328
+ afterCleanup(1e3);
1329
+ },
1330
+ onError: (e) => {
1331
+ opts.onError?.(e);
1332
+ afterCleanup(500);
1333
+ },
1334
+ onCancel: () => {
1335
+ opts.onCancel?.();
1336
+ afterCleanup(500);
1337
+ },
1338
+ onReady: opts.onReady
1339
+ });
1340
+ handler.listen();
1341
+ let revealed = false;
1342
+ const revealOnce = () => {
1343
+ if (revealed) return;
1344
+ revealed = true;
1345
+ clearTimeout(revealTimer);
1346
+ if (loader.element) transitionLoaderToIframe(loader.element, iframe, () => loader.destroy());
1347
+ };
1348
+ const revealTimer = setTimeout(() => {
1349
+ emitDiagnostic("READY_TIMEOUT_REVEAL", "warn", "Widget did not signal READY in time; revealing anyway");
1350
+ revealOnce();
1351
+ }, 4e3);
1352
+ handler.onReadyOnce(revealOnce);
1353
+ return {
1354
+ iframe,
1355
+ destroy: cleanupAll
1356
+ };
1357
+ }
1358
+ };
1359
+ FlonkKYC.version = SDK_VERSION;
1360
+
1361
+ exports.API_VERSION = API_VERSION;
1362
+ exports.FlonkError = FlonkError;
1363
+ exports.FlonkKYC = FlonkKYC;
1364
+ exports.FlonkValidationError = FlonkValidationError;
1365
+ exports.PROTOCOL_VERSION = PROTOCOL_VERSION;
1366
+ exports.SDK_VERSION = SDK_VERSION;
1367
+ exports.WIDGET_EVENTS = WIDGET_EVENTS;
1368
+ exports.WIDGET_PARAMS = WIDGET_PARAMS;
1369
+ exports.addDiagnosticHandler = addDiagnosticHandler;
1370
+ //# sourceMappingURL=core.cjs.map
1371
+ //# sourceMappingURL=core.cjs.map