Lifecycle
The bits of the SDK that aren’t sync(). All small surface area; all critical for correctness. Each method exists in all three runtimes — pick whichever fits your app’s bootstrap.
From JS, every runtime method lives on the
NitroPushClientreturned fromconfigureWith()/configure(). None of these are top-level exports anymore — capture the client at module scope and use it everywhere.
notifyAppReady()
Confirm the just-installed bundle is healthy. You must call this on every launch — typically from your root component (JS) or your splash-screen finish handler (native).
Signature
Synchronous on the calling thread; safe from the main thread.
func notifyAppReady() Synchronous on the calling thread; safe from the main thread.
fun notifyAppReady() Method on the NitroPushClient. Returns a Promise but resolves on the next tick — feel free to fire-and-forget.
client.notifyAppReady(): Promise<void>; Why it matters
When the SDK activates a new bundle, it sets a “this bundle is unconfirmed” flag. notifyAppReady() clears that flag.
If the new bundle crashes before notifyAppReady() runs:
- The
unconfirmedflag survives the crash (it’s persisted toUserDefaults/SharedPreferences). - On the next launch, the SDK sees the flag still set, deletes the bundle directory, and falls back to the previous version.
- An
install_failed_rollbackanalytics event fires for the failed release. Any matching notification integrations (Slack/Discord/etc.) light up.
This is the rollback safety net. Without notifyAppReady(), every launch would think the install never succeeded and roll back forever — a poison-pill bundle would still ship to users.
Where to call it
From AppDelegate.applicationDidBecomeActive, OR after your splash screen finishes.
import NitroPush
func applicationDidBecomeActive(_ application: UIApplication) {
NitroPushSdk.shared.notifyAppReady()
} From a Lifecycle observer in MainActivity, OR after your splash screen finishes.
import com.nitropush.sdk.NitroPushSdk
override fun onResume() {
super.onResume()
NitroPushSdk.shared.notifyAppReady()
} From a top-level useEffect that runs on every mount of your root component.
import { useEffect } from 'react';
import { client } from './updates'; // wherever you called configureWith()
export function App() {
useEffect(() => {
client.notifyAppReady().catch(() => {});
}, []);
return /* your app */;
} It’s idempotent — safe to call repeatedly. After the first call per launch, subsequent calls are no-ops.
What “healthy” means
The SDK’s definition of healthy is “your app reached a known-good state and ran notifyAppReady()”. If your app shows a splash screen for 5 seconds and then renders, that’s already a healthy boot — call notifyAppReady() from the splash screen’s useEffect (or its native equivalent), not from deep inside a tabbed flow. Otherwise users with a deep-link straight to a broken tab would never confirm a healthy boot for a fine bundle.
restartApp(onlyIfUpdateIsPending?)
Force a JS engine restart.
Signature
Throws on integrity failure (the staged bundle's hash didn't match).
func restartApp(onlyIfUpdateIsPending: Bool) throws Throws on integrity failure (the staged bundle's hash didn't match).
fun restartApp(onlyIfUpdateIsPending: Boolean) Resolves once the bridge has been told to reload.
client.restartApp(onlyIfUpdateIsPending: boolean): Promise<void>; | Arg | Behaviour |
|---|---|
false | Always restart. |
true | Restart only if there’s a pending bundle to swap in. No-op if there isn’t. |
When to use it
The common case is a “Update available — restart now?” UI. The user clicked Yes; you want to apply the pending bundle:
Call after presenting the user-facing confirmation.
try? NitroPushSdk.shared.restartApp(onlyIfUpdateIsPending: true) Call after presenting the user-facing confirmation.
NitroPushSdk.shared.restartApp(onlyIfUpdateIsPending = true) Wire it to a button onPress.
await client.restartApp(true); InstallMode.IMMEDIATE calls this internally — you don’t need to. ON_NEXT_RESTART does NOT — that’s the whole point. So if you want a manual “restart now” button alongside ON_NEXT_RESTART syncs, this is the call.
rollback(releaseId)
Explicitly roll back a release. Three behaviours, all by the same call:
| Case | Effect |
|---|---|
releaseId is the pending bundle | Drops pending + its bundle directory. Same as clearPendingUpdate() but scoped. |
releaseId is the active bundle and a previous bundle exists | Swaps previous into active, deletes the failed bundle, reloads the JS bridge. |
| Anything else | iOS throws; Android no-ops. |
Throws if the release isn't pending or active, or if active has no previous to roll back to.
try NitroPushSdk.shared.rollback(releaseId: "rel-abc-123") No-ops if the releaseId isn't reachable from the current pointers.
NitroPushSdk.shared.rollback("rel-abc-123") On the LocalPackage instance (returned from getCurrentPackage / getPendingPackage / remote.download). Mirrors local.install().
// Scoped to a package — no releaseId needed; the LocalPackage already
// knows its own id.
const pending = await client.getPendingPackage();
if (pending) {
await pending.rollback();
} The rollback safety net described under notifyAppReady() calls this for you on the next launch when an install never confirmed. This explicit form is for cases where the current session has noticed a problem (a feature-flag service flips off the new bundle, a custom canary check fails, etc.) and you want to back out now.
clearPendingUpdate() (native-only)
Discard a pending bundle before it activates. Useful for “cancel” affordances in custom update UIs.
Synchronous; safe from any thread.
NitroPushSdk.shared.clearPendingUpdate() Synchronous; safe from any thread.
NitroPushSdk.shared.clearPendingUpdate() Not exposed via JS. From JS, the equivalent is calling `pending.rollback()` on the LocalPackage returned by `client.getPendingPackage()`.
// No top-level / client equivalent. Use the LocalPackage method:
const pending = await client.getPendingPackage();
if (pending) await pending.rollback(); After this, getPendingPackage() returns null and the next restartApp() won’t swap.
clearUpdates()
Wipe all locally-stored OTA bundles. The next launch falls back to the binary-shipped bundle.
NitroPushSdk.shared.clearUpdates() NitroPushSdk.shared.clearUpdates() await client.clearUpdates(); Use this for:
- Testing the rollback path (clear + crash + relaunch).
- “Reset to factory bundle” support flows.
- Forcing a re-download on the next sync (e.g. after rotating storage keys).
Inspecting state
Snapshot of what’s running and what’s staged.
Both methods return NPLocalPackage? — synchronous, safe to call from any thread.
let active = NitroPushSdk.shared.getCurrentPackage() // running now
let pending = NitroPushSdk.shared.getPendingPackage() // staged for next activation
if let pending {
print("Update ready: \(pending.label)")
} Both methods return NlLocalPackage? — synchronous, safe to call from any thread.
val active = NitroPushSdk.shared.getCurrentPackage() // running now
val pending = NitroPushSdk.shared.getPendingPackage() // staged for next activation
if (pending != null) {
println("Update ready: ${pending.label}")
} JS exposes a sync variant on the client too (`getUpdateMetadataSync`) for first-paint usage.
import { client } from './updates';
// Async — what's running now (or null when on the binary bundle).
const running = await client.getCurrentPackage();
// Sync variant — useful for showing the running version on a settings
// screen at first paint without a microtask hop.
const runningSync = client.getUpdateMetadataSync();
// Async — what's staged for next activation.
const pending = await client.getPendingPackage();
if (pending) {
// show "Update ready — restart to apply"
} Returns null if running the binary-shipped bundle.
Bundle URL hand-off
This one’s only native — the JS surface doesn’t expose it. Used by your AppDelegate.bundleURL() / ReactNativeHost.getJSBundleFile() to hand React Native the active OTA bundle path. Wiring covered in Installation.
Call from AppDelegate.bundleURL() — falls back to the binary bundle if no OTA is active.
override func bundleURL() -> URL? {
#if DEBUG
return RCTBundleURLProvider.sharedSettings()
.jsBundleURL(forBundleRoot: "index")
#else
return NitroPushSdk.shared.activeBundleURL()
?? Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
} Call from ReactNativeHost.getJSBundleFile() — returns null if no OTA is active.
override fun getJSBundleFile(): String? {
if (BuildConfig.DEBUG) return null
return NitroPushSdk.shared.activeBundleFile()
} Download progress
For per-call progress, pass onProgress directly to sync(client, options, onStatus, onProgress) or to remote.download(onProgress). Native code also has a global listener that fires for every download (JS- or native-triggered) — handy for a global progress bar:
Returns a numeric listener id; pass it to remove.
let id = NitroPushSdk.shared.addDownloadProgressListener { progress in
print("\(progress.receivedBytes) / \(progress.totalBytes)")
}
// later
NitroPushSdk.shared.removeDownloadProgressListener(listenerId: id) Returns a numeric listener id; pass it to remove.
val id = NitroPushSdk.shared.addDownloadProgressListener { p ->
println("${p.receivedBytes} / ${p.totalBytes}")
}
// later
NitroPushSdk.shared.removeDownloadProgressListener(id) No top-level subscription. Use sync()'s onProgress or remote.download(onProgress) on the manual flow.
// JS: progress is per-call. Two equivalent forms:
// 1. Through sync()'s fourth arg.
await sync(client, {}, undefined, (p) => {
console.log(`${p.receivedBytes} / ${p.totalBytes}`);
});
// 2. Manual flow — the callback lives on remote.download.
const remote = await client.checkForUpdate();
if (remote) {
const local = await remote.download((p) => {
console.log(`${p.receivedBytes} / ${p.totalBytes}`);
});
await local.install(InstallMode.ON_NEXT_RESTART, 0);
} Debug logging — setEnableLogs()
Native-only. Off by default. When enabled, the SDK logs every action — configure, checkForUpdate (with the request URL), downloadManifestRelease (with the URL hit), installUpdate (with mode), notifyAppReady, restartApp, rollback, lifecycle-driven activations (ON_NEXT_RESUME / ON_NEXT_SUSPEND), and any failure with URL + HTTP status + content-type + a 200-char body snippet.
Logs to stdout via print(). Tag prefix: [NitroPush]. Visible in Xcode console + Console.app on a connected device.
// In AppDelegate.didFinishLaunchingWithOptions, after configure():
NitroPushSdk.shared.setEnableLogs(true)
// → [NitroPush] configure serverUrl=… deploymentKey=… …
// [NitroPush] checkForUpdate deploymentKeyOverride=(none)
// [NitroPush] downloadManifestRelease GET https://…/manifest.json
// [NitroPush] installUpdate.ON_NEXT_RESTART staged; will activate on next cold start
// [NitroPush] notifyAppReady → install_completed releaseId=… Logs via android.util.Log.i (and .w for failures). Tag: NitroPush. Tail with `adb logcat | grep NitroPush`.
// In MainApplication.onCreate, after configure():
NitroPushSdk.shared.setEnableLogs(true)
// → I/NitroPush: configure serverUrl=… deploymentKey=… …
// I/NitroPush: checkForUpdate GET http://…/api/sdk/releases/latest?…
// I/NitroPush: downloadManifestRelease GET https://…/manifest.json
// I/NitroPush: installUpdate.ON_NEXT_RESTART staged; will activate on next cold start
// I/NitroPush: notifyAppReady → install_completed releaseId=… Not exposed via JS — toggle from native at bootstrap time. The native trace covers JS-triggered calls too.
// No JS toggle today. Flip it on from MainApplication.kt / AppDelegate.swift.
// The native trace covers everything — including calls triggered from JS. Leave it OFF in production. The toggle itself always logs (“setEnableLogs: true” / “setEnableLogs: false”) so flipping it leaves a visible trail. Toggle is idempotent and process-local — no persistence.