@smileid/web-components 11.4.3 → 11.4.5

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 (34) hide show
  1. package/dist/esm/{DocumentCaptureScreens-D1oMAv4n.js → DocumentCaptureScreens-D2G0NOQr.js} +4 -4
  2. package/dist/esm/{DocumentCaptureScreens-D1oMAv4n.js.map → DocumentCaptureScreens-D2G0NOQr.js.map} +1 -1
  3. package/dist/esm/{EndUserConsent-D26UoVk5.js → EndUserConsent-uHfA3txP.js} +3 -3
  4. package/dist/esm/{EndUserConsent-D26UoVk5.js.map → EndUserConsent-uHfA3txP.js.map} +1 -1
  5. package/dist/esm/{Navigation-nvehze1F.js → Navigation-Bb7MPLE8.js} +2 -2
  6. package/dist/esm/{Navigation-nvehze1F.js.map → Navigation-Bb7MPLE8.js.map} +1 -1
  7. package/dist/esm/{SelfieCaptureScreens-CC-y0CpT.js → SelfieCaptureScreens-Dr7VzON7.js} +2322 -2187
  8. package/dist/esm/SelfieCaptureScreens-Dr7VzON7.js.map +1 -0
  9. package/dist/esm/{TotpConsent-owUOdKzP.js → TotpConsent-Depzg0ti.js} +2 -2
  10. package/dist/esm/{TotpConsent-owUOdKzP.js.map → TotpConsent-Depzg0ti.js.map} +1 -1
  11. package/dist/esm/combobox.js +1 -1
  12. package/dist/esm/document.js +1 -1
  13. package/dist/esm/end-user-consent.js +1 -1
  14. package/dist/esm/{index-5Nn2kzHI.js → index-C4RTMbgw.js} +74 -74
  15. package/dist/esm/{index-5Nn2kzHI.js.map → index-C4RTMbgw.js.map} +1 -1
  16. package/dist/esm/localisation.js +1 -1
  17. package/dist/esm/main.js +6 -6
  18. package/dist/esm/navigation.js +1 -1
  19. package/dist/esm/{package-BxstV9r_.js → package-D6YrpMcO.js} +3 -3
  20. package/dist/esm/{package-BxstV9r_.js.map → package-D6YrpMcO.js.map} +1 -1
  21. package/dist/esm/selfie.js +1 -1
  22. package/dist/esm/smart-camera-web.js +9 -12
  23. package/dist/esm/smart-camera-web.js.map +1 -1
  24. package/dist/esm/totp-consent.js +1 -1
  25. package/dist/smart-camera-web.js +51 -51
  26. package/dist/smart-camera-web.js.map +1 -1
  27. package/dist/types/main.d.ts +2 -0
  28. package/lib/components/selfie/src/SelfieCaptureScreens.js +83 -0
  29. package/lib/components/selfie/src/selfie-capture-wrapper/SelfieCaptureWrapper.tsx +275 -88
  30. package/lib/components/selfie/src/smartselfie-capture/utils/mediapipeManager.ts +145 -9
  31. package/lib/components/signature-pad/package.json +1 -1
  32. package/lib/components/smart-camera-web/src/SmartCameraWeb.js +6 -4
  33. package/package.json +2 -2
  34. package/dist/esm/SelfieCaptureScreens-CC-y0CpT.js.map +0 -1
@@ -122,6 +122,62 @@ export class UnsupportedMediapipeEnvironmentError extends Error {
122
122
  }
123
123
  }
124
124
 
