From 6bfb1c61dbbf69f3bb005aa451b507362a451f39 Mon Sep 17 00:00:00 2001 From: Aliaksandr Babrykovich Date: Fri, 24 Jul 2026 12:19:04 +0200 Subject: [PATCH 1/2] feat: add Expo managed workflow (expo-dev-client) support for iOS - Add runtime ObjC dispatch via objc_getClass/performSelector to derive the sandbox bundle URL from EXDevLauncherController.sourceUrl without a hard compile-time dependency on expo-dev-launcher headers - Add KVO on EXDevLauncherController.sourceUrl in SandboxReactNativeViewComponentView to retry sandbox load once the Expo host bundle URL becomes available (set asynchronously after deep-link network check, ~1-2s after launch) - Expose bundleURL as a public method on SandboxReactNativeDelegate - Add apps/expo-demo: a minimal Expo SDK 54 / RN 0.81.4 managed-workflow app that exercises the sandbox inside an expo-dev-client build - Remove orphaned apps/expo/ (abandoned WIP native build output, no package.json), replace with apps/expo-demo using managed workflow - Pin metro extraNodeModules.react to workspace root to avoid bun hoisting a stale react@19.0.0 into the app's local node_modules Co-Authored-By: Claude Sonnet 4.6 --- apps/expo-demo/.gitignore | 2 + apps/expo-demo/App.tsx | 71 + apps/expo-demo/CrashIfYouCanDemo.tsx | 67 + apps/expo-demo/app.json | 17 + apps/expo-demo/babel.config.js | 6 + apps/expo-demo/index.js | 4 + apps/expo-demo/metro.config.js | 31 + apps/expo-demo/package.json | 26 + apps/expo-demo/sandbox.js | 7 + apps/expo-demo/tsconfig.json | 6 + .../com/callstack/expodemo/MainActivity.kt | 61 - .../com/callstack/expodemo/MainApplication.kt | 57 - .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 - .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 - .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 8031 -> 0 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 5079 -> 0 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 11145 -> 0 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 18064 -> 0 bytes .../ic_launcher_foreground.webp | Bin 25030 -> 0 bytes .../app/src/main/res/values-night/colors.xml | 1 - .../ios/ExpoDemo.xcodeproj/project.pbxproj | 572 -------- .../xcshareddata/xcschemes/ExpoDemo.xcscheme | 88 -- .../contents.xcworkspacedata | 10 - apps/expo/ios/ExpoDemo/AppDelegate.swift | 70 - .../ios/ExpoDemo/ExpoDemo-Bridging-Header.h | 3 - apps/expo/ios/ExpoDemo/ExpoDemo.entitlements | 5 - .../App-Icon-1024x1024@1x.png | Bin 59468 -> 0 bytes .../AppIcon.appiconset/Contents.json | 14 - .../ExpoDemo/Images.xcassets/Contents.json | 6 - .../Contents.json | 20 - .../SplashScreenLogo.imageset/Contents.json | 23 - .../SplashScreenLogo.imageset/image.png | Bin 60870 -> 0 bytes .../SplashScreenLogo.imageset/image@2x.png | Bin 60870 -> 0 bytes .../SplashScreenLogo.imageset/image@3x.png | Bin 60870 -> 0 bytes apps/expo/ios/ExpoDemo/Info.plist | 76 -- apps/expo/ios/ExpoDemo/PrivacyInfo.xcprivacy | 48 - .../expo/ios/ExpoDemo/SplashScreen.storyboard | 44 - apps/expo/ios/ExpoDemo/Supporting/Expo.plist | 12 - bun.lock | 1180 ++++++++++++++++- .../ios/SandboxReactNativeDelegate.h | 7 + .../ios/SandboxReactNativeDelegate.mm | 34 +- .../SandboxReactNativeViewComponentView.mm | 68 +- 42 files changed, 1470 insertions(+), 1176 deletions(-) create mode 100644 apps/expo-demo/.gitignore create mode 100644 apps/expo-demo/App.tsx create mode 100644 apps/expo-demo/CrashIfYouCanDemo.tsx create mode 100644 apps/expo-demo/app.json create mode 100644 apps/expo-demo/babel.config.js create mode 100644 apps/expo-demo/index.js create mode 100644 apps/expo-demo/metro.config.js create mode 100644 apps/expo-demo/package.json create mode 100644 apps/expo-demo/sandbox.js create mode 100644 apps/expo-demo/tsconfig.json delete mode 100644 apps/expo/android/app/src/main/java/com/callstack/expodemo/MainActivity.kt delete mode 100644 apps/expo/android/app/src/main/java/com/callstack/expodemo/MainApplication.kt delete mode 100644 apps/expo/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml delete mode 100644 apps/expo/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml delete mode 100644 apps/expo/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp delete mode 100644 apps/expo/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp delete mode 100644 apps/expo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp delete mode 100644 apps/expo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp delete mode 100644 apps/expo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp delete mode 100644 apps/expo/android/app/src/main/res/values-night/colors.xml delete mode 100644 apps/expo/ios/ExpoDemo.xcodeproj/project.pbxproj delete mode 100644 apps/expo/ios/ExpoDemo.xcodeproj/xcshareddata/xcschemes/ExpoDemo.xcscheme delete mode 100644 apps/expo/ios/ExpoDemo.xcworkspace/contents.xcworkspacedata delete mode 100644 apps/expo/ios/ExpoDemo/AppDelegate.swift delete mode 100644 apps/expo/ios/ExpoDemo/ExpoDemo-Bridging-Header.h delete mode 100644 apps/expo/ios/ExpoDemo/ExpoDemo.entitlements delete mode 100644 apps/expo/ios/ExpoDemo/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png delete mode 100644 apps/expo/ios/ExpoDemo/Images.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 apps/expo/ios/ExpoDemo/Images.xcassets/Contents.json delete mode 100644 apps/expo/ios/ExpoDemo/Images.xcassets/SplashScreenBackground.colorset/Contents.json delete mode 100644 apps/expo/ios/ExpoDemo/Images.xcassets/SplashScreenLogo.imageset/Contents.json delete mode 100644 apps/expo/ios/ExpoDemo/Images.xcassets/SplashScreenLogo.imageset/image.png delete mode 100644 apps/expo/ios/ExpoDemo/Images.xcassets/SplashScreenLogo.imageset/image@2x.png delete mode 100644 apps/expo/ios/ExpoDemo/Images.xcassets/SplashScreenLogo.imageset/image@3x.png delete mode 100644 apps/expo/ios/ExpoDemo/Info.plist delete mode 100644 apps/expo/ios/ExpoDemo/PrivacyInfo.xcprivacy delete mode 100644 apps/expo/ios/ExpoDemo/SplashScreen.storyboard delete mode 100644 apps/expo/ios/ExpoDemo/Supporting/Expo.plist diff --git a/apps/expo-demo/.gitignore b/apps/expo-demo/.gitignore new file mode 100644 index 0000000..8c0a260 --- /dev/null +++ b/apps/expo-demo/.gitignore @@ -0,0 +1,2 @@ +ios/ +android/ diff --git a/apps/expo-demo/App.tsx b/apps/expo-demo/App.tsx new file mode 100644 index 0000000..4339566 --- /dev/null +++ b/apps/expo-demo/App.tsx @@ -0,0 +1,71 @@ +import SandboxReactNativeView from '@callstack/react-native-sandbox' +import React, {useState} from 'react' +import {SafeAreaView, StyleSheet, Text, View} from 'react-native' + +import CrashIfYouCanDemo from './CrashIfYouCanDemo' + +const DemoApp: React.FC = () => { + const [lastError, setLastError] = useState(null) + + return ( + + + + Main App + + + + Sandboxed + {lastError && {lastError}} + { + const message = `${error.isFatal ? '[fatal]' : '[warn]'} ${error.name}: ${error.message}` + console.warn('Sandbox error:', message) + setLastError(message) + return false + }} + /> + + + + ) +} + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + }, + container: { + flex: 1, + flexDirection: 'row', + padding: 16, + }, + columnSandbox: { + borderWidth: 1, + borderColor: '#8232ff', + borderRadius: 4, + }, + column: { + flex: 1, + padding: 8, + }, + header: { + fontWeight: 'bold', + fontSize: 16, + marginBottom: 8, + textAlign: 'center', + }, + sandboxView: { + flex: 1, + }, + errorText: { + color: 'red', + fontSize: 11, + marginBottom: 4, + }, +}) + +export default DemoApp diff --git a/apps/expo-demo/CrashIfYouCanDemo.tsx b/apps/expo-demo/CrashIfYouCanDemo.tsx new file mode 100644 index 0000000..672b98f --- /dev/null +++ b/apps/expo-demo/CrashIfYouCanDemo.tsx @@ -0,0 +1,67 @@ +import React, {useState} from 'react' +import { + Button, + LogBox, + NativeModules, + ScrollView, + StyleSheet, + View, +} from 'react-native' + +export default function CrashIfYouCanDemo() { + const [counter, setCounter] = useState(0) + + const triggerCrash = () => { + // @ts-ignore + global.nonExistentMethod() // Should crash the app + } + + const overwriteGlobal = () => { + // Overwrite console.log to something harmful + console.log = () => { + throw new Error('console.log has been hijacked!') + } + console.log('This will now throw') // This will crash or break logs + } + + const accessBlockedTurboModule = () => { + const FileReaderModule = NativeModules.FileReaderModule + FileReaderModule.readAsText('/some/file.txt') + .then((text: string) => console.log(text)) + .catch((err: any) => console.log(err.message)) + } + + const infiniteLoop = () => { + while (true) {} + } + + const incrementCounter = () => { + setCounter(counter + 1) + } + + return ( + +