@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,224 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ isTokenConsentComplete,
4
+ tokenConsentMissingSubfields,
5
+ parseTokenPayload,
6
+ bindsRequiredUserDetails,
7
+ type TokenPayload,
8
+ } from "../token_payload";
9
+
10
+ // Canonical backend claim — user-detail values are opaque vault
11
+ // references; only key presence is meaningful client-side.
12
+ const FULL_CLAIM = {
13
+ given_names: "pii_abc",
14
+ last_name: "pii_def",
15
+ email: "pii_ghi",
16
+ phone_number: "pii_jkl",
17
+ consent: {
18
+ granted: true,
19
+ granted_at: "2026-01-01T09:00:00.000Z",
20
+ notice_language: "EN",
21
+ notice_privacy_policy_url: "https://partner.test/privacy",
22
+ },
23
+ } as const;
24
+
25
+ describe("parseTokenPayload — claim-shape tolerance", () => {
26
+ it("parses the full canonical claim into presence flags + complete consent", () => {
27
+ const payload = parseTokenPayload(FULL_CLAIM);
28
+ expect(payload).toEqual({
29
+ hasGivenNames: true,
30
+ hasLastName: true,
31
+ hasEmail: true,
32
+ hasPhoneNumber: true,
33
+ consent: {
34
+ granted: true,
35
+ grantedAt: "2026-01-01T09:00:00.000Z",
36
+ noticeLanguage: "EN",
37
+ noticePrivacyPolicyUrl: "https://partner.test/privacy",
38
+ },
39
+ });
40
+ });
41
+
42
+ it("parses a subset claim — only the present keys flag true", () => {
43
+ const payload = parseTokenPayload({ email: "pii_x" });
44
+ expect(payload).toEqual({
45
+ hasGivenNames: false,
46
+ hasLastName: false,
47
+ hasEmail: true,
48
+ hasPhoneNumber: false,
49
+ });
50
+ });
51
+
52
+ it("returns undefined for a non-object claim (string / array / number / null)", () => {
53
+ expect(parseTokenPayload("payload")).toBeUndefined();
54
+ expect(parseTokenPayload(["pii_x"])).toBeUndefined();
55
+ expect(parseTokenPayload(42)).toBeUndefined();
56
+ expect(parseTokenPayload(null)).toBeUndefined();
57
+ expect(parseTokenPayload(undefined)).toBeUndefined();
58
+ });
59
+
60
+ it("treats an empty string value as not binding the field", () => {
61
+ const payload = parseTokenPayload({ given_names: "", email: "pii_x" });
62
+ expect(payload?.hasGivenNames).toBe(false);
63
+ expect(payload?.hasEmail).toBe(true);
64
+ });
65
+
66
+ it("degrades a wrong-typed identity field to absent without dropping its siblings", () => {
67
+ const payload = parseTokenPayload({
68
+ given_names: 42,
69
+ last_name: ["pii"],
70
+ email: "pii_x",
71
+ });
72
+ expect(payload?.hasGivenNames).toBe(false);
73
+ expect(payload?.hasLastName).toBe(false);
74
+ expect(payload?.hasEmail).toBe(true);
75
+ });
76
+
77
+ it("ignores unknown keys and the out-of-scope id/callback fields", () => {
78
+ const payload = parseTokenPayload({
79
+ country: "NG",
80
+ id_type: "NIN",
81
+ id_number: "pii_id",
82
+ callback_url: "callback_x",
83
+ something_new: { nested: true },
84
+ last_name: "pii_def",
85
+ });
86
+ expect(payload).toEqual({
87
+ hasGivenNames: false,
88
+ hasLastName: true,
89
+ hasEmail: false,
90
+ hasPhoneNumber: false,
91
+ });
92
+ });
93
+
94
+ it("treats a non-object consent (string / array) as absent", () => {
95
+ expect(parseTokenPayload({ consent: "granted" })?.consent).toBeUndefined();
96
+ expect(parseTokenPayload({ consent: [true] })?.consent).toBeUndefined();
97
+ });
98
+
99
+ it("treats an empty consent object as absent", () => {
100
+ expect(parseTokenPayload({ consent: {} })?.consent).toBeUndefined();
101
+ });
102
+
103
+ it('never trusts granted: false or granted: "yes" as a binding', () => {
104
+ const deniedConsent = parseTokenPayload({
105
+ consent: { granted: false, granted_at: "2026-01-01T09:00:00.000Z" },
106
+ })?.consent;
107
+ expect(deniedConsent?.granted).toBeUndefined();
108
+ expect(deniedConsent?.grantedAt).toBe("2026-01-01T09:00:00.000Z");
109
+
110
+ const stringGranted = parseTokenPayload({
111
+ consent: { granted: "yes" },
112
+ })?.consent;
113
+ expect(stringGranted).toBeUndefined();
114
+ });
115
+
116
+ it("degrades wrong-typed consent subfields individually", () => {
117
+ const consent = parseTokenPayload({
118
+ consent: { granted: true, granted_at: 12345, notice_language: "EN" },
119
+ })?.consent;
120
+ expect(consent).toEqual({ granted: true, noticeLanguage: "EN" });
121
+ });
122
+
123
+ it("never throws for arbitrary claim shapes", () => {
124
+ const inputs: unknown[] = [
125
+ {},
126
+ FULL_CLAIM,
127
+ { consent: { granted: null } },
128
+ { given_names: { nested: true } },
129
+ Object.create(null),
130
+ ];
131
+ for (const input of inputs) {
132
+ expect(() => parseTokenPayload(input)).not.toThrow();
133
+ }
134
+ });
135
+ });
136
+
137
+ describe("isTokenConsentComplete", () => {
138
+ const completeConsent = {
139
+ granted: true,
140
+ grantedAt: "2026-01-01T09:00:00.000Z",
141
+ noticeLanguage: "EN",
142
+ noticePrivacyPolicyUrl: "https://partner.test/privacy",
143
+ } as const;
144
+
145
+ it("is true only when granted and all three subfields are non-blank", () => {
146
+ expect(isTokenConsentComplete(completeConsent)).toBe(true);
147
+ });
148
+
149
+ it("is false when any subfield is missing or blank", () => {
150
+ expect(isTokenConsentComplete(undefined)).toBe(false);
151
+ const { granted: _granted, ...withoutGranted } = completeConsent;
152
+ expect(isTokenConsentComplete(withoutGranted)).toBe(false);
153
+ const { noticeLanguage: _language, ...withoutLanguage } = completeConsent;
154
+ expect(isTokenConsentComplete(withoutLanguage)).toBe(false);
155
+ expect(
156
+ isTokenConsentComplete({ ...completeConsent, grantedAt: " " }),
157
+ ).toBe(false);
158
+ expect(
159
+ isTokenConsentComplete({
160
+ ...completeConsent,
161
+ noticePrivacyPolicyUrl: "",
162
+ }),
163
+ ).toBe(false);
164
+ });
165
+
166
+ it("missingSubfields names exactly the absent or blank consent subfields", () => {
167
+ expect(tokenConsentMissingSubfields(completeConsent)).toEqual([]);
168
+ const { granted: _granted, ...withoutGranted } = completeConsent;
169
+ expect(tokenConsentMissingSubfields(withoutGranted)).toEqual(["granted"]);
170
+ const { noticeLanguage: _language, ...withoutLanguage } = completeConsent;
171
+ expect(
172
+ tokenConsentMissingSubfields({ ...withoutLanguage, grantedAt: " " }),
173
+ ).toEqual(["granted_at", "notice_language"]);
174
+ expect(tokenConsentMissingSubfields({})).toEqual([
175
+ "granted",
176
+ "granted_at",
177
+ "notice_language",
178
+ "notice_privacy_policy_url",
179
+ ]);
180
+ });
181
+ });
182
+
183
+ describe("bindsRequiredUserDetails", () => {
184
+ function payload(overrides: Partial<TokenPayload>): TokenPayload {
185
+ return {
186
+ hasGivenNames: false,
187
+ hasLastName: false,
188
+ hasEmail: false,
189
+ hasPhoneNumber: false,
190
+ ...overrides,
191
+ };
192
+ }
193
+
194
+ it("requires both names plus at least one contact field", () => {
195
+ expect(
196
+ bindsRequiredUserDetails(
197
+ payload({ hasGivenNames: true, hasLastName: true, hasEmail: true }),
198
+ ),
199
+ ).toBe(true);
200
+ expect(
201
+ bindsRequiredUserDetails(
202
+ payload({
203
+ hasGivenNames: true,
204
+ hasLastName: true,
205
+ hasPhoneNumber: true,
206
+ }),
207
+ ),
208
+ ).toBe(true);
209
+ });
210
+
211
+ it("is false when a name or every contact field is missing", () => {
212
+ expect(
213
+ bindsRequiredUserDetails(
214
+ payload({ hasGivenNames: true, hasLastName: true }),
215
+ ),
216
+ ).toBe(false);
217
+ expect(
218
+ bindsRequiredUserDetails(
219
+ payload({ hasGivenNames: true, hasEmail: true }),
220
+ ),
221
+ ).toBe(false);
222
+ expect(bindsRequiredUserDetails(undefined)).toBe(false);
223
+ });
224
+ });
@@ -0,0 +1,100 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { UserDetails } from "../../../core/models/params";
3
+ import {
4
+ isUserDetailsPayloadEmpty,
5
+ userDetailsPayloadFrom,
6
+ userDetailsPayloadToWire,
7
+ } from "../user_details_payload";
8
+
9
+ // Pins the `user_details` wire carrier: token-bound fields are stripped,
10
+ // nothing-stripped output matches the legacy `UserDetails` encoding, and an
11
+ // all-stripped payload reads as empty so the part is omitted rather than
12
+ // sent as `{}`.
13
+ describe("UserDetailsPayload", () => {
14
+ const details: UserDetails = {
15
+ givenNames: "Ada",
16
+ lastName: "Lovelace",
17
+ email: "ada@example.test",
18
+ phoneNumber: "+2348000000000",
19
+ };
20
+
21
+ it("no token binding keeps every field and matches the UserDetails wire encoding", () => {
22
+ const payload = userDetailsPayloadFrom(details, undefined);
23
+
24
+ expect(payload).toEqual(details);
25
+ expect(isUserDetailsPayloadEmpty(payload)).toBe(false);
26
+ expect(userDetailsPayloadToWire(payload)).toEqual({
27
+ given_names: "Ada",
28
+ last_name: "Lovelace",
29
+ email: "ada@example.test",
30
+ phone_number: "+2348000000000",
31
+ });
32
+ });
33
+
34
+ it("an empty token payload binding nothing keeps every field", () => {
35
+ const payload = userDetailsPayloadFrom(details, {
36
+ hasGivenNames: false,
37
+ hasLastName: false,
38
+ hasEmail: false,
39
+ hasPhoneNumber: false,
40
+ });
41
+
42
+ expect(payload).toEqual(details);
43
+ });
44
+
45
+ it("token-bound fields are dropped individually", () => {
46
+ const payload = userDetailsPayloadFrom(details, {
47
+ hasGivenNames: true,
48
+ hasLastName: true,
49
+ hasEmail: false,
50
+ hasPhoneNumber: false,
51
+ });
52
+
53
+ expect(payload.givenNames).toBeUndefined();
54
+ expect(payload.lastName).toBeUndefined();
55
+ expect(payload.email).toBe("ada@example.test");
56
+ expect(payload.phoneNumber).toBe("+2348000000000");
57
+ expect(Object.keys(userDetailsPayloadToWire(payload)).sort()).toEqual([
58
+ "email",
59
+ "phone_number",
60
+ ]);
61
+ });
62
+
63
+ it("fully bound details read as empty", () => {
64
+ const payload = userDetailsPayloadFrom(details, {
65
+ hasGivenNames: true,
66
+ hasLastName: true,
67
+ hasEmail: true,
68
+ hasPhoneNumber: true,
69
+ });
70
+
71
+ expect(isUserDetailsPayloadEmpty(payload)).toBe(true);
72
+ });
73
+
74
+ it("blank contact fields count as empty and are omitted from the wire", () => {
75
+ const payload = userDetailsPayloadFrom(
76
+ { givenNames: "Ada", lastName: "Lovelace", email: " ", phoneNumber: "" },
77
+ {
78
+ hasGivenNames: true,
79
+ hasLastName: true,
80
+ hasEmail: false,
81
+ hasPhoneNumber: false,
82
+ },
83
+ );
84
+
85
+ // Blank contacts encode to nothing, so nothing remains.
86
+ expect(isUserDetailsPayloadEmpty(payload)).toBe(true);
87
+ expect(userDetailsPayloadToWire(payload)).toEqual({});
88
+ });
89
+
90
+ it("contact fields are trimmed on the wire", () => {
91
+ const wire = userDetailsPayloadToWire({
92
+ givenNames: "Ada",
93
+ lastName: "Lovelace",
94
+ email: " ada@example.test ",
95
+ });
96
+
97
+ expect(wire["email"]).toBe("ada@example.test");
98
+ expect(wire["phone_number"]).toBeUndefined();
99
+ });
100
+ });
@@ -2,8 +2,8 @@ import type {
2
2
  ConsentInformation,
3
3
  EnhancedKYCParams,
4
4
  BiometricKYCParams,
5
- UserDetails,
6
5
  } from "../../core/models/params";
