react-native-nitro-version-check 2.0.0 → 2.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.
@@ -66,7 +66,7 @@ class HybridVersionCheck : HybridVersionCheckSpec() {
66
66
  override fun needsUpdate(): Promise<Boolean> {
67
67
  return Promise.async {
68
68
  try {
69
- val latest = getLatestVersion().await()
69
+ val latest = getLatestVersion(null).await()
70
70
  version != latest
71
71
  } catch (e: Exception) {
72
72
  throw Exception("Failed to check for updates: ${e.message}", e)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-version-check",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "A lightweight, fast version-checking library for React Native, powered by Nitro Modules",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",
@@ -8,6 +8,7 @@
8
8
  "react-native": "src/index",
9
9
  "source": "src/index",
10
10
  "files": [
11
+ "src",
11
12
  "lib",
12
13
  "react-native.config.js",
13
14
  "nitrogen",
@@ -157,7 +158,7 @@
157
158
  "nitrogen": "^0.35.0",
158
159
  "react": "19.2.0",
159
160
  "react-native": "0.83.0",
160
- "react-native-nitro-modules": "*",
161
+ "react-native-nitro-modules": ">=0.35.0",
161
162
  "release-it": "^19.2.4",
162
163
  "ts-jest": "^29.1.2",
163
164
  "typescript": "^5.8.3"
@@ -165,6 +166,6 @@
165
166
  "peerDependencies": {
166
167
  "react": "*",
167
168
  "react-native": "*",
168
- "react-native-nitro-modules": ">=35.0"
169
+ "react-native-nitro-modules": ">=0.35.0"
169
170
  }
170
171
  }
@@ -0,0 +1,283 @@
1
+ import { type UpdateLevel, VersionCheck } from "../index";
2
+
3
+ // Mock the NitroModules
4
+ jest.mock("react-native-nitro-modules", () => ({
5
+ NitroModules: {
6
+ createHybridObject: jest.fn(() => ({
7
+ version: "1.2.0",
8
+ buildNumber: "42",
9
+ packageName: "com.example.app",
10
+ installSource: "appstore",
11
+ getCountry: jest.fn(() => "US"),
12
+ getStoreUrl: jest.fn(() => Promise.resolve("https://apps.apple.com/app/example")),
13
+ getLatestVersion: jest.fn(() => Promise.resolve("1.3.0")),
14
+ })),
15
+ },
16
+ }));
17
+
18
+ describe("VersionCheck API", () => {
19
+ describe("structure", () => {
20
+ it("should export VersionCheck object with all required properties", () => {
21
+ expect(VersionCheck).toBeDefined();
22
+ expect(typeof VersionCheck).toBe("object");
23
+ });
24
+
25
+ it("should have sync properties", () => {
26
+ expect(VersionCheck.version).toBeDefined();
27
+ expect(typeof VersionCheck.version).toBe("string");
28
+
29
+ expect(VersionCheck.buildNumber).toBeDefined();
30
+ expect(typeof VersionCheck.buildNumber).toBe("string");
31
+
32
+ expect(VersionCheck.packageName).toBeDefined();
33
+ expect(typeof VersionCheck.packageName).toBe("string");
34
+ });
35
+
36
+ it("should have optional installSource property", () => {
37
+ expect(VersionCheck.installSource).toBeDefined();
38
+ expect(
39
+ VersionCheck.installSource === undefined ||
40
+ VersionCheck.installSource === "appstore" ||
41
+ VersionCheck.installSource === "testflight" ||
42
+ VersionCheck.installSource === "playstore"
43
+ ).toBe(true);
44
+ });
45
+
46
+ it("should have sync method getCountry", () => {
47
+ expect(typeof VersionCheck.getCountry).toBe("function");
48
+ expect(VersionCheck.getCountry()).toBeDefined();
49
+ });
50
+
51
+ it("should have async method getStoreUrl", () => {
52
+ expect(typeof VersionCheck.getStoreUrl).toBe("function");
53
+ const result = VersionCheck.getStoreUrl();
54
+ expect(result).toBeInstanceOf(Promise);
55
+ });
56
+
57
+ it("should have async method getLatestVersion", () => {
58
+ expect(typeof VersionCheck.getLatestVersion).toBe("function");
59
+ const result = VersionCheck.getLatestVersion();
60
+ expect(result).toBeInstanceOf(Promise);
61
+ });
62
+
63
+ it("should have async method needsUpdate", () => {
64
+ expect(typeof VersionCheck.needsUpdate).toBe("function");
65
+ const result = VersionCheck.needsUpdate();
66
+ expect(result).toBeInstanceOf(Promise);
67
+ });
68
+ });
69
+
70
+ describe("property access", () => {
71
+ it("should return string values for sync properties", () => {
72
+ expect(typeof VersionCheck.version).toBe("string");
73
+ expect(typeof VersionCheck.buildNumber).toBe("string");
74
+ expect(typeof VersionCheck.packageName).toBe("string");
75
+ });
76
+
77
+ it("should have consistent version format", () => {
78
+ const version = VersionCheck.version;
79
+ // Should be a semantic version like "1.2.0"
80
+ expect(version).toMatch(/^\d+\.\d+\.\d+$/);
81
+ });
82
+
83
+ it("should return a valid country code for getCountry()", () => {
84
+ const country = VersionCheck.getCountry();
85
+ // Should be a 2-letter ISO country code (uppercase letters)
86
+ expect(country).toMatch(/^[A-Z]{2}$/);
87
+ });
88
+ });
89
+
90
+ describe("needsUpdate", () => {
91
+ it("should return a Promise<boolean>", async () => {
92
+ const result = VersionCheck.needsUpdate();
93
+ expect(result).toBeInstanceOf(Promise);
94
+ const resolved = await result;
95
+ expect(typeof resolved).toBe("boolean");
96
+ });
97
+
98
+ it("should accept options with level parameter", async () => {
99
+ const levels: UpdateLevel[] = ["major", "minor", "patch"];
100
+ for (const level of levels) {
101
+ const result = VersionCheck.needsUpdate({ level });
102
+ expect(result).toBeInstanceOf(Promise);
103
+ const resolved = await result;
104
+ expect(typeof resolved).toBe("boolean");
105
+ }
106
+ });
107
+
108
+ it("should default to patch level when no options provided", async () => {
109
+ const result = await VersionCheck.needsUpdate();
110
+ expect(typeof result).toBe("boolean");
111
+ });
112
+
113
+ it("should compare current version with latest", async () => {
114
+ // With mocked latest version as '1.3.0' and current as '1.2.0'
115
+ const needsUpdate = await VersionCheck.needsUpdate();
116
+ expect(typeof needsUpdate).toBe("boolean");
117
+ });
118
+ });
119
+
120
+ describe("API consistency", () => {
121
+ it("should provide consistent property access across multiple calls", () => {
122
+ expect(VersionCheck.version).toBe(VersionCheck.version);
123
+ expect(VersionCheck.buildNumber).toBe(VersionCheck.buildNumber);
124
+ expect(VersionCheck.packageName).toBe(VersionCheck.packageName);
125
+ expect(VersionCheck.getCountry()).toBe(VersionCheck.getCountry());
126
+ });
127
+
128
+ it("should have readonly type signature", () => {
129
+ // Type checking ensures VersionCheck object is readonly
130
+ // This test verifies all methods and properties exist as expected
131
+ expect(VersionCheck).toBeDefined();
132
+ expect(Object.keys(VersionCheck).length > 0).toBe(true);
133
+ });
134
+ });
135
+
136
+ describe("async operations", () => {
137
+ it("should fetch store URL", async () => {
138
+ const url = await VersionCheck.getStoreUrl();
139
+ expect(url).toMatch(/^https?:\/\//);
140
+ expect(url).toMatch(/apps\.apple\.com|play\.google\.com/);
141
+ });
142
+
143
+ it("should fetch store URL with custom country code", async () => {
144
+ const urlUS = await VersionCheck.getStoreUrl({ countryCode: "US" });
145
+ const urlGB = await VersionCheck.getStoreUrl({ countryCode: "GB" });
146
+ expect(urlUS).toMatch(/^https?:\/\//);
147
+ expect(urlGB).toMatch(/^https?:\/\//);
148
+ expect(urlUS).toMatch(/apps\.apple\.com|play\.google\.com/);
149
+ expect(urlGB).toMatch(/apps\.apple\.com|play\.google\.com/);
150
+ });
151
+
152
+ it("should fetch store URL with undefined country code", async () => {
153
+ const url = await VersionCheck.getStoreUrl({ countryCode: undefined });
154
+ expect(url).toMatch(/^https?:\/\//);
155
+ expect(url).toMatch(/apps\.apple\.com|play\.google\.com/);
156
+ });
157
+
158
+ it("should fetch latest version", async () => {
159
+ const latest = await VersionCheck.getLatestVersion();
160
+ expect(typeof latest).toBe("string");
161
+ // Should be a semantic version
162
+ expect(latest).toMatch(/^\d+\.\d+\.\d+$/);
163
+ });
164
+
165
+ it("should fetch latest version with custom country code", async () => {
166
+ const latestUS = await VersionCheck.getLatestVersion({ countryCode: "US" });
167
+ const latestGB = await VersionCheck.getLatestVersion({ countryCode: "GB" });
168
+ expect(typeof latestUS).toBe("string");
169
+ expect(typeof latestGB).toBe("string");
170
+ expect(latestUS).toMatch(/^\d+\.\d+\.\d+$/);
171
+ expect(latestGB).toMatch(/^\d+\.\d+\.\d+$/);
172
+ });
173
+
174
+ it("should use device country code by default", async () => {
175
+ const latest = await VersionCheck.getLatestVersion();
176
+ const latestWithDefault = await VersionCheck.getLatestVersion({ countryCode: undefined });
177
+ expect(typeof latest).toBe("string");
178
+ expect(typeof latestWithDefault).toBe("string");
179
+ });
180
+
181
+ it("should handle getLatestVersion and needsUpdate in parallel", async () => {
182
+ const [latest, needsUpdate] = await Promise.all([VersionCheck.getLatestVersion(), VersionCheck.needsUpdate()]);
183
+ expect(typeof latest).toBe("string");
184
+ expect(typeof needsUpdate).toBe("boolean");
185
+ });
186
+
187
+ it("should handle all async operations in parallel", async () => {
188
+ const [storeUrl, latestVersion, needsUpdate] = await Promise.all([
189
+ VersionCheck.getStoreUrl(),
190
+ VersionCheck.getLatestVersion(),
191
+ VersionCheck.needsUpdate(),
192
+ ]);
193
+ expect(typeof storeUrl).toBe("string");
194
+ expect(typeof latestVersion).toBe("string");
195
+ expect(typeof needsUpdate).toBe("boolean");
196
+ });
197
+ });
198
+
199
+ describe("common usage patterns", () => {
200
+ it("should support destructuring", () => {
201
+ const { version, buildNumber, packageName, installSource } = VersionCheck;
202
+ expect(typeof version).toBe("string");
203
+ expect(typeof buildNumber).toBe("string");
204
+ expect(typeof packageName).toBe("string");
205
+ expect(installSource === undefined || typeof installSource === "string").toBe(true);
206
+ });
207
+
208
+ it("should support destructuring with getCountry", () => {
209
+ const { getCountry } = VersionCheck;
210
+ expect(typeof getCountry).toBe("function");
211
+ expect(getCountry()).toBeDefined();
212
+ });
213
+
214
+ it("should support common update flow", async () => {
215
+ const url = await VersionCheck.getStoreUrl();
216
+ const needsUpdate = await VersionCheck.needsUpdate();
217
+
218
+ expect(url).toMatch(/^https?:\/\//);
219
+ expect(url).toMatch(/apps\.apple\.com|play\.google\.com/);
220
+ expect(typeof needsUpdate).toBe("boolean");
221
+
222
+ if (needsUpdate) {
223
+ expect(url).toBeTruthy();
224
+ }
225
+ });
226
+
227
+ it("should support granular update checks", async () => {
228
+ const majorUpdate = await VersionCheck.needsUpdate({ level: "major" });
229
+ const minorUpdate = await VersionCheck.needsUpdate({ level: "minor" });
230
+ const patchUpdate = await VersionCheck.needsUpdate({ level: "patch" });
231
+
232
+ // All should return boolean values
233
+ expect([majorUpdate, minorUpdate, patchUpdate]).toEqual(
234
+ expect.arrayContaining([true, false].includes(majorUpdate) ? [majorUpdate] : [])
235
+ );
236
+ expect(typeof majorUpdate).toBe("boolean");
237
+ expect(typeof minorUpdate).toBe("boolean");
238
+ expect(typeof patchUpdate).toBe("boolean");
239
+
240
+ // Logical consistency: granular checks have semantic ordering
241
+ // If major update, then minor and patch should also be true
242
+ if (majorUpdate) {
243
+ expect(minorUpdate).toBe(true);
244
+ expect(patchUpdate).toBe(true);
245
+ }
246
+ // If minor but not major, patch should still be true
247
+ if (minorUpdate && !majorUpdate) {
248
+ expect(patchUpdate).toBe(true);
249
+ }
250
+ });
251
+
252
+ it("should support install source detection", () => {
253
+ const source = VersionCheck.installSource;
254
+
255
+ if (source !== undefined) {
256
+ expect(["appstore", "testflight", "playstore"]).toContain(source);
257
+ }
258
+ });
259
+
260
+ it("should support region-specific store URLs", async () => {
261
+ const urlUS = await VersionCheck.getStoreUrl({ countryCode: "US" });
262
+ const urlGB = await VersionCheck.getStoreUrl({ countryCode: "GB" });
263
+ const urlJP = await VersionCheck.getStoreUrl({ countryCode: "JP" });
264
+
265
+ expect(urlUS).toMatch(/^https?:\/\//);
266
+ expect(urlGB).toMatch(/^https?:\/\//);
267
+ expect(urlJP).toMatch(/^https?:\/\//);
268
+ expect(urlUS).toMatch(/apps\.apple\.com|play\.google\.com/);
269
+ expect(urlGB).toMatch(/apps\.apple\.com|play\.google\.com/);
270
+ expect(urlJP).toMatch(/apps\.apple\.com|play\.google\.com/);
271
+ });
272
+
273
+ it("should support region-specific version checks", async () => {
274
+ const latestUS = await VersionCheck.getLatestVersion({ countryCode: "US" });
275
+ const latestGB = await VersionCheck.getLatestVersion({ countryCode: "GB" });
276
+ const latestJP = await VersionCheck.getLatestVersion({ countryCode: "JP" });
277
+
278
+ expect(typeof latestUS).toBe("string");
279
+ expect(typeof latestGB).toBe("string");
280
+ expect(typeof latestJP).toBe("string");
281
+ });
282
+ });
283
+ });
@@ -0,0 +1,145 @@
1
+ import { compareVersions, isNewerVersion } from "../semver";
2
+
3
+ describe("semver", () => {
4
+ describe("compareVersions", () => {
5
+ it("should return -1 when first version is older", () => {
6
+ expect(compareVersions("1.2.0", "1.3.0")).toBe(-1);
7
+ expect(compareVersions("1.2.0", "2.0.0")).toBe(-1);
8
+ expect(compareVersions("1.2.3", "1.2.4")).toBe(-1);
9
+ });
10
+
11
+ it("should return 1 when first version is newer", () => {
12
+ expect(compareVersions("2.0.0", "1.9.9")).toBe(1);
13
+ expect(compareVersions("1.3.0", "1.2.0")).toBe(1);
14
+ expect(compareVersions("1.2.4", "1.2.3")).toBe(1);
15
+ });
16
+
17
+ it("should return 0 when versions are equal", () => {
18
+ expect(compareVersions("1.0.0", "1.0.0")).toBe(0);
19
+ expect(compareVersions("2.5.10", "2.5.10")).toBe(0);
20
+ });
21
+
22
+ it("should handle major version differences", () => {
23
+ expect(compareVersions("1.0.0", "2.0.0")).toBe(-1);
24
+ expect(compareVersions("3.0.0", "2.0.0")).toBe(1);
25
+ });
26
+
27
+ it("should handle minor version differences", () => {
28
+ expect(compareVersions("1.2.0", "1.3.0")).toBe(-1);
29
+ expect(compareVersions("1.5.0", "1.3.0")).toBe(1);
30
+ });
31
+
32
+ it("should handle patch version differences", () => {
33
+ expect(compareVersions("1.0.5", "1.0.10")).toBe(-1);
34
+ expect(compareVersions("1.0.10", "1.0.5")).toBe(1);
35
+ });
36
+
37
+ it("should handle versions with leading zeros", () => {
38
+ expect(compareVersions("01.02.03", "1.2.3")).toBe(0);
39
+ });
40
+
41
+ it("should handle malformed versions gracefully", () => {
42
+ // Missing parts should be treated as 0
43
+ expect(compareVersions("1", "1.0.0")).toBe(0);
44
+ expect(compareVersions("1.2", "1.2.0")).toBe(0);
45
+ });
46
+
47
+ it("should handle real-world version scenarios", () => {
48
+ // Bug fix scenario
49
+ expect(compareVersions("2.2.0", "2.0.22")).toBe(1); // 2.2.0 is newer than 2.0.22
50
+ expect(compareVersions("2.0.22", "2.2.0")).toBe(-1);
51
+
52
+ // Update checks
53
+ expect(compareVersions("1.0.0", "1.0.1")).toBe(-1);
54
+ expect(compareVersions("1.0.0", "1.1.0")).toBe(-1);
55
+ expect(compareVersions("1.0.0", "2.0.0")).toBe(-1);
56
+ });
57
+ });
58
+
59
+ describe("isNewerVersion", () => {
60
+ describe("with patch level (default)", () => {
61
+ it("should return true for any version increase", () => {
62
+ expect(isNewerVersion("1.0.0", "1.0.1", "patch")).toBe(true);
63
+ expect(isNewerVersion("1.0.0", "1.1.0", "patch")).toBe(true);
64
+ expect(isNewerVersion("1.0.0", "2.0.0", "patch")).toBe(true);
65
+ });
66
+
67
+ it("should return false when current is same or newer", () => {
68
+ expect(isNewerVersion("1.0.0", "1.0.0", "patch")).toBe(false);
69
+ expect(isNewerVersion("1.0.1", "1.0.0", "patch")).toBe(false);
70
+ expect(isNewerVersion("2.0.0", "1.0.0", "patch")).toBe(false);
71
+ });
72
+
73
+ it("should use patch level as default", () => {
74
+ expect(isNewerVersion("1.0.0", "1.0.1")).toBe(true);
75
+ expect(isNewerVersion("1.0.0", "1.0.0")).toBe(false);
76
+ });
77
+ });
78
+
79
+ describe("with minor level", () => {
80
+ it("should return true for major or minor bumps", () => {
81
+ expect(isNewerVersion("1.0.0", "1.1.0", "minor")).toBe(true);
82
+ expect(isNewerVersion("1.0.0", "2.0.0", "minor")).toBe(true);
83
+ });
84
+
85
+ it("should return false for patch-only bumps", () => {
86
+ expect(isNewerVersion("1.0.0", "1.0.1", "minor")).toBe(false);
87
+ });
88
+
89
+ it("should return false when current is same or newer", () => {
90
+ expect(isNewerVersion("1.1.0", "1.1.0", "minor")).toBe(false);
91
+ expect(isNewerVersion("1.1.0", "1.0.0", "minor")).toBe(false);
92
+ expect(isNewerVersion("2.0.0", "1.5.0", "minor")).toBe(false);
93
+ });
94
+ });
95
+
96
+ describe("with major level", () => {
97
+ it("should return true only for major bumps", () => {
98
+ expect(isNewerVersion("1.0.0", "2.0.0", "major")).toBe(true);
99
+ expect(isNewerVersion("1.5.9", "2.0.0", "major")).toBe(true);
100
+ });
101
+
102
+ it("should return false for minor or patch bumps", () => {
103
+ expect(isNewerVersion("1.0.0", "1.1.0", "major")).toBe(false);
104
+ expect(isNewerVersion("1.0.0", "1.0.1", "major")).toBe(false);
105
+ });
106
+
107
+ it("should return false when current is same or newer", () => {
108
+ expect(isNewerVersion("1.0.0", "1.0.0", "major")).toBe(false);
109
+ expect(isNewerVersion("2.0.0", "1.0.0", "major")).toBe(false);
110
+ });
111
+ });
112
+
113
+ describe("real-world scenarios", () => {
114
+ it("should handle the GitHub issue scenario", () => {
115
+ // From the issue: latest is 2.0.22, current is 2.2.0
116
+ expect(isNewerVersion("2.2.0", "2.0.22", "patch")).toBe(false);
117
+ expect(isNewerVersion("2.2.0", "2.0.22", "minor")).toBe(false);
118
+ expect(isNewerVersion("2.2.0", "2.0.22", "major")).toBe(false);
119
+ });
120
+
121
+ it("should handle typical update scenarios", () => {
122
+ // App is on 1.2.0, store has 1.2.1
123
+ expect(isNewerVersion("1.2.0", "1.2.1", "patch")).toBe(true);
124
+ expect(isNewerVersion("1.2.0", "1.2.1", "minor")).toBe(false);
125
+ expect(isNewerVersion("1.2.0", "1.2.1", "major")).toBe(false);
126
+
127
+ // App is on 1.2.0, store has 1.3.0
128
+ expect(isNewerVersion("1.2.0", "1.3.0", "patch")).toBe(true);
129
+ expect(isNewerVersion("1.2.0", "1.3.0", "minor")).toBe(true);
130
+ expect(isNewerVersion("1.2.0", "1.3.0", "major")).toBe(false);
131
+
132
+ // App is on 1.2.0, store has 2.0.0
133
+ expect(isNewerVersion("1.2.0", "2.0.0", "patch")).toBe(true);
134
+ expect(isNewerVersion("1.2.0", "2.0.0", "minor")).toBe(true);
135
+ expect(isNewerVersion("1.2.0", "2.0.0", "major")).toBe(true);
136
+ });
137
+
138
+ it("should correctly identify when app is already up to date", () => {
139
+ expect(isNewerVersion("1.5.0", "1.4.0")).toBe(false);
140
+ expect(isNewerVersion("2.0.0", "1.9.9")).toBe(false);
141
+ expect(isNewerVersion("3.0.0", "2.99.99")).toBe(false);
142
+ });
143
+ });
144
+ });
145
+ });
package/src/index.ts ADDED
@@ -0,0 +1,179 @@
1
+ import { NitroModules } from "react-native-nitro-modules";
2
+ import type { UpdateLevel } from "./semver";
3
+ import { compareVersions, isNewerVersion } from "./semver";
4
+ import type { VersionCheck as VersionCheckType } from "./specs/Version.nitro";
5
+
6
+ const HybridVersionCheck = NitroModules.createHybridObject<VersionCheckType>("VersionCheck");
7
+
8
+ // Cached at module init — plain JS values, no JSI overhead on repeated access.
9
+ const version = HybridVersionCheck.version;
10
+ const buildNumber = HybridVersionCheck.buildNumber;
11
+ const packageName = HybridVersionCheck.packageName;
12
+ const installSource = HybridVersionCheck.installSource;
13
+
14
+ /**
15
+ * All version-check APIs in one object.
16
+ *
17
+ * Provides access to app version information, store URLs, and update checking.
18
+ * Sync properties are cached at module init for zero native overhead.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * VersionCheck.version // "1.2.0"
23
+ * VersionCheck.buildNumber // "42"
24
+ * VersionCheck.packageName // "com.example.app"
25
+ * VersionCheck.getCountry() // "US"
26
+ *
27
+ * const url = await VersionCheck.getStoreUrl();
28
+ * if (await VersionCheck.needsUpdate()) {
29
+ * Linking.openURL(url);
30
+ * }
31
+ * ```
32
+ */
33
+ export const VersionCheck = {
34
+ /**
35
+ * The current app version string.
36
+ *
37
+ * Read from `CFBundleShortVersionString` on iOS and `versionName` on Android.
38
+ * Cached at module init, so repeated reads have zero native overhead.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * VersionCheck.version // "1.2.0"
43
+ * ```
44
+ */
45
+ version,
46
+ /**
47
+ * The current build number.
48
+ *
49
+ * Read from `CFBundleVersion` on iOS and `versionCode` on Android.
50
+ * Cached at module init.
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * VersionCheck.buildNumber // "42"
55
+ * ```
56
+ */
57
+ buildNumber,
58
+ /**
59
+ * The app's unique identifier.
60
+ *
61
+ * This is the Bundle ID on iOS and the Application ID on Android.
62
+ * Cached at module init.
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * VersionCheck.packageName // "com.example.app"
67
+ * ```
68
+ */
69
+ packageName,
70
+ /**
71
+ * Where the app was installed from, or `undefined` for dev/sideloaded builds.
72
+ *
73
+ * - iOS: `"appstore"` | `"testflight"` | `undefined`
74
+ * - Android: `"playstore"` | `undefined`
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * if (VersionCheck.installSource === "testflight") {
79
+ * // running a TestFlight build
80
+ * }
81
+ * ```
82
+ */
83
+ installSource,
84
+ /**
85
+ * Returns the device's current 2-letter ISO country code.
86
+ *
87
+ * This is a synchronous Nitro call, no `await` needed.
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * VersionCheck.getCountry() // "US"
92
+ * ```
93
+ */
94
+ getCountry: () => HybridVersionCheck.getCountry(),
95
+ /**
96
+ * Returns the App Store (iOS) or Play Store (Android) URL for this app.
97
+ *
98
+ * @param options - Optional configuration
99
+ * @param options.countryCode - 2-letter ISO country code (e.g., "US", "GB")
100
+ * Defaults to the device's current country from `getCountry()`.
101
+ * Only used on iOS; ignored on Android.
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * const url = await VersionCheck.getStoreUrl();
106
+ * const urlUS = await VersionCheck.getStoreUrl({ countryCode: "US" });
107
+ * Linking.openURL(url);
108
+ * ```
109
+ */
110
+ getStoreUrl: async (options?: { countryCode?: string }): Promise<string> => {
111
+ return HybridVersionCheck.getStoreUrl(options?.countryCode);
112
+ },
113
+ /**
114
+ * Fetches the latest version of this app available in the store.
115
+ *
116
+ * Queries the iTunes API on iOS and the Play Store on Android.
117
+ * On iOS, uses the device's current country code by default but can be overridden.
118
+ *
119
+ * @param options - Optional configuration
120
+ * @param options.countryCode - 2-letter ISO country code (e.g., "US", "GB")
121
+ * Defaults to the device's current country from `getCountry()`.
122
+ * If the device region changes, the next call will use the new country.
123
+ * Only used on iOS; ignored on Android.
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * const latest = await VersionCheck.getLatestVersion(); // Uses current device country
128
+ * const latestUS = await VersionCheck.getLatestVersion({ countryCode: "US" });
129
+ * const latestGB = await VersionCheck.getLatestVersion({ countryCode: "GB" });
130
+ * ```
131
+ */
132
+ getLatestVersion: async (options?: { countryCode?: string }): Promise<string> => {
133
+ return HybridVersionCheck.getLatestVersion(options?.countryCode);
134
+ },
135
+ /**
136
+ * Checks whether an app update is available by comparing the current
137
+ * version against the latest store version.
138
+ *
139
+ * Uses semantic version comparison. By default checks for any version
140
+ * increase, but you can filter by granularity:
141
+ *
142
+ * - `"major"` — only returns `true` for major bumps (1.x → 2.x)
143
+ * - `"minor"` — returns `true` for major or minor bumps
144
+ * - `"patch"` — returns `true` for any version increase (default)
145
+ *
146
+ * @example
147
+ * ```ts
148
+ * if (await VersionCheck.needsUpdate()) {
149
+ * const url = await VersionCheck.getStoreUrl();
150
+ * Linking.openURL(url);
151
+ * }
152
+ *
153
+ * // Only prompt for major updates
154
+ * const majorUpdate = await VersionCheck.needsUpdate({ level: "major" });
155
+ * ```
156
+ */
157
+ needsUpdate: async (options?: { level?: UpdateLevel }): Promise<boolean> => {
158
+ const latest = await HybridVersionCheck.getLatestVersion();
159
+ return isNewerVersion(version, latest, options?.level ?? "patch");
160
+ },
161
+ /**
162
+ * Compares two semantic version strings.
163
+ *
164
+ * @returns -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * VersionCheck.compareVersions('1.0.0', '1.0.1') // -1
169
+ * VersionCheck.compareVersions('2.0.0', '2.0.0') // 0
170
+ * VersionCheck.compareVersions('3.0.0', '2.9.9') // 1
171
+ *
172
+ * if (VersionCheck.compareVersions(currentVersion, minimumVersion) < 0) {
173
+ * // Current version is below minimum — force update
174
+ * }
175
+ * ```
176
+ */
177
+ compareVersions,
178
+ } as const;
179
+ export type { UpdateLevel };
package/src/semver.ts ADDED
@@ -0,0 +1,54 @@
1
+ export type UpdateLevel = "major" | "minor" | "patch";
2
+
3
+ type SemVer = [major: number, minor: number, patch: number];
4
+
5
+ function parseVersion(version: string): SemVer {
6
+ const parts = version.split(".");
7
+ return [Number(parts[0]) || 0, Number(parts[1]) || 0, Number(parts[2]) || 0];
8
+ }
9
+
10
+ /**
11
+ * Compares two version strings.
12
+ *
13
+ * @returns `-1` if `v1 < v2`, `0` if equal, `1` if `v1 > v2`
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * compareVersions("1.2.0", "1.3.0") // -1
18
+ * compareVersions("2.0.0", "1.9.9") // 1
19
+ * compareVersions("1.0.0", "1.0.0") // 0
20
+ * ```
21
+ */
22
+ export function compareVersions(v1: string, v2: string): -1 | 0 | 1 {
23
+ const a = parseVersion(v1);
24
+ const b = parseVersion(v2);
25
+
26
+ for (let i = 0; i < 3; i++) {
27
+ if (a[i]! > b[i]!) return 1;
28
+ if (a[i]! < b[i]!) return -1;
29
+ }
30
+
31
+ return 0;
32
+ }
33
+
34
+ /**
35
+ * Checks whether `latest` is newer than `current` at the given granularity.
36
+ *
37
+ * - `"major"` — only returns `true` for major bumps (1.x → 2.x)
38
+ * - `"minor"` — returns `true` for major or minor bumps
39
+ * - `"patch"` — returns `true` for any version increase (default)
40
+ */
41
+ export function isNewerVersion(current: string, latest: string, level: UpdateLevel = "patch"): boolean {
42
+ const [curMajor, curMinor, curPatch] = parseVersion(current);
43
+ const [latMajor, latMinor, latPatch] = parseVersion(latest);
44
+
45
+ if (latMajor > curMajor) return true;
46
+ if (latMajor < curMajor) return false;
47
+ if (level === "major") return false;
48
+
49
+ if (latMinor > curMinor) return true;
50
+ if (latMinor < curMinor) return false;
51
+ if (level === "minor") return false;
52
+
53
+ return latPatch > curPatch;
54
+ }
@@ -0,0 +1,16 @@
1
+ import type { HybridObject } from "react-native-nitro-modules";
2
+
3
+ export interface VersionCheck
4
+ extends HybridObject<{
5
+ ios: "swift";
6
+ android: "kotlin";
7
+ }> {
8
+ readonly version: string;
9
+ readonly buildNumber: string;
10
+ readonly packageName: string;
11
+ readonly installSource: string | undefined;
12
+ getCountry(): string;
13
+ getStoreUrl(countryCode?: string): Promise<string>;
14
+ getLatestVersion(countryCode?: string): Promise<string>;
15
+ needsUpdate(): Promise<boolean>;
16
+ }