@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
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import * as React from "react";
3
+ import { create as createRenderer, act } from "react-test-renderer";
4
+
5
+ // `vi.mock` is hoisted above the imports, so anything its factory closes over
6
+ // must be created with `vi.hoisted` or it is still in the TDZ when the factory
7
+ // runs.
8
+ const { invalidProps, successProps } = vi.hoisted(() => ({
9
+ invalidProps: { current: undefined as Record<string, unknown> | undefined },
10
+ successProps: { current: undefined as Record<string, unknown> | undefined },
11
+ }));
12
+
13
+ // Stand in for the two flow renderers so these tests exercise the
14
+ // build-and-dispatch decision the component owns, not the screen runtime
15
+ // (which has its own suites).
16
+ vi.mock("../../use_smile_id_flow", () => ({
17
+ FlowInvalidDispatcher: (props: Record<string, unknown>) => {
18
+ invalidProps.current = props;
19
+ return null;
20
+ },
21
+ FlowSuccessRenderer: (props: Record<string, unknown>) => {
22
+ successProps.current = props;
23
+ return null;
24
+ },
25
+ }));
26
+
27
+ import { UseSmileIDBuilder } from "../use_smile_id_builder";
28
+ import type { UseSmileIDFlowBuilder } from "../use_smile_id_flow_builder";
29
+
30
+ /**
31
+ * `<UseSmileIDBuilder>` runs the partner's closure exactly once per mount,
32
+ * builds the flow, and routes the outcome: a valid configuration renders the
33
+ * flow, an invalid one hands the partner's `onResult` and debug-mode flag to
34
+ * the invalid dispatcher. Re-running the closure on every render would
35
+ * restart a partner's flow mid-capture.
36
+ */
37
+
38
+ function render(element: React.ReactElement) {
39
+ let tree!: ReturnType<typeof createRenderer>;
40
+ act(() => {
41
+ tree = createRenderer(element);
42
+ });
43
+ return tree;
44
+ }
45
+
46
+ function validFlow(builder: UseSmileIDFlowBuilder): void {
47
+ builder.screens((screens) => {
48
+ screens.instructions();
49
+ });
50
+ }
51
+
52
+ describe("UseSmileIDBuilder", () => {
53
+ beforeEach(() => {
54
+ invalidProps.current = undefined;
55
+ successProps.current = undefined;
56
+ });
57
+
58
+ it("renders the flow when the configuration builds", () => {
59
+ render(<UseSmileIDBuilder builder={validFlow} />);
60
+
61
+ expect(invalidProps.current).toBeUndefined();
62
+ expect(successProps.current).toBeDefined();
63
+ const configuration = successProps.current?.configuration as {
64
+ screens: { steps: readonly unknown[] };
65
+ };
66
+ expect(configuration.screens.steps).toHaveLength(1);
67
+ });
68
+
69
+ it("routes an invalid configuration to the dispatcher with the partner callback", () => {
70
+ const onResult = vi.fn();
71
+ render(
72
+ <UseSmileIDBuilder
73
+ builder={(builder) => {
74
+ builder.onResult = onResult;
75
+ }}
76
+ />,
77
+ );
78
+
79
+ expect(successProps.current).toBeUndefined();
80
+ expect(invalidProps.current?.onResult).toBe(onResult);
81
+ expect(invalidProps.current?.enableDebugMode).toBe(false);
82
+ const result = invalidProps.current?.result as { kind: string };
83
+ expect(result.kind).toBe("invalid");
84
+ });
85
+
86
+ it("forwards the partner's debug-mode opt-in to the dispatcher", () => {
87
+ render(
88
+ <UseSmileIDBuilder
89
+ builder={(builder) => {
90
+ builder.config((config) => {
91
+ config.enableDebugMode = true;
92
+ });
93
+ }}
94
+ />,
95
+ );
96
+
97
+ expect(invalidProps.current?.enableDebugMode).toBe(true);
98
+ });
99
+
100
+ it("reports debug mode as off when the config block is never opened", () => {
101
+ render(<UseSmileIDBuilder builder={() => {}} />);
102
+
103
+ expect(invalidProps.current?.enableDebugMode).toBe(false);
104
+ });
105
+
106
+ it("runs the builder closure once per mount, not per render", () => {
107
+ const builder = vi.fn(validFlow);
108
+ const tree = render(<UseSmileIDBuilder builder={builder} />);
109
+
110
+ act(() => {
111
+ tree.update(<UseSmileIDBuilder builder={builder} />);
112
+ });
113
+ act(() => {
114
+ tree.update(<UseSmileIDBuilder builder={vi.fn(validFlow)} />);
115
+ });
116
+
117
+ expect(builder).toHaveBeenCalledTimes(1);
118
+ expect(successProps.current).toBeDefined();
119
+ });
120
+ });
@@ -29,15 +29,13 @@ export type ButtonSlot = (props: {
29
29
 
30
30
  // --- Per-screen builders ---------------------------------------------------
31
31
 
32
+ // Required fields stay optional on the type because partners assign them by mutating this builder; the flow validator rejects a missing one.
32
33
  export class ConsentConfigBuilder {
33
- /** Your company name, shown in the consent copy. */
34
+ /** Required. Your company name, shown in the consent copy. */
34
35
  partnerName = "";
35
- /**
36
- * Optional partner logo shown on the consent screen, rendered next to
37
- * the Smile ID icon at the top.
38
- */
36
+ /** Required. Your logo, shown on the consent screen next to the Smile ID icon. */
39
37
  partnerIcon?: ReactNode;
40
- /** URL of your privacy policy, linked from the consent screen. */
38
+ /** Required. URL of your privacy policy, linked from the consent screen. */
41
39
  partnerPrivacyPolicyUrl?: string;
42
40
  /**
43
41
  * Whether to show the "Powered by Smile ID" attribution. Defaults to
@@ -33,6 +33,8 @@ import {
33
33
  } from "../validation/flow_validator";
34
34
  import { ValidationState } from "../validation/validation_state";
35
35
  import type { FlowBuildResult } from "./flow_build_result";
36
+ import { decodeSmileIDToken } from "../../../data/util/use_smile_id_token_decoder";
37
+ import type { TokenPayload } from "../../../data/model/token_payload";
36
38
 
37
39
  /**
38
40
  * Top-level DSL builder.
@@ -326,6 +328,13 @@ export class UseSmileIDFlowBuilder {
326
328
  const screens = this._screens?.build();
327
329
  const network = this._network?.build();
328
330
  const mlConfig = this._ml?.build();
331
+ const config = this._config?.build();
332
+
333
+ // Decode the JWT `payload` claim (pure, synchronous) so validation
334
+ // can relax the userDetails/consent requirements the token already
335
+ // covers. Skip rules mirror FlowNavigationManager's decode: offline
336
+ // mode and a missing/blank token keep the strict legacy rules.
337
+ const tokenPayload = decodeTokenPayload(config?.allowOfflineMode, network);
329
338
 
330
339
  const issues = runFlowValidation({
331
340
  steps: screens?.steps ?? [],
@@ -338,6 +347,7 @@ export class UseSmileIDFlowBuilder {
338
347
  enhancedDocumentVerificationParams:
339
348
  this.enhancedDocumentVerificationParams,
340
349
  userDetails: this.userDetails,
350
+ tokenPayload,
341
351
  });
342
352
  const state = ValidationState.invalid(issues);
343
353
  const blocking = firstErrorIssue(state);
@@ -386,7 +396,7 @@ export class UseSmileIDFlowBuilder {
386
396
  : {}),
387
397
  ...(this.userDetails ? { userDetails: this.userDetails } : {}),
388
398
  ...(this.userId ? { userId: this.userId } : {}),
389
- ...(this._config ? { config: this._config.build() } : {}),
399
+ ...(config ? { config } : {}),
390
400
  ...(this._theme ? { theme: this._theme.build() } : {}),
391
401
  ...(network ? { network } : {}),
392
402
  ...(mlConfig ? { ml: mlConfig } : {}),
@@ -402,6 +412,22 @@ export class UseSmileIDFlowBuilder {
402
412
  }
403
413
  }
404
414
 
415
+ /**
416
+ * Decode the `payload` claim from the partner JWT for build-time
417
+ * validation. `undefined` for offline mode, no/blank token, or an
418
+ * unparseable token — every validation rule then stays exactly as
419
+ * strict as before token support existed.
420
+ */
421
+ function decodeTokenPayload(
422
+ allowOfflineMode: boolean | undefined,
423
+ network: NetworkConfig | undefined,
424
+ ): TokenPayload | undefined {
425
+ if (allowOfflineMode) return undefined;
426
+ const token = network?.config?.token;
427
+ if (!token || token.length === 0) return undefined;
428
+ return decodeSmileIDToken(token)?.tokenPayload;
429
+ }
430
+
405
431
  /**
406
432
  * Frozen snapshot produced by `UseSmileIDFlowBuilder.build()`.
407
433
  *
@@ -1,18 +1,9 @@
1
1
  import type { ScreenSpec } from "./dsl/builder/screens_builder";
2
2
  import type { FlowConfigurationSnapshot } from "./dsl/use_smile_id_flow_builder";
3
3
  import type { JobSubmissionState } from "./job_submission_state";
4
- import type {
5
- DocumentVerificationRequest,
6
- EnhancedDocumentVerificationRequest,
7
- JobRequest,
8
- } from "../../data/model/job_request";
4
+ import type { JobRequest } from "../../data/model/job_request";
9
5
  import type { UseSmileIDRepository } from "../../data/repository/use_smile_id_repository";
10
- import type {
11
- ConsentInformation,
12
- DocumentVerificationParams,
13
- EnhancedDocumentVerificationParams,
14
- UserDetails,
15
- } from "../../core/models/params";
6
+ import type { ConsentInformation, UserDetails } from "../../core/models/params";
16
7
  import { JobType } from "../../core/models/job_type";
17
8
  import { CaptureType } from "../../core/models/ml_config";
18
9
  import {
@@ -20,17 +11,17 @@ import {
20
11
  type DocumentType,
21
12
  } from "../../core/models/document_type";
22
13
  import type { JobSubmissionResponse } from "../../data/model/job_submission_response";
23
- import { MissingRequiredFieldException } from "../../core/exception/job_validation_exceptions";
14
+ import {
15
+ MissingJobTypeException,
16
+ MissingRequiredFieldException,
17
+ } from "../../core/exception/job_validation_exceptions";
24
18
  import {
25
19
  UseSmileIDAnalyticsEventTypes,
26
20
  UseSmileIDAnalyticsParamKeys,
27
21
  UseSmileIDAnalyticsScreenNames,
28
22
  type UseSmileIDAnalyticsEventType,
29
23
  } from "./analytics/use_smile_id_analytics_event";
30
- import type {
31
- MetadataEntry,
32
- UseSmileIDMetadata,
33
- } from "../../metadata/use_smile_id_metadata";
24
+ import type { MetadataEntry } from "../../metadata/use_smile_id_metadata";
34
25
  import {
35
26
  UseSmileIDMetadataFactory,
36
27
  type MetadataProvider,
@@ -52,6 +43,16 @@ import {
52
43
  decodeSmileIDToken,
53
44
  type DecodedToken,
54
45
  } from "../../data/util/use_smile_id_token_decoder";
46
+ import {
47
+ isTokenConsentComplete,
48
+ bindsRequiredUserDetails,
49
+ type TokenPayload,
50
+ } from "../../data/model/token_payload";
51
+ import {
52
+ isUserDetailsPayloadEmpty,
53
+ userDetailsPayloadFrom,
54
+ } from "../../data/model/user_details_payload";
55
+ import type { UserDetailsPayload } from "../../data/model/user_details_payload";
55
56
  import {
56
57
  DocumentSide,
57
58
  EnhancedSmartSelfieConfig,
@@ -166,6 +167,15 @@ export class FlowNavigationManager {
166
167
  /** Canonical `partner_id` decoded from the JWT (raw); resolved via the [partnerId] getter. */
167
168
  private readonly _resolvedPartnerId: string | undefined;
168
169
 
170
+ /**
171
+ * Token-bound user-detail/consent bindings decoded from the JWT
172
+ * `payload` claim, resolved once at construction alongside
173
+ * `resolvedSdkConfig`. `undefined` for offline mode / no token /
174
+ * legacy token — assembly then requires and sends everything exactly
175
+ * as before token support existed.
176
+ */
177
+ private readonly _resolvedTokenPayload: TokenPayload | undefined;
178
+
169
179
  /**
170
180
  * Partner identifier the capture screen stamps into the Smile EXIF tag — the tag's HMAC/AES
171
181
  * keys derive from it and the backend validates the tag against the request's partner_id.
@@ -188,7 +198,6 @@ export class FlowNavigationManager {
188
198
 
189
199
  constructor(options: FlowNavigationManagerOptions) {
190
200
  this._configuration = options.configuration;
191
- this._steps = options.configuration.screens.steps;
192
201
  this._onExit = options.onExit;
193
202
  this._repository = options.repository;
194
203
  // Fall back to a fresh factory so capture-only harnesses still build
@@ -202,6 +211,14 @@ export class FlowNavigationManager {
202
211
  );
203
212
  this.resolvedSdkConfig = decodedToken?.sdkConfig;
204
213
  this._resolvedPartnerId = decodedToken?.partnerId;
214
+ this._resolvedTokenPayload = decodedToken?.tokenPayload;
215
+ // Token consent always wins: a complete binding drops the consent
216
+ // screen from the flow — the server injects the token's record, so
217
+ // nothing is collected here.
218
+ const steps = options.configuration.screens.steps;
219
+ this._steps = isTokenConsentComplete(decodedToken?.tokenPayload?.consent)
220
+ ? steps.filter((s) => s.kind !== "consent")
221
+ : steps;
205
222
  // Seed the path with the first screen so currentScreen/renderer have a
206
223
  // deterministic start. Empty path → host renders the "no screens"
207
224
  // sentinel (validation rejects empty screens at build time anyway).
@@ -775,10 +792,18 @@ export class FlowNavigationManager {
775
792
  private _assembleJob(): JobRequest {
776
793
  const config = this._configuration;
777
794
  const jobType = config.network?.config?.jobType ?? JobType.unknown;
778
- const consent =
779
- this._captured.consentInformation ??
780
- config.consentInformation ??
781
- DEFAULT_CONSENT;
795
+ // Token consent always wins: the `consent` part is omitted — the
796
+ // server injects the token's record — and any local preset or
797
+ // capture is ignored. Without a token binding, a local source
798
+ // (captured on the consent screen, else the builder preset) is
799
+ // sent in full, and the legacy DEFAULT_CONSENT placeholder covers
800
+ // the no-source case.
801
+ const consent = isTokenConsentComplete(this._resolvedTokenPayload?.consent)
802
+ ? undefined
803
+ : (this._captured.consentInformation ??
804
+ config.consentInformation ??
805
+ DEFAULT_CONSENT);
806
+ const consentField = consent !== undefined ? { consent } : {};
782
807
  // Build the per-submission metadata snapshot so it travels on the
783
808
  // request (API layer serializes `request.metadata` directly).
784
809
  // Dynamic fields (orientation, locale, timezone) re-resolve per
@@ -806,8 +831,8 @@ export class FlowNavigationManager {
806
831
  jobType: JobType.smartSelfieEnrollment,
807
832
  selfieImage: selfie.selfiePath,
808
833
  livenessImages: selfie.livenessPaths,
809
- userDetails,
810
- consent,
834
+ ...(userDetails !== undefined ? { userDetails } : {}),
835
+ ...consentField,
811
836
  metadata,
812
837
  enhanced: false,
813
838
  expectedLivenessImageCount: this._expectedLivenessImageCount(),
@@ -822,9 +847,9 @@ export class FlowNavigationManager {
822
847
  jobType: JobType.smartSelfieAuthentication,
823
848
  selfieImage: selfie.selfiePath,
824
849
  livenessImages: selfie.livenessPaths,
825
- userDetails,
850
+ ...(userDetails !== undefined ? { userDetails } : {}),
826
851
  userId,
827
- consent,
852
+ ...consentField,
828
853
  metadata,
829
854
  enhanced: false,
830
855
  expectedLivenessImageCount: this._expectedLivenessImageCount(),
@@ -845,8 +870,8 @@ export class FlowNavigationManager {
845
870
  selfieImage: selfie.selfiePath,
846
871
  livenessImages: selfie.livenessPaths,
847
872
  params,
848
- userDetails,
849
- consent,
873
+ ...(userDetails !== undefined ? { userDetails } : {}),
874
+ ...consentField,
850
875
  metadata,
851
876
  expectedLivenessImageCount: this._expectedLivenessImageCount(),
852
877
  };
@@ -862,14 +887,17 @@ export class FlowNavigationManager {
862
887
  });
863
888
  }
864
889
  const userDetails = this._requireUserDetails(config);
865
- return this._buildDocumentRequest(
866
- selfie,
867
- front,
868
- params,
869
- userDetails,
870
- consent,
890
+ return {
891
+ jobType: JobType.documentVerification,
892
+ ...this._documentRequestFields(selfie, front),
893
+ country: params.country,
894
+ // Optional — omit when unset so the server classifies the document.
895
+ ...(params.idType ? { idType: params.idType } : {}),
896
+ ...(userDetails !== undefined ? { userDetails } : {}),
897
+ ...consentField,
871
898
  metadata,
872
- );
899
+ expectedLivenessImageCount: this._expectedLivenessImageCount(),
900
+ };
873
901
  }
874
902
 
875
903
  case JobType.enhancedDocumentVerification: {
@@ -882,14 +910,16 @@ export class FlowNavigationManager {
882
910
  });
883
911
  }
884
912
  const userDetails = this._requireUserDetails(config);
885
- return this._buildEnhancedDocumentRequest(
886
- selfie,
887
- front,
888
- params,
889
- userDetails,
890
- consent,
913
+ return {
914
+ jobType: JobType.enhancedDocumentVerification,
915
+ ...this._documentRequestFields(selfie, front),
916
+ country: params.country,
917
+ idType: params.idType,
918
+ ...(userDetails !== undefined ? { userDetails } : {}),
919
+ ...consentField,
891
920
  metadata,
892
- );
921
+ expectedLivenessImageCount: this._expectedLivenessImageCount(),
922
+ };
893
923
  }
894
924
 
895
925
  case JobType.enhancedKyc: {
@@ -903,8 +933,8 @@ export class FlowNavigationManager {
903
933
  return {
904
934
  jobType: JobType.enhancedKyc,
905
935
  params,
906
- userDetails,
907
- consent,
936
+ ...(userDetails !== undefined ? { userDetails } : {}),
937
+ ...consentField,
908
938
  metadata,
909
939
  };
910
940
  }
@@ -924,18 +954,15 @@ export class FlowNavigationManager {
924
954
  return {
925
955
  jobType: JobType.bvn,
926
956
  params,
927
- userDetails,
928
- consent,
957
+ ...(userDetails !== undefined ? { userDetails } : {}),
958
+ ...consentField,
929
959
  metadata,
930
960
  };
931
961
  }
932
962
 
933
963
  case JobType.unknown:
934
964
  default:
935
- throw new Error(
936
- "Job type not configured — call `b.network((n) => n.config((c) => { c.jobType = JobType.<...>; }))` " +
937
- "before mounting <UseSmileIDBuilder>.",
938
- );
965
+ throw new MissingJobTypeException();
939
966
  }
940
967
  }
941
968
 
@@ -950,12 +977,28 @@ export class FlowNavigationManager {
950
977
  return { selfiePath, livenessPaths };
951
978
  }
952
979
 
953
- private _requireUserDetails(config: FlowConfigurationSnapshot): UserDetails {
980
+ /**
981
+ * Resolve `b.userDetails` for the wire. May legitimately return
982
+ * `undefined` when the token binds every required field
983
+ * (`given_names`, `last_name`, and a contact field) — the
984
+ * `user_details` part is then omitted and the server injects the
985
+ * token's values. Otherwise throws exactly as before token support.
986
+ */
987
+ private _requireUserDetails(
988
+ config: FlowConfigurationSnapshot,
989
+ ): UserDetailsPayload | undefined {
954
990
  const userDetails = config.userDetails;
955
991
  if (!userDetails) {
992
+ if (bindsRequiredUserDetails(this._resolvedTokenPayload)) {
993
+ return undefined;
994
+ }
956
995
  throw new MissingRequiredFieldException({ fieldName: "userDetails" });
957
996
  }
958
- return userDetails;
997
+ const payload = userDetailsPayloadFrom(
998
+ userDetails,
999
+ this._resolvedTokenPayload,
1000
+ );
1001
+ return isUserDetailsPayloadEmpty(payload) ? undefined : payload;
959
1002
  }
960
1003
 
961
1004
  /**
@@ -979,58 +1022,25 @@ export class FlowNavigationManager {
979
1022
  return frontDocumentPath;
980
1023
  }
981
1024
 
982
- private _buildDocumentRequest(
1025
+ /** Capture-artifact fields shared by both document request variants. */
1026
+ private _documentRequestFields(
983
1027
  selfie: {
984
1028
  readonly selfiePath: string;
985
1029
  readonly livenessPaths: readonly string[];
986
1030
  },
987
1031
  frontDocument: string,
988
- params: DocumentVerificationParams,
989
- userDetails: UserDetails,
990
- consent: ConsentInformation,
991
- metadata: UseSmileIDMetadata,
992
- ): DocumentVerificationRequest {
993
- const { backDocumentPath } = this._captured;
994
- return {
995
- jobType: JobType.documentVerification,
996
- selfieImage: selfie.selfiePath,
997
- livenessImages: selfie.livenessPaths,
998
- frontDocument,
999
- ...(backDocumentPath ? { backDocument: backDocumentPath } : {}),
1000
- country: params.country,
1001
- // Optional — omit when unset so the server classifies the document.
1002
- ...(params.idType ? { idType: params.idType } : {}),
1003
- userDetails,
1004
- consent,
1005
- metadata,
1006
- expectedLivenessImageCount: this._expectedLivenessImageCount(),
1007
- };
1008
- }
1009
-
1010
- private _buildEnhancedDocumentRequest(
1011
- selfie: {
1012
- readonly selfiePath: string;
1013
- readonly livenessPaths: readonly string[];
1014
- },
1015
- frontDocument: string,
1016
- params: EnhancedDocumentVerificationParams,
1017
- userDetails: UserDetails,
1018
- consent: ConsentInformation,
1019
- metadata: UseSmileIDMetadata,
1020
- ): EnhancedDocumentVerificationRequest {
1032
+ ): {
1033
+ readonly selfieImage: string;
1034
+ readonly livenessImages: readonly string[];
1035
+ readonly frontDocument: string;
1036
+ readonly backDocument?: string;
1037
+ } {
1021
1038
  const { backDocumentPath } = this._captured;
1022
1039
  return {
1023
- jobType: JobType.enhancedDocumentVerification,
1024
1040
  selfieImage: selfie.selfiePath,
1025
1041
  livenessImages: selfie.livenessPaths,
1026
1042
  frontDocument,
1027
1043
  ...(backDocumentPath ? { backDocument: backDocumentPath } : {}),
1028
- country: params.country,
1029
- idType: params.idType,
1030
- userDetails,
1031
- consent,
1032
- metadata,
1033
- expectedLivenessImageCount: this._expectedLivenessImageCount(),
1034
1044
  };
1035
1045
  }
1036
1046
 
@@ -0,0 +1,102 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import * as React from "react";
3
+ import { create as createRenderer, act } from "react-test-renderer";
4
+
5
+ // `vi.mock` is hoisted above the imports, so anything its factory closes over
6
+ // must be created with `vi.hoisted` or it is still in the TDZ when the factory
7
+ // runs.
8
+ const { dispatched } = vi.hoisted(() => ({
9
+ dispatched: {
10
+ current: undefined as
11
+ | { screen: string; configuration: unknown }
12
+ | undefined,
13
+ },
14
+ }));
15
+
16
+ function stub(screen: string) {
17
+ return ({ configuration }: { configuration: unknown }) => {
18
+ dispatched.current = { screen, configuration };
19
+ return null;
20
+ };
21
+ }
22
+
23
+ // Stand in for every orchestrated screen: this suite asserts the switch routes
24
+ // each step kind (and its config) to the right screen — each screen has its own
25
+ // suite for what it then does with that config.
26
+ vi.mock("../../screen/orchestrated_capture_screen", () => ({
27
+ OrchestratedCaptureScreen: stub("capture"),
28
+ }));
29
+ vi.mock("../../screen/orchestrated_consent_screen", () => ({
30
+ OrchestratedConsentScreen: stub("consent"),
31
+ }));
32
+ vi.mock("../../screen/orchestrated_instructions_screen", () => ({
33
+ OrchestratedInstructionsScreen: stub("instructions"),
34
+ }));
35
+ vi.mock("../../screen/orchestrated_preview_screen", () => ({
36
+ OrchestratedPreviewScreen: stub("preview"),
37
+ }));
38
+ vi.mock("../../screen/orchestrated_processing_screen", () => ({
39
+ OrchestratedProcessingScreen: stub("processing"),
40
+ }));
41
+
42
+ import { ScreenRenderer } from "../screen_renderer";
43
+ import {
44
+ CaptureType,
45
+ ScreensBuilder,
46
+ type ScreenSpec,
47
+ } from "../../dsl/builder/screens_builder";
48
+
49
+ const steps: readonly ScreenSpec[] = (() => {
50
+ const screensBuilder = new ScreensBuilder();
51
+ screensBuilder.consent();
52
+ screensBuilder.instructions();
53
+ screensBuilder.capture((capture) => {
54
+ capture.captureType = CaptureType.selfie;
55
+ });
56
+ screensBuilder.preview();
57
+ screensBuilder.processing();
58
+ return screensBuilder.build().steps;
59
+ })();
60
+
61
+ function stepOfKind(kind: ScreenSpec["kind"]): ScreenSpec {
62
+ const step = steps.find((candidate) => candidate.kind === kind);
63
+ if (!step) throw new Error(`no ${kind} step built`);
64
+ return step;
65
+ }
66
+
67
+ function render(step: ScreenSpec) {
68
+ act(() => {
69
+ createRenderer(<ScreenRenderer step={step} />);
70
+ });
71
+ const current = dispatched.current;
72
+ if (!current) throw new Error("ScreenRenderer dispatched nothing");
73
+ return current;
74
+ }
75
+
76
+ beforeEach(() => {
77
+ dispatched.current = undefined;
78
+ });
79
+
80
+ describe("ScreenRenderer", () => {
81
+ it.each([
82
+ ["consent"],
83
+ ["instructions"],
84
+ ["capture"],
85
+ ["preview"],
86
+ ["processing"],
87
+ ] as const)("routes a %s step to its orchestrated screen", (kind) => {
88
+ const step = stepOfKind(kind);
89
+ const result = render(step);
90
+
91
+ expect(result.screen).toBe(kind);
92
+ expect(result.configuration).toBe(step.config);
93
+ });
94
+
95
+ it("hands each step its own config object, not a shared one", () => {
96
+ const capture = render(stepOfKind("capture")).configuration;
97
+ const preview = render(stepOfKind("preview")).configuration;
98
+
99
+ expect(capture).not.toBe(preview);
100
+ expect(capture).toBe(stepOfKind("capture").config);
101
+ });
102
+ });