Overview

B3 OTA (Over-The-Air) provides a universal infrastructure to update your React Native application's logic and assets instantly. It eliminates the need for App Store or Google Play resubmissions for non-native changes, ensuring your users always have the latest features and fixes.

Universal Workflow

1. Build JS Bundle → 2. Package to .zip → 3. Upload to OTA Dashboard → 4. App auto-checks for updates → 5. Download & Apply updates.

Installation

Integrate the B3 OTA core SDK into your project using your preferred package manager.

npm install react-native-ota-hot-update react-native-blob-util

Then run pod install (iOS) or gradle sync (Android) to install native dependencies.

iOS Integration (Xcode)

Open your project in Xcode and modify AppDelegate.mm to allow the B3 engine to resolve the dynamic bundle path.

1 Source URL Resolution

#import 

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
#if DEBUG
  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
  // B3 Manager resolves the highest priority local bundle
  return [B3OTAManager getActiveBundleURL]; 
#endif
}

Android Integration (Studio)

Modify MainApplication.kt (or .java) to point the React host to the B3-managed storage path.

1 Bundle Path Override

import com.b3.ota.B3OTAManager

override fun getJSBundleFile(): String? {
    // B3OTAManager checks for OTA updates, fallback to assets
    return B3OTAManager.resolveBundlePath(applicationContext) 
}

JS Initialization

Initialize OTA update checking service in your React Native application.

Create OTA Service

Create src/services/ota/otaService.ts

import hotUpdate from 'react-native-ota-hot-update';
import ReactNativeBlobUtil from 'react-native-blob-util';
import * as Device from 'expo-device';
import * as Application from 'expo-application';

const OTA_CONFIG = {
  apiUrl: 'https://ota.2maru.com',
  apiKey: 'your_api_key_here',
  bundleId: 'com.yourcompany.app',
  platform: Platform.OS,
  deviceId: '', // Will be set during initialization
};

export async function checkForUpdates() {
  const response = await fetch(`${OTA_CONFIG.apiUrl}/api/v1/ota/check-update`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': OTA_CONFIG.apiKey,
    },
    body: JSON.stringify({
      bundleId: OTA_CONFIG.bundleId,
      platform: OTA_CONFIG.platform,
      currentVersion: '1.0.0',
      deviceId: OTA_CONFIG.deviceId,
    }),
  });
  
  const data = await response.json();
  return data.data;
}

export async function performUpdate(onProgress) {
  const updateInfo = await checkForUpdates();
  
  if (!updateInfo?.updateAvailable) {
    return { success: false, message: 'No update available' };
  }

  return new Promise((resolve) => {
    hotUpdate.downloadBundleUri(
      ReactNativeBlobUtil,
      updateInfo.downloadUrl,
      updateInfo.latestVersion,
      {
        updateSuccess: () => resolve({ success: true }),
        updateFail: (msg) => resolve({ success: false, message: msg }),
        updateProgress: onProgress,
        restartAfterInstall: true,
      }
    );
  });
}

Check for Updates on App Launch

// App.tsx ??UpdateScreen.tsx
import { checkForUpdates, performUpdate } from './services/ota/otaService';

useEffect(() => {
  const checkAndUpdate = async () => {
    const updateInfo = await checkForUpdates();
    
    if (updateInfo?.updateAvailable) {
      await performUpdate((progress) => {
        console.log('Download progress:', progress);
      });
    }
  };
  
  checkAndUpdate();
}, []);

Bundle Packaging

Prepare your application assets and package them into .zip files for OTA platform upload. Below are packaging scripts for various frameworks.

1 Expo (Recommended)

Use Expo's built-in tools to export JS Bundle

# iOS
npx expo export:embed --platform ios --entry-file index.js --bundle-output ios/main.jsbundle --assets-dest ios/assets
cd ios && zip -r main.jsbundle.zip main.jsbundle assets && cd ..

# Android  
npx expo export:embed --platform android --entry-file index.js --bundle-output android/index.android.bundle --assets-dest android/assets
cd android && zip -r index.android.bundle.zip index.android.bundle assets && cd ..

2 React Native CLI (Pure RN)

# iOS
npx react-native bundle \
  --platform ios \
  --dev false \
  --entry-file index.js \
  --bundle-output ios/main.jsbundle \
  --assets-dest ios/assets
cd ios && zip -r main.jsbundle.zip main.jsbundle assets && cd ..

# Android
npx react-native bundle \
  --platform android \
  --dev false \
  --entry-file index.js \
  --bundle-output android/index.android.bundle \
  --assets-dest android/assets
cd android && zip -r index.android.bundle.zip index.android.bundle assets && cd ..

3 Using Xcode Build Script (iOS)

Add Build Phase Script in Xcode

#!/bin/bash

set -e

BUNDLE_FILE="$CONFIGURATION_BUILD_DIR/main.jsbundle"
ASSETS_DIR="$CONFIGURATION_BUILD_DIR/assets"

# Export Bundle
export NODE_BINARY=node
../node_modules/react-native/scripts/react-native-xcode.sh