6
+ import type { UserDetailsPayload } from "./user_details_payload";
7
7
  import { JobType } from "../../core/models/job_type";
8
8
  import type { UseSmileIDMetadata } from "../../metadata/use_smile_id_metadata";
9
9
 
@@ -12,10 +12,11 @@ import type { UseSmileIDMetadata } from "../../metadata/use_smile_id_metadata";
12
12
  * narrows the union and tells the repository which API method to
13
13
  * dispatch to.
14
14
  *
15
- * Every variant intersects [BaseJobRequest], which carries the two
15
+ * Every variant intersects [BaseJobRequest], which carries the
16
16
  * fields shared by all submissions: the partner-captured `consent`
17
- * record and the per-submission `metadata` snapshot. Variants keep a
18
- * narrowed `jobType` literal so the union still discriminates.
17
+ * record, the per-submission `metadata` snapshot, and the decoded
18
+ * token bindings. Variants keep a narrowed `jobType` literal so the
19
+ * union still discriminates.
19
20
  *
20
21
  * `metadata` is built at the request-construction site (the flow
21
22
  * navigation manager) and travels on the request itself — the API
@@ -23,7 +24,14 @@ import type { UseSmileIDMetadata } from "../../metadata/use_smile_id_metadata";
23
24
  * multipart part rather than generating the snapshot at submit time.
