@smileid/usesmileid 12.0.0 → 12.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/core/exception/agent_mode_with_enhanced_liveness_exception.d.ts +11 -0
  3. package/dist/core/exception/agent_mode_with_enhanced_liveness_exception.d.ts.map +1 -0
  4. package/dist/core/exception/agent_mode_with_enhanced_liveness_exception.js +17 -0
  5. package/dist/core/exception/agent_mode_with_enhanced_liveness_exception.js.map +1 -0
  6. package/dist/core/exception/capture_validation_exceptions.d.ts +1 -0
  7. package/dist/core/exception/capture_validation_exceptions.d.ts.map +1 -1
  8. package/dist/core/exception/capture_validation_exceptions.js +1 -0
  9. package/dist/core/exception/capture_validation_exceptions.js.map +1 -1
  10. package/dist/core/exception/index.d.ts +1 -0
  11. package/dist/core/exception/index.d.ts.map +1 -1
  12. package/dist/core/exception/index.js +1 -0
  13. package/dist/core/exception/index.js.map +1 -1
  14. package/dist/core/exception/use_smile_id_error_code.d.ts +1 -0
  15. package/dist/core/exception/use_smile_id_error_code.d.ts.map +1 -1
  16. package/dist/core/exception/use_smile_id_error_code.js +1 -0
  17. package/dist/core/exception/use_smile_id_error_code.js.map +1 -1
  18. package/dist/core/models/config.d.ts +19 -0
  19. package/dist/core/models/config.d.ts.map +1 -1
  20. package/dist/core/models/document_type.d.ts +1 -1
  21. package/dist/core/models/document_type.d.ts.map +1 -1
  22. package/dist/core/models/document_type.js +2 -2
  23. package/dist/core/models/document_type.js.map +1 -1
  24. package/dist/core/models/global_privacy_control.d.ts +36 -0
  25. package/dist/core/models/global_privacy_control.d.ts.map +1 -0
  26. package/dist/core/models/global_privacy_control.js +39 -0
  27. package/dist/core/models/global_privacy_control.js.map +1 -0
  28. package/dist/core/models/network_config.d.ts +2 -2
  29. package/dist/core/models/network_config.js +2 -2
  30. package/dist/data/api/interceptors/smile_auth_interceptor.d.ts.map +1 -1
  31. package/dist/data/api/interceptors/smile_auth_interceptor.js +3 -17
  32. package/dist/data/api/interceptors/smile_auth_interceptor.js.map +1 -1
  33. package/dist/data/api/smile_id_api.d.ts +1 -1
  34. package/dist/data/api/smile_id_api.d.ts.map +1 -1
  35. package/dist/data/dsl/builder/retry_config_builder.d.ts +4 -4
  36. package/dist/data/dsl/builder/retry_config_builder.d.ts.map +1 -1
  37. package/dist/data/dsl/builder/retry_config_builder.js +6 -6
  38. package/dist/data/dsl/builder/retry_config_builder.js.map +1 -1
  39. package/dist/data/smile_id_urls.d.ts +1 -1
  40. package/dist/data/smile_id_urls.js +1 -1
  41. package/dist/data/smile_id_urls.js.map +1 -1
  42. package/dist/data/util/retry_util.d.ts +1 -1
  43. package/dist/data/util/retry_util.d.ts.map +1 -1
  44. package/dist/data/util/retry_util.js +5 -0
  45. package/dist/data/util/retry_util.js.map +1 -1
  46. package/dist/index.d.ts +2 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +2 -2
  49. package/dist/index.js.map +1 -1
  50. package/dist/metadata/builder_configuration.d.ts +5 -0
  51. package/dist/metadata/builder_configuration.d.ts.map +1 -1
  52. package/dist/metadata/builder_configuration.js +1 -0
  53. package/dist/metadata/builder_configuration.js.map +1 -1
  54. package/dist/metadata/metadata_configuration_mappers.d.ts +2 -3
  55. package/dist/metadata/metadata_configuration_mappers.d.ts.map +1 -1
  56. package/dist/metadata/metadata_configuration_mappers.js +13 -6
  57. package/dist/metadata/metadata_configuration_mappers.js.map +1 -1
  58. package/dist/metadata/use_smile_id_metadata_factory.d.ts.map +1 -1
  59. package/dist/metadata/use_smile_id_metadata_factory.js +14 -5
  60. package/dist/metadata/use_smile_id_metadata_factory.js.map +1 -1
  61. package/dist/presentation/components/use_smile_id_enhanced_smart_selfie_clip.d.ts +5 -2
  62. package/dist/presentation/components/use_smile_id_enhanced_smart_selfie_clip.d.ts.map +1 -1
  63. package/dist/presentation/components/use_smile_id_enhanced_smart_selfie_clip.js +8 -3
  64. package/dist/presentation/components/use_smile_id_enhanced_smart_selfie_clip.js.map +1 -1
  65. package/dist/presentation/components/use_smile_id_instructions_hero.d.ts.map +1 -1
  66. package/dist/presentation/components/use_smile_id_instructions_hero.js +8 -3
  67. package/dist/presentation/components/use_smile_id_instructions_hero.js.map +1 -1
  68. package/dist/presentation/flow/dsl/builder/config_builder.d.ts +16 -0
  69. package/dist/presentation/flow/dsl/builder/config_builder.d.ts.map +1 -1
  70. package/dist/presentation/flow/dsl/builder/config_builder.js +18 -0
  71. package/dist/presentation/flow/dsl/builder/config_builder.js.map +1 -1
  72. package/dist/presentation/flow/dsl/builder/screens_builder.d.ts +3 -6
  73. package/dist/presentation/flow/dsl/builder/screens_builder.d.ts.map +1 -1
  74. package/dist/presentation/flow/dsl/builder/screens_builder.js +3 -6
  75. package/dist/presentation/flow/dsl/builder/screens_builder.js.map +1 -1
  76. package/dist/presentation/flow/flow_navigation_manager.d.ts +5 -1
  77. package/dist/presentation/flow/flow_navigation_manager.d.ts.map +1 -1
  78. package/dist/presentation/flow/flow_navigation_manager.js +7 -1
  79. package/dist/presentation/flow/flow_navigation_manager.js.map +1 -1
  80. package/dist/presentation/flow/model/instructions_defaults.d.ts.map +1 -1
  81. package/dist/presentation/flow/model/instructions_defaults.js +1 -1
  82. package/dist/presentation/flow/model/instructions_defaults.js.map +1 -1
  83. package/dist/presentation/flow/screen/orchestrated_capture_screen.d.ts.map +1 -1
  84. package/dist/presentation/flow/screen/orchestrated_capture_screen.js +13 -3
  85. package/dist/presentation/flow/screen/orchestrated_capture_screen.js.map +1 -1
  86. package/dist/presentation/flow/screen/orchestrated_consent_screen.d.ts.map +1 -1
  87. package/dist/presentation/flow/screen/orchestrated_consent_screen.js +3 -1
  88. package/dist/presentation/flow/screen/orchestrated_consent_screen.js.map +1 -1
  89. package/dist/presentation/flow/screen/orchestrated_preview_screen.d.ts.map +1 -1
  90. package/dist/presentation/flow/screen/orchestrated_preview_screen.js +0 -8
  91. package/dist/presentation/flow/screen/orchestrated_preview_screen.js.map +1 -1
  92. package/dist/presentation/flow/use_smile_id_flow.d.ts.map +1 -1
  93. package/dist/presentation/flow/use_smile_id_flow.js +10 -4
  94. package/dist/presentation/flow/use_smile_id_flow.js.map +1 -1
  95. package/dist/presentation/flow/validation/flow_validator.d.ts.map +1 -1
  96. package/dist/presentation/flow/validation/flow_validator.js +42 -20
  97. package/dist/presentation/flow/validation/flow_validator.js.map +1 -1
  98. package/dist/presentation/screen/helpers/capture_ui_state.d.ts +6 -6
  99. package/dist/presentation/screen/helpers/capture_ui_state.d.ts.map +1 -1
  100. package/dist/presentation/screen/helpers/capture_ui_state.js +19 -19
  101. package/dist/presentation/screen/helpers/capture_ui_state.js.map +1 -1
  102. package/dist/presentation/screen/helpers/ess_turn_haptics.d.ts +2 -2
  103. package/dist/presentation/screen/helpers/ess_turn_haptics.d.ts.map +1 -1
  104. package/dist/presentation/screen/helpers/ess_turn_haptics.js +6 -6
  105. package/dist/presentation/screen/helpers/ess_turn_haptics.js.map +1 -1
  106. package/dist/presentation/screen/helpers/scan_state_to_overlay_state.d.ts +5 -5
  107. package/dist/presentation/screen/helpers/scan_state_to_overlay_state.d.ts.map +1 -1
  108. package/dist/presentation/screen/helpers/scan_state_to_overlay_state.js +11 -11
  109. package/dist/presentation/screen/helpers/scan_state_to_overlay_state.js.map +1 -1
  110. package/dist/presentation/screen/helpers/use_capture_orientation_lock.d.ts +13 -3
  111. package/dist/presentation/screen/helpers/use_capture_orientation_lock.d.ts.map +1 -1
  112. package/dist/presentation/screen/helpers/use_capture_orientation_lock.js +48 -6
  113. package/dist/presentation/screen/helpers/use_capture_orientation_lock.js.map +1 -1
  114. package/dist/presentation/screen/use_smile_id_capture_screen.d.ts +4 -2
  115. package/dist/presentation/screen/use_smile_id_capture_screen.d.ts.map +1 -1
  116. package/dist/presentation/screen/use_smile_id_capture_screen.js +36 -15
  117. package/dist/presentation/screen/use_smile_id_capture_screen.js.map +1 -1
  118. package/dist/presentation/screen/use_smile_id_consent_screen.d.ts +7 -0
  119. package/dist/presentation/screen/use_smile_id_consent_screen.d.ts.map +1 -1
  120. package/dist/presentation/screen/use_smile_id_consent_screen.js +6 -5
  121. package/dist/presentation/screen/use_smile_id_consent_screen.js.map +1 -1
  122. package/dist/presentation/screen/use_smile_id_document_back_instructions.d.ts.map +1 -1
  123. package/dist/presentation/screen/use_smile_id_document_back_instructions.js +5 -13
  124. package/dist/presentation/screen/use_smile_id_document_back_instructions.js.map +1 -1
  125. package/dist/presentation/screen/use_smile_id_document_preview_screen.js +1 -1
  126. package/dist/presentation/screen/use_smile_id_document_preview_screen.js.map +1 -1
  127. package/dist/presentation/screen/use_smile_id_preview_screen.d.ts +1 -21
  128. package/dist/presentation/screen/use_smile_id_preview_screen.d.ts.map +1 -1
  129. package/dist/presentation/screen/use_smile_id_preview_screen.js +3 -14
  130. package/dist/presentation/screen/use_smile_id_preview_screen.js.map +1 -1
  131. package/dist/presentation/screen/use_smile_id_processing_screen.js +1 -1
  132. package/dist/presentation/screen/use_smile_id_processing_screen.js.map +1 -1
  133. package/dist/presentation/utils/locale_utils.d.ts +10 -4
  134. package/dist/presentation/utils/locale_utils.d.ts.map +1 -1
  135. package/dist/presentation/utils/locale_utils.js +13 -5
  136. package/dist/presentation/utils/locale_utils.js.map +1 -1
  137. package/dist/testing/fake_face_analyzer.d.ts +43 -0
  138. package/dist/testing/fake_face_analyzer.d.ts.map +1 -0
  139. package/dist/testing/fake_face_analyzer.js +52 -0
  140. package/dist/testing/fake_face_analyzer.js.map +1 -0
  141. package/dist/testing/index.d.ts +5 -0
  142. package/dist/testing/index.d.ts.map +1 -0
  143. package/dist/testing/index.js +7 -0
  144. package/dist/testing/index.js.map +1 -0
  145. package/dist/testing/scenario_expectations.d.ts +28 -0
  146. package/dist/testing/scenario_expectations.d.ts.map +1 -0
  147. package/dist/testing/scenario_expectations.js +65 -0
  148. package/dist/testing/scenario_expectations.js.map +1 -0
  149. package/dist/testing/scenario_loader.d.ts +11 -0
  150. package/dist/testing/scenario_loader.d.ts.map +1 -0
  151. package/dist/testing/scenario_loader.js +64 -0
  152. package/dist/testing/scenario_loader.js.map +1 -0
  153. package/dist/testing/scripted_scan_results.d.ts +17 -0
  154. package/dist/testing/scripted_scan_results.d.ts.map +1 -0
  155. package/dist/testing/scripted_scan_results.js +52 -0
  156. package/dist/testing/scripted_scan_results.js.map +1 -0
  157. package/package.json +11 -4
  158. package/src/__tests__/dsl.test.ts +51 -1
  159. package/src/__tests__/setup_react_native.ts +17 -2
  160. package/src/__tests__/snapshots/__snapshots__/screens_structural.test.tsx.snap +5261 -0
  161. package/src/__tests__/snapshots/screens_structural.test.tsx +230 -0
  162. package/src/camera/__tests__/use_smile_id_camera_system.test.ts +103 -0
  163. package/src/core/exception/__tests__/builder_validation_exceptions.test.ts +67 -0
  164. package/src/core/exception/agent_mode_with_enhanced_liveness_exception.ts +20 -0
  165. package/src/core/exception/capture_validation_exceptions.ts +1 -0
  166. package/src/core/exception/index.ts +1 -0
  167. package/src/core/exception/use_smile_id_error_code.ts +1 -0
  168. package/src/core/models/__tests__/document_type.test.ts +2 -2
  169. package/src/core/models/__tests__/flow_models.test.ts +26 -0
  170. package/src/core/models/config.ts +22 -0
  171. package/src/core/models/document_type.ts +2 -2
  172. package/src/core/models/global_privacy_control.ts +45 -0
  173. package/src/core/models/network_config.ts +2 -2
  174. package/src/data/api/__tests__/smile_id_api.test.ts +1 -1
  175. package/src/data/api/interceptors/__tests__/gzip_interceptor.test.ts +83 -0
  176. package/src/data/api/interceptors/smile_auth_interceptor.ts +3 -18
  177. package/src/data/compression/__tests__/payload_compressor_real_jpeg.test.ts +6 -2
  178. package/src/data/dsl/builder/__tests__/config_builders.test.ts +62 -0
  179. package/src/data/dsl/builder/retry_config_builder.ts +6 -6
  180. package/src/data/factory/__tests__/retry_gzip_mac_composition.test.ts +163 -0
  181. package/src/data/factory/__tests__/signed_job_submission_headers.test.ts +1 -1
  182. package/src/data/repository/__tests__/network_retries.test.ts +1 -1
  183. package/src/data/repository/__tests__/use_smile_id_repository.test.ts +1 -1
  184. package/src/data/security/__tests__/request_canonicalizer.test.ts +132 -0
  185. package/src/data/smile_id_urls.ts +1 -1
  186. package/src/data/util/__tests__/retry_util.test.ts +25 -0
  187. package/src/data/util/retry_util.ts +9 -1
  188. package/src/index.ts +6 -4
  189. package/src/metadata/__tests__/metadata_configuration_mappers.test.ts +39 -0
  190. package/src/metadata/__tests__/metadata_configuration_objects.test.ts +2 -1
  191. package/src/metadata/__tests__/metadata_device_context.test.ts +3 -3
  192. package/src/metadata/__tests__/metadata_device_os_android.test.ts +18 -0
  193. package/src/metadata/__tests__/metadata_golden_fixture.json +1 -1
  194. package/src/metadata/__tests__/metadata_payload_golden.test.ts +4 -4
  195. package/src/metadata/__tests__/use_smile_id_metadata.test.ts +1 -1
  196. package/src/metadata/builder_configuration.ts +6 -0
  197. package/src/metadata/metadata_configuration_mappers.ts +21 -7
  198. package/src/metadata/use_smile_id_metadata_factory.ts +17 -6
  199. package/src/presentation/assets/si_document_green_book_hero_animation.json +1 -4668
  200. package/src/presentation/assets/si_document_hero_animation.json +1 -4799
  201. package/src/presentation/assets/si_document_hero_back_animation.json +1 -2039
  202. package/src/presentation/assets/si_document_passport_hero_animation.json +1 -14491
  203. package/src/presentation/assets/si_ess_all_directions_hero_animation.json +1 -0
  204. package/src/presentation/assets/si_ess_animation_all_directions.json +1 -6589
  205. package/src/presentation/assets/si_selfie_hero_animation.json +1 -1439
  206. package/src/presentation/assets/si_smile_to_capture_animation.json +1 -1439
  207. package/src/presentation/components/__tests__/use_smile_id_enhanced_smart_selfie_clip.test.tsx +9 -4
  208. package/src/presentation/components/__tests__/use_smile_id_instructions_hero.test.tsx +44 -0
  209. package/src/presentation/components/use_smile_id_enhanced_smart_selfie_clip.tsx +9 -3
  210. package/src/presentation/components/use_smile_id_instructions_hero.tsx +10 -0
  211. package/src/presentation/flow/__tests__/document_flow_navigation.test.ts +3 -3
  212. package/src/presentation/flow/__tests__/flow_navigation_manager.test.ts +49 -0
  213. package/src/presentation/flow/dsl/builder/__tests__/config_builder.test.ts +25 -0
  214. package/src/presentation/flow/dsl/builder/config_builder.ts +21 -0
  215. package/src/presentation/flow/dsl/builder/screens_builder.ts +3 -6
  216. package/src/presentation/flow/flow_navigation_manager.ts +6 -1
  217. package/src/presentation/flow/model/instructions_defaults.ts +1 -1
  218. package/src/presentation/flow/screen/orchestrated_capture_screen.tsx +18 -3
  219. package/src/presentation/flow/screen/orchestrated_consent_screen.tsx +5 -0
  220. package/src/presentation/flow/screen/orchestrated_preview_screen.tsx +0 -12
  221. package/src/presentation/flow/use_smile_id_flow.tsx +11 -4
  222. package/src/presentation/flow/validation/__tests__/flow_validator.test.ts +61 -1
  223. package/src/presentation/flow/validation/flow_validator.ts +55 -26
  224. package/src/presentation/screen/__tests__/capture_ui_state.test.ts +66 -47
  225. package/src/presentation/screen/__tests__/ess_turn_haptics.test.ts +15 -11
  226. package/src/presentation/screen/__tests__/scan_state_to_overlay_state.test.ts +47 -27
  227. package/src/presentation/screen/__tests__/use_smile_id_capture_screen.test.tsx +176 -14
  228. package/src/presentation/screen/__tests__/use_smile_id_document_back_instructions.test.tsx +1 -1
  229. package/src/presentation/screen/__tests__/use_smile_id_preview_screen.test.tsx +10 -40
  230. package/src/presentation/screen/helpers/__tests__/use_capture_orientation_lock.test.tsx +212 -0
  231. package/src/presentation/screen/helpers/capture_ui_state.ts +27 -27
  232. package/src/presentation/screen/helpers/ess_turn_haptics.ts +9 -9
  233. package/src/presentation/screen/helpers/scan_state_to_overlay_state.ts +13 -13
  234. package/src/presentation/screen/helpers/use_capture_orientation_lock.ts +69 -7
  235. package/src/presentation/screen/use_smile_id_capture_screen.tsx +47 -19
  236. package/src/presentation/screen/use_smile_id_consent_screen.tsx +16 -5
  237. package/src/presentation/screen/use_smile_id_document_back_instructions.tsx +14 -23
  238. package/src/presentation/screen/use_smile_id_document_preview_screen.tsx +1 -1
  239. package/src/presentation/screen/use_smile_id_preview_screen.tsx +13 -45
  240. package/src/presentation/screen/use_smile_id_processing_screen.tsx +1 -1
  241. package/src/presentation/utils/__tests__/locale_utils.test.ts +46 -5
  242. package/src/presentation/utils/locale_utils.ts +13 -4
  243. package/src/testing/__tests__/fake_face_analyzer.test.ts +42 -0
  244. package/src/testing/__tests__/scenario_expectations.test.ts +110 -0
  245. package/src/testing/__tests__/scenario_loader.test.ts +64 -0
  246. package/src/testing/fake_face_analyzer.ts +82 -0
  247. package/src/testing/index.ts +15 -0
  248. package/src/testing/scenario_expectations.ts +87 -0
  249. package/src/testing/scenario_loader.ts +83 -0
  250. package/src/testing/scripted_scan_results.ts +66 -0
  251. package/dist/presentation/components/use_smile_id_face_silhouette_overlay.d.ts +0 -15
  252. package/dist/presentation/components/use_smile_id_face_silhouette_overlay.d.ts.map +0 -1
  253. package/dist/presentation/components/use_smile_id_face_silhouette_overlay.js +0 -19
  254. package/dist/presentation/components/use_smile_id_face_silhouette_overlay.js.map +0 -1
  255. package/src/presentation/components/use_smile_id_face_silhouette_overlay.tsx +0 -27