# Package as zip
cd "$CONFIGURATION_BUILD_DIR"
zip -r main.jsbundle.zip main.jsbundle assets
echo "Bundle packaged: main.jsbundle.zip"

4 Using Gradle Task (Android)

Add to android/app/build.gradle

task bundleReleaseJsAndAssets(type: Exec) {
    def bundleFile = "$buildDir/intermediates/assets/release/index.android.bundle"
    def assetsDir = "$buildDir/intermediates/assets/release"
    
    commandLine "npx", "react-native", "bundle",
        "--platform", "android",
        "--dev", "false",
        "--entry-file", "index.js",
        "--bundle-output", bundleFile,
        "--assets-dest", assetsDir
    
    doLast {
        exec {
            workingDir buildDir
            commandLine "zip", "-r", "index.android.bundle.zip", 
                "intermediates/assets/release/index.android.bundle",
                "intermediates/assets/release/assets"
        }
    }
}

// Run before assembleRelease
assembleRelease.dependsOn bundleReleaseJsAndAssets

5 Automation Script (Node.js)

Create scripts/build-bundle.js

const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');

const platform = process.argv[2]; // 'ios' or 'android'
const outputDir = platform === 'ios' ? 'ios' : 'android';
const bundleName = platform === 'ios' ? 'main.jsbundle' : 'index.android.bundle';

// 1. Build Bundle
console.log(`Building ${platform} bundle...`);
execSync(`npx react-native bundle \\
  --platform ${platform} \\
  --dev false \\
  --entry-file index.js \\
  --bundle-output ${outputDir}/${bundleName} \\
  --assets-dest ${outputDir}/assets`, { stdio: 'inherit' });

// 2. Package as zip
console.log('Creating zip file...');
execSync(`cd ${outputDir} && zip -r ${bundleName}.zip ${bundleName} assets`, 
  { stdio: 'inherit' });

console.log(`??Bundle ready: ${outputDir}/${bundleName}.zip`);

// Usage:
// node scripts/build-bundle.js ios
// node scripts/build-bundle.js android
Important Note

Files uploaded to OTA platform must be .zip files containing the Bundle file and assets folder. Ensure zip file names meet platform requirements (iOS: main.jsbundle.zip, Android: index.android.bundle.zip).

Hermes Bytecode (Optional)

If your app uses Hermes engine, you can pre-compile Bundle to HBC format for better performance. Note: Current OTA system supports standard JS Bundle, HBC support is under development.

# Compile to HBC (Future support)
./node_modules/react-native/sdks/hermesc/osx-bin/hermesc \
  -emit-binary \
  -out index.android.hbc \
  index.android.bundle

# Package
zip -r index.android.bundle.zip index.android.hbc assets

Dashboard Operations Guide

Complete OTA management platform workflow, from creating organizations to releasing updates.

Step 1: Create Organization

  1. Click "New Organization" button in top right
  2. Enter organization name (e.g., "AMC Group")
  3. Add optional description
  4. Click "Create"

Step 2: Add Project (Application)

  1. Click "Add Project" in Organization card
  2. Fill in project information:
    • Project Name: Application name
    • Bundle ID: com.company.app
    • Platform: Select iOS or Android
  3. Click "Create Project"

Step 3: Create Test Group (Optional)

  1. Go to "Test Groups" page
  2. Click "New Test Group"
  3. Enter test group name (e.g., "QA Team")
  4. Select target Project
  5. Add Device IDs of test devices

Step 4: Upload Bundle

  1. Click Project card to enter details page
  2. Click "Upload OTA Bundle" button
  3. Enter version number (e.g., 1.0.8)
  4. Select Initial Release Strategy:
    • Public Release: Release to all users immediately
    • Staged Rollout: Gradual release (10%, 25%, 50%)
    • Beta Release: Release to Beta test group
    • Internal Testing: Internal test group only
  5. If Beta/Internal selected, choose corresponding Test Group
  6. Drag & drop .zip file or click to select file
  7. Click "Start Upload"

Step 5: Manage Release Status

TESTING

Click "Promote" to upgrade to ACTIVE (public release)

ACTIVE

Click "Pause" to pause release (when issues found)

PAUSED

Click "Resume" to restore release or "Archive" to permanently archive

OTA Release Strategies

Choose the right release strategy to ensure update stability and safety.

Public Release

Release to all users immediately, suitable for stable updates.

Use Cases: Bug fixes, minor feature updates

Staged Rollout

Gradual release (10% ??25% ??50% ??100%), reducing risk.

Use Cases: Major feature updates, architecture changes

Beta Release

Release to Beta test group, collect user feedback.

Use Cases: New feature testing, UX validation

Internal Testing

Internal team only, for development testing.

Use Cases: Development phase, QA testing

Recommended Release Flow

1 Internal Testing
2 Beta Release
3 Staged Rollout
4 Public Release
Emergency Rollback

If a critical bug is found, immediately click "Pause" to stop the current version, then click "Resume" on the previous stable version. Users will automatically rollback on next update check.