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 +130 -0
- package/package.json +21 -0
- package/src/checkForUpdate.js +66 -0
- package/src/compareVersions.js +19 -0
- package/src/index.js +6 -0
- package/src/openUpdate.js +15 -0
- package/src/useUpdateChecker.js +35 -0
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 };
|