@smileid/usesmileid 12.0.0 → 12.0.1

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 (255) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/core/exception/agent_mode_with_enhanced_liveness_exception.d.ts +11 -0
  3. package/dist/core/exception/agent_mode_with_enhanced_liveness_exception.d.ts.map +1 -0
  4. package/dist/core/exception/agent_mode_with_enhanced_liveness_exception.js +17 -0
  5. package/dist/core/exception/agent_mode_with_enhanced_liveness_exception.js.map +1 -0
  6. package/dist/core/exception/capture_validation_exceptions.d.ts +1 -0
  7. package/dist/core/exception/capture_validation_exceptions.d.ts.map +1 -1
  8. package/dist/core/exception/capture_validation_exceptions.js +1 -0
  9. package/dist/core/exception/capture_validation_exceptions.js.map +1 -1
  10. package/dist/core/exception/index.d.ts +1 -0
  11. package/dist/core/exception/index.d.ts.map +1 -1
  12. package/dist/core/exception/index.js +1 -0
  13. package/dist/core/exception/index.js.map +1 -1
  14. package/dist/core/exception/use_smile_id_error_code.d.ts +1 -0
  15. package/dist/core/exception/use_smile_id_error_code.d.ts.map +1 -1
  16. package/dist/core/exception/use_smile_id_error_code.js +1 -0
  17. package/dist/core/exception/use_smile_id_error_code.js.map +1 -1
  18. package/dist/core/models/config.d.ts +19 -0
  19. package/dist/core/models/config.d.ts.map +1 -1
  20. package/dist/core/models/document_type.d.ts +1 -1
  21. package/dist/core/models/document_type.d.ts.map +1 -1
  22. package/dist/core/models/document_type.js +2 -2
  23. package/dist/core/models/document_type.js.map +1 -1
  24. package/dist/core/models/global_privacy_control.d.ts +36 -0
  25. package/dist/core/models/global_privacy_control.d.ts.map +1 -0
  26. package/dist/core/models/global_privacy_control.js +39 -0
  27. package/dist/core/models/global_privacy_control.js.map +1 -0
  28. package/dist/core/models/network_config.d.ts +2 -2
  29. package/dist/core/models/network_config.js +2 -2
  30. package/dist/data/api/interceptors/smile_auth_interceptor.d.ts.map +1 -1
  31. package/dist/data/api/interceptors/smile_auth_interceptor.js +3 -17
  32. package/dist/data/api/interceptors/smile_auth_interceptor.js.map +1 -1
  33. package/dist/data/api/smile_id_api.d.ts +1 -1
  34. package/dist/data/api/smile_id_api.d.ts.map +1 -1
  35. package/dist/data/dsl/builder/retry_config_builder.d.ts +4 -4
  36. package/dist/data/dsl/builder/retry_config_builder.d.ts.map +1 -1
  37. package/dist/data/dsl/builder/retry_config_builder.js +6 -6
  38. package/dist/data/dsl/builder/retry_config_builder.js.map +1 -1
  39. package/dist/data/smile_id_urls.d.ts +1 -1
  40. package/dist/data/smile_id_urls.js +1 -1
  41. package/dist/data/smile_id_urls.js.map +1 -1
  42. package/dist/data/util/retry_util.d.ts +1 -1
  43. package/dist/data/util/retry_util.d.ts.map +1 -1
  44. package/dist/data/util/retry_util.js +5 -0
  45. package/dist/data/util/retry_util.js.map +1 -1
  46. package/dist/index.d.ts +2 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +2 -2
  49. package/dist/index.js.map +1 -1
  50. package/dist/metadata/builder_configuration.d.ts +5 -0
  51. package/dist/metadata/builder_configuration.d.ts.map +1 -1
  52. package/dist/metadata/builder_configuration.js +1 -0
  53. package/dist/metadata/builder_configuration.js.map +1 -1
  54. package/dist/metadata/metadata_configuration_mappers.d.ts +2 -3
  55. package/dist/metadata/metadata_configuration_mappers.d.ts.map +1 -1
  56. package/dist/metadata/metadata_configuration_mappers.js +13 -6
  57. package/dist/metadata/metadata_configuration_mappers.js.map +1 -1
  58. package/dist/metadata/use_smile_id_metadata_factory.d.ts.map +1 -1
  59. package/dist/metadata/use_smile_id_metadata_factory.js +14 -5
  60. package/dist/metadata/use_smile_id_metadata_factory.js.map +1 -1
  61. package/dist/presentation/components/use_smile_id_enhanced_smart_selfie_clip.d.ts +5 -2
  62. package/dist/presentation/components/use_smile_id_enhanced_smart_selfie_clip.d.ts.map +1 -1
  63. package/dist/presentation/components/use_smile_id_enhanced_smart_selfie_clip.js +8 -3
  64. package/dist/presentation/components/use_smile_id_enhanced_smart_selfie_clip.js.map +1 -1
  65. package/dist/presentation/components/use_smile_id_instructions_hero.d.ts.map +1 -1
  66. package/dist/presentation/components/use_smile_id_instructions_hero.js +8 -3
  67. package/dist/presentation/components/use_smile_id_instructions_hero.js.map +1 -1
  68. package/dist/presentation/flow/dsl/builder/config_builder.d.ts +16 -0
  69. package/dist/presentation/flow/dsl/builder/config_builder.d.ts.map +1 -1
  70. package/dist/presentation/flow/dsl/builder/config_builder.js +18 -0
  71. package/dist/presentation/flow/dsl/builder/config_builder.js.map +1 -1
  72. package/dist/presentation/flow/dsl/builder/screens_builder.d.ts +3 -6
  73. package/dist/presentation/flow/dsl/builder/screens_builder.d.ts.map +1 -1
  74. package/dist/presentation/flow/dsl/builder/screens_builder.js +3 -6
  75. package/dist/presentation/flow/dsl/builder/screens_builder.js.map +1 -1
  76. package/dist/presentation/flow/flow_navigation_manager.d.ts +5 -1
  77. package/dist/presentation/flow/flow_navigation_manager.d.ts.map +1 -1
  78. package/dist/presentation/flow/flow_navigation_manager.js +7 -1
  79. package/dist/presentation/flow/flow_navigation_manager.js.map +1 -1
  80. package/dist/presentation/flow/model/instructions_defaults.d.ts.map +1 -1
  81. package/dist/presentation/flow/model/instructions_defaults.js +1 -1
  82. package/dist/presentation/flow/model/instructions_defaults.js.map +1 -1
  83. package/dist/presentation/flow/screen/orchestrated_capture_screen.d.ts.map +1 -1
  84. package/dist/presentation/flow/screen/orchestrated_capture_screen.js +13 -3
  85. package/dist/presentation/flow/screen/orchestrated_capture_screen.js.map +1 -1
  86. package/dist/presentation/flow/screen/orchestrated_consent_screen.d.ts.map +1 -1
  87. package/dist/presentation/flow/screen/orchestrated_consent_screen.js +3 -1
  88. package/dist/presentation/flow/screen/orchestrated_consent_screen.js.map +1 -1
  89. package/dist/presentation/flow/screen/orchestrated_preview_screen.d.ts.map +1 -1
  90. package/dist/presentation/flow/screen/orchestrated_preview_screen.js +0 -8
  91. package/dist/presentation/flow/screen/orchestrated_preview_screen.js.map +1 -1
  92. package/dist/presentation/flow/use_smile_id_flow.d.ts.map +1 -1
  93. package/dist/presentation/flow/use_smile_id_flow.js +10 -4
  94. package/dist/presentation/flow/use_smile_id_flow.js.map +1 -1
  95. package/dist/presentation/flow/validation/flow_validator.d.ts.map +1 -1
  96. package/dist/presentation/flow/validation/flow_validator.js +42 -20
  97. package/dist/presentation/flow/validation/flow_validator.js.map +1 -1
  98. package/dist/presentation/screen/helpers/capture_ui_state.d.ts +6 -6
  99. package/dist/presentation/screen/helpers/capture_ui_state.d.ts.map +1 -1
  100. package/dist/presentation/screen/helpers/capture_ui_state.js +19 -19
  101. package/dist/presentation/screen/helpers/capture_ui_state.js.map +1 -1
  102. package/dist/presentation/screen/helpers/ess_turn_haptics.d.ts +2 -2
  103. package/dist/presentation/screen/helpers/ess_turn_haptics.d.ts.map +1 -1
  104. package/dist/presentation/screen/helpers/ess_turn_haptics.js +6 -6
  105. package/dist/presentation/screen/helpers/ess_turn_haptics.js.map +1 -1
  106. package/dist/presentation/screen/helpers/scan_state_to_overlay_state.d.ts +5 -5
  107. package/dist/presentation/screen/helpers/scan_state_to_overlay_state.d.ts.map +1 -1
  108. package/dist/presentation/screen/helpers/scan_state_to_overlay_state.js +11 -11
  109. package/dist/presentation/screen/helpers/scan_state_to_overlay_state.js.map +1 -1
  110. package/dist/presentation/screen/helpers/use_capture_orientation_lock.d.ts +13 -3
  111. package/dist/presentation/screen/helpers/use_capture_orientation_lock.d.ts.map +1 -1
  112. package/dist/presentation/screen/helpers/use_capture_orientation_lock.js +48 -6
  113. package/dist/presentation/screen/helpers/use_capture_orientation_lock.js.map +1 -1
  114. package/dist/presentation/screen/use_smile_id_capture_screen.d.ts +4 -2
  115. package/dist/presentation/screen/use_smile_id_capture_screen.d.ts.map +1 -1
  116. package/dist/presentation/screen/use_smile_id_capture_screen.js +36 -15
  117. package/dist/presentation/screen/use_smile_id_capture_screen.js.map +1 -1
  118. package/dist/presentation/screen/use_smile_id_consent_screen.d.ts +7 -0
  119. package/dist/presentation/screen/use_smile_id_consent_screen.d.ts.map +1 -1
  120. package/dist/presentation/screen/use_smile_id_consent_screen.js +6 -5
  121. package/dist/presentation/screen/use_smile_id_consent_screen.js.map +1 -1
  122. package/dist/presentation/screen/use_smile_id_document_back_instructions.d.ts.map +1 -1
  123. package/dist/presentation/screen/use_smile_id_document_back_instructions.js +5 -13
  124. package/dist/presentation/screen/use_smile_id_document_back_instructions.js.map +1 -1
  125. package/dist/presentation/screen/use_smile_id_document_preview_screen.js +1 -1
  126. package/dist/presentation/screen/use_smile_id_document_preview_screen.js.map +1 -1
  127. package/dist/presentation/screen/use_smile_id_preview_screen.d.ts +1 -21
  128. package/dist/presentation/screen/use_smile_id_preview_screen.d.ts.map +1 -1
  129. package/dist/presentation/screen/use_smile_id_preview_screen.js +3 -14
  130. package/dist/presentation/screen/use_smile_id_preview_screen.js.map +1 -1
  131. package/dist/presentation/screen/use_smile_id_processing_screen.js +1 -1
  132. package/dist/presentation/screen/use_smile_id_processing_screen.js.map +1 -1
  133. package/dist/presentation/utils/locale_utils.d.ts +10 -4
  134. package/dist/presentation/utils/locale_utils.d.ts.map +1 -1
  135. package/dist/presentation/utils/locale_utils.js +13 -5
  136. package/dist/presentation/utils/locale_utils.js.map +1 -1
  137. package/dist/testing/fake_face_analyzer.d.ts +43 -0
  138. package/dist/testing/fake_face_analyzer.d.ts.map +1 -0
  139. package/dist/testing/fake_face_analyzer.js +52 -0
  140. package/dist/testing/fake_face_analyzer.js.map +1 -0
  141. package/dist/testing/index.d.ts +5 -0
  142. package/dist/testing/index.d.ts.map +1 -0
  143. package/dist/testing/index.js +7 -0
  144. package/dist/testing/index.js.map +1 -0
  145. package/dist/testing/scenario_expectations.d.ts +28 -0
  146. package/dist/testing/scenario_expectations.d.ts.map +1 -0
  147. package/dist/testing/scenario_expectations.js +65 -0
  148. package/dist/testing/scenario_expectations.js.map +1 -0
  149. package/dist/testing/scenario_loader.d.ts +11 -0
  150. package/dist/testing/scenario_loader.d.ts.map +1 -0
  151. package/dist/testing/scenario_loader.js +64 -0
  152. package/dist/testing/scenario_loader.js.map +1 -0
  153. package/dist/testing/scripted_scan_results.d.ts +17 -0
  154. package/dist/testing/scripted_scan_results.d.ts.map +1 -0
  155. package/dist/testing/scripted_scan_results.js +52 -0
  156. package/dist/testing/scripted_scan_results.js.map +1 -0
  157. package/package.json +11 -4
  158. package/src/__tests__/dsl.test.ts +51 -1
  159. package/src/__tests__/setup_react_native.ts +17 -2
  160. package/src/__tests__/snapshots/__snapshots__/screens_structural.test.tsx.snap +5261 -0
  161. package/src/__tests__/snapshots/screens_structural.test.tsx +230 -0
  162. package/src/camera/__tests__/use_smile_id_camera_system.test.ts +103 -0
  163. package/src/core/exception/__tests__/builder_validation_exceptions.test.ts +67 -0
  164. package/src/core/exception/agent_mode_with_enhanced_liveness_exception.ts +20 -0
  165. package/src/core/exception/capture_validation_exceptions.ts +1 -0
  166. package/src/core/exception/index.ts +1 -0
  167. package/src/core/exception/use_smile_id_error_code.ts +1 -0
  168. package/src/core/models/__tests__/document_type.test.ts +2 -2
  169. package/src/core/models/__tests__/flow_models.test.ts +26 -0
  170. package/src/core/models/config.ts +22 -0
  171. package/src/core/models/document_type.ts +2 -2
  172. package/src/core/models/global_privacy_control.ts +45 -0
  173. package/src/core/models/network_config.ts +2 -2
  174. package/src/data/api/__tests__/smile_id_api.test.ts +1 -1
  175. package/src/data/api/interceptors/__tests__/gzip_interceptor.test.ts +83 -0
  176. package/src/data/api/interceptors/smile_auth_interceptor.ts +3 -18
  177. package/src/data/compression/__tests__/payload_compressor_real_jpeg.test.ts +6 -2
  178. package/src/data/dsl/builder/__tests__/config_builders.test.ts +62 -0
  179. package/src/data/dsl/builder/retry_config_builder.ts +6 -6
  180. package/src/data/factory/__tests__/retry_gzip_mac_composition.test.ts +163 -0
  181. package/src/data/factory/__tests__/signed_job_submission_headers.test.ts +1 -1
  182. package/src/data/repository/__tests__/network_retries.test.ts +1 -1
  183. package/src/data/repository/__tests__/use_smile_id_repository.test.ts +1 -1
  184. package/src/data/security/__tests__/request_canonicalizer.test.ts +132 -0
  185. package/src/data/smile_id_urls.ts +1 -1
  186. package/src/data/util/__tests__/retry_util.test.ts +25 -0
  187. package/src/data/util/retry_util.ts +9 -1
  188. package/src/index.ts +6 -4
  189. package/src/metadata/__tests__/metadata_configuration_mappers.test.ts +39 -0
  190. package/src/metadata/__tests__/metadata_configuration_objects.test.ts +2 -1
  191. package/src/metadata/__tests__/metadata_device_context.test.ts +3 -3
  192. package/src/metadata/__tests__/metadata_device_os_android.test.ts +18 -0
  193. package/src/metadata/__tests__/metadata_golden_fixture.json +1 -1
  194. package/src/metadata/__tests__/metadata_payload_golden.test.ts +4 -4
  195. package/src/metadata/__tests__/use_smile_id_metadata.test.ts +1 -1
  196. package/src/metadata/builder_configuration.ts +6 -0
  197. package/src/metadata/metadata_configuration_mappers.ts +21 -7
  198. package/src/metadata/use_smile_id_metadata_factory.ts +17 -6
  199. package/src/presentation/assets/si_document_green_book_hero_animation.json +1 -4668
  200. package/src/presentation/assets/si_document_hero_animation.json +1 -4799
  201. package/src/presentation/assets/si_document_hero_back_animation.json +1 -2039
  202. package/src/presentation/assets/si_document_passport_hero_animation.json +1 -14491
  203. package/src/presentation/assets/si_ess_all_directions_hero_animation.json +1 -0
  204. package/src/presentation/assets/si_ess_animation_all_directions.json +1 -6589
  205. package/src/presentation/assets/si_selfie_hero_animation.json +1 -1439
  206. package/src/presentation/assets/si_smile_to_capture_animation.json +1 -1439
  207. package/src/presentation/components/__tests__/use_smile_id_enhanced_smart_selfie_clip.test.tsx +9 -4
  208. package/src/presentation/components/__tests__/use_smile_id_instructions_hero.test.tsx +44 -0
  209. package/src/presentation/components/use_smile_id_enhanced_smart_selfie_clip.tsx +9 -3
  210. package/src/presentation/components/use_smile_id_instructions_hero.tsx +10 -0
  211. package/src/presentation/flow/__tests__/document_flow_navigation.test.ts +3 -3
  212. package/src/presentation/flow/__tests__/flow_navigation_manager.test.ts +49 -0
  213. package/src/presentation/flow/dsl/builder/__tests__/config_builder.test.ts +25 -0
  214. package/src/presentation/flow/dsl/builder/config_builder.ts +21 -0
  215. package/src/presentation/flow/dsl/builder/screens_builder.ts +3 -6
  216. package/src/presentation/flow/flow_navigation_manager.ts +6 -1
  217. package/src/presentation/flow/model/instructions_defaults.ts +1 -1
  218. package/src/presentation/flow/screen/orchestrated_capture_screen.tsx +18 -3
  219. package/src/presentation/flow/screen/orchestrated_consent_screen.tsx +5 -0
  220. package/src/presentation/flow/screen/orchestrated_preview_screen.tsx +0 -12
  221. package/src/presentation/flow/use_smile_id_flow.tsx +11 -4
  222. package/src/presentation/flow/validation/__tests__/flow_validator.test.ts +61 -1
  223. package/src/presentation/flow/validation/flow_validator.ts +55 -26
  224. package/src/presentation/screen/__tests__/capture_ui_state.test.ts +66 -47
  225. package/src/presentation/screen/__tests__/ess_turn_haptics.test.ts +15 -11
  226. package/src/presentation/screen/__tests__/scan_state_to_overlay_state.test.ts +47 -27
  227. package/src/presentation/screen/__tests__/use_smile_id_capture_screen.test.tsx +176 -14
  228. package/src/presentation/screen/__tests__/use_smile_id_document_back_instructions.test.tsx +1 -1
  229. package/src/presentation/screen/__tests__/use_smile_id_preview_screen.test.tsx +10 -40
  230. package/src/presentation/screen/helpers/__tests__/use_capture_orientation_lock.test.tsx +212 -0
  231. package/src/presentation/screen/helpers/capture_ui_state.ts +27 -27
  232. package/src/presentation/screen/helpers/ess_turn_haptics.ts +9 -9
  233. package/src/presentation/screen/helpers/scan_state_to_overlay_state.ts +13 -13
  234. package/src/presentation/screen/helpers/use_capture_orientation_lock.ts +69 -7
  235. package/src/presentation/screen/use_smile_id_capture_screen.tsx +47 -19
  236. package/src/presentation/screen/use_smile_id_consent_screen.tsx +16 -5
  237. package/src/presentation/screen/use_smile_id_document_back_instructions.tsx +14 -23
  238. package/src/presentation/screen/use_smile_id_document_preview_screen.tsx +1 -1
  239. package/src/presentation/screen/use_smile_id_preview_screen.tsx +13 -45
  240. package/src/presentation/screen/use_smile_id_processing_screen.tsx +1 -1
  241. package/src/presentation/utils/__tests__/locale_utils.test.ts +46 -5
  242. package/src/presentation/utils/locale_utils.ts +13 -4
  243. package/src/testing/__tests__/fake_face_analyzer.test.ts +42 -0
  244. package/src/testing/__tests__/scenario_expectations.test.ts +110 -0
  245. package/src/testing/__tests__/scenario_loader.test.ts +64 -0
  246. package/src/testing/fake_face_analyzer.ts +82 -0
  247. package/src/testing/index.ts +15 -0
  248. package/src/testing/scenario_expectations.ts +87 -0
  249. package/src/testing/scenario_loader.ts +83 -0
  250. package/src/testing/scripted_scan_results.ts +66 -0
  251. package/dist/presentation/components/use_smile_id_face_silhouette_overlay.d.ts +0 -15
  252. package/dist/presentation/components/use_smile_id_face_silhouette_overlay.d.ts.map +0 -1
  253. package/dist/presentation/components/use_smile_id_face_silhouette_overlay.js +0 -19
  254. package/dist/presentation/components/use_smile_id_face_silhouette_overlay.js.map +0 -1
  255. package/src/presentation/components/use_smile_id_face_silhouette_overlay.tsx +0 -27
