@smileid/usesmileid 12.0.0 → 12.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.
Files changed (365) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +1 -1
  3. package/dist/camera/camera_defaults.d.ts +2 -0
  4. package/dist/camera/camera_defaults.d.ts.map +1 -1
  5. package/dist/camera/camera_defaults.js +2 -0
  6. package/dist/camera/camera_defaults.js.map +1 -1
  7. package/dist/core/exception/agent_mode_with_enhanced_liveness_exception.d.ts +11 -0
  8. package/dist/core/exception/agent_mode_with_enhanced_liveness_exception.d.ts.map +1 -0
  9. package/dist/core/exception/agent_mode_with_enhanced_liveness_exception.js +17 -0
  10. package/dist/core/exception/agent_mode_with_enhanced_liveness_exception.js.map +1 -0
  11. package/dist/core/exception/capture_validation_exceptions.d.ts +1 -0
  12. package/dist/core/exception/capture_validation_exceptions.d.ts.map +1 -1
  13. package/dist/core/exception/capture_validation_exceptions.js +1 -0
  14. package/dist/core/exception/capture_validation_exceptions.js.map +1 -1
  15. package/dist/core/exception/flow_exceptions.d.ts +4 -2
  16. package/dist/core/exception/flow_exceptions.d.ts.map +1 -1
  17. package/dist/core/exception/flow_exceptions.js +3 -2
  18. package/dist/core/exception/flow_exceptions.js.map +1 -1
  19. package/dist/core/exception/index.d.ts +1 -0
  20. package/dist/core/exception/index.d.ts.map +1 -1
  21. package/dist/core/exception/index.js +1 -0
  22. package/dist/core/exception/index.js.map +1 -1
  23. package/dist/core/exception/missing_job_type_exception.d.ts +1 -3
  24. package/dist/core/exception/missing_job_type_exception.d.ts.map +1 -1
  25. package/dist/core/exception/missing_job_type_exception.js +2 -4
  26. package/dist/core/exception/missing_job_type_exception.js.map +1 -1
  27. package/dist/core/exception/use_smile_id_error_code.d.ts +1 -0
  28. package/dist/core/exception/use_smile_id_error_code.d.ts.map +1 -1
  29. package/dist/core/exception/use_smile_id_error_code.js +1 -0
  30. package/dist/core/exception/use_smile_id_error_code.js.map +1 -1
  31. package/dist/core/models/config.d.ts +19 -0
  32. package/dist/core/models/config.d.ts.map +1 -1
  33. package/dist/core/models/document_type.d.ts +1 -1
  34. package/dist/core/models/document_type.d.ts.map +1 -1
  35. package/dist/core/models/document_type.js +2 -2
  36. package/dist/core/models/document_type.js.map +1 -1
  37. package/dist/core/models/global_privacy_control.d.ts +36 -0
  38. package/dist/core/models/global_privacy_control.d.ts.map +1 -0
  39. package/dist/core/models/global_privacy_control.js +39 -0
  40. package/dist/core/models/global_privacy_control.js.map +1 -0
  41. package/dist/core/models/network_config.d.ts +2 -2
  42. package/dist/core/models/network_config.js +2 -2
  43. package/dist/data/api/interceptors/smile_auth_interceptor.d.ts.map +1 -1
  44. package/dist/data/api/interceptors/smile_auth_interceptor.js +3 -17
  45. package/dist/data/api/interceptors/smile_auth_interceptor.js.map +1 -1
  46. package/dist/data/api/smile_id_api.d.ts +5 -1
  47. package/dist/data/api/smile_id_api.d.ts.map +1 -1
  48. package/dist/data/api/smile_id_api.js +30 -27
  49. package/dist/data/api/smile_id_api.js.map +1 -1
  50. package/dist/data/dsl/builder/retry_config_builder.d.ts +4 -4
  51. package/dist/data/dsl/builder/retry_config_builder.d.ts.map +1 -1
  52. package/dist/data/dsl/builder/retry_config_builder.js +6 -6
  53. package/dist/data/dsl/builder/retry_config_builder.js.map +1 -1
  54. package/dist/data/model/job_request.d.ts +28 -12
  55. package/dist/data/model/job_request.d.ts.map +1 -1
  56. package/dist/data/model/token_payload.d.ts +78 -0
  57. package/dist/data/model/token_payload.d.ts.map +1 -0
  58. package/dist/data/model/token_payload.js +132 -0
  59. package/dist/data/model/token_payload.js.map +1 -0
  60. package/dist/data/model/user_details_payload.d.ts +39 -0
  61. package/dist/data/model/user_details_payload.d.ts.map +1 -0
  62. package/dist/data/model/user_details_payload.js +53 -0
  63. package/dist/data/model/user_details_payload.js.map +1 -0
  64. package/dist/data/smile_id_urls.d.ts +1 -1
  65. package/dist/data/smile_id_urls.js +1 -1
  66. package/dist/data/smile_id_urls.js.map +1 -1
  67. package/dist/data/util/retry_util.d.ts +1 -1
  68. package/dist/data/util/retry_util.d.ts.map +1 -1
  69. package/dist/data/util/retry_util.js +5 -0
  70. package/dist/data/util/retry_util.js.map +1 -1
  71. package/dist/data/util/use_smile_id_token_decoder.d.ts +15 -3
  72. package/dist/data/util/use_smile_id_token_decoder.d.ts.map +1 -1
  73. package/dist/data/util/use_smile_id_token_decoder.js +20 -8
  74. package/dist/data/util/use_smile_id_token_decoder.js.map +1 -1
  75. package/dist/index.d.ts +2 -2
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +2 -2
  78. package/dist/index.js.map +1 -1
  79. package/dist/l10n/strings_en.d.ts +4 -1
  80. package/dist/l10n/strings_en.d.ts.map +1 -1
  81. package/dist/l10n/strings_en.js +4 -1
  82. package/dist/l10n/strings_en.js.map +1 -1
  83. package/dist/l10n/use_smile_id_localizations.d.ts +10 -0
  84. package/dist/l10n/use_smile_id_localizations.d.ts.map +1 -1
  85. package/dist/l10n/use_smile_id_localizations.js +59 -3
  86. package/dist/l10n/use_smile_id_localizations.js.map +1 -1
  87. package/dist/metadata/builder_configuration.d.ts +5 -0
  88. package/dist/metadata/builder_configuration.d.ts.map +1 -1
  89. package/dist/metadata/builder_configuration.js +1 -0
  90. package/dist/metadata/builder_configuration.js.map +1 -1
  91. package/dist/metadata/metadata_configuration_mappers.d.ts +2 -3
  92. package/dist/metadata/metadata_configuration_mappers.d.ts.map +1 -1
  93. package/dist/metadata/metadata_configuration_mappers.js +13 -6
  94. package/dist/metadata/metadata_configuration_mappers.js.map +1 -1
  95. package/dist/metadata/use_smile_id_metadata_factory.d.ts.map +1 -1
  96. package/dist/metadata/use_smile_id_metadata_factory.js +14 -5
  97. package/dist/metadata/use_smile_id_metadata_factory.js.map +1 -1
  98. package/dist/presentation/components/use_smile_id_button.d.ts.map +1 -1
  99. package/dist/presentation/components/use_smile_id_button.js +8 -2
  100. package/dist/presentation/components/use_smile_id_button.js.map +1 -1
  101. package/dist/presentation/components/use_smile_id_enhanced_smart_selfie_clip.d.ts +5 -2
  102. package/dist/presentation/components/use_smile_id_enhanced_smart_selfie_clip.d.ts.map +1 -1
  103. package/dist/presentation/components/use_smile_id_enhanced_smart_selfie_clip.js +8 -3
  104. package/dist/presentation/components/use_smile_id_enhanced_smart_selfie_clip.js.map +1 -1
  105. package/dist/presentation/components/use_smile_id_instructions_hero.d.ts.map +1 -1
  106. package/dist/presentation/components/use_smile_id_instructions_hero.js +8 -3
  107. package/dist/presentation/components/use_smile_id_instructions_hero.js.map +1 -1
  108. package/dist/presentation/flow/dsl/builder/config_builder.d.ts +16 -0
  109. package/dist/presentation/flow/dsl/builder/config_builder.d.ts.map +1 -1
  110. package/dist/presentation/flow/dsl/builder/config_builder.js +18 -0
  111. package/dist/presentation/flow/dsl/builder/config_builder.js.map +1 -1
  112. package/dist/presentation/flow/dsl/builder/screens_builder.d.ts +6 -12
  113. package/dist/presentation/flow/dsl/builder/screens_builder.d.ts.map +1 -1
  114. package/dist/presentation/flow/dsl/builder/screens_builder.js +7 -12
  115. package/dist/presentation/flow/dsl/builder/screens_builder.js.map +1 -1
  116. package/dist/presentation/flow/dsl/use_smile_id_flow_builder.d.ts.map +1 -1
  117. package/dist/presentation/flow/dsl/use_smile_id_flow_builder.js +23 -1
  118. package/dist/presentation/flow/dsl/use_smile_id_flow_builder.js.map +1 -1
  119. package/dist/presentation/flow/flow_navigation_manager.d.ts +22 -3
  120. package/dist/presentation/flow/flow_navigation_manager.d.ts.map +1 -1
  121. package/dist/presentation/flow/flow_navigation_manager.js +84 -46
  122. package/dist/presentation/flow/flow_navigation_manager.js.map +1 -1
  123. package/dist/presentation/flow/model/instructions_defaults.d.ts.map +1 -1
  124. package/dist/presentation/flow/model/instructions_defaults.js +1 -1
  125. package/dist/presentation/flow/model/instructions_defaults.js.map +1 -1
  126. package/dist/presentation/flow/screen/orchestrated_capture_screen.d.ts.map +1 -1
  127. package/dist/presentation/flow/screen/orchestrated_capture_screen.js +13 -3
  128. package/dist/presentation/flow/screen/orchestrated_capture_screen.js.map +1 -1
  129. package/dist/presentation/flow/screen/orchestrated_consent_screen.d.ts.map +1 -1
  130. package/dist/presentation/flow/screen/orchestrated_consent_screen.js +3 -1
  131. package/dist/presentation/flow/screen/orchestrated_consent_screen.js.map +1 -1
  132. package/dist/presentation/flow/screen/orchestrated_preview_screen.d.ts.map +1 -1
  133. package/dist/presentation/flow/screen/orchestrated_preview_screen.js +0 -8
  134. package/dist/presentation/flow/screen/orchestrated_preview_screen.js.map +1 -1
  135. package/dist/presentation/flow/screen/orchestrated_processing_screen.d.ts.map +1 -1
  136. package/dist/presentation/flow/screen/orchestrated_processing_screen.js +25 -5
  137. package/dist/presentation/flow/screen/orchestrated_processing_screen.js.map +1 -1
  138. package/dist/presentation/flow/use_smile_id_flow.d.ts.map +1 -1
  139. package/dist/presentation/flow/use_smile_id_flow.js +45 -12
  140. package/dist/presentation/flow/use_smile_id_flow.js.map +1 -1
  141. package/dist/presentation/flow/validation/flow_validator.d.ts +16 -2
  142. package/dist/presentation/flow/validation/flow_validator.d.ts.map +1 -1
  143. package/dist/presentation/flow/validation/flow_validator.js +61 -47
  144. package/dist/presentation/flow/validation/flow_validator.js.map +1 -1
  145. package/dist/presentation/flow/validation/job_type_validator.d.ts +7 -0
  146. package/dist/presentation/flow/validation/job_type_validator.d.ts.map +1 -1
  147. package/dist/presentation/flow/validation/job_type_validator.js +52 -27
  148. package/dist/presentation/flow/validation/job_type_validator.js.map +1 -1
  149. package/dist/presentation/flow/validation/user_details_validator.d.ts +9 -0
  150. package/dist/presentation/flow/validation/user_details_validator.d.ts.map +1 -0
  151. package/dist/presentation/flow/validation/user_details_validator.js +40 -0
  152. package/dist/presentation/flow/validation/user_details_validator.js.map +1 -0
  153. package/dist/presentation/screen/helpers/capture_screen_helpers.d.ts.map +1 -1
  154. package/dist/presentation/screen/helpers/capture_screen_helpers.js +1 -0
  155. package/dist/presentation/screen/helpers/capture_screen_helpers.js.map +1 -1
  156. package/dist/presentation/screen/helpers/capture_ui_state.d.ts +6 -6
  157. package/dist/presentation/screen/helpers/capture_ui_state.d.ts.map +1 -1
  158. package/dist/presentation/screen/helpers/capture_ui_state.js +19 -19
  159. package/dist/presentation/screen/helpers/capture_ui_state.js.map +1 -1
  160. package/dist/presentation/screen/helpers/ess_turn_haptics.d.ts +2 -2
  161. package/dist/presentation/screen/helpers/ess_turn_haptics.d.ts.map +1 -1
  162. package/dist/presentation/screen/helpers/ess_turn_haptics.js +6 -6
  163. package/dist/presentation/screen/helpers/ess_turn_haptics.js.map +1 -1
  164. package/dist/presentation/screen/helpers/scan_state_to_overlay_state.d.ts +5 -5
  165. package/dist/presentation/screen/helpers/scan_state_to_overlay_state.d.ts.map +1 -1
  166. package/dist/presentation/screen/helpers/scan_state_to_overlay_state.js +11 -11
  167. package/dist/presentation/screen/helpers/scan_state_to_overlay_state.js.map +1 -1
  168. package/dist/presentation/screen/helpers/use_capture_orientation_lock.d.ts +13 -3
  169. package/dist/presentation/screen/helpers/use_capture_orientation_lock.d.ts.map +1 -1
  170. package/dist/presentation/screen/helpers/use_capture_orientation_lock.js +48 -6
  171. package/dist/presentation/screen/helpers/use_capture_orientation_lock.js.map +1 -1
  172. package/dist/presentation/screen/use_smile_id_capture_screen.d.ts +4 -2
  173. package/dist/presentation/screen/use_smile_id_capture_screen.d.ts.map +1 -1
  174. package/dist/presentation/screen/use_smile_id_capture_screen.js +90 -23
  175. package/dist/presentation/screen/use_smile_id_capture_screen.js.map +1 -1
  176. package/dist/presentation/screen/use_smile_id_consent_screen.d.ts +7 -0
  177. package/dist/presentation/screen/use_smile_id_consent_screen.d.ts.map +1 -1
  178. package/dist/presentation/screen/use_smile_id_consent_screen.js +6 -5
  179. package/dist/presentation/screen/use_smile_id_consent_screen.js.map +1 -1
  180. package/dist/presentation/screen/use_smile_id_document_back_instructions.d.ts.map +1 -1
  181. package/dist/presentation/screen/use_smile_id_document_back_instructions.js +5 -13
  182. package/dist/presentation/screen/use_smile_id_document_back_instructions.js.map +1 -1
  183. package/dist/presentation/screen/use_smile_id_document_preview_screen.js +1 -1
  184. package/dist/presentation/screen/use_smile_id_document_preview_screen.js.map +1 -1
  185. package/dist/presentation/screen/use_smile_id_preview_screen.d.ts +1 -21
  186. package/dist/presentation/screen/use_smile_id_preview_screen.d.ts.map +1 -1
  187. package/dist/presentation/screen/use_smile_id_preview_screen.js +3 -14
  188. package/dist/presentation/screen/use_smile_id_preview_screen.js.map +1 -1
  189. package/dist/presentation/screen/use_smile_id_processing_screen.js +1 -1
  190. package/dist/presentation/screen/use_smile_id_processing_screen.js.map +1 -1
  191. package/dist/presentation/utils/locale_utils.d.ts +10 -4
  192. package/dist/presentation/utils/locale_utils.d.ts.map +1 -1
  193. package/dist/presentation/utils/locale_utils.js +13 -5
  194. package/dist/presentation/utils/locale_utils.js.map +1 -1
  195. package/dist/testing/fake_face_analyzer.d.ts +43 -0
  196. package/dist/testing/fake_face_analyzer.d.ts.map +1 -0
  197. package/dist/testing/fake_face_analyzer.js +52 -0
  198. package/dist/testing/fake_face_analyzer.js.map +1 -0
  199. package/dist/testing/index.d.ts +5 -0
  200. package/dist/testing/index.d.ts.map +1 -0
  201. package/dist/testing/index.js +7 -0
  202. package/dist/testing/index.js.map +1 -0
  203. package/dist/testing/scenario_expectations.d.ts +28 -0
  204. package/dist/testing/scenario_expectations.d.ts.map +1 -0
  205. package/dist/testing/scenario_expectations.js +65 -0
  206. package/dist/testing/scenario_expectations.js.map +1 -0
  207. package/dist/testing/scenario_loader.d.ts +11 -0
  208. package/dist/testing/scenario_loader.d.ts.map +1 -0
  209. package/dist/testing/scenario_loader.js +64 -0
  210. package/dist/testing/scenario_loader.js.map +1 -0
  211. package/dist/testing/scripted_scan_results.d.ts +17 -0
  212. package/dist/testing/scripted_scan_results.d.ts.map +1 -0
  213. package/dist/testing/scripted_scan_results.js +52 -0
  214. package/dist/testing/scripted_scan_results.js.map +1 -0
  215. package/package.json +11 -4
  216. package/src/__tests__/dsl.test.ts +51 -1
  217. package/src/__tests__/react_native_mock.tsx +22 -0
  218. package/src/__tests__/setup_react_native.ts +17 -2
  219. package/src/__tests__/snapshots/__snapshots__/screens_structural.test.tsx.snap +5267 -0
  220. package/src/__tests__/snapshots/screens_structural.test.tsx +230 -0
  221. package/src/camera/__tests__/camera_permissions.test.ts +72 -0
  222. package/src/camera/__tests__/camera_system_focus.test.ts +173 -0
  223. package/src/camera/__tests__/use_smile_id_camera_preview.test.tsx +461 -0
  224. package/src/camera/__tests__/use_smile_id_camera_system.test.ts +103 -0
  225. package/src/camera/__tests__/use_smile_id_frame_stream_hook.test.tsx +229 -0
  226. package/src/camera/camera_defaults.ts +3 -0
  227. package/src/core/exception/__tests__/builder_validation_exceptions.test.ts +81 -0
  228. package/src/core/exception/agent_mode_with_enhanced_liveness_exception.ts +20 -0
  229. package/src/core/exception/capture_validation_exceptions.ts +1 -0
  230. package/src/core/exception/flow_exceptions.test.ts +10 -0
  231. package/src/core/exception/flow_exceptions.ts +3 -2
  232. package/src/core/exception/index.ts +1 -0
  233. package/src/core/exception/missing_job_type_exception.ts +2 -4
  234. package/src/core/exception/use_smile_id_error_code.ts +1 -0
  235. package/src/core/models/__tests__/document_type.test.ts +2 -2
  236. package/src/core/models/__tests__/flow_models.test.ts +26 -0
  237. package/src/core/models/config.ts +22 -0
  238. package/src/core/models/document_type.ts +2 -2
  239. package/src/core/models/global_privacy_control.ts +45 -0
  240. package/src/core/models/network_config.ts +2 -2
  241. package/src/data/api/__tests__/smile_id_api.test.ts +275 -1
  242. package/src/data/api/interceptors/__tests__/credential_header_redaction.test.ts +144 -0
  243. package/src/data/api/interceptors/__tests__/gzip_interceptor.test.ts +83 -0
  244. package/src/data/api/interceptors/smile_auth_interceptor.ts +3 -18
  245. package/src/data/api/smile_id_api.ts +36 -49
  246. package/src/data/compression/__tests__/payload_compressor_real_jpeg.test.ts +6 -2
  247. package/src/data/dsl/__tests__/use_smile_id_network_builder.test.ts +157 -0
  248. package/src/data/dsl/builder/__tests__/config_builders.test.ts +62 -0
  249. package/src/data/dsl/builder/retry_config_builder.ts +6 -6
  250. package/src/data/factory/__tests__/retry_gzip_mac_composition.test.ts +163 -0
  251. package/src/data/factory/__tests__/signed_job_submission_headers.test.ts +1 -1
  252. package/src/data/model/__tests__/token_payload.test.ts +224 -0
  253. package/src/data/model/__tests__/user_details_payload.test.ts +100 -0
  254. package/src/data/model/job_request.ts +27 -12
  255. package/src/data/model/token_payload.ts +182 -0
  256. package/src/data/model/user_details_payload.ts +84 -0
  257. package/src/data/repository/__tests__/network_retries.test.ts +1 -1
  258. package/src/data/repository/__tests__/repository_dispatch.test.ts +366 -0
  259. package/src/data/repository/__tests__/use_smile_id_repository.test.ts +1 -1
  260. package/src/data/security/__tests__/request_canonicalizer.test.ts +132 -0
  261. package/src/data/security/__tests__/signing_crypto.test.ts +203 -0
  262. package/src/data/smile_id_urls.ts +1 -1
  263. package/src/data/util/__tests__/retry_util.test.ts +25 -0
  264. package/src/data/util/__tests__/use_smile_id_token_decoder.test.ts +179 -0
  265. package/src/data/util/retry_util.ts +9 -1
  266. package/src/data/util/use_smile_id_token_decoder.ts +29 -8
  267. package/src/index.ts +6 -4
  268. package/src/l10n/__tests__/use_smile_id_localizations.test.ts +122 -5
  269. package/src/l10n/__tests__/use_smile_id_localizations_provider.test.tsx +112 -0
  270. package/src/l10n/__tests__/use_smile_id_localizations_surface.test.ts +231 -0
  271. package/src/l10n/strings_en.ts +5 -1
  272. package/src/l10n/use_smile_id_localizations.ts +80 -3
  273. package/src/metadata/__tests__/device_orientation_sampling.test.ts +165 -0
  274. package/src/metadata/__tests__/geolocation.test.ts +150 -0
  275. package/src/metadata/__tests__/metadata_configuration_mappers.test.ts +39 -0
  276. package/src/metadata/__tests__/metadata_configuration_objects.test.ts +2 -1
  277. package/src/metadata/__tests__/metadata_device_context.test.ts +3 -3
  278. package/src/metadata/__tests__/metadata_device_os_android.test.ts +18 -0
  279. package/src/metadata/__tests__/metadata_golden_fixture.json +1 -1
  280. package/src/metadata/__tests__/metadata_payload_golden.test.ts +4 -4
  281. package/src/metadata/__tests__/optional_native_modules.test.ts +234 -0
  282. package/src/metadata/__tests__/use_smile_id_metadata.test.ts +1 -1
  283. package/src/metadata/__tests__/use_smile_id_metadata_factory.test.ts +615 -0
  284. package/src/metadata/builder_configuration.ts +6 -0
  285. package/src/metadata/metadata_configuration_mappers.ts +21 -7
  286. package/src/metadata/use_smile_id_metadata_factory.ts +17 -6
  287. package/src/presentation/assets/si_document_green_book_hero_animation.json +1 -4668
  288. package/src/presentation/assets/si_document_hero_animation.json +1 -4799
  289. package/src/presentation/assets/si_document_hero_back_animation.json +1 -2039
  290. package/src/presentation/assets/si_document_passport_hero_animation.json +1 -14491
  291. package/src/presentation/assets/si_ess_all_directions_hero_animation.json +1 -0
  292. package/src/presentation/assets/si_ess_animation_all_directions.json +1 -6589
  293. package/src/presentation/assets/si_selfie_hero_animation.json +1 -1439
  294. package/src/presentation/assets/si_smile_to_capture_animation.json +1 -1439
  295. package/src/presentation/components/__tests__/use_smile_id_content_card.test.tsx +112 -0
  296. package/src/presentation/components/__tests__/use_smile_id_enhanced_smart_selfie_clip.test.tsx +9 -4
  297. package/src/presentation/components/__tests__/use_smile_id_face_oval_image.test.tsx +190 -0
  298. package/src/presentation/components/__tests__/use_smile_id_instructions_hero.test.tsx +44 -0
  299. package/src/presentation/components/__tests__/use_smile_id_learn_more_content.test.tsx +159 -0
  300. package/src/presentation/components/__tests__/use_smile_id_processing_center_indicator.test.tsx +88 -0
  301. package/src/presentation/components/use_smile_id_button.tsx +7 -2
  302. package/src/presentation/components/use_smile_id_enhanced_smart_selfie_clip.tsx +9 -3
  303. package/src/presentation/components/use_smile_id_instructions_hero.tsx +10 -0
  304. package/src/presentation/flow/__tests__/document_flow_navigation.test.ts +3 -3
  305. package/src/presentation/flow/__tests__/flow_navigation_manager.test.ts +372 -1
  306. package/src/presentation/flow/__tests__/use_smile_id_flow.test.tsx +651 -0
  307. package/src/presentation/flow/dsl/__tests__/use_smile_id_builder_component.test.tsx +120 -0
  308. package/src/presentation/flow/dsl/builder/__tests__/config_builder.test.ts +25 -0
  309. package/src/presentation/flow/dsl/builder/config_builder.ts +21 -0
  310. package/src/presentation/flow/dsl/builder/screens_builder.ts +7 -12
  311. package/src/presentation/flow/dsl/use_smile_id_flow_builder.ts +27 -1
  312. package/src/presentation/flow/flow_navigation_manager.ts +108 -93
  313. package/src/presentation/flow/graph/__tests__/screen_renderer.test.tsx +102 -0
  314. package/src/presentation/flow/model/instructions_defaults.ts +1 -1
  315. package/src/presentation/flow/screen/__tests__/orchestrated_capture_screen.test.tsx +836 -0
  316. package/src/presentation/flow/screen/__tests__/orchestrated_consent_screen.test.tsx +165 -0
  317. package/src/presentation/flow/screen/__tests__/orchestrated_instructions_screen.test.tsx +163 -0
  318. package/src/presentation/flow/screen/__tests__/orchestrated_preview_screen.test.tsx +224 -0
  319. package/src/presentation/flow/screen/__tests__/orchestrated_processing_screen.test.tsx +338 -0
  320. package/src/presentation/flow/screen/orchestrated_capture_screen.tsx +18 -3
  321. package/src/presentation/flow/screen/orchestrated_consent_screen.tsx +5 -0
  322. package/src/presentation/flow/screen/orchestrated_preview_screen.tsx +0 -12
  323. package/src/presentation/flow/screen/orchestrated_processing_screen.tsx +42 -5
  324. package/src/presentation/flow/use_smile_id_flow.tsx +63 -12
  325. package/src/presentation/flow/validation/__tests__/flow_validator.test.ts +219 -1
  326. package/src/presentation/flow/validation/__tests__/job_type_validator.test.ts +200 -0
  327. package/src/presentation/flow/validation/flow_validator.ts +85 -57
  328. package/src/presentation/flow/validation/job_type_validator.ts +73 -39
  329. package/src/presentation/flow/validation/user_details_validator.ts +53 -0
  330. package/src/presentation/screen/__tests__/capture_ui_state.test.ts +66 -47
  331. package/src/presentation/screen/__tests__/ess_turn_haptics.test.ts +15 -11
  332. package/src/presentation/screen/__tests__/feedback_copy_table.test.ts +145 -0
  333. package/src/presentation/screen/__tests__/scan_state_to_overlay_state.test.ts +47 -27
  334. package/src/presentation/screen/__tests__/use_smile_id_capture_screen.test.tsx +524 -14
  335. package/src/presentation/screen/__tests__/use_smile_id_document_back_instructions.test.tsx +4 -2
  336. package/src/presentation/screen/__tests__/use_smile_id_permission_screen.test.tsx +68 -0
  337. package/src/presentation/screen/__tests__/use_smile_id_preview_screen.test.tsx +10 -40
  338. package/src/presentation/screen/helpers/__tests__/use_capture_orientation_lock.test.tsx +212 -0
  339. package/src/presentation/screen/helpers/capture_screen_helpers.ts +1 -0
  340. package/src/presentation/screen/helpers/capture_ui_state.ts +27 -27
  341. package/src/presentation/screen/helpers/ess_turn_haptics.ts +9 -9
  342. package/src/presentation/screen/helpers/scan_state_to_overlay_state.ts +13 -13
  343. package/src/presentation/screen/helpers/use_capture_orientation_lock.ts +69 -7
  344. package/src/presentation/screen/use_smile_id_capture_screen.tsx +107 -28
  345. package/src/presentation/screen/use_smile_id_consent_screen.tsx +16 -5
  346. package/src/presentation/screen/use_smile_id_document_back_instructions.tsx +14 -23
  347. package/src/presentation/screen/use_smile_id_document_preview_screen.tsx +1 -1
  348. package/src/presentation/screen/use_smile_id_preview_screen.tsx +13 -45
  349. package/src/presentation/screen/use_smile_id_processing_screen.tsx +1 -1
  350. package/src/presentation/theme/__tests__/use_use_smile_id_fonts.test.tsx +166 -0
  351. package/src/presentation/utils/__tests__/locale_utils.test.ts +46 -5
  352. package/src/presentation/utils/locale_utils.ts +13 -4
  353. package/src/testing/__tests__/fake_face_analyzer.test.ts +42 -0
  354. package/src/testing/__tests__/scenario_expectations.test.ts +110 -0
  355. package/src/testing/__tests__/scenario_loader.test.ts +64 -0
  356. package/src/testing/fake_face_analyzer.ts +82 -0
  357. package/src/testing/index.ts +15 -0
  358. package/src/testing/scenario_expectations.ts +87 -0
  359. package/src/testing/scenario_loader.ts +83 -0
  360. package/src/testing/scripted_scan_results.ts +66 -0
  361. package/dist/presentation/components/use_smile_id_face_silhouette_overlay.d.ts +0 -15
  362. package/dist/presentation/components/use_smile_id_face_silhouette_overlay.d.ts.map +0 -1
  363. package/dist/presentation/components/use_smile_id_face_silhouette_overlay.js +0 -19
  364. package/dist/presentation/components/use_smile_id_face_silhouette_overlay.js.map +0 -1
  365. package/src/presentation/components/use_smile_id_face_silhouette_overlay.tsx +0 -27
