smartcomply-web-sdk 1.0.90 → 1.0.91

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 (35) hide show
  1. package/dist/camera/ActionDetector.d.ts +2 -1
  2. package/dist/camera/ActionDetector.d.ts.map +1 -1
  3. package/dist/camera/ActionDetector.js +21 -5
  4. package/dist/camera/ActionDetector.js.map +1 -1
  5. package/dist/client/Smartcomply.d.ts +2 -2
  6. package/dist/client/Smartcomply.js +2 -2
  7. package/dist/esm/camera/ActionDetector.d.ts +2 -1
  8. package/dist/esm/camera/ActionDetector.d.ts.map +1 -1
  9. package/dist/esm/camera/ActionDetector.js +21 -5
  10. package/dist/esm/camera/ActionDetector.js.map +1 -1
  11. package/dist/esm/client/Smartcomply.d.ts +2 -2
  12. package/dist/esm/client/Smartcomply.js +2 -2
  13. package/dist/esm/flow/SmartComplyFlow.d.ts +10 -0
  14. package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
  15. package/dist/esm/flow/SmartComplyFlow.js +92 -32
  16. package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
  17. package/dist/esm/flow/spinner.js +14 -14
  18. package/dist/esm/modules/liveness/LivenessUI.js +83 -83
  19. package/dist/flow/SmartComplyFlow.d.ts +10 -0
  20. package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
  21. package/dist/flow/SmartComplyFlow.js +93 -32
  22. package/dist/flow/SmartComplyFlow.js.map +1 -1
  23. package/dist/flow/spinner.js +14 -14
  24. package/dist/modules/liveness/LivenessUI.js +83 -83
  25. package/dist/smartcomply.browser.js +46 -15
  26. package/dist/smartcomply.browser.js.map +2 -2
  27. package/package.json +1 -1
  28. package/dist/esm/components/CameraView.d.ts +0 -59
  29. package/dist/esm/components/CameraView.d.ts.map +0 -1
  30. package/dist/esm/components/CameraView.js +0 -238
  31. package/dist/esm/components/CameraView.js.map +0 -1
  32. package/dist/esm/components/DocumentUpload.d.ts +0 -21
  33. package/dist/esm/components/DocumentUpload.d.ts.map +0 -1
  34. package/dist/esm/components/DocumentUpload.js +0 -216
  35. package/dist/esm/components/DocumentUpload.js.map +0 -1
