Skip to content

Commit 496d97c

Browse files
committed
fix(ios): address PR review feedback — revert spacing, add Expo docs, improve SPM comments
1 parent 7b431cb commit 496d97c

9 files changed

Lines changed: 169 additions & 1223 deletions

File tree

.github/workflows/tests_e2e_ios.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,8 @@ jobs:
218218
continue-on-error: true
219219
with:
220220
path: tests/ios/Pods
221-
key: ${{ runner.os }}-ios-pods-v3-${{ matrix.dep-resolution }}-${{ hashFiles('tests/ios/Podfile.lock') }}
222-
restore-keys: ${{ runner.os }}-ios-pods-v3-${{ matrix.dep-resolution }}
221+
key: ${{ runner.os }}-${{ matrix.dep-resolution }}-ios-pods-v3-${{ hashFiles('tests/ios/Podfile.lock') }}
222+
restore-keys: ${{ runner.os }}-${{ matrix.dep-resolution }}-ios-pods-v3
223223

224224
- name: Configure Dependency Resolution Mode
225225
run: |

DOCUMENTACION_SPM_IMPLEMENTACION.md

Lines changed: 0 additions & 1135 deletions
This file was deleted.

packages/analytics/RNFBAnalytics.podspec

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ Pod::Spec.new do |s|
4747

4848
# Firebase dependencies
4949
# Analytics has conditional dependencies that vary between SPM and CocoaPods.
50-
# SPM: use FirebaseAnalyticsWithoutAdIdSupport when $RNFirebaseAnalyticsWithoutAdIdSupport = true
51-
# to avoid GoogleAppMeasurement APM symbols that require FirebaseRemoteConfig (linker error).
50+
# SPM: use FirebaseAnalyticsCore when $RNFirebaseAnalyticsWithoutAdIdSupport = true
51+
# to avoid GoogleAppMeasurement APM symbols (APMETaskManager, APMMeasurement)
52+
# that require FirebasePerformance at link time.
5253
# CocoaPods: IdentitySupport is a separate subspec controlled by $RNFirebaseAnalyticsWithoutAdIdSupport.
5354
if defined?(spm_dependency) && !defined?($RNFirebaseDisableSPM) &&
5455
defined?($RNFirebaseAnalyticsWithoutAdIdSupport) && $RNFirebaseAnalyticsWithoutAdIdSupport
@@ -80,10 +81,21 @@ Pod::Spec.new do |s|
8081
s.frameworks = 'AdSupport'
8182
end
8283

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

89101
if defined?($RNFirebaseAsStaticFramework)

packages/app-check/ios/RNFBAppCheck/RNFBAppCheckModule.m

Lines changed: 56 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@ + (instancetype)sharedInstance {
6060
: (BOOL)isTokenAutoRefreshEnabled
6161
: (RCTPromiseResolveBlock)resolve rejecter
6262
: (RCTPromiseRejectBlock)reject) {
63-
DLog(@"deprecated API, provider will be deviceCheck / token refresh %d for "
64-
@"app %@",
63+
DLog(@"deprecated API, provider will be deviceCheck / token refresh %d for app %@",
6564
isTokenAutoRefreshEnabled, firebaseApp.name);
6665
[[RNFBAppCheckModule sharedInstance].providerFactory configure:firebaseApp
6766
providerName:@"deviceCheck"
@@ -94,8 +93,8 @@ + (instancetype)sharedInstance {
9493
appCheck.isTokenAutoRefreshEnabled = isTokenAutoRefreshEnabled;
9594
}
9695

97-
// Not present in JS or Android - it is iOS-specific so we only call this in
98-
// testing - it is not in index.d.ts
96+
// Not present in JS or Android - it is iOS-specific so we only call this in testing - it is not in
97+
// index.d.ts
9998
RCT_EXPORT_METHOD(isTokenAutoRefreshEnabled
10099
: (FIRApp *)firebaseApp
101100
: (RCTPromiseResolveBlock)resolve rejecter
@@ -113,36 +112,33 @@ + (instancetype)sharedInstance {
113112
: (RCTPromiseRejectBlock)reject) {
114113
FIRAppCheck *appCheck = [FIRAppCheck appCheckWithApp:firebaseApp];
115114
DLog(@"appName %@", firebaseApp.name);
116-
[appCheck tokenForcingRefresh:forceRefresh
117-
completion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) {
118-
if (error != nil) {
119-
// Handle any errors if the token was not retrieved.
120-
DLog(@"RNFBAppCheck - getToken - Unable to retrieve App "
121-
@"Check token: %@",
122-
error);
123-
[RNFBSharedUtils
124-
rejectPromiseWithUserInfo:reject
125-
userInfo:(NSMutableDictionary *)@{
126-
@"code" : @"token-error",
127-
@"message" : [error localizedDescription],
128-
}];
129-
return;
130-
}
131-
if (token == nil) {
132-
DLog(@"RNFBAppCheck - getToken - Unable to retrieve App "
133-
@"Check token.");
134-
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
135-
userInfo:(NSMutableDictionary *)@{
136-
@"code" : @"token-null",
137-
@"message" : @"no token fetched",
138-
}];
139-
return;
140-
}
141-
142-
NSMutableDictionary *tokenResultDictionary = [NSMutableDictionary new];
143-
tokenResultDictionary[@"token"] = token.token;
144-
resolve(tokenResultDictionary);
145-
}];
115+
[appCheck
116+
tokenForcingRefresh:forceRefresh
117+
completion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) {
118+
if (error != nil) {
119+
// Handle any errors if the token was not retrieved.
120+
DLog(@"RNFBAppCheck - getToken - Unable to retrieve App Check token: %@", error);
121+
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
122+
userInfo:(NSMutableDictionary *)@{
123+
@"code" : @"token-error",
124+
@"message" : [error localizedDescription],
125+
}];
126+
return;
127+
}
128+
if (token == nil) {
129+
DLog(@"RNFBAppCheck - getToken - Unable to retrieve App Check token.");
130+
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
131+
userInfo:(NSMutableDictionary *)@{
132+
@"code" : @"token-null",
133+
@"message" : @"no token fetched",
134+
}];
135+
return;
136+
}
137+
138+
NSMutableDictionary *tokenResultDictionary = [NSMutableDictionary new];
139+
tokenResultDictionary[@"token"] = token.token;
140+
resolve(tokenResultDictionary);
141+
}];
146142
}
147143