24
25
  */
25
26
  export interface BaseJobRequest {
26
- readonly consent: ConsentInformation;
27
+ /**
28
+ * Local consent record (captured on the consent screen or
29
+ * pre-supplied via the builder). Omitted only when no local source
30
+ * exists AND the token binds complete consent — the API layer then
31
+ * omits the `consent` multipart part and the server injects the
32
+ * token's consent.
33
+ */
34
+ readonly consent?: ConsentInformation;
27
35
  readonly metadata: UseSmileIDMetadata;
28
36
  }
29
37
 
@@ -31,7 +39,8 @@ export type SmartSelfieEnrollmentRequest = BaseJobRequest & {
31
39
  readonly jobType: typeof JobType.smartSelfieEnrollment;
32
40
  readonly selfieImage: string;
33
41
  readonly livenessImages: readonly string[];
34
- readonly userDetails: UserDetails;
42
+ /** Omitted when the token binds every required field, or reduced to the unbound remainder. */
43
+ readonly userDetails?: UserDetailsPayload;
35
44
  /**
36
45
  * Screen-level enhanced-liveness intent marker. The wire path is
37
46
  * always `/v3/registration` (and `/v3/authentication` on the auth
@@ -59,7 +68,8 @@ export type SmartSelfieAuthenticationRequest = BaseJobRequest & {
59
68
  readonly jobType: typeof JobType.smartSelfieAuthentication;
60
69
  readonly selfieImage: string;
61
70
  readonly livenessImages: readonly string[];
62
- readonly userDetails: UserDetails;
71
+ /** Omitted when the token binds every required field, or reduced to the unbound remainder. */
72
+ readonly userDetails?: UserDetailsPayload;
63
73
  /**
64
74
  * The `user_id` returned by a prior enrollment — the v3
65
75
  * `/v3/authentication` endpoint requires it so the server can match
@@ -77,7 +87,8 @@ export type SmartSelfieAuthenticationRequest = BaseJobRequest & {
77
87
  export type EnhancedKycRequest = BaseJobRequest & {
78
88
  readonly jobType: typeof JobType.enhancedKyc;
79
89
  readonly params: EnhancedKYCParams;
80
- readonly userDetails: UserDetails;
90
+ /** Omitted when the token binds every required field, or reduced to the unbound remainder. */
91
+ readonly userDetails?: UserDetailsPayload;
81
92
  };
