Expo (bare)
The bare Airborne integration for Expo is the same as the plain React Native flow: the same native dependencies (in.juspay:airborne 2.2.8-rc.25 on Android, the Airborne pod 0.42.0 on iOS), the same HyperOTAServices / AirborneServices wiring, and the same reload-after-download capability. Only a handful of Expo-specific touchpoints differ.
- Run
npx expo prebuildso the nativeandroid/andios/projects exist — the bare flow edits native code directly. - Read React Native (bare) first. This page only lists what changes for Expo; the Airborne wiring itself is unchanged.
For most Expo apps, the React Native SDK Expo track is simpler. Use the bare flow only when you need the low-level control described in the overview.
What is identical
Follow these steps from React Native (bare) unchanged:
- Android — Step 1 (add the Maven repository) and Step 2 (add the
in.juspay:airborne:2.2.8-rc.25dependency). - iOS — Step 4 (add
pod 'Airborne', '0.42.0', thenpod install). - The Airborne initialization itself: creating
HyperOTAServices/AirborneServices, resolving the bundle fromgetIndexBundlePath(), and theonPackageDownloadedhandling.
Android — Expo touchpoints
The Airborne wiring in MainApplication (creating HyperOTAServices, createApplicationManager, loadApplication, and resolving getIndexBundlePath()) is exactly as in the plain React Native flow. Preserve these Expo specifics on top of it:
-
Keep Expo's lifecycle dispatch. Call
ApplicationLifecycleDispatcher.onApplicationCreate(this)at the end ofonCreate(), and set the New Architecture release level, just as Expo's generatedMainApplicationdoes:override fun onCreate() {super.onCreate()DefaultNewArchitectureEntryPoint.releaseLevel = try {ReleaseLevel.valueOf(BuildConfig.REACT_NATIVE_RELEASE_LEVEL.uppercase())} catch (e: IllegalArgumentException) {ReleaseLevel.STABLE}startAirborne() // your Airborne init (unchanged)loadReactNative(this)ApplicationLifecycleDispatcher.onApplicationCreate(this) // keep Expo's dispatch} -
Use Expo's JS entry. Where the plain flow uses
indexas the JS main module, Expo resolves its entry through the virtual metro module.expo/.virtual-metro-entry. Keep that value wherever your host declares the JS main module name — usingindexbreaks Expo's bundling. -
Keep Expo's host / activity wrappers. If your generated project wraps the host in
ReactNativeHostWrapperand the activity delegate inReactActivityDelegateWrapper, keep those wrappers so Expo modules keep working. The Airborne bundle path is still supplied through the resolved bundle path (getIndexBundlePath()) — see the wrapper mechanics in the standard Expo Android setup.
iOS — Expo touchpoints
Your Expo app delegate extends ExpoAppDelegate. Take the AppDelegate from React Native (bare) — Step 5 and apply these three changes:
-
Extend
ExpoAppDelegate(which already declaresreactNativeDelegate/reactNativeFactory), and makeapplication(_:didFinishLaunchingWithOptions:)apublic overridethat callssuper. Expo generates threeapplicationfunctions — update onlydidFinishLaunchingWithOptions; leave the URL/universal-link ones untouched.@mainpublic class AppDelegate: ExpoAppDelegate, AirborneDelegate {var launchOptions: [UIApplication.LaunchOptionsKey: Any]?private var airborne: AirborneServices?private var reactStarted = falsepublic override func application(_ application: UIApplication,didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {self.launchOptions = launchOptionswindow = UIWindow(frame: UIScreen.main.bounds)airborne = AirborneServices(releaseConfigURL: RELEASE_CONFIG_URL, delegate: self)return super.application(application, didFinishLaunchingWithOptions: launchOptions)}// …AirborneDelegate methods unchanged from the RN bare page…} -
Bind the factory and use module name
"main". InbootReactNative, callbindReactNativeFactory(factory)(provided byExpoAppDelegate) before starting, and passwithModuleName: "main"— Expo registers its root component undermain, not your app name:let factory = RCTReactNativeFactory(delegate: delegate)reactNativeDelegate = delegatereactNativeFactory = factorybindReactNativeFactory(factory) // Expo-specificfactory.startReactNative(withModuleName: "main", // Expo registers under "main"in: window,launchOptions: launchOptions) -
Keep the
ReactNativeDelegateexactly as in the plain flow — it still returns the Airborne-resolved bundle URL frombundleURL().
Everything else — the AirborneDelegate methods, the reactStarted guard, and the onPackageDownloaded reload flow — is identical to the plain React Native flow.
Next
- Reload after a post-timeout update — wire
onPackageDownloadedinto a reload prompt (works the same on Expo). - Create & target a release — release creation is unchanged from the dashboard.