@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.
- package/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/dist/camera/camera_defaults.d.ts +2 -0
- package/dist/camera/camera_defaults.d.ts.map +1 -1
- package/dist/camera/camera_defaults.js +2 -0
- package/dist/camera/camera_defaults.js.map +1 -1
- package/dist/core/exception/flow_exceptions.d.ts +4 -2
- package/dist/core/exception/flow_exceptions.d.ts.map +1 -1
- package/dist/core/exception/flow_exceptions.js +3 -2
- package/dist/core/exception/flow_exceptions.js.map +1 -1
- package/dist/core/exception/missing_job_type_exception.d.ts +1 -3
- package/dist/core/exception/missing_job_type_exception.d.ts.map +1 -1
- package/dist/core/exception/missing_job_type_exception.js +2 -4
- package/dist/core/exception/missing_job_type_exception.js.map +1 -1
- package/dist/data/api/interceptors/smile_auth_interceptor.js +1 -1
- package/dist/data/api/interceptors/smile_auth_interceptor.js.map +1 -1
- package/dist/data/api/smile_id_api.d.ts +4 -0
- package/dist/data/api/smile_id_api.d.ts.map +1 -1
- package/dist/data/api/smile_id_api.js +30 -27
- package/dist/data/api/smile_id_api.js.map +1 -1
- package/dist/data/model/job_request.d.ts +28 -12
- package/dist/data/model/job_request.d.ts.map +1 -1
- package/dist/data/model/token_payload.d.ts +78 -0
- package/dist/data/model/token_payload.d.ts.map +1 -0
- package/dist/data/model/token_payload.js +132 -0
- package/dist/data/model/token_payload.js.map +1 -0
- package/dist/data/model/user_details_payload.d.ts +39 -0
- package/dist/data/model/user_details_payload.d.ts.map +1 -0
- package/dist/data/model/user_details_payload.js +53 -0
- package/dist/data/model/user_details_payload.js.map +1 -0
- package/dist/data/util/use_smile_id_token_decoder.d.ts +15 -3
- package/dist/data/util/use_smile_id_token_decoder.d.ts.map +1 -1
- package/dist/data/util/use_smile_id_token_decoder.js +20 -8
- package/dist/data/util/use_smile_id_token_decoder.js.map +1 -1
- package/dist/l10n/strings_en.d.ts +4 -1
- package/dist/l10n/strings_en.d.ts.map +1 -1
- package/dist/l10n/strings_en.js +4 -1
- package/dist/l10n/strings_en.js.map +1 -1
- package/dist/l10n/use_smile_id_localizations.d.ts +10 -0
- package/dist/l10n/use_smile_id_localizations.d.ts.map +1 -1
- package/dist/l10n/use_smile_id_localizations.js +59 -3
- package/dist/l10n/use_smile_id_localizations.js.map +1 -1
- package/dist/metadata/metadata_configuration_mappers.js +1 -1
- package/dist/metadata/metadata_configuration_mappers.js.map +1 -1
- package/dist/metadata/use_smile_id_metadata_factory.js +1 -1
- package/dist/metadata/use_smile_id_metadata_factory.js.map +1 -1
- package/dist/presentation/components/use_smile_id_button.d.ts.map +1 -1
- package/dist/presentation/components/use_smile_id_button.js +8 -2
- package/dist/presentation/components/use_smile_id_button.js.map +1 -1
- package/dist/presentation/flow/dsl/builder/screens_builder.d.ts +3 -6
- package/dist/presentation/flow/dsl/builder/screens_builder.d.ts.map +1 -1
- package/dist/presentation/flow/dsl/builder/screens_builder.js +4 -6
- package/dist/presentation/flow/dsl/builder/screens_builder.js.map +1 -1
- package/dist/presentation/flow/dsl/use_smile_id_flow_builder.d.ts.map +1 -1
- package/dist/presentation/flow/dsl/use_smile_id_flow_builder.js +23 -1
- package/dist/presentation/flow/dsl/use_smile_id_flow_builder.js.map +1 -1
- package/dist/presentation/flow/flow_navigation_manager.d.ts +17 -2
- package/dist/presentation/flow/flow_navigation_manager.d.ts.map +1 -1
- package/dist/presentation/flow/flow_navigation_manager.js +77 -45
- package/dist/presentation/flow/flow_navigation_manager.js.map +1 -1
- package/dist/presentation/flow/screen/orchestrated_processing_screen.d.ts.map +1 -1
- package/dist/presentation/flow/screen/orchestrated_processing_screen.js +25 -5
- package/dist/presentation/flow/screen/orchestrated_processing_screen.js.map +1 -1
- package/dist/presentation/flow/use_smile_id_flow.d.ts.map +1 -1
- package/dist/presentation/flow/use_smile_id_flow.js +35 -8
- package/dist/presentation/flow/use_smile_id_flow.js.map +1 -1
- package/dist/presentation/flow/validation/flow_validator.d.ts +16 -2
- package/dist/presentation/flow/validation/flow_validator.d.ts.map +1 -1
- package/dist/presentation/flow/validation/flow_validator.js +19 -27
- package/dist/presentation/flow/validation/flow_validator.js.map +1 -1
- package/dist/presentation/flow/validation/job_type_validator.d.ts +7 -0
- package/dist/presentation/flow/validation/job_type_validator.d.ts.map +1 -1
- package/dist/presentation/flow/validation/job_type_validator.js +52 -27
- package/dist/presentation/flow/validation/job_type_validator.js.map +1 -1
- package/dist/presentation/flow/validation/user_details_validator.d.ts +9 -0
- package/dist/presentation/flow/validation/user_details_validator.d.ts.map +1 -0
- package/dist/presentation/flow/validation/user_details_validator.js +40 -0
- package/dist/presentation/flow/validation/user_details_validator.js.map +1 -0
- package/dist/presentation/screen/helpers/capture_screen_helpers.d.ts.map +1 -1
- package/dist/presentation/screen/helpers/capture_screen_helpers.js +1 -0
- package/dist/presentation/screen/helpers/capture_screen_helpers.js.map +1 -1
- package/dist/presentation/screen/use_smile_id_capture_screen.d.ts.map +1 -1
- package/dist/presentation/screen/use_smile_id_capture_screen.js +64 -18
- package/dist/presentation/screen/use_smile_id_capture_screen.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/react_native_mock.tsx +22 -0
- package/src/__tests__/snapshots/__snapshots__/screens_structural.test.tsx.snap +14 -8
- package/src/camera/__tests__/camera_permissions.test.ts +72 -0
- package/src/camera/__tests__/camera_system_focus.test.ts +173 -0
- package/src/camera/__tests__/use_smile_id_camera_preview.test.tsx +461 -0
- package/src/camera/__tests__/use_smile_id_frame_stream_hook.test.tsx +229 -0
- package/src/camera/camera_defaults.ts +3 -0
- package/src/core/exception/__tests__/builder_validation_exceptions.test.ts +14 -0
- package/src/core/exception/flow_exceptions.test.ts +10 -0
- package/src/core/exception/flow_exceptions.ts +3 -2
- package/src/core/exception/missing_job_type_exception.ts +2 -4
- package/src/data/api/__tests__/smile_id_api.test.ts +275 -1
- package/src/data/api/interceptors/__tests__/credential_header_redaction.test.ts +144 -0
- package/src/data/api/interceptors/smile_auth_interceptor.ts +1 -1
- package/src/data/api/smile_id_api.ts +36 -49
- package/src/data/dsl/__tests__/use_smile_id_network_builder.test.ts +157 -0
- package/src/data/factory/__tests__/signed_job_submission_headers.test.ts +1 -1
- package/src/data/model/__tests__/token_payload.test.ts +224 -0
- package/src/data/model/__tests__/user_details_payload.test.ts +100 -0
- package/src/data/model/job_request.ts +27 -12
- package/src/data/model/token_payload.ts +182 -0
- package/src/data/model/user_details_payload.ts +84 -0
- package/src/data/repository/__tests__/network_retries.test.ts +1 -1
- package/src/data/repository/__tests__/repository_dispatch.test.ts +366 -0
- package/src/data/repository/__tests__/use_smile_id_repository.test.ts +1 -1
- package/src/data/security/__tests__/signing_crypto.test.ts +203 -0
- package/src/data/util/__tests__/use_smile_id_token_decoder.test.ts +179 -0
- package/src/data/util/use_smile_id_token_decoder.ts +29 -8
- package/src/l10n/__tests__/use_smile_id_localizations.test.ts +122 -5
- package/src/l10n/__tests__/use_smile_id_localizations_provider.test.tsx +112 -0
- package/src/l10n/__tests__/use_smile_id_localizations_surface.test.ts +231 -0
- package/src/l10n/strings_en.ts +5 -1
- package/src/l10n/use_smile_id_localizations.ts +80 -3
- package/src/metadata/__tests__/device_orientation_sampling.test.ts +165 -0
- package/src/metadata/__tests__/geolocation.test.ts +150 -0
- package/src/metadata/__tests__/metadata_configuration_objects.test.ts +1 -1
- package/src/metadata/__tests__/metadata_device_context.test.ts +3 -3
- package/src/metadata/__tests__/metadata_golden_fixture.json +1 -1
- package/src/metadata/__tests__/metadata_payload_golden.test.ts +4 -4
- package/src/metadata/__tests__/optional_native_modules.test.ts +234 -0
- package/src/metadata/__tests__/use_smile_id_metadata.test.ts +1 -1
- package/src/metadata/__tests__/use_smile_id_metadata_factory.test.ts +615 -0
- package/src/metadata/metadata_configuration_mappers.ts +1 -1
- package/src/metadata/use_smile_id_metadata_factory.ts +1 -1
- package/src/presentation/components/__tests__/use_smile_id_content_card.test.tsx +112 -0
- package/src/presentation/components/__tests__/use_smile_id_face_oval_image.test.tsx +190 -0
- package/src/presentation/components/__tests__/use_smile_id_learn_more_content.test.tsx +159 -0
- package/src/presentation/components/__tests__/use_smile_id_processing_center_indicator.test.tsx +88 -0
- package/src/presentation/components/use_smile_id_button.tsx +7 -2
- package/src/presentation/flow/__tests__/flow_navigation_manager.test.ts +323 -1
- package/src/presentation/flow/__tests__/use_smile_id_flow.test.tsx +651 -0
- package/src/presentation/flow/dsl/__tests__/use_smile_id_builder_component.test.tsx +120 -0
- package/src/presentation/flow/dsl/builder/screens_builder.ts +4 -6
- package/src/presentation/flow/dsl/use_smile_id_flow_builder.ts +27 -1
- package/src/presentation/flow/flow_navigation_manager.ts +102 -92
- package/src/presentation/flow/graph/__tests__/screen_renderer.test.tsx +102 -0
- package/src/presentation/flow/screen/__tests__/orchestrated_capture_screen.test.tsx +836 -0
- package/src/presentation/flow/screen/__tests__/orchestrated_consent_screen.test.tsx +165 -0
- package/src/presentation/flow/screen/__tests__/orchestrated_instructions_screen.test.tsx +163 -0
- package/src/presentation/flow/screen/__tests__/orchestrated_preview_screen.test.tsx +224 -0
- package/src/presentation/flow/screen/__tests__/orchestrated_processing_screen.test.tsx +338 -0
- package/src/presentation/flow/screen/orchestrated_processing_screen.tsx +42 -5
- package/src/presentation/flow/use_smile_id_flow.tsx +52 -8
- package/src/presentation/flow/validation/__tests__/flow_validator.test.ts +158 -0
- package/src/presentation/flow/validation/__tests__/job_type_validator.test.ts +200 -0
- package/src/presentation/flow/validation/flow_validator.ts +30 -31
- package/src/presentation/flow/validation/job_type_validator.ts +73 -39
- package/src/presentation/flow/validation/user_details_validator.ts +53 -0
- package/src/presentation/screen/__tests__/feedback_copy_table.test.ts +145 -0
- package/src/presentation/screen/__tests__/use_smile_id_capture_screen.test.tsx +348 -0
- package/src/presentation/screen/__tests__/use_smile_id_document_back_instructions.test.tsx +3 -1
- package/src/presentation/screen/__tests__/use_smile_id_permission_screen.test.tsx +68 -0
- package/src/presentation/screen/helpers/capture_screen_helpers.ts +1 -0
- package/src/presentation/screen/use_smile_id_capture_screen.tsx +69 -18
- 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
|
-
...(
|
|
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 {
|
|
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
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
866
|
-
|
|
867
|
-
front,
|
|
868
|
-
params,
|
|
869
|
-
|
|
870
|
-
|
|
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
|
|
886
|
-
|
|
887
|
-
front,
|
|
888
|
-
params,
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
+
});
|