@rollbird/react-native
v0.1.2
Published
React Native client for Rollbird hosted OTA updates.
Downloads
115
Readme
@rollbird/react-native
A thin wrapper over @hot-updater/react-native's
HotUpdater.wrap() that points your app at Rollbird and reports install /
failure beacons back to your project's dashboard.
This package only wraps the client side. Run npx rollbird init (from
@rollbird/cli) first — it detects your project, creates
or links a Rollbird project, generates a signing key pair for it server-side,
and writes the channel + public key into your native config. rollbird
release then builds, signs, and publishes the bundles this wrapper checks
for and downloads.
Install
npm i @rollbird/react-native @hot-updater/react-native@hot-updater/react-native is a peer dependency pinned to 0.36.13 — install
that exact version alongside this package (rollbird init does this for you).
Usage
import { Rollbird } from "@rollbird/react-native";
import App from "./App";
export default Rollbird.wrap({
projectKey: "proj_xxx",
})(App);projectKey is the only required option. Everything else has a sane default:
| Option | Default | Notes |
| --- | --- | --- |
| apiUrl | https://ota.rollbird.dev | Development only (pointing at a local pnpm dev API). Production apps leave it alone: the endpoint ships with the SDK and does not change. |
| strategy | "fingerprint" | Or "appVersion". |
| fallbackComponent | none | Rendered with { progress, status } while an update downloads. |
| onProgress | none | Called with { progress } on every download tick. |
| onUpdateProcessCompleted | none | Called with the raw Hot Updater result. |
| onNotifyAppReady | none | Called with Hot Updater's launch report ({ status: "RECOVERED" \| "STABLE", crashedBundleId? }). |
| onError | none | Called with the error that aborted the update. |
| reloadOnForceUpdate | true | Passed through to Hot Updater. |
| requestTimeout | Hot Updater default | Passed through to Hot Updater. |
| requestHeaders | none | Passed through to Hot Updater. |
Expo (CNG)
expo prebuild regenerates ios/ and android/ from scratch, which deletes
anything rollbird init writes into a bare project's native config — the app
keeps running and simply stops checking for updates. This package ships an Expo
config plugin that puts it back on every prebuild, locally and on EAS Build.
npx rollbird init registers it for you; to do it by hand, add it to
app.json:
{
"expo": {
"plugins": [["@rollbird/react-native", { "channel": "production" }]]
}
}channel is the only prop. Everything else is resolved at prebuild time, in
this order:
- the plugin prop (
channelonly), channel/publicKeyPem/fingerprints.<platform>inrollbird.json, written byrollbird init,- the
ROLLBIRD_CHANNEL/ROLLBIRD_PUBLIC_KEY/ROLLBIRD_FINGERPRINT_IOS/ROLLBIRD_FINGERPRINT_ANDROIDenvironment variables.
The channel falls back to production. The public key and the fingerprint
have no fallback: a build that can't verify a bundle signature would install
updates it cannot authenticate, and one with the wrong fingerprint silently
never updates — so the plugin fails the prebuild instead, naming both fixes
(npx rollbird init, or the EAS secrets below).
What it writes into the freshly generated native project:
| Platform | Where | Keys |
| --- | --- | --- |
| iOS | Info.plist | HOT_UPDATER_CHANNEL, HOT_UPDATER_PUBLIC_KEY, HOT_UPDATER_FINGERPRINT_HASH |
| Android | AndroidManifest.xml meta-data | com.hotupdater.CHANNEL, com.hotupdater.PUBLIC_KEY, com.hotupdater.FINGERPRINT_HASH |
| both | AppDelegate / MainApplication | Hot Updater's own entry-point patch, so a downloaded bundle actually launches |
Every value is read, never computed. rollbird init computes the Expo
fingerprint once per platform and stores it in rollbird.json; the plugin
stamps that exact string into the build and rollbird release tags releases
with the same one, so a binary and its updates can't disagree. The plugin must
not compute its own: @expo/fingerprint hashes an extra dir:android (and
dir:ios) source as soon as those directories exist, so on a machine that has
prebuilt locally — which is every machine, right after this plugin runs — it
returns a different value than a clean checkout does, and the binary would carry
a fingerprint no release ever matches. Re-run npx rollbird init after changing
native dependencies to refresh the stored value, then prebuild and rebuild.
Android values go in manifest meta-data, not strings.xml, because that's
what the native module reads first — and because Hot Updater's own plugin
deletes those string resources.
EAS Build
rollbird.json carries your project id and key, so many teams gitignore it. On
a build worker where the file is absent, pass the values as secrets:
eas env:create --name ROLLBIRD_PUBLIC_KEY --value "$(node -p "require('./rollbird.json').publicKeyPem")"
eas env:create --name ROLLBIRD_CHANNEL --value production
eas env:create --name ROLLBIRD_FINGERPRINT_IOS --value "$(node -p "require('./rollbird.json').fingerprints.ios")"
eas env:create --name ROLLBIRD_FINGERPRINT_ANDROID --value "$(node -p "require('./rollbird.json').fingerprints.android")"The plugin reads them exactly like the file. (EAS Build itself is documented but not yet verified end to end by Rollbird.)
Requirements
@expo/config-plugins(any version from 9 up, i.e. Expo SDK 52+) — already in every Expo project; it's an optional peer dependency here so bare projects don't have to install it.@expo/fingerprint— used byrollbird initandrollbird release, not by the plugin;npx expo install @expo/fingerprintifinitsays it can't be loaded.expo-updatescannot be installed. It owns the same update process Hot Updater does: both swap the JS bundle at launch, and the last one to win decides which bundle runs.rollbird initrefuses to set up a project that has it. Removeexpo-updates(and itsexpo.updatesconfig) before adding Rollbird.- Expo Go can't run custom native code, so updates only work in a development build or a release build.
Verified on Expo SDK 53 (RN 0.79) and SDK 57 (RN 0.86), New Architecture on.
Installing from a path, not npm
If you depend on this package by path — file:../packages/react-native in a
monorepo, or npm link — that install brings its devDependencies with it,
including the React Native copy kept here for tests. Metro then resolves
react-native for this package's modules from that copy instead of your app's,
and your app bundles two React Natives. The older one's TurboModuleRegistry
looks up a global that newer React Native no longer defines, so every native
module lookup returns null and the app dies on launch with
TurboModuleRegistry.getEnforcing('HotUpdater') could not be found — an error
that says nothing about resolution.
Installing from npm (or a packed tarball) avoids it entirely, since
devDependencies aren't installed and react-native stays a peer. If you must
link by path, make react and react-native singletons in your
metro.config.js by resolving them from the app root.
Don't register @hot-updater/react-native's own config plugin as well.
Rollbird's writes everything it would (Hot Updater's native module is
autolinked from your dependencies either way), and the two take their values
from different places — Hot Updater's from hot-updater.config.ts and its
local signing keys. With both registered the plugin listed first wins (mods
registered first run last), so the plugin warns when it sees them together.
How it works
Rollbird.wrap does no update logic of its own. It builds a baseURL
(<apiUrl>/v1/ota/<projectKey>) and an updateStrategy, then hands both to
the open-source Hot Updater client's own HotUpdater.wrap() — so bundle
resolution, download, and apply/reload behavior are exactly upstream Hot
Updater's, unmodified. Rollbird's hosted API implements the same check
contract Hot Updater's OSS backends do, and adds channels, staged rollout,
and rollback on top; this wrapper adds only the apiUrl/projectKey
plumbing and the beacon reporting below.
The native config HotUpdater.wrap() reads at runtime (release channel,
public key for signature verification) is written by rollbird init, not by
this package — see @rollbird/cli.
Beacons
Rollbird.wrap posts a best-effort, fire-and-forget beacon to
${apiUrl}/v1/events for:
download_complete— once per bundle, when the offered bundle finishes downloading (it runs on the next launch). It carries the release id taken from the update check, so a background download that ends after Hot Updater reports the process complete is still attributed.Rollbird.wrappasses Hot Updater a resolver that wrapscreateDefaultResolver(baseURL)to read that id; requests are unchanged.install_success— immediately afterdownload_complete, once for the same offered bundle. It uses the same release id and signed offer receipt. The install metric therefore records a successfully downloaded and staged bundle; activation happens on the next launch.install_failure— the app launchedRECOVEREDafter a bundle crashed (carries the crashed bundle id), or the update process errored (no release id, since none is known at that point).
Beacon requests never throw and never block your callbacks — a failed POST is
swallowed silently and your own onProgress / onUpdateProcessCompleted /
onNotifyAppReady / onError callbacks are always still invoked with the
original arguments.
The server counts a beacon only when it carries the short-lived signed offer receipt returned by the update check. That proves the event refers to an offer Rollbird issued; it is not hardware/device attestation. A crash reported after the process restarts currently has no in-memory receipt and is intentionally dropped rather than counted as trusted telemetry.
Passthrough
Rollbird.checkForUpdate, Rollbird.runUpdateProcess, Rollbird.getBundleId,
Rollbird.getChannel, Rollbird.getAppVersion, and Rollbird.reload are the
same functions exported by @hot-updater/react-native's HotUpdater, so you
don't need to import both packages for manual update flows.
