Skip to content

riteshshukla04/react-native-nitro-ota

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

102 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

react-native-nitro-ota

Still in Alpha and will have issues

⚑️ High-performance Over-The-Air (OTA) updates for React Native - Powered by Nitro Modules

Download, unzip, and apply JavaScript bundle updates at runtime without going through the App Store or Play Store review process.

✨ Features

  • πŸš€ Native Performance - Built with Nitro Modules for maximum speed
  • 🧡 Off JS Thread - All operations run on different threads, keeping your JS thread free
  • 🌐 Server Agnostic - Works with any CDN, S3, GitHub Releases, or custom server
  • πŸ“¦ Automatic Bundle Management - Handles download, extraction, and cleanup
  • πŸ”’ Version Control - Built-in version checking and management
  • πŸ›‘οΈ Crash Safety - Auto-rollback if a new bundle crashes the app on first launch
  • ↩️ Rollback - Manual rollback to the previous bundle with one call
  • 🚫 Blacklisting - Bad versions are never re-downloaded
  • πŸ“Š Download Progress - Track download progress with a callback

πŸ“¦ Installation

npm install react-native-nitro-ota react-native-nitro-modules
# or
yarn add react-native-nitro-ota react-native-nitro-modules

Note: react-native-nitro-modules is required as this library relies on Nitro Modules.

πŸ“± Platform-Specific Setup (Required!)

Android: Native Bundle Loading

In your MainApplication.kt, add the bundle path loader:

import com.facebook.react.ReactApplication
import com.facebook.react.ReactNativeHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.margelo.nitro.nitroota.core.getStoredBundlePath

class MainApplication : Application(), ReactApplication {

  override val reactNativeHost: ReactNativeHost =
      object : DefaultReactNativeHost(this) {

        override fun getPackages(): List<ReactPackage> =
            PackageList(this).packages

        override fun getJSMainModuleName(): String = "index"

        override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG

        // πŸ”₯ Load OTA bundle if available, otherwise use default
        override fun getJSBundleFile(): String? {
          return getStoredBundlePath(this@MainApplication)
        }
      }
}

If using modern React host:

import com.facebook.react.ReactHost
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.margelo.nitro.nitroota.core.getStoredBundlePath

class MainApplication : Application(), ReactApplication {

  override val reactHost: ReactHost by lazy {
    getDefaultReactHost(
      context = applicationContext,
      packageList = PackageList(this).packages,
      jsBundleFilePath = getStoredBundlePath(applicationContext)
    )
  }
}

iOS: NitroOtaBundleManager

Install pods:

cd ios && pod install

Update AppDelegate.swift:

import UIKit
import React
import NitroOtaBundleManager

class AppDelegate: UIResponder, UIApplicationDelegate {

    override func bundleURL() -> URL? {
        #if DEBUG
        return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
        #else
        // Use OTA bundle if available, otherwise fall back to the bundled file
        return NitroOtaBundleManager.shared.getStoredBundleURL()
            ?? Bundle.main.url(forResource: "main", withExtension: "jsbundle")
        #endif
    }
}

πŸš€ Quick Start

Option 1: GitHub OTA (Easiest! πŸ”₯)

Use the githubOTA helper to point directly to a GitHub repository:

import { githubOTA, OTAUpdateManager } from 'react-native-nitro-ota';

// Configure GitHub URLs
const { downloadUrl, versionUrl } = githubOTA({
  githubUrl: 'https://github.com/your-username/your-ota-repo',
  otaVersionPath: 'ota.version', // or 'ota.version.json' for advanced features
  ref: 'main', // optional, defaults to 'main'
});

// Create update manager
const otaManager = new OTAUpdateManager(downloadUrl, versionUrl);

// Check for updates
const hasUpdate = await otaManager.checkForUpdates();
if (hasUpdate) {
  await otaManager.downloadUpdate();
  otaManager.reloadApp();
}

