smartcomply-web-sdk 1.0.26 → 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.
- package/dist/camera/ActionDetector.js +5 -5
- package/dist/camera/ActionDetector.js.map +1 -1
- package/dist/esm/camera/ActionDetector.js +5 -5
- package/dist/esm/camera/ActionDetector.js.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.js +2 -7
- package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
- package/dist/esm/modules/liveness/LivenessUI.d.ts +0 -12
- package/dist/esm/modules/liveness/LivenessUI.d.ts.map +1 -1
- package/dist/esm/modules/liveness/LivenessUI.js +180 -310
- package/dist/esm/modules/liveness/LivenessUI.js.map +1 -1
- package/dist/esm/modules/liveness/liveness.js +2 -2
- package/dist/esm/modules/liveness/liveness.js.map +1 -1
- package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/flow/SmartComplyFlow.js +2 -7
- package/dist/flow/SmartComplyFlow.js.map +1 -1
- package/dist/modules/liveness/LivenessUI.d.ts +0 -12
- package/dist/modules/liveness/LivenessUI.d.ts.map +1 -1
- package/dist/modules/liveness/LivenessUI.js +180 -310
- package/dist/modules/liveness/LivenessUI.js.map +1 -1
- package/dist/modules/liveness/liveness.js +2 -2
- package/dist/modules/liveness/liveness.js.map +1 -1
- package/dist/smartcomply.browser.js +160 -271
- package/dist/smartcomply.browser.js.map +2 -2
- package/package.json +1 -1
|
@@ -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: "
|
|
9
|
-
TURN_LEFT: "
|
|
10
|
-
TURN_RIGHT: "
|
|
11
|
-
TURN_HEAD: "
|
|
12
|
-
OPEN_MOUTH: "
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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-left 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%
|
|
55
|
-
50%
|
|
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%
|
|
68
|
-
75%
|
|
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%
|
|
73
|
-
60%
|
|
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);
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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:
|
|
126
|
-
|
|
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
|
|
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", "
|
|
134
|
-
bgOval.setAttribute("cy", "
|
|
135
|
-
bgOval.setAttribute("rx", "
|
|
136
|
-
bgOval.setAttribute("ry", "
|
|
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.
|
|
139
|
-
bgOval.setAttribute("stroke-width", "2
|
|
140
|
-
bgOval.setAttribute("stroke-dasharray", "
|
|
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", "
|
|
144
|
-
confOval.setAttribute("cy", "
|
|
145
|
-
confOval.setAttribute("rx", "
|
|
146
|
-
confOval.setAttribute("ry", "
|
|
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
|
|
150
|
-
const
|
|
151
|
-
confOval.setAttribute("stroke-dasharray", `${
|
|
152
|
-
confOval.setAttribute("stroke-dashoffset", `${
|
|
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
|
-
//
|
|
160
|
-
const
|
|
161
|
-
|
|
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.
|
|
164
|
-
padding:
|
|
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:
|
|
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
|
-
|
|
173
|
-
//
|
|
174
|
-
const
|
|
175
|
-
|
|
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.
|
|
178
|
-
padding:24px
|
|
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.
|
|
184
|
-
margin-bottom:
|
|
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,85 +186,55 @@ class LivenessUI {
|
|
|
189
186
|
border-radius:2px;transition:width 0.4s ease;
|
|
190
187
|
`;
|
|
191
188
|
progressWrap.appendChild(this.progressBarEl);
|
|
192
|
-
|
|
193
|
-
// Steps — compact pill row in top-right corner, always visible
|
|
189
|
+
// Steps list
|
|
194
190
|
this.stepsEl = document.createElement("div");
|
|
195
|
-
this.stepsEl.style.cssText =
|
|
196
|
-
position:absolute;top:10px;right:10px;
|
|
197
|
-
display:flex;flex-direction:row;gap:6px;
|
|
198
|
-
flex-wrap:wrap;justify-content:flex-end;
|
|
199
|
-
z-index:6;pointer-events:none;
|
|
200
|
-
max-width:calc(100% - 20px);
|
|
201
|
-
`;
|
|
191
|
+
this.stepsEl.style.cssText = "display:flex;flex-direction:column;gap:6px;flex:1;";
|
|
202
192
|
for (let i = 0; i < challenge.actions.length; i++) {
|
|
203
193
|
const action = challenge.actions[i];
|
|
204
194
|
const step = document.createElement("div");
|
|
205
195
|
step.dataset.action = action;
|
|
206
196
|
step.style.cssText = `
|
|
207
|
-
display:flex;align-items:center;gap:
|
|
208
|
-
padding:
|
|
209
|
-
background:rgba(
|
|
210
|
-
|
|
211
|
-
border:1.5px solid rgba(255,255,255,0.15);
|
|
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);
|
|
212
201
|
transition:all 0.3s ease;
|
|
213
|
-
opacity:${i === 0 ? "1" : "0.
|
|
202
|
+
opacity:${i === 0 ? "1" : "0.45"};
|
|
214
203
|
`;
|
|
215
204
|
const num = document.createElement("div");
|
|
216
205
|
num.className = "sc-step-num";
|
|
217
206
|
num.style.cssText = `
|
|
218
|
-
width:
|
|
207
|
+
width:24px;height:24px;border-radius:50%;flex-shrink:0;
|
|
219
208
|
display:flex;align-items:center;justify-content:center;
|
|
220
|
-
font-size:
|
|
221
|
-
background:rgba(255,255,255,0.
|
|
222
|
-
border:1.5px solid rgba(255,255,255,0.
|
|
223
|
-
transition:all 0.3s ease;
|
|
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;
|
|
224
213
|
`;
|
|
225
214
|
num.textContent = String(i + 1);
|
|
215
|
+
const label = document.createElement("div");
|
|
216
|
+
label.style.cssText = "color:#fff;font-size:12px;font-weight:500;flex:1;line-height:1.3;";
|
|
217
|
+
label.textContent = this.describeAction(action);
|
|
226
218
|
const icon = document.createElement("div");
|
|
227
|
-
icon.
|
|
228
|
-
icon.style.cssText = "font-size:14px;flex-shrink:0;";
|
|
219
|
+
icon.style.cssText = "font-size:16px;flex-shrink:0;";
|
|
229
220
|
icon.textContent = ICONS[action] || "";
|
|
230
221
|
step.appendChild(num);
|
|
222
|
+
step.appendChild(label);
|
|
231
223
|
step.appendChild(icon);
|
|
232
224
|
this.stepsEl.appendChild(step);
|
|
233
225
|
}
|
|
234
|
-
|
|
235
|
-
this.
|
|
236
|
-
|
|
237
|
-
text-align:center;color:rgba(255,255,255,0.5);
|
|
238
|
-
font-size:11px;font-variant-numeric:tabular-nums;
|
|
239
|
-
`;
|
|
240
|
-
bottomPanel.appendChild(this.timerEl);
|
|
241
|
-
// Result overlay (used for success / failure / timeout)
|
|
226
|
+
stepsCol.appendChild(progressWrap);
|
|
227
|
+
stepsCol.appendChild(this.stepsEl);
|
|
228
|
+
// Result overlay — full root
|
|
242
229
|
this.overlayEl = document.createElement("div");
|
|
243
230
|
this.overlayEl.style.cssText = `
|
|
244
231
|
position:absolute;inset:0;display:none;align-items:center;
|
|
245
232
|
justify-content:center;z-index:10;
|
|
246
|
-
background:rgba(0,0,0,0.
|
|
247
|
-
transition:opacity 0.3s ease;
|
|
248
|
-
`;
|
|
249
|
-
// Action cue — large centred animation shown when an action is active
|
|
250
|
-
this.actionCueEl = document.createElement("div");
|
|
251
|
-
this.actionCueEl.style.cssText = `
|
|
252
|
-
position:absolute;
|
|
253
|
-
top:50%;left:50%;transform:translate(-50%,-50%);
|
|
254
|
-
z-index:5;pointer-events:none;
|
|
255
|
-
display:none;
|
|
256
|
-
text-align:center;
|
|
257
|
-
background:rgba(0,0,0,0.55);
|
|
258
|
-
backdrop-filter:blur(4px);
|
|
259
|
-
border-radius:20px;
|
|
260
|
-
padding:18px 28px;
|
|
261
|
-
min-width:160px;
|
|
262
|
-
border:1.5px solid rgba(255,255,255,0.15);
|
|
233
|
+
background:rgba(0,0,0,0.88);backdrop-filter:blur(8px);
|
|
263
234
|
`;
|
|
264
|
-
// Assemble
|
|
265
|
-
this.root.appendChild(
|
|
266
|
-
this.root.appendChild(
|
|
267
|
-
this.root.appendChild(topBar);
|
|
268
|
-
this.root.appendChild(this.stepsEl); // pinned top-right corner
|
|
269
|
-
this.root.appendChild(bottomPanel);
|
|
270
|
-
this.root.appendChild(this.actionCueEl);
|
|
235
|
+
// ── Assemble ──────────────────────────────────────────────────────
|
|
236
|
+
this.root.appendChild(camCol);
|
|
237
|
+
this.root.appendChild(stepsCol);
|
|
271
238
|
this.root.appendChild(this.overlayEl);
|
|
272
239
|
container.appendChild(this.root);
|
|
273
240
|
this.startTimer(challenge.time_limit_seconds);
|
|
@@ -278,7 +245,6 @@ class LivenessUI {
|
|
|
278
245
|
return;
|
|
279
246
|
let completed = 0;
|
|
280
247
|
const activeState = states.find((s) => s.active && !s.detected) ?? null;
|
|
281
|
-
const activeAction = activeState?.action ?? null;
|
|
282
248
|
for (const state of states) {
|
|
283
249
|
const step = this.stepsEl.querySelector(`[data-action="${state.action}"]`);
|
|
284
250
|
if (!step)
|
|
@@ -287,46 +253,42 @@ class LivenessUI {
|
|
|
287
253
|
if (state.detected) {
|
|
288
254
|
completed++;
|
|
289
255
|
step.style.opacity = "1";
|
|
290
|
-
step.style.background = "rgba(34,197,94,0.
|
|
256
|
+
step.style.background = "rgba(34,197,94,0.12)";
|
|
257
|
+
step.style.borderColor = "rgba(34,197,94,0.3)";
|
|
291
258
|
if (num) {
|
|
292
259
|
num.style.background = "#22c55e";
|
|
293
260
|
num.style.borderColor = "#22c55e";
|
|
294
|
-
num.textContent = "
|
|
261
|
+
num.textContent = "✓";
|
|
295
262
|
}
|
|
296
263
|
}
|
|
297
264
|
else if (state.active) {
|
|
298
265
|
step.style.opacity = "1";
|
|
299
|
-
step.style.background = "rgba(59,130,246,0.
|
|
266
|
+
step.style.background = "rgba(59,130,246,0.12)";
|
|
267
|
+
step.style.borderColor = "rgba(59,130,246,0.3)";
|
|
300
268
|
if (num) {
|
|
301
269
|
num.style.borderColor = "#3b82f6";
|
|
302
270
|
num.style.background = "rgba(59,130,246,0.3)";
|
|
303
271
|
}
|
|
304
272
|
}
|
|
305
273
|
}
|
|
306
|
-
// Update progress bar
|
|
307
274
|
const pct = this.totalActions > 0 ? (completed / this.totalActions) * 100 : 0;
|
|
308
275
|
this.progressBarEl.style.width = `${pct}%`;
|
|
309
|
-
// Update confidence ring for the active action
|
|
310
276
|
if (this.confidenceRingEl) {
|
|
311
|
-
const
|
|
277
|
+
const circ = 2 * Math.PI * Math.sqrt((64 * 64 + 86 * 86) / 2);
|
|
312
278
|
const conf = activeState ? activeState.confidence : 0;
|
|
313
|
-
|
|
314
|
-
this.confidenceRingEl.setAttribute("stroke-dashoffset", `${offset}`);
|
|
279
|
+
this.confidenceRingEl.setAttribute("stroke-dashoffset", `${circ * (1 - conf)}`);
|
|
315
280
|
this.confidenceRingEl.setAttribute("stroke", conf > 0.6 ? "#22c55e" : "#3b82f6");
|
|
316
281
|
}
|
|
317
|
-
|
|
318
|
-
this.updateActionCue(activeAction);
|
|
282
|
+
this.updateActionCue(activeState?.action ?? null);
|
|
319
283
|
}
|
|
320
284
|
updateActionCue(action) {
|
|
321
285
|
if (!this.actionCueEl)
|
|
322
286
|
return;
|
|
323
|
-
// No active action → hide cue
|
|
324
287
|
if (!action) {
|
|
325
288
|
this.actionCueEl.style.display = "none";
|
|
326
289
|
this.currentCueAction = null;
|
|
327
290
|
return;
|
|
328
291
|
}
|
|
329
|
-
// Same action already showing — no DOM thrash
|
|
330
292
|
if (action === this.currentCueAction)
|
|
331
293
|
return;
|
|
332
294
|
this.currentCueAction = action;
|
|
@@ -335,27 +297,23 @@ class LivenessUI {
|
|
|
335
297
|
this.actionCueEl.style.display = "none";
|
|
336
298
|
return;
|
|
337
299
|
}
|
|
338
|
-
// Inject content and animate in
|
|
339
300
|
this.actionCueEl.innerHTML = cueHtml;
|
|
340
301
|
this.actionCueEl.style.display = "block";
|
|
341
302
|
this.actionCueEl.style.animation = "none";
|
|
342
|
-
// Force reflow then trigger animation
|
|
343
303
|
void this.actionCueEl.offsetWidth;
|
|
344
304
|
this.actionCueEl.style.animation = "sc-pop-in 0.3s ease forwards";
|
|
345
305
|
}
|
|
346
306
|
updateInstruction(text) {
|
|
347
|
-
if (this.instructionEl)
|
|
307
|
+
if (this.instructionEl)
|
|
348
308
|
this.instructionEl.textContent = text;
|
|
349
|
-
}
|
|
350
309
|
}
|
|
351
310
|
stopTimer() {
|
|
352
311
|
if (this.timerInterval) {
|
|
353
312
|
clearInterval(this.timerInterval);
|
|
354
313
|
this.timerInterval = null;
|
|
355
314
|
}
|
|
356
|
-
if (this.timerEl)
|
|
315
|
+
if (this.timerEl)
|
|
357
316
|
this.timerEl.textContent = "";
|
|
358
|
-
}
|
|
359
317
|
}
|
|
360
318
|
showResult(status, verificationId) {
|
|
361
319
|
return new Promise((resolve) => {
|
|
@@ -366,40 +324,22 @@ class LivenessUI {
|
|
|
366
324
|
if (this.actionCueEl)
|
|
367
325
|
this.actionCueEl.style.display = "none";
|
|
368
326
|
this.stopTimer();
|
|
369
|
-
const
|
|
327
|
+
const ok = status === "processing";
|
|
370
328
|
this.overlayEl.style.display = "flex";
|
|
371
329
|
this.overlayEl.innerHTML = `
|
|
372
330
|
<div style="text-align:center;color:#fff;animation:sc-pop-in 0.35s ease;padding:24px;">
|
|
373
|
-
<div style="
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
background:${isProcessing ? "rgba(34,197,94,0.2)" : "rgba(239,68,68,0.2)"};
|
|
378
|
-
border:3px solid ${isProcessing ? "#22c55e" : "#ef4444"};
|
|
379
|
-
">${isProcessing ? "\u2713" : "\u2717"}</div>
|
|
380
|
-
<div style="font-size:19px;font-weight:700;margin-bottom:8px;">
|
|
381
|
-
${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 ? "✓" : "✕"}
|
|
382
335
|
</div>
|
|
383
|
-
<div style="font-size:
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
</div>
|
|
388
|
-
${verificationId ? `
|
|
389
|
-
<div style="
|
|
390
|
-
margin-top:4px;padding:8px 14px;border-radius:8px;
|
|
391
|
-
background:rgba(255,255,255,0.08);display:inline-block;
|
|
392
|
-
font-size:11px;color:rgba(255,255,255,0.5);letter-spacing:0.3px;
|
|
393
|
-
">Ref: #${verificationId}</div>` : ""}
|
|
394
|
-
</div>
|
|
395
|
-
`;
|
|
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>`;
|
|
396
340
|
setTimeout(resolve, 2800);
|
|
397
341
|
});
|
|
398
342
|
}
|
|
399
|
-
/**
|
|
400
|
-
* Show timeout screen with an optional retry callback.
|
|
401
|
-
* Resolves immediately so the orchestrator can move to next step.
|
|
402
|
-
*/
|
|
403
343
|
showTimeout(onRetry) {
|
|
404
344
|
return new Promise((resolve) => {
|
|
405
345
|
if (!this.overlayEl) {
|
|
@@ -412,58 +352,26 @@ class LivenessUI {
|
|
|
412
352
|
this.overlayEl.style.display = "flex";
|
|
413
353
|
this.overlayEl.innerHTML = `
|
|
414
354
|
<div style="text-align:center;color:#fff;animation:sc-pop-in 0.35s ease;padding:24px;">
|
|
415
|
-
<div style="
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
<div style="font-size:13px;opacity:0.65;margin-bottom:20px;">
|
|
424
|
-
You ran out of time. Please try again.
|
|
425
|
-
</div>
|
|
426
|
-
<button id="sc-retry-btn" style="
|
|
427
|
-
padding:12px 28px;border-radius:10px;
|
|
428
|
-
background:#3b82f6;color:#fff;
|
|
429
|
-
border:none;cursor:pointer;
|
|
430
|
-
font-size:14px;font-weight:600;
|
|
431
|
-
width:100%;max-width:200px;
|
|
432
|
-
">Try Again</button>
|
|
433
|
-
</div>
|
|
434
|
-
`;
|
|
435
|
-
// Wire retry button
|
|
436
|
-
const retryBtn = this.overlayEl.querySelector("#sc-retry-btn");
|
|
437
|
-
if (retryBtn) {
|
|
438
|
-
retryBtn.addEventListener("click", () => {
|
|
439
|
-
onRetry();
|
|
440
|
-
resolve();
|
|
441
|
-
});
|
|
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(); });
|
|
442
363
|
}
|
|
443
364
|
else {
|
|
444
|
-
// fallback — auto-resolve after 5s
|
|
445
365
|
setTimeout(resolve, 5000);
|
|
446
366
|
}
|
|
447
367
|
});
|
|
448
368
|
}
|
|
449
|
-
getVideoElement() {
|
|
450
|
-
return this.videoEl;
|
|
451
|
-
}
|
|
452
|
-
/**
|
|
453
|
-
* Mount a document scanning step before the liveness challenge.
|
|
454
|
-
* Reuses the already-open camera stream — no second permission prompt.
|
|
455
|
-
* Resolves with the captured document Blob.
|
|
456
|
-
*/
|
|
369
|
+
getVideoElement() { return this.videoEl; }
|
|
457
370
|
mountDocumentCapture(container, stream) {
|
|
458
371
|
return new Promise((resolve, reject) => {
|
|
459
372
|
injectLivenessKeyframes();
|
|
460
373
|
const docRoot = document.createElement("div");
|
|
461
|
-
docRoot.style.cssText = `
|
|
462
|
-
position:relative;width:100%;max-width:420px;margin:0 auto;
|
|
463
|
-
background:#0a0a0a;border-radius:16px;overflow:hidden;
|
|
464
|
-
font-family:system-ui,-apple-system,sans-serif;
|
|
465
|
-
box-shadow:0 8px 32px rgba(0,0,0,0.4);
|
|
466
|
-
`;
|
|
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);`;
|
|
467
375
|
this.docCaptureRoot = docRoot;
|
|
468
376
|
const docVideo = document.createElement("video");
|
|
469
377
|
docVideo.autoplay = true;
|
|
@@ -471,50 +379,23 @@ class LivenessUI {
|
|
|
471
379
|
docVideo.muted = true;
|
|
472
380
|
docVideo.style.cssText = "width:100%;display:block;aspect-ratio:4/3;object-fit:cover;";
|
|
473
381
|
docVideo.srcObject = stream;
|
|
474
|
-
// Rectangle guide with blue corner accents
|
|
475
382
|
const guideOverlay = document.createElement("div");
|
|
476
383
|
guideOverlay.style.cssText = "position:absolute;inset:0;pointer-events:none;display:flex;align-items:center;justify-content:center;";
|
|
477
|
-
guideOverlay.innerHTML =
|
|
478
|
-
<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;">
|
|
479
|
-
<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>
|
|
480
|
-
<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>
|
|
481
|
-
<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>
|
|
482
|
-
<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>
|
|
483
|
-
</div>
|
|
484
|
-
`;
|
|
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>`;
|
|
485
385
|
const header = document.createElement("div");
|
|
486
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;";
|
|
487
|
-
header.innerHTML =
|
|
488
|
-
<div style="color:#fff;font-size:16px;font-weight:700;margin-bottom:6px;">Scan Your ID Document</div>
|
|
489
|
-
<div style="color:rgba(255,255,255,0.6);font-size:12px;line-height:1.6;">
|
|
490
|
-
Fit your document inside the frame.<br>Make sure all text and corners are visible.
|
|
491
|
-
</div>
|
|
492
|
-
`;
|
|
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>`;
|
|
493
388
|
const bottomPanel = document.createElement("div");
|
|
494
|
-
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
|
|
495
|
-
bottomPanel.innerHTML =
|
|
496
|
-
<div style="font-size:11px;color:rgba(255,255,255,0.4);text-align:center;margin-bottom:12px;">
|
|
497
|
-
💡 Good lighting · No glare · All 4 corners visible
|
|
498
|
-
</div>
|
|
499
|
-
<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;">
|
|
500
|
-
Capture Document
|
|
501
|
-
</button>
|
|
502
|
-
<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;">
|
|
503
|
-
<span>📁</span><span>Upload from gallery instead</span>
|
|
504
|
-
<input id="sc-upload-doc-input" type="file" accept="image/*" style="display:none;" />
|
|
505
|
-
</label>
|
|
506
|
-
`;
|
|
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>`;
|
|
507
391
|
docRoot.appendChild(docVideo);
|
|
508
392
|
docRoot.appendChild(guideOverlay);
|
|
509
393
|
docRoot.appendChild(header);
|
|
510
394
|
docRoot.appendChild(bottomPanel);
|
|
511
395
|
container.appendChild(docRoot);
|
|
512
396
|
docVideo.play().catch(() => { });
|
|
513
|
-
const cleanup = () => {
|
|
514
|
-
|
|
515
|
-
docRoot.parentElement.removeChild(docRoot);
|
|
516
|
-
this.docCaptureRoot = null;
|
|
517
|
-
};
|
|
397
|
+
const cleanup = () => { if (docRoot.parentElement)
|
|
398
|
+
docRoot.parentElement.removeChild(docRoot); this.docCaptureRoot = null; };
|
|
518
399
|
const captureBtn = bottomPanel.querySelector("#sc-capture-doc-btn");
|
|
519
400
|
if (captureBtn) {
|
|
520
401
|
captureBtn.addEventListener("click", () => {
|
|
@@ -528,15 +409,13 @@ class LivenessUI {
|
|
|
528
409
|
return;
|
|
529
410
|
}
|
|
530
411
|
ctx.drawImage(docVideo, 0, 0);
|
|
531
|
-
canvas.toBlob((blob) => {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
}
|
|
539
|
-
}, "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);
|
|
540
419
|
}
|
|
541
420
|
catch (err) {
|
|
542
421
|
reject(err);
|
|
@@ -545,23 +424,20 @@ class LivenessUI {
|
|
|
545
424
|
}
|
|
546
425
|
const uploadInput = bottomPanel.querySelector("#sc-upload-doc-input");
|
|
547
426
|
if (uploadInput) {
|
|
548
|
-
uploadInput.addEventListener("change", () => {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
resolve(file);
|
|
553
|
-
}
|
|
554
|
-
});
|
|
427
|
+
uploadInput.addEventListener("change", () => { const file = uploadInput.files?.[0]; if (file) {
|
|
428
|
+
cleanup();
|
|
429
|
+
resolve(file);
|
|
430
|
+
} });
|
|
555
431
|
}
|
|
556
432
|
});
|
|
557
433
|
}
|
|
558
434
|
unmount() {
|
|
559
435
|
this.stopTimer();
|
|
560
|
-
if (this.docCaptureRoot
|
|
436
|
+
if (this.docCaptureRoot?.parentElement) {
|
|
561
437
|
this.docCaptureRoot.parentElement.removeChild(this.docCaptureRoot);
|
|
562
438
|
this.docCaptureRoot = null;
|
|
563
439
|
}
|
|
564
|
-
if (this.root
|
|
440
|
+
if (this.root?.parentElement) {
|
|
565
441
|
this.root.parentElement.removeChild(this.root);
|
|
566
442
|
}
|
|
567
443
|
this.root = null;
|
|
@@ -581,26 +457,20 @@ class LivenessUI {
|
|
|
581
457
|
return;
|
|
582
458
|
const m = Math.floor(remaining / 60);
|
|
583
459
|
const s = remaining % 60;
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
if (remaining <= 5) {
|
|
460
|
+
this.timerEl.textContent = `${m > 0 ? `${m}:${String(s).padStart(2, "0")}` : `${s}s`} remaining`;
|
|
461
|
+
if (remaining <= 5)
|
|
587
462
|
this.timerEl.style.color = "#ef4444";
|
|
588
|
-
}
|
|
589
463
|
};
|
|
590
464
|
update();
|
|
591
|
-
this.timerInterval = setInterval(() => {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (remaining <= 0 && this.timerInterval) {
|
|
595
|
-
clearInterval(this.timerInterval);
|
|
596
|
-
}
|
|
597
|
-
}, 1000);
|
|
465
|
+
this.timerInterval = setInterval(() => { remaining--; update(); if (remaining <= 0 && this.timerInterval) {
|
|
466
|
+
clearInterval(this.timerInterval);
|
|
467
|
+
} }, 1000);
|
|
598
468
|
}
|
|
599
469
|
describeAction(action) {
|
|
600
470
|
const labels = {
|
|
601
471
|
BLINK: "Blink your eyes",
|
|
602
|
-
TURN_LEFT: "Turn your head
|
|
603
|
-
TURN_RIGHT: "Turn your head
|
|
472
|
+
TURN_LEFT: "Turn your head to the side",
|
|
473
|
+
TURN_RIGHT: "Turn your head to the side",
|
|
604
474
|
TURN_HEAD: "Turn your head side to side",
|
|
605
475
|
OPEN_MOUTH: "Open your mouth wide",
|
|
606
476
|
};
|