Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/tests_e2e_ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,8 @@ jobs:
continue-on-error: true
with:
path: tests/ios/Pods
key: ${{ runner.os }}-ios-pods-v3-${{ matrix.dep-resolution }}-${{ hashFiles('tests/ios/Podfile.lock') }}
restore-keys: ${{ runner.os }}-ios-pods-v3-${{ matrix.dep-resolution }}
key: ${{ runner.os }}-${{ matrix.dep-resolution }}-ios-pods-v3-${{ hashFiles('tests/ios/Podfile.lock') }}
restore-keys: ${{ runner.os }}-${{ matrix.dep-resolution }}-ios-pods-v3

- name: Configure Dependency Resolution Mode
run: |
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ typings/

# Output of 'npm pack'
*.tgz
# Exception: tarballs/ is a versioned folder for distributing prebuilt packages
!tarballs/*.tgz

# Yarn Integrity file
.yarn-integrity
Expand Down
1,135 changes: 0 additions & 1,135 deletions DOCUMENTACION_SPM_IMPLEMENTACION.md

This file was deleted.

26 changes: 19 additions & 7 deletions packages/analytics/RNFBAnalytics.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ Pod::Spec.new do |s|

# Firebase dependencies
# Analytics has conditional dependencies that vary between SPM and CocoaPods.
# SPM: use FirebaseAnalyticsWithoutAdIdSupport when $RNFirebaseAnalyticsWithoutAdIdSupport = true
# to avoid GoogleAppMeasurement APM symbols that require FirebaseRemoteConfig (linker error).
# SPM: use FirebaseAnalyticsCore when $RNFirebaseAnalyticsWithoutAdIdSupport = true
# to avoid GoogleAppMeasurement APM symbols (APMETaskManager, APMMeasurement)
# that require FirebasePerformance at link time.
# CocoaPods: IdentitySupport is a separate subspec controlled by $RNFirebaseAnalyticsWithoutAdIdSupport.
if defined?(spm_dependency) && !defined?($RNFirebaseDisableSPM) &&
if defined?(spm_dependency) && !rnfirebase_spm_disabled? &&
defined?($RNFirebaseAnalyticsWithoutAdIdSupport) && $RNFirebaseAnalyticsWithoutAdIdSupport
# FirebaseAnalyticsCore uses GoogleAppMeasurementCore (no IDFA, no APM objects).
# FirebaseAnalytics uses GoogleAppMeasurement which has APMETaskManager/APMMeasurement
Expand All @@ -61,7 +62,7 @@ Pod::Spec.new do |s|
firebase_dependency(s, firebase_sdk_version, ['FirebaseAnalytics'], 'FirebaseAnalytics/Core')
end

unless defined?(spm_dependency)
unless defined?(spm_dependency) && !rnfirebase_spm_disabled?
# CocoaPods-only: conditional IdentitySupport subspec
if defined?($RNFirebaseAnalyticsWithoutAdIdSupport) && ($RNFirebaseAnalyticsWithoutAdIdSupport == true)
Pod::UI.puts "#{s.name}: Not installing FirebaseAnalytics/IdentitySupport Pod, no IDFA will be collected."
Expand All @@ -80,10 +81,21 @@ Pod::Spec.new do |s|
s.frameworks = 'AdSupport'
end

# GoogleAdsOnDeviceConversion (CocoaPods only, not available in firebase-ios-sdk SPM)
# GoogleAdsOnDeviceConversion (CocoaPods only)
# This is a static xcframework distributed separately from firebase-ios-sdk.
# It is NOT available as an SPM product in the firebase-ios-sdk Package.swift.
# When using SPM (dynamic linkage), this static xcframework causes duplicate
# symbol errors. Use CocoaPods mode ($RNFirebaseDisableSPM = true) if you need
# on-device conversion measurement.
# See: https://developers.google.com/google-ads/api/docs/conversions/upload-identifiers
if defined?($RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion) && ($RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion == true)
Pod::UI.puts "#{s.name}: GoogleAdsOnDeviceConversion pod added"
s.dependency 'GoogleAdsOnDeviceConversion'
if defined?(spm_dependency) && !rnfirebase_spm_disabled?
Pod::UI.warn "#{s.name}: GoogleAdsOnDeviceConversion is not available in SPM mode. " \
"Set $RNFirebaseDisableSPM = true in your Podfile to use this feature."
else
Pod::UI.puts "#{s.name}: GoogleAdsOnDeviceConversion pod added"
s.dependency 'GoogleAdsOnDeviceConversion'
end
end

if defined?($RNFirebaseAsStaticFramework)
Expand Down
119 changes: 56 additions & 63 deletions packages/app-check/ios/RNFBAppCheck/RNFBAppCheckModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ + (instancetype)sharedInstance {
: (BOOL)isTokenAutoRefreshEnabled
: (RCTPromiseResolveBlock)resolve rejecter
: (RCTPromiseRejectBlock)reject) {
DLog(@"deprecated API, provider will be deviceCheck / token refresh %d for "
@"app %@",
DLog(@"deprecated API, provider will be deviceCheck / token refresh %d for app %@",
isTokenAutoRefreshEnabled, firebaseApp.name);
[[RNFBAppCheckModule sharedInstance].providerFactory configure:firebaseApp
providerName:@"deviceCheck"
Expand Down Expand Up @@ -94,8 +93,8 @@ + (instancetype)sharedInstance {
appCheck.isTokenAutoRefreshEnabled = isTokenAutoRefreshEnabled;
}

// Not present in JS or Android - it is iOS-specific so we only call this in
// testing - it is not in index.d.ts
// Not present in JS or Android - it is iOS-specific so we only call this in testing - it is not in
// index.d.ts
RCT_EXPORT_METHOD(isTokenAutoRefreshEnabled
: (FIRApp *)firebaseApp
: (RCTPromiseResolveBlock)resolve rejecter
Expand All @@ -113,36 +112,33 @@ + (instancetype)sharedInstance {
: (RCTPromiseRejectBlock)reject) {
FIRAppCheck *appCheck = [FIRAppCheck appCheckWithApp:firebaseApp];
DLog(@"appName %@", firebaseApp.name);
[appCheck tokenForcingRefresh:forceRefresh
completion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) {
if (error != nil) {
// Handle any errors if the token was not retrieved.
DLog(@"RNFBAppCheck - getToken - Unable to retrieve App "
@"Check token: %@",
error);
[RNFBSharedUtils
rejectPromiseWithUserInfo:reject
userInfo:(NSMutableDictionary *)@{
@"code" : @"token-error",
@"message" : [error localizedDescription],
}];
return;
}
if (token == nil) {
DLog(@"RNFBAppCheck - getToken - Unable to retrieve App "
@"Check token.");
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
userInfo:(NSMutableDictionary *)@{
@"code" : @"token-null",
@"message" : @"no token fetched",
}];
return;
}

NSMutableDictionary *tokenResultDictionary = [NSMutableDictionary new];
tokenResultDictionary[@"token"] = token.token;
resolve(tokenResultDictionary);
}];
[appCheck
tokenForcingRefresh:forceRefresh
completion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) {
if (error != nil) {
// Handle any errors if the token was not retrieved.
DLog(@"RNFBAppCheck - getToken - Unable to retrieve App Check token: %@", error);
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
userInfo:(NSMutableDictionary *)@{
@"code" : @"token-error",
@"message" : [error localizedDescription],
}];
return;
}
if (token == nil) {
DLog(@"RNFBAppCheck - getToken - Unable to retrieve App Check token.");
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
userInfo:(NSMutableDictionary *)@{
@"code" : @"token-null",
@"message" : @"no token fetched",
}];
return;
}

NSMutableDictionary *tokenResultDictionary = [NSMutableDictionary new];
tokenResultDictionary[@"token"] = token.token;
resolve(tokenResultDictionary);
}];
}

RCT_EXPORT_METHOD(getLimitedUseToken
Expand All @@ -151,35 +147,32 @@ + (instancetype)sharedInstance {
: (RCTPromiseRejectBlock)reject) {
FIRAppCheck *appCheck = [FIRAppCheck appCheckWithApp:firebaseApp];
DLog(@"appName %@", firebaseApp.name);
[appCheck
limitedUseTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) {
if (error != nil) {
// Handle any errors if the token was not retrieved.
DLog(@"RNFBAppCheck - getLimitedUseToken - Unable to retrieve App Check "
@"token: %@",
error);
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
userInfo:(NSMutableDictionary *)@{
@"code" : @"token-error",
@"message" : [error localizedDescription],
}];
return;
}
if (token == nil) {
DLog(@"RNFBAppCheck - getLimitedUseToken - Unable to retrieve App Check "
@"token.");
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
userInfo:(NSMutableDictionary *)@{
@"code" : @"token-null",
@"message" : @"no token fetched",
}];
return;
}

NSMutableDictionary *tokenResultDictionary = [NSMutableDictionary new];
tokenResultDictionary[@"token"] = token.token;
resolve(tokenResultDictionary);
}];
[appCheck limitedUseTokenWithCompletion:^(FIRAppCheckToken *_Nullable token,
NSError *_Nullable error) {
if (error != nil) {
// Handle any errors if the token was not retrieved.
DLog(@"RNFBAppCheck - getLimitedUseToken - Unable to retrieve App Check token: %@", error);
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
userInfo:(NSMutableDictionary *)@{
@"code" : @"token-error",
@"message" : [error localizedDescription],
}];
return;
}
if (token == nil) {
DLog(@"RNFBAppCheck - getLimitedUseToken - Unable to retrieve App Check token.");
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
userInfo:(NSMutableDictionary *)@{
@"code" : @"token-null",
@"message" : @"no token fetched",
}];
return;
}

NSMutableDictionary *tokenResultDictionary = [NSMutableDictionary new];
tokenResultDictionary[@"token"] = token.token;
resolve(tokenResultDictionary);
}];
}

@end
5 changes: 4 additions & 1 deletion packages/app/.npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,8 @@ type-test.ts
scripts
__tests__

# Force include generated version file (needed for linking)
# Force include generated version file (needed for linking).
# This file is in .gitignore but must be in the published npm package.
# Note: this package does not use a "files" array in package.json,
# so .npmignore is the sole mechanism controlling published file set.
!ios/RNFBApp/RNFBVersion.m
68 changes: 62 additions & 6 deletions packages/app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,23 @@ yarn add @react-native-firebase/app

## iOS Dependency Resolution: SPM vs CocoaPods

Starting with React Native >= 0.75, `@react-native-firebase` supports **Swift Package Manager (SPM)** for resolving Firebase iOS SDK dependencies. SPM is enabled by default — no configuration needed.
Starting with React Native 0.75+, `@react-native-firebase` supports **Swift Package Manager (SPM)** for resolving Firebase iOS SDK dependencies. SPM is enabled by default when the `spm_dependency` macro is available (injected by React Native >= 0.75) — no configuration needed.

### How it works

Each RNFB module uses `firebase_dependency()` (defined in `firebase_spm.rb`) to declare its Firebase dependencies. This helper automatically chooses between:

| Condition | Resolution | When to use |
|-----------|-----------|-------------|
| React Native >= 0.75 and `$RNFirebaseDisableSPM` **not set** | **SPM** (default) | Dynamic linkage (`use_frameworks! :linkage => :dynamic`) |
| `$RNFirebaseDisableSPM = true` in Podfile | **CocoaPods** | Static linkage or projects that need CocoaPods-only resolution |
| React Native < 0.75 | **CocoaPods** (automatic fallback) | Older React Native versions without `spm_dependency` support |
| RN >= 0.75 and `$RNFirebaseDisableSPM` **not set** | **SPM** (default) | Dynamic linkage / pre-built RN core (`use_frameworks! :linkage => :dynamic`) |
| `$RNFirebaseDisableSPM = true` in Podfile | **CocoaPods** | Static linkage / no pre-built RN core (`use_frameworks! :linkage => :static`) |
| RN < 0.75 | **CocoaPods** (automatic fallback) | Older React Native versions without `spm_dependency` support |

> **Note on linkage:** firebase-ios-sdk SPM products use dynamic linkage. When using `use_frameworks! :linkage => :static`, each pod embeds its own copy of Firebase SPM products, causing duplicate symbol errors. Use CocoaPods mode (`$RNFirebaseDisableSPM = true`) with static linkage.

### Configuration

**Option A — SPM (default, recommended for Xcode 26+)**
#### Option A — SPM (default, recommended for Xcode 26+)

No changes needed. Just make sure your Podfile uses dynamic linkage:

Expand All @@ -69,7 +71,7 @@ use_frameworks! :linkage => :dynamic
> This does NOT disable SPM — it only tells the Swift compiler to use implicit module discovery
> (the Xcode 16 default) so transitive SPM targets are resolved automatically.

**Option B — CocoaPods only**
#### Option B — CocoaPods only

Add this line at the top of your Podfile (before any `target` block):

Expand All @@ -81,6 +83,52 @@ $RNFirebaseDisableSPM = true
This forces all RNFB modules to use traditional `s.dependency` CocoaPods declarations.
You can use either static or dynamic linkage with this option.

#### Expo

For Expo managed projects, use `expo-build-properties` to configure linkage and Podfile directives:

```json
// app.json
{
"expo": {
"plugins": [
[
"expo-build-properties",
{
"ios": {
"useFrameworks": "dynamic"
}
}
]
]
}
}
```

To disable SPM in Expo, add a Podfile directive via a config plugin or `app.json`:

```json
// app.json
{
"expo": {
"plugins": [
[
"expo-build-properties",
{
"ios": {
"useFrameworks": "static",
"extraPods": []
}
}
]
]
}
}
```

Then create a small [config plugin](https://docs.expo.dev/config-plugins/introduction/) to prepend
`$RNFirebaseDisableSPM = true` to the generated Podfile, or add it manually if you have ejected.

### How to verify

During `pod install`, you will see messages indicating which resolution mode is active:
Expand All @@ -94,6 +142,14 @@ During `pod install`, you will see messages indicating which resolution mode is
[react-native-firebase] RNFBApp: SPM disabled ($RNFirebaseDisableSPM = true), using CocoaPods for Firebase dependencies
```

### Monorepo / pnpm notes

The `firebase_spm.rb` helper is loaded by each RNFB podspec via `require '../app/firebase_spm'`.
This relative path assumes the standard `node_modules/@react-native-firebase/` layout. If your
package manager hoists dependencies differently (e.g., pnpm strict mode), you may need to verify
that the require path resolves correctly. The SPM URL is read from
`@react-native-firebase/app/package.json` at the location of `firebase_spm.rb`.

## License

- See [LICENSE](/LICENSE)
Expand Down
12 changes: 11 additions & 1 deletion packages/app/RNFBApp.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ Pod::Spec.new do |s|
"DEFINES_MODULE" => "YES",
}

# RNFBUtilsModule.mm uses PHAsset (Photos.framework) to resolve local asset paths.
# Not declaring this explicitly used to work by luck (CocoaPods normally relies on
# this declaration -- not Clang autolinking -- to populate OTHER_LDFLAGS), but with
# use_frameworks! each pod is a standalone dynamic framework that must resolve its
# own symbols at its own link step, so the missing declaration now surfaces as
# "Undefined symbols ... _OBJC_CLASS_$_PHAsset". iOS/macOS only -- PhotoKit doesn't
# exist on tvOS.
s.ios.frameworks = 'Photos'
s.osx.frameworks = 'Photos'

# React Native dependencies
if defined?(install_modules_dependencies()) != nil
install_modules_dependencies(s);
Expand All @@ -62,7 +72,7 @@ Pod::Spec.new do |s|
end

# Firebase dependencies
firebase_dependency(s, firebase_sdk_version, ['FirebaseCore'], 'Firebase/CoreOnly')
firebase_dependency(s, firebase_sdk_version, ['FirebaseCore', 'FirebaseInstallations'], 'Firebase/CoreOnly')

if defined?($RNFirebaseAsStaticFramework)
Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'"
Expand Down
Loading
Loading