// Or use advanced JS checking (supports JSON format)
const updateInfo = await otaManager.checkForUpdatesJS();
if (updateInfo?.hasUpdate && updateInfo.isCompatible) {
  console.log('Compatible update available:', updateInfo.remoteVersion);
  await otaManager.downloadUpdate();
  otaManager.reloadApp();
}

Option 2: Custom Server/CDN

import {
  checkForOTAUpdates,
  downloadZipFromUrl,
  reloadApp,
} from 'react-native-nitro-ota';

const hasUpdate = await checkForOTAUpdates('https://your-cdn.com/ota.version');

if (hasUpdate) {
  await downloadZipFromUrl('https://your-cdn.com/bundle.zip');
  reloadApp();
}

Custom Bundle File Path

By default the library auto-detects the bundle file inside the zip by scanning for .bundle (Android) or .jsbundle (iOS). If your zip uses a different file name or extension, pass the relative path as the third argument:

// Bundle is at the zip root with a .js extension
await downloadZipFromUrl(
  'https://your-cdn.com/bundle.zip',
  undefined, // no progress callback
  'index.js'
);

// Bundle is inside a subfolder
await downloadZipFromUrl(
  'https://your-cdn.com/bundle.zip',
  (received, total) => console.log(`${received}/${total}`),
  'build/main.bundle'
);

// Using OTAUpdateManager
const otaManager = new OTAUpdateManager(downloadUrl, versionUrl);
await otaManager.downloadUpdate(undefined, 'dist/index.js');

When omitted (or undefined), the existing auto-detection logic is used β€” no changes needed for existing setups.

πŸ“Š Download Progress

Track download progress with an optional callback:

import { downloadZipFromUrl } from 'react-native-nitro-ota';

await downloadZipFromUrl(
  'https://your-cdn.com/bundle.zip',
  (received, total) => {
    if (received === total) {
      console.log(`Download complete: ${received} bytes`);
    } else if (total > 0) {
      const percent = Math.round((received / total) * 100);
      console.log(`Downloading... ${percent}%`);
    } else {
      console.log(`Downloading... ${received} bytes`);
    }
  }
);

Via OTAUpdateManager:

await otaManager.downloadUpdate((received, total) => {
  setProgress(total > 0 ? received / total : -1);
});

Note: total is -1 during download when the server omits Content-Length. The final callback always fires with received === total (the actual file size) to signal completion β€” use this instead of relying on the Promise resolve if you need the final byte count.

πŸ›‘οΈ Crash Safety & Rollback

How it works

The library uses a "pending confirmation" pattern to protect against bad bundles:

  1. A new bundle is downloaded β†’ ota_pending_validation = true is stored
  2. On the next app launch, the crash handler activates only if pending_validation == true
  3. You call confirmBundle() after verifying your app works β†’ guard is disabled
  4. If the app crashes while unconfirmed β†’ the crash handler automatically rolls back to the previous bundle, blacklists the bad version, and the next launch uses the restored bundle

Important: Crashes in confirmed bundles are completely unaffected β€” the crash handler passes through to your existing crash reporter (Crashlytics, Sentry, etc.).

1. Confirm a bundle after download

import {
  downloadZipFromUrl,
  confirmBundle,
  reloadApp,
} from 'react-native-nitro-ota';

// After download, the bundle is "pending validation"
await downloadZipFromUrl(url);
reloadApp();

// On the new bundle: call confirmBundle() after verifying the app works
// (e.g. after a successful API call, a key screen loading, etc.)
confirmBundle();

2. Manual rollback

import { rollbackToPreviousBundle, reloadApp } from 'react-native-nitro-ota';

const success = await rollbackToPreviousBundle();
if (success) {
  reloadApp(); // restarts on the previous (or original) bundle
}

3. Mark a bundle as bad manually

import { markCurrentBundleAsBad, reloadApp } from 'react-native-nitro-ota';

