@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,53 @@
|
|
|
1
|
+
import type { UserDetails } from "../../../core/models/params";
|
|
2
|
+
import { InvalidFieldValueException } from "../../../core/exception/job_validation_exceptions";
|
|
3
|
+
import type { TokenPayload } from "../../../data/model/token_payload";
|
|
4
|
+
import type { ValidationIssue } from "./validation_state";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Field-union rule shared by the content validator and the job-type required
|
|
8
|
+
* rule: a field is covered when the token binds it or the builder supplies it.
|
|
9
|
+
*/
|
|
10
|
+
export function missingUserDetailsFieldIssues(
|
|
11
|
+
userDetails: UserDetails | undefined,
|
|
12
|
+
tokenPayload: TokenPayload | undefined,
|
|
13
|
+
): ValidationIssue[] {
|
|
14
|
+
const issues: ValidationIssue[] = [];
|
|
15
|
+
const fieldReason =
|
|
16
|
+
tokenPayload === undefined
|
|
17
|
+
? "must not be blank"
|
|
18
|
+
: "not bound by the token and not supplied on the builder";
|
|
19
|
+
const tokenBindsField = {
|
|
20
|
+
givenNames: tokenPayload?.hasGivenNames === true,
|
|
21
|
+
lastName: tokenPayload?.hasLastName === true,
|
|
22
|
+
} as const;
|
|
23
|
+
for (const field of ["givenNames", "lastName"] as const) {
|
|
24
|
+
if (tokenBindsField[field]) continue;
|
|
25
|
+
const value = userDetails?.[field];
|
|
26
|
+
if (!value || !value.trim()) {
|
|
27
|
+
issues.push(
|
|
28
|
+
new InvalidFieldValueException({
|
|
29
|
+
fieldName: `userDetails.${field}`,
|
|
30
|
+
reason: fieldReason,
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// email and phoneNumber are each optional, but one must come from the builder or the token.
|
|
36
|
+
const hasEmail = !!userDetails?.email && userDetails.email.trim().length > 0;
|
|
37
|
+
const hasPhone =
|
|
38
|
+
!!userDetails?.phoneNumber && userDetails.phoneNumber.trim().length > 0;
|
|
39
|
+
const tokenHasContact =
|
|
40
|
+
tokenPayload?.hasEmail === true || tokenPayload?.hasPhoneNumber === true;
|
|
41
|
+
if (!hasEmail && !hasPhone && !tokenHasContact) {
|
|
42
|
+
issues.push(
|
|
43
|
+
new InvalidFieldValueException({
|
|
44
|
+
fieldName: "userDetails",
|
|
45
|
+
reason:
|
|
46
|
+
tokenPayload === undefined
|
|
47
|
+
? "at least one of email or phoneNumber must be non blank"
|
|
48
|
+
: "at least one contact field must be bound by the token or supplied on the builder",
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return issues;
|
|
53
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
documentFeedbackKeyFor,
|
|
5
|
+
resolveFeedbackCopy,
|
|
6
|
+
selfieFeedbackKeyFor,
|
|
7
|
+
} from "../helpers/capture_screen_helpers";
|
|
8
|
+
import { UseSmileIDOverlayState } from "../../state/use_smile_id_overlay_state";
|
|
9
|
+
import { UseSmileIDLocalizations } from "../../../l10n/use_smile_id_localizations";
|
|
10
|
+
import { enStrings, type SiKey } from "../../../l10n/strings_en";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The feedback-key → copy table is the single seam between the capture
|
|
14
|
+
* overlay's stable `si_*` keys and the partner's localization instance.
|
|
15
|
+
* Every entry must resolve to the canonical English default and must
|
|
16
|
+
* honour a partner override, otherwise a translated build silently
|
|
17
|
+
* shows English (or the wrong string) mid-capture.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const loc = new UseSmileIDLocalizations({});
|
|
21
|
+
|
|
22
|
+
// Every key the capture surface can emit as feedback.
|
|
23
|
+
const FEEDBACK_KEYS: readonly SiKey[] = [
|
|
24
|
+
"si_capture_initial_instruction",
|
|
25
|
+
"si_start_capture_instruction",
|
|
26
|
+
"si_capture_too_close",
|
|
27
|
+
"si_capture_too_far",
|
|
28
|
+
"si_capture_center_face",
|
|
29
|
+
"si_capture_multiple_faces",
|
|
30
|
+
"si_capture_low_light",
|
|
31
|
+
"si_capture_bright_light",
|
|
32
|
+
"si_capture_rotate_device",
|
|
33
|
+
"si_capture_smile",
|
|
34
|
+
"si_capture_move_left",
|
|
35
|
+
"si_capture_move_right",
|
|
36
|
+
"si_capture_move_higher",
|
|
37
|
+
"si_capture_move_lower",
|
|
38
|
+
"si_capture_ready",
|
|
39
|
+
"si_capture_hold_steady",
|
|
40
|
+
"si_capture_timeout",
|
|
41
|
+
"si_capturing",
|
|
42
|
+
"si_selfie_capture_complete",
|
|
43
|
+
"si_selfie_capture_timeout",
|
|
44
|
+
"si_selfie_capture_unsatisfied",
|
|
45
|
+
"si_ess_capture_initial_instruction",
|
|
46
|
+
"si_ess_hold_still",
|
|
47
|
+
"si_ess_look_left",
|
|
48
|
+
"si_ess_look_right",
|
|
49
|
+
"si_ess_look_up",
|
|
50
|
+
"si_ess_look_at_camera",
|
|
51
|
+
"si_document_initial_instruction",
|
|
52
|
+
"si_document_ready",
|
|
53
|
+
"si_document_capturing",
|
|
54
|
+
"si_document_capture_complete",
|
|
55
|
+
"si_document_capture_unsatisfied",
|
|
56
|
+
"si_document_capture_timeout",
|
|
57
|
+
"si_document_too_close",
|
|
58
|
+
"si_document_too_far",
|
|
59
|
+
"si_document_center",
|
|
60
|
+
"si_document_hold_still",
|
|
61
|
+
"si_document_gallery_image_too_small",
|
|
62
|
+
"si_document_gallery_image_failed",
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
describe("resolveFeedbackCopy", () => {
|
|
66
|
+
for (const key of FEEDBACK_KEYS) {
|
|
67
|
+
it(`resolves ${key} to its canonical English default`, () => {
|
|
68
|
+
expect(resolveFeedbackCopy(key, loc)).toBe(enStrings[key]);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
it("honours a partner override for every feedback key", () => {
|
|
73
|
+
const overrides = Object.fromEntries(
|
|
74
|
+
FEEDBACK_KEYS.map((key) => [key, `translated:${key}`]),
|
|
75
|
+
);
|
|
76
|
+
const translated = new UseSmileIDLocalizations(overrides);
|
|
77
|
+
|
|
78
|
+
for (const key of FEEDBACK_KEYS) {
|
|
79
|
+
expect(resolveFeedbackCopy(key, translated)).toBe(`translated:${key}`);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("falls back to the initial instruction for a key outside the table", () => {
|
|
84
|
+
// `si_consent_title` is a real key that the capture surface never emits.
|
|
85
|
+
expect(resolveFeedbackCopy("si_consent_title" as SiKey, loc)).toBe(
|
|
86
|
+
loc.siCaptureInitialInstruction,
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("documentFeedbackKeyFor", () => {
|
|
92
|
+
it("maps each document guidance state to its stable key", () => {
|
|
93
|
+
expect(
|
|
94
|
+
documentFeedbackKeyFor(UseSmileIDOverlayState.DOCUMENT_TOO_CLOSE),
|
|
95
|
+
).toBe("si_document_too_close");
|
|
96
|
+
expect(
|
|
97
|
+
documentFeedbackKeyFor(UseSmileIDOverlayState.DOCUMENT_TOO_FAR),
|
|
98
|
+
).toBe("si_document_too_far");
|
|
99
|
+
expect(
|
|
100
|
+
documentFeedbackKeyFor(UseSmileIDOverlayState.DOCUMENT_NOT_CENTERED),
|
|
101
|
+
).toBe("si_document_center");
|
|
102
|
+
expect(
|
|
103
|
+
documentFeedbackKeyFor(UseSmileIDOverlayState.DOCUMENT_HOLD_STILL),
|
|
104
|
+
).toBe("si_document_hold_still");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("returns null for lifecycle states that carry no guidance", () => {
|
|
108
|
+
expect(
|
|
109
|
+
documentFeedbackKeyFor(UseSmileIDOverlayState.INSTRUCTION),
|
|
110
|
+
).toBeNull();
|
|
111
|
+
expect(
|
|
112
|
+
documentFeedbackKeyFor(UseSmileIDOverlayState.DOCUMENT_READY),
|
|
113
|
+
).toBeNull();
|
|
114
|
+
expect(documentFeedbackKeyFor(UseSmileIDOverlayState.CAPTURING)).toBeNull();
|
|
115
|
+
// A selfie-only guidance state must not leak into the document table.
|
|
116
|
+
expect(documentFeedbackKeyFor(UseSmileIDOverlayState.TOO_CLOSE)).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("resolves every document key it emits through the copy table", () => {
|
|
120
|
+
const states = [
|
|
121
|
+
UseSmileIDOverlayState.DOCUMENT_TOO_CLOSE,
|
|
122
|
+
UseSmileIDOverlayState.DOCUMENT_TOO_FAR,
|
|
123
|
+
UseSmileIDOverlayState.DOCUMENT_NOT_CENTERED,
|
|
124
|
+
UseSmileIDOverlayState.DOCUMENT_HOLD_STILL,
|
|
125
|
+
];
|
|
126
|
+
for (const state of states) {
|
|
127
|
+
const key = documentFeedbackKeyFor(state);
|
|
128
|
+
expect(key).not.toBeNull();
|
|
129
|
+
expect(resolveFeedbackCopy(key as SiKey, loc)).toBe(
|
|
130
|
+
enStrings[key as SiKey],
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("selfieFeedbackKeyFor", () => {
|
|
137
|
+
it("returns null for the document-only states", () => {
|
|
138
|
+
expect(
|
|
139
|
+
selfieFeedbackKeyFor(UseSmileIDOverlayState.DOCUMENT_TOO_CLOSE),
|
|
140
|
+
).toBeNull();
|
|
141
|
+
expect(
|
|
142
|
+
selfieFeedbackKeyFor(UseSmileIDOverlayState.DOCUMENT_HOLD_STILL),
|
|
143
|
+
).toBeNull();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -476,6 +476,18 @@ describe("UseSmileIDCaptureScreen", () => {
|
|
|
476
476
|
expect(mockedSetBrightness).toHaveBeenCalledWith(1);
|
|
477
477
|
});
|
|
478
478
|
|
|
479
|
+
it("asks for dark status-bar content, matching the white capture scrim", () => {
|
|
480
|
+
const { root } = render(
|
|
481
|
+
<UseSmileIDCaptureScreen
|
|
482
|
+
analyzer={fakeAnalyzer}
|
|
483
|
+
captureType={CaptureType.selfie}
|
|
484
|
+
/>,
|
|
485
|
+
);
|
|
486
|
+
expect(root.findAllByType(StatusBar)[0]!.props.barStyle).toBe(
|
|
487
|
+
"dark-content",
|
|
488
|
+
);
|
|
489
|
+
});
|
|
490
|
+
|
|
479
491
|
it("renders a translucent status bar so the preview is edge-to-edge", () => {
|
|
480
492
|
// Translucent status bar = full-bleed window = full-screen oval reference.
|
|
481
493
|
const { root } = render(
|
|
@@ -777,4 +789,340 @@ describe("session-boundary epoch reset — debouncer regression", () => {
|
|
|
777
789
|
expect(mockedTakePhoto).toHaveBeenCalledTimes(1);
|
|
778
790
|
});
|
|
779
791
|
});
|
|
792
|
+
|
|
793
|
+
describe("selfie capture timeout recovery", () => {
|
|
794
|
+
function renderSelfieScreen() {
|
|
795
|
+
return render(
|
|
796
|
+
<UseSmileIDCaptureScreen
|
|
797
|
+
analyzer={fakeAnalyzer}
|
|
798
|
+
captureType={CaptureType.selfie}
|
|
799
|
+
/>,
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function startCapture(root: ReactTestInstance) {
|
|
804
|
+
act(() => {
|
|
805
|
+
findByTestID(root, "si_capture_start_button")!.props.onClick();
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function emit(state: UseSmileIDScanState) {
|
|
810
|
+
expect(capturedOnResult).not.toBeNull();
|
|
811
|
+
act(() => {
|
|
812
|
+
capturedOnResult!({
|
|
813
|
+
output: { faces: [], brightness: 0 },
|
|
814
|
+
state,
|
|
815
|
+
capturedEvents: [],
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
it("offers Start again after a timeout", () => {
|
|
821
|
+
const { root } = renderSelfieScreen();
|
|
822
|
+
startCapture(root);
|
|
823
|
+
|
|
824
|
+
emit({ type: UseSmileIDScanStateType.Timeout });
|
|
825
|
+
|
|
826
|
+
const start = findByTestID(root, "si_capture_start_button");
|
|
827
|
+
expect(start).not.toBeNull();
|
|
828
|
+
expect(start!.props.enabled).toBe(true);
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
it("resets the native session when Start is tapped after a timeout", async () => {
|
|
832
|
+
const { root } = renderSelfieScreen();
|
|
833
|
+
startCapture(root);
|
|
834
|
+
emit({ type: UseSmileIDScanStateType.Timeout });
|
|
835
|
+
fakeAnalyzer.reset.mockClear();
|
|
836
|
+
|
|
837
|
+
startCapture(root);
|
|
838
|
+
await act(async () => {
|
|
839
|
+
await Promise.resolve();
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
// The native session is terminal after a timeout — without a reset the
|
|
843
|
+
// first retry frame re-reports Timeout and re-latches the error.
|
|
844
|
+
expect(fakeAnalyzer.reset).toHaveBeenCalledTimes(1);
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it("restarts the session when Start is tapped after a timeout", () => {
|
|
848
|
+
const onStartCapture = vi.fn();
|
|
849
|
+
const { root } = render(
|
|
850
|
+
<UseSmileIDCaptureScreen
|
|
851
|
+
analyzer={fakeAnalyzer}
|
|
852
|
+
captureType={CaptureType.selfie}
|
|
853
|
+
onStartCapture={onStartCapture}
|
|
854
|
+
/>,
|
|
855
|
+
);
|
|
856
|
+
startCapture(root);
|
|
857
|
+
emit({ type: UseSmileIDScanStateType.Timeout });
|
|
858
|
+
|
|
859
|
+
startCapture(root);
|
|
860
|
+
|
|
861
|
+
expect(onStartCapture).toHaveBeenCalledTimes(2);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it("still delivers a capture that completes on the retry", async () => {
|
|
865
|
+
const onCaptureComplete = vi.fn();
|
|
866
|
+
const { root } = render(
|
|
867
|
+
<UseSmileIDCaptureScreen
|
|
868
|
+
analyzer={fakeAnalyzer}
|
|
869
|
+
captureType={CaptureType.selfie}
|
|
870
|
+
onCaptureComplete={onCaptureComplete}
|
|
871
|
+
/>,
|
|
872
|
+
);
|
|
873
|
+
startCapture(root);
|
|
874
|
+
emit({ type: UseSmileIDScanStateType.Timeout });
|
|
875
|
+
startCapture(root);
|
|
876
|
+
|
|
877
|
+
emit({ type: UseSmileIDScanStateType.Finished });
|
|
878
|
+
await act(async () => {
|
|
879
|
+
await Promise.resolve();
|
|
880
|
+
await Promise.resolve();
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
expect(onCaptureComplete).toHaveBeenCalledTimes(1);
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
it("drops a capture that lands after the timeout was already reported", async () => {
|
|
887
|
+
const onCaptureComplete = vi.fn();
|
|
888
|
+
const onCaptureTimeout = vi.fn();
|
|
889
|
+
const { root } = render(
|
|
890
|
+
<UseSmileIDCaptureScreen
|
|
891
|
+
analyzer={fakeAnalyzer}
|
|
892
|
+
captureType={CaptureType.selfie}
|
|
893
|
+
onCaptureComplete={onCaptureComplete}
|
|
894
|
+
onCaptureTimeout={onCaptureTimeout}
|
|
895
|
+
/>,
|
|
896
|
+
);
|
|
897
|
+
startCapture(root);
|
|
898
|
+
|
|
899
|
+
emit({ type: UseSmileIDScanStateType.Timeout });
|
|
900
|
+
emit({ type: UseSmileIDScanStateType.Finished });
|
|
901
|
+
await act(async () => {
|
|
902
|
+
await Promise.resolve();
|
|
903
|
+
await Promise.resolve();
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
expect(onCaptureTimeout).toHaveBeenCalledTimes(1);
|
|
907
|
+
expect(onCaptureComplete).not.toHaveBeenCalled();
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
it("does not pad the retry with frames banked before the timeout", async () => {
|
|
911
|
+
const onCaptureComplete = vi.fn();
|
|
912
|
+
const { root } = render(
|
|
913
|
+
<UseSmileIDCaptureScreen
|
|
914
|
+
analyzer={fakeAnalyzer}
|
|
915
|
+
captureType={CaptureType.selfie}
|
|
916
|
+
onCaptureComplete={onCaptureComplete}
|
|
917
|
+
/>,
|
|
918
|
+
);
|
|
919
|
+
startCapture(root);
|
|
920
|
+
act(() => {
|
|
921
|
+
capturedOnResult!({
|
|
922
|
+
output: { faces: [], brightness: 0 },
|
|
923
|
+
state: { type: UseSmileIDScanStateType.Found },
|
|
924
|
+
capturedEvents: [
|
|
925
|
+
{
|
|
926
|
+
kind: CaptureReadyEventKind.Liveness,
|
|
927
|
+
captureIndex: 0,
|
|
928
|
+
url: "file:///stale.jpg",
|
|
929
|
+
},
|
|
930
|
+
] as CaptureReadyEvent[],
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
emit({ type: UseSmileIDScanStateType.Timeout });
|
|
934
|
+
|
|
935
|
+
startCapture(root);
|
|
936
|
+
act(() => {
|
|
937
|
+
capturedOnResult!({
|
|
938
|
+
output: { faces: [], brightness: 0 },
|
|
939
|
+
state: { type: UseSmileIDScanStateType.Found },
|
|
940
|
+
capturedEvents: [
|
|
941
|
+
{
|
|
942
|
+
kind: CaptureReadyEventKind.Selfie,
|
|
943
|
+
captureIndex: 0,
|
|
944
|
+
url: "file:///fresh.jpg",
|
|
945
|
+
},
|
|
946
|
+
] as CaptureReadyEvent[],
|
|
947
|
+
});
|
|
948
|
+
});
|
|
949
|
+
emit({ type: UseSmileIDScanStateType.Finished });
|
|
950
|
+
await act(async () => {
|
|
951
|
+
await Promise.resolve();
|
|
952
|
+
await Promise.resolve();
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
expect(onCaptureComplete).toHaveBeenCalledTimes(1);
|
|
956
|
+
const [delivered] = onCaptureComplete.mock.calls[0] as [
|
|
957
|
+
readonly CaptureReadyEvent[],
|
|
958
|
+
];
|
|
959
|
+
expect(delivered.map((event) => event.url)).toEqual([
|
|
960
|
+
"file:///fresh.jpg",
|
|
961
|
+
]);
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
it("reports the timeout to the partner exactly once", () => {
|
|
965
|
+
const onCaptureTimeout = vi.fn();
|
|
966
|
+
const { root } = render(
|
|
967
|
+
<UseSmileIDCaptureScreen
|
|
968
|
+
analyzer={fakeAnalyzer}
|
|
969
|
+
captureType={CaptureType.selfie}
|
|
970
|
+
onCaptureTimeout={onCaptureTimeout}
|
|
971
|
+
/>,
|
|
972
|
+
);
|
|
973
|
+
startCapture(root);
|
|
974
|
+
|
|
975
|
+
emit({ type: UseSmileIDScanStateType.Timeout });
|
|
976
|
+
emit({ type: UseSmileIDScanStateType.Timeout });
|
|
977
|
+
|
|
978
|
+
expect(onCaptureTimeout).toHaveBeenCalledTimes(1);
|
|
979
|
+
});
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
describe("no camera frames at all", () => {
|
|
983
|
+
beforeEach(() => {
|
|
984
|
+
vi.useFakeTimers();
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
afterEach(() => {
|
|
988
|
+
vi.useRealTimers();
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
function renderAndStart() {
|
|
992
|
+
const tree = render(
|
|
993
|
+
<UseSmileIDCaptureScreen
|
|
994
|
+
analyzer={fakeAnalyzer}
|
|
995
|
+
captureType={CaptureType.selfie}
|
|
996
|
+
/>,
|
|
997
|
+
);
|
|
998
|
+
act(() => {
|
|
999
|
+
findByTestID(tree.root, "si_capture_start_button")!.props.onClick();
|
|
1000
|
+
});
|
|
1001
|
+
return tree;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function elapseFirstFrameWait() {
|
|
1005
|
+
act(() => {
|
|
1006
|
+
vi.advanceTimersByTime(10_000);
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
it("names the cause instead of sitting on the initial instruction", () => {
|
|
1011
|
+
const { root } = renderAndStart();
|
|
1012
|
+
|
|
1013
|
+
elapseFirstFrameWait();
|
|
1014
|
+
|
|
1015
|
+
expect(collectText(findByTestID(root, "si_capture_feedback_text")!)).toBe(
|
|
1016
|
+
enStrings.si_capture_camera_unavailable,
|
|
1017
|
+
);
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
it("leaves the user a way to retry", () => {
|
|
1021
|
+
const { root } = renderAndStart();
|
|
1022
|
+
|
|
1023
|
+
elapseFirstFrameWait();
|
|
1024
|
+
|
|
1025
|
+
const start = findByTestID(root, "si_capture_start_button");
|
|
1026
|
+
expect(start).not.toBeNull();
|
|
1027
|
+
expect(start!.props.enabled).toBe(true);
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
it("reports a terminal timeout so the flow is not left waiting", () => {
|
|
1031
|
+
const onCaptureTimeout = vi.fn();
|
|
1032
|
+
const tree = render(
|
|
1033
|
+
<UseSmileIDCaptureScreen
|
|
1034
|
+
analyzer={fakeAnalyzer}
|
|
1035
|
+
captureType={CaptureType.selfie}
|
|
1036
|
+
onCaptureTimeout={onCaptureTimeout}
|
|
1037
|
+
/>,
|
|
1038
|
+
);
|
|
1039
|
+
act(() => {
|
|
1040
|
+
findByTestID(tree.root, "si_capture_start_button")!.props.onClick();
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
elapseFirstFrameWait();
|
|
1044
|
+
|
|
1045
|
+
expect(onCaptureTimeout).toHaveBeenCalledTimes(1);
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
it("drops the stall copy once a frame finally arrives", () => {
|
|
1049
|
+
const { root } = renderAndStart();
|
|
1050
|
+
elapseFirstFrameWait();
|
|
1051
|
+
expect(collectText(findByTestID(root, "si_capture_feedback_text")!)).toBe(
|
|
1052
|
+
enStrings.si_capture_camera_unavailable,
|
|
1053
|
+
);
|
|
1054
|
+
|
|
1055
|
+
act(() => {
|
|
1056
|
+
capturedOnResult!({
|
|
1057
|
+
output: { faces: [], brightness: 0 },
|
|
1058
|
+
state: { type: UseSmileIDScanStateType.Initial },
|
|
1059
|
+
capturedEvents: [],
|
|
1060
|
+
});
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
expect(
|
|
1064
|
+
collectText(findByTestID(root, "si_capture_feedback_text")!),
|
|
1065
|
+
).not.toBe(enStrings.si_capture_camera_unavailable);
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
it("still fires when the partner passes an inline callback", () => {
|
|
1069
|
+
const onCaptureTimeout = vi.fn();
|
|
1070
|
+
const screen = (
|
|
1071
|
+
<UseSmileIDCaptureScreen
|
|
1072
|
+
analyzer={fakeAnalyzer}
|
|
1073
|
+
captureType={CaptureType.selfie}
|
|
1074
|
+
onCaptureTimeout={(state) => onCaptureTimeout(state)}
|
|
1075
|
+
/>
|
|
1076
|
+
);
|
|
1077
|
+
const tree = render(screen);
|
|
1078
|
+
act(() => {
|
|
1079
|
+
findByTestID(tree.root, "si_capture_start_button")!.props.onClick();
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
for (let step = 0; step < 4; step += 1) {
|
|
1083
|
+
act(() => {
|
|
1084
|
+
tree.update(
|
|
1085
|
+
<UseSmileIDCaptureScreen
|
|
1086
|
+
analyzer={fakeAnalyzer}
|
|
1087
|
+
captureType={CaptureType.selfie}
|
|
1088
|
+
onCaptureTimeout={(state) => onCaptureTimeout(state)}
|
|
1089
|
+
/>,
|
|
1090
|
+
);
|
|
1091
|
+
});
|
|
1092
|
+
act(() => {
|
|
1093
|
+
vi.advanceTimersByTime(3_000);
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
expect(onCaptureTimeout).toHaveBeenCalledTimes(1);
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
it("stays quiet once a frame has been analyzed", () => {
|
|
1101
|
+
const onCaptureTimeout = vi.fn();
|
|
1102
|
+
const tree = render(
|
|
1103
|
+
<UseSmileIDCaptureScreen
|
|
1104
|
+
analyzer={fakeAnalyzer}
|
|
1105
|
+
captureType={CaptureType.selfie}
|
|
1106
|
+
onCaptureTimeout={onCaptureTimeout}
|
|
1107
|
+
/>,
|
|
1108
|
+
);
|
|
1109
|
+
act(() => {
|
|
1110
|
+
findByTestID(tree.root, "si_capture_start_button")!.props.onClick();
|
|
1111
|
+
});
|
|
1112
|
+
act(() => {
|
|
1113
|
+
capturedOnResult!({
|
|
1114
|
+
output: { faces: [], brightness: 0 },
|
|
1115
|
+
state: { type: UseSmileIDScanStateType.Initial },
|
|
1116
|
+
capturedEvents: [],
|
|
1117
|
+
});
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
elapseFirstFrameWait();
|
|
1121
|
+
|
|
1122
|
+
expect(onCaptureTimeout).not.toHaveBeenCalled();
|
|
1123
|
+
expect(
|
|
1124
|
+
collectText(findByTestID(tree.root, "si_capture_feedback_text")!),
|
|
1125
|
+
).not.toBe(enStrings.si_capture_camera_unavailable);
|
|
1126
|
+
});
|
|
1127
|
+
});
|
|
780
1128
|
});
|
|
@@ -62,7 +62,9 @@ describe("UseSmileIDDocumentBackInstructions", () => {
|
|
|
62
62
|
/>,
|
|
63
63
|
);
|
|
64
64
|
expect(findByTestID(root, "si_document_back_instructions")).not.toBeNull();
|
|
65
|
-
expect(
|
|
65
|
+
expect(
|
|
66
|
+
findTextNode(root, "Flip your \u2068Green Book\u2069"),
|
|
67
|
+
).not.toBeNull();
|
|
66
68
|
});
|
|
67
69
|
|
|
68
70
|
it("invokes onCaptureBack from the primary button", () => {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { create as createRenderer, act } from "react-test-renderer";
|
|
4
|
+
import { UseSmileIDPermissionScreen } from "../use_smile_id_permission_screen";
|
|
5
|
+
import { UseSmileIDThemeProvider } from "../../theme/theme_context";
|
|
6
|
+
import { defaultDarkTheme, defaultLightTheme } from "../../theme/theme";
|
|
7
|
+
|
|
8
|
+
function render(element: React.ReactElement) {
|
|
9
|
+
let tree!: ReturnType<typeof createRenderer>;
|
|
10
|
+
act(() => {
|
|
11
|
+
tree = createRenderer(element);
|
|
12
|
+
});
|
|
13
|
+
return tree;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function rootStyle(
|
|
17
|
+
tree: ReturnType<typeof createRenderer>,
|
|
18
|
+
): Record<string, unknown> {
|
|
19
|
+
const json = tree.toJSON() as { props: { style: Record<string, unknown> } };
|
|
20
|
+
return json.props.style;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("UseSmileIDPermissionScreen", () => {
|
|
24
|
+
it("shows a default camera rationale when the partner supplies none", () => {
|
|
25
|
+
const tree = render(
|
|
26
|
+
<UseSmileIDPermissionScreen theme={defaultLightTheme} />,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
expect(JSON.stringify(tree.toJSON())).toContain(
|
|
30
|
+
"Camera permission required",
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("shows the partner's rationale when one is supplied", () => {
|
|
35
|
+
const tree = render(
|
|
36
|
+
<UseSmileIDPermissionScreen
|
|
37
|
+
theme={defaultLightTheme}
|
|
38
|
+
cameraRationale="Acme needs your camera to verify your ID"
|
|
39
|
+
/>,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const text = JSON.stringify(tree.toJSON());
|
|
43
|
+
expect(text).toContain("Acme needs your camera to verify your ID");
|
|
44
|
+
expect(text).not.toContain("Camera permission required");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("paints on the theme background from the theme prop", () => {
|
|
48
|
+
const tree = render(
|
|
49
|
+
<UseSmileIDPermissionScreen theme={defaultDarkTheme} />,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(rootStyle(tree).backgroundColor).toBe(
|
|
53
|
+
defaultDarkTheme.colors.background,
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("falls back to the theme from context when no theme prop is given", () => {
|
|
58
|
+
const tree = render(
|
|
59
|
+
<UseSmileIDThemeProvider value={defaultDarkTheme}>
|
|
60
|
+
<UseSmileIDPermissionScreen />
|
|
61
|
+
</UseSmileIDThemeProvider>,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(rootStyle(tree).backgroundColor).toBe(
|
|
65
|
+
defaultDarkTheme.colors.background,
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -83,6 +83,7 @@ const FEEDBACK_COPY: Partial<
|
|
|
83
83
|
si_capture_ready: (loc) => loc.siCaptureReady,
|
|
84
84
|
si_capture_hold_steady: (loc) => loc.siCaptureHoldSteady,
|
|
85
85
|
si_capture_timeout: (loc) => loc.siCaptureTimeout,
|
|
86
|
+
si_capture_camera_unavailable: (loc) => loc.siCaptureCameraUnavailable,
|
|
86
87
|
si_capturing: (loc) => loc.siCapturing,
|
|
87
88
|
si_selfie_capture_complete: (loc) => loc.siSelfieCaptureComplete,
|
|
88
89
|
si_selfie_capture_timeout: (loc) => loc.siSelfieCaptureTimeout,
|