smartcomply-web-sdk 1.0.52 → 1.0.56

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.
@@ -4521,6 +4521,20 @@ var SmartComplySDK = (() => {
4521
4521
  0%,100% { opacity:0.3; transform:scale(0.85); }
4522
4522
  50% { opacity:1; transform:scale(1); }
4523
4523
  }
4524
+ @keyframes sc-scanline-sweep {
4525
+ 0% { transform:translateY(-4%); opacity:0; }
4526
+ 8% { opacity:0.9; }
4527
+ 92% { opacity:0.9; }
4528
+ 100% { transform:translateY(104%); opacity:0; }
4529
+ }
4530
+ @keyframes sc-corner-pulse {
4531
+ 0%,100% { opacity:0.55; }
4532
+ 50% { opacity:1; }
4533
+ }
4534
+ @keyframes sc-oval-breathe {
4535
+ 0%,100% { opacity:0.5; }
4536
+ 50% { opacity:0.85; }
4537
+ }
4524
4538
  `;
4525
4539
  document.head.appendChild(s2);
4526
4540
  }
@@ -4530,11 +4544,15 @@ var SmartComplySDK = (() => {
4530
4544
  this.videoEl = null;
4531
4545
  this.instructionEl = null;
4532
4546
  this.confidenceRingEl = null;
4547
+ this.bgOvalEl = null;
4548
+ this.guideWrapEl = null;
4549
+ this.scanlineEl = null;
4533
4550
  this.overlayEl = null;
4534
4551
  this.dotsEl = null;
4535
4552
  this.docCaptureRoot = null;
4536
4553
  this.scanStartMs = 0;
4537
4554
  this.scanDurationMs = 0;
4555
+ this.faceTracked = false;
4538
4556
  }
4539
4557
  mount(container, challenge) {
4540
4558
  injectLivenessKeyframes();
@@ -4552,41 +4570,110 @@ var SmartComplySDK = (() => {
4552
4570
  this.videoEl.muted = true;
4553
4571
  this.videoEl.style.cssText = `
4554
4572
  width:100%;height:100%;display:block;
4573
+ filter:brightness(1.03) contrast(1.03);
4555
4574
  transform:scaleX(-1);object-fit:cover;
4575
+ `;
4576
+ const vignette = document.createElement("div");
4577
+ vignette.style.cssText = `
4578
+ position:absolute;inset:0;pointer-events:none;
4579
+ background:radial-gradient(ellipse 34% 44% at 50% 47%, transparent 55%, rgba(0,0,0,0.55) 100%);
4556
4580
  `;
4557
4581
  const guideWrap = document.createElement("div");
4558
4582
  guideWrap.style.cssText = `
4559
4583
  position:absolute;top:50%;left:50%;
4560
4584
  transform:translate(-50%,-52%);
4561
4585
  width:64%;aspect-ratio:140/180;pointer-events:none;
4586
+ filter:drop-shadow(0 0 18px rgba(59,130,246,0.35));
4587
+ transition:filter 0.3s ease;
4562
4588
  `;
4589
+ this.guideWrapEl = guideWrap;
4563
4590
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
4564
4591
  svg.setAttribute("viewBox", "0 0 140 180");
4565
- svg.style.cssText = "width:100%;height:100%;";
4592
+ svg.style.cssText = "width:100%;height:100%;overflow:visible;";
4593
+ const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
4594
+ const clip = document.createElementNS("http://www.w3.org/2000/svg", "clipPath");
4595
+ clip.setAttribute("id", "sc-oval-clip");
4596
+ const clipOval = document.createElementNS("http://www.w3.org/2000/svg", "ellipse");
4597
+ clipOval.setAttribute("cx", "70");
4598
+ clipOval.setAttribute("cy", "90");
4599
+ clipOval.setAttribute("rx", "64");
4600
+ clipOval.setAttribute("ry", "86");
4601
+ clip.appendChild(clipOval);
4602
+ defs.appendChild(clip);
4603
+ svg.appendChild(defs);
4604
+ const trackOval = document.createElementNS("http://www.w3.org/2000/svg", "ellipse");
4605
+ trackOval.setAttribute("cx", "70");
4606
+ trackOval.setAttribute("cy", "90");
4607
+ trackOval.setAttribute("rx", "58");
4608
+ trackOval.setAttribute("ry", "80");
4609
+ trackOval.setAttribute("fill", "none");
4610
+ trackOval.setAttribute("stroke", "rgba(255,255,255,0.16)");
4611
+ trackOval.setAttribute("stroke-width", "1");
4612
+ trackOval.setAttribute("stroke-dasharray", "2 5");
4613
+ svg.appendChild(trackOval);
4566
4614
  const bgOval = document.createElementNS("http://www.w3.org/2000/svg", "ellipse");
4567
4615
  bgOval.setAttribute("cx", "70");
4568
4616
  bgOval.setAttribute("cy", "90");
4569
4617
  bgOval.setAttribute("rx", "64");
4570
4618
  bgOval.setAttribute("ry", "86");
4571
4619
  bgOval.setAttribute("fill", "none");
4572
- bgOval.setAttribute("stroke", "rgba(255,255,255,0.35)");
4573
- bgOval.setAttribute("stroke-width", "2");
4620
+ bgOval.setAttribute("stroke", "rgba(255,255,255,0.4)");
4621
+ bgOval.setAttribute("stroke-width", "1.5");
4622
+ bgOval.style.cssText = "animation:sc-oval-breathe 2.4s ease-in-out infinite;transition:stroke 0.3s ease;";
4623
+ this.bgOvalEl = bgOval;
4574
4624
  const confOval = document.createElementNS("http://www.w3.org/2000/svg", "ellipse");
4575
4625
  confOval.setAttribute("cx", "70");
4576
4626
  confOval.setAttribute("cy", "90");
4577
4627
  confOval.setAttribute("rx", "64");
4578
4628
  confOval.setAttribute("ry", "86");
4579
4629
  confOval.setAttribute("fill", "none");
4580
- confOval.setAttribute("stroke", "#22c55e");
4581
- confOval.setAttribute("stroke-width", "3");
4630
+ confOval.setAttribute("stroke", "#3b82f6");
4631
+ confOval.setAttribute("stroke-width", "2.5");
4632
+ confOval.setAttribute("stroke-linecap", "round");
4582
4633
  const circ = 2 * Math.PI * Math.sqrt((64 * 64 + 86 * 86) / 2);
4583
4634
  confOval.setAttribute("stroke-dasharray", `${circ}`);
4584
4635
  confOval.setAttribute("stroke-dashoffset", `${circ}`);
4585
- confOval.style.cssText = "transition:stroke-dashoffset 0.2s linear;transform:rotate(-90deg);transform-origin:70px 90px;";
4636
+ confOval.style.cssText = "transition:stroke-dashoffset 0.2s linear,stroke 0.3s ease;transform:rotate(-90deg);transform-origin:70px 90px;";
4586
4637
  this.confidenceRingEl = confOval;
4638
+ const bracketGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
4639
+ bracketGroup.setAttribute("stroke", "#3b82f6");
4640
+ bracketGroup.setAttribute("stroke-width", "3");
4641
+ bracketGroup.setAttribute("stroke-linecap", "round");
4642
+ bracketGroup.setAttribute("fill", "none");
4643
+ bracketGroup.style.cssText = "animation:sc-corner-pulse 2s ease-in-out infinite;";
4644
+ const bracketLen = 14;
4645
+ const bx0 = 70 - 64 * 0.74, bx1 = 70 + 64 * 0.74;
4646
+ const by0 = 90 - 86 * 0.82, by1 = 90 + 86 * 0.82;
4647
+ const brackets = [
4648
+ `M${bx0},${by0 + bracketLen} L${bx0},${by0} L${bx0 + bracketLen},${by0}`,
4649
+ `M${bx1 - bracketLen},${by0} L${bx1},${by0} L${bx1},${by0 + bracketLen}`,
4650
+ `M${bx0},${by1 - bracketLen} L${bx0},${by1} L${bx0 + bracketLen},${by1}`,
4651
+ `M${bx1 - bracketLen},${by1} L${bx1},${by1} L${bx1},${by1 - bracketLen}`
4652
+ ];
4653
+ for (const d2 of brackets) {
4654
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
4655
+ path.setAttribute("d", d2);
4656
+ bracketGroup.appendChild(path);
4657
+ }
4587
4658
  svg.appendChild(bgOval);
4588
4659
  svg.appendChild(confOval);
4660
+ svg.appendChild(bracketGroup);
4589
4661
  guideWrap.appendChild(svg);
4662
+ const scanlineClipWrap = document.createElement("div");
4663
+ scanlineClipWrap.style.cssText = `
4664
+ position:absolute;inset:0;
4665
+ clip-path:ellipse(45.7% 47.8% at 50% 50%);
4666
+ overflow:hidden;
4667
+ `;
4668
+ const scanline = document.createElement("div");
4669
+ scanline.style.cssText = `
4670
+ position:absolute;left:0;right:0;height:26%;top:0;
4671
+ background:linear-gradient(180deg,transparent 0%,rgba(59,130,246,0.32) 45%,rgba(147,197,253,0.55) 50%,rgba(59,130,246,0.32) 55%,transparent 100%);
4672
+ animation:sc-scanline-sweep 2.6s ease-in-out infinite;
4673
+ `;
4674
+ this.scanlineEl = scanline;
4675
+ scanlineClipWrap.appendChild(scanline);
4676
+ guideWrap.appendChild(scanlineClipWrap);
4590
4677
  const camTop = document.createElement("div");
4591
4678
  camTop.style.cssText = `
4592
4679
  position:absolute;top:0;left:0;right:0;
@@ -4610,13 +4697,14 @@ var SmartComplySDK = (() => {
4610
4697
  for (let i2 = 0; i2 < 3; i2++) {
4611
4698
  const dot = document.createElement("div");
4612
4699
  dot.style.cssText = `
4613
- width:6px;height:6px;border-radius:50%;background:#22c55e;
4700
+ width:6px;height:6px;border-radius:50%;background:#3b82f6;
4614
4701
  animation:sc-scan-dot 1.1s ease-in-out ${i2 * 0.18}s infinite;
4615
4702
  `;
4616
4703
  this.dotsEl.appendChild(dot);
4617
4704
  }
4618
4705
  camBottom.appendChild(this.dotsEl);
4619
4706
  this.root.appendChild(this.videoEl);
4707
+ this.root.appendChild(vignette);
4620
4708
  this.root.appendChild(guideWrap);
4621
4709
  this.root.appendChild(camTop);
4622
4710
  this.root.appendChild(camBottom);
@@ -4650,6 +4738,25 @@ var SmartComplySDK = (() => {
4650
4738
  this.confidenceRingEl.setAttribute("stroke-dashoffset", `${circ * (1 - pct)}`);
4651
4739
  this.confidenceRingEl.setAttribute("stroke", livenessConfirmed ? "#22c55e" : "#3b82f6");
4652
4740
  }
4741
+ /**
4742
+ * Reflects whether a face is currently detected in-frame — brightens the
4743
+ * oval guide's glow/brackets when locked on, and dims the scan-line sweep
4744
+ * so the UI visibly distinguishes "searching" from "actively scanning".
4745
+ */
4746
+ setFaceTracking(tracked) {
4747
+ if (this.faceTracked === tracked) return;
4748
+ this.faceTracked = tracked;
4749
+ if (this.guideWrapEl) {
4750
+ this.guideWrapEl.style.filter = tracked ? "drop-shadow(0 0 22px rgba(34,197,94,0.45))" : "drop-shadow(0 0 18px rgba(59,130,246,0.35))";
4751
+ }
4752
+ if (this.bgOvalEl) {
4753
+ this.bgOvalEl.setAttribute("stroke", tracked ? "rgba(74,222,128,0.55)" : "rgba(255,255,255,0.4)");
4754
+ }
4755
+ if (this.scanlineEl) {
4756
+ this.scanlineEl.style.animationPlayState = tracked ? "running" : "paused";
4757
+ this.scanlineEl.style.opacity = tracked ? "1" : "0.4";
4758
+ }
4759
+ }
4653
4760
  updateInstruction(text) {
4654
4761
  if (this.instructionEl) this.instructionEl.textContent = text;
4655
4762
  }
@@ -4791,6 +4898,10 @@ var SmartComplySDK = (() => {
4791
4898
  this.confidenceRingEl = null;
4792
4899
  this.overlayEl = null;
4793
4900
  this.dotsEl = null;
4901
+ this.bgOvalEl = null;
4902
+ this.guideWrapEl = null;
4903
+ this.scanlineEl = null;
4904
+ this.faceTracked = false;
4794
4905
  }
4795
4906
  };
4796
4907
 
@@ -4984,6 +5095,7 @@ var SmartComplySDK = (() => {
4984
5095
  }
4985
5096
  if (videoForDetection.readyState >= 2) {
4986
5097
  const detection = faceEngine.detect(videoForDetection, performance.now());
5098
+ ui2.setFaceTracking(detection.faceDetected);
4987
5099
  if (detection.faceDetected) {
4988
5100
  stableFrames++;
4989
5101
  missedFrames = 0;
@@ -5058,6 +5170,7 @@ var SmartComplySDK = (() => {
5058
5170
  const detection = faceEngine.detect(videoEl, performance.now());
5059
5171
  const scan = actionDetector.check(detection);
5060
5172
  ui2.updateScanProgress(scan.livenessConfirmed);
5173
+ ui2.setFaceTracking(detection.faceDetected);
5061
5174
  if (!detection.faceDetected) {
5062
5175
  ui2.updateInstruction("Move your face into the oval");
5063
5176
  } else {
@@ -6423,7 +6536,6 @@ var SmartComplySDK = (() => {
6423
6536
  "traceback",
6424
6537
  "str(e)",
6425
6538
  "error:",
6426
- "failed:",
6427
6539
  "django",
6428
6540
  "python",
6429
6541
  "http ",
@@ -6435,11 +6547,19 @@ var SmartComplySDK = (() => {
6435
6547
  "confidence_percentage",
6436
6548
  "threshold_percentage",
6437
6549
  "polling_fallback",
6438
- "resolved_by"
6550
+ "resolved_by",
6551
+ // Billing/wallet messages are an operator problem (fund the account,
6552
+ // configure billing) — never the end user's to act on.
6553
+ "wallet",
6554
+ "fund your wallet",
6555
+ "billing is not configured",
6556
+ "insufficient balance",
6557
+ "insufficient fund"
6439
6558
  ];
6440
6559
  for (const blocked of blocklist) {
6441
6560
  if (lower.includes(blocked)) return null;
6442
6561
  }
6562
+ if (/failed:\S/i.test(raw)) return null;
6443
6563
  if (/[a-z]+_[a-z]+/.test(raw) && !/\s/.test(raw.trim())) return null;
6444
6564
  if (raw.trim()[0] === raw.trim()[0].toLowerCase() && /^[a-z]/.test(raw.trim())) return null;
6445
6565
  return raw.trim();
@@ -7048,7 +7168,12 @@ var SmartComplySDK = (() => {
7048
7168
  { match: "GHANA", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "Ghana Card number must be 8–20 alphanumeric characters" } },
7049
7169
  { match: "SSNIT", rule: { minLen: 7, maxLen: 15, alphanumeric: true, hint: "SSNIT number must be 7–15 alphanumeric characters" } },
7050
7170
  { match: "TIN", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "TIN must be 8–20 characters" } },
7051
- { match: "KRA", rule: { minLen: 10, maxLen: 12, alphanumeric: true, hint: "KRA PIN must be 10–12 characters" } }
7171
+ { match: "KRA", rule: { minLen: 10, maxLen: 12, alphanumeric: true, hint: "KRA PIN must be 10–12 characters" } },
7172
+ // Backend accepts DD-MM-YYYY, DD/MM/YYYY, YYYY-MM-DD or YYYY/MM/DD
7173
+ // (see _normalize_date_str in ivs_run_check.py) — DD-MM-YYYY shown here
7174
+ // since it matches the date format printed on NIN/BVN records.
7175
+ { match: "DATE OF BIRTH", rule: { pattern: /^\d{2}[-/]\d{2}[-/]\d{4}$|^\d{4}[-/]\d{2}[-/]\d{2}$/, hint: "Format: DD-MM-YYYY (e.g. 15-06-1996)", placeholder: "DD-MM-YYYY" } },
7176
+ { match: "DOB", rule: { pattern: /^\d{2}[-/]\d{2}[-/]\d{4}$|^\d{4}[-/]\d{2}[-/]\d{2}$/, hint: "Format: DD-MM-YYYY (e.g. 15-06-1996)", placeholder: "DD-MM-YYYY" } }
7052
7177
  ];
7053
7178
  function getFieldRule(label) {
7054
7179
  const upper = label.toUpperCase();
@@ -7063,6 +7188,7 @@ var SmartComplySDK = (() => {
7063
7188
  const v2 = value.trim();
7064
7189
  if (rule.numeric && !/^\d+$/.test(v2)) return `${label} must contain digits only`;
7065
7190
  if (rule.alphanumeric && !/^[A-Za-z0-9]+$/.test(v2)) return `${label} must be alphanumeric (letters and digits only)`;
7191
+ if (rule.pattern && !rule.pattern.test(v2)) return rule.hint;
7066
7192
  if (rule.minLen && v2.length < rule.minLen) return rule.hint;
7067
7193
  if (rule.maxLen && v2.length > rule.maxLen) return rule.hint;
7068
7194
  return null;
@@ -7156,7 +7282,7 @@ var SmartComplySDK = (() => {
7156
7282
  input.id = inputId;
7157
7283
  input.setAttribute("aria-labelledby", labelId);
7158
7284
  input.setAttribute("aria-describedby", hintId);
7159
- input.placeholder = `Enter ${field.label}`;
7285
+ input.placeholder = rule?.placeholder || `Enter ${field.label}`;
7160
7286
  input.maxLength = rule?.maxLen ? rule.maxLen + 2 : 50;
7161
7287
  if (rule?.numeric) input.inputMode = "numeric";
7162
7288
  input.style.cssText = `
@@ -7256,7 +7382,7 @@ var SmartComplySDK = (() => {
7256
7382
  }
7257
7383
  this.showStep("confirm_identity");
7258
7384
  } else {
7259
- errorEl.textContent = this.verificationResult.message || "We could not verify your details. Please check and try again.";
7385
+ errorEl.textContent = _sanitizeUserMessage(this.verificationResult.message) || "We could not verify your details. Please check and try again.";
7260
7386
  errorEl.style.display = "block";
7261
7387
  submitBtn.textContent = "Verify & Continue";
7262
7388
  submitBtn.disabled = false;
@@ -7267,11 +7393,11 @@ var SmartComplySDK = (() => {
7267
7393
  if (Array.isArray(fieldErrors) && fieldErrors.length > 0) {
7268
7394
  const firstMsg = fieldErrors[0]?.message || fieldErrors[0]?.detail || "";
7269
7395
  errorEl.textContent = _sanitizeUserMessage(firstMsg) || "Please check your details and try again.";
7270
- } else if (err.code === "VERIFICATION_FAILED") {
7271
- errorEl.textContent = "We could not verify those details. Please check your ID number and try again.";
7272
- } else if (err.code === "CHANNEL_NOT_CONFIGURED") {
7396
+ } else if (err.errorCode === "VERIFICATION_FAILED") {
7397
+ errorEl.textContent = _sanitizeUserMessage(err.message) || "We could not verify those details. Please check your ID number and try again.";
7398
+ } else if (err.errorCode === "CHANNEL_NOT_CONFIGURED") {
7273
7399
  errorEl.textContent = "This ID type is not currently available. Please choose a different option.";
7274
- } else if (err.code === "INSUFFICIENT_BALANCE") {
7400
+ } else if (err.errorCode === "INSUFFICIENT_BALANCE") {
7275
7401
  errorEl.textContent = "Verification is temporarily unavailable. Please contact support.";
7276
7402
  } else {
7277
7403
  errorEl.textContent = _sanitizeUserMessage(err.message) || "Something went wrong. Please try again.";
@@ -7902,9 +8028,10 @@ var SmartComplySDK = (() => {
7902
8028
  const isDocQuality = /blurry|blur|quality|glare|dark|bright|lighting/i.test(message);
7903
8029
  const isExpiredDoc = /document.*expired|expired.*document/i.test(message);
7904
8030
  const isFaceMatch = /face.*match|selfie.*match|does not match/i.test(message);
7905
- const errorIcon = isCameraErr ? "📷" : isNetworkErr ? "📡" : isAuthErr ? "⏱" : isDocQuality ? "🔍" : isExpiredDoc ? "📅" : isFaceMatch ? "🤳" : "✕";
7906
- 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";
7907
- 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.";
8031
+ const isBillingErr = /wallet|balance|insufficient.*fund|payment.*required/i.test(message);
8032
+ const errorIcon = isCameraErr ? "📷" : isNetworkErr ? "📡" : isAuthErr ? "⏱" : isDocQuality ? "🔍" : isExpiredDoc ? "📅" : isFaceMatch ? "🤳" : isBillingErr ? "⚠️" : "✕";
8033
+ const errorTitle = isCameraErr ? "Camera Access Required" : isNetworkErr ? "Connection Problem" : isAuthErr ? "Session Expired" : isDocQuality ? "Document Photo Unclear" : isExpiredDoc ? "Document Expired" : isFaceMatch ? "Face Match Failed" : isBillingErr ? "Verification Unavailable" : "Verification Unsuccessful";
8034
+ 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." : isBillingErr ? "Verification is temporarily unavailable. Please contact support." : _sanitizeUserMessage(message) || "We were unable to complete your verification. Please try again.";
7908
8035
  const iconWrap = document.createElement("div");
7909
8036
  iconWrap.style.cssText = `
7910
8037
  width:72px;height:72px;border-radius:50%;