Google Play In-App Updates on Android Β· App Store version check on iOS Β· one typed hook
π Documentation Β Β·Β π Quick start Β Β·Β π API Β Β·Β π± Example app Β Β·Β π Report a bug
A small, typed wrapper around the Google Play In-App Updates API (Play Core app-update 2.1.0) on Android, and an App Store version check on iOS. Add one hook to your root component and users are prompted to update whenever a newer version is available β with Play's full-screen update screen on Android, and with your app's App Store page, shown inside the app, on iOS.
useInAppUpdate(); // that's it β¨|
Getting started |
Guides |
Reference |
Project |
|
Shows the update screen on app open and on every return to the foreground β and again if the user backs out. |
Downloads in the background while the app keeps running, with progress, so you can show your own "Restart to update" prompt. |
|
|
Checks the App Store for a newer version and shows the App Store page inside your app. No extra setup, no API keys. |
|
Full control with |
Built as a TurboModule. Web is a safe no-op that reports |
| Requirement | |
|---|---|
| βοΈ React Native | New Architecture (TurboModules) enabled |
| π€ Android | minSdkVersion 24+ Β· app installed from Google Play |
| π iOS | 15.1+ (React Native's minimum) Β· app live on the App Store |
npm install @nameisjayant/react-native-in-app-updateUsing Yarn?
yarn add @nameisjayant/react-native-in-app-updateThen install pods for iOS:
cd ios && pod installNote
There are no extra setup steps on either platform. Autolinking adds the Play Core dependency on Android and links StoreKit on iOS.
Call the hook once in your root component:
import { useInAppUpdate } from '@nameisjayant/react-native-in-app-update';
export default function App() {
useInAppUpdate({ enabled: !__DEV__ });
return <RootNavigator />;
}That's all you need. When a new version is on Play or the App Store, users see the update screen and can't continue until they update. π
Tip
enabled: !__DEV__ turns the hook off in debug builds. Builds installed from Android Studio or yarn android aren't from Play, so they can't receive updates.
useInAppUpdate(); // same as useInAppUpdate({ mandatory: true })flowchart LR
A([App opened / resumed]) --> B{Update on Play?}
B -- No --> Z([Do nothing])
B -- Yes --> C{Immediate allowed?}
C -- Yes --> D[Full-screen update]
C -- No --> E[Flexible download] --> F[Auto-install when done]
D -- User backs out --> D
How it behaves on Android:
| Situation | Behaviour |
|---|---|
| App opened or resumed, update available | Shows Play's full-screen immediate update |
| User backs out of the update screen | Shows it again right away (or on next resume) |
| Immediate update was interrupted | Resumes it |
| Play doesn't allow an immediate update | Downloads it as a flexible update and installs it automatically when done |
| Flexible update downloaded in an earlier session | Installs it on launch |
| No update, or app not installed from Play | Does nothing (status is up_to_date or error) |
On iOS, see π iOS.
Let users keep using the app while the update downloads, then ask them to restart:
import { useInAppUpdate } from '@nameisjayant/react-native-in-app-update';
function UpdateBanner() {
const { status, progress, completeUpdate } = useInAppUpdate({
mandatory: false,
});
if (status === 'downloading') {
return <Text>Downloading update⦠{Math.round(progress * 100)}%</Text>;
}
if (status === 'downloaded') {
return (
<Snackbar
text="An update has been downloaded"
action="Restart"
onPress={completeUpdate} // installs and restarts the app
/>
);
}
return null;
}Use these when you want to decide yourself when and how to update β for example, only for high-priority releases:
import {
checkForUpdate,
startUpdate,
completeUpdate,
addInstallStateListener,
} from '@nameisjayant/react-native-in-app-update';
const info = await checkForUpdate();
if (info.updateAvailability === 'available' && info.updatePriority >= 4) {
if (info.isImmediateAllowed) {
const result = await startUpdate('immediate'); // 'accepted' | 'canceled' | 'failed'
}
}
// Track a flexible download
const sub = addInstallStateListener((state) => {
console.log(state.installStatus, state.bytesDownloaded, state.totalBytesToDownload);
if (state.installStatus === 'downloaded') completeUpdate();
});
// later
sub.remove();iOS has no equivalent of Play's in-app updates, so the library does what most iOS apps do:
- Check β
checkForUpdate()looks up your bundle ID with Apple's public iTunes Lookup API and compares the App Storeversionwith your app'sCFBundleShortVersionString. It uses the store for the device's region, then falls back to the US store. - Prompt β
startUpdate()shows your app's App Store page inside the app (SKStoreProductViewController). The user taps Update there. If the sheet can't load, it opens the App Store app instead.
The same hook works unchanged:
| Situation | mandatory: true (default) |
mandatory: false |
|---|---|---|
| Update available on app open/resume | Shows the App Store page | Shows the App Store page |
| User closes the page | Shows it again right away | status becomes canceled, app continues |
| User taps Update | iOS installs it and relaunches the app on the new version | Same |
| Device's iOS is older than the new version's minimum | Treated as no update | Same |
Important
Differences from Android
updateInfo.availableVersion(e.g."2.1.0") andupdateInfo.releaseNotes(the "What's New" text) are filled in.availableVersionCodeandupdatePriorityare always0.clientVersionStalenessDaysis the number of days since the App Store version was released, so you can force an update only after a grace period.startUpdate()always resolves'canceled'once the App Store page closes. Apple doesn't tell apps whether the user updated. If they did, iOS relaunches the app on the new version.type(immediate/flexible),completeUpdate()andaddInstallStateListener()don't apply. They're accepted and do nothing.- Apple's lookup can lag a few hours behind a new release, so not every user sees the update the moment it goes live.
| Option | Type | Default | Description |
|---|---|---|---|
mandatory |
boolean |
true |
Force the update: immediate flow (App Store page on iOS), shown again on cancel, flexible downloads installed automatically |
type |
'immediate' | 'flexible' |
'flexible' |
Android flow to use when mandatory is false (ignored on iOS) |
checkOnMount |
boolean |
true |
Check for an update when the hook mounts (app open) |
checkOnResume |
boolean |
true |
Check again whenever the app returns to the foreground |
enabled |
boolean |
true |
Turn the hook off entirely (e.g. !__DEV__) |
onError |
(error: Error) => void |
β | Called with any Play or App Store error |
| Field | Type | Description |
|---|---|---|
status |
UpdateStatus |
Current state of the update flow (see below) |
updateInfo |
UpdateInfo | null |
Result of the last check |
error |
Error | null |
Last error, if any |
progress |
number |
Download progress of a flexible update, 0β1 |
checkNow |
() => Promise<void> |
Run the check β update flow right now |
completeUpdate |
() => Promise<void> |
Install a downloaded flexible update (restarts the app) |
| Value | Meaning |
|---|---|
βͺ idle |
Nothing has happened yet |
π checking |
Asking Play or the App Store for updates |
β
up_to_date |
No update available |
π² prompting |
The Play update screen or App Store page is showing |
β©οΈ canceled |
User dismissed the update screen (shown again when mandatory) |
β¬οΈ downloading |
Flexible update is downloading |
π¦ downloaded |
Flexible update is ready; call completeUpdate() |
βοΈ installing |
Update is being installed |
π« unsupported |
Platform has no in-app updates (web) |
β error |
Something failed (see error) |
| Function | Returns | Description |
|---|---|---|
checkForUpdate() |
Promise<UpdateInfo> |
Asks Google Play or the App Store whether a newer version is available |
startUpdate(type = 'immediate') |
Promise<UpdateFlowResult> |
Shows the Play update UI, or the App Store page on iOS. Rejects if no update can be started (and always on web) |
completeUpdate() |
Promise<void> |
Installs a downloaded flexible update. The app restarts. No-op on iOS |
addInstallStateListener(listener) |
EventSubscription |
Listens to download/install progress. Call .remove() to unsubscribe. Never fires on iOS |
Show all types
type UpdateType = 'immediate' | 'flexible';
type UpdateFlowResult = 'accepted' | 'canceled' | 'failed'; // always 'canceled' on iOS
type UpdateInfo = {
updateAvailability: 'unknown' | 'not_available' | 'available' | 'in_progress' | 'unsupported';
installStatus: InstallStatus;
availableVersionCode: number; // Play versionCode, 0 on iOS
availableVersion: string | null; // App Store version, null on Android
releaseNotes: string | null; // App Store "What's New", null on Android
isImmediateAllowed: boolean;
isFlexibleAllowed: boolean;
clientVersionStalenessDays: number | null; // days since Play learned about / App Store released the update
updatePriority: number; // 0β5, set via the Play Developer API, 0 on iOS
bytesDownloaded: number;
totalBytesToDownload: number;
};
type InstallState = {
installStatus: InstallStatus;
installErrorCode: number; // Play InstallErrorCode, 0 = no error
bytesDownloaded: number;
totalBytesToDownload: number;
};
type InstallStatus =
| 'unknown' | 'pending' | 'downloading' | 'downloaded'
| 'installing' | 'installed' | 'failed' | 'canceled' | 'requires_ui_intent';All types are exported from the package:
import type { UpdateInfo, UpdateStatus, UseInAppUpdateOptions } from '@nameisjayant/react-native-in-app-update';| Platform | Support | Behaviour |
|---|---|---|
| π€ Android | β | Full support via Google Play In-App Updates |
| π iOS | β | App Store version check + in-app App Store page (see iOS) |
| π Web | β | No-op. checkForUpdate() returns updateAvailability: 'unsupported', the hook reports status: 'unsupported' |
Note
startUpdate() throws on web. The hook never does, so it's safe to call unconditionally.
π€ Android
In-app updates only work for an app installed from Google Play. Use internal app sharing to test without publishing:
- Set up. In Play Console, enable internal app sharing and add your Google account as a tester. On the device, open Play Store β Settings β About, tap Play Store version 7 times, then turn on Internal app sharing.
- Install v1. Build a release APK/AAB with
versionCode 1, upload it to internal app sharing, open the link on the device and install it. - Publish v2. Bump to
versionCode 2, build and upload again. Open the new link but don't tap Update. Just let Play see it. - Try it. Open the app from step 2. The Play update screen appears; press back and it appears again.
π iOS
The check uses your app's live App Store listing, so there's nothing to upload:
- Set the app's bundle ID to one that's live on the App Store.
- Set its Version (
CFBundleShortVersionString) lower than the App Store version, e.g.0.0.1. - Run it on a device or simulator. The App Store page appears; close it and it appears again.
[!WARNING] On the simulator the App Store page may not load (there's no App Store app), so test on a real device for the full flow.
The example/ app shows the current status and version info, and has a Mandatory/Flexible (Optional on iOS) switch for this.
Install Error(-10): The app is not owned by any user on this device
The app wasn't installed from Play (e.g. yarn android). Expected in dev. The hook reports status: 'error' and the app keeps working. Use enabled: !__DEV__.
Update screen never shows
Make sure the installed versionCode is lower than the one on Play, and open the Play Store listing once so Play's cache refreshes.
status is unsupported
You're on web or another platform without in-app updates.
iOS: status is up_to_date though a new version is live
Apple's lookup can lag a few hours behind a release. Also check that the bundle ID matches the App Store app and that the device meets the new version's minimum iOS.
iOS: E_NOT_AVAILABLE from startUpdate()
There's no newer App Store version for this device. Call checkForUpdate() first.
| Command | What it does |
|---|---|
yarn |
Install dependencies (Yarn 4, Node version from .nvmrc) |
yarn example android / yarn example ios |
Run the example app |
yarn test |
JS unit tests (Jest): API and hook on Android, iOS and web |
yarn test:android |
Android unit tests (JUnit): Play Core β JS mapping. Needs JDK 21 |
yarn test:ios |
iOS unit tests (XCTest): App Store lookup, version compare, update info. macOS + Xcode |
yarn typecheck |
TypeScript check |
yarn lint |
ESLint + Prettier |
yarn prepare |
Build lib/ with react-native-builder-bob |
π Project layout
src/
index.tsx Public exports
InAppUpdate.native.ts Imperative API (Android/iOS)
InAppUpdate.ts Web fallback
useInAppUpdate.ts Mandatory/flexible update hook
NativeInAppUpdate.ts TurboModule spec
types.ts Public types
__tests__/ Jest tests, per platform
android/ Kotlin module (com.inappupdate), Google Play Core app-update
src/main/.../InAppUpdateMapping.kt Play β JS mapping (pure, unit tested)
src/test/ JUnit tests
ios/ Objective-C++ module, App Store lookup + SKStoreProductViewController
InAppUpdateInfo.{h,m} App Store lookup β JS mapping (Foundation only, unit tested)
Tests/ XCTest tests (scripts/test-ios.sh)
example/ Example app using the library from source
GitHub Actions workflows live in .github/workflows/:
| Workflow | Trigger | What it does |
|---|---|---|
ci.yml |
Push / PR to main |
Lint + typecheck, JS tests, Android (JUnit) and iOS (XCTest) unit tests, library build, example app builds for Android and iOS |
docs.yml |
Push to main touching docs/**, or manual run |
Publishes docs/ to the gh-pages branch, served at nameisjayant.github.io/react-native-in-app-update |
release.yml |
Push of a v* tag |
Checks the tag matches package.json, runs lint/typecheck/tests, builds, publishes to npm (with provenance) and creates a GitHub release |
π’ Releasing a new version
-
Add an npm Automation access token as the
NPM_TOKENrepository secret (Settings β Secrets and variables β Actions). One-time setup. -
Bump the version and push the tag:
npm version patch # or minor / major β updates package.json and creates the vX.Y.Z tag git push origin main --follow-tags
The release workflow does the rest.
π Docs website
The site is a single static page at docs/index.html. Edit it, push to main, and the docs workflow redeploys it.
Contributions are welcome! π
MIT Β© Jayant Kumar
If this library helps you, consider giving it a β on GitHub!
Made with π for the React Native community