smartcomply-web-sdk 1.0.24 โ†’ 1.0.30

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.
@@ -1,48 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LivenessUI = void 0;
4
- /**
5
- * Icons for the 5 backend-supported challenge actions (UPPERCASE).
6
- */
7
4
  const ICONS = {
8
- BLINK: "\uD83D\uDE09",
9
- TURN_LEFT: "\u2B05",
10
- TURN_RIGHT: "\u27A1",
11
- TURN_HEAD: "\uD83D\uDE42",
12
- OPEN_MOUTH: "\uD83D\uDE2E",
5
+ BLINK: "๐Ÿ‘",
6
+ TURN_LEFT: "โ†”",
7
+ TURN_RIGHT: "โ†”",
8
+ TURN_HEAD: "โ†”",
9
+ OPEN_MOUTH: "๐Ÿ˜ฎ",
13
10
  };
14
- /**
15
- * Large animated SVG/HTML cues shown in the centre of the camera frame
16
- * so the user knows exactly what to do โ€” like a real liveness test.
17
- */
18
11
  const ACTION_CUES = {
19
- BLINK: `
20
- <div style="display:flex;flex-direction:column;align-items:center;gap:10px;">
21
- <div style="font-size:64px;line-height:1;animation:sc-blink 1.2s ease-in-out infinite;">๐Ÿ‘๏ธ</div>
22
- <div style="font-size:13px;font-weight:600;color:#fff;opacity:0.9;letter-spacing:0.5px;">BLINK YOUR EYES</div>
23
- </div>`,
24
- TURN_LEFT: `
25
- <div style="display:flex;flex-direction:column;align-items:center;gap:10px;">
26
- <div style="font-size:56px;line-height:1;animation:sc-slide-left 1s ease-in-out infinite;">โฌ…๏ธ</div>
27
- <div style="font-size:13px;font-weight:600;color:#fff;opacity:0.9;letter-spacing:0.5px;">TURN HEAD RIGHT</div>
28
- </div>`,
29
- TURN_RIGHT: `
30
- <div style="display:flex;flex-direction:column;align-items:center;gap:10px;">
31
- <div style="font-size:56px;line-height:1;animation:sc-slide-right 1s ease-in-out infinite;">โžก๏ธ</div>
32
- <div style="font-size:13px;font-weight:600;color:#fff;opacity:0.9;letter-spacing:0.5px;">TURN HEAD LEFT</div>
33
- </div>`,
34
- TURN_HEAD: `
35
- <div style="display:flex;flex-direction:column;align-items:center;gap:10px;">
36
- <div style="font-size:56px;line-height:1;animation:sc-turn-head 1.4s ease-in-out infinite;">โ†”๏ธ</div>
37
- <div style="font-size:13px;font-weight:600;color:#fff;opacity:0.9;letter-spacing:0.5px;">TURN HEAD SIDE TO SIDE</div>
38
- </div>`,
39
- OPEN_MOUTH: `
40
- <div style="display:flex;flex-direction:column;align-items:center;gap:10px;">
41
- <div style="font-size:64px;line-height:1;animation:sc-mouth 1.2s ease-in-out infinite;">๐Ÿ˜ฎ</div>
42
- <div style="font-size:13px;font-weight:600;color:#fff;opacity:0.9;letter-spacing:0.5px;">OPEN YOUR MOUTH</div>
43
- </div>`,
12
+ BLINK: `<div style="font-size:52px;line-height:1;animation:sc-blink 1.2s ease-in-out infinite;">๐Ÿ‘๏ธ</div>
13
+ <div style="font-size:12px;font-weight:600;color:#fff;margin-top:6px;letter-spacing:0.5px;">BLINK YOUR EYES</div>`,
14
+ TURN_LEFT: `<div style="font-size:52px;line-height:1;animation:sc-turn-head 1.4s ease-in-out infinite;">โ†”๏ธ</div>
15
+ <div style="font-size:12px;font-weight:600;color:#fff;margin-top:6px;letter-spacing:0.5px;">TURN HEAD SIDEWAYS</div>`,
16
+ TURN_RIGHT: `<div style="font-size:52px;line-height:1;animation:sc-turn-head 1.4s ease-in-out infinite;">โ†”๏ธ</div>
17
+ <div style="font-size:12px;font-weight:600;color:#fff;margin-top:6px;letter-spacing:0.5px;">TURN HEAD SIDEWAYS</div>`,
18
+ TURN_HEAD: `<div style="font-size:52px;line-height:1;animation:sc-turn-head 1.4s ease-in-out infinite;">โ†”๏ธ</div>
19
+ <div style="font-size:12px;font-weight:600;color:#fff;margin-top:6px;letter-spacing:0.5px;">TURN HEAD SIDE TO SIDE</div>`,
20
+ OPEN_MOUTH: `<div style="font-size:52px;line-height:1;animation:sc-mouth 1.2s ease-in-out infinite;">๐Ÿ˜ฎ</div>
21
+ <div style="font-size:12px;font-weight:600;color:#fff;margin-top:6px;letter-spacing:0.5px;">OPEN YOUR MOUTH</div>`,
44
22
  };
