@smileid/web-components 11.3.0 → 11.4.1

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 (101) hide show
  1. package/dist/esm/DocumentCaptureScreens-wvJcTVkA.js +4147 -0
  2. package/dist/esm/DocumentCaptureScreens-wvJcTVkA.js.map +1 -0
  3. package/dist/esm/{EndUserConsent-BgKCCMMn.js → EndUserConsent-BXvS7t8z.js} +4 -4
  4. package/dist/esm/{EndUserConsent-BgKCCMMn.js.map → EndUserConsent-BXvS7t8z.js.map} +1 -1
  5. package/dist/esm/Navigation-BRFmg7s1.js +138 -0
  6. package/dist/esm/Navigation-BRFmg7s1.js.map +1 -0
  7. package/dist/esm/{SelfieCaptureScreens-DKd0f7K8.js → SelfieCaptureScreens-BkJBfzHv.js} +2651 -3086
  8. package/dist/esm/SelfieCaptureScreens-BkJBfzHv.js.map +1 -0
  9. package/dist/esm/{TotpConsent-BQm8j4-u.js → TotpConsent-Cn2DkVza.js} +2 -2
  10. package/dist/esm/{TotpConsent-BQm8j4-u.js.map → TotpConsent-Cn2DkVza.js.map} +1 -1
  11. package/dist/esm/combobox.js +14 -16
  12. package/dist/esm/combobox.js.map +1 -1
  13. package/dist/esm/document.js +1 -1
  14. package/dist/esm/end-user-consent.js +1 -1
  15. package/dist/esm/{index-Cjzs1eA_.js → index-DBUdxnp9.js} +91 -91
  16. package/dist/esm/{index-Cjzs1eA_.js.map → index-DBUdxnp9.js.map} +1 -1
  17. package/dist/esm/localisation.js +1 -1
  18. package/dist/esm/main.js +6 -6
  19. package/dist/esm/navigation.js +1 -1
  20. package/dist/esm/package-Dax8ezDK.js +565 -0
  21. package/dist/esm/package-Dax8ezDK.js.map +1 -0
  22. package/dist/esm/selfie.js +1 -1
  23. package/dist/esm/smart-camera-web.js +16 -11
  24. package/dist/esm/smart-camera-web.js.map +1 -1
  25. package/dist/esm/{styles-BOEZtbuc.js → styles-BTEClL7R.js} +2 -2
  26. package/dist/esm/{styles-BOEZtbuc.js.map → styles-BTEClL7R.js.map} +1 -1
  27. package/dist/esm/totp-consent.js +1 -1
  28. package/dist/smart-camera-web.js +446 -179
  29. package/dist/smart-camera-web.js.map +1 -1
  30. package/dist/types/main.d.ts +183 -33
  31. package/lib/components/combobox/src/Combobox.js +14 -12
  32. package/lib/components/document/src/DocumentCaptureScreens.js +15 -5
  33. package/lib/components/document/src/assets/icons/guidelines/greenbook/good.svg +77 -0
  34. package/lib/components/document/src/assets/icons/guidelines/greenbook/not-blurry.svg +84 -0
  35. package/lib/components/document/src/assets/icons/guidelines/greenbook/not-cropped.svg +83 -0
  36. package/lib/components/document/src/assets/icons/guidelines/greenbook/not-reflective.svg +89 -0
  37. package/lib/components/document/src/assets/icons/guidelines/id-card/good.svg +87 -0
  38. package/lib/components/document/src/assets/icons/guidelines/id-card/not-blurry.svg +100 -0
  39. package/lib/components/document/src/assets/icons/guidelines/id-card/not-cropped.svg +93 -0
  40. package/lib/components/document/src/assets/icons/guidelines/id-card/not-reflective.svg +98 -0
  41. package/lib/components/document/src/assets/icons/guidelines/passport/good.svg +91 -0
  42. package/lib/components/document/src/assets/icons/guidelines/passport/not-blurry.svg +109 -0
  43. package/lib/components/document/src/assets/icons/guidelines/passport/not-cropped.svg +102 -0
  44. package/lib/components/document/src/assets/icons/guidelines/passport/not-reflective.svg +207 -0
  45. package/lib/components/document/src/assets/lottie/taking photo of ID FLIP 2D.lottie +0 -0
  46. package/lib/components/document/src/assets/lottie/taking photo of ID.lottie +0 -0
  47. package/lib/components/document/src/assets/lottie/taking photo of green book passport.lottie +0 -0
  48. package/lib/components/document/src/assets/lottie/taking photo of passport 2.lottie +0 -0
  49. package/lib/components/document/src/document-capture-instructions/DocumentCaptureInstructions.stories.js +123 -12
  50. package/lib/components/document/src/document-capture-instructions/DocumentCaptureInstructions.tsx +808 -0
  51. package/lib/components/document/src/document-capture-instructions/index.js +1 -0
  52. package/lib/components/navigation/src/Navigation.js +57 -56
  53. package/lib/components/navigation/src/Navigation.stories.js +7 -0
  54. package/lib/components/selfie/src/smartselfie-capture/SmartSelfieCapture.tsx +3 -15
  55. package/lib/components/selfie/src/smartselfie-capture/components/CameraPreview.tsx +6 -10
  56. package/lib/components/selfie/src/smartselfie-capture/components/CaptureControls.tsx +7 -1
  57. package/lib/components/selfie/src/smartselfie-capture/hooks/useFaceCapture.ts +33 -19
  58. package/lib/components/selfie/src/smartselfie-capture/utils/alertMessages.ts +0 -1
  59. package/lib/components/selfie/src/smartselfie-capture/utils/mediapipeManager.ts +127 -30
  60. package/lib/components/signature-pad/package-lock.json +24 -19
  61. package/lib/components/signature-pad/package.json +1 -1
  62. package/lib/components/smart-camera-web/src/SmartCameraWeb.js +7 -1
  63. package/lib/styles/src/styles.js +1 -1
  64. package/package.json +5 -3
  65. package/dist/README.md +0 -15
  66. package/dist/components/README.md +0 -14
  67. package/dist/components/document/src/README.md +0 -111
  68. package/dist/components/document/src/document-capture/README.md +0 -90
  69. package/dist/components/document/src/document-capture-instructions/README.md +0 -56
  70. package/dist/components/document/src/document-capture-review/README.md +0 -79
  71. package/dist/components/selfie/README.md +0 -225
  72. package/dist/components/smart-camera-web/src/README.md +0 -206
  73. package/dist/domain/camera/src/README.md +0 -38
  74. package/dist/domain/file-upload/README.md +0 -35
  75. package/dist/esm/DocumentCaptureScreens-N-0o7eE5.js +0 -1580
  76. package/dist/esm/DocumentCaptureScreens-N-0o7eE5.js.map +0 -1
  77. package/dist/esm/Navigation-DOFYqTZt.js +0 -144
  78. package/dist/esm/Navigation-DOFYqTZt.js.map +0 -1
  79. package/dist/esm/SelfieCaptureScreens-DKd0f7K8.js.map +0 -1
  80. package/dist/esm/package-KQ2l43v1.js +0 -90
  81. package/dist/esm/package-KQ2l43v1.js.map +0 -1
  82. package/dist/package-lock.json +0 -4948
  83. package/dist/package.json +0 -59
  84. package/dist/smart-camera-web.js.gz +0 -0
  85. package/dist/src/components/combobox/src/index.js +0 -425
  86. package/dist/src/components/combobox/src/index.js.map +0 -7
  87. package/dist/src/components/document/src/index.js +0 -1423
  88. package/dist/src/components/document/src/index.js.map +0 -7
  89. package/dist/src/components/end-user-consent/src/index.js +0 -1586
  90. package/dist/src/components/end-user-consent/src/index.js.map +0 -7
  91. package/dist/src/components/selfie/src/index.js +0 -1221
  92. package/dist/src/components/selfie/src/index.js.map +0 -7
  93. package/dist/src/components/signature-pad/src/index.js +0 -796
  94. package/dist/src/components/signature-pad/src/index.js.map +0 -7
  95. package/dist/src/components/smart-camera-web/src/SmartCameraWeb.js +0 -2754
  96. package/dist/src/components/smart-camera-web/src/SmartCameraWeb.js.map +0 -7
  97. package/dist/src/components/totp-consent/src/index.js +0 -1305
  98. package/dist/src/components/totp-consent/src/index.js.map +0 -7
  99. package/dist/src/index.js.map +0 -7
  100. package/dist/styles/README.md +0 -3
  101. package/dist/types/locale.d.ts +0 -19
