@smileid/web-components 11.4.2 → 11.4.4
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/esm/{DocumentCaptureScreens-zEVFc_Kr.js → DocumentCaptureScreens-bLFW-yEM.js} +4 -4
- package/dist/esm/{DocumentCaptureScreens-zEVFc_Kr.js.map → DocumentCaptureScreens-bLFW-yEM.js.map} +1 -1
- package/dist/esm/{EndUserConsent-BXvS7t8z.js → EndUserConsent-D26UoVk5.js} +3 -3
- package/dist/esm/{EndUserConsent-BXvS7t8z.js.map → EndUserConsent-D26UoVk5.js.map} +1 -1
- package/dist/esm/{Navigation-BRFmg7s1.js → Navigation-nvehze1F.js} +2 -2
- package/dist/esm/{Navigation-BRFmg7s1.js.map → Navigation-nvehze1F.js.map} +1 -1
- package/dist/esm/{SelfieCaptureScreens-DsFp21uW.js → SelfieCaptureScreens-BXIs6_tl.js} +1837 -1801
- package/dist/esm/SelfieCaptureScreens-BXIs6_tl.js.map +1 -0
- package/dist/esm/{TotpConsent-Cn2DkVza.js → TotpConsent-owUOdKzP.js} +2 -2
- package/dist/esm/{TotpConsent-Cn2DkVza.js.map → TotpConsent-owUOdKzP.js.map} +1 -1
- package/dist/esm/combobox.js +1 -1
- package/dist/esm/document.js +1 -1
- package/dist/esm/end-user-consent.js +1 -1
- package/dist/esm/{index-DBUdxnp9.js → index-5Nn2kzHI.js} +4 -4
- package/dist/esm/{index-DBUdxnp9.js.map → index-5Nn2kzHI.js.map} +1 -1
- package/dist/esm/localisation.js +1 -1
- package/dist/esm/main.js +6 -6
- package/dist/esm/navigation.js +1 -1
- package/dist/esm/{package-Do9oHVnx.js → package-DmH-I6GW.js} +3 -3
- package/dist/esm/{package-Do9oHVnx.js.map → package-DmH-I6GW.js.map} +1 -1
- package/dist/esm/selfie.js +1 -1
- package/dist/esm/smart-camera-web.js +5 -5
- package/dist/esm/totp-consent.js +1 -1
- package/dist/smart-camera-web.js +59 -50
- package/dist/smart-camera-web.js.map +1 -1
- package/dist/types/main.d.ts +4 -4
- package/lib/components/selfie/src/selfie-capture-wrapper/SelfieCaptureWrapper.tsx +145 -81
- package/lib/components/selfie/src/smartselfie-capture/SmartSelfieCapture.tsx +72 -39
- package/lib/components/selfie/src/smartselfie-capture/hooks/useCamera.ts +12 -0
- package/lib/components/signature-pad/package.json +1 -1
- package/package.json +10 -1
- package/dist/esm/SelfieCaptureScreens-DsFp21uW.js.map +0 -1
package/dist/types/main.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ declare class ComboboxRoot extends HTMLElement {
|
|
|
38
38
|
declare class ComboboxTrigger extends HTMLElement {
|
|
39
39
|
handleKeyUp(event: any): void;
|
|
40
40
|
handleKeyDown(event: any): void;
|
|
41
|
+
handleInput(event: any): void;
|
|
42
|
+
handleChange(event: any): void;
|
|
41
43
|
handleSelection(event: any): void;
|
|
42
44
|
toggleExpansionState(): void;
|
|
43
45
|
get type(): string;
|
|
@@ -87,14 +89,13 @@ export declare class DocumentCaptureScreens extends HTMLElement {
|
|
|
87
89
|
setUpEventListeners(): void;
|
|
88
90
|
_publishSelectedImages(): void;
|
|
89
91
|
get hideInstructions(): boolean;
|
|
90
|
-
get autoCapture(): boolean;
|
|
91
|
-
get autoCaptureMode(): string;
|
|
92
92
|
get hideBackOfId(): boolean;
|
|
93
93
|
get showNavigation(): "" | "show-navigation";
|
|
94
94
|
get title(): string;
|
|
95
95
|
get documentCaptureModes(): string;
|
|
96
96
|
get documentType(): string;
|
|
97
97
|
get hideAttribution(): "" | "hide-attribution";
|
|
98
|
+
get newInstructions(): boolean;
|
|
98
99
|
get themeColor(): string;
|
|
99
100
|
handleBackEvents(): void;
|
|
100
101
|
handleCloseEvents(): void;
|
|
@@ -330,14 +331,13 @@ export declare class SmartCameraWeb extends HTMLElement {
|
|
|
330
331
|
get isPortraitCaptureView(): boolean;
|
|
331
332
|
get hideInstructions(): "" | "hide-instructions";
|
|
332
333
|
get hideBackOfId(): "" | "hide-back-of-id";
|
|
334
|
+
get newInstructions(): "" | "new-instructions";
|
|
333
335
|
get showNavigation(): "" | "show-navigation";
|
|
334
336
|
get hideBackToHost(): "" | "hide-back";
|
|
335
337
|
get allowAgentMode(): string;
|
|
336
338
|
get allowAgentModeTests(): "" | "show-agent-mode-for-tests";
|
|
337
339
|
get title(): string;
|
|
338
340
|
get documentCaptureModes(): string;
|
|
339
|
-
get autoCapture(): string;
|
|
340
|
-
get autoCaptureMode(): string;
|
|
341
341
|
get disableImageTests(): "" | "disable-image-tests";
|
|
342
342
|
get allowLegacySelfieFallback(): string;
|
|
343
343
|
get hideAttribution(): "" | "hide-attribution";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect } from 'preact/hooks';
|
|
1
|
+
import { useState, useEffect, useRef } from 'preact/hooks';
|
|
2
2
|
import { IconLoader2 } from '@tabler/icons-preact';
|
|
3
3
|
import register from 'preact-custom-element';
|
|
4
4
|
import type { FunctionComponent } from 'preact';
|
|
@@ -30,7 +30,7 @@ declare global {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
interface Props {
|
|
33
|
-
timeout?: number;
|
|
33
|
+
timeout?: string | number;
|
|
34
34
|
interval?: number;
|
|
35
35
|
duration?: number;
|
|
36
36
|
'theme-color'?: string;
|
|
@@ -45,8 +45,15 @@ interface Props {
|
|
|
45
45
|
hidden?: string | boolean;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
// Default deadlines for the Mediapipe load attempt. These are used when the
|
|
49
|
+
// host did NOT provide an explicit `timeout` prop; an explicit `timeout`
|
|
50
|
+
// always wins regardless of the legacy-fallback flag.
|
|
51
|
+
// - With legacy fallback allowed: 20s, so users on broken networks get a
|
|
52
|
+
// usable capture quickly.
|
|
53
|
+
// - Without legacy fallback: 90s, so we keep waiting before giving up to
|
|
54
|
+
// the error UI.
|
|
55
|
+
const DEFAULT_MEDIAPIPE_WAIT_MS = 90 * 1000;
|
|
56
|
+
const DEFAULT_WAIT_MS = 20 * 1000;
|
|
50
57
|
// Cap retries on transient init failures so we don't spin forever, while still
|
|
51
58
|
// allowing recovery from short-lived issues (e.g. CDN hiccups while the
|
|
52
59
|
// wrapper is preloading in a hidden state). Retries are spaced with
|
|
@@ -58,7 +65,7 @@ const MEDIAPIPE_RETRY_BASE_DELAY_MS = 500;
|
|
|
58
65
|
// SmartSelfieCapture (Mediapipe-based) or fallback to the legacy `selfie-capture`
|
|
59
66
|
// web component after a timeout (default 90 seconds).
|
|
60
67
|
const SelfieCaptureWrapper: FunctionComponent<Props> = ({
|
|
61
|
-
timeout
|
|
68
|
+
timeout,
|
|
62
69
|
'start-countdown': startCountdownProp = false,
|
|
63
70
|
'allow-legacy-selfie-fallback': allowLegacySelfieFallbackProp = false,
|
|
64
71
|
hidden: hiddenProp = false,
|
|
@@ -85,26 +92,51 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
|
|
|
85
92
|
const hidden = getBoolProp(hiddenProp);
|
|
86
93
|
const startCountdown = getBoolProp(startCountdownProp);
|
|
87
94
|
const allowLegacySelfieFallback = getBoolProp(allowLegacySelfieFallbackProp);
|
|
88
|
-
|
|
95
|
+
|
|
96
|
+
// Resolve how long we'll wait for Mediapipe before the hard deadline fires.
|
|
97
|
+
// Precedence:
|
|
98
|
+
// 1. Explicit `timeout` prop (parsed defensively because web component
|
|
99
|
+
// attributes arrive as strings).
|
|
100
|
+
// 2. With legacy fallback allowed: DEFAULT_WAIT_MS (20s).
|
|
101
|
+
// 3. Otherwise: DEFAULT_MEDIAPIPE_WAIT_MS (90s).
|
|
102
|
+
const parsedTimeout = typeof timeout === 'string' ? Number(timeout) : timeout;
|
|
103
|
+
const hasExplicitTimeout =
|
|
104
|
+
typeof parsedTimeout === 'number' &&
|
|
105
|
+
Number.isFinite(parsedTimeout) &&
|
|
106
|
+
parsedTimeout > 0;
|
|
107
|
+
const defaultLoadingTime = allowLegacySelfieFallback
|
|
108
|
+
? DEFAULT_WAIT_MS
|
|
109
|
+
: DEFAULT_MEDIAPIPE_WAIT_MS;
|
|
110
|
+
const loadingTime = hasExplicitTimeout
|
|
111
|
+
? (parsedTimeout as number)
|
|
112
|
+
: defaultLoadingTime;
|
|
89
113
|
|
|
90
114
|
// Component state:
|
|
91
115
|
// - mediapipeReady: whether the mediapipe instance has successfully loaded
|
|
92
|
-
// - loadingProgress: percentage used for the visible loading UI
|
|
116
|
+
// - loadingProgress: percentage used for the visible loading UI (cosmetic)
|
|
117
|
+
// - loadDeadlineExceeded: hard cap signal — once true, we stop waiting for
|
|
118
|
+
// Mediapipe and commit to the legacy fallback (or error UI). Kept
|
|
119
|
+
// separate from `loadingProgress` so the decision is driven by a single
|
|
120
|
+
// setTimeout firing once, not by a 200ms ticking interval that can race
|
|
121
|
+
// with the Mediapipe promise resolution.
|
|
93
122
|
// - initialSessionCompleted: set when the legacy component emits publish/cancel/close
|
|
94
123
|
// - mediapipeLoading: true while attempting to load mediapipe
|
|
95
124
|
// - usingSelfieCapture: whether we've mounted the legacy `selfie-capture` element
|
|
96
125
|
const [mediapipeReady, setMediapipeReady] = useState(false);
|
|
97
126
|
const [loadingProgress, setLoadingProgress] = useState(isCypress ? 100 : 0);
|
|
127
|
+
const [loadDeadlineExceeded, setLoadDeadlineExceeded] = useState(isCypress);
|
|
98
128
|
const [initialSessionCompleted, setInitialSessionCompleted] = useState(false);
|
|
99
129
|
const [mediapipeLoading, setMediapipeLoading] = useState(false);
|
|
100
130
|
// `unsupportedEnvironment` is a permanent, one-shot signal: we know
|
|
101
131
|
// MediaPipe cannot run here, so stop trying.
|
|
102
132
|
const [unsupportedEnvironment, setUnsupportedEnvironment] = useState(false);
|
|
103
|
-
// Bounded retry counter for transient init failures.
|
|
104
|
-
|
|
133
|
+
// Bounded retry counter for transient init failures. Stored in a ref so
|
|
134
|
+
// incrementing it does not trigger a re-render — it's only read inside the
|
|
135
|
+
// load effect.
|
|
136
|
+
const mediapipeInitAttemptsRef = useRef(0);
|
|
105
137
|
// Dedup flag so we only report a given init failure to Sentry once per
|
|
106
|
-
// wrapper instance, even if we end up retrying.
|
|
107
|
-
const
|
|
138
|
+
// wrapper instance, even if we end up retrying. Ref for the same reason.
|
|
139
|
+
const mediapipeInitReportedRef = useRef(false);
|
|
108
140
|
const [usingSelfieCapture, setUsingSelfieCapture] = useState(false);
|
|
109
141
|
|
|
110
142
|
// Attempt to load Mediapipe (with a small bounded retry budget). If
|
|
@@ -117,7 +149,7 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
|
|
|
117
149
|
mediapipeReady ||
|
|
118
150
|
mediapipeLoading ||
|
|
119
151
|
unsupportedEnvironment ||
|
|
120
|
-
|
|
152
|
+
mediapipeInitAttemptsRef.current >= MAX_MEDIAPIPE_INIT_ATTEMPTS ||
|
|
121
153
|
isCypress
|
|
122
154
|
)
|
|
123
155
|
return undefined;
|
|
@@ -127,8 +159,8 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
|
|
|
127
159
|
|
|
128
160
|
const loadMediapipe = async () => {
|
|
129
161
|
setMediapipeLoading(true);
|
|
130
|
-
const attemptNumber =
|
|
131
|
-
|
|
162
|
+
const attemptNumber = mediapipeInitAttemptsRef.current + 1;
|
|
163
|
+
mediapipeInitAttemptsRef.current = attemptNumber;
|
|
132
164
|
try {
|
|
133
165
|
await getMediapipeInstance();
|
|
134
166
|
if (cancelled) return;
|
|
@@ -145,8 +177,8 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
|
|
|
145
177
|
// Report to Sentry (when the host page has exposed it on window) so we
|
|
146
178
|
// can observe how often users land on the fallback path and which
|
|
147
179
|
// environments are affected. Dedup so retries don't flood Sentry.
|
|
148
|
-
if (!
|
|
149
|
-
|
|
180
|
+
if (!mediapipeInitReportedRef.current) {
|
|
181
|
+
mediapipeInitReportedRef.current = true;
|
|
150
182
|
window.Sentry?.captureException(error, {
|
|
151
183
|
tags: {
|
|
152
184
|
area: 'mediapipe_init',
|
|
@@ -164,6 +196,7 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
|
|
|
164
196
|
if (isUnsupportedEnvironment) {
|
|
165
197
|
setUnsupportedEnvironment(true);
|
|
166
198
|
setLoadingProgress(100);
|
|
199
|
+
setLoadDeadlineExceeded(true);
|
|
167
200
|
setMediapipeLoading(false);
|
|
168
201
|
return;
|
|
169
202
|
}
|
|
@@ -194,22 +227,23 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
|
|
|
194
227
|
clearTimeout(retryTimeoutId);
|
|
195
228
|
}
|
|
196
229
|
};
|
|
197
|
-
}, [
|
|
198
|
-
mediapipeReady,
|
|
199
|
-
mediapipeLoading,
|
|
200
|
-
unsupportedEnvironment,
|
|
201
|
-
mediapipeInitAttempts,
|
|
202
|
-
mediapipeInitReported,
|
|
203
|
-
]);
|
|
230
|
+
}, [mediapipeReady, mediapipeLoading, unsupportedEnvironment]);
|
|
204
231
|
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
232
|
+
// Cosmetic loading progress: ticks 0→100 over `loadingTime` so the UI can
|
|
233
|
+
// show "slow connection" copy past the SLOW_CONNECTION_THRESHOLD. This is
|
|
234
|
+
// purely visual — it does NOT decide when we fall back. The decision is
|
|
235
|
+
// driven by `loadDeadlineExceeded` below.
|
|
208
236
|
useEffect(() => {
|
|
209
237
|
if (hidden || !startCountdown || mediapipeReady) return undefined;
|
|
210
238
|
|
|
211
239
|
const timer = setInterval(() => {
|
|
212
|
-
setLoadingProgress((prev: number) =>
|
|
240
|
+
setLoadingProgress((prev: number) => {
|
|
241
|
+
if (prev >= 100) {
|
|
242
|
+
clearInterval(timer);
|
|
243
|
+
return 100;
|
|
244
|
+
}
|
|
245
|
+
return prev + 1;
|
|
246
|
+
});
|
|
213
247
|
}, loadingTime / 100);
|
|
214
248
|
|
|
215
249
|
return () => {
|
|
@@ -217,8 +251,43 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
|
|
|
217
251
|
};
|
|
218
252
|
}, [hidden, startCountdown, loadingTime, mediapipeReady]);
|
|
219
253
|
|
|
254
|
+
// Hard deadline: a single setTimeout that flips `loadDeadlineExceeded`
|
|
255
|
+
// exactly once. This is the signal the render path uses to commit to the
|
|
256
|
+
// fallback. Skipped when hidden, when Mediapipe is already ready, or under
|
|
257
|
+
// Cypress (where the flag is pre-seeded to true).
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
if (hidden || mediapipeReady || loadDeadlineExceeded || isCypress)
|
|
260
|
+
return undefined;
|
|
261
|
+
|
|
262
|
+
const id = setTimeout(() => {
|
|
263
|
+
setLoadDeadlineExceeded(true);
|
|
264
|
+
}, loadingTime);
|
|
265
|
+
|
|
266
|
+
return () => clearTimeout(id);
|
|
267
|
+
}, [hidden, mediapipeReady, loadDeadlineExceeded, loadingTime, isCypress]);
|
|
268
|
+
|
|
269
|
+
// Latch the legacy fallback decision in an effect rather than during
|
|
270
|
+
// render. Effects only run after commit, so by the time this runs, any
|
|
271
|
+
// Mediapipe-ready update scheduled in the same batch as the deadline tick
|
|
272
|
+
// will already be visible — closing the race where `setLoadDeadlineExceeded`
|
|
273
|
+
// and `setMediapipeReady` fire in adjacent microtasks.
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
if (hidden || usingSelfieCapture || mediapipeReady) return;
|
|
276
|
+
if (!loadDeadlineExceeded) return;
|
|
277
|
+
const legacyFallbackAllowed = allowLegacySelfieFallback || isCypress;
|
|
278
|
+
if (!legacyFallbackAllowed) return;
|
|
279
|
+
setUsingSelfieCapture(true);
|
|
280
|
+
}, [
|
|
281
|
+
hidden,
|
|
282
|
+
usingSelfieCapture,
|
|
283
|
+
mediapipeReady,
|
|
284
|
+
loadDeadlineExceeded,
|
|
285
|
+
allowLegacySelfieFallback,
|
|
286
|
+
isCypress,
|
|
287
|
+
]);
|
|
288
|
+
|
|
220
289
|
useEffect(() => {
|
|
221
|
-
if (hidden || mediapipeReady ||
|
|
290
|
+
if (hidden || mediapipeReady || !loadDeadlineExceeded) return undefined;
|
|
222
291
|
|
|
223
292
|
const setupEventForwarding = () => {
|
|
224
293
|
const selfieCapture = document.querySelector('selfie-capture');
|
|
@@ -267,7 +336,7 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
|
|
|
267
336
|
return () => {
|
|
268
337
|
clearTimeout(timeoutId);
|
|
269
338
|
};
|
|
270
|
-
}, [hidden, mediapipeReady,
|
|
339
|
+
}, [hidden, mediapipeReady, loadDeadlineExceeded]);
|
|
271
340
|
|
|
272
341
|
// Dispatch allow_legacy_selfie_fallback config for observability
|
|
273
342
|
useEffect(() => {
|
|
@@ -312,60 +381,55 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
|
|
|
312
381
|
return <SmartSelfieCapture {...props} />;
|
|
313
382
|
}
|
|
314
383
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
384
|
+
// Legacy capture is mounted only once the latch effect has set
|
|
385
|
+
// `usingSelfieCapture`. The effect re-checks `mediapipeReady` after commit,
|
|
386
|
+
// so if Mediapipe became ready in the same batch as the deadline tick, the
|
|
387
|
+
// latch is skipped and the SmartSelfie branch above wins. While we're
|
|
388
|
+
// waiting for the effect to fire (a single microtask), keep showing the
|
|
389
|
+
// spinner instead of flashing legacy.
|
|
390
|
+
if (usingSelfieCapture) {
|
|
391
|
+
const propsWithoutHidden = { ...props };
|
|
392
|
+
delete (propsWithoutHidden as any).hidden;
|
|
321
393
|
|
|
322
|
-
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
};
|
|
358
|
-
|
|
359
|
-
el.addEventListener('selfie-capture.publish', forwardEvent);
|
|
360
|
-
el.addEventListener('selfie-capture.cancelled', forwardEvent);
|
|
361
|
-
el.addEventListener('selfie-capture.close', forwardEvent);
|
|
362
|
-
}
|
|
363
|
-
}}
|
|
364
|
-
/>
|
|
365
|
-
);
|
|
366
|
-
}
|
|
394
|
+
return (
|
|
395
|
+
// @ts-expect-error --- preact-custom-element doesn't have proper types for refs
|
|
396
|
+
<selfie-capture
|
|
397
|
+
{...propsWithoutHidden}
|
|
398
|
+
ref={(el: HTMLElement) => {
|
|
399
|
+
if (el && !el.hasAttribute('data-events-setup')) {
|
|
400
|
+
el.setAttribute('data-events-setup', 'true');
|
|
401
|
+
|
|
402
|
+
const forwardEvent = (event: Event) => {
|
|
403
|
+
const customEvent = event as CustomEvent;
|
|
404
|
+
|
|
405
|
+
if (
|
|
406
|
+
customEvent.type === 'selfie-capture.publish' ||
|
|
407
|
+
customEvent.type === 'selfie-capture.cancelled' ||
|
|
408
|
+
customEvent.type === 'selfie-capture.close'
|
|
409
|
+
) {
|
|
410
|
+
setInitialSessionCompleted(true);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
window.dispatchEvent(
|
|
414
|
+
new CustomEvent(customEvent.type, {
|
|
415
|
+
detail: customEvent.detail,
|
|
416
|
+
bubbles: true,
|
|
417
|
+
}),
|
|
418
|
+
);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
el.addEventListener('selfie-capture.publish', forwardEvent);
|
|
422
|
+
el.addEventListener('selfie-capture.cancelled', forwardEvent);
|
|
423
|
+
el.addEventListener('selfie-capture.close', forwardEvent);
|
|
424
|
+
}
|
|
425
|
+
}}
|
|
426
|
+
/>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
367
429
|
|
|
368
|
-
|
|
430
|
+
// Hard deadline elapsed and legacy fallback isn't allowed: show the
|
|
431
|
+
// connection error.
|
|
432
|
+
if (loadDeadlineExceeded && !(allowLegacySelfieFallback || isCypress)) {
|
|
369
433
|
return (
|
|
370
434
|
<div style={{ textAlign: 'center', marginTop: '20%', padding: '0 20px' }}>
|
|
371
435
|
<p style={{ fontSize: '1.2rem', color: '#333' }}>
|
|
@@ -3,6 +3,7 @@ import register from 'preact-custom-element';
|
|
|
3
3
|
import type { FunctionComponent } from 'preact';
|
|
4
4
|
|
|
5
5
|
import { getBoolProp } from '../../../../utils/props';
|
|
6
|
+
import { translate } from '../../../../domain/localisation';
|
|
6
7
|
import { useFaceCapture, useCamera } from './hooks';
|
|
7
8
|
import { CameraPreview } from './components/CameraPreview';
|
|
8
9
|
import { AlertDisplay } from './components/AlertDisplay';
|
|
@@ -61,8 +62,8 @@ const SmartSelfieCapture: FunctionComponent<Props> = ({
|
|
|
61
62
|
getFacingMode: () => camera.facingMode,
|
|
62
63
|
});
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
const initializeCamera = async () => {
|
|
66
|
+
try {
|
|
66
67
|
await camera.startCamera(initialFacingMode, (cameraName) => {
|
|
67
68
|
const smartCameraWeb = document.querySelector('smart-camera-web');
|
|
68
69
|
smartCameraWeb?.dispatchEvent(
|
|
@@ -71,15 +72,20 @@ const SmartSelfieCapture: FunctionComponent<Props> = ({
|
|
|
71
72
|
}),
|
|
72
73
|
);
|
|
73
74
|
});
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
} catch {
|
|
76
|
+
// startCamera already set cameraError; surface UI and skip downstream init
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
await camera.checkAgentSupport();
|
|
80
|
+
await faceCapture.initializeFaceLandmarker();
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
faceCapture.setupCanvas();
|
|
84
|
+
faceCapture.startDetectionLoop();
|
|
85
|
+
}, 500);
|
|
86
|
+
};
|
|
82
87
|
|
|
88
|
+
useEffect(() => {
|
|
83
89
|
camera.registerCameraSwitchCallback(() => {
|
|
84
90
|
try {
|
|
85
91
|
faceCapture.resetFaceDetectionState();
|
|
@@ -142,39 +148,57 @@ const SmartSelfieCapture: FunctionComponent<Props> = ({
|
|
|
142
148
|
<smileid-navigation ref={navigationRef} theme-color={themeColor} />
|
|
143
149
|
)}
|
|
144
150
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
isCapturing={faceCapture.isCapturing.value}
|
|
164
|
-
hasFinishedCapture={faceCapture.hasFinishedCapture.value}
|
|
165
|
-
isReadyToCapture={faceCapture.isReadyToCapture.value}
|
|
166
|
-
captureButtonFallbackEnabled={
|
|
167
|
-
faceCapture.captureButtonFallbackEnabled.value
|
|
168
|
-
}
|
|
169
|
-
allowAgentMode={allowAgentMode}
|
|
170
|
-
agentSupported={camera.agentSupported}
|
|
171
|
-
showAgentModeForTests={showAgentModeForTests}
|
|
151
|
+
{camera.cameraError ? (
|
|
152
|
+
<div className="camera-error" role="alert">
|
|
153
|
+
<p>{camera.cameraError}</p>
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
className="btn-primary"
|
|
157
|
+
onClick={() => {
|
|
158
|
+
initializeCamera();
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
{translate('camera.error.retry')}
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
164
|
+
) : (
|
|
165
|
+
<>
|
|
166
|
+
<CameraPreview
|
|
167
|
+
videoRef={camera.videoRef}
|
|
168
|
+
canvasRef={canvasRef}
|
|
172
169
|
facingMode={camera.facingMode}
|
|
170
|
+
progress={
|
|
171
|
+
faceCapture.capturesTaken.value > 0
|
|
172
|
+
? faceCapture.capturesTaken.value /
|
|
173
|
+
faceCapture.totalCaptures.value
|
|
174
|
+
: 0
|
|
175
|
+
}
|
|
176
|
+
interval={interval}
|
|
173
177
|
themeColor={themeColor}
|
|
174
|
-
onStartCapture={faceCapture.startCapture}
|
|
175
|
-
onSwitchCamera={camera.switchCamera}
|
|
176
178
|
/>
|
|
177
|
-
|
|
179
|
+
|
|
180
|
+
<AlertDisplay alertTitle={faceCapture.alertTitle.value} />
|
|
181
|
+
|
|
182
|
+
{!faceCapture.isCapturing.value &&
|
|
183
|
+
!faceCapture.hasFinishedCapture.value && (
|
|
184
|
+
<CaptureControls
|
|
185
|
+
isCapturing={faceCapture.isCapturing.value}
|
|
186
|
+
hasFinishedCapture={faceCapture.hasFinishedCapture.value}
|
|
187
|
+
isReadyToCapture={faceCapture.isReadyToCapture.value}
|
|
188
|
+
captureButtonFallbackEnabled={
|
|
189
|
+
faceCapture.captureButtonFallbackEnabled.value
|
|
190
|
+
}
|
|
191
|
+
allowAgentMode={allowAgentMode}
|
|
192
|
+
agentSupported={camera.agentSupported}
|
|
193
|
+
showAgentModeForTests={showAgentModeForTests}
|
|
194
|
+
facingMode={camera.facingMode}
|
|
195
|
+
themeColor={themeColor}
|
|
196
|
+
onStartCapture={faceCapture.startCapture}
|
|
197
|
+
onSwitchCamera={camera.switchCamera}
|
|
198
|
+
/>
|
|
199
|
+
)}
|
|
200
|
+
</>
|
|
201
|
+
)}
|
|
178
202
|
|
|
179
203
|
{/* @ts-expect-error -- preact-custom-element doesn't have proper types for refs */}
|
|
180
204
|
{!hideAttribution && <powered-by-smile-id />}
|
|
@@ -227,6 +251,15 @@ const SmartSelfieCapture: FunctionComponent<Props> = ({
|
|
|
227
251
|
padding: 1rem;
|
|
228
252
|
font-family: sans-serif;
|
|
229
253
|
}
|
|
254
|
+
|
|
255
|
+
.camera-error {
|
|
256
|
+
margin-top: 1.5rem;
|
|
257
|
+
padding: 1rem 1.5rem;
|
|
258
|
+
color: #b00020;
|
|
259
|
+
text-align: center;
|
|
260
|
+
font-size: 1rem;
|
|
261
|
+
font-weight: 500;
|
|
262
|
+
}
|
|
230
263
|
`}</style>
|
|
231
264
|
</div>
|
|
232
265
|
);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { useRef, useState, useEffect } from 'preact/hooks';
|
|
2
|
+
import SmartCamera from '../../../../../domain/camera/src/SmartCamera';
|
|
2
3
|
|
|
3
4
|
export const useCamera = (initialFacingMode: CameraFacingMode = 'user') => {
|
|
4
5
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
5
6
|
const streamRef = useRef<MediaStream | null>(null);
|
|
6
7
|
const [facingMode, setFacingMode] = useState(initialFacingMode);
|
|
7
8
|
const [agentSupported, setAgentSupported] = useState(false);
|
|
9
|
+
const [cameraError, setCameraError] = useState<string | null>(null);
|
|
8
10
|
const onCameraSwitchCallbackRef = useRef<(() => void) | null>(null);
|
|
9
11
|
const isSwitchingCameraRef = useRef(false);
|
|
10
12
|
const timeoutIdsRef = useRef<Set<NodeJS.Timeout>>(new Set());
|
|
@@ -63,6 +65,7 @@ export const useCamera = (initialFacingMode: CameraFacingMode = 'user') => {
|
|
|
63
65
|
video: { facingMode: targetFacingMode || facingMode },
|
|
64
66
|
});
|
|
65
67
|
streamRef.current = stream;
|
|
68
|
+
setCameraError(null);
|
|
66
69
|
|
|
67
70
|
const track = stream.getVideoTracks()[0];
|
|
68
71
|
const settings = track.getSettings();
|
|
@@ -94,9 +97,15 @@ export const useCamera = (initialFacingMode: CameraFacingMode = 'user') => {
|
|
|
94
97
|
}
|
|
95
98
|
} catch (error) {
|
|
96
99
|
console.error('Failed to start camera:', error);
|
|
100
|
+
setCameraError(SmartCamera.handleCameraError(error as Error));
|
|
101
|
+
throw error;
|
|
97
102
|
}
|
|
98
103
|
};
|
|
99
104
|
|
|
105
|
+
const retryCamera = async () => {
|
|
106
|
+
await startCamera(facingMode);
|
|
107
|
+
};
|
|
108
|
+
|
|
100
109
|
const switchCamera = async () => {
|
|
101
110
|
const newFacingMode = facingMode === 'user' ? 'environment' : 'user';
|
|
102
111
|
isSwitchingCameraRef.current = true;
|
|
@@ -118,6 +127,7 @@ export const useCamera = (initialFacingMode: CameraFacingMode = 'user') => {
|
|
|
118
127
|
await startCamera(previousFacingMode);
|
|
119
128
|
} catch (restoreError) {
|
|
120
129
|
console.error('Failed to restore previous camera:', restoreError);
|
|
130
|
+
setCameraError(SmartCamera.handleCameraError(restoreError as Error));
|
|
121
131
|
}
|
|
122
132
|
}
|
|
123
133
|
};
|
|
@@ -229,7 +239,9 @@ export const useCamera = (initialFacingMode: CameraFacingMode = 'user') => {
|
|
|
229
239
|
streamRef,
|
|
230
240
|
facingMode,
|
|
231
241
|
agentSupported,
|
|
242
|
+
cameraError,
|
|
232
243
|
startCamera,
|
|
244
|
+
retryCamera,
|
|
233
245
|
switchCamera,
|
|
234
246
|
checkAgentSupport,
|
|
235
247
|
stopCamera,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smileid/web-components",
|
|
3
|
-
"version": "11.4.
|
|
3
|
+
"version": "11.4.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "dist/esm/main.js",
|
|
6
6
|
"module": "dist/esm/main.js",
|
|
@@ -76,6 +76,15 @@
|
|
|
76
76
|
],
|
|
77
77
|
"type": "module",
|
|
78
78
|
"author": "SmileID <support@usesmileid.com> (https://usesmileid.com)",
|
|
79
|
+
"repository": {
|
|
80
|
+
"type": "git",
|
|
81
|
+
"url": "git+https://github.com/smileidentity/web-client.git",
|
|
82
|
+
"directory": "packages/web-components"
|
|
83
|
+
},
|
|
84
|
+
"bugs": {
|
|
85
|
+
"url": "https://github.com/smileidentity/web-client/issues"
|
|
86
|
+
},
|
|
87
|
+
"homepage": "https://github.com/smileidentity/web-client#readme",
|
|
79
88
|
"dependencies": {
|
|
80
89
|
"@lottiefiles/dotlottie-web": "^0.71.0",
|
|
81
90
|
"@mediapipe/tasks-vision": "^0.10.22-rc.20250304",
|