@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.
Files changed (160) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +1 -1
  3. package/dist/camera/camera_defaults.d.ts +2 -0
  4. package/dist/camera/camera_defaults.d.ts.map +1 -1
  5. package/dist/camera/camera_defaults.js +2 -0
  6. package/dist/camera/camera_defaults.js.map +1 -1
  7. package/dist/core/exception/flow_exceptions.d.ts +4 -2
  8. package/dist/core/exception/flow_exceptions.d.ts.map +1 -1
  9. package/dist/core/exception/flow_exceptions.js +3 -2
  10. package/dist/core/exception/flow_exceptions.js.map +1 -1
  11. package/dist/core/exception/missing_job_type_exception.d.ts +1 -3
  12. package/dist/core/exception/missing_job_type_exception.d.ts.map +1 -1
  13. package/dist/core/exception/missing_job_type_exception.js +2 -4
  14. package/dist/core/exception/missing_job_type_exception.js.map +1 -1
  15. package/dist/data/api/interceptors/smile_auth_interceptor.js +1 -1
  16. package/dist/data/api/interceptors/smile_auth_interceptor.js.map +1 -1
  17. package/dist/data/api/smile_id_api.d.ts +4 -0
  18. package/dist/data/api/smile_id_api.d.ts.map +1 -1
  19. package/dist/data/api/smile_id_api.js +30 -27
  20. package/dist/data/api/smile_id_api.js.map +1 -1
  21. package/dist/data/model/job_request.d.ts +28 -12
  22. package/dist/data/model/job_request.d.ts.map +1 -1
  23. package/dist/data/model/token_payload.d.ts +78 -0
  24. package/dist/data/model/token_payload.d.ts.map +1 -0
  25. package/dist/data/model/token_payload.js +132 -0
  26. package/dist/data/model/token_payload.js.map +1 -0
  27. package/dist/data/model/user_details_payload.d.ts +39 -0
  28. package/dist/data/model/user_details_payload.d.ts.map +1 -0
  29. package/dist/data/model/user_details_payload.js +53 -0
  30. package/dist/data/model/user_details_payload.js.map +1 -0
  31. package/dist/data/util/use_smile_id_token_decoder.d.ts +15 -3
  32. package/dist/data/util/use_smile_id_token_decoder.d.ts.map +1 -1
  33. package/dist/data/util/use_smile_id_token_decoder.js +20 -8
  34. package/dist/data/util/use_smile_id_token_decoder.js.map +1 -1
  35. package/dist/l10n/strings_en.d.ts +4 -1
  36. package/dist/l10n/strings_en.d.ts.map +1 -1
  37. package/dist/l10n/strings_en.js +4 -1
  38. package/dist/l10n/strings_en.js.map +1 -1
  39. package/dist/l10n/use_smile_id_localizations.d.ts +10 -0
  40. package/dist/l10n/use_smile_id_localizations.d.ts.map +1 -1
  41. package/dist/l10n/use_smile_id_localizations.js +59 -3
  42. package/dist/l10n/use_smile_id_localizations.js.map +1 -1
  43. package/dist/metadata/metadata_configuration_mappers.js +1 -1
  44. package/dist/metadata/metadata_configuration_mappers.js.map +1 -1
  45. package/dist/metadata/use_smile_id_metadata_factory.js +1 -1
  46. package/dist/metadata/use_smile_id_metadata_factory.js.map +1 -1
  47. package/dist/presentation/components/use_smile_id_button.d.ts.map +1 -1
  48. package/dist/presentation/components/use_smile_id_button.js +8 -2
  49. package/dist/presentation/components/use_smile_id_button.js.map +1 -1
  50. package/dist/presentation/flow/dsl/builder/screens_builder.d.ts +3 -6
  51. package/dist/presentation/flow/dsl/builder/screens_builder.d.ts.map +1 -1
  52. package/dist/presentation/flow/dsl/builder/screens_builder.js +4 -6
  53. package/dist/presentation/flow/dsl/builder/screens_builder.js.map +1 -1
  54. package/dist/presentation/flow/dsl/use_smile_id_flow_builder.d.ts.map +1 -1
  55. package/dist/presentation/flow/dsl/use_smile_id_flow_builder.js +23 -1
  56. package/dist/presentation/flow/dsl/use_smile_id_flow_builder.js.map +1 -1
  57. package/dist/presentation/flow/flow_navigation_manager.d.ts +17 -2
  58. package/dist/presentation/flow/flow_navigation_manager.d.ts.map +1 -1
  59. package/dist/presentation/flow/flow_navigation_manager.js +77 -45
  60. package/dist/presentation/flow/flow_navigation_manager.js.map +1 -1
  61. package/dist/presentation/flow/screen/orchestrated_processing_screen.d.ts.map +1 -1
  62. package/dist/presentation/flow/screen/orchestrated_processing_screen.js +25 -5
  63. package/dist/presentation/flow/screen/orchestrated_processing_screen.js.map +1 -1
  64. package/dist/presentation/flow/use_smile_id_flow.d.ts.map +1 -1
  65. package/dist/presentation/flow/use_smile_id_flow.js +35 -8
  66. package/dist/presentation/flow/use_smile_id_flow.js.map +1 -1
  67. package/dist/presentation/flow/validation/flow_validator.d.ts +16 -2
  68. package/dist/presentation/flow/validation/flow_validator.d.ts.map +1 -1
  69. package/dist/presentation/flow/validation/flow_validator.js +19 -27
  70. package/dist/presentation/flow/validation/flow_validator.js.map +1 -1
  71. package/dist/presentation/flow/validation/job_type_validator.d.ts +7 -0
  72. package/dist/presentation/flow/validation/job_type_validator.d.ts.map +1 -1
  73. package/dist/presentation/flow/validation/job_type_validator.js +52 -27
  74. package/dist/presentation/flow/validation/job_type_validator.js.map +1 -1
  75. package/dist/presentation/flow/validation/user_details_validator.d.ts +9 -0
  76. package/dist/presentation/flow/validation/user_details_validator.d.ts.map +1 -0
  77. package/dist/presentation/flow/validation/user_details_validator.js +40 -0
  78. package/dist/presentation/flow/validation/user_details_validator.js.map +1 -0
  79. package/dist/presentation/screen/helpers/capture_screen_helpers.d.ts.map +1 -1
  80. package/dist/presentation/screen/helpers/capture_screen_helpers.js +1 -0
  81. package/dist/presentation/screen/helpers/capture_screen_helpers.js.map +1 -1
  82. package/dist/presentation/screen/use_smile_id_capture_screen.d.ts.map +1 -1
  83. package/dist/presentation/screen/use_smile_id_capture_screen.js +64 -18
  84. package/dist/presentation/screen/use_smile_id_capture_screen.js.map +1 -1
  85. package/package.json +3 -3
  86. package/src/__tests__/react_native_mock.tsx +22 -0
  87. package/src/__tests__/snapshots/__snapshots__/screens_structural.test.tsx.snap +14 -8
  88. package/src/camera/__tests__/camera_permissions.test.ts +72 -0
  89. package/src/camera/__tests__/camera_system_focus.test.ts +173 -0
  90. package/src/camera/__tests__/use_smile_id_camera_preview.test.tsx +461 -0
  91. package/src/camera/__tests__/use_smile_id_frame_stream_hook.test.tsx +229 -0
  92. package/src/camera/camera_defaults.ts +3 -0
  93. package/src/core/exception/__tests__/builder_validation_exceptions.test.ts +14 -0
  94. package/src/core/exception/flow_exceptions.test.ts +10 -0
  95. package/src/core/exception/flow_exceptions.ts +3 -2
  96. package/src/core/exception/missing_job_type_exception.ts +2 -4
  97. package/src/data/api/__tests__/smile_id_api.test.ts +275 -1
  98. package/src/data/api/interceptors/__tests__/credential_header_redaction.test.ts +144 -0
  99. package/src/data/api/interceptors/smile_auth_interceptor.ts +1 -1
  100. package/src/data/api/smile_id_api.ts +36 -49
  101. package/src/data/dsl/__tests__/use_smile_id_network_builder.test.ts +157 -0
  102. package/src/data/factory/__tests__/signed_job_submission_headers.test.ts +1 -1
  103. package/src/data/model/__tests__/token_payload.test.ts +224 -0
  104. package/src/data/model/__tests__/user_details_payload.test.ts +100 -0
  105. package/src/data/model/job_request.ts +27 -12
  106. package/src/data/model/token_payload.ts +182 -0
  107. package/src/data/model/user_details_payload.ts +84 -0
  108. package/src/data/repository/__tests__/network_retries.test.ts +1 -1
  109. package/src/data/repository/__tests__/repository_dispatch.test.ts +366 -0
  110. package/src/data/repository/__tests__/use_smile_id_repository.test.ts +1 -1
  111. package/src/data/security/__tests__/signing_crypto.test.ts +203 -0
  112. package/src/data/util/__tests__/use_smile_id_token_decoder.test.ts +179 -0
  113. package/src/data/util/use_smile_id_token_decoder.ts +29 -8
  114. package/src/l10n/__tests__/use_smile_id_localizations.test.ts +122 -5
  115. package/src/l10n/__tests__/use_smile_id_localizations_provider.test.tsx +112 -0
  116. package/src/l10n/__tests__/use_smile_id_localizations_surface.test.ts +231 -0
  117. package/src/l10n/strings_en.ts +5 -1
  118. package/src/l10n/use_smile_id_localizations.ts +80 -3
  119. package/src/metadata/__tests__/device_orientation_sampling.test.ts +165 -0
  120. package/src/metadata/__tests__/geolocation.test.ts +150 -0
  121. package/src/metadata/__tests__/metadata_configuration_objects.test.ts +1 -1
  122. package/src/metadata/__tests__/metadata_device_context.test.ts +3 -3
  123. package/src/metadata/__tests__/metadata_golden_fixture.json +1 -1
  124. package/src/metadata/__tests__/metadata_payload_golden.test.ts +4 -4
  125. package/src/metadata/__tests__/optional_native_modules.test.ts +234 -0
  126. package/src/metadata/__tests__/use_smile_id_metadata.test.ts +1 -1
  127. package/src/metadata/__tests__/use_smile_id_metadata_factory.test.ts +615 -0
  128. package/src/metadata/metadata_configuration_mappers.ts +1 -1
  129. package/src/metadata/use_smile_id_metadata_factory.ts +1 -1
  130. package/src/presentation/components/__tests__/use_smile_id_content_card.test.tsx +112 -0
  131. package/src/presentation/components/__tests__/use_smile_id_face_oval_image.test.tsx +190 -0
  132. package/src/presentation/components/__tests__/use_smile_id_learn_more_content.test.tsx +159 -0
  133. package/src/presentation/components/__tests__/use_smile_id_processing_center_indicator.test.tsx +88 -0
  134. package/src/presentation/components/use_smile_id_button.tsx +7 -2
  135. package/src/presentation/flow/__tests__/flow_navigation_manager.test.ts +323 -1
  136. package/src/presentation/flow/__tests__/use_smile_id_flow.test.tsx +651 -0
  137. package/src/presentation/flow/dsl/__tests__/use_smile_id_builder_component.test.tsx +120 -0
  138. package/src/presentation/flow/dsl/builder/screens_builder.ts +4 -6
  139. package/src/presentation/flow/dsl/use_smile_id_flow_builder.ts +27 -1
  140. package/src/presentation/flow/flow_navigation_manager.ts +102 -92
  141. package/src/presentation/flow/graph/__tests__/screen_renderer.test.tsx +102 -0
  142. package/src/presentation/flow/screen/__tests__/orchestrated_capture_screen.test.tsx +836 -0
  143. package/src/presentation/flow/screen/__tests__/orchestrated_consent_screen.test.tsx +165 -0
  144. package/src/presentation/flow/screen/__tests__/orchestrated_instructions_screen.test.tsx +163 -0
  145. package/src/presentation/flow/screen/__tests__/orchestrated_preview_screen.test.tsx +224 -0
  146. package/src/presentation/flow/screen/__tests__/orchestrated_processing_screen.test.tsx +338 -0
  147. package/src/presentation/flow/screen/orchestrated_processing_screen.tsx +42 -5
  148. package/src/presentation/flow/use_smile_id_flow.tsx +52 -8
  149. package/src/presentation/flow/validation/__tests__/flow_validator.test.ts +158 -0
  150. package/src/presentation/flow/validation/__tests__/job_type_validator.test.ts +200 -0
  151. package/src/presentation/flow/validation/flow_validator.ts +30 -31
  152. package/src/presentation/flow/validation/job_type_validator.ts +73 -39
  153. package/src/presentation/flow/validation/user_details_validator.ts +53 -0
  154. package/src/presentation/screen/__tests__/feedback_copy_table.test.ts +145 -0
  155. package/src/presentation/screen/__tests__/use_smile_id_capture_screen.test.tsx +348 -0
  156. package/src/presentation/screen/__tests__/use_smile_id_document_back_instructions.test.tsx +3 -1
  157. package/src/presentation/screen/__tests__/use_smile_id_permission_screen.test.tsx +68 -0
  158. package/src/presentation/screen/helpers/capture_screen_helpers.ts +1 -0
  159. package/src/presentation/screen/use_smile_id_capture_screen.tsx +69 -18
  160. package/src/presentation/theme/__tests__/use_use_smile_id_fonts.test.tsx +166 -0
