@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,338 @@
|
|
|
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 { screenProps } = vi.hoisted(() => ({
|
|
9
|
+
screenProps: { current: {} as Record<string, unknown> },
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
// Stand in for the leaf screen so these tests exercise the orchestration
|
|
13
|
+
// (submission kick-off, state mapping, exit routing) rather than re-testing
|
|
14
|
+
// the processing UI, which has its own component suite.
|
|
15
|
+
vi.mock("../../../screen/use_smile_id_processing_screen", () => ({
|
|
16
|
+
UseSmileIDProcessingScreen: (props: Record<string, unknown>) => {
|
|
17
|
+
screenProps.current = props;
|
|
18
|
+
return null;
|
|
19
|
+
},
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
import { OrchestratedProcessingScreen } from "../orchestrated_processing_screen";
|
|
23
|
+
import { FlowNavigationProvider } from "../../flow_navigation_context";
|
|
24
|
+
import {
|
|
25
|
+
NetworkServerException,
|
|
26
|
+
NetworkTransportException,
|
|
27
|
+
NetworkUnknownException,
|
|
28
|
+
ProcessingCancelledException,
|
|
29
|
+
} from "../../../../core/exception";
|
|
30
|
+
import { JobType } from "../../../../core/models/job_type";
|
|
31
|
+
import { ProcessingConfigBuilder } from "../../dsl/builder/screens_builder";
|
|
32
|
+
import type { FlowNavigationManager } from "../../flow_navigation_manager";
|
|
33
|
+
import type { JobSubmissionState } from "../../job_submission_state";
|
|
34
|
+
|
|
35
|
+
type Fn = ReturnType<typeof vi.fn>;
|
|
36
|
+
|
|
37
|
+
type FakeManager = FlowNavigationManager & {
|
|
38
|
+
beginSubmissionIfNeeded: Fn;
|
|
39
|
+
retrySubmission: Fn;
|
|
40
|
+
navigateToNext: Fn;
|
|
41
|
+
exit: Fn;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type ManagerOverrides = {
|
|
45
|
+
readonly jobType?: number;
|
|
46
|
+
readonly submissionState?: JobSubmissionState;
|
|
47
|
+
readonly frontDocumentPath?: string;
|
|
48
|
+
readonly selfiePath?: string;
|
|
49
|
+
readonly documentFrameAspectRatio?: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function makeManager(overrides: ManagerOverrides = {}): FakeManager {
|
|
53
|
+
return {
|
|
54
|
+
beginSubmissionIfNeeded: vi.fn(),
|
|
55
|
+
retrySubmission: vi.fn(),
|
|
56
|
+
navigateToNext: vi.fn(),
|
|
57
|
+
navigateBack: vi.fn(),
|
|
58
|
+
exit: vi.fn(),
|
|
59
|
+
jobSubmissionState: overrides.submissionState ?? { kind: "notAttempted" },
|
|
60
|
+
documentFrameAspectRatio: overrides.documentFrameAspectRatio,
|
|
61
|
+
captured: {
|
|
62
|
+
...(overrides.frontDocumentPath !== undefined
|
|
63
|
+
? { frontDocumentPath: overrides.frontDocumentPath }
|
|
64
|
+
: {}),
|
|
65
|
+
...(overrides.selfiePath !== undefined
|
|
66
|
+
? { selfiePath: overrides.selfiePath }
|
|
67
|
+
: {}),
|
|
68
|
+
},
|
|
69
|
+
configuration: {
|
|
70
|
+
network:
|
|
71
|
+
overrides.jobType !== undefined
|
|
72
|
+
? { config: { jobType: overrides.jobType } }
|
|
73
|
+
: undefined,
|
|
74
|
+
},
|
|
75
|
+
} as unknown as FakeManager;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function makeConfiguration(
|
|
79
|
+
overrides: Partial<ProcessingConfigBuilder> = {},
|
|
80
|
+
): ProcessingConfigBuilder {
|
|
81
|
+
return Object.assign(new ProcessingConfigBuilder(), overrides);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function renderWith(
|
|
85
|
+
manager: FlowNavigationManager,
|
|
86
|
+
configuration: ProcessingConfigBuilder = makeConfiguration(),
|
|
87
|
+
) {
|
|
88
|
+
act(() => {
|
|
89
|
+
createRenderer(
|
|
90
|
+
<FlowNavigationProvider manager={manager}>
|
|
91
|
+
<OrchestratedProcessingScreen configuration={configuration} />
|
|
92
|
+
</FlowNavigationProvider>,
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
beforeEach(() => {
|
|
98
|
+
screenProps.current = {};
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("OrchestratedProcessingScreen — submission lifecycle", () => {
|
|
102
|
+
it("kicks off submission exactly once on mount", () => {
|
|
103
|
+
const manager = makeManager();
|
|
104
|
+
renderWith(manager);
|
|
105
|
+
|
|
106
|
+
expect(manager.beginSubmissionIfNeeded).toHaveBeenCalledTimes(1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("retry re-fires submission rather than navigating", () => {
|
|
110
|
+
const manager = makeManager({
|
|
111
|
+
submissionState: { kind: "failure", error: new Error("network down") },
|
|
112
|
+
});
|
|
113
|
+
renderWith(manager);
|
|
114
|
+
|
|
115
|
+
(screenProps.current.onRetry as () => void)();
|
|
116
|
+
|
|
117
|
+
expect(manager.retrySubmission).toHaveBeenCalledTimes(1);
|
|
118
|
+
expect(manager.navigateToNext).not.toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("continue advances the flow", () => {
|
|
122
|
+
const manager = makeManager();
|
|
123
|
+
renderWith(manager);
|
|
124
|
+
|
|
125
|
+
(screenProps.current.onContinue as () => void)();
|
|
126
|
+
|
|
127
|
+
expect(manager.navigateToNext).toHaveBeenCalledTimes(1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("exit ends the flow with a typed ProcessingCancelledException", () => {
|
|
131
|
+
const manager = makeManager();
|
|
132
|
+
renderWith(manager);
|
|
133
|
+
|
|
134
|
+
(screenProps.current.onExit as () => void)();
|
|
135
|
+
|
|
136
|
+
const [[reason]] = manager.exit.mock.calls as [
|
|
137
|
+
[{ kind: string; error: unknown }],
|
|
138
|
+
];
|
|
139
|
+
expect(reason.kind).toBe("failed");
|
|
140
|
+
expect(reason.error).toBeInstanceOf(ProcessingCancelledException);
|
|
141
|
+
expect(manager.navigateToNext).not.toHaveBeenCalled();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("exit after a failed submission delivers that failure, not a cancellation", () => {
|
|
145
|
+
const submissionError = new Error("401 — invalid API key");
|
|
146
|
+
const manager = makeManager({
|
|
147
|
+
submissionState: { kind: "failure", error: submissionError },
|
|
148
|
+
});
|
|
149
|
+
renderWith(manager);
|
|
150
|
+
|
|
151
|
+
(screenProps.current.onExit as () => void)();
|
|
152
|
+
|
|
153
|
+
const [[reason]] = manager.exit.mock.calls as [
|
|
154
|
+
[{ kind: string; error: unknown }],
|
|
155
|
+
];
|
|
156
|
+
expect(reason.kind).toBe("failed");
|
|
157
|
+
expect(reason.error).toBe(submissionError);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("exit while the job is still in flight is a cancellation", () => {
|
|
161
|
+
const manager = makeManager({ submissionState: { kind: "loading" } });
|
|
162
|
+
renderWith(manager);
|
|
163
|
+
|
|
164
|
+
(screenProps.current.onExit as () => void)();
|
|
165
|
+
|
|
166
|
+
const [[reason]] = manager.exit.mock.calls as [
|
|
167
|
+
[{ kind: string; error: unknown }],
|
|
168
|
+
];
|
|
169
|
+
expect(reason.error).toBeInstanceOf(ProcessingCancelledException);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("OrchestratedProcessingScreen — submission state mapping", () => {
|
|
174
|
+
it("shows loading during the pre-submission window so the card is never empty", () => {
|
|
175
|
+
renderWith(makeManager({ submissionState: { kind: "notAttempted" } }));
|
|
176
|
+
expect(screenProps.current.state).toEqual({ kind: "loading" });
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("shows loading while the job is in flight", () => {
|
|
180
|
+
renderWith(makeManager({ submissionState: { kind: "loading" } }));
|
|
181
|
+
expect(screenProps.current.state).toEqual({ kind: "loading" });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("surfaces the server's message on success", () => {
|
|
185
|
+
renderWith(
|
|
186
|
+
makeManager({
|
|
187
|
+
submissionState: {
|
|
188
|
+
kind: "success",
|
|
189
|
+
response: {
|
|
190
|
+
userId: "user-1",
|
|
191
|
+
jobId: "job-1",
|
|
192
|
+
status: "approved",
|
|
193
|
+
message: "Job approved",
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
expect(screenProps.current.state).toEqual({
|
|
200
|
+
kind: "success",
|
|
201
|
+
message: "Job approved",
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("surfaces the server's own text on a server failure", () => {
|
|
206
|
+
renderWith(
|
|
207
|
+
makeManager({
|
|
208
|
+
submissionState: {
|
|
209
|
+
kind: "failure",
|
|
210
|
+
error: new NetworkServerException({
|
|
211
|
+
statusCode: 401,
|
|
212
|
+
message: "Invalid token",
|
|
213
|
+
}),
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
expect(screenProps.current.state).toEqual({
|
|
219
|
+
kind: "failed",
|
|
220
|
+
errorMessage: "Invalid token",
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("shows the no-connection copy on a transport failure", () => {
|
|
225
|
+
renderWith(
|
|
226
|
+
makeManager({
|
|
227
|
+
submissionState: {
|
|
228
|
+
kind: "failure",
|
|
229
|
+
error: new NetworkTransportException({ cause: new Error("ECONNREFUSED") }),
|
|
230
|
+
},
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
expect(screenProps.current.state).toEqual({
|
|
235
|
+
kind: "failed",
|
|
236
|
+
errorMessage: "No internet connection.",
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("shows the generic copy for unknown network failures", () => {
|
|
241
|
+
renderWith(
|
|
242
|
+
makeManager({
|
|
243
|
+
submissionState: {
|
|
244
|
+
kind: "failure",
|
|
245
|
+
error: new NetworkUnknownException({
|
|
246
|
+
cause: new Error("ENOENT /data/user/0/…/selfie.jpg"),
|
|
247
|
+
}),
|
|
248
|
+
},
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
expect(screenProps.current.state).toEqual({
|
|
253
|
+
kind: "failed",
|
|
254
|
+
errorMessage: "Something went wrong. Try again.",
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("surfaces the error message on other failures", () => {
|
|
259
|
+
renderWith(
|
|
260
|
+
makeManager({
|
|
261
|
+
submissionState: { kind: "failure", error: new Error("upload failed") },
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
expect(screenProps.current.state).toEqual({
|
|
266
|
+
kind: "failed",
|
|
267
|
+
errorMessage: "upload failed",
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("OrchestratedProcessingScreen — artifact rendering", () => {
|
|
273
|
+
it("renders the captured front document for a document job", () => {
|
|
274
|
+
renderWith(
|
|
275
|
+
makeManager({
|
|
276
|
+
jobType: JobType.documentVerification,
|
|
277
|
+
frontDocumentPath: "file:///front.jpg",
|
|
278
|
+
documentFrameAspectRatio: 1.586,
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
expect(screenProps.current.documentImageUri).toBe("file:///front.jpg");
|
|
283
|
+
expect(screenProps.current.documentAspectRatio).toBeCloseTo(1.586, 5);
|
|
284
|
+
expect(screenProps.current.showFaceOval).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("omits the document aspect ratio when the manager has none", () => {
|
|
288
|
+
renderWith(
|
|
289
|
+
makeManager({
|
|
290
|
+
jobType: JobType.documentVerification,
|
|
291
|
+
frontDocumentPath: "file:///front.jpg",
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
expect(screenProps.current.documentImageUri).toBe("file:///front.jpg");
|
|
296
|
+
expect("documentAspectRatio" in screenProps.current).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("ignores a captured document on a selfie-only job", () => {
|
|
300
|
+
renderWith(
|
|
301
|
+
makeManager({
|
|
302
|
+
jobType: JobType.smartSelfieEnrollment,
|
|
303
|
+
frontDocumentPath: "file:///front.jpg",
|
|
304
|
+
selfiePath: "file:///selfie.jpg",
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
expect("documentImageUri" in screenProps.current).toBe(false);
|
|
309
|
+
expect(screenProps.current.capturedSelfiePath).toBe("file:///selfie.jpg");
|
|
310
|
+
expect(screenProps.current.showFaceOval).toBe(true);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("hides the face oval for a job that captures neither", () => {
|
|
314
|
+
renderWith(makeManager({ jobType: JobType.enhancedKyc }));
|
|
315
|
+
|
|
316
|
+
expect(screenProps.current.showFaceOval).toBe(false);
|
|
317
|
+
expect("capturedSelfiePath" in screenProps.current).toBe(false);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("treats a flow with no network config as the unknown job type", () => {
|
|
321
|
+
renderWith(makeManager());
|
|
322
|
+
|
|
323
|
+
expect(screenProps.current.showFaceOval).toBe(false);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("forwards the partner's progress and attribution switches", () => {
|
|
327
|
+
renderWith(
|
|
328
|
+
makeManager(),
|
|
329
|
+
makeConfiguration({
|
|
330
|
+
showProgressPercentage: false,
|
|
331
|
+
showAttribution: false,
|
|
332
|
+
}),
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
expect(screenProps.current.showProgressPercentage).toBe(false);
|
|
336
|
+
expect(screenProps.current.showAttribution).toBe(false);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
@@ -7,7 +7,15 @@ import {
|
|
|
7
7
|
type ProcessingState,
|
|
8
8
|
} from "../../screen/use_smile_id_processing_screen";
|
|
9
9
|
import { UseSmileIDOrchestratedScreenHost } from "./use_smile_id_orchestrated_screen_host";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
NetworkResponseParseException,
|
|
12
|
+
NetworkTimeoutException,
|
|
13
|
+
NetworkTransportException,
|
|
14
|
+
NetworkUnknownException,
|
|
15
|
+
ProcessingCancelledException,
|
|
16
|
+
} from "../../../core/exception";
|
|
17
|
+
import { useUseSmileIDLocalizations } from "../../../l10n/use_smile_id_localizations_provider";
|
|
18
|
+
import type { UseSmileIDLocalizations } from "../../../l10n/use_smile_id_localizations";
|
|
11
19
|
import {
|
|
12
20
|
JobType,
|
|
13
21
|
requiresDocument,
|
|
@@ -68,9 +76,11 @@ function ProcessingScreenBody(props: ProcessingScreenBodyProps) {
|
|
|
68
76
|
: undefined;
|
|
69
77
|
const documentAspectRatio = navigationManager.documentFrameAspectRatio;
|
|
70
78
|
|
|
79
|
+
const loc = useUseSmileIDLocalizations();
|
|
80
|
+
|
|
71
81
|
return (
|
|
72
82
|
<UseSmileIDProcessingScreen
|
|
73
|
-
state={mapToProcessingState(navigationManager.jobSubmissionState)}
|
|
83
|
+
state={mapToProcessingState(navigationManager.jobSubmissionState, loc)}
|
|
74
84
|
showFaceOval={requiresSelfie(jobType) || requiresDocument(jobType)}
|
|
75
85
|
showProgressPercentage={props.processingConfig.showProgressPercentage}
|
|
76
86
|
showAttribution={props.processingConfig.showAttribution}
|
|
@@ -86,19 +96,29 @@ function ProcessingScreenBody(props: ProcessingScreenBodyProps) {
|
|
|
86
96
|
onExit={() =>
|
|
87
97
|
navigationManager.exit({
|
|
88
98
|
kind: "failed",
|
|
89
|
-
error:
|
|
99
|
+
error: exitError(navigationManager.jobSubmissionState),
|
|
90
100
|
})
|
|
91
101
|
}
|
|
92
102
|
/>
|
|
93
103
|
);
|
|
94
104
|
}
|
|
95
105
|
|
|
106
|
+
/** The error the flow exits with when the user leaves the processing screen; an already-failed submission is delivered as its own failure. */
|
|
107
|
+
function exitError(state: JobSubmissionState): Error {
|
|
108
|
+
return state.kind === "failure"
|
|
109
|
+
? state.error
|
|
110
|
+
: new ProcessingCancelledException();
|
|
111
|
+
}
|
|
112
|
+
|
|
96
113
|
/**
|
|
97
114
|
* Translate `JobSubmissionState` to the screen's `ProcessingState`.
|
|
98
115
|
* `notAttempted` maps to `loading` so the brief pre-submission window
|
|
99
116
|
* doesn't render an empty card.
|
|
100
117
|
*/
|
|
101
|
-
function mapToProcessingState(
|
|
118
|
+
function mapToProcessingState(
|
|
119
|
+
state: JobSubmissionState,
|
|
120
|
+
loc: UseSmileIDLocalizations,
|
|
121
|
+
): ProcessingState {
|
|
102
122
|
switch (state.kind) {
|
|
103
123
|
case "notAttempted":
|
|
104
124
|
case "loading":
|
|
@@ -106,6 +126,23 @@ function mapToProcessingState(state: JobSubmissionState): ProcessingState {
|
|
|
106
126
|
case "success":
|
|
107
127
|
return { kind: "success", message: state.response.message };
|
|
108
128
|
case "failure":
|
|
109
|
-
return { kind: "failed", errorMessage: state.error
|
|
129
|
+
return { kind: "failed", errorMessage: userFacingMessage(state.error, loc) };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Transport/timeout and parse/unknown network failures carry technical text — every other failure keeps its message. */
|
|
134
|
+
function userFacingMessage(error: Error, loc: UseSmileIDLocalizations): string {
|
|
135
|
+
if (
|
|
136
|
+
error instanceof NetworkTransportException ||
|
|
137
|
+
error instanceof NetworkTimeoutException
|
|
138
|
+
) {
|
|
139
|
+
return loc.siProcessingErrorNoConnection;
|
|
140
|
+
}
|
|
141
|
+
if (
|
|
142
|
+
error instanceof NetworkResponseParseException ||
|
|
143
|
+
error instanceof NetworkUnknownException
|
|
144
|
+
) {
|
|
145
|
+
return loc.siProcessingErrorGeneric;
|
|
110
146
|
}
|
|
147
|
+
return error.message;
|
|
111
148
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useEffect, useMemo, useReducer, useRef } from "react";
|
|
2
|
-
import { Text, useColorScheme, View } from "react-native";
|
|
1
|
+
import { useEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
2
|
+
import { AppState, Text, useColorScheme, View } from "react-native";
|
|
3
3
|
import type { FlowConfiguration } from "./config/flow_configuration";
|
|
4
4
|
import type { FlowBuildResult } from "./dsl/flow_build_result";
|
|
5
5
|
import {
|
|
@@ -102,6 +102,9 @@ export type FlowSuccessRendererProps = {
|
|
|
102
102
|
readonly configuration: FlowConfiguration;
|
|
103
103
|
};
|
|
104
104
|
|
|
105
|
+
/** Ceiling on the first-frame font wait — past it the flow renders regardless. */
|
|
106
|
+
const FONT_REGISTRATION_TIMEOUT_MS = 1000;
|
|
107
|
+
|
|
105
108
|
/**
|
|
106
109
|
* Walks the partner through the configured screen list, dispatches
|
|
107
110
|
* frames to the chosen analyzer, submits the resulting artifacts, and
|
|
@@ -141,10 +144,17 @@ export function FlowSuccessRenderer({
|
|
|
141
144
|
? defaultDarkTheme
|
|
142
145
|
: defaultLightTheme;
|
|
143
146
|
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
// The first frame waits on font registration: a label laid out in the system font and re-measured in DM Sans can settle clipped.
|
|
148
|
+
const fontsLoaded = useUseSmileIDFonts();
|
|
149
|
+
const [fontWaitElapsed, setFontWaitElapsed] = useState(false);
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (fontsLoaded) return;
|
|
152
|
+
const timer = setTimeout(
|
|
153
|
+
() => setFontWaitElapsed(true),
|
|
154
|
+
FONT_REGISTRATION_TIMEOUT_MS,
|
|
155
|
+
);
|
|
156
|
+
return () => clearTimeout(timer);
|
|
157
|
+
}, [fontsLoaded]);
|
|
148
158
|
const enableCrashReporting =
|
|
149
159
|
configuration.config?.enableCrashReporting !== false;
|
|
150
160
|
// Sandbox sessions are test traffic and must not pollute the production
|
|
@@ -185,6 +195,9 @@ export function FlowSuccessRenderer({
|
|
|
185
195
|
const onResultRef = useRef(configuration.onResult);
|
|
186
196
|
onResultRef.current = configuration.onResult;
|
|
187
197
|
|
|
198
|
+
// Latched by `handleExit` so an unmounted-before-finishing flow can deliver exactly once.
|
|
199
|
+
const finishedRef = useRef(false);
|
|
200
|
+
|
|
188
201
|
// Force-update reducer drives re-renders when the manager's cursor
|
|
189
202
|
// or captured artifacts change. Cheaper than mirroring state into
|
|
190
203
|
// React state — the manager is the single source of truth.
|
|
@@ -220,6 +233,7 @@ export function FlowSuccessRenderer({
|
|
|
220
233
|
|
|
221
234
|
const manager = useMemo(() => {
|
|
222
235
|
const handleExit = (reason: FlowExitReason) => {
|
|
236
|
+
finishedRef.current = true;
|
|
223
237
|
const onResult = onResultRef.current;
|
|
224
238
|
try {
|
|
225
239
|
if (reason.kind === "completed") {
|
|
@@ -271,6 +285,17 @@ export function FlowSuccessRenderer({
|
|
|
271
285
|
};
|
|
272
286
|
}, [manager]);
|
|
273
287
|
|
|
288
|
+
// A flow unmounted before finishing delivers a cancelled result exactly once.
|
|
289
|
+
useEffect(
|
|
290
|
+
() => () => {
|
|
291
|
+
if (!finishedRef.current) {
|
|
292
|
+
finishedRef.current = true;
|
|
293
|
+
onResultRef.current(cancelled());
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
[],
|
|
297
|
+
);
|
|
298
|
+
|
|
274
299
|
// Resolve localizations:
|
|
275
300
|
// 1. Explicit `configuration.localizations` (partner-set on the
|
|
276
301
|
// builder) — wins over everything.
|
|
@@ -279,13 +304,23 @@ export function FlowSuccessRenderer({
|
|
|
279
304
|
// registered locales with `UseSmileIDLocalizations.register(...)`.
|
|
280
305
|
// 3. English defaults — no provider mounted, screens read from
|
|
281
306
|
// the context's default English-only instance.
|
|
307
|
+
// A device locale change requires leaving the app, so re-reading on foreground is what makes a mid-flow switch observable.
|
|
308
|
+
const [localeEpoch, bumpLocaleEpoch] = useReducer((x: number) => x + 1, 0);
|
|
309
|
+
useEffect(() => {
|
|
310
|
+
const subscription = AppState.addEventListener("change", (nextState) => {
|
|
311
|
+
if (nextState === "active") bumpLocaleEpoch();
|
|
312
|
+
});
|
|
313
|
+
return () => subscription.remove();
|
|
314
|
+
}, []);
|
|
315
|
+
|
|
316
|
+
const activeLocaleKey = UseSmileIDLocalizations.activeLocaleKey;
|
|
282
317
|
const resolved = useMemo(() => {
|
|
283
318
|
if (configuration.localizations) return configuration.localizations;
|
|
284
319
|
if (UseSmileIDLocalizations.hasRegisteredLocales()) {
|
|
285
320
|
return UseSmileIDLocalizations.forActiveLocale();
|
|
286
321
|
}
|
|
287
322
|
return null;
|
|
288
|
-
}, [configuration.localizations]);
|
|
323
|
+
}, [configuration.localizations, activeLocaleKey, localeEpoch]);
|
|
289
324
|
|
|
290
325
|
const step = manager.currentScreen;
|
|
291
326
|
|
|
@@ -331,7 +366,16 @@ export function FlowSuccessRenderer({
|
|
|
331
366
|
// resolve — the context default (English) covers the no-provider
|
|
332
367
|
// path without an extra wrapper.
|
|
333
368
|
const themed = (
|
|
334
|
-
<UseSmileIDThemeProvider value={theme}>
|
|
369
|
+
<UseSmileIDThemeProvider value={theme}>
|
|
370
|
+
{fontsLoaded || fontWaitElapsed ? (
|
|
371
|
+
inner
|
|
372
|
+
) : (
|
|
373
|
+
<View
|
|
374
|
+
style={{ flex: 1, backgroundColor: theme.colors.background }}
|
|
375
|
+
testID="si_flow_font_gate"
|
|
376
|
+
/>
|
|
377
|
+
)}
|
|
378
|
+
</UseSmileIDThemeProvider>
|
|
335
379
|
);
|
|
336
380
|
|
|
337
381
|
if (resolved) {
|