148144
RCT_EXPORT_METHOD(getLimitedUseToken
@@ -151,35 +147,32 @@ + (instancetype)sharedInstance {
151147
: (RCTPromiseRejectBlock)reject) {
152148
FIRAppCheck *appCheck = [FIRAppCheck appCheckWithApp:firebaseApp];
153149
DLog(@"appName %@", firebaseApp.name);
154-
[appCheck
155-
limitedUseTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) {
156-
if (error != nil) {
157-
// Handle any errors if the token was not retrieved.
158-
DLog(@"RNFBAppCheck - getLimitedUseToken - Unable to retrieve App Check "
159-
@"token: %@",
160-
error);
161-
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
162-
userInfo:(NSMutableDictionary *)@{
163-
@"code" : @"token-error",
164-
@"message" : [error localizedDescription],
165-
}];
166-
return;
167-
}
168-
if (token == nil) {
169-
DLog(@"RNFBAppCheck - getLimitedUseToken - Unable to retrieve App Check "
170-
@"token.");
171-
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
172-
userInfo:(NSMutableDictionary *)@{
173-
@"code" : @"token-null",
174-
@"message" : @"no token fetched",
175-
}];
176-
return;
177-
}
178-
179-
NSMutableDictionary *tokenResultDictionary = [NSMutableDictionary new];
180-
tokenResultDictionary[@"token"] = token.token;
181-
resolve(tokenResultDictionary);
182-
}];
150+
[appCheck limitedUseTokenWithCompletion:^(FIRAppCheckToken *_Nullable token,
151+
NSError *_Nullable error) {
152+
if (error != nil) {
153+
// Handle any errors if the token was not retrieved.
154+
DLog(@"RNFBAppCheck - getLimitedUseToken - Unable to retrieve App Check token: %@", error);
155+
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
156+
userInfo:(NSMutableDictionary *)@{
157+
@"code" : @"token-error",
158+
@"message" : [error localizedDescription],
159+
}];
160+
return;
161+
}
162+
if (token == nil) {
163+
DLog(@"RNFBAppCheck - getLimitedUseToken - Unable to retrieve App Check token.");
164+
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
165+
userInfo:(NSMutableDictionary *)@{
166+
@"code" : @"token-null",
167+
@"message" : @"no token fetched",
168+
}];
169+
return;
170+
}
171+
172+
NSMutableDictionary *tokenResultDictionary = [NSMutableDictionary new];
173+
tokenResultDictionary[@"token"] = token.token;
174+
resolve(tokenResultDictionary);
175+
}];
183176
}
184177

185178
@end

packages/app/.npmignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,8 @@ type-test.ts
6969
scripts
7070
__tests__
7171

