@smileid/web-components 11.4.5 → 11.6.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 (132) hide show
  1. package/dist/esm/DocumentCaptureScreens-DjSTdVP-.js +5398 -0
  2. package/dist/esm/DocumentCaptureScreens-DjSTdVP-.js.map +1 -0
  3. package/dist/esm/{Navigation-Bb7MPLE8.js → Navigation-6DH3vF4-.js} +28 -22
  4. package/dist/esm/Navigation-6DH3vF4-.js.map +1 -0
  5. package/dist/esm/{PoweredBySmileId-CxbaihMu.js → PoweredBySmileId-DoKwoPUd.js} +424 -6
  6. package/dist/esm/PoweredBySmileId-DoKwoPUd.js.map +1 -0
  7. package/dist/esm/SelfieCaptureScreens-CtX-4Tco.js +11470 -0
  8. package/dist/esm/SelfieCaptureScreens-CtX-4Tco.js.map +1 -0
  9. package/dist/esm/combobox.js +1 -1
  10. package/dist/esm/document.js +1 -1
  11. package/dist/esm/end-user-consent.js +713 -2
  12. package/dist/esm/end-user-consent.js.map +1 -1
  13. package/dist/esm/index-BqyuTk9f.js +1366 -0
  14. package/dist/esm/{index-C4RTMbgw.js.map → index-BqyuTk9f.js.map} +1 -1
  15. package/dist/esm/localisation.js +1 -1
  16. package/dist/esm/main.js +14 -14
  17. package/dist/esm/navigation.js +1 -1
  18. package/dist/esm/package-CjZI-cNQ.js +2540 -0
  19. package/dist/esm/package-CjZI-cNQ.js.map +1 -0
  20. package/dist/esm/selfie.js +1 -1
  21. package/dist/esm/smart-camera-web.js +81 -37
  22. package/dist/esm/smart-camera-web.js.map +1 -1
  23. package/dist/esm/totp-consent.js +731 -2
  24. package/dist/esm/totp-consent.js.map +1 -1
  25. package/dist/esm/validate.js +31 -0
  26. package/dist/esm/validate.js.map +1 -0
  27. package/dist/smart-camera-web.js +1513 -383
  28. package/dist/smart-camera-web.js.map +1 -1
  29. package/dist/types/main.d.ts +18 -1
  30. package/dist/types/validate.d.ts +21 -0
  31. package/lib/components/document/src/DocumentCaptureScreens.js +97 -18
  32. package/lib/components/document/src/assets/lottie.d.ts +12 -0
  33. package/lib/components/document/src/assets/svg-inline.d.ts +8 -0
  34. package/lib/components/document/src/document-auto-capture/DocumentAutoCapture.stories.js +75 -0
  35. package/lib/components/document/src/document-auto-capture/DocumentAutoCapture.tsx +1458 -0
  36. package/lib/components/document/src/document-auto-capture/README.md +73 -0
  37. package/lib/components/document/src/document-auto-capture/assets/Greenbook_Shimmer.svg +42 -0
  38. package/lib/components/document/src/document-auto-capture/assets/ID_Back_Shimmer.svg +8 -0
  39. package/lib/components/document/src/document-auto-capture/assets/ID_Front_Shimmer.svg +20 -0
  40. package/lib/components/document/src/document-auto-capture/assets/Passport-Shimmer.svg +143 -0
  41. package/lib/components/document/src/document-auto-capture/assets/shimmers.ts +21 -0
  42. package/lib/components/document/src/document-auto-capture/assets/svg-raw.d.ts +4 -0
  43. package/lib/components/document/src/document-auto-capture/components/CaptureButton.tsx +122 -0
  44. package/lib/components/document/src/document-auto-capture/components/Overlay.tsx +167 -0
  45. package/lib/components/document/src/document-auto-capture/components/TuningPanel.tsx +856 -0
  46. package/lib/components/document/src/document-auto-capture/constants/captureLayout.ts +58 -0
  47. package/lib/components/document/src/document-auto-capture/detection/cvErrorRecovery.ts +40 -0
  48. package/lib/components/document/src/document-auto-capture/detection/documentAspect.ts +20 -0
  49. package/lib/components/document/src/document-auto-capture/detection/qualityScoring.ts +35 -0
  50. package/lib/components/document/src/document-auto-capture/detection/seamRejection.ts +209 -0
  51. package/lib/components/document/src/document-auto-capture/detection/synthesisTiming.ts +10 -0
  52. package/lib/components/document/src/document-auto-capture/hooks/useCamera.ts +117 -0
  53. package/lib/components/document/src/document-auto-capture/hooks/useCardDetection.ts +3059 -0
  54. package/lib/components/document/src/document-auto-capture/index.ts +4 -0
  55. package/lib/components/document/src/document-auto-capture/theme.ts +40 -0
  56. package/lib/components/document/src/document-auto-capture/utils/debug.ts +25 -0
  57. package/lib/components/document/src/document-auto-capture/utils/opencvLoader.ts +86 -0
  58. package/lib/components/document/src/document-capture-instructions/DocumentCaptureInstructions.tsx +327 -244
  59. package/lib/components/document/src/document-capture-review/DocumentCaptureReview.js +153 -189
  60. package/lib/components/document/src/document-capture-submission/DocumentCaptureSubmission.tsx +432 -0
  61. package/lib/components/document/src/document-capture-submission/index.js +3 -0
  62. package/lib/components/navigation/src/Navigation.js +27 -8
  63. package/lib/components/selfie/README.md +13 -0
  64. package/lib/components/selfie/src/SelfieCaptureScreens.js +56 -8
  65. package/lib/components/selfie/src/enhanced-smartselfie-capture/EnhancedSmartSelfieCapture.tsx +684 -0
  66. package/lib/components/selfie/src/enhanced-smartselfie-capture/EnhancedSmartSelfieConsent.tsx +71 -0
  67. package/lib/components/selfie/src/enhanced-smartselfie-capture/EnhancedSmartSelfieSubmission.tsx +181 -0
  68. package/lib/components/selfie/src/enhanced-smartselfie-capture/OvalProgress.tsx +87 -0
  69. package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/Icon.svg +8 -0
  70. package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/accessories.svg +77 -0
  71. package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/active_liveness_animation.lottie +0 -0
  72. package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/device.svg +12 -0
  73. package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/device_orientation.lottie +0 -0
  74. package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/good.svg +52 -0
  75. package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/id-card.svg +9 -0
  76. package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/illustrations.tsx +852 -0
  77. package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/instructions-img.svg +3 -0
  78. package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/multiple-faces.svg +69 -0
  79. package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/person.svg +6 -0
  80. package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/phone.svg +8 -0
  81. package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/poor-lighting.svg +53 -0
  82. package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/too_dark_animation.lottie +0 -0
  83. package/lib/components/selfie/src/enhanced-smartselfie-capture/components/ActiveLivenessOverlay.tsx +226 -0
  84. package/lib/components/selfie/src/enhanced-smartselfie-capture/components/AlertDisplay.tsx +38 -0
  85. package/lib/components/selfie/src/enhanced-smartselfie-capture/components/BackNavigation.tsx +45 -0
  86. package/lib/components/selfie/src/enhanced-smartselfie-capture/components/CameraPreview.tsx +96 -0
  87. package/lib/components/selfie/src/enhanced-smartselfie-capture/components/CaptureControls.tsx +97 -0
  88. package/lib/components/selfie/src/enhanced-smartselfie-capture/components/CaptureGuidelines.tsx +374 -0
  89. package/lib/components/selfie/src/enhanced-smartselfie-capture/components/ConsentView.tsx +460 -0
  90. package/lib/components/selfie/src/enhanced-smartselfie-capture/components/SubmissionView.tsx +426 -0
  91. package/lib/components/selfie/src/enhanced-smartselfie-capture/components/index.ts +3 -0
  92. package/lib/components/selfie/src/enhanced-smartselfie-capture/constants.ts +23 -0
  93. package/lib/components/selfie/src/enhanced-smartselfie-capture/hooks/index.ts +2 -0
  94. package/lib/components/selfie/src/enhanced-smartselfie-capture/hooks/useCamera.ts +238 -0
  95. package/lib/components/selfie/src/enhanced-smartselfie-capture/hooks/useFaceCapture.ts +1075 -0
  96. package/lib/components/selfie/src/enhanced-smartselfie-capture/index.ts +1 -0
  97. package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/alertMessages.ts +20 -0
  98. package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/canvas.ts +108 -0
  99. package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/faceDetection.ts +545 -0
  100. package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/imageCapture.ts +66 -0
  101. package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/imageQuality.ts +151 -0
  102. package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/index.ts +5 -0
  103. package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/mediapipeManager.ts +215 -0
  104. package/lib/components/selfie/src/selfie-capture-wrapper/SelfieCaptureWrapper.tsx +24 -1
  105. package/lib/components/selfie/src/smartselfie-capture/SmartSelfieCapture.tsx +2 -2
  106. package/lib/components/selfie/src/smartselfie-capture/hooks/useFaceCapture.ts +15 -7
  107. package/lib/components/selfie/src/smartselfie-capture/utils/canvas.ts +4 -6
  108. package/lib/components/signature-pad/package.json +1 -1
  109. package/lib/components/smart-camera-web/src/README.md +11 -0
  110. package/lib/components/smart-camera-web/src/SmartCameraWeb.js +89 -8
  111. package/lib/components/totp-consent/src/TotpConsent.js +1 -1
  112. package/lib/domain/localisation/index.js +2 -2
  113. package/package.json +9 -5
  114. package/dist/esm/DocumentCaptureScreens-D2G0NOQr.js +0 -4147
  115. package/dist/esm/DocumentCaptureScreens-D2G0NOQr.js.map +0 -1
  116. package/dist/esm/EndUserConsent-uHfA3txP.js +0 -717
  117. package/dist/esm/EndUserConsent-uHfA3txP.js.map +0 -1
  118. package/dist/esm/Navigation-Bb7MPLE8.js.map +0 -1
  119. package/dist/esm/PoweredBySmileId-CxbaihMu.js.map +0 -1
  120. package/dist/esm/SelfieCaptureScreens-Dr7VzON7.js +0 -7651
  121. package/dist/esm/SelfieCaptureScreens-Dr7VzON7.js.map +0 -1
  122. package/dist/esm/TotpConsent-Depzg0ti.js +0 -734
  123. package/dist/esm/TotpConsent-Depzg0ti.js.map +0 -1
  124. package/dist/esm/index-C4RTMbgw.js +0 -1360
  125. package/dist/esm/package-D6YrpMcO.js +0 -565
  126. package/dist/esm/package-D6YrpMcO.js.map +0 -1
  127. package/dist/esm/styles-BTEClL7R.js +0 -419
  128. package/dist/esm/styles-BTEClL7R.js.map +0 -1
  129. /package/lib/components/document/src/assets/lottie/{taking photo of green book passport.lottie → greenbook.lottie} +0 -0
  130. /package/lib/components/document/src/assets/lottie/{taking photo of ID FLIP 2D.lottie → id-card-flip.lottie} +0 -0
  131. /package/lib/components/document/src/assets/lottie/{taking photo of ID.lottie → id-card.lottie} +0 -0
  132. /package/lib/components/document/src/assets/lottie/{taking photo of passport 2.lottie → passport.lottie} +0 -0
