react-native-rollout-updater 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # react-native-rollout-updater
2
+
3
+ Check a React Native app's version against the `version.json` published by
4
+ [Rollout](../../README.md), and send the user to download the new APK. No native setup,
5
+ no extra permissions — it just uses `fetch` and React Native's built-in `Linking`.
6
+
7
+ ## Install
8
+
9
+ This package isn't published to npm — use it straight from this repo, either by copying the
10
+ `react-native-rollout-updater` folder into your app, or by pointing your app's
11
+ `package.json` at it:
12
+
13
+ ```json
14
+ {
15
+ "dependencies": {
16
+ "react-native-rollout-updater": "file:../rollout/packages/react-native-rollout-updater"
17
+ }
18
+ }
19
+ ```
20
+
21
+ Then run `npm install` (or `yarn install`) in your app.
22
+
23
+ ## Find your `versionUrl`
24
+
25
+ It's the **raw** URL of the `version.json` file in your GitHub repo:
26
+
27
+ ```
28
+ https://raw.githubusercontent.com/<owner>/<repo>/<branch>/version.json
29
+ ```
30
+
31
+ For example, for the `moohhiit/stamploop_apk` repo on `main`:
32
+
33
+ ```
34
+ https://raw.githubusercontent.com/moohhiit/stamploop_apk/main/version.json
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### Plain function
40
+
41
+ ```js
42
+ import { checkForUpdate, openUpdate } from 'react-native-rollout-updater';
43
+ import DeviceInfo from 'react-native-device-info'; // or however you track your app's version
44
+
45
+ async function checkAppUpdate() {
46
+ const result = await checkForUpdate({
47
+ versionUrl: 'https://raw.githubusercontent.com/moohhiit/stamploop_apk/main/version.json',
48
+ currentVersion: DeviceInfo.getVersion(), // e.g. "2.4.5"
49
+ });
50
+
51
+ if (result.updateAvailable) {
52
+ // show your own UI with result.latestVersion / result.releaseNotes, then:
53
+ await openUpdate(result.apkUrl); // opens the APK URL — user downloads & installs it
54
+ }
55
+ }
56
+ ```
57
+
58
+ ### React hook
59
+
60
+ ```jsx
61
+ import { useUpdateChecker, openUpdate } from 'react-native-rollout-updater';
62
+ import DeviceInfo from 'react-native-device-info';
63
+ import { Alert } from 'react-native';
64
+
65
+ function UpdateGate() {
66
+ const { loading, result } = useUpdateChecker({
67
+ versionUrl: 'https://raw.githubusercontent.com/moohhiit/stamploop_apk/main/version.json',
68
+ currentVersion: DeviceInfo.getVersion(),
69
+ });
70
+
71
+ useEffect(() => {
72
+ if (result?.updateAvailable) {
73
+ Alert.alert(
74
+ `Update available: v${result.latestVersion}`,
75
+ result.releaseNotes || 'A new version is available.',
76
+ [
77
+ { text: 'Later', style: 'cancel' },
78
+ { text: 'Update', onPress: () => openUpdate(result.apkUrl) },
79
+ ]
80
+ );
81
+ }
82
+ }, [result]);
83
+
84
+ return null;
85
+ }
86
+ ```
87
+
88
+ ## API
89
+
90
+ ### `checkForUpdate(options)`
91
+
92
+ | Option | Required | Default | Notes |
93
+ |--------------------|----------|-------------------|----------------------------------------------------------------|
94
+ | `versionUrl` | yes | — | Raw URL to `version.json` |
95
+ | `currentVersion` | yes | — | The app's currently running version |
96
+ | `versionKey` | no | `"version"` | JSON key for the version string |
97
+ | `apkUrlKey` | no | `"apkUrl"` | JSON key for the APK download URL |
98
+ | `releaseNotesKey` | no | `"releaseNotes"` | JSON key for the release notes |
99
+ | `bypassCache` | no | `true` | Appends `?t=<timestamp>` since raw.githubusercontent.com is CDN-cached for a few minutes |
100
+
101
+ Returns a promise resolving to:
102
+
103
+ ```ts
104
+ {
105
+ updateAvailable: boolean,
106
+ currentVersion: string,
107
+ latestVersion: string | null,
108
+ apkUrl: string | null,
109
+ releaseNotes: string | string[] | null,
110
+ raw: object // the full parsed version.json
111
+ }
112
+ ```
113
+
114
+ ### `openUpdate(apkUrl)`
115
+
116
+ Opens the APK URL via `Linking.openURL`. The device's browser/downloader takes over from there;
117
+ the user taps the downloaded file to install (Android will prompt to enable "install unknown
118
+ apps" for that source the first time, same as any APK downloaded from a browser).
119
+
120
+ ### `useUpdateChecker(options, deps?)`
121
+
122
+ Runs `checkForUpdate` on mount and returns `{ loading, result, error, recheck }`. Pass a `deps`
123
+ array (like `useEffect`) if you want it to re-check when something changes; call `recheck()` to
124
+ run it again manually.
125
+
126
+ ### `compareVersions(a, b)`
127
+
128
+ Exported in case you want to compare versions yourself. Returns `1` if `a > b`, `-1` if `a < b`,
129
+ `0` if equal. Compares dot-separated numeric segments (`"1.9.0"` < `"1.10.0"`), and tolerates a
130
+ leading `v`.
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "react-native-rollout-updater",
3
+ "version": "1.0.0",
4
+ "description": "Check a React Native app's version against a Rollout version.json and prompt an APK update.",
5
+ "main": "src/index.js",
6
+ "files": [
7
+ "src"
8
+ ],
9
+ "keywords": [
10
+ "react-native",
11
+ "apk",
12
+ "update",
13
+ "in-app-update",
14
+ "rollout"
15
+ ],
16
+ "peerDependencies": {
17
+ "react": "*",
18
+ "react-native": "*"
19
+ },
20
+ "license": "UNLICENSED"
21
+ }
@@ -0,0 +1,66 @@
1
+ const { compareVersions } = require('./compareVersions');
2
+
3
+ /**
4
+ * @typedef {Object} CheckForUpdateOptions
5
+ * @property {string} versionUrl - Raw URL to the version.json published by Rollout,
6
+ * e.g. "https://raw.githubusercontent.com/<owner>/<repo>/<branch>/version.json".
7
+ * @property {string} currentVersion - The running app's version (e.g. from `react-native-device-info`
8
+ * `DeviceInfo.getVersion()`, or your own constant).
9
+ * @property {string} [versionKey] - JSON key holding the version string. Defaults to "version".
10
+ * @property {string} [apkUrlKey] - JSON key holding the APK download URL. Defaults to "apkUrl".
11
+ * @property {string} [releaseNotesKey] - JSON key holding the release notes. Defaults to "releaseNotes".
12
+ * @property {boolean} [bypassCache] - Appends a cache-busting query param. Useful because
13
+ * raw.githubusercontent.com is CDN-cached for a few minutes after a deploy. Defaults to true.
14
+ */
15
+
16
+ /**
17
+ * @typedef {Object} UpdateCheckResult
18
+ * @property {boolean} updateAvailable
19
+ * @property {string} currentVersion
20
+ * @property {string|null} latestVersion
21
+ * @property {string|null} apkUrl
22
+ * @property {string|string[]|null} releaseNotes
23
+ * @property {Object} raw - The full parsed version.json.
24
+ */
25
+
26
+ /**
27
+ * Fetches version.json and compares it against the app's current version.
28
+ * @param {CheckForUpdateOptions} options
29
+ * @returns {Promise<UpdateCheckResult>}
30
+ */
31
+ async function checkForUpdate(options) {
32
+ const {
33
+ versionUrl,
34
+ currentVersion,
35
+ versionKey = 'version',
36
+ apkUrlKey = 'apkUrl',
37
+ releaseNotesKey = 'releaseNotes',
38
+ bypassCache = true
39
+ } = options || {};
40
+
41
+ if (!versionUrl) throw new Error('checkForUpdate: "versionUrl" is required.');
42
+ if (!currentVersion) throw new Error('checkForUpdate: "currentVersion" is required.');
43
+
44
+ const url = bypassCache
45
+ ? `${versionUrl}${versionUrl.includes('?') ? '&' : '?'}t=${Date.now()}`
46
+ : versionUrl;
47
+
48
+ const res = await fetch(url, { headers: { 'Cache-Control': 'no-cache' } });
49
+ if (!res.ok) throw new Error(`checkForUpdate: request failed with status ${res.status}`);
50
+ const raw = await res.json();
51
+
52
+ const latestVersion = raw[versionKey] || null;
53
+ const apkUrl = raw[apkUrlKey] || null;
54
+ const releaseNotes = raw[releaseNotesKey] || null;
55
+
56
+ return {
57
+ updateAvailable: latestVersion ? compareVersions(latestVersion, currentVersion) > 0 : false,
58
+ currentVersion,
59
+ latestVersion,
60
+ apkUrl,
61
+ releaseNotes,
62
+ raw
63
+ };
64
+ }
65
+
66
+ module.exports = { checkForUpdate };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Compares two dot-separated version strings numerically, e.g. "1.9.0" vs "1.10.0".
3
+ * Returns 1 if a > b, -1 if a < b, 0 if equal. Non-numeric/missing parts count as 0.
4
+ */
5
+ function compareVersions(a, b) {
6
+ const partsA = String(a || '0').replace(/^v/i, '').split('.');
7
+ const partsB = String(b || '0').replace(/^v/i, '').split('.');
8
+ const len = Math.max(partsA.length, partsB.length);
9
+
10
+ for (let i = 0; i < len; i++) {
11
+ const numA = parseInt(partsA[i], 10) || 0;
12
+ const numB = parseInt(partsB[i], 10) || 0;
13
+ if (numA > numB) return 1;
14
+ if (numA < numB) return -1;
15
+ }
16
+ return 0;
17
+ }
18
+
19
+ module.exports = { compareVersions };
package/src/index.js ADDED
@@ -0,0 +1,6 @@
1
+ const { checkForUpdate } = require('./checkForUpdate');
2
+ const { openUpdate } = require('./openUpdate');
3
+ const { useUpdateChecker } = require('./useUpdateChecker');
4
+ const { compareVersions } = require('./compareVersions');
5
+
6
+ module.exports = { checkForUpdate, openUpdate, useUpdateChecker, compareVersions };
@@ -0,0 +1,15 @@
1
+ const { Linking } = require('react-native');
2
+
3
+ /**
4
+ * Opens the APK download URL with the system handler (browser/downloader).
5
+ * The user finishes the install manually — this requires no extra native setup.
6
+ * @param {string} apkUrl
7
+ */
8
+ async function openUpdate(apkUrl) {
9
+ if (!apkUrl) throw new Error('openUpdate: "apkUrl" is required.');
10
+ const supported = await Linking.canOpenURL(apkUrl);
11
+ if (!supported) throw new Error(`openUpdate: cannot open URL "${apkUrl}"`);
12
+ await Linking.openURL(apkUrl);
13
+ }
14
+
15
+ module.exports = { openUpdate };
@@ -0,0 +1,35 @@
1
+ const { useEffect, useState, useCallback } = require('react');
2
+ const { checkForUpdate } = require('./checkForUpdate');
3
+
4
+ /**
5
+ * React hook that runs checkForUpdate on mount (and whenever `deps` change).
6
+ * @param {import('./checkForUpdate').CheckForUpdateOptions} options
7
+ * @param {any[]} [deps]
8
+ * @returns {{ loading: boolean, result: import('./checkForUpdate').UpdateCheckResult|null, error: Error|null, recheck: () => void }}
9
+ */
10
+ function useUpdateChecker(options, deps = []) {
11
+ const [loading, setLoading] = useState(true);
12
+ const [result, setResult] = useState(null);
13
+ const [error, setError] = useState(null);
14
+ const [tick, setTick] = useState(0);
15
+
16
+ useEffect(() => {
17
+ let cancelled = false;
18
+ setLoading(true);
19
+ setError(null);
20
+
21
+ checkForUpdate(options)
22
+ .then((r) => { if (!cancelled) setResult(r); })
23
+ .catch((e) => { if (!cancelled) setError(e); })
24
+ .finally(() => { if (!cancelled) setLoading(false); });
25
+
26
+ return () => { cancelled = true; };
27
+ // eslint-disable-next-line react-hooks/exhaustive-deps
28
+ }, [tick, ...deps]);
29
+
30
+ const recheck = useCallback(() => setTick((t) => t + 1), []);
31
+
32
+ return { loading, result, error, recheck };
33
+ }
34
+
35
+ module.exports = { useUpdateChecker };