@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.
- package/README.md +61 -0
- package/dist/components/smart-camera-web/src/README.md +0 -1
- package/dist/esm/{DocumentCaptureScreens-C5BhNB-0.js → DocumentCaptureScreens-BbtA-WkX.js} +199 -193
- package/dist/esm/DocumentCaptureScreens-BbtA-WkX.js.map +1 -0
- package/dist/esm/{EndUserConsent-D4fd1ovG.js → EndUserConsent-HVufMamg.js} +65 -63
- package/dist/esm/EndUserConsent-HVufMamg.js.map +1 -0
- package/dist/esm/{Navigation-CTjK6tLU.js → Navigation-B-dqPkZj.js} +17 -9
- package/dist/esm/Navigation-B-dqPkZj.js.map +1 -0
- package/dist/esm/{SelfieCaptureScreens-KoQpCxtc.js → SelfieCaptureScreens-ChAMfKi3.js} +3274 -3329
- package/dist/esm/SelfieCaptureScreens-ChAMfKi3.js.map +1 -0
- package/dist/esm/{TotpConsent-CQU5jQi4.js → TotpConsent-XxR8TNxy.js} +13 -9
- package/dist/esm/TotpConsent-XxR8TNxy.js.map +1 -0
- package/dist/esm/combobox.js +20 -19
- package/dist/esm/combobox.js.map +1 -1
- package/dist/esm/document.js +1 -1
- package/dist/esm/end-user-consent.js +1 -1
- package/dist/esm/index-B_ozpejI.js +1360 -0
- package/dist/esm/index-B_ozpejI.js.map +1 -0
- package/dist/esm/localisation.js +21 -0
- package/dist/esm/localisation.js.map +1 -0
- package/dist/esm/main.js +34 -17
- package/dist/esm/main.js.map +1 -1
- package/dist/esm/navigation.js +1 -1
- package/dist/esm/{package-B-UwEdv7.js → package-u3FEJ3Fm.js} +25 -40
- package/dist/esm/package-u3FEJ3Fm.js.map +1 -0
- package/dist/esm/selfie.js +1 -1
- package/dist/esm/smart-camera-web.js +32 -23
- package/dist/esm/smart-camera-web.js.map +1 -1
- package/dist/esm/totp-consent.js +1 -1
- package/dist/package.json +1 -1
- package/dist/smart-camera-web.js +144 -160
- package/dist/smart-camera-web.js.map +1 -1
- package/dist/src/components/combobox/src/index.js +424 -1
- package/dist/src/components/document/src/index.js +1422 -1
- package/dist/src/components/end-user-consent/src/index.js +1573 -1
- package/dist/src/components/selfie/src/index.js +1220 -1
- package/dist/src/components/signature-pad/src/index.js +787 -1
- package/dist/src/components/smart-camera-web/src/SmartCameraWeb.js +2753 -1
- package/dist/src/components/totp-consent/src/index.js +1292 -1
- package/dist/types/combobox.d.ts +2 -2
- package/dist/types/document.d.ts +2 -2
- package/dist/types/end-user-consent.d.ts +2 -2
- package/dist/types/locale.d.ts +19 -0
- package/dist/types/localisation.d.ts +21 -0
- package/dist/types/main.d.ts +35 -26
- package/dist/types/navigation.d.ts +2 -2
- package/dist/types/selfie.d.ts +2 -2
- package/dist/types/signature-pad.d.ts +2 -2
- package/dist/types/smart-camera-web.d.ts +2 -2
- package/dist/types/totp-consent.d.ts +2 -2
- package/lib/components/camera-permission/CameraPermission.js +9 -4
- package/lib/components/combobox/src/Combobox.js +4 -2
- package/lib/components/document/src/DocumentCaptureScreens.js +4 -3
- package/lib/components/document/src/DocumentCaptureScreens.stories.js +37 -13
- package/lib/components/document/src/document-capture/DocumentCapture.js +23 -17
- package/lib/components/document/src/document-capture/DocumentCapture.stories.js +11 -2
- package/lib/components/document/src/document-capture-instructions/DocumentCaptureInstructions.js +19 -14
- package/lib/components/document/src/document-capture-instructions/DocumentCaptureInstructions.stories.js +14 -5
- package/lib/components/document/src/document-capture-review/DocumentCaptureReview.js +14 -10
- package/lib/components/document/src/document-capture-review/DocumentCaptureReview.stories.js +14 -5
- package/lib/components/end-user-consent/src/EndUserConsent.js +30 -29
- package/lib/components/end-user-consent/src/EndUserConsent.stories.js +12 -2
- package/lib/components/navigation/src/Navigation.js +15 -2
- package/lib/components/navigation/src/Navigation.stories.js +20 -4
- package/lib/components/selfie/src/SelfieCaptureScreens.js +12 -8
- package/lib/components/selfie/src/SelfieCaptureScreens.stories.js +16 -4
- package/lib/components/selfie/src/selfie-capture/SelfieCapture.js +25 -18
- package/lib/components/selfie/src/selfie-capture/SelfieCapture.stories.js +19 -7
- package/lib/components/selfie/src/selfie-capture-instructions/SelfieCaptureInstructions.js +19 -14
- package/lib/components/selfie/src/selfie-capture-instructions/SelfieCaptureInstructions.stories.js +14 -5
- package/lib/components/selfie/src/selfie-capture-review/SelfieCaptureReview.js +13 -8
- package/lib/components/selfie/src/selfie-capture-review/SelfieCaptureReview.stories.js +14 -5
- package/lib/components/selfie/src/selfie-capture-wrapper/SelfieCaptureWrapper.tsx +98 -47
- package/lib/components/selfie/src/smartselfie-capture/SmartSelfieCapture.tsx +2 -2
- package/lib/components/selfie/src/smartselfie-capture/components/CaptureControls.tsx +5 -2
- package/lib/components/selfie/src/smartselfie-capture/hooks/useCamera.ts +4 -4
- package/lib/components/selfie/src/smartselfie-capture/hooks/useFaceCapture.ts +6 -5
- package/lib/components/selfie/src/smartselfie-capture/utils/alertMessages.ts +11 -9
- package/lib/components/selfie/src/smartselfie-capture/utils/imageCapture.ts +3 -1
- package/lib/components/signature-pad/package.json +1 -1
- package/lib/components/smart-camera-web/src/SmartCameraWeb.js +9 -1
- package/lib/components/totp-consent/src/TotpConsent.js +8 -3
- package/lib/domain/camera/src/SmartCamera.js +7 -22
- package/lib/domain/constants/src/Constants.js +28 -0
- package/lib/domain/file-upload/src/SmartFileUpload.js +9 -10
- package/lib/domain/localisation/index.js +456 -0
- package/package.json +13 -7
- package/dist/esm/DocumentCaptureScreens-C5BhNB-0.js.map +0 -1
- package/dist/esm/EndUserConsent-D4fd1ovG.js.map +0 -1
- package/dist/esm/Navigation-CTjK6tLU.js.map +0 -1
- package/dist/esm/SelfieCaptureScreens-KoQpCxtc.js.map +0 -1
- package/dist/esm/TotpConsent-CQU5jQi4.js.map +0 -1
- 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 '
|
|
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
|
|
35
|
+
// web component after a timeout (default 90 seconds).
|
|
31
36
|
const SelfieCaptureWrapper: FunctionComponent<Props> = ({
|
|
32
|
-
timeout =
|
|
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
|
-
},
|
|
108
|
+
}, loadingTime / 100);
|
|
100
109
|
|
|
101
110
|
return () => {
|
|
102
111
|
clearInterval(timer);
|
|
103
112
|
};
|
|
104
|
-
}, [hidden, startCountdown,
|
|
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
|
|
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,
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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'
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 = '
|
|
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 = '
|
|
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 = '
|
|
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
|
|
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':
|
|
3
|
-
'no-face':
|
|
4
|
-
'out-of-bounds':
|
|
5
|
-
'too-close':
|
|
6
|
-
'too-far':
|
|
7
|
-
'neutral-expression':
|
|
8
|
-
'smile-required': '
|
|
9
|
-
'open-mouth-smile':
|
|
10
|
-
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
|
};
|
|
@@ -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('
|
|
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
|
-
|
|
51
|
+
tHtml('fileUpload.error.fileTooLarge', {
|
|
52
|
+
filename: file.name,
|
|
53
|
+
size: SmartFileUpload.getHumanSize(SmartFileUpload.memoryLimit),
|
|
54
|
+
}),
|
|
56
55
|
);
|
|
57
56
|
}
|
|
58
57
|
|