Visitar URL original
refactor(app): port native App helpers to Swift and Kotlin by mikehardy · Pull Request #9337 · invertase/react-native-firebase · GitHub
Skip to content

refactor(app): port native App helpers to Swift and Kotlin - #9337

Draft
mikehardy wants to merge 60 commits into
mainfrom
app-native-language-port
Draft

mikehardy wants to merge 60 commits into
mainfrom
app-native-language-port

Conversation

@mikehardy

@mikehardy mikehardy commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Ports the @react-native-firebase/app native helpers to Kotlin (Android) and Swift (iOS) behind unchanged public Java/ObjC facades: handle map, preferences, JSON/meta, shared utils, event emitter, RCTConvert, Utils and the App module. Perf, Auth and Remote Config slices are on follow-up PRs.

Also in here:

  • ktlint Gradle wrapper and a pinned clang-format for Apple Silicon
  • iOS CI probe that links App against a dynamic Firebase package (SDK pin check, single FirebaseCore copy check, launch smoke)
  • OKF notes for the language-port track

Possible breaking change (iOS)

initializeApp now reads automaticDataCollectionEnabled as a boolean. Before, it was a pointer cast (BOOL)[appConfig valueForKey:...], so any non-nil value, including false, turned data collection on. Now false, 0, null and absent turn it off.

This fixes the incorrect behaviour, but anyone passing false and (unknowingly) getting collection anyway would see it change on upgrade. Commit is 3c8714ceb if we'd rather pull it into its own fix PR. @mikehardy what do you think, keep here or split out?

Smaller one: RNFBSharedUtils.h, RCTConvert+FIRApp.h and RCTConvert+FIROptions.h no longer import FirebaseCore (and RNFBUtilsModule.h no longer imports Photos). Every package in this repo imports what it needs, but third-party code relying on the transitive import would need to add it.

@mikehardy
mikehardy force-pushed the app-native-language-port branch from 9ce2c9a to 01b2988 Compare September 24, 2026 00:55
@codecov

codecov Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.47507% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.00%. Comparing base (c147145) to head (cb01509).

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #9337      +/-   ##
============================================
+ Coverage     70.00%   72.00%   +2.00%     
- Complexity     2134     2171      +37     
============================================
  Files           550      576      +26     
  Lines         38877    39862     +985     
  Branches       5301     5252      -49     
