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 — renderPrompt on FireOTAProvider. 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: false and the imperative API.

What the built-in prompt does

My App

Update ready

Fix checkout crash

LaterRestart

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

Check for updates

<UpdateScreen />

The built-in UI with the default theme: the prompt after a download, the mandatory-update screen, and <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 />.

js
// 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

LaterRestart

Default theme

My App

Update ready

Fix checkout crash

LaterRestart

theme: accent #6D28D9, light card

My App

New version available

Restart to get the latest fixes.

LaterRestart

Custom title and message

Left: default theme. Middle: the theme above. Right: the same theme with custom text (option 2).

Default values, for reference:

ts
accent: "#FF5A1F"   background: "#0E1013"   card: "#171A1F"
text:   "#F4F5F7"   muted:      "#9AA1AC"   danger: "#F05252"
Tip: Want light and dark versions? Compute the theme from 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.

tsx
// 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." />
    </>
  );
}
Note: Render <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).

Important: 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.
  1. 1
    Change 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);
  2. 2
    Create 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" },
    });
  3. 3
    Build a release build, publish an update and open the app twice to see the new prompt.
A new version is readyUpdate

My App

Top bar (renderPrompt)

My App

What's new

• Faster checkout • Fixed a crash on login

Restart nowNot now

Dialog with release notes

Two prompts built with renderPrompt: a top bar, and a dialog that lists the release notes.
Caution: When you replace the prompt you also replace the mandatory-update screen. Always handle 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) or error.
  • available — the update found by the last check: id, message, notes, mandatory, runtimeVersion, version (optional label). null when there is none.
  • pendingReleaseId — downloaded and waiting for a restart. currentReleaseId — the update running now, null for the store build.
  • runtimeVersion, channel, appVersion, lastCheckedAt, error, configured.
  • Actions: check(), install() (download + verify, applies on next start), restart(). busy is true while one runs; actionError holds 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 }.

tsx
// 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:

tsx
<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:

js
// 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"
}
Note: The launch confirmation still happens automatically: configure() runs at start even with checkOnAppStart: false, so a broken update is still rolled back.

Testing your prompt

  1. 1
    Install a release build that contains your prompt code (or publish the prompt itself as an update first).
  2. 2
    Publish a small change with a clear message: npx fireota publish … --message "Testing the new prompt".
  3. 3
    Open the app and wait a few seconds — your prompt should appear once the download finishes.
  4. 4
    To test the mandatory path, publish again with --mandatory.
  5. 5
    Use the beta channel for these tests so production users never see them.