Skip to content

Commit 388cc49

Browse files
committed
feat(ios): add SPM dependency resolution support alongside CocoaPods
Add dual SPM/CocoaPods dependency resolution for Firebase iOS SDK. When React Native >= 0.75 is detected, Firebase dependencies are resolved via Swift Package Manager (spm_dependency). For older versions or when explicitly disabled ($RNFirebaseDisableSPM = true), CocoaPods is used. Changes: - Add firebase_spm.rb helper with firebase_dependency() function - Add firebaseSpmUrl to packages/app/package.json (single source of truth) - Update all 16 podspecs to use firebase_dependency() - Add #if __has_include guards in 43 native iOS files for dual imports - Add CI matrix (spm × cocoapods × debug × release) in E2E workflow - Add Ruby unit tests for firebase_spm.rb - Add documentation at docs/ios-spm.md
1 parent f3941a0 commit 388cc49

File tree

72 files changed

+1625
-136
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+1625
-136
lines changed

.github/workflows/tests_e2e_ios.yml

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,21 @@ jobs:
7575
// we want to test debug and release - they generate different code
7676
let buildmode = ['debug', 'release'];
7777
78+
// Test both SPM and CocoaPods dependency resolution modes
79+
let depResolution = ['spm', 'cocoapods'];
80+
7881
return {
7982
"iteration": iterationArray,
80-
"buildmode": buildmode
83+
"buildmode": buildmode,
84+
"dep-resolution": depResolution
8185
}
8286
- name: Debug Output
8387
run: echo "${{ steps.build-matrix.outputs.result }}"
8488

8589
# This uses the matrix generated from the matrix-prep stage
8690
# it will run unit tests on whatever OS combinations are desired
8791
ios:
88-
name: iOS (${{ matrix.buildmode }}, ${{ matrix.iteration }})
92+
name: iOS (${{ matrix.buildmode }}, ${{ matrix.dep-resolution }}, ${{ matrix.iteration }})
8993
runs-on: macos-15
9094
needs: matrix_prep
9195
# TODO matrix across APIs, at least 11 and 15 (lowest to highest)
@@ -182,7 +186,7 @@ jobs:
182186
- uses: hendrikmuhs/ccache-action@v1
183187
name: Xcode Compile Cache
184188
with:
185-
key: ${{ runner.os }}-${{ matrix.buildmode }}-ios-v3 # makes a unique key w/related restore key internally
189+
key: ${{ runner.os }}-${{ matrix.buildmode }}-${{ matrix.dep-resolution }}-ios-v3 # makes a unique key w/related restore key internally
186190
save: "${{ github.ref == 'refs/heads/main' }}"
187191
create-symlink: true
188192
max-size: 1500M
@@ -214,8 +218,21 @@ jobs:
214218
continue-on-error: true
215219
with:
216220
path: tests/ios/Pods
217-
key: ${{ runner.os }}-ios-pods-v3-${{ hashFiles('tests/ios/Podfile.lock') }}
218-
restore-keys: ${{ runner.os }}-ios-pods-v3
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 }}
223+
224+
- name: Configure Dependency Resolution Mode
225+
run: |
226+
if [[ "${{ matrix.dep-resolution }}" == "cocoapods" ]]; then
227+
echo "Configuring CocoaPods-only mode (disabling SPM)"
228+
cd tests/ios
229+
sed -i '' "s/^linkage = 'dynamic'/linkage = 'static'/" Podfile
230+
printf '%s\n' '$RNFirebaseDisableSPM = true' | cat - Podfile > Podfile.tmp && mv Podfile.tmp Podfile
231+
sed -i '' "/SWIFT_ENABLE_EXPLICIT_MODULES/d" Podfile
232+
echo "Podfile configured for CocoaPods-only mode"
233+
else
234+
echo "Using default SPM mode (dynamic linkage)"
235+
fi
219236
220237
- name: Pod Install
221238
uses: nick-fields/retry@v3

