@smileid/web-components 11.0.1 → 11.0.2

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.
@@ -5,7 +5,9 @@ import type { FunctionComponent } from 'preact';
5
5
 
6
6
  import { getBoolProp } from '@/utils/props';
7
7
  import SmartSelfieCapture from '../smartselfie-capture/SmartSelfieCapture';
8
+ // Legacy web component fallback (used when Mediapipe isn't available)
8
9
  import '../selfie-capture/SelfieCapture';
10
+ // Mediapipe loader/manager used by SmartSelfieCapture
9
11
  import { getMediapipeInstance } from '../smartselfie-capture/utils/mediapipeManager';
10
12
 
11
13
  interface Props {
@@ -23,12 +25,16 @@ interface Props {
23
25
  hidden?: string | boolean;
24
26
  }
25
27
 
28
+ // Wrapper component that decides whether to use the modern
29
+ // SmartSelfieCapture (Mediapipe-based) or fallback to the legacy `selfie-capture`
30
+ // web component after a timeout of 20 seconds.
26
31
  const SelfieCaptureWrapper: FunctionComponent<Props> = ({
27
32
  timeout = 20000,
28
33
  'start-countdown': startCountdownProp = false,
29
34
  hidden: hiddenProp = false,
30
35
  ...props
31
36
  }) => {
37
+ // Detect if tests are running under Cypress/Electron (affects loading behavior)
32
38
  const isParentCypress = (() => {
33
39
  try {
34
40
  return (
@@ -48,12 +54,21 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
48
54
 
49
55
  const hidden = getBoolProp(hiddenProp);
50
56
  const startCountdown = getBoolProp(startCountdownProp);
57
+ // Component state:
58
+ // - mediapipeReady: whether the mediapipe instance has successfully loaded
59
+ // - loadingProgress: percentage used for the visible loading UI
60
+ // - initialSessionCompleted: set when the legacy component emits publish/cancel/close
61
+ // - mediapipeLoading: true while attempting to load mediapipe
62
+ // - usingSelfieCapture: whether we've mounted the legacy `selfie-capture` element
51
63
  const [mediapipeReady, setMediapipeReady] = useState(false);
52
64
  const [loadingProgress, setLoadingProgress] = useState(isCypress ? 100 : 0);
53
65
  const [initialSessionCompleted, setInitialSessionCompleted] = useState(false);
54
66
  const [mediapipeLoading, setMediapipeLoading] = useState(false);
55
67
  const [usingSelfieCapture, setUsingSelfieCapture] = useState(false);
56
68
 
69
+ // Attempt to load Mediapipe (once). If Mediapipe is already ready, loading,
70
+ // or running under Cypress, skip the attempt. This side-effect flips
71
+ // mediapipeReady/mediapipeLoading flags which control which component is used.
57
72
  useEffect(() => {
58
73
  if (mediapipeReady || mediapipeLoading || isCypress) return;
59
74
 
@@ -63,6 +78,8 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
63
78
  await getMediapipeInstance();
64
79
  setMediapipeReady(true);
65
80
  } catch (error) {
81
+ // Loading failed; we'll fall back to the legacy selfie-capture component
82
+ // after the loadingProgress reaches 100%.
66
83
  console.error('Failed to load Mediapipe:', error);
67
84
  }
68
85
  setMediapipeLoading(false);
@@ -71,6 +88,9 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
71
88
  loadMediapipe();
72
89
  }, [mediapipeReady, mediapipeLoading]);
73
90
 
91
+ // When using the loading countdown (startCountdown), increment the
92
+ // visible loading progress. This is only used while mediapipe hasn't
93
+ // reported ready; once mediapipeReady becomes true we stop the timer.
74
94
  useEffect(() => {
75
95
  if (hidden || !startCountdown || mediapipeReady) return undefined;
76
96
 
@@ -135,6 +155,8 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
135
155
  };
136
156
  }, [hidden, mediapipeReady, loadingProgress]);
137
157
 
158
+ // 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.
138
160
  useEffect(() => {
139
161
  if (hidden || mediapipeLoading) return;
140
162
 
@@ -163,6 +185,9 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
163
185
  }
164
186
 
165
187
  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).
166
191
  if (!usingSelfieCapture) {
167
192
  setUsingSelfieCapture(true);
168
193
  }
@@ -1,5 +1,8 @@
1
1
  import { FaceLandmarker, FilesetResolver } from '@mediapipe/tasks-vision';
2
2
 
3
+ // SM-S931 (for the standard S25), SM-S936 (for the S25+), and SM-S938 (for the S25 Ultra)
4
+ const EXCLUDED_DEVICES = ['sm-s936', 'sm-s931', 'sm-s938'];
5
+
3
6
  declare global {
4
7
  interface Window {
5
8
  __smileIdentityMediapipe?: {
@@ -10,6 +13,42 @@ declare global {
10
13
  }
11
14
  }
12
15
 
16
+ /**
17
+ * @description Detects if the user is on an excluded device using the modern and more accurate
18
+ * User-Agent Client Hints (UA-CH) API to get the device model.
19
+ * @returns {Promise<boolean>} - True if the device model is in the exclusion list.
20
+ */
21
+ const isExcludedDeviceUsingHints = async (): Promise<boolean> => {
22
+ // Check for User-Agent Client Hints API support
23
+ if (typeof navigator !== 'undefined' && navigator.userAgentData) {
24
+ try {
25
+ // Request the 'model' high-entropy value and destructure it directly
26
+ const { model } = await navigator.userAgentData.getHighEntropyValues([
27
+ 'model',
28
+ ]);
29
+
30
+ if (!model) {
31
+ return false;
32
+ }
33
+
34
+ const lowerModel = model.toLowerCase();
35
+
36
+ // Check if the extracted model string matches any of the excluded prefixes
37
+ return EXCLUDED_DEVICES.some((prefix) => lowerModel.includes(prefix));
38
+ } catch (error) {
39
+ // Log the error but fail safe (return false)
40
+ console.warn(
41
+ 'UA-CH model fetch failed, falling back to UA string check.',
42
+ error,
43
+ );
44
+ return false;
45
+ }
46
+ }
47
+ // If API is not supported, return false (rely on synchronous isExcludedDevice)
48
+ return false;
49
+ };
50
+
51
+ // this was added because devices (mostly older) that do not support FP16 will fail to load the model.
13
52
  const hasFP16Support = () => {
14
53
  const canvas = document.createElement('canvas');
15
54
  const gl =
@@ -52,10 +91,12 @@ export const getMediapipeInstance = async (): Promise<FaceLandmarker> => {
52
91
  'https://web-models.smileidentity.com/mediapipe-tasks-vision-wasm',
53
92
  );
54
93
 
94
+ const isExcluded = await isExcludedDeviceUsingHints();
95
+
55
96
  const faceLandmarker = await FaceLandmarker.createFromOptions(vision, {
56
97
  baseOptions: {
57
98
  modelAssetPath: `https://web-models.smileidentity.com/face_landmarker/face_landmarker.task`,
58
- delegate: hasFP16Support() ? 'GPU' : 'CPU',
99
+ delegate: isExcluded || !hasFP16Support() ? 'CPU' : 'GPU',
59
100
  },
60
101
  outputFaceBlendshapes: true,
61
102
  runningMode: 'VIDEO',
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smileid/signature-pad",
3
- "version": "11.0.1",
3
+ "version": "11.0.2",
4
4
  "private": "true",
5
5
  "exports": {
6
6
  ".": "./index.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smileid/web-components",
3
- "version": "11.0.1",
3
+ "version": "11.0.2",
4
4
  "main": "dist/esm/main.js",
5
5
  "module": "dist/esm/main.js",
6
6
  "types": "dist/types/main.d.ts",
@@ -104,7 +104,7 @@
104
104
  "prettier": "^3.6.2",
105
105
  "rollup-plugin-visualizer": "^6.0.3",
106
106
  "typescript": "^5.8.3",
107
- "vite": "^7.1.5",
107
+ "vite": "^7.1.12",
108
108
  "vite-plugin-dts": "^4.5.4",
109
109
  "vite-plugin-tsconfig-paths": "^1.4.1"
110
110
  }