@flonkid/kyc 1.4.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 ADDED
@@ -0,0 +1,993 @@
1
+ import { useRef, useMemo, useEffect } from 'react';
2
+ import { jsx } from 'react/jsx-runtime';
3
+
4
+ // src/shared/constants.ts
5
+ var SDK_VERSION = "1.4.0";
6
+ var DEFAULT_WIDGET_URL = "https://widget.flonk.id";
7
+ var DEFAULT_API_BASE = "https://api.flonk.id/v1";
8
+ var WIDGET_EVENTS = {
9
+ READY: "KYC_WIDGET_READY",
10
+ COMPLETE: "KYC_COMPLETE",
11
+ CANCEL: "KYC_CANCEL",
12
+ ERROR: "KYC_ERROR"
13
+ };
14
+
15
+ // src/shared/errors.ts
16
+ var FlonkError = class extends Error {
17
+ constructor(message, code, statusCode) {
18
+ super(message);
19
+ this.code = code;
20
+ this.statusCode = statusCode;
21
+ this.name = "FlonkError";
22
+ }
23
+ };
24
+ var FlonkValidationError = class extends FlonkError {
25
+ constructor(message) {
26
+ super(message, "validation_error", 400);
27
+ this.name = "FlonkValidationError";
28
+ }
29
+ };
30
+
31
+ // src/browser/utils.ts
32
+ function getOrigin(url) {
33
+ try {
34
+ return new URL(url).origin;
35
+ } catch {
36
+ return window.location.origin;
37
+ }
38
+ }
39
+ function isDesktop() {
40
+ return window.innerWidth >= 1024 && !("ontouchstart" in window || navigator.maxTouchPoints > 0);
41
+ }
42
+ function setStyles(el, styles) {
43
+ Object.assign(el.style, styles);
44
+ }
45
+ function createEl(tag, styles, attrs) {
46
+ const el = document.createElement(tag);
47
+ if (styles) setStyles(el, styles);
48
+ if (attrs) {
49
+ for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
50
+ }
51
+ return el;
52
+ }
53
+ function debounce(fn, ms) {
54
+ let timer;
55
+ return ((...args) => {
56
+ clearTimeout(timer);
57
+ timer = setTimeout(() => fn(...args), ms);
58
+ });
59
+ }
60
+ function generateSecondaryColor(hex) {
61
+ try {
62
+ const h = hex.replace("#", "");
63
+ const r = parseInt(h.substring(0, 2), 16);
64
+ const g = parseInt(h.substring(2, 4), 16);
65
+ const b = parseInt(h.substring(4, 6), 16);
66
+ const f = 0.6;
67
+ const toHex = (n) => {
68
+ const s = n.toString(16);
69
+ return s.length === 1 ? "0" + s : s;
70
+ };
71
+ return "#" + toHex(Math.round(r + (255 - r) * f)) + toHex(Math.round(g + (255 - g) * f)) + toHex(Math.round(b + (255 - b) * f));
72
+ } catch {
73
+ return "#93c5fd";
74
+ }
75
+ }
76
+ async function fetchWidgetToken(pk, apiBase) {
77
+ const res = await fetch(`${apiBase}/public/widget-token`, {
78
+ headers: { "x-kyc-pk": pk },
79
+ credentials: "include"
80
+ });
81
+ if (!res.ok) throw new Error(`Widget token request failed: ${res.status}`);
82
+ return res.json();
83
+ }
84
+ async function fetchDesignTokens(apiBase, opts) {
85
+ const params = [];
86
+ if (opts.sessionId) params.push(`sessionId=${encodeURIComponent(opts.sessionId)}`);
87
+ else if (opts.clientId) params.push(`clientId=${encodeURIComponent(opts.clientId)}`);
88
+ const url = `${apiBase}/public/design-tokens${params.length ? "?" + params.join("&") : ""}`;
89
+ try {
90
+ const res = await fetch(url, {
91
+ headers: opts.pk ? { "x-kyc-pk": opts.pk } : {},
92
+ credentials: "omit"
93
+ });
94
+ return res.ok ? res.json() : null;
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+ function validateServerUrl(url) {
100
+ if (url.startsWith("/")) return;
101
+ try {
102
+ const parsed = new URL(url);
103
+ const isLocalhost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
104
+ if (parsed.protocol !== "https:" && !isLocalhost) {
105
+ throw new Error(
106
+ `serverUrl must use HTTPS in production. Got: ${parsed.protocol}//. Use HTTPS ('https://api.myapp.com/...') or a relative path ('/api/...')`
107
+ );
108
+ }
109
+ } catch (e) {
110
+ if (e.message.includes("serverUrl must use HTTPS")) throw e;
111
+ throw new Error(`Invalid serverUrl: ${url}`);
112
+ }
113
+ }
114
+ async function fetchSessionFromServer(serverUrl, clientMetadata) {
115
+ validateServerUrl(serverUrl);
116
+ const res = await fetch(serverUrl, {
117
+ method: "POST",
118
+ headers: { "Content-Type": "application/json" },
119
+ body: JSON.stringify({ clientMetadata })
120
+ });
121
+ if (!res.ok) throw new Error(`Server session request failed: ${res.status}`);
122
+ return res.json();
123
+ }
124
+ async function fetchPublicSession(apiBase, sessionId, embedToken) {
125
+ const res = await fetch(`${apiBase}/public/session/${sessionId}`, {
126
+ headers: {
127
+ "Content-Type": "application/json",
128
+ "Authorization": `Bearer ${embedToken}`
129
+ }
130
+ });
131
+ if (!res.ok) throw new Error(`Failed to fetch session: ${res.status}`);
132
+ return res.json();
133
+ }
134
+ async function exchangeSessionForToken(apiBase, sessionId) {
135
+ const res = await fetch(`${apiBase}/public/session/${sessionId}/token`, {
136
+ method: "POST",
137
+ headers: { "Content-Type": "application/json" }
138
+ });
139
+ if (!res.ok) throw new Error(`Token exchange failed: ${res.status}`);
140
+ return res.json();
141
+ }
142
+
143
+ // src/browser/iframe-manager.ts
144
+ function createIframe(src) {
145
+ const d = isDesktop();
146
+ const iframe = createEl(
147
+ "iframe",
148
+ {
149
+ border: "0",
150
+ width: window.innerWidth + "px",
151
+ height: window.innerHeight + "px",
152
+ position: "fixed",
153
+ top: "0",
154
+ left: "0",
155
+ zIndex: "9999",
156
+ background: "transparent",
157
+ backgroundColor: "transparent",
158
+ opacity: "0",
159
+ visibility: "hidden",
160
+ borderRadius: d ? "0" : "",
161
+ boxShadow: d ? "none" : "",
162
+ colorScheme: "normal"
163
+ },
164
+ {
165
+ src,
166
+ allow: "camera;microphone;clipboard-read;clipboard-write",
167
+ sandbox: "allow-scripts allow-forms allow-same-origin allow-popups",
168
+ "aria-label": "KYC Verification",
169
+ allowtransparency: "true"
170
+ }
171
+ );
172
+ try {
173
+ iframe.style.setProperty("background", "transparent", "important");
174
+ iframe.style.setProperty("background-color", "transparent", "important");
175
+ iframe.style.setProperty("color-scheme", "normal", "important");
176
+ } catch {
177
+ }
178
+ return iframe;
179
+ }
180
+ function adjustZIndex(loader, iframe) {
181
+ if (!isDesktop()) return;
182
+ const all = Array.from(document.querySelectorAll("*"));
183
+ const maxZ = Math.max(
184
+ ...all.map((el) => parseInt(getComputedStyle(el).zIndex) || 0).filter((z) => z < 999999)
185
+ );
186
+ if (maxZ > 9998) {
187
+ loader.style.zIndex = String(maxZ + 1);
188
+ iframe.style.zIndex = String(maxZ + 2);
189
+ }
190
+ }
191
+ function transitionLoaderToIframe(loader, iframe, onDone) {
192
+ const d = isDesktop();
193
+ const dur = d ? 300 : 500;
194
+ setStyles(iframe, { opacity: "0", visibility: "hidden" });
195
+ const card = loader.querySelector("div");
196
+ if (card) {
197
+ setStyles(card, {
198
+ transform: d ? "translateY(-10px) scale(0.98)" : "translateY(-15px) scale(0.96)",
199
+ opacity: "0"
200
+ });
201
+ }
202
+ loader.style.opacity = "0";
203
+ setTimeout(() => {
204
+ onDone();
205
+ setStyles(iframe, {
206
+ transition: d ? "opacity 300ms cubic-bezier(0.4,0,0.2,1),visibility 0ms" : "opacity 400ms ease-out,visibility 0ms",
207
+ opacity: "1",
208
+ visibility: "visible"
209
+ });
210
+ }, dur);
211
+ }
212
+
213
+ // src/browser/loader.ts
214
+ var LOADER_I18N = {
215
+ en: { title: "Initializing...", subtitle: "Loading KYC widget", errorTitle: "Something went wrong", close: "Close" },
216
+ de: { title: "Initialisierung...", subtitle: "KYC-Widget wird geladen", errorTitle: "Ein Fehler ist aufgetreten", close: "Schlie\xDFen" },
217
+ 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" }
218
+ };
219
+ var KEYFRAMES = `
220
+ @keyframes kycspin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}
221
+ @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}}
222
+ @keyframes kycprogress{0%{transform:translateX(-100%)}50%{transform:translateX(0%)}100%{transform:translateX(250%)}}
223
+ `.trim();
224
+ var styleInjected = false;
225
+ var Loader = class {
226
+ constructor() {
227
+ this.overlay = null;
228
+ this.cleanup = null;
229
+ this.origBodyStyles = null;
230
+ }
231
+ show(primaryColor, lang) {
232
+ const color = primaryColor || "#15BA68";
233
+ const strings = LOADER_I18N[lang || ""] || LOADER_I18N.en;
234
+ const d = isDesktop();
235
+ if (!styleInjected) {
236
+ const style = document.createElement("style");
237
+ style.textContent = KEYFRAMES;
238
+ document.head.appendChild(style);
239
+ styleInjected = true;
240
+ }
241
+ const overlay = createEl("div", {
242
+ position: "fixed",
243
+ top: "0",
244
+ left: "0",
245
+ right: "0",
246
+ bottom: "0",
247
+ width: window.innerWidth + "px",
248
+ height: window.innerHeight + "px",
249
+ background: d ? "rgba(0,0,0,0.05)" : "transparent",
250
+ backdropFilter: d ? "blur(2px)" : "none",
251
+ display: "flex",
252
+ alignItems: "center",
253
+ justifyContent: "center",
254
+ zIndex: "9998",
255
+ transition: "opacity 600ms ease-out,transform 400ms ease-out",
256
+ opacity: "1",
257
+ overflow: "hidden",
258
+ margin: "0",
259
+ padding: "0"
260
+ });
261
+ const card = createEl("div", {
262
+ width: d ? "min(400px, 35vw)" : "min(360px, 85vw)",
263
+ backgroundColor: "#FFF",
264
+ borderRadius: d ? "32px" : "24px",
265
+ 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)",
266
+ display: "flex",
267
+ flexDirection: "column",
268
+ alignItems: "center",
269
+ justifyContent: "center",
270
+ padding: d ? "48px 36px" : "36px 24px",
271
+ gap: d ? "24px" : "20px",
272
+ transition: "transform 400ms ease-out,opacity 400ms ease-out"
273
+ });
274
+ const wrap = createEl("div", {
275
+ width: "48px",
276
+ height: "48px",
277
+ display: "flex",
278
+ alignItems: "center",
279
+ justifyContent: "center"
280
+ });
281
+ const NS = "http://www.w3.org/2000/svg";
282
+ const svg = document.createElementNS(NS, "svg");
283
+ svg.setAttribute("width", "48");
284
+ svg.setAttribute("height", "48");
285
+ svg.setAttribute("viewBox", "0 0 48 48");
286
+ svg.style.animation = "kycspin 1.2s cubic-bezier(0.4,0,0.6,1) infinite";
287
+ const bg = document.createElementNS(NS, "circle");
288
+ for (const [k, v] of Object.entries({
289
+ cx: "24",
290
+ cy: "24",
291
+ r: "20",
292
+ "stroke-width": "3",
293
+ fill: "none",
294
+ stroke: color + "33"
295
+ })) bg.setAttribute(k, v);
296
+ const fg = document.createElementNS(NS, "circle");
297
+ for (const [k, v] of Object.entries({
298
+ cx: "24",
299
+ cy: "24",
300
+ r: "20",
301
+ "stroke-width": "3",
302
+ fill: "none",
303
+ stroke: color,
304
+ "stroke-linecap": "round",
305
+ "stroke-dasharray": "62.8",
306
+ "stroke-dashoffset": "15.7"
307
+ })) fg.setAttribute(k, v);
308
+ setStyles(fg, { transformOrigin: "center", animation: "kycdash 1.5s ease-in-out infinite" });
309
+ svg.append(bg, fg);
310
+ wrap.appendChild(svg);
311
+ const font = 'Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif';
312
+ const textBox = createEl("div", { textAlign: "center" });
313
+ const title = createEl("h3", { fontFamily: font, fontWeight: "600", fontSize: "18px", lineHeight: "1.3", color: "#1F2937", margin: "0 0 4px 0" });
314
+ title.textContent = strings.title;
315
+ const subtitle = createEl("p", { fontFamily: font, fontWeight: "400", fontSize: "13px", lineHeight: "1.4", color: "rgba(31,41,55,0.7)", margin: "0" });
316
+ subtitle.textContent = strings.subtitle;
317
+ textBox.append(title, subtitle);
318
+ const track = createEl("div", { width: "100%", maxWidth: "240px", height: "3px", backgroundColor: color + "1A", borderRadius: "2px", overflow: "hidden" });
319
+ const bar = createEl("div", { width: "40%", height: "100%", backgroundColor: color, borderRadius: "2px", transform: "translateX(-100%)", animation: "kycprogress 2000ms ease-in-out infinite" });
320
+ track.appendChild(bar);
321
+ card.append(wrap, textBox, track);
322
+ overlay.appendChild(card);
323
+ this.origBodyStyles = { overflow: document.body.style.overflow, position: document.body.style.position };
324
+ setStyles(document.body, { overflow: "hidden", position: "fixed", width: "100%", height: "100%" });
325
+ document.body.appendChild(overlay);
326
+ let prevW = window.innerWidth;
327
+ let prevH = window.innerHeight;
328
+ const onResize = debounce(() => {
329
+ const w = window.innerWidth;
330
+ const h = window.innerHeight;
331
+ if (Math.abs(w - prevW) > 1 || Math.abs(h - prevH) > 1) {
332
+ setStyles(overlay, { width: w + "px", height: h + "px" });
333
+ prevW = w;
334
+ prevH = h;
335
+ }
336
+ }, d ? 50 : 150);
337
+ window.addEventListener("resize", onResize);
338
+ this.overlay = overlay;
339
+ this.cleanup = () => {
340
+ window.removeEventListener("resize", onResize);
341
+ if (this.origBodyStyles) {
342
+ setStyles(document.body, { ...this.origBodyStyles, width: "", height: "" });
343
+ }
344
+ };
345
+ return overlay;
346
+ }
347
+ get element() {
348
+ return this.overlay;
349
+ }
350
+ showError(message, lang) {
351
+ if (!this.overlay) return;
352
+ const strings = LOADER_I18N[lang || ""] || LOADER_I18N.en;
353
+ const card = this.overlay.querySelector("div");
354
+ if (!card) return;
355
+ card.innerHTML = "";
356
+ const font = 'Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif';
357
+ const iconWrap = createEl("div", {
358
+ width: "48px",
359
+ height: "48px",
360
+ borderRadius: "50%",
361
+ backgroundColor: "rgba(239, 68, 68, 0.1)",
362
+ display: "flex",
363
+ alignItems: "center",
364
+ justifyContent: "center"
365
+ });
366
+ const NS = "http://www.w3.org/2000/svg";
367
+ const svg = document.createElementNS(NS, "svg");
368
+ svg.setAttribute("width", "24");
369
+ svg.setAttribute("height", "24");
370
+ svg.setAttribute("viewBox", "0 0 24 24");
371
+ svg.setAttribute("fill", "none");
372
+ svg.setAttribute("stroke", "#ef4444");
373
+ svg.setAttribute("stroke-width", "2");
374
+ svg.setAttribute("stroke-linecap", "round");
375
+ const line1 = document.createElementNS(NS, "line");
376
+ line1.setAttribute("x1", "18");
377
+ line1.setAttribute("y1", "6");
378
+ line1.setAttribute("x2", "6");
379
+ line1.setAttribute("y2", "18");
380
+ const line2 = document.createElementNS(NS, "line");
381
+ line2.setAttribute("x1", "6");
382
+ line2.setAttribute("y1", "6");
383
+ line2.setAttribute("x2", "18");
384
+ line2.setAttribute("y2", "18");
385
+ svg.append(line1, line2);
386
+ iconWrap.appendChild(svg);
387
+ const title = createEl("h3", {
388
+ fontFamily: font,
389
+ fontWeight: "600",
390
+ fontSize: "18px",
391
+ lineHeight: "1.3",
392
+ color: "#1F2937",
393
+ margin: "0",
394
+ textAlign: "center"
395
+ });
396
+ title.textContent = strings.errorTitle;
397
+ const msg = createEl("p", {
398
+ fontFamily: font,
399
+ fontWeight: "400",
400
+ fontSize: "13px",
401
+ lineHeight: "1.5",
402
+ color: "rgba(31,41,55,0.7)",
403
+ margin: "0",
404
+ textAlign: "center",
405
+ wordBreak: "break-word",
406
+ maxWidth: "300px"
407
+ });
408
+ msg.textContent = message;
409
+ const btn = createEl("button", {
410
+ fontFamily: font,
411
+ fontWeight: "600",
412
+ fontSize: "14px",
413
+ padding: "10px 32px",
414
+ borderRadius: "12px",
415
+ border: "none",
416
+ backgroundColor: "#1F2937",
417
+ color: "#fff",
418
+ cursor: "pointer",
419
+ transition: "opacity 150ms"
420
+ });
421
+ btn.textContent = strings.close;
422
+ btn.onmouseenter = () => {
423
+ btn.style.opacity = "0.85";
424
+ };
425
+ btn.onmouseleave = () => {
426
+ btn.style.opacity = "1";
427
+ };
428
+ btn.onclick = () => this.destroy();
429
+ card.append(iconWrap, title, msg, btn);
430
+ }
431
+ fadeOut() {
432
+ if (!this.overlay) return;
433
+ const d = isDesktop();
434
+ const card = this.overlay.querySelector("div");
435
+ if (card) {
436
+ setStyles(card, {
437
+ transform: d ? "translateY(-10px) scale(0.98)" : "translateY(-15px) scale(0.96)",
438
+ opacity: "0"
439
+ });
440
+ }
441
+ this.overlay.style.opacity = "0";
442
+ }
443
+ destroy() {
444
+ this.cleanup?.();
445
+ if (this.overlay?.parentNode) this.overlay.remove();
446
+ this.overlay = null;
447
+ this.cleanup = null;
448
+ }
449
+ };
450
+
451
+ // src/browser/message-handler.ts
452
+ var MessageHandler = class {
453
+ constructor(iframeSrc, iframe, callbacks) {
454
+ this.iframeSrc = iframeSrc;
455
+ this.iframe = iframe;
456
+ this.callbacks = callbacks;
457
+ this.listener = null;
458
+ this.readyListener = null;
459
+ this.completionHandled = false;
460
+ }
461
+ /**
462
+ * Start listening for postMessage events from the widget iframe.
463
+ */
464
+ listen() {
465
+ const origin = getOrigin(this.iframeSrc);
466
+ this.listener = (e) => {
467
+ if (e.origin !== origin) return;
468
+ if (e.source !== this.iframe.contentWindow) return;
469
+ const data = e.data || {};
470
+ const type = data.type;
471
+ if (type === WIDGET_EVENTS.COMPLETE && this.callbacks.onSuccess) {
472
+ if (this.completionHandled) return;
473
+ this.completionHandled = true;
474
+ this.callbacks.onSuccess(data.result);
475
+ setTimeout(() => this.iframe.remove(), 1e3);
476
+ } else if (type === WIDGET_EVENTS.CANCEL && this.callbacks.onCancel) {
477
+ if (this.completionHandled) return;
478
+ this.completionHandled = true;
479
+ this.callbacks.onCancel();
480
+ setTimeout(() => this.iframe.remove(), 500);
481
+ } else if (type === WIDGET_EVENTS.ERROR && this.callbacks.onError) {
482
+ if (this.completionHandled) return;
483
+ this.completionHandled = true;
484
+ this.callbacks.onError(data.error || "Unknown error");
485
+ setTimeout(() => this.iframe.remove(), 500);
486
+ } else if (type === WIDGET_EVENTS.READY && this.callbacks.onReady) {
487
+ this.callbacks.onReady();
488
+ }
489
+ };
490
+ window.addEventListener("message", this.listener);
491
+ }
492
+ /**
493
+ * Listen for the first READY event, then call the callback once.
494
+ */
495
+ onReadyOnce(callback) {
496
+ const origin = getOrigin(this.iframeSrc);
497
+ this.readyListener = (e) => {
498
+ if (e.origin !== origin || e.data?.type !== WIDGET_EVENTS.READY) return;
499
+ window.removeEventListener("message", this.readyListener);
500
+ this.readyListener = null;
501
+ callback();
502
+ };
503
+ window.addEventListener("message", this.readyListener);
504
+ }
505
+ destroy() {
506
+ if (this.listener) {
507
+ window.removeEventListener("message", this.listener);
508
+ this.listener = null;
509
+ }
510
+ if (this.readyListener) {
511
+ window.removeEventListener("message", this.readyListener);
512
+ this.readyListener = null;
513
+ }
514
+ }
515
+ };
516
+
517
+ // src/browser/viewport.ts
518
+ function getVV() {
519
+ if (window.visualViewport) {
520
+ return {
521
+ width: window.visualViewport.width,
522
+ height: window.visualViewport.height,
523
+ offsetTop: window.visualViewport.offsetTop || 0,
524
+ offsetLeft: window.visualViewport.offsetLeft || 0
525
+ };
526
+ }
527
+ return { width: window.innerWidth, height: window.innerHeight, offsetTop: 0, offsetLeft: 0 };
528
+ }
529
+ function ensureViewportMeta() {
530
+ try {
531
+ const meta = document.querySelector('meta[name="viewport"]');
532
+ if (!meta) {
533
+ const m = document.createElement("meta");
534
+ m.setAttribute("name", "viewport");
535
+ m.setAttribute("content", "width=device-width, initial-scale=1.0, viewport-fit=cover");
536
+ document.head.appendChild(m);
537
+ } else {
538
+ const c = meta.getAttribute("content") || "";
539
+ if (!c.includes("viewport-fit=cover")) {
540
+ meta.setAttribute("content", c + ", viewport-fit=cover");
541
+ }
542
+ }
543
+ } catch {
544
+ }
545
+ }
546
+ function setupViewportSizing(overlay, iframe) {
547
+ ensureViewportMeta();
548
+ let baselineHeight = 0;
549
+ let desktop = isDesktop();
550
+ let rafId = null;
551
+ const initBaseline = () => {
552
+ const vv = getVV();
553
+ baselineHeight = window.innerHeight || vv.height || document.documentElement.clientHeight || 0;
554
+ desktop = isDesktop();
555
+ if (rafId) cancelAnimationFrame(rafId);
556
+ rafId = requestAnimationFrame(applySize);
557
+ };
558
+ const applySize = () => {
559
+ try {
560
+ const vv = getVV();
561
+ const inner = window.innerHeight || vv.height;
562
+ const kbThreshold = desktop ? 200 : 150;
563
+ const isKeyboard = inner - vv.height > kbThreshold;
564
+ const height = isKeyboard ? vv.height : baselineHeight || inner;
565
+ 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" };
566
+ if (overlay) setStyles(overlay, dims);
567
+ if (iframe) setStyles(iframe, dims);
568
+ } catch {
569
+ }
570
+ };
571
+ let prevW = window.innerWidth;
572
+ let prevH = window.innerHeight;
573
+ let prevX = 0;
574
+ let prevY = 0;
575
+ const debouncedApply = debounce(() => {
576
+ if (rafId) cancelAnimationFrame(rafId);
577
+ rafId = requestAnimationFrame(applySize);
578
+ }, desktop ? 50 : 150);
579
+ const handleResize = () => {
580
+ const vv = getVV();
581
+ const w = vv.width;
582
+ const h = vv.height;
583
+ const x = vv.offsetLeft;
584
+ const y = vv.offsetTop;
585
+ const changed = Math.abs(w - prevW) > 1 || Math.abs(h - prevH) > 1 || Math.abs(x - prevX) > 1 || Math.abs(y - prevY) > 1;
586
+ if (!changed) return;
587
+ if (Math.abs(h - prevH) > 1) initBaseline();
588
+ prevW = w;
589
+ prevH = h;
590
+ prevX = x;
591
+ prevY = y;
592
+ if (desktop) {
593
+ if (rafId) cancelAnimationFrame(rafId);
594
+ rafId = requestAnimationFrame(applySize);
595
+ } else {
596
+ debouncedApply();
597
+ }
598
+ };
599
+ const handleOrientation = () => {
600
+ initBaseline();
601
+ if (rafId) cancelAnimationFrame(rafId);
602
+ rafId = requestAnimationFrame(applySize);
603
+ };
604
+ initBaseline();
605
+ applySize();
606
+ window.addEventListener("resize", handleResize);
607
+ window.addEventListener("orientationchange", handleOrientation);
608
+ if (window.visualViewport) {
609
+ window.visualViewport.addEventListener("resize", handleResize);
610
+ window.visualViewport.addEventListener("scroll", handleResize);
611
+ }
612
+ return () => {
613
+ try {
614
+ if (rafId) cancelAnimationFrame(rafId);
615
+ window.removeEventListener("resize", handleResize);
616
+ window.removeEventListener("orientationchange", handleOrientation);
617
+ if (window.visualViewport) {
618
+ window.visualViewport.removeEventListener("resize", handleResize);
619
+ window.visualViewport.removeEventListener("scroll", handleResize);
620
+ }
621
+ } catch {
622
+ }
623
+ };
624
+ }
625
+
626
+ // src/browser/index.ts
627
+ var FlonkKYC = class {
628
+ constructor(options = {}) {
629
+ this.widgetUrl = (options.widgetUrl || DEFAULT_WIDGET_URL).replace(/\/$/, "");
630
+ this.apiBase = (options.apiBase || DEFAULT_API_BASE).replace(/\/$/, "");
631
+ }
632
+ // ── Public API ───────────────────────────────────────
633
+ /**
634
+ * Open the KYC verification widget.
635
+ *
636
+ * Flows (pick one):
637
+ * 1. `{ serverUrl }` — auto-create session via your backend (recommended)
638
+ * 2. `{ sessionId, embedToken }` — server-to-server with pre-created session
639
+ * 3. `{ publishableKey }` — client-side only
640
+ */
641
+ async init(config) {
642
+ if (!config) throw new FlonkValidationError("config is required");
643
+ if (config.serverUrl) {
644
+ return this.initWithServerUrl(config);
645
+ }
646
+ if (config.sessionId && config.embedToken) {
647
+ return this.initWithEmbedToken(config);
648
+ }
649
+ if (config.sessionId) {
650
+ return this.initWithSession(config);
651
+ }
652
+ const pk = config.publishableKey;
653
+ if (!pk || !/^pk_/.test(pk)) {
654
+ throw new FlonkValidationError(
655
+ "Provide one of: serverUrl, sessionId + embedToken, or publishableKey (pk_*)"
656
+ );
657
+ }
658
+ return this.initWithPublishableKey(config);
659
+ }
660
+ /**
661
+ * Preview mode — no API calls, mock data.
662
+ */
663
+ preview(config = {}) {
664
+ const colors = config.colors || { primaryColor: "#3b82f6", secondaryColor: "#93c5fd" };
665
+ return this.openWidget({
666
+ mode: "preview",
667
+ isPreview: "true",
668
+ previewColors: JSON.stringify(colors),
669
+ allowManualUpload: "true",
670
+ documentType: config.documentType || "id_card",
671
+ lang: config.lang || "de",
672
+ overlayColor: config.overlayColor
673
+ }, {
674
+ primaryColor: colors.primaryColor || "#3b82f6",
675
+ lang: config.lang,
676
+ onSuccess: config.onSuccess,
677
+ onError: config.onError,
678
+ onCancel: config.onCancel
679
+ });
680
+ }
681
+ /**
682
+ * Embed inline preview in a container (for dashboards).
683
+ */
684
+ embed(config) {
685
+ if (!config?.container) throw new FlonkValidationError("container is required");
686
+ const container = typeof config.container === "string" ? document.querySelector(config.container) : config.container;
687
+ if (!container) throw new FlonkValidationError("Container element not found");
688
+ let colors = config.colors || { primaryColor: "#3b82f6", secondaryColor: "#93c5fd" };
689
+ let device = config.device || "mobile";
690
+ let scale = config.scale ?? 1;
691
+ const lang = config.lang || "de";
692
+ const buildSrc = (c, d) => {
693
+ const p = new URLSearchParams({
694
+ mode: "preview",
695
+ isPreview: "true",
696
+ previewColors: JSON.stringify(c),
697
+ allowManualUpload: "true",
698
+ documentType: config.documentType || "id_card",
699
+ device: d,
700
+ lang
701
+ });
702
+ return `${this.widgetUrl}/?${p.toString()}`;
703
+ };
704
+ const applyScale = () => {
705
+ iframe.style.transform = scale !== 1 ? `scale(${scale})` : "";
706
+ iframe.style.transformOrigin = scale !== 1 ? "top left" : "";
707
+ };
708
+ const iframe = document.createElement("iframe");
709
+ iframe.style.cssText = "border:none;width:100%;height:100%;display:block";
710
+ iframe.src = buildSrc(colors, device);
711
+ iframe.title = "KYC Widget Preview";
712
+ iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
713
+ applyScale();
714
+ container.innerHTML = "";
715
+ container.appendChild(iframe);
716
+ return {
717
+ iframe,
718
+ setColors(newColors) {
719
+ colors = { ...colors, ...newColors };
720
+ if (newColors.primaryColor && !newColors.secondaryColor) {
721
+ colors.secondaryColor = generateSecondaryColor(newColors.primaryColor);
722
+ }
723
+ iframe.src = buildSrc(colors, device);
724
+ },
725
+ setDevice(d) {
726
+ device = d;
727
+ iframe.src = buildSrc(colors, device);
728
+ },
729
+ getColors: () => ({ ...colors }),
730
+ destroy: () => iframe.remove()
731
+ };
732
+ }
733
+ // ── Private flows ────────────────────────────────────
734
+ /**
735
+ * Flow 1: serverUrl — POST to client's backend, get sessionId + embedToken.
736
+ */
737
+ async initWithServerUrl(config) {
738
+ const { sessionId, embedToken } = await fetchSessionFromServer(
739
+ config.serverUrl,
740
+ config.clientMetadata
741
+ );
742
+ return this.initWithEmbedToken({ ...config, sessionId, embedToken });
743
+ }
744
+ /**
745
+ * Flow 2: sessionId + embedToken — fetch session data, open widget.
746
+ */
747
+ async initWithEmbedToken(config) {
748
+ const designTokens = await fetchDesignTokens(this.apiBase, {
749
+ sessionId: config.sessionId
750
+ });
751
+ const primaryColor = designTokens?.colors?.primary?.cannabis || "#15BA68";
752
+ const loader = new Loader();
753
+ loader.show(primaryColor, config.lang);
754
+ try {
755
+ const sessionData = await fetchPublicSession(
756
+ this.apiBase,
757
+ config.sessionId,
758
+ config.embedToken
759
+ );
760
+ const session = {
761
+ id: sessionData.id,
762
+ allowManualUpload: sessionData.allowManualUpload ?? config.allowManualUpload ?? true,
763
+ clientMetadata: sessionData.clientMetadata || config.clientMetadata,
764
+ qrCodeUrl: sessionData.qrCodeUrl,
765
+ testMode: sessionData.testMode || false,
766
+ poaEnabled: sessionData.poaEnabled || false,
767
+ poaRequired: sessionData.poaRequired || false
768
+ };
769
+ return this.buildWidget(config.embedToken, session, config, loader, designTokens);
770
+ } catch (err) {
771
+ const msg = err.message || "Failed to initialize verification";
772
+ loader.showError(msg, config.lang);
773
+ config.onError?.(msg);
774
+ throw err;
775
+ }
776
+ }
777
+ /**
778
+ * Flow 3: sessionId only — exchange for embedToken, then init.
779
+ */
780
+ async initWithSession(config) {
781
+ const designTokens = await fetchDesignTokens(this.apiBase, {
782
+ sessionId: config.sessionId
783
+ });
784
+ const primaryColor = designTokens?.colors?.primary?.cannabis || "#15BA68";
785
+ const loader = new Loader();
786
+ loader.show(primaryColor, config.lang);
787
+ try {
788
+ const { embedToken, session } = await exchangeSessionForToken(
789
+ this.apiBase,
790
+ config.sessionId
791
+ );
792
+ return this.buildWidget(embedToken, session, config, loader, designTokens);
793
+ } catch (err) {
794
+ const msg = err.message || "Failed to initialize verification";
795
+ loader.showError(msg, config.lang);
796
+ config.onError?.(msg);
797
+ throw err;
798
+ }
799
+ }
800
+ /**
801
+ * Flow 4: publishableKey — fetch widget token, open widget.
802
+ */
803
+ async initWithPublishableKey(config) {
804
+ const pk = config.publishableKey;
805
+ const designTokens = await fetchDesignTokens(this.apiBase, { pk });
806
+ const primaryColor = designTokens?.colors?.primary?.cannabis || "#15BA68";
807
+ const loader = new Loader();
808
+ loader.show(primaryColor, config.lang);
809
+ try {
810
+ const data = await fetchWidgetToken(pk, this.apiBase);
811
+ const params = {
812
+ mode: "embedded",
813
+ publishableKey: pk,
814
+ allowManualUpload: String(data.allowManualUpload !== false)
815
+ };
816
+ if (data.token) params.token = data.token;
817
+ if (config.clientMetadata) {
818
+ params.clientMetadata = JSON.stringify(config.clientMetadata);
819
+ }
820
+ if (config.lang) params.lang = config.lang;
821
+ if (config.overlayColor) params.overlayColor = config.overlayColor;
822
+ return this.openWidget(params, {
823
+ primaryColor,
824
+ lang: config.lang,
825
+ loader,
826
+ onSuccess: config.onSuccess,
827
+ onError: config.onError,
828
+ onCancel: config.onCancel,
829
+ onReady: config.onReady,
830
+ mount: config.mount
831
+ });
832
+ } catch (err) {
833
+ const msg = err.message || "Failed to initialize verification";
834
+ loader.showError(msg, config.lang);
835
+ config.onError?.(msg);
836
+ throw err;
837
+ }
838
+ }
839
+ // ── Core widget builder ──────────────────────────────
840
+ buildWidget(token, session, config, loader, designTokens) {
841
+ const params = {
842
+ mode: "embedded",
843
+ sessionId: config.sessionId || session.id,
844
+ token,
845
+ allowManualUpload: String(session.allowManualUpload !== false)
846
+ };
847
+ if (session.testMode) params.testMode = "true";
848
+ if (session.poaEnabled) params.poaEnabled = "true";
849
+ if (session.poaRequired) params.poaRequired = "true";
850
+ if (designTokens?.colors) {
851
+ params.designTokens = JSON.stringify(designTokens);
852
+ }
853
+ if (config.embedToken) params.embedToken = config.embedToken;
854
+ if (session.qrCodeUrl) params.qrCodeUrl = session.qrCodeUrl;
855
+ const clientMetadata = session.clientMetadata || config.clientMetadata;
856
+ if (clientMetadata) {
857
+ params.clientMetadata = JSON.stringify(clientMetadata);
858
+ }
859
+ if (config.lang) params.lang = config.lang;
860
+ if (config.overlayColor) params.overlayColor = config.overlayColor;
861
+ return this.openWidget(params, {
862
+ primaryColor: designTokens?.colors?.primary?.cannabis || "#15BA68",
863
+ lang: config.lang,
864
+ loader,
865
+ onSuccess: config.onSuccess,
866
+ onError: config.onError,
867
+ onCancel: config.onCancel,
868
+ onReady: config.onReady,
869
+ mount: config.mount
870
+ });
871
+ }
872
+ /**
873
+ * Core: create iframe, attach listeners, return WidgetInstance.
874
+ */
875
+ openWidget(params, opts) {
876
+ const filtered = Object.fromEntries(
877
+ Object.entries(params).filter(([, v]) => v != null)
878
+ );
879
+ const search = new URLSearchParams(filtered);
880
+ const src = `${this.widgetUrl}/?${search.toString()}`;
881
+ const iframe = createIframe(src);
882
+ const mountTarget = opts.mount || document.body;
883
+ const loader = opts.loader ?? (() => {
884
+ const l = new Loader();
885
+ l.show(opts.primaryColor, opts.lang);
886
+ return l;
887
+ })();
888
+ if (loader.element) adjustZIndex(loader.element, iframe);
889
+ mountTarget.appendChild(iframe);
890
+ const cleanupViewport = setupViewportSizing(loader.element, iframe);
891
+ let cleaned = false;
892
+ const cleanupAll = () => {
893
+ if (cleaned) return;
894
+ cleaned = true;
895
+ try {
896
+ handler.destroy();
897
+ cleanupViewport();
898
+ loader.destroy();
899
+ if (iframe.parentNode) iframe.remove();
900
+ } catch {
901
+ }
902
+ };
903
+ const afterCleanup = (delayMs) => () => setTimeout(cleanupAll, delayMs);
904
+ const handler = new MessageHandler(src, iframe, {
905
+ onSuccess: opts.onSuccess ? (r) => {
906
+ opts.onSuccess?.(r);
907
+ afterCleanup(1e3)();
908
+ } : void 0,
909
+ onError: opts.onError ? (e) => {
910
+ opts.onError?.(e);
911
+ afterCleanup(500)();
912
+ } : void 0,
913
+ onCancel: opts.onCancel ? () => {
914
+ opts.onCancel?.();
915
+ afterCleanup(500)();
916
+ } : void 0,
917
+ onReady: opts.onReady
918
+ });
919
+ handler.listen();
920
+ handler.onReadyOnce(() => {
921
+ transitionLoaderToIframe(loader.element, iframe, () => loader.destroy());
922
+ });
923
+ return {
924
+ iframe,
925
+ destroy: cleanupAll
926
+ };
927
+ }
928
+ };
929
+ FlonkKYC.version = SDK_VERSION;
930
+ function FlonkKYCWidget({
931
+ widgetUrl,
932
+ apiBase,
933
+ autoOpen = true,
934
+ publishableKey,
935
+ serverUrl,
936
+ sessionId,
937
+ embedToken,
938
+ clientMetadata,
939
+ lang,
940
+ overlayColor,
941
+ allowManualUpload,
942
+ onSuccess,
943
+ onError,
944
+ onCancel,
945
+ onReady
946
+ }) {
947
+ const mountRef = useRef(null);
948
+ const widgetRef = useRef(null);
949
+ const destroyedRef = useRef(false);
950
+ const callbacksRef = useRef({ onSuccess, onError, onCancel, onReady });
951
+ callbacksRef.current = { onSuccess, onError, onCancel, onReady };
952
+ const sdk = useMemo(() => new FlonkKYC({ widgetUrl, apiBase }), [widgetUrl, apiBase]);
953
+ useEffect(() => {
954
+ if (!autoOpen) return;
955
+ destroyedRef.current = false;
956
+ const config = {
957
+ publishableKey,
958
+ serverUrl,
959
+ sessionId,
960
+ embedToken,
961
+ clientMetadata,
962
+ lang,
963
+ overlayColor,
964
+ allowManualUpload,
965
+ mount: mountRef.current || void 0,
966
+ onSuccess: (r) => callbacksRef.current.onSuccess?.(r),
967
+ onError: (e) => callbacksRef.current.onError?.(e),
968
+ onCancel: () => callbacksRef.current.onCancel?.(),
969
+ onReady: () => callbacksRef.current.onReady?.()
970
+ };
971
+ sdk.init(config).then((instance) => {
972
+ if (destroyedRef.current) {
973
+ instance.destroy();
974
+ } else {
975
+ widgetRef.current = instance;
976
+ }
977
+ }).catch((err) => {
978
+ if (!destroyedRef.current) {
979
+ callbacksRef.current.onError?.(err.message);
980
+ }
981
+ });
982
+ return () => {
983
+ destroyedRef.current = true;
984
+ widgetRef.current?.destroy();
985
+ widgetRef.current = null;
986
+ };
987
+ }, [sdk, publishableKey, serverUrl, sessionId, autoOpen]);
988
+ return /* @__PURE__ */ jsx("div", { ref: mountRef });
989
+ }
990
+
991
+ export { FlonkError, FlonkKYC, FlonkKYCWidget, FlonkValidationError };
992
+ //# sourceMappingURL=index.js.map
993
+ //# sourceMappingURL=index.js.map