.github/workflows/tests_jest.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ jobs:
5252
retry_wait_seconds: 60
5353
max_attempts: 3
5454
command: yarn
55+
- name: Test Firebase SPM Helper
56+
run: ruby packages/app/__tests__/firebase_spm_test.rb
5557
- name: Jest
5658
run: yarn tests:jest-coverage
5759
- uses: codecov/codecov-action@v5

docs/ios-spm.md

Lines changed: 760 additions & 0 deletions
Large diffs are not rendered by default.

docs/sidebar.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
- '/migrating-to-v24'
1010
- - TypeScript
1111
- '/typescript'
12+
- - iOS SPM Support
13+
- '/ios-spm'
1214
- - Platforms
1315
- '/platforms'
1416
- - Release Notes

packages/analytics/RNFBAnalytics.podspec

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'json'
2+
require '../app/firebase_spm'
23
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
34
appPackage = JSON.parse(File.read(File.join('..', 'app', 'package.json')))
45

@@ -45,24 +46,31 @@ Pod::Spec.new do |s|
4546
end
4647

4748
# Firebase dependencies
48-
s.dependency 'FirebaseAnalytics/Core', firebase_sdk_version
49-
if defined?($RNFirebaseAnalyticsWithoutAdIdSupport) && ($RNFirebaseAnalyticsWithoutAdIdSupport == true)
50-
Pod::UI.puts "#{s.name}: Not installing FirebaseAnalytics/IdentitySupport Pod, no IDFA will be collected."
51-
else
52-
if !defined?($RNFirebaseAnalyticsWithoutAdIdSupport)
53-
Pod::UI.puts "#{s.name}: Using FirebaseAnalytics/IdentitySupport with Ad Ids. May require App Tracking Transparency. Not allowed for Kids apps."
54-
Pod::UI.puts "#{s.name}: You may set variable `$RNFirebaseAnalyticsWithoutAdIdSupport=true` in Podfile to use analytics without ad ids."
55-
end
56-
s.dependency 'FirebaseAnalytics/IdentitySupport', firebase_sdk_version
49+
# Analytics has conditional dependencies that vary between SPM and CocoaPods.
50+
# SPM: FirebaseAnalytics includes ad ID support by default.
51+
# CocoaPods: IdentitySupport is a separate subspec controlled by $RNFirebaseAnalyticsWithoutAdIdSupport.
52+
firebase_dependency(s, firebase_sdk_version, ['FirebaseAnalytics'], 'FirebaseAnalytics/Core')
5753

58-
# Special pod for on-device conversion
59-
if defined?($RNFirebaseAnalyticsEnableAdSupport) && ($RNFirebaseAnalyticsEnableAdSupport == true)
60-
Pod::UI.puts "#{s.name}: Adding Apple AdSupport.framework dependency for optional analytics features"
61-
s.frameworks = 'AdSupport'
54+
unless defined?(spm_dependency)
55+
# CocoaPods-only: conditional IdentitySupport subspec
56+
if defined?($RNFirebaseAnalyticsWithoutAdIdSupport) && ($RNFirebaseAnalyticsWithoutAdIdSupport == true)
57+
Pod::UI.puts "#{s.name}: Not installing FirebaseAnalytics/IdentitySupport Pod, no IDFA will be collected."
58+
else
59+
if !defined?($RNFirebaseAnalyticsWithoutAdIdSupport)
60+
Pod::UI.puts "#{s.name}: Using FirebaseAnalytics/IdentitySupport with Ad Ids. May require App Tracking Transparency. Not allowed for Kids apps."
61+
Pod::UI.puts "#{s.name}: You may set variable `$RNFirebaseAnalyticsWithoutAdIdSupport=true` in Podfile to use analytics without ad ids."
62+
end
63+
s.dependency 'FirebaseAnalytics/IdentitySupport', firebase_sdk_version
6264
end
6365
end
6466

