@matiks/react-native-ota
v1.0.4
Published
Over-the-Air (OTA) JavaScript bundle updates for React Native apps on Android and iOS
Downloads
98
Readme
react-native-ota-update
Over-the-Air (OTA) JavaScript bundle updates for React Native apps on Android and iOS.
Features
- ✅ Download and apply JavaScript bundles at runtime
- ✅ ZIP support - Download bundles with assets
- ✅ Works on both Android and iOS
- ✅ Simple API
- ✅ Automatic bundle switching on app restart
- ✅ Bundle verification and error handling
- ✅ Asset extraction and management both Android and iOS
- ✅ Easy to Use: Simple JavaScript API
- ✅ TypeScript Support: Full TypeScript definitions included
- ✅ Secure: Downloads to app's private storage
- ✅ Lightweight: Minimal dependencies
- ✅ Production Ready: Used in production apps
Installation
npm install react-native-ota-update
# or
yarn add react-native-ota-updateiOS Setup
- Install pods:
cd ios && pod install && cd ..- Update your
AppDelegate.mm:
#import "OTAUpdateManager.h"
// In the bundleURL method, add this before returning:
- (NSURL *)bundleURL
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
// Check for OTA update bundle first
NSURL *customBundleURL = [[OTAUpdateManager sharedManager] getCustomBundleURL];
if (customBundleURL) {
return customBundleURL;
}
// Fall back to default bundle
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}Android Setup
- Update your
MainApplication.ktorMainApplication.java:
Kotlin:
import com.otaupdate.OTAUpdateManager
// Note: OTAUpdatePackage is autolinked, no need to add manually to getPackages()
// In ReactNativeHost, add getJSBundleFile():
override fun getJSBundleFile(): String? {
return try {
val otaManager = OTAUpdateManager(this@MainApplication)
val bundleFile = OTAUpdateManager.getBundleFile(this@MainApplication)
// Use OTA bundle if it exists (regardless of pending flag)
if (bundleFile.exists() && bundleFile.length() > 0) {
// Check if file is a ZIP (magic number: 504b0304)
val isZip = try {
val bytes = bundleFile.inputStream().use { it.readNBytes(4) }
bytes.size == 4 && bytes[0] == 0x50.toByte() && bytes[1] == 0x4B.toByte()
} catch (e: Exception) {
false
}
if (isZip) {
// Bundle file is corrupted (ZIP instead of JS), delete it
bundleFile.delete()
otaManager.setPendingUpdate(false)
super.getJSBundleFile()
} else {
// Clear pending flag after first load
if (otaManager.hasPendingUpdate()) {
otaManager.setPendingUpdate(false)
}
bundleFile.absolutePath
}
} else {
super.getJSBundleFile()
}
} catch (e: Exception) {
super.getJSBundleFile()
}
}
// In Application.onCreate(), BEFORE SoLoader.init():
override fun onCreate() {
super.onCreate()
// Setup OTA asset loading BEFORE React Native initializes
try {
val otaInitClass = Class.forName("com.otaupdate.OTAReactContextInitializer")
val setupMethod = otaInitClass.getDeclaredMethod("setup", android.content.Context::class.java)
setupMethod.invoke(otaInitClass.getDeclaredField("INSTANCE").get(null), this)
} catch (e: Exception) {
android.util.Log.e("OTA", "Failed to setup OTA assets: ${e.message}", e)
}
SoLoader.init(this, false)
// ... rest of your onCreate code
}Java:
import com.otaupdate.OTAUpdateManager;
import java.io.File;
import java.io.FileInputStream;
// Note: OTAUpdatePackage is autolinked, no need to add manually to getPackages()
// In ReactNativeHost:
@Override
protected String getJSBundleFile() {
try {
OTAUpdateManager otaManager = new OTAUpdateManager(getApplicationContext());
File bundleFile = OTAUpdateManager.getBundleFile(getApplicationContext());
if (bundleFile.exists() && bundleFile.length() > 0) {
// Check if file is a ZIP
boolean isZip = false;
try {
FileInputStream fis = new FileInputStream(bundleFile);
byte[] bytes = new byte[4];
fis.read(bytes);
fis.close();
isZip = bytes.length == 4 && bytes[0] == 0x50 && bytes[1] == 0x4B;
} catch (Exception e) {
// ignore
}
if (isZip) {
bundleFile.delete();
otaManager.setPendingUpdate(false);
return super.getJSBundleFile();
} else {
if (otaManager.hasPendingUpdate()) {
otaManager.setPendingUpdate(false);
}
return bundleFile.getAbsolutePath();
}
}
return super.getJSBundleFile();
} catch (Exception e) {
return super.getJSBundleFile();
}
}
// In Application.onCreate(), BEFORE SoLoader.init():
@Override
public void onCreate() {
super.onCreate();
// Setup OTA asset loading BEFORE React Native initializes
try {
Class<?> otaInitClass = Class.forName("com.otaupdate.OTAReactContextInitializer");
java.lang.reflect.Method setupMethod = otaInitClass.getDeclaredMethod("setup", android.content.Context.class);
setupMethod.invoke(otaInitClass.getDeclaredField("INSTANCE").get(null), this);
} catch (Exception e) {
android.util.Log.e("OTA", "Failed to setup OTA assets: " + e.getMessage(), e);
}
SoLoader.init(this, false);
// ... rest of your onCreate code
}- Add Kotlin coroutines dependency to
android/app/build.gradle:
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
}- (Optional but Recommended) Add network security configuration for local HTTP server support:
Create android/app/src/main/res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Allow cleartext traffic to localhost for development/testing -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">127.0.0.1</domain>
<domain includeSubdomains="true">10.0.2.2</domain>
</domain-config>
</network-security-config>Update android/app/src/main/AndroidManifest.xml:
<application
...
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true">
...
</application>Usage
import OTAUpdateManager from 'react-native-ota-update';
// Download an update
async function downloadUpdate() {
try {
const result = await OTAUpdateManager.downloadUpdate(
'https://your-server.com/bundle.js'
);
console.log(result); // "Bundle downloaded successfully. Restart app to apply."
// Restart to apply update
await OTAUpdateManager.restartApp();
} catch (error) {
console.error('Download failed:', error);
}
}
// Get bundle information
async function checkUpdate() {
const info = await OTAUpdateManager.getBundleInfo();
console.log('Bundle exists:', info.exists);
console.log('Bundle size:', info.size, 'bytes');
console.log('Pending update:', info.pendingUpdate);
}
// Clear update
async function clearUpdate() {
await OTAUpdateManager.clearUpdate();
console.log('Update cleared');
}API Reference
downloadUpdate(url: string): Promise<string>
Downloads a JavaScript bundle from the specified URL.
Parameters:
url- The URL to download the bundle from
Returns: Promise that resolves with a success message
Example:
await OTAUpdateManager.downloadUpdate('https://example.com/bundle.js');getBundleInfo(): Promise<BundleInfo>
Gets information about the downloaded bundle.
Returns: Promise that resolves with bundle information:
{
exists: boolean; // Whether a bundle exists
size: number; // Size in bytes
path: string; // File system path
pendingUpdate: boolean; // Whether update is pending
}clearUpdate(): Promise<string>
Clears the downloaded bundle and reverts to the original.
Returns: Promise that resolves with a success message
restartApp(): Promise<string>
Restarts the app to apply the update.
Note: On iOS, this only shows a message. Users must manually restart. On Android, the app restarts programmatically.
Returns: Promise that resolves with a success message or instruction
getPlatform(): 'android' | 'ios'
Returns the current platform.
canRestartProgrammatically(): boolean
Returns true for Android, false for iOS.
Creating Bundles
For Android:
npx react-native bundle \
--platform android \
--dev false \
--entry-file index.js \
--bundle-output ./bundle/index.android.bundle \
--assets-dest ./bundle/assetsFor iOS:
npx react-native bundle \
--platform ios \
--dev false \
--entry-file index.js \
--bundle-output ./bundle/main.jsbundle \
--assets-dest ./bundle/assetsFor Expo:
Android:
npx expo export:embed \
--platform android \
--entry-file node_modules/expo/AppEntry.js \
--bundle-output ./bundle/index.android.bundle \
--assets-dest ./bundle/assets \
--dev falseiOS:
npx expo export:embed \
--platform ios \
--entry-file node_modules/expo/AppEntry.js \
--bundle-output ./bundle/main.jsbundle \
--assets-dest ./bundle/assets \
--dev falseServer Setup
You need a server to host your bundles. Here's a simple Express.js example:
const express = require('express');
const app = express();
app.get('/update/android', (req, res) => {
res.sendFile('/path/to/index.android.bundle');
});
app.get('/update/ios', (req, res) => {
res.sendFile('/path/to/main.jsbundle');
});
app.listen(3000);Or use a CDN:
- AWS S3 + CloudFront
- Google Cloud Storage
- Firebase Storage
- Azure Blob Storage
Platform Differences
| Feature | Android | iOS |
|---------|---------|-----|
| Bundle Name | index.android.bundle | main.jsbundle |
| Storage | Internal storage | Documents directory |
| Restart | Programmatic ✅ | Manual ⚠️ |
| Permissions | None needed | None needed |
Security Considerations
Production Checklist
- [ ] Use HTTPS only
- [ ] Add authentication (API keys, JWT)
- [ ] Implement bundle signature verification
- [ ] Add version control
- [ ] Monitor crash rates after updates
- [ ] Implement rollback mechanism
- [ ] Use gradual rollouts
What Can Be Updated
✅ Allowed:
- JavaScript code
- React components
- Business logic
- UI changes
- Bug fixes
❌ Not Allowed:
- Native code (Kotlin/Swift/Objective-C)
- Native dependencies
- App permissions
- AndroidManifest.xml / Info.plist
App Store Guidelines
Google Play (Android)
- ✅ Fully allowed
- ✅ No restrictions
Apple App Store (iOS)
- ✅ Allowed for JavaScript updates
- ⚠️ Don't change core app functionality
- ⚠️ Only update interpreted code (JavaScript)
- ⚠️ Document for App Review if asked
Troubleshooting
Android
Module not found:
cd android && ./gradlew clean && cd ..
npx react-native run-androidBundle not loading:
adb shell run-as <your.package.name> ls -lh files/iOS
Module not found:
cd ios && pod install && cd ..
npx react-native run-iosBundle not loading:
Check Xcode console for [OTAUpdateManager] logs
Example App
See the example directory for a complete working example.
Contributing
Contributions are welcome! Please open an issue or submit a PR.
License
MIT
Credits
Created by [Your Name]