============================================
+ Hits          27213    28698    +1485     
+ Misses         9865     9432     -433     
+ Partials       1799     1732      -67     
Flag Coverage Δ
android-native 68.63% <100.00%> (+2.66%) ⬆️
e2e-ts-android 53.92% <ø> (ø)
e2e-ts-ios 53.39% <ø> (ø)
e2e-ts-macos 49.94% <ø> (ø)
ios-native 73.04% <100.00%> (+2.79%) ⬆️
ios-ruby 100.00% <100.00%> (ø)
jest 49.22% <87.31%> (+0.23%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 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.

1.8.0 only ships darwin x86_64 and needs Rosetta. 1.7.0 ships
native arm64 under the misnamed darwin_x64 path. Intel Mac is not
a supported local lint:ios host.
@mikehardy
mikehardy force-pushed the app-native-language-port branch from 176071b to 93d2d11 Compare October 1, 2026 12:05

@russellwheatley russellwheatley left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Java/ObjC facades look good from what I checked. One blocker (Android doesn't compile against SDK 37) plus a few things inline. I've left the dynamic RNFBFirebase probe out of this review, will pick that up separately.

Comment on lines +165 to +173
val task = taskInfo[0].taskInfo
currentActivity =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (task.baseActivity != null) platform(task.baseActivity).shortClassName else ""
} else {
if (task.origActivity != null) {
platform(task.origActivity).shortClassName
} else {
platform(task.baseIntent.component).shortClassName

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is what's failing the Android e2e build. getTaskInfo() is annotated nullable in SDK 37 (what tests/android compiles against), so reading task.baseActivity / task.origActivity / task.baseIntent directly won't compile. I think the unit tests only pass because the package itself builds against compileSdk 34, but any app on a newer compile SDK would hit this too.

Old Java would have NPE'd here anyway, so maybe just skip it when null:

val task = taskInfo[0].taskInfo
if (task != null) {
  currentActivity = ...
}

import java.util.ArrayList

class ReactNativeFirebaseJSON private constructor() {
private var jsonObject: JSONObject? = JSONObject(BuildConfig.FIREBASE_JSON_RAW)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The Java constructor wrapped this in a try/catch for JSONException and left jsonObject null. Here a parse failure would blow up the singleton init, and since TaskExecutorService calls getSharedInstance() in its init, every module would fail to construct after that. Gradle validates the JSON so it's unlikely, but might be worth keeping the try/catch for parity.

Comment on lines +65 to +83
static func create(googleAppID: String?, gcmSenderID: String?) -> NSObject {
#if canImport(RNFBFirebase)
RNFBFirebaseOptionsClient.create(googleAppID: googleAppID, gcmSenderID: gcmSenderID)
#else
// Pre-port forwarded nil into `initWithGoogleAppID:GCMSenderID:`. The Swift
// `FirebaseOptions(googleAppID:gcmSenderID:)` overlay requires non-optional String,
// so call the ObjC initializer directly to preserve nil.
let allocated = FirebaseOptions.perform(NSSelectorFromString("alloc"))!
.takeRetainedValue() as! NSObject
// `perform` returns +0. The extra retain is what `takeRetainedValue()` consumes
// when `init` returns the same instance, so the object is not over-released and
// still deallocated with its last owner.
_ = Unmanaged.passRetained(allocated)
return allocated.perform(
NSSelectorFromString("initWithGoogleAppID:GCMSenderID:"),
with: googleAppID,
with: gcmSenderID
)!.takeRetainedValue() as! NSObject
#endif

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the retain counts here do balance, but it took me a while to convince myself, and the comments disagree with each other (the doc comment says both calls are +1, the inline one says perform returns +0).

If the only reason is passing nil through to the ObjC initializer, could we just do FirebaseOptions(googleAppID: googleAppID ?? "", gcmSenderID: gcmSenderID ?? "")? As far as I can tell JS always sends appId and messagingSenderId, so the nil case might not matter in practice.

#endif
#import <React/RCTBridgeModule.h>

@class FIRApp;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Dropping the FirebaseCore import here (and in RCTConvert+FIRApp.h / RCTConvert+FIROptions.h, plus Photos in RNFBUtilsModule.h) is fine for everything in this repo. I checked and all the packages import FirebaseCore / Photos themselves. Third-party code that includes these headers and relied on the transitive import would stop compiling though. Probably worth a changelog note so it's not a surprise.

@mikehardy
mikehardy force-pushed the app-native-language-port branch from 93d2d11 to c4f4a79 Compare October 1, 2026 15:16
A Debug e2e link needs CDP symbols that the Release Hermes engine does not export. Select the engine from the Xcode configuration.
Behavior change vs the pre-port Objective-C: the old code cast the option value to a pointer, so any non-nil value (including JS false) enabled automatic data collection. It now uses [dataCollection boolValue], so NO, 0 and NSNull disable it. This is intentional.

Also adds host-side unit tests for the App and Utils TurboModule shells and restructures the @catch handling in RNFBAppModuleImplementation.
The import guard now scans .h/.hpp/.m/.mm/.c/.cpp, unprefixed quoted headers and digit separators. FIROptions allocation no longer uses perform(alloc)/passRetained, force unwraps are gone from RNFBAppModuleFirebase, and unused RNFBFirebase API is removed except RNFBFirebaseAppClient.configure(). Test coverage for the touched Swift, bridge and Ruby code is extended.
Debug cocoapods jobs could restore a Pods directory saved by the Release archive job, which had the Release Hermes engine swapped in, and failed linking facebook::hermes::cdp symbols. The cache key and restore key now include the build mode.
A failing xcodebuild -showBuildSettings was swallowed and reported as a missing app. Capture the exit status, print the stderr tail and fail with the real cause.
The iOS unit test host stub RNFBAppTurboModules.h sits next to the codegen header under packages/app/ios, so findSingleMatch found two matches and the Jest parity suite failed in CI. Skip HostStubs directories and cover the lookup with a unit test.
The dynamic Firebase probe pins firebase-ios-sdk with exact: in packages/app/ios/RNFBFirebase/Package.swift, and SDK bumps only touch sdkVersions.ios.firebase in packages/app/package.json. Add a Node script that fails with both values on mismatch and run it as the first step of the probe job.
The probe only checked that RNFBApp.framework does not define FIRApp, which does not show FirebaseCore is linked once. In probe mode the build now also requires RNFBFirebase.framework to define FIRApp, no standalone FirebaseCore framework in the products, and no other framework binary or the app executable defining it. The checks live in a sourced helper with Jest tests against stubbed and real nm output.
The probe job only built, so it never proved RNFBFirebase.framework is embedded and loads, or that RNFBFirebaseAppClient.configure() runs. The launch smoke now builds into its own DerivedData and, in probe mode, checks the facade symbol, the embedded and mapped framework, and the FirebaseCore configure log. It also reaps the log stream on failure.
…plicit Swift condition

canImport(RNFBFirebase) picked the probe path in shipped code based on whether a module with that name happened to be importable. Probe mode now sets RNFB_DYNAMIC_FIREBASE_PROBE on the RNFBApp pod and the app targets, and a flag-off install removes it again, so only the probe compiles that path.

This branch has not been deployed

No deployments
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.

2 participants