65-
# Special pod for on-device conversion
67+
# AdSupport framework (works with both SPM and CocoaPods)
68+
if defined?($RNFirebaseAnalyticsEnableAdSupport) && ($RNFirebaseAnalyticsEnableAdSupport == true)
69+
Pod::UI.puts "#{s.name}: Adding Apple AdSupport.framework dependency for optional analytics features"
70+
s.frameworks = 'AdSupport'
71+
end
72+
73+
# GoogleAdsOnDeviceConversion (CocoaPods only, not available in firebase-ios-sdk SPM)
6674
if defined?($RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion) && ($RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion == true)
6775
Pod::UI.puts "#{s.name}: GoogleAdsOnDeviceConversion pod added"
6876
s.dependency 'GoogleAdsOnDeviceConversion'

packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
*
1616
*/
1717

18+
#if __has_include(<Firebase/Firebase.h>)
1819
#import <Firebase/Firebase.h>
20+
#else
21+
@import FirebaseCore;
22+
@import FirebaseAnalytics;
23+
#endif
1924
#import <React/RCTUtils.h>
2025

2126
#if __has_include(<RNFBAnalytics/RNFBAnalytics-Swift.h>)

packages/app-check/RNFBAppCheck.podspec

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'json'
2+
require '../app/firebase_spm'
23
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
34
appPackage = JSON.parse(File.read(File.join('..', 'app', 'package.json')))
45

@@ -44,7 +45,7 @@ Pod::Spec.new do |s|
4445
end
4546

4647
# Firebase dependencies
47-
s.dependency 'Firebase/AppCheck', firebase_sdk_version
48+
firebase_dependency(s, firebase_sdk_version, ['FirebaseAppCheck'], 'Firebase/AppCheck')
4849

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

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

Lines changed: 71 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,15 @@
1515
*
1616
*/
1717

18+
#if __has_include(<Firebase/Firebase.h>)
1819
#import <Firebase/Firebase.h>
19-
#import <FirebaseAppCheck/FIRAppCheck.h>
20+
#elif __has_include(<FirebaseAppCheck/FirebaseAppCheck.h>)
21+
#import <FirebaseAppCheck/FirebaseAppCheck.h>
22+
#import <FirebaseCore/FirebaseCore.h>
23+
#else
24+
@import FirebaseCore;
25+
@import FirebaseAppCheck;
26+
#endif
2027

2128
#import <React/RCTUtils.h>
2229

@@ -53,7 +60,8 @@ + (instancetype)sharedInstance {
5360
: (BOOL)isTokenAutoRefreshEnabled
5461
: (RCTPromiseResolveBlock)resolve rejecter
5562
: (RCTPromiseRejectBlock)reject) {
56-
DLog(@"deprecated API, provider will be deviceCheck / token refresh %d for app %@",
63+
DLog(@"deprecated API, provider will be deviceCheck / token refresh %d for "
64+
@"app %@",
5765
isTokenAutoRefreshEnabled, firebaseApp.name);
5866
[[RNFBAppCheckModule sharedInstance].providerFactory configure:firebaseApp
5967
providerName:@"deviceCheck"
@@ -86,8 +94,8 @@ + (instancetype)sharedInstance {
8694
appCheck.isTokenAutoRefreshEnabled = isTokenAutoRefreshEnabled;
8795
}
8896

89-
// Not present in JS or Android - it is iOS-specific so we only call this in testing - it is not in
90-
// index.d.ts
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
9199
RCT_EXPORT_METHOD(isTokenAutoRefreshEnabled
92100
: (FIRApp *)firebaseApp
93101
: (RCTPromiseResolveBlock)resolve rejecter
@@ -105,33 +113,36 @@ + (instancetype)sharedInstance {
105113
: (RCTPromiseRejectBlock)reject) {
106114
FIRAppCheck *appCheck = [FIRAppCheck appCheckWithApp:firebaseApp];
107115
DLog(@"appName %@", firebaseApp.name);
108-
[appCheck
109-
tokenForcingRefresh:forceRefresh
110-
completion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) {
111-
if (error != nil) {
112-
// Handle any errors if the token was not retrieved.
113-
DLog(@"RNFBAppCheck - getToken - Unable to retrieve App Check token: %@", error);
114-
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
115-
userInfo:(NSMutableDictionary *)@{
116-
@"code" : @"token-error",
117-
@"message" : [error localizedDescription],
118-
}];
119-
return;
120-
}
121-
if (token == nil) {
122-
DLog(@"RNFBAppCheck - getToken - Unable to retrieve App Check token.");
123-
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
124-
userInfo:(NSMutableDictionary *)@{
125-
@"code" : @"token-null",
126-
@"message" : @"no token fetched",
127-
}];
128-
return;
129-
}
130-
131-
NSMutableDictionary *tokenResultDictionary = [NSMutableDictionary new];
132-
tokenResultDictionary[@"token"] = token.token;
133-
resolve(tokenResultDictionary);
134-
}];
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+
}];
135146
}
136147