@@ -0,0 +1,615 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+
3
+ // `vi.mock` is hoisted above the imports, so anything its factory closes over
4
+ // must be created with `vi.hoisted` or it is still in the TDZ when the factory
5
+ // runs.
6
+ const {
7
+ loadExpoDevice,
8
+ loadExpoNetwork,
9
+ loadExpoBattery,
10
+ loadExpoCellular,
11
+ loadAsyncStorage,
12
+ resolveGeolocation,
13
+ privileges,
14
+ signals,
15
+ generateHardwareAttestation,
16
+ dimensionsGet,
17
+ } = vi.hoisted(() => ({
18
+ loadExpoDevice: vi.fn(),
19
+ loadExpoNetwork: vi.fn(),
20
+ loadExpoBattery: vi.fn(),
21
+ loadExpoCellular: vi.fn(),
22
+ loadAsyncStorage: vi.fn(),
23
+ resolveGeolocation: vi.fn(),
24
+ privileges: vi.fn(),
25
+ signals: vi.fn(),
26
+ generateHardwareAttestation: vi.fn(),
27
+ dimensionsGet: vi.fn(),
28
+ }));
29
+
30
+ vi.mock("../optional_native_modules", () => ({
31
+ loadExpoDevice,
32
+ loadExpoNetwork,
33
+ loadExpoBattery,
34
+ loadExpoCellular,
35
+ loadAsyncStorage,
36
+ }));
37
+
38
+ vi.mock("../geolocation", () => ({ resolveGeolocation }));
39
+
40
+ // The shared react-native stub has no `Dimensions` — the factory's screen /
41
+ // orientation resolvers are the only consumers, so they are supplied here
42
+ // rather than widening the stub for every other suite.
43
+ vi.mock("react-native", async () => {
44
+ const actual =
45
+ await vi.importActual<typeof import("react-native")>("react-native");
46
+ return { ...actual, Dimensions: { get: dimensionsGet } };
47
+ });
48
+
49
+ vi.mock("@smileid/usesmileid_bridge", async () => {
50
+ const actual = await vi.importActual<
51
+ typeof import("@smileid/usesmileid_bridge")
52
+ >("@smileid/usesmileid_bridge");
53
+ return {
54
+ ...actual,
55
+ PrivilegesProvider: { privileges },
56
+ DeviceIntegrityProvider: { signals },
57
+ Attestation: { generateHardwareAttestation },
58
+ };
59
+ });
60
+
61
+ import { Platform } from "react-native";
62
+ import {
63
+ UseSmileIDMetadataFactory,
64
+ generateSessionId,
65
+ } from "../use_smile_id_metadata_factory";
66
+ import type { UseSmileIDMetadata } from "../use_smile_id_metadata";
67
+
68
+ /**
69
+ * Boundary tests for the metadata factory's optional-peer wiring: the
70
+ * `expo-device` constructor branch, the fire-and-forget warmup fan-out
71
+ * (network / battery / launch count / geolocation / privileges /
72
+ * device integrity), and the resolver fallbacks. Every optional module
73
+ * is mocked at the `optional_native_modules` seam, which is where the
74
+ * production code isolates the peer dependency.
75
+ */
76
+
77
+ type MutablePlatform = { OS: string };
78
+ const platform = Platform as unknown as MutablePlatform;
79
+ const originalPlatformOs = platform.OS;
80
+
81
+ /** Lets the constructor's unawaited warmup promises settle. */
82
+ async function flush(): Promise<void> {
83
+ await new Promise((resolve) => setTimeout(resolve, 0));
84
+ }
85
+
86
+ function entries(metadata: UseSmileIDMetadata): Map<string, string> {
87
+ return new Map(
88
+ metadata.toEntries().map((entry) => [entry.name, entry.value]),
89
+ );
90
+ }
91
+
92
+ function names(metadata: UseSmileIDMetadata): readonly string[] {
93
+ return metadata.toEntries().map((entry) => entry.name);
94
+ }
95
+
96
+ /** A minimal expo-network stand-in — the warmup fan-out is gated on it. */
97
+ function networkModule(type: string | null = "WIFI") {
98
+ return { getNetworkStateAsync: vi.fn(async () => ({ type })) };
99
+ }
100
+
101
+ beforeEach(() => {
102
+ loadExpoDevice.mockReset().mockReturnValue(null);
103
+ loadExpoNetwork.mockReset().mockReturnValue(null);
104
+ loadExpoBattery.mockReset().mockReturnValue(null);
105
+ loadExpoCellular.mockReset().mockReturnValue(null);
106
+ loadAsyncStorage.mockReset().mockReturnValue(null);
107
+ resolveGeolocation.mockReset().mockResolvedValue(null);
108
+ privileges.mockReset().mockResolvedValue("");
109
+ signals.mockReset().mockResolvedValue({});
110
+ generateHardwareAttestation.mockReset().mockResolvedValue({
111
+ securityLevel: -2,
112
+ supportsAppAttest: false,
113
+ });
114
+ dimensionsGet
115
+ .mockReset()
116
+ .mockImplementation((dimension: string) =>
117
+ dimension === "screen"
118
+ ? { width: 390, height: 844, scale: 3 }
119
+ : { width: 390, height: 844 },
120
+ );
121
+ platform.OS = originalPlatformOs;
122
+ });
123
+
124
+ afterEach(() => {
125
+ vi.restoreAllMocks();
126
+ platform.OS = originalPlatformOs;
127
+ });
128
+
129
+ describe("UseSmileIDMetadataFactory — expo-device constructor branch", () => {
130
+ it("joins brand and model, converts memory to MB, and reads the iOS build fields", async () => {
131
+ platform.OS = "ios";
132
+ loadExpoDevice.mockReturnValue({
133
+ brand: " Apple ",
134
+ modelName: " iPhone 15 ",
135
+ supportedCpuArchitectures: [" arm64e "],
136
+ totalMemory: 6 * 1024 * 1024 * 1024,
137
+ modelId: " iPhone16,2 ",
138
+ designName: "ignored-on-ios",
139
+ osBuildFingerprint: " apple/iphone:17 ",
140
+ productName: " iphone15 ",
141
+ });
142
+
143
+ const factory = new UseSmileIDMetadataFactory();
144
+ await flush();
145
+ const values = entries(factory.make());
146
+
147
+ expect(values.get("device_model")).toBe("Apple iPhone 15");
148
+ expect(values.get("system_architecture")).toBe("arm64e");
149
+ expect(values.get("memory_info")).toBe("6144");
150
+ expect(values.get("build_brand")).toBe("Apple");
151
+ expect(values.get("build_device")).toBe("iPhone16,2");
152
+ expect(values.get("build_fingerprint")).toBe("apple/iphone:17");
153
+ expect(values.get("build_product")).toBe("iphone15");
154
+ });
155
+
156
+ it("reads Build.DEVICE (designName) for the build_device field on Android", async () => {
157
+ platform.OS = "android";
158
+ loadExpoDevice.mockReturnValue({
159
+ brand: "google",
160
+ modelName: "Pixel 8",
161
+ designName: "husky",
162
+ modelId: "ignored-on-android",
163
+ supportedCpuArchitectures: ["arm64-v8a"],
164
+ totalMemory: 1024 * 1024,
165
+ });
166
+
167
+ const factory = new UseSmileIDMetadataFactory();
168
+ await flush();
169
+ const values = entries(factory.make());
170
+
171
+ expect(values.get("build_device")).toBe("husky");
172
+ expect(values.get("memory_info")).toBe("1");
173
+ });
174
+
175
+ it("tolerates a device module with every field null", async () => {
176
+ loadExpoDevice.mockReturnValue({
177
+ brand: null,
178
+ modelName: null,
179
+ supportedCpuArchitectures: null,
180
+ totalMemory: null,
181
+ designName: null,
182
+ modelId: null,
183
+ osBuildFingerprint: null,
184
+ productName: null,
185
+ });
186
+
187
+ const factory = new UseSmileIDMetadataFactory();
188
+ await flush();
189
+ const metadata = factory.make();
190
+
191
+ // device_model is always-present; the rest drop out when empty / zero.
192
+ expect(entries(metadata).get("device_model")).toBe("");
193
+ expect(names(metadata)).not.toContain("build_brand");
194
+ expect(names(metadata)).not.toContain("memory_info");
195
+ expect(names(metadata)).not.toContain("system_architecture");
196
+ });
197
+
198
+ it("treats a non-positive totalMemory as unknown", async () => {
199
+ loadExpoDevice.mockReturnValue({
200
+ brand: "google",
201
+ modelName: "Pixel",
202
+ totalMemory: 0,
203
+ });
204
+
205
+ const factory = new UseSmileIDMetadataFactory();
206
+ await flush();
207
+ expect(names(factory.make())).not.toContain("memory_info");
208
+ });
209
+
210
+ it("falls back to empty device fields when expo-device is absent", async () => {
211
+ const factory = new UseSmileIDMetadataFactory();
212
+ await flush();
213
+ const metadata = factory.make();
214
+
215
+ expect(entries(metadata).get("device_model")).toBe("");
216
+ expect(names(metadata)).not.toContain("build_product");
217
+ });
218
+
219
+ it("reads the carrier name from expo-cellular", async () => {
220
+ loadExpoCellular.mockReturnValue({ carrier: " MTN " });
221
+
222
+ const factory = new UseSmileIDMetadataFactory();
223
+ await flush();
224
+ expect(entries(factory.make()).get("carrier_info")).toBe("MTN");
225
+ });
226
+ });
227
+
228
+ describe("UseSmileIDMetadataFactory — async warmup", () => {
229
+ it("resolves network connection and client IP from expo-network", async () => {
230
+ loadExpoNetwork.mockReturnValue({
231
+ getNetworkStateAsync: vi.fn(async () => ({ type: "WIFI" })),
232
+ getIpAddressAsync: vi.fn(async () => "10.0.0.7"),
233
+ });
234
+
235
+ const factory = new UseSmileIDMetadataFactory();
236
+ await factory.warmup();
237
+ const values = entries(factory.make());
238
+
239
+ expect(values.get("network_connection")).toBe("WIFI");
240
+ expect(values.get("client_ip")).toBe("10.0.0.7");
241
+ });
242
+
243
+ it("keeps the empty defaults when the network calls reject", async () => {
244
+ loadExpoNetwork.mockReturnValue({
245
+ getNetworkStateAsync: vi.fn(async () => {
246
+ throw new Error("no radio");
247
+ }),
248
+ getIpAddressAsync: vi.fn(async () => {
249
+ throw new Error("no ip");
250
+ }),
251
+ });
252
+
253
+ const factory = new UseSmileIDMetadataFactory();
254
+ await factory.warmup();
255
+
256
+ expect(names(factory.make())).not.toContain("network_connection");
257
+ expect(names(factory.make())).not.toContain("client_ip");
258
+ });
259
+
260
+ it("skips the IP probe when the module does not expose one", async () => {
261
+ const network = networkModule(null);
262
+ loadExpoNetwork.mockReturnValue(network);
263
+
264
+ const factory = new UseSmileIDMetadataFactory();
265
+ await factory.warmup();
266
+
267
+ expect(network.getNetworkStateAsync).toHaveBeenCalled();
268
+ expect(names(factory.make())).not.toContain("client_ip");
269
+ });
270
+
271
+ it("maps expo-battery states to the cross-SDK plug status", async () => {
272
+ const cases: ReadonlyArray<readonly [number, string]> = [
273
+ [1, "0"],
274
+ [2, "1"],
275
+ [3, "1"],
276
+ [0, "-1"],
277
+ ];
278
+ for (const [nativeState, expected] of cases) {
279
+ loadExpoNetwork.mockReturnValue(networkModule());
280
+ loadExpoBattery.mockReturnValue({
281
+ getBatteryStateAsync: vi.fn(async () => nativeState),
282
+ });
283
+ const factory = new UseSmileIDMetadataFactory();
284
+ await factory.warmup();
285
+ expect(entries(factory.make()).get("battery_status")).toBe(expected);
286
+ }
287
+ });
288
+
289
+ it("omits battery_status when the battery probe throws", async () => {
290
+ loadExpoNetwork.mockReturnValue(networkModule());
291
+ loadExpoBattery.mockReturnValue({
292
+ getBatteryStateAsync: vi.fn(async () => {
293
+ throw new Error("no battery service");
294
+ }),
295
+ });
296
+
297
+ const factory = new UseSmileIDMetadataFactory();
298
+ await factory.warmup();
299
+ expect(names(factory.make())).not.toContain("battery_status");
300
+ });
301
+
302
+ it("increments and persists the launch counter through async-storage", async () => {
303
+ const setItem = vi.fn(async () => {});
304
+ loadExpoNetwork.mockReturnValue(networkModule());
305
+ loadAsyncStorage.mockReturnValue({
306
+ getItem: vi.fn(async () => "4"),
307
+ setItem,
308
+ });
309
+
310
+ const factory = new UseSmileIDMetadataFactory();
311
+ await factory.warmup();
312
+
313
+ expect(setItem).toHaveBeenCalledWith(
314
+ "com.usesmileid.metadata.sdk_launch_count",
315
+ "5",
316
+ );
317
+ expect(entries(factory.make()).get("sdk_launch_count")).toBe("5");
318
+ });
319
+
320
+ it("starts the launch counter at 1 when nothing is stored", async () => {
321
+ loadExpoNetwork.mockReturnValue(networkModule());
322
+ loadAsyncStorage.mockReturnValue({
323
+ getItem: vi.fn(async () => null),
324
+ setItem: vi.fn(async () => {}),
325
+ });
326
+
327
+ const factory = new UseSmileIDMetadataFactory();
328
+ await factory.warmup();
329
+ expect(entries(factory.make()).get("sdk_launch_count")).toBe("1");
330
+ });
331
+
332
+ it("omits sdk_launch_count when the store rejects", async () => {
333
+ loadExpoNetwork.mockReturnValue(networkModule());
334
+ loadAsyncStorage.mockReturnValue({
335
+ getItem: vi.fn(async () => {
336
+ throw new Error("storage unavailable");
337
+ }),
338
+ setItem: vi.fn(async () => {}),
339
+ });
340
+
341
+ const factory = new UseSmileIDMetadataFactory();
342
+ await factory.warmup();
343
+ expect(names(factory.make())).not.toContain("sdk_launch_count");
344
+ });
345
+
346
+ it("emits the resolved geolocation and the privileges blob", async () => {
347
+ loadExpoNetwork.mockReturnValue(networkModule());
348
+ resolveGeolocation.mockResolvedValue({
349
+ latitude: 1.5,
350
+ longitude: -2.5,
351
+ accuracy: "precise",
352
+ source: "gps",
353
+ });
354
+ privileges.mockResolvedValue('{"1":"100010"}');
355
+
356
+ const factory = new UseSmileIDMetadataFactory();
357
+ await factory.warmup();
358
+ const values = entries(factory.make());
359
+
360
+ expect(values.get("geolocation")).toContain('"latitude":1.5');
361
+ expect(values.get("privileges")).toBe('{"1":"100010"}');
362
+ });
363
+
364
+ it("omits privileges and geolocation when neither resolves", async () => {
365
+ loadExpoNetwork.mockReturnValue(networkModule());
366
+ privileges.mockResolvedValue("");
367
+ resolveGeolocation.mockResolvedValue(null);
368
+
369
+ const factory = new UseSmileIDMetadataFactory();
370
+ await factory.warmup();
371
+
372
+ expect(names(factory.make())).not.toContain("privileges");
373
+ expect(names(factory.make())).not.toContain("geolocation");
374
+ });
375
+
376
+ it("copies the native device-integrity signals onto the payload", async () => {
377
+ loadExpoNetwork.mockReturnValue(networkModule());
378
+ signals.mockResolvedValue({
379
+ buildHardware: "husky",
380
+ buildSource: "com.android.vending",
381
+ packageName: "com.partner.app",
382
+ fingerprint: "android-id-123",
383
+ vpn: true,
384
+ certificateDigest: ["AA:BB"],
385
+ });
386
+
387
+ const factory = new UseSmileIDMetadataFactory();
388
+ await factory.warmup();
389
+ const values = entries(factory.make());
390
+
391
+ expect(values.get("build_hardware")).toBe("husky");
392
+ expect(values.get("build_source")).toBe("com.android.vending");
393
+ expect(values.get("package_name")).toBe("com.partner.app");
394
+ expect(values.get("fingerprint")).toBe("android-id-123");
395
+ expect(values.get("vpn")).toBe("true");
396
+ expect(values.get("certificate_digest")).toBe('["AA:BB"]');
397
+ });
398
+
399
+ it("short-circuits the whole fan-out when expo-network is absent", async () => {
400
+ loadExpoNetwork.mockReturnValue(null);
401
+
402
+ const factory = new UseSmileIDMetadataFactory();
403
+ await factory.warmup();
404
+
405
+ expect(privileges).not.toHaveBeenCalled();
406
+ expect(signals).not.toHaveBeenCalled();
407
+ expect(names(factory.make())).not.toContain("client_ip");
408
+ });
409
+ });
410
+
411
+ describe("UseSmileIDMetadataFactory — make()", () => {
412
+ it("carries the partner-supplied app name and host application", async () => {
413
+ const factory = new UseSmileIDMetadataFactory({
414
+ appName: "PartnerApp",
415
+ hostApplication: "PartnerApp v1.2.3",
416
+ });
417
+ await flush();
418
+ const values = entries(factory.make());
419
+
420
+ expect(values.get("app_name")).toBe("PartnerApp");
421
+ expect(values.get("host_application")).toBe("PartnerApp v1.2.3");
422
+ expect(values.get("session_id")).toBe(factory.sessionId);
423
+ });
424
+
425
+ it("reports the sdk_config provenance and the resolved capture dimensions", async () => {
426
+ const factory = new UseSmileIDMetadataFactory();
427
+ await flush();
428
+
429
+ const defaults = entries(factory.make());
430
+ expect(defaults.get("sdk_config_source")).toBe("default");
431
+ expect(defaults.get("selfie_image_dimensions")).toBe("480x640");
432
+ expect(defaults.get("liveness_image_dimensions")).toBe("480x640");
433
+
434
+ const overridden = entries(
435
+ factory.make({
436
+ selfie: { width: 720, height: 1280 },
437
+ liveness: { width: 320, height: 240 },
438
+ }),
439
+ );
440
+ expect(overridden.get("sdk_config_source")).toBe("sdk_config");
441
+ expect(overridden.get("selfie_image_dimensions")).toBe("720x1280");
442
+ expect(overridden.get("liveness_image_dimensions")).toBe("320x240");
443
+ });
444
+
445
+ it("emits the Android Keystore attestation fields only on Android", async () => {
446
+ platform.OS = "android";
447
+ generateHardwareAttestation.mockResolvedValue({
448
+ securityLevel: 2,
449
+ certificateChain: ["leaf", "root"],
450
+ supportsAppAttest: false,
451
+ });
452
+
453
+ const factory = new UseSmileIDMetadataFactory();
454
+ await flush();
455
+ const values = entries(factory.make());
456
+
457
+ expect(values.get("supports_hardware_attestation")).toBe("2");
458
+ expect(values.get("attestation_certificate_chain")).toContain("leaf");
459
+ expect(values.has("supports_app_attest")).toBe(false);
460
+ });
461
+
462
+ it("omits the certificate chain when the Keystore minted none", async () => {
463
+ platform.OS = "android";
464
+ generateHardwareAttestation.mockResolvedValue({
465
+ securityLevel: 1,
466
+ certificateChain: [],
467
+ supportsAppAttest: false,
468
+ });
469
+
470
+ const factory = new UseSmileIDMetadataFactory();
471
+ await flush();
472
+ const metadata = factory.make();
473
+
474
+ expect(entries(metadata).get("supports_hardware_attestation")).toBe("1");
475
+ expect(names(metadata)).not.toContain("attestation_certificate_chain");
476
+ });
477
+
478
+ it("emits the App Attest capability only on iOS", async () => {
479
+ platform.OS = "ios";
480
+ generateHardwareAttestation.mockResolvedValue({
481
+ securityLevel: -2,
482
+ supportsAppAttest: true,
483
+ });
484
+
485
+ const factory = new UseSmileIDMetadataFactory();
486
+ await flush();
487
+ const values = entries(factory.make());
488
+
489
+ expect(values.get("supports_app_attest")).toBe("true");
490
+ expect(values.has("supports_hardware_attestation")).toBe(false);
491
+ });
492
+
493
+ it("omits the attestation fields when the native probe throws", async () => {
494
+ platform.OS = "android";
495
+ generateHardwareAttestation.mockRejectedValue(new Error("no keystore"));
496
+
497
+ const factory = new UseSmileIDMetadataFactory();
498
+ await flush();
499
+
500
+ expect(names(factory.make())).not.toContain(
501
+ "supports_hardware_attestation",
502
+ );
503
+ });
504
+
505
+ it("resolves the dynamic screen, orientation and locale fields per call", async () => {
506
+ const factory = new UseSmileIDMetadataFactory();
507
+ await flush();
508
+ const values = entries(factory.make());
509
+
510
+ expect(values.get("screen_resolution")).toBe("1170 × 2532");
511
+ expect(values.get("device_orientation")).toBe("Portrait");
512
+ expect(values.get("timezone")).toBe(
513
+ Intl.DateTimeFormat().resolvedOptions().timeZone,
514
+ );
515
+ expect(values.get("locale")).toBe(
516
+ Intl.DateTimeFormat().resolvedOptions().locale,
517
+ );
518
+ expect(values.get("wrapper_name")).toBe("react_native_expo");
519
+ expect(values.get("wrapper_version")).toBe(values.get("sdk_version"));
520
+ });
521
+
522
+ it("reports Landscape when the window is wider than it is tall", async () => {
523
+ const factory = new UseSmileIDMetadataFactory();
524
+ await flush();
525
+ dimensionsGet.mockImplementation(() => ({
526
+ width: 844,
527
+ height: 390,
528
+ scale: 3,
529
+ }));
530
+
531
+ expect(entries(factory.make()).get("device_orientation")).toBe("Landscape");
532
+ });
533
+
534
+ it("drops the screen fields when Dimensions reports a zero-sized surface", async () => {
535
+ const factory = new UseSmileIDMetadataFactory();
536
+ await flush();
537
+ dimensionsGet.mockImplementation(() => ({
538
+ width: 0,
539
+ height: 0,
540
+ scale: 0,
541
+ }));
542
+ const metadata = factory.make();
543
+
544
+ expect(names(metadata)).not.toContain("screen_resolution");
545
+ expect(names(metadata)).not.toContain("device_orientation");
546
+ });
547
+
548
+ it("falls back to empty strings when Dimensions, Intl and Date throw", async () => {
549
+ const factory = new UseSmileIDMetadataFactory();
550
+ await flush();
551
+
552
+ dimensionsGet.mockImplementation(() => {
553
+ throw new Error("no display metrics");
554
+ });
555
+ vi.spyOn(Intl, "DateTimeFormat").mockImplementation(() => {
556
+ throw new Error("no ICU");
557
+ });
558
+ vi.spyOn(Date.prototype, "toISOString").mockImplementation(() => {
559
+ throw new Error("no clock");
560
+ });
561
+
562
+ const metadata = factory.make();
563
+
564
+ expect(names(metadata)).not.toContain("screen_resolution");
565
+ expect(names(metadata)).not.toContain("device_orientation");
566
+ expect(names(metadata)).not.toContain("timezone");
567
+ expect(names(metadata)).not.toContain("locale");
568
+ expect(names(metadata)).not.toContain("local_time_of_enrolment");
569
+ });
570
+ });
571
+
572
+ describe("generateSessionId", () => {
573
+ function withCrypto(value: unknown, body: () => void): void {
574
+ const descriptor = Object.getOwnPropertyDescriptor(globalThis, "crypto");
575
+ Object.defineProperty(globalThis, "crypto", {
576
+ value,
577
+ configurable: true,
578
+ writable: true,
579
+ });
580
+ try {
581
+ body();
582
+ } finally {
583
+ if (descriptor) Object.defineProperty(globalThis, "crypto", descriptor);
584
+ else delete (globalThis as { crypto?: unknown }).crypto;
585
+ }
586
+ }
587
+
588
+ it("prefers crypto.randomUUID when available", () => {
589
+ withCrypto(
590
+ { randomUUID: () => "11111111-2222-4333-8444-555555555555" },
591
+ () => {
592
+ expect(generateSessionId()).toBe(
593
+ "11111111-2222-4333-8444-555555555555",
594
+ );
595
+ },
596
+ );
597
+ });
598
+
599
+ it("falls back to a UUID-v4-shaped id when crypto is unavailable", () => {
600
+ withCrypto(undefined, () => {
601
+ const id = generateSessionId();
602
+ const parts = id.split("-");
603
+ expect(parts).toHaveLength(5);
604
+ expect(parts[2]?.startsWith("4")).toBe(true);
605
+ expect(/^[0-9a-f-]+$/.test(id)).toBe(true);
606
+ expect(generateSessionId()).not.toBe(id);
607
+ });
608
+ });
609
+
610
+ it("falls back when crypto exists without randomUUID", () => {
611
+ withCrypto({}, () => {
612
+ expect(generateSessionId().split("-")).toHaveLength(5);
613
+ });
614
+ });
615
+ });
@@ -243,6 +243,6 @@ function faceDescriptorOf(
243
243
  const NOOP_DOCUMENT_DESCRIPTOR: ModelDescriptor = {
244
244
  provider: ModelProvider.Smile,
245
245
  name: "noop_document_detector",
246
- version: "12.0.1",
246
+ version: "12.0.2", // x-release-please-version
247
247
  bundled: true,
248
248
  };
@@ -61,7 +61,7 @@ import { CameraDefaults } from "../camera/camera_defaults";
61
61
 
62
62
  // Beta release version reported to the backend as `sdk_version`. Keep in sync
63
63
  // with the `version` field across the published package.json files.
64
- const SDK_VERSION = "12.0.1";
64
+ const SDK_VERSION = "12.0.2"; // x-release-please-version
65
65
  // The security-policy axis is versioned independently of the SDK release tag,
66
66
  // so it stays at the base `12.0.0` (not the prerelease tag).
67
67
  const SECURITY_POLICY_VERSION = "12.0.0";