Skip to content

Move Android bridge to ReactNative repo + New Architecture TurboModules#450

Merged
wmathurin merged 46 commits into
forcedotcom:devfrom
wmathurin:rn-android-in-react-native
May 28, 2026
Merged

Move Android bridge to ReactNative repo + New Architecture TurboModules#450
wmathurin merged 46 commits into
forcedotcom:devfrom
wmathurin:rn-android-in-react-native

Conversation

@wmathurin
Copy link
Copy Markdown
Contributor

@wmathurin wmathurin commented May 26, 2026

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

  • All Android bridge modules moved to android/src/main/java/
  • Kotlin TurboModule implementations with unified SF* prefix names
  • Pre-built C++ codegen (JavaTurboModule wrappers) in android/generated/
  • react-native.config.js for React Native autolinking
  • postinstall script creates symlink for codegen at RN-expected path

New Architecture

  • Unified single-callback pattern on both iOS and Android
  • SalesforceReactActivity updated for bridgeless mode
  • Activity retry mechanism for getCurrentActivity() timing
  • Lazy TurboModule lookups in JS (bridgeless mode initializes modules after bundle load)
  • codegenConfig with android section for proper C++ JNI wrapper generation

Testing

  • androidTests/ directory mirrors iosTests/ pattern
  • prepareandroid.js, updatesdk.js, updatebundle.js (same structure as iOS)
  • Android CI workflow added (reusable-android-workflow.yaml with Firebase Test Lab)
  • Harness tests pass (2/2), SmartStore tests pass (11/11)
  • iOS tests: 34/35 pass (1 network timeout, not a regression)

Module Names (Unified)

Both platforms now use the same names matching the TypeScript specs:

  • SFOauthReactBridge (was SalesforceOauthReactBridge on Android)
  • SFNetReactBridge (was SalesforceNetReactBridge)
  • SFSmartStoreReactBridge (was SmartStoreReactBridge)
  • SFMobileSyncReactBridge (was MobileSyncReactBridge)

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:

  1. In bridgeless mode, the only path to resolve TurboModules is through a C++ function (RNSalesforceReactSpec_ModuleProvider) that the autolinking system generates in autolinking.cpp. This function calls into our codegen-produced JavaTurboModule wrapper classes.

  2. Codegen normally runs via the com.facebook.react gradle plugin, but that plugin can only be applied to the app module — not to library subprojects included via autolinking.

  3. 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.sh during releases (alongside dist/ rebuild). Can also be run manually: npx react-native codegen --path . --outputPath . --platform android then move output from android/app/build/generated/source/codegen to android/generated/source/codegen.

Related PRs

Before Merging

  • Revert iosTests/package.json react-native-force to forcedotcom#dev
  • Revert androidTests/package.json react-native-force to forcedotcom#dev

Test Plan

  • All 4 RN templates build on Android
  • All 4 RN templates build on iOS
  • Android harness tests pass (2/2)
  • Android SmartStore tests pass (11/11)
  • iOS tests pass (34/35)
  • Template app runtime: login + contacts load on Android
  • TypeScript compiles cleanly

wmathurin added 30 commits May 22, 2026 13:10
- 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.
wmathurin added 13 commits May 23, 2026 13:56
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.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 26, 2026

TestsPassed ✅SkippedFailed
iOS ^18 Test Results35 ran35 ✅
TestResult
No test annotations available

@codecov
Copy link
Copy Markdown

codecov Bot commented May 26, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.62%. Comparing base (0abd756) to head (5995a6f).
⚠️ Report is 7 commits behind head on dev.

Additional details and impacted files

Impacted file tree graph

@@            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:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 26, 2026

TestsPassed ☑️SkippedFailed ❌️
iOS ^26 Test Results35 ran34 ✅1 ❌
TestResult
iOS ^26 Test Results
ReactMobileSyncTests.testCleanResyncGhosts❌ failure

Copy link
Copy Markdown
Contributor

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻

@wmathurin wmathurin merged commit 8f7944f into forcedotcom:dev May 28, 2026
5 of 10 checks passed
uses: android-actions/setup-android@v3
- uses: gradle/actions/setup-gradle@v4
with:
gradle-version: "8.14.3"
Copy link
Copy Markdown
Contributor

@brandonpage brandonpage May 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very NIT: This is probably out of date. This won't cause the build to fail or anything, it will just take more time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saw your comments after merging.
Will revisit in the next PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done here: c9941ba

--type instrumentation \
--app "$APP_APK" \
--test "$TEST_APK" \
--device model=Pixel2,version=33 \
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did this device configuration come from?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saw your comments after merging.
Will revisit in the next PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done here: c9941ba

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.

3 participants