smartcomply-web-sdk 1.0.49 → 1.0.51

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.
Files changed (41) hide show
  1. package/dist/camera/ActionDetector.d.ts +15 -21
  2. package/dist/camera/ActionDetector.d.ts.map +1 -1
  3. package/dist/camera/ActionDetector.js +51 -104
  4. package/dist/camera/ActionDetector.js.map +1 -1
  5. package/dist/esm/camera/ActionDetector.d.ts +15 -21
  6. package/dist/esm/camera/ActionDetector.d.ts.map +1 -1
  7. package/dist/esm/camera/ActionDetector.js +51 -104
  8. package/dist/esm/camera/ActionDetector.js.map +1 -1
  9. package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
  10. package/dist/esm/flow/SmartComplyFlow.js +3 -4
  11. package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
  12. package/dist/esm/modules/liveness/LivenessUI.d.ts +19 -15
  13. package/dist/esm/modules/liveness/LivenessUI.d.ts.map +1 -1
  14. package/dist/esm/modules/liveness/LivenessUI.js +99 -311
  15. package/dist/esm/modules/liveness/LivenessUI.js.map +1 -1
  16. package/dist/esm/modules/liveness/liveness.d.ts +10 -9
  17. package/dist/esm/modules/liveness/liveness.d.ts.map +1 -1
  18. package/dist/esm/modules/liveness/liveness.js +57 -104
  19. package/dist/esm/modules/liveness/liveness.js.map +1 -1
  20. package/dist/esm/utils/constants.d.ts +12 -6
  21. package/dist/esm/utils/constants.d.ts.map +1 -1
  22. package/dist/esm/utils/constants.js +13 -7
  23. package/dist/esm/utils/constants.js.map +1 -1
  24. package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
  25. package/dist/flow/SmartComplyFlow.js +3 -4
  26. package/dist/flow/SmartComplyFlow.js.map +1 -1
  27. package/dist/modules/liveness/LivenessUI.d.ts +19 -15
  28. package/dist/modules/liveness/LivenessUI.d.ts.map +1 -1
  29. package/dist/modules/liveness/LivenessUI.js +99 -311
  30. package/dist/modules/liveness/LivenessUI.js.map +1 -1
  31. package/dist/modules/liveness/liveness.d.ts +10 -9
  32. package/dist/modules/liveness/liveness.d.ts.map +1 -1
  33. package/dist/modules/liveness/liveness.js +56 -103
  34. package/dist/modules/liveness/liveness.js.map +1 -1
  35. package/dist/smartcomply.browser.js +213 -513
  36. package/dist/smartcomply.browser.js.map +3 -3
  37. package/dist/utils/constants.d.ts +12 -6
  38. package/dist/utils/constants.d.ts.map +1 -1
  39. package/dist/utils/constants.js +13 -7
  40. package/dist/utils/constants.js.map +1 -1
  41. package/package.json +2 -2