125
+ /**
126
+ * @description Thrown when `FaceLandmarker.createFromOptions` does not settle
127
+ * within {@link MEDIAPIPE_INIT_TIMEOUT_MS}. The WASM and model assets download
128
+ * over the network, but the subsequent WASM compile + GPU/CPU graph
129
+ * initialization runs on-device and can stall indefinitely on some drivers.
130
+ * Without a timeout the cached `loading` promise never resolves nor rejects,
131
+ * which (a) keeps the loading UI spinning until the wrapper's hard deadline and
132
+ * (b) poisons the singleton so retries/remounts await the same stuck promise.
133
+ * Treated as a transient failure by callers so the bounded retry can re-run.
134
+ */
135
+ export class MediapipeInitTimeoutError extends Error {
136
+ constructor(message: string) {
137
+ super(message);
138
+ this.name = 'MediapipeInitTimeoutError';
139
+ }
140
+ }
141
+
142
+ // Last-resort hang guard for `createFromOptions`. This call also downloads the
143
+ // ~3MB model, so the budget must cover a slow/uncached fetch as well as the
144
+ // on-device init — hence it is deliberately generous. It exists only so a truly
145
+ // wedged init eventually rejects (letting the wrapper's bounded retry / hard
146
+ // deadline take over) rather than hanging forever; it is NOT used to decide the
147
+ // GPU→CPU fallback (that keys off real init errors — see getMediapipeInstance).
148
+ const MEDIAPIPE_INIT_TIMEOUT_MS = 45000;
149
+
150
+ /**
151
+ * @description Races a promise against a timeout. On timeout, rejects with a
152
+ * {@link MediapipeInitTimeoutError}. The underlying promise is not (and cannot
153
+ * be) aborted — we just stop awaiting it so callers can recover.
154
+ * @param {Promise<T>} promise The work to bound.
155
+ * @param {number} ms Timeout in milliseconds.
156
+ * @param {string} message Message for the timeout error.
157
+ * @returns {Promise<T>} Resolves/rejects with the promise, or rejects on timeout.
158
+ */
159
+ const withTimeout = <T>(
160
+ promise: Promise<T>,
161
+ ms: number,
162
+ message: string,
163
+ ): Promise<T> =>
164
+ new Promise<T>((resolve, reject) => {
165
+ const timeoutId = setTimeout(() => {
166
+ reject(new MediapipeInitTimeoutError(message));
167
+ }, ms);
168
+
169
+ promise.then(
170
+ (value) => {
171
+ clearTimeout(timeoutId);
172
+ resolve(value);
173
+ },
174
+ (error) => {
175
+ clearTimeout(timeoutId);
176
+ reject(error);
177
+ },
178
+ );
179
+ });
180
+
125
181
  /**
126
182
  * @description Reads system architecture hints from User-Agent Client Hints.
127
183
  * @returns {Promise<string | null>} Lower-cased hint string or null when hints are unavailable.
@@ -207,6 +263,28 @@ const hasFP16Support = () => {
207
263
  return !!(hasHalfFloatExt && hasColorBufferHalfFloat && hasHalfFloatLinear);
208
264
  };
209
265
 
266
+ /**
267
+ * @description Creates a FaceLandmarker with the given compute delegate.
268
+ * Extracted so the init can be retried with a different delegate without
269
+ * duplicating the options.
270
+ * @param {Awaited<ReturnType<typeof FilesetResolver.forVisionTasks>>} vision Resolved WASM fileset.
271
+ * @param {'CPU' | 'GPU'} delegate Compute delegate to use.
272
+ * @returns {Promise<FaceLandmarker>} The created FaceLandmarker.
273
+ */
274
+ const createLandmarker = (
275
+ vision: Awaited<ReturnType<typeof FilesetResolver.forVisionTasks>>,
276
+ delegate: 'CPU' | 'GPU',
277
+ ): Promise<FaceLandmarker> =>
278
+ FaceLandmarker.createFromOptions(vision, {
279
+ baseOptions: {
280
+ modelAssetPath: `https://web-models.smileidentity.com/face_landmarker/face_landmarker.task`,
281
+ delegate,
282
+ },
283
+ outputFaceBlendshapes: true,
284
+ runningMode: 'VIDEO',
285
+ numFaces: 2,
286
+ });
287
+
210
288
  export const getMediapipeInstance = async (): Promise<FaceLandmarker> => {
211
289
  if (!window.__smileIdentityMediapipe) {
212
290
  window.__smileIdentityMediapipe = {
@@ -249,15 +327,69 @@ export const getMediapipeInstance = async (): Promise<FaceLandmarker> => {
249
327
  const delegate =
250
328
  gpuDelegate === 'CPU' || !hasFP16Support() ? 'CPU' : 'GPU';
251
329
 
252
- const faceLandmarker = await FaceLandmarker.createFromOptions(vision, {
253
- baseOptions: {
254
- modelAssetPath: `https://web-models.smileidentity.com/face_landmarker/face_landmarker.task`,
255
- delegate,
256
- },
257
- outputFaceBlendshapes: true,
258
- runningMode: 'VIDEO',
259
- numFaces: 2,
260
- });
330
+ // A GPU-delegate init can throw on some drivers (WebGL context/shader
331
+ // failures) even though the model already downloaded; on a genuine GPU
332
+ // *error* we retry once on CPU before giving up. We deliberately do NOT
333
+ // fall back on a timeout: the timeout also covers the model download, so
334
+ // a slow fetch could trip it while a healthy GPU init is still in
335
+ // progress — abandoning it to start a redundant CPU init only makes
336
+ // things worse. A timeout therefore propagates as a transient failure for
337
+ // the wrapper's bounded retry / hard deadline to handle.
338
+ //
339
+ // Orphan handling: when `withTimeout` fires, the underlying
340
+ // `createLandmarker` promise is still in flight (it cannot be aborted).
341
+ // If it eventually resolves, the resulting `FaceLandmarker` would leak a
342
+ // GPU/WebGL context, so we attach a best-effort `.close()` cleanup to the
343
+ // original promise reference for both the GPU and CPU init paths.
344
+ const closeOrphan = (orphan: FaceLandmarker) => {
345
+ try {
346
+ orphan.close();
347
+ } catch {
348
+ /* best effort */
349
+ }
350
+ };
351
+
352
+ let faceLandmarker: FaceLandmarker;
353
+ const initPromise = createLandmarker(vision, delegate);
354
+ try {
355
+ faceLandmarker = await withTimeout(
356
+ initPromise,
357
+ MEDIAPIPE_INIT_TIMEOUT_MS,
358
+ `MediaPipe initialization timed out after ${MEDIAPIPE_INIT_TIMEOUT_MS}ms (delegate: ${delegate}).`,
359
+ );
360
+ } catch (error) {
361
+ const isTimeout = error instanceof MediapipeInitTimeoutError;
362
+ if (isTimeout) {
363
+ // Stop awaiting the in-flight init, but if it eventually resolves,
364
+ // close the orphaned instance to avoid leaking a GPU/WebGL context.
365
+ initPromise.then(closeOrphan, () => {
366
+ /* already failed; nothing to clean up */
367
+ });
368
+ }
369
+ if (delegate === 'GPU' && !isTimeout) {
370
+ console.warn(
371
+ '[SmileID] GPU MediaPipe init failed; retrying with CPU delegate.',
372
+ error,
373
+ );
374
+ const cpuInitPromise = createLandmarker(vision, 'CPU');
375
+ try {
376
+ faceLandmarker = await withTimeout(
377
+ cpuInitPromise,
378
+ MEDIAPIPE_INIT_TIMEOUT_MS,
379
+ `MediaPipe CPU initialization timed out after ${MEDIAPIPE_INIT_TIMEOUT_MS}ms.`,
380
+ );
381
+ } catch (cpuError) {
382
+ if (cpuError instanceof MediapipeInitTimeoutError) {
383
+ cpuInitPromise.then(closeOrphan, () => {
384
+ /* already failed; nothing to clean up */
385
+ });
386
+ }
387
+ throw cpuError;
388
+ }
389
+ } else {
390
+ throw error;
391
+ }
392
+ }
261
393
 
262
394
  mediapipeGlobal.instance = faceLandmarker;
263
395
  mediapipeGlobal.loaded = true;
@@ -265,6 +397,8 @@ export const getMediapipeInstance = async (): Promise<FaceLandmarker> => {
265
397
 
266
398
  return faceLandmarker;
267
399
  } catch (error) {
400
+ // Always clear the poisoned promise so the wrapper's bounded retry — and
401
+ // any later remount — can re-attempt instead of awaiting a dead promise.
268
402
  mediapipeGlobal.loading = null;
269
403
  throw error;
270
404
  }
@@ -277,4 +411,6 @@ export const __testUtils = {
277
411
  matchesExcludedGpu,
278
412
  getDelegateFromGpuDetection,
279
413
  supportsWasmReftypes,
414
+ withTimeout,
415
+ MEDIAPIPE_INIT_TIMEOUT_MS,
280
416
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smileid/signature-pad",
3
- "version": "11.4.3",
3
+ "version": "11.4.5",
4
4
  "private": true,
5
5
  "exports": {
6
6
  ".": "./index.js"
@@ -166,11 +166,13 @@ class SmartCameraWeb extends HTMLElement {
166
166
  this.documentCapture.addEventListener(
167
167
  'document-capture-screens.cancelled',
168
168
  () => {
169
- this.SelfieCaptureScreens.setAttribute(
170
- 'initial-screen',
171
- 'selfie-capture',
172
- );
173
169
  this.setActiveScreen(this.SelfieCaptureScreens);
170
+ // Land on a clean selfie capture screen by driving the navigation
171
+ // explicitly. Previously this set `initial-screen="selfie-capture"`,
172
+ // whose side effect was a full SelfieCaptureScreens rebuild — re-fired
173
+ // on every back-navigation (setAttribute invokes attributeChangedCallback
174
+ // even when the value is unchanged).
175
+ this.SelfieCaptureScreens.restartSelfieCapture();
174
176
  this.SelfieCaptureScreens.removeAttribute('data-camera-error');
175
177
  this.SelfieCaptureScreens.setAttribute('data-camera-ready', true);
176
178
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smileid/web-components",
3
- "version": "11.4.3",
3
+ "version": "11.4.5",
4
4
  "private": false,
5
5
  "main": "dist/esm/main.js",
6
6
  "module": "dist/esm/main.js",
@@ -105,7 +105,7 @@
105
105
  "@typescript-eslint/eslint-plugin": "^8.49.0",
106
106
  "@typescript-eslint/parser": "^8.49.0",
107
107
  "cross-env": "^7.0.3",
108
- "cypress": "^13.15.0",
108
+ "cypress": "^13.17.0",
109
109
  "eslint": "^8.57.0",
110
110
  "eslint-config-airbnb-base": "^15.0.0",
111
111
  "eslint-config-prettier": "^9.1.0",