@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,150 @@
1
+ import { describe, it, expect, beforeEach, 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 { loadExpoLocation } = vi.hoisted(() => ({
7
+ loadExpoLocation: vi.fn(),
8
+ }));
9
+
10
+ vi.mock("../optional_native_modules", () => ({ loadExpoLocation }));
11
+
12
+ import { resolveGeolocation } from "../geolocation";
13
+
14
+ function makeLocationModule(overrides: {
15
+ granted?: boolean;
16
+ position?: {
17
+ coords: { latitude: number; longitude: number; accuracy: number | null };
18
+ } | null;
19
+ permissionsError?: Error;
20
+ positionError?: Error;
21
+ }) {
22
+ return {
23
+ getForegroundPermissionsAsync: vi.fn(async () => {
24
+ if (overrides.permissionsError) throw overrides.permissionsError;
25
+ return { granted: overrides.granted ?? true };
26
+ }),
27
+ getLastKnownPositionAsync: vi.fn(async () => {
28
+ if (overrides.positionError) throw overrides.positionError;
29
+ return overrides.position ?? null;
30
+ }),
31
+ requestForegroundPermissionsAsync: vi.fn(),
32
+ };
33
+ }
34
+
35
+ function position(accuracy: number | null) {
36
+ return { coords: { latitude: 6.5244, longitude: 3.3792, accuracy } };
37
+ }
38
+
39
+ beforeEach(() => {
40
+ loadExpoLocation.mockReset();
41
+ });
42
+
43
+ describe("resolveGeolocation", () => {
44
+ it("returns null when expo-location isn't installed", async () => {
45
+ loadExpoLocation.mockReturnValue(null);
46
+
47
+ await expect(resolveGeolocation()).resolves.toBeNull();
48
+ });
49
+
50
+ it("never prompts — it only reads an already-granted permission", async () => {
51
+ const location = makeLocationModule({
52
+ granted: true,
53
+ position: position(5),
54
+ });
55
+ loadExpoLocation.mockReturnValue(location);
56
+
57
+ await resolveGeolocation();
58
+
59
+ expect(location.getForegroundPermissionsAsync).toHaveBeenCalledTimes(1);
60
+ expect(location.requestForegroundPermissionsAsync).not.toHaveBeenCalled();
61
+ });
62
+
63
+ it("returns null and skips the location read when permission is absent", async () => {
64
+ const location = makeLocationModule({ granted: false });
65
+ loadExpoLocation.mockReturnValue(location);
66
+
67
+ await expect(resolveGeolocation()).resolves.toBeNull();
68
+ expect(location.getLastKnownPositionAsync).not.toHaveBeenCalled();
69
+ });
70
+
71
+ it("returns null when there is no cached fix", async () => {
72
+ loadExpoLocation.mockReturnValue(
73
+ makeLocationModule({ granted: true, position: null }),
74
+ );
75
+
76
+ await expect(resolveGeolocation()).resolves.toBeNull();
77
+ });
78
+
79
+ it("classifies a sub-20m fix as precise and tags the source as gps", async () => {
80
+ loadExpoLocation.mockReturnValue(
81
+ makeLocationModule({ granted: true, position: position(12) }),
82
+ );
83
+
84
+ await expect(resolveGeolocation()).resolves.toEqual({
85
+ latitude: 6.5244,
86
+ longitude: 3.3792,
87
+ accuracy: "precise",
88
+ source: "gps",
89
+ });
90
+ });
91
+
92
+ it("classifies a sub-100m fix as approximate", async () => {
93
+ loadExpoLocation.mockReturnValue(
94
+ makeLocationModule({ granted: true, position: position(45) }),
95
+ );
96
+
97
+ const result = await resolveGeolocation();
98
+
99
+ expect(result?.accuracy).toBe("approximate");
100
+ });
101
+
102
+ it("classifies a coarse fix as unknown rather than overstating precision", async () => {
103
+ loadExpoLocation.mockReturnValue(
104
+ makeLocationModule({ granted: true, position: position(250) }),
105
+ );
106
+
107
+ const result = await resolveGeolocation();
108
+
109
+ expect(result?.accuracy).toBe("unknown");
110
+ });
111
+
112
+ it("treats a missing accuracy reading as unknown", async () => {
113
+ loadExpoLocation.mockReturnValue(
114
+ makeLocationModule({ granted: true, position: position(null) }),
115
+ );
116
+
117
+ const result = await resolveGeolocation();
118
+
119
+ expect(result?.accuracy).toBe("unknown");
120
+ });
121
+
122
+ it("treats a non-positive accuracy reading as unknown", async () => {
123
+ loadExpoLocation.mockReturnValue(
124
+ makeLocationModule({ granted: true, position: position(0) }),
125
+ );
126
+
127
+ const result = await resolveGeolocation();
128
+
129
+ expect(result?.accuracy).toBe("unknown");
130
+ });
131
+
132
+ it("swallows a throwing permission check so metadata collection never fails", async () => {
133
+ loadExpoLocation.mockReturnValue(
134
+ makeLocationModule({ permissionsError: new Error("location off") }),
135
+ );
136
+
137
+ await expect(resolveGeolocation()).resolves.toBeNull();
138
+ });
139
+
140
+ it("swallows a throwing location read", async () => {
141
+ loadExpoLocation.mockReturnValue(
142
+ makeLocationModule({
143
+ granted: true,
144
+ positionError: new Error("provider unavailable"),
145
+ }),
146
+ );
147
+
148
+ await expect(resolveGeolocation()).resolves.toBeNull();
149
+ });
150
+ });
@@ -38,7 +38,7 @@ const modelConfiguration: ModelConfiguration = {
38
38
  documentModel: {
39
39
  provider: ModelProvider.Smile,
40
40
  name: "noop_document_detector",
41
- version: "12.0.1",
41
+ version: "12.0.2",
42
42
  bundled: true,
43
43
  },
44
44
  };
@@ -9,7 +9,7 @@ function fields(
9
9
  overrides: Partial<UseSmileIDMetadataFields> = {},
10
10
  ): UseSmileIDMetadataFields {
11
11
  return {
12
- sdkVersion: "12.0.1",
12
+ sdkVersion: "12.0.2",
13
13
  sdk: "react-native-expo",
14
14
  deviceOs: "Android 14",
15
15
  deviceModel: "",
@@ -89,10 +89,10 @@ describe("device-context keys (62-key parity)", () => {
89
89
  it("emits wrapper identity when present and omits it when blank", () => {
90
90
  const populated = fields({
91
91
  wrapperName: "react_native_expo",
92
- wrapperVersion: "12.0.1",
92
+ wrapperVersion: "12.0.2",
93
93
  });
94
94
  expect(value("wrapper_name", populated)).toBe("react_native_expo");
95
- expect(value("wrapper_version", populated)).toBe("12.0.1");
95
+ expect(value("wrapper_version", populated)).toBe("12.0.2");
96
96
 
97
97
  const names = new UseSmileIDMetadata(fields())
98
98
  .toEntries()
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "_comment": "Shared cross-SDK golden parity vectors. These metadata keys must serialise to the EXACT same wire value on Android, iOS, Flutter, and React Native for the standard fully-populated golden snapshot. Each SDK's metadata-payload golden test asserts its payload contains every entry below verbatim. Maintained manually — keep the four copies (android/ios/flutter/react-native-expo test dirs) in sync.",
3
3
  "expected": {
4
- "sdk_version": "12.0.1",
4
+ "sdk_version": "12.0.2",
5
5
  "proxy": "true",
6
6
  "proximity_sensor": "true",
7
7
  "sdk_config_source": "sdk_config",
@@ -17,7 +17,7 @@ import goldenFixture from "./metadata_golden_fixture.json";
17
17
  describe("metadata payload (golden, device-context fields)", () => {
18
18
  function fullyPopulated(): UseSmileIDMetadataFields {
19
19
  return {
20
- sdkVersion: "12.0.1",
20
+ sdkVersion: "12.0.2",
21
21
  sdk: "react-native-expo",
22
22
  deviceOs: "Android 14",
23
23
  deviceModel: "Google Pixel 8",
@@ -56,7 +56,7 @@ describe("metadata payload (golden, device-context fields)", () => {
56
56
  vpn: false,
57
57
  certificateDigest: ["AA:BB", "CC:DD"],
58
58
  wrapperName: "react_native_expo",
59
- wrapperVersion: "12.0.1",
59
+ wrapperVersion: "12.0.2",
60
60
  sdkLaunchCount: 5,
61
61
  geolocation: {
62
62
  latitude: 1.5,
@@ -128,7 +128,7 @@ describe("metadata payload (golden, device-context fields)", () => {
128
128
 
129
129
  it("emits scalar + meaningful-falsey + JSON-shaped values correctly", () => {
130
130
  const map = entriesOf(fullyPopulated());
131
- expect(map.get("sdk_version")).toBe("12.0.1");
131
+ expect(map.get("sdk_version")).toBe("12.0.2");
132
132
  expect(map.get("proxy")).toBe("true");
133
133
  expect(map.get("battery_status")).toBe("0"); // 0 is meaningful
134
134
  expect(map.get("sdk_config_source")).toBe("sdk_config");
@@ -153,7 +153,7 @@ describe("metadata payload (golden, device-context fields)", () => {
153
153
 
154
154
  it("default snapshot emits only the always-present keys", () => {
155
155
  const minimal: UseSmileIDMetadataFields = {
156
- sdkVersion: "12.0.1",
156
+ sdkVersion: "12.0.2",
157
157
  sdk: "react-native-expo",
158
158
  deviceOs: "Android 14",
159
159
  deviceModel: "",
@@ -0,0 +1,234 @@
1
+ import { describe, it, expect, afterEach, beforeEach, vi } from "vitest";
2
+ import Module from "node:module";
3
+
4
+ import {
5
+ loadExpoDevice,
6
+ loadExpoNetwork,
7
+ loadExpoBattery,
8
+ loadExpoCellular,
9
+ loadAsyncStorage,
10
+ loadExpoSensors,
11
+ loadExpoLocation,
12
+ _resetOptionalModulesForTesting,
13
+ } from "../optional_native_modules";
14
+
15
+ /**
16
+ * The optional peer bridges resolve through a dynamic `require()` so a
17
+ * partner who skips `expo-battery` / `expo-sensors` / async-storage
18
+ * still gets a bundle that builds. `vi.mock` can't reach a CommonJS
19
+ * `require`, so these tests intercept Node's module loader itself —
20
+ * that is the exact seam the production code depends on.
21
+ */
22
+
23
+ type Loader = (id: string, parent: unknown, isMain: boolean) => unknown;
24
+
25
+ const loaderHost = Module as unknown as { _load: Loader };
26
+ const originalLoad: Loader = loaderHost._load;
27
+
28
+ /** Resolution table consulted by the patched loader; unlisted ids fall through. */
29
+ let resolutions: Record<string, unknown | (() => never)> = {};
30
+ let requestedIds: string[] = [];
31
+
32
+ beforeEach(() => {
33
+ resolutions = {};
34
+ requestedIds = [];
35
+ _resetOptionalModulesForTesting();
36
+ vi.spyOn(loaderHost, "_load").mockImplementation((id, parent, isMain) => {
37
+ requestedIds.push(id);
38
+ if (Object.prototype.hasOwnProperty.call(resolutions, id)) {
39
+ const entry = resolutions[id];
40
+ if (typeof entry === "function") return (entry as () => never)();
41
+ return entry;
42
+ }
43
+ return originalLoad.call(Module, id, parent, isMain);
44
+ });
45
+ });
46
+
47
+ afterEach(() => {
48
+ vi.restoreAllMocks();
49
+ _resetOptionalModulesForTesting();
50
+ });
51
+
52
+ function throwsMissing(id: string) {
53
+ return () => {
54
+ const error = new Error(`Cannot find module '${id}'`) as Error & {
55
+ code: string;
56
+ };
57
+ error.code = "MODULE_NOT_FOUND";
58
+ throw error;
59
+ };
60
+ }
61
+
62
+ /** Stands in for `expo-modules-core`'s non-throwing native-module lookup. */
63
+ function expoCore(natives: Record<string, unknown>) {
64
+ return {
65
+ requireOptionalNativeModule: <T>(name: string): T | null =>
66
+ (natives[name] as T) ?? null,
67
+ };
68
+ }
69
+
70
+ describe("optional native module bridges", () => {
71
+ describe("expo-modules-core backed loaders", () => {
72
+ it("returns the raw ExpoDevice native module when it is linked", () => {
73
+ const native = { modelName: "Pixel 8", brand: "google" };
74
+ resolutions["expo-modules-core"] = expoCore({ ExpoDevice: native });
75
+
76
+ expect(loadExpoDevice()).toBe(native);
77
+ });
78
+
79
+ it("returns null when the native module is not linked", () => {
80
+ resolutions["expo-modules-core"] = expoCore({});
81
+
82
+ expect(loadExpoDevice()).toBeNull();
83
+ expect(loadExpoNetwork()).toBeNull();
84
+ expect(loadExpoBattery()).toBeNull();
85
+ expect(loadExpoCellular()).toBeNull();
86
+ });
87
+
88
+ it("returns null when expo-modules-core itself cannot be resolved", () => {
89
+ resolutions["expo-modules-core"] = throwsMissing("expo-modules-core");
90
+
91
+ expect(loadExpoDevice()).toBeNull();
92
+ expect(loadExpoNetwork()).toBeNull();
93
+ expect(loadExpoBattery()).toBeNull();
94
+ expect(loadExpoCellular()).toBeNull();
95
+ });
96
+
97
+ it("returns null when the core build predates requireOptionalNativeModule", () => {
98
+ resolutions["expo-modules-core"] = {};
99
+
100
+ expect(loadExpoDevice()).toBeNull();
101
+ expect(loadExpoBattery()).toBeNull();
102
+ });
103
+
104
+ it("resolves ExpoNetwork, ExpoBattery and ExpoCellular by name", () => {
105
+ const network = { getNetworkStateAsync: async () => ({ type: "WIFI" }) };
106
+ const battery = { getBatteryStateAsync: async () => 2 };
107
+ const cellular = { carrier: "MTN" };
108
+ resolutions["expo-modules-core"] = expoCore({
109
+ ExpoNetwork: network,
110
+ ExpoBattery: battery,
111
+ ExpoCellular: cellular,
112
+ });
113
+
114
+ expect(loadExpoNetwork()).toBe(network);
115
+ expect(loadExpoBattery()).toBe(battery);
116
+ expect(loadExpoCellular()).toBe(cellular);
117
+ });
118
+
119
+ it("caches the resolution — a second call never re-requires", () => {
120
+ const native = { modelName: "Pixel 8" };
121
+ resolutions["expo-modules-core"] = expoCore({ ExpoDevice: native });
122
+
123
+ expect(loadExpoDevice()).toBe(native);
124
+ const requiresAfterFirst = requestedIds.filter(
125
+ (id) => id === "expo-modules-core",
126
+ ).length;
127
+
128
+ // Swap the table: a cached loader must not observe the change.
129
+ resolutions["expo-modules-core"] = expoCore({
130
+ ExpoDevice: { modelName: "Different" },
131
+ });
132
+ expect(loadExpoDevice()).toBe(native);
133
+ expect(
134
+ requestedIds.filter((id) => id === "expo-modules-core").length,
135
+ ).toBe(requiresAfterFirst);
136
+ });
137
+
138
+ it("caches the null resolution too — a missing module is asked for once", () => {
139
+ resolutions["expo-modules-core"] = throwsMissing("expo-modules-core");
140
+
141
+ expect(loadExpoBattery()).toBeNull();
142
+ const count = requestedIds.filter(
143
+ (id) => id === "expo-modules-core",
144
+ ).length;
145
+ expect(loadExpoBattery()).toBeNull();
146
+ expect(
147
+ requestedIds.filter((id) => id === "expo-modules-core").length,
148
+ ).toBe(count);
149
+ });
150
+ });
151
+
152
+ describe("async storage", () => {
153
+ it("unwraps the CommonJS default export", () => {
154
+ const storage = {
155
+ getItem: async () => null,
156
+ setItem: async () => {},
157
+ };
158
+ resolutions["@react-native-async-storage/async-storage"] = {
159
+ default: storage,
160
+ };
161
+
162
+ expect(loadAsyncStorage()).toBe(storage);
163
+ });
164
+
165
+ it("accepts a namespace without a default export", () => {
166
+ const storage = {
167
+ getItem: async () => null,
168
+ setItem: async () => {},
169
+ };
170
+ resolutions["@react-native-async-storage/async-storage"] = storage;
171
+
172
+ expect(loadAsyncStorage()).toBe(storage);
173
+ });
174
+
175
+ it("returns null when the package is not installed", () => {
176
+ resolutions["@react-native-async-storage/async-storage"] = throwsMissing(
177
+ "@react-native-async-storage/async-storage",
178
+ );
179
+
180
+ expect(loadAsyncStorage()).toBeNull();
181
+ });
182
+ });
183
+
184
+ describe("expo-sensors and expo-location", () => {
185
+ it("returns the module namespace when installed", () => {
186
+ const sensors = {
187
+ Accelerometer: {
188
+ setUpdateInterval: () => {},
189
+ addListener: () => ({ remove: () => {} }),
190
+ },
191
+ };
192
+ const location = {
193
+ getForegroundPermissionsAsync: async () => ({ granted: true }),
194
+ getLastKnownPositionAsync: async () => null,
195
+ };
196
+ resolutions["expo-sensors"] = sensors;
197
+ resolutions["expo-location"] = location;
198
+
199
+ expect(loadExpoSensors()).toBe(sensors);
200
+ expect(loadExpoLocation()).toBe(location);
201
+ });
202
+
203
+ it("returns null when either peer dep is absent", () => {
204
+ resolutions["expo-sensors"] = throwsMissing("expo-sensors");
205
+ resolutions["expo-location"] = throwsMissing("expo-location");
206
+
207
+ expect(loadExpoSensors()).toBeNull();
208
+ expect(loadExpoLocation()).toBeNull();
209
+ });
210
+ });
211
+
212
+ it("_resetOptionalModulesForTesting clears every cached bridge", () => {
213
+ const first = { modelName: "First" };
214
+ resolutions["expo-modules-core"] = expoCore({ ExpoDevice: first });
215
+ resolutions["expo-sensors"] = { Accelerometer: {} };
216
+ expect(loadExpoDevice()).toBe(first);
217
+ expect(loadExpoSensors()).toEqual({ Accelerometer: {} });
218
+
219
+ _resetOptionalModulesForTesting();
220
+
221
+ const second = { modelName: "Second" };
222
+ resolutions["expo-modules-core"] = expoCore({ ExpoDevice: second });
223
+ resolutions["expo-sensors"] = throwsMissing("expo-sensors");
224
+ resolutions["expo-location"] = throwsMissing("expo-location");
225
+ resolutions["@react-native-async-storage/async-storage"] = throwsMissing(
226
+ "@react-native-async-storage/async-storage",
227
+ );
228
+
229
+ expect(loadExpoDevice()).toBe(second);
230
+ expect(loadExpoSensors()).toBeNull();
231
+ expect(loadExpoLocation()).toBeNull();
232
+ expect(loadAsyncStorage()).toBeNull();
233
+ });
234
+ });
@@ -23,7 +23,7 @@ import { SdkConfigSource } from "../../data/model/sdk_config_source";
23
23
  // survive `toEntries()` from this fixture are the truly-always-present ones
24
24
  // (SDK constants, sessionId, sdk_config_source).
25
25
  const baseFields: UseSmileIDMetadataFields = Object.freeze({
26
- sdkVersion: "12.0.1",
26
+ sdkVersion: "12.0.2",
27
27
  sdk: "react-native-expo",
28
28
  deviceOs: "",
29
29
  deviceModel: "",