// Blacklists the current version and rolls back
await markCurrentBundleAsBad('payment_screen_broken');
reloadApp();

4. Listen for rollback events

Subscribe to rollback events in your app root. The callback fires:

  • Immediately if a crash rollback happened during the previous session (detected from persisted history)
  • In the current session when rollbackToPreviousBundle() or markCurrentBundleAsBad() succeeds
import { onRollback } from 'react-native-nitro-ota';

// Register early β€” e.g. at the top of your App component
const unsubscribe = onRollback((record) => {
  console.log('Rollback happened!');
  console.log('  From version:', record.fromVersion);
  console.log('  To version:  ', record.toVersion);
  console.log('  Reason:      ', record.reason);
  console.log('  Timestamp:   ', new Date(record.timestamp).toISOString());

  // Send to your analytics or show a user-facing notice
});

// Call unsubscribe() when the component unmounts

reason values:

Value Meaning
"crash_detected" Crash handler auto-rolled back the bundle
"manual" rollbackToPreviousBundle() was called
"max_rollbacks_exceeded" Rollback counter > 3; reset to original bundle
custom string Passed to markCurrentBundleAsBad(reason)

5. Check rollback history

import { getRollbackHistory } from 'react-native-nitro-ota';

const history = await getRollbackHistory();
// [
//   {
//     timestamp: 1712345678000,
//     fromVersion: "2",
//     toVersion: "1",
//     reason: "crash_detected"
//   },
//   ...
// ]

6. Check & clear the blacklist

import { getBlacklistedVersions } from 'react-native-nitro-ota';

const blacklist = await getBlacklistedVersions();
console.log('Blacklisted versions:', blacklist); // ["2", "3"]

Blacklisted versions are automatically skipped by checkForOTAUpdates() β€” they will never be downloaded again.

Rollback limits

Consecutive rollbacks Behaviour
1–3 Previous bundle is restored
> 3 All OTA data cleared; app falls back to the original .jsbundle

The counter resets to 0 whenever a new bundle is successfully downloaded.

Using OTAUpdateManager (class API)

All rollback features are also available on the class:

const otaManager = new OTAUpdateManager(downloadUrl, versionUrl);

// Listen for rollbacks
const unsub = otaManager.onRollback((record) => {
  console.log('Rollback:', record.reason);
});

// Confirm bundle is working
otaManager.confirm();

// Manual rollback
const ok = await otaManager.rollback();
if (ok) otaManager.reloadApp();

// Mark as bad with a custom reason
await otaManager.markAsBad('checkout_screen_crash');
otaManager.reloadApp();

// Inspect history and blacklist
const history = await otaManager.getHistory();
const blacklist = await otaManager.getBlacklist();

πŸ”„ Background Updates (Experimental)

⚠️ HIGHLY ALPHA FEATURE - This feature is experimental and needs thorough testing. Use with caution in production.

Schedule automatic background checks for updates that run periodically:

import { OTAUpdateManager } from 'react-native-nitro-ota';

const otaManager = new OTAUpdateManager(downloadUrl, versionCheckUrl);

// Schedule background check every hour (3600 seconds)
otaManager.scheduleBackgroundCheck(3600);

Note: Android uses WorkManager (minimum 15-minute interval). iOS uses background tasks (behavior depends on iOS version and system conditions).

πŸ“ Understanding Version Files

Basic: ota.version (Simple Text)

The ota.version file is a simple text file that contains your current bundle version. The version can be anything - numbers, strings, or creative identifiers like "apple", "winter2024", "bugfix-v3".

echo "1.0.0" > ota.version

Advanced: ota.version.json (With Metadata)

For more control, use the JSON format with semantic versioning and target app versions:

{
  "version": "1.2.3",
  "isSemver": true,
  "targetVersions": {
    "android": ["2.30.1", "2.30.2"],
    "ios": ["2.30.1"]
  },
  "releaseNotes": "Bug fixes and improvements"
}

