My Expo native module works on Android but is null on iOS — what am I missing?
TL;DR: I have a custom Expo module (@utapza/expo-mifare-scanner) currently on npm, that loads correctly on Android. On iOS, requireNativeModule('ExpoMifareScanner') returns null and the module never appears in global.expo.modules. I’ve tried prebuilding, a config plugin that copies Swift files into the Xcode project, and EAS Build with --clear-cache. The config plugin does run (entitlements and Info.plist are applied), but the native module is still absent from the iOS binary. I’d love help figuring out what’s needed to make the module visible on iOS.
The setup
- Module: @utapza/expo-mifare-scanner — Expo module for MIFARE/NFC (reading and emulation).
- App: Expo (SDK 52) with a development build, not Expo Go.
-
What works: On Android, the module loads:
requireNativeModule('ExpoMifareScanner')(via the Android class name) works, and the native code runs. -
What doesn’t: On iOS,
requireNativeModule('ExpoMifareScanner')returnsnull. At runtime,global.expo.modulesdoes not containExpoMifareScanner, and the proxy’sexportedMethodsdon’t list it either. So the installed iOS app was built without the module in the binary.
The module is in the app’s package.json and in app.json plugins. Locally, npx expo-modules-autolinking resolve --platform apple --json does list @utapza/expo-mifare-scanner with ExpoMifareScannerModule. So the codebase and config look correct on my machine.
{"extraDependencies":[],"coreFeatures":[],"modules":[{"packageName":"@utapza/expo-mifare-scanner","pods":[{"podName":"ExpoMifareScanner","podspecDir":"/Users/nonwork/dev/uni-card-wallet-setup/node_modules/@utapza/expo-mifare-scanner/ios"}],"swiftModuleNames":["ExpoMifareScanner"],"modules":["ExpoMifareScannerModule"],"appDelegateSubscribers":[],"reactDelegateHandlers":[],"debugOnly":false,"packageVersion":"2.0.8"}, ...}
What I’ve already tried
1. Standard prebuild and EAS Build
- Ran
npx expo prebuild --platform ios(and full prebuild). - Built with EAS Build:
eas build --platform ios --profile development. - Result: iOS build succeeds, but the module is still null at runtime.
Latest build log
- The Xcode build reports 122 targets in the dependency graph. The app target
uTaplinks Pods such asreact-native-nfc-manager,expo-dev-menu,Sentry,Stripe,ExpoModulesCore, etc. - A search of the entire log for
ExpoMifareScanner,CardEmulationHandler, orExpoMifareScannerModule.swiftreturns zero matches. So the native module is completely absent: no target, no Swift file references, no compilation of the module. - The config plugin does run for this build (entitlements and Info.plist are applied — NFC usage description and capabilities appear in the project). So the “config plugin” part works; the actual native module (Swift code) was never added to the Xcode project or Podfile in this build.
- Conclusion: the Swift files were never copied into the project, never compiled, and never linked. That’s why at runtime
global.expo.modulesdoesn’t containExpoMifareScannerandrequireNativeModule('ExpoMifareScanner')returns null.
2. Config plugin: entitlements + Info.plist (already in place)
The module’s app.plugin.js already:
- Sets NFCReaderUsageDescription in Info.plist.
- Adds NFC entitlements (
com.apple.developer.nfc.readersession.formats=['TAG']).
So the “config plugin” part runs: provisioning and plist show the NFC usage and capabilities. But that doesn’t add the actual Swift code to the app.
3. Config plugin: copy Swift files into the app target
To force the Swift code into the build when the pod might not be linked, I extended the plugin to:
-
Copy
ExpoMifareScannerModule.swiftandCardEmulationHandler.swiftfromnode_modules/@utapza/expo-mifare-scanner/ios/ExpoMifareScanner/into the app’s iOS target folder (e.g.ios/<AppName>/). -
Add those files to the Xcode project with
addBuildSourceFileToGroupfrom@expo/config-plugins(so they’re in “Compile Sources” for the app target).
So the plugin now does more than plist/entitlements — it copies and links the Swift files. I’m relying on the installed package (from npm or git) still containing the ios/ folder so the plugin can copy from it.
4. Clean EAS Build
- Ran
eas build --platform ios --profile development --clear-cacheso EAS doesn’t reuse an old native project. - Same result: build succeeds, but the module is still null on device. The build log does not contain any “[expo-mifare-scanner] Copied … to Xcode project” messages, the copy step didn’t run on EAS (e.g. package path or
projectNamediffering).
5. Verifying the package locally
- The module’s
package.jsonhas"files": [ "src", "android/...", "ios", "expo-module.config.json", "app.plugin.js", ... ]soiosis published. - Locally, a small script checks that the installed package has
ios/ExpoMifareScanner/ExpoMifareScannerModule.swiftand thatexpo-modules-autolinking resolve --platform applelists the module. That passes when I install from the repo or from a tarball.
So on my machine the module is present and autolinking sees it. On EAS, the installed version might differ (e.g. npm tarball omitting or differing from what I expect), but even with the plugin copying the Swift files, the module doesn’t show up at runtime on iOS.
What I’m unsure about
ExpoModulesProvider on iOS
I know the app loads the native module list from something likeExpoModulesProviderin the main bundle. If the module isn’t in that provider, it won’t be inglobal.expo.modules. Is the provider generated only from expo-modules-autolinking’s resolve at build time? If EAS installs a package that doesn’t get resolved (e.g. missing or differentexpo-module.config.jsonorios/in the tarball), would the provider simply omit the module even if I’ve copied the Swift files into the app target?Pod vs “files in app target”
When the module is linked as a pod (via autolinking), the Podfile gets a pod and the provider gets the module. When I only copy the Swift files into the app target (no pod), the code compiles and links, but does the Expo runtime still need the module to be listed inExpoModulesProvider? If yes, then copying files alone isn’t enough — I’d need the module to be in the autolinking result so the provider is generated with it.EAS dependency resolution
Is there a known case where the published npm package (or the commit used by a git dependency) doesn’t include theios/folder or has a different structure, so EAS’s install doesn’t match what I have locally? Any recommended way to verify exactly what EAS installs (e.g. a build step that dumpsls node_modules/@utapza/expo-mifare-scanner/iosor the result ofexpo-modules-autolinking resolve)?Anything iOS-specific for “making the module visible”
Beyond the usual (Expo module API,expo-module.config.jsonwithios.modules, podspec, and the config plugin for plist/entitlements), is there an extra step or convention on iOS to make the module show up inglobal.expo.modules(e.g. registering in a specific way, or something in the Podfile / build phases)?
What I’d love from you
- Confirmation or correction of how ExpoModulesProvider is populated on iOS and whether “copy Swift into app target” is enough or the module must be in the autolinking result.
- Any checklist or debugging steps you use when a module works on Android but is null on iOS (e.g. “always check X”, “run Y on EAS and inspect Z”).
- If you’ve seen EAS installing a different package shape than local (e.g.
.npmignore/filescausingios/to be missing), how you verified and fixed it. - Any iOS-specific registration or build step that’s easy to miss when coming from Android.
I’m happy to share the relevant parts of the module (e.g. expo-module.config.json, podspec, plugin snippet) or build logs if that helps. Thanks in advance.
Top comments (0)