@smileid/web-components 11.0.3 → 11.2.0

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 (93) hide show
  1. package/README.md +61 -0
  2. package/dist/components/smart-camera-web/src/README.md +0 -1
  3. package/dist/esm/{DocumentCaptureScreens-C5BhNB-0.js → DocumentCaptureScreens-BbtA-WkX.js} +199 -193
  4. package/dist/esm/DocumentCaptureScreens-BbtA-WkX.js.map +1 -0
  5. package/dist/esm/{EndUserConsent-D4fd1ovG.js → EndUserConsent-HVufMamg.js} +65 -63
  6. package/dist/esm/EndUserConsent-HVufMamg.js.map +1 -0
  7. package/dist/esm/{Navigation-CTjK6tLU.js → Navigation-B-dqPkZj.js} +17 -9
  8. package/dist/esm/Navigation-B-dqPkZj.js.map +1 -0
  9. package/dist/esm/{SelfieCaptureScreens-KoQpCxtc.js → SelfieCaptureScreens-ChAMfKi3.js} +3274 -3329
  10. package/dist/esm/SelfieCaptureScreens-ChAMfKi3.js.map +1 -0
  11. package/dist/esm/{TotpConsent-CQU5jQi4.js → TotpConsent-XxR8TNxy.js} +13 -9
  12. package/dist/esm/TotpConsent-XxR8TNxy.js.map +1 -0
  13. package/dist/esm/combobox.js +20 -19
  14. package/dist/esm/combobox.js.map +1 -1
  15. package/dist/esm/document.js +1 -1
  16. package/dist/esm/end-user-consent.js +1 -1
  17. package/dist/esm/index-B_ozpejI.js +1360 -0
  18. package/dist/esm/index-B_ozpejI.js.map +1 -0
  19. package/dist/esm/localisation.js +21 -0
  20. package/dist/esm/localisation.js.map +1 -0
  21. package/dist/esm/main.js +34 -17
  22. package/dist/esm/main.js.map +1 -1
  23. package/dist/esm/navigation.js +1 -1
  24. package/dist/esm/{package-B-UwEdv7.js → package-u3FEJ3Fm.js} +25 -40
  25. package/dist/esm/package-u3FEJ3Fm.js.map +1 -0
  26. package/dist/esm/selfie.js +1 -1
  27. package/dist/esm/smart-camera-web.js +32 -23
  28. package/dist/esm/smart-camera-web.js.map +1 -1
  29. package/dist/esm/totp-consent.js +1 -1
  30. package/dist/package.json +1 -1
  31. package/dist/smart-camera-web.js +144 -160
  32. package/dist/smart-camera-web.js.map +1 -1
  33. package/dist/src/components/combobox/src/index.js +424 -1
  34. package/dist/src/components/document/src/index.js +1422 -1
  35. package/dist/src/components/end-user-consent/src/index.js +1573 -1
  36. package/dist/src/components/selfie/src/index.js +1220 -1
  37. package/dist/src/components/signature-pad/src/index.js +787 -1
  38. package/dist/src/components/smart-camera-web/src/SmartCameraWeb.js +2753 -1
  39. package/dist/src/components/totp-consent/src/index.js +1292 -1
  40. package/dist/types/combobox.d.ts +2 -2
  41. package/dist/types/document.d.ts +2 -2
  42. package/dist/types/end-user-consent.d.ts +2 -2
  43. package/dist/types/locale.d.ts +19 -0
  44. package/dist/types/localisation.d.ts +21 -0
  45. package/dist/types/main.d.ts +35 -26
  46. package/dist/types/navigation.d.ts +2 -2
  47. package/dist/types/selfie.d.ts +2 -2
  48. package/dist/types/signature-pad.d.ts +2 -2
  49. package/dist/types/smart-camera-web.d.ts +2 -2
  50. package/dist/types/totp-consent.d.ts +2 -2
  51. package/lib/components/camera-permission/CameraPermission.js +9 -4
  52. package/lib/components/combobox/src/Combobox.js +4 -2
  53. package/lib/components/document/src/DocumentCaptureScreens.js +4 -3
  54. package/lib/components/document/src/DocumentCaptureScreens.stories.js +37 -13
  55. package/lib/components/document/src/document-capture/DocumentCapture.js +23 -17
  56. package/lib/components/document/src/document-capture/DocumentCapture.stories.js +11 -2
  57. package/lib/components/document/src/document-capture-instructions/DocumentCaptureInstructions.js +19 -14
  58. package/lib/components/document/src/document-capture-instructions/DocumentCaptureInstructions.stories.js +14 -5
  59. package/lib/components/document/src/document-capture-review/DocumentCaptureReview.js +14 -10
  60. package/lib/components/document/src/document-capture-review/DocumentCaptureReview.stories.js +14 -5
  61. package/lib/components/end-user-consent/src/EndUserConsent.js +30 -29
  62. package/lib/components/end-user-consent/src/EndUserConsent.stories.js +12 -2
  63. package/lib/components/navigation/src/Navigation.js +15 -2
  64. package/lib/components/navigation/src/Navigation.stories.js +20 -4
  65. package/lib/components/selfie/src/SelfieCaptureScreens.js +12 -8
  66. package/lib/components/selfie/src/SelfieCaptureScreens.stories.js +16 -4
  67. package/lib/components/selfie/src/selfie-capture/SelfieCapture.js +25 -18
  68. package/lib/components/selfie/src/selfie-capture/SelfieCapture.stories.js +19 -7
  69. package/lib/components/selfie/src/selfie-capture-instructions/SelfieCaptureInstructions.js +19 -14
  70. package/lib/components/selfie/src/selfie-capture-instructions/SelfieCaptureInstructions.stories.js +14 -5
  71. package/lib/components/selfie/src/selfie-capture-review/SelfieCaptureReview.js +13 -8
  72. package/lib/components/selfie/src/selfie-capture-review/SelfieCaptureReview.stories.js +14 -5
  73. package/lib/components/selfie/src/selfie-capture-wrapper/SelfieCaptureWrapper.tsx +98 -47
  74. package/lib/components/selfie/src/smartselfie-capture/SmartSelfieCapture.tsx +2 -2
  75. package/lib/components/selfie/src/smartselfie-capture/components/CaptureControls.tsx +5 -2
  76. package/lib/components/selfie/src/smartselfie-capture/hooks/useCamera.ts +4 -4
  77. package/lib/components/selfie/src/smartselfie-capture/hooks/useFaceCapture.ts +6 -5
  78. package/lib/components/selfie/src/smartselfie-capture/utils/alertMessages.ts +11 -9
  79. package/lib/components/selfie/src/smartselfie-capture/utils/imageCapture.ts +3 -1
  80. package/lib/components/signature-pad/package.json +1 -1
  81. package/lib/components/smart-camera-web/src/SmartCameraWeb.js +9 -1
  82. package/lib/components/totp-consent/src/TotpConsent.js +8 -3
  83. package/lib/domain/camera/src/SmartCamera.js +7 -22
  84. package/lib/domain/constants/src/Constants.js +28 -0
  85. package/lib/domain/file-upload/src/SmartFileUpload.js +9 -10
  86. package/lib/domain/localisation/index.js +456 -0
  87. package/package.json +13 -7
  88. package/dist/esm/DocumentCaptureScreens-C5BhNB-0.js.map +0 -1
  89. package/dist/esm/EndUserConsent-D4fd1ovG.js.map +0 -1
  90. package/dist/esm/Navigation-CTjK6tLU.js.map +0 -1
  91. package/dist/esm/SelfieCaptureScreens-KoQpCxtc.js.map +0 -1
  92. package/dist/esm/TotpConsent-CQU5jQi4.js.map +0 -1
  93. package/dist/esm/package-B-UwEdv7.js.map +0 -1
