@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.
Files changed (32) hide show
  1. package/dist/esm/{DocumentCaptureScreens-zEVFc_Kr.js → DocumentCaptureScreens-bLFW-yEM.js} +4 -4
  2. package/dist/esm/{DocumentCaptureScreens-zEVFc_Kr.js.map → DocumentCaptureScreens-bLFW-yEM.js.map} +1 -1
  3. package/dist/esm/{EndUserConsent-BXvS7t8z.js → EndUserConsent-D26UoVk5.js} +3 -3
  4. package/dist/esm/{EndUserConsent-BXvS7t8z.js.map → EndUserConsent-D26UoVk5.js.map} +1 -1
  5. package/dist/esm/{Navigation-BRFmg7s1.js → Navigation-nvehze1F.js} +2 -2
  6. package/dist/esm/{Navigation-BRFmg7s1.js.map → Navigation-nvehze1F.js.map} +1 -1
  7. package/dist/esm/{SelfieCaptureScreens-DsFp21uW.js → SelfieCaptureScreens-BXIs6_tl.js} +1837 -1801
  8. package/dist/esm/SelfieCaptureScreens-BXIs6_tl.js.map +1 -0
  9. package/dist/esm/{TotpConsent-Cn2DkVza.js → TotpConsent-owUOdKzP.js} +2 -2
  10. package/dist/esm/{TotpConsent-Cn2DkVza.js.map → TotpConsent-owUOdKzP.js.map} +1 -1
  11. package/dist/esm/combobox.js +1 -1
  12. package/dist/esm/document.js +1 -1
  13. package/dist/esm/end-user-consent.js +1 -1
  14. package/dist/esm/{index-DBUdxnp9.js → index-5Nn2kzHI.js} +4 -4
  15. package/dist/esm/{index-DBUdxnp9.js.map → index-5Nn2kzHI.js.map} +1 -1
  16. package/dist/esm/localisation.js +1 -1
  17. package/dist/esm/main.js +6 -6
  18. package/dist/esm/navigation.js +1 -1
  19. package/dist/esm/{package-Do9oHVnx.js → package-DmH-I6GW.js} +3 -3
  20. package/dist/esm/{package-Do9oHVnx.js.map → package-DmH-I6GW.js.map} +1 -1
  21. package/dist/esm/selfie.js +1 -1
  22. package/dist/esm/smart-camera-web.js +5 -5
  23. package/dist/esm/totp-consent.js +1 -1
  24. package/dist/smart-camera-web.js +59 -50
  25. package/dist/smart-camera-web.js.map +1 -1
  26. package/dist/types/main.d.ts +4 -4
  27. package/lib/components/selfie/src/selfie-capture-wrapper/SelfieCaptureWrapper.tsx +145 -81
  28. package/lib/components/selfie/src/smartselfie-capture/SmartSelfieCapture.tsx +72 -39
  29. package/lib/components/selfie/src/smartselfie-capture/hooks/useCamera.ts +12 -0
  30. package/lib/components/signature-pad/package.json +1 -1
  31. package/package.json +10 -1
  32. package/dist/esm/SelfieCaptureScreens-DsFp21uW.js.map +0 -1
@@ -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
- const DEFAULT_MEDIAPIPE_WAIT_MS = 90 * 1000; // For when legacy fallback is NOT allowed, we wait the full 90s for mediapipe to load before showing an error.
49
- const DEFAULT_WAIT_MS = 20 * 1000; // default for when legacy fallback is allowed we wait for 20s
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 = DEFAULT_MEDIAPIPE_WAIT_MS,
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
- const loadingTime = allowLegacySelfieFallback ? DEFAULT_WAIT_MS : timeout;
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
- const [mediapipeInitAttempts, setMediapipeInitAttempts] = useState(0);
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 [mediapipeInitReported, setMediapipeInitReported] = useState(false);
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
- mediapipeInitAttempts >= MAX_MEDIAPIPE_INIT_ATTEMPTS ||
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 = mediapipeInitAttempts + 1;
131
- setMediapipeInitAttempts(attemptNumber);
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 (!mediapipeInitReported) {
149
- setMediapipeInitReported(true);
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
- // When using the loading countdown (startCountdown), increment the
206
- // visible loading progress. This is only used while mediapipe hasn't
207
- // reported ready; once mediapipeReady becomes true we stop the timer.
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) => Math.min(prev + 1, 100));
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 || loadingProgress < 100) return undefined;
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, loadingProgress]);
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
- if (loadingProgress >= 100) {
316
- // When loading completes without Mediapipe becoming ready, check if legacy
317
- // fallback is allowed. Legacy is allowed if:
318
- // 1. allow-legacy-selfie-fallback attribute is set to true, OR
319
- // 2. Running under Cypress (to keep existing test behavior)
320
- const legacyFallbackAllowed = allowLegacySelfieFallback || isCypress;
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
- if (legacyFallbackAllowed) {
323
- // Mount the legacy `selfie-capture` web component. We also set
324
- // `usingSelfieCapture` so other effects can react (e.g. metadata dispatch).
325
- if (!usingSelfieCapture) {
326
- setUsingSelfieCapture(true);
327
- }
328
-
329
- const propsWithoutHidden = { ...props };
330
- delete (propsWithoutHidden as any).hidden;
331
-
332
- return (
333
- // @ts-expect-error --- preact-custom-element doesn't have proper types for refs
334
- <selfie-capture
335
- {...propsWithoutHidden}
336
- ref={(el: HTMLElement) => {
337
- if (el && !el.hasAttribute('data-events-setup')) {
338
- el.setAttribute('data-events-setup', 'true');
339
-
340
- const forwardEvent = (event: Event) => {
341
- const customEvent = event as CustomEvent;
342
-
343
- if (
344
- customEvent.type === 'selfie-capture.publish' ||
345
- customEvent.type === 'selfie-capture.cancelled' ||
346
- customEvent.type === 'selfie-capture.close'
347
- ) {
348
- setInitialSessionCompleted(true);
349
- }
350
-
351
- window.dispatchEvent(
352
- new CustomEvent(customEvent.type, {
353
- detail: customEvent.detail,
354
- bubbles: true,
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
- // Legacy fallback is NOT allowed: show error message
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
- useEffect(() => {
65
- const initializeCamera = async () => {
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
- await camera.checkAgentSupport();
75
- await faceCapture.initializeFaceLandmarker();
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
- setTimeout(() => {
78
- faceCapture.setupCanvas();
79
- faceCapture.startDetectionLoop();
80
- }, 500);
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
- <CameraPreview
146
- videoRef={camera.videoRef}
147
- canvasRef={canvasRef}
148
- facingMode={camera.facingMode}
149
- progress={
150
- faceCapture.capturesTaken.value > 0
151
- ? faceCapture.capturesTaken.value / faceCapture.totalCaptures.value
152
- : 0
153
- }
154
- interval={interval}
155
- themeColor={themeColor}
156
- />
157
-
158
- <AlertDisplay alertTitle={faceCapture.alertTitle.value} />
159
-
160
- {!faceCapture.isCapturing.value &&
161
- !faceCapture.hasFinishedCapture.value && (
162
- <CaptureControls
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,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smileid/signature-pad",
3
- "version": "11.4.2",
3
+ "version": "11.4.4",
4
4
  "private": true,
5
5
  "exports": {
6
6
  ".": "./index.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smileid/web-components",
3
- "version": "11.4.2",
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",