Skip to content

feat: Expo managed workflow (expo-dev-client) support for iOS#42

Draft
CAMOBAP wants to merge 2 commits into
mainfrom
feat/expo-managed-workflow-support
Draft

feat: Expo managed workflow (expo-dev-client) support for iOS#42
CAMOBAP wants to merge 2 commits into
mainfrom
feat/expo-managed-workflow-support

Conversation

@CAMOBAP

@CAMOBAP CAMOBAP commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds support for running @callstack/react-native-sandbox inside an Expo managed workflow app that uses expo-dev-client (tested on SDK 54 / RN 0.81.4).

Problem

Expo dev-client does not configure RCTBundleURLProvider, so jsBundleURLForBundleRoot: returns nil. Additionally, EXDevLauncherController.sourceUrl (the host app's Metro bundle URL) is set asynchronously ~1–2s after launch, after a deep-link network check completes — making it nil at the time the sandbox view first tries to load.

Solution

  • Runtime ObjC dispatch (objc_getClass / performSelector:) to access EXDevLauncherController without a hard compile-time dependency on expo-dev-launcher headers (which pull in Swift-generated headers that fail to compile from a library pod)
  • KVO on EXDevLauncherController.sourceUrl to retry sandbox load once the URL becomes available, instead of polling or relying on RCTJavaScriptDidLoadNotification (which doesn't fire in New Architecture / bridgeless mode)
  • Clean URL construction — Expo's sourceUrl contains transform params (transform.routerRoot, transform.bytecode, etc.) that break Metro when applied to the sandbox bundle; only standard params (platform=ios&dev=true&hot=false) are used

Changes

Library (packages/react-native-sandbox/ios/)

  • SandboxReactNativeDelegate.mm — Expo URL derivation in bundleURL via runtime ObjC dispatch
  • SandboxReactNativeDelegate.h — expose bundleURL as a public method
  • SandboxReactNativeViewComponentView.mm — KVO observer on sourceUrl with proper lifecycle (cleanup in prepareForRecycle and dealloc), typed KVO context pointer

Demo app (apps/expo-demo/)

  • Minimal Expo SDK 54 managed-workflow app exercising the sandbox inside expo-dev-client
  • Replaces the orphaned apps/expo/ directory (abandoned WIP with no package.json)
  • Metro config pins react to workspace root to prevent bun from hoisting a stale transitive version

What's not included (follow-up)

  • Android support
  • CI matrix entry for expo-demo
  • Expo config plugin

Test plan

  • iOS: cd apps/expo-demo && npx expo run:ios (requires expo-dev-client build)
  • Open via deep link: exp+expo-demo://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8082
  • Sandbox panel renders alongside host app panel
  • Error boundary (onError) fires correctly for sandbox errors
  • "Increment" counter works without React version mismatch errors

🤖 Generated with Claude Code

CAMOBAP and others added 2 commits July 24, 2026 12:19
- 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 <noreply@anthropic.com>
- Fix stale comment referencing RCTJavaScriptDidLoadNotification (the
  implementation uses KVO, not the notification)
- Forward unhandled KVO observations to super in
  observeValueForKeyPath:ofObject:change:context:
- Remove duplicate #import <objc/runtime.h> in SandboxReactNativeDelegate.mm
- Use a typed static context pointer (kExpoSourceUrlKVOContext) for KVO
  registration/removal instead of nil, preventing accidental dispatch of
  superclass observations into our handler
- Correct infiniteLoop comment: hangs sandbox JS thread only, host app
  stays responsive (each sandbox runs in its own Hermes instance)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant