@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,157 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { UseSmileIDNetworkBuilder } from "../use_smile_id_network_builder";
|
|
4
|
+
import { JobType } from "../../../core/models/job_type";
|
|
5
|
+
import {
|
|
6
|
+
LogLevel,
|
|
7
|
+
RetryStrategy,
|
|
8
|
+
type Interceptor,
|
|
9
|
+
} from "../../../core/models/network_config";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The network DSL's top level: every block is lazily constructed, a
|
|
13
|
+
* repeated block keeps mutating the same receiver, and `build()` emits
|
|
14
|
+
* only the blocks the partner actually opened — an eagerly-created
|
|
15
|
+
* builder would ship defaults the partner never asked for.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
describe("UseSmileIDNetworkBuilder", () => {
|
|
19
|
+
it("emits an empty config when no block is opened", () => {
|
|
20
|
+
expect(new UseSmileIDNetworkBuilder().build()).toEqual({});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("freezes the built configuration", () => {
|
|
24
|
+
const built = new UseSmileIDNetworkBuilder().build();
|
|
25
|
+
expect(Object.isFrozen(built)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("builds the config block from the partner's identity and job type", () => {
|
|
29
|
+
const builder = new UseSmileIDNetworkBuilder();
|
|
30
|
+
builder.config((config) => {
|
|
31
|
+
config.jobType = JobType.enhancedKyc;
|
|
32
|
+
config.token = "jwt-token";
|
|
33
|
+
config.partnerConfig((partner) => {
|
|
34
|
+
partner.partnerId = "0001";
|
|
35
|
+
partner.useSandbox = true;
|
|
36
|
+
});
|
|
37
|
+
config.logging((logging) => {
|
|
38
|
+
logging.level = LogLevel.body;
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const built = builder.build();
|
|
43
|
+
expect(built.config?.jobType).toBe(JobType.enhancedKyc);
|
|
44
|
+
expect(built.config?.token).toBe("jwt-token");
|
|
45
|
+
expect(built.config?.partnerConfig?.partnerId).toBe("0001");
|
|
46
|
+
expect(built.config?.partnerConfig?.useSandbox).toBe(true);
|
|
47
|
+
expect(built.config?.logging?.level).toBe(LogLevel.body);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("builds per-phase timeouts", () => {
|
|
51
|
+
const builder = new UseSmileIDNetworkBuilder();
|
|
52
|
+
builder.timeouts((timeouts) => {
|
|
53
|
+
timeouts.connect = 5;
|
|
54
|
+
timeouts.read = 10;
|
|
55
|
+
timeouts.write = 15;
|
|
56
|
+
timeouts.call = 20;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(builder.build().timeouts).toEqual({
|
|
60
|
+
connect: 5,
|
|
61
|
+
read: 10,
|
|
62
|
+
write: 15,
|
|
63
|
+
call: 20,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("builds the cache block, including the megabytes helper", () => {
|
|
68
|
+
const builder = new UseSmileIDNetworkBuilder();
|
|
69
|
+
builder.cache((cache) => {
|
|
70
|
+
cache.enabled = false;
|
|
71
|
+
cache.maxSize = cache.megabytes(8);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(builder.build().cache).toEqual({
|
|
75
|
+
enabled: false,
|
|
76
|
+
maxSize: 8 * 1024 * 1024,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("builds the retry block", () => {
|
|
81
|
+
const builder = new UseSmileIDNetworkBuilder();
|
|
82
|
+
builder.retry((retry) => {
|
|
83
|
+
retry.strategy = RetryStrategy.exponential;
|
|
84
|
+
retry.maxAttempts = 4;
|
|
85
|
+
retry.initialDelay = 2;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const retry = builder.build().retry;
|
|
89
|
+
expect(retry?.strategy).toBe(RetryStrategy.exponential);
|
|
90
|
+
expect(retry?.maxAttempts).toBe(4);
|
|
91
|
+
expect(retry?.initialDelay).toBe(2);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("builds the interceptor chain in registration order", () => {
|
|
95
|
+
const custom: Interceptor = { attach: vi.fn() };
|
|
96
|
+
const builder = new UseSmileIDNetworkBuilder();
|
|
97
|
+
builder.interceptors((interceptors) => {
|
|
98
|
+
interceptors.add(interceptors.gzip());
|
|
99
|
+
interceptors.add(custom);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const chain = builder.build().interceptors;
|
|
103
|
+
expect(chain).toHaveLength(2);
|
|
104
|
+
expect(chain?.[1]).toBe(custom);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("reuses each block's builder across repeated calls", () => {
|
|
108
|
+
const builder = new UseSmileIDNetworkBuilder();
|
|
109
|
+
const seen: unknown[] = [];
|
|
110
|
+
|
|
111
|
+
builder.cache((cache) => {
|
|
112
|
+
seen.push(cache);
|
|
113
|
+
cache.maxSize = 1;
|
|
114
|
+
});
|
|
115
|
+
builder.cache((cache) => {
|
|
116
|
+
seen.push(cache);
|
|
117
|
+
cache.enabled = false;
|
|
118
|
+
});
|
|
119
|
+
builder.retry((retry) => {
|
|
120
|
+
seen.push(retry);
|
|
121
|
+
retry.strategy = RetryStrategy.fixed;
|
|
122
|
+
});
|
|
123
|
+
builder.retry((retry) => {
|
|
124
|
+
seen.push(retry);
|
|
125
|
+
retry.fixedDelay = 3;
|
|
126
|
+
});
|
|
127
|
+
builder.interceptors((interceptors) => {
|
|
128
|
+
seen.push(interceptors);
|
|
129
|
+
interceptors.add({ attach: vi.fn() });
|
|
130
|
+
});
|
|
131
|
+
builder.interceptors((interceptors) => {
|
|
132
|
+
seen.push(interceptors);
|
|
133
|
+
interceptors.add({ attach: vi.fn() });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(seen[0]).toBe(seen[1]);
|
|
137
|
+
expect(seen[2]).toBe(seen[3]);
|
|
138
|
+
expect(seen[4]).toBe(seen[5]);
|
|
139
|
+
|
|
140
|
+
const built = builder.build();
|
|
141
|
+
// Both mutations survived because the receiver was reused.
|
|
142
|
+
expect(built.cache).toEqual({ enabled: false, maxSize: 1 });
|
|
143
|
+
expect(built.retry?.strategy).toBe(RetryStrategy.fixed);
|
|
144
|
+
expect(built.retry?.fixedDelay).toBe(3);
|
|
145
|
+
expect(built.interceptors).toHaveLength(2);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("omits blocks the partner never opened", () => {
|
|
149
|
+
const builder = new UseSmileIDNetworkBuilder();
|
|
150
|
+
builder.cache((cache) => {
|
|
151
|
+
cache.enabled = false;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const built = builder.build();
|
|
155
|
+
expect(Object.keys(built)).toEqual(["cache"]);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
isTokenConsentComplete,
|
|
4
|
+
tokenConsentMissingSubfields,
|
|
5
|
+
parseTokenPayload,
|
|
6
|
+
bindsRequiredUserDetails,
|
|
7
|
+
type TokenPayload,
|
|
8
|
+
} from "../token_payload";
|
|
9
|
+
|
|
10
|
+
// Canonical backend claim — user-detail values are opaque vault
|
|
11
|
+
// references; only key presence is meaningful client-side.
|
|
12
|
+
const FULL_CLAIM = {
|
|
13
|
+
given_names: "pii_abc",
|
|
14
|
+
last_name: "pii_def",
|
|
15
|
+
email: "pii_ghi",
|
|
16
|
+
phone_number: "pii_jkl",
|
|
17
|
+
consent: {
|
|
18
|
+
granted: true,
|
|
19
|
+
granted_at: "2026-01-01T09:00:00.000Z",
|
|
20
|
+
notice_language: "EN",
|
|
21
|
+
notice_privacy_policy_url: "https://partner.test/privacy",
|
|
22
|
+
},
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
describe("parseTokenPayload — claim-shape tolerance", () => {
|
|
26
|
+
it("parses the full canonical claim into presence flags + complete consent", () => {
|
|
27
|
+
const payload = parseTokenPayload(FULL_CLAIM);
|
|
28
|
+
expect(payload).toEqual({
|
|
29
|
+
hasGivenNames: true,
|
|
30
|
+
hasLastName: true,
|
|
31
|
+
hasEmail: true,
|
|
32
|
+
hasPhoneNumber: true,
|
|
33
|
+
consent: {
|
|
34
|
+
granted: true,
|
|
35
|
+
grantedAt: "2026-01-01T09:00:00.000Z",
|
|
36
|
+
noticeLanguage: "EN",
|
|
37
|
+
noticePrivacyPolicyUrl: "https://partner.test/privacy",
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("parses a subset claim — only the present keys flag true", () => {
|
|
43
|
+
const payload = parseTokenPayload({ email: "pii_x" });
|
|
44
|
+
expect(payload).toEqual({
|
|
45
|
+
hasGivenNames: false,
|
|
46
|
+
hasLastName: false,
|
|
47
|
+
hasEmail: true,
|
|
48
|
+
hasPhoneNumber: false,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("returns undefined for a non-object claim (string / array / number / null)", () => {
|
|
53
|
+
expect(parseTokenPayload("payload")).toBeUndefined();
|
|
54
|
+
expect(parseTokenPayload(["pii_x"])).toBeUndefined();
|
|
55
|
+
expect(parseTokenPayload(42)).toBeUndefined();
|
|
56
|
+
expect(parseTokenPayload(null)).toBeUndefined();
|
|
57
|
+
expect(parseTokenPayload(undefined)).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("treats an empty string value as not binding the field", () => {
|
|
61
|
+
const payload = parseTokenPayload({ given_names: "", email: "pii_x" });
|
|
62
|
+
expect(payload?.hasGivenNames).toBe(false);
|
|
63
|
+
expect(payload?.hasEmail).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("degrades a wrong-typed identity field to absent without dropping its siblings", () => {
|
|
67
|
+
const payload = parseTokenPayload({
|
|
68
|
+
given_names: 42,
|
|
69
|
+
last_name: ["pii"],
|
|
70
|
+
email: "pii_x",
|
|
71
|
+
});
|
|
72
|
+
expect(payload?.hasGivenNames).toBe(false);
|
|
73
|
+
expect(payload?.hasLastName).toBe(false);
|
|
74
|
+
expect(payload?.hasEmail).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("ignores unknown keys and the out-of-scope id/callback fields", () => {
|
|
78
|
+
const payload = parseTokenPayload({
|
|
79
|
+
country: "NG",
|
|
80
|
+
id_type: "NIN",
|
|
81
|
+
id_number: "pii_id",
|
|
82
|
+
callback_url: "callback_x",
|
|
83
|
+
something_new: { nested: true },
|
|
84
|
+
last_name: "pii_def",
|
|
85
|
+
});
|
|
86
|
+
expect(payload).toEqual({
|
|
87
|
+
hasGivenNames: false,
|
|
88
|
+
hasLastName: true,
|
|
89
|
+
hasEmail: false,
|
|
90
|
+
hasPhoneNumber: false,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("treats a non-object consent (string / array) as absent", () => {
|
|
95
|
+
expect(parseTokenPayload({ consent: "granted" })?.consent).toBeUndefined();
|
|
96
|
+
expect(parseTokenPayload({ consent: [true] })?.consent).toBeUndefined();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("treats an empty consent object as absent", () => {
|
|
100
|
+
expect(parseTokenPayload({ consent: {} })?.consent).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('never trusts granted: false or granted: "yes" as a binding', () => {
|
|
104
|
+
const deniedConsent = parseTokenPayload({
|
|
105
|
+
consent: { granted: false, granted_at: "2026-01-01T09:00:00.000Z" },
|
|
106
|
+
})?.consent;
|
|
107
|
+
expect(deniedConsent?.granted).toBeUndefined();
|
|
108
|
+
expect(deniedConsent?.grantedAt).toBe("2026-01-01T09:00:00.000Z");
|
|
109
|
+
|
|
110
|
+
const stringGranted = parseTokenPayload({
|
|
111
|
+
consent: { granted: "yes" },
|
|
112
|
+
})?.consent;
|
|
113
|
+
expect(stringGranted).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("degrades wrong-typed consent subfields individually", () => {
|
|
117
|
+
const consent = parseTokenPayload({
|
|
118
|
+
consent: { granted: true, granted_at: 12345, notice_language: "EN" },
|
|
119
|
+
})?.consent;
|
|
120
|
+
expect(consent).toEqual({ granted: true, noticeLanguage: "EN" });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("never throws for arbitrary claim shapes", () => {
|
|
124
|
+
const inputs: unknown[] = [
|
|
125
|
+
{},
|
|
126
|
+
FULL_CLAIM,
|
|
127
|
+
{ consent: { granted: null } },
|
|
128
|
+
{ given_names: { nested: true } },
|
|
129
|
+
Object.create(null),
|
|
130
|
+
];
|
|
131
|
+
for (const input of inputs) {
|
|
132
|
+
expect(() => parseTokenPayload(input)).not.toThrow();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("isTokenConsentComplete", () => {
|
|
138
|
+
const completeConsent = {
|
|
139
|
+
granted: true,
|
|
140
|
+
grantedAt: "2026-01-01T09:00:00.000Z",
|
|
141
|
+
noticeLanguage: "EN",
|
|
142
|
+
noticePrivacyPolicyUrl: "https://partner.test/privacy",
|
|
143
|
+
} as const;
|
|
144
|
+
|
|
145
|
+
it("is true only when granted and all three subfields are non-blank", () => {
|
|
146
|
+
expect(isTokenConsentComplete(completeConsent)).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("is false when any subfield is missing or blank", () => {
|
|
150
|
+
expect(isTokenConsentComplete(undefined)).toBe(false);
|
|
151
|
+
const { granted: _granted, ...withoutGranted } = completeConsent;
|
|
152
|
+
expect(isTokenConsentComplete(withoutGranted)).toBe(false);
|
|
153
|
+
const { noticeLanguage: _language, ...withoutLanguage } = completeConsent;
|
|
154
|
+
expect(isTokenConsentComplete(withoutLanguage)).toBe(false);
|
|
155
|
+
expect(
|
|
156
|
+
isTokenConsentComplete({ ...completeConsent, grantedAt: " " }),
|
|
157
|
+
).toBe(false);
|
|
158
|
+
expect(
|
|
159
|
+
isTokenConsentComplete({
|
|
160
|
+
...completeConsent,
|
|
161
|
+
noticePrivacyPolicyUrl: "",
|
|
162
|
+
}),
|
|
163
|
+
).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("missingSubfields names exactly the absent or blank consent subfields", () => {
|
|
167
|
+
expect(tokenConsentMissingSubfields(completeConsent)).toEqual([]);
|
|
168
|
+
const { granted: _granted, ...withoutGranted } = completeConsent;
|
|
169
|
+
expect(tokenConsentMissingSubfields(withoutGranted)).toEqual(["granted"]);
|
|
170
|
+
const { noticeLanguage: _language, ...withoutLanguage } = completeConsent;
|
|
171
|
+
expect(
|
|
172
|
+
tokenConsentMissingSubfields({ ...withoutLanguage, grantedAt: " " }),
|
|
173
|
+
).toEqual(["granted_at", "notice_language"]);
|
|
174
|
+
expect(tokenConsentMissingSubfields({})).toEqual([
|
|
175
|
+
"granted",
|
|
176
|
+
"granted_at",
|
|
177
|
+
"notice_language",
|
|
178
|
+
"notice_privacy_policy_url",
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("bindsRequiredUserDetails", () => {
|
|
184
|
+
function payload(overrides: Partial<TokenPayload>): TokenPayload {
|
|
185
|
+
return {
|
|
186
|
+
hasGivenNames: false,
|
|
187
|
+
hasLastName: false,
|
|
188
|
+
hasEmail: false,
|
|
189
|
+
hasPhoneNumber: false,
|
|
190
|
+
...overrides,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
it("requires both names plus at least one contact field", () => {
|
|
195
|
+
expect(
|
|
196
|
+
bindsRequiredUserDetails(
|
|
197
|
+
payload({ hasGivenNames: true, hasLastName: true, hasEmail: true }),
|
|
198
|
+
),
|
|
199
|
+
).toBe(true);
|
|
200
|
+
expect(
|
|
201
|
+
bindsRequiredUserDetails(
|
|
202
|
+
payload({
|
|
203
|
+
hasGivenNames: true,
|
|
204
|
+
hasLastName: true,
|
|
205
|
+
hasPhoneNumber: true,
|
|
206
|
+
}),
|
|
207
|
+
),
|
|
208
|
+
).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("is false when a name or every contact field is missing", () => {
|
|
212
|
+
expect(
|
|
213
|
+
bindsRequiredUserDetails(
|
|
214
|
+
payload({ hasGivenNames: true, hasLastName: true }),
|
|
215
|
+
),
|
|
216
|
+
).toBe(false);
|
|
217
|
+
expect(
|
|
218
|
+
bindsRequiredUserDetails(
|
|
219
|
+
payload({ hasGivenNames: true, hasEmail: true }),
|
|
220
|
+
),
|
|
221
|
+
).toBe(false);
|
|
222
|
+
expect(bindsRequiredUserDetails(undefined)).toBe(false);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { UserDetails } from "../../../core/models/params";
|
|
3
|
+
import {
|
|
4
|
+
isUserDetailsPayloadEmpty,
|
|
5
|
+
userDetailsPayloadFrom,
|
|
6
|
+
userDetailsPayloadToWire,
|
|
7
|
+
} from "../user_details_payload";
|
|
8
|
+
|
|
9
|
+
// Pins the `user_details` wire carrier: token-bound fields are stripped,
|
|
10
|
+
// nothing-stripped output matches the legacy `UserDetails` encoding, and an
|
|
11
|
+
// all-stripped payload reads as empty so the part is omitted rather than
|
|
12
|
+
// sent as `{}`.
|
|
13
|
+
describe("UserDetailsPayload", () => {
|
|
14
|
+
const details: UserDetails = {
|
|
15
|
+
givenNames: "Ada",
|
|
16
|
+
lastName: "Lovelace",
|
|
17
|
+
email: "ada@example.test",
|
|
18
|
+
phoneNumber: "+2348000000000",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
it("no token binding keeps every field and matches the UserDetails wire encoding", () => {
|
|
22
|
+
const payload = userDetailsPayloadFrom(details, undefined);
|
|
23
|
+
|
|
24
|
+
expect(payload).toEqual(details);
|
|
25
|
+
expect(isUserDetailsPayloadEmpty(payload)).toBe(false);
|
|
26
|
+
expect(userDetailsPayloadToWire(payload)).toEqual({
|
|
27
|
+
given_names: "Ada",
|
|
28
|
+
last_name: "Lovelace",
|
|
29
|
+
email: "ada@example.test",
|
|
30
|
+
phone_number: "+2348000000000",
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("an empty token payload binding nothing keeps every field", () => {
|
|
35
|
+
const payload = userDetailsPayloadFrom(details, {
|
|
36
|
+
hasGivenNames: false,
|
|
37
|
+
hasLastName: false,
|
|
38
|
+
hasEmail: false,
|
|
39
|
+
hasPhoneNumber: false,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(payload).toEqual(details);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("token-bound fields are dropped individually", () => {
|
|
46
|
+
const payload = userDetailsPayloadFrom(details, {
|
|
47
|
+
hasGivenNames: true,
|
|
48
|
+
hasLastName: true,
|
|
49
|
+
hasEmail: false,
|
|
50
|
+
hasPhoneNumber: false,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(payload.givenNames).toBeUndefined();
|
|
54
|
+
expect(payload.lastName).toBeUndefined();
|
|
55
|
+
expect(payload.email).toBe("ada@example.test");
|
|
56
|
+
expect(payload.phoneNumber).toBe("+2348000000000");
|
|
57
|
+
expect(Object.keys(userDetailsPayloadToWire(payload)).sort()).toEqual([
|
|
58
|
+
"email",
|
|
59
|
+
"phone_number",
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("fully bound details read as empty", () => {
|
|
64
|
+
const payload = userDetailsPayloadFrom(details, {
|
|
65
|
+
hasGivenNames: true,
|
|
66
|
+
hasLastName: true,
|
|
67
|
+
hasEmail: true,
|
|
68
|
+
hasPhoneNumber: true,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(isUserDetailsPayloadEmpty(payload)).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("blank contact fields count as empty and are omitted from the wire", () => {
|
|
75
|
+
const payload = userDetailsPayloadFrom(
|
|
76
|
+
{ givenNames: "Ada", lastName: "Lovelace", email: " ", phoneNumber: "" },
|
|
77
|
+
{
|
|
78
|
+
hasGivenNames: true,
|
|
79
|
+
hasLastName: true,
|
|
80
|
+
hasEmail: false,
|
|
81
|
+
hasPhoneNumber: false,
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Blank contacts encode to nothing, so nothing remains.
|
|
86
|
+
expect(isUserDetailsPayloadEmpty(payload)).toBe(true);
|
|
87
|
+
expect(userDetailsPayloadToWire(payload)).toEqual({});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("contact fields are trimmed on the wire", () => {
|
|
91
|
+
const wire = userDetailsPayloadToWire({
|
|
92
|
+
givenNames: "Ada",
|
|
93
|
+
lastName: "Lovelace",
|
|
94
|
+
email: " ada@example.test ",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(wire["email"]).toBe("ada@example.test");
|
|
98
|
+
expect(wire["phone_number"]).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -2,8 +2,8 @@ import type {
|
|
|
2
2
|
ConsentInformation,
|
|
3
3
|
EnhancedKYCParams,
|
|
4
4
|
BiometricKYCParams,
|
|
5
|
-
UserDetails,
|
|
6
5
|
} from "../../core/models/params";
|
|
6
|
+
import type { UserDetailsPayload } from "./user_details_payload";
|
|
7
7
|
import { JobType } from "../../core/models/job_type";
|
|
8
8
|
import type { UseSmileIDMetadata } from "../../metadata/use_smile_id_metadata";
|
|
9
9
|
|
|
@@ -12,10 +12,11 @@ import type { UseSmileIDMetadata } from "../../metadata/use_smile_id_metadata";
|
|
|
12
12
|
* narrows the union and tells the repository which API method to
|
|
13
13
|
* dispatch to.
|
|
14
14
|
*
|
|
15
|
-
* Every variant intersects [BaseJobRequest], which carries the
|
|
15
|
+
* Every variant intersects [BaseJobRequest], which carries the
|
|
16
16
|
* fields shared by all submissions: the partner-captured `consent`
|
|
17
|
-
* record
|
|
18
|
-
* narrowed `jobType` literal so the
|
|
17
|
+
* record, the per-submission `metadata` snapshot, and the decoded
|
|
18
|
+
* token bindings. Variants keep a narrowed `jobType` literal so the
|
|
19
|
+
* union still discriminates.
|
|
19
20
|
*
|
|
20
21
|
* `metadata` is built at the request-construction site (the flow
|
|
21
22
|
* navigation manager) and travels on the request itself — the API
|
|
@@ -23,7 +24,14 @@ import type { UseSmileIDMetadata } from "../../metadata/use_smile_id_metadata";
|
|
|
23
24
|
* multipart part rather than generating the snapshot at submit time.
|
|
24
25
|
*/
|
|
25
26
|
export interface BaseJobRequest {
|
|
26
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Local consent record (captured on the consent screen or
|
|
29
|
+
* pre-supplied via the builder). Omitted only when no local source
|
|
30
|
+
* exists AND the token binds complete consent — the API layer then
|
|
31
|
+
* omits the `consent` multipart part and the server injects the
|
|
32
|
+
* token's consent.
|
|
33
|
+
*/
|
|
34
|
+
readonly consent?: ConsentInformation;
|
|
27
35
|
readonly metadata: UseSmileIDMetadata;
|
|
28
36
|
}
|
|
29
37
|
|
|
@@ -31,7 +39,8 @@ export type SmartSelfieEnrollmentRequest = BaseJobRequest & {
|
|
|
31
39
|
readonly jobType: typeof JobType.smartSelfieEnrollment;
|
|
32
40
|
readonly selfieImage: string;
|
|
33
41
|
readonly livenessImages: readonly string[];
|
|
34
|
-
|
|
42
|
+
/** Omitted when the token binds every required field, or reduced to the unbound remainder. */
|
|
43
|
+
readonly userDetails?: UserDetailsPayload;
|
|
35
44
|
/**
|
|
36
45
|
* Screen-level enhanced-liveness intent marker. The wire path is
|
|
37
46
|
* always `/v3/registration` (and `/v3/authentication` on the auth
|
|
@@ -59,7 +68,8 @@ export type SmartSelfieAuthenticationRequest = BaseJobRequest & {
|
|
|
59
68
|
readonly jobType: typeof JobType.smartSelfieAuthentication;
|
|
60
69
|
readonly selfieImage: string;
|
|
61
70
|
readonly livenessImages: readonly string[];
|
|
62
|
-
|
|
71
|
+
/** Omitted when the token binds every required field, or reduced to the unbound remainder. */
|
|
72
|
+
readonly userDetails?: UserDetailsPayload;
|
|
63
73
|
/**
|
|
64
74
|
* The `user_id` returned by a prior enrollment — the v3
|
|
65
75
|
* `/v3/authentication` endpoint requires it so the server can match
|
|
@@ -77,7 +87,8 @@ export type SmartSelfieAuthenticationRequest = BaseJobRequest & {
|
|
|
77
87
|
export type EnhancedKycRequest = BaseJobRequest & {
|
|
78
88
|
readonly jobType: typeof JobType.enhancedKyc;
|
|
79
89
|
readonly params: EnhancedKYCParams;
|
|
80
|
-
|
|
90
|
+
/** Omitted when the token binds every required field, or reduced to the unbound remainder. */
|
|
91
|
+
readonly userDetails?: UserDetailsPayload;
|
|
81
92
|
};
|
|
82
93
|
|
|
83
94
|
/**
|
|
@@ -90,7 +101,8 @@ export type EnhancedKycRequest = BaseJobRequest & {
|
|
|
90
101
|
export type BvnRequest = BaseJobRequest & {
|
|
91
102
|
readonly jobType: typeof JobType.bvn;
|
|
92
103
|
readonly params: EnhancedKYCParams;
|
|
93
|
-
|
|
104
|
+
/** Omitted when the token binds every required field, or reduced to the unbound remainder. */
|
|
105
|
+
readonly userDetails?: UserDetailsPayload;
|
|
94
106
|
};
|
|
95
107
|
|
|
96
108
|
export type BiometricKycRequest = BaseJobRequest & {
|
|
@@ -98,7 +110,8 @@ export type BiometricKycRequest = BaseJobRequest & {
|
|
|
98
110
|
readonly selfieImage: string;
|
|
99
111
|
readonly livenessImages: readonly string[];
|
|
100
112
|
readonly params: BiometricKYCParams;
|
|
101
|
-
|
|
113
|
+
/** Omitted when the token binds every required field, or reduced to the unbound remainder. */
|
|
114
|
+
readonly userDetails?: UserDetailsPayload;
|
|
102
115
|
/** See `SmartSelfieEnrollmentRequest.expectedLivenessImageCount`. */
|
|
103
116
|
readonly expectedLivenessImageCount: number;
|
|
104
117
|
};
|
|
@@ -114,7 +127,8 @@ export type DocumentVerificationRequest = BaseJobRequest & {
|
|
|
114
127
|
readonly country: string;
|
|
115
128
|
/** Optional — the server classifies the document when omitted. */
|
|
116
129
|
readonly idType?: string;
|
|
117
|
-
|
|
130
|
+
/** Omitted when the token binds every required field, or reduced to the unbound remainder. */
|
|
131
|
+
readonly userDetails?: UserDetailsPayload;
|
|
118
132
|
/** See `SmartSelfieEnrollmentRequest.expectedLivenessImageCount`. */
|
|
119
133
|
readonly expectedLivenessImageCount: number;
|
|
120
134
|
};
|
|
@@ -135,7 +149,8 @@ export type EnhancedDocumentVerificationRequest = BaseJobRequest & {
|
|
|
135
149
|
readonly backDocument?: string;
|
|
136
150
|
readonly country: string;
|
|
137
151
|
readonly idType: string;
|
|
138
|
-
|
|
152
|
+
/** Omitted when the token binds every required field, or reduced to the unbound remainder. */
|
|
153
|
+
readonly userDetails?: UserDetailsPayload;
|
|
139
154
|
/** See `SmartSelfieEnrollmentRequest.expectedLivenessImageCount`. */
|
|
140
155
|
readonly expectedLivenessImageCount: number;
|
|
141
156
|
};
|