@@ -296,7 +296,8 @@ var SmartComplySDK = (() => {
296
296
  var MAX_DOCUMENT_BYTES = 5 * 1024 * 1024;
297
297
  var MAX_VIDEO_BYTES = 50 * 1024 * 1024;
298
298
  var MAX_SNAPSHOT_BYTES = 2 * 1024 * 1024;
299
- var DEFAULT_CHALLENGE_ACTIONS = ["BLINK", "TURN_HEAD", "OPEN_MOUTH"];
299
+ var DEFAULT_CHALLENGE_ACTIONS = ["BLINK"];
300
+ var LIVENESS_TIME_LIMIT_SECONDS = 12;
300
301
 
301
302
  // src/camera/CameraManager.ts
302
303
  var CameraManager = class {
@@ -3635,7 +3636,7 @@ var SmartComplySDK = (() => {
3635
3636
  Pn(s2, 1, (i2.left + i2.right) / 2), Pn(s2, 2, (i2.top + i2.bottom) / 2), Pn(s2, 4, i2.right - i2.left), Pn(s2, 3, i2.bottom - i2.top);
3636
3637
  } else Pn(s2, 1, 0.5), Pn(s2, 2, 0.5), Pn(s2, 4, 1), Pn(s2, 3, 1);
3637
3638
  if (n2?.rotationDegrees) {
3638
- if (n2?.rotationDegrees % 90 != 0) throw Error("Expected rotation to be a multiple of 90\xB0.");
3639
+ if (n2?.rotationDegrees % 90 != 0) throw Error("Expected rotation to be a multiple of 90°.");
3639
3640
  if (Pn(s2, 5, -Math.PI * n2.rotationDegrees / 180), n2?.rotationDegrees % 180 != 0) {
3640
3641
  const [t3, r3] = sa(e2);
3641
3642
  n2 = Fn(s2, 3) * r3 / t3, i2 = Fn(s2, 4) * t3 / r3, Pn(s2, 4, n2), Pn(s2, 3, i2);
@@ -4455,158 +4456,83 @@ var SmartComplySDK = (() => {
4455
4456
  };
4456
4457
 
4457
4458
  // src/camera/ActionDetector.ts
4458
- var ACTION_CONFIGS = {
4459
- BLINK: { threshold: 0.25, holdFrames: 2 },
4460
- TURN_LEFT: { threshold: 0.3, holdFrames: 3 },
4461
- TURN_RIGHT: { threshold: 0.3, holdFrames: 3 },
4462
- TURN_HEAD: { threshold: 0.9, holdFrames: 5 },
4463
- OPEN_MOUTH: { threshold: 0.3, holdFrames: 2 }
4464
- };
4465
- var DEFAULT_CONFIG = { threshold: 0.4, holdFrames: 4 };
4459
+ var BLINK_THRESHOLD = 0.25;
4460
+ var BLINK_HOLD_FRAMES = 2;
4461
+ var YAW_MOTION_THRESHOLD_DEG = 6;
4466
4462
  var ActionDetector = class {
4467
- // ~0.6s of frames
4468
- constructor(requiredActions) {
4469
- this.requiredActions = requiredActions;
4470
- this.completedActions = /* @__PURE__ */ new Set();
4471
- this.holdCounters = /* @__PURE__ */ new Map();
4472
- this.yawHistory = [];
4473
- this.YAW_HISTORY_SIZE = 18;
4463
+ /** Kept for API compatibility with callers that still pass a list in. Unused internally. */
4464
+ constructor(_requiredActions) {
4465
+ this.blinkHoldCount = 0;
4466
+ this.blinkDetected = false;
4467
+ this.motionDetected = false;
4468
+ this.baselineYaw = null;
4474
4469
  }
4475
4470
  check(result) {
4476
- const currentAction = this.getCurrentAction();
4477
4471
  if (!result.faceDetected) {
4478
- return this.buildStates(currentAction, 0);
4472
+ return this.buildProgress(false);
4479
4473
  }
4480
4474
  const bs2 = result.blendshapes;
4481
- if (currentAction && !this.completedActions.has(currentAction)) {
4482
- const conf = this.getConfidence(currentAction, bs2, result.transformMatrix);
4483
- const cfg = ACTION_CONFIGS[currentAction] ?? DEFAULT_CONFIG;
4484
- if (conf >= cfg.threshold) {
4485
- const count = (this.holdCounters.get(currentAction) || 0) + 1;
4486
- this.holdCounters.set(currentAction, count);
4487
- if (count >= cfg.holdFrames) {
4488
- this.completedActions.add(currentAction);
4489
- this.holdCounters.delete(currentAction);
4475
+ const blinkConf = ((bs2.get("eyeBlinkLeft") || 0) + (bs2.get("eyeBlinkRight") || 0)) / 2;
4476
+ if (!this.blinkDetected) {
4477
+ if (blinkConf >= BLINK_THRESHOLD) {
4478
+ this.blinkHoldCount++;
4479
+ if (this.blinkHoldCount >= BLINK_HOLD_FRAMES) {
4480
+ this.blinkDetected = true;
4490
4481
  }
4491
4482
  } else {
4492
- this.holdCounters.set(currentAction, 0);
4483
+ this.blinkHoldCount = 0;
4493
4484
  }
4494
- return this.buildStates(currentAction, conf);
4495
- }
4496
- return this.buildStates(currentAction, 0);
4497
- }
4498
- getCurrentAction() {
4499
- for (const action of this.requiredActions) {
4500
- if (!this.completedActions.has(action)) return action;
4501
4485
  }
4502
- return null;
4503
- }
4504
- buildStates(currentAction, currentConf) {
4505
- return this.requiredActions.map((action) => ({
4506
- action,
4507
- detected: this.completedActions.has(action),
4508
- active: action === currentAction,
4509
- confidence: action === currentAction ? currentConf : 0
4510
- }));
4511
- }
4512
- getConfidence(action, bs2, matrix) {
4513
- switch (action) {
4514
- case "BLINK": {
4515
- const left = bs2.get("eyeBlinkLeft") || 0;
4516
- const right = bs2.get("eyeBlinkRight") || 0;
4517
- return (left + right) / 2;
4518
- }
4519
- case "TURN_LEFT": {
4520
- const yaw = Math.abs(this.getHeadYaw(matrix));
4521
- return yaw > 5 ? Math.min(1, yaw / 20) : 0;
4522
- }
4523
- case "TURN_RIGHT": {
4524
- const yaw = Math.abs(this.getHeadYaw(matrix));
4525
- return yaw > 5 ? Math.min(1, yaw / 20) : 0;
4486
+ if (!this.motionDetected) {
4487
+ const yaw = this.getHeadYaw(result.transformMatrix);
4488
+ if (this.baselineYaw === null) {
4489
+ this.baselineYaw = yaw;
4490
+ } else if (Math.abs(yaw - this.baselineYaw) >= YAW_MOTION_THRESHOLD_DEG) {
4491
+ this.motionDetected = true;
4526
4492
  }
4527
- case "TURN_HEAD": {
4528
- const yaw = this.getHeadYaw(matrix);
4529
- this.yawHistory.push(yaw);
4530
- if (this.yawHistory.length > this.YAW_HISTORY_SIZE) {
4531
- this.yawHistory.shift();
4532
- }
4533
- if (this.yawHistory.length < this.YAW_HISTORY_SIZE) {
4534
- return 0;
4535
- }
4536
- const left = this.yawHistory.some((y2) => y2 <= -10);
4537
- const right = this.yawHistory.some((y2) => y2 >= 10);
4538
- if (left && right) return 1;
4539
- if (left || right) return 0.5;
4540
- return 0;
4541
- }
4542
- case "OPEN_MOUTH":
4543
- return bs2.get("jawOpen") || 0;
4544
- default:
4545
- return 0;
4546
4493
  }
4494
+ return this.buildProgress(true);
4495
+ }
4496
+ buildProgress(faceDetected) {
4497
+ return {
4498
+ faceDetected,
4499
+ blinkDetected: this.blinkDetected,
4500
+ motionDetected: this.motionDetected,
4501
+ livenessConfirmed: this.blinkDetected || this.motionDetected,
4502
+ progress: 0
4503
+ // caller fills in time-based progress
4504
+ };
4547
4505
  }
4548
4506
  getHeadYaw(matrix) {
4549
4507
  if (!matrix || matrix.length < 16) return 0;
4550
4508
  return Math.atan2(-matrix[2], matrix[0]) * (180 / Math.PI);
4551
4509
  }
4552
- allCompleted() {
4553
- return this.requiredActions.every((a2) => this.completedActions.has(a2));
4554
- }
4555
- completedCount() {
4556
- return this.completedActions.size;
4510
+ livenessConfirmed() {
4511
+ return this.blinkDetected || this.motionDetected;
4557
4512
  }
4558
4513
  reset() {
4559
- this.completedActions.clear();
4560
- this.holdCounters.clear();
4561
- this.yawHistory = [];
4514
+ this.blinkHoldCount = 0;
4515
+ this.blinkDetected = false;
4516
+ this.motionDetected = false;
4517
+ this.baselineYaw = null;
4562
4518
  }
4563
4519
  };
4564
4520
 
4565
4521
  // src/modules/liveness/LivenessUI.ts
4566
- var ICONS = {
4567
- BLINK: "\u{1F441}",
4568
- TURN_LEFT: "\u2194",
4569
- TURN_RIGHT: "\u2194",
4570
- TURN_HEAD: "\u2194",
4571
- OPEN_MOUTH: "\u{1F62E}"
4572
- };
4573
- var ACTION_CUES = {
4574
- BLINK: `<div style="font-size:52px;line-height:1;animation:sc-blink 1.2s ease-in-out infinite;">\u{1F441}\uFE0F</div>
4575
- <div style="font-size:12px;font-weight:600;color:#fff;margin-top:6px;letter-spacing:0.5px;">BLINK YOUR EYES</div>`,
4576
- TURN_LEFT: `<div style="font-size:52px;line-height:1;animation:sc-turn-head 1.4s ease-in-out infinite;">\u2194\uFE0F</div>
4577
- <div style="font-size:12px;font-weight:600;color:#fff;margin-top:6px;letter-spacing:0.5px;">TURN HEAD SIDEWAYS</div>`,
4578
- TURN_RIGHT: `<div style="font-size:52px;line-height:1;animation:sc-turn-head 1.4s ease-in-out infinite;">\u2194\uFE0F</div>
4579
- <div style="font-size:12px;font-weight:600;color:#fff;margin-top:6px;letter-spacing:0.5px;">TURN HEAD SIDEWAYS</div>`,
4580
- TURN_HEAD: `<div style="font-size:52px;line-height:1;animation:sc-turn-head 1.4s ease-in-out infinite;">\u2194\uFE0F</div>
4581
- <div style="font-size:12px;font-weight:600;color:#fff;margin-top:6px;letter-spacing:0.5px;">TURN HEAD SIDE TO SIDE</div>`,
4582
- OPEN_MOUTH: `<div style="font-size:52px;line-height:1;animation:sc-mouth 1.2s ease-in-out infinite;">\u{1F62E}</div>
4583
- <div style="font-size:12px;font-weight:600;color:#fff;margin-top:6px;letter-spacing:0.5px;">OPEN YOUR MOUTH</div>`
4584
- };
4585
4522
  function injectLivenessKeyframes() {
4586
4523
  if (document.getElementById("sc-liveness-kf")) return;
4587
4524
  const s2 = document.createElement("style");
4588
4525
  s2.id = "sc-liveness-kf";
4589
4526
  s2.textContent = `
4590
- @keyframes sc-blink {
4591
- 0%,100% { transform:scaleY(1); }
4592
- 40% { transform:scaleY(0.1); }
4593
- 50% { transform:scaleY(1); }
4594
- }
4595
- @keyframes sc-turn-head {
4596
- 0%,100% { transform:translateX(0); }
4597
- 25% { transform:translateX(-12px); }
4598
- 75% { transform:translateX(12px); }
4599
- }
4600
- @keyframes sc-mouth {
4601
- 0%,100% { transform:scaleY(1); }
4602
- 40% { transform:scaleY(1.25); }
4603
- 60% { transform:scaleY(1); }
4604
- }
4605
4527
  @keyframes sc-pop-in {
4606
4528
  0% { transform:scale(0.7); opacity:0; }
4607
4529
  60% { transform:scale(1.05); }
4608
4530
  100% { transform:scale(1); opacity:1; }
4609
4531
  }
4532
+ @keyframes sc-scan-dot {
4533
+ 0%,100% { opacity:0.3; transform:scale(0.85); }
4534
+ 50% { opacity:1; transform:scale(1); }
4535
+ }
4610
4536
  `;
4611
4537
  document.head.appendChild(s2);
4612
4538
  }
@@ -4615,39 +4541,22 @@ var SmartComplySDK = (() => {
4615
4541
  this.root = null;
4616
4542
  this.videoEl = null;
4617
4543
  this.instructionEl = null;
4618
- this.stepsEl = null;
4619
- this.progressBarEl = null;
4620
4544
  this.confidenceRingEl = null;
4621
- this.timerEl = null;
4622
4545
  this.overlayEl = null;
4623
- this.actionCueEl = null;
4624
- this.timerInterval = null;
4625
- this.totalActions = 0;
4626
- this.currentCueAction = null;
4546
+ this.dotsEl = null;
4627
4547
  this.docCaptureRoot = null;
4628
- // updateActions() is called on every requestAnimationFrame tick (~60fps,
4629
- // up to the challenge's ~45s time limit) — cache each action's step
4630
- // element (avoids a querySelector per action per frame) and its last
4631
- // rendered detected/active combination (avoids rewriting the same style
4632
- // properties every frame when nothing about that action changed).
4633
- this.stepEls = null;
4634
- this.lastActionRender = /* @__PURE__ */ new Map();
4548
+ this.scanStartMs = 0;
4549
+ this.scanDurationMs = 0;
4635
4550
  }
4636
4551
  mount(container, challenge) {
4637
4552
  injectLivenessKeyframes();
4638
- this.totalActions = challenge.actions.length;
4639
4553
  this.root = document.createElement("div");
4640
4554
  this.root.style.cssText = `
4641
- display:flex;flex-direction:row;width:100%;margin:0 auto;
4555
+ position:relative;width:100%;max-width:420px;margin:0 auto;
4642
4556
  background:#0a0a0a;border-radius:16px;overflow:hidden;
4643
4557
  font-family:system-ui,-apple-system,sans-serif;
4644
4558
  box-shadow:0 8px 32px rgba(0,0,0,0.4);
4645
- min-height:340px;
4646
- `;
4647
- const camCol = document.createElement("div");
4648
- camCol.style.cssText = `
4649
- position:relative;flex:0 0 55%;
4650
- background:#000;overflow:hidden;
4559
+ aspect-ratio:3/4;
4651
4560
  `;
4652
4561
  this.videoEl = document.createElement("video");
4653
4562
  this.videoEl.autoplay = true;
@@ -4656,13 +4565,12 @@ var SmartComplySDK = (() => {
4656
4565
  this.videoEl.style.cssText = `
4657
4566
  width:100%;height:100%;display:block;
4658
4567
  transform:scaleX(-1);object-fit:cover;
4659
- min-height:320px;
4660
4568
  `;
4661
4569
  const guideWrap = document.createElement("div");
4662
4570
  guideWrap.style.cssText = `
4663
4571
  position:absolute;top:50%;left:50%;
4664
- transform:translate(-50%,-55%);
4665
- width:140px;height:180px;pointer-events:none;
4572
+ transform:translate(-50%,-52%);
4573
+ width:64%;aspect-ratio:140/180;pointer-events:none;
4666
4574
  `;
4667
4575
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
4668
4576
  svg.setAttribute("viewBox", "0 0 140 180");
@@ -4673,9 +4581,8 @@ var SmartComplySDK = (() => {
4673
4581
  bgOval.setAttribute("rx", "64");
4674
4582
  bgOval.setAttribute("ry", "86");
4675
4583
  bgOval.setAttribute("fill", "none");
4676
- bgOval.setAttribute("stroke", "rgba(255,255,255,0.3)");
4584
+ bgOval.setAttribute("stroke", "rgba(255,255,255,0.35)");
4677
4585
  bgOval.setAttribute("stroke-width", "2");
4678
- bgOval.setAttribute("stroke-dasharray", "6 3");
4679
4586
  const confOval = document.createElementNS("http://www.w3.org/2000/svg", "ellipse");
4680
4587
  confOval.setAttribute("cx", "70");
4681
4588
  confOval.setAttribute("cy", "90");
@@ -4687,7 +4594,7 @@ var SmartComplySDK = (() => {
4687
4594
  const circ = 2 * Math.PI * Math.sqrt((64 * 64 + 86 * 86) / 2);
4688
4595
  confOval.setAttribute("stroke-dasharray", `${circ}`);
4689
4596
  confOval.setAttribute("stroke-dashoffset", `${circ}`);
4690
- confOval.style.cssText = "transition:stroke-dashoffset 0.15s ease;transform:rotate(-90deg);transform-origin:70px 90px;";
4597
+ confOval.style.cssText = "transition:stroke-dashoffset 0.2s linear;transform:rotate(-90deg);transform-origin:70px 90px;";
4691
4598
  this.confidenceRingEl = confOval;
4692
4599
  svg.appendChild(bgOval);
4693
4600
  svg.appendChild(confOval);
@@ -4696,11 +4603,11 @@ var SmartComplySDK = (() => {
4696
4603
  camTop.style.cssText = `
4697
4604
  position:absolute;top:0;left:0;right:0;
4698
4605
  background:linear-gradient(180deg,rgba(0,0,0,0.8) 0%,transparent 100%);
4699
- padding:10px 12px 28px;
4606
+ padding:14px 16px 32px;
4700
4607
  `;
4701
4608
  this.instructionEl = document.createElement("div");
4702
4609
  this.instructionEl.style.cssText = `
4703
- color:#fff;font-size:13px;font-weight:600;text-align:center;line-height:1.4;
4610
+ color:#fff;font-size:14px;font-weight:600;text-align:center;line-height:1.4;
4704
4611
  `;
4705
4612
  this.instructionEl.textContent = challenge.instruction;
4706
4613
  camTop.appendChild(this.instructionEl);
@@ -4708,177 +4615,62 @@ var SmartComplySDK = (() => {
4708
4615
  camBottom.style.cssText = `
4709
4616
  position:absolute;bottom:0;left:0;right:0;
4710
4617
  background:linear-gradient(0deg,rgba(0,0,0,0.7) 0%,transparent 100%);
4711
- padding:24px 10px 8px;text-align:center;
4712
- `;
4713
- this.timerEl = document.createElement("div");
4714
- this.timerEl.style.cssText = `
4715
- color:rgba(255,255,255,0.5);font-size:11px;font-variant-numeric:tabular-nums;
4716
- `;
4717
- camBottom.appendChild(this.timerEl);
4718
- this.actionCueEl = document.createElement("div");
4719
- this.actionCueEl.style.cssText = `
4720
- position:absolute;bottom:36px;left:50%;transform:translateX(-50%);
4721
- z-index:5;pointer-events:none;display:none;text-align:center;
4722
- background:rgba(0,0,0,0.6);backdrop-filter:blur(4px);
4723
- border-radius:14px;padding:10px 18px;min-width:170px;
4724
- border:1.5px solid rgba(255,255,255,0.12);
4618
+ padding:28px 10px 18px;display:flex;justify-content:center;
4725
4619
  `;
4726
- camCol.appendChild(this.videoEl);
4727
- camCol.appendChild(guideWrap);
4728
- camCol.appendChild(camTop);
4729
- camCol.appendChild(camBottom);
4730
- camCol.appendChild(this.actionCueEl);
4731
- const stepsCol = document.createElement("div");
4732
- stepsCol.style.cssText = `
4733
- flex:1;display:flex;flex-direction:column;
4734
- padding:16px 14px;gap:8px;
4735
- background:#111;border-left:1px solid rgba(255,255,255,0.06);
4736
- justify-content:space-between;
4737
- `;
4738
- const progressWrap = document.createElement("div");
4739
- progressWrap.style.cssText = `
4740
- height:3px;background:rgba(255,255,255,0.1);border-radius:2px;overflow:hidden;
4741
- margin-bottom:4px;
4742
- `;
4743
- this.progressBarEl = document.createElement("div");
4744
- this.progressBarEl.style.cssText = `
4745
- height:100%;width:0%;background:linear-gradient(90deg,#22c55e,#4ade80);
4746
- border-radius:2px;transition:width 0.4s ease;
4747
- `;
4748
- progressWrap.appendChild(this.progressBarEl);
4749
- this.stepsEl = document.createElement("div");
4750
- this.stepsEl.style.cssText = "display:flex;flex-direction:column;gap:6px;flex:1;";
4751
- this.stepEls = /* @__PURE__ */ new Map();
4752
- this.lastActionRender = /* @__PURE__ */ new Map();
4753
- for (let i2 = 0; i2 < challenge.actions.length; i2++) {
4754
- const action = challenge.actions[i2];
4755
- const step = document.createElement("div");
4756
- step.dataset.action = action;
4757
- step.style.cssText = `
4758
- display:flex;align-items:center;gap:8px;
4759
- padding:8px 10px;border-radius:10px;
4760
- background:rgba(255,255,255,0.04);
4761
- border:1px solid rgba(255,255,255,0.06);
4762
- transition:all 0.3s ease;
4763
- opacity:${i2 === 0 ? "1" : "0.45"};
4764
- `;
4765
- const num = document.createElement("div");
4766
- num.className = "sc-step-num";
4767
- num.style.cssText = `
4768
- width:24px;height:24px;border-radius:50%;flex-shrink:0;
4769
- display:flex;align-items:center;justify-content:center;
4770
- font-size:12px;font-weight:700;color:#fff;
4771
- background:rgba(255,255,255,0.1);
4772
- border:1.5px solid rgba(255,255,255,0.2);
4773
- transition:all 0.3s ease;
4620
+ this.dotsEl = document.createElement("div");
4621
+ this.dotsEl.style.cssText = "display:flex;gap:6px;";
4622
+ for (let i2 = 0; i2 < 3; i2++) {
4623
+ const dot = document.createElement("div");
4624
+ dot.style.cssText = `
4625
+ width:6px;height:6px;border-radius:50%;background:#22c55e;
4626
+ animation:sc-scan-dot 1.1s ease-in-out ${i2 * 0.18}s infinite;
4774
4627
  `;
4775
- num.textContent = String(i2 + 1);
4776
- const label = document.createElement("div");
4777
- label.style.cssText = "color:#fff;font-size:12px;font-weight:500;flex:1;line-height:1.3;";
4778
- label.textContent = this.describeAction(action);
4779
- const icon = document.createElement("div");
4780
- icon.style.cssText = "font-size:16px;flex-shrink:0;";
4781
- icon.textContent = ICONS[action] || "";
4782
- step.appendChild(num);
4783
- step.appendChild(label);
4784
- step.appendChild(icon);
4785
- this.stepsEl.appendChild(step);
4786
- this.stepEls.set(action, { step, num });
4787
- }
4788
- stepsCol.appendChild(progressWrap);
4789
- stepsCol.appendChild(this.stepsEl);
4628
+ this.dotsEl.appendChild(dot);
4629
+ }
4630
+ camBottom.appendChild(this.dotsEl);
4631
+ this.root.appendChild(this.videoEl);
4632
+ this.root.appendChild(guideWrap);
4633
+ this.root.appendChild(camTop);
4634
+ this.root.appendChild(camBottom);
4790
4635
  this.overlayEl = document.createElement("div");
4791
4636
  this.overlayEl.style.cssText = `
4792
4637
  position:absolute;inset:0;display:none;align-items:center;
4793
4638
  justify-content:center;z-index:10;
4794
4639
  background:rgba(0,0,0,0.88);backdrop-filter:blur(8px);
4795
4640
  `;
4796
- this.root.appendChild(camCol);
4797
- this.root.appendChild(stepsCol);
4798
4641
  this.root.appendChild(this.overlayEl);
4799
4642
  container.appendChild(this.root);
4800
- this.startTimer(challenge.time_limit_seconds);
4643
+ this.scanDurationMs = challenge.time_limit_seconds * 1e3;
4644
+ this.startScanClock();
4801
4645
  return this.videoEl;
4802
4646
  }
4803
- updateActions(states) {
4804
- if (!this.stepsEl || !this.progressBarEl || !this.stepEls) return;
4805
- let completed = 0;
4806
- for (const state of states) {
4807
- if (state.detected) completed++;
4808
- const renderKey = state.detected ? "detected" : state.active ? "active" : "idle";
4809
- if (this.lastActionRender.get(state.action) === renderKey) continue;
4810
- this.lastActionRender.set(state.action, renderKey);
4811
- const cached = this.stepEls.get(state.action);
4812
- if (!cached) continue;
4813
- const { step, num } = cached;
4814
- if (state.detected) {
4815
- step.style.opacity = "1";
4816
- step.style.background = "rgba(34,197,94,0.12)";
4817
- step.style.borderColor = "rgba(34,197,94,0.3)";
4818
- if (num) {
4819
- num.style.background = "#22c55e";
4820
- num.style.borderColor = "#22c55e";
4821
- num.textContent = "\u2713";
4822
- }
4823
- } else if (state.active) {
4824
- step.style.opacity = "1";
4825
- step.style.background = "rgba(59,130,246,0.12)";
4826
- step.style.borderColor = "rgba(59,130,246,0.3)";
4827
- if (num) {
4828
- num.style.borderColor = "#3b82f6";
4829
- num.style.background = "rgba(59,130,246,0.3)";
4830
- }
4831
- }
4832
- }
4833
- const activeState = states.find((s2) => s2.active && !s2.detected) ?? null;
4834
- const pct = this.totalActions > 0 ? completed / this.totalActions * 100 : 0;
4835
- this.progressBarEl.style.width = `${pct}%`;
4836
- if (this.confidenceRingEl) {
4837
- const circ = 2 * Math.PI * Math.sqrt((64 * 64 + 86 * 86) / 2);
4838
- const conf = activeState ? activeState.confidence : 0;
4839
- this.confidenceRingEl.setAttribute("stroke-dashoffset", `${circ * (1 - conf)}`);
4840
- this.confidenceRingEl.setAttribute("stroke", conf > 0.6 ? "#22c55e" : "#3b82f6");
4841
- }
4842
- this.updateActionCue(activeState?.action ?? null);
4843
- }
4844
- updateActionCue(action) {
4845
- if (!this.actionCueEl) return;
4846
- if (!action) {
4847
- this.actionCueEl.style.display = "none";
4848
- this.currentCueAction = null;
4849
- return;
4850
- }
4851
- if (action === this.currentCueAction) return;
4852
- this.currentCueAction = action;
4853
- const cueHtml = ACTION_CUES[action];
4854
- if (!cueHtml) {
4855
- this.actionCueEl.style.display = "none";
4856
- return;
4857
- }
4858
- this.actionCueEl.innerHTML = cueHtml;
4859
- this.actionCueEl.style.display = "block";
4860
- this.actionCueEl.style.animation = "none";
4861
- void this.actionCueEl.offsetWidth;
4862
- this.actionCueEl.style.animation = "sc-pop-in 0.3s ease forwards";
4647
+ /**
4648
+ * Resets the progress ring's clock to now. The caller (liveness.ts) mounts
4649
+ * the UI, then waits ~1s for camera AE/AF to settle before the detection
4650
+ * loop actually starts — call this again right when detection begins so
4651
+ * the ring's timing matches the real scan window instead of running ahead.
4652
+ */
4653
+ startScanClock() {
4654
+ this.scanStartMs = performance.now();
4655
+ }
4656
+ /** Advances the scan progress ring based on elapsed time in the window. */
4657
+ updateScanProgress(livenessConfirmed) {
4658
+ if (!this.confidenceRingEl) return;
4659
+ const elapsed = performance.now() - this.scanStartMs;
4660
+ const pct = this.scanDurationMs > 0 ? Math.min(1, elapsed / this.scanDurationMs) : 0;
4661
+ const circ = 2 * Math.PI * Math.sqrt((64 * 64 + 86 * 86) / 2);
4662
+ this.confidenceRingEl.setAttribute("stroke-dashoffset", `${circ * (1 - pct)}`);
4663
+ this.confidenceRingEl.setAttribute("stroke", livenessConfirmed ? "#22c55e" : "#3b82f6");
4863
4664
  }
4864
4665
  updateInstruction(text) {
4865
4666
  if (this.instructionEl) this.instructionEl.textContent = text;
4866
4667
  }
4867
- stopTimer() {
4868
- if (this.timerInterval) {
4869
- clearInterval(this.timerInterval);
4870
- this.timerInterval = null;
4871
- }
4872
- if (this.timerEl) this.timerEl.textContent = "";
4873
- }
4874
4668
  showResult(status, _verificationId) {
4875
4669
  return new Promise((resolve) => {
4876
4670
  if (!this.overlayEl) {
4877
4671
  resolve();
4878
4672
  return;
4879
4673
  }
4880
- if (this.actionCueEl) this.actionCueEl.style.display = "none";
4881
- this.stopTimer();
4882
4674
  const ok = status === "processing";
4883
4675
  this.overlayEl.style.display = "flex";
4884
4676
  this.overlayEl.innerHTML = `
@@ -4886,10 +4678,10 @@ var SmartComplySDK = (() => {
4886
4678
  <div style="width:72px;height:72px;border-radius:50%;margin:0 auto 16px;display:flex;align-items:center;justify-content:center;font-size:32px;
4887
4679
  background:${ok ? "rgba(34,197,94,0.2)" : "rgba(239,68,68,0.2)"};
4888
4680
  border:3px solid ${ok ? "#22c55e" : "#ef4444"};">
4889
- ${ok ? "\u2713" : "\u2715"}
4681
+ ${ok ? "✓" : "✕"}
4890
4682
  </div>
4891
4683
  <div style="font-size:17px;font-weight:700;margin-bottom:8px;">${ok ? "Verification submitted" : "Submission failed"}</div>
4892
- <div style="font-size:12px;opacity:0.6;line-height:1.6;">${ok ? "Hold on while we wrap up\u2026" : "Something went wrong. Please try again."}</div>
4684
+ <div style="font-size:12px;opacity:0.6;line-height:1.6;">${ok ? "Hold on while we wrap up…" : "Something went wrong. Please try again."}</div>
4893
4685
  </div>`;
4894
4686
  setTimeout(resolve, 1800);
4895
4687
  });
@@ -4900,14 +4692,12 @@ var SmartComplySDK = (() => {
4900
4692
  resolve();
4901
4693
  return;
4902
4694
  }
4903
- if (this.actionCueEl) this.actionCueEl.style.display = "none";
4904
- this.stopTimer();
4905
4695
  this.overlayEl.style.display = "flex";
4906
4696
  this.overlayEl.innerHTML = `
4907
4697
  <div style="text-align:center;color:#fff;animation:sc-pop-in 0.35s ease;padding:24px;">
4908
- <div style="width:72px;height:72px;border-radius:50%;margin:0 auto 14px;display:flex;align-items:center;justify-content:center;font-size:32px;background:rgba(251,191,36,0.15);border:3px solid #fbbf24;">\u23F1</div>
4698
+ <div style="width:72px;height:72px;border-radius:50%;margin:0 auto 14px;display:flex;align-items:center;justify-content:center;font-size:32px;background:rgba(251,191,36,0.15);border:3px solid #fbbf24;">⏱</div>
4909
4699
  <div style="font-size:17px;font-weight:700;margin-bottom:8px;">Time's Up</div>
4910
- <div style="font-size:12px;opacity:0.6;margin-bottom:18px;">You ran out of time. Please try again.</div>
4700
+ <div style="font-size:12px;opacity:0.6;margin-bottom:18px;">We couldn't confirm liveness in time. Please try again.</div>
4911
4701
  <button id="sc-retry-btn" style="padding:11px 28px;border-radius:10px;background:#3b82f6;color:#fff;border:none;cursor:pointer;font-size:14px;font-weight:600;width:100%;max-width:180px;">Try Again</button>
4912
4702
  </div>`;
4913
4703
  const btn = this.overlayEl.querySelector("#sc-retry-btn");
@@ -4944,7 +4734,7 @@ var SmartComplySDK = (() => {
4944
4734
  header.innerHTML = `<div style="color:#fff;font-size:16px;font-weight:700;margin-bottom:6px;">Scan Your ID Document</div><div style="color:rgba(255,255,255,0.6);font-size:12px;line-height:1.6;">Fit your document inside the frame.<br>Make sure all text and corners are visible.</div>`;
4945
4735
  const bottomPanel = document.createElement("div");
4946
4736
  bottomPanel.style.cssText = "position:absolute;bottom:0;left:0;right:0;background:linear-gradient(0deg,rgba(0,0,0,0.92) 0%,rgba(0,0,0,0.6) 70%,transparent 100%);padding:16px;";
4947
- bottomPanel.innerHTML = `<div style="font-size:11px;color:rgba(255,255,255,0.4);text-align:center;margin-bottom:12px;">\u{1F4A1} Good lighting \xB7 No glare \xB7 All 4 corners visible</div><button id="sc-capture-doc-btn" style="width:100%;padding:12px;border-radius:12px;background:#3b82f6;color:#fff;border:none;cursor:pointer;font-size:15px;font-weight:700;margin-bottom:8px;">Capture Document</button><label style="width:100%;display:flex;align-items:center;justify-content:center;gap:6px;padding:9px;border-radius:12px;border:1.5px solid rgba(255,255,255,0.18);cursor:pointer;color:rgba(255,255,255,0.6);font-size:13px;font-weight:500;"><span>\u{1F4C1}</span><span>Upload from gallery instead</span><input id="sc-upload-doc-input" type="file" accept="image/*" style="display:none;" /></label>`;
4737
+ bottomPanel.innerHTML = `<div style="font-size:11px;color:rgba(255,255,255,0.4);text-align:center;margin-bottom:12px;">💡 Good lighting · No glare · All 4 corners visible</div><button id="sc-capture-doc-btn" style="width:100%;padding:12px;border-radius:12px;background:#3b82f6;color:#fff;border:none;cursor:pointer;font-size:15px;font-weight:700;margin-bottom:8px;">Capture Document</button><label style="width:100%;display:flex;align-items:center;justify-content:center;gap:6px;padding:9px;border-radius:12px;border:1.5px solid rgba(255,255,255,0.18);cursor:pointer;color:rgba(255,255,255,0.6);font-size:13px;font-weight:500;"><span>📁</span><span>Upload from gallery instead</span><input id="sc-upload-doc-input" type="file" accept="image/*" style="display:none;" /></label>`;
4948
4738
  docRoot.appendChild(docVideo);
4949
4739
  docRoot.appendChild(guideOverlay);
4950
4740
  docRoot.appendChild(header);
@@ -4998,7 +4788,6 @@ var SmartComplySDK = (() => {
4998
4788
  });
4999
4789
  }
5000
4790
  unmount() {
5001
- this.stopTimer();
5002
4791
  try {
5003
4792
  this.docCaptureRoot?.remove();
5004
4793
  } catch (_2) {
@@ -5011,42 +4800,9 @@ var SmartComplySDK = (() => {
5011
4800
  this.root = null;
5012
4801
  this.videoEl = null;
5013
4802
  this.instructionEl = null;
5014
- this.stepsEl = null;
5015
- this.progressBarEl = null;
5016
4803
  this.confidenceRingEl = null;
5017
- this.timerEl = null;
5018
4804
  this.overlayEl = null;
5019
- this.actionCueEl = null;
5020
- this.stepEls = null;
5021
- this.lastActionRender.clear();
5022
- }
5023
- startTimer(seconds) {
5024
- let remaining = seconds;
5025
- const update = () => {
5026
- if (!this.timerEl) return;
5027
- const m2 = Math.floor(remaining / 60);
5028
- const s2 = remaining % 60;
5029
- this.timerEl.textContent = `${m2 > 0 ? `${m2}:${String(s2).padStart(2, "0")}` : `${s2}s`} remaining`;
5030
- if (remaining <= 5) this.timerEl.style.color = "#ef4444";
5031
- };
5032
- update();
5033
- this.timerInterval = setInterval(() => {
5034
- remaining--;
5035
- update();
5036
- if (remaining <= 0 && this.timerInterval) {
5037
- clearInterval(this.timerInterval);
5038
- }
5039
- }, 1e3);
5040
- }
5041
- describeAction(action) {
5042
- const labels = {
5043
- BLINK: "Blink your eyes",
5044
- TURN_LEFT: "Turn your head to the side",
5045
- TURN_RIGHT: "Turn your head to the side",
5046
- TURN_HEAD: "Turn your head side to side",
5047
- OPEN_MOUTH: "Open your mouth wide"
5048
- };
5049
- return labels[action] || action.replace(/_/g, " ").toLowerCase();
4805
+ this.dotsEl = null;
5050
4806
  }
5051
4807
  };
5052
4808
 
@@ -5158,23 +4914,23 @@ var SmartComplySDK = (() => {
5158
4914
  /**
5159
4915
  * Full liveness check — orchestrates the entire flow:
5160
4916
  *
5161
- * 1. Take autoshot selfie from camera
5162
- * 2. Create liveness entry on backend (with challenge actions + selfie + optional doc)
5163
- * 3. Run action detection loop (camera + MediaPipe)
5164
- * 4. Record video (document/liveness flows) OR skip recording (data verification)
5165
- * 5. Submit to backend and return response (status: "processing")
4917
+ * 1. Run action detection loop (camera + MediaPipe) — challenge actions only,
4918
+ * not recorded (fast head/mouth motion produces blurry frames).
4919
+ * 2. Create liveness entry on backend (with challenge actions + optional doc)
4920
+ * 3. Record a short, clean, post-challenge clip (neutral pose + a natural
4921
+ * blink) — this is what's sent for face comparison in LIVENESS mode.
4922
+ * 4. Submit to backend and return response (status: "processing")
5166
4923
  *
5167
- * For data_verification the backend uses COMPARISON mode (autoshot vs govt DB photo)
5168
- * so video recording is unnecessary — we skip it to save bandwidth and latency.
4924
+ * Backend runs LIVENESS mode (blink/motion check, then its own comparison
4925
+ * frame extracted from this same video) for both data and document
4926
+ * verification — see FACE_COMPARISON_USER_GUIDE.md.
5169
4927
  *
5170
4928
  * @param container - DOM element to mount the liveness UI into
5171
4929
  * @param params - identifier info and optional document image
5172
- * @param actions - override challenge actions (default: BLINK, TURN_HEAD, OPEN_MOUTH)
4930
+ * @param actions - override the challenge_actions tag sent to the backend (default: BLINK)
5173
4931
  * @param existingEntryId - reuse an already-created LivenessEntry (avoids double charge)
5174
4932
  */
5175
4933
  async startCheck(container, params, actions, existingEntryId) {
5176
- const isDataVerification = params.verification_type === "data_verification" || !params.document && !params.document_front && !params.id_file && !!params.identity_check;
5177
- const skipVideo = isDataVerification;
5178
4934
  const camera = new CameraManager();
5179
4935
  const recorder = new VideoRecorder();
5180
4936
  const faceEngine = new FaceDetectorEngine();
@@ -5290,76 +5046,39 @@ var SmartComplySDK = (() => {
5290
5046
  tempVideo.remove();
5291
5047
  const uiChallenge = {
5292
5048
  actions: entry.challenge_actions,
5293
- time_limit_seconds: 45,
5294
- instruction: "Complete each action below to verify it's you",
5295
- // Use backend-provided instructions if available (mirror-corrected),
5296
- // otherwise fall back to the frontend labels.
5297
- instructions: entry.challenge_instructions
5049
+ time_limit_seconds: LIVENESS_TIME_LIMIT_SECONDS,
5050
+ instruction: "Blink and look around naturally"
5298
5051
  };
5299
5052
  ui2.unmount();
5300
5053
  const videoEl = ui2.mount(container, uiChallenge);
5301
5054
  videoEl.srcObject = stream;
5302
5055
  await videoEl.play();
5303
5056
  await new Promise((r2) => setTimeout(r2, 1e3));
5304
- if (!skipVideo) {
5305
- recorder.start(stream);
5306
- }
5307
5057
  ui2.updateInstruction(uiChallenge.instruction);
5308
- const actionDetector = new ActionDetector(entry.challenge_actions);
5309
- let lastAction = null;
5310
- let pauseUntil = 0;
5311
- let pendingNextAction = null;
5312
- let pendingNextLabel = null;
5058
+ recorder.start(stream);
5059
+ ui2.startScanClock();
5060
+ const MIN_SCAN_MS = 2e3;
5061
+ const actionDetector = new ActionDetector();
5313
5062
  const detectionResult = await new Promise(
5314
5063
  (resolve) => {
5315
5064
  let animFrameId;
5065
+ const scanStart = performance.now();
5316
5066
  const timeout = setTimeout(() => {
5317
5067
  cancelAnimationFrame(animFrameId);
5318
5068
  resolve("timeout");
5319
5069
  }, uiChallenge.time_limit_seconds * 1e3);
5320
- const getLabel = (action) => uiChallenge.instructions?.[action] || this.describeAction(action);
5321
5070
  const detectLoop = () => {
5322
- const now = performance.now();
5323
- const remaining = pauseUntil - now;
5324
- if (remaining > 0) {
5325
- setTimeout(() => {
5326
- animFrameId = requestAnimationFrame(detectLoop);
5327
- }, remaining);
5328
- return;
5329
- }
5330
- if (pendingNextAction) {
5331
- lastAction = pendingNextAction;
5332
- if (pendingNextLabel) ui2.updateInstruction(pendingNextLabel);
5333
- pendingNextAction = null;
5334
- pendingNextLabel = null;
5335
- }
5336
5071
  if (videoEl.readyState >= 2) {
5337
- const detection = faceEngine.detect(
5338
- videoEl,
5339
- performance.now()
5340
- );
5341
- const states = actionDetector.check(detection);
5342
- ui2.updateActions(states);
5343
- const current = actionDetector.getCurrentAction();
5072
+ const detection = faceEngine.detect(videoEl, performance.now());
5073
+ const scan = actionDetector.check(detection);
5074
+ ui2.updateScanProgress(scan.livenessConfirmed);
5344
5075
  if (!detection.faceDetected) {
5345
5076
  ui2.updateInstruction("Move your face into the oval");
5346
- } else if (current) {
5347
- if (current !== lastAction) {
5348
- if (lastAction !== null) {
5349
- pauseUntil = now + 600;
5350
- pendingNextAction = current;
5351
- pendingNextLabel = getLabel(current);
5352
- ui2.updateInstruction(`Great! ${pendingNextLabel}`);
5353
- animFrameId = requestAnimationFrame(detectLoop);
5354
- return;
5355
- }
5356
- lastAction = current;
5357
- ui2.updateInstruction(getLabel(current));
5358
- }
5359
5077
  } else {
5360
- ui2.updateInstruction("All actions completed! \u2713");
5078
+ ui2.updateInstruction(uiChallenge.instruction);
5361
5079
  }
5362
- if (actionDetector.allCompleted()) {
5080
+ const elapsed = performance.now() - scanStart;
5081
+ if (detection.faceDetected && scan.livenessConfirmed && elapsed >= MIN_SCAN_MS) {
5363
5082
  clearTimeout(timeout);
5364
5083
  cancelAnimationFrame(animFrameId);
5365
5084
  resolve("completed");
@@ -5373,10 +5092,10 @@ var SmartComplySDK = (() => {
5373
5092
  );
5374
5093
  if (detectionResult === "timeout") {
5375
5094
  cleanedUp = true;
5376
- recorder.stop().catch(() => {
5377
- });
5378
5095
  return new Promise((resolveRetry, rejectRetry) => {
5379
5096
  ui2.showTimeout(() => {
5097
+ recorder.stop().catch(() => {
5098
+ });
5380
5099
  faceEngine.destroy();
5381
5100
  camera.stop();
5382
5101
  ui2.unmount();
@@ -5389,25 +5108,17 @@ var SmartComplySDK = (() => {
5389
5108
  });
5390
5109
  });
5391
5110
  }
5392
- ui2.updateInstruction("Almost done \u2014 hold still...");
5393
- await new Promise((r2) => setTimeout(r2, 800));
5111
+ ui2.updateInstruction("Verifying...");
5394
5112
  const autoshotBlob = await this.captureFrame(videoEl);
5395
5113
  if (!autoshotBlob || autoshotBlob.size === 0) {
5396
- throw new Error("Failed to capture selfie. Please ensure your face is visible and try again.");
5397
- }
5398
- const snapshotBlob = await this.captureFrame(videoEl);
5399
- let videoBlob;
5400
- let durationSeconds;
5401
- if (skipVideo) {
5402
5114
  recorder.stop().catch(() => {
5403
5115
  });
5404
- videoBlob = autoshotBlob;
5405
- durationSeconds = void 0;
5406
- } else {
5407
- const recorded = await recorder.stop();
5408
- videoBlob = recorded.blob;
5409
- durationSeconds = recorded.durationSeconds;
5116
+ throw new Error("Failed to capture selfie. Please ensure your face is visible and try again.");
5410
5117
  }
5118
+ const snapshotBlob = await this.captureFrame(videoEl);
5119
+ const recorded = await recorder.stop();
5120
+ const videoBlob = recorded.blob;
5121
+ const durationSeconds = recorded.durationSeconds;
5411
5122
  const submitResult = await this.submit(
5412
5123
  entry.id,
5413
5124
  videoBlob,
@@ -5521,16 +5232,6 @@ var SmartComplySDK = (() => {
5521
5232
  }
5522
5233
  return n2 > 0 ? sum / n2 : 0;
5523
5234
  }
5524
- describeAction(action) {
5525
- const descriptions = {
5526
- BLINK: "Blink your eyes",
5527
- TURN_LEFT: "Turn your head to the side",
5528
- TURN_RIGHT: "Turn your head to the side",
5529
- TURN_HEAD: "Turn your head side to side",
5530
- OPEN_MOUTH: "Open your mouth wide"
5531
- };
5532
- return descriptions[action] || action.replace(/_/g, " ").toLowerCase();
5533
- }
5534
5235
  };
5535
5236
 
5536
5237
  // src/client/Smartcomply.ts
@@ -5908,7 +5609,7 @@ var SmartComplySDK = (() => {
5908
5609
  const docLabel = this.getDocumentLabel(documentType);
5909
5610
  const requiresBack = this.shouldCaptureBackSide(documentType);
5910
5611
  const isCapturingFront = this.currentSide === "front";
5911
- const sideLabel = requiresBack ? isCapturingFront ? " \u2014 Front side" : " \u2014 Back side" : "";
5612
+ const sideLabel = requiresBack ? isCapturingFront ? " — Front side" : " — Back side" : "";
5912
5613
  const headerRow = document.createElement("div");
5913
5614
  headerRow.style.cssText = `
5914
5615
  display:flex;align-items:center;gap:14px;
@@ -5929,7 +5630,7 @@ var SmartComplySDK = (() => {
5929
5630
  titleEl.textContent = `Capture your ${docLabel}`;
5930
5631
  const subEl = document.createElement("div");
5931
5632
  subEl.style.cssText = `color:${theme.textSecondary};font-size:12px;margin-top:2px;`;
5932
- subEl.textContent = requiresBack ? isCapturingFront ? "Step 1 of 2 \u2014 front side" : "Step 2 of 2 \u2014 back side" : "Choose how to provide your document";
5633
+ subEl.textContent = requiresBack ? isCapturingFront ? "Step 1 of 2 — front side" : "Step 2 of 2 — back side" : "Choose how to provide your document";
5933
5634
  titleBlock.appendChild(titleEl);
5934
5635
  titleBlock.appendChild(subEl);
5935
5636
  headerRow.appendChild(iconBox);
@@ -5950,7 +5651,7 @@ var SmartComplySDK = (() => {
5950
5651
  transition:all 0.15s ease;min-height:80px;
5951
5652
  box-shadow:0 4px 14px ${theme.primary}30;
5952
5653
  `;
5953
- cameraBtn.innerHTML = `<span style="font-size:26px;">\u{1F4F7}</span><span>Take Photo</span><span style="font-size:10px;opacity:0.8;font-weight:500;">Use camera</span>`;
5654
+ cameraBtn.innerHTML = `<span style="font-size:26px;">📷</span><span>Take Photo</span><span style="font-size:10px;opacity:0.8;font-weight:500;">Use camera</span>`;
5954
5655
  cameraBtn.addEventListener("mouseenter", () => {
5955
5656
  cameraBtn.style.opacity = "0.9";
5956
5657
  cameraBtn.style.transform = "translateY(-1px)";
@@ -5972,7 +5673,7 @@ var SmartComplySDK = (() => {
5972
5673
  font-size:13px;font-weight:600;font-family:inherit;
5973
5674
  transition:all 0.15s ease;min-height:80px;
5974
5675
  `;
5975
- uploadBtn.innerHTML = `<span style="font-size:26px;">\u{1F5BC}\uFE0F</span><span>From Gallery</span><span style="font-size:10px;color:${theme.textMuted || theme.textSecondary};font-weight:500;">Upload file</span>`;
5676
+ uploadBtn.innerHTML = `<span style="font-size:26px;">🖼️</span><span>From Gallery</span><span style="font-size:10px;color:${theme.textMuted || theme.textSecondary};font-weight:500;">Upload file</span>`;
5976
5677
  uploadBtn.addEventListener("mouseenter", () => {
5977
5678
  uploadBtn.style.borderColor = theme.primary;
5978
5679
  uploadBtn.style.transform = "translateY(-1px)";
@@ -6006,7 +5707,7 @@ var SmartComplySDK = (() => {
6006
5707
  tipItems.forEach((t2) => {
6007
5708
  const row = document.createElement("div");
6008
5709
  row.style.cssText = `display:flex;align-items:center;gap:5px;font-size:11px;color:${theme.textSecondary};`;
6009
- row.innerHTML = `<span style="color:${theme.primary};font-weight:700;flex-shrink:0;">\u2713</span><span>${t2}</span>`;
5710
+ row.innerHTML = `<span style="color:${theme.primary};font-weight:700;flex-shrink:0;">✓</span><span>${t2}</span>`;
6010
5711
  tipsGrid.appendChild(row);
6011
5712
  });
6012
5713
  tips.appendChild(tipsGrid);
@@ -6020,7 +5721,7 @@ var SmartComplySDK = (() => {
6020
5721
  display:flex;align-items:center;gap:8px;
6021
5722
  font-size:12px;color:${theme.textSecondary};line-height:1.5;
6022
5723
  `;
6023
- notice.innerHTML = `<span style="font-size:16px;flex-shrink:0;">\u2139\uFE0F</span><span>You'll capture both the <strong style="color:${theme.text}">front</strong> and <strong style="color:${theme.text}">back</strong> of this document.</span>`;
5724
+ notice.innerHTML = `<span style="font-size:16px;flex-shrink:0;">ℹ️</span><span>You'll capture both the <strong style="color:${theme.text}">front</strong> and <strong style="color:${theme.text}">back</strong> of this document.</span>`;
6024
5725
  this.root.appendChild(notice);
6025
5726
  }
6026
5727
  const fileInput = document.createElement("input");
@@ -6046,10 +5747,10 @@ var SmartComplySDK = (() => {
6046
5747
  /** Returns an appropriate emoji icon for the document type. */
6047
5748
  _docIcon(documentType) {
6048
5749
  const t2 = documentType.toLowerCase();
6049
- if (t2.includes("passport")) return "\u{1F4D8}";
6050
- if (t2.includes("driver") || t2.includes("licen")) return "\u{1F697}";
6051
- if (t2.includes("voter") || t2.includes("pvc")) return "\u{1F5F3}\uFE0F";
6052
- return "\u{1FAAA}";
5750
+ if (t2.includes("passport")) return "📘";
5751
+ if (t2.includes("driver") || t2.includes("licen")) return "🚗";
5752
+ if (t2.includes("voter") || t2.includes("pvc")) return "🗳️";
5753
+ return "🪪";
6053
5754
  }
6054
5755
  renderCameraView(container, theme, documentType) {
6055
5756
  const wrapper = document.createElement("div");
@@ -6259,12 +5960,12 @@ var SmartComplySDK = (() => {
6259
5960
  `;
6260
5961
  const issueLine = document.createElement("div");
6261
5962
  issueLine.style.cssText = "font-weight:700;color:#b8860b;";
6262
- issueLine.textContent = `\u26A0\uFE0F ${quality.message}`;
5963
+ issueLine.textContent = `⚠️ ${quality.message}`;
6263
5964
  warn.appendChild(issueLine);
6264
5965
  if (quality.recommendations.length > 0) {
6265
5966
  const rec = document.createElement("div");
6266
5967
  rec.style.cssText = "margin-top:4px;color:" + theme.textSecondary + ";";
6267
- rec.textContent = quality.recommendations.join(" \xB7 ");
5968
+ rec.textContent = quality.recommendations.join(" · ");
6268
5969
  warn.appendChild(rec);
6269
5970
  }
6270
5971
  this.root.appendChild(warn);
@@ -6280,7 +5981,7 @@ var SmartComplySDK = (() => {
6280
5981
  this.root.innerHTML = "";
6281
5982
  this.renderChoiceScreen(container, theme, documentType);
6282
5983
  });
6283
- const useBtn = this.createButton(qualityIsPoor ? "Use Anyway \u26A0" : "Use Photo \u2713", theme, !qualityIsPoor);
5984
+ const useBtn = this.createButton(qualityIsPoor ? "Use Anyway ⚠" : "Use Photo ✓", theme, !qualityIsPoor);
6284
5985
  useBtn.style.flex = "1";
6285
5986
  useBtn.addEventListener("click", () => {
6286
5987
  if (img._blobUrl) {
@@ -6426,7 +6127,7 @@ var SmartComplySDK = (() => {
6426
6127
  position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
6427
6128
  font-size:28px;animation:pulse 2s ease-in-out infinite;
6428
6129
  `;
6429
- centerIcon.textContent = "\u{1F4C4}";
6130
+ centerIcon.textContent = "📄";
6430
6131
  spinnerContainer.appendChild(centerIcon);
6431
6132
  this.root.appendChild(spinnerContainer);
6432
6133
  const loadingText = document.createElement("div");
@@ -6461,7 +6162,7 @@ var SmartComplySDK = (() => {
6461
6162
  color:#ff4444;font-size:14px;margin-top:16px;animation:shake 0.5s ease-in-out;
6462
6163
  `;
6463
6164
  errorContainer.innerHTML = `
6464
- <span style="font-size:18px;">\u26A0\uFE0F</span>
6165
+ <span style="font-size:18px;">⚠️</span>
6465
6166
  <span>${message}</span>
6466
6167
  `;
6467
6168
  if (!document.head.querySelector("style[data-shake-animation]")) {
@@ -6646,13 +6347,13 @@ var SmartComplySDK = (() => {
6646
6347
 
6647
6348
  // src/flow/SmartComplyFlow.ts
6648
6349
  var COUNTRY_LABELS = {
6649
- nigeria: "\xF0\u0178\u2021\xB3\xF0\u0178\u2021\xAC Nigeria",
6650
- global: "\xF0\u0178\u0152\x8D Other Countries",
6651
- ghana: "\xF0\u0178\u2021\xAC\xF0\u0178\u2021\xAD Ghana",
6652
- kenya: "\xF0\u0178\u2021\xB0\xF0\u0178\u2021\xAA Kenya",
6653
- south_africa: "\xF0\u0178\u2021\xBF\xF0\u0178\u2021\xA6 South Africa",
6654
- united_states: "\xF0\u0178\u2021\xBA\xF0\u0178\u2021\xB8 United States",
6655
- united_kingdom: "\xF0\u0178\u2021\xAC\xF0\u0178\u2021\xA7 United Kingdom"
6350
+ nigeria: "🇳🇬 Nigeria",
6351
+ global: "🌍 Other Countries",
6352
+ ghana: "🇬🇭 Ghana",
6353
+ kenya: "🇰🇪 Kenya",
6354
+ south_africa: "🇿🇦 South Africa",
6355
+ united_states: "🇺🇸 United States",
6356
+ united_kingdom: "🇬🇧 United Kingdom"
6656
6357
  };
6657
6358
  function isUploadFieldType(type) {
6658
6359
  return type === "upload" || type === "image" || type === "file";
@@ -6666,37 +6367,37 @@ var SmartComplySDK = (() => {
6666
6367
  return t2.includes("voter") || t2.includes("driver") || t2.includes("license") || t2.includes("national id") || t2.includes("national id card");
6667
6368
  };
6668
6369
  if (u2.includes("BVN") || u2.includes("BANK VERIFICATION")) {
6669
- return { icon: "\xF0\u0178\x8F\xA6", desc: "Enter your 11-digit Bank Verification Number (BVN)", requiresBack: false };
6370
+ return { icon: "🏦", desc: "Enter your 11-digit Bank Verification Number (BVN)", requiresBack: false };
6670
6371
  }
6671
6372
  if (u2.includes("NIN") || u2.includes("NATIONAL IDENTITY NUMBER")) {
6672
- return { icon: "\xF0\u0178\u2020\u201D", desc: "Enter your 11-digit National Identification Number (NIN)", requiresBack: false };
6373
+ return { icon: "🆔", desc: "Enter your 11-digit National Identification Number (NIN)", requiresBack: false };
6673
6374
  }
6674
6375
  if (u2.includes("GHANA")) {
6675
- if (isDocumentFlow) return { icon: "\xF0\u0178\xAA\xAA", desc: "Capture or upload your Ghana Card", requiresBack: true };
6676
- return { icon: "\xF0\u0178\xAA\xAA", desc: "Enter your Ghana Card number", requiresBack: false };
6376
+ if (isDocumentFlow) return { icon: "🪪", desc: "Capture or upload your Ghana Card", requiresBack: true };
6377
+ return { icon: "🪪", desc: "Enter your Ghana Card number", requiresBack: false };
6677
6378
  }
6678
6379
  if (u2.includes("SSNIT"))
6679
- return { icon: "\xF0\u0178\x8F\u203A\xEF\xB8\x8F", desc: "Enter your SSNIT number", requiresBack: false };
6380
+ return { icon: "🏛️", desc: "Enter your SSNIT number", requiresBack: false };
6680
6381
  if (u2.includes("KENYA") || u2.includes("NATIONAL") && u2.includes("KENYA"))
6681
- return { icon: "\xF0\u0178\u2020\u201D", desc: "Enter your Kenya National ID number", requiresBack: false };
6382
+ return { icon: "🆔", desc: "Enter your Kenya National ID number", requiresBack: false };
6682
6383
  if (u2.includes("TIN") || u2.includes("TAX IDENTIFICATION"))
6683
- return { icon: "\xF0\u0178\xA7\xBE", desc: "Enter your Tax Identification Number (TIN)", requiresBack: false };
6384
+ return { icon: "🧾", desc: "Enter your Tax Identification Number (TIN)", requiresBack: false };
6684
6385
  if (u2.includes("CAC") || u2.includes("CORPORATE AFFAIRS") || u2.includes("COMPANY"))
6685
- return { icon: "\xF0\u0178\x8F\xA2", desc: advanced ? "Enter RC number, company name & type" : "Enter your company registration number", requiresBack: false };
6386
+ return { icon: "🏢", desc: advanced ? "Enter RC number, company name & type" : "Enter your company registration number", requiresBack: false };
6686
6387
  if (u2.includes("VOTER") || u2.includes("PVC")) {
6687
6388
  const requiresBack = requiresBackSide(typeName);
6688
6389
  return {
6689
- icon: "\xF0\u0178\u2014\xB3\xEF\xB8\x8F",
6390
+ icon: "🗳️",
6690
6391
  desc: isDocumentFlow ? `Capture or upload your voter's card${requiresBack ? " (front & back)" : ""}` : "Enter your voter ID number",
6691
6392
  requiresBack: isDocumentFlow ? requiresBack : false
6692
6393
  };
6693
6394
  }
6694
6395
  if (u2.includes("PASSPORT"))
6695
- return { icon: "\xF0\u0178\u201C\u02DC", desc: isDocumentFlow ? "Capture or upload your passport" : "Enter your passport number", requiresBack: false };
6396
+ return { icon: "📘", desc: isDocumentFlow ? "Capture or upload your passport" : "Enter your passport number", requiresBack: false };
6696
6397
  if (u2.includes("DRIVER") || u2.includes("LICENSE") || u2.includes("LICENCE")) {
6697
6398
  const requiresBack = requiresBackSide(typeName);
6698
6399
  return {
6699
- icon: "\xF0\u0178\u0161\u2014",
6400
+ icon: "🚗",
6700
6401
  desc: isDocumentFlow ? `Capture or upload your driver's license${requiresBack ? " (front & back)" : ""}` : "Enter your driver's license number",
6701
6402
  requiresBack: isDocumentFlow ? requiresBack : false
6702
6403
  };
@@ -6704,15 +6405,15 @@ var SmartComplySDK = (() => {
6704
6405
  if (u2.includes("NATIONAL ID") || u2.includes("NATIONAL") && u2.includes("CARD")) {
6705
6406
  const requiresBack = requiresBackSide(typeName);
6706
6407
  return {
6707
- icon: "\xF0\u0178\xAA\xAA",
6408
+ icon: "🪪",
6708
6409
  desc: isDocumentFlow ? `Capture or upload your ID card${requiresBack ? " (front & back)" : ""}` : "Enter your National ID number",
6709
6410
  requiresBack: isDocumentFlow ? requiresBack : false
6710
6411
  };
6711
6412
  }
6712
6413
  if (u2.includes("LIVENESS") || u2.includes("FACE COMPARISON") || u2.includes("FACE MATCH"))
6713
- return { icon: "\xF0\u0178\xA4\xB3", desc: "We'll capture a short selfie video to verify you", requiresBack: false };
6414
+ return { icon: "🤳", desc: "We'll capture a short selfie video to verify you", requiresBack: false };
6714
6415
  return {
6715
- icon: isDocumentFlow ? "\xF0\u0178\u201C\u201E" : "\xF0\u0178\u2020\u201D",
6416
+ icon: isDocumentFlow ? "📄" : "🆔",
6716
6417
  desc: isDocumentFlow ? "Capture or upload your document" : "Enter your ID number",
6717
6418
  requiresBack: false
6718
6419
  };
@@ -7124,7 +6825,7 @@ var SmartComplySDK = (() => {
7124
6825
  display:flex;align-items:center;justify-content:center;
7125
6826
  font-size:22px;
7126
6827
  `;
7127
- centre.textContent = "\u{1F6E1}\uFE0F";
6828
+ centre.textContent = "🛡️";
7128
6829
  spinnerWrap.appendChild(ring);
7129
6830
  spinnerWrap.appendChild(centre);
7130
6831
  container.appendChild(spinnerWrap);
@@ -7151,7 +6852,7 @@ var SmartComplySDK = (() => {
7151
6852
  background:linear-gradient(135deg,${this.theme.primary}22,${this.theme.primaryGlow});
7152
6853
  border:1.5px solid ${this.theme.primary}33;
7153
6854
  `;
7154
- iconWrap.textContent = isDocFlow ? "\xF0\u0178\xAA\xAA" : "\xF0\u0178\u203A\xA1\xEF\xB8\x8F";
6855
+ iconWrap.textContent = isDocFlow ? "🪪" : "🛡️";
7155
6856
  topRow.appendChild(iconWrap);
7156
6857
  const badge = document.createElement("div");
7157
6858
  badge.style.cssText = `
@@ -7162,7 +6863,7 @@ var SmartComplySDK = (() => {
7162
6863
  color:${this.theme.primary};
7163
6864
  border:1px solid ${this.theme.primary}30;
7164
6865
  `;
7165
- badge.textContent = isDocFlow ? "\xF0\u0178\u201C\u201E Document Verification" : "\xF0\u0178\u201D\x8D Data Verification";
6866
+ badge.textContent = isDocFlow ? "📄 Document Verification" : "🔍 Data Verification";
7166
6867
  topRow.appendChild(badge);
7167
6868
  container.appendChild(topRow);
7168
6869
  const titleWrap = document.createElement("div");
@@ -7184,13 +6885,13 @@ var SmartComplySDK = (() => {
7184
6885
  titleWrap.appendChild(desc);
7185
6886
  container.appendChild(titleWrap);
7186
6887
  const steps = isDocFlow ? [
7187
- { icon: "\xF0\u0178\xAA\xAA", text: "Choose your document type", sub: "Passport, National ID, Driver License" },
7188
- { icon: "\xF0\u0178\u201C\xB7", text: "Scan your document", sub: "Upload or capture a photo of your ID" },
7189
- { icon: "\xF0\u0178\xA4\xB3", text: "Face check", sub: "Quick selfie to confirm it's you" }
6888
+ { icon: "🪪", text: "Choose your document type", sub: "Passport, National ID, Driver License" },
6889
+ { icon: "📷", text: "Scan your document", sub: "Upload or capture a photo of your ID" },
6890
+ { icon: "🤳", text: "Face check", sub: "Quick selfie to confirm it's you" }
7190
6891
  ] : [
7191
- { icon: "\xF0\u0178\u2020\u201D", text: "Choose your ID type", sub: "NIN, BVN, Voter's Card and more" },
7192
- { icon: "\xF0\u0178\u201C\x9D", text: "Enter your ID details", sub: "Exactly as shown on your ID" },
7193
- { icon: "\xF0\u0178\xA4\xB3", text: "Face check", sub: "Quick selfie to confirm it's you" }
6892
+ { icon: "🆔", text: "Choose your ID type", sub: "NIN, BVN, Voter's Card and more" },
6893
+ { icon: "📝", text: "Enter your ID details", sub: "Exactly as shown on your ID" },
6894
+ { icon: "🤳", text: "Face check", sub: "Quick selfie to confirm it's you" }
7194
6895
  ];
7195
6896
  const stepsWrap = document.createElement("div");
7196
6897
  stepsWrap.style.cssText = `
@@ -7230,7 +6931,7 @@ var SmartComplySDK = (() => {
7230
6931
  stepsWrap.appendChild(row);
7231
6932
  });
7232
6933
  container.appendChild(stepsWrap);
7233
- const btn = this.createPrimaryButton("Get Started \xE2\u2020\u2019");
6934
+ const btn = this.createPrimaryButton("Get Started →");
7234
6935
  btn.style.cssText += "margin-top:2px;";
7235
6936
  btn.addEventListener("click", () => this.showStep("country"));
7236
6937
  container.appendChild(btn);
@@ -7239,7 +6940,7 @@ var SmartComplySDK = (() => {
7239
6940
  display:flex;align-items:center;gap:6px;
7240
6941
  color:${this.theme.textMuted};font-size:10.5px;
7241
6942
  `;
7242
- trust.textContent = "\xF0\u0178\u201D\u2019 End-to-end encrypted";
6943
+ trust.textContent = "🔒 End-to-end encrypted";
7243
6944
  container.appendChild(trust);
7244
6945
  }
7245
6946
  // ── Country Select ──────────────────────────────────────────────
@@ -7292,7 +6993,7 @@ var SmartComplySDK = (() => {
7292
6993
  const requiresBack = requiresBackFromBackend ?? info.requiresBack;
7293
6994
  let subtitle2 = info.desc;
7294
6995
  if (requiresBack && isThisDocFlow) {
7295
- subtitle2 += " \xE2\u0161\xA0\xEF\xB8\x8F Both sides required";
6996
+ subtitle2 += " ⚠️ Both sides required";
7296
6997
  }
7297
6998
  const displayName = typeName.replace(/\s*with\s+face\s*/gi, " ").replace(/\s*\(NIN\)\s*/gi, " ").replace(/\s*\(BVN\)\s*/gi, " ").replace(/\s{2,}/g, " ").trim();
7298
6999
  const card = this.createOptionCard(
@@ -7355,13 +7056,13 @@ var SmartComplySDK = (() => {
7355
7056
  { match: "BVN", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "BVN must be exactly 11 digits" } },
7356
7057
  { match: "NIN", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "NIN must be exactly 11 digits" } },
7357
7058
  { match: "NATIONAL IDENTITY", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "NIN must be exactly 11 digits" } },
7358
- { match: "PASSPORT", rule: { minLen: 6, maxLen: 20, alphanumeric: true, hint: "Passport number must be 6\xE2\u20AC\u201C20 alphanumeric characters" } },
7359
- { match: "DRIVER", rule: { minLen: 3, maxLen: 30, alphanumeric: true, hint: "Licence number must be 3\xE2\u20AC\u201C30 alphanumeric characters" } },
7360
- { match: "VOTER", rule: { minLen: 6, maxLen: 30, alphanumeric: true, hint: "Voter ID must be 6\xE2\u20AC\u201C30 alphanumeric characters" } },
7361
- { match: "GHANA", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "Ghana Card number must be 8\xE2\u20AC\u201C20 alphanumeric characters" } },
7362
- { match: "SSNIT", rule: { minLen: 7, maxLen: 15, alphanumeric: true, hint: "SSNIT number must be 7\xE2\u20AC\u201C15 alphanumeric characters" } },
7363
- { match: "TIN", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "TIN must be 8\xE2\u20AC\u201C20 characters" } },
7364
- { match: "KRA", rule: { minLen: 10, maxLen: 12, alphanumeric: true, hint: "KRA PIN must be 10\xE2\u20AC\u201C12 characters" } }
7059
+ { match: "PASSPORT", rule: { minLen: 6, maxLen: 20, alphanumeric: true, hint: "Passport number must be 6–20 alphanumeric characters" } },
7060
+ { match: "DRIVER", rule: { minLen: 3, maxLen: 30, alphanumeric: true, hint: "Licence number must be 3–30 alphanumeric characters" } },
7061
+ { match: "VOTER", rule: { minLen: 6, maxLen: 30, alphanumeric: true, hint: "Voter ID must be 6–30 alphanumeric characters" } },
7062
+ { match: "GHANA", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "Ghana Card number must be 8–20 alphanumeric characters" } },
7063
+ { match: "SSNIT", rule: { minLen: 7, maxLen: 15, alphanumeric: true, hint: "SSNIT number must be 7–15 alphanumeric characters" } },
7064
+ { match: "TIN", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "TIN must be 8–20 characters" } },
7065
+ { match: "KRA", rule: { minLen: 10, maxLen: 12, alphanumeric: true, hint: "KRA PIN must be 10–12 characters" } }
7365
7066
  ];
7366
7067
  function getFieldRule(label) {
7367
7068
  const upper = label.toUpperCase();
@@ -7647,7 +7348,7 @@ var SmartComplySDK = (() => {
7647
7348
  this._prewarmCamera();
7648
7349
  const iconWrap = document.createElement("div");
7649
7350
  iconWrap.style.cssText = `width:64px;height:64px;border-radius:50%;background:${this.theme.primary}15;border:2px solid ${this.theme.primary}30;display:flex;align-items:center;justify-content:center;font-size:28px;`;
7650
- iconWrap.textContent = "\xF0\u0178\u203A\xA1\xEF\xB8\x8F";
7351
+ iconWrap.textContent = "🛡️";
7651
7352
  container.appendChild(iconWrap);
7652
7353
  const title = document.createElement("h2");
7653
7354
  title.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
@@ -7684,7 +7385,7 @@ var SmartComplySDK = (() => {
7684
7385
  sub.textContent = "Your details have been confirmed. Please continue to the face check.";
7685
7386
  container.appendChild(sub);
7686
7387
  }
7687
- const yesBtn = this.createPrimaryButton("Yes, that's me \xE2\u20AC\u201D continue");
7388
+ const yesBtn = this.createPrimaryButton("Yes, that's me — continue");
7688
7389
  yesBtn.addEventListener("click", () => {
7689
7390
  this._confirmRetries = 0;
7690
7391
  this.showStep("liveness");
@@ -7711,7 +7412,7 @@ var SmartComplySDK = (() => {
7711
7412
  this._prewarmCamera();
7712
7413
  const iconWrap = document.createElement("div");
7713
7414
  iconWrap.style.cssText = `width:64px;height:64px;border-radius:50%;background:${this.theme.primary}15;border:2px solid ${this.theme.primary}30;display:flex;align-items:center;justify-content:center;font-size:28px;`;
7714
- iconWrap.textContent = "\xF0\u0178\u201C\u201E";
7415
+ iconWrap.textContent = "📄";
7715
7416
  container.appendChild(iconWrap);
7716
7417
  const title = document.createElement("h2");
7717
7418
  title.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
@@ -7734,7 +7435,7 @@ var SmartComplySDK = (() => {
7734
7435
  if (this._ocrExpirySoon && !this._ocrIsExpired) {
7735
7436
  const warn = document.createElement("div");
7736
7437
  warn.style.cssText = `width:100%;padding:10px 14px;border-radius:10px;background:${this.theme.warning}18;border:1px solid ${this.theme.warning}40;color:${this.theme.warning};font-size:12px;text-align:left;line-height:1.5;`;
7737
- warn.textContent = "\xE2\u0161\xA0\xEF\xB8\x8F Your document expires soon. Some services may not accept it, but you can still continue.";
7438
+ warn.textContent = "⚠️ Your document expires soon. Some services may not accept it, but you can still continue.";
7738
7439
  container.appendChild(warn);
7739
7440
  }
7740
7441
  const card = document.createElement("div");
@@ -7797,18 +7498,18 @@ var SmartComplySDK = (() => {
7797
7498
  ring.style.cssText = `position:absolute;inset:0;border-radius:50%;border:3px solid ${this.theme.border};border-top-color:${this.theme.primary};animation:sc-spin 0.9s linear infinite;`;
7798
7499
  const centre = document.createElement("div");
7799
7500
  centre.style.cssText = "position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;";
7800
- centre.textContent = "\xF0\u0178\u201C\u201E";
7501
+ centre.textContent = "📄";
7801
7502
  spinnerWrap.appendChild(ring);
7802
7503
  spinnerWrap.appendChild(centre);
7803
7504
  container.appendChild(spinnerWrap);
7804
7505
  const statusEl = document.createElement("div");
7805
7506
  statusEl.setAttribute("aria-live", "polite");
7806
7507
  statusEl.style.cssText = `color:${this.theme.text};font-size:15px;font-weight:600;`;
7807
- statusEl.textContent = "Reading your document\xE2\u20AC\xA6";
7508
+ statusEl.textContent = "Reading your document…";
7808
7509
  container.appendChild(statusEl);
7809
7510
  const subEl = document.createElement("div");
7810
7511
  subEl.style.cssText = `color:${this.theme.textSecondary};font-size:12px;max-width:260px;line-height:1.5;`;
7811
- subEl.textContent = "This usually takes 5\xE2\u20AC\u201C15 seconds. Please wait.";
7512
+ subEl.textContent = "This usually takes 5–15 seconds. Please wait.";
7812
7513
  container.appendChild(subEl);
7813
7514
  if (!this.livenessEntryId) {
7814
7515
  this.showErrorStep("Document upload failed. Please try again.");
@@ -7853,8 +7554,8 @@ var SmartComplySDK = (() => {
7853
7554
  if (polls < MAX_POLLS && !this.isDestroyed) {
7854
7555
  this._ocrPollTimer = setTimeout(poll, 2e3);
7855
7556
  } else if (!this.isDestroyed) {
7856
- statusEl.textContent = "Taking longer than usual\xE2\u20AC\xA6";
7857
- subEl.textContent = "We'll continue \xE2\u20AC\u201D you can confirm your details on the next screen.";
7557
+ statusEl.textContent = "Taking longer than usual…";
7558
+ subEl.textContent = "We'll continue — you can confirm your details on the next screen.";
7858
7559
  this._ocrPollTimer = setTimeout(() => {
7859
7560
  if (!this.isDestroyed) this.showStep("document_confirm");
7860
7561
  }, 2e3);
@@ -7873,7 +7574,7 @@ var SmartComplySDK = (() => {
7873
7574
  container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:20px 0;";
7874
7575
  const icon = document.createElement("div");
7875
7576
  icon.style.cssText = `width:72px;height:72px;border-radius:50%;background:${this.theme.errorBg};border:3px solid ${this.theme.error};display:flex;align-items:center;justify-content:center;font-size:28px;`;
7876
- icon.textContent = "\xF0\u0178\u201C\u2026";
7577
+ icon.textContent = "📅";
7877
7578
  container.appendChild(icon);
7878
7579
  const t2 = document.createElement("h2");
7879
7580
  t2.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
@@ -7902,7 +7603,7 @@ var SmartComplySDK = (() => {
7902
7603
  container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:20px 0;";
7903
7604
  const icon = document.createElement("div");
7904
7605
  icon.style.cssText = `width:72px;height:72px;border-radius:50%;background:${this.theme.errorBg};border:3px solid ${this.theme.error};display:flex;align-items:center;justify-content:center;font-size:28px;`;
7905
- icon.textContent = "\xF0\u0178\u201D\x8D";
7606
+ icon.textContent = "🔍";
7906
7607
  container.appendChild(icon);
7907
7608
  const t2 = document.createElement("h2");
7908
7609
  t2.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
@@ -7935,20 +7636,19 @@ var SmartComplySDK = (() => {
7935
7636
  });
7936
7637
  const iconWrap = document.createElement("div");
7937
7638
  iconWrap.style.cssText = `width:80px;height:80px;border-radius:50%;background:${this.theme.successBg};border:3px solid ${this.theme.success};display:flex;align-items:center;justify-content:center;font-size:36px;font-weight:700;color:${this.theme.success};animation:sc-pop 0.4s ease;`;
7938
- iconWrap.textContent = "\xE2\u0153\u201C";
7639
+ iconWrap.textContent = "✓";
7939
7640
  container.appendChild(iconWrap);
7940
7641
  const title = document.createElement("h2");
7941
7642
  title.style.cssText = `color:${this.theme.text};font-size:22px;font-weight:700;margin:0;`;
7942
7643
  title.textContent = "You're all done!";
7943
7644
  container.appendChild(title);
7944
- const brandName = this.sdkConfig?.brand_name || "the service";
7945
7645
  const desc = document.createElement("p");
7946
7646
  desc.style.cssText = `color:${this.theme.textSecondary};font-size:14px;line-height:1.6;margin:0;max-width:300px;`;
7947
- desc.textContent = `Your verification is complete. ${brandName} will review and be in touch shortly.`;
7647
+ desc.textContent = "Your verification is complete.";
7948
7648
  container.appendChild(desc);
7949
7649
  const info = document.createElement("div");
7950
7650
  info.style.cssText = `color:${this.theme.textMuted};font-size:11px;display:flex;align-items:center;gap:4px;`;
7951
- info.textContent = "\xF0\u0178\u201D\u2019 Your data is encrypted and secure";
7651
+ info.textContent = "🔒 Your data is encrypted and secure";
7952
7652
  container.appendChild(info);
7953
7653
  const doneBtn = this.createPrimaryButton("Close");
7954
7654
  doneBtn.addEventListener("click", () => {
@@ -7961,7 +7661,7 @@ var SmartComplySDK = (() => {
7961
7661
  container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:24px 0;";
7962
7662
  const icon = document.createElement("div");
7963
7663
  icon.style.cssText = `width:72px;height:72px;border-radius:50%;background:${this.theme.errorBg};border:3px solid ${this.theme.error};display:flex;align-items:center;justify-content:center;font-size:28px;`;
7964
- icon.textContent = "\xE2\u0161\xA0\xEF\xB8\x8F";
7664
+ icon.textContent = "⚠️";
7965
7665
  container.appendChild(icon);
7966
7666
  const t2 = document.createElement("h2");
7967
7667
  t2.style.cssText = `color:${this.theme.text};font-size:18px;font-weight:700;margin:0;`;
@@ -8017,7 +7717,7 @@ var SmartComplySDK = (() => {
8017
7717
  <div style="display:flex;flex-direction:column;align-items:center;gap:14px;padding:28px 20px;">
8018
7718
  <div style="position:relative;width:52px;height:52px;">
8019
7719
  <div style="position:absolute;inset:0;border-radius:50%;border:3px solid ${this.theme.border};border-top-color:${this.theme.primary};animation:sc-spin 0.8s linear infinite;"></div>
8020
- <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;">\xF0\u0178\u201D\x8D</div>
7720
+ <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;">🔍</div>
8021
7721
  </div>
8022
7722
  <div style="color:${this.theme.text};font-size:14px;font-weight:600;">Scanning document...</div>
8023
7723
  <div style="color:${this.theme.textSecondary};font-size:12px;text-align:center;max-width:200px;line-height:1.5;">Running OCR and verifying your identity</div>
@@ -8040,7 +7740,7 @@ var SmartComplySDK = (() => {
8040
7740
  identifier,
8041
7741
  identifier_type: this.selectedIdType,
8042
7742
  country,
8043
- challenge_actions: ["BLINK", "TURN_HEAD", "OPEN_MOUTH"],
7743
+ challenge_actions: ["BLINK"],
8044
7744
  document: blob,
8045
7745
  document_back: this.documentBackBlob || void 0,
8046
7746
  device_type: deviceMeta.device_type,
@@ -8075,7 +7775,7 @@ var SmartComplySDK = (() => {
8075
7775
  <div style="display:flex;flex-direction:column;align-items:center;gap:16px;padding:28px 20px;text-align:center;">
8076
7776
  <div style="position:relative;width:52px;height:52px;">
8077
7777
  <div style="position:absolute;inset:0;border-radius:50%;border:3px solid ${this.theme.border};border-top-color:${this.theme.primary};animation:sc-spin 0.8s linear infinite;"></div>
8078
- <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;">\xF0\u0178\u201D\u201E</div>
7778
+ <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;">🔄</div>
8079
7779
  </div>
8080
7780
  <div style="color:${this.theme.text};font-size:14px;font-weight:600;">Retrying...</div>
8081
7781
  <div style="color:${this.theme.textSecondary};font-size:12px;max-width:240px;line-height:1.5;">Attempt ${retryCount} of ${maxRetries}</div>
@@ -8116,7 +7816,7 @@ var SmartComplySDK = (() => {
8116
7816
  wrap.setAttribute("role", "alert");
8117
7817
  const icon = document.createElement("div");
8118
7818
  icon.style.cssText = `width:56px;height:56px;border-radius:50%;background:${this.theme.errorBg};border:3px solid ${this.theme.error};display:flex;align-items:center;justify-content:center;font-size:26px;`;
8119
- icon.textContent = "\xF0\u0178\u201D\x8D";
7819
+ icon.textContent = "🔍";
8120
7820
  wrap.appendChild(icon);
8121
7821
  const titleEl = document.createElement("div");
8122
7822
  titleEl.style.cssText = `color:${this.theme.text};font-size:14px;font-weight:700;`;
@@ -8182,7 +7882,7 @@ var SmartComplySDK = (() => {
8182
7882
  // B4: pass pre-warmed stream so camera.open() is skipped
8183
7883
  prewarmedStream: this._prewarmedStream || void 0
8184
7884
  },
8185
- ["BLINK", "TURN_HEAD", "OPEN_MOUTH"],
7885
+ ["BLINK"],
8186
7886
  this.livenessEntryId || void 0
8187
7887
  ).then((result) => {
8188
7888
  if (result.status === "failed") {
@@ -8216,7 +7916,7 @@ var SmartComplySDK = (() => {
8216
7916
  const isDocQuality = /blurry|blur|quality|glare|dark|bright|lighting/i.test(message);
8217
7917
  const isExpiredDoc = /document.*expired|expired.*document/i.test(message);
8218
7918
  const isFaceMatch = /face.*match|selfie.*match|does not match/i.test(message);
8219
- const errorIcon = isCameraErr ? "\xF0\u0178\u201C\xB7" : isNetworkErr ? "\xF0\u0178\u201C\xA1" : isAuthErr ? "\xE2\x8F\xB1" : isDocQuality ? "\xF0\u0178\u201D\x8D" : isExpiredDoc ? "\xF0\u0178\u201C\u2026" : isFaceMatch ? "\xF0\u0178\xA4\xB3" : "\xE2\u0153\u2022";
7919
+ const errorIcon = isCameraErr ? "📷" : isNetworkErr ? "📡" : isAuthErr ? "⏱" : isDocQuality ? "🔍" : isExpiredDoc ? "📅" : isFaceMatch ? "🤳" : "✕";
8220
7920
  const errorTitle = isCameraErr ? "Camera Access Required" : isNetworkErr ? "Connection Problem" : isAuthErr ? "Session Expired" : isDocQuality ? "Document Photo Unclear" : isExpiredDoc ? "Document Expired" : isFaceMatch ? "Face Match Failed" : "Verification Unsuccessful";
8221
7921
  const userMessage = isCameraErr ? "Please allow camera access in your browser settings and try again." : isNetworkErr ? "Check your internet connection and try again." : isAuthErr ? "Your session has expired. Please start again." : isDocQuality ? _sanitizeUserMessage(message) || "Please retake your document photo in good lighting with no glare." : isExpiredDoc ? "The document you submitted has expired. Please use a valid, current ID." : isFaceMatch ? "Your selfie did not match the photo on your ID. Please ensure you are using your own valid document and face the camera directly." : _sanitizeUserMessage(message) || "We were unable to complete your verification. Please try again.";
8222
7922
  const iconWrap = document.createElement("div");
@@ -8321,7 +8021,7 @@ var SmartComplySDK = (() => {
8321
8021
  stepIndicator.appendChild(this.stepLabel);
8322
8022
  header.appendChild(stepIndicator);
8323
8023
  const closeBtn = document.createElement("button");
8324
- closeBtn.innerHTML = "\xE2\u0153\u2022";
8024
+ closeBtn.innerHTML = "✕";
8325
8025
  closeBtn.setAttribute("aria-label", "Close identity verification");
8326
8026
  closeBtn.style.cssText = `
8327
8027
  width:32px;height:32px;border-radius:8px;flex-shrink:0;
@@ -8402,7 +8102,7 @@ var SmartComplySDK = (() => {
8402
8102
  }
8403
8103
  const arrow = document.createElement("span");
8404
8104
  arrow.style.cssText = `color:${this.theme.textMuted};font-size:18px;flex-shrink:0;`;
8405
- arrow.textContent = "\xE2\u2020\u2019";
8105
+ arrow.textContent = "→";
8406
8106
  card.appendChild(textWrap);
8407
8107
  card.appendChild(arrow);
8408
8108
  return card;
@@ -8474,7 +8174,7 @@ var SmartComplySDK = (() => {
8474
8174
  if (this.currentStep === "loading" || this.currentStep === "welcome") {
8475
8175
  this.stepLabel.textContent = "";
8476
8176
  } else if (this.currentStep === "done" || this.currentStep === "result") {
8477
- this.stepLabel.textContent = "Complete \xE2\u0153\u201C";
8177
+ this.stepLabel.textContent = "Complete ✓";
8478
8178
  } else if (stage2Steps.includes(this.currentStep)) {
8479
8179
  this.stepLabel.textContent = "Stage 2 of 2: Face Check";
8480
8180
  } else if (stage1Steps.includes(this.currentStep)) {
@@ -8588,20 +8288,20 @@ var SmartComplySDK = (() => {
8588
8288
  /** Emit an informational message (debug mode only). */
8589
8289
  info(message, ...args) {
8590
8290
  if (!isDebugEnabled()) return;
8591
- console.info(`${PREFIX} \u2139 ${message}`, ...args);
8291
+ console.info(`${PREFIX} ℹ ${message}`, ...args);
8592
8292
  },
8593
8293
  /** Emit a warning (always visible). */
8594
8294
  warn(message, ...args) {
8595
- console.warn(`${PREFIX} \u26A0 ${message}`, ...args);
8295
+ console.warn(`${PREFIX} ⚠ ${message}`, ...args);
8596
8296
  },
8597
8297
  /** Emit an error (always visible). */
8598
8298
  error(message, ...args) {
8599
- console.error(`${PREFIX} \u2716 ${message}`, ...args);
8299
+ console.error(`${PREFIX} ✖ ${message}`, ...args);
8600
8300
  },
8601
8301
  /** Emit a debug trace (debug mode only — lowest priority). */
8602
8302
  debug(message, ...args) {
8603
8303
  if (!isDebugEnabled()) return;
8604
- console.debug(`${PREFIX} \u2B21 ${message}`, ...args);
8304
+ console.debug(`${PREFIX} ⬡ ${message}`, ...args);
8605
8305
  },
8606
8306
  /** Log a timed group of related messages (debug mode only). */
8607
8307
  group(label, fn2) {
@@ -8759,7 +8459,7 @@ var SmartComplySDK = (() => {
8759
8459
  }
8760
8460
  };
8761
8461
  xhr.onerror = async () => {
8762
- SDKLogger.warn(`Upload to ${path} failed \u2014 network error`, { retriesLeft });
8462
+ SDKLogger.warn(`Upload to ${path} failed — network error`, { retriesLeft });
8763
8463
  if (retriesLeft > 0) {
8764
8464
  await new Promise((r2) => setTimeout(r2, RETRY_DELAY_MS));
8765
8465
  try {