@@ -11,19 +11,37 @@ import * as Brightness from "expo-brightness";
11
11
  import { StatusBar } from "react-native";
12
12
  import { UseSmileIDCaptureScreen } from "../use_smile_id_capture_screen";
13
13
  import { CaptureType } from "../../../core/models/ml_config";
14
+ import { DocumentCaptureMode } from "../../../core/models/document_capture_mode";
15
+ import { mockedTakePhoto } from "../../../__tests__/setup_react_native";
14
16
  import { enStrings } from "../../../l10n/strings_en";
15
17
  import {
16
- ScanStateType,
18
+ UseSmileIDScanStateType,
17
19
  CaptureReadyEventKind,
18
20
  } from "@smileid/usesmileid_bridge";
19
21
  import type {
20
- ScanState,
22
+ UseSmileIDScanState,
21
23
  CaptureReadyEvent,
22
24
  FaceScanResult,
23
25
  } from "@smileid/usesmileid_bridge";
26
+ import { scriptedFaceScanResults } from "../../../testing/scripted_scan_results";
27
+ import { parseScriptedScenario } from "../../../testing/scenario_loader";
24
28
  import { isSessionBoundary } from "../helpers/capture_ui_state";
25
29
  import { UseSmileIDFeedbackDebouncer } from "../helpers/feedback_debouncer";