JavaScript API for Advanced Checking:

import { checkForOTAUpdatesJS } from 'react-native-nitro-ota';

const result = await checkForOTAUpdatesJS(
  'https://example.com/ota.version.json'
);
if (result?.hasUpdate && result.isCompatible) {
  console.log(`New version: ${result.remoteVersion}`);
  console.log(`Notes: ${result.metadata?.releaseNotes}`);
}

Note: Both formats are supported. The library automatically detects which one you're using.

πŸ“¦ Creating and Uploading Bundles

1. Generate the JavaScript Bundle

For Android

npx react-native bundle \
  --platform android \
  --dev false \
  --entry-file index.js \
  --bundle-output android/App-Bundles/index.android.bundle \
  --assets-dest android/App-Bundles

For iOS

npx react-native bundle \
  --platform ios \
  --dev false \
  --entry-file index.js \
  --bundle-output ios/App-Bundles/index.jsbundle \
  --assets-dest ios/App-Bundles

2. Package the Bundle

# For Android
cd android && zip -r App-Bundles.zip App-Bundles

# For iOS
cd ios && zip -r App-Bundles.zip App-Bundles

3. Distribute the Bundle

Upload the zipped bundle to your CDN, S3 bucket, GitHub Releases, or any file host.


πŸ”‘ Real-World Example

In the Jellify App:

πŸ“š API Reference

Functions

Function Description
checkForOTAUpdates(url) Returns true if a new version is available
downloadZipFromUrl(url, onProgress?, bundleFilePath?) Downloads and unzips the bundle. Optional progress callback (received, total) => void. Optional bundleFilePath is the relative path to the bundle file inside the zip (e.g. "index.js", "build/main.bundle"); when omitted, auto-detection is used
getStoredOtaVersion() Returns the currently active OTA version string, or null
getStoredUnzippedPath() Returns the path to the active bundle file, or null
reloadApp() Restarts the app to apply a downloaded bundle
confirmBundle() Marks the current bundle as verified β€” disables crash guard
rollbackToPreviousBundle() Rolls back to previous bundle; returns true on success
markCurrentBundleAsBad(reason) Blacklists current bundle and triggers rollback
getBlacklistedVersions() Returns string[] of blacklisted OTA versions
getRollbackHistory() Returns RollbackHistoryRecord[]
onRollback(callback) Subscribes to rollback events; returns an unsubscribe function
checkForOTAUpdatesJS(url?, appVersion?) JS-side version check with detailed result
hasOTAUpdate(url?, appVersion?) Simplified compatible-update check

OTAUpdateManager class

Method Description
checkForUpdates() Native version check
checkForUpdatesJS(appVersion?) JS-side version check
hasCompatibleUpdate(appVersion?) Simple compatible-update check
downloadUpdate(onProgress?, bundleFilePath?) Download with optional progress and custom bundle path
getVersion() Current OTA version
getUnzippedPath() Path to active bundle
reloadApp() Restart the app
confirm() Confirm bundle is working
rollback() Roll back to previous bundle
markAsBad(reason?) Blacklist + rollback with custom reason
getBlacklist() List of blacklisted versions
getHistory() Full rollback history
onRollback(callback) Subscribe to rollback events
scheduleBackgroundCheck(interval) Schedule periodic native background check

RollbackHistoryRecord

interface RollbackHistoryRecord {
  timestamp: number; // Unix ms
  fromVersion: string; // OTA version that was active
  toVersion: string; // Version restored ("original" = no OTA)
  reason: 'crash_detected' | 'manual' | 'max_rollbacks_exceeded' | string; // custom reason from markCurrentBundleAsBad()
}

🀝 Contributing

See CONTRIBUTING.md for development workflow and guidelines.

πŸ“„ License

MIT

About

No description, website, or topics provided.

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors