@smileid/usesmileid 12.0.1 → 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 (160) hide show
  1. package/CHANGELOG.md +4 -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/flow_exceptions.d.ts +4 -2
  8. package/dist/core/exception/flow_exceptions.d.ts.map +1 -1
  9. package/dist/core/exception/flow_exceptions.js +3 -2
  10. package/dist/core/exception/flow_exceptions.js.map +1 -1
  11. package/dist/core/exception/missing_job_type_exception.d.ts +1 -3
  12. package/dist/core/exception/missing_job_type_exception.d.ts.map +1 -1
  13. package/dist/core/exception/missing_job_type_exception.js +2 -4
  14. package/dist/core/exception/missing_job_type_exception.js.map +1 -1
  15. package/dist/data/api/interceptors/smile_auth_interceptor.js +1 -1
  16. package/dist/data/api/interceptors/smile_auth_interceptor.js.map +1 -1
  17. package/dist/data/api/smile_id_api.d.ts +4 -0
  18. package/dist/data/api/smile_id_api.d.ts.map +1 -1
  19. package/dist/data/api/smile_id_api.js +30 -27
  20. package/dist/data/api/smile_id_api.js.map +1 -1
  21. package/dist/data/model/job_request.d.ts +28 -12
  22. package/dist/data/model/job_request.d.ts.map +1 -1
  23. package/dist/data/model/token_payload.d.ts +78 -0
  24. package/dist/data/model/token_payload.d.ts.map +1 -0
  25. package/dist/data/model/token_payload.js +132 -0
  26. package/dist/data/model/token_payload.js.map +1 -0
  27. package/dist/data/model/user_details_payload.d.ts +39 -0
  28. package/dist/data/model/user_details_payload.d.ts.map +1 -0
  29. package/dist/data/model/user_details_payload.js +53 -0
  30. package/dist/data/model/user_details_payload.js.map +1 -0
  31. package/dist/data/util/use_smile_id_token_decoder.d.ts +15 -3
  32. package/dist/data/util/use_smile_id_token_decoder.d.ts.map +1 -1
  33. package/dist/data/util/use_smile_id_token_decoder.js +20 -8
  34. package/dist/data/util/use_smile_id_token_decoder.js.map +1 -1
  35. package/dist/l10n/strings_en.d.ts +4 -1
  36. package/dist/l10n/strings_en.d.ts.map +1 -1
  37. package/dist/l10n/strings_en.js +4 -1
  38. package/dist/l10n/strings_en.js.map +1 -1
  39. package/dist/l10n/use_smile_id_localizations.d.ts +10 -0
  40. package/dist/l10n/use_smile_id_localizations.d.ts.map +1 -1
  41. package/dist/l10n/use_smile_id_localizations.js +59 -3
  42. package/dist/l10n/use_smile_id_localizations.js.map +1 -1
  43. package/dist/metadata/metadata_configuration_mappers.js +1 -1
  44. package/dist/metadata/metadata_configuration_mappers.js.map +1 -1
  45. package/dist/metadata/use_smile_id_metadata_factory.js +1 -1
  46. package/dist/metadata/use_smile_id_metadata_factory.js.map +1 -1
  47. package/dist/presentation/components/use_smile_id_button.d.ts.map +1 -1
  48. package/dist/presentation/components/use_smile_id_button.js +8 -2
  49. package/dist/presentation/components/use_smile_id_button.js.map +1 -1
  50. package/dist/presentation/flow/dsl/builder/screens_builder.d.ts +3 -6
  51. package/dist/presentation/flow/dsl/builder/screens_builder.d.ts.map +1 -1
  52. package/dist/presentation/flow/dsl/builder/screens_builder.js +4 -6
  53. package/dist/presentation/flow/dsl/builder/screens_builder.js.map +1 -1
  54. package/dist/presentation/flow/dsl/use_smile_id_flow_builder.d.ts.map +1 -1
  55. package/dist/presentation/flow/dsl/use_smile_id_flow_builder.js +23 -1
  56. package/dist/presentation/flow/dsl/use_smile_id_flow_builder.js.map +1 -1
  57. package/dist/presentation/flow/flow_navigation_manager.d.ts +17 -2
  58. package/dist/presentation/flow/flow_navigation_manager.d.ts.map +1 -1
  59. package/dist/presentation/flow/flow_navigation_manager.js +77 -45
  60. package/dist/presentation/flow/flow_navigation_manager.js.map +1 -1
  61. package/dist/presentation/flow/screen/orchestrated_processing_screen.d.ts.map +1 -1
  62. package/dist/presentation/flow/screen/orchestrated_processing_screen.js +25 -5
  63. package/dist/presentation/flow/screen/orchestrated_processing_screen.js.map +1 -1
  64. package/dist/presentation/flow/use_smile_id_flow.d.ts.map +1 -1
  65. package/dist/presentation/flow/use_smile_id_flow.js +35 -8
  66. package/dist/presentation/flow/use_smile_id_flow.js.map +1 -1
  67. package/dist/presentation/flow/validation/flow_validator.d.ts +16 -2
  68. package/dist/presentation/flow/validation/flow_validator.d.ts.map +1 -1
  69. package/dist/presentation/flow/validation/flow_validator.js +19 -27
  70. package/dist/presentation/flow/validation/flow_validator.js.map +1 -1
  71. package/dist/presentation/flow/validation/job_type_validator.d.ts +7 -0
  72. package/dist/presentation/flow/validation/job_type_validator.d.ts.map +1 -1
  73. package/dist/presentation/flow/validation/job_type_validator.js +52 -27
  74. package/dist/presentation/flow/validation/job_type_validator.js.map +1 -1
  75. package/dist/presentation/flow/validation/user_details_validator.d.ts +9 -0
  76. package/dist/presentation/flow/validation/user_details_validator.d.ts.map +1 -0
  77. package/dist/presentation/flow/validation/user_details_validator.js +40 -0
  78. package/dist/presentation/flow/validation/user_details_validator.js.map +1 -0
  79. package/dist/presentation/screen/helpers/capture_screen_helpers.d.ts.map +1 -1
  80. package/dist/presentation/screen/helpers/capture_screen_helpers.js +1 -0
  81. package/dist/presentation/screen/helpers/capture_screen_helpers.js.map +1 -1
  82. package/dist/presentation/screen/use_smile_id_capture_screen.d.ts.map +1 -1
  83. package/dist/presentation/screen/use_smile_id_capture_screen.js +64 -18
  84. package/dist/presentation/screen/use_smile_id_capture_screen.js.map +1 -1
  85. package/package.json +3 -3
  86. package/src/__tests__/react_native_mock.tsx +22 -0
  87. package/src/__tests__/snapshots/__snapshots__/screens_structural.test.tsx.snap +14 -8
  88. package/src/camera/__tests__/camera_permissions.test.ts +72 -0
  89. package/src/camera/__tests__/camera_system_focus.test.ts +173 -0
  90. package/src/camera/__tests__/use_smile_id_camera_preview.test.tsx +461 -0
  91. package/src/camera/__tests__/use_smile_id_frame_stream_hook.test.tsx +229 -0
  92. package/src/camera/camera_defaults.ts +3 -0
  93. package/src/core/exception/__tests__/builder_validation_exceptions.test.ts +14 -0
  94. package/src/core/exception/flow_exceptions.test.ts +10 -0
  95. package/src/core/exception/flow_exceptions.ts +3 -2
  96. package/src/core/exception/missing_job_type_exception.ts +2 -4
  97. package/src/data/api/__tests__/smile_id_api.test.ts +275 -1
  98. package/src/data/api/interceptors/__tests__/credential_header_redaction.test.ts +144 -0
  99. package/src/data/api/interceptors/smile_auth_interceptor.ts +1 -1
  100. package/src/data/api/smile_id_api.ts +36 -49
  101. package/src/data/dsl/__tests__/use_smile_id_network_builder.test.ts +157 -0
  102. package/src/data/factory/__tests__/signed_job_submission_headers.test.ts +1 -1
  103. package/src/data/model/__tests__/token_payload.test.ts +224 -0
  104. package/src/data/model/__tests__/user_details_payload.test.ts +100 -0
  105. package/src/data/model/job_request.ts +27 -12
  106. package/src/data/model/token_payload.ts +182 -0
  107. package/src/data/model/user_details_payload.ts +84 -0
  108. package/src/data/repository/__tests__/network_retries.test.ts +1 -1
  109. package/src/data/repository/__tests__/repository_dispatch.test.ts +366 -0
  110. package/src/data/repository/__tests__/use_smile_id_repository.test.ts +1 -1
  111. package/src/data/security/__tests__/signing_crypto.test.ts +203 -0
  112. package/src/data/util/__tests__/use_smile_id_token_decoder.test.ts +179 -0
  113. package/src/data/util/use_smile_id_token_decoder.ts +29 -8
  114. package/src/l10n/__tests__/use_smile_id_localizations.test.ts +122 -5
  115. package/src/l10n/__tests__/use_smile_id_localizations_provider.test.tsx +112 -0
  116. package/src/l10n/__tests__/use_smile_id_localizations_surface.test.ts +231 -0
  117. package/src/l10n/strings_en.ts +5 -1
  118. package/src/l10n/use_smile_id_localizations.ts +80 -3
  119. package/src/metadata/__tests__/device_orientation_sampling.test.ts +165 -0
  120. package/src/metadata/__tests__/geolocation.test.ts +150 -0
  121. package/src/metadata/__tests__/metadata_configuration_objects.test.ts +1 -1
  122. package/src/metadata/__tests__/metadata_device_context.test.ts +3 -3
  123. package/src/metadata/__tests__/metadata_golden_fixture.json +1 -1
  124. package/src/metadata/__tests__/metadata_payload_golden.test.ts +4 -4
  125. package/src/metadata/__tests__/optional_native_modules.test.ts +234 -0
  126. package/src/metadata/__tests__/use_smile_id_metadata.test.ts +1 -1
  127. package/src/metadata/__tests__/use_smile_id_metadata_factory.test.ts +615 -0
  128. package/src/metadata/metadata_configuration_mappers.ts +1 -1
  129. package/src/metadata/use_smile_id_metadata_factory.ts +1 -1
  130. package/src/presentation/components/__tests__/use_smile_id_content_card.test.tsx +112 -0
  131. package/src/presentation/components/__tests__/use_smile_id_face_oval_image.test.tsx +190 -0
  132. package/src/presentation/components/__tests__/use_smile_id_learn_more_content.test.tsx +159 -0
  133. package/src/presentation/components/__tests__/use_smile_id_processing_center_indicator.test.tsx +88 -0
  134. package/src/presentation/components/use_smile_id_button.tsx +7 -2
  135. package/src/presentation/flow/__tests__/flow_navigation_manager.test.ts +323 -1
  136. package/src/presentation/flow/__tests__/use_smile_id_flow.test.tsx +651 -0
  137. package/src/presentation/flow/dsl/__tests__/use_smile_id_builder_component.test.tsx +120 -0
  138. package/src/presentation/flow/dsl/builder/screens_builder.ts +4 -6
  139. package/src/presentation/flow/dsl/use_smile_id_flow_builder.ts +27 -1
  140. package/src/presentation/flow/flow_navigation_manager.ts +102 -92
  141. package/src/presentation/flow/graph/__tests__/screen_renderer.test.tsx +102 -0
  142. package/src/presentation/flow/screen/__tests__/orchestrated_capture_screen.test.tsx +836 -0
  143. package/src/presentation/flow/screen/__tests__/orchestrated_consent_screen.test.tsx +165 -0
  144. package/src/presentation/flow/screen/__tests__/orchestrated_instructions_screen.test.tsx +163 -0
  145. package/src/presentation/flow/screen/__tests__/orchestrated_preview_screen.test.tsx +224 -0
  146. package/src/presentation/flow/screen/__tests__/orchestrated_processing_screen.test.tsx +338 -0
  147. package/src/presentation/flow/screen/orchestrated_processing_screen.tsx +42 -5
  148. package/src/presentation/flow/use_smile_id_flow.tsx +52 -8
  149. package/src/presentation/flow/validation/__tests__/flow_validator.test.ts +158 -0
  150. package/src/presentation/flow/validation/__tests__/job_type_validator.test.ts +200 -0
  151. package/src/presentation/flow/validation/flow_validator.ts +30 -31
  152. package/src/presentation/flow/validation/job_type_validator.ts +73 -39
  153. package/src/presentation/flow/validation/user_details_validator.ts +53 -0
  154. package/src/presentation/screen/__tests__/feedback_copy_table.test.ts +145 -0
  155. package/src/presentation/screen/__tests__/use_smile_id_capture_screen.test.tsx +348 -0
  156. package/src/presentation/screen/__tests__/use_smile_id_document_back_instructions.test.tsx +3 -1
  157. package/src/presentation/screen/__tests__/use_smile_id_permission_screen.test.tsx +68 -0
  158. package/src/presentation/screen/helpers/capture_screen_helpers.ts +1 -0
  159. package/src/presentation/screen/use_smile_id_capture_screen.tsx +69 -18
  160. package/src/presentation/theme/__tests__/use_use_smile_id_fonts.test.tsx +166 -0