@@ -3,7 +3,8 @@ import { IconLoader2 } from '@tabler/icons-preact';
3
3
  import register from 'preact-custom-element';
4
4
  import type { FunctionComponent } from 'preact';
5
5
 
6
- import { getBoolProp } from '@/utils/props';
6
+ import { getBoolProp } from '../../../../utils/props';
7
+ import { translate, translateHtml } from '../../../../domain/localisation';
7
8
  import SmartSelfieCapture from '../smartselfie-capture/SmartSelfieCapture';
8
9
  // Legacy web component fallback (used when Mediapipe isn't available)
9
10
  import '../selfie-capture/SelfieCapture';
@@ -17,6 +18,7 @@ interface Props {
17
18
  'theme-color'?: string;
18
19
  'show-navigation'?: string | boolean;
19
20
  'allow-agent-mode'?: string | boolean;
21
+ 'allow-legacy-selfie-fallback'?: string | boolean;
20
22
  'show-agent-mode-for-tests'?: string | boolean;
21
23
  'hide-attribution'?: string | boolean;
22
24
  'disable-image-tests'?: string | boolean;
@@ -25,12 +27,16 @@ interface Props {
25
27
  hidden?: string | boolean;
26
28
  }
27
29
 
30
+ 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.
31
+ const DEFAULT_WAIT_MS = 20 * 1000; // default for when legacy fallback is allowed we wait for 20s
32
+
28
33
  // Wrapper component that decides whether to use the modern
29
34
  // SmartSelfieCapture (Mediapipe-based) or fallback to the legacy `selfie-capture`
30
- // web component after a timeout of 20 seconds.
35
+ // web component after a timeout (default 90 seconds).
31
36
  const SelfieCaptureWrapper: FunctionComponent<Props> = ({
32
- timeout = 20000,
37
+ timeout = DEFAULT_MEDIAPIPE_WAIT_MS,
33
38
  'start-countdown': startCountdownProp = false,
39
+ 'allow-legacy-selfie-fallback': allowLegacySelfieFallbackProp = false,
34
40
  hidden: hiddenProp = false,
35
41
  ...props
36
42
  }) => {
@@ -54,6 +60,9 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
54
60
 
55
61
  const hidden = getBoolProp(hiddenProp);
56
62
  const startCountdown = getBoolProp(startCountdownProp);
63
+ const allowLegacySelfieFallback = getBoolProp(allowLegacySelfieFallbackProp);
64
+ const loadingTime = allowLegacySelfieFallback ? DEFAULT_WAIT_MS : timeout;
65
+
57
66
  // Component state:
58
67
  // - mediapipeReady: whether the mediapipe instance has successfully loaded
59
68
  // - loadingProgress: percentage used for the visible loading UI
@@ -96,12 +105,12 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
96
105
 
97
106
  const timer = setInterval(() => {
98
107
  setLoadingProgress((prev: number) => Math.min(prev + 1, 100));
99
- }, timeout / 100);
108
+ }, loadingTime / 100);
100
109
 
