Skip to content

refactor: migrate native module from Expo Modules API to React Native Turbo Modules#149

Open
V3RON wants to merge 9 commits into
mainfrom
refactor/turbomodule
Open

refactor: migrate native module from Expo Modules API to React Native Turbo Modules#149
V3RON wants to merge 9 commits into
mainfrom
refactor/turbomodule

Conversation

@V3RON
Copy link
Copy Markdown
Contributor

@V3RON V3RON commented May 12, 2026

Why this matters for Voltra users

Until now, Voltra required expo-modules-core as part of its native layer. This was invisible to Expo users, but it meant that any React Native project not using Expo had to drag in the entire Expo Modules runtime just to use Voltra — a significant and surprising dependency for something as self-contained as Live Activities and home screen widgets.

This PR removes that constraint. After this change, Voltra works in any React Native project, Expo or bare, with no Expo runtime required at all. The Expo Config Plugin (app.plugin.js) is unaffected — it's a build-time tool and stays as-is.

Closes #148

What changed

TypeScript / JS

  • CodeGen specs added: NativeVoltraModule.ios.ts and NativeVoltraModule.android.ts — processed at build time by react-native-codegen to generate the native protocol/interface for each platform
  • Fabric component spec added: VoltraRNNativeComponent.ts for the VoltraRN view
  • requireNativeModule (Expo) → TurboModuleRegistry.getEnforcing (React Native) in all VoltraModule.ts files
  • requireNativeView (Expo) → requireNativeComponent (React Native) in all VoltraView.tsx files
  • Expo's two-arg addListener(event, listener)NativeEventEmitter pattern in all events.ts files
  • expo removed from peer dependencies in ios-client, android-client, and voltra
  • codegenConfig added to packages/voltra/package.json
  • expo-module.config.json deleted

iOS

  • VoltraModule.swift rewritten as RCTEventEmitter with @objc methods and Task-based async/await bridging
  • VoltraModuleBridge.m created — RCT_EXTERN_MODULE + RCT_EXTERN_METHOD declarations that register the Swift class with React Native
  • VoltraRNComponentView.mm created — ObjC++ Fabric bridge connecting the codegen-generated RCTViewComponentView to the Swift VoltraRN view
  • VoltraRN.swift changed from ExpoView to plain UIView (@objc public)
  • VoltraOptions.swift, VoltraImagePreload.swift, VoltraModuleImpl.swift — replaced Record/@Field (Expo auto-deserialization) with plain Swift structs that parse from NSDictionary at the module boundary and serialize back via toDictionary()
  • Voltra.podspec — replaced ExpoModulesCore with React-Core + React-RCTFabric, added .m/.mm to source files

Android

  • VoltraModule.kt rewritten as ReactContextBaseJavaModule with @ReactMethod annotations; async methods take a Promise parameter; sync query methods use isBlockingSynchronousMethod = true
  • VoltraRNBridgeExtensions.kt created — ReadableMap builder functions and WritableMap serializers for notification option/result types, keeping the module code free of raw map access
  • VoltraRN.kt changed from ExpoView to plain FrameLayout; AppContext dependency removed
  • VoltraRNManager.kt created — SimpleViewManager exposing payload and viewId via @ReactProp
  • VoltraPackage.kt created — TurboReactPackage that registers both the module and the view manager
  • react-native.config.js created — configures React Native autolinking for the package
  • build.gradle updated — Expo plugin machinery removed, com.facebook.react:react-android added

Test plan

  • iOS: build the example app and verify Live Activities start, update, and end correctly
  • iOS: verify widget update/reload/clear functions work
  • iOS: verify VoltraView renders correctly inline
  • iOS: verify interaction events fire and reach JS via NativeEventEmitter
  • Android: build the example app and verify ongoing notifications start/update/stop
  • Android: verify widget update/reload/clear functions work
  • Android: verify VoltraView renders correctly inline
  • Bare RN project: install voltra without Expo and confirm no expo-modules-core in the dependency tree

V3RON added 9 commits May 12, 2026 12:18
- Add missing startAndroidLiveUpdate/updateAndroidLiveUpdate/stopAndroidLiveUpdate
  stubs to VoltraAndroidModuleSpec; these methods existed in the old spec but were
  never implemented natively — Expo's requireNativeModule masked the mismatch
- Extend VoltraModuleSpec with TurboModule in packages/voltra
- Fix StyleProp<ViewStyle> vs ViewStyle type on NativeVoltraView style prop
- sendEventWithName -> sendEvent(withName:body:) per React Native rename
- VoltraErrors was nested inside the old Expo Module class; move it to a
  top-level enum so VoltraModuleImpl can reference it without the module prefix
VoltraRNNativeComponent.ts was being processed by CodeGen for both platforms.
On Android, the generated Fabric component auto-registration collided with the
VoltraRNManager registered via VoltraPackage.createViewManagers, producing
"Tried to register two views with the same name VoltraRN".

Renaming to .ios.ts ensures CodeGen generates the component descriptor headers
only for iOS (needed by VoltraRNComponentView.mm), while Android continues to
use the SimpleViewManager path without any conflicting auto-registration.
The Podfile snippet for the widget extension target was using
expo-modules-autolinking to locate the voltra package, which stopped
working after expo-module.config.json was removed.

Replace with standard Node module resolution (require.resolve) which
works for both npm workspaces and regular installs. Also expose
./package.json in the voltra exports map so it can be resolved, and add
the iOS podspecPath to react-native.config.js for RN autolinking.
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.

Migrate native module from Expo Modules to Turbo Modules

1 participant