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.
- 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 +176 -304
- 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 +176 -304
- 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 +157 -264
- 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-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%
|
|
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,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:
|
|
201
|
-
padding:8px
|
|
202
|
-
background:rgba(255,255,255,0.
|
|
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.
|
|
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:
|
|
204
|
+
width:24px;height:24px;border-radius:50%;flex-shrink:0;
|
|
210
205
|
display:flex;align-items:center;justify-content:center;
|
|
211
|
-
font-size:
|
|
212
|
-
background:rgba(255,255,255,0.
|
|
213
|
-
border:
|
|
214
|
-
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;
|
|
215
210
|
`;
|
|
216
211
|
num.textContent = String(i + 1);
|
|
217
212
|
const label = document.createElement("div");
|
|
218
|
-
label.style.cssText = "color:#fff;font-size:
|
|
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.
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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.
|
|
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(
|
|
262
|
-
this.root.appendChild(
|
|
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.
|
|
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 = "
|
|
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.
|
|
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
|
|
274
|
+
const circ = 2 * Math.PI * Math.sqrt((64 * 64 + 86 * 86) / 2);
|
|
307
275
|
const conf = activeState ? activeState.confidence : 0;
|
|
308
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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:
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
|
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 ยท No glare ยท 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
|
-
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
|
433
|
+
if (this.docCaptureRoot?.parentElement) {
|
|
556
434
|
this.docCaptureRoot.parentElement.removeChild(this.docCaptureRoot);
|
|
557
435
|
this.docCaptureRoot = null;
|
|
558
436
|
}
|
|
559
|
-
if (this.root
|
|
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
|
-
|
|
580
|
-
|
|
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
|
-
|
|
588
|
-
|
|
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
|
|
598
|
-
TURN_RIGHT: "Turn your head
|
|
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
|
};
|