@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,112 @@
|
|
|
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 { Text, View } from "react-native";
|
|
5
|
+
import { UseSmileIDContentCard } from "../use_smile_id_content_card";
|
|
6
|
+
import { UseSmileIDThemeProvider } from "../../theme/theme_context";
|
|
7
|
+
import { defaultDarkTheme, defaultLightTheme } from "../../theme/theme";
|
|
8
|
+
|
|
9
|
+
function render(element: React.ReactElement) {
|
|
10
|
+
let tree!: ReturnType<typeof createRenderer>;
|
|
11
|
+
act(() => {
|
|
12
|
+
tree = createRenderer(element);
|
|
13
|
+
});
|
|
14
|
+
return tree;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Flatten the style array the card composes so a single key can be read. */
|
|
18
|
+
function surfaceStyle(
|
|
19
|
+
tree: ReturnType<typeof createRenderer>,
|
|
20
|
+
): Record<string, unknown> {
|
|
21
|
+
const surface = tree.root.findAllByType(View)[0];
|
|
22
|
+
if (!surface) throw new Error("card surface did not render");
|
|
23
|
+
const style = surface.props.style as unknown;
|
|
24
|
+
const parts = Array.isArray(style) ? style : [style];
|
|
25
|
+
return Object.assign(
|
|
26
|
+
{},
|
|
27
|
+
...parts.filter((part): part is Record<string, unknown> => Boolean(part)),
|
|
28
|
+
) as Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("UseSmileIDContentCard", () => {
|
|
32
|
+
it("renders its children inside the card surface", () => {
|
|
33
|
+
const tree = render(
|
|
34
|
+
<UseSmileIDContentCard theme={defaultLightTheme}>
|
|
35
|
+
<Text testID="si_child">Body</Text>
|
|
36
|
+
</UseSmileIDContentCard>,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(tree.root.findAllByProps({ testID: "si_child" })).not.toHaveLength(
|
|
40
|
+
0,
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("uses the card surface colour and radius by default", () => {
|
|
45
|
+
const tree = render(
|
|
46
|
+
<UseSmileIDContentCard theme={defaultLightTheme}>
|
|
47
|
+
<Text>Body</Text>
|
|
48
|
+
</UseSmileIDContentCard>,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const style = surfaceStyle(tree);
|
|
52
|
+
expect(style.backgroundColor).toBe(defaultLightTheme.colors.cardBackground);
|
|
53
|
+
expect(style.borderRadius).toBe(defaultLightTheme.shapes.cardRadius);
|
|
54
|
+
expect(style.padding).toBe(defaultLightTheme.spacing.medium);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("switches to the instruction tint for the instruction variant", () => {
|
|
58
|
+
const tree = render(
|
|
59
|
+
<UseSmileIDContentCard variant="instruction" theme={defaultLightTheme}>
|
|
60
|
+
<Text>Body</Text>
|
|
61
|
+
</UseSmileIDContentCard>,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(surfaceStyle(tree).backgroundColor).toBe(
|
|
65
|
+
defaultLightTheme.colors.instructionCardBackground,
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("falls back to the theme from context when no theme prop is given", () => {
|
|
70
|
+
const tree = render(
|
|
71
|
+
<UseSmileIDThemeProvider value={defaultDarkTheme}>
|
|
72
|
+
<UseSmileIDContentCard>
|
|
73
|
+
<Text>Body</Text>
|
|
74
|
+
</UseSmileIDContentCard>
|
|
75
|
+
</UseSmileIDThemeProvider>,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expect(surfaceStyle(tree).backgroundColor).toBe(
|
|
79
|
+
defaultDarkTheme.colors.cardBackground,
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("lets a caller-supplied style override the computed card style", () => {
|
|
84
|
+
const tree = render(
|
|
85
|
+
<UseSmileIDContentCard
|
|
86
|
+
theme={defaultLightTheme}
|
|
87
|
+
style={{ backgroundColor: "#123456", borderRadius: 99 }}
|
|
88
|
+
>
|
|
89
|
+
<Text>Body</Text>
|
|
90
|
+
</UseSmileIDContentCard>,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const style = surfaceStyle(tree);
|
|
94
|
+
expect(style.backgroundColor).toBe("#123456");
|
|
95
|
+
expect(style.borderRadius).toBe(99);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("carries exactly one platform shadow treatment, never both", () => {
|
|
99
|
+
const tree = render(
|
|
100
|
+
<UseSmileIDContentCard theme={defaultLightTheme}>
|
|
101
|
+
<Text>Body</Text>
|
|
102
|
+
</UseSmileIDContentCard>,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const style = surfaceStyle(tree);
|
|
106
|
+
// Setting `elevation` and the iOS shadow* keys together double-draws the
|
|
107
|
+
// shadow on Android, so exactly one family must be present.
|
|
108
|
+
const hasAndroidShadow = "elevation" in style;
|
|
109
|
+
const hasIosShadow = "shadowOpacity" in style;
|
|
110
|
+
expect(hasAndroidShadow !== hasIosShadow).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {
|
|
4
|
+
create as createRenderer,
|
|
5
|
+
act,
|
|
6
|
+
type ReactTestInstance,
|
|
7
|
+
} from "react-test-renderer";
|
|
8
|
+
|
|
9
|
+
// The shared setup's `react-native-svg` stub omits `Image`, which this
|
|
10
|
+
// component needs. Re-stub locally (same host-forwarding shape) so the SVG
|
|
11
|
+
// tree is inspectable without touching the workspace-wide setup.
|
|
12
|
+
vi.mock("react-native-svg", async () => {
|
|
13
|
+
const React = await import("react");
|
|
14
|
+
function SvgHost(props: Record<string, unknown>) {
|
|
15
|
+
const { children, ...rest } = props as {
|
|
16
|
+
children?: React.ReactNode;
|
|
17
|
+
} & Record<string, unknown>;
|
|
18
|
+
return React.createElement("Svg", rest, children);
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
__esModule: true,
|
|
22
|
+
default: SvgHost,
|
|
23
|
+
Svg: SvgHost,
|
|
24
|
+
Defs: SvgHost,
|
|
25
|
+
ClipPath: SvgHost,
|
|
26
|
+
Ellipse: SvgHost,
|
|
27
|
+
Image: SvgHost,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
import { UseSmileIDFaceOvalImage } from "../use_smile_id_face_oval_image";
|
|
32
|
+
import { computeFaceOvalGeometry } from "../face_oval_defaults";
|
|
33
|
+
|
|
34
|
+
function render(element: React.ReactElement) {
|
|
35
|
+
let tree!: ReturnType<typeof createRenderer>;
|
|
36
|
+
act(() => {
|
|
37
|
+
tree = createRenderer(element);
|
|
38
|
+
});
|
|
39
|
+
return tree;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The host (string-typed) instance carrying `testID`, not the composite. */
|
|
43
|
+
function host(
|
|
44
|
+
tree: ReturnType<typeof createRenderer>,
|
|
45
|
+
testID: string,
|
|
46
|
+
): ReactTestInstance {
|
|
47
|
+
const match = tree.root
|
|
48
|
+
.findAllByProps({ testID })
|
|
49
|
+
.find((instance) => typeof instance.type === "string");
|
|
50
|
+
if (!match) throw new Error(`no host element with testID ${testID}`);
|
|
51
|
+
return match;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function flatStyle(instance: ReactTestInstance): Record<string, unknown> {
|
|
55
|
+
const style = instance.props.style as unknown;
|
|
56
|
+
const parts = Array.isArray(style) ? style : [style];
|
|
57
|
+
return Object.assign(
|
|
58
|
+
{},
|
|
59
|
+
...parts.filter((part): part is Record<string, unknown> => Boolean(part)),
|
|
60
|
+
) as Record<string, unknown>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const URI = "file:///selfie.jpg";
|
|
64
|
+
|
|
65
|
+
describe("UseSmileIDFaceOvalImage", () => {
|
|
66
|
+
it("renders nothing until the canvas has measured", () => {
|
|
67
|
+
const tree = render(
|
|
68
|
+
<UseSmileIDFaceOvalImage
|
|
69
|
+
uri={URI}
|
|
70
|
+
containerWidth={0}
|
|
71
|
+
containerHeight={0}
|
|
72
|
+
/>,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(tree.toJSON()).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("renders nothing when only one dimension has measured", () => {
|
|
79
|
+
const tree = render(
|
|
80
|
+
<UseSmileIDFaceOvalImage
|
|
81
|
+
uri={URI}
|
|
82
|
+
containerWidth={320}
|
|
83
|
+
containerHeight={0}
|
|
84
|
+
/>,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(tree.toJSON()).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("renders nothing when there is no captured file yet", () => {
|
|
91
|
+
const tree = render(
|
|
92
|
+
<UseSmileIDFaceOvalImage
|
|
93
|
+
uri=""
|
|
94
|
+
containerWidth={320}
|
|
95
|
+
containerHeight={480}
|
|
96
|
+
/>,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
expect(tree.toJSON()).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("positions the oval at the canonical face-oval rect", () => {
|
|
103
|
+
const geometry = computeFaceOvalGeometry(320, 480);
|
|
104
|
+
const tree = render(
|
|
105
|
+
<UseSmileIDFaceOvalImage
|
|
106
|
+
uri={URI}
|
|
107
|
+
containerWidth={320}
|
|
108
|
+
containerHeight={480}
|
|
109
|
+
testID="si_face_oval_image"
|
|
110
|
+
/>,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const style = flatStyle(host(tree, "si_face_oval_image"));
|
|
114
|
+
expect(style.width).toBeCloseTo(geometry.ovalWidth, 5);
|
|
115
|
+
expect(style.height).toBeCloseTo(geometry.ovalHeight, 5);
|
|
116
|
+
expect(style.left).toBeCloseTo(geometry.ovalLeft, 5);
|
|
117
|
+
expect(style.top).toBeCloseTo(geometry.ovalTop, 5);
|
|
118
|
+
expect(style.position).toBe("absolute");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("draws the captured file clipped to the ellipse", () => {
|
|
122
|
+
const geometry = computeFaceOvalGeometry(320, 480);
|
|
123
|
+
const tree = render(
|
|
124
|
+
<UseSmileIDFaceOvalImage
|
|
125
|
+
uri={URI}
|
|
126
|
+
containerWidth={320}
|
|
127
|
+
containerHeight={480}
|
|
128
|
+
/>,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const json = JSON.stringify(tree.toJSON());
|
|
132
|
+
expect(json).toContain(URI);
|
|
133
|
+
// The clip id ties the <Image> to the <ClipPath><Ellipse>; a mismatch
|
|
134
|
+
// renders an unclipped rectangle over the preview.
|
|
135
|
+
expect(json).toContain("url(#si_face_oval_image_clip)");
|
|
136
|
+
expect(json).toContain(`0 0 ${geometry.ovalWidth} ${geometry.ovalHeight}`);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("sizes the ellipse to the full oval so the crop is not inset", () => {
|
|
140
|
+
const geometry = computeFaceOvalGeometry(320, 480);
|
|
141
|
+
const tree = render(
|
|
142
|
+
<UseSmileIDFaceOvalImage
|
|
143
|
+
uri={URI}
|
|
144
|
+
containerWidth={320}
|
|
145
|
+
containerHeight={480}
|
|
146
|
+
/>,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const ellipse = tree.root
|
|
150
|
+
.findAllByType("Svg" as unknown as React.ComponentType)
|
|
151
|
+
.find((instance) => instance.props.rx !== undefined);
|
|
152
|
+
expect(ellipse?.props.rx).toBeCloseTo(geometry.ovalWidth / 2, 5);
|
|
153
|
+
expect(ellipse?.props.ry).toBeCloseTo(geometry.ovalHeight / 2, 5);
|
|
154
|
+
expect(ellipse?.props.cx).toBeCloseTo(geometry.ovalWidth / 2, 5);
|
|
155
|
+
expect(ellipse?.props.cy).toBeCloseTo(geometry.ovalHeight / 2, 5);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("rescales the oval when the canvas resizes", () => {
|
|
159
|
+
const small = flatStyle(
|
|
160
|
+
host(
|
|
161
|
+
render(
|
|
162
|
+
<UseSmileIDFaceOvalImage
|
|
163
|
+
uri={URI}
|
|
164
|
+
containerWidth={320}
|
|
165
|
+
containerHeight={480}
|
|
166
|
+
testID="si_face_oval_image"
|
|
167
|
+
/>,
|
|
168
|
+
),
|
|
169
|
+
"si_face_oval_image",
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
const large = flatStyle(
|
|
173
|
+
host(
|
|
174
|
+
render(
|
|
175
|
+
<UseSmileIDFaceOvalImage
|
|
176
|
+
uri={URI}
|
|
177
|
+
containerWidth={640}
|
|
178
|
+
containerHeight={960}
|
|
179
|
+
testID="si_face_oval_image"
|
|
180
|
+
/>,
|
|
181
|
+
),
|
|
182
|
+
"si_face_oval_image",
|
|
183
|
+
),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
expect(large.width as number).toBeCloseTo((small.width as number) * 2, 5);
|
|
187
|
+
expect(large.height as number).toBeCloseTo((small.height as number) * 2, 5);
|
|
188
|
+
expect(large.top as number).toBeCloseTo((small.top as number) * 2, 5);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach, vi } from "vitest";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {
|
|
4
|
+
create as createRenderer,
|
|
5
|
+
act,
|
|
6
|
+
type ReactTestInstance,
|
|
7
|
+
} from "react-test-renderer";
|
|
8
|
+
import { Linking, Text } from "react-native";
|
|
9
|
+
import { UseSmileIDCrashReporting } from "@smileid/usesmileid_bridge";
|
|
10
|
+
|
|
11
|
+
import { UseSmileIDLearnMoreContent } from "../use_smile_id_learn_more_content";
|
|
12
|
+
import { enStrings } from "../../../l10n/strings_en";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The consent screen's expandable privacy section. Two things must
|
|
16
|
+
* hold: the `{smile_id}` / `{partner}` / `{retention}` tokens are all
|
|
17
|
+
* substituted (a leaked token ships raw template text to a user), and
|
|
18
|
+
* a device with no browser must surface through crash reporting rather
|
|
19
|
+
* than presenting as a dead tap.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const SMILE_ID_PRIVACY_URL = "https://usesmileid.com/privacy-policy/";
|
|
23
|
+
|
|
24
|
+
function render(element: React.ReactElement) {
|
|
25
|
+
let tree!: ReturnType<typeof createRenderer>;
|
|
26
|
+
act(() => {
|
|
27
|
+
tree = createRenderer(element);
|
|
28
|
+
});
|
|
29
|
+
return tree;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function collectText(node: ReactTestInstance): string {
|
|
33
|
+
return node.children
|
|
34
|
+
.map((child) =>
|
|
35
|
+
typeof child === "string"
|
|
36
|
+
? child
|
|
37
|
+
: collectText(child as ReactTestInstance),
|
|
38
|
+
)
|
|
39
|
+
.join("");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The pressable inline links, in render order: DSAR, Smile ID, partner. */
|
|
43
|
+
function links(root: ReactTestInstance): ReactTestInstance[] {
|
|
44
|
+
return root.findAll(
|
|
45
|
+
(node) => node.type === Text && typeof node.props.onPress === "function",
|
|
46
|
+
{ deep: true },
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("UseSmileIDLearnMoreContent", () => {
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
vi.restoreAllMocks();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("substitutes every token in the description template", () => {
|
|
56
|
+
const { root } = render(
|
|
57
|
+
<UseSmileIDLearnMoreContent
|
|
58
|
+
partnerName="Acme Bank"
|
|
59
|
+
testID="si_learn_more"
|
|
60
|
+
/>,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const text = collectText(root.findByProps({ testID: "si_learn_more" }));
|
|
64
|
+
expect(text).toContain("Acme Bank");
|
|
65
|
+
expect(text).toContain(
|
|
66
|
+
enStrings.si_consent_learn_more_description_smile_id,
|
|
67
|
+
);
|
|
68
|
+
expect(text).toContain(
|
|
69
|
+
enStrings.si_consent_learn_more_description_retention,
|
|
70
|
+
);
|
|
71
|
+
expect(text).not.toContain("{smile_id}");
|
|
72
|
+
expect(text).not.toContain("{partner}");
|
|
73
|
+
expect(text).not.toContain("{retention}");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("opens the Smile ID privacy policy from the DSAR and privacy links", async () => {
|
|
77
|
+
const openURL = vi.spyOn(Linking, "openURL").mockResolvedValue(undefined);
|
|
78
|
+
const { root } = render(
|
|
79
|
+
<UseSmileIDLearnMoreContent partnerName="Acme Bank" />,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
await act(async () => {
|
|
83
|
+
links(root)[0]?.props.onPress();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(openURL).toHaveBeenCalledWith(SMILE_ID_PRIVACY_URL);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("opens the partner's own policy when one is configured", async () => {
|
|
90
|
+
const openURL = vi.spyOn(Linking, "openURL").mockResolvedValue(undefined);
|
|
91
|
+
const { root } = render(
|
|
92
|
+
<UseSmileIDLearnMoreContent
|
|
93
|
+
partnerName="Acme Bank"
|
|
94
|
+
partnerPrivacyPolicyUrl="https://acme.test/privacy"
|
|
95
|
+
/>,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const partnerLink = links(root).at(-1);
|
|
99
|
+
await act(async () => {
|
|
100
|
+
partnerLink?.props.onPress();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(openURL).toHaveBeenCalledWith("https://acme.test/privacy");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("is a silent no-op when the partner supplied no policy URL", async () => {
|
|
107
|
+
const openURL = vi.spyOn(Linking, "openURL").mockResolvedValue(undefined);
|
|
108
|
+
const { root } = render(
|
|
109
|
+
<UseSmileIDLearnMoreContent partnerName="Acme Bank" />,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const partnerLink = links(root).at(-1);
|
|
113
|
+
await act(async () => {
|
|
114
|
+
partnerLink?.props.onPress();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(openURL).not.toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("reports a failed Smile ID link open to crash reporting", async () => {
|
|
121
|
+
const failure = new Error("no browser registered");
|
|
122
|
+
vi.spyOn(Linking, "openURL").mockRejectedValue(failure);
|
|
123
|
+
const captureException = vi
|
|
124
|
+
.spyOn(UseSmileIDCrashReporting, "captureException")
|
|
125
|
+
.mockImplementation(() => {});
|
|
126
|
+
const { root } = render(
|
|
127
|
+
<UseSmileIDLearnMoreContent partnerName="Acme Bank" />,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
await act(async () => {
|
|
131
|
+
links(root)[0]?.props.onPress();
|
|
132
|
+
await Promise.resolve();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(captureException).toHaveBeenCalledWith(failure);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("reports a failed partner link open to crash reporting", async () => {
|
|
139
|
+
const failure = new Error("malformed partner url");
|
|
140
|
+
vi.spyOn(Linking, "openURL").mockRejectedValue(failure);
|
|
141
|
+
const captureException = vi
|
|
142
|
+
.spyOn(UseSmileIDCrashReporting, "captureException")
|
|
143
|
+
.mockImplementation(() => {});
|
|
144
|
+
const { root } = render(
|
|
145
|
+
<UseSmileIDLearnMoreContent
|
|
146
|
+
partnerName="Acme Bank"
|
|
147
|
+
partnerPrivacyPolicyUrl="not-a-url"
|
|
148
|
+
/>,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const partnerLink = links(root).at(-1);
|
|
152
|
+
await act(async () => {
|
|
153
|
+
partnerLink?.props.onPress();
|
|
154
|
+
await Promise.resolve();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(captureException).toHaveBeenCalledWith(failure);
|
|
158
|
+
});
|
|
159
|
+
});
|
package/src/presentation/components/__tests__/use_smile_id_processing_center_indicator.test.tsx
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
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 { UseSmileIDProcessingCenterIndicator } from "../use_smile_id_processing_center_indicator";
|
|
5
|
+
import { UseSmileIDLoadingIndicator } from "../use_smile_id_loading_indicator";
|
|
6
|
+
import { UseSmileIDStatusIndicator } from "../use_smile_id_status_indicator";
|
|
7
|
+
import { computeFaceOvalGeometry } from "../face_oval_defaults";
|
|
8
|
+
import { defaultLightTheme } from "../../theme/theme";
|
|
9
|
+
import type { ProcessingState } from "../../state/processing_state";
|
|
10
|
+
|
|
11
|
+
function render(state: ProcessingState, width = 320, height = 480) {
|
|
12
|
+
let tree!: ReturnType<typeof createRenderer>;
|
|
13
|
+
act(() => {
|
|
14
|
+
tree = createRenderer(
|
|
15
|
+
<UseSmileIDProcessingCenterIndicator
|
|
16
|
+
state={state}
|
|
17
|
+
containerWidth={width}
|
|
18
|
+
containerHeight={height}
|
|
19
|
+
theme={defaultLightTheme}
|
|
20
|
+
testID="si_center_indicator"
|
|
21
|
+
/>,
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
return tree;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("UseSmileIDProcessingCenterIndicator", () => {
|
|
28
|
+
it("renders nothing until the canvas has measured", () => {
|
|
29
|
+
expect(render({ kind: "loading" }, 0, 0).toJSON()).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("renders nothing when only the height has measured", () => {
|
|
33
|
+
expect(render({ kind: "loading" }, 0, 480).toJSON()).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("anchors to the same rect the captured image occupies", () => {
|
|
37
|
+
const geometry = computeFaceOvalGeometry(320, 480);
|
|
38
|
+
const tree = render({ kind: "loading" });
|
|
39
|
+
|
|
40
|
+
const container = tree.root
|
|
41
|
+
.findAllByProps({ testID: "si_center_indicator" })
|
|
42
|
+
.find((instance) => typeof instance.type === "string");
|
|
43
|
+
const style = Object.assign(
|
|
44
|
+
{},
|
|
45
|
+
...(container!.props.style as Record<string, unknown>[]),
|
|
46
|
+
) as Record<string, unknown>;
|
|
47
|
+
|
|
48
|
+
expect(style.width).toBeCloseTo(geometry.ovalWidth, 5);
|
|
49
|
+
expect(style.height).toBeCloseTo(geometry.ovalHeight, 5);
|
|
50
|
+
expect(style.left).toBeCloseTo(geometry.ovalLeft, 5);
|
|
51
|
+
expect(style.top).toBeCloseTo(geometry.ovalTop, 5);
|
|
52
|
+
expect(style.position).toBe("absolute");
|
|
53
|
+
expect(style.alignItems).toBe("center");
|
|
54
|
+
expect(style.justifyContent).toBe("center");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("shows the spinner while the job is in flight", () => {
|
|
58
|
+
const tree = render({ kind: "loading" });
|
|
59
|
+
|
|
60
|
+
expect(tree.root.findAllByType(UseSmileIDLoadingIndicator)).toHaveLength(1);
|
|
61
|
+
expect(tree.root.findAllByType(UseSmileIDStatusIndicator)).toHaveLength(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("shows the success status when the server accepts", () => {
|
|
65
|
+
const tree = render({ kind: "success", message: "approved" });
|
|
66
|
+
|
|
67
|
+
const status = tree.root.findAllByType(UseSmileIDStatusIndicator);
|
|
68
|
+
expect(status).toHaveLength(1);
|
|
69
|
+
expect(status[0]!.props.state).toBe("success");
|
|
70
|
+
expect(tree.root.findAllByType(UseSmileIDLoadingIndicator)).toHaveLength(0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("shows the failure status when the job is rejected", () => {
|
|
74
|
+
const tree = render({ kind: "failed", errorMessage: "declined" });
|
|
75
|
+
|
|
76
|
+
const status = tree.root.findAllByType(UseSmileIDStatusIndicator);
|
|
77
|
+
expect(status).toHaveLength(1);
|
|
78
|
+
expect(status[0]!.props.state).toBe("failure");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("passes the resolved theme down to the child indicator", () => {
|
|
82
|
+
const tree = render({ kind: "loading" });
|
|
83
|
+
|
|
84
|
+
expect(tree.root.findByType(UseSmileIDLoadingIndicator).props.theme).toBe(
|
|
85
|
+
defaultLightTheme,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -65,7 +65,10 @@ export function UseSmileIDButton(props: UseSmileIDButtonProps) {
|
|
|
65
65
|
]}
|
|
66
66
|
>
|
|
67
67
|
<Text
|
|
68
|
-
|
|
68
|
+
// Shrink rather than truncate: a longer localized label must stay fully readable.
|
|
69
|
+
numberOfLines={2}
|
|
70
|
+
adjustsFontSizeToFit
|
|
71
|
+
minimumFontScale={0.75}
|
|
69
72
|
style={{
|
|
70
73
|
...theme.typography.button,
|
|
71
74
|
color: disabled
|
|
@@ -84,7 +87,9 @@ export function UseSmileIDButton(props: UseSmileIDButtonProps) {
|
|
|
84
87
|
|
|
85
88
|
const styles = StyleSheet.create({
|
|
86
89
|
base: {
|
|
87
|
-
height
|
|
90
|
+
// `minHeight`, not `height`: the pill grows only for a label that has to wrap.
|
|
91
|
+
minHeight: 48,
|
|
92
|
+
paddingVertical: 8,
|
|
88
93
|
paddingHorizontal: 24,
|
|
89
94
|
alignItems: "center",
|
|
90
95
|
justifyContent: "center",
|