@@ -8,8 +8,9 @@ import {
8
8
  import { AccessibilityInfo } from "react-native";
9
9
  import {
10
10
  UseSmileIDEnhancedSmartSelfieClip,
11
- ENHANCED_SMART_SELFIE_ANIMATION_SIZE,
11
+ ENHANCED_SMART_SELFIE_ARROW_FIT,
12
12
  } from "../use_smile_id_enhanced_smart_selfie_clip";
13
+ import { FaceOvalDefaults } from "../face_oval_defaults";
13
14
  import {
14
15
  ENHANCED_SMART_SELFIE_ANIMATION_SPEED,
15
16
  ENHANCED_SMART_SELFIE_TOTAL_FRAMES,
@@ -195,7 +196,7 @@ describe("UseSmileIDEnhancedSmartSelfieClip", () => {
195
196
  });
196
197
 
197
198
  describe("positioning", () => {
198
- it("produces a container sized to ENHANCED_SMART_SELFIE_ANIMATION_SIZE", async () => {
199
+ it("sizes the container to the oval width times the arrow-fit factor", async () => {
199
200
  const tree = renderClip({ pingPong: false });
200
201
  await act(async () => {
201
202
  await Promise.resolve();
@@ -209,8 +210,12 @@ describe("UseSmileIDEnhancedSmartSelfieClip", () => {
209
210
  );
210
211
  expect(container).toBeDefined();
211
212
  const style = container!.props.style as { width: number; height: number };
212
- expect(style.width).toBe(ENHANCED_SMART_SELFIE_ANIMATION_SIZE);
213
- expect(style.height).toBe(ENHANCED_SMART_SELFIE_ANIMATION_SIZE);
213
+ const expectedSize =
214
+ BASE_CONTAINER_WIDTH *
215
+ FaceOvalDefaults.ovalWidthRatio *
216
+ ENHANCED_SMART_SELFIE_ARROW_FIT;
217
+ expect(style.width).toBe(expectedSize);
218
+ expect(style.height).toBe(expectedSize);
214
219
  });
215
220
  });
216
221
 
@@ -78,6 +78,50 @@ describe("UseSmileIDInstructionsHero", () => {
78
78
  }
79
79
  });
80
80
 
81
+ it("pins oval-less heroes to the software Lottie engine on iOS", () => {
82
+ // lottie-ios's CA engine mis-renders the doc heroes' track mattes, so
83
+ // oval-less heroes must pin renderMode SOFTWARE on iOS.
84
+ const docTree = render(
85
+ <UseSmileIDInstructionsHero
86
+ source={SELFIE_HERO_ANIMATION}
87
+ showOval={false}
88
+ showOvalProgress={false}
89
+ />,
90
+ );
91
+ const docHero = docTree.root.findByProps({
92
+ testID: "si_instructions_hero",
93
+ });
94
+ act(() => {
95
+ docHero.props.onLayout({
96
+ nativeEvent: { layout: { width: 390, height: 600 } },
97
+ });
98
+ });
99
+ expect(docTree.root.findByType(AnimatedLottieView).props.renderMode).toBe(
100
+ "SOFTWARE",
101
+ );
102
+ act(() => docTree.unmount());
103
+
104
+ const ovalTree = render(
105
+ <UseSmileIDInstructionsHero
106
+ source={SELFIE_HERO_ANIMATION}
107
+ showOval
108
+ showOvalProgress={false}
109
+ />,
110
+ );
111
+ const ovalHero = ovalTree.root.findByProps({
112
+ testID: "si_instructions_hero",
113
+ });
114
+ act(() => {
115
+ ovalHero.props.onLayout({
116
+ nativeEvent: { layout: { width: 390, height: 600 } },
117
+ });
118
+ });
119
+ expect(
120
+ ovalTree.root.findByType(AnimatedLottieView).props.renderMode,
121
+ ).toBeUndefined();
122
+ act(() => ovalTree.unmount());
123
+ });
124
+
81
125
  it("gives the document card hero explicit dimensions (no intrinsic-size flash)", () => {
82
126
  const tree = render(
83
127
  <UseSmileIDInstructionsHero
@@ -13,8 +13,11 @@ import {
13
13
  export const ESS_ALL_DIRECTIONS_ANIMATION =
14
14
  require("../assets/si_ess_animation_all_directions.json") as AnimationObject;
15
15
 
16
- /** Square box the Enhanced Smart Selfie cues render in, centred on the oval. */
17
- export const ENHANCED_SMART_SELFIE_ANIMATION_SIZE = 383;
16
+ /**
17
+ * Box width as a multiple of the oval width; the baked arrow tips touch the
18
+ * oval boundary at ≈1.19× — keep this factor below that.
19
+ */
20
+ export const ENHANCED_SMART_SELFIE_ARROW_FIT = 1.15;
18
21
  /** Nudge so the composition's face lands on the oval centre. */
19
22
  export const VERTICAL_ALIGNMENT_OFFSET = -15;
20
23
 
@@ -46,7 +49,10 @@ export function UseSmileIDEnhancedSmartSelfieClip(
46
49
  return null;
47
50
  }
48
51
 
49
- const size = ENHANCED_SMART_SELFIE_ANIMATION_SIZE;
52
+ const size =
53
+ containerWidth *
54
+ FaceOvalDefaults.ovalWidthRatio *
55
+ ENHANCED_SMART_SELFIE_ARROW_FIT;
50
56
  const ovalCenterY = containerHeight * FaceOvalDefaults.ovalCenterYRatio;
51
57
 
52
58
  const containerStyle = {
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react";
2
2
  import {
3
3
  Animated,
4
4
  Easing,
5
+ Platform,
5
6
  StyleSheet,
6
7
  View,
7
8
  type LayoutChangeEvent,
@@ -160,6 +161,13 @@ export function UseSmileIDInstructionsHero({
160
161
  // motion is reduced; otherwise let it play on its own clock (autoPlay+loop).
161
162
  const lottieDrivenByProgress = showOvalProgress || reducedMotion;
162
163
 
164
+ // lottie-ios's Core Animation engine mis-renders the document heroes' track mattes,
165
+ // so oval-less heroes use the main-thread engine on iOS; Android keeps its default.
166
+ const renderModeProps =
167
+ Platform.OS === "ios" && !showOval
168
+ ? ({ renderMode: "SOFTWARE" } as const)
169
+ : {};
170
+
163
171
  // Inset the ring by half the stroke so it isn't clipped at the SVG edge.
164
172
  const ellipseProps = {
165
173
  cx: ovalWidth / 2,
@@ -196,6 +204,7 @@ export function UseSmileIDInstructionsHero({
196
204
  autoPlay={!lottieDrivenByProgress}
197
205
  loop={!lottieDrivenByProgress}
198
206
  {...(lottieDrivenByProgress ? { progress: heroProgress } : {})}
207
+ {...renderModeProps}
199
208
  resizeMode="contain"
200
209
  style={{ width: cardWidth, height: cardHeight }}
201
210
  />
@@ -266,6 +275,7 @@ export function UseSmileIDInstructionsHero({
266
275
  autoPlay={!lottieDrivenByProgress}
267
276
  loop={!lottieDrivenByProgress}
268
277
  {...(lottieDrivenByProgress ? { progress: heroProgress } : {})}
278
+ {...renderModeProps}
269
279
  style={{
270
280
  position: "absolute",
271
281
  width: animationSize,
@@ -74,15 +74,15 @@ describe("FlowNavigationManager — document per-side preview loop", () => {
74
74
  true,
75
75
  );
76
76
 
77
- const passportFlow = buildScreens((screens) => {
77
+ const greenBookFlow = buildScreens((screens) => {
78
78
  screens.capture((capture) => {
79
79
  capture.captureType = CaptureType.document;
80
80
  capture.document((document) => {
81
- document.documentType = DocumentType.Passport; // no back side
81
+ document.documentType = DocumentType.SouthAfricaGreenBook; // no back side
82
82
  });
83
83
  });
84
84
  });
85
- expect(makeManager(passportFlow).documentCapturesBack()).toBe(false);
85
+ expect(makeManager(greenBookFlow).documentCapturesBack()).toBe(false);
86
86
  });
87
87
 
88
88
  it("after the FRONT preview, routes back to the document capture for the BACK", () => {
@@ -602,6 +602,55 @@ describe("FlowNavigationManager — submission lifecycle", () => {
602
602
  expect(manager.jobSubmissionState.kind).toBe("success");
603
603
  });
604
604
 
605
+ it("retrySubmission double-trigger fires exactly one new submission", async () => {
606
+ // Concrete scenario: the processing screen's retry button is
607
+ // double-tapped after a failed submission. The second tap must not
608
+ // reset the in-flight `loading` latch and launch a duplicate job.
609
+ const repository = new StubRepository();
610
+ repository.result = failure(new Error("transient"));
611
+ const manager = makeManager(
612
+ buildScreens((screens) => screens.processing()),
613
+ vi.fn(),
614
+ {
615
+ enhancedKYCParams: {
616
+ idType: "BVN",
617
+ idNumber: "1",
618
+ country: "NG",
619
+ },
620
+ userDetails: SAMPLE_USER_DETAILS,
621
+ network: {
622
+ config: {
623
+ jobType: JobType.enhancedKyc,
624
+ token: "k",
625
+ partnerConfig: {
626
+ partnerId: "p",
627
+ callbackUrl: "",
628
+ useSandbox: true,
629
+ },
630
+ },
631
+ },
632
+ },
633
+ repository,
634
+ );
635
+
636
+ // Initial attempt fails, leaving the retry affordance visible.
637
+ manager.beginSubmissionIfNeeded();
638
+ await flushMicrotasks();
639
+ expect(manager.jobSubmissionState.kind).toBe("failure");
640
+ expect(repository.received).toHaveLength(1);
641
+
642
+ // The retried submission stays in flight so both taps land while
643
+ // the request is loading.
644
+ repository.manual = true;
645
+ manager.retrySubmission();
646
+ manager.retrySubmission(); // double-tap — ignored while in flight
647
+
648
+ await flushMicrotasks();
649
+ expect(manager.jobSubmissionState.kind).toBe("loading");
650
+ // Exactly one new submission: the initial failure plus one retry.
651
+ expect(repository.received).toHaveLength(2);
652
+ });
653
+
605
654
  it("does not update state if the manager is disposed mid-submission", async () => {
606
655
  const repository = new StubRepository();
607
656
  repository.manual = true;
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import * as ScreenOrientation from "expo-screen-orientation";
3
+ import { GlobalPrivacyControl } from "../../../../../core/models/global_privacy_control";
4
+ import { UseSmileIDConfigBuilder } from "../config_builder";
5
+
6
+ describe("UseSmileIDConfigBuilder", () => {
7
+ it("snapshots the documented defaults", () => {
8
+ expect(new UseSmileIDConfigBuilder().build()).toEqual({
9
+ enableCrashReporting: true,
10
+ enableDebugMode: false,
11
+ allowOfflineMode: false,
12
+ globalPrivacyControl: GlobalPrivacyControl.OptOut,
13
+ exitOrientations: ScreenOrientation.OrientationLock.PORTRAIT_UP,
14
+ });
15
+ });
16
+
17
+ it("carries an overridden exit orientation lock into the snapshot", () => {
18
+ const builder = new UseSmileIDConfigBuilder();
19
+ builder.exitOrientations = ScreenOrientation.OrientationLock.DEFAULT;
20
+
21
+ expect(builder.build().exitOrientations).toBe(
22
+ ScreenOrientation.OrientationLock.DEFAULT,
23
+ );
24
+ });
25
+ });
@@ -1,4 +1,6 @@
1
+ import * as ScreenOrientation from "expo-screen-orientation";
1
2
  import type { ConfigOptions } from "../../../../core/models/config";
3
+ import { GlobalPrivacyControl } from "../../../../core/models/global_privacy_control";
2
4
 
3
5
  /**
4
6
  * `b.config((c) => { c.enableCrashReporting = false; ... })`
@@ -26,11 +28,30 @@ export class UseSmileIDConfigBuilder {
26
28
  */
27
29
  allowOfflineMode = false;
28
30
 
31
+ /**
32
+ * Consent posture for every session (`global_privacy_control`). Defaults
33
+ * to `GlobalPrivacyControl.OptOut` — the opt-out consent layout is
34
+ * enforced universally and regional detection is ignored. Set
35
+ * `GlobalPrivacyControl.OptIn` to keep the legacy regional mechanics
36
+ * (opt-in inside the EU, opt-out elsewhere).
37
+ */
38
+ globalPrivacyControl: GlobalPrivacyControl = GlobalPrivacyControl.OptOut;
39
+
40
+ /**
41
+ * Orientation lock applied when a capture screen leaves the screen.
42
+ * Defaults to portrait-up; set `ScreenOrientation.OrientationLock.DEFAULT`
43
+ * if your app supports more than one orientation.
44
+ */
45
+ exitOrientations: ScreenOrientation.OrientationLock =
46
+ ScreenOrientation.OrientationLock.PORTRAIT_UP;
47
+
29
48
  build(): ConfigOptions {
30
49
  return Object.freeze({
31
50
  enableCrashReporting: this.enableCrashReporting,
32
51
  enableDebugMode: this.enableDebugMode,
33
52
  allowOfflineMode: this.allowOfflineMode,
53
+ globalPrivacyControl: this.globalPrivacyControl,
54
+ exitOrientations: this.exitOrientations,
34
55
  });
35
56
  }
36
57
  }
@@ -89,8 +89,9 @@ export class InstructionsConfigBuilder {
89
89
 
90
90
  export class SelfieCaptureConfigBuilder {
91
91
  /**
92
- * When true, lets an assisting agent flip to the rear camera to capture
93
- * the subject. Defaults to false.
92
+ * When true, the whole selfie session captures on the rear camera so an
93
+ * assisting agent can point it at the subject. Cannot be combined with
94
+ * {@link enableEnhancedLiveness}.
94
95
  */
95
96
  allowAgentMode = false;
96
97
  /**
@@ -180,10 +181,6 @@ export class PreviewConfigBuilder {
180
181
  * true.
181
182
  */
182
183
  showAttribution = true;
183
- /** Provides a custom continue button in place of the default. */
184
- continueButton?: ButtonSlot;
185
- /** Provides a custom cancel button in place of the default. */
186
- cancelButton?: ButtonSlot;
187
184
  }
188
185
 
189
186
  export class ProcessingConfigBuilder {
@@ -693,9 +693,14 @@ export class FlowNavigationManager {
693
693
  void this._submit();
694
694
  }
695
695
 
696
- /** Reset to `notAttempted` and re-fire submission (processing screen's onRetry). */
696
+ /**
697
+ * Reset to `notAttempted` and re-fire submission (processing screen's
698
+ * onRetry). Ignored while a submission is in flight — resetting then would
699
+ * defeat `beginSubmissionIfNeeded()`'s single-flight guard.
700
+ */
697
701
  retrySubmission(): void {
698
702
  if (this._disposed) return;
703
+ if (this._jobSubmissionState.kind === "loading") return;
699
704
  this._setSubmissionState({ kind: "notAttempted" });
700
705
  this.beginSubmissionIfNeeded();
701
706
  }
@@ -31,7 +31,7 @@ export const SELFIE_HERO_ANIMATION =
31
31
  require("../../assets/si_selfie_hero_animation.json") as AnimationObject;
32
32
  // eslint-disable-next-line @typescript-eslint/no-require-imports
33
33
  export const ESS_HERO_ANIMATION =
34
- require("../../assets/si_ess_animation_all_directions.json") as AnimationObject;
34
+ require("../../assets/si_ess_all_directions_hero_animation.json") as AnimationObject;
35
35
 
36
36
  // Document hero animations — per-document-type front instruction heroes, plus
37
37
  // the back-of-ID (flip) hero shown on the back-capture interstitial.
@@ -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();
@@ -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,
@@ -147,6 +147,10 @@ export function FlowSuccessRenderer({
147
147
  useUseSmileIDFonts();
148
148
  const enableCrashReporting =
149
149
  configuration.config?.enableCrashReporting !== false;
150
+ // Sandbox sessions are test traffic and must not pollute the production
151
+ // Sentry project; the reporter also drops debug bundles internally.
152
+ const useSandbox =
153
+ configuration.network?.config?.partnerConfig?.useSandbox === true;
150
154
 
151
155
  // Per-flow session id used as the Sentry user-id so every event
152
156
  // captured during this verification flow correlates under one issue
@@ -160,7 +164,10 @@ export function FlowSuccessRenderer({
160
164
 
161
165
  useEffect(() => {
162
166
  if (enableCrashReporting) {
163
- UseSmileIDCrashReporting.enable();
167
+ // Tear down any prior client first so a changed useSandbox flag takes
168
+ // effect — enable() short-circuits when already enabled.
169
+ UseSmileIDCrashReporting.disable();
170
+ UseSmileIDCrashReporting.enable({ useSandbox });
164
171
  UseSmileIDCrashReporting.setUserId(flowSessionIdRef.current!);
165
172
  UseSmileIDCrashReporting.captureBreadcrumb({
166
173
  category: "flow.navigation",
@@ -170,7 +177,7 @@ export function FlowSuccessRenderer({
170
177
  return () => {
171
178
  UseSmileIDCrashReporting.disable();
172
179
  };
173
- }, [enableCrashReporting]);
180
+ }, [enableCrashReporting, useSandbox]);
174
181
 
175
182
  // `onResult` lives in a ref so the manager's `onExit` closure
176
183
  // captures the latest partner callback without recreating the
@@ -20,7 +20,10 @@ import {
20
20
  InvalidJobTypeScreenConfigException,
21
21
  MissingMLConfigurationBlockException,
22
22
  } from "../../../../core/exception/job_validation_exceptions";
23
- import { InvalidDocumentCaptureConfigException } from "../../../../core/exception/capture_validation_exceptions";
23
+ import {
24
+ AgentModeWithEnhancedLivenessException,
25
+ InvalidDocumentCaptureConfigException,
26
+ } from "../../../../core/exception/capture_validation_exceptions";
24
27
  import {
25
28
  runDocumentVerificationParamsValidation,
26
29
  runEnhancedDocumentVerificationParamsValidation,
@@ -189,6 +192,63 @@ describe("FlowValidator — document capture config", () => {
189
192
  });
190
193
  });
191
194
 
195
+ describe("FlowValidator — selfie capture config", () => {
196
+ function selfieCaptureIssues(
197
+ configure: (s: {
198
+ allowAgentMode: boolean;
199
+ enableEnhancedLiveness: boolean;
200
+ }) => void,
201
+ ) {
202
+ const config = new CaptureConfigBuilder();
203
+ config.captureType = CaptureType.selfie;
204
+ config.selfie(configure);
205
+ return runFlowValidation({
206
+ steps: [{ kind: "capture" as const, config }, processingStep()],
207
+ });
208
+ }
209
+
210
+ function agentModeIssues(
211
+ configure: (s: {
212
+ allowAgentMode: boolean;
213
+ enableEnhancedLiveness: boolean;
214
+ }) => void,
215
+ ): AgentModeWithEnhancedLivenessException[] {
216
+ return selfieCaptureIssues(configure).filter(
217
+ (i): i is AgentModeWithEnhancedLivenessException =>
218
+ i instanceof AgentModeWithEnhancedLivenessException,
219
+ );
220
+ }
221
+
222
+ const rejectsAgentModePair = (
223
+ configure: Parameters<typeof agentModeIssues>[0],
224
+ ): boolean => agentModeIssues(configure).length > 0;
225
+
226
+ it("rejects agent mode combined with enhanced liveness", () => {
227
+ const issues = agentModeIssues((s) => {
228
+ s.allowAgentMode = true;
229
+ s.enableEnhancedLiveness = true;
230
+ });
231
+ expect(issues).toHaveLength(1);
232
+ // Wire string partner tooling pivots on — a rename is a breaking change.
233
+ expect(issues[0]?.errorCode).toBe(
234
+ "BUILDER_AGENT_MODE_WITH_ENHANCED_LIVENESS",
235
+ );
236
+ });
237
+
238
+ it("accepts agent mode and enhanced liveness on their own", () => {
239
+ expect(
240
+ rejectsAgentModePair((s) => {
241
+ s.allowAgentMode = true;
242
+ }),
243
+ ).toBe(false);
244
+ expect(
245
+ rejectsAgentModePair((s) => {
246
+ s.enableEnhancedLiveness = true;
247
+ }),
248
+ ).toBe(false);
249
+ });
250
+ });
251
+
192
252
  describe("FlowValidator — analyzer-per-capture-type", () => {
193
253
  for (const { name, value: jobType } of SELFIE_JOB_TYPES) {
194
254
  it(`${name}: missing ml block emits "is required" + MissingMLConfigurationBlockException`, () => {
@@ -10,7 +10,11 @@ import type {
10
10
  UserDetails,
11
11
  } from "../../../core/models/params";
12
12
  import { JobType } from "../../../core/models/job_type";
13
- import type { ScreenSpec } from "../dsl/builder/screens_builder";
13
+ import type {
14
+ DocumentCaptureConfigBuilder,
15
+ ScreenSpec,
16
+ SelfieCaptureConfigBuilder,
17
+ } from "../dsl/builder/screens_builder";
14
18
 
15
19
  type ConsentScreenStep = Extract<ScreenSpec, { kind: "consent" }>;
16
20
  import { IssueSeverity } from "../../../core/exception/issue_severity";
@@ -25,6 +29,7 @@ import {
25
29
  MissingNetworkConfigurationBlockException,
26
30
  } from "../../../core/exception/job_validation_exceptions";
27
31
  import {
32
+ AgentModeWithEnhancedLivenessException,
28
33
  InvalidDocumentCaptureConfigException,
29
34
  InvalidMissingCaptureTypeException,
30
35
  } from "../../../core/exception/capture_validation_exceptions";
@@ -69,6 +74,51 @@ type FlowValidatorInput = {
69
74
  readonly userDetails?: UserDetails | undefined;
70
75
  };
71
76
 
77
+ /**
78
+ * Agent mode captures on the rear lens while the head-turn challenge is only
79
+ * validated for front-camera framing, so the pair is refused rather than
80
+ * silently dropping one of them.
81
+ */
82
+ function selfieConfigIssues(
83
+ config: SelfieCaptureConfigBuilder | undefined,
84
+ ): ValidationIssue[] {
85
+ if (config?.allowAgentMode && config.enableEnhancedLiveness) {
86
+ return [new AgentModeWithEnhancedLivenessException()];
87
+ }
88
+ return [];
89
+ }
90
+
91
+ /** Surfaces at most one issue: missing block > missing type > bad aspect ratio. */
92
+ function documentConfigIssues(
93
+ config: DocumentCaptureConfigBuilder | undefined,
94
+ ): ValidationIssue[] {
95
+ if (config === undefined) {
96
+ return [
97
+ new InvalidDocumentCaptureConfigException({
98
+ reason: "document is required when mode is Document",
99
+ }),
100
+ ];
101
+ }
102
+ if (config.documentType === undefined) {
103
+ return [
104
+ new InvalidDocumentCaptureConfigException({
105
+ reason: "documentType is required for document capture",
106
+ }),
107
+ ];
108
+ }
109
+ if (
110
+ config.knownIdAspectRatio !== undefined &&
111
+ config.knownIdAspectRatio <= 0
112
+ ) {
113
+ return [
114
+ new InvalidDocumentCaptureConfigException({
115
+ reason: "knownIdAspectRatio must be > 0 if provided",
116
+ }),
117
+ ];
118
+ }
119
+ return [];
120
+ }
121
+
72
122
  /**
73
123
  * Quick triage of the in-progress builder state. Today the only rule
74
124
  * surfaced is the empty-screens-block error — `b.screens(...)` must
@@ -113,32 +163,11 @@ export function runFlowValidation(
113
163
  if (step.config.captureType === undefined) {
114
164
  issues.push(new InvalidMissingCaptureTypeException());
115
165
  }
116
- // Document captures carry required config: surface at most one issue per
117
- // step (missing block > missing type > bad aspect ratio).
166
+ if (step.config.captureType === CaptureType.selfie) {
167
+ issues.push(...selfieConfigIssues(step.config.selfieConfig));
168
+ }
118
169
  if (step.config.captureType === CaptureType.document) {
119
- const documentConfig = step.config.documentConfig;
120
- if (documentConfig === undefined) {
121
- issues.push(
122
- new InvalidDocumentCaptureConfigException({
123
- reason: "document is required when mode is Document",
124
- }),
125
- );
126
- } else if (documentConfig.documentType === undefined) {
127
- issues.push(
128
- new InvalidDocumentCaptureConfigException({
129
- reason: "documentType is required for document capture",
130
- }),
131
- );
132
- } else if (
133
- documentConfig.knownIdAspectRatio !== undefined &&
134
- documentConfig.knownIdAspectRatio <= 0
135
- ) {
136
- issues.push(
137
- new InvalidDocumentCaptureConfigException({
138
- reason: "knownIdAspectRatio must be > 0 if provided",
139
- }),
140
- );
141
- }
170
+ issues.push(...documentConfigIssues(step.config.documentConfig));
142
171
  }
143
172
  }
144
173
  }