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