@@ -21,7 +21,12 @@ import {
21
21
  } from "../../../core/models/result";
22
22
  import { JobType } from "../../../core/models/job_type";
23
23
  import { DocumentType } from "../../../core/models/document_type";
24
- import { MissingRequiredFieldException } from "../../../core/exception";
24
+ import { GlobalPrivacyControl } from "../../../core/models/global_privacy_control";
25
+ import {
26
+ MissingJobTypeException,
27
+ MissingRequiredFieldException,
28
+ UseSmileIDErrorCode,
29
+ } from "../../../core/exception";
25
30
  import type { UseSmileIDAnalyticsEvent } from "../analytics/use_smile_id_analytics_event";
26
31
 
27
32
  function buildScreens(block: (s: ScreensBuilder) => void): ScreensConfig {
@@ -528,6 +533,41 @@ describe("FlowNavigationManager — submission lifecycle", () => {
528
533
  expect(manager.jobSubmissionState.kind).toBe("failure");
529
534
  });
530
535
 
536
+ it("an unrecognised jobType fails submission with the typed builder exception", async () => {
537
+ const repository = new StubRepository();
538
+ const manager = makeManager(
539
+ buildScreens((screens) => screens.processing()),
540
+ vi.fn(),
541
+ {
542
+ userDetails: SAMPLE_USER_DETAILS,
543
+ network: {
544
+ config: {
545
+ jobType: 99 as JobType,
546
+ token: "k",
547
+ partnerConfig: {
548
+ partnerId: "p",
549
+ callbackUrl: "",
550
+ useSandbox: true,
551
+ },
552
+ },
553
+ },
554
+ },
555
+ repository,
556
+ );
557
+
558
+ manager.beginSubmissionIfNeeded();
559
+ await flushMicrotasks();
560
+ expect(manager.jobSubmissionState.kind).toBe("failure");
561
+ if (manager.jobSubmissionState.kind === "failure") {
562
+ expect(manager.jobSubmissionState.error).toBeInstanceOf(
563
+ MissingJobTypeException,
564
+ );
565
+ expect(
566
+ (manager.jobSubmissionState.error as MissingJobTypeException).errorCode,
567
+ ).toBe(UseSmileIDErrorCode.Builder.MissingJobType);
568
+ }
569
+ });
570
+
531
571
  it("beginSubmissionIfNeeded is idempotent — second call while loading is a no-op", async () => {
532
572
  const repository = new StubRepository();
533
573
  repository.manual = true;
@@ -938,6 +978,288 @@ describe("FlowNavigationManager — request assembly per JobType", () => {
938
978
  });
939
979
  });
940
980
 
981
+ describe("FlowNavigationManager — token-provided user details & consent (assembly)", () => {
982
+ // Mint a structurally valid JWT with a caller-supplied payload — only
983
+ // the base64url-encoded middle segment is decoded.
984
+ function makeJwt(payload: Record<string, unknown>): string {
985
+ const payloadSegment = Buffer.from(JSON.stringify(payload)).toString(
986
+ "base64url",
987
+ );
988
+ return `eyJhbGciOiJub25lIn0.${payloadSegment}.stub-sig`;
989
+ }
990
+
991
+ const completeConsentClaim = {
992
+ granted: true,
993
+ granted_at: "2026-01-01T09:00:00.000Z",
994
+ notice_language: "EN",
995
+ notice_privacy_policy_url: "https://partner.test/privacy",
996
+ } as const;
997
+
998
+ const fullBindingToken = makeJwt({
999
+ payload: {
1000
+ given_names: "pii_abc",
1001
+ last_name: "pii_def",
1002
+ email: "pii_ghi",
1003
+ phone_number: "pii_jkl",
1004
+ consent: completeConsentClaim,
1005
+ },
1006
+ });
1007
+
1008
+ // Real tokens minted by the /v3/token deployment (same fixtures as
1009
+ // use_smile_id_token_decoder.test.ts): ES256-signed, expired minutes
1010
+ // after minting, PII values as opaque vault references. Decoded through
1011
+ // the production decoder so the whole flow — decode, screen skip,
1012
+ // assembly reduction — runs exactly as in production.
1013
+ const MINTED_FULL_BINDING_TOKEN =
1014
+ "eyJhbGciOiJFUzI1NiIsImtpZCI6ImtleV8wMWtibWd6bXczZXloOW5jdjlqcG1kNDlqZiIsInR5cCI6IkpXVCJ9." +
1015
+ "eyJhcGlfdXJsIjoiaHR0cHM6Ly92My10b2tlbi1jb25zZW50LXBheWxvYWQuYXBpLnNtaWxlaWQuY28vdjMiLCJrZXlfaWQiOiIyY2NjOWMyMy01Y2U3LTRkMGUtYWRkYy1lN2I1NGQ4YjkxZGQiLCJwYXlsb2FkIjp7ImNvbnNlbnQiOnsiZ3JhbnRlZCI6dHJ1ZSwiZ3JhbnRlZF9hdCI6IjIwMjYtMDEtMDFUMDk6MDA6MDAuMDAwWiIsIm5vdGljZV9sYW5ndWFnZSI6IkVOIiwibm90aWNlX3ByaXZhY3lfcG9saWN5X3VybCI6Imh0dHBzOi8vanVtYS5wcml2YWN5LXBvbGljeS5jb20ifSwiY291bnRyeSI6Ik5HIiwiaWRfdHlwZSI6Ik5JTl9WMiIsImVtYWlsIjoicGlpXzAxa3Azczl0NzhmZXg5MXFoNDZ5cDg3cmpxIiwiZ2l2ZW5fbmFtZXMiOiJwaWlfMDFrenJtMTcwdGY2OXJxOTUzOHJ6Z2V0eXoiLCJpZF9udW1iZXIiOiJwaWlfMDFra2dhYTliNWZ2Z2EyMnllZXM1ZjRyNnQiLCJsYXN0X25hbWUiOiJwaWlfMDFrcDNzOXQ3cmU4aHI2ZGVyOXEzbnB6N2MiLCJwaG9uZV9udW1iZXIiOiJwaWlfMDFrenJtdHdkeGZlenJ0dGRmOXQzc3Rzem4iLCJjYWxsYmFja191cmwiOiJjYWxsYmFja18wMWt6cm10d2VmZTV5YWgzbmR2NW03MDc3ZyJ9LCJwYXJ0bmVyX2lkIjoiMTQ4MyIsInBvbGljeSI6MTUsImF1ZCI6InNtaWxlaWQtYXBpIiwiZXhwIjoxNzg2NDYwNzEyLCJpYXQiOjE3ODY0NTk4MTIsImlzcyI6InNtaWxlaWQtYXV0aCJ9." +
1016
+ "cuZgo64eEt5vNvND_RvCVpJyTv4rqax9ebkLV4i4uI4raz6fBhazxspbaoErYC0wxwRmn5EhPAdAdDnNKI6tEQ";
1017
+
1018
+ const MINTED_NAMES_AND_EMAIL_TOKEN =
1019
+ "eyJhbGciOiJFUzI1NiIsImtpZCI6ImtleV8wMWtibWd6bXczZXloOW5jdjlqcG1kNDlqZiIsInR5cCI6IkpXVCJ9." +
1020
+ "eyJhcGlfdXJsIjoiaHR0cHM6Ly92My10b2tlbi1jb25zZW50LXBheWxvYWQuYXBpLnNtaWxlaWQuY28vdjMiLCJrZXlfaWQiOiIyY2NjOWMyMy01Y2U3LTRkMGUtYWRkYy1lN2I1NGQ4YjkxZGQiLCJwYXlsb2FkIjp7ImNvbnNlbnQiOnsiZ3JhbnRlZCI6dHJ1ZSwiZ3JhbnRlZF9hdCI6IjIwMjYtMDEtMDFUMDk6MDA6MDAuMDAwWiIsIm5vdGljZV9sYW5ndWFnZSI6IkVOIiwibm90aWNlX3ByaXZhY3lfcG9saWN5X3VybCI6Imh0dHBzOi8vanVtYS5wcml2YWN5LXBvbGljeS5jb20ifSwiZW1haWwiOiJwaWlfMDFrcDNzOXQ3OGZleDkxcWg0NnlwODdyanEiLCJnaXZlbl9uYW1lcyI6InBpaV8wMWt6cm0xNzB0ZjY5cnE5NTM4cnpnZXR5eiIsImxhc3RfbmFtZSI6InBpaV8wMWtwM3M5dDdyZThocjZkZXI5cTNucHo3YyJ9LCJwYXJ0bmVyX2lkIjoiMTQ4MyIsInBvbGljeSI6MTUsImF1ZCI6InNtaWxlaWQtYXBpIiwiZXhwIjoxNzg2NDU5ODcxLCJpYXQiOjE3ODY0NTg5NzEsImlzcyI6InNtaWxlaWQtYXV0aCJ9." +
1021
+ "s5BFthU2B1aKxYDYyyWYDmRpLoHibH34r2Lk4OS-wZQC8pmYROjWBiORx6n2_0srmxz_SA2eZo_FHyAr-ew_Zg";
1022
+
1023
+ function assemble(
1024
+ token: string,
1025
+ extraConfig: Partial<FlowConfigurationSnapshot> = {},
1026
+ seedManager: (m: FlowNavigationManager) => void = () => {},
1027
+ ): {
1028
+ readonly repository: StubRepository;
1029
+ readonly manager: FlowNavigationManager;
1030
+ } {
1031
+ const repository = new StubRepository();
1032
+ const manager = makeManager(
1033
+ buildScreens((screens) => screens.processing()),
1034
+ vi.fn(),
1035
+ {
1036
+ enhancedKYCParams: {
1037
+ idType: "PASSPORT",
1038
+ idNumber: "X1",
1039
+ country: "NG",
1040
+ },
1041
+ ...extraConfig,
1042
+ network: {
1043
+ config: {
1044
+ jobType: JobType.enhancedKyc,
1045
+ token,
1046
+ partnerConfig: {
1047
+ partnerId: "p",
1048
+ callbackUrl: "",
1049
+ useSandbox: true,
1050
+ },
1051
+ },
1052
+ },
1053
+ },
1054
+ repository,
1055
+ );
1056
+ seedManager(manager);
1057
+ manager.beginSubmissionIfNeeded();
1058
+ return { repository, manager };
1059
+ }
1060
+
1061
+ it("assembles without userDetails when the token binds names + a contact field", () => {
1062
+ const { repository } = assemble(fullBindingToken);
1063
+ expect(repository.received).toHaveLength(1);
1064
+ const request = repository.received[0]!;
1065
+ expect(request.userDetails).toBeUndefined();
1066
+ });
1067
+
1068
+ it("omits the consent record when token consent is complete and no local source exists", () => {
1069
+ // The DEFAULT_CONSENT granted:false placeholder must NOT be emitted
1070
+ // on the token-complete path — the server injects the token's consent.
1071
+ const { repository } = assemble(fullBindingToken, {
1072
+ userDetails: SAMPLE_USER_DETAILS,
1073
+ });
1074
+ const request = repository.received[0]!;
1075
+ expect(request.consent).toBeUndefined();
1076
+ });
1077
+
1078
+ it("still throws MissingRequiredFieldException(userDetails) when the token binds names only", async () => {
1079
+ const namesOnlyToken = makeJwt({
1080
+ payload: { given_names: "pii_abc", last_name: "pii_def" },
1081
+ });
1082
+ const { repository, manager } = assemble(namesOnlyToken);
1083
+ await flushMicrotasks();
1084
+ expect(repository.received).toHaveLength(0);
1085
+ const state = manager.jobSubmissionState;
1086
+ expect(state.kind).toBe("failure");
1087
+ if (state.kind === "failure") {
1088
+ expect(state.error).toBeInstanceOf(MissingRequiredFieldException);
1089
+ expect(
1090
+ (state.error as MissingRequiredFieldException).errorDetails,
1091
+ ).toEqual({ fieldName: "userDetails" });
1092
+ }
1093
+ });
1094
+
1095
+ it("keeps the granted:false placeholder when token consent is only partial", () => {
1096
+ const partialConsentToken = makeJwt({
1097
+ payload: {
1098
+ given_names: "pii_abc",
1099
+ last_name: "pii_def",
1100
+ email: "pii_ghi",
1101
+ consent: { granted: true, granted_at: "2026-01-01T09:00:00.000Z" },
1102
+ },
1103
+ });
1104
+ const { repository } = assemble(partialConsentToken);
1105
+ const request = repository.received[0]!;
1106
+ expect(request.consent).toEqual({
1107
+ granted: false,
1108
+ grantedAt: "",
1109
+ noticeLanguage: "",
1110
+ noticePrivacyPolicyUrl: "",
1111
+ });
1112
+ });
1113
+
1114
+ it("a complete token consent omits the consent record even when a local one exists", () => {
1115
+ // Token data always wins: even a captured local consent is not sent —
1116
+ // the part is omitted and the server injects the token's record.
1117
+ const { repository } = assemble(
1118
+ fullBindingToken,
1119
+ { userDetails: SAMPLE_USER_DETAILS },
1120
+ (m) => m.setConsent(consentInfo),
1121
+ );
1122
+ const request = repository.received[0]!;
1123
+ expect(request.consent).toBeUndefined();
1124
+ });
1125
+
1126
+ it("a complete token consent drops the consent screen from the flow", () => {
1127
+ const repository = new StubRepository();
1128
+ const manager = makeManager(
1129
+ buildScreens((screens) => {
1130
+ screens.consent((c) => {
1131
+ c.partnerName = "Acme";
1132
+ c.partnerPrivacyPolicyUrl = "https://example.test/privacy";
1133
+ });
1134
+ screens.capture((c) => {
1135
+ c.captureType = CaptureType.selfie;
1136
+ });
1137
+ }),
1138
+ vi.fn(),
1139
+ {
1140
+ userDetails: SAMPLE_USER_DETAILS,
1141
+ network: {
1142
+ config: {
1143
+ jobType: JobType.smartSelfieEnrollment,
1144
+ token: fullBindingToken,
1145
+ partnerConfig: {
1146
+ partnerId: "p",
1147
+ callbackUrl: "",
1148
+ useSandbox: true,
1149
+ },
1150
+ },
1151
+ },
1152
+ },
1153
+ repository,
1154
+ );
1155
+ expect(manager.currentScreen?.kind).toBe("capture");
1156
+ expect(manager.navigationPath.some((step) => step.kind === "consent")).toBe(
1157
+ false,
1158
+ );
1159
+ });
1160
+
1161
+ it("a preset consentInformation is still sent in full when token consent is partial", () => {
1162
+ const partialConsentToken = makeJwt({
1163
+ payload: { consent: { granted: true } },
1164
+ });
1165
+ const { repository } = assemble(partialConsentToken, {
1166
+ userDetails: SAMPLE_USER_DETAILS,
1167
+ consentInformation: consentInfo,
1168
+ });
1169
+ const request = repository.received[0]!;
1170
+ expect(request.consent).toEqual(consentInfo);
1171
+ });
1172
+
1173
+ it("the minted full-binding token drives the flow end to end with nothing local", () => {
1174
+ const repository = new StubRepository();
1175
+ const manager = makeManager(
1176
+ buildScreens((screens) => {
1177
+ screens.consent((c) => {
1178
+ c.partnerName = "Acme";
1179
+ c.partnerPrivacyPolicyUrl = "https://example.test/privacy";
1180
+ });
1181
+ screens.capture((c) => {
1182
+ c.captureType = CaptureType.selfie;
1183
+ });
1184
+ screens.processing();
1185
+ }),
1186
+ vi.fn(),
1187
+ {
1188
+ network: {
1189
+ config: {
1190
+ jobType: JobType.smartSelfieEnrollment,
1191
+ token: MINTED_FULL_BINDING_TOKEN,
1192
+ partnerConfig: {
1193
+ partnerId: "p",
1194
+ callbackUrl: "",
1195
+ useSandbox: true,
1196
+ },
1197
+ },
1198
+ },
1199
+ },
1200
+ repository,
1201
+ );
1202
+
1203
+ expect(manager.currentScreen?.kind).toBe("capture");
1204
+ expect(manager.navigationPath.some((step) => step.kind === "consent")).toBe(
1205
+ false,
1206
+ );
1207
+
1208
+ manager.setCapturedSelfie("file:///selfie.jpg", ["file:///l1.jpg"]);
1209
+ manager.beginSubmissionIfNeeded();
1210
+
1211
+ expect(repository.received).toHaveLength(1);
1212
+ const request = repository.received[0]!;
1213
+ expect(request.userDetails).toBeUndefined();
1214
+ expect(request.consent).toBeUndefined();
1215
+ });
1216
+
1217
+ it("the minted names-and-email token reduces builder details to the unbound phone number", () => {
1218
+ const { repository } = assemble(MINTED_NAMES_AND_EMAIL_TOKEN, {
1219
+ userDetails: SAMPLE_USER_DETAILS,
1220
+ });
1221
+ const request = repository.received[0]!;
1222
+ expect(request.userDetails).toEqual({ phoneNumber: "+2348000000000" });
1223
+ expect(request.consent).toBeUndefined();
1224
+ });
1225
+
1226
+ it("regression: without a payload claim the request carries userDetails and the placeholder consent", () => {
1227
+ const legacyToken = makeJwt({ sdk_config: { livenessImageCount: 5 } });
1228
+ const { repository } = assemble(legacyToken, {
1229
+ userDetails: SAMPLE_USER_DETAILS,
1230
+ });
1231
+ const request = repository.received[0]!;
1232
+ expect(request.userDetails).toEqual(SAMPLE_USER_DETAILS);
1233
+ expect(request.tokenPayload).toBeUndefined();
1234
+ expect(request.consent).toEqual({
1235
+ granted: false,
1236
+ grantedAt: "",
1237
+ noticeLanguage: "",
1238
+ noticePrivacyPolicyUrl: "",
1239
+ });
1240
+ });
1241
+
1242
+ it("allowOfflineMode skips the token decode — strict legacy assembly applies", async () => {
1243
+ const { repository, manager } = assemble(fullBindingToken, {
1244
+ config: {
1245
+ enableCrashReporting: false,
1246
+ enableDebugMode: false,
1247
+ allowOfflineMode: true,
1248
+ globalPrivacyControl: GlobalPrivacyControl.OptOut,
1249
+ },
1250
+ });
1251
+ await flushMicrotasks();
1252
+ // Token binds everything, but offline mode never reads it — the
1253
+ // missing builder userDetails still fails assembly.
1254
+ expect(repository.received).toHaveLength(0);
1255
+ const state = manager.jobSubmissionState;
1256
+ expect(state.kind).toBe("failure");
1257
+ if (state.kind === "failure") {
1258
+ expect(state.error).toBeInstanceOf(MissingRequiredFieldException);
1259
+ }
1260
+ });
1261
+ });
1262
+
941
1263
  describe("FlowNavigationManager — typed missing-field validation (parity)", () => {
942
1264
  // Drives a submission to its failure state and returns the surfaced
943
1265
  // error so we can assert the typed `MissingRequiredFieldException`