@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,338 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import * as React from "react";
3
+ import { create as createRenderer, act } from "react-test-renderer";
4
+
5
+ // `vi.mock` is hoisted above the imports, so anything its factory closes over
6
+ // must be created with `vi.hoisted` or it is still in the TDZ when the factory
7
+ // runs.
8
+ const { screenProps } = vi.hoisted(() => ({
9
+ screenProps: { current: {} as Record<string, unknown> },
10
+ }));
11
+
12
+ // Stand in for the leaf screen so these tests exercise the orchestration
13
+ // (submission kick-off, state mapping, exit routing) rather than re-testing
14
+ // the processing UI, which has its own component suite.
15
+ vi.mock("../../../screen/use_smile_id_processing_screen", () => ({
16
+ UseSmileIDProcessingScreen: (props: Record<string, unknown>) => {
17
+ screenProps.current = props;
18
+ return null;
19
+ },
20
+ }));
21
+
22
+ import { OrchestratedProcessingScreen } from "../orchestrated_processing_screen";
23
+ import { FlowNavigationProvider } from "../../flow_navigation_context";
24
+ import {
25
+ NetworkServerException,
26
+ NetworkTransportException,
27
+ NetworkUnknownException,
28
+ ProcessingCancelledException,
29
+ } from "../../../../core/exception";
30
+ import { JobType } from "../../../../core/models/job_type";
31
+ import { ProcessingConfigBuilder } from "../../dsl/builder/screens_builder";
32
+ import type { FlowNavigationManager } from "../../flow_navigation_manager";
33
+ import type { JobSubmissionState } from "../../job_submission_state";
34
+
35
+ type Fn = ReturnType<typeof vi.fn>;
36
+
37
+ type FakeManager = FlowNavigationManager & {
38
+ beginSubmissionIfNeeded: Fn;
39
+ retrySubmission: Fn;
40
+ navigateToNext: Fn;
41
+ exit: Fn;
42
+ };
43
+
44
+ type ManagerOverrides = {
45
+ readonly jobType?: number;
46
+ readonly submissionState?: JobSubmissionState;
47
+ readonly frontDocumentPath?: string;
48
+ readonly selfiePath?: string;
49
+ readonly documentFrameAspectRatio?: number;
50
+ };
51
+
52
+ function makeManager(overrides: ManagerOverrides = {}): FakeManager {
53
+ return {
54
+ beginSubmissionIfNeeded: vi.fn(),
55
+ retrySubmission: vi.fn(),
56
+ navigateToNext: vi.fn(),
57
+ navigateBack: vi.fn(),
58
+ exit: vi.fn(),
59
+ jobSubmissionState: overrides.submissionState ?? { kind: "notAttempted" },
60
+ documentFrameAspectRatio: overrides.documentFrameAspectRatio,
61
+ captured: {
62
+ ...(overrides.frontDocumentPath !== undefined
63
+ ? { frontDocumentPath: overrides.frontDocumentPath }
64
+ : {}),
65
+ ...(overrides.selfiePath !== undefined
66
+ ? { selfiePath: overrides.selfiePath }
67
+ : {}),
68
+ },
69
+ configuration: {
70
+ network:
71
+ overrides.jobType !== undefined
72
+ ? { config: { jobType: overrides.jobType } }
73
+ : undefined,
74
+ },
75
+ } as unknown as FakeManager;
76
+ }
77
+
78
+ function makeConfiguration(
79
+ overrides: Partial<ProcessingConfigBuilder> = {},
80
+ ): ProcessingConfigBuilder {
81
+ return Object.assign(new ProcessingConfigBuilder(), overrides);
82
+ }
83
+
84
+ function renderWith(
85
+ manager: FlowNavigationManager,
86
+ configuration: ProcessingConfigBuilder = makeConfiguration(),
87
+ ) {
88
+ act(() => {
89
+ createRenderer(
90
+ <FlowNavigationProvider manager={manager}>
91
+ <OrchestratedProcessingScreen configuration={configuration} />
92
+ </FlowNavigationProvider>,
93
+ );
94
+ });
95
+ }
96
+
97
+ beforeEach(() => {
98
+ screenProps.current = {};
99
+ });
100
+
101
+ describe("OrchestratedProcessingScreen — submission lifecycle", () => {
102
+ it("kicks off submission exactly once on mount", () => {
103
+ const manager = makeManager();
104
+ renderWith(manager);
105
+
106
+ expect(manager.beginSubmissionIfNeeded).toHaveBeenCalledTimes(1);
107
+ });
108
+
109
+ it("retry re-fires submission rather than navigating", () => {
110
+ const manager = makeManager({
111
+ submissionState: { kind: "failure", error: new Error("network down") },
112
+ });
113
+ renderWith(manager);
114
+
115
+ (screenProps.current.onRetry as () => void)();
116
+
117
+ expect(manager.retrySubmission).toHaveBeenCalledTimes(1);
118
+ expect(manager.navigateToNext).not.toHaveBeenCalled();
119
+ });
120
+
121
+ it("continue advances the flow", () => {
122
+ const manager = makeManager();
123
+ renderWith(manager);
124
+
125
+ (screenProps.current.onContinue as () => void)();
126
+
127
+ expect(manager.navigateToNext).toHaveBeenCalledTimes(1);
128
+ });
129
+
130
+ it("exit ends the flow with a typed ProcessingCancelledException", () => {
131
+ const manager = makeManager();
132
+ renderWith(manager);
133
+
134
+ (screenProps.current.onExit as () => void)();
135
+
136
+ const [[reason]] = manager.exit.mock.calls as [
137
+ [{ kind: string; error: unknown }],
138
+ ];
139
+ expect(reason.kind).toBe("failed");
140
+ expect(reason.error).toBeInstanceOf(ProcessingCancelledException);
141
+ expect(manager.navigateToNext).not.toHaveBeenCalled();
142
+ });
143
+
144
+ it("exit after a failed submission delivers that failure, not a cancellation", () => {
145
+ const submissionError = new Error("401 — invalid API key");
146
+ const manager = makeManager({
147
+ submissionState: { kind: "failure", error: submissionError },
148
+ });
149
+ renderWith(manager);
150
+
151
+ (screenProps.current.onExit as () => void)();
152
+
153
+ const [[reason]] = manager.exit.mock.calls as [
154
+ [{ kind: string; error: unknown }],
155
+ ];
156
+ expect(reason.kind).toBe("failed");
157
+ expect(reason.error).toBe(submissionError);
158
+ });
159
+
160
+ it("exit while the job is still in flight is a cancellation", () => {
161
+ const manager = makeManager({ submissionState: { kind: "loading" } });
162
+ renderWith(manager);
163
+
164
+ (screenProps.current.onExit as () => void)();
165
+
166
+ const [[reason]] = manager.exit.mock.calls as [
167
+ [{ kind: string; error: unknown }],
168
+ ];
169
+ expect(reason.error).toBeInstanceOf(ProcessingCancelledException);
170
+ });
171
+ });
172
+
173
+ describe("OrchestratedProcessingScreen — submission state mapping", () => {
174
+ it("shows loading during the pre-submission window so the card is never empty", () => {
175
+ renderWith(makeManager({ submissionState: { kind: "notAttempted" } }));
176
+ expect(screenProps.current.state).toEqual({ kind: "loading" });
177
+ });
178
+
179
+ it("shows loading while the job is in flight", () => {
180
+ renderWith(makeManager({ submissionState: { kind: "loading" } }));
181
+ expect(screenProps.current.state).toEqual({ kind: "loading" });
182
+ });
183
+
184
+ it("surfaces the server's message on success", () => {
185
+ renderWith(
186
+ makeManager({
187
+ submissionState: {
188
+ kind: "success",
189
+ response: {
190
+ userId: "user-1",
191
+ jobId: "job-1",
192
+ status: "approved",
193
+ message: "Job approved",
194
+ },
195
+ },
196
+ }),
197
+ );
198
+
199
+ expect(screenProps.current.state).toEqual({
200
+ kind: "success",
201
+ message: "Job approved",
202
+ });
203
+ });
204
+
205
+ it("surfaces the server's own text on a server failure", () => {
206
+ renderWith(
207
+ makeManager({
208
+ submissionState: {
209
+ kind: "failure",
210
+ error: new NetworkServerException({
211
+ statusCode: 401,
212
+ message: "Invalid token",
213
+ }),
214
+ },
215
+ }),
216
+ );
217
+
218
+ expect(screenProps.current.state).toEqual({
219
+ kind: "failed",
220
+ errorMessage: "Invalid token",
221
+ });
222
+ });
223
+
224
+ it("shows the no-connection copy on a transport failure", () => {
225
+ renderWith(
226
+ makeManager({
227
+ submissionState: {
228
+ kind: "failure",
229
+ error: new NetworkTransportException({ cause: new Error("ECONNREFUSED") }),
230
+ },
231
+ }),
232
+ );
233
+
234
+ expect(screenProps.current.state).toEqual({
235
+ kind: "failed",
236
+ errorMessage: "No internet connection.",
237
+ });
238
+ });
239
+
240
+ it("shows the generic copy for unknown network failures", () => {
241
+ renderWith(
242
+ makeManager({
243
+ submissionState: {
244
+ kind: "failure",
245
+ error: new NetworkUnknownException({
246
+ cause: new Error("ENOENT /data/user/0/…/selfie.jpg"),
247
+ }),
248
+ },
249
+ }),
250
+ );
251
+
252
+ expect(screenProps.current.state).toEqual({
253
+ kind: "failed",
254
+ errorMessage: "Something went wrong. Try again.",
255
+ });
256
+ });
257
+
258
+ it("surfaces the error message on other failures", () => {
259
+ renderWith(
260
+ makeManager({
261
+ submissionState: { kind: "failure", error: new Error("upload failed") },
262
+ }),
263
+ );
264
+
265
+ expect(screenProps.current.state).toEqual({
266
+ kind: "failed",
267
+ errorMessage: "upload failed",
268
+ });
269
+ });
270
+ });
271
+
272
+ describe("OrchestratedProcessingScreen — artifact rendering", () => {
273
+ it("renders the captured front document for a document job", () => {
274
+ renderWith(
275
+ makeManager({
276
+ jobType: JobType.documentVerification,
277
+ frontDocumentPath: "file:///front.jpg",
278
+ documentFrameAspectRatio: 1.586,
279
+ }),
280
+ );
281
+
282
+ expect(screenProps.current.documentImageUri).toBe("file:///front.jpg");
283
+ expect(screenProps.current.documentAspectRatio).toBeCloseTo(1.586, 5);
284
+ expect(screenProps.current.showFaceOval).toBe(true);
285
+ });
286
+
287
+ it("omits the document aspect ratio when the manager has none", () => {
288
+ renderWith(
289
+ makeManager({
290
+ jobType: JobType.documentVerification,
291
+ frontDocumentPath: "file:///front.jpg",
292
+ }),
293
+ );
294
+
295
+ expect(screenProps.current.documentImageUri).toBe("file:///front.jpg");
296
+ expect("documentAspectRatio" in screenProps.current).toBe(false);
297
+ });
298
+
299
+ it("ignores a captured document on a selfie-only job", () => {
300
+ renderWith(
301
+ makeManager({
302
+ jobType: JobType.smartSelfieEnrollment,
303
+ frontDocumentPath: "file:///front.jpg",
304
+ selfiePath: "file:///selfie.jpg",
305
+ }),
306
+ );
307
+
308
+ expect("documentImageUri" in screenProps.current).toBe(false);
309
+ expect(screenProps.current.capturedSelfiePath).toBe("file:///selfie.jpg");
310
+ expect(screenProps.current.showFaceOval).toBe(true);
311
+ });
312
+
313
+ it("hides the face oval for a job that captures neither", () => {
314
+ renderWith(makeManager({ jobType: JobType.enhancedKyc }));
315
+
316
+ expect(screenProps.current.showFaceOval).toBe(false);
317
+ expect("capturedSelfiePath" in screenProps.current).toBe(false);
318
+ });
319
+
320
+ it("treats a flow with no network config as the unknown job type", () => {
321
+ renderWith(makeManager());
322
+
323
+ expect(screenProps.current.showFaceOval).toBe(false);
324
+ });
325
+
326
+ it("forwards the partner's progress and attribution switches", () => {
327
+ renderWith(
328
+ makeManager(),
329
+ makeConfiguration({
330
+ showProgressPercentage: false,
331
+ showAttribution: false,
332
+ }),
333
+ );
334
+
335
+ expect(screenProps.current.showProgressPercentage).toBe(false);
336
+ expect(screenProps.current.showAttribution).toBe(false);
337
+ });
338
+ });
@@ -75,10 +75,15 @@ export function OrchestratedCaptureScreen({
75
75
  // selfie to portrait. The flip interstitial is a portrait instruction screen,
76
76
  // so it must NOT inherit the document's landscape lock — force portrait while
77
77
  // it shows, then revert to the document orientation for the back capture.
78
+ const exitOrientations =
79
+ navigationManager.configuration.config?.exitOrientations;
78
80
  useCaptureOrientationLock(
79
81
  captureType === CaptureType.document && !showBackInstructions
80
82
  ? (documentType?.orientation ?? "Landscape")
81
83
  : "Portrait",
84
+ {
85
+ ...(exitOrientations !== undefined ? { exitLock: exitOrientations } : {}),
86
+ },
82
87
  );
83
88
 
84
89
  // Selfie also pushes the registry-built wire FaceConfig; documents have no
@@ -184,6 +189,7 @@ export function OrchestratedCaptureScreen({
184
189
  enableEnhancedLiveness={
185
190
  configuration.selfieConfig?.enableEnhancedLiveness ?? false
186
191
  }
192
+ allowAgentMode={configuration.selfieConfig?.allowAgentMode ?? false}
187
193
  {...(captureType === CaptureType.document && documentConfig
188
194
  ? {
189
195
  // Type ratio → partner override → orientation default; matches
@@ -261,9 +267,18 @@ export function OrchestratedCaptureScreen({
261
267
  ...(fingerprint ? { fingerprint } : {}),
262
268
  }),
263
269
  )
264
- .then(() =>
265
- SmileExifTagger.finalize(primary, imageTypeForCapture),
266
- )
270
+ .then(async () => {
271
+ await SmileExifTagger.finalize(primary, imageTypeForCapture);
272
+ // Liveness frames ship in the same submission, so they need the tag too.
273
+ await Promise.all(
274
+ livenessUrls.map((livenessUrl) =>
275
+ SmileExifTagger.finalize(livenessUrl, ImageType.Liveness),
276
+ ),
277
+ );
278
+ })
279
+ // Tagging is fail-open: a rejection must not become an unhandled
280
+ // rejection, and navigation still runs in `finally`.
281
+ .catch(() => undefined)
267
282
  .finally(async () => {
268
283
  if (captureType === CaptureType.selfie) {
269
284
  navigationManager.setCapturedSelfie(primary, livenessUrls);
@@ -2,6 +2,7 @@ import type { ConsentConfigBuilder } from "../dsl/builder/screens_builder";
2
2
  import type { ConsentInformation } from "../../../core/models/params";
3
3
  import { UseSmileIDCrashReporting } from "@smileid/usesmileid_bridge";
4
4
  import { ConsentDeniedException } from "../../../core/exception";
5
+ import { GlobalPrivacyControl } from "../../../core/models/global_privacy_control";
5
6
  import { UseSmileIDConsentScreen } from "../../screen/use_smile_id_consent_screen";
6
7
  import { UseSmileIDOrchestratedScreenHost } from "./use_smile_id_orchestrated_screen_host";
7
8
 
@@ -25,6 +26,10 @@ export function OrchestratedConsentScreen({
25
26
  content={({ navigationManager, onNavigateNext }) => (
26
27
  <UseSmileIDConsentScreen
27
28
  partnerName={configuration.partnerName}
29
+ globalPrivacyControl={
30
+ navigationManager.configuration.config?.globalPrivacyControl ??
31
+ GlobalPrivacyControl.OptOut
32
+ }
28
33
  {...(configuration.partnerIcon !== undefined
29
34
  ? { partnerIcon: configuration.partnerIcon }
30
35
  : {})}
@@ -50,18 +50,6 @@ export function OrchestratedPreviewScreen({
50
50
  {...(navigationManager.captured.selfiePath !== undefined
51
51
  ? { capturedSelfiePath: navigationManager.captured.selfiePath }
52
52
  : {})}
53
- {...(configuration.continueButton
54
- ? {
55
- continueButton: ({ onClick }: { onClick: () => void }) =>
56
- configuration.continueButton?.({ onClick, enabled: true }),
57
- }
58
- : {})}
59
- {...(configuration.cancelButton
60
- ? {
61
- cancelButton: ({ onClick }: { onClick: () => void }) =>
62
- configuration.cancelButton?.({ onClick, enabled: true }),
63
- }
64
- : {})}
65
53
  onAccept={onNavigateNext}
66
54
  onReject={() => {
67
55
  if (configuration.allowRetake) onBack();
@@ -7,7 +7,15 @@ import {
7
7
  type ProcessingState,
8
8
  } from "../../screen/use_smile_id_processing_screen";
9
9
  import { UseSmileIDOrchestratedScreenHost } from "./use_smile_id_orchestrated_screen_host";
10
- import { ProcessingCancelledException } from "../../../core/exception";
10
+ import {
11
+ NetworkResponseParseException,
12
+ NetworkTimeoutException,
13
+ NetworkTransportException,
14
+ NetworkUnknownException,
15
+ ProcessingCancelledException,
16
+ } from "../../../core/exception";
17
+ import { useUseSmileIDLocalizations } from "../../../l10n/use_smile_id_localizations_provider";
18
+ import type { UseSmileIDLocalizations } from "../../../l10n/use_smile_id_localizations";
11
19
  import {
12
20
  JobType,
13
21
  requiresDocument,
@@ -68,9 +76,11 @@ function ProcessingScreenBody(props: ProcessingScreenBodyProps) {
68
76
  : undefined;
69
77
  const documentAspectRatio = navigationManager.documentFrameAspectRatio;
70
78
 
79
+ const loc = useUseSmileIDLocalizations();
80
+
71
81
  return (
72
82
  <UseSmileIDProcessingScreen
73
- state={mapToProcessingState(navigationManager.jobSubmissionState)}
83
+ state={mapToProcessingState(navigationManager.jobSubmissionState, loc)}
74
84
  showFaceOval={requiresSelfie(jobType) || requiresDocument(jobType)}
75
85
  showProgressPercentage={props.processingConfig.showProgressPercentage}
76
86
  showAttribution={props.processingConfig.showAttribution}
@@ -86,19 +96,29 @@ function ProcessingScreenBody(props: ProcessingScreenBodyProps) {
86
96
  onExit={() =>
87
97
  navigationManager.exit({
88
98
  kind: "failed",
89
- error: new ProcessingCancelledException(),
99
+ error: exitError(navigationManager.jobSubmissionState),
90
100
  })
91
101
  }
92
102
  />
93
103
  );
94
104
  }
95
105
 
106
+ /** The error the flow exits with when the user leaves the processing screen; an already-failed submission is delivered as its own failure. */
107
+ function exitError(state: JobSubmissionState): Error {
108
+ return state.kind === "failure"
109
+ ? state.error
110
+ : new ProcessingCancelledException();
111
+ }
112
+
96
113
  /**
97
114
  * Translate `JobSubmissionState` to the screen's `ProcessingState`.
98
115
  * `notAttempted` maps to `loading` so the brief pre-submission window
99
116
  * doesn't render an empty card.
100
117
  */
101
- function mapToProcessingState(state: JobSubmissionState): ProcessingState {
118
+ function mapToProcessingState(
119
+ state: JobSubmissionState,
120
+ loc: UseSmileIDLocalizations,
121
+ ): ProcessingState {
102
122
  switch (state.kind) {
103
123
  case "notAttempted":
104
124
  case "loading":
@@ -106,6 +126,23 @@ function mapToProcessingState(state: JobSubmissionState): ProcessingState {
106
126
  case "success":
107
127
  return { kind: "success", message: state.response.message };
108
128
  case "failure":
109
- return { kind: "failed", errorMessage: state.error.message };
129
+ return { kind: "failed", errorMessage: userFacingMessage(state.error, loc) };
130
+ }
131
+ }
132
+
133
+ /** Transport/timeout and parse/unknown network failures carry technical text — every other failure keeps its message. */
134
+ function userFacingMessage(error: Error, loc: UseSmileIDLocalizations): string {
135
+ if (
136
+ error instanceof NetworkTransportException ||
137
+ error instanceof NetworkTimeoutException
138
+ ) {
139
+ return loc.siProcessingErrorNoConnection;
140
+ }
141
+ if (
142
+ error instanceof NetworkResponseParseException ||
143
+ error instanceof NetworkUnknownException
144
+ ) {
145
+ return loc.siProcessingErrorGeneric;
110
146
  }
147
+ return error.message;
111
148
  }
@@ -1,5 +1,5 @@
1
- import { useEffect, useMemo, useReducer, useRef } from "react";
2
- import { Text, useColorScheme, View } from "react-native";
1
+ import { useEffect, useMemo, useReducer, useRef, useState } from "react";
2
+ import { AppState, Text, useColorScheme, View } from "react-native";
3
3
  import type { FlowConfiguration } from "./config/flow_configuration";
4
4
  import type { FlowBuildResult } from "./dsl/flow_build_result";
5
5
  import {
@@ -39,8 +39,8 @@ import {
39
39
  * whether the partner constructs `network.retry` explicitly or not.
40
40
  */
41
41
  const DEFAULT_RETRY: RetryConfig = Object.freeze({
42
- maxAttempts: 3,
43
- strategy: RetryStrategy.exponential,
42
+ maxAttempts: 1,
43
+ strategy: RetryStrategy.none,
44
44
  initialDelay: 1,
45
45
  multiplier: 2,
46
46
  maxDelay: 32,
@@ -102,6 +102,9 @@ export type FlowSuccessRendererProps = {
102
102
  readonly configuration: FlowConfiguration;
103
103
  };
104
104
 
105
+ /** Ceiling on the first-frame font wait — past it the flow renders regardless. */
106
+ const FONT_REGISTRATION_TIMEOUT_MS = 1000;
107
+
105
108
  /**
106
109
  * Walks the partner through the configured screen list, dispatches
107
110
  * frames to the chosen analyzer, submits the resulting artifacts, and
@@ -141,12 +144,23 @@ export function FlowSuccessRenderer({
141
144
  ? defaultDarkTheme
142
145
  : defaultLightTheme;
143
146
 
144
- // Registers the bundled DM Sans faces (no-op after the first flow).
145
- // Rendering does not wait on this — text re-renders into DM Sans the
146
- // moment registration completes.
147
- useUseSmileIDFonts();
147
+ // The first frame waits on font registration: a label laid out in the system font and re-measured in DM Sans can settle clipped.
148
+ const fontsLoaded = useUseSmileIDFonts();
149
+ const [fontWaitElapsed, setFontWaitElapsed] = useState(false);
150
+ useEffect(() => {
151
+ if (fontsLoaded) return;
152
+ const timer = setTimeout(
153
+ () => setFontWaitElapsed(true),
154
+ FONT_REGISTRATION_TIMEOUT_MS,
155
+ );
156
+ return () => clearTimeout(timer);
157
+ }, [fontsLoaded]);
148
158
  const enableCrashReporting =
149
159
  configuration.config?.enableCrashReporting !== false;
160
+ // Sandbox sessions are test traffic and must not pollute the production
161
+ // Sentry project; the reporter also drops debug bundles internally.
162
+ const useSandbox =
163
+ configuration.network?.config?.partnerConfig?.useSandbox === true;
150
164
 
151
165
  // Per-flow session id used as the Sentry user-id so every event
152
166
  // captured during this verification flow correlates under one issue
@@ -160,7 +174,10 @@ export function FlowSuccessRenderer({
160
174
 
161
175
  useEffect(() => {
162
176
  if (enableCrashReporting) {
163
- UseSmileIDCrashReporting.enable();
177
+ // Tear down any prior client first so a changed useSandbox flag takes
178
+ // effect — enable() short-circuits when already enabled.
179
+ UseSmileIDCrashReporting.disable();
180
+ UseSmileIDCrashReporting.enable({ useSandbox });
164
181
  UseSmileIDCrashReporting.setUserId(flowSessionIdRef.current!);
165
182
  UseSmileIDCrashReporting.captureBreadcrumb({
166
183
  category: "flow.navigation",
@@ -170,7 +187,7 @@ export function FlowSuccessRenderer({
170
187
  return () => {
171
188
  UseSmileIDCrashReporting.disable();
172
189
  };
173
- }, [enableCrashReporting]);
190
+ }, [enableCrashReporting, useSandbox]);
174
191
 
175
192
  // `onResult` lives in a ref so the manager's `onExit` closure
176
193
  // captures the latest partner callback without recreating the
@@ -178,6 +195,9 @@ export function FlowSuccessRenderer({
178
195
  const onResultRef = useRef(configuration.onResult);
179
196
  onResultRef.current = configuration.onResult;
180
197
 
198
+ // Latched by `handleExit` so an unmounted-before-finishing flow can deliver exactly once.
199
+ const finishedRef = useRef(false);
200
+
181
201
  // Force-update reducer drives re-renders when the manager's cursor
182
202
  // or captured artifacts change. Cheaper than mirroring state into
183
203
  // React state — the manager is the single source of truth.
@@ -213,6 +233,7 @@ export function FlowSuccessRenderer({
213
233
 
214
234
  const manager = useMemo(() => {
215
235
  const handleExit = (reason: FlowExitReason) => {
236
+ finishedRef.current = true;
216
237
  const onResult = onResultRef.current;
217
238
  try {
218
239
  if (reason.kind === "completed") {
@@ -264,6 +285,17 @@ export function FlowSuccessRenderer({
264
285
  };
265
286
  }, [manager]);
266
287
 
288
+ // A flow unmounted before finishing delivers a cancelled result exactly once.
289
+ useEffect(
290
+ () => () => {
291
+ if (!finishedRef.current) {
292
+ finishedRef.current = true;
293
+ onResultRef.current(cancelled());
294
+ }
295
+ },
296
+ [],
297
+ );
298
+
267
299
  // Resolve localizations:
268
300
  // 1. Explicit `configuration.localizations` (partner-set on the
269
301
  // builder) — wins over everything.
@@ -272,13 +304,23 @@ export function FlowSuccessRenderer({
272
304
  // registered locales with `UseSmileIDLocalizations.register(...)`.
273
305
  // 3. English defaults — no provider mounted, screens read from
274
306
  // the context's default English-only instance.
307
+ // A device locale change requires leaving the app, so re-reading on foreground is what makes a mid-flow switch observable.
308
+ const [localeEpoch, bumpLocaleEpoch] = useReducer((x: number) => x + 1, 0);
309
+ useEffect(() => {
310
+ const subscription = AppState.addEventListener("change", (nextState) => {
311
+ if (nextState === "active") bumpLocaleEpoch();
312
+ });
313
+ return () => subscription.remove();
314
+ }, []);
315
+
316
+ const activeLocaleKey = UseSmileIDLocalizations.activeLocaleKey;
275
317
  const resolved = useMemo(() => {
276
318
  if (configuration.localizations) return configuration.localizations;
277
319
  if (UseSmileIDLocalizations.hasRegisteredLocales()) {
278
320
  return UseSmileIDLocalizations.forActiveLocale();
279
321
  }
280
322
  return null;
281
- }, [configuration.localizations]);
323
+ }, [configuration.localizations, activeLocaleKey, localeEpoch]);
282
324
 
283
325
  const step = manager.currentScreen;
284
326
 
@@ -324,7 +366,16 @@ export function FlowSuccessRenderer({
324
366
  // resolve — the context default (English) covers the no-provider
325
367
  // path without an extra wrapper.
326
368
  const themed = (
327
- <UseSmileIDThemeProvider value={theme}>{inner}</UseSmileIDThemeProvider>
369
+ <UseSmileIDThemeProvider value={theme}>
370
+ {fontsLoaded || fontWaitElapsed ? (
371
+ inner
372
+ ) : (
373
+ <View
374
+ style={{ flex: 1, backgroundColor: theme.colors.background }}
375
+ testID="si_flow_font_gate"
376
+ />
377
+ )}
378
+ </UseSmileIDThemeProvider>
328
379
  );
329
380
 
330
381
  if (resolved) {