1.0.0 • Published 5 months ago

@dangtrungthang123/react-native-ota v1.0.0

Weekly downloads
-
License
MIT
Repository
github
Last release
5 months ago

@dangtrungthang123/react-native-ota

A comprehensive React Native library for Over-The-Air (OTA) updates with download, extraction, and automatic bundle loading capabilities for both Android and iOS platforms.

🚀 Features

  • Cross-platform - Works on both Android and iOS
  • Download OTA updates from remote URLs (ZIP files)
  • Extract ZIP bundles automatically
  • Automatic bundle loading - Apps automatically use OTA bundles when available
  • Debug/Release mode detection - Handles development and production scenarios
  • TypeScript support - Full type definitions included
  • New Architecture ready - Built with Turbo Modules
  • Zero configuration - Works out of the box

📦 Installation

npm install @dangtrungthang123/react-native-ota
# or
yarn add @dangtrungthang123/react-native-ota

iOS Setup

Add to your ios/Podfile:

pod 'ReactNativeOta', :path => '../node_modules/@speed/react-native-ota'

Then run:

cd ios && pod install

Android Setup

No additional setup required - auto-linking handles everything!

🔧 Configuration

iOS Bundle Loading (AppDelegate.swift)

import UIKit
import React
import React_RCTAppDelegate
import ReactAppDependencyProvider

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?
  var reactNativeDelegate: ReactNativeDelegate?
  var reactNativeFactory: RCTReactNativeFactory?

  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
  ) -> Bool {
    let delegate = ReactNativeDelegate()
    let factory = RCTReactNativeFactory(delegate: delegate)
    delegate.dependencyProvider = RCTAppDependencyProvider()

    reactNativeDelegate = delegate
    reactNativeFactory = factory

    window = UIWindow(frame: UIScreen.main.bounds)

    factory.startReactNative(
      withModuleName: "YourAppName",
      in: window,
      launchOptions: launchOptions
    )

    return true
  }
}

class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
  override func sourceURL(for bridge: RCTBridge) -> URL? {
    self.bundleURL()
  }

  override func bundleURL() -> URL? {
    // Check for OTA bundle first
    let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
    let otaDirectory = (documentsPath as NSString).appendingPathComponent("otaupdate")
    let otaBundlePath = (otaDirectory as NSString).appendingPathComponent("index.ios.bundle")
    
    if FileManager.default.fileExists(atPath: otaBundlePath) {
      print("OTA: Loading OTA bundle from \(otaBundlePath)")
      return URL(fileURLWithPath: otaBundlePath)
    }
    
    // Fallback to default behavior
#if DEBUG
    print("OTA: Debug mode - using Metro server")
    return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
#else
    print("OTA: Release mode - using main bundle")
    return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
  }
}

Android Bundle Loading (MainApplication.kt)

package your.package.name

import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
import java.io.File

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

        override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
        override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED

        // Override to use OTA bundle when available
        override fun getJSBundleFile(): String? {
          return try {
            // Check if OTA bundle exists
            val otaDir = File(applicationContext.filesDir, "otaupdate")
            val otaBundleFile = File(otaDir, "index.android.bundle")
            
            if (otaBundleFile.exists()) {
              // Use OTA bundle
              android.util.Log.d("OTA", "Loading OTA bundle: ${otaBundleFile.absolutePath}")
              otaBundleFile.absolutePath
            } else {
              // Use default behavior (assets or Metro in debug)
              if (BuildConfig.DEBUG) {
                android.util.Log.d("OTA", "Debug mode: using Metro server")
                null // Let Metro handle it
              } else {
                android.util.Log.d("OTA", "Release mode: using assets bundle")
                super.getJSBundleFile()
              }
            }
          } catch (e: Exception) {
            android.util.Log.e("OTA", "Error checking OTA bundle: ${e.message}")
            super.getJSBundleFile()
          }
        }
      }

  override val reactHost: ReactHost
    get() = getDefaultReactHost(applicationContext, reactNativeHost)

  override fun onCreate() {
    super.onCreate()
    SoLoader.init(this, OpenSourceMergedSoMapping)
    if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
      load()
    }
  }
}

📱 Usage

import {
  downloadAndInstallOTAUpdate,
  checkOTABundleExists,
  isDebugMode,
  getOTADirectory,
  getJSBundleFile,
  downloadOTAUpdate,
  extractOTAUpdate,
} from '@dangtrungthang123/react-native-ota';

// Check current status
const hasOTABundle = checkOTABundleExists();
const debugMode = isDebugMode();
const otaDirectory = getOTADirectory();
const currentBundle = getJSBundleFile();

