Move Android bridge to ReactNative repo + New Architecture TurboModules#450
Merged
wmathurin merged 46 commits intoMay 28, 2026
Merged
Conversation
- Rewrite exec() to use single code path for both platforms - Both iOS and Android now use callback(error, result) pattern - Remove ModuleIOS/ModuleAndroid type separation - Update architecture docs to reflect unified callback pattern
The dist/ compiled JS was stale - tsconfig has noEmit:true so the build script only type-checks. Regenerated dist/ to include the unified single-callback exec() for both platforms.
In RN 0.81.5 bridgeless new architecture, native modules are not available at JS import time. Defer TurboModuleRegistry.get() calls to function invocation time so modules are found after initialization.
Register SalesforceReactPackage through React Native's autolinking system so TurboModules are properly discovered in bridgeless mode. Manual package registration via getPackages() doesn't work because the TurboModuleManager resolves modules through the autolinking- generated PackageList code path.
The autolinking system needs an android/ directory to detect native code. The actual implementation is in SalesforceMobileSDK-Android via composite build. This stub enables package discovery.
Move all SalesforceReact native code into android/ directory with standard src/main/java/ layout. Create standalone build.gradle.kts that declares dependencies via Maven coordinates (substituted by composite build in dev, resolved from Maven Central in production). Includes: 4 Kotlin bridge modules (TurboModule), SDK manager, activity, activity delegate, logger, bridge helper, and resources.
Create Android test infrastructure: - prepareandroid.js: clones Android SDK, generates JS test bundle - package.json: sdkDependencies for Android SDK clone - android/ Gradle project with composite build wiring - Test classes moved from SalesforceMobileSDK-Android/libs/test/SalesforceReactTest/ - create_test_credentials_from_env.js for CI - metro.config.js and babel.config.js for bundle generation
RN 0.81.5 autolinking generates C++ module provider code for all autolinked libraries. Since our TurboModules are Java/Kotlin-based (not C++ JSI), provide a stub header that returns nullptr from the C++ provider. The Java BaseReactPackage handles actual module creation. Also remove codegenConfig.android from package.json since we don't use Android codegen (bridges are manually implemented in Kotlin).
- Add settings.gradle.kts to android/ for plugin resolution in composite builds - Add version numbers to plugins in android/build.gradle.kts - Fix includeBuild path in androidTests (../../android not ../../../android) - Add gradlePluginPortal and extra classpath deps for Android SDK compatibility
- Create shared/test/ directory (mirrors Android/iOS SDK pattern) - test_credentials.json is gitignored (contains real credentials) - iosTests/test_credentials.json -> symlink to shared/test/ - iosTests/ios/test_credentials.json -> symlink to shared/test/ - androidTests assets -> symlink to shared/test/ - Add gitignore entries for Android test generated assets
- Patch cloned SDK to AGP 8.12.0 (avoid multi-AGP version error) - Add dependencyResolutionManagement to android/settings.gradle.kts - Add gradle.properties with android.useAndroidX=true to both projects - Build infrastructure now resolves all dependencies correctly - Remaining: test source code compilation fixes (cross-source-set refs)
- Move TEST_NAME constant to ReactActivityTestDelegate (fix cross-source-set ref) - Update .gitignore to exclude androidTests build output and .gradle/ - Add gradle wrapper (gradlew, gradlew.bat, gradle-wrapper.jar) - test_credentials.json is NOT committed (created by CI script or manually) - Test APK compiles and runs on emulator (auth test host needs new arch migration)
- Convert ReactNativeTestHost from legacy ReactInstanceManager to DefaultReactNativeHost - Convert SalesforceReactTestApp to Kotlin with ReactHost support - Enable new arch (isNewArchEnabled=true) and Hermes - Replace JSC dependency with hermes-android - Tests deploy and start but need further work on bridgeless test runner
- Switch to Groovy settings/build files matching template pattern - Add com.facebook.react plugin for autolinking and native builds - Use Gradle 8.14.3 with RN gradle plugin and Kotlin 2.3.20 - Enable newArchEnabled=true and hermesEnabled=true - Remove deprecated getReactInstanceManager().destroy() from test delegate - Tests now start in bridgeless mode (testPassing passes!)
…loader)
- Include react-native-force as a subproject (not composite build)
to ensure TurboModule interface is in the same classloader
- Switch to Groovy settings/build files matching template pattern
- Use Gradle 8.14.3 for RN gradle plugin compatibility
- Point package.json at rn-android-in-react-native branch
- Library uses apply(plugin) instead of plugins{} block for subproject compat
- ReactNativeTestHost now uses PackageList(this).getPackages() - react-android changed to compileOnly (avoid classloader duplication) - Remove explicit includeBuild for SalesforceReact in test settings - testPassing continues to pass; testAsyncPassing (TurboModule bridge) still times out - same fundamental issue as template had pre-fix
- Remove explicit include for react-native-force (let autolinking handle it) - Use compileOnly for react-android in library to avoid classloader duplication - Add debug logging in test app for package verification - testPassing passes; testAsyncPassing fails because SalesforceReactActivity uses legacy getReactNativeHost().getReactInstanceManager() APIs that are incompatible with bridgeless mode - activity needs new arch migration
- Remove shouldAskOverlayPermission() (used legacy ReactInstanceManager) - restartReactNativeApp() uses recreate() in bridgeless mode - showReactDevOptionsDialog() guards against null instance manager - Simplify shouldReactBeRunning() without overlay permission check - Add getReactInstanceManager() helper that returns null safely in bridgeless
The autolinking C++ provider is the ONLY resolution path in bridgeless mode. Returning nullptr meant modules were never found by JS. Now creates JavaTurboModule instances that delegate to our Java/Kotlin implementations for all 4 bridge module names.
Confirms: getModule is called, modules implement TurboModule correctly. The issue is in C++ autolinking layer - javaModuleProvider (our stub) returns nullptr but the TurboModuleManager doesn't fallback to the Java BaseReactPackage.getModule() path. Next: Need actual C++ codegen or RN version that supports Java-only TurboModules through autolinking without C++ JNI wrappers.
Re-add android section to codegenConfig so React Native generates proper JavaTurboModule C++ wrappers (JNI bindings). Remove manual JNI stub that returned nullptr. The codegen generates correct wrappers that delegate to our Java/Kotlin module implementations.
Run `react-native codegen` to generate proper C++ JavaTurboModule wrappers that the bridgeless TurboModuleManager needs. These files are committed to the repo (and npm package) so they're available at CMake configure time without needing a separate codegen step. Generated from src/specs/Native*.ts via React Native Codegen.
The codegen C++ provider matches modules by the spec names (SFOauthReactBridge, SFNetReactBridge, etc). Android bridges previously used different names (SalesforceOauthReactBridge, etc) causing the C++ provider to return nullptr. Now all bridges use the same names as the TypeScript specs: - SFOauthReactBridge (was SalesforceOauthReactBridge) - SFNetReactBridge (was SalesforceNetReactBridge) - SFSmartStoreReactBridge (was SmartStoreReactBridge) - SFMobileSyncReactBridge (was MobileSyncReactBridge) This also means both platforms use the same module names, simplifying the JS lookup (TurboModuleRegistry.get works with one name for both iOS and Android).
The key fix was aligning ReactModuleInfo name/className args with the actual module names (SF* prefix). With correct names, the codegen C++ provider matches and creates proper JavaTurboModule wrappers. Test now reaches the bridge (getAuthCredentials is called) but fails with "SalesforceReactActivity not found" - this is a test timing issue (getCurrentActivity returns null between activity transitions), NOT a TurboModule issue. The module IS discovered and methods ARE called.
- Split prepareandroid.js into updatesdk.js + updatebundle.js (mirrors iosTests) - prepareandroid.js now calls the separate scripts like prepareios.js does - Add reusable-android-workflow.yaml for Android test execution - Add android-pr job to pr.yaml - Add android-nightly job to nightly.yaml
The android/build/ directory is excluded by npm/yarn when installing from git (npm convention). Moving to android/generated/ ensures the pre-built C++ JavaTurboModule wrappers are included in the npm package. Also set cmakeListsPath in react-native.config.js to point to the new location so autolinking finds the CMakeLists.txt correctly.
|
||||||||||||||
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev #450 +/- ##
==========================================
- Coverage 74.95% 72.62% -2.33%
==========================================
Files 13 13
Lines 559 559
==========================================
- Hits 419 406 -13
- Misses 140 153 +13 🚀 New features to boost your workflow:
|
|
||||||||||||||||
brandonpage
reviewed
May 28, 2026
| uses: android-actions/setup-android@v3 | ||
| - uses: gradle/actions/setup-gradle@v4 | ||
| with: | ||
| gradle-version: "8.14.3" |
Contributor
There was a problem hiding this comment.
Very NIT: This is probably out of date. This won't cause the build to fail or anything, it will just take more time.
Contributor
Author
There was a problem hiding this comment.
Saw your comments after merging.
Will revisit in the next PR.
brandonpage
reviewed
May 28, 2026
| --type instrumentation \ | ||
| --app "$APP_APK" \ | ||
| --test "$TEST_APK" \ | ||
| --device model=Pixel2,version=33 \ |
Contributor
There was a problem hiding this comment.
Where did this device configuration come from?
Contributor
Author
There was a problem hiding this comment.
Saw your comments after merging.
Will revisit in the next PR.
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Moves the Android React Native bridge code from
SalesforceMobileSDK-Android/libs/SalesforceReact/into this repo (android/), mirroring the iOS bridge layout. Enables React Native New Architecture (TurboModules) with proper autolinking and codegen on both platforms.Key Changes
Bridge Migration
android/src/main/java/SF*prefix namesandroid/generated/react-native.config.jsfor React Native autolinkingpostinstallscript creates symlink for codegen at RN-expected pathNew Architecture
SalesforceReactActivityupdated for bridgeless modegetCurrentActivity()timingcodegenConfigwith android section for proper C++ JNI wrapper generationTesting
androidTests/directory mirrorsiosTests/patternprepareandroid.js,updatesdk.js,updatebundle.js(same structure as iOS)reusable-android-workflow.yamlwith Firebase Test Lab)Module Names (Unified)
Both platforms now use the same names matching the TypeScript specs:
SFOauthReactBridge(wasSalesforceOauthReactBridgeon Android)SFNetReactBridge(wasSalesforceNetReactBridge)SFSmartStoreReactBridge(wasSmartStoreReactBridge)SFMobileSyncReactBridge(wasMobileSyncReactBridge)Why Pre-built Codegen is Committed
The
android/generated/source/codegen/directory contains C++ files generated by React Native's codegen from our TypeScript specs (src/specs/NativeSF*.ts). These are committed rather than generated at build time because:In bridgeless mode, the only path to resolve TurboModules is through a C++ function (
RNSalesforceReactSpec_ModuleProvider) that the autolinking system generates inautolinking.cpp. This function calls into our codegen-producedJavaTurboModulewrapper classes.Codegen normally runs via the
com.facebook.reactgradle plugin, but that plugin can only be applied to the app module — not to library subprojects included via autolinking.CMake configuration (which needs the codegen output directory to exist) runs before any Gradle tasks, so even a pre-build copy task can't produce the files in time.
The committed files are small (a header + one .cpp mapping module names to JavaTurboModule subclasses with correct method signatures). They only need regeneration if bridge methods are added/removed/renamed in the TypeScript specs. Regenerated automatically by
setversion.shduring releases (alongsidedist/rebuild). Can also be run manually:npx react-native codegen --path . --outputPath . --platform androidthen move output fromandroid/app/build/generated/source/codegentoandroid/generated/source/codegen.Related PRs
Before Merging
iosTests/package.jsonreact-native-force toforcedotcom#devandroidTests/package.jsonreact-native-force toforcedotcom#devTest Plan