@@ -6,30 +6,30 @@ function injectLivenessKeyframes() {
6
6
  return;
7
7
  const s = document.createElement("style");
8
8
  s.id = "sc-liveness-kf";
9
- s.textContent = `
10
- @keyframes sc-pop-in {
11
- 0% { transform:scale(0.7); opacity:0; }
12
- 60% { transform:scale(1.05); }
13
- 100% { transform:scale(1); opacity:1; }
14
- }
15
- @keyframes sc-scan-dot {
16
- 0%,100% { opacity:0.3; transform:scale(0.85); }
17
- 50% { opacity:1; transform:scale(1); }
18
- }
19
- @keyframes sc-scanline-sweep {
20
- 0% { transform:translateY(-4%); opacity:0; }
21
- 8% { opacity:0.9; }
22
- 92% { opacity:0.9; }
23
- 100% { transform:translateY(104%); opacity:0; }
24
- }
25
- @keyframes sc-corner-pulse {
26
- 0%,100% { opacity:0.55; }
27
- 50% { opacity:1; }
28
- }
29
- @keyframes sc-oval-breathe {
30
- 0%,100% { opacity:0.5; }
31
- 50% { opacity:0.85; }
32
- }
9
+ s.textContent = `
10
+ @keyframes sc-pop-in {
11
+ 0% { transform:scale(0.7); opacity:0; }
12
+ 60% { transform:scale(1.05); }
13
+ 100% { transform:scale(1); opacity:1; }
14
+ }
15
+ @keyframes sc-scan-dot {
16
+ 0%,100% { opacity:0.3; transform:scale(0.85); }
17
+ 50% { opacity:1; transform:scale(1); }
18
+ }
19
+ @keyframes sc-scanline-sweep {
20
+ 0% { transform:translateY(-4%); opacity:0; }
21
+ 8% { opacity:0.9; }
22
+ 92% { opacity:0.9; }
23
+ 100% { transform:translateY(104%); opacity:0; }
24
+ }
25
+ @keyframes sc-corner-pulse {
26
+ 0%,100% { opacity:0.55; }
27
+ 50% { opacity:1; }
28
+ }
29
+ @keyframes sc-oval-breathe {
30
+ 0%,100% { opacity:0.5; }
31
+ 50% { opacity:0.85; }
32
+ }
33
33
  `;
34
34
  document.head.appendChild(s);
35
35
  }
@@ -59,38 +59,38 @@ class LivenessUI {
59
59
  mount(container, challenge) {
60
60
  injectLivenessKeyframes();
61
61
  this.root = document.createElement("div");
62
- this.root.style.cssText = `
63
- position:relative;width:100%;max-width:420px;margin:0 auto;
64
- background:#0a0a0a;border-radius:16px;overflow:hidden;
65
- font-family:system-ui,-apple-system,sans-serif;
66
- box-shadow:0 8px 32px rgba(0,0,0,0.4);
67
- aspect-ratio:3/4;
62
+ this.root.style.cssText = `
63
+ position:relative;width:100%;max-width:420px;margin:0 auto;
64
+ background:#0a0a0a;border-radius:16px;overflow:hidden;
65
+ font-family:system-ui,-apple-system,sans-serif;
66
+ box-shadow:0 8px 32px rgba(0,0,0,0.4);
67
+ aspect-ratio:3/4;
68
68
  `;
69
69
  this.videoEl = document.createElement("video");
70
70
  this.videoEl.autoplay = true;
71
71
  this.videoEl.playsInline = true;
72
72
  this.videoEl.muted = true;
73
73
  this.videoEl.setAttribute("aria-label", "Live camera preview for face verification");
74
- this.videoEl.style.cssText = `
75
- width:100%;height:100%;display:block;
76
- filter:brightness(1.03) contrast(1.03);
77
- transform:scaleX(-1);object-fit:cover;
74
+ this.videoEl.style.cssText = `
75
+ width:100%;height:100%;display:block;
76
+ filter:brightness(1.03) contrast(1.03);
77
+ transform:scaleX(-1);object-fit:cover;
78
78
  `;
79
79
  // Dim everything outside the oval so the guide reads as a scan aperture
80
80
  // rather than a shape floating over the raw feed.
81
81
  const vignette = document.createElement("div");
82
- vignette.style.cssText = `
83
- position:absolute;inset:0;pointer-events:none;
84
- background:radial-gradient(ellipse 34% 44% at 50% 47%, transparent 55%, rgba(0,0,0,0.55) 100%);
82
+ vignette.style.cssText = `
83
+ position:absolute;inset:0;pointer-events:none;
84
+ background:radial-gradient(ellipse 34% 44% at 50% 47%, transparent 55%, rgba(0,0,0,0.55) 100%);
85
85
  `;
86
86
  // Oval guide — centred
87
87
  const guideWrap = document.createElement("div");
88
- guideWrap.style.cssText = `
89
- position:absolute;top:50%;left:50%;
90
- transform:translate(-50%,-52%);
91
- width:64%;aspect-ratio:140/180;pointer-events:none;
92
- filter:drop-shadow(0 0 18px rgba(59,130,246,0.35));
93
- transition:filter 0.3s ease;
88
+ guideWrap.style.cssText = `
89
+ position:absolute;top:50%;left:50%;
90
+ transform:translate(-50%,-52%);
91
+ width:64%;aspect-ratio:140/180;pointer-events:none;
92
+ filter:drop-shadow(0 0 18px rgba(59,130,246,0.35));
93
+ transition:filter 0.3s ease;
94
94
  `;
95
95
  this.guideWrapEl = guideWrap;
96
96
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
@@ -179,49 +179,49 @@ class LivenessUI {
179
179
  // Scan-line sweep — clipped to the oval, gives a visible "actively
180
180
  // scanning" motion cue instead of a static outline.
181
181
  const scanlineClipWrap = document.createElement("div");
182
- scanlineClipWrap.style.cssText = `
183
- position:absolute;inset:0;
184
- clip-path:ellipse(45.7% 47.8% at 50% 50%);
185
- overflow:hidden;
182
+ scanlineClipWrap.style.cssText = `
183
+ position:absolute;inset:0;
184
+ clip-path:ellipse(45.7% 47.8% at 50% 50%);
185
+ overflow:hidden;
186
186
  `;
187
187
  const scanline = document.createElement("div");
188
- scanline.style.cssText = `
189
- position:absolute;left:0;right:0;height:26%;top:0;
190
- 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%);
191
- animation:sc-scanline-sweep 2.6s ease-in-out infinite;
188
+ scanline.style.cssText = `
189
+ position:absolute;left:0;right:0;height:26%;top:0;
190
+ 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%);
191
+ animation:sc-scanline-sweep 2.6s ease-in-out infinite;
192
192
  `;
193
193
  this.scanlineEl = scanline;
194
194
  scanlineClipWrap.appendChild(scanline);
195
195
  guideWrap.appendChild(scanlineClipWrap);
196
196
  // Instruction — top, gradient scrim
197
197
  const camTop = document.createElement("div");
198
- camTop.style.cssText = `
199
- position:absolute;top:0;left:0;right:0;
200
- background:linear-gradient(180deg,rgba(0,0,0,0.8) 0%,transparent 100%);
201
- padding:14px 16px 32px;
198
+ camTop.style.cssText = `
199
+ position:absolute;top:0;left:0;right:0;
200
+ background:linear-gradient(180deg,rgba(0,0,0,0.8) 0%,transparent 100%);
201
+ padding:14px 16px 32px;
202
202
  `;