45
- /** Keyframe CSS injected once */
46
23
  function injectLivenessKeyframes() {
47
24
  if (document.getElementById("sc-liveness-kf"))
48
25
  return;
@@ -51,37 +28,23 @@ function injectLivenessKeyframes() {
51
28
  s.textContent = `
52
29
  @keyframes sc-blink {
53
30
  0%,100% { transform:scaleY(1); }
54
- 40% { transform:scaleY(0.1); }
55
- 50% { transform:scaleY(1); }
56
- }
57
- @keyframes sc-slide-left {
58
- 0%,100% { transform:translateX(0); opacity:1; }
59
- 50% { transform:translateX(-14px); opacity:0.6; }
60
- }
61
- @keyframes sc-slide-right {
62
- 0%,100% { transform:translateX(0); opacity:1; }
63
- 50% { transform:translateX(14px); opacity:0.6; }
31
+ 40% { transform:scaleY(0.1); }
32
+ 50% { transform:scaleY(1); }
64
33
  }
65
34
  @keyframes sc-turn-head {
66
- 0%,100% { transform:translateX(0); }
67
- 25% { transform:translateX(-12px); }
68
- 75% { transform:translateX(12px); }
35
+ 0%,100% { transform:translateX(0); }
36
+ 25% { transform:translateX(-12px); }
37
+ 75% { transform:translateX(12px); }
69
38
  }
70
39
  @keyframes sc-mouth {
71
- 0%,100% { transform:scaleY(1); }
72
- 40% { transform:scaleY(1.25); }
73
- 60% { transform:scaleY(1); }
40
+ 0%,100% { transform:scaleY(1); }
41
+ 40% { transform:scaleY(1.25); }
42
+ 60% { transform:scaleY(1); }
74
43
  }
75
44
  @keyframes sc-pop-in {
76
45
  0% { transform:scale(0.7); opacity:0; }
77
46
  60% { transform:scale(1.05); }
78
- 100% { transform:scale(1); opacity:1; }
79
- }
80
- @keyframes sc-cue-fade {
81
- 0% { opacity:0; transform:translateY(8px); }
82
- 20% { opacity:1; transform:translateY(0); }
83
- 80% { opacity:1; transform:translateY(0); }
84
- 100% { opacity:0; transform:translateY(-8px); }
47
+ 100% { transform:scale(1); opacity:1; }
85
48
  }
86
49
  `;
87
50
  document.head.appendChild(s);
@@ -105,83 +68,117 @@ class LivenessUI {
105
68
  mount(container, challenge) {
106
69
  injectLivenessKeyframes();
107
70
  this.totalActions = challenge.actions.length;
71
+ // โ”€โ”€ Root โ€” two-column layout โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
108
72
  this.root = document.createElement("div");
109
73
  this.root.style.cssText = `
110
- position:relative;width:100%;max-width:420px;margin:0 auto;
74
+ display:flex;flex-direction:row;width:100%;max-width:560px;margin:0 auto;
111
75
  background:#0a0a0a;border-radius:16px;overflow:hidden;
112
76
  font-family:system-ui,-apple-system,sans-serif;
113
77
  box-shadow:0 8px 32px rgba(0,0,0,0.4);
78
+ min-height:340px;
79
+ `;
80
+ // โ”€โ”€ LEFT: Camera column โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
81
+ const camCol = document.createElement("div");
82
+ camCol.style.cssText = `
83
+ position:relative;flex:0 0 58%;
84
+ background:#000;overflow:hidden;
114
85
  `;
115
- // Video
116
86
  this.videoEl = document.createElement("video");
117
87
  this.videoEl.autoplay = true;
118
88
  this.videoEl.playsInline = true;
119
89
  this.videoEl.muted = true;
120
- this.videoEl.style.cssText =
121
- "width:100%;display:block;transform:scaleX(-1);aspect-ratio:3/4;object-fit:cover;";
122
- // Face guide โ€” oval with confidence ring
90
+ this.videoEl.style.cssText = `
91
+ width:100%;height:100%;display:block;
92
+ transform:scaleX(-1);object-fit:cover;
93
+ min-height:320px;
94
+ `;
95
+ // Oval guide โ€” centred in camera column
123
96
  const guideWrap = document.createElement("div");
124
97
  guideWrap.style.cssText = `
125
- position:absolute;top:8%;left:50%;transform:translateX(-50%);
126
- width:180px;height:240px;pointer-events:none;
98
+ position:absolute;top:50%;left:50%;
99
+ transform:translate(-50%,-55%);
100
+ width:140px;height:180px;pointer-events:none;
127
101
  `;
128
102
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
129
- svg.setAttribute("viewBox", "0 0 180 240");
103
+ svg.setAttribute("viewBox", "0 0 140 180");
130
104
  svg.style.cssText = "width:100%;height:100%;";
131
- // Background oval (guide)
132
105
  const bgOval = document.createElementNS("http://www.w3.org/2000/svg", "ellipse");
133
- bgOval.setAttribute("cx", "90");
134
- bgOval.setAttribute("cy", "120");
135
- bgOval.setAttribute("rx", "85");
136
- bgOval.setAttribute("ry", "115");
106
+ bgOval.setAttribute("cx", "70");
107
+ bgOval.setAttribute("cy", "90");
108
+ bgOval.setAttribute("rx", "64");
109
+ bgOval.setAttribute("ry", "86");
137
110
  bgOval.setAttribute("fill", "none");
138
- bgOval.setAttribute("stroke", "rgba(255,255,255,0.25)");
139
- bgOval.setAttribute("stroke-width", "2.5");
140
- bgOval.setAttribute("stroke-dasharray", "8 4");
141
- // Confidence progress ring
111
+ bgOval.setAttribute("stroke", "rgba(255,255,255,0.3)");
112
+ bgOval.setAttribute("stroke-width", "2");
113
+ bgOval.setAttribute("stroke-dasharray", "6 3");
142
114
  const confOval = document.createElementNS("http://www.w3.org/2000/svg", "ellipse");
143
- confOval.setAttribute("cx", "90");
144
- confOval.setAttribute("cy", "120");
145
- confOval.setAttribute("rx", "85");
146
- confOval.setAttribute("ry", "115");
115
+ confOval.setAttribute("cx", "70");
116
+ confOval.setAttribute("cy", "90");
117
+ confOval.setAttribute("rx", "64");
118
+ confOval.setAttribute("ry", "86");
147
119
  confOval.setAttribute("fill", "none");
148
120
  confOval.setAttribute("stroke", "#22c55e");
149
- confOval.setAttribute("stroke-width", "3.5");
150
- const circumference = 2 * Math.PI * Math.sqrt((85 * 85 + 115 * 115) / 2);
151
- confOval.setAttribute("stroke-dasharray", `${circumference}`);
152
- confOval.setAttribute("stroke-dashoffset", `${circumference}`);
153
- confOval.style.cssText =
154
- "transition:stroke-dashoffset 0.15s ease;transform:rotate(-90deg);transform-origin:90px 120px;";
121
+ confOval.setAttribute("stroke-width", "3");
122
+ const circ = 2 * Math.PI * Math.sqrt((64 * 64 + 86 * 86) / 2);
123
+ confOval.setAttribute("stroke-dasharray", `${circ}`);
124
+ confOval.setAttribute("stroke-dashoffset", `${circ}`);
125
+ confOval.style.cssText = "transition:stroke-dashoffset 0.15s ease;transform:rotate(-90deg);transform-origin:70px 90px;";
155
126
  this.confidenceRingEl = confOval;
156
127
  svg.appendChild(bgOval);
157
128
  svg.appendChild(confOval);
158
129
  guideWrap.appendChild(svg);
159
- // Top bar โ€” instruction
160
- const topBar = document.createElement("div");
161
- topBar.style.cssText = `
130
+ // Instruction text โ€” top of camera column
131
+ const camTop = document.createElement("div");
132
+ camTop.style.cssText = `
162
133
  position:absolute;top:0;left:0;right:0;
163
- background:linear-gradient(180deg,rgba(0,0,0,0.7) 0%,transparent 100%);
164
- padding:16px 16px 32px;
134
+ background:linear-gradient(180deg,rgba(0,0,0,0.8) 0%,transparent 100%);
135
+ padding:10px 12px 28px;
165
136
  `;
166
137
  this.instructionEl = document.createElement("div");
167
138
  this.instructionEl.style.cssText = `
168
- color:#fff;font-size:15px;font-weight:600;text-align:center;
169
- line-height:1.4;
139
+ color:#fff;font-size:13px;font-weight:600;text-align:center;line-height:1.4;
170
140
  `;
171
141
  this.instructionEl.textContent = challenge.instruction;
172
- topBar.appendChild(this.instructionEl);
173
- // Bottom panel
174
- const bottomPanel = document.createElement("div");
175
- bottomPanel.style.cssText = `
142
+ camTop.appendChild(this.instructionEl);
143
+ // Timer โ€” bottom of camera column
144
+ const camBottom = document.createElement("div");
145
+ camBottom.style.cssText = `
176
146
  position:absolute;bottom:0;left:0;right:0;
177
- background:linear-gradient(0deg,rgba(0,0,0,0.85) 0%,rgba(0,0,0,0.6) 60%,transparent 100%);
178
- padding:24px 16px 16px;
147
+ background:linear-gradient(0deg,rgba(0,0,0,0.7) 0%,transparent 100%);
148
+ padding:24px 10px 8px;text-align:center;
149
+ `;
150
+ this.timerEl = document.createElement("div");
151
+ this.timerEl.style.cssText = `
152
+ color:rgba(255,255,255,0.5);font-size:11px;font-variant-numeric:tabular-nums;
153
+ `;
154
+ camBottom.appendChild(this.timerEl);
155
+ // Action cue โ€” centre of camera column
156
+ this.actionCueEl = document.createElement("div");
157
+ this.actionCueEl.style.cssText = `
158
+ position:absolute;bottom:36px;left:50%;transform:translateX(-50%);
159
+ z-index:5;pointer-events:none;display:none;text-align:center;
160
+ background:rgba(0,0,0,0.6);backdrop-filter:blur(4px);
161
+ border-radius:14px;padding:10px 18px;min-width:130px;
162
+ border:1.5px solid rgba(255,255,255,0.12);
163
+ `;
164
+ camCol.appendChild(this.videoEl);
165
+ camCol.appendChild(guideWrap);
166
+ camCol.appendChild(camTop);
167
+ camCol.appendChild(camBottom);
168
+ camCol.appendChild(this.actionCueEl);
169
+ // โ”€โ”€ RIGHT: Steps column โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
170
+ const stepsCol = document.createElement("div");
171
+ stepsCol.style.cssText = `
172
+ flex:1;display:flex;flex-direction:column;
173
+ padding:16px 14px;gap:8px;
174
+ background:#111;border-left:1px solid rgba(255,255,255,0.06);
175
+ justify-content:space-between;
179
176
  `;
180
- // Progress bar
177
+ // Progress bar at top of steps column
181
178
  const progressWrap = document.createElement("div");
182
179
  progressWrap.style.cssText = `
183
- height:3px;background:rgba(255,255,255,0.15);border-radius:2px;
184
- margin-bottom:14px;overflow:hidden;
180
+ height:3px;background:rgba(255,255,255,0.1);border-radius:2px;overflow:hidden;
181
+ margin-bottom:4px;
185
182
  `;
186
183
  this.progressBarEl = document.createElement("div");
187
184
  this.progressBarEl.style.cssText = `
@@ -189,83 +186,55 @@ class LivenessUI {
189
186
  border-radius:2px;transition:width 0.4s ease;
190
187
  `;
191
188
  progressWrap.appendChild(this.progressBarEl);
192
- bottomPanel.appendChild(progressWrap);
193
189
  // Steps list
194
190
  this.stepsEl = document.createElement("div");
195
- this.stepsEl.style.cssText = `
196
- display:flex;flex-direction:column;gap:8px;margin-bottom:12px;
197
- `;
191
+ this.stepsEl.style.cssText = "display:flex;flex-direction:column;gap:6px;flex:1;";
198
192
  for (let i = 0; i < challenge.actions.length; i++) {
199
193
  const action = challenge.actions[i];
200
194
  const step = document.createElement("div");
201
195
  step.dataset.action = action;
202
196
  step.style.cssText = `
203
- display:flex;align-items:center;gap:10px;
204
- padding:8px 12px;border-radius:10px;
205
- background:rgba(255,255,255,0.06);
197
+ display:flex;align-items:center;gap:8px;
198
+ padding:8px 10px;border-radius:10px;
199
+ background:rgba(255,255,255,0.04);
200
+ border:1px solid rgba(255,255,255,0.06);
206
201
  transition:all 0.3s ease;
207
- opacity:${i === 0 ? "1" : "0.4"};
202
+ opacity:${i === 0 ? "1" : "0.45"};
208
203
  `;
209
204
  const num = document.createElement("div");
210
205
  num.className = "sc-step-num";
211
206
  num.style.cssText = `
212
- width:28px;height:28px;border-radius:50%;
207
+ width:24px;height:24px;border-radius:50%;flex-shrink:0;
213
208
  display:flex;align-items:center;justify-content:center;
214
- font-size:13px;font-weight:700;color:#fff;
215
- background:rgba(255,255,255,0.12);
216
- border:2px solid rgba(255,255,255,0.2);
217
- transition:all 0.3s ease;flex-shrink:0;
209
+ font-size:12px;font-weight:700;color:#fff;
210
+ background:rgba(255,255,255,0.1);
211
+ border:1.5px solid rgba(255,255,255,0.2);
212
+ transition:all 0.3s ease;
218
213
  `;
219
214
  num.textContent = String(i + 1);
220
215
  const label = document.createElement("div");
221
- label.style.cssText = "color:#fff;font-size:13px;flex:1;";
216
+ label.style.cssText = "color:#fff;font-size:12px;font-weight:500;flex:1;line-height:1.3;";
222
217
  label.textContent = this.describeAction(action);
223
218
  const icon = document.createElement("div");
224
- icon.className = "sc-step-icon";
225
- icon.style.cssText = "font-size:18px;flex-shrink:0;";
219
+ icon.style.cssText = "font-size:16px;flex-shrink:0;";
226
220
  icon.textContent = ICONS[action] || "";
227
221
  step.appendChild(num);
228
222
  step.appendChild(label);
229
223
  step.appendChild(icon);
230
224
  this.stepsEl.appendChild(step);
231
225
  }
232
- bottomPanel.appendChild(this.stepsEl);
233
- // Timer
234
- this.timerEl = document.createElement("div");
235
- this.timerEl.style.cssText = `
236
- text-align:center;color:rgba(255,255,255,0.5);
237
- font-size:11px;font-variant-numeric:tabular-nums;
238
- `;
239
- bottomPanel.appendChild(this.timerEl);
240
- // Result overlay (used for success / failure / timeout)
226
+ stepsCol.appendChild(progressWrap);
227
+ stepsCol.appendChild(this.stepsEl);
228
+ // Result overlay โ€” full root
241
229
  this.overlayEl = document.createElement("div");
242
230
  this.overlayEl.style.cssText = `
243
231
  position:absolute;inset:0;display:none;align-items:center;
244
232
  justify-content:center;z-index:10;
245
- background:rgba(0,0,0,0.85);backdrop-filter:blur(8px);
246
- transition:opacity 0.3s ease;
247
- `;
248
- // Action cue โ€” large centred animation shown when an action is active
249
- this.actionCueEl = document.createElement("div");
250
- this.actionCueEl.style.cssText = `
251
- position:absolute;
252
- top:50%;left:50%;transform:translate(-50%,-50%);
253
- z-index:5;pointer-events:none;
254
- display:none;
255
- text-align:center;
256
- background:rgba(0,0,0,0.55);
257
- backdrop-filter:blur(4px);
258
- border-radius:20px;
259
- padding:18px 28px;
260
- min-width:160px;
261
- border:1.5px solid rgba(255,255,255,0.15);
233
+ background:rgba(0,0,0,0.88);backdrop-filter:blur(8px);
262
234
  `;
263
- // Assemble
264
- this.root.appendChild(this.videoEl);
265
- this.root.appendChild(guideWrap);
266
- this.root.appendChild(topBar);
267
- this.root.appendChild(bottomPanel);
268
- this.root.appendChild(this.actionCueEl);
235
+ // โ”€โ”€ Assemble โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
236
+ this.root.appendChild(camCol);
237
+ this.root.appendChild(stepsCol);
269
238
  this.root.appendChild(this.overlayEl);
270
239
  container.appendChild(this.root);
271
240
  this.startTimer(challenge.time_limit_seconds);
@@ -276,7 +245,6 @@ class LivenessUI {
276
245
  return;
277
246
  let completed = 0;
278
247
  const activeState = states.find((s) => s.active && !s.detected) ?? null;
279
- const activeAction = activeState?.action ?? null;
280
248
  for (const state of states) {
281
249
  const step = this.stepsEl.querySelector(`[data-action="${state.action}"]`);
282
250
  if (!step)
@@ -285,46 +253,42 @@ class LivenessUI {
285
253
  if (state.detected) {
286
254
  completed++;
287
255
  step.style.opacity = "1";
288
- step.style.background = "rgba(34,197,94,0.15)";
256
+ step.style.background = "rgba(34,197,94,0.12)";
257
+ step.style.borderColor = "rgba(34,197,94,0.3)";
289
258
  if (num) {
290
259
  num.style.background = "#22c55e";
291
260
  num.style.borderColor = "#22c55e";
292
- num.textContent = "\u2713";
261
+ num.textContent = "โœ“";
293
262
  }
294
263
  }
295
264
  else if (state.active) {
296
265
  step.style.opacity = "1";
297
- step.style.background = "rgba(59,130,246,0.15)";
266
+ step.style.background = "rgba(59,130,246,0.12)";
267
+ step.style.borderColor = "rgba(59,130,246,0.3)";
298
268
  if (num) {
299
269
  num.style.borderColor = "#3b82f6";
300
270
  num.style.background = "rgba(59,130,246,0.3)";
301
271
  }
302
272
  }
303
273
  }
304
- // Update progress bar
305
274
  const pct = this.totalActions > 0 ? (completed / this.totalActions) * 100 : 0;
306
275
  this.progressBarEl.style.width = `${pct}%`;
307
- // Update confidence ring for the active action
308
276
  if (this.confidenceRingEl) {
309
- const circumference = 2 * Math.PI * Math.sqrt((85 * 85 + 115 * 115) / 2);
277
+ const circ = 2 * Math.PI * Math.sqrt((64 * 64 + 86 * 86) / 2);
310
278
  const conf = activeState ? activeState.confidence : 0;
311
- const offset = circumference * (1 - conf);
312
- this.confidenceRingEl.setAttribute("stroke-dashoffset", `${offset}`);
279
+ this.confidenceRingEl.setAttribute("stroke-dashoffset", `${circ * (1 - conf)}`);
313
280
  this.confidenceRingEl.setAttribute("stroke", conf > 0.6 ? "#22c55e" : "#3b82f6");
314
281
  }
315
- // Show / switch large animated action cue
316
- this.updateActionCue(activeAction);
282
+ this.updateActionCue(activeState?.action ?? null);
317
283
  }
318
284
  updateActionCue(action) {
319
285
  if (!this.actionCueEl)
320
286
  return;
321
- // No active action โ†’ hide cue
322
287
  if (!action) {
323
288
  this.actionCueEl.style.display = "none";
324
289
  this.currentCueAction = null;
325
290
  return;
326
291
  }
327
- // Same action already showing โ€” no DOM thrash
328
292
  if (action === this.currentCueAction)
329
293
  return;
330
294
  this.currentCueAction = action;
@@ -333,27 +297,23 @@ class LivenessUI {
333
297
  this.actionCueEl.style.display = "none";
334
298
  return;
335
299
  }
336
- // Inject content and animate in
337
300
  this.actionCueEl.innerHTML = cueHtml;
338
301
  this.actionCueEl.style.display = "block";
339
302
  this.actionCueEl.style.animation = "none";
340
- // Force reflow then trigger animation
341
303
  void this.actionCueEl.offsetWidth;
342
304
  this.actionCueEl.style.animation = "sc-pop-in 0.3s ease forwards";
343
305
  }
344
306
  updateInstruction(text) {
345
- if (this.instructionEl) {
307
+ if (this.instructionEl)
346
308
  this.instructionEl.textContent = text;
347
- }
348
309
  }
349
310
  stopTimer() {
350
311
  if (this.timerInterval) {
351
312
  clearInterval(this.timerInterval);
352
313
  this.timerInterval = null;
353
314
  }
354
- if (this.timerEl) {
315
+ if (this.timerEl)
355
316
  this.timerEl.textContent = "";
356
- }
357
317
  }
358
318
  showResult(status, verificationId) {
359
319
  return new Promise((resolve) => {
@@ -364,40 +324,22 @@ class LivenessUI {
364
324
  if (this.actionCueEl)
365
325
  this.actionCueEl.style.display = "none";
366
326
  this.stopTimer();
367
- const isProcessing = status === "processing";
327
+ const ok = status === "processing";
368
328
  this.overlayEl.style.display = "flex";
369
329
  this.overlayEl.innerHTML = `
370
330
  <div style="text-align:center;color:#fff;animation:sc-pop-in 0.35s ease;padding:24px;">
371
- <div style="
372
- width:80px;height:80px;border-radius:50%;margin:0 auto 20px;
373
- display:flex;align-items:center;justify-content:center;
374
- font-size:36px;
375
- background:${isProcessing ? "rgba(34,197,94,0.2)" : "rgba(239,68,68,0.2)"};
376
- border:3px solid ${isProcessing ? "#22c55e" : "#ef4444"};
377
- ">${isProcessing ? "\u2713" : "\u2717"}</div>
378
- <div style="font-size:19px;font-weight:700;margin-bottom:8px;">
379
- ${isProcessing ? "Identity Check Submitted" : "Submission Failed"}
331
+ <div style="width:72px;height:72px;border-radius:50%;margin:0 auto 16px;display:flex;align-items:center;justify-content:center;font-size:32px;
332
+ background:${ok ? "rgba(34,197,94,0.2)" : "rgba(239,68,68,0.2)"};
333
+ border:3px solid ${ok ? "#22c55e" : "#ef4444"};">
334
+ ${ok ? "โœ“" : "โœ•"}
380
335
  </div>
381
- <div style="font-size:13px;opacity:0.65;line-height:1.6;margin-bottom:${verificationId ? "16px" : "0"};">
382
- ${isProcessing
383
- ? "Your identity is being verified.<br>You\'ll be notified of the outcome shortly."
384
- : "Something went wrong. Please try again."}
385
- </div>
386
- ${verificationId ? `
387
- <div style="
388
- margin-top:4px;padding:8px 14px;border-radius:8px;
389
- background:rgba(255,255,255,0.08);display:inline-block;
390
- font-size:11px;color:rgba(255,255,255,0.5);letter-spacing:0.3px;
391
- ">Ref: #${verificationId}</div>` : ""}
392
- </div>
393
- `;
336
+ <div style="font-size:17px;font-weight:700;margin-bottom:8px;">${ok ? "Identity Check Submitted" : "Submission Failed"}</div>
337
+ <div style="font-size:12px;opacity:0.6;line-height:1.6;">${ok ? "Your identity is being verified.<br>You'll be notified of the outcome shortly." : "Something went wrong. Please try again."}</div>
338
+ ${verificationId ? `<div style="margin-top:12px;padding:6px 12px;border-radius:8px;background:rgba(255,255,255,0.07);display:inline-block;font-size:11px;color:rgba(255,255,255,0.4);">Ref: #${verificationId}</div>` : ""}
339
+ </div>`;
394
340
  setTimeout(resolve, 2800);
395
341
  });
396
342
  }
397
- /**
398
- * Show timeout screen with an optional retry callback.
399
- * Resolves immediately so the orchestrator can move to next step.
400
- */
401
343
  showTimeout(onRetry) {
402
344
  return new Promise((resolve) => {
403
345
  if (!this.overlayEl) {
@@ -410,58 +352,26 @@ class LivenessUI {
410
352
  this.overlayEl.style.display = "flex";
411
353
  this.overlayEl.innerHTML = `
412
354
  <div style="text-align:center;color:#fff;animation:sc-pop-in 0.35s ease;padding:24px;">
413
- <div style="
414
- width:80px;height:80px;border-radius:50%;margin:0 auto 16px;
415
- display:flex;align-items:center;justify-content:center;
416
- font-size:36px;
417
- background:rgba(251,191,36,0.15);
418
- border:3px solid #fbbf24;
419
- ">โฑ</div>
420
- <div style="font-size:18px;font-weight:700;margin-bottom:8px;">Time's Up</div>
421
- <div style="font-size:13px;opacity:0.65;margin-bottom:20px;">
422
- You ran out of time. Please try again.
423
- </div>
424
- <button id="sc-retry-btn" style="
425
- padding:12px 28px;border-radius:10px;
426
- background:#3b82f6;color:#fff;
427
- border:none;cursor:pointer;
428
- font-size:14px;font-weight:600;
429
- width:100%;max-width:200px;
430
- ">Try Again</button>
431
- </div>
432
- `;
433
- // Wire retry button
434
- const retryBtn = this.overlayEl.querySelector("#sc-retry-btn");
435
- if (retryBtn) {
436
- retryBtn.addEventListener("click", () => {
437
- onRetry();
438
- resolve();
439
- });
355
+ <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>
356
+ <div style="font-size:17px;font-weight:700;margin-bottom:8px;">Time's Up</div>
357
+ <div style="font-size:12px;opacity:0.6;margin-bottom:18px;">You ran out of time. Please try again.</div>
358
+ <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>
359
+ </div>`;
360
+ const btn = this.overlayEl.querySelector("#sc-retry-btn");
361
+ if (btn) {
362
+ btn.addEventListener("click", () => { onRetry(); resolve(); });
440
363
  }
441
364
  else {
442
- // fallback โ€” auto-resolve after 5s
443
365
  setTimeout(resolve, 5000);
444
366
  }
445
367
  });
446
368
  }
447
- getVideoElement() {
448
- return this.videoEl;
449
- }
450
- /**
451
- * Mount a document scanning step before the liveness challenge.
452
- * Reuses the already-open camera stream โ€” no second permission prompt.
453
- * Resolves with the captured document Blob.
454
- */
369
+ getVideoElement() { return this.videoEl; }
455
370
  mountDocumentCapture(container, stream) {
456
371
  return new Promise((resolve, reject) => {
457
372
  injectLivenessKeyframes();
458
373
  const docRoot = document.createElement("div");
459
- docRoot.style.cssText = `
460
- position:relative;width:100%;max-width:420px;margin:0 auto;
461
- background:#0a0a0a;border-radius:16px;overflow:hidden;
462
- font-family:system-ui,-apple-system,sans-serif;
463
- box-shadow:0 8px 32px rgba(0,0,0,0.4);
464
- `;
374
+ docRoot.style.cssText = `position:relative;width:100%;max-width:420px;margin:0 auto;background:#0a0a0a;border-radius:16px;overflow:hidden;font-family:system-ui,-apple-system,sans-serif;box-shadow:0 8px 32px rgba(0,0,0,0.4);`;
465
375
  this.docCaptureRoot = docRoot;
466
376
  const docVideo = document.createElement("video");
467
377
  docVideo.autoplay = true;
@@ -469,50 +379,23 @@ class LivenessUI {
469
379
  docVideo.muted = true;
470
380
  docVideo.style.cssText = "width:100%;display:block;aspect-ratio:4/3;object-fit:cover;";
471
381
  docVideo.srcObject = stream;
472
- // Rectangle guide with blue corner accents
473
382
  const guideOverlay = document.createElement("div");
474
383
  guideOverlay.style.cssText = "position:absolute;inset:0;pointer-events:none;display:flex;align-items:center;justify-content:center;";
475
- guideOverlay.innerHTML = `
476
- <div style="width:83%;height:53%;border:2px dashed rgba(255,255,255,0.55);border-radius:10px;box-shadow:0 0 0 2000px rgba(0,0,0,0.45);position:relative;">
477
- <div style="position:absolute;top:-2px;left:-2px;width:18px;height:18px;border-top:3px solid #3b82f6;border-left:3px solid #3b82f6;border-radius:4px 0 0 0;"></div>
478
- <div style="position:absolute;top:-2px;right:-2px;width:18px;height:18px;border-top:3px solid #3b82f6;border-right:3px solid #3b82f6;border-radius:0 4px 0 0;"></div>
479
- <div style="position:absolute;bottom:-2px;left:-2px;width:18px;height:18px;border-bottom:3px solid #3b82f6;border-left:3px solid #3b82f6;border-radius:0 0 0 4px;"></div>
480
- <div style="position:absolute;bottom:-2px;right:-2px;width:18px;height:18px;border-bottom:3px solid #3b82f6;border-right:3px solid #3b82f6;border-radius:0 0 4px 0;"></div>
481
- </div>
482
- `;
384
+ guideOverlay.innerHTML = `<div style="width:83%;height:53%;border:2px dashed rgba(255,255,255,0.55);border-radius:10px;box-shadow:0 0 0 2000px rgba(0,0,0,0.45);position:relative;"><div style="position:absolute;top:-2px;left:-2px;width:18px;height:18px;border-top:3px solid #3b82f6;border-left:3px solid #3b82f6;border-radius:4px 0 0 0;"></div><div style="position:absolute;top:-2px;right:-2px;width:18px;height:18px;border-top:3px solid #3b82f6;border-right:3px solid #3b82f6;border-radius:0 4px 0 0;"></div><div style="position:absolute;bottom:-2px;left:-2px;width:18px;height:18px;border-bottom:3px solid #3b82f6;border-left:3px solid #3b82f6;border-radius:0 0 0 4px;"></div><div style="position:absolute;bottom:-2px;right:-2px;width:18px;height:18px;border-bottom:3px solid #3b82f6;border-right:3px solid #3b82f6;border-radius:0 0 4px 0;"></div></div>`;
483
385
  const header = document.createElement("div");
484
386
  header.style.cssText = "position:absolute;top:0;left:0;right:0;background:linear-gradient(180deg,rgba(0,0,0,0.88) 0%,transparent 100%);padding:18px 16px 40px;text-align:center;";
485
- header.innerHTML = `
486
- <div style="color:#fff;font-size:16px;font-weight:700;margin-bottom:6px;">Scan Your ID Document</div>
487
- <div style="color:rgba(255,255,255,0.6);font-size:12px;line-height:1.6;">
488
- Fit your document inside the frame.<br>Make sure all text and corners are visible.
489
- </div>
490
- `;
387
+ 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>`;
491
388
  const bottomPanel = document.createElement("div");
492
- 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 16px;";
493
- bottomPanel.innerHTML = `
494
- <div style="font-size:11px;color:rgba(255,255,255,0.4);text-align:center;margin-bottom:12px;">
495
- ๐Ÿ’ก Good lighting &nbsp;ยท&nbsp; No glare &nbsp;ยท&nbsp; All 4 corners visible
496
- </div>
497
- <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;">
498
- Capture Document
499
- </button>
500
- <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;">
501
- <span>๐Ÿ“</span><span>Upload from gallery instead</span>
502
- <input id="sc-upload-doc-input" type="file" accept="image/*" style="display:none;" />
503
- </label>
504
- `;
389
+ 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;";
390
+ 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>`;
505
391
  docRoot.appendChild(docVideo);
506
392
  docRoot.appendChild(guideOverlay);
507
393
  docRoot.appendChild(header);
508
394
  docRoot.appendChild(bottomPanel);
509
395
  container.appendChild(docRoot);
510
396
  docVideo.play().catch(() => { });
511
- const cleanup = () => {
512
- if (docRoot.parentElement)
513
- docRoot.parentElement.removeChild(docRoot);
514
- this.docCaptureRoot = null;
515
- };
397
+ const cleanup = () => { if (docRoot.parentElement)
398
+ docRoot.parentElement.removeChild(docRoot); this.docCaptureRoot = null; };
516
399
  const captureBtn = bottomPanel.querySelector("#sc-capture-doc-btn");
517
400
  if (captureBtn) {
518
401
  captureBtn.addEventListener("click", () => {
@@ -526,15 +409,13 @@ class LivenessUI {
526
409
  return;
527
410
  }
528
411
  ctx.drawImage(docVideo, 0, 0);
529
- canvas.toBlob((blob) => {
530
- if (blob) {
531
- cleanup();
532
- resolve(blob);
533
- }
534
- else {
535
- reject(new Error("Failed to capture document image"));
536
- }
537
- }, "image/jpeg", 0.92);
412
+ canvas.toBlob((blob) => { if (blob) {
413
+ cleanup();
414
+ resolve(blob);
415
+ }
416
+ else {
417
+ reject(new Error("Failed to capture document image"));
418
+ } }, "image/jpeg", 0.92);
538
419
  }
539
420
  catch (err) {
540
421
  reject(err);
@@ -543,23 +424,20 @@ class LivenessUI {
543
424
  }
544
425
  const uploadInput = bottomPanel.querySelector("#sc-upload-doc-input");
545
426
  if (uploadInput) {
546
- uploadInput.addEventListener("change", () => {
547
- const file = uploadInput.files?.[0];
548
- if (file) {
549
- cleanup();
550
- resolve(file);
551
- }
552
- });
427
+ uploadInput.addEventListener("change", () => { const file = uploadInput.files?.[0]; if (file) {
428
+ cleanup();
429
+ resolve(file);
430
+ } });
553
431
  }
554
432
  });
555
433
  }
556
434
  unmount() {
557
435
  this.stopTimer();
558
- if (this.docCaptureRoot && this.docCaptureRoot.parentElement) {
436
+ if (this.docCaptureRoot?.parentElement) {
559
437
  this.docCaptureRoot.parentElement.removeChild(this.docCaptureRoot);
560
438
  this.docCaptureRoot = null;
561
439
  }
562
- if (this.root && this.root.parentElement) {
440
+ if (this.root?.parentElement) {
563
441
  this.root.parentElement.removeChild(this.root);
564
442
  }
565
443
  this.root = null;
@@ -579,26 +457,20 @@ class LivenessUI {
579
457
  return;
580
458
  const m = Math.floor(remaining / 60);
581
459
  const s = remaining % 60;
582
- const display = m > 0 ? `${m}:${String(s).padStart(2, "0")}` : `${s}s`;
583
- this.timerEl.textContent = `${display} remaining`;
584
- if (remaining <= 5) {
460
+ this.timerEl.textContent = `${m > 0 ? `${m}:${String(s).padStart(2, "0")}` : `${s}s`} remaining`;
461
+ if (remaining <= 5)
585
462
  this.timerEl.style.color = "#ef4444";
586
- }
587
463
  };
588
464
  update();
589
- this.timerInterval = setInterval(() => {
590
- remaining--;
591
- update();
592
- if (remaining <= 0 && this.timerInterval) {
593
- clearInterval(this.timerInterval);
594
- }
595
- }, 1000);
465
+ this.timerInterval = setInterval(() => { remaining--; update(); if (remaining <= 0 && this.timerInterval) {
466
+ clearInterval(this.timerInterval);
467
+ } }, 1000);
596
468
  }
597
469
  describeAction(action) {
598
470
  const labels = {
599
471
  BLINK: "Blink your eyes",
600
- TURN_LEFT: "Turn your head right", // camera-left = user's right (mirror)
601
- TURN_RIGHT: "Turn your head left", // camera-right = user's left (mirror)
472
+ TURN_LEFT: "Turn your head to the side",
473
+ TURN_RIGHT: "Turn your head to the side",
602
474
  TURN_HEAD: "Turn your head side to side",
603
475
  OPEN_MOUTH: "Open your mouth wide",
604
476
  };