@@ -0,0 +1,166 @@
1
+ import {
2
+ describe,
3
+ it,
4
+ expect,
5
+ vi,
6
+ beforeAll,
7
+ beforeEach,
8
+ afterEach,
9
+ } from "vitest";
10
+ import * as React from "react";
11
+ import { create as createRenderer, act } from "react-test-renderer";
12
+ import Module from "node:module";
13
+ import { DM_SANS_FONT_NAMES } from "../theme";
14
+
15
+ /**
16
+ * The hook `require()`s the bundled `.ttf` assets — Metro resolves those to
17
+ * numeric asset ids, Node does not. Register a CommonJS handler that mimics
18
+ * Metro's numeric id so the font map can be asserted without a bundler.
19
+ */
20
+ const nodeExtensions = (
21
+ Module as unknown as {
22
+ _extensions: Record<
23
+ string,
24
+ (mod: { exports: unknown }, filename: string) => void
25
+ >;
26
+ }
27
+ )._extensions;
28
+
29
+ let nextAssetId = 0;
30
+
31
+ beforeAll(() => {
32
+ nodeExtensions[".ttf"] = (mod) => {
33
+ nextAssetId += 1;
34
+ mod.exports = nextAssetId;
35
+ };
36
+ });
37
+
38
+ type FontsModule = typeof import("../use_use_smile_id_fonts");
39
+ type ExpoFontModule = { loadAsync: ReturnType<typeof vi.fn> };
40
+
41
+ /** Fresh copy of the module so its one-shot `loadPromise` latch is reset. */
42
+ async function freshModules(): Promise<{
43
+ fonts: FontsModule;
44
+ expoFont: ExpoFontModule;
45
+ }> {
46
+ vi.resetModules();
47
+ const expoFont = (await import("expo-font")) as unknown as ExpoFontModule;
48
+ // The shared setup's `expo-font` mock survives `resetModules`, so its spy
49
+ // carries calls across tests unless cleared here.
50
+ expoFont.loadAsync.mockClear();
51
+ const fonts = (await import("../use_use_smile_id_fonts")) as FontsModule;
52
+ return { fonts, expoFont };
53
+ }
54
+
55
+ let warn: ReturnType<typeof vi.spyOn>;
56
+
57
+ beforeEach(() => {
58
+ warn = vi.spyOn(console, "warn").mockImplementation(() => {});
59
+ });
60
+
61
+ afterEach(() => {
62
+ warn.mockRestore();
63
+ });
64
+
65
+ describe("loadUseSmileIDFonts", () => {
66
+ it("registers every bundled DM Sans weight under its typography face name", async () => {
67
+ const { fonts, expoFont } = await freshModules();
68
+
69
+ await fonts.loadUseSmileIDFonts();
70
+
71
+ expect(expoFont.loadAsync).toHaveBeenCalledTimes(1);
72
+ const [map] = expoFont.loadAsync.mock.calls[0] as [Record<string, number>];
73
+ expect(Object.keys(map).sort()).toEqual(
74
+ Object.values(DM_SANS_FONT_NAMES).sort(),
75
+ );
76
+ for (const face of Object.values(DM_SANS_FONT_NAMES)) {
77
+ expect(typeof map[face]).toBe("number");
78
+ }
79
+ });
80
+
81
+ it("shares one in-flight load across concurrent flows", async () => {
82
+ const { fonts, expoFont } = await freshModules();
83
+
84
+ const [first, second] = [
85
+ fonts.loadUseSmileIDFonts(),
86
+ fonts.loadUseSmileIDFonts(),
87
+ ];
88
+ await Promise.all([first, second]);
89
+ await fonts.loadUseSmileIDFonts();
90
+
91
+ expect(first).toBe(second);
92
+ expect(expoFont.loadAsync).toHaveBeenCalledTimes(1);
93
+ });
94
+ });
95
+
96
+ describe("useUseSmileIDFonts", () => {
97
+ function renderHook(useHook: () => boolean) {
98
+ const seen: boolean[] = [];
99
+ function Probe() {
100
+ seen.push(useHook());
101
+ return null;
102
+ }
103
+ let renderer!: ReturnType<typeof createRenderer>;
104
+ act(() => {
105
+ renderer = createRenderer(<Probe />);
106
+ });
107
+ return { seen, renderer };
108
+ }
109
+
110
+ async function settle() {
111
+ await act(async () => {
112
+ await new Promise((resolve) => setTimeout(resolve, 0));
113
+ });
114
+ }
115
+
116
+ it("renders before the faces are registered, then flips to loaded", async () => {
117
+ const { fonts } = await freshModules();
118
+
119
+ const { seen } = renderHook(fonts.useUseSmileIDFonts);
120
+ expect(seen[0]).toBe(false);
121
+
122
+ await settle();
123
+
124
+ expect(seen.at(-1)).toBe(true);
125
+ });
126
+
127
+ it("never blocks the flow when font registration fails — warns and falls back", async () => {
128
+ const { fonts, expoFont } = await freshModules();
129
+ expoFont.loadAsync.mockRejectedValueOnce(new Error("asset registry down"));
130
+
131
+ const { seen } = renderHook(fonts.useUseSmileIDFonts);
132
+ await settle();
133
+
134
+ expect(seen.at(-1)).toBe(false);
135
+ expect(warn).toHaveBeenCalledWith(
136
+ expect.stringContaining("asset registry down"),
137
+ );
138
+ });
139
+
140
+ it("reports a non-Error rejection without crashing on `.message`", async () => {
141
+ const { fonts, expoFont } = await freshModules();
142
+ expoFont.loadAsync.mockRejectedValueOnce("native module missing");
143
+
144
+ renderHook(fonts.useUseSmileIDFonts);
145
+ await settle();
146
+
147
+ expect(warn).toHaveBeenCalledWith(
148
+ expect.stringContaining("native module missing"),
149
+ );
150
+ });
151
+
152
+ it("stops tracking the load after unmount", async () => {
153
+ const { fonts } = await freshModules();
154
+
155
+ const { renderer, seen } = renderHook(fonts.useUseSmileIDFonts);
156
+ expect(seen).toEqual([false]);
157
+ act(() => {
158
+ renderer.unmount();
159
+ });
160
+ await settle();
161
+
162
+ // The load resolves after the screen is gone; the cleanup latch keeps it
163
+ // from re-rendering the unmounted tree.
164
+ expect(seen).toEqual([false]);
165
+ });
166
+ });
@@ -1,37 +1,78 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
+ import {
4
+ GlobalPrivacyControl,
5
+ globalPrivacyControlFromWire,
6
+ GLOBAL_PRIVACY_CONTROL_WIRE_KEY,
7
+ } from "../../../core/models/global_privacy_control";
3
8
  import { getDefaultConsentState, isDeviceLocaleInEU } from "../locale_utils";
