Host-App Integration Guide
This guide is the complete list of host-app native configuration
required to consume react-native-image-stitcher. The example/
directory in this repo is the canonical reference implementation
of everything described below — if anything here ever drifts from
the example, the example is the source of truth.
If you skip any step on this page, the symptom you see is named in the Troubleshooting section at the end. We strongly suggest scanning that table first if you came here from a crash or error.
Why so much setup?
react-native-image-stitcher is a camera + sensor + native-OpenCV
SDK. To do its job it depends on:
| Dependency | What we use it for |
|---|---|
react-native-vision-camera | The actual camera preview + frame capture |
react-native-sensors | Accelerometer (orientation detection + IMU translation gate) and gyroscope (non-AR pose integration) |
react-native-safe-area-context | UI insets so the shutter sits above the home bar |
Each one imposes a small amount of native-side wiring: Vision Camera
wants permission strings and a podfile declaration; react-native- sensors is a legacy bridge module that needs a mavenCentral() swap
on RN 0.84+ (one patch-package patch). That's it — no Expo
modules infrastructure required as of v0.2.0.
The good news: every step below is mechanical and the example app demonstrates each one. Reading through this page once should be a ~10 minute exercise.
Supported React Native versions
This SDK is currently tested against React Native 0.84.x with
the New Architecture enabled (newArchEnabled=true /
RCTNewArchEnabled=true). Older RN versions may work but the
react-native-sensors jcenter() patch below is RN-0.84 / Gradle-9
specific.
Required peer dependencies
The SDK declares these as peer dependencies in package.json.
Pin the exact versions or use a ^ range as your taste dictates —
the SDK doesn't require a specific patch version of any of them.
{
"dependencies": {
"react-native-sensors": "^7.3.4",
"react-native-vision-camera": "^4.0.0",
"react-native-safe-area-context": "^5.5.2"
}
}
After updating package.json, run:
npm install
cd ios && pod install && cd ..
iOS
1. ios/Podfile
Standard React Native 0.84 Podfile. No extra plugin macros, no post-install patches.
# Resolve react_native_pods.rb with node to allow for hoisting
require Pod::Executable.execute_command('node', ['-p',
'require.resolve(
"react-native/scripts/react_native_pods.rb",
{paths: [process.argv[1]]},
)', __dir__]).strip
platform :ios, min_ios_version_supported
prepare_react_native_project!
linkage = ENV['USE_FRAMEWORKS']
if linkage != nil
Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
use_frameworks! :linkage => linkage.to_sym
end
target 'YourApp' do
config = use_native_modules!
use_react_native!(
:path => config[:reactNativePath],
:app_path => "#{Pod::Config.instance.installation_root}/.."
)
post_install do |installer|
react_native_post_install(
installer,
config[:reactNativePath],
:mac_catalyst_enabled => false,
)
end
end
2. ios/<YourApp>/AppDelegate.swift
Standard React Native 0.84 AppDelegate using RCTReactNativeFactory.
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: "YourApp",
in: window,
launchOptions: launchOptions
)
return true
}
}
class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
override func sourceURL(for bridge: RCTBridge) -> URL? {
self.bundleURL()
}
override func bundleURL() -> URL? {
#if DEBUG
RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
#else
Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}
}
3. ios/<YourApp>/Info.plist — required permission strings
iOS force-kills any app that accesses the camera, motion
sensors, or photo library without a declared usage description.
Every key below is required — even if your app never uses that
subsystem directly, Vision Camera or react-native-sensors will
touch it during init and your app will SIGABRT on launch.
<key>NSCameraUsageDescription</key>
<string>YourApp uses the camera to capture and stitch panoramas.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Vision Camera initializes the microphone even in photo-only mode.</string>
<key>NSMotionUsageDescription</key>
<string>YourApp reads device motion to drive pose-aware panorama capture.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>YourApp saves captured panoramas to your photo library.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>YourApp saves captured panoramas to your photo library.</string>
4. ios/<YourApp>/Info.plist — network for live reload (optional)
If you ever want to develop against the Metro packager on a
physical iPhone (i.e., load JS from your Mac instead of the
bundled main.jsbundle), the iPhone needs to reach
http://<your-mac>:8081. NSAllowsLocalNetworking=true only
covers localhost and .local mDNS — NOT raw LAN IPs.
For dev convenience, set:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
Apple will reject the App Store submission if NSAllowsArbitraryLoads
ships in your Release Info.plist. You can scope it to debug
builds with a separate Info-Debug.plist if you want a clean
production build.
Android
1. android/settings.gradle
Standard React Native 0.84 settings file.
pluginManagement {
includeBuild("../node_modules/@react-native/gradle-plugin")
}
plugins {
id("com.facebook.react.settings")
}
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
rootProject.name = 'YourApp'
include ':app'
includeBuild('../node_modules/@react-native/gradle-plugin')
2. android/build.gradle (top-level)
buildscript {
ext {
buildToolsVersion = "36.0.0"
minSdkVersion = 24
compileSdkVersion = 36
targetSdkVersion = 36
ndkVersion = "27.1.12297006"
kotlinVersion = "2.1.20"
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle")
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
}
}
apply plugin: "com.facebook.react.rootproject"
3. android/gradle.properties
android.useAndroidX=true
newArchEnabled=true
hermesEnabled=true
4. android/app/src/main/java/<your.pkg>/MainApplication.kt
Standard React Native 0.84 MainApplication using
DefaultReactHost.getDefaultReactHost.
package your.pkg
import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
class MainApplication : Application(), ReactApplication {
override val reactHost: ReactHost by lazy {
getDefaultReactHost(
context = applicationContext,
packageList = PackageList(this).packages,
jsMainModulePath = "index",
)
}
override fun onCreate() {
super.onCreate()
loadReactNative(this)
}
}
5. android/app/src/main/AndroidManifest.xml — permissions + ARCore meta-data
[!WARNING] Android silently auto-denies any permission that isn't declared in the manifest. If you omit
<uses-permission android:name="android.permission.CAMERA" />,requestPermissions()returns denied without ever showing the user a system dialog — the SDK will display its own "Camera permission denied" UI and your users will (rightly) be confused. Same trap applies to ARCore's high-rate sensor permission: omitting it doesn't fail at install, it fails insideSession.resume()deep in ARCore's native code at runtime.
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<!-- Required by react-native-vision-camera. Without this,
requestPermissions() silently auto-denies (see warning). -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- Required by ARCore on Android 12+ (API 31) because it
polls accelerometer + gyroscope at >= 200 Hz to fuse pose.
Normal protectionLevel: auto-granted at install, no
runtime prompt needed. Without it, Session.resume() throws
a FatalException from android_sensors.cc. -->
<uses-permission android:name="android.permission.HIGH_SAMPLING_RATE_SENSORS" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature android:name="android.hardware.sensor.gyroscope"
android:required="false" />
<uses-feature android:name="android.hardware.sensor.accelerometer"
android:required="false" />
<application … >
<!-- ARCore meta-data: required by com.google.ar:core.
Without this, ArCoreApk.requestInstall() throws on the
first call. `optional` (not `required`) keeps the app
installable on non-ARCore devices, which fall back to
the non-AR vision-camera + gyro capture path. -->
<meta-data android:name="com.google.ar.core" android:value="optional" />
<activity … />
</application>
</manifest>
android:required="false" on the sensor <uses-feature> lets the
app install on devices that don't have a gyroscope; Play Store won't
filter them out, and the SDK gracefully falls back to non-AR
capture.
patch-package — one required patch
One upstream package needs a patch to compile cleanly against React
Native 0.84. We recommend setting up
patch-package in your
host app and committing the patch under patches/.
patches/react-native-sensors+7.3.6.patch
react-native-sensors@7.3.6 references jcenter() in its
build.gradle — Bintray retired jcenter in 2022 and Gradle 9
removed the alias method entirely. Swap to mavenCentral().
diff --git a/node_modules/react-native-sensors/android/build.gradle b/node_modules/react-native-sensors/android/build.gradle
@@ -6,7 +6,7 @@ def safeExtGet(prop, fallback) {
repositories {
mavenCentral()
google()
- jcenter()
+ mavenCentral()
}
buildscript {
repositories {
google()
- jcenter()
+ mavenCentral()
}
Setting up patch-package
Add to your host app's package.json:
{
"scripts": {
"postinstall": "npx patch-package"
},
"devDependencies": {
"patch-package": "^8.0.0"
}
}
Now every npm install automatically re-applies the patch.
Network access from devices to Metro
| Platform | What happens |
|---|---|
| iOS Simulator | localhost:8081 resolves to the Mac. Works out of the box. |
| iOS Physical Device | localhost on the iPhone = the iPhone, not the Mac. Either run npx react-native run-ios (it injects the Mac's IP into NSUserDefaults) or set the bundler IP manually in the in-app dev menu (Shake → Configure Bundler). |
| Android Emulator | 10.0.2.2:8081 from the emulator hits the host. RN auto-configures this. |
| Android Physical Device | adb reverse tcp:8081 tcp:8081 forwards the device's localhost:8081 over USB to the Mac. npx react-native run-android runs this automatically; re-run if you reconnect USB. |
If you set NSAllowsArbitraryLoads=true (per "iOS — Info.plist
network" above) the iPhone can also reach Metro at the Mac's mDNS
hostname (e.g. http://my-mac.local:8081) which is stable across
DHCP changes.
Troubleshooting
| Symptom | Most likely cause | Fix |
|---|---|---|
Cannot read property 'useContext' of null at runtime | Two copies of React in the bundle (commonly: a nested node_modules/react in the SDK or a file: linked package). Both Reacts have independent context registries. | Ensure only ONE react in find node_modules -name react -type d -path '*node_modules/react'. Add resolver.blockList to Metro config if needed. |
Cannot read property 'eventEmitter' of undefined | react-native-sensors not registered on New Arch. Usually a stale pod install or autolinking cache. | Run cd ios && pod deintegrate && pod install and rebuild. For Android, cd android && ./gradlew clean && cd .. && npx react-native run-android. |
| iOS app SIGABRTs on launch immediately | Missing NSCameraUsageDescription / NSMotionUsageDescription in Info.plist. | Add the keys per "iOS — Info.plist permission strings" above. iOS will not even log this — the only signature is App terminated due to signal 6. in devicectl --console output. The .ips crash log in Settings → Privacy & Security → Analytics & Improvements → Analytics Data will say verbatim "The app's Info.plist must contain an NSCameraUsageDescription key...". |
| Android: SDK shows "Camera permission denied" but no system dialog ever appeared | Missing <uses-permission android:name="android.permission.CAMERA" /> in AndroidManifest.xml. Android silently auto-denies any permission not declared in the manifest — requestPermissions() returns denied without prompting the user. | Add the <uses-permission> line per "Android — AndroidManifest.xml" above and rebuild. This is not a runtime bug — the manifest is the contract for what the app can request. |
Android: AR mode crashes deep in native (android_sensors.cc) | Missing <uses-permission android:name="android.permission.HIGH_SAMPLING_RATE_SENSORS" />. ARCore polls IMU at ≥200 Hz and Android 12+ rate-limits that without the permission. | Add the <uses-permission> line. This is a normal-protection-level permission, no runtime prompt. |
Android: AR mode crashes on ArCoreApk.requestInstall() | Missing <meta-data android:name="com.google.ar.core" android:value="optional" /> inside <application>. | Add the meta-data tag per the manifest section above. |
Android Gradle: Could not find method jcenter() | react-native-sensors@7.3.6 references the retired Bintray jcenter. | Apply the react-native-sensors+7.3.6.patch from above. |
Android Gradle: Gradle requires JVM 17 or later to run. Your build is currently configured to use JVM 11. | Default JDK on macOS/Linux is often Java 11 even when 17 is installed. | Set JAVA_HOME to a Java 17 install before running gradle. Homebrew: export JAVA_HOME=/usr/local/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home. |
iOS Pod install: framework not found 'opencv2' | The npm postinstall fetcher hasn't downloaded the OpenCV xcframework. | Re-run npm install. If you're offline, set SKIP_OPENCV_FETCH=1 and place the framework manually under node_modules/react-native-image-stitcher/ios/Frameworks/. |
iOS Pod install: Unicode Normalization not appropriate for ASCII-8BIT (Ruby 3.4 + CocoaPods 1.16) | Known Ruby-stdlib / CocoaPods interaction bug. | Prepend LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 to the pod install command. |
iOS: dyld: Library not loaded: @rpath/React.framework/React at launch | CocoaPods didn't embed React.framework (a known intermittent issue with the prebuilt React-Core-prebuilt pod). | Run pod deintegrate && pod install from ios/. |
iOS Build: Signing for "YourApp" requires a development team | Xcode signing not configured. | In your.xcodeproj → target settings → Signing & Capabilities, pick your team and ensure CODE_SIGN_STYLE=Automatic. For headless builds, pass -allowProvisioningUpdates to xcodebuild. |
react-native run-ios --device exits with No simulator available with udid "undefined" | RN CLI bug: when no fallback simulator (iPhone 14/13/12/11) is installed, getFallbackSimulator throws even on the device code path. | xcrun simctl create "iPhone 14" "com.apple.CoreSimulator.SimDeviceType.iPhone-14" "com.apple.CoreSimulator.SimRuntime.iOS-17-5" to create a dummy simulator. |
What "fully working" looks like
After everything above is in place, a clean npm install && cd ios && pod install && cd .. && npx react-native run-ios on a
fresh clone should:
- Run
patch-packageautomatically as part of postinstall, applying thereact-native-sensors+7.3.6.patch. - Run the SDK's own postinstall fetcher, pulling the OpenCV
xcframework + Android per-ABI
.sofiles intonode_modules/react-native-image-stitcher/. - Run
pod install, installing ~75 pods (includingVisionCamera,RNSensors,react-native-image-stitcher, and all the standard RN 0.84 pods). - Build + install + launch the iPhone app, which shows the
<Camera>preview. - Tap shutter → photo captured. Hold + pan → panorama stitched.
Any of those steps failing → consult the Troubleshooting table above first. If the symptom isn't listed there, please open an issue at github.com/bhargavkanda/react-native-image-stitcher/issues.