console.log('OTA Bundle available:', hasOTABundle);
console.log('Debug mode:', debugMode);
console.log('OTA Directory:', otaDirectory);
console.log('Current bundle:', currentBundle);

// Download and install OTA update
async function updateApp() {
  try {
    const success = await downloadAndInstallOTAUpdate(
      'https://your-server.com/updates/app-bundle.zip'
    );
    
    if (success) {
      console.log('Update downloaded successfully!');
      // Restart the app to use new bundle
    } else {
      console.log('Update failed');
    }
  } catch (error) {
    console.error('Update error:', error);
  }
}

// Or use separate download and extract steps
async function manualUpdate() {
  try {
    // Download ZIP file
    const zipPath = await downloadOTAUpdate(
      'https://your-server.com/updates/app-bundle.zip'
    );
    
    if (zipPath) {
      // Extract ZIP file
      const success = await extractOTAUpdate(zipPath);
      console.log('Extract success:', success);
    }
  } catch (error) {
    console.error('Manual update error:', error);
  }
}

🎯 API Reference

Core Functions

downloadAndInstallOTAUpdate(url: string): Promise<boolean>

Downloads a ZIP file from the given URL and extracts it to the OTA directory.

Parameters:

  • url - The URL of the ZIP file containing the update bundle

Returns: Promise that resolves to true if successful, false otherwise


downloadOTAUpdate(url: string): Promise<string | null>

Downloads a ZIP file from the given URL.

Parameters:

  • url - The URL of the ZIP file

Returns: Promise that resolves to the local file path if successful, null otherwise


extractOTAUpdate(zipPath: string): Promise<boolean>

Extracts a ZIP file to the OTA directory.

Parameters:

  • zipPath - Local path to the ZIP file

Returns: Promise that resolves to true if successful, false otherwise


Status Functions

checkOTABundleExists(): boolean

Checks if an OTA bundle is currently available.

Returns: true if OTA bundle exists, false otherwise


isDebugMode(): boolean

Checks if the app is running in debug mode.

Returns: true if in debug mode, false if in release mode


getOTADirectory(): string

Gets the path to the OTA updates directory.

Returns: Absolute path to the OTA directory


getJSBundleFile(): string

Gets the path to the currently loaded JS bundle.

Returns: Path to the current bundle file


multiply(a: number, b: number): number

A simple utility function for testing the library integration.

Parameters:

  • a - First number
  • b - Second number

Returns: The product of a and b

🏗️ Bundle Creation

To create OTA bundles, you can use the React Native CLI:

Android Bundle

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

iOS Bundle

npx react-native bundle \
  --platform ios \
  --dev false \
  --entry-file index.js \
  --bundle-output index.ios.bundle \
  --assets-dest ./assets \
  --sourcemap-output index.ios.bundle.map

Create ZIP Package

# Create a ZIP file containing both bundles
zip -r app-update.zip index.android.bundle index.ios.bundle assets/

🔄 How It Works

  1. Bundle Detection: The app checks for OTA bundles in the designated directory on startup
  2. Automatic Loading: If an OTA bundle exists, it's loaded instead of the default bundle
  3. Fallback: If no OTA bundle is found, the app uses the default bundle (Metro in debug, assets in release)
  4. Download & Extract: The library downloads ZIP files and extracts them to the correct location
  5. Cross-Platform: Works seamlessly on both Android and iOS with platform-specific optimizations

🛡️ Platform Differences

Android

  • OTA bundles stored in: /data/data/[package]/files/otaupdate/
  • Uses index.android.bundle
  • ZIP extraction via native ZIP APIs
  • HTTP downloads with OkHttp

iOS

  • OTA bundles stored in: Documents/otaupdate/
  • Uses index.ios.bundle
  • ZIP extraction via command line unzip
  • HTTP downloads with NSData

🐛 Troubleshooting

Bundle Not Loading

  1. Check if OTA bundle exists: checkOTABundleExists()
  2. Verify bundle file names match platform expectations
  3. Ensure app has proper file permissions
  4. Check logs for bundle loading messages

Download Issues

  1. Verify URL is accessible
  2. Check network permissions
  3. Ensure sufficient storage space
  4. Validate ZIP file format

Debug Mode

  • In debug mode, Metro server takes precedence over OTA bundles
  • Build a release version to test OTA functionality
  • Use isDebugMode() to detect current mode

📄 License

MIT License - see the LICENSE file for details.

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

📞 Support

If you have any questions or issues, please create an issue on GitHub.