-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Description
Issue
Background: The iOS Keychain holds onto credentials even after an app is uninstalled or a local environment is reset. In development, this creates a "zombie session" where the client attempts an handshake with the Firebase Auth Emulator using stale credentials that no longer exist in the emulator's volatile memory.
Issue: When these "zombie" handshakes fail, React Native Firebase (RNFB) maps the underlying native FIRAuthErrorDomain to generic UNKNOWN or INTERNAL_ERROR codes in RNFBFunctions.mm. This obfuscates the true cause of the failure.
Steps to Reproduce:
-
Start Firebase Auth and Functions emulator
-
On an iOS simulator/device, sign in a user (e.g., via "Suggested Password"). The Keychain now stores these credentials.
-
Manually delete the user from the Auth Emulator UI (or restart the emulator) without calling
signOut()on the client. -
Attempt to call an httpsCallable Cloud Function.
- If the Auth Emulator is unreachable:
- RNFBFunctions.mm catches an FIRAuthErrorDomain error but returns a generic "Network Error"
- If the Auth Emulator is reachable:
- RNFBFunctions.mm catches an FIRAuthErrorDomain error but returns a generic "Internal Error"
Result: Generic errors, such as "Network Error" or "Internal Error," can mislead developers into believing the issue lies within their source code or Firebase Functions configuration. In reality, the underlying native error correctly identifies a disconnected Auth emulator or an invalid authentication handshakeβdetails that are unfortunately obscured by the React Native bridge.
Work Around: If a Cloud Function is reachable via curl or fetch but fails when called from the iOS Simulator, use "Erase All Content and Settings" on the simulator. This fully wipes the system Keychain and removes the "zombie" credentials causing the conflict.
Preventable Steps: To avoid this state, always explicitly sign out development users before resetting the emulator environment, or disable Keychain during the development.
RNFBFunctionsModule.mm - lines 42:103
- (void)httpsCallable:(NSString *)appName
region:(NSString *)customUrlOrRegion
emulatorHost:(NSString *_Nullable)emulatorHost
emulatorPort:(double)emulatorPort
name:(NSString *)name
data:(JS::NativeRNFBTurboFunctions::SpecHttpsCallableData &)data
options:(JS::NativeRNFBTurboFunctions::SpecHttpsCallableOptions &)options
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
NSURL *url = [NSURL URLWithString:customUrlOrRegion];
FIRApp *firebaseApp = [RCTConvert firAppFromString:appName];
// π οΈ Setup - No issue
// β© ... skipped for brevity
// π¨π Issue in the handle Reject/Resolve 'Promise' block.
[callable callWithObject:callableData
completion:^(FIRHTTPSCallableResult *_Nullable result, NSError *_Nullable error) {
if (error) {
/* βΉοΈ (Examples of errors I got here)
-- If auth emulator is not running, and the user is in a zombie session...
domain: @"FIRAuthErrorDomain"
Error: NETWORK_ERROR
Code: 17020
LocalizedDescription: "A network error (such as timeout,
interrupted connection or unreachable host) has occured."
-- If auth emulator is running, but the user is in a zombie session...
domain: @"FIRAuthErrorDomain"
Error: INVALID_REFRESH_TOKEN
Code: 17999
LocalizedDescription: "An internal error has occurred, print
and inspect the error details for more information."
*/
NSObject *details = [NSNull null];
NSString *message = error.localizedDescription;
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
// π¨π Only errors from this domain are handled.
if ([error.domain isEqual:@"com.firebase.functions"]) {
details = error.userInfo[@"details"];
if (details == nil) {
details = [NSNull null];
}
}
userInfo[@"code"] = [self getErrorCodeName:error]; //< β incorrect
userInfo[@"message"] = message; //< β
correct, but not very helpful.
userInfo[@"details"] = details; //< β incorrect
/* π (Stripped error messages)
-- If auth emulator is not running, and the user is in a zombie session...
code: UKNOWN
message: "A network error ... has occured"
details: ''
-- If auth emulator is running, but the user is in a zombie session...
code: UKNOWN
message: "An internal error has occurred ..."
details: ''
*/
[RNFBSharedUtils rejectPromiseWithUserInfo:reject userInfo:userInfo];
} else {
resolve(@{@"data" : [result data]});
}
}];
}RNFBFunctionsModule.mm - lines 171:242
- (NSString *)getErrorCodeName:(NSError *)error {
// π¨ if error.domain = @"FIRAuthErrorDomain" β "@com.firebase.functions"
NSString *code = @"UNKNOWN";
if ([error.domain isEqual:@"com.firebase.functions"]) {
// β© ...skipped
}
return code; //< β @"UKNOWN"
}Project Files
- iOS:
RNFBFunctionsModule.mmcontents.
Javascript
Click To Expand
package.json:
# N/Afirebase.json for react-native-firebase v6:
# N/AiOS
Click To Expand
ios/Podfile:
- I'm not using Pods
- I'm using Pods and my Podfile looks like:
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
require 'json'
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
def ccache_enabled?(podfile_properties)
# Environment variable takes precedence
return ENV['USE_CCACHE'] == '1' if ENV['USE_CCACHE']
# Fall back to Podfile properties
podfile_properties['apple.ccacheEnabled'] == 'true'
end
ENV['RCT_NEW_ARCH_ENABLED'] ||= '0' if podfile_properties['newArchEnabled'] == 'false'
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] ||= podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
ENV['RCT_USE_RN_DEP'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
ENV['RCT_USE_PREBUILT_RNCORE'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
prepare_react_native_project!
target 'MyProject' do
use_expo_modules!
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
else
config_command = [
'node',
'--no-warnings',
'--eval',
'require(\'expo/bin/autolinking\')',
'expo-modules-autolinking',
'react-native-config',
'--json',
'--platform',
'ios'
]
end
config = use_native_modules!(config_command)
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
# An absolute path to your application root.
:app_path => "#{Pod::Config.instance.installation_root}/..",
:privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
)
post_install do |installer|
react_native_post_install(
installer,
config[:reactNativePath],
:mac_catalyst_enabled => false,
:ccache_enabled => ccache_enabled?(podfile_properties),
)
end
endAppDelegate.m:
import Expo
import FirebaseCore
import React
import ReactAppDependencyProvider
@UIApplicationMain
public class AppDelegate: ExpoAppDelegate {
var window: UIWindow?
var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
var reactNativeFactory: RCTReactNativeFactory?
public override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
let delegate = ReactNativeDelegate()
let factory = ExpoReactNativeFactory(delegate: delegate)
delegate.dependencyProvider = RCTAppDependencyProvider()
reactNativeDelegate = delegate
reactNativeFactory = factory
bindReactNativeFactory(factory)
#if os(iOS) || os(tvOS)
window = UIWindow(frame: UIScreen.main.bounds)
// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-10e8520570672fd76b2403b7e1e27f5198a6349a
FirebaseApp.configure()
// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions
factory.startReactNative(
withModuleName: "main",
in: window,
launchOptions: launchOptions)
#endif
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// Linking API
public override func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options)
}
// Universal Links
public override func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result
}
}
class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
// Extension point for config-plugins
override func sourceURL(for bridge: RCTBridge) -> URL? {
// needed to return the correct URL for expo-dev-client.
bridge.bundleURL ?? bundleURL()
}
override func bundleURL() -> URL? {
#if DEBUG
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
#else
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}
}Android
Click To Expand
Have you converted to AndroidX?
- my application is an AndroidX application?
- I am using
android/gradle.settingsjetifier=truefor Android compatibility? - I am using the NPM package
jetifierfor react-native compatibility?
android/build.gradle:
// N/Aandroid/app/build.gradle:
// N/Aandroid/settings.gradle:
// N/AMainApplication.java:
// N/AAndroidManifest.xml:
<!-- N/A -->Environment
Click To Expand
react-native info output:
System:
OS: macOS 26.2
CPU: (8) arm64 Apple M1
Memory: 126.69 MB / 16.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 22.12.0
path: /Users/<user>/.nvm/versions/node/v22.12.0/bin/node
Yarn: Not Found
npm:
version: 11.0.0
path: /Users/<user>/.nvm/versions/node/v22.12.0/bin/npm
Watchman: Not Found
Managers:
CocoaPods:
version: 1.16.2
path: /opt/homebrew/bin/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 25.2
- iOS 26.2
- macOS 26.2
- tvOS 26.2
- visionOS 26.2
- watchOS 26.2
Android SDK: Not Found
IDEs:
Android Studio: 2025.2 AI-252.28238.7.2523.14688667
Xcode:
version: 26.2/17C52
path: /usr/bin/xcodebuild
Languages:
Java:
version: 21.0.10
path: /opt/homebrew/Cellar/openjdk@21/21.0.10/libexec/openjdk.jdk/Contents/Home/bin/javac
Ruby:
version: 2.6.10
path: /usr/bin/ruby
npmPackages:
"@react-native-community/cli":
installed: 20.1.1
wanted: ^20.1.1
react:
installed: 19.1.0
wanted: 19.1.0
react-native:
installed: 0.81.5
wanted: 0.81.5
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: true
iOS:
hermesEnabled: true
newArchEnabled: true
- Platform that you're experiencing the issue on:
- iOS
- Android
- iOS but have not tested behavior on Android
- Android but have not tested behavior on iOS
- Both
react-native-firebaseversion you're using that has this issue:- 23.8.4
Firebasemodule(s) you're using that has the issue:- @react-native-firebase/functions
- @react-native-firebase/auth
- Are you using
TypeScript?N
- π Check out
React Native FirebaseandInvertaseon Twitter for updates on the library.