137148
RCT_EXPORT_METHOD(getLimitedUseToken
@@ -140,32 +151,35 @@ + (instancetype)sharedInstance {
140151
: (RCTPromiseRejectBlock)reject) {
141152
FIRAppCheck *appCheck = [FIRAppCheck appCheckWithApp:firebaseApp];
142153
DLog(@"appName %@", firebaseApp.name);
143-
[appCheck limitedUseTokenWithCompletion:^(FIRAppCheckToken *_Nullable token,
144-
NSError *_Nullable error) {
145-
if (error != nil) {
146-
// Handle any errors if the token was not retrieved.
147-
DLog(@"RNFBAppCheck - getLimitedUseToken - Unable to retrieve App Check token: %@", error);
148-
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
149-
userInfo:(NSMutableDictionary *)@{
150-
@"code" : @"token-error",
151-
@"message" : [error localizedDescription],
152-
}];
153-
return;
154-
}
155-
if (token == nil) {
156-
DLog(@"RNFBAppCheck - getLimitedUseToken - Unable to retrieve App Check token.");
157-
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
158-
userInfo:(NSMutableDictionary *)@{
159-
@"code" : @"token-null",
160-
@"message" : @"no token fetched",
161-
}];
162-
return;
163-
}
164-
165-
NSMutableDictionary *tokenResultDictionary = [NSMutableDictionary new];
166-
tokenResultDictionary[@"token"] = token.token;
167-
resolve(tokenResultDictionary);
168-
}];
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+
}];
169183
}
170184

171185
@end

packages/app-check/ios/RNFBAppCheck/RNFBAppCheckProvider.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,15 @@
1515
*
1616
*/
1717

18+
#if __has_include(<Firebase/Firebase.h>)
1819
#import <Firebase/Firebase.h>
19-
#import <FirebaseAppCheck/FIRAppCheck.h>
20+
#elif __has_include(<FirebaseAppCheck/FirebaseAppCheck.h>)
21+
#import <FirebaseAppCheck/FirebaseAppCheck.h>
22+
#import <FirebaseCore/FirebaseCore.h>
23+
#else
24+
@import FirebaseCore;
25+
@import FirebaseAppCheck;
26+
#endif
2027

2128
@interface RNFBAppCheckProvider : NSObject <FIRAppCheckProvider>
2229

packages/app-distribution/RNFBAppDistribution.podspec

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'json'
2+
require '../app/firebase_spm'
23
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
34
appPackage = JSON.parse(File.read(File.join('..', 'app', 'package.json')))
45

@@ -42,7 +43,7 @@ Pod::Spec.new do |s|
4243
end
4344

4445
# Firebase dependencies
45-
s.dependency 'Firebase/AppDistribution', firebase_sdk_version
46+
firebase_dependency(s, firebase_sdk_version, ['FirebaseAppDistribution-Beta'], 'Firebase/AppDistribution')
4647

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

0 commit comments

Comments
 (0)