4
9
 
5
10
  describe("locale_utils", () => {
6
11
  it("EU locale (de-DE) is recognised as EU and default consent is false", () => {
7
12
  expect(isDeviceLocaleInEU("de-DE")).toBe(true);
8
- expect(getDefaultConsentState("de-DE")).toBe(false);
13
+ expect(getDefaultConsentState(GlobalPrivacyControl.OptIn, "de-DE")).toBe(
14
+ false,
15
+ );
9
16
  });
10
17
 
11
18
  it("non-EU locale (en-US) is not EU and default consent is true", () => {
12
19
  expect(isDeviceLocaleInEU("en-US")).toBe(false);
13
- expect(getDefaultConsentState("en-US")).toBe(true);
20
+ expect(getDefaultConsentState(GlobalPrivacyControl.OptIn, "en-US")).toBe(
21
+ true,
22
+ );
14
23
  });
15
24
 
16
25
  it("EU locale (fr-FR) is recognised as EU and default consent is false", () => {
17
26
  expect(isDeviceLocaleInEU("fr-FR")).toBe(true);
18
- expect(getDefaultConsentState("fr-FR")).toBe(false);
27
+ expect(getDefaultConsentState(GlobalPrivacyControl.OptIn, "fr-FR")).toBe(
28
+ false,
29
+ );
19
30
  });
20
31
 
21
32
  it("non-EU locale (en-NG) is not EU and default consent is true", () => {
22
33
  expect(isDeviceLocaleInEU("en-NG")).toBe(false);
23
- expect(getDefaultConsentState("en-NG")).toBe(true);
34
+ expect(getDefaultConsentState(GlobalPrivacyControl.OptIn, "en-NG")).toBe(
35
+ true,
36
+ );
24
37
  });
25
38
 
26
39
  it("bare language tag with no region falls back to non-EU", () => {
27
40
  // `"fr"` has no region segment — the BCP-47 split returns
28
41
  // undefined for the country, which never matches the EU set.
29
42
  expect(isDeviceLocaleInEU("fr")).toBe(false);
30
- expect(getDefaultConsentState("fr")).toBe(true);
43
+ expect(getDefaultConsentState(GlobalPrivacyControl.OptIn, "fr")).toBe(true);
31
44
  });
32
45
 
33
46
  it("empty locale string falls back to non-EU", () => {
34
47
  expect(isDeviceLocaleInEU("")).toBe(false);
35
48
  expect(getDefaultConsentState("")).toBe(true);
36
49
  });
50
+
51
+ describe("global privacy control", () => {
52
+ it("default posture forces opt-out even for EU locales", () => {
53
+ expect(getDefaultConsentState()).toBe(true);
54
+ expect(getDefaultConsentState(GlobalPrivacyControl.OptOut, "de-DE")).toBe(
55
+ true,
56
+ );
57
+ expect(getDefaultConsentState(GlobalPrivacyControl.OptOut, "en-NG")).toBe(
58
+ true,
59
+ );
60
+ });
61
+
62
+ it("parses wire values and defaults unknowns to opt-out", () => {
63
+ expect(globalPrivacyControlFromWire("opt-out")).toBe(
64
+ GlobalPrivacyControl.OptOut,
65
+ );
66
+ expect(globalPrivacyControlFromWire("opt-in")).toBe(
67
+ GlobalPrivacyControl.OptIn,
68
+ );
69
+ expect(globalPrivacyControlFromWire(null)).toBe(
70
+ GlobalPrivacyControl.OptOut,
71
+ );
72
+ expect(globalPrivacyControlFromWire("user_settings")).toBe(
73
+ GlobalPrivacyControl.OptOut,
74
+ );
75
+ expect(GLOBAL_PRIVACY_CONTROL_WIRE_KEY).toBe("global_privacy_control");
76
+ });
77
+ });
37
78
  });