82
93
 
83
94
  /**
@@ -90,7 +101,8 @@ export type EnhancedKycRequest = BaseJobRequest & {
90
101
  export type BvnRequest = BaseJobRequest & {
91
102
  readonly jobType: typeof JobType.bvn;
92
103
  readonly params: EnhancedKYCParams;
93
- readonly userDetails: UserDetails;
104
+ /** Omitted when the token binds every required field, or reduced to the unbound remainder. */
105
+ readonly userDetails?: UserDetailsPayload;
94
106
  };
95
107
 
96
108
  export type BiometricKycRequest = BaseJobRequest & {
@@ -98,7 +110,8 @@ export type BiometricKycRequest = BaseJobRequest & {
98
110
  readonly selfieImage: string;
99
111
  readonly livenessImages: readonly string[];
100
112
  readonly params: BiometricKYCParams;
101
- readonly userDetails: UserDetails;
113
+ /** Omitted when the token binds every required field, or reduced to the unbound remainder. */
114
+ readonly userDetails?: UserDetailsPayload;
102
115
  /** See `SmartSelfieEnrollmentRequest.expectedLivenessImageCount`. */
103
116
  readonly expectedLivenessImageCount: number;
104
117
  };
@@ -114,7 +127,8 @@ export type DocumentVerificationRequest = BaseJobRequest & {
114
127
  readonly country: string;
115
128
  /** Optional — the server classifies the document when omitted. */
116
129
  readonly idType?: string;
117
- readonly userDetails: UserDetails;
130
+ /** Omitted when the token binds every required field, or reduced to the unbound remainder. */
131
+ readonly userDetails?: UserDetailsPayload;
118
132
  /** See `SmartSelfieEnrollmentRequest.expectedLivenessImageCount`. */
119
133
  readonly expectedLivenessImageCount: number;
120
134
  };
@@ -135,7 +149,8 @@ export type EnhancedDocumentVerificationRequest = BaseJobRequest & {
135
149
  readonly backDocument?: string;
136
150
  readonly country: string;
137
151
  readonly idType: string;
138
- readonly userDetails: UserDetails;
152
+ /** Omitted when the token binds every required field, or reduced to the unbound remainder. */
153
+ readonly userDetails?: UserDetailsPayload;
139
154
  /** See `SmartSelfieEnrollmentRequest.expectedLivenessImageCount`. */
140
155
  readonly expectedLivenessImageCount: number;
141
156
  };
@@ -0,0 +1,182 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * User-detail and consent bindings delivered via the JWT auth token.
5
+ *
6
+ * The backend embeds this object under the top-level `payload` claim
7
+ * (JWT payload, base64url-decoded middle segment) when the partner
8
+ * binds user details and/or consent into the token on their backend.
9
+ * User-detail values in the claim are opaque vault references — only
10
+ * key PRESENCE is meaningful client-side. Consent subfields are
11
+ * plaintext. `country` / `id_type` / `id_number` / `callback_url` may
12
+ * also appear in the claim but are deliberately ignored.
13
+ *
14
+ * Every field is optional and every field tolerates a wrong wire type
15
+ * by degrading to absent — a malformed claim never rejects the token.
16
+ *
17
+ * Field names are a deliberate cross-SDK shared contract that
18
+ * intentionally omits the `UseSmileID` prefix. Internal — not
19
+ * re-exported from the package entrypoint (`src/index.ts`).
20
+ */
21
+
22
+ const tokenConsentClaimSchema = z.object({
23
+ /** Only a literal `true` counts — `false` or a non-boolean is never trusted as a binding. */
24
+ granted: z.literal(true).optional().catch(undefined),
25
+ granted_at: z.string().optional().catch(undefined),
26
+ notice_language: z.string().optional().catch(undefined),
27
+ notice_privacy_policy_url: z.string().optional().catch(undefined),
28
+ });
29
+
30
+ /**
31
+ * Tolerant wire schema for the `payload` claim. Unknown keys are
32
+ * stripped; a wrong-typed field degrades to `undefined` (per-field
33
+ * `.catch`) instead of failing the whole claim.
34
+ */
35
+ export const tokenPayloadSchema = z.object({
36
+ given_names: z.string().optional().catch(undefined),
37
+ last_name: z.string().optional().catch(undefined),
38
+ email: z.string().optional().catch(undefined),
39
+ phone_number: z.string().optional().catch(undefined),
40
+ /** Non-object / array consent values degrade to absent. */
41
+ consent: tokenConsentClaimSchema.optional().catch(undefined),
42
+ });
43
+
44
+ /**
45
+ * Consent subfields bound into the token. Token consent always wins:
46
+ * a complete binding (see {@link isTokenConsentComplete}) supersedes
47
+ * every local consent source, and a partial binding is a build-time
48
+ * validation error naming the missing subfields (see
49
+ * {@link tokenConsentMissingSubfields}).
50
+ */
51
+ export type TokenConsent = {
52
+ /** Present (and `true`) only when the claim carried a literal `granted: true`. */
53
+ readonly granted?: boolean;
54
+ /** ISO-8601 timestamp from the claim's `granted_at`. */
55
+ readonly grantedAt?: string;
56
+ /** Language code from the claim's `notice_language`. */
57
+ readonly noticeLanguage?: string;
58
+ /** URL from the claim's `notice_privacy_policy_url`. */
59
+ readonly noticePrivacyPolicyUrl?: string;
60
+ };
61
+
62
+ /**
63
+ * Presence flags for the identity fields the token binds, parsed from
64
+ * the `payload` claim. A flag is `true` iff the claim carries the key
65
+ * with a non-empty string value (the value itself is an opaque vault
66
+ * reference — the server injects the real value at submission).
67
+ */
68
+ export type TokenPayload = {
69
+ readonly hasGivenNames: boolean;
70
+ readonly hasLastName: boolean;
71
+ readonly hasEmail: boolean;
72
+ readonly hasPhoneNumber: boolean;
73
+ readonly consent?: TokenConsent;
74
+ };
75
+
76
+ /**
77
+ * Parse a raw `payload` claim value into a `TokenPayload`, or
78
+ * `undefined` when the claim is absent or not a JSON object. Mirrors
79
+ * the `sdk_config` degradation semantics: never throws, tolerates
80
+ * unknown keys, and degrades wrong-typed fields to absent.
81
+ */
82
+ export function parseTokenPayload(rawClaim: unknown): TokenPayload | undefined {
83
+ if (
84
+ rawClaim === null ||
85
+ typeof rawClaim !== "object" ||
86
+ Array.isArray(rawClaim)
87
+ ) {
88
+ return undefined;
89
+ }
90
+ const parseResult = tokenPayloadSchema.safeParse(rawClaim);
91
+ if (!parseResult.success) {
92
+ // Unreachable in practice (every field carries a `.catch`), but
93
+ // degrade gracefully to preserve the never-throws contract.
94
+ return undefined;
95
+ }
96
+ const claim = parseResult.data;
97
+ const consent = tokenConsentFromClaim(claim.consent);
98
+ return {
99
+ hasGivenNames: isPresent(claim.given_names),
100
+ hasLastName: isPresent(claim.last_name),
101
+ hasEmail: isPresent(claim.email),
102
+ hasPhoneNumber: isPresent(claim.phone_number),
103
+ ...(consent !== undefined ? { consent } : {}),
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Token consent is COMPLETE when `granted` is `true` and every other
109
+ * subfield is non-blank. A complete binding — granted with every
110
+ * notice subfield non-blank — satisfies the consent requirement
111
+ * without a local source. Token consent always wins, so a partial
112
+ * binding is a build-time validation error.
113
+ */
114
+ export function isTokenConsentComplete(
115
+ consent: TokenConsent | undefined,
116
+ ): boolean {
117
+ return (
118
+ consent !== undefined && tokenConsentMissingSubfields(consent).length === 0
119
+ );
120
+ }
121
+
122
+ /**
123
+ * Wire-key names of the consent subfields the binding is missing. A
124
+ * token that carries a consent object must carry all four subfields —
125
+ * a non-empty list is a build-time validation error naming exactly
126
+ * what the token must add.
127
+ */
128
+ export function tokenConsentMissingSubfields(
129
+ consent: TokenConsent,
130
+ ): readonly string[] {
131
+ const missing: string[] = [];
132
+ if (consent.granted !== true) missing.push("granted");
133
+ if (!isNonBlank(consent.grantedAt)) missing.push("granted_at");
134
+ if (!isNonBlank(consent.noticeLanguage)) missing.push("notice_language");
135
+ if (!isNonBlank(consent.noticePrivacyPolicyUrl)) {
136
+ missing.push("notice_privacy_policy_url");
137
+ }
138
+ return missing;
139
+ }
140
+
141
+ /**
142
+ * Whether the token binds every user-detail field the SDK would
143
+ * otherwise require from the builder: `given_names` AND `last_name`
144
+ * AND at least one of `email` / `phone_number`. Only then may
145
+ * `builder.userDetails` be omitted entirely.
146
+ */
147
+ export function bindsRequiredUserDetails(
148
+ payload: TokenPayload | undefined,
149
+ ): boolean {
150
+ if (payload === undefined) return false;
151
+ return (
152
+ payload.hasGivenNames &&
153
+ payload.hasLastName &&
154
+ (payload.hasEmail || payload.hasPhoneNumber)
155
+ );
156
+ }
157
+
158
+ function tokenConsentFromClaim(
159
+ claim: z.infer<typeof tokenConsentClaimSchema> | undefined,
160
+ ): TokenConsent | undefined {
161
+ if (claim === undefined) return undefined;
162
+ const consent: TokenConsent = {
163
+ ...(claim.granted === true ? { granted: true } : {}),
164
+ ...(claim.granted_at !== undefined ? { grantedAt: claim.granted_at } : {}),
165
+ ...(claim.notice_language !== undefined
166
+ ? { noticeLanguage: claim.notice_language }
167
+ : {}),
168
+ ...(claim.notice_privacy_policy_url !== undefined
169
+ ? { noticePrivacyPolicyUrl: claim.notice_privacy_policy_url }
170
+ : {}),
171
+ };
172
+ // An empty consent object binds nothing — treat it as absent.
173
+ return Object.keys(consent).length > 0 ? consent : undefined;
174
+ }
175
+
176
+ function isPresent(value: string | undefined): boolean {
177
+ return value !== undefined && value.length > 0;
178
+ }
179
+
180
+ function isNonBlank(value: string | undefined): boolean {
181
+ return value !== undefined && value.trim().length > 0;
182
+ }
@@ -0,0 +1,84 @@
1
+ import type { UserDetails } from "../../core/models/params";
2
+ import type { TokenPayload } from "./token_payload";
3
+
4
+ /**
5
+ * Wire carrier for the `user_details` multipart part.
6
+ *
7
+ * Unlike `UserDetails` — the partner-facing input model whose names are
8
+ * required — every field here is optional so the part can carry only the
9
+ * fields the token does **not** already bind. Token-bound fields are dropped
10
+ * at assembly (`userDetailsPayloadFrom`): the server injects the bound values
11
+ * and they take precedence over anything the client sends. When nothing
12
+ * remains, assembly omits the part entirely (`isUserDetailsPayloadEmpty`).
13
+ *
14
+ * Type and helper names are a deliberate cross-SDK shared contract and
15
+ * intentionally omit the `UseSmileID` prefix (like `TokenPayload`).
16
+ */
17
+ export type UserDetailsPayload = {
18
+ readonly givenNames?: string;
19
+ readonly lastName?: string;
20
+ readonly email?: string;
21
+ readonly phoneNumber?: string;
22
+ };
23
+
24
+ /**
25
+ * Build the payload from builder-supplied details, dropping every field the
26
+ * token binds. With no token binding this reproduces `UserDetails`' wire
27
+ * encoding unchanged.
28
+ */
29
+ export function userDetailsPayloadFrom(
30
+ userDetails: UserDetails,
31
+ droppingFieldsBoundBy: TokenPayload | undefined,
32
+ ): UserDetailsPayload {
33
+ return {
34
+ ...(droppingFieldsBoundBy?.hasGivenNames
35
+ ? {}
36
+ : { givenNames: userDetails.givenNames }),
37
+ ...(droppingFieldsBoundBy?.hasLastName
38
+ ? {}
39
+ : { lastName: userDetails.lastName }),
40
+ ...(droppingFieldsBoundBy?.hasEmail || userDetails.email === undefined
41
+ ? {}
42
+ : { email: userDetails.email }),
43
+ ...(droppingFieldsBoundBy?.hasPhoneNumber ||
44
+ userDetails.phoneNumber === undefined
45
+ ? {}
46
+ : { phoneNumber: userDetails.phoneNumber }),
47
+ };
48
+ }
49
+
50
+ /**
51
+ * True when `userDetailsPayloadToWire` would produce an empty object — the
52
+ * caller omits the part instead of sending `{}`. Blank contacts count as
53
+ * empty, mirroring the wire encoding that trims and drops them.
54
+ */
55
+ export function isUserDetailsPayloadEmpty(
56
+ payload: UserDetailsPayload,
57
+ ): boolean {
58
+ return Object.keys(userDetailsPayloadToWire(payload)).length === 0;
59
+ }
60
+
61
+ /**
62
+ * Wire shape: snake_case keys. Names are emitted as supplied when non-blank;
63
+ * contact fields are trimmed and omitted when blank.
64
+ */
65
+ export function userDetailsPayloadToWire(
66
+ payload: UserDetailsPayload,
67
+ ): Record<string, string> {
68
+ const wire: Record<string, string> = {};
69
+ if (payload.givenNames !== undefined && payload.givenNames.trim() !== "") {
70
+ wire["given_names"] = payload.givenNames;
71
+ }
72
+ if (payload.lastName !== undefined && payload.lastName.trim() !== "") {
73
+ wire["last_name"] = payload.lastName;
74
+ }
75
+ const trimmedEmail = payload.email?.trim();
76
+ if (trimmedEmail) {
77
+ wire["email"] = trimmedEmail;
78
+ }
79
+ const trimmedPhone = payload.phoneNumber?.trim();
80
+ if (trimmedPhone) {
81
+ wire["phone_number"] = trimmedPhone;
82
+ }
83
+ return wire;
84
+ }