react-native-store-version 1.4.2 → 2.0.0-beta.1
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/CHANGELOG.md +28 -0
- package/README.md +514 -43
- package/dist/checkVersion.d.ts +73 -0
- package/dist/checkVersion.d.ts.map +1 -0
- package/dist/checkVersion.js +193 -0
- package/dist/cli/check.js +38 -0
- package/dist/cli/checks/ids.js +111 -0
- package/dist/cli/checks/installed.js +51 -0
- package/dist/cli/checks/peers.js +31 -0
- package/dist/cli/checks/rollout.js +141 -0
- package/dist/cli/checks/store.js +71 -0
- package/dist/cli/checks/versions.js +83 -0
- package/dist/cli/index.js +43 -0
- package/dist/cli/main.js +182 -0
- package/dist/cli/output.js +77 -0
- package/dist/cli/packages.js +40 -0
- package/dist/cli/project/android.js +60 -0
- package/dist/cli/project/expo.js +194 -0
- package/dist/cli/project/files.js +42 -0
- package/dist/cli/project/gradle.js +145 -0
- package/dist/cli/project/index.js +10 -0
- package/dist/cli/project/ios.js +190 -0
- package/dist/cli/project/jsx.js +327 -0
- package/dist/cli/project/plist.js +166 -0
- package/dist/cli/project/sources.js +86 -0
- package/dist/cli/result.js +12 -0
- package/dist/compare.d.ts +31 -0
- package/dist/compare.d.ts.map +1 -0
- package/dist/compare.js +68 -0
- package/dist/detect.d.ts +68 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +103 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +23 -0
- package/dist/fetchers/android.d.ts +53 -0
- package/dist/fetchers/android.d.ts.map +1 -0
- package/dist/fetchers/android.js +91 -0
- package/dist/fetchers/custom.d.ts +29 -0
- package/dist/fetchers/custom.d.ts.map +1 -0
- package/dist/fetchers/custom.js +41 -0
- package/dist/fetchers/ios.d.ts +61 -0
- package/dist/fetchers/ios.d.ts.map +1 -0
- package/dist/fetchers/ios.js +79 -0
- package/dist/index.d.ts +15 -14
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -58
- package/dist/openStore.d.ts +29 -0
- package/dist/openStore.d.ts.map +1 -0
- package/dist/openStore.js +46 -0
- package/dist/policy.d.ts +75 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +128 -0
- package/dist/react/DefaultForceModal.d.ts +26 -0
- package/dist/react/DefaultForceModal.d.ts.map +1 -0
- package/dist/react/DefaultForceModal.js +58 -0
- package/dist/react/DefaultWarningSheet.d.ts +26 -0
- package/dist/react/DefaultWarningSheet.d.ts.map +1 -0
- package/dist/react/DefaultWarningSheet.js +66 -0
- package/dist/react/StoreVersionModal.d.ts +52 -0
- package/dist/react/StoreVersionModal.d.ts.map +1 -0
- package/dist/react/StoreVersionModal.js +250 -0
- package/dist/react/StoreVersionProvider.d.ts +51 -0
- package/dist/react/StoreVersionProvider.d.ts.map +1 -0
- package/dist/react/StoreVersionProvider.js +69 -0
- package/dist/react/defaultUi.d.ts +72 -0
- package/dist/react/defaultUi.d.ts.map +1 -0
- package/dist/react/defaultUi.js +118 -0
- package/dist/react/engine.d.ts +146 -0
- package/dist/react/engine.d.ts.map +1 -0
- package/dist/react/engine.js +541 -0
- package/dist/react/texts.d.ts +43 -0
- package/dist/react/texts.d.ts.map +1 -0
- package/dist/react/texts.js +57 -0
- package/dist/react/useIsomorphicLayoutEffect.d.ts +3 -0
- package/dist/react/useIsomorphicLayoutEffect.d.ts.map +1 -0
- package/dist/react/useIsomorphicLayoutEffect.js +6 -0
- package/dist/react/useStoreVersion.d.ts +121 -0
- package/dist/react/useStoreVersion.d.ts.map +1 -0
- package/dist/react/useStoreVersion.js +23 -0
- package/dist/request.d.ts +78 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +227 -0
- package/dist/resolve.d.ts +98 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +218 -0
- package/dist/texts/ja.d.ts +8 -0
- package/dist/texts/ja.d.ts.map +1 -0
- package/dist/texts/ja.js +18 -0
- package/dist/vendor/compare-versions.d.ts +52 -0
- package/dist/vendor/compare-versions.d.ts.map +1 -0
- package/dist/vendor/compare-versions.js +111 -0
- package/package.json +83 -44
- package/src/checkVersion.ts +273 -0
- package/src/cli/check.ts +59 -0
- package/src/cli/checks/ids.ts +163 -0
- package/src/cli/checks/installed.ts +75 -0
- package/src/cli/checks/peers.ts +34 -0
- package/src/cli/checks/rollout.ts +183 -0
- package/src/cli/checks/store.ts +105 -0
- package/src/cli/checks/versions.ts +108 -0
- package/src/cli/index.ts +51 -0
- package/src/cli/main.ts +229 -0
- package/src/cli/output.ts +122 -0
- package/src/cli/packages.ts +43 -0
- package/src/cli/project/android.ts +96 -0
- package/src/cli/project/expo.ts +232 -0
- package/src/cli/project/files.ts +56 -0
- package/src/cli/project/gradle.ts +181 -0
- package/src/cli/project/index.ts +18 -0
- package/src/cli/project/ios.ts +284 -0
- package/src/cli/project/jsx.ts +354 -0
- package/src/cli/project/plist.ts +179 -0
- package/src/cli/project/sources.ts +118 -0
- package/src/cli/result.ts +34 -0
- package/src/compare.ts +101 -0
- package/src/detect.ts +178 -0
- package/src/errors.ts +63 -0
- package/src/fetchers/android.ts +136 -0
- package/src/fetchers/custom.ts +84 -0
- package/src/fetchers/ios.ts +160 -0
- package/src/index.ts +37 -0
- package/src/openStore.ts +74 -0
- package/src/policy.ts +223 -0
- package/src/react/DefaultForceModal.tsx +100 -0
- package/src/react/DefaultWarningSheet.tsx +115 -0
- package/src/react/StoreVersionModal.tsx +365 -0
- package/src/react/StoreVersionProvider.tsx +149 -0
- package/src/react/defaultUi.tsx +205 -0
- package/src/react/engine.ts +768 -0
- package/src/react/texts.ts +119 -0
- package/src/react/useIsomorphicLayoutEffect.ts +8 -0
- package/src/react/useStoreVersion.ts +153 -0
- package/src/request.ts +328 -0
- package/src/resolve.ts +388 -0
- package/src/texts/ja.ts +26 -0
- package/src/vendor/compare-versions.ts +128 -0
- package/texts/ja.d.ts +2 -0
- package/texts/ja.js +3 -0
- package/.eslintrc.js +0 -51
- package/.github/FUNDING.yml +0 -3
- package/.github/workflows/pr.yml +0 -48
- package/.github/workflows/release.yml +0 -259
- package/.prettierrc.js +0 -6
- package/babel.config.js +0 -3
- package/dist/android.d.ts +0 -1
- package/dist/android.js +0 -17
- package/dist/ios.d.ts +0 -1
- package/dist/ios.js +0 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## v2.0.0
|
|
4
|
+
|
|
5
|
+
A rewrite. Drop in `<StoreVersionProvider>` and the app shows a force-update modal or a dismissible "update available" sheet on its own; the v1 `checkVersion()` / `compareVersion()` keep working. Still JavaScript only, now with **zero runtime dependencies**. See [MIGRATION.md](./MIGRATION.md) for upgrading and [README.md](./README.md) for the full guide.
|
|
6
|
+
|
|
7
|
+
### New
|
|
8
|
+
|
|
9
|
+
- `StoreVersionProvider`: checks the store on every launch without blocking rendering, reads the app version and ID automatically (`expo-application` or `react-native-device-info`, both optional), and never shows UI on failure (errors go to `onError`). Props: `policy`, `country`, `timeoutMs`, `recheckOnForeground`, `enabled`, `getLatestVersion`, `getMinimumVersion`, `onResult`, `onError`, `texts`, `icon`, `components`, `primaryColor`, `ui`, plus `version` / `bundleId` / `packageName` overrides.
|
|
10
|
+
- `useStoreVersion()`: `status`, `result` / `detail` (same values as v1), `action`, `diff`, `dismissed`, `local`, `remote`, `storeUrl`, `releaseNotes`, `openStore()`, `dismiss()`, `recheck()`, `error`. Usable anywhere under the Provider without extra requests.
|
|
11
|
+
- Default UI: a force card (not dismissible) and a warning bottom sheet ("Later" lasts until the next launch), light / dark, accessible, scrolls with large fonts. `policy` decides which one to show (default: force on major, warning on minor / patch; also `'force'` or a function).
|
|
12
|
+
- `texts` with `{remote}` / `{local}` placeholders, a Japanese preset at `react-native-store-version/texts/ja`, `icon`, `primaryColor`, and `components` to replace the card / sheet contents.
|
|
13
|
+
- `getLatestVersion` / `getMinimumVersion`: use Firebase Remote Config, a JSON file or your API instead of (or alongside) the store. A minimum version forces updates regardless of the policy.
|
|
14
|
+
- `StoreVersionError` with a `code` (`TIMEOUT`, `NETWORK`, `NOT_FOUND`, `PARSE_FAILED`, `INVALID_VERSION`, ...).
|
|
15
|
+
- CLI `react-native-store-version check` (Node 22.14+) and a GitHub Action (`uses: watanabeyu/react-native-store-version@v2` once 2.0.0 is released) that verify an app's setup: installation (C1), bundle ID / package detection (C2), store availability (C3), native version consistency (C4), auto-detection peers (C7) and the Android staged-rollout note (C8).
|
|
16
|
+
- `checkVersion`: all arguments optional (omitted ones are detected), new `timeoutMs`.
|
|
17
|
+
- Android: a more robust version extraction from the Play page. More version formats are read (`5`, `1.0.0-beta.1`, `v2.1`), and an old version elsewhere on the page is no longer picked up by mistake.
|
|
18
|
+
|
|
19
|
+
### Breaking changes
|
|
20
|
+
|
|
21
|
+
- Requirements: React Native `>=0.73` and React `>=18` (peer dependencies; React 18 / 19 supported), TypeScript `>=5.0` in your app, Expo SDK `>=52`, Node.js `>=22.14` for the CLI.
|
|
22
|
+
- The package is ESM (`"type": "module"`) with an `exports` map. Only the root, `./texts/ja` and `./package.json` are exported; deep imports (`react-native-store-version/dist/...`) are no longer supported (they fail where `exports` is honored, and are internal where older tooling still resolves them). `dist/ios` / `dist/android` (`getIOSVersion` / `getAndroidVersion`) no longer exist.
|
|
23
|
+
- `compareVersion` follows compare-versions 6.1.1 (vendored) instead of 4.1.x: prerelease edge cases (`1.0.0-alpha` vs `1.0.0-alpha.0` is now `equal`) and versions with a leading `^` / `~` / `<` / `>` / `=` / `V` are now compared by their numbers.
|
|
24
|
+
- `checkVersion` on Android returns versions v1 rejected (`5`, `1.0.0-beta.1`, `v2.1`), so `remote` isn't limited to digits and dots. With omitted arguments it detects them instead of throwing; with a detected bundle ID on iOS the messages are `can't get ios version` (non-200 response, `results[0]` is null, or its `version` is not a string) and `appID(<bundleId>) is not released.`. `fetch` now receives a `signal`, and concurrent identical requests share one fetch. A `timeoutMs` expiry rejects with `StoreVersionError` (`code: 'TIMEOUT'`).
|
|
25
|
+
- The default export remains, but the named import `{ checkVersion }` is recommended.
|
|
26
|
+
|
|
27
|
+
### Planned
|
|
28
|
+
|
|
29
|
+
- `checkVersion` is planned to be updated in 2.1.0 to follow the rest of v2. This may include breaking changes.
|
|
30
|
+
|
|
3
31
|
## v1.4.2
|
|
4
32
|
|
|
5
33
|
- Changed the publish flow only (npm Trusted Publishing with provenance). No changes to the library code; no impact on users.
|
package/README.md
CHANGED
|
@@ -1,70 +1,541 @@
|
|
|
1
1
|
# react-native-store-version
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Check the App Store / Google Play version of your React Native / Expo app and show a **force update** or a dismissible **update available** prompt. JavaScript only, no native code, zero runtime dependencies.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { StoreVersionProvider } from 'react-native-store-version';
|
|
7
|
+
|
|
8
|
+
export default function Root() {
|
|
9
|
+
return (
|
|
10
|
+
<StoreVersionProvider>
|
|
11
|
+
<App />
|
|
12
|
+
</StoreVersionProvider>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
That is all. On launch, the Provider reads the app's version and bundle ID / package name (via `expo-application` or `react-native-device-info`), asks the store for the latest version, and shows a modal when the app is behind: a force-update card for a **major** difference and a dismissible bottom sheet for **minor** / **patch**. Your app renders immediately and keeps working if the check fails.
|
|
5
18
|
|
|
6
19
|
I've only been updating occasionally, but I'd be happy to sponsor you to keep me motivated.
|
|
7
20
|
https://github.com/sponsors/watanabeyu
|
|
8
21
|
|
|
22
|
+
Upgrading from v1? See [MIGRATION.md](./MIGRATION.md). Release notes are in [CHANGELOG.md](./CHANGELOG.md).
|
|
23
|
+
|
|
24
|
+
## Table of contents
|
|
25
|
+
|
|
26
|
+
- [Installation](#installation)
|
|
27
|
+
- [Requirements](#requirements)
|
|
28
|
+
- [How it works](#how-it-works)
|
|
29
|
+
- [Policy: force, warning or nothing](#policy-force-warning-or-nothing)
|
|
30
|
+
- [Customizing the UI](#customizing-the-ui)
|
|
31
|
+
- [Your own version source](#your-own-version-source)
|
|
32
|
+
- [`useStoreVersion` hook](#usestoreversion-hook)
|
|
33
|
+
- [Provider props](#provider-props)
|
|
34
|
+
- [Errors](#errors)
|
|
35
|
+
- [Notes and caveats](#notes-and-caveats)
|
|
36
|
+
- [CI check](#ci-check)
|
|
37
|
+
- [Performance](#performance)
|
|
38
|
+
- [`checkVersion` / `compareVersion`](#checkversion--compareversion)
|
|
39
|
+
- [Example app](#example-app)
|
|
40
|
+
- [Contributing](#contributing)
|
|
41
|
+
- [Credits and license](#credits-and-license)
|
|
42
|
+
|
|
9
43
|
## Installation
|
|
10
44
|
|
|
11
45
|
```bash
|
|
12
|
-
|
|
46
|
+
npm install react-native-store-version
|
|
47
|
+
# or
|
|
48
|
+
pnpm add react-native-store-version
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
To let the Provider read the app version and ID automatically, have one of these in your app (both are optional peer dependencies; nothing else is required):
|
|
52
|
+
|
|
53
|
+
| App | Package |
|
|
54
|
+
| ----------------- | -------------------------------------------------------- |
|
|
55
|
+
| Expo | `expo-application` (`npx expo install expo-application`) |
|
|
56
|
+
| Bare React Native | `react-native-device-info` |
|
|
57
|
+
|
|
58
|
+
If neither is installed, pass `version`, `bundleId` and `packageName` to the Provider yourself (see [Provider props](#provider-props)).
|
|
59
|
+
|
|
60
|
+
## Requirements
|
|
61
|
+
|
|
62
|
+
| | Supported |
|
|
63
|
+
| --------------------------- | --------- |
|
|
64
|
+
| React Native | `>=0.73` |
|
|
65
|
+
| React | 18 / 19 |
|
|
66
|
+
| Expo SDK | `>=52` |
|
|
67
|
+
| TypeScript (your app) | `>=5.0` |
|
|
68
|
+
| Node.js (CI check CLI only) | `>=22.14` |
|
|
69
|
+
|
|
70
|
+
The library runs anywhere React Native runs except **Expo Go**: inside Expo Go the Provider does nothing (`status: 'skipped'`) because the detected bundle ID belongs to Expo Go itself. Use a development build to try it. It also does nothing on web and on platforms other than iOS / Android.
|
|
71
|
+
|
|
72
|
+
## How it works
|
|
73
|
+
|
|
74
|
+
- **Checks on every launch.** Nothing is cached or persisted. The check runs when the Provider mounts (and again on `recheck()`, when `enabled` turns true, or on foreground if you opt in with `recheckOnForeground`).
|
|
75
|
+
- **Never blocks your app.** Children render right away. The modal is layered on top once the result is known.
|
|
76
|
+
- **Fails quietly.** Timeouts (default 10 s) and errors never show UI; they go to `onError` (and to `console.warn` in development if you don't pass `onError`).
|
|
77
|
+
- **Sources.** iOS uses the iTunes Lookup API by bundle ID. Android reads the Google Play web page by package name (about 1.25 MB per check). You can replace both with [your own source](#your-own-version-source).
|
|
78
|
+
- **"Later" lasts until the next launch.** Dismissing the warning is kept in memory only, so it shows again next time the app starts (or sooner, if a newer version appears on a recheck).
|
|
79
|
+
- **Store country.** Defaults to the device locale's region (falls back to `us`). Set `country` if your app is published elsewhere.
|
|
80
|
+
|
|
81
|
+
## Policy: force, warning or nothing
|
|
82
|
+
|
|
83
|
+
The `policy` prop maps the kind of difference to an action. The default is:
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
<StoreVersionProvider policy={{ major: 'force', minor: 'warning', patch: 'warning' }}>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Three forms are accepted:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
// one action for every difference (same feeling as v1's result === 'new')
|
|
93
|
+
<StoreVersionProvider policy="force">
|
|
94
|
+
|
|
95
|
+
// per kind; omitted keys use the default
|
|
96
|
+
<StoreVersionProvider policy={{ minor: 'force' }}>
|
|
97
|
+
|
|
98
|
+
// a function
|
|
99
|
+
<StoreVersionProvider policy={(local, remote) => (remote.startsWith('3.') ? 'force' : 'warning')}>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Order of evaluation:
|
|
103
|
+
|
|
104
|
+
1. If `getMinimumVersion` returns a version and the app is below it, the action is `force` regardless of the policy.
|
|
105
|
+
2. If the app is up to date (or newer than the store), the action is `none`.
|
|
106
|
+
3. Otherwise the kind of difference (`major` / `minor` / `patch`) picks the action from the policy.
|
|
107
|
+
|
|
108
|
+
A difference only in the 4th segment (`1.2.3.4` → `1.2.3.5`) or only in a prerelease tag (`1.2.3-beta` → `1.2.3`) counts as `patch`.
|
|
109
|
+
|
|
110
|
+
> **Warning: Android staged rollouts.**
|
|
111
|
+
> If you use a staged rollout on Google Play, users outside the rollout cannot download the new version yet, but the store page already shows it. A force update driven by the store version would lock those users out of your app with no way to update.
|
|
112
|
+
> On Android, use `major: 'warning'` (for example `policy={{ major: Platform.OS === 'android' ? 'warning' : 'force' }}`), or decide force updates with [`getMinimumVersion`](#your-own-version-source) instead of the store version.
|
|
113
|
+
> iOS phased releases are not affected: they only throttle _automatic_ updates, and anyone can update manually from the App Store at any time.
|
|
114
|
+
|
|
115
|
+
## Customizing the UI
|
|
116
|
+
|
|
117
|
+
Three levels, from the smallest change to full control.
|
|
118
|
+
|
|
119
|
+
### 1. Texts
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
<StoreVersionProvider
|
|
123
|
+
texts={{
|
|
124
|
+
force: { title: 'Update required', update: 'Update now' },
|
|
125
|
+
warning: { later: 'Not now' },
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
13
128
|
```
|
|
14
129
|
|
|
15
|
-
|
|
130
|
+
Keys you don't set fall back to the English defaults (merged per `force` / `warning`). `{remote}` and `{local}` are replaced with the latest / current version when shown.
|
|
131
|
+
|
|
132
|
+
| Key | Default |
|
|
133
|
+
| ----------------- | ------------------------------------------------------------------------------- |
|
|
134
|
+
| `force.title` | Update required |
|
|
135
|
+
| `force.message` | A new version ({remote}) is available. Please update to continue using the app. |
|
|
136
|
+
| `force.update` | Update |
|
|
137
|
+
| `warning.title` | Update available |
|
|
138
|
+
| `warning.message` | Version {remote} is now available. |
|
|
139
|
+
| `warning.update` | Update |
|
|
140
|
+
| `warning.later` | Later |
|
|
141
|
+
|
|
142
|
+
A Japanese preset is included:
|
|
16
143
|
|
|
17
|
-
|
|
144
|
+
```tsx
|
|
145
|
+
import ja from 'react-native-store-version/texts/ja';
|
|
146
|
+
|
|
147
|
+
<StoreVersionProvider texts={ja}>
|
|
148
|
+
|
|
149
|
+
// start from the preset and change one key
|
|
150
|
+
<StoreVersionProvider texts={{ ...ja, force: { ...ja.force, title: '更新してください' } }}>
|
|
151
|
+
```
|
|
18
152
|
|
|
19
|
-
|
|
153
|
+
Changing `texts` updates a modal that is already on screen, so it follows an in-app language switch. To use an i18n library, put a small wrapper inside its provider:
|
|
20
154
|
|
|
21
155
|
```tsx
|
|
22
|
-
import
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
156
|
+
import { useTranslation } from 'react-i18next';
|
|
157
|
+
|
|
158
|
+
function AppStoreVersionProvider({ children }: { children: React.ReactNode }) {
|
|
159
|
+
const { t } = useTranslation();
|
|
160
|
+
return (
|
|
161
|
+
<StoreVersionProvider
|
|
162
|
+
texts={{
|
|
163
|
+
force: {
|
|
164
|
+
title: t('update.force.title'),
|
|
165
|
+
message: t('update.force.message'),
|
|
166
|
+
update: t('update.button'),
|
|
167
|
+
},
|
|
168
|
+
warning: {
|
|
169
|
+
title: t('update.warning.title'),
|
|
170
|
+
message: t('update.warning.message'),
|
|
171
|
+
update: t('update.button'),
|
|
172
|
+
later: t('update.later'),
|
|
173
|
+
},
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
{children}
|
|
177
|
+
</StoreVersionProvider>
|
|
178
|
+
);
|
|
45
179
|
}
|
|
46
180
|
```
|
|
47
181
|
|
|
48
|
-
|
|
182
|
+
### Icon and button color
|
|
49
183
|
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
184
|
+
```tsx
|
|
185
|
+
<StoreVersionProvider
|
|
186
|
+
icon={require('./assets/icon.png')} // shown above the title (force) / at the top left (warning); omitted = no icon
|
|
187
|
+
primaryColor="#E11D48" // the "Update" button fill and the "Later" label; same in light and dark
|
|
188
|
+
>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The app icon isn't read automatically (it can't be done from JavaScript reliably on both platforms), so pass it as an image source. `primaryColor` applies to both color schemes; if you want a different color per scheme, pass a value you picked with `useColorScheme()` (or `DynamicColorIOS` on iOS). Keep it readable on white (button label) and on the dark sheet background.
|
|
192
|
+
|
|
193
|
+
### 2. Components
|
|
194
|
+
|
|
195
|
+
Replace the contents of the force card and / or the warning sheet. The library still owns the modal, the backdrop, the placement (force: centered with 24 px side margins; warning: full width at the bottom), the back-button / backdrop-tap behavior, and the open / close animation. Your component draws the surface itself (background, radius, shadow, max width).
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
import type { StoreVersionForceProps, StoreVersionWarningProps } from 'react-native-store-version';
|
|
199
|
+
|
|
200
|
+
function MyForce({ texts, local, remote, releaseNotes, openStore }: StoreVersionForceProps) {
|
|
201
|
+
return (
|
|
202
|
+
<View style={styles.card}>
|
|
203
|
+
<Text accessibilityRole="header">{texts.title}</Text>
|
|
204
|
+
<Text>{texts.message}</Text>
|
|
205
|
+
<Text>{local} → {remote ?? '?'}</Text>
|
|
206
|
+
{releaseNotes ? <Text numberOfLines={4}>{releaseNotes}</Text> : null}
|
|
207
|
+
<Button title={texts.update} onPress={() => void openStore()} />
|
|
208
|
+
</View>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function MyWarning({ texts, openStore, dismiss }: StoreVersionWarningProps) {
|
|
213
|
+
return (
|
|
214
|
+
<View style={styles.sheet}>
|
|
215
|
+
<Text accessibilityRole="header">{texts.title}</Text>
|
|
216
|
+
<Text>{texts.message}</Text>
|
|
217
|
+
<Button title={texts.later} onPress={dismiss} />
|
|
218
|
+
<Button title={texts.update} onPress={() => void openStore()} />
|
|
219
|
+
</View>
|
|
220
|
+
);
|
|
57
221
|
}
|
|
58
222
|
|
|
59
|
-
//
|
|
223
|
+
// define the components outside render: a new component function on each render remounts the card's contents
|
|
224
|
+
// (a new `components` object on each render is fine)
|
|
225
|
+
const components = { Force: MyForce, Warning: MyWarning };
|
|
226
|
+
|
|
227
|
+
<StoreVersionProvider components={components}>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Props: `local`, `remote` (may be `null` when only the minimum version could be compared), `storeUrl`, `releaseNotes` (iOS with the store version only; `undefined` on Android and with `getLatestVersion`), `texts` (placeholders already filled), `icon`, `openStore()`; `Warning` also gets `dismiss()`. `primaryColor` does not apply to replaced components. A full example is in [`example/src/CustomUi.tsx`](./example/src/CustomUi.tsx).
|
|
231
|
+
|
|
232
|
+
### 3. Hook only
|
|
233
|
+
|
|
234
|
+
Pass `ui={false}` and render whatever you like with [`useStoreVersion`](#usestoreversion-hook), for example a dedicated screen in your navigator.
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
<StoreVersionProvider ui={false}>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Your own version source
|
|
241
|
+
|
|
242
|
+
Instead of (or in addition to) the store, you can return versions from Firebase Remote Config, a JSON file, or your API. Both props are async functions that return a version string. `getMinimumVersion` may also return `null` / `undefined` / `''` to mean "no minimum". `getLatestVersion` must return a version: an empty value fails the check (`LATEST_VERSION_FAILED`, no UI, no fallback to the store).
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
type VersionGetter = (ctx: {
|
|
246
|
+
platform: 'ios' | 'android';
|
|
247
|
+
signal: AbortSignal; // aborted on timeout; pass it to fetch
|
|
248
|
+
}) => Promise<string | null | undefined>;
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
| Prop | Effect |
|
|
252
|
+
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
253
|
+
| `getLatestVersion` | Replaces the store version. Android never contacts Google Play; iOS still looks up the bundle ID once, only to get the store URL for the "Update" button. If this function fails, the check fails (no fallback to the store). |
|
|
254
|
+
| `getMinimumVersion` | Any app version below this is forced to update, whatever the policy says. Works with the store version or with `getLatestVersion`. If it fails, only the minimum-version rule is skipped (the error still goes to `onError`). |
|
|
255
|
+
|
|
256
|
+
### A static JSON file (GitHub Pages, S3, a CDN, ...)
|
|
257
|
+
|
|
258
|
+
```json
|
|
60
259
|
{
|
|
61
|
-
|
|
260
|
+
"ios": { "latest": "2.4.0", "minimum": "2.3.0" },
|
|
261
|
+
"android": { "latest": "2.4.0", "minimum": "2.3.0" }
|
|
62
262
|
}
|
|
63
263
|
```
|
|
64
264
|
|
|
65
|
-
|
|
66
|
-
|
|
265
|
+
```tsx
|
|
266
|
+
type VersionJson = Record<'ios' | 'android', { latest?: string; minimum?: string }>;
|
|
267
|
+
|
|
268
|
+
const fetchVersionJson = async (signal: AbortSignal): Promise<VersionJson> => {
|
|
269
|
+
const res = await fetch('https://example.com/app-version.json', { signal });
|
|
270
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
271
|
+
return res.json();
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
<StoreVersionProvider
|
|
275
|
+
getLatestVersion={async ({ platform, signal }) => (await fetchVersionJson(signal))[platform].latest}
|
|
276
|
+
getMinimumVersion={async ({ platform, signal }) => (await fetchVersionJson(signal))[platform].minimum}
|
|
277
|
+
>
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
This fetches the file twice per check. Both getters receive the **same** `signal` within one check, so you can dedupe on it:
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
const pending = new WeakMap<AbortSignal, Promise<VersionJson>>();
|
|
284
|
+
|
|
285
|
+
const fetchVersionJsonOnce = (signal: AbortSignal): Promise<VersionJson> => {
|
|
286
|
+
let promise = pending.get(signal);
|
|
287
|
+
if (!promise) {
|
|
288
|
+
promise = fetchVersionJson(signal);
|
|
289
|
+
pending.set(signal, promise);
|
|
290
|
+
}
|
|
291
|
+
return promise;
|
|
292
|
+
};
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Your own API
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
<StoreVersionProvider
|
|
299
|
+
getMinimumVersion={async ({ platform, signal }) => {
|
|
300
|
+
const res = await fetch(`https://api.example.com/app/minimum-version?platform=${platform}`, {
|
|
301
|
+
signal,
|
|
302
|
+
headers: { Accept: 'application/json' },
|
|
303
|
+
});
|
|
304
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
305
|
+
const { minimumVersion } = await res.json();
|
|
306
|
+
return minimumVersion; // '2.3.0'
|
|
307
|
+
}}
|
|
308
|
+
>
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Firebase Remote Config (`@react-native-firebase/remote-config`)
|
|
312
|
+
|
|
313
|
+
```tsx
|
|
314
|
+
import remoteConfig from '@react-native-firebase/remote-config';
|
|
315
|
+
|
|
316
|
+
<StoreVersionProvider
|
|
317
|
+
getMinimumVersion={async ({ platform }) => {
|
|
318
|
+
await remoteConfig().fetchAndActivate();
|
|
319
|
+
return remoteConfig().getValue(`minimum_version_${platform}`).asString(); // '' means "not set"
|
|
320
|
+
}}
|
|
321
|
+
>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Notes:
|
|
325
|
+
|
|
326
|
+
- Throw on failure; the error reaches `onError` (as `LATEST_VERSION_FAILED` / `MINIMUM_VERSION_FAILED` with the original error in `cause`).
|
|
327
|
+
- Pass `signal` to `fetch`. Otherwise the request keeps running after the timeout (its result is ignored).
|
|
328
|
+
- A JSON file behind a CDN is only as fresh as the CDN cache. Use a short `Cache-Control` or add a timestamp to the URL.
|
|
329
|
+
- In React `StrictMode` (development only), both getters may be called twice on mount. Requests to the stores are always deduplicated.
|
|
330
|
+
- Changing an inline getter does not trigger a new check; the check runs on mount, `recheck()`, foreground (if enabled) and when `enabled` turns true.
|
|
331
|
+
|
|
332
|
+
## `useStoreVersion` hook
|
|
333
|
+
|
|
334
|
+
Works anywhere under the Provider. The Provider holds a single result, so using the hook in many places does not add requests.
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
import { useStoreVersion } from 'react-native-store-version';
|
|
338
|
+
|
|
339
|
+
// a banner at the top of the settings screen, visible even after the warning was dismissed
|
|
340
|
+
function UpdateBanner() {
|
|
341
|
+
const { result, remote, openStore } = useStoreVersion();
|
|
342
|
+
if (result !== 'new') return null;
|
|
343
|
+
return (
|
|
344
|
+
<Pressable onPress={openStore} style={styles.banner}>
|
|
345
|
+
<Text>Version {remote} is available. Tap to update.</Text>
|
|
346
|
+
</Pressable>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// a "Version" row that shows current / latest and rechecks on tap
|
|
351
|
+
function VersionRow() {
|
|
352
|
+
const { local, remote, status, recheck } = useStoreVersion();
|
|
353
|
+
return (
|
|
354
|
+
<Pressable onPress={recheck}>
|
|
355
|
+
<Text>
|
|
356
|
+
Current {local ?? '-'} / Latest{' '}
|
|
357
|
+
{status === 'checking' ? 'checking…' : (remote ?? '-')}
|
|
358
|
+
</Text>
|
|
359
|
+
</Pressable>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
| Field | Type | Meaning |
|
|
365
|
+
| ------------------ | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
366
|
+
| `status` | `'idle' \| 'checking' \| 'ready' \| 'skipped' \| 'error'` | Progress. `skipped` = web / Expo Go / unsupported platform / `enabled={false}`. |
|
|
367
|
+
| `result` | `'new' \| 'old' \| 'equal' \| null` | Comparison, same values as v1's `checkVersion()`. Not affected by `policy`. |
|
|
368
|
+
| `detail` | `'remote > local' \| 'remote < local' \| 'remote === local' \| null` | Same as v1. |
|
|
369
|
+
| `action` | `'force' \| 'warning' \| 'none' \| null` | What the policy (and the minimum version) decided. The default UI follows this. |
|
|
370
|
+
| `diff` | `'major' \| 'minor' \| 'patch' \| null` | Kind of difference when `result === 'new'`. |
|
|
371
|
+
| `dismissed` | `boolean` | `action` is `warning` but "Later" was tapped. |
|
|
372
|
+
| `local` / `remote` | `string \| null` | Current / latest version. |
|
|
373
|
+
| `storeUrl` | `string \| null` | Store page URL. `null` on iOS while it is being looked up or if the lookup failed. |
|
|
374
|
+
| `releaseNotes` | `string \| undefined` | From the App Store, iOS only. `undefined` on Android and when `getLatestVersion` is used. |
|
|
375
|
+
| `openStore()` | `() => Promise<boolean>` | Opens the store page. Resolves `true` if it opened, `false` otherwise (never rejects; failures go to `onError`). |
|
|
376
|
+
| `dismiss()` | `() => void` | Closes the warning until the next launch (or a newer version). |
|
|
377
|
+
| `recheck()` | `() => void` | Checks again. The previous result stays visible meanwhile and is restored if the recheck fails. |
|
|
378
|
+
| `error` | `StoreVersionError \| null` | Why the check failed (only when `status === 'error'`). |
|
|
379
|
+
|
|
380
|
+
During a recheck (`status: 'checking'`) the previous `result` / `action` remain, so a force modal never disappears while rechecking. Calling the hook outside the Provider throws.
|
|
381
|
+
|
|
382
|
+
## Provider props
|
|
383
|
+
|
|
384
|
+
All props are optional.
|
|
385
|
+
|
|
386
|
+
| Prop | Default | Description |
|
|
387
|
+
| --------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
388
|
+
| `enabled` | `true` | `false` does nothing (`skipped`). Switching to `true` starts a check; switching to `false` cancels it. |
|
|
389
|
+
| `version` | auto | The app version. **Must be the native version** (`CFBundleShortVersionString` / `versionName`). Don't pass `Constants.expoConfig.version` or `package.json`'s version: they live in the JS bundle and change with OTA updates, which would hide a needed force update. Auto-detected from `expo-application` → `react-native-device-info`. |
|
|
390
|
+
| `bundleId` | auto | iOS bundle ID. |
|
|
391
|
+
| `packageName` | auto | Android package name. |
|
|
392
|
+
| `country` | device region, else `us` | App Store country for the lookup. |
|
|
393
|
+
| `policy` | `{ major: 'force', minor: 'warning', patch: 'warning' }` | See [Policy](#policy-force-warning-or-nothing). Changing it re-evaluates `action` without a new request. |
|
|
394
|
+
| `timeoutMs` | `10000` | Timeout for the whole check. On timeout: no UI, `TIMEOUT` to `onError`. |
|
|
395
|
+
| `recheckOnForeground` | `false` | Recheck when the app returns from the background (`background` → `active`). On Android this also fires after permission dialogs and other apps, and each check downloads the Play page (about 1.25 MB). |
|
|
396
|
+
| `getLatestVersion` | – | See [Your own version source](#your-own-version-source). |
|
|
397
|
+
| `getMinimumVersion` | – | See [Your own version source](#your-own-version-source). |
|
|
398
|
+
| `onResult` | – | `(r) => void`, called with `{ result, detail, action, diff, local, remote }` each time a check completes, and again when a `policy` change changes `action`. Not called for `skipped` / `error`. |
|
|
399
|
+
| `onError` | `console.warn` in dev | `(e: StoreVersionError) => void`. May be called more than once per check (for example a failed minimum-version fetch plus a failed store lookup). |
|
|
400
|
+
| `texts` | English | See [Texts](#1-texts). |
|
|
401
|
+
| `icon` | none | Image source for the default UI. |
|
|
402
|
+
| `components` | default UI | See [Components](#2-components). |
|
|
403
|
+
| `primaryColor` | blue | Button color of the default UI. |
|
|
404
|
+
| `ui` | `true` | `false` hides the default UI (hook only). |
|
|
405
|
+
|
|
406
|
+
## Errors
|
|
407
|
+
|
|
408
|
+
`onError` and `useStoreVersion().error` receive a `StoreVersionError` with a `code` (and the original error in `cause` when there is one). No code ever shows UI on its own; the app keeps working.
|
|
409
|
+
|
|
410
|
+
| `code` | When | Check result |
|
|
411
|
+
| --------------------------- | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
412
|
+
| `LOCAL_VERSION_UNAVAILABLE` | The app version / ID can't be read (no `expo-application` / `react-native-device-info` and nothing passed). | `error` |
|
|
413
|
+
| `TIMEOUT` | The latest version didn't arrive within `timeoutMs`. | `error` |
|
|
414
|
+
| `NETWORK` | HTTP / network failure, or an unreadable App Store response. | `error` |
|
|
415
|
+
| `NOT_FOUND` | The app isn't on the store (App Store returned no results / Google Play 404). | `error` |
|
|
416
|
+
| `PARSE_FAILED` | The Google Play page changed and the version couldn't be extracted. | `error` |
|
|
417
|
+
| `INVALID_VERSION` | A version can't be compared (for example Google Play's "Varies with device"). | `error` (or `ready` with `action: 'force'` if the minimum version still applies) |
|
|
418
|
+
| `LATEST_VERSION_FAILED` | `getLatestVersion` threw or returned an empty / non-string value. | `error` |
|
|
419
|
+
| `MINIMUM_VERSION_FAILED` | `getMinimumVersion` threw, returned a non-string or a version that can't be compared, or timed out. | `ready` (minimum rule skipped) |
|
|
420
|
+
| `POLICY_FAILED` | A `policy` function threw or returned something other than `force` / `warning` / `none`. | `error` |
|
|
421
|
+
| `STORE_URL_UNAVAILABLE` | The store URL couldn't be looked up or opened (`openStore()` returns `false`). | unchanged |
|
|
422
|
+
|
|
423
|
+
## Notes and caveats
|
|
424
|
+
|
|
425
|
+
- **Native version only.** See `version` in [Provider props](#provider-props). OTA updates (Expo Updates, CodePush) don't change the native version, and that is what the store compares against.
|
|
426
|
+
- **App Store cache.** The iTunes Lookup API sits behind a CDN. The library adds a timestamp to the URL and sends `cache-control: no-cache` to get fresh data, but right after you release a new version the lookup may still lag behind for a while.
|
|
427
|
+
- **Google Play is scraped.** There is no public API for the Play version, so the library reads the store's web page. The page can change without notice (the version is then reported as `PARSE_FAILED`, with no UI), the request is about 1.25 MB, and Google's terms of service may restrict automated access; review them. If you need certainty, or want to avoid contacting Google Play altogether, use `getLatestVersion` with your own source (`getMinimumVersion` alone still reads the Play page for the latest version).
|
|
428
|
+
- **Android staged rollouts** can lock users out when force is driven by the store version. See [Policy](#policy-force-warning-or-nothing).
|
|
429
|
+
- **"Later" is not persisted.** There is no storage; the warning returns on the next launch. If you need a longer snooze, use `ui={false}` and the hook with your own storage.
|
|
430
|
+
- **`recheckOnForeground` on Android** downloads the Play page on every return to the foreground.
|
|
431
|
+
- **Metro `allowOptionalDependencies`.** The optional peers are loaded with `try { require('expo-application') }`. With Metro's default settings a missing peer is fine; if your `metro.config.js` sets `transformer.allowOptionalDependencies: false`, bundling fails when a peer is missing, so install one of them or re-enable the option.
|
|
432
|
+
- **Other modals.** The default UI is a React Native `Modal` at your root. On iOS, while another `Modal` (or a native modal screen from `react-native-screens` / `expo-router`) is presented, the force / warning modal may not appear, and there is no guarantee that it appears after the other one closes (the reverse can also happen: your modal may not show while this one is open). This has not been verified on real devices yet. If a force update must never be missed, use `ui={false}` and show the hook's result in your own screen.
|
|
433
|
+
- **Warning dismissal.** The warning sheet also closes with the Android back button, a tap on the backdrop, and the VoiceOver escape gesture, all equivalent to "Later". The force card can't be dismissed.
|
|
434
|
+
- **StrictMode.** In development, `getLatestVersion` / `getMinimumVersion` may run twice on mount.
|
|
435
|
+
- **`onError` can fire more than once per check.**
|
|
436
|
+
- **Expo Go** is skipped. Use a development build.
|
|
437
|
+
|
|
438
|
+
## CI check
|
|
439
|
+
|
|
440
|
+
The package ships a CLI that checks an app's setup, and a GitHub Action that runs it after your dependencies are installed.
|
|
441
|
+
|
|
442
|
+
```bash
|
|
443
|
+
npx react-native-store-version check [--cwd <dir>] [--strict] [--ignore C4,C8] [--country us] [--json]
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
```yaml
|
|
447
|
+
jobs:
|
|
448
|
+
store-version:
|
|
449
|
+
runs-on: ubuntu-latest
|
|
450
|
+
permissions:
|
|
451
|
+
contents: read
|
|
452
|
+
steps:
|
|
453
|
+
- uses: actions/checkout@v7
|
|
454
|
+
with:
|
|
455
|
+
persist-credentials: false
|
|
456
|
+
- uses: actions/setup-node@v7
|
|
457
|
+
with:
|
|
458
|
+
node-version: 24
|
|
459
|
+
- run: npm ci
|
|
460
|
+
- uses: watanabeyu/react-native-store-version@v2
|
|
461
|
+
with:
|
|
462
|
+
working-directory: . # the app directory (monorepo: apps/mobile)
|
|
463
|
+
strict: false # true fails on warnings too
|
|
464
|
+
ignore: '' # e.g. C8
|
|
465
|
+
country: us # App Store country for C3
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
| ID | What it checks | Level |
|
|
469
|
+
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
470
|
+
| C1 | `react-native-store-version` is installed and is v2 or later. | error if missing (or, in the Action, if only v1 is installed) / info if v1 |
|
|
471
|
+
| C2 | The iOS bundle ID and Android package can be determined (Expo config, `ios/` project, `android/app/build.gradle`). | error if neither / info if only one |
|
|
472
|
+
| C3 | The app can be found on the App Store (`country`) and Google Play with those IDs. Store access from CI is unreliable, so failures stay warnings. | warning |
|
|
473
|
+
| C4 | The native versions (`MARKETING_VERSION`, `versionName`) and the Expo `version` match. Skipped when there is no `ios/` / `android/` directory (Continuous Native Generation). | warning (info if a native version can't be determined) |
|
|
474
|
+
| C7 | `expo-application` or `react-native-device-info` is installed for auto-detection. | info |
|
|
475
|
+
| C8 | Only when an Android package was found: the Provider seems to force updates from the store version alone (no `policy`, `'force'` in it, an object without `major`, or `{undefined}` / `{null}`, and no `getLatestVersion` / `getMinimumVersion` anywhere in the sources). Prints the staged-rollout note. | warning (info if the scan was cut short) |
|
|
476
|
+
|
|
477
|
+
- Exit code is 1 on any error, and also on warnings with `--strict`. Info never fails. On GitHub Actions the results are shown as annotations and in the job summary.
|
|
478
|
+
- C8 is a plain text search and can be a false positive (a policy that already uses `major: 'warning'` only on Android still contains `'force'`). Silence it with `--ignore C8` / `ignore: C8`.
|
|
479
|
+
- Node.js 22.14 or later is required for the CLI. The Action uses the Node set up in your job.
|
|
480
|
+
- **The CLI evaluates your `app.config.ts` / `app.config.js`**, which means code from the checked commit runs in the job. Treat the job as untrusted: don't use it in `pull_request_target` jobs, don't expose secrets through `env:` at the job or workflow level, keep the token read-only (`permissions: contents: read`), and don't leave credentials on disk from earlier steps (`persist-credentials: false` on checkout, no `.npmrc` with a token). The Action itself takes no token or secrets.
|
|
481
|
+
- Pin `uses:` to the major tag (`@v2`) or to a commit SHA.
|
|
482
|
+
- The Action fails (C1) when v1 is installed, because v1 has no CLI.
|
|
483
|
+
|
|
484
|
+
## Performance
|
|
485
|
+
|
|
486
|
+
Measured in the [example app](./example) on release builds, on a simulator / emulator (iPhone 17 on iOS 26.5; Pixel 3a on API 34), against a published app (GitHub, country `jp`), from mount to `onResult`, median of 5 runs after one warm-up:
|
|
487
|
+
|
|
488
|
+
| | v1 `checkVersion()` | v2 Provider (`ui={false}`, IDs given) |
|
|
489
|
+
| ------- | ------------------- | ------------------------------------- |
|
|
490
|
+
| iOS | 199 ms | 161 ms |
|
|
491
|
+
| Android | 205 ms | 198 ms |
|
|
492
|
+
|
|
493
|
+
Added bundle size for the Provider and the default UI: about 39.8 KB minified / about 11 KB gzipped (Hermes bytecode: about 75 KB more than an empty app). The library has no runtime dependencies.
|
|
494
|
+
|
|
495
|
+
## `checkVersion` / `compareVersion`
|
|
496
|
+
|
|
497
|
+
The lightest way to use the library: call `checkVersion` outside React (for example once at app start) and decide what to do with the result yourself. It returns the same values and error messages as v1 (see [MIGRATION.md](./MIGRATION.md) for the few differences).
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
import { checkVersion, compareVersion } from 'react-native-store-version';
|
|
501
|
+
|
|
502
|
+
const { local, remote, result, detail } = await checkVersion({
|
|
503
|
+
version: '1.0.0', // the native version
|
|
504
|
+
iosStoreURL: 'https://apps.apple.com/jp/app/github/id1477376905',
|
|
505
|
+
androidStoreURL:
|
|
506
|
+
'https://play.google.com/store/apps/details?id=com.github.android',
|
|
507
|
+
country: 'jp', // default 'jp' (unlike the Provider)
|
|
508
|
+
timeoutMs: 10000, // new in v2; omitted = no timeout
|
|
509
|
+
});
|
|
510
|
+
// result: 'new' | 'old' | 'equal', detail: 'remote > local' | 'remote < local' | 'remote === local'
|
|
511
|
+
|
|
512
|
+
compareVersion('1.0.0', '1.1.0'); // 'new'
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
In v2 every argument is optional; omitted ones are auto-detected like the Provider does. `checkVersion` does not skip Expo Go, so pass everything explicitly when testing there. Failures reject with an `Error` carrying the v1 message, except timeouts (`StoreVersionError`, `code: 'TIMEOUT'`).
|
|
516
|
+
|
|
517
|
+
> **Planned for 2.1.0:** `checkVersion` will be updated to follow the rest of v2. This may include breaking changes.
|
|
518
|
+
|
|
519
|
+
## Example app
|
|
520
|
+
|
|
521
|
+
[`example/`](./example) is an Expo app in this repository that switches between the store, a static JSON file and a mock API, and shows the Japanese preset, `icon`, `components`, `primaryColor`, `ui={false}`, the settings-screen banner, and the timing measurement against v1.
|
|
522
|
+
|
|
523
|
+
```bash
|
|
524
|
+
pnpm install
|
|
525
|
+
pnpm build
|
|
526
|
+
cd example
|
|
527
|
+
pnpm ios # or pnpm android
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
## Contributing
|
|
531
|
+
|
|
532
|
+
- `pnpm install --frozen-lockfile`, then `pnpm lint` (Biome), `pnpm typecheck`, `pnpm test` (Jest), `pnpm build`.
|
|
533
|
+
- Tests live in `__tests__/` mirroring `src/` (for example `src/fetchers/ios.ts` → `__tests__/fetchers/ios.test.ts`); fixtures in `__tests__/__fixtures__/`. Tests must not hit the real stores: mock `fetch` and return a fixture.
|
|
534
|
+
- UI tests use `@testing-library/react-native`. Query through `screen` (not the result of `render`), prefer `getByRole` / `findByRole` over text or test IDs, and `await` every `findBy*` / `waitFor`. Any `console.error` / `console.warn` during a test fails it.
|
|
535
|
+
- Keep the library free of native code and runtime dependencies. The CLI (`src/cli/`) may only use Node's standard modules and the library's own code.
|
|
536
|
+
|
|
537
|
+
## Credits and license
|
|
67
538
|
|
|
68
|
-
|
|
539
|
+
Version comparison is a vendored copy of [compare-versions](https://github.com/omichelsen/compare-versions) 6.1.1 by Ole Michelsen (MIT), kept in `src/vendor/compare-versions.ts` with its license header, so the package has no runtime dependencies.
|
|
69
540
|
|
|
70
|
-
|
|
541
|
+
MIT. See [LICENSE.md](./LICENSE.md).
|