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