@@ -1,3 +1,4 @@
1
1
  import DocumentInstruction from './DocumentCaptureInstructions';
2
+ import './DocumentCaptureInstructions.tsx';
2
3
 
3
4
  export default DocumentInstruction;
@@ -4,6 +4,13 @@ class Navigation extends HTMLElement {
4
4
  connectedCallback() {
5
5
  const shadow = this.attachShadow({ mode: 'open' });
6
6
  const direction = getDirection();
7
+ const hostPadding = '0px';
8
+ const buttonSize = '40px';
9
+ const buttonBackground = 'rgba(132, 130, 130, 0.9)';
10
+ const buttonBorder = '1px solid rgba(255, 255, 255, 0.1)';
11
+ const iconSize = '20px';
12
+ const iconColor = this.hasThemeColor ? this.themeColor : '#FFFFFF';
13
+ const focusColor = '#FFFFFF';
7
14
 
8
15
  const style = document.createElement('style');
9
16
  style.textContent = `
@@ -12,6 +19,8 @@ class Navigation extends HTMLElement {
12
19
  max-inline-size: 100%;
13
20
  justify-content: ${this.showBackButton ? 'space-between' : 'flex-end'};
14
21
  direction: ${direction};
22
+ padding: var(--smileid-navigation-padding, ${hostPadding});
23
+ gap: 1rem;
15
24
  }
16
25
 
17
26
  :host([dir="rtl"]) .back-button svg,
@@ -20,50 +29,48 @@ class Navigation extends HTMLElement {
20
29
  }
21
30
 
22
31
  button {
23
- --button-color: var(--color-default);
24
- --flow-space: 3rem;
25
32
  -webkit-appearance: none;
26
33
  -moz-appearance: none;
27
- align-items: center;
28
34
  appearance: none;
29
- background-color: transparent;
30
- border-radius: 2.5rem;
31
- border: none;
32
- color: #ffffff;
33
- cursor: pointer;
34
- display: inline-flex;
35
- font-size: 1rem;
36
- font-weight: 500;
37
- inline-size: 100%;
38
- justify-content: center;
39
- letter-spacing: 0.05ch;
40
- line-height: 1;
41
- padding: 1rem 2.5rem;
42
- text-align: center;
43
- text-decoration: none;
44
- }
45
-
46
- button[data-type="icon"] {
35
+ width: ${buttonSize};
36
+ height: ${buttonSize};
37
+ border-radius: 50%;
38
+ background: var(--smileid-navigation-button-bg, ${buttonBackground});
39
+ border: ${buttonBorder};
40
+ display: flex;
47
41
  align-items: center;
48
- background-color: transparent;
49
- border: 0;
42
+ justify-content: center;
43
+ color: var(--smileid-navigation-icon-color, ${iconColor});
50
44
  cursor: pointer;
51
- display: flex;
52
45
  padding: 0;
53
- width: auto;
46
+ flex-shrink: 0;
47
+ transition: box-shadow 0.15s ease;
48
+ }
49
+
50
+ button:hover {
51
+ box-shadow: inset 0 0 0 999px rgba(0, 0, 0, 0.15);
52
+ }
53
+
54
+ button:focus-visible {
55
+ outline: 2px solid var(--smileid-navigation-focus-color, ${focusColor});
56
+ outline-offset: 3px;
57
+ }
58
+
59
+ button svg {
60
+ width: ${iconSize};
61
+ height: ${iconSize};
54
62
  }
55
63
 
56
64
  :host::part(back-button) {
57
65
  display: flex;
58
66
  align-items: center;
59
- }
60
-
61
- :host::part(back-button-text) {
62
- line-height: 1;
63
- color: ${this.hasThemeColor ? this.themeColor : 'rgb(21, 31, 114)'} !important;
67
+ justify-content: center;
64
68
  }
65
69
 
66
70
  :host::part(close-button) {
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
67
74
  }
68
75
 
69
76
  .visually-hidden {
@@ -79,57 +86,51 @@ button[data-type="icon"] {
79
86
 
80
87
  const backButton = document.createElement('button');
81
88
  backButton.setAttribute('class', 'back-button');
82
- backButton.setAttribute('data-type', 'icon');
83
89
  backButton.setAttribute('part', 'back-button');
84
90
  backButton.setAttribute('type', 'button');
91
+ backButton.setAttribute('aria-label', t('navigation.back'));
85
92
  backButton.innerHTML = `
86
93
  <svg
87
- xmlns="http://www.w3.org/2000/svg"
88
- width="24"
89
- height="24"
94
+ aria-hidden="true"
90
95
  viewBox="0 0 24 24"
91
96
  fill="none"
97
+ xmlns="http://www.w3.org/2000/svg"
92
98
  data-rtl="${direction === 'rtl'}"
93
99
  >
94
100
  <path
95
- fill="#DBDBC4"
96
- d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z"
97
- opacity=".4"
98
- />
99
- <path
100
- fill="${this.themeColor}"
101
- d="M15.5 11.25h-5.19l1.72-1.72c.29-.29.29-.77 0-1.06a.754.754 0 0 0-1.06 0l-3 3c-.29.29-.29.77 0 1.06l3 3c.15.15.34.22.53.22s.38-.07.53-.22c.29-.29.29-.77 0-1.06l-1.72-1.72h5.19c.41 0 .75-.34.75-.75s-.34-.75-.75-.75Z"
101
+ d="M19 12H5M5 12L12 19M5 12L12 5"
102
+ stroke="currentColor"
103
+ stroke-width="2"
104
+ stroke-linecap="round"
105
+ stroke-linejoin="round"
102
106
  />
103
107
  </svg>
104
- <span part="back-button-text">${t('navigation.back')}</span>
108
+ <span part="back-button-text" class="visually-hidden">${t('navigation.back')}</span>
105
109
  `;
106
110
 
107
111
  const closeButton = document.createElement('button');
108
112
  closeButton.setAttribute('class', 'close-button');
109
- closeButton.setAttribute('data-type', 'icon');
110
113
  closeButton.setAttribute('part', 'close-button');
111
114
  closeButton.setAttribute('type', 'button');
115
+ closeButton.setAttribute(
116
+ 'aria-label',
117
+ t('navigation.closeVerificationFrame'),
118
+ );
112
119
  closeButton.innerHTML = `
113
120
  <svg
114
- xmlns="http://www.w3.org/2000/svg"
121
+ aria-hidden="true"
115
122
  viewBox="0 0 24 24"
116
- width="24"
117
- height="24"
118
123
  fill="none"
124
+ xmlns="http://www.w3.org/2000/svg"
119
125
  >
120
126
  <path
121
- fill="#DBDBC4"
122
- d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z"
123
- opacity=".4"
124
- />
125
- <path
126
- fill="#91190F"
127
- d="m13.06 12 2.3-2.3c.29-.29.29-.77 0-1.06a.754.754 0 0 0-1.06 0l-2.3 2.3-2.3-2.3a.754.754 0 0 0-1.06 0c-.29.29-.29.77 0 1.06l2.3 2.3-2.3 2.3c-.29.29-.29.77 0 1.06.15.15.34.22.53.22s.38-.07.53-.22l2.3-2.3 2.3 2.3c.15.15.34.22.53.22s.38-.07.53-.22c.29-.29.29-.77 0-1.06l-2.3-2.3Z"
127
+ d="M18 6L6 18M6 6L18 18"
128
+ stroke="currentColor"
129
+ stroke-width="2"
130
+ stroke-linecap="round"
131
+ stroke-linejoin="round"
128
132
  />
129
133
  </svg>
130
- <span class="visually-hidden"
131
- >${t('navigation.closeVerificationFrame')}</span
132
- >
133
134
  `;
134
135
 
135
136
  shadow.appendChild(style);
@@ -12,6 +12,13 @@ const meta = {
12
12
  },
13
13
  },
14
14
  component: 'smileid-navigation',
15
+ decorators: [
16
+ (story) => `
17
+ <div style="background: #1a1a2e; padding: 24px; min-height: 100px;">
18
+ ${story()}
19
+ </div>
20
+ `,
21
+ ],
15
22
  };
16
23
 
17
24
  export default meta;
@@ -1,8 +1,6 @@
1
1
  import { useRef, useEffect } from 'preact/hooks';
2
- import { useSignal } from '@preact/signals';
3
2
  import register from 'preact-custom-element';
4
3
  import type { FunctionComponent } from 'preact';
5
- import throttle from 'lodash/throttle';
6
4
 
7
5
  import { getBoolProp } from '../../../../utils/props';
8
6
  import { useFaceCapture, useCamera } from './hooks';
@@ -50,13 +48,6 @@ const SmartSelfieCapture: FunctionComponent<Props> = ({
50
48
  const initialFacingMode = allowAgentMode ? 'environment' : 'user';
51
49
  const camera = useCamera(initialFacingMode);
52
50
 
53
- const throttledMultipleFaces = useSignal(false);
54
- const updateMultipleFacesUI = useRef(
55
- throttle((value: boolean) => {
56
- throttledMultipleFaces.value = value;
57
- }, 100),
58
- ).current;
59
-
60
51
  const faceCapture = useFaceCapture({
61
52
  videoRef: camera.videoRef,
62
53
  canvasRef,
@@ -106,14 +97,9 @@ const SmartSelfieCapture: FunctionComponent<Props> = ({
106
97
  faceCapture.stopDetectionLoop();
107
98
  camera.stopCamera();
108
99
  faceCapture.cleanup();
109
- updateMultipleFacesUI.cancel();
110
100
  };
111
101
  }, []);
112
102
 
113
- useEffect(() => {
114
- updateMultipleFacesUI(faceCapture.multipleFaces.value);
115
- }, [faceCapture.multipleFaces.value]);
116
-
117
103
  useEffect(() => {
118
104
  const navigation = navigationRef.current;
119
105
 
@@ -160,7 +146,6 @@ const SmartSelfieCapture: FunctionComponent<Props> = ({
160
146
  videoRef={camera.videoRef}
161
147
  canvasRef={canvasRef}
162
148
  facingMode={camera.facingMode}
163
- multipleFaces={throttledMultipleFaces.value}
164
149
  progress={
165
150
  faceCapture.capturesTaken.value > 0
166
151
  ? faceCapture.capturesTaken.value / faceCapture.totalCaptures.value
@@ -178,6 +163,9 @@ const SmartSelfieCapture: FunctionComponent<Props> = ({
178
163
  isCapturing={faceCapture.isCapturing.value}
179
164
  hasFinishedCapture={faceCapture.hasFinishedCapture.value}
180
165
  isReadyToCapture={faceCapture.isReadyToCapture.value}
166
+ captureButtonFallbackEnabled={
167
+ faceCapture.captureButtonFallbackEnabled.value
168
+ }
181
169
  allowAgentMode={allowAgentMode}
182
170
  agentSupported={camera.agentSupported}
183
171
  showAgentModeForTests={showAgentModeForTests}
@@ -5,7 +5,6 @@ interface CameraPreviewProps {
5
5
  videoRef: Ref<HTMLVideoElement>;
6
6
  canvasRef: Ref<HTMLCanvasElement>;
7
7
  facingMode: 'user' | 'environment';
8
- multipleFaces: boolean;
9
8
  progress: number;
10
9
  interval: number;
11
10
  themeColor: string;
@@ -15,7 +14,6 @@ export const CameraPreview: FunctionComponent<CameraPreviewProps> = ({
15
14
  videoRef,
16
15
  canvasRef,
17
16
  facingMode,
18
- multipleFaces,
19
17
  progress,
20
18
  interval,
21
19
  themeColor,
@@ -25,7 +23,7 @@ export const CameraPreview: FunctionComponent<CameraPreviewProps> = ({
25
23
  <div
26
24
  className="video-wrapper"
27
25
  style={{
28
- clipPath: multipleFaces ? 'none' : 'url(#selfie-clip-path)',
26
+ clipPath: 'url(#selfie-clip-path)',
29
27
  }}
30
28
  >
31
29
  <div className="video-container">
@@ -42,13 +40,11 @@ export const CameraPreview: FunctionComponent<CameraPreviewProps> = ({
42
40
  />
43
41
  </div>
44
42
  </div>
45
- {!multipleFaces && (
46
- <OvalProgress
47
- progress={progress}
48
- duration={interval}
49
- themeColor={themeColor}
50
- />
51
- )}
43
+ <OvalProgress
44
+ progress={progress}
45
+ duration={interval}
46
+ themeColor={themeColor}
47
+ />
52
48
  </div>
53
49
 
54
50
  <style>{`
@@ -5,6 +5,7 @@ interface CaptureControlsProps {
5
5
  isCapturing: boolean;
6
6
  hasFinishedCapture: boolean;
7
7
  isReadyToCapture: boolean;
8
+ captureButtonFallbackEnabled: boolean;
8
9
  allowAgentMode: boolean;
9
10
  agentSupported: boolean;
10
11
  showAgentModeForTests: boolean;
@@ -18,6 +19,7 @@ export const CaptureControls: FunctionComponent<CaptureControlsProps> = ({
18
19
  isCapturing,
19
20
  hasFinishedCapture,
20
21
  isReadyToCapture,
22
+ captureButtonFallbackEnabled,
21
23
  allowAgentMode,
22
24
  agentSupported,
23
25
  showAgentModeForTests,
@@ -32,7 +34,11 @@ export const CaptureControls: FunctionComponent<CaptureControlsProps> = ({
32
34
  id="start-image-capture"
33
35
  class="btn-primary"
34
36
  onClick={onStartCapture}
35
- disabled={isCapturing || hasFinishedCapture || !isReadyToCapture}
37
+ disabled={
38
+ isCapturing ||
39
+ hasFinishedCapture ||
40
+ (!isReadyToCapture && !captureButtonFallbackEnabled)
41
+ }
36
42
  >
37
43
  {t('selfie.capture.button.startCapture')}
38
44
  </button>
@@ -51,11 +51,11 @@ export const useFaceCapture = ({
51
51
  const animationFrameRef = useRef<number | null>(null);
52
52
  const captureTimerRef = useRef<NodeJS.Timeout | null>(null);
53
53
  const resumeCaptureRef = useRef<(() => void) | null>(null);
54
+ const fallbackTimerRef = useRef<NodeJS.Timeout | null>(null);
54
55
 
55
56
  const faceDetected = useSignal(false);
56
57
  const faceInBounds = useSignal(false);
57
58
  const faceProximity = useSignal<'too-close' | 'too-far' | 'good'>('good');
58
- const multipleFaces = useSignal(false);
59
59
  const videoAspectRatio = useSignal(16 / 9);
60
60
  const faceLandmarks = useSignal<any[]>([]);
61
61
  const currentSmileScore = useSignal(0);
@@ -64,6 +64,7 @@ export const useFaceCapture = ({
64
64
  const lastSmileTime = useSignal(0);
65
65
  const alertTitle = useSignal('');
66
66
  const isInitializing = useSignal(true);
67
+ const captureButtonFallbackEnabled = useSignal(false);
67
68
 
68
69
  const isCapturing = useSignal(false);
69
70
  const isPaused = useSignal(false);
@@ -83,8 +84,7 @@ export const useFaceCapture = ({
83
84
  () =>
84
85
  faceDetected.value &&
85
86
  faceInBounds.value &&
86
- faceProximity.value === 'good' &&
87
- !multipleFaces.value,
87
+ faceProximity.value === 'good',
88
88
  );
89
89
 
90
90
  const updateAlertImmediate = (messageKey: MessageKey | null) => {
@@ -101,6 +101,19 @@ export const useFaceCapture = ({
101
101
  }, 600),
102
102
  ).current;
103
103
 
104
+ const CAPTURE_FALLBACK_TIMEOUT_MS = 10000;
105
+
106
+ const startFallbackTimer = () => {
107
+ if (fallbackTimerRef.current) {
108
+ clearTimeout(fallbackTimerRef.current);
109
+ }
110
+ fallbackTimerRef.current = setTimeout(() => {
111
+ if (!isReadyToCapture.value) {
112
+ captureButtonFallbackEnabled.value = true;
113
+ }
114
+ }, CAPTURE_FALLBACK_TIMEOUT_MS);
115
+ };
116
+
104
117
  const initializeFaceLandmarker = async () => {
105
118
  try {
106
119
  const isAlreadyLoaded =
@@ -114,10 +127,15 @@ export const useFaceCapture = ({
114
127
 
115
128
  faceLandmarkerRef.current = await getMediapipeInstance();
116
129
  isInitializing.value = false;
130
+ startFallbackTimer();
117
131
  } catch (error) {
118
132
  console.error('Failed to initialize MediaPipe:', error);
119
133
  isInitializing.value = false;
134
+ // MediaPipe failed — start the fallback timer so the button eventually
135
+ // enables and the user isn't permanently stuck.
136
+ startFallbackTimer();
120
137
  }
138
+ startFallbackTimer();
121
139
  };
122
140
 
123
141
  const setupCanvas = () => {
@@ -165,8 +183,6 @@ export const useFaceCapture = ({
165
183
  const updateAlerts = () => {
166
184
  if (isInitializing.value) {
167
185
  updateAlertImmediate('initializing');
168
- } else if (multipleFaces.value) {
169
- updateAlert('multiple-faces');
170
186
  } else if (!faceDetected.value) {
171
187
  updateAlert('no-face');
172
188
  } else if (faceProximity.value === 'too-close') {
@@ -260,13 +276,12 @@ export const useFaceCapture = ({
260
276
 
261
277
  // Check number of faces
262
278
  const numFaces = results.faceLandmarks ? results.faceLandmarks.length : 0;
263
- multipleFaces.value = numFaces > 1;
264
279
 
265
280
  // Check if face is detected
266
281
  const hasFace =
267
282
  results.faceBlendshapes &&
268
283
  results.faceBlendshapes.length > 0 &&
269
- numFaces === 1;
284
+ numFaces >= 1;
270
285
  faceDetected.value = hasFace;
271
286
 
272
287
  if (hasFace && results.faceLandmarks) {
@@ -322,7 +337,7 @@ export const useFaceCapture = ({
322
337
  }
323
338
  }
324
339
  } else {
325
- // No face detected or multiple faces - reset values
340
+ // No face detected - reset values
326
341
  currentSmileScore.value = 0;
327
342
  currentFaceSize.value = 0;
328
343
  currentMouthOpen.value = 0;
@@ -334,7 +349,6 @@ export const useFaceCapture = ({
334
349
  } catch {
335
350
  faceDetected.value = false;
336
351
  faceInBounds.value = false;
337
- multipleFaces.value = false;
338
352
  faceProximity.value = 'good';
339
353
  currentMouthOpen.value = 0;
340
354
 
@@ -417,7 +431,6 @@ export const useFaceCapture = ({
417
431
  isPaused.value = true;
418
432
 
419
433
  if (
420
- !multipleFaces.value &&
421
434
  faceDetected.value &&
422
435
  faceInBounds.value &&
423
436
  faceProximity.value === 'good'
@@ -437,11 +450,6 @@ export const useFaceCapture = ({
437
450
  return;
438
451
  }
439
452
 
440
- if (multipleFaces.value) {
441
- pauseCapture();
442
- return;
443
- }
444
-
445
453
  if (!faceDetected.value) {
446
454
  return;
447
455
  }
@@ -474,8 +482,7 @@ export const useFaceCapture = ({
474
482
  if (
475
483
  faceDetected.value &&
476
484
  faceProximity.value === 'good' &&
477
- faceInBounds.value &&
478
- !multipleFaces.value
485
+ faceInBounds.value
479
486
  ) {
480
487
  const isInSmileZone = capturesTaken.value >= smileCheckpoint.value;
481
488
  if (isInSmileZone) {
@@ -556,6 +563,9 @@ export const useFaceCapture = ({
556
563
  if (captureTimerRef.current) {
557
564
  clearInterval(captureTimerRef.current);
558
565
  }
566
+ if (fallbackTimerRef.current) {
567
+ clearTimeout(fallbackTimerRef.current);
568
+ }
559
569
  stopDetectionLoop();
560
570
  updateAlert.cancel();
561
571
  };
@@ -564,12 +574,16 @@ export const useFaceCapture = ({
564
574
  faceDetected.value = false;
565
575
  faceInBounds.value = false;
566
576
  faceProximity.value = 'good';
567
- multipleFaces.value = false;
568
577
  faceLandmarks.value = [];
569
578
  currentSmileScore.value = 0;
570
579
  currentFaceSize.value = 0;
571
580
  currentMouthOpen.value = 0;
572
581
  lastSmileTime.value = 0;
582
+ captureButtonFallbackEnabled.value = false;
583
+ if (fallbackTimerRef.current) {
584
+ clearTimeout(fallbackTimerRef.current);
585
+ fallbackTimerRef.current = null;
586
+ }
573
587
 
574
588
  if (canvasRef.current) {
575
589
  clearCanvas(canvasRef.current);
@@ -580,7 +594,6 @@ export const useFaceCapture = ({
580
594
  faceDetected,
581
595
  faceInBounds,
582
596
  faceProximity,
583
- multipleFaces,
584
597
  videoAspectRatio,
585
598
  faceLandmarks,
586
599
  currentSmileScore,
@@ -590,6 +603,7 @@ export const useFaceCapture = ({
590
603
  alertTitle,
591
604
  isInitializing,
592
605
  isReadyToCapture,
606
+ captureButtonFallbackEnabled,
593
607
 
594
608
  isCapturing,
595
609
  isPaused,
@@ -1,7 +1,6 @@
1
1
  import { t } from '../../../../../domain/localisation';
2
2
 
3
3
  export const MESSAGES = {
4
- 'multiple-faces': () => t('selfie.smart.alert.multipleFaces'),
5
4
  'no-face': () => t('selfie.smart.alert.noFace'),
6
5
  'out-of-bounds': () => t('selfie.smart.alert.outOfBounds'),
7
6
  'too-close': () => t('selfie.smart.alert.tooClose'),