# RNForge In-App Updates Source: https://rnforge.dev/docs/in-app-updates # Overview (/docs/in-app-updates) Call `getUpdateStatus()` first, then start only the flows the current install supports. The same status shape works for Play Store installs, debug builds, sideloaded installs, and iOS. Android can start Play Core update flows when they are available. Other installs return unavailable states, so your app can fall back to the store page. ## When To Use This [#when-to-use-this] * Your Android app is distributed through Google Play and you need Play Core immediate or flexible update flows from JavaScript. * For iOS, use the App Store page fallback to send users to the store. * If you use Expo, this package needs a development build because Expo Go cannot load native modules. ## Support Matrix [#support-matrix] | Platform | Update flow | Store page | Install listener | | ---------------------------------- | -------------------- | ------------------- | ---------------------- | | Android installed from Google Play | Immediate + flexible | Yes | Yes | | Android sideload/debug install | Not available | Play Store fallback | No | | iOS | Not available | App Store fallback | Unsupported event only | ## Features [#features] * Check update availability, current app version, and store version details. * Start Android immediate updates when Play policy allows them. * Start Android flexible updates, track download progress, and complete the downloaded update. * Open the Play Store or App Store as a fallback. * Handle unsupported devices, unsupported install sources, user cancellation, and missing store metadata as typed results. ## Public API [#public-api] | Area | APIs | | -------------------- | -------------------------------------------------------------------------------- | | Status | `getUpdateStatus`, `isUpdateAvailable` | | Android update flows | `startImmediateUpdate`, `startFlexibleUpdate`, `completeFlexibleUpdate` | | Store fallback | `openStorePage`, `canOpenStorePage` | | Flexible progress | `addInstallStateListener`, `supportsInstallStateListener` | | Flow guards | `canStartImmediateUpdate`, `canStartFlexibleUpdate`, `canCompleteFlexibleUpdate` | ## Start Here [#start-here] 1. [Install the package](/docs/in-app-updates/install) and Nitro peer dependency. 2. Call `getUpdateStatus()` before starting any update flow. 3. Use helper functions like `canStartImmediateUpdate(status)` before starting a flow. 4. Use `openStorePage()` as the iOS and unsupported-install fallback. See the [Quick Start](/docs/in-app-updates/quickstart) for copy-paste examples, [App Integration](/docs/in-app-updates/app-integration) for a full React component, the [API Guide](/docs/in-app-updates/api) for the status model and error handling, or [Troubleshooting](/docs/in-app-updates/troubleshooting) for common issues. --- # Installation (/docs/in-app-updates/install) ## Install the package [#install-the-package] ### npm ```bash npm install @rnforge/react-native-in-app-updates react-native-nitro-modules ``` ### pnpm ```bash pnpm add @rnforge/react-native-in-app-updates react-native-nitro-modules ``` ### yarn ```bash yarn add @rnforge/react-native-in-app-updates react-native-nitro-modules ``` ### bun ```bash bun add @rnforge/react-native-in-app-updates react-native-nitro-modules ``` ## Rebuild the app [#rebuild-the-app] This package includes native code. After installing it, rebuild the native app for the platform you use. ### Android [#android] Rebuild the Android app from Android Studio or your React Native CLI workflow. ### iOS [#ios] Install pods, then rebuild the iOS app from Xcode or your React Native CLI workflow. ```bash cd ios && pod install ``` ### Expo [#expo] Expo Go cannot load this package because it includes native code. Use a development build. ```bash npx expo prebuild ``` Then build and run the development app with your Expo workflow. ## Requirements [#requirements] The package is built with Nitro and requires: | Area | Requirement | | ------------- | -------------------------------------------------------- | | React Native | Version supported by `react-native-nitro-modules` | | Native bridge | `react-native-nitro-modules` | | Android | `compileSdkVersion 34+`, NDK `27+`, Google Play Services | | iOS | Xcode `16.4+`, Swift `5.9+` | ## Platform Setup [#platform-setup] ### Android [#android-1] No JavaScript setup is required after installation. The native module uses Google Play Core for real in-app update flows. Real update flows require the app to be: * installed from Google Play, not sideloaded or installed from a debug build * signed with the same certificate as the Play track build * running on a device with Google Play Services available * built without legacy APK expansion (`.obb`) files If those requirements are not met, APIs return typed unsupported results instead of pretending an update can run. ### iOS [#ios-1] No additional setup is required. iOS does not support Google Play-style immediate or flexible in-app updates, so update-flow APIs return typed unsupported results. Use `openStorePage({ ios: { appStoreId } })` to send users to the App Store. ## Verify Installation [#verify-installation] After installing, call the API once to confirm the native module loads: ```typescript import { getUpdateStatus } from '@rnforge/react-native-in-app-updates'; const status = await getUpdateStatus(); console.log(status.platform); console.log(status.supported); console.log(status.reason); ``` On iOS without an App Store ID, `supported` is `false` and `reason` is `missing-app-store-id`. On Android debug or sideload installs, `supported` is usually `false` with `unsupported-install-source`. See [Troubleshooting](./troubleshooting) if the status does not match what you expect. --- # Quick Start (/docs/in-app-updates/quickstart) Use these examples when you want the shortest working snippets. For screen-level wiring, see [App Integration](./app-integration). ## Check Update Status [#check-update-status] ```typescript import { getUpdateStatus, isUpdateAvailable } from '@rnforge/react-native-in-app-updates'; const status = await getUpdateStatus(); if (!status.supported) { console.log('Updates are not supported:', status.reason); } else if (isUpdateAvailable(status)) { console.log('Update available:', status.latestStoreVersion ?? status.latestStoreBuild); } else { console.log('App is up to date'); } ``` ## Immediate Update (Android Play) [#immediate-update-android-play] ```typescript import { getUpdateStatus, startImmediateUpdate, canStartImmediateUpdate, } from '@rnforge/react-native-in-app-updates'; const status = await getUpdateStatus(); if (canStartImmediateUpdate(status)) { const result = await startImmediateUpdate(); console.log('Immediate update result:', result.reason); } ``` ## Flexible Update (Android Play) [#flexible-update-android-play] ```typescript import { getUpdateStatus, startFlexibleUpdate, completeFlexibleUpdate, addInstallStateListener, canStartFlexibleUpdate, } from '@rnforge/react-native-in-app-updates'; const status = await getUpdateStatus(); if (canStartFlexibleUpdate(status)) { let subscription: { remove: () => void } | undefined; subscription = addInstallStateListener((event) => { if (event.installStatus === 'downloading') { console.log('Download progress:', event.progress); } if (event.installStatus === 'downloaded') { console.log('Flexible update downloaded'); void completeFlexibleUpdate().finally(() => { subscription?.remove(); }); } }); try { const result = await startFlexibleUpdate(); console.log('Flexible update result:', result.reason); } catch (error) { subscription.remove(); throw error; } } ``` Keep the subscription alive while the flexible download is active. In a React component, call `subscription.remove()` from your cleanup function when the screen unmounts. ## Store Page Fallback [#store-page-fallback] ```typescript import { getUpdateStatus, openStorePage, canOpenStorePage, } from '@rnforge/react-native-in-app-updates'; const storeOptions = { ios: { appStoreId: '1234567890', country: 'us', }, }; const status = await getUpdateStatus(storeOptions); if (canOpenStorePage(status)) { await openStorePage(storeOptions); } ``` Pass the same iOS options to both calls so the status and store fallback agree. On Android, `openStorePage()` can be called without options. On iOS, `ios.appStoreId` is required. ## Common Helpers [#common-helpers] | Helper | Use it when | | ----------------------------------- | ------------------------------------------------------------ | | `isUpdateAvailable(status)` | You only need to know whether a newer version is available. | | `canStartImmediateUpdate(status)` | You want to start the blocking Android Play update UI. | | `canStartFlexibleUpdate(status)` | You want to start a background Android Play update download. | | `canCompleteFlexibleUpdate(status)` | A flexible update has downloaded and can be installed. | | `canOpenStorePage(status)` | You want a store-page fallback for unsupported update flows. | See [App Integration](./app-integration) for a full React component example that combines these helpers. --- # App Integration (/docs/in-app-updates/app-integration) This example shows one React component that loads update status, starts immediate or flexible update flows, shows flexible download progress, and falls back to the store page when needed. ## InAppUpdatePanel [#inappupdatepanel] ```tsx import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Button, Text, View } from 'react-native'; import { addInstallStateListener, canOpenStorePage, canStartFlexibleUpdate, canStartImmediateUpdate, completeFlexibleUpdate, getUpdateStatus, InAppUpdatesError, openStorePage, startFlexibleUpdate, startImmediateUpdate, } from '@rnforge/react-native-in-app-updates'; import type { InstallStateSubscription, UpdateStatus } from '@rnforge/react-native-in-app-updates'; const storeOptions = { ios: { appStoreId: '1234567890', country: 'us', }, }; export function InAppUpdatePanel() { const [status, setStatus] = useState(null); const [loading, setLoading] = useState(false); const [message, setMessage] = useState(null); const [progress, setProgress] = useState(null); const subscriptionRef = useRef(null); const completingRef = useRef(false); const loadStatus = useCallback(async () => { setLoading(true); setMessage(null); try { const next = await getUpdateStatus(storeOptions); setStatus(next); } catch (error) { if (error instanceof InAppUpdatesError) { setMessage(error.message); } else { setMessage('Unable to check for updates.'); } } finally { setLoading(false); } }, []); useEffect(() => { void loadStatus(); return () => { subscriptionRef.current?.remove(); subscriptionRef.current = null; }; }, [loadStatus]); const startImmediate = useCallback(async () => { if (!status || !canStartImmediateUpdate(status)) return; setLoading(true); setMessage(null); try { const next = await startImmediateUpdate(); setStatus(next); } catch (error) { if (error instanceof InAppUpdatesError) { setMessage(error.message); } else { setMessage('Unable to start the update.'); } } finally { setLoading(false); } }, [status]); const startFlexible = useCallback(async () => { if (!status || !canStartFlexibleUpdate(status)) return; setLoading(true); setMessage(null); subscriptionRef.current?.remove(); subscriptionRef.current = null; completingRef.current = false; setProgress(null); subscriptionRef.current = addInstallStateListener((event) => { if (event.installStatus === 'downloading') { setProgress(event.progress ?? null); } if (event.installStatus === 'downloaded') { if (completingRef.current) return; completingRef.current = true; subscriptionRef.current?.remove(); subscriptionRef.current = null; void completeFlexibleUpdate() .then((next) => { setStatus(next); setProgress(null); }) .catch((error) => { completingRef.current = false; if (error instanceof InAppUpdatesError) { setMessage(error.message); } else { setMessage('Unable to complete the update.'); } }); } }); try { const next = await startFlexibleUpdate(); setStatus(next); } catch (error) { subscriptionRef.current?.remove(); subscriptionRef.current = null; completingRef.current = false; setProgress(null); if (error instanceof InAppUpdatesError) { setMessage(error.message); } else { setMessage('Unable to start the download.'); } } finally { setLoading(false); } }, [status]); const openStore = useCallback(async () => { if (!status || !canOpenStorePage(status)) return; setLoading(true); setMessage(null); try { await openStorePage(storeOptions); } catch (error) { if (error instanceof InAppUpdatesError) { setMessage(error.message); } else { setMessage('Unable to open the store.'); } } finally { setLoading(false); } }, [status]); return ( Platform: {status?.platform ?? '...'} Supported: {status == null ? '...' : status.supported ? 'yes' : 'no'} Update available: {status == null ? '...' : status.updateAvailable == null ? 'unknown' : status.updateAvailable ? 'yes' : 'no'} {progress == null ? null : Download: {Math.round(progress * 100)}%} {status?.reason ? Reason: {status.reason} : null} {message ? {message} : null}