npm package: @matiks/react-native-ota
Description: Over-the-Air (OTA) JavaScript bundle updates for React Native apps on Android and iOS
Author: Rishav Barnwal
Latest version: 1.0.4

# 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

```bash
npm install react-native-ota-update
# or
yarn add react-native-ota-update
```

### iOS Setup

1. Install pods:
```bash
cd ios && pod install && cd ..
```

2. Update your `AppDelegate.mm`:

```objc
#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

1. Update your `MainApplication.kt` or `MainApplication.java`:

**Kotlin:**
```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:**
```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
}
```

2. Add Kotlin coroutines dependency to `android/app/build.gradle`:

```gradle
dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
}
```

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
<?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`:
```xml
<application
    ...
    android:networkSecurityConfig="@xml/network_security_config"
    android:usesCleartextTraffic="true">
    ...
</application>
```

## Usage

```javascript
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:**
```javascript
await OTAUpdateManager.downloadUpdate('https://example.com/bundle.js');
```

### `getBundleInfo(): Promise<BundleInfo>`

Gets information about the downloaded bundle.

**Returns:** Promise that resolves with bundle information:
```typescript
{
  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:

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

### For iOS:

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

### For Expo:

**Android:**
```bash
npx expo export:embed \
  --platform android \
  --entry-file node_modules/expo/AppEntry.js \
  --bundle-output ./bundle/index.android.bundle \
  --assets-dest ./bundle/assets \
  --dev false
```

**iOS:**
```bash
npx expo export:embed \
  --platform ios \
  --entry-file node_modules/expo/AppEntry.js \
  --bundle-output ./bundle/main.jsbundle \
  --assets-dest ./bundle/assets \
  --dev false
```

## Server Setup

You need a server to host your bundles. Here's a simple Express.js example:

```javascript
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:**
```bash
cd android && ./gradlew clean && cd ..
npx react-native run-android
```

**Bundle not loading:**
```bash
adb shell run-as <your.package.name> ls -lh files/
```

### iOS

**Module not found:**
```bash
cd ios && pod install && cd ..
npx react-native run-ios
```

**Bundle not loading:**
Check Xcode console for `[OTAUpdateManager]` logs

## Example App

See the [example](./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]

## Support

- 🐛 [Report a bug](https://github.com/matiks-com/react-native-ota/issues)
- 💡 [Request a feature](https://github.com/matiks-com/react-native-ota/issues)
- 📖 [Documentation](https://github.com/matiks-com/react-native-ota#readme)