101
110
  return () => {
102
111
  clearInterval(timer);
103
112
  };
104
- }, [hidden, startCountdown, timeout, mediapipeReady]);
113
+ }, [hidden, startCountdown, loadingTime, mediapipeReady]);
105
114
 
106
115
  useEffect(() => {
107
116
  if (hidden || mediapipeReady || loadingProgress < 100) return undefined;
@@ -155,8 +164,22 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
155
164
  };
156
165
  }, [hidden, mediapipeReady, loadingProgress]);
157
166
 
167
+ // Dispatch allow_legacy_selfie_fallback config for observability
168
+ useEffect(() => {
169
+ if (hidden) return;
170
+
171
+ const smartCameraWeb = document.querySelector('smart-camera-web');
172
+ smartCameraWeb?.dispatchEvent(
173
+ new CustomEvent('metadata.allow-legacy-selfie-fallback', {
174
+ detail: {
175
+ allow_legacy_selfie_fallback: allowLegacySelfieFallback,
176
+ },
177
+ }),
178
+ );
179
+ }, [hidden, allowLegacySelfieFallback]);
180
+
158
181
  // Announce to any `smart-camera-web` element which liveness version is active.
159
- // The old capture users 0.0.1, the new one 1.0.0.
182
+ // The old capture uses 0.0.1, the new one 1.0.0.
160
183
  useEffect(() => {
161
184
  if (hidden || mediapipeLoading) return;
162
185
 
@@ -185,52 +208,71 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
185
208
  }
186
209
 
187
210
  if (loadingProgress >= 100) {
188
- // When loading completes without Mediapipe becoming ready, mount the legacy
189
- // `selfie-capture` web component. We also set `usingSelfieCapture` so other
190
- // effects can react (e.g. metadata dispatch).
191
- if (!usingSelfieCapture) {
192
- setUsingSelfieCapture(true);
193
- }
211
+ // When loading completes without Mediapipe becoming ready, check if legacy
212
+ // fallback is allowed. Legacy is allowed if:
213
+ // 1. allow-legacy-selfie-fallback attribute is set to true, OR
214
+ // 2. Running under Cypress (to keep existing test behavior)
215
+ const legacyFallbackAllowed = allowLegacySelfieFallback || isCypress;
216
+
217
+ if (legacyFallbackAllowed) {
218
+ // Mount the legacy `selfie-capture` web component. We also set
219
+ // `usingSelfieCapture` so other effects can react (e.g. metadata dispatch).
220
+ if (!usingSelfieCapture) {
221
+ setUsingSelfieCapture(true);
222
+ }
223
+
224
+ const propsWithoutHidden = { ...props };
225
+ delete (propsWithoutHidden as any).hidden;
226
+
227
+ return (
228
+ // @ts-expect-error --- preact-custom-element doesn't have proper types for refs
229
+ <selfie-capture
230
+ {...propsWithoutHidden}
231
+ ref={(el: HTMLElement) => {
232
+ if (el && !el.hasAttribute('data-events-setup')) {
233
+ el.setAttribute('data-events-setup', 'true');
194
234
 
195
- const propsWithoutHidden = { ...props };
196
- delete (propsWithoutHidden as any).hidden;
235
+ const forwardEvent = (event: Event) => {
236
+ const customEvent = event as CustomEvent;
197
237
 
238
+ if (
239
+ customEvent.type === 'selfie-capture.publish' ||
240
+ customEvent.type === 'selfie-capture.cancelled' ||
241
+ customEvent.type === 'selfie-capture.close'
242
+ ) {
243
+ setInitialSessionCompleted(true);
244
+ }
245
+
246
+ window.dispatchEvent(
247
+ new CustomEvent(customEvent.type, {
248
+ detail: customEvent.detail,
249
+ bubbles: true,
250
+ }),
251
+ );
252
+ };
253
+
254
+ el.addEventListener('selfie-capture.publish', forwardEvent);
255
+ el.addEventListener('selfie-capture.cancelled', forwardEvent);
256
+ el.addEventListener('selfie-capture.close', forwardEvent);
257
+ }
258
+ }}
259
+ />
260
+ );
261
+ }
262
+
263
+ // Legacy fallback is NOT allowed: show error message
198
264
  return (
199
- // @ts-ignore
200
- <selfie-capture
201
- {...propsWithoutHidden}
202
- ref={(el: HTMLElement) => {
203
- if (el && !el.hasAttribute('data-events-setup')) {
204
- el.setAttribute('data-events-setup', 'true');
205
-
206
- const forwardEvent = (event: Event) => {
207
- const customEvent = event as CustomEvent;
208
-
209
- if (
210
- customEvent.type === 'selfie-capture.publish' ||
211
- customEvent.type === 'selfie-capture.cancelled' ||
212
- customEvent.type === 'selfie-capture.close'
213
- ) {
214
- setInitialSessionCompleted(true);
215
- }
216
-
217
- window.dispatchEvent(
218
- new CustomEvent(customEvent.type, {
219
- detail: customEvent.detail,
220
- bubbles: true,
221
- }),
222
- );
223
- };
224
-
225
- el.addEventListener('selfie-capture.publish', forwardEvent);
226
- el.addEventListener('selfie-capture.cancelled', forwardEvent);
227
- el.addEventListener('selfie-capture.close', forwardEvent);
228
- }
229
- }}
230
- />
265
+ <div style={{ textAlign: 'center', marginTop: '20%', padding: '0 20px' }}>
266
+ <p style={{ fontSize: '1.2rem', color: '#333' }}>
267
+ {translate('selfie.capture.loading.connectionError')}
268
+ </p>
269
+ </div>
231
270
  );
232
271
  }
233
272
 
273
+ // Midpoint threshold (40s out of 90s ≈ 44%)
274
+ const SLOW_CONNECTION_THRESHOLD = 44;
275
+
234
276
  return (
235
277
  <div style={{ textAlign: 'center', marginTop: '20%' }}>
236
278
  <div
@@ -245,7 +287,15 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
245
287
  style={{ animation: 'spin 1s linear infinite' }}
246
288
  />
247
289
  </div>
248
- <p>Loading... {loadingProgress}%</p>
290
+ {loadingProgress >= SLOW_CONNECTION_THRESHOLD ? (
291
+ <p>{translate('selfie.capture.loading.slowConnection')}</p>
292
+ ) : (
293
+ <p>
294
+ {translateHtml('selfie.capture.loading.progress', {
295
+ progress: loadingProgress,
296
+ })}
297
+ </p>
298
+ )}
249
299
  <style>{`
250
300
  @keyframes spin {
251
301
  from { transform: rotate(0deg); }
@@ -267,6 +317,7 @@ if (!customElements.get('selfie-capture-wrapper')) {
267
317
  'theme-color',
268
318
  'show-navigation',
269
319
  'allow-agent-mode',
320
+ 'allow-legacy-selfie-fallback',
270
321
  'show-agent-mode-for-tests',
271
322
  'hide-attribution',
272
323
  'disable-image-tests',
@@ -152,7 +152,7 @@ const SmartSelfieCapture: FunctionComponent<Props> = ({
152
152
  return (
153
153
  <div className="smartselfie-capture">
154
154
  {showNavigation && (
155
- // @ts-ignore
155
+ // @ts-expect-error --- preact-custom-element doesn't have proper types for refs
156
156
  <smileid-navigation ref={navigationRef} theme-color={themeColor} />
157
157
  )}
158
158
 
@@ -188,7 +188,7 @@ const SmartSelfieCapture: FunctionComponent<Props> = ({
188
188
  />
189
189
  )}
190
190
 
191
- {/* @ts-ignore */}
191
+ {/* @ts-expect-error -- preact-custom-element doesn't have proper types for refs */}
192
192
  {!hideAttribution && <powered-by-smile-id />}
193
193
 
194
194
  <style>{`
@@ -1,4 +1,5 @@
1
1
  import type { FunctionComponent } from 'preact';
2
+ import { t } from '../../../../../domain/localisation';
2
3
 
3
4
  interface CaptureControlsProps {
4
5
  isCapturing: boolean;
@@ -33,7 +34,7 @@ export const CaptureControls: FunctionComponent<CaptureControlsProps> = ({
33
34
  onClick={onStartCapture}
34
35
  disabled={isCapturing || hasFinishedCapture || !isReadyToCapture}
35
36
  >
36
- Start Capture
37
+ {t('selfie.capture.button.startCapture')}
37
38
  </button>
38
39
 
39
40
  {allowAgentMode && (agentSupported || showAgentModeForTests) && (
@@ -43,7 +44,9 @@ export const CaptureControls: FunctionComponent<CaptureControlsProps> = ({
43
44
  className="agent-mode-btn"
44
45
  disabled={isCapturing || hasFinishedCapture}
45
46
  >
46
- {facingMode === 'user' ? 'Agent Mode Off' : 'Agent Mode On'}
47
+ {facingMode === 'user'
48
+ ? t('selfie.capture.agentMode.off')
49
+ : t('selfie.capture.agentMode.on')}
47
50
  </button>
48
51
  )}
49
52
  </div>
@@ -110,7 +110,7 @@ export const useCamera = (initialFacingMode: CameraFacingMode = 'user') => {
110
110
  }
111
111
 
112
112
  await startCamera(newFacingMode);
113
- } catch (error) {
113
+ } catch {
114
114
  setFacingMode(previousFacingMode);
115
115
  isSwitchingCameraRef.current = false;
116
116
 
@@ -180,7 +180,7 @@ export const useCamera = (initialFacingMode: CameraFacingMode = 'user') => {
180
180
  userCameraId =
181
181
  userStream.getVideoTracks()[0].getSettings().deviceId ?? null;
182
182
  userStream.getTracks().forEach((track) => track.stop());
183
- } catch (error) {
183
+ } catch {
184
184
  // no user-facing camera available
185
185
  }
186
186
 
@@ -192,7 +192,7 @@ export const useCamera = (initialFacingMode: CameraFacingMode = 'user') => {
192
192
  environmentCameraId =
193
193
  envStream.getVideoTracks()[0].getSettings().deviceId ?? null;
194
194
  envStream.getTracks().forEach((track) => track.stop());
195
- } catch (error) {
195
+ } catch {
196
196
  // no environment-facing camera available
197
197
  }
198
198
 
@@ -207,7 +207,7 @@ export const useCamera = (initialFacingMode: CameraFacingMode = 'user') => {
207
207
  }
208
208
 
209
209
  setAgentSupported(!isGecko);
210
- } catch (error) {
210
+ } catch {
211
211
  setAgentSupported(false);
212
212
  }
213
213
  };
@@ -17,6 +17,7 @@ import { captureImageFromVideo } from '../utils/imageCapture';
17
17
  import { ImageType } from '../constants';
18
18
  import { MESSAGES, type MessageKey } from '../utils/alertMessages';
19
19
  import { getMediapipeInstance } from '../utils/mediapipeManager';
20
+ import { t } from '../../../../../domain/localisation';
20
21
  import packageJson from '../../../../../../package.json';
21
22
 
22
23
  const COMPONENTS_VERSION = packageJson.version;
@@ -88,7 +89,7 @@ export const useFaceCapture = ({
88
89
 
89
90
  const updateAlertImmediate = (messageKey: MessageKey | null) => {
90
91
  if (messageKey && MESSAGES[messageKey]) {
91
- alertTitle.value = MESSAGES[messageKey];
92
+ alertTitle.value = MESSAGES[messageKey]?.();
92
93
  } else {
93
94
  alertTitle.value = '';
94
95
  }
@@ -141,7 +142,7 @@ export const useFaceCapture = ({
141
142
  const isInSmileZone = capturesTaken.value >= smileCheckpoint.value;
142
143
 
143
144
  if (isInNeutralZone) {
144
- alertTitle.value = 'Capturing...';
145
+ alertTitle.value = t('selfie.smart.status.capturing');
145
146
  } else if (isInSmileZone) {
146
147
  const timeSinceSmile = Date.now() - lastSmileTime.value;
147
148
  if (timeSinceSmile > smileCooldown) {
@@ -154,7 +155,7 @@ export const useFaceCapture = ({
154
155
  updateAlert('smile-required');
155
156
  }
156
157
  } else {
157
- alertTitle.value = 'Keep smiling!';
158
+ alertTitle.value = t('selfie.smart.status.keepSmiling');
158
159
  }
159
160
  } else {
160
161
  updateAlert(null);
@@ -177,7 +178,7 @@ export const useFaceCapture = ({
177
178
  } else if (isCapturing.value) {
178
179
  updateCaptureAlerts();
179
180
  } else {
180
- alertTitle.value = 'Ready to capture';
181
+ alertTitle.value = t('selfie.smart.status.readyToCapture');
181
182
  }
182
183
  };
183
184
 
@@ -330,7 +331,7 @@ export const useFaceCapture = ({
330
331
  }
331
332
 
332
333
  updateAlerts();
333
- } catch (error) {
334
+ } catch {
334
335
  faceDetected.value = false;
335
336
  faceInBounds.value = false;
336
337
  multipleFaces.value = false;
@@ -1,13 +1,15 @@
1
+ import { t } from '../../../../../domain/localisation';
2
+
1
3
  export const MESSAGES = {
2
- 'multiple-faces': 'Ensure only one face is visible',
3
- 'no-face': 'Position your face in the oval',
4
- 'out-of-bounds': 'Position your face in the oval',
5
- 'too-close': 'Move farther away',
6
- 'too-far': 'Move closer',
7
- 'neutral-expression': 'Neutral expression',
8
- 'smile-required': 'Smile!',
9
- 'open-mouth-smile': 'Wider smile - teeth visible',
10
- initializing: 'Initializing...',
4
+ 'multiple-faces': () => t('selfie.smart.alert.multipleFaces'),
5
+ 'no-face': () => t('selfie.smart.alert.noFace'),
6
+ 'out-of-bounds': () => t('selfie.smart.alert.outOfBounds'),
7
+ 'too-close': () => t('selfie.smart.alert.tooClose'),
8
+ 'too-far': () => t('selfie.smart.alert.tooFar'),
9
+ 'neutral-expression': () => t('selfie.smart.alert.neutralExpression'),
10
+ 'smile-required': () => t('selfie.smart.alert.smileRequired'),
11
+ 'open-mouth-smile': () => t('selfie.smart.alert.openMouthSmile'),
12
+ initializing: () => t('selfie.smart.alert.initializing'),
11
13
  };
12
14
 
13
15
  export type MessageKey = keyof typeof MESSAGES;
@@ -1,3 +1,5 @@
1
+ import { JPEG_QUALITY } from '../../../../../domain/constants/src/Constants';
2
+
1
3
  export const captureImageFromVideo = (
2
4
  videoElement: HTMLVideoElement,
3
5
  isReference: boolean = false,
@@ -60,5 +62,5 @@ export const captureImageFromVideo = (
60
62
  canvas.height,
61
63
  );
62
64
 
63
- return canvas.toDataURL('image/jpeg');
65
+ return canvas.toDataURL('image/jpeg', JPEG_QUALITY);
64
66
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smileid/signature-pad",
3
- "version": "11.0.3",
3
+ "version": "11.2.0",
4
4
  "private": "true",
5
5
  "exports": {
6
6
  ".": "./index.js"
@@ -20,7 +20,7 @@ function scwTemplateString() {
20
20
  <div style="height: 100%;">
21
21
  <camera-permission ${this.applyComponentThemeColor} ${this.title} ${this.showNavigation} ${this.hideInstructions ? '' : 'hidden'} ${this.hideAttribution}></camera-permission>
22
22
  <selfie-capture-screens ${this.applyComponentThemeColor} ${this.title} ${this.showNavigation} ${this.disableImageTests} ${this.hideAttribution} ${this.hideInstructions} hidden
23
- ${this.hideBackToHost} ${this.allowAgentMode} ${this.allowAgentModeTests}
23
+ ${this.hideBackToHost} ${this.allowAgentMode} ${this.allowAgentModeTests} ${this.allowLegacySelfieFallback}
24
24
  ></selfie-capture-screens>
25
25
  <document-capture-screens ${this.applyComponentThemeColor} document-type=${this.documentType} ${this.title} ${this.documentCaptureModes} ${this.showNavigation} ${this.hideAttribution}
26
26
  ${this.hideBackOfId} ${this.applyComponentThemeColor} hidden></document-capture-screens>
@@ -69,6 +69,7 @@ class SmartCameraWeb extends HTMLElement {
69
69
  static get observedAttributes() {
70
70
  return [
71
71
  'allow-agent-mode',
72
+ 'allow-legacy-selfie-fallback',
72
73
  'disable-image-tests',
73
74
  'document-capture-modes',
74
75
  'document-type',
@@ -83,6 +84,7 @@ class SmartCameraWeb extends HTMLElement {
83
84
  attributeChangedCallback(name) {
84
85
  switch (name) {
85
86
  case 'allow-agent-mode':
87
+ case 'allow-legacy-selfie-fallback':
86
88
  case 'disable-image-tests':
87
89
  case 'document-capture-modes':
88
90
  case 'document-type':
@@ -266,6 +268,12 @@ class SmartCameraWeb extends HTMLElement {
266
268
  : '';
267
269
  }
268
270
 
271
+ get allowLegacySelfieFallback() {
272
+ return this.hasAttribute('allow-legacy-selfie-fallback')
273
+ ? `allow-legacy-selfie-fallback='${this.getAttribute('allow-legacy-selfie-fallback')}'`
274
+ : '';
275
+ }
276
+
269
277
  get hideAttribution() {
270
278
  return this.hasAttribute('hide-attribution') ? 'hide-attribution' : '';
271
279
  }
@@ -1,4 +1,5 @@
1
1
  import validate from 'validate.js';
2
+ import { getDirection } from '../../../domain/localisation';
2
3
 
3
4
  function postData(url, data) {
4
5
  return fetch(url, {
@@ -284,7 +285,7 @@ function markup() {
284
285
  }
285
286
  </style>
286
287
 
287
- <div class='flow center' id='id-entry'>
288
+ <div class='flow center' id='id-entry' dir='${this.direction}'>
288
289
  <div class="nav">
289
290
  <div class="back-wrapper">
290
291
  <button type='button' data-type='icon' id="back-button" class="back-button">
@@ -331,7 +332,7 @@ function markup() {
331
332
  </form>
332
333
  </div>
333
334
 
334
- <div hidden class='flow center' id='select-mode'>
335
+ <div hidden class='flow center' id='select-mode' dir='${this.direction}'>
335
336
  <div class="nav">
336
337
  <div class="back-wrapper">
337
338
  <button type='button' data-type='icon' id="back-to-entry-button" class="back-button">
@@ -459,7 +460,7 @@ function markup() {
459
460
  </form>
460
461
  </div>
461
462
 
462
- <div hidden class='flow center' id='otp-verification'>
463
+ <div hidden class='flow center' id='otp-verification' dir='${this.direction}'>
463
464
  <div class="nav justify-right">
464
465
  <button data-type='icon' type='button' class='close-iframe'>
465
466
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none">
@@ -899,6 +900,10 @@ class TotpConsent extends HTMLElement {
899
900
  return this.getAttribute('theme-color') || '#001096';
900
901
  }
901
902
 
903
+ get direction() {
904
+ return this.getAttribute('dir') || getDirection() || 'ltr';
905
+ }
906
+
902
907
  get hideBack() {
903
908
  return this.hasAttribute('hide-back');
904
909
  }
@@ -1,3 +1,5 @@
1
+ import { t } from '../../localisation';
2
+
1
3
  class SmartCamera {
2
4
  static stream = null;
3
5
 
@@ -74,32 +76,15 @@ class SmartCamera {
74
76
  switch (e.name) {
75
77
  case 'NotAllowedError':
76
78
  case 'SecurityError':
77
- return `
78
- Looks like camera access was not granted, or was blocked by a browser
79
- level setting / extension. Please follow the prompt from the URL bar,
80
- or extensions, and enable access.
81
- You may need to refresh to start all over again
82
- `;
79
+ return t('camera.error.notAllowed');
83
80
  case 'AbortError':
84
- return `
85
- Oops! Something happened, and we lost access to your stream.
86
- Please refresh to start all over again
87
- `;
81
+ return t('camera.error.abort');
88
82
  case 'NotReadableError':
89
- return `
90
- There seems to be a problem with your device's camera, or its connection.
91
- Please check this, and when resolved, try again. Or try another device.
92
- `;
83
+ return t('camera.error.notReadable');
93
84
  case 'NotFoundError':
94
- return `
95
- We are unable to find a video stream.
96
- You may need to refresh to start all over again
97
- `;
85
+ return t('camera.error.notFound');
98
86
  case 'TypeError':
99
- return `
100
- This site is insecure, and as such cannot have access to your camera.
101
- Try to navigate to a secure version of this page, or contact the owner.
102
- `;
87
+ return t('camera.error.insecure');
103
88
  default:
104
89
  return e.message;
105
90
  }
@@ -23,5 +23,33 @@ export const IMAGE_TYPE = {
23
23
  };
24
24
 
25
25
  export const DEFAULT_NO_OF_LIVENESS_FRAMES = 8;
26
+
27
+ /**
28
+ * JPEG compression quality for captured images (selfies, liveness frames, ID documents).
29
+ *
30
+ * Value: 0.92 (92% quality)
31
+ *
32
+ * This value is optimized for identity verification and biometric matching:
33
+ * - Preserves fine facial details needed for accurate facial recognition
34
+ * - Maintains skin tone gradients without JPEG blocking artifacts
35
+ * - Ensures ID document text remains readable for OCR processing
36
+ * - Provides minimal compression artifacts that could affect liveness detection
37
+ *
38
+ * Quality comparison:
39
+ * - 1.0: No compression, very large files
40
+ * - 0.95: Virtually lossless, larger files
41
+ * - 0.92: Good quality, reasonable file size
42
+ * - 0.85: Visible artifacts in gradients and skin tones
43
+ * - 0.80: Noticeable blocking artifacts that hurt matching accuracy
44
+ *
45
+ * WARNING: DO NOT LOWER THIS VALUE BELOW 0.92
46
+ * Reducing JPEG quality can negatively impact downstream systems:
47
+ * - Facial recognition matching accuracy may decrease
48
+ * - Liveness detection anti-spoofing checks may fail
49
+ * - ID document OCR text extraction may produce errors
50
+ * - Overall verification success rates may drop
51
+ *
52
+ */
53
+ export const JPEG_QUALITY = 0.92;
26
54
  export const PORTRAIT_ID_PREVIEW_WIDTH = 396;
27
55
  export const PORTRAIT_ID_PREVIEW_HEIGHT = 527;
@@ -1,3 +1,5 @@
1
+ import { t, tHtml } from '../../localisation';
2
+
1
3
  class SmartFileUpload {
2
4
  static memoryLimit = 10240000;
3
5
 
@@ -27,11 +29,7 @@ class SmartFileUpload {
27
29
  resolve(e.target.result);
28
30
  };
29
31
  reader.onerror = () => {
30
- reject(
31
- new Error(
32
- 'An error occurred reading the file. Please check the file, and try again',
33
- ),
34
- );
32
+ reject(new Error(t('fileUpload.error.readingFile')));
35
33
  };
36
34
  reader.readAsDataURL(file);
37
35
  });
@@ -39,20 +37,21 @@ class SmartFileUpload {
39
37
 
40
38
  static async retrieve(files) {
41
39
  if (files.length > 1) {
42
- throw new Error('Only one file upload is permitted at a time');
40
+ throw new Error(t('fileUpload.error.multipleFiles'));
43
41
  }
44
42
 
45
43
  const file = files[0];
46
44
 
47
45
  if (!SmartFileUpload.supportedTypes.includes(file.type)) {
48
- throw new Error(
49
- 'Unsupported file format. Please ensure that you are providing a JPG or PNG image',
50
- );
46
+ throw new Error(t('fileUpload.error.unsupportedFormat'));
51
47
  }
52
48
 
53
49
  if (file.size > SmartFileUpload.memoryLimit) {
54
50
  throw new Error(
55
- `${file.name} is too large. Please ensure that the file is less than ${SmartFileUpload.getHumanSize(SmartFileUpload.memoryLimit)}.`,
51
+ tHtml('fileUpload.error.fileTooLarge', {
52
+ filename: file.name,
53
+ size: SmartFileUpload.getHumanSize(SmartFileUpload.memoryLimit),
54
+ }),
56
55
  );
57
56
  }
58
57