203
203
  this.instructionEl = document.createElement("div");
204
204
  this.instructionEl.setAttribute("aria-live", "polite");
205
205
  this.instructionEl.setAttribute("role", "status");
206
- this.instructionEl.style.cssText = `
207
- color:#fff;font-size:14px;font-weight:600;text-align:center;line-height:1.4;
206
+ this.instructionEl.style.cssText = `
207
+ color:#fff;font-size:14px;font-weight:600;text-align:center;line-height:1.4;
208
208
  `;
209
209
  this.instructionEl.textContent = challenge.instruction;
210
210
  camTop.appendChild(this.instructionEl);
211
211
  // Scan progress dots — bottom
212
212
  const camBottom = document.createElement("div");
213
- camBottom.style.cssText = `
214
- position:absolute;bottom:0;left:0;right:0;
215
- background:linear-gradient(0deg,rgba(0,0,0,0.7) 0%,transparent 100%);
216
- padding:28px 10px 18px;display:flex;justify-content:center;
213
+ camBottom.style.cssText = `
214
+ position:absolute;bottom:0;left:0;right:0;
215
+ background:linear-gradient(0deg,rgba(0,0,0,0.7) 0%,transparent 100%);
216
+ padding:28px 10px 18px;display:flex;justify-content:center;
217
217
  `;
218
218
  this.dotsEl = document.createElement("div");
219
219
  this.dotsEl.style.cssText = "display:flex;gap:6px;";
