sync(client, options, onStatus, onProgress)

The main update flow:

  1. Asks the server: is there anything newer?
  2. If yes, downloads the bundle + assets directly from your storage bucket.
  3. 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 / IMMEDIATERight now. JS triggers a reload on completion. Best for: dev / staging / mandatory critical fixes.
ON_NEXT_RESTART / .onNextRestart / ON_NEXT_RESTARTNext cold start. Recommended for most apps — no user-visible jank.
ON_NEXT_RESUME / .onNextResume / ON_NEXT_RESUMENext time the app comes to the foreground after minimumBackgroundDuration seconds.
ON_NEXT_SUSPEND / .onNextSuspend / ON_NEXT_SUSPENDWhen 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.

StatusMeaning
CHECKING_FOR_UPDATEHitting the server.
AWAITING_USER_ACTIONShowing the updateDialog (if configured).
DOWNLOADING_PACKAGEStreaming bundle + assets.
INSTALLING_UPDATEPointer flip + install scheduling.
UPDATE_INSTALLEDNew bundle activated (or queued, depending on installMode).
UPDATE_IGNOREDUser dismissed the dialog.
UP_TO_DATENo new release.
SYNC_IN_PROGRESSA previous sync() is still running.
UNKNOWN_ERRORSomething 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-level useEffect (or its native equivalent). See Lifecycle for the rollback safety net.
  • It does NOT throw on “no update”. Returns UP_TO_DATE (JS) or nil / 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 →