@@ -0,0 +1,66 @@
1
+ import { JPEG_QUALITY } from '../../../../../domain/constants/src/Constants';
2
+
3
+ export const captureImageFromVideo = (
4
+ videoElement: HTMLVideoElement,
5
+ isReference: boolean = false,
6
+ ): string | null => {
7
+ const canvas = document.createElement('canvas');
8
+ const ctx = canvas.getContext('2d');
9
+ if (!ctx) return null;
10
+
11
+ const isPortrait = videoElement.videoHeight > videoElement.videoWidth;
12
+
13
+ if (isReference) {
14
+ if (isPortrait) {
15
+ canvas.width = 480;
16
+ canvas.height = Math.max(
17
+ 640,
18
+ (canvas.width * videoElement.videoHeight) / videoElement.videoWidth,
19
+ );
20
+ } else {
21
+ canvas.width = 640;
22
+ canvas.height = Math.max(
23
+ 480,
24
+ (canvas.width * videoElement.videoHeight) / videoElement.videoWidth,
25
+ );
26
+ }
27
+ } else if (isPortrait) {
28
+ canvas.width = 240;
29
+ canvas.height = Math.max(
30
+ 320,
31
+ (canvas.width * videoElement.videoHeight) / videoElement.videoWidth,
32
+ );
33
+ } else {
34
+ canvas.width = 320;
35
+ canvas.height = Math.max(
36
+ 240,
37
+ (canvas.width * videoElement.videoHeight) / videoElement.videoWidth,
38
+ );
39
+ }
40
+
41
+ // capture more of the user's head and avoid clipping
42
+ const zoomOutFactor = 1;
43
+ const sourceWidth = videoElement.videoWidth * zoomOutFactor;
44
+ const sourceHeight = videoElement.videoHeight * zoomOutFactor;
45
+
46
+ // center the zoomed out area
47
+ const offsetX = (sourceWidth - videoElement.videoWidth) / 2;
48
+ const offsetY = (sourceHeight - videoElement.videoHeight) / 2;
49
+
50
+ // vertical offset to shift up and capture full head
51
+ const verticalOffset = 0;
52
+
53
+ ctx.drawImage(
54
+ videoElement,
55
+ -offsetX,
56
+ -offsetY - verticalOffset,
57
+ sourceWidth,
58
+ sourceHeight,
59
+ 0,
60
+ 0,
61
+ canvas.width,
62
+ canvas.height,
63
+ );
64
+
65
+ return canvas.toDataURL('image/jpeg', JPEG_QUALITY);
66
+ };
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Lightweight image quality checks used by the Enhanced SmartSelfie active
3
+ * liveness flow. Both routines work on small ROIs so they can run inside the
4
+ * face detection loop without noticeable cost.
5
+ */
6
+
7
+ const SAMPLE_STEP = 4; // Sample every 4th pixel — keeps work to ~6% of full ROI.
8
+
9
+ /**
10
+ * Module-scoped scratch canvases reused across frames. Allocating a fresh
11
+ * `<canvas>` per call during the capture loop (~10 fps × 2 routines) creates
12
+ * meaningful GC churn and re-enters the `willReadFrequently` software path on
13
+ * every frame. We keep one canvas per routine and only resize when the
14
+ * computed downsample target changes — assigning `width`/`height` also clears
15
+ * the bitmap, so no manual clear is required.
16
+ */
17
+ let luminanceCanvas: HTMLCanvasElement | null = null;
18
+ let luminanceCtx: CanvasRenderingContext2D | null = null;
19
+ let blurCanvas: HTMLCanvasElement | null = null;
20
+ let blurCtx: CanvasRenderingContext2D | null = null;
21
+
22
+ const getScratchContext = (
23
+ which: 'luminance' | 'blur',
24
+ dw: number,
25
+ dh: number,
26
+ ): CanvasRenderingContext2D | null => {
27
+ let canvas = which === 'luminance' ? luminanceCanvas : blurCanvas;
28
+ let ctx = which === 'luminance' ? luminanceCtx : blurCtx;
29
+
30
+ if (!canvas) {
31
+ canvas = document.createElement('canvas');
32
+ ctx = canvas.getContext('2d', { willReadFrequently: true });
33
+ if (which === 'luminance') {
34
+ luminanceCanvas = canvas;
35
+ luminanceCtx = ctx;
36
+ } else {
37
+ blurCanvas = canvas;
38
+ blurCtx = ctx;
39
+ }
40
+ }
41
+ if (!ctx) return null;
42
+
43
+ if (canvas.width !== dw) canvas.width = dw;
44
+ if (canvas.height !== dh) canvas.height = dh;
45
+ return ctx;
46
+ };
47
+
48
+ /**
49
+ * Average BT.601 luma (0–255) over a region of the video frame.
50
+ *
51
+ * @param video live video element
52
+ * @param region optional normalised ROI (defaults to centre 60%)
53
+ */
54
+ export const calculateLuminance = (
55
+ video: HTMLVideoElement,
56
+ region?: { x: number; y: number; width: number; height: number },
57
+ ): number => {
58
+ if (!video || video.videoWidth === 0 || video.videoHeight === 0) return 0;
59
+
60
+ const w = video.videoWidth;
61
+ const h = video.videoHeight;
62
+ const r = region ?? { x: 0.2, y: 0.2, width: 0.6, height: 0.6 };
63
+
64
+ const sx = Math.max(0, Math.floor(r.x * w));
65
+ const sy = Math.max(0, Math.floor(r.y * h));
66
+ const sw = Math.min(w - sx, Math.floor(r.width * w));
67
+ const sh = Math.min(h - sy, Math.floor(r.height * h));
68
+ if (sw <= 0 || sh <= 0) return 0;
69
+
70
+ // Downsample heavily — 64x64 max — to keep this near-free.
71
+ const targetSize = 64;
72
+ const scale = Math.min(1, targetSize / Math.max(sw, sh));
73
+ const dw = Math.max(1, Math.round(sw * scale));
74
+ const dh = Math.max(1, Math.round(sh * scale));
75
+
76
+ const ctx = getScratchContext('luminance', dw, dh);
77
+ if (!ctx) return 0;
78
+
79
+ ctx.drawImage(video, sx, sy, sw, sh, 0, 0, dw, dh);
80
+ const { data } = ctx.getImageData(0, 0, dw, dh);
81
+
82
+ let total = 0;
83
+ let count = 0;
84
+ for (let i = 0; i < data.length; i += 4 * SAMPLE_STEP) {
85
+ // BT.601 luma — same approximation OpenCV uses for cvtColor RGB→GRAY.
86
+ total += 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
87
+ count += 1;
88
+ }
89
+ return count === 0 ? 0 : total / count;
90
+ };
91
+
92
+ /**
93
+ * Estimate sharpness as the variance of a 3x3 Laplacian filter applied to the
94
+ * grayscaled ROI. Higher = sharper. Empirically values < ~30 indicate
95
+ * meaningful motion blur for a downsampled webcam frame.
96
+ */
97
+ export const calculateBlurScore = (
98
+ video: HTMLVideoElement,
99
+ region?: { x: number; y: number; width: number; height: number },
100
+ ): number => {
101
+ if (!video || video.videoWidth === 0 || video.videoHeight === 0) return 0;
102
+
103
+ const w = video.videoWidth;
104
+ const h = video.videoHeight;
105
+ const r = region ?? { x: 0.25, y: 0.25, width: 0.5, height: 0.5 };
106
+
107
+ const sx = Math.max(0, Math.floor(r.x * w));
108
+ const sy = Math.max(0, Math.floor(r.y * h));
109
+ const sw = Math.min(w - sx, Math.floor(r.width * w));
110
+ const sh = Math.min(h - sy, Math.floor(r.height * h));
111
+ if (sw <= 0 || sh <= 0) return 0;
112
+
113
+ const targetSize = 96;
114
+ const scale = Math.min(1, targetSize / Math.max(sw, sh));
115
+ const dw = Math.max(3, Math.round(sw * scale));
116
+ const dh = Math.max(3, Math.round(sh * scale));
117
+
118
+ const ctx = getScratchContext('blur', dw, dh);
119
+ if (!ctx) return 0;
120
+
121
+ ctx.drawImage(video, sx, sy, sw, sh, 0, 0, dw, dh);
122
+ const { data } = ctx.getImageData(0, 0, dw, dh);
123
+
124
+ // Convert to grayscale once.
125
+ const gray = new Float32Array(dw * dh);
126
+ for (let i = 0; i < gray.length; i += 1) {
127
+ const o = i * 4;
128
+ gray[i] = 0.299 * data[o] + 0.587 * data[o + 1] + 0.114 * data[o + 2];
129
+ }
130
+
131
+ // Variance of 3x3 Laplacian (skip 1-px border).
132
+ let sum = 0;
133
+ let sumSq = 0;
134
+ let n = 0;
135
+ for (let y = 1; y < dh - 1; y += 1) {
136
+ for (let x = 1; x < dw - 1; x += 1) {
137
+ const i = y * dw + x;
138
+ const lap =
139
+ 4 * gray[i] - gray[i - 1] - gray[i + 1] - gray[i - dw] - gray[i + dw];
140
+ sum += lap;
141
+ sumSq += lap * lap;
142
+ n += 1;
143
+ }
144
+ }
145
+ if (n === 0) return 0;
146
+ const mean = sum / n;
147
+ return sumSq / n - mean * mean;
148
+ };
149
+
150
+ export const DEFAULT_LUMINANCE_MIN = 85; // average luma below this = "too dark"
151
+ export const DEFAULT_BLUR_MIN = 80; // Laplacian variance below this = "too blurry"
@@ -0,0 +1,5 @@
1
+ export * from './faceDetection';
2
+ export * from './canvas';
3
+ export * from './imageCapture';
4
+ export * from './imageQuality';
5
+ export * from './alertMessages';
@@ -0,0 +1,215 @@
1
+ import { FaceLandmarker, FilesetResolver } from '@mediapipe/tasks-vision';
2
+
3
+ const EXCLUDED_GPUS = [
4
+ 'adreno-830',
5
+ 'adreno-8xx',
6
+ 'adreno-9xx',
7
+ 'adreno-840',
8
+ 'adreno-810',
9
+ ];
10
+
11
+ const normalizeGpuText = (value: string): string =>
12
+ value
13
+ .toLowerCase()
14
+ .replace(/\(tm\)|\btm\b/g, '')
15
+ .replace(/[^a-z0-9]/g, '');
16
+
17
+ const matchesExcludedGpu = (value: string): boolean => {
18
+ const normalizedValue = normalizeGpuText(value);
19
+
20
+ return EXCLUDED_GPUS.some((gpuPattern) => {
21
+ const normalizedPattern = normalizeGpuText(gpuPattern);
22
+
23
+ if (normalizedPattern.endsWith('xx')) {
24
+ const familyPrefix = normalizedPattern.slice(0, -2);
25
+ return new RegExp(`${familyPrefix}\\d{2}`).test(normalizedValue);
26
+ }
27
+
28
+ return normalizedValue.includes(normalizedPattern);
29
+ });
30
+ };
31
+
32
+ /**
33
+ * @description Gets the GPU renderer string using WebGL debug info extension.
34
+ * @returns {string | null} The GPU renderer string or null if unavailable.
35
+ */
36
+ const getGpuRenderer = (): string | null => {
37
+ try {
38
+ const canvas = document.createElement('canvas');
39
+ const gl =
40
+ canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
41
+ if (!gl || !(gl instanceof WebGLRenderingContext)) return null;
42
+
43
+ const ext = gl.getExtension('WEBGL_debug_renderer_info');
44
+ if (!ext) return null;
45
+
46
+ return gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) as string | null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ };
51
+
52
+ /**
53
+ * @description Checks if the GPU renderer matches any excluded GPU.
54
+ * @param {string | null} [renderer] Optional GPU renderer string to use. If not provided, it will be fetched via WebGL.
55
+ * @returns {boolean} True if the GPU is excluded.
56
+ */
57
+ const isExcludedGpuFromWebGL = (renderer?: string | null): boolean => {
58
+ const rendererString = (renderer ?? getGpuRenderer())?.toLowerCase() ?? '';
59
+ if (!rendererString) return false;
60
+
61
+ return matchesExcludedGpu(rendererString);
62
+ };
63
+
64
+ declare global {
65
+ interface Window {
66
+ __smileIdentityMediapipe?: {
67
+ instance: FaceLandmarker | null;
68
+ loading: Promise<FaceLandmarker> | null;
69
+ loaded: boolean;
70
+ };
71
+ }
72
+ }
73
+
74
+ /**
75
+ * @description Reads system architecture hints from User-Agent Client Hints.
76
+ * @returns {Promise<string | null>} Lower-cased hint string or null when hints are unavailable.
77
+ */
78
+ const getSystemArchitectureHints = async (): Promise<string | null> => {
79
+ if (typeof navigator === 'undefined' || !(navigator as any).userAgentData) {
80
+ return null;
81
+ }
82
+
83
+ try {
84
+ const hints = await (navigator as any).userAgentData.getHighEntropyValues([
85
+ 'architecture',
86
+ 'model',
87
+ 'platform',
88
+ 'platformVersion',
89
+ 'fullVersionList',
90
+ ]);
91
+
92
+ return JSON.stringify(hints).toLowerCase();
93
+ } catch (error) {
94
+ console.warn('UA-CH architecture hints fetch failed.', error);
95
+ return null;
96
+ }
97
+ };
98
+
99
+ /**
100
+ * @description Determines the MediaPipe delegate based on WebGL renderer info and UA-CH hints.
101
+ * Uses WebGL renderer as primary detection, UA-CH hints as secondary.
102
+ * @returns {Promise<'CPU' | 'GPU'>} CPU when excluded GPU is detected; otherwise GPU.
103
+ */
104
+ const getDelegateFromGpuDetection = async (): Promise<'CPU' | 'GPU'> => {
105
+ const renderer = getGpuRenderer();
106
+ const hasWebGlRendererInfo = !!renderer;
107
+
108
+ // Primary check: WebGL renderer info (most reliable for GPU detection)
109
+ if (isExcludedGpuFromWebGL(renderer)) {
110
+ console.info(`[SmileID] Excluded GPU via WebGL: ${renderer}. Using CPU.`);
111
+ return 'CPU';
112
+ }
113
+
114
+ // Secondary check: UA-CH hints (may contain GPU info in some browsers)
115
+ const hintString = await getSystemArchitectureHints();
116
+ const hasUaHints = !!hintString;
117
+
118
+ if (hintString) {
119
+ const hasExcludedGpuInHints = matchesExcludedGpu(hintString);
120
+
121
+ if (hasExcludedGpuInHints) {
122
+ console.info(`[SmileID] Excluded GPU via UA-CH hints. Using CPU.`);
123
+ return 'CPU';
124
+ }
125
+ }
126
+
127
+ if (!hasWebGlRendererInfo && !hasUaHints) {
128
+ console.info(
129
+ '[SmileID] No WebGL renderer or UA-CH hints available. Using CPU.',
130
+ );
131
+ return 'CPU';
132
+ }
133
+
134
+ // Default to GPU when no exclusion is detected
135
+ console.info(
136
+ `[SmileID] No excluded GPU detected. WebGL renderer: ${renderer ?? 'unavailable'}. Using GPU.`,
137
+ );
138
+ return 'GPU';
139
+ };
140
+
141
+ // this was added because devices (mostly older) that do not support FP16 will fail to load the model.
142
+ const hasFP16Support = () => {
143
+ const canvas = document.createElement('canvas');
144
+ const gl =
145
+ canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
146
+ if (!gl) return false;
147
+
148
+ const hasHalfFloatExt = (gl as any).getExtension('OES_texture_half_float');
149
+ const hasHalfFloatLinear = (gl as any).getExtension(
150
+ 'OES_texture_half_float_linear',
151
+ );
152
+ const hasColorBufferHalfFloat = (gl as any).getExtension(
153
+ 'EXT_color_buffer_half_float',
154
+ );
155
+
156
+ return !!(hasHalfFloatExt && hasColorBufferHalfFloat && hasHalfFloatLinear);
157
+ };
158
+
159
+ export const getMediapipeInstance = async (): Promise<FaceLandmarker> => {
160
+ if (!window.__smileIdentityMediapipe) {
161
+ window.__smileIdentityMediapipe = {
162
+ instance: null,
163
+ loading: null,
164
+ loaded: false,
165
+ };
166
+ }
167
+
168
+ const mediapipeGlobal = window.__smileIdentityMediapipe;
169
+
170
+ if (mediapipeGlobal.loaded && mediapipeGlobal.instance) {
171
+ return mediapipeGlobal.instance;
172
+ }
173
+
174
+ if (mediapipeGlobal.loading) {
175
+ return mediapipeGlobal.loading;
176
+ }
177
+
178
+ mediapipeGlobal.loading = (async () => {
179
+ try {
180
+ const vision = await FilesetResolver.forVisionTasks(
181
+ 'https://web-models.smileidentity.com/mediapipe-tasks-vision-wasm',
182
+ );
183
+
184
+ const gpuDelegate = await getDelegateFromGpuDetection();
185
+ const delegate =
186
+ gpuDelegate === 'CPU' || !hasFP16Support() ? 'CPU' : 'GPU';
187
+
188
+ const faceLandmarker = await FaceLandmarker.createFromOptions(vision, {
189
+ baseOptions: {
190
+ modelAssetPath: `https://web-models.smileidentity.com/face_landmarker/face_landmarker.task`,
191
+ delegate,
192
+ },
193
+ outputFaceBlendshapes: true,
194
+ runningMode: 'VIDEO',
195
+ numFaces: 2,
196
+ });
197
+
198
+ mediapipeGlobal.instance = faceLandmarker;
199
+ mediapipeGlobal.loaded = true;
200
+ mediapipeGlobal.loading = null;
201
+
202
+ return faceLandmarker;
203
+ } catch (error) {
204
+ mediapipeGlobal.loading = null;
205
+ throw error;
206
+ }
207
+ })();
208
+
209
+ return mediapipeGlobal.loading;
210
+ };
211
+
212
+ export const __testUtils = {
213
+ matchesExcludedGpu,
214
+ getDelegateFromGpuDetection,
215
+ };
@@ -6,6 +6,7 @@ import type { FunctionComponent } from 'preact';
6
6
  import { getBoolProp } from '../../../../utils/props';
