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