220
220
  for (let i = 0; i < 3; i++) {
221
221
  const dot = document.createElement("div");
222
- dot.style.cssText = `
223
- width:6px;height:6px;border-radius:50%;background:${this.colors.primary};
224
- animation:sc-scan-dot 1.1s ease-in-out ${i * 0.18}s infinite;
222
+ dot.style.cssText = `
223
+ width:6px;height:6px;border-radius:50%;background:${this.colors.primary};
224
+ animation:sc-scan-dot 1.1s ease-in-out ${i * 0.18}s infinite;
225
225
  `;
226
226
  this.dotsEl.appendChild(dot);
227
227
  }
@@ -233,10 +233,10 @@ class LivenessUI {
233
233
  this.root.appendChild(camBottom);
234
234
  // Result overlay
235
235
  this.overlayEl = document.createElement("div");
236
- this.overlayEl.style.cssText = `
237
- position:absolute;inset:0;display:none;align-items:center;
238
- justify-content:center;z-index:10;
239
- background:rgba(0,0,0,0.88);backdrop-filter:blur(8px);
236
+ this.overlayEl.style.cssText = `
237
+ position:absolute;inset:0;display:none;align-items:center;
238
+ justify-content:center;z-index:10;
239
+ background:rgba(0,0,0,0.88);backdrop-filter:blur(8px);
240
240
  `;
241
241
  this.root.appendChild(this.overlayEl);
242
242
  container.appendChild(this.root);
@@ -297,15 +297,15 @@ class LivenessUI {
297
297
  }
298
298
  const ok = status === "processing";
299
299
  this.overlayEl.style.display = "flex";
300
- this.overlayEl.innerHTML = `
301
- <div style="text-align:center;color:#fff;animation:sc-pop-in 0.35s ease;padding:24px;">
302
- <div style="width:72px;height:72px;border-radius:50%;margin:0 auto 16px;display:flex;align-items:center;justify-content:center;font-size:32px;
303
- background:${ok ? "rgba(34,197,94,0.2)" : "rgba(239,68,68,0.2)"};
304
- border:3px solid ${ok ? this.colors.success : this.colors.error};">
305
- ${ok ? "✓" : "✕"}
306
- </div>
307
- <div style="font-size:17px;font-weight:700;margin-bottom:8px;">${ok ? "Verification submitted" : "Submission failed"}</div>
308
- <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>
300
+ this.overlayEl.innerHTML = `
301
+ <div style="text-align:center;color:#fff;animation:sc-pop-in 0.35s ease;padding:24px;">
302
+ <div style="width:72px;height:72px;border-radius:50%;margin:0 auto 16px;display:flex;align-items:center;justify-content:center;font-size:32px;
303
+ background:${ok ? "rgba(34,197,94,0.2)" : "rgba(239,68,68,0.2)"};
304
+ border:3px solid ${ok ? this.colors.success : this.colors.error};">
305
+ ${ok ? "✓" : "✕"}
306
+ </div>
307
+ <div style="font-size:17px;font-weight:700;margin-bottom:8px;">${ok ? "Verification submitted" : "Submission failed"}</div>
308
+ <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>
309
309
  </div>`;
310
310
  setTimeout(resolve, 1800);
311
311
  });
@@ -317,12 +317,12 @@ class LivenessUI {
317
317
  return;
318
318
  }
319
319
  this.overlayEl.style.display = "flex";
320
- this.overlayEl.innerHTML = `
321
- <div style="text-align:center;color:#fff;animation:sc-pop-in 0.35s ease;padding:24px;">
322
- <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 ${this.colors.warning};">⏱</div>
323
- <div style="font-size:17px;font-weight:700;margin-bottom:8px;">Time's Up</div>
324
- <div style="font-size:12px;opacity:0.6;margin-bottom:18px;">We couldn't confirm liveness in time. Please try again.</div>
325
- <button id="sc-retry-btn" style="padding:11px 28px;border-radius:10px;background:${this.colors.primary};color:#fff;border:none;cursor:pointer;font-size:14px;font-weight:600;width:100%;max-width:180px;">Try Again</button>
320
+ this.overlayEl.innerHTML = `
321
+ <div style="text-align:center;color:#fff;animation:sc-pop-in 0.35s ease;padding:24px;">
322
+ <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 ${this.colors.warning};">⏱</div>
323
+ <div style="font-size:17px;font-weight:700;margin-bottom:8px;">Time's Up</div>
324
+ <div style="font-size:12px;opacity:0.6;margin-bottom:18px;">We couldn't confirm liveness in time. Please try again.</div>
325
+ <button id="sc-retry-btn" style="padding:11px 28px;border-radius:10px;background:${this.colors.primary};color:#fff;border:none;cursor:pointer;font-size:14px;font-weight:600;width:100%;max-width:180px;">Try Again</button>
326
326
  </div>`;
327
327
  const btn = this.overlayEl.querySelector("#sc-retry-btn");
328
328
  if (btn) {
@@ -4632,7 +4632,10 @@ var SmartComplySDK = (() => {
4632
4632
  this.blinkHoldCount = 0;
4633
4633
  this.blinkDetected = false;
4634
4634
  this.motionDetected = false;
4635
- this.baselineYaw = null;
4635
+ // Running min/max yaw seen so far in the scan, not a single fixed
4636
+ // first-frame baseline (see check() below for why).
4637
+ this.minYaw = null;
4638
+ this.maxYaw = null;
4636
4639
  }
4637
4640
  check(result) {
4638
4641
  if (!result.faceDetected) {
@@ -4652,9 +4655,9 @@ var SmartComplySDK = (() => {
4652
4655
  }
4653
4656
  if (!this.motionDetected) {
4654
4657
  const yaw = this.getHeadYaw(result.transformMatrix);
4655
- if (this.baselineYaw === null) {
4656
- this.baselineYaw = yaw;
4657
- } else if (Math.abs(yaw - this.baselineYaw) >= YAW_MOTION_THRESHOLD_DEG) {
4658
+ if (this.minYaw === null || yaw < this.minYaw) this.minYaw = yaw;
4659
+ if (this.maxYaw === null || yaw > this.maxYaw) this.maxYaw = yaw;
4660
+ if (this.maxYaw - this.minYaw >= YAW_MOTION_THRESHOLD_DEG) {
4658
4661
  this.motionDetected = true;
4659
4662
  }
4660
4663
  }
@@ -5779,7 +5782,7 @@ var SmartComplySDK = (() => {
5779
5782
  * the request, so the resume_token has to be the one in use throughout.
5780
5783
  *
5781
5784
  * No expires_at is set here — the resume_token's own issuance response
5782
- * only carries resume_expires_at (a 72h invite-link window), not the
5785
+ * only carries resume_expires_at (a 24h invite-link window), not the
5783
5786
  * session's own runtime expiry the way createSession() returns one. The
5784
5787
  * backend still enforces the real expiry server-side regardless; this
5785
5788
  * only means the client-side "session about to expire" warning timer
@@ -5834,7 +5837,7 @@ var SmartComplySDK = (() => {
5834
5837
  * CDN tag they installed with (e.g. @1, which floats to the latest
5835
5838
  * matching release) actually resolved to the version they expect.
5836
5839
  */
5837
- _SmartComply.VERSION = "1.0.90";
5840
+ _SmartComply.VERSION = "1.0.91";
5838
5841
  var SmartComply = _SmartComply;
5839
5842
 
5840
5843
  // src/flow/theme.ts
@@ -7358,6 +7361,9 @@ var SmartComplySDK = (() => {
7358
7361
  function isUploadFieldType(type) {
7359
7362
  return type === "upload" || type === "image" || type === "file";
7360
7363
  }
7364
+ function isResumeLinkErrorMessage(message) {
7365
+ return /resume link|resubmit link/i.test(message);
7366
+ }
7361
7367
  function resolveIdTypeInfo(typeName, isDocumentFlow) {
7362
7368
  const u2 = typeName.toUpperCase();
7363
7369
  const advanced = u2.includes("ADVANCED");
@@ -7835,7 +7841,12 @@ var SmartComplySDK = (() => {
7835
7841
  this.brandLogoImg.style.display = "block";
7836
7842
  }
7837
7843
  if (this.isDestroyed) return;
7838
- this.showStep(this.options.resumeToken ? await this._resolveResumeStep() : "welcome");
7844
+ if (this.options.resumeToken) {
7845
+ const resumeStep = await this._resolveResumeStep();
7846
+ if (resumeStep !== null) this.showStep(resumeStep);
7847
+ } else {
7848
+ this.showStep("welcome");
7849
+ }
7839
7850
  } catch (err) {
7840
7851
  this.showErrorStep(err.message || "Failed to initialize. Please try again.");
7841
7852
  }
@@ -7845,9 +7856,23 @@ var SmartComplySDK = (() => {
7845
7856
  // same state renderCountrySelect/renderIdTypeSelect's own click handlers
7846
7857
  // set) so document_capture/liveness render correctly instead of assuming
7847
7858
  // state that was never set. Falls back to "welcome" on anything that
7848
- // doesn't resolve cleanly — this is a UX improvement, not a requirement:
7849
- // the backend's own resume-match at create/ time still works correctly
7850
- // even if the widget can't figure out which screen to pre-select.
7859
+ // doesn't resolve cleanly for an UNCERTAIN reason (network error, timeout)
7860
+ // — this is a UX improvement, not a requirement: the backend's own
7861
+ // resume-match at create/ time still works correctly even if the widget
7862
+ // can't figure out which screen to pre-select.
7863
+ //
7864
+ // Returns null instead when the lookup fails for a CERTAIN, known reason
7865
+ // — the resume link itself is provably expired or already used
7866
+ // (RESUME_TARGET_UNAVAILABLE) or was never a resume link at all
7867
+ // (NOT_A_RESUME_TOKEN). Unlike a transient failure, there is no "try the
7868
+ // real request and it'll still work" fallback for these: create/'s own
7869
+ // resume-match reads the exact same resume_target_entry binding this
7870
+ // lookup just found missing/invalid, so falling through to a generic
7871
+ // welcome screen would silently walk the customer into a brand-new,
7872
+ // unrelated verification attempt with no explanation — worse than telling
7873
+ // them plainly that their link no longer works. The caller shows this
7874
+ // error itself (via showErrorStep, called here directly) rather than
7875
+ // rendering a step.
7851
7876
  async _resolveResumeStep() {
7852
7877
  try {
7853
7878
  const info = await this.sdk.getResumeInfo();
@@ -7886,7 +7911,12 @@ var SmartComplySDK = (() => {
7886
7911
  }
7887
7912
  }
7888
7913
  return "welcome";
7889
- } catch {
7914
+ } catch (err) {
7915
+ const code = err instanceof SDKError ? err.errorCode : void 0;
7916
+ if (code === "RESUME_TARGET_UNAVAILABLE" || code === "NOT_A_RESUME_TOKEN") {
7917
+ this.showErrorStep("This resume link has expired or is no longer valid.");
7918
+ return null;
7919
+ }
7890
7920
  return "welcome";
7891
7921
  }
7892
7922
  }
@@ -9209,14 +9239,15 @@ var SmartComplySDK = (() => {
9209
9239
  `;
9210
9240
  const isCameraErr = /camera|permission|notallowed|notfound/i.test(message);
9211
9241
  const isNetworkErr = /network|timeout|connection|fetch/i.test(message);
9212
- const isAuthErr = /session.*expired|token.*expired|authentication/i.test(message);
9242
+ const isResumeLinkErr = isResumeLinkErrorMessage(message);
9243
+ const isAuthErr = !isResumeLinkErr && /session.*expired|token.*expired|authentication/i.test(message);
9213
9244
  const isDocQuality = /blurry|blur|quality|glare|dark|bright|lighting/i.test(message);
9214
9245
  const isExpiredDoc = /document.*expired|expired.*document/i.test(message);
9215
9246
  const isFaceMatch = /face.*match|selfie.*match|does not match/i.test(message);
9216
9247
  const isBillingErr = /wallet|balance|insufficient.*fund|payment.*required/i.test(message);
9217
- const errorIconName = isCameraErr ? "camera" : isNetworkErr ? "network" : isAuthErr ? "clock" : isDocQuality ? "search" : isExpiredDoc ? "calendar" : isFaceMatch ? "face" : isBillingErr ? "warning" : "close";
9218
- 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";
9219
- 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.";
9248
+ const errorIconName = isCameraErr ? "camera" : isNetworkErr ? "network" : isResumeLinkErr ? "clock" : isAuthErr ? "clock" : isDocQuality ? "search" : isExpiredDoc ? "calendar" : isFaceMatch ? "face" : isBillingErr ? "warning" : "close";
9249
+ const errorTitle = isCameraErr ? "Camera Access Required" : isNetworkErr ? "Connection Problem" : isResumeLinkErr ? "Link No Longer Valid" : isAuthErr ? "Session Expired" : isDocQuality ? "Document Photo Unclear" : isExpiredDoc ? "Document Expired" : isFaceMatch ? "Face Match Failed" : isBillingErr ? "Verification Unavailable" : "Verification Unsuccessful";
9250
+ const userMessage = isCameraErr ? "Please allow camera access in your browser settings and try again." : isNetworkErr ? "Check your internet connection and try again." : isResumeLinkErr ? "This link has expired or has already been used. Please contact us to request a new one." : 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.";
9220
9251
  const iconWrap = document.createElement("div");
9221
9252
  iconWrap.style.cssText = `
9222
9253
  width:72px;height:72px;border-radius:50%;