Customize the update prompt
Change the colours and text of the built-in prompt, replace it with your own design, or build a fully custom update flow.
Pick the level of control you need
The prompt is ordinary React Native UI from @fireota/react-native. Customizing it never touches native code, so every change below can itself ship as an OTA update — no store build needed.
- Colours only — pass a
theme. One line, keeps the built-in layout. - Colours and wording — render
<UpdatePrompt title message />yourself. - Your own design —
renderPromptonFireOTAProvider. Full control over the look; FireOTA still decides when it shows. - Your own screens —
useFireOTA()anywhere in the app, e.g. a Settings row. - Your own timing —
checkOnAppStart: falseand the imperative API.
What the built-in prompt does
My App
Update ready
Fix checkout crash
Normal update — downloaded
Installing an important update
This only takes a moment.
Mandatory update
App updates
App version1.0.0
Runtime1.0.0
Channelproduction
Runningupdate 3f9a12c0
StatusUp to date
<UpdateScreen />
- A card appears at the bottom once a normal update is downloaded (status
installed). It shows the update's--message. - Restart applies it now; Later hides the card for that update — it still applies on the next cold start.
- For a mandatory update a full-screen “Installing an important update” notice blocks the app while it downloads, then the app restarts.
- Nothing is shown when there is no update, while checking, or after an error.
Option 1. Change the colours
Pass a theme as the third argument of withFireOTA in index.js. Every key is optional; missing keys keep the default. The same theme also styles <UpdateScreen />.
// index.js
import { AppRegistry } from "react-native";
import { withFireOTA } from "@fireota/react-native";
import App from "./App";
import { name as appName } from "./app.json";
import fireotaConfig from "./fireota.config";
AppRegistry.registerComponent(appName, () =>
withFireOTA(App, fireotaConfig, {
theme: {
accent: "#6D28D9", // Restart button, spinner, primary buttons
background: "#FFFFFF", // mandatory-update screen and <UpdateScreen /> background
card: "#F4F1FB", // the prompt card and <UpdateScreen /> cards
text: "#1F1235", // titles and values
muted: "#6B6280", // message, "Later", labels
danger: "#DC2626", // error text
},
}),
);My App
Update ready
Fix checkout crash
Default theme
My App
Update ready
Fix checkout crash
theme: accent #6D28D9, light card
My App
New version available
Restart to get the latest fixes.
Custom title and message
Default values, for reference:
accent: "#FF5A1F" background: "#0E1013" card: "#171A1F" text: "#F4F5F7" muted: "#9AA1AC" danger: "#F05252"
Appearance.getColorScheme() before calling withFireOTA, or use option 3 with useColorScheme() inside your component to follow live changes.Option 2. Change the title and message
<UpdatePrompt /> takes title (default “Update ready”) and message (default: the update's message, or “A new version is ready. Restart to use it.”). Turn the automatic prompt off with { prompt: false } and place your own instance — otherwise two prompts appear.
// index.js — turn the built-in prompt off…
AppRegistry.registerComponent(appName, () => withFireOTA(App, fireotaConfig, { prompt: false }));
// App.tsx — …and render it yourself with your own text
import { UpdatePrompt } from "@fireota/react-native";
export default function App() {
return (
<>
<RootNavigator />
<UpdatePrompt title="New version available" message="Restart to get the latest fixes." />
</>
);
}<UpdatePrompt /> after your navigator so it draws on top of the screens. It positions itself at the bottom and handles mandatory updates the same way as the built-in one.Option 3. Replace it with your own design
renderPrompt replaces the built-in prompt with any component you like. FireOTA calls it on every state change with the current update state and the actions, and renders whatever you return on top of the app (return null to show nothing).
withFireOTA only accepts prompt and theme. To use renderPrompt, replace withFireOTA(App, fireotaConfig) in index.js with a small Root component that wraps the app in FireOTAProvider, as below. Behaviour is otherwise identical.- 1Change
index.js:tsx// index.js — use FireOTAProvider instead of withFireOTA to pass renderPrompt import { AppRegistry } from "react-native"; import { FireOTAProvider } from "@fireota/react-native"; import App from "./App"; import { name as appName } from "./app.json"; import fireotaConfig from "./fireota.config"; import { UpdateBanner } from "./src/UpdateBanner"; function Root() { return ( <FireOTAProvider config={fireotaConfig} theme={{ accent: "#059669" }} // optional, also used by <UpdateScreen /> renderPrompt={(ota) => <UpdateBanner ota={ota} />} > <App /> </FireOTAProvider> ); } AppRegistry.registerComponent(appName, () => Root); - 2Create the component. This example shows a green bar at the top for normal updates and keeps a blocking screen for mandatory ones:tsx
// src/UpdateBanner.tsx import { useState } from "react"; import { ActivityIndicator, Modal, Pressable, StyleSheet, Text, View } from "react-native"; import type { FireOTAContextValue } from "@fireota/react-native"; export function UpdateBanner({ ota }: { ota: FireOTAContextValue }) { const [hiddenFor, setHiddenFor] = useState<string | null>(null); const { status, available, pendingReleaseId, currentReleaseId, restart, busy } = ota; // 1. Mandatory update downloading: block the app until it restarts. if (available?.mandatory && (status === "downloading" || status === "downloaded")) { return ( <Modal visible transparent animationType="fade"> <View style={styles.blocker}> <ActivityIndicator color="#fff" size="large" /> <Text style={styles.blockerText}>Installing an important update…</Text> </View> </Modal> ); } // 2. Normal update downloaded and waiting for a restart. const ready = status === "installed" && pendingReleaseId && pendingReleaseId !== currentReleaseId; if (!ready || hiddenFor === pendingReleaseId) return null; return ( <View style={styles.bar}> <Text style={styles.text} numberOfLines={1}> {available?.message || "A new version is ready"} </Text> <Pressable onPress={() => setHiddenFor(pendingReleaseId)}> <Text style={styles.later}>Later</Text> </Pressable> <Pressable style={styles.button} disabled={busy} onPress={restart}> <Text style={styles.buttonText}>Update</Text> </Pressable> </View> ); } const styles = StyleSheet.create({ bar: { position: "absolute", top: 48, left: 12, right: 12, flexDirection: "row", alignItems: "center", gap: 12, padding: 12, borderRadius: 12, backgroundColor: "#059669" }, text: { flex: 1, color: "#fff", fontWeight: "600" }, later: { color: "#D1FAE5" }, button: { backgroundColor: "#fff", borderRadius: 8, paddingHorizontal: 12, paddingVertical: 6 }, buttonText: { color: "#047857", fontWeight: "700" }, blocker: { flex: 1, alignItems: "center", justifyContent: "center", gap: 16, backgroundColor: "rgba(0,0,0,0.85)" }, blockerText: { color: "#fff", fontSize: 16, fontWeight: "600" }, }); - 3Build a release build, publish an update and open the app twice to see the new prompt.
My App
Top bar (renderPrompt)
My App
What's new
• Faster checkout • Fixed a crash on login
Restart nowNot nowDialog with release notes
available?.mandatory as in the example, or users may keep using the app until the forced restart happens.What renderPrompt and useFireOTA() receive
status—idle,checking,upToDate,downloading,downloaded,installed(ready, waiting for a restart) orerror.available— the update found by the last check:id,message,notes,mandatory,runtimeVersion,version(optional label).nullwhen there is none.pendingReleaseId— downloaded and waiting for a restart.currentReleaseId— the update running now,nullfor the store build.runtimeVersion,channel,appVersion,lastCheckedAt,error,configured.- Actions:
check(),install()(download + verify, applies on next start),restart().busyis true while one runs;actionErrorholds its last error. theme— the merged theme, so your component can reuse the colours you passed.
A normal update is ready to show when status === "installed" and pendingReleaseId differs from currentReleaseId — that is exactly the condition the built-in prompt uses.
Option 4. Show update state in your own screens
useFireOTA() works in any component under withFireOTA or FireOTAProvider. Use it for a Settings row, a badge on a tab, or an “About” screen. Keep the built-in prompt or hide it with { prompt: false }.
// Any screen, e.g. Settings → "About this app"
import { Button, Text, View } from "react-native";
import { useFireOTA } from "@fireota/react-native";
export function AboutUpdates() {
const { status, available, currentReleaseId, runtimeVersion, channel, error, busy, check, install, restart } = useFireOTA();
return (
<View>
<Text>Runtime {runtimeVersion} · {channel}</Text>
<Text>Running {currentReleaseId ? `update ${currentReleaseId.slice(0, 8)}` : "the store build"}</Text>
{error ? <Text style={{ color: "red" }}>{error}</Text> : null}
{status === "installed" ? (
<Button title="Restart to apply the update" onPress={restart} />
) : available ? (
<>
<Text>{available.message || "Improvements and fixes."}</Text>
<Button title="Download update" disabled={busy} onPress={install} />
</>
) : (
<Button title={status === "checking" ? "Checking…" : "Check for updates"} disabled={busy} onPress={check} />
)}
</View>
);
}Prefer a ready-made screen? <UpdateScreen title footer /> shows version, runtime, channel, status and the right button, styled with your theme:
<Stack.Screen name="Updates" component={UpdateScreen} />Option 5. Decide when to check
By default the SDK checks and downloads on every launch. To check at a moment you choose — after login, on a button, or only on Wi-Fi — turn that off and drive it yourself:
// fireota.config.js — stop the automatic check
const fireotaConfig = { /* appId, serverUrl, channel… */ checkOnAppStart: false };
// Later, when it suits your app (e.g. after login):
import FireOTA from "@fireota/react-native";
const result = await FireOTA.checkForUpdate();
if (result.updateAvailable && result.release) {
await FireOTA.downloadUpdate(result.release);
await FireOTA.installUpdate(result.release, "nextRestart"); // or "immediate"
}configure() runs at start even with checkOnAppStart: false, so a broken update is still rolled back.Testing your prompt
- 1Install a release build that contains your prompt code (or publish the prompt itself as an update first).
- 2Publish a small change with a clear message:
npx fireota publish … --message "Testing the new prompt". - 3Open the app and wait a few seconds — your prompt should appear once the download finishes.
- 4To test the mandatory path, publish again with
--mandatory. - 5Use the beta channel for these tests so production users never see them.