fix(ios): CoreLocation import + wrap TurboModule constants as callable functions#357
Conversation
CLLocationManager/CLLocation are used for mock-location detection on iOS 15+ without importing CoreLocation, causing compile failures when RCT_NEW_ARCH_ENABLED is set (GantMan#354). Link CoreLocation in the podspec so the framework is available at link time. Made-with: Cursor
Expo 55 / RN 0.83 dev-client example with local jail-monkey via file:.. Postinstall copies library files so iOS autolinking sees a real podspec. Add install-example-expo55 script at repo root.
The native iOS module exports isJailBroken, canMockLocation, etc. via constantsToExport as plain booleans. The old JS entry point only wrapped these into functions for the legacy NativeModules path — when the TurboModule loaded successfully, callers got raw booleans, causing "isJailBroken is not a function" crashes on new-arch builds. Now the wrapping is applied uniformly regardless of module source. Also updates ExampleExpo55 tooling: tarball-based install, build scripts, and removes deleted android source files.
Accidentally included android/ deletions from working tree in the previous commit. These files are the entire Android native implementation.
|
please merge this |
There was a problem hiding this comment.
Pull request overview
This PR improves iOS New Architecture compatibility by ensuring CoreLocation is linked/imported and by normalizing the JS API surface so TurboModule and legacy NativeModules callers get consistent callable methods instead of raw constants (avoiding "isJailBroken is not a function" crashes). It also adds an Expo SDK 55 example app and local packaging/build scripts to validate new-arch behavior end-to-end.
Changes:
- Add CoreLocation framework linkage (podspec) and header import (ObjC++) for new-arch iOS builds.
- Update JS entrypoint to detect/load the TurboModule via
try/catchand wrap exported “constant” values into callable functions uniformly. - Introduce
ExampleExpo55/and supporting scripts/ignore rules for local tarball-based testing.
Reviewed changes
Copilot reviewed 11 out of 16 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| package.json | Adds bun-based pack/install/build scripts for the Expo 55 example workflow. |
| jail-monkey.podspec | Links CoreLocation framework for iOS builds. |
| index.tsx | Reworks TurboModule detection + wraps constants into callable functions across module sources. |
| JailMonkey/JailMonkey.mm | Imports CoreLocation header needed by code paths using CLLocationManager. |
| ExampleExpo55/tsconfig.json | Adds strict TS config for the Expo 55 example app. |
| ExampleExpo55/package.json | Defines Expo 55 example dependencies and scripts using the packed tarball. |
| ExampleExpo55/index.tsx | Registers a SafeArea-wrapped root component for the example app. |
| ExampleExpo55/App.tsx | Example UI exercising the module APIs (new-arch oriented). |
| ExampleExpo55/app.json | Expo app configuration and asset wiring. |
| ExampleExpo55/.gitignore | Ignores generated native folders and typical Expo artifacts. |
| ExampleExpo55/assets/* | Adds icon/splash assets for the Expo example app. |
| .npmignore | Excludes ExampleExpo55 and local artifacts/ from npm package. |
| .gitignore | Ignores local artifacts/ output and .vscode/settings.json. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -6,9 +7,53 @@ const LINKING_ERROR = | |||
| '- You rebuilt the app after installing the package\n' + | |||
| '- You are not using Expo Go\n'; | |||
|
|
|||
| // @ts-expect-error | |||
| const isTurboModuleEnabled = global.__turboModuleProxy != null; | |||
| // Prefer TurboModule when available. | |||
| // Expo SDK / RN setup can make `global.__turboModuleProxy` unreliable, so | |||
| // we attempt to load the TurboModule and fall back to legacy NativeModules. | |||
| let turboModule: any = null; | |||
| try { | |||
| // This will throw if the TurboModule isn't registered/available. | |||
| // eslint-disable-next-line @typescript-eslint/no-var-requires | |||
| turboModule = require('./specs/NativeJailMonkey.ts').default; | |||
| } catch { | |||
| turboModule = null; | |||
| } | |||
|
|
|||
| export default isTurboModuleEnabled | |||
| ? require('./specs/NativeJailMonkey.ts').default | |||
| : NativeModules.JailMonkey; | |||
| if (turboModule != null) { | |||
| // fall through to the `exported` constant below | |||
| } | |||
|
|
|||
| const legacy = NativeModules.JailMonkey; | |||
| const wrapBool = (v: any) => | |||
| typeof v === 'function' ? v : () => Boolean(v); | |||
|
|
|||
| const source = turboModule ?? legacy; | |||
There was a problem hiding this comment.
Should show link error if suitable source is not found
| rootedDetectionMethods: | ||
| typeof source?.rootedDetectionMethods === 'function' | ||
| ? source.rootedDetectionMethods | ||
| : undefined, |
|
@hbabathe can you check co-pilot comments, after we can merge |
- Throw LINKING_ERROR via Proxy when neither TurboModule nor NativeModule resolves, so missing installs fail loudly instead of reporting a clean non-jailbroken device. - Normalize rootedDetectionMethods to a callable on old-arch Android (constant object via getConstants) to match new-arch's function shape. - Replace file-wide @ts-nocheck with narrow @ts-expect-error on the dynamic TurboModule require and the react-native peer-dep import. - Drop the no-op `if (turboModule != null)` block. - Remove contradictory `accessible={false} accessibilityLabel=""` from the ExampleExpo55 content View.
Switches ExampleProject to the artifacts tarball (matching ExampleExpo55) and regenerates Podfile.lock so jail-monkey actually links on iOS. NOTE: the legacy ExampleProject (RN 0.73 / Gradle 8.3 / AGP 8.1) still needs a broader upgrade per PR review comments — it currently requires JDK 17 (not the Android Studio JBR JDK 21) to avoid a JdkImageTransform failure on core-for-system-modules.jar. Tracking a follow-up to bump Gradle/AGP/RN here so JDK 21 builds work without an override.
|
Made updates suggested by the copilot. Note:
|
Please use Yarn to align with the project, rather than adding new tooling. |
levibuzolic
left a comment
There was a problem hiding this comment.
Thanks for this, just a couple of minor cleanups, otherwise should be good to go.
There was a problem hiding this comment.
I reckon we'd like to rename this directory to ExampleExpo -- we'll likely want to update the SDK version here over time without creating a new exmaple project with every major SDK.
| "expo-dev-client": "~55.0.19", | ||
| "expo-status-bar": "~55.0.4", | ||
| "expo-system-ui": "~55.0.11", | ||
| "jail-monkey": "file:../artifacts/jail-monkey-3.0.0.tgz", |
There was a problem hiding this comment.
Would definitely prefer yarn symlinking here, so we can skip the build step + avoid hardcoding versions.
There was a problem hiding this comment.
ok will do a smoke test with yarn and push changes
drop bun tooling
|
I’ll merge and release it tonight unless any issues are raised. |
Summary
#import <CoreLocation/CoreLocation.h>needed for new architecture buildsisJailBroken,canMockLocation, etc. viaconstantsToExportas plain booleans. The JS entry point only wrapped these into functions for the legacyNativeModulespath — when the TurboModule loaded on new-arch builds, callers got raw booleans instead of functions, causing"isJailBroken is not a function"runtime crashes. Now wrapping is applied uniformly regardless of module source.global.__turboModuleProxycheck with a try/catch require of the TurboModule specnpm pack, build scripts, SafeAreaContext fix, entry point corrected to.tsx.gitignore/.npmignoreto excludeartifacts/andExampleExpo55/Test plan
JailMonkey.isJailBroken()returns a boolean instead of crashing