7
7
  import { translate, translateHtml } from '../../../../domain/localisation';
8
8
  import SmartSelfieCapture from '../smartselfie-capture/SmartSelfieCapture';
9
+ import EnhancedSmartSelfieCapture from '../enhanced-smartselfie-capture/EnhancedSmartSelfieCapture';
9
10
  // Legacy web component fallback (used when Mediapipe isn't available)
10
11
  import '../selfie-capture/SelfieCapture';
11
12
  // Mediapipe loader/manager used by SmartSelfieCapture
@@ -40,6 +41,8 @@ interface Props {
40
41
  'show-agent-mode-for-tests'?: string | boolean;
41
42
  'hide-attribution'?: string | boolean;
42
43
  'disable-image-tests'?: string | boolean;
44
+ 'use-strict-mode'?: string | boolean;
45
+ 'show-back-on-guidelines'?: string | boolean;
43
46
  key?: string;
44
47
  'start-countdown'?: string | boolean;
45
48
  hidden?: string | boolean;
@@ -73,6 +76,7 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
73
76
  timeout,
74
77
  'start-countdown': startCountdownProp = false,
75
78
  'allow-legacy-selfie-fallback': allowLegacySelfieFallbackProp = false,
79
+ 'use-strict-mode': useStrictModeProp = false,
76
80
  hidden: hiddenProp = false,
77
81
  ...props
78
82
  }) => {
@@ -106,7 +110,11 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
106
110
 
107
111
  const hidden = getBoolProp(hiddenProp);
108
112
  const startCountdown = getBoolProp(startCountdownProp);
109
- const allowLegacySelfieFallback = getBoolProp(allowLegacySelfieFallbackProp);
113
+ const useStrictMode = getBoolProp(useStrictModeProp);
114
+ // Strict mode (Enhanced SmartSelfie) requires Mediapipe head-pose detection,
115
+ // so the legacy fallback is force-disabled regardless of the partner setting.
116
+ const allowLegacySelfieFallback =
117
+ !useStrictMode && getBoolProp(allowLegacySelfieFallbackProp);
110
118
 
111
119
  // Resolve how long we'll wait for Mediapipe before the hard deadline fires.
112
120
  // Precedence:
@@ -463,6 +471,19 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
463
471
  return null;
464
472
  }
465
473
 
474
+ // Strict mode (Enhanced SmartSelfie) owns its own loading UX. Mount it
475
+ // immediately — the guidelines screen doesn't need Mediapipe, and by the
476
+ // time the user reaches the capture screen the background load started by
477
+ // `useFaceCapture.initializeFaceLandmarker()` will normally have resolved.
478
+ if (useStrictMode) {
479
+ return (
480
+ <>
481
+ <style>{`:host { display: block; height: 100%; }`}</style>
482
+ <EnhancedSmartSelfieCapture {...(props as any)} />
483
+ </>
484
+ );
485
+ }
486
+
466
487
  // on retakes, prefer SmartSelfieCapture if Mediapipe is ready
467
488
  if (initialSessionCompleted && mediapipeReady && !usingSelfieCapture) {
468
489
  return <SmartSelfieCapture {...props} />;
@@ -613,6 +634,8 @@ if (!customElements.get('selfie-capture-wrapper')) {
613
634
  'show-agent-mode-for-tests',
614
635
  'hide-attribution',
615
636
  'disable-image-tests',
637
+ 'use-strict-mode',
638
+ 'show-back-on-guidelines',
616
639
  'key',
617
640
  'start-countdown',
618
641
  'hidden',
@@ -237,11 +237,11 @@ const SmartSelfieCapture: FunctionComponent<Props> = ({
237
237
  font-family: "DM Sans", sans-serif;
238
238
  cursor: pointer;
239
239
  }
240
-
240
+
241
241
  button.btn-primary:hover {
242
242
  background-color: #2d2b2a;
243
243
  }
244
-
244
+
245
245
  button.btn-primary:disabled {
246
246
  background-color: #666;
247
247
  cursor: not-allowed;
@@ -336,6 +336,17 @@ export const useFaceCapture = ({
336
336
  }, 0);
337
337
  }
338
338
  }
339
+
340
+ // Before the smile zone, resume automatically when face returns to a
341
+ // valid position — no smile is needed yet.
342
+ if (
343
+ isPaused.value &&
344
+ isCapturing.value &&
345
+ capturesTaken.value < smileCheckpoint.value &&
346
+ resumeCaptureRef.current
347
+ ) {
348
+ resumeCaptureRef.current();
349
+ }
339
350
  } else {
340
351
  // No face detected - reset values
341
352
  currentSmileScore.value = 0;
@@ -414,9 +425,7 @@ export const useFaceCapture = ({
414
425
  };
415
426
 
416
427
  window.dispatchEvent(
417
- new CustomEvent('selfie-capture.publish', {
418
- detail: eventDetail,
419
- }),
428
+ new CustomEvent('selfie-capture.publish', { detail: eventDetail }),
420
429
  );
421
430
 
422
431
  hasFinishedCapture.value = true;
@@ -515,10 +524,9 @@ export const useFaceCapture = ({
515
524
  smartCameraWeb?.dispatchEvent(
516
525
  new CustomEvent('metadata.selfie-origin', {
517
526
  detail: {
518
- imageOrigin: {
519
- environment: 'back_camera',
520
- user: 'front_camera',
521
- }[getFacingMode()],
527
+ imageOrigin: { environment: 'back_camera', user: 'front_camera' }[
528
+ getFacingMode()
529
+ ],
522
530
  },
523
531
  }),
524
532
  );
@@ -1,8 +1,8 @@
1
- import { DrawingUtils, FaceLandmarker } from '@mediapipe/tasks-vision';
2
-
3
1
  /**
4
2
  * Create a cropped square canvas from video for face detection
5
3
  */
4
+ import { DrawingUtils, FaceLandmarker } from '@mediapipe/tasks-vision';
5
+
6
6
  export const createCroppedVideoFrame = (
7
7
  videoElement: HTMLVideoElement,
8
8
  ): HTMLCanvasElement | null => {
@@ -51,16 +51,14 @@ export const drawFaceMesh = (
51
51
  const canvasHeight = canvas.height;
52
52
 
53
53
  ctx.clearRect(0, 0, canvasWidth, canvasHeight);
54
- const drawingUtils = new DrawingUtils(ctx);
55
54
 
56
- // use this if scaling is needed
57
- // const scaleFactor = Math.sqrt(canvasWidth * canvasHeight) / 500;
55
+ const drawingUtils = new DrawingUtils(ctx);
58
56
 
59
57
  landmarks.forEach((landmark) => {
60
58
  if (!landmark || landmark.length === 0) return;
61
59
 
62
60
  const outlineColor = 'rgba(162, 155, 254,0.4)';
63
- const lineWidth = 2; // Math.max(1, scaleFactor * 2);
61
+ const lineWidth = 2;
64
62
  ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
65
63
  ctx.lineWidth = lineWidth;
66
64
  ctx.lineCap = 'round';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smileid/signature-pad",
3
- "version": "11.4.5",
3
+ "version": "11.6.0",
4
4
  "private": true,
5
5
  "exports": {
6
6
  ".": "./index.js"
@@ -193,6 +193,17 @@ After installation and necessary imports:
193
193
 
194
194
  This approach can also be achieved using other Server to Server libraries.
195
195
 
196
+ ### Enhanced SmartSelfie
197
+
198
+ For web-component integrations, Enhanced SmartSelfie is enabled with the
199
+ `use-strict-mode` attribute on the component.
200
+
201
+ ```html
202
+ <smart-camera-web use-strict-mode="true"></smart-camera-web>
203
+ ```
204
+
205
+ - Set `use-strict-mode="true"` to enable strict-mode selfie capture.
206
+
196
207
  ## Compatibility
197
208
 
198
209
  `SmartCameraWeb` is compatible with most JavaScript frameworks and libraries. For integration with [ReactJS](https://reactjs.org), refer to this [tutorial](https://www.robinwieruch.de/react-web-components) due to React-WebComponents compatibility issues.