sync(client, options, onStatus, onProgress)
The main update flow:
- Asks the server: is there anything newer?
- If yes, downloads the bundle + assets directly from your storage bucket.
- Schedules an install according to
installMode(immediate / next restart / next resume / next suspend).
The JS sync() is the high-level helper you’ll reach for in 95% of cases. It takes the NitroPushClient returned from configureWith() (or configure()) as its first argument — every other method also lives on that client. The native side exposes the same three steps as separate methods (checkForUpdate / downloadUpdate / installUpdate) so you can run the cycle without touching the JS bridge — useful for background updates, pre-JS downloads, or recovering when the JS thread is broken.
sync() is idempotent and safe to call repeatedly. Concurrent JS calls coalesce — the second one resolves with SYNC_IN_PROGRESS and never duplicates work.
Signature
Three async methods on NitroPushSdk.shared. URLSession does I/O on background queues — call from any actor.
// 1. Check
func checkForUpdate(deploymentKeyOverride: String? = nil)
async throws -> NPRemotePackage?
// 2. Download
func downloadUpdate(_ pkg: NPRemotePackage)
async throws -> NPLocalPackage
// 3. Install
func installUpdate(
pkg: NPLocalPackage,
installMode: NPInstallMode,
minimumBackgroundDuration: Double
) async throws Three blocking methods on NitroPushSdk.shared. Run them off the main thread (Dispatchers.IO, WorkManager, etc.).
// 1. Check (BLOCKING — call from a worker thread)
fun checkForUpdate(deploymentKeyOverride: String? = null): NlRemotePackage?
// 2. Download (BLOCKING)
fun downloadUpdate(pkg: NlRemotePackage): NlLocalPackage
// 3. Install
fun installUpdate(
pkg: NlLocalPackage,
installMode: NlInstallMode,
minimumBackgroundDurationSeconds: Double,
) One Promise-returning helper that wraps all three native steps. The client is the first arg.
sync(
client: NitroPushClient,
options?: SyncOptions,
onStatus?: SyncStatusChangedCallback,
onProgress?: DownloadProgressCallback,
): Promise<SyncStatus>;
interface SyncOptions {
/** Default: `ON_NEXT_RESTART`. */
installMode?: InstallMode;
/** Used only for mandatory releases. Default: `IMMEDIATE`. */
mandatoryInstallMode?: InstallMode;
/** For `ON_NEXT_RESUME`: how long the app must have been backgrounded. */
minimumBackgroundDuration?: number;
/** Bypass the configured deployment key for this one call. */
deploymentKey?: string;
/** Show a built-in confirmation dialog before downloading. */
updateDialog?: false | UpdateDialogOptions;
} Install modes
| Mode (JS / Swift / Kotlin) | When the new bundle activates |
|---|---|
IMMEDIATE / .immediate / IMMEDIATE | Right now. JS triggers a reload on completion. Best for: dev / staging / mandatory critical fixes. |
ON_NEXT_RESTART / .onNextRestart / ON_NEXT_RESTART | Next cold start. Recommended for most apps — no user-visible jank. |
ON_NEXT_RESUME / .onNextResume / ON_NEXT_RESUME | Next time the app comes to the foreground after minimumBackgroundDuration seconds. |
ON_NEXT_SUSPEND / .onNextSuspend / ON_NEXT_SUSPEND | When the app is sent to background. Bundle activates on the next foreground without a visible reload. |
The full cycle, end-to-end
Swift async/await. Call from a Task — typically in a BGTaskScheduler handler or a pre-JS launch routine.
import NitroPush
func runUpdate() async {
do {
// 1. Anything new? Returns nil if up to date.
guard let remote = try await NitroPushSdk.shared.checkForUpdate() else {
return
}
// 2. Stream + verify SHA-256.
let local = try await NitroPushSdk.shared.downloadUpdate(remote)
// 3. Stage. The next launch picks it up.
try await NitroPushSdk.shared.installUpdate(
pkg: local,
installMode: .onNextRestart,
minimumBackgroundDuration: 0
)
} catch {
print("nitropush error: \(error.localizedDescription)")
}
} Kotlin coroutines. Run in withContext(Dispatchers.IO) or wrap in a CoroutineWorker / Thread.
import com.nitropush.sdk.NitroPushSdk
import com.nitropush.sdk.NlInstallMode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
suspend fun runUpdate() = withContext(Dispatchers.IO) {
try {
// 1. Anything new?
val remote = NitroPushSdk.shared.checkForUpdate() ?: return@withContext
// 2. Stream + verify SHA-256.
val local = NitroPushSdk.shared.downloadUpdate(remote)
// 3. Stage for next cold start.
NitroPushSdk.shared.installUpdate(
pkg = local,
installMode = NlInstallMode.ON_NEXT_RESTART,
minimumBackgroundDurationSeconds = 0.0
)
} catch (e: Throwable) {
println("nitropush error: ${e.message}")
}
} The default React Native pattern. sync() handles all three steps internally. `client` is the value returned from configureWith().
import { sync, InstallMode } from '@nitropush/react-native';
import { client } from './updates'; // wherever you called configureWith()
// Three native steps, one JS call. Resolves with a SyncStatus.
const status = await sync(client, {
installMode: InstallMode.ON_NEXT_RESTART,
}); Status callback (JS only)
onStatus(status, error?) fires at each transition. The native side doesn’t have an equivalent — each native call returns or throws synchronously, so you observe transitions by where you are in your code.
| Status | Meaning |
|---|---|
CHECKING_FOR_UPDATE | Hitting the server. |
AWAITING_USER_ACTION | Showing the updateDialog (if configured). |
DOWNLOADING_PACKAGE | Streaming bundle + assets. |
INSTALLING_UPDATE | Pointer flip + install scheduling. |
UPDATE_INSTALLED | New bundle activated (or queued, depending on installMode). |
UPDATE_IGNORED | User dismissed the dialog. |
UP_TO_DATE | No new release. |
SYNC_IN_PROGRESS | A previous sync() is still running. |
UNKNOWN_ERROR | Something else broke. error is non-nil. |
Progress callback
Progress events fire throughout the download phase. All three runtimes expose them — the native side via a listener you register, JS via the fourth sync() argument.
Register a listener BEFORE calling downloadUpdate. Listeners fire on the main thread.
let id = NitroPushSdk.shared.addDownloadProgressListener { progress in
let pct = progress.totalBytes > 0
? Int(progress.receivedBytes / progress.totalBytes * 100)
: 0
print("nitropush: \(pct)%")
}
// …later, when you no longer need progress:
NitroPushSdk.shared.removeDownloadProgressListener(listenerId: id) Register a listener BEFORE calling downloadUpdate. Listeners fire on the main thread.
val id = NitroPushSdk.shared.addDownloadProgressListener { progress ->
val pct = if (progress.totalBytes > 0)
(progress.receivedBytes / progress.totalBytes * 100).toInt()
else 0
println("nitropush: $pct%")
}
// …later:
NitroPushSdk.shared.removeDownloadProgressListener(id) Pass a callback as the fourth sync() argument (after client, options, onStatus).
import { useState } from 'react';
import { sync, type DownloadProgress } from '@nitropush/react-native';
import { client } from './updates';
function UpdateScreen() {
const [progress, setProgress] = useState<DownloadProgress | null>(null);
const checkForUpdate = async () => {
const status = await sync(client, {}, undefined, setProgress);
// …handle terminal status
};
return (
<View>
{progress
? <Text>{progress.receivedBytes} / {progress.totalBytes}</Text>
: <Button title="Check for updates" onPress={checkForUpdate} />}
</View>
);
} Common patterns
Silent background update on every launch
The most common pattern. Apps without strict uptime requirements:
From a BGTaskScheduler handler — runs while the app is suspended.
// In your BGAppRefreshTask handler:
Task {
if let remote = try? await NitroPushSdk.shared.checkForUpdate() {
let local = try? await NitroPushSdk.shared.downloadUpdate(remote)
if let local {
try? await NitroPushSdk.shared.installUpdate(
pkg: local,
installMode: .onNextRestart,
minimumBackgroundDuration: 0
)
}
}
} From a CoroutineWorker scheduled with WorkManager — runs while the app is closed.
// Inside CoroutineWorker.doWork():
val remote = NitroPushSdk.shared.checkForUpdate() ?: return Result.success()
val local = NitroPushSdk.shared.downloadUpdate(remote)
NitroPushSdk.shared.installUpdate(
pkg = local,
installMode = NlInstallMode.ON_NEXT_RESTART,
minimumBackgroundDurationSeconds = 0.0
)
Result.success() From a useEffect at app entry — runs after JS boot.
useEffect(() => {
void sync(client, { installMode: InstallMode.ON_NEXT_RESTART });
}, []); The user keeps using the current bundle; the next time they cold-start the app, they’re on the new bundle. No prompts, no jank.
Foreground-resume update
For apps users keep open for a long time. Bundle downloads now, install waits until they’ve backgrounded the app for at least N seconds:
minimumBackgroundDuration is a Double in seconds.
let local = try await NitroPushSdk.shared.downloadUpdate(remote)
try await NitroPushSdk.shared.installUpdate(
pkg: local,
installMode: .onNextResume,
minimumBackgroundDuration: 60
) minimumBackgroundDurationSeconds is a Double in seconds.
val local = NitroPushSdk.shared.downloadUpdate(remote)
NitroPushSdk.shared.installUpdate(
pkg = local,
installMode = NlInstallMode.ON_NEXT_RESUME,
minimumBackgroundDurationSeconds = 60.0
) minimumBackgroundDuration is a Number in seconds.
await sync(client, {
installMode: InstallMode.ON_NEXT_RESUME,
minimumBackgroundDuration: 60,
}); Manual “Check for updates” button (JS pattern)
IMMEDIATE triggers a JS reload as soon as the install completes — appropriate when the user explicitly asked.
const onCheck = async () => {
setBusy(true);
try {
const status = await sync(
client,
{ installMode: InstallMode.IMMEDIATE },
(s) => setStatus(SyncStatus[s]),
setProgress,
);
if (status === SyncStatus.UP_TO_DATE) {
Alert.alert("You're on the latest version.");
}
} finally {
setBusy(false);
}
};
What sync() does NOT do
- It does NOT call
notifyAppReady()for you. You still need to call it from your app’s top-leveluseEffect(or its native equivalent). See Lifecycle for the rollback safety net. - It does NOT throw on “no update”. Returns
UP_TO_DATE(JS) ornil/null(native). - It does NOT retry network failures. A failed call returns
UNKNOWN_ERROR(JS) or throws (native). Either retry on a backoff yourself, or let the next launch’s call pick up where you left off.
Next: Lifecycle →