@@ -1,3 +1,4 @@
1
+ import { GlobalPrivacyControl } from "../../core/models/global_privacy_control";
1
2
  const EU_COUNTRY_CODES: ReadonlySet<string> = new Set([
2
3
  "AT",
3
4
  "BE",
@@ -59,13 +60,21 @@ export function isDeviceLocaleInEU(
59
60
  }
60
61
 
61
62
  /**
62
- * Returns the default consent checkbox state based on the device locale.
63
+ * Returns the default consent checkbox state for the partner-declared
64
+ * `GlobalPrivacyControl` posture.
63
65
  *
64
- * - EU locales: unticked (`false`) to comply with GDPR opt-in.
65
- * - Non-EU locales: ticked (`true`).
66
+ * - `GlobalPrivacyControl.OptOut` (default): ticked (`true`) universally —
67
+ * the opt-out layout is enforced for every session and the device region
68
+ * is never consulted.
69
+ * - `GlobalPrivacyControl.OptIn`: legacy regional mechanics — EU locales
70
+ * start unticked (`false`, explicit opt-in per GDPR), everywhere else
71
+ * starts ticked (`true`).
66
72
  */
67
73
  export function getDefaultConsentState(
74
+ globalPrivacyControl: GlobalPrivacyControl = GlobalPrivacyControl.OptOut,
68
75
  locale: string = getDeviceLocale(),
69
76
  ): boolean {
70
- return !isDeviceLocaleInEU(locale);
77
+ return globalPrivacyControl === GlobalPrivacyControl.OptOut
78
+ ? true
79
+ : !isDeviceLocaleInEU(locale);
71
80
  }
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { makeFakeFaceAnalyzer } from "../fake_face_analyzer";
3
+
4
+ const scenario = {
5
+ name: "selfie-happy-path",
6
+ captureType: "selfie",
7
+ steps: [
8
+ { emit: "noFace", holdFrames: 2 },
9
+ { emit: "faceCentered", holdFrames: 3 },
10
+ { emit: "satisfied" },
11
+ ],
12
+ } as const;
13
+
14
+ describe("FakeFaceAnalyzer", () => {
15
+ it("replays holdFrames per step and repeats the final step indefinitely", () => {
16
+ const fake = makeFakeFaceAnalyzer(scenario);
17
+ const emits = Array.from({ length: 9 }, () => fake.nextEmit());
18
+ expect(emits).toEqual([
19
+ "noFace",
20
+ "noFace",
21
+ "faceCentered",
22
+ "faceCentered",
23
+ "faceCentered",
24
+ "satisfied",
25
+ "satisfied",
26
+ "satisfied",
27
+ "satisfied",
28
+ ]);
29
+ });
30
+
31
+ it("records the JS lifecycle calls", async () => {
32
+ const fake = makeFakeFaceAnalyzer(scenario);
33
+ await fake.configure();
34
+ await fake.reset();
35
+ await fake.flush();
36
+ await fake.close();
37
+ expect(fake.configureCallCount).toBe(1);
38
+ expect(fake.resetCallCount).toBe(1);
39
+ expect(fake.flushCallCount).toBe(1);
40
+ expect(fake.closeCallCount).toBe(1);
41
+ });
42
+ });
@@ -0,0 +1,110 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { describe, expect, it } from "vitest";
5
+ import {
6
+ expectSubsetInOrder,
7
+ parseScenarioExpectations,
8
+ } from "../scenario_expectations";
9
+
10
+ // Read the canonical shared scenario at run time (see the capture-screen test
11
+ // for the same rationale) so a drift in the shared `expected` block is caught
12
+ // here per-PR rather than silently diverging.
13
+ const scenarioDir = dirname(fileURLToPath(import.meta.url));
14
+ const selfieScenario = JSON.parse(
15
+ readFileSync(
16
+ join(scenarioDir, "../../../../e2e/scenarios/selfie-happy-path.json"),
17
+ "utf8",
18
+ ),
19
+ ) as unknown;
20
+
21
+ const agentModeScenario = JSON.parse(
22
+ readFileSync(
23
+ join(scenarioDir, "../../../../e2e/scenarios/selfie-agent-mode.json"),
24
+ "utf8",
25
+ ),
26
+ ) as unknown;
27
+
28
+ describe("parseScenarioExpectations", () => {
29
+ // Nothing is relaxed for the agent, so the shared script must carry the same
30
+ // terminal contract as the front-camera happy path.
31
+ it("gives the shared agent-mode scenario the same terminal contract as the happy path", () => {
32
+ const agentMode = parseScenarioExpectations(agentModeScenario);
33
+ const happyPath = parseScenarioExpectations(selfieScenario);
34
+ expect(agentMode.screens).toEqual(happyPath.screens);
35
+ expect(agentMode.events).toEqual(happyPath.events);
36
+ expect(agentMode.terminal).toBe("submitted");
37
+ });
38
+
39
+ it("surfaces the shared selfie scenario's expected screens/events/terminal", () => {
40
+ const expected = parseScenarioExpectations(selfieScenario);
41
+ expect(expected.screens).toEqual([
42
+ "consent",
43
+ "instructions",
44
+ "capture",
45
+ "preview",
46
+ "processing",
47
+ ]);
48
+ expect(expected.events).toEqual([
49
+ "flow_started",
50
+ "consent_captured",
51
+ "selfie_captured",
52
+ "job_submitted",
53
+ "flow_completed",
54
+ ]);
55
+ expect(expected.terminal).toBe("submitted");
56
+ });
57
+
58
+ it("defaults to empty arrays when the block is absent", () => {
59
+ const expected = parseScenarioExpectations({ name: "x" });
60
+ expect(expected.screens).toEqual([]);
61
+ expect(expected.events).toEqual([]);
62
+ });
63
+
64
+ it("rejects a malformed expected block", () => {
65
+ expect(() =>
66
+ parseScenarioExpectations({ expected: { screens: [1, 2] } }),
67
+ ).toThrow(/must be an array of strings/);
68
+ expect(() =>
69
+ parseScenarioExpectations({ expected: { terminal: "exploded" } }),
70
+ ).toThrow(/terminal must be one of/);
71
+ });
72
+ });
73
+
74
+ describe("expectSubsetInOrder", () => {
75
+ it("passes when every expected item appears in order (interleaving allowed)", () => {
76
+ const { screens, events } = parseScenarioExpectations(selfieScenario);
77
+ // Recorded stream may carry extra, unlisted events between the required
78
+ // ones — the contract is subset-in-order, not exact match.
79
+ const recordedEvents = [
80
+ "flow_started",
81
+ "screen_viewed",
82
+ "consent_captured",
83
+ "screen_viewed",
84
+ "selfie_captured",
85
+ "image_quality_ok",
86
+ "job_submitted",
87
+ "flow_completed",
88
+ ];
89
+ expect(() => expectSubsetInOrder(recordedEvents, events)).not.toThrow();
90
+ expect(() => expectSubsetInOrder([...screens], screens)).not.toThrow();
91
+ });
92
+
93
+ it("throws when an expected item is missing", () => {
94
+ expect(() =>
95
+ expectSubsetInOrder(
96
+ ["flow_started", "flow_completed"],
97
+ ["flow_started", "job_submitted", "flow_completed"],
98
+ ),
99
+ ).toThrow(/job_submitted/);
100
+ });
101
+
102
+ it("throws when expected items appear out of order", () => {
103
+ expect(() =>
104
+ expectSubsetInOrder(
105
+ ["flow_completed", "flow_started"],
106
+ ["flow_started", "flow_completed"],
107
+ ),
108
+ ).toThrow(/to appear in order/);
109
+ });
110
+ });
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parseScriptedScenario } from "../scenario_loader";
3
+
4
+ describe("parseScriptedScenario", () => {
5
+ const valid = {
6
+ name: "selfie-happy-path",
7
+ captureType: "selfie",
8
+ steps: [
9
+ { emit: "noFace", holdFrames: 10 },
10
+ { emit: "faceCentered", holdFrames: 20 },
11
+ { emit: "satisfied" },
12
+ ],
13
+ // An `expected` block may ride alongside; the loader ignores extra keys.
14
+ expected: { screens: [], events: [], terminal: "submitted" },
15
+ };
16
+
17
+ it("returns the typed scenario for a valid shape", () => {
18
+ const scenario = parseScriptedScenario(valid);
19
+ expect(scenario.name).toBe("selfie-happy-path");
20
+ expect(scenario.captureType).toBe("selfie");
21
+ expect(scenario.steps).toHaveLength(3);
22
+ expect(scenario.steps[2]).toEqual({ emit: "satisfied" });
23
+ });
24
+
25
+ it("rejects an unknown emit (vocabulary drift)", () => {
26
+ expect(() =>
27
+ parseScriptedScenario({
28
+ name: "x",
29
+ captureType: "selfie",
30
+ steps: [{ emit: "wink" }],
31
+ }),
32
+ ).toThrow(/not a known emission/);
33
+ });
34
+
35
+ it("rejects an unknown captureType", () => {
36
+ expect(() =>
37
+ parseScriptedScenario({
38
+ name: "x",
39
+ captureType: "iris",
40
+ steps: [{ emit: "noFace" }],
41
+ }),
42
+ ).toThrow(/captureType/);
43
+ });
44
+
45
+ it("rejects empty steps", () => {
46
+ expect(() =>
47
+ parseScriptedScenario({ name: "x", captureType: "selfie", steps: [] }),
48
+ ).toThrow(/non-empty/);
49
+ });
50
+
51
+ it("rejects a non-integer holdFrames", () => {
52
+ expect(() =>
53
+ parseScriptedScenario({
54
+ name: "x",
55
+ captureType: "selfie",
56
+ steps: [{ emit: "noFace", holdFrames: 1.5 }],
57
+ }),
58
+ ).toThrow(/holdFrames/);
59
+ });
60
+
61
+ it("rejects a non-object input", () => {
62
+ expect(() => parseScriptedScenario("nope")).toThrow(/JSON object/);
63
+ });
64
+ });
@@ -0,0 +1,82 @@
1
+ import type { FaceAnalyzer } from "@smileid/usesmileid_platform_interface";
2
+ import { AnalyzerFactoryKey } from "@smileid/usesmileid_bridge";
3
+
4
+ /**
5
+ * Deterministic fake face analyzer for scripted-scenario E2E runs. Exported
6
+ * only behind the `@smileid/usesmileid/testing` subpath so it cannot leak into
7
+ * partner production bundles.
8
+ *
9
+ * Per-frame detection runs in the provider's native frame-processor plugin
10
+ * (pixels never cross into JS), so `frameProcessorPluginName` points at a
11
+ * plugin that does not exist — the capture screen surfaces that as its
12
+ * camera-unavailable state. This fake covers the DSL registration seam and the
13
+ * capture-free flows; capture progression is driven via `onResult` instead.
14
+ */
15
+ export interface ScriptedScenarioStep {
16
+ readonly emit: string;
17
+ readonly holdFrames?: number;
18
+ }
19
+
20
+ export interface ScriptedScenario {
21
+ readonly name: string;
22
+ readonly captureType: string;
23
+ readonly steps: readonly ScriptedScenarioStep[];
24
+ }
25
+
26
+ export class FakeFaceAnalyzer implements FaceAnalyzer {
27
+ readonly kind = "face" as const;
28
+ readonly key: AnalyzerFactoryKey = AnalyzerFactoryKey.MLKitFace;
29
+ readonly frameProcessorPluginName = "e2e-fake-face";
30
+
31
+ configureCallCount = 0;
32
+ resetCallCount = 0;
33
+ flushCallCount = 0;
34
+ closeCallCount = 0;
35
+
36
+ private stepIndex = 0;
37
+ private framesLeftInStep: number;
38
+
39
+ constructor(private readonly scenario: ScriptedScenario) {
40
+ this.framesLeftInStep = scenario.steps[0]?.holdFrames ?? 1;
41
+ }
42
+
43
+ /** Frame-by-frame cursor; the final step repeats indefinitely. */
44
+ nextEmit(): string {
45
+ const steps = this.scenario.steps;
46
+ const step = steps[this.stepIndex];
47
+ if (step === undefined) return "noFace";
48
+ if (this.stepIndex === steps.length - 1) return step.emit;
49
+ this.framesLeftInStep -= 1;
50
+ if (this.framesLeftInStep <= 0) {
51
+ this.stepIndex += 1;
52
+ this.framesLeftInStep = steps[this.stepIndex]?.holdFrames ?? 1;
53
+ }
54
+ return step.emit;
55
+ }
56
+
57
+ configure(): Promise<void> {
58
+ this.configureCallCount += 1;
59
+ return Promise.resolve();
60
+ }
61
+
62
+ reset(): Promise<void> {
63
+ this.resetCallCount += 1;
64
+ return Promise.resolve();
65
+ }
66
+
67
+ flush(): Promise<void> {
68
+ this.flushCallCount += 1;
69
+ return Promise.resolve();
70
+ }
71
+
72
+ close(): Promise<void> {
73
+ this.closeCallCount += 1;
74
+ return Promise.resolve();
75
+ }
76
+ }
77
+
78
+ export function makeFakeFaceAnalyzer(
79
+ scenario: ScriptedScenario,
80
+ ): FakeFaceAnalyzer {
81
+ return new FakeFaceAnalyzer(scenario);
82
+ }
@@ -0,0 +1,15 @@
1
+ // Test-support entry — exported behind the `@smileid/usesmileid/testing`
2
+ // subpath only. Nothing here may be re-exported from the main index.
3
+ export {
4
+ FakeFaceAnalyzer,
5
+ makeFakeFaceAnalyzer,
6
+ type ScriptedScenario,
7
+ type ScriptedScenarioStep,
8
+ } from "./fake_face_analyzer";
9
+ export { scriptedFaceScanResults } from "./scripted_scan_results";
10
+ export { parseScriptedScenario } from "./scenario_loader";
11
+ export {
12
+ type ScenarioExpectations,
13
+ parseScenarioExpectations,
14
+ expectSubsetInOrder,
15
+ } from "./scenario_expectations";
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Capture-scoped expectations from the shared `e2e-scenarios` schema's `expected`
3
+ * block (screen + analytics-event names, terminal state). Framework-agnostic (no
4
+ * vitest/jest import) so a partner app's own e2e runner can reuse it.
5
+ */
6
+ export interface ScenarioExpectations {
7
+ /** Ordered `screen_viewed` sequence for the canonical consent-first flow. */
8
+ readonly screens: readonly string[];
9
+ /** Ordered analytics event types that must fire (subset-in-order). */
10
+ readonly events: readonly string[];
11
+ /** How the capture session ends. */
12
+ readonly terminal?: "submitted" | "timeout" | "unsatisfied";
13
+ }
14
+
15
+ const TERMINALS: ReadonlySet<string> = new Set([
16
+ "submitted",
17
+ "timeout",
18
+ "unsatisfied",
19
+ ]);
20
+
21
+ /**
22
+ * Extracts and validates the `expected` block from an already-parsed shared
23
+ * scenario JSON object. Returns empty screen/event arrays when the block is
24
+ * absent (older scenarios), and throws on a malformed block so drift fails
25
+ * loudly rather than silently weakening the contract.
26
+ */
27
+ export function parseScenarioExpectations(raw: unknown): ScenarioExpectations {
28
+ if (typeof raw !== "object" || raw === null) {
29
+ throw new Error("scenario must be a JSON object");
30
+ }
31
+ const expected = (raw as Record<string, unknown>).expected;
32
+ if (expected === undefined) {
33
+ return { screens: [], events: [] };
34
+ }
35
+ if (typeof expected !== "object" || expected === null) {
36
+ throw new Error("scenario.expected must be an object when present");
37
+ }
38
+ const record = expected as Record<string, unknown>;
39
+ const screens = readStringArray(record.screens, "scenario.expected.screens");
40
+ const events = readStringArray(record.events, "scenario.expected.events");
41
+ const terminal = record.terminal;
42
+ if (terminal === undefined) {
43
+ return { screens, events };
44
+ }
45
+ if (typeof terminal !== "string" || !TERMINALS.has(terminal)) {
46
+ throw new Error(
47
+ `scenario.expected.terminal must be one of ${[...TERMINALS].join(", ")}, got ${String(terminal)}`,
48
+ );
49
+ }
50
+ return {
51
+ screens,
52
+ events,
53
+ terminal: terminal as "submitted" | "timeout" | "unsatisfied",
54
+ };
55
+ }
56
+
57
+ function readStringArray(value: unknown, label: string): readonly string[] {
58
+ if (value === undefined) return [];
59
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
60
+ throw new Error(`${label} must be an array of strings`);
61
+ }
62
+ return value as string[];
63
+ }
64
+
65
+ /**
66
+ * Asserts that every element of [expected] appears in [recorded] in the same
67
+ * relative order (a minimal ordered-subset match — unlisted items may
68
+ * interleave, exact-match is deliberately NOT required). Throws a descriptive
69
+ * Error on failure so it works under any test runner.
70
+ */
71
+ export function expectSubsetInOrder(
72
+ recorded: readonly string[],
73
+ expected: readonly string[],
74
+ label = "sequence",
75
+ ): void {
76
+ let cursor = 0;
77
+ for (const item of recorded) {
78
+ if (cursor < expected.length && item === expected[cursor]) cursor += 1;
79
+ }
80
+ if (cursor !== expected.length) {
81
+ throw new Error(
82
+ `${label}: expected [${expected
83
+ .slice(cursor)
84
+ .join(", ")}] to appear in order within [${recorded.join(", ")}]`,
85
+ );
86
+ }
87
+ }