26
30
  import { UseSmileIDMotion } from "../../theme/use_smile_id_motion";
31
+ import { readFileSync } from "node:fs";
32
+ import { dirname, join } from "node:path";
33
+
34
+ // The shared scenario set lives at the repo root (e2e/scenarios/), outside this
35
+ // package's tsc rootDir (./src), so the test reads selfie-happy-path.json at run
36
+ // time. These minimal ambient decls keep the read typed without pulling
37
+ // @types/node in (which would collide with react-native's global timer types).
38
+ declare module "node:fs" {
39
+ export function readFileSync(path: string, encoding: "utf8"): string;
40
+ }
41
+ declare module "node:path" {
42
+ export function dirname(path: string): string;
43
+ export function join(...parts: string[]): string;
44
+ }
27
45
 
28
46
  // Capture the screen's per-frame `onResult` so tests can drive synthetic
29
47
  // sequences — the vision-camera worklet can't run in vitest, and `onResult`
@@ -195,6 +213,18 @@ describe("UseSmileIDCaptureScreen", () => {
195
213
  expect(mockedUseCameraDevice).toHaveBeenCalledWith("back");
196
214
  });
197
215
 
216
+ it("uses the back lens for an agent-mode selfie capture", () => {
217
+ const { root } = render(
218
+ <UseSmileIDCaptureScreen
219
+ analyzer={fakeAnalyzer}
220
+ captureType={CaptureType.selfie}
221
+ allowAgentMode
222
+ />,
223
+ );
224
+ expect(findByTestID(root, "si_capture_camera_preview")).not.toBeNull();
225
+ expect(mockedUseCameraDevice).toHaveBeenCalledWith("back");
226
+ });
227
+
198
228
  it("shows a placeholder (not the camera) when permission is undetermined", () => {
199
229
  const requestPermission = vi.fn(async () => true);
200
230
  mockedUseCameraPermission.mockReturnValue({
@@ -278,9 +308,9 @@ describe("UseSmileIDCaptureScreen", () => {
278
308
  // NOTE: the legacy `overlayState` / `captureProgress` prop-driven tests
279
309
  // were removed in Phase 3 of the per-frame migration. The capture
280
310
  // screen now owns its overlay state internally (derived from the
281
- // analyzer's per-frame ScanState), so there's no external prop to
311
+ // analyzer's per-frame UseSmileIDScanState), so there's no external prop to
282
312
  // pin against. Phase 4 follow-up re-authors these as analyzer-driven
283
- // tests that feed synthetic ScanState transitions through a mock
313
+ // tests that feed synthetic UseSmileIDScanState transitions through a mock
284
314
  // analyzer + frame stream.
285
315
 
286
316
  it("forwards the back tap to onBack when the back button is supplied", () => {
@@ -331,7 +361,7 @@ describe("UseSmileIDCaptureScreen", () => {
331
361
  });
332
362
 
333
363
  // Removed in Phase 3 — feedback text is now driven by the analyzer's
334
- // ScanState rather than a partner-supplied overlayState prop. Phase 4
364
+ // UseSmileIDScanState rather than a partner-supplied overlayState prop. Phase 4
335
365
  // re-authors this against a mock analyzer that yields a synthetic
336
366
  // "found + feedback=si_capture_low_light" result through the
337
367
  // frame stream.
@@ -507,22 +537,22 @@ describe("UseSmileIDCaptureScreen", () => {
507
537
  // Stale events → unsatisfied reset (drops them) → fresh events → finished.
508
538
  emit({
509
539
  output,
510
- state: { type: ScanStateType.Found },
540
+ state: { type: UseSmileIDScanStateType.Found },
511
541
  capturedEvents: [staleSelfie, staleLiveness],
512
542
  });
513
543
  emit({
514
544
  output,
515
- state: { type: ScanStateType.Unsatisfied },
545
+ state: { type: UseSmileIDScanStateType.Unsatisfied },
516
546
  capturedEvents: [],
517
547
  });
518
548
  emit({
519
549
  output,
520
- state: { type: ScanStateType.Found },
550
+ state: { type: UseSmileIDScanStateType.Found },
521
551
  capturedEvents: [freshSelfie, freshLiveness],
522
552
  });
523
553
  emit({
524
554
  output,
525
- state: { type: ScanStateType.Finished },
555
+ state: { type: UseSmileIDScanStateType.Finished },
526
556
  capturedEvents: [],
527
557
  });
528
558
 
@@ -541,6 +571,58 @@ describe("UseSmileIDCaptureScreen", () => {
541
571
  expect.objectContaining({ retries: 1 }),
542
572
  );
543
573
  });
574
+
575
+ it("replays the shared selfie scenario to a full capture completion", async () => {
576
+ // Drives the capture gap headlessly through the frame stream's onResult
577
+ // seam: the shared scenario replayed to `finished` must reach
578
+ // onCaptureComplete with the scripted selfie + liveness events.
579
+ const onCaptureComplete = vi.fn();
580
+ const analyzer = makeFakeAnalyzer();
581
+ capturedOnResult = null;
582
+ render(
583
+ <UseSmileIDCaptureScreen
584
+ analyzer={analyzer}
585
+ captureType={CaptureType.selfie}
586
+ onCaptureComplete={onCaptureComplete}
587
+ />,
588
+ );
589
+ expect(capturedOnResult).not.toBeNull();
590
+
591
+ // Consume the real shared scenario (not a hand-written copy) so a drift in
592
+ // the canonical script is caught here rather than silently diverging.
593
+ const scenarioDir = dirname(import.meta.url.replace(/^file:\/\//, ""));
594
+ const scenario = parseScriptedScenario(
595
+ JSON.parse(
596
+ readFileSync(
597
+ join(
598
+ scenarioDir,
599
+ "../../../../../e2e/scenarios/selfie-happy-path.json",
600
+ ),
601
+ "utf8",
602
+ ),
603
+ ),
604
+ );
605
+ for (const result of scriptedFaceScanResults(scenario, {
606
+ livenessCount: 2,
607
+ })) {
608
+ act(() => {
609
+ capturedOnResult!(result);
610
+ });
611
+ }
612
+ await act(async () => {
613
+ await Promise.resolve();
614
+ await Promise.resolve();
615
+ });
616
+
617
+ expect(onCaptureComplete).toHaveBeenCalledTimes(1);
618
+ const events = onCaptureComplete.mock.calls[0][0] as CaptureReadyEvent[];
619
+ expect(
620
+ events.filter((e) => e.kind === CaptureReadyEventKind.Selfie),
621
+ ).toHaveLength(1);
622
+ expect(
623
+ events.filter((e) => e.kind === CaptureReadyEventKind.Liveness),
624
+ ).toHaveLength(2);
625
+ });
544
626
  });
545
627
 
546
628
  /**
@@ -566,9 +648,9 @@ describe("session-boundary epoch reset — debouncer regression", () => {
566
648
  isImmediate: () => false,
567
649
  });
568
650
 
569
- let previousScanState: ScanState | null = null;
651
+ let previousScanState: UseSmileIDScanState | null = null;
570
652
  const processTransition = (
571
- nextScanState: ScanState,
653
+ nextScanState: UseSmileIDScanState,
572
654
  nextFeedbackKey: string,
573
655
  ) => {
574
656
  const boundary =
@@ -585,7 +667,7 @@ describe("session-boundary epoch reset — debouncer regression", () => {
585
667
 
586
668
  // found — smile key settles after the debounce window.
587
669
  processTransition(
588
- { type: ScanStateType.Found, feedback: "si_capture_smile" },
670
+ { type: UseSmileIDScanStateType.Found, feedback: "si_capture_smile" },
589
671
  "si_capture_smile",
590
672
  );
591
673
  vi.advanceTimersByTime(UseSmileIDMotion.feedbackDebounce);
@@ -593,7 +675,7 @@ describe("session-boundary epoch reset — debouncer regression", () => {
593
675
 
594
676
  // Entering unsatisfied is not a boundary — no reset.
595
677
  processTransition(
596
- { type: ScanStateType.Unsatisfied },
678
+ { type: UseSmileIDScanStateType.Unsatisfied },
597
679
  "si_selfie_capture_unsatisfied",
598
680
  );
599
681
  vi.advanceTimersByTime(UseSmileIDMotion.feedbackDebounce);
@@ -605,7 +687,7 @@ describe("session-boundary epoch reset — debouncer regression", () => {
605
687
  // Leaving unsatisfied is a boundary — without the reset the repeated
606
688
  // smile key would be suppressed as a duplicate.
607
689
  processTransition(
608
- { type: ScanStateType.Found, feedback: "si_capture_smile" },
690
+ { type: UseSmileIDScanStateType.Found, feedback: "si_capture_smile" },
609
691
  "si_capture_smile",
610
692
  );
611
693
  // seed() emits synchronously; no timer advance needed.
@@ -615,4 +697,84 @@ describe("session-boundary epoch reset — debouncer regression", () => {
615
697
  "si_capture_smile",
616
698
  ]);
617
699
  });
700
+
701
+ // Document capture has no Start button, so a scan timeout has to leave the
702
+ // user a way forward — the sibling SDKs surface the manual shutter as the
703
+ // recovery affordance (their shared scan state sets `showCaptureButton` on
704
+ // the timeout state). Without these, a timed-out flow is a dead end: only
705
+ // Back and ✕ remain.
706
+ describe("document capture timeout recovery", () => {
707
+ function renderDocumentScreen(props: {
708
+ readonly captureMode: ReturnType<typeof DocumentCaptureMode.AutoCapture>;
709
+ readonly allowGalleryUpload?: boolean;
710
+ }) {
711
+ const tree = render(
712
+ <UseSmileIDCaptureScreen
713
+ analyzer={fakeAnalyzer}
714
+ captureType={CaptureType.document}
715
+ captureMode={props.captureMode}
716
+ allowGalleryUpload={props.allowGalleryUpload ?? true}
717
+ />,
718
+ );
719
+ // The overlay and its controls only mount once the canvas is measured;
720
+ // `onLayout` never fires under the test renderer, so drive it (landscape,
721
+ // as a document capture screen is).
722
+ act(() => {
723
+ findByTestID(tree.root, "si_capture_screen")!.props.onLayout({
724
+ nativeEvent: { layout: { width: 800, height: 400 } },
725
+ });
726
+ });
727
+ return tree;
728
+ }
729
+
730
+ function emitTimeout() {
731
+ expect(capturedOnResult).not.toBeNull();
732
+ act(() => {
733
+ capturedOnResult!({
734
+ output: { faces: [], brightness: 0 },
735
+ state: { type: UseSmileIDScanStateType.Timeout },
736
+ capturedEvents: [],
737
+ });
738
+ });
739
+ }
740
+
741
+ it("offers the manual shutter after a timeout, in every capture mode", () => {
742
+ const { root } = renderDocumentScreen({
743
+ captureMode: DocumentCaptureMode.AutoCapture,
744
+ });
745
+
746
+ emitTimeout();
747
+
748
+ expect(findByTestID(root, "si_document_manual_shutter")).not.toBeNull();
749
+ });
750
+
751
+ it("keeps the gallery button available after a timeout", () => {
752
+ const { root } = renderDocumentScreen({
753
+ captureMode: DocumentCaptureMode.AutoCaptureWithManualFallback({
754
+ activateManualAfterMs: 3000,
755
+ }),
756
+ });
757
+
758
+ emitTimeout();
759
+
760
+ expect(findByTestID(root, "si_document_gallery_upload")).not.toBeNull();
761
+ });
762
+
763
+ it("leaves the manual shutter usable after a timeout", () => {
764
+ const { root } = renderDocumentScreen({
765
+ captureMode: DocumentCaptureMode.ManualCapture,
766
+ });
767
+
768
+ emitTimeout();
769
+
770
+ const shutter = findByTestID(root, "si_document_manual_shutter");
771
+ expect(shutter).not.toBeNull();
772
+ // The timeout must not latch the one-shot delivery gate, or the button
773
+ // renders but does nothing.
774
+ act(() => {
775
+ shutter!.props.onPress();
776
+ });
777
+ expect(mockedTakePhoto).toHaveBeenCalledTimes(1);
778
+ });
779
+ });
618
780
  });
@@ -104,7 +104,7 @@ describe("UseSmileIDDocumentBackInstructions", () => {
104
104
  const skip = findByTestID(root, "si_document_skip_back_button");
105
105
  expect(skip).not.toBeNull();
106
106
  act(() => {
107
- (skip!.props.onPress as () => void)();
107
+ (skip!.props.onClick as () => void)();
108
108
  });
109
109
  expect(onSkip).toHaveBeenCalledTimes(1);
110
110
  });
@@ -5,7 +5,7 @@ import {
5
5
  act,
6
6
  type ReactTestInstance,
7
7
  } from "react-test-renderer";
8
- import { I18nManager, Pressable, StyleSheet, Text } from "react-native";
8
+ import { I18nManager, StyleSheet, Text } from "react-native";
9
9
  import { UseSmileIDPreviewScreen } from "../use_smile_id_preview_screen";
10
10
  import { defaultLightTheme } from "../../theme/theme";
11
11
 
@@ -14,7 +14,7 @@ import { defaultLightTheme } from "../../theme/theme";
14
14
  * - Image at the oval rect (not full-bleed)
15
15
  * - Title + confirmation buttons + optional attribution in card
16
16
  * - Accept / Retake gating from `resolvePreviewState`
17
- * - Slot contract (any slot replaces the entire row)
17
+ * - The confirm/retake pair is fixed (not customisable)
18
18
  */
19
19
 
20
20
  function render(element: React.ReactElement) {
@@ -164,46 +164,16 @@ describe("UseSmileIDPreviewScreen", () => {
164
164
  }
165
165
  });
166
166
 
167
- it("replaces the entire row when any button slot is supplied", () => {
168
- // Concrete scenario this test documents: the slot contract is
169
- // "all-or-nothing" — providing only continueButton hides the
170
- // default Retake. Partners must supply both slots to keep both
171
- // affordances.
172
- const onAccept = vi.fn();
173
- const onReject = vi.fn();
167
+ it("always renders the fixed confirmation-button pair (not customisable)", () => {
168
+ // Preview buttons are intentionally not customisable: the screen always
169
+ // renders the fixed confirm/retake pair. Re-adding a slot API is a product
170
+ // decision, not a bug fix.
174
171
  const { root } = render(
175
- <UseSmileIDPreviewScreen
176
- capturedSelfiePath={PATH}
177
- onAccept={onAccept}
178
- onReject={onReject}
179
- continueButton={({ onClick }) => (
180
- <Pressable testID="si_custom_continue" onPress={onClick}>
181
- <Text>Confirm</Text>
182
- </Pressable>
183
- )}
184
- cancelButton={({ onClick }) => (
185
- <Pressable testID="si_custom_cancel" onPress={onClick}>
186
- <Text>Retake</Text>
187
- </Pressable>
188
- )}
189
- />,
172
+ <UseSmileIDPreviewScreen capturedSelfiePath={PATH} />,
190
173
  );
191
-
192
- expect(findByTestID(root, "si_custom_continue")).not.toBeNull();
193
- expect(findByTestID(root, "si_custom_cancel")).not.toBeNull();
194
- // Default row must be gone — guards against a future refactor
195
- // that double-renders the row alongside the slots.
196
- expect(findByTestID(root, "si_preview_confirmation_buttons")).toBeNull();
197
-
198
- act(() => {
199
- (findByTestID(root, "si_custom_continue")!.props.onPress as () => void)();
200
- });
201
- expect(onAccept).toHaveBeenCalledTimes(1);
202
-
203
- act(() => {
204
- (findByTestID(root, "si_custom_cancel")!.props.onPress as () => void)();
205
- });
206
- expect(onReject).toHaveBeenCalledTimes(1);
174
+ expect(
175
+ findByTestID(root, "si_preview_confirmation_buttons"),
176
+ ).not.toBeNull();
207
177
  });
208
178
 
209
179
  it("renders the card title in the theme primary color", () => {
@@ -0,0 +1,212 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { create as createRenderer, act } from "react-test-renderer";
3
+ import * as ScreenOrientation from "expo-screen-orientation";
4
+ import { UseSmileIDCrashReporting } from "@smileid/usesmileid_bridge";
5
+ import type { DocumentOrientation } from "../../../../core/models/document_type";
6
+ import {
7
+ useCaptureOrientationLock,
8
+ resetUnsupportedLockWarningsForTesting,
9
+ } from "../use_capture_orientation_lock";
10
+
11
+ /**
12
+ * Lifecycle tests for `useCaptureOrientationLock`:
13
+ * - mount asserts the screen's own orientation
14
+ * - unmount applies the configured exit lock (portrait-up by default)
15
+ * - a host build that cannot honour the lock is warned about (dev builds,
16
+ * once per target) and reported (every build)
17
+ *
18
+ * The `expo-screen-orientation` mock lives in `setup_react_native.ts`.
19
+ * `__DEV__` is bundler-injected and deliberately NOT defined suite-wide —
20
+ * each test sets exactly the build shape it pins.
21
+ */
22
+
23
+ const mockedLockAsync = vi.mocked(ScreenOrientation.lockAsync);
24
+ const mockedSupportsLock = vi.mocked(
25
+ ScreenOrientation.supportsOrientationLockAsync,
26
+ );
27
+
28
+ const globalWithDev = globalThis as { __DEV__?: boolean };
29
+
30
+ function LockHost(props: {
31
+ readonly orientation: DocumentOrientation;
32
+ readonly exitLock?: ScreenOrientation.OrientationLock;
33
+ }) {
34
+ useCaptureOrientationLock(props.orientation, {
35
+ ...(props.exitLock !== undefined ? { exitLock: props.exitLock } : {}),
36
+ });
37
+ return null;
38
+ }
39
+
40
+ async function flushPromises(): Promise<void> {
41
+ // A macrotask tick drains every pending microtask chain first, so this stays
42
+ // correct if the hook gains or loses `await`s.
43
+ await new Promise<void>((resolve) => setTimeout(resolve, 0));
44
+ }
45
+
46
+ async function mountHost(
47
+ props: Parameters<typeof LockHost>[0],
48
+ ): Promise<ReturnType<typeof createRenderer>> {
49
+ let tree!: ReturnType<typeof createRenderer>;
50
+ act(() => {
51
+ tree = createRenderer(<LockHost {...props} />);
52
+ });
53
+ await act(async () => {
54
+ await flushPromises();
55
+ });
56
+ return tree;
57
+ }
58
+
59
+ describe("useCaptureOrientationLock", () => {
60
+ let savedDev: boolean | undefined;
61
+
62
+ beforeEach(() => {
63
+ savedDev = globalWithDev.__DEV__;
64
+ delete globalWithDev.__DEV__;
65
+ resetUnsupportedLockWarningsForTesting();
66
+ mockedLockAsync.mockReset();
67
+ mockedLockAsync.mockResolvedValue(undefined);
68
+ mockedSupportsLock.mockReset();
69
+ mockedSupportsLock.mockResolvedValue(true);
70
+ });
71
+
72
+ afterEach(() => {
73
+ if (savedDev === undefined) {
74
+ delete globalWithDev.__DEV__;
75
+ } else {
76
+ globalWithDev.__DEV__ = savedDev;
77
+ }
78
+ vi.restoreAllMocks();
79
+ });
80
+
81
+ it("locks landscape on mount for a landscape document", async () => {
82
+ const tree = await mountHost({ orientation: "Landscape" });
83
+
84
+ expect(mockedLockAsync).toHaveBeenCalledTimes(1);
85
+ expect(mockedLockAsync).toHaveBeenCalledWith(
86
+ ScreenOrientation.OrientationLock.LANDSCAPE,
87
+ );
88
+
89
+ await act(async () => {
90
+ tree.unmount();
91
+ await flushPromises();
92
+ });
93
+ });
94
+
95
+ it("restores portrait-up on unmount when no exit lock is configured", async () => {
96
+ const tree = await mountHost({ orientation: "Landscape" });
97
+ mockedLockAsync.mockClear();
98
+
99
+ await act(async () => {
100
+ tree.unmount();
101
+ await flushPromises();
102
+ });
103
+
104
+ expect(mockedLockAsync).toHaveBeenCalledTimes(1);
105
+ expect(mockedLockAsync).toHaveBeenCalledWith(
106
+ ScreenOrientation.OrientationLock.PORTRAIT_UP,
107
+ );
108
+ });
109
+
110
+ it("applies the configured exit lock on unmount instead of portrait-up", async () => {
111
+ const tree = await mountHost({
112
+ orientation: "Landscape",
113
+ exitLock: ScreenOrientation.OrientationLock.DEFAULT,
114
+ });
115
+ mockedLockAsync.mockClear();
116
+
117
+ await act(async () => {
118
+ tree.unmount();
119
+ await flushPromises();
120
+ });
121
+
122
+ expect(mockedLockAsync).toHaveBeenCalledTimes(1);
123
+ expect(mockedLockAsync).toHaveBeenCalledWith(
124
+ ScreenOrientation.OrientationLock.DEFAULT,
125
+ );
126
+ expect(mockedLockAsync).not.toHaveBeenCalledWith(
127
+ ScreenOrientation.OrientationLock.PORTRAIT_UP,
128
+ );
129
+ });
130
+
131
+ it("warns and reports once when the host build cannot honour the lock", async () => {
132
+ // Pins the reporting contract for any build where the platform declares
133
+ // the lock unsupported (lockAsync would resolve without rotating).
134
+ globalWithDev.__DEV__ = true;
135
+ mockedSupportsLock.mockResolvedValue(false);
136
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
137
+ const captureException = vi
138
+ .spyOn(UseSmileIDCrashReporting, "captureException")
139
+ .mockImplementation(() => {});
140
+
141
+ const tree = await mountHost({ orientation: "Landscape" });
142
+
143
+ expect(captureException).toHaveBeenCalledTimes(1);
144
+ expect(warn).toHaveBeenCalledTimes(1);
145
+ expect(warn.mock.calls[0]?.[0]).toContain("orientation");
146
+
147
+ await act(async () => {
148
+ tree.unmount();
149
+ await flushPromises();
150
+ });
151
+ });
152
+
153
+ it("reports without warning when __DEV__ is absent (production build)", async () => {
154
+ mockedSupportsLock.mockResolvedValue(false);
155
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
156
+ const captureException = vi
157
+ .spyOn(UseSmileIDCrashReporting, "captureException")
158
+ .mockImplementation(() => {});
159
+
160
+ const tree = await mountHost({ orientation: "Landscape" });
161
+
162
+ expect(captureException).toHaveBeenCalledTimes(1);
163
+ expect(warn).not.toHaveBeenCalled();
164
+
165
+ await act(async () => {
166
+ tree.unmount();
167
+ await flushPromises();
168
+ });
169
+ });
170
+
171
+ it("warns once across capture-screen remounts for the same target", async () => {
172
+ globalWithDev.__DEV__ = true;
173
+ mockedSupportsLock.mockResolvedValue(false);
174
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
175
+ const captureException = vi
176
+ .spyOn(UseSmileIDCrashReporting, "captureException")
177
+ .mockImplementation(() => {});
178
+
179
+ const first = await mountHost({ orientation: "Landscape" });
180
+ await act(async () => {
181
+ first.unmount();
182
+ await flushPromises();
183
+ });
184
+ const second = await mountHost({ orientation: "Landscape" });
185
+
186
+ expect(warn).toHaveBeenCalledTimes(1);
187
+ expect(captureException).toHaveBeenCalledTimes(2);
188
+
189
+ await act(async () => {
190
+ second.unmount();
191
+ await flushPromises();
192
+ });
193
+ });
194
+
195
+ it("stays silent when the host build permits the lock", async () => {
196
+ globalWithDev.__DEV__ = true;
197
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
198
+ const captureException = vi
199
+ .spyOn(UseSmileIDCrashReporting, "captureException")
200
+ .mockImplementation(() => {});
201
+
202
+ const tree = await mountHost({ orientation: "Portrait" });
203
+
204
+ expect(captureException).not.toHaveBeenCalled();
205
+ expect(warn).not.toHaveBeenCalled();
206
+
207
+ await act(async () => {
208
+ tree.unmount();
209
+ await flushPromises();
210
+ });
211
+ });
212
+ });