72-
# Force include generated version file (needed for linking)
72+
# Force include generated version file (needed for linking).
73+
# This file is in .gitignore but must be in the published npm package.
74+
# Note: this package does not use a "files" array in package.json,
75+
# so .npmignore is the sole mechanism controlling published file set.
7376
!ios/RNFBApp/RNFBVersion.m

packages/app/README.md

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,23 @@ yarn add @react-native-firebase/app
3838

3939
## iOS Dependency Resolution: SPM vs CocoaPods
4040

41-
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.
41+
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.
4242

4343
### How it works
4444

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

4747
| Condition | Resolution | When to use |
4848
|-----------|-----------|-------------|
49-
| React Native >= 0.75 and `$RNFirebaseDisableSPM` **not set** | **SPM** (default) | Dynamic linkage (`use_frameworks! :linkage => :dynamic`) |
50-
| `$RNFirebaseDisableSPM = true` in Podfile | **CocoaPods** | Static linkage or projects that need CocoaPods-only resolution |
51-
| React Native < 0.75 | **CocoaPods** (automatic fallback) | Older React Native versions without `spm_dependency` support |
49+
| RN >= 0.75 and `$RNFirebaseDisableSPM` **not set** | **SPM** (default) | Dynamic linkage / pre-built RN core (`use_frameworks! :linkage => :dynamic`) |
50+
| `$RNFirebaseDisableSPM = true` in Podfile | **CocoaPods** | Static linkage / no pre-built RN core (`use_frameworks! :linkage => :static`) |
51+
| RN < 0.75 | **CocoaPods** (automatic fallback) | Older React Native versions without `spm_dependency` support |
52+
53+
> **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.
5254
5355
### Configuration
5456

55-
**Option A — SPM (default, recommended for Xcode 26+)**
57+
#### Option A — SPM (default, recommended for Xcode 26+)
5658

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

@@ -69,7 +71,7 @@ use_frameworks! :linkage => :dynamic
6971
> This does NOT disable SPM — it only tells the Swift compiler to use implicit module discovery
7072
> (the Xcode 16 default) so transitive SPM targets are resolved automatically.
7173
72-
**Option B — CocoaPods only**
74+
#### Option B — CocoaPods only
7375
7476
Add this line at the top of your Podfile (before any `target` block):
7577
@@ -81,6 +83,52 @@ $RNFirebaseDisableSPM = true
8183
This forces all RNFB modules to use traditional `s.dependency` CocoaPods declarations.
8284
You can use either static or dynamic linkage with this option.
8385

