@dangtrungthang123/react-native-ota v1.0.0
@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-otaiOS Setup
Add to your ios/Podfile:
pod 'ReactNativeOta', :path => '../node_modules/@speed/react-native-ota'Then run:
cd ios && pod installAndroid 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 numberb- 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.mapiOS 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.mapCreate ZIP Package
# Create a ZIP file containing both bundles
zip -r app-update.zip index.android.bundle index.ios.bundle assets/🔄 How It Works
- Bundle Detection: The app checks for OTA bundles in the designated directory on startup
- Automatic Loading: If an OTA bundle exists, it's loaded instead of the default bundle
- Fallback: If no OTA bundle is found, the app uses the default bundle (Metro in debug, assets in release)
- Download & Extract: The library downloads ZIP files and extracts them to the correct location
- 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
- Check if OTA bundle exists:
checkOTABundleExists() - Verify bundle file names match platform expectations
- Ensure app has proper file permissions
- Check logs for bundle loading messages
Download Issues
- Verify URL is accessible
- Check network permissions
- Ensure sufficient storage space
- 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.
5 months ago