86+
#### Expo
87+
88+
For Expo managed projects, use `expo-build-properties` to configure linkage and Podfile directives:
89+
90+
```json
91+
// app.json
92+
{
93+
"expo": {
94+
"plugins": [
95+
[
96+
"expo-build-properties",
97+
{
98+
"ios": {
99+
"useFrameworks": "dynamic"
100+
}
101+
}
102+
]
103+
]
104+
}
105+
}
106+
```
107+
108+
To disable SPM in Expo, add a Podfile directive via a config plugin or `app.json`:
109+
110+
```json
111+
// app.json
112+
{
113+
"expo": {
114+
"plugins": [
115+
[
116+
"expo-build-properties",
117+
{
118+
"ios": {
119+
"useFrameworks": "static",
120+
"extraPods": []
121+
}
122+
}
123+
]
124+
]
125+
}
126+
}
127+
```
128+
129+
Then create a small [config plugin](https://docs.expo.dev/config-plugins/introduction/) to prepend
130+
`$RNFirebaseDisableSPM = true` to the generated Podfile, or add it manually if you have ejected.
131+
84132
### How to verify
85133

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

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

99155
- See [LICENSE](/LICENSE)

packages/app/firebase_spm.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@
1818

1919
require 'json'
2020

21-
# Read Firebase SPM URL from app package.json (single source of truth)
21+
# Read Firebase SPM URL from app package.json (single source of truth).
22+
# __dir__ resolves to the directory of this file (packages/app/).
23+
# In monorepos with hoisted dependencies or pnpm, the path from other packages
24+
# (e.g., `require '../app/firebase_spm'`) must resolve correctly to this location.
25+
# If your package manager hoists differently, you may need to adjust the require
26+
# path in individual podspecs.
2227
$firebase_spm_url ||= begin
2328
app_package_path = File.join(__dir__, 'package.json')
2429
app_package = JSON.parse(File.read(app_package_path))
@@ -32,10 +37,15 @@
3237
# the traditional CocoaPods `s.dependency` declaration.
3338
#
3439
# Set `$RNFirebaseDisableSPM = true` in your Podfile to force CocoaPods-only
35-
# dependency resolution. This is required when using `use_frameworks! :linkage => :static`
40+
# dependency resolution. You must disable SPM when using `use_frameworks! :linkage => :static`
3641
# because static frameworks cause each pod to embed Firebase SPM products,
3742
# resulting in duplicate symbol linker errors.
3843
#
44+
# firebase-ios-sdk SPM requires dynamic linkage. There is no upstream statement
45+
# from Google that SPM+static is supported. See:
46+
# https://github.com/firebase/firebase-ios-sdk/blob/main/Package.swift
47+
# (all products use .library(type: .dynamic))
48+
#
3949
# @param spec [Pod::Specification] The podspec object (the `s` in podspec DSL)
4050
# @param version [String] Firebase SDK version (e.g., '12.10.0')
4151
# @param spm_products [Array<String>] SPM product names (e.g., ['FirebaseAuth'])

packages/crashlytics/ios_config.sh

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,20 @@ elif [[ -f "${PROJECT_DIR}/FirebaseCrashlytics.framework/run" ]]; then
2323
echo "info: Exec FirebaseCrashlytics Run from framework"
2424
"${PROJECT_DIR}/FirebaseCrashlytics.framework/run"
2525
else
26-
# SPM: upload-symbols is in the SourcePackages checkout
27-
SPM_UPLOAD_SYMBOLS=$(find "${BUILD_DIR%Build/*}SourcePackages/checkouts/firebase-ios-sdk/Crashlytics" -name "upload-symbols" -type f 2>/dev/null | head -1)
28-
if [[ -n "${SPM_UPLOAD_SYMBOLS}" ]]; then
26+
# SPM: upload-symbols is at a known path in the SourcePackages checkout.
27+
# BUILD_DIR is typically DerivedData/Project-hash/Build/Products — strip from /Build onward
28+
# to get the DerivedData project root where SourcePackages lives.
29+
SPM_CRASHLYTICS_DIR="${BUILD_DIR%Build/*}SourcePackages/checkouts/firebase-ios-sdk/Crashlytics"
30+
SPM_UPLOAD_SYMBOLS="${SPM_CRASHLYTICS_DIR}/upload-symbols"
31+
if [[ -x "${SPM_UPLOAD_SYMBOLS}" ]]; then
2932
echo "info: Exec FirebaseCrashlytics upload-symbols from SPM"
3033
"${SPM_UPLOAD_SYMBOLS}" -gsp "${PROJECT_DIR}/GoogleService-Info.plist" -p ios "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}"
34+
elif [[ -f "${SPM_UPLOAD_SYMBOLS}" ]]; then
35+
echo "info: Exec FirebaseCrashlytics upload-symbols from SPM (chmod +x)"
36+
chmod +x "${SPM_UPLOAD_SYMBOLS}"
37+
"${SPM_UPLOAD_SYMBOLS}" -gsp "${PROJECT_DIR}/GoogleService-Info.plist" -p ios "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}"
3138
else
32-
echo "warning: FirebaseCrashlytics run script not found, skipping dSYM upload"
39+
echo "warning: FirebaseCrashlytics run script not found at CocoaPods, framework, or SPM paths. Skipping dSYM upload."
40+
echo "warning: Checked: \${PODS_ROOT}/FirebaseCrashlytics/run, \${PROJECT_DIR}/FirebaseCrashlytics.framework/run, ${SPM_UPLOAD_SYMBOLS}"
3341
fi
3442
fi

packages/ml/RNFBML.podspec

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,15 @@ Pod::Spec.new do |s|
4747
end
4848

4949
# Firebase dependencies
50-
# NOTE: Firebase/MLModelDownloader dependency is currently disabled
51-
# When re-enabled, use: firebase_dependency(s, firebase_sdk_version,
52-
# ['FirebaseMLModelDownloader'], 'Firebase/MLModelDownloader')
53-
# s.dependency 'Firebase/MLModelDownloader', firebase_sdk_version
50+
# NOTE: Firebase/MLModelDownloader dependency is currently disabled.
51+
# When re-enabled, use:
52+
# firebase_dependency(s, firebase_sdk_version,
53+
# ['FirebaseMLModelDownloader'], 'Firebase/MLModelDownloader')
5454

5555
if defined?($RNFirebaseAsStaticFramework)
5656
Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'"
5757
s.static_framework = $RNFirebaseAsStaticFramework
5858
else
59-
# raise "#{s.name}: Underlying Firebase/MLModelDownloader requires $RNFirebaseAsStaticFrameworks = true and !use_frameworks in your Podfile"
6059
s.static_framework = false
6160
end
6261
end

0 commit comments

Comments
 (0)