From 05eea1938f92731aa23283f26d4f3a22149accc2 Mon Sep 17 00:00:00 2001 From: jsnavarroc Date: Tue, 17 Mar 2026 11:20:20 -0500 Subject: [PATCH 1/2] feat(ios): add SPM dependency resolution support alongside CocoaPods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .github/workflows/tests_e2e_ios.yml | 27 +- .github/workflows/tests_jest.yml | 2 + docs/ios-spm.md | 760 ++++++++++++++++++ docs/sidebar.yaml | 2 + packages/analytics/RNFBAnalytics.podspec | 36 +- .../ios/RNFBAnalytics/RNFBAnalyticsModule.m | 5 + packages/app-check/RNFBAppCheck.podspec | 3 +- .../ios/RNFBAppCheck/RNFBAppCheckModule.m | 128 +-- .../ios/RNFBAppCheck/RNFBAppCheckProvider.h | 9 +- .../RNFBAppDistribution.podspec | 3 +- .../RNFBAppDistributionModule.m | 5 + packages/app/.npmignore | 3 + packages/app/README.md | 58 ++ packages/app/RNFBApp.podspec | 3 +- packages/app/__tests__/README.md | 29 + packages/app/__tests__/firebase_spm_test.rb | 120 +++ packages/app/firebase_spm.rb | 70 ++ packages/app/ios/RNFBApp/RCTConvert+FIRApp.h | 4 + .../app/ios/RNFBApp/RCTConvert+FIROptions.h | 4 + packages/app/ios/RNFBApp/RNFBAppModule.m | 4 + packages/app/ios/RNFBApp/RNFBSharedUtils.h | 4 + packages/app/package.json | 1 + packages/auth/RNFBAuth.podspec | 3 +- packages/auth/ios/RNFBAuth/RNFBAuthModule.h | 6 + packages/auth/ios/RNFBAuth/RNFBAuthModule.m | 6 + packages/crashlytics/RNFBCrashlytics.podspec | 9 +- .../RNFBCrashlyticsInitProvider.h | 5 + .../RNFBCrashlyticsInitProvider.m | 12 + .../RNFBCrashlytics/RNFBCrashlyticsModule.m | 5 + .../RNFBCrashlyticsNativeHelper.m | 5 + packages/crashlytics/ios_config.sh | 13 +- packages/database/RNFBDatabase.podspec | 3 +- .../ios/RNFBDatabase/RNFBDatabaseCommon.h | 5 + .../ios/RNFBDatabase/RNFBDatabaseModule.m | 5 + .../RNFBDatabaseOnDisconnectModule.m | 5 + .../ios/RNFBDatabase/RNFBDatabaseQuery.h | 5 + .../RNFBDatabase/RNFBDatabaseQueryModule.h | 5 + .../RNFBDatabaseReferenceModule.m | 5 + .../RNFBDatabaseTransactionModule.m | 5 + packages/firestore/RNFBFirestore.podspec | 3 +- .../RNFBFirestore/RCTConvert+FIRLoggerLevel.h | 5 + .../RNFBFirestoreCollectionModule.h | 5 + .../ios/RNFBFirestore/RNFBFirestoreCommon.h | 5 + .../RNFBFirestoreDocumentModule.h | 5 + .../ios/RNFBFirestore/RNFBFirestoreModule.h | 5 + .../ios/RNFBFirestore/RNFBFirestoreQuery.h | 5 + .../RNFBFirestore/RNFBFirestoreSerialize.h | 5 + .../RNFBFirestore/RNFBFirestoreSerialize.m | 4 + .../RNFBFirestoreTransactionModule.h | 5 + packages/functions/RNFBFunctions.podspec | 3 +- .../RNFBFunctionsCallHandler.swift | 26 +- .../ios/RNFBFunctions/RNFBFunctionsModule.mm | 77 +- .../RNFBInAppMessaging.podspec | 3 +- .../ios/RNFBFiam/RNFBFiamModule.m | 5 + .../installations/RNFBInstallations.podspec | 3 +- .../RNFBInstallationsModule.m | 5 + packages/messaging/RNFBMessaging.podspec | 9 +- .../RNFBMessaging/RNFBMessaging+AppDelegate.m | 5 + .../RNFBMessaging+FIRMessagingDelegate.h | 5 + .../RNFBMessaging+NSNotificationCenter.m | 5 + .../ios/RNFBMessaging/RNFBMessagingModule.m | 5 + .../RNFBMessaging/RNFBMessagingSerializer.h | 5 + packages/ml/RNFBML.podspec | 4 + packages/perf/RNFBPerf.podspec | 3 +- packages/perf/ios/RNFBPerf/RNFBPerfModule.m | 5 + .../remote-config/RNFBRemoteConfig.podspec | 3 +- .../ios/RNFBConfig/RNFBConfigModule.m | 5 + packages/storage/RNFBStorage.podspec | 3 +- .../ios/RNFBStorage/RNFBStorageCommon.m | 5 + .../ios/RNFBStorage/RNFBStorageModule.m | 5 + tests/ios/Podfile | 13 +- tests/local-tests/index.js | 2 + tests/local-tests/spm-verification.jsx | 131 +++ 73 files changed, 1628 insertions(+), 136 deletions(-) create mode 100644 docs/ios-spm.md create mode 100644 packages/app/__tests__/README.md create mode 100644 packages/app/__tests__/firebase_spm_test.rb create mode 100644 packages/app/firebase_spm.rb create mode 100644 tests/local-tests/spm-verification.jsx diff --git a/.github/workflows/tests_e2e_ios.yml b/.github/workflows/tests_e2e_ios.yml index ec08799e97..7d78a895f7 100644 --- a/.github/workflows/tests_e2e_ios.yml +++ b/.github/workflows/tests_e2e_ios.yml @@ -75,9 +75,13 @@ jobs: // we want to test debug and release - they generate different code let buildmode = ['debug', 'release']; + // Test both SPM and CocoaPods dependency resolution modes + let depResolution = ['spm', 'cocoapods']; + return { "iteration": iterationArray, - "buildmode": buildmode + "buildmode": buildmode, + "dep-resolution": depResolution } - name: Debug Output run: echo "${{ steps.build-matrix.outputs.result }}" @@ -85,7 +89,7 @@ jobs: # This uses the matrix generated from the matrix-prep stage # it will run unit tests on whatever OS combinations are desired ios: - name: iOS (${{ matrix.buildmode }}, ${{ matrix.iteration }}) + name: iOS (${{ matrix.buildmode }}, ${{ matrix.dep-resolution }}, ${{ matrix.iteration }}) runs-on: macos-15 needs: matrix_prep # TODO matrix across APIs, at least 11 and 15 (lowest to highest) @@ -182,7 +186,7 @@ jobs: - uses: hendrikmuhs/ccache-action@v1 name: Xcode Compile Cache with: - key: ${{ runner.os }}-${{ matrix.buildmode }}-ios-v3 # makes a unique key w/related restore key internally + key: ${{ runner.os }}-${{ matrix.buildmode }}-${{ matrix.dep-resolution }}-ios-v3 # makes a unique key w/related restore key internally save: "${{ github.ref == 'refs/heads/main' }}" create-symlink: true max-size: 1500M @@ -214,8 +218,21 @@ jobs: continue-on-error: true with: path: tests/ios/Pods - key: ${{ runner.os }}-ios-pods-v3-${{ hashFiles('tests/ios/Podfile.lock') }} - restore-keys: ${{ runner.os }}-ios-pods-v3 + key: ${{ runner.os }}-ios-pods-v3-${{ matrix.dep-resolution }}-${{ hashFiles('tests/ios/Podfile.lock') }} + restore-keys: ${{ runner.os }}-ios-pods-v3-${{ matrix.dep-resolution }} + + - name: Configure Dependency Resolution Mode + run: | + if [[ "${{ matrix.dep-resolution }}" == "cocoapods" ]]; then + echo "Configuring CocoaPods-only mode (disabling SPM)" + cd tests/ios + sed -i '' "s/^linkage = 'dynamic'/linkage = 'static'/" Podfile + printf '%s\n' '$RNFirebaseDisableSPM = true' | cat - Podfile > Podfile.tmp && mv Podfile.tmp Podfile + sed -i '' "/SWIFT_ENABLE_EXPLICIT_MODULES/d" Podfile + echo "Podfile configured for CocoaPods-only mode" + else + echo "Using default SPM mode (dynamic linkage)" + fi - name: Pod Install uses: nick-fields/retry@v3 diff --git a/.github/workflows/tests_jest.yml b/.github/workflows/tests_jest.yml index caff239d3b..8577b0d368 100644 --- a/.github/workflows/tests_jest.yml +++ b/.github/workflows/tests_jest.yml @@ -52,6 +52,8 @@ jobs: retry_wait_seconds: 60 max_attempts: 3 command: yarn + - name: Test Firebase SPM Helper + run: ruby packages/app/__tests__/firebase_spm_test.rb - name: Jest run: yarn tests:jest-coverage - uses: codecov/codecov-action@v5 diff --git a/docs/ios-spm.md b/docs/ios-spm.md new file mode 100644 index 0000000000..7c75d5fe10 --- /dev/null +++ b/docs/ios-spm.md @@ -0,0 +1,760 @@ +# iOS SPM (Swift Package Manager) Support for Firebase Dependencies + +> **Firebase SDK:** 12.10.0 +> **Minimum React Native for SPM:** 0.75+ + +--- + +## Table of Contents + +1. [Executive Summary](#1-executive-summary) +2. [Architecture](#2-architecture) +3. [Changes Made](#3-changes-made) +4. [Function Reference](#4-function-reference) +5. [Integration Guide — Legacy Projects](#5-integration-guide--legacy-projects) +6. [Integration Guide — Upgrading to SPM](#6-integration-guide--upgrading-to-spm) +7. [Glossary](#7-glossary) + +--- + +## 1. Executive Summary + +### What problem does this solve? + +When Apple released **Xcode 26** (2026), it introduced a significant change: the Swift compiler now uses **"explicit modules"** by default. This means the compiler needs to know exactly where every module (every library) is located before compiling. + +The problem is that **Firebase iOS SDK**, when installed via **CocoaPods** (the traditional iOS dependency manager), has internal modules (`FirebaseCoreInternal`, `FirebaseSharedSwift`) that **are not exposed as public products**. In Xcode 16, this was not a problem because the compiler found them automatically. In Xcode 26, the compiler no longer searches for them on its own and throws compilation errors. + +**The solution:** Use **Swift Package Manager (SPM)** as the primary method for resolving Firebase dependencies. SPM is Apple's native package manager and correctly handles internal module visibility. As an alternative, CocoaPods is maintained for projects that need it, with a workaround (`SWIFT_ENABLE_EXPLICIT_MODULES=NO`). + +### What was implemented + +A **dual dependency resolution system** that allows choosing between SPM and CocoaPods for Firebase, transparently, without changing app code. The system: + +1. **Automatically detects** if SPM is available (React Native >= 0.75) +2. **Uses SPM by default** when available +3. **Falls back to CocoaPods** when SPM is not available or explicitly disabled +4. **Requires no changes** to JavaScript/TypeScript app code +5. **Requires no changes** to native (Objective-C/Swift) app code + +### Critical points before integrating + +| Point | Detail | +|-------|--------| +| **Linkage** | SPM requires **dynamic linkage**. CocoaPods requires **static linkage**. They cannot be mixed. | +| **Xcode 26** | If using CocoaPods with Xcode 26, you MUST add `SWIFT_ENABLE_EXPLICIT_MODULES = 'NO'` in your Podfile post_install. | +| **React Native < 0.75** | Only works with CocoaPods (SPM is not available in earlier versions). | +| **Duplicate symbols** | If using SPM with `static linkage`, each pod embeds Firebase SPM products → linker error from duplicate symbols. That's why SPM = dynamic. | +| **FirebaseCoreExtension** | Some packages (Messaging, Crashlytics) need `FirebaseCoreExtension` as an explicit dependency in CocoaPods, but SPM resolves it automatically as a transitive dependency. | + +--- + +## 2. Architecture + +### 2.1 Decision Flow Diagram + +``` + ┌─────────────────────────┐ + │ pod install / build │ + └────────────┬─────────────┘ + │ + ┌────────────▼─────────────┐ + │ Podspec loads │ + │ firebase_spm.rb │ + └────────────┬─────────────┘ + │ + ┌────────────▼─────────────┐ + │ Is spm_dependency() │ + │ defined? (RN >= 0.75) │ + └──────┬──────────┬─────────┘ + │ │ + YES NO + │ │ + ┌────────────▼──┐ ┌──▼──────────────────┐ + │ Is $RNFirebase│ │ Use CocoaPods │ + │ DisableSPM │ │ spec.dependency() │ + │ set? │ └─────────────────────┘ + └───┬───────┬───┘ + │ │ + YES NO + │ │ + ┌────────────▼──┐ ┌─▼──────────────────┐ + │ Use CocoaPods │ │ Use SPM │ + │ (forced) │ │ spm_dependency() │ + └───────────────┘ └────────────────────┘ +``` + +### 2.2 System Components + +The system has **5 components** that interact with each other: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ CENTRAL COMPONENT │ +│ packages/app/firebase_spm.rb │ +│ → Defines the firebase_dependency() function │ +│ → Reads the SPM URL from package.json │ +│ → Automatically decides: SPM or CocoaPods │ +└────────────────────────────┬────────────────────────────────────┘ + │ required by + ┌──────────────────┼────────────────┐ + │ │ │ + ┌───────▼──────┐ ┌───────▼──────┐ ┌───────▼──────┐ + │ 16 Podspecs │ │ package.json │ │ 43 native │ + │ (*.podspec) │ │ sdkVersions │ │ iOS files │ + │ │ │ │ │ (.h, .m) │ + │ Each one │ │ Defines: │ │ │ + │ calls │ │ - Firebase │ │ Use #if │ + │ firebase_ │ │ version │ │ __has_include│ + │ dependency() │ │ - SPM URL │ │ for dual │ + └──────────────┘ └──────────────┘ │ imports │ + │ └──────────────┘ + │ + ┌───────▼──────────────────────────────────────────┐ + │ CI/CD: tests_e2e_ios.yml │ + │ → Tests BOTH modes on every PR │ + │ → Matrix: spm × cocoapods × debug × release │ + └──────────────────────────────────────────────────┘ +``` + +### 2.3 Design Decisions and Rationale + +| Decision | Rationale | +|----------|-----------| +| **SPM as default** | Apple promotes SPM as the standard. Xcode 26 works best with SPM. The iOS community is migrating to SPM. | +| **Keep CocoaPods as fallback** | Many legacy projects depend on CocoaPods. React Native < 0.75 does not support SPM. Some setups (static frameworks) need CocoaPods. | +| **Single helper function** | Instead of modifying each podspec individually, the logic is centralized in `firebase_dependency()`. If the logic changes, it changes in one place. | +| **SPM URL in package.json** | Single source of truth: the Firebase version and SPM URL are in a single file. Prevents desynchronization between packages. | +| **`$RNFirebaseDisableSPM` flag** | Escape hatch: if something fails with SPM, users can revert to CocoaPods with a single line in the Podfile. | +| **Dynamic linkage for SPM** | Prevents each pod from embedding a copy of Firebase SPM products (which causes "duplicate symbols" in static). | +| **`SWIFT_ENABLE_EXPLICIT_MODULES=NO` for CocoaPods** | Allows the Swift compiler to use implicit module discovery (like Xcode 16), avoiding errors with Firebase's internal modules. | +| **`#if __has_include` in native code** | Allows the same .m/.h file to compile with both SPM (framework headers) and CocoaPods (@import). No changes needed in app code. | + +--- + +## 3. Changes Made + +### 3.1 `packages/app/firebase_spm.rb` — The Core Logic + +- **What it is:** A Ruby file defining a helper function +- **Where:** `packages/app/firebase_spm.rb` +- **Why it exists:** Because each react-native-firebase package (auth, analytics, messaging, etc.) has a `.podspec` file that declares its iOS dependencies. Previously, each called `s.dependency 'Firebase/Auth', version` directly. Now, they all call `firebase_dependency()` which decides whether to use SPM or CocoaPods. +- **Purpose:** Centralize SPM vs CocoaPods decision logic in a single place + +```ruby +# STEP 1: Read the Firebase SPM repository URL from package.json +# This runs ONCE when the file is loaded +$firebase_spm_url ||= begin + app_package_path = File.join(__dir__, 'package.json') + app_package = JSON.parse(File.read(app_package_path)) + app_package['sdkVersions']['ios']['firebaseSpmUrl'] + # Result: "https://github.com/firebase/firebase-ios-sdk.git" +end + +# STEP 2: Function called by each podspec +def firebase_dependency(spec, version, spm_products, pods) + # Condition 1: Does the spm_dependency function exist? + # → YES if React Native >= 0.75 (they added it) + # → NO if React Native < 0.75 + # + # Condition 2: Did the user NOT define $RNFirebaseDisableSPM? + # → If the user added $RNFirebaseDisableSPM = true in their Podfile, + # this variable EXISTS and the condition is false + + if defined?(spm_dependency) && !defined?($RNFirebaseDisableSPM) + # SPM PATH: Register dependency via Swift Package Manager + spm_dependency(spec, + url: $firebase_spm_url, + requirement: { kind: 'upToNextMajorVersion', minimumVersion: version }, + products: spm_products + ) + else + # COCOAPODS PATH: Register dependency via traditional CocoaPods + pods = [pods] unless pods.is_a?(Array) # Normalize to array + pods.each do |pod| + spec.dependency pod, version + end + end +end +``` + +### 3.2 `packages/app/__tests__/firebase_spm_test.rb` — Unit Tests + +- **What it is:** Ruby test file using the Minitest framework +- **Where:** `packages/app/__tests__/firebase_spm_test.rb` +- **Why it exists:** To verify SPM vs CocoaPods decision logic works correctly in CI without needing a real iOS project +- **Purpose:** Detect regressions if someone modifies `firebase_spm.rb` + +**The 5 tests:** + +| Test | What it verifies | +|------|-----------------| +| `test_cocoapods_single_pod` | When SPM is NOT available, CocoaPods is used with a single pod | +| `test_cocoapods_multiple_pods` | When SPM is NOT available, multiple pods are registered | +| `test_spm_single_product` | When SPM IS available, `spm_dependency` is called with correct parameters | +| `test_spm_multiple_products_ignores_cocoapods_extras` | SPM only uses SPM products, not the extra CocoaPods pods | +| `test_reads_spm_url_from_package_json` | The URL is correctly read from package.json | + +### 3.3 `packages/app/package.json` — Source of Truth + +A `firebaseSpmUrl` field was added inside `sdkVersions.ios`: + +```json +{ + "sdkVersions": { + "ios": { + "firebase": "12.10.0", + "firebaseSpmUrl": "https://github.com/firebase/firebase-ios-sdk.git", + "iosTarget": "15.0", + "macosTarget": "10.15", + "tvosTarget": "15.0" + } + } +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `firebase` | String | Firebase iOS SDK version. Used by all podspecs. | +| `firebaseSpmUrl` | String | Git repository URL for Firebase SPM. | +| `iosTarget` | String | Minimum supported iOS version. | +| `macosTarget` | String | Minimum supported macOS version. | +| `tvosTarget` | String | Minimum supported tvOS version. | + +### 3.4 The 16 `.podspec` Files — Helper Consumers + +Every react-native-firebase package has a `.podspec` file. **All were modified** to use `firebase_dependency()` instead of direct `s.dependency`. + +**Before (original):** +```ruby +# In RNFBAuth.podspec +s.dependency 'Firebase/Auth', firebase_sdk_version +``` + +**After (with SPM support):** +```ruby +# In RNFBAuth.podspec +require '../app/firebase_spm' # Load the helper +firebase_dependency(s, firebase_sdk_version, ['FirebaseAuth'], 'Firebase/Auth') +``` + +**Complete table of all 16 packages:** + +| Package | Podspec | SPM Products | CocoaPods Pods | Notes | +|---------|---------|--------------|----------------|-------| +| **app** | `RNFBApp.podspec` | `['FirebaseCore']` | `'Firebase/CoreOnly'` | Base package, required by all | +| **auth** | `RNFBAuth.podspec` | `['FirebaseAuth']` | `'Firebase/Auth'` | Authentication | +| **analytics** | `RNFBAnalytics.podspec` | `['FirebaseAnalytics']` | `'FirebaseAnalytics/Core'` | Has extra logic for IdentitySupport | +| **messaging** | `RNFBMessaging.podspec` | `['FirebaseMessaging']` | `['Firebase/Messaging', 'FirebaseCoreExtension']` | Needs 2 pods in CocoaPods | +| **crashlytics** | `RNFBCrashlytics.podspec` | `['FirebaseCrashlytics']` | `['Firebase/Crashlytics', 'FirebaseCoreExtension']` | Needs 2 pods in CocoaPods | +| **firestore** | `RNFBFirestore.podspec` | `['FirebaseFirestore']` | `'Firebase/Firestore'` | NoSQL database | +| **database** | `RNFBDatabase.podspec` | `['FirebaseDatabase']` | `'Firebase/Database'` | Realtime Database | +| **storage** | `RNFBStorage.podspec` | `['FirebaseStorage']` | `'Firebase/Storage'` | File storage | +| **functions** | `RNFBFunctions.podspec` | `['FirebaseFunctions']` | `'Firebase/Functions'` | Cloud Functions | +| **perf** | `RNFBPerf.podspec` | `['FirebasePerformance']` | `'Firebase/Performance'` | Performance Monitoring | +| **app-check** | `RNFBAppCheck.podspec` | `['FirebaseAppCheck']` | `'Firebase/AppCheck'` | App integrity verification | +| **installations** | `RNFBInstallations.podspec` | `['FirebaseInstallations']` | `'Firebase/Installations'` | Installation IDs | +| **remote-config** | `RNFBRemoteConfig.podspec` | `['FirebaseRemoteConfig']` | `'Firebase/RemoteConfig'` | Remote configuration | +| **in-app-messaging** | `RNFBInAppMessaging.podspec` | `['FirebaseInAppMessaging-Beta']` | `'Firebase/InAppMessaging'` | In-app messages | +| **app-distribution** | `RNFBAppDistribution.podspec` | `['FirebaseAppDistribution-Beta']` | `'Firebase/AppDistribution'` | App distribution | +| **ml** | `RNFBML.podspec` | *(disabled)* | *(disabled)* | Machine Learning (commented out) | + +**Why do Messaging and Crashlytics need 2 pods in CocoaPods but only 1 SPM product?** + +Because `FirebaseCoreExtension` is a **transitive** dependency in SPM — when you install `FirebaseMessaging` via SPM, SPM automatically includes `FirebaseCoreExtension`. But in CocoaPods, each dependency must be declared explicitly. + +### 3.5 The 43 Native iOS Files — Dual Imports + +- **What they are:** `.h` (header) and `.m`/`.mm` (implementation) files in Objective-C +- **Where:** Inside `packages/*/ios/RNFB*/` +- **Why they were modified:** Because SPM and CocoaPods expose Firebase headers differently +- **Purpose:** Allow the same code to compile with both SPM and CocoaPods + +**Dual import pattern:** + +```objc +// BEFORE (CocoaPods only): +#import // Umbrella header that includes everything + +// AFTER (SPM + CocoaPods): +#if __has_include() + // Path 1: CocoaPods — the umbrella header exists + #import +#elif __has_include() + // Path 2: SPM — each module has its own header + #import + #import +#else + // Path 3: @import (Clang modules) — final fallback + @import FirebaseCore; + @import FirebaseAuth; +#endif +``` + +**Pattern explanation:** + +| Directive | What it does | When it's used | +|-----------|-------------|----------------| +| `#if __has_include()` | Asks the compiler: "does this header exist in the project?" | At compile time. If CocoaPods installed Firebase, this header exists. | +| `#import ` | Imports the Firebase umbrella header (includes EVERYTHING) | Only with CocoaPods, because CocoaPods creates this header that bundles everything. | +| `#elif __has_include()` | Asks: "does the individual module header exist?" | At compile time. If SPM installed FirebaseAuth, this header exists. | +| `#import ` | Imports the module-specific header | With SPM, because each SPM product has its own namespace. | +| `@import FirebaseAuth;` | Clang module import (Objective-C modules) | Fallback: works in both modes but requires modules to be enabled. | + +**Files modified by package:** + +| Package | Files | Imported Headers | +|---------|-------|-----------------| +| auth | `RNFBAuthModule.h`, `RNFBAuthModule.m` | `FirebaseCore`, `FirebaseAuth` | +| analytics | `RNFBAnalyticsModule.m` | `FirebaseCore`, `FirebaseAnalytics` | +| messaging | `RNFBMessagingModule.m`, `RNFBMessagingSerializer.m` | `FirebaseCore`, `FirebaseMessaging` | +| crashlytics | `RNFBCrashlyticsModule.m`, `RNFBCrashlyticsInitProvider.h`, `RNFBCrashlyticsInitProvider.m`, `RNFBCrashlyticsNativeHelper.m` | `FirebaseCore`, `FirebaseCrashlytics`, `FirebaseCoreExtension` | +| firestore | `RNFBFirestoreCommon.h`, `RNFBFirestoreCollectionModule.h`, `RNFBFirestoreSerialize.h`, `RNFBFirestoreSerialize.m`, `RNFBFirestoreQuery.h` | `FirebaseCore`, `FirebaseFirestore` | +| database | `RNFBDatabaseCommon.h`, `RNFBDatabaseQuery.h`, `RNFBDatabaseQueryModule.h`, `RNFBDatabaseReferenceModule.m`, `RNFBDatabaseOnDisconnectModule.m` | `FirebaseCore`, `FirebaseDatabaseInternal` | +| storage | `RNFBStorageModule.m`, `RNFBStorageCommon.h` | `FirebaseCore`, `FirebaseStorage` | +| functions | `RNFBFunctionsModule.mm` | `FirebaseCore`, `FirebaseFunctions` | +| perf | `RNFBPerfModule.m` | `FirebaseCore`, `FirebasePerformance` | +| app-check | `RNFBAppCheckProvider.h`, `RNFBAppCheckModule.m` | `FirebaseCore`, `FirebaseAppCheck` | +| installations | `RNFBInstallationsModule.m` | `FirebaseCore`, `FirebaseInstallations` | +| remote-config | `RNFBRemoteConfigModule.m` | `FirebaseCore`, `FirebaseRemoteConfig` | +| in-app-messaging | `RNFBInAppMessagingModule.m` | `FirebaseCore`, `FirebaseInAppMessaging` | +| app-distribution | `RNFBAppDistributionModule.m` | `FirebaseCore`, `FirebaseAppDistribution` | +| app | `RNFBUtilsModule.m`, `RNFBJSON.m`, `RNFBMeta.m`, `RNFBPreferences.m`, `RNFBSharedUtils.m`, `RNFBRCTAppDelegate.m` | `FirebaseCore` | + +### 3.6 `.github/workflows/tests_e2e_ios.yml` — CI with Dual Matrix + +The CI E2E workflow was extended to test **both modes** (SPM and CocoaPods) on every Pull Request. + +**Key change 1 — Expanded matrix:** + +```yaml +# BEFORE: only tested debug and release +let buildmode = ['debug', 'release']; + +# AFTER: also tests SPM and CocoaPods +let buildmode = ['debug', 'release']; +let depResolution = ['spm', 'cocoapods']; // NEW +``` + +This generates **4 E2E job combinations**: +- `iOS (debug, spm, 0)` +- `iOS (debug, cocoapods, 0)` +- `iOS (release, spm, 0)` +- `iOS (release, cocoapods, 0)` + +**Key change 2 — "Configure Dependency Resolution Mode" step:** + +```yaml +- name: Configure Dependency Resolution Mode + run: | + if [[ "${{ matrix.dep-resolution }}" == "cocoapods" ]]; then + echo "Configuring CocoaPods-only mode (disabling SPM)" + cd tests/ios + + # 1. Switch linkage from dynamic to static + sed -i '' "s/^linkage = 'dynamic'/linkage = 'static'/" Podfile + + # 2. Inject $RNFirebaseDisableSPM = true at the top of the Podfile + printf '%s\n' '$RNFirebaseDisableSPM = true' | cat - Podfile > Podfile.tmp && mv Podfile.tmp Podfile + + # 3. Remove SWIFT_ENABLE_EXPLICIT_MODULES (not needed without SPM in CI) + sed -i '' "/SWIFT_ENABLE_EXPLICIT_MODULES/d" Podfile + + echo "Podfile configured for CocoaPods-only mode" + else + echo "Using default SPM mode (dynamic linkage)" + fi +``` + +### 3.7 `tests/ios/Podfile` — Test Podfile with Xcode 26 Workaround + +**Key lines:** + +```ruby +# Dynamic linkage for SPM (CI switches to 'static' for CocoaPods mode) +linkage = 'dynamic' + +# Xcode 26 workaround +installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['SWIFT_ENABLE_EXPLICIT_MODULES'] = 'NO' + end +end +``` + +**What is `SWIFT_ENABLE_EXPLICIT_MODULES`?** + +An Xcode build setting that controls how the Swift compiler discovers modules: +- `YES` (default in Xcode 26): The compiler ONLY finds modules that are explicitly declared. If a module is not listed as "public", it won't be found. +- `NO` (default in Xcode 16): The compiler searches for modules automatically in all search paths. More permissive but less strict. + +Firebase has internal modules that are not public. With `YES`, Xcode 26 can't find them → compilation error. With `NO`, it works as before. + +--- + +## 4. Function Reference + +### 4.1 `firebase_dependency(spec, version, spm_products, pods)` + +**Purpose:** Register a Firebase dependency in a podspec, automatically choosing between SPM and CocoaPods. + +**Parameters:** + +| Parameter | Ruby Type | Required | Description | Example | +|-----------|-----------|----------|-------------|---------| +| `spec` | `Pod::Specification` | Yes | The podspec object (the `s` in podspec DSL). Represents the package being configured. | `s` (from podspec context) | +| `version` | `String` | Yes | Firebase iOS SDK version to use. Must match the version in package.json. | `'12.10.0'` | +| `spm_products` | `Array` | Yes | List of Firebase SPM product names. These names are the ones in the firebase-ios-sdk `Package.swift`. | `['FirebaseAuth']` or `['FirebaseCrashlytics']` | +| `pods` | `String` or `Array` | Yes | CocoaPods dependency name(s). Can be a string (1 dependency) or array (multiple). These are the names from Firebase's Podspec. | `'Firebase/Auth'` or `['Firebase/Messaging', 'FirebaseCoreExtension']` | + +**Return value:** `nil` — The function has no return value. Its effect is a side-effect: it registers the dependency in the system (SPM or CocoaPods). + +**Usage example in a podspec:** + +```ruby +# RNFBAuth.podspec +require '../app/firebase_spm' + +Pod::Spec.new do |s| + # ... podspec configuration ... + + firebase_sdk_version = appPackage['sdkVersions']['ios']['firebase'] + + # Register FirebaseAuth as a dependency + # - If SPM: calls spm_dependency(s, url: "...", products: ['FirebaseAuth']) + # - If CocoaPods: calls s.dependency('Firebase/Auth', '12.10.0') + firebase_dependency(s, firebase_sdk_version, ['FirebaseAuth'], 'Firebase/Auth') +end +``` + +**Example with multiple CocoaPods dependencies:** + +```ruby +# RNFBCrashlytics.podspec +firebase_dependency(s, firebase_sdk_version, + ['FirebaseCrashlytics'], # SPM: only needs this + ['Firebase/Crashlytics', 'FirebaseCoreExtension'] # CocoaPods: needs both +) +``` + +### 4.2 Global Variable `$firebase_spm_url` + +**Purpose:** Stores the Firebase iOS SDK git repository URL for SPM. + +| Property | Value | +|----------|-------| +| **Type** | `String` (Ruby global variable) | +| **Default value** | `nil` (assigned when `firebase_spm.rb` is loaded) | +| **Value after loading** | `'https://github.com/firebase/firebase-ios-sdk.git'` | +| **Can be overridden** | Yes. If you define `$firebase_spm_url = 'other-url'` BEFORE loading `firebase_spm.rb`, it will use your URL. | + +**Override use case:** + +```ruby +# In your Podfile, before any pod install: +$firebase_spm_url = 'https://github.com/my-company/firebase-ios-sdk-fork.git' +# Now all RNFB packages will use your Firebase fork +``` + +### 4.3 Global Variable `$RNFirebaseDisableSPM` + +**Purpose:** Flag to force CocoaPods usage and disable SPM. + +| Property | Value | +|----------|-------| +| **Type** | Any (checked with `defined?()`, not by value) | +| **Default value** | Not defined (SPM enabled) | +| **How to activate** | `$RNFirebaseDisableSPM = true` in your Podfile | +| **Effect** | `firebase_dependency()` will always use CocoaPods | + +**IMPORTANT:** The function checks `defined?($RNFirebaseDisableSPM)`, NOT the value. This means even `$RNFirebaseDisableSPM = false` DISABLES SPM, because the variable is "defined". To enable SPM, simply don't define this variable. + +### 4.4 `spm_dependency` Function (provided by React Native) + +**NOT defined in this project.** It is a function that React Native (>= 0.75) injects during the `pod install` process. If it exists, it means the environment supports SPM. + +| Parameter | Type | Description | +|-----------|------|-------------| +| `spec` | `Pod::Specification` | Podspec to add the dependency to | +| `url:` | `String` | Git repository URL of the Swift package | +| `requirement:` | `Hash` | Version constraint. Format: `{ kind: 'upToNextMajorVersion', minimumVersion: '12.10.0' }` | +| `products:` | `Array` | List of SPM products to include | + +--- + +## 5. Integration Guide — Legacy Projects + +> **Legacy project** = A project using React Native with CocoaPods that does NOT have SPM support. + +### 5.1 Prerequisites + +| Requirement | Minimum | Recommended | +|-------------|---------|-------------| +| React Native | 0.73+ | 0.75+ (for SPM) | +| Xcode | 15.0 | 26+ | +| CocoaPods | 1.14+ | 1.16+ | +| iOS target | 15.0+ | 15.1+ | +| Ruby | 2.7+ | 3.0+ | + +### 5.2 Step by step + +#### Step 1: Update `@react-native-firebase` to the version with SPM support + +```bash +# In your React Native project +yarn add @react-native-firebase/app@latest +yarn add @react-native-firebase/auth@latest +# ... repeat for each module you use +``` + +#### Step 2: Decide — SPM or CocoaPods? + +**Use SPM if:** +- React Native >= 0.75 +- Xcode 26+ +- You don't have dependencies requiring static linkage +- You want the Apple-recommended approach + +**Use CocoaPods if:** +- React Native < 0.75 +- You have `use_frameworks! :linkage => :static` in your Podfile +- You have other dependencies incompatible with SPM +- You prefer not to change anything (legacy mode) + +#### Step 3A: Configuration for SPM (recommended) + +```ruby +# ios/Podfile + +# Make sure you have dynamic linkage +linkage = 'dynamic' +use_frameworks! :linkage => linkage.to_sym + +target 'YourApp' do + # ... your pods ... + + post_install do |installer| + # REQUIRED for Xcode 26+ + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['SWIFT_ENABLE_EXPLICIT_MODULES'] = 'NO' + end + end + end +end +``` + +Then: +```bash +cd ios && pod install +``` + +You should see messages like: +``` +[react-native-firebase] RNFBApp: Using SPM for Firebase dependency resolution (products: FirebaseCore) +[react-native-firebase] RNFBAuth: Using SPM for Firebase dependency resolution (products: FirebaseAuth) +``` + +#### Step 3B: Configuration for CocoaPods (legacy) + +```ruby +# ios/Podfile — BEFORE target declarations + +$RNFirebaseDisableSPM = true # Force CocoaPods + +# Static linkage (required for CocoaPods) +linkage = 'static' +use_frameworks! :linkage => linkage.to_sym + +target 'YourApp' do + # ... your pods ... + + post_install do |installer| + # REQUIRED if using Xcode 26+ + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['SWIFT_ENABLE_EXPLICIT_MODULES'] = 'NO' + end + end + end +end +``` + +You should see: +``` +[react-native-firebase] RNFBApp: SPM disabled ($RNFirebaseDisableSPM = true), using CocoaPods for Firebase dependencies +``` + +#### Step 4: Verify it compiles + +```bash +cd ios && xcodebuild -workspace YourApp.xcworkspace -scheme YourApp -sdk iphonesimulator build +``` + +### 5.3 Common conflicts + +| Conflict | Symptom | Solution | +|----------|---------|----------| +| **Duplicate symbols** | `duplicate symbol '_FIRApp' in ...` | You're using SPM with static linkage. Switch to `dynamic` or enable `$RNFirebaseDisableSPM` | +| **Module not found** | `No such module 'FirebaseAuth'` | Missing `SWIFT_ENABLE_EXPLICIT_MODULES = 'NO'` in Xcode 26 | +| **Header not found** | `'Firebase/Firebase.h' file not found` | The current mode (SPM) doesn't generate that umbrella header. Native files already use `#if __has_include` to handle this. | +| **Pod::UI undefined** | `NameError: uninitialized constant Pod` | You're running `firebase_spm.rb` outside CocoaPods (in tests). The `defined?(Pod)` guard already handles this. | +| **Version mismatch** | `unable to satisfy version requirement` | Make sure the version in `@react-native-firebase/app` package.json matches your pods. | + +### 5.4 Post-integration checklist + +- [ ] `pod install` completes without errors +- [ ] Log messages show the correct mode (SPM or CocoaPods) +- [ ] Project compiles in Xcode without errors +- [ ] App launches and `Firebase.configure()` executes +- [ ] Firebase features (auth, analytics, etc.) work +- [ ] Existing tests pass + +--- + +## 6. Integration Guide — Upgrading to SPM + +### 6.1 What is SPM? + +**Swift Package Manager (SPM)** is Apple's native package manager, integrated into Xcode. Unlike CocoaPods (a third-party tool), SPM is built into Xcode and Swift. + +| Aspect | CocoaPods | SPM | +|--------|-----------|-----| +| **Installation** | `gem install cocoapods` | Comes with Xcode | +| **Config file** | `Podfile` | `Package.swift` | +| **Lock file** | `Podfile.lock` | `Package.resolved` | +| **Resolution** | Centralized (trunk server) | Decentralized (git repos) | +| **Type** | External Ruby gem | Native Apple tool | + +### 6.2 Dependencies and minimum versions + +| Dependency | Minimum Version | Reason | +|------------|----------------|--------| +| `react-native` / `react-native-tvos` | 0.75.0 | First version that exposes `spm_dependency()` in the CocoaPods runtime | +| `@react-native-firebase/app` | Version with SPM support (this PR) | Needs `firebase_spm.rb` | +| Xcode | 15.0 (functional), 26+ (recommended) | SPM has been integrated since Xcode 11, but Xcode 26 changes the compiler | +| Firebase iOS SDK | 12.10.0+ | Version tested with this system | +| CocoaPods | 1.14+ | For `spm_dependency()` to work correctly in the pod install context | + +### 6.3 Step-by-step instructions + +#### Step 1: Verify React Native version + +```bash +node -p "require('./package.json').dependencies['react-native']" +# or for tvOS: +node -p "require('./package.json').dependencies['react-native-tvos']" +``` + +If it's < 0.75, you need to upgrade React Native first. SPM is not available in earlier versions. + +#### Step 2: Verify linkage in your Podfile + +Open `ios/Podfile` and look for: +```ruby +use_frameworks! :linkage => :static +``` + +If you have `:static`, you need to change it to `:dynamic` for SPM: +```ruby +use_frameworks! :linkage => :dynamic +``` + +**WARNING:** Switching from static to dynamic may affect other dependencies. Verify that all your dependencies support dynamic linkage. + +#### Step 3: Remove `$RNFirebaseDisableSPM` if present + +If your Podfile has this line, remove it: +```ruby +$RNFirebaseDisableSPM = true # REMOVE THIS LINE +``` + +#### Step 4: Add Xcode 26 workaround (if applicable) + +In your Podfile `post_install`: +```ruby +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['SWIFT_ENABLE_EXPLICIT_MODULES'] = 'NO' + end + end +end +``` + +#### Step 5: Clean and reinstall + +```bash +cd ios +rm -rf Pods +rm Podfile.lock +pod install +``` + +#### Step 6: Verify in pod install output + +Look for these lines: +``` +[react-native-firebase] RNFBApp: Using SPM for Firebase dependency resolution (products: FirebaseCore) +``` + +If you see "Using SPM", SPM mode is active. + +If you see "SPM not available", your React Native version doesn't support SPM. + +### 6.4 Common errors and solutions + +| Error | Cause | Solution | +|-------|-------|----------| +| `duplicate symbol` during linking | SPM + static linkage | Switch to `dynamic` linkage | +| `No such module 'FirebaseCore'` | Xcode 26 with explicit modules | Add `SWIFT_ENABLE_EXPLICIT_MODULES = 'NO'` | +| `spm_dependency is not defined` | RN < 0.75 | Update React Native to >= 0.75 or use CocoaPods with `$RNFirebaseDisableSPM = true` | +| `multiple commands produce Firebase.framework` | Conflict between SPM and CocoaPods for Firebase | Make sure you DON'T have a manual `pod 'Firebase/Core'` in your Podfile if SPM is active | +| `unable to resolve package` | Incorrect SPM URL | Check `firebaseSpmUrl` in `packages/app/package.json` | +| Pod install loop / version conflict | Firebase version mismatch between SPM and CocoaPods | Make sure you use the same version in `package.json` and any manual pods | + +### 6.5 Rollback to CocoaPods + +If SPM doesn't work, you can revert to CocoaPods in 30 seconds: + +```ruby +# Add to the top of your Podfile: +$RNFirebaseDisableSPM = true + +# Change linkage to static: +use_frameworks! :linkage => :static +``` + +```bash +cd ios && rm -rf Pods && pod install +``` + +--- + +## 7. Glossary + +| Term | Definition | +|------|-----------| +| **SPM (Swift Package Manager)** | Apple's tool for managing dependencies in iOS/macOS projects. Comes integrated with Xcode. The modern replacement for CocoaPods. | +| **CocoaPods** | Third-party tool (written in Ruby) for managing iOS dependencies. Was the standard for years. Uses a `Podfile` to declare dependencies. | +| **Podspec (.podspec)** | Configuration file describing a library distributed via CocoaPods. Defines name, version, source files, dependencies, etc. | +| **Podfile** | File in the `ios/` directory root that declares what dependencies your project needs. CocoaPods reads it during `pod install`. | +| **Linkage (static vs dynamic)** | Defines how libraries are linked to the final binary. **Static**: the library code is copied into your app. **Dynamic**: the library is a separate file loaded at runtime. | +| **Framework** | In iOS, a way to package a library with its headers, resources, and metadata. Can be static or dynamic. | +| **Header (.h)** | File declaring the public interface of an Objective-C/C library. Tells the compiler what functions/classes exist. | +| **Implementation (.m, .mm)** | File with the actual code (implementation) in Objective-C (.m) or Objective-C++ (.mm). | +| **`#if __has_include`** | C/Objective-C preprocessor directive that asks: "does this file exist in the search paths?" Returns true/false. Evaluated at compile time. | +| **`@import`** | Modern way to import a module in Objective-C. Equivalent to `#import` but more efficient (uses Clang modules). | +| **Explicit modules** | Xcode 26 feature: the compiler only recognizes modules that are explicitly declared. Internal/transitive modules are not found automatically. | +| **Implicit modules** | Pre-Xcode 26 behavior: the compiler searches for modules automatically in all search paths, including transitive ones. | +| **Transitive dependency** | A dependency you don't declare directly, but is required by a dependency you did declare. Example: if you use `FirebaseMessaging` and it needs `FirebaseCoreExtension`, then `FirebaseCoreExtension` is transitive. | +| **`defined?()` (Ruby)** | Ruby operator that checks if an expression is defined. Returns a description string or `nil`. Does NOT raise an error if undefined. | +| **Ruby global variable (`$var`)** | Variable starting with `$` in Ruby. Accessible from anywhere in the program. Used here for shared configuration between files. | +| **`spm_dependency()`** | Function that React Native (>= 0.75) injects into the CocoaPods context during `pod install`. Allows a podspec to declare an SPM dependency. | +| **YAML block scalar (`|`)** | In YAML files, `|` indicates a multiline text block where line breaks are preserved. Used in GitHub Actions for multiline scripts. | +| **Umbrella header** | A `.h` file that imports all headers of a framework. CocoaPods creates `Firebase/Firebase.h` which includes everything. SPM does not generate this file. | +| **Xcode build setting** | Configuration that controls how Xcode compiles your project. Defined in the `.xcodeproj` file or via CocoaPods `post_install`. Example: `SWIFT_ENABLE_EXPLICIT_MODULES`. | +| **CI/CD** | Continuous Integration / Continuous Deployment. Automated system that compiles, tests, and deploys code on every change. GitHub Actions is used here. | +| **Matrix (CI)** | GitHub Actions strategy to run the same job with different parameter combinations. Example: `buildmode: [debug, release]` × `dep-resolution: [spm, cocoapods]` = 4 runs. | +| **Interop layer** | Compatibility layer that allows old code to work with new APIs. React Native 0.81 with Old Architecture uses interop so Objective-C bridges work with the new system. | +| **react-native-firebase** | Open-source library providing Firebase modules for React Native. Each Firebase service (Auth, Analytics, etc.) is a separate package. Original repo: `invertase/react-native-firebase`. | diff --git a/docs/sidebar.yaml b/docs/sidebar.yaml index c4c80e96ce..77983213eb 100644 --- a/docs/sidebar.yaml +++ b/docs/sidebar.yaml @@ -9,6 +9,8 @@ - '/migrating-to-v24' - - TypeScript - '/typescript' +- - iOS SPM Support + - '/ios-spm' - - Platforms - '/platforms' - - Release Notes diff --git a/packages/analytics/RNFBAnalytics.podspec b/packages/analytics/RNFBAnalytics.podspec index 9a03bd0a3f..2ac6d595da 100644 --- a/packages/analytics/RNFBAnalytics.podspec +++ b/packages/analytics/RNFBAnalytics.podspec @@ -1,4 +1,5 @@ require 'json' +require '../app/firebase_spm' package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) appPackage = JSON.parse(File.read(File.join('..', 'app', 'package.json'))) @@ -45,24 +46,31 @@ Pod::Spec.new do |s| end # Firebase dependencies - s.dependency 'FirebaseAnalytics/Core', firebase_sdk_version - if defined?($RNFirebaseAnalyticsWithoutAdIdSupport) && ($RNFirebaseAnalyticsWithoutAdIdSupport == true) - Pod::UI.puts "#{s.name}: Not installing FirebaseAnalytics/IdentitySupport Pod, no IDFA will be collected." - else - if !defined?($RNFirebaseAnalyticsWithoutAdIdSupport) - Pod::UI.puts "#{s.name}: Using FirebaseAnalytics/IdentitySupport with Ad Ids. May require App Tracking Transparency. Not allowed for Kids apps." - Pod::UI.puts "#{s.name}: You may set variable `$RNFirebaseAnalyticsWithoutAdIdSupport=true` in Podfile to use analytics without ad ids." - end - s.dependency 'FirebaseAnalytics/IdentitySupport', firebase_sdk_version + # Analytics has conditional dependencies that vary between SPM and CocoaPods. + # SPM: FirebaseAnalytics includes ad ID support by default. + # CocoaPods: IdentitySupport is a separate subspec controlled by $RNFirebaseAnalyticsWithoutAdIdSupport. + firebase_dependency(s, firebase_sdk_version, ['FirebaseAnalytics'], 'FirebaseAnalytics/Core') - # Special pod for on-device conversion - if defined?($RNFirebaseAnalyticsEnableAdSupport) && ($RNFirebaseAnalyticsEnableAdSupport == true) - Pod::UI.puts "#{s.name}: Adding Apple AdSupport.framework dependency for optional analytics features" - s.frameworks = 'AdSupport' + unless defined?(spm_dependency) + # 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." + else + if !defined?($RNFirebaseAnalyticsWithoutAdIdSupport) + Pod::UI.puts "#{s.name}: Using FirebaseAnalytics/IdentitySupport with Ad Ids. May require App Tracking Transparency. Not allowed for Kids apps." + Pod::UI.puts "#{s.name}: You may set variable `$RNFirebaseAnalyticsWithoutAdIdSupport=true` in Podfile to use analytics without ad ids." + end + s.dependency 'FirebaseAnalytics/IdentitySupport', firebase_sdk_version end end - # Special pod for on-device conversion + # AdSupport framework (works with both SPM and CocoaPods) + if defined?($RNFirebaseAnalyticsEnableAdSupport) && ($RNFirebaseAnalyticsEnableAdSupport == true) + Pod::UI.puts "#{s.name}: Adding Apple AdSupport.framework dependency for optional analytics features" + s.frameworks = 'AdSupport' + end + + # GoogleAdsOnDeviceConversion (CocoaPods only, not available in firebase-ios-sdk SPM) if defined?($RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion) && ($RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion == true) Pod::UI.puts "#{s.name}: GoogleAdsOnDeviceConversion pod added" s.dependency 'GoogleAdsOnDeviceConversion' diff --git a/packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m b/packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m index afb9d4f2a2..8b4bd878fa 100644 --- a/packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m +++ b/packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseAnalytics; +#endif #import #if __has_include() diff --git a/packages/app-check/RNFBAppCheck.podspec b/packages/app-check/RNFBAppCheck.podspec index d1d65d0636..acd459ce4d 100644 --- a/packages/app-check/RNFBAppCheck.podspec +++ b/packages/app-check/RNFBAppCheck.podspec @@ -1,4 +1,5 @@ require 'json' +require '../app/firebase_spm' package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) appPackage = JSON.parse(File.read(File.join('..', 'app', 'package.json'))) @@ -44,7 +45,7 @@ Pod::Spec.new do |s| end # Firebase dependencies - s.dependency 'Firebase/AppCheck', firebase_sdk_version + firebase_dependency(s, firebase_sdk_version, ['FirebaseAppCheck'], 'Firebase/AppCheck') if defined?($RNFirebaseAsStaticFramework) Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'" diff --git a/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckModule.m b/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckModule.m index f1121678a0..c1706d6b5c 100644 --- a/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckModule.m +++ b/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckModule.m @@ -15,8 +15,15 @@ * */ +#if __has_include() #import -#import +#elif __has_include() +#import +#import +#else +@import FirebaseCore; +@import FirebaseAppCheck; +#endif #import @@ -53,7 +60,8 @@ + (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" @@ -86,8 +94,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 @@ -105,33 +113,36 @@ + (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 @@ -140,32 +151,35 @@ + (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 diff --git a/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckProvider.h b/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckProvider.h index e1877a1d9d..023f7d59b4 100644 --- a/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckProvider.h +++ b/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckProvider.h @@ -15,8 +15,15 @@ * */ +#if __has_include() #import -#import +#elif __has_include() +#import +#import +#else +@import FirebaseCore; +@import FirebaseAppCheck; +#endif @interface RNFBAppCheckProvider : NSObject diff --git a/packages/app-distribution/RNFBAppDistribution.podspec b/packages/app-distribution/RNFBAppDistribution.podspec index f3e0b6e240..7592751a51 100644 --- a/packages/app-distribution/RNFBAppDistribution.podspec +++ b/packages/app-distribution/RNFBAppDistribution.podspec @@ -1,4 +1,5 @@ require 'json' +require '../app/firebase_spm' package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) appPackage = JSON.parse(File.read(File.join('..', 'app', 'package.json'))) @@ -42,7 +43,7 @@ Pod::Spec.new do |s| end # Firebase dependencies - s.dependency 'Firebase/AppDistribution', firebase_sdk_version + firebase_dependency(s, firebase_sdk_version, ['FirebaseAppDistribution-Beta'], 'Firebase/AppDistribution') if defined?($RNFirebaseAsStaticFramework) Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'" diff --git a/packages/app-distribution/ios/RNFBAppDistribution/RNFBAppDistributionModule.m b/packages/app-distribution/ios/RNFBAppDistribution/RNFBAppDistributionModule.m index a13b881f2b..22942e4ee7 100644 --- a/packages/app-distribution/ios/RNFBAppDistribution/RNFBAppDistributionModule.m +++ b/packages/app-distribution/ios/RNFBAppDistribution/RNFBAppDistributionModule.m @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseAppDistribution; +#endif #import #import "RNFBApp/RNFBSharedUtils.h" diff --git a/packages/app/.npmignore b/packages/app/.npmignore index 00d2a23cb0..757d1f0ef2 100644 --- a/packages/app/.npmignore +++ b/packages/app/.npmignore @@ -68,3 +68,6 @@ android/.settings type-test.ts scripts __tests__ + +# Force include generated version file (needed for linking) +!ios/RNFBApp/RNFBVersion.m diff --git a/packages/app/README.md b/packages/app/README.md index 90525afa05..873f65b6e9 100644 --- a/packages/app/README.md +++ b/packages/app/README.md @@ -36,6 +36,64 @@ yarn add @react-native-firebase/app - [Utils](https://rnfirebase.io/app/utils) +## 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. + +### 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 | + +### Configuration + +**Option A — SPM (default, recommended for Xcode 26+)** + +No changes needed. Just make sure your Podfile uses dynamic linkage: + +```ruby +# Podfile +use_frameworks! :linkage => :dynamic +``` + +> **Xcode 26 note:** If you see build errors about `FirebaseCoreInternal` or `FirebaseSharedSwift` +> module resolution, add this to your Podfile `post_install`: +> ```ruby +> config.build_settings['SWIFT_ENABLE_EXPLICIT_MODULES'] = 'NO' +> ``` +> 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** + +Add this line at the top of your Podfile (before any `target` block): + +```ruby +# Podfile +$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. + +### How to verify + +During `pod install`, you will see messages indicating which resolution mode is active: + +``` +# SPM mode: +[react-native-firebase] RNFBApp: Using SPM for Firebase dependency resolution (products: FirebaseCore) +[react-native-firebase] RNFBAuth: Using SPM for Firebase dependency resolution (products: FirebaseAuth) + +# CocoaPods mode: +[react-native-firebase] RNFBApp: SPM disabled ($RNFirebaseDisableSPM = true), using CocoaPods for Firebase dependencies +``` + ## License - See [LICENSE](/LICENSE) diff --git a/packages/app/RNFBApp.podspec b/packages/app/RNFBApp.podspec index 0f8ca1d42c..9a24fa02f1 100644 --- a/packages/app/RNFBApp.podspec +++ b/packages/app/RNFBApp.podspec @@ -1,5 +1,6 @@ require 'json' require './firebase_json' +require './firebase_spm' package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) firebase_sdk_version = package['sdkVersions']['ios']['firebase'] firebase_ios_target = package['sdkVersions']['ios']['iosTarget'] @@ -61,7 +62,7 @@ Pod::Spec.new do |s| end # Firebase dependencies - s.dependency 'Firebase/CoreOnly', firebase_sdk_version + firebase_dependency(s, firebase_sdk_version, ['FirebaseCore'], 'Firebase/CoreOnly') if defined?($RNFirebaseAsStaticFramework) Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'" diff --git a/packages/app/__tests__/README.md b/packages/app/__tests__/README.md new file mode 100644 index 0000000000..62db126b93 --- /dev/null +++ b/packages/app/__tests__/README.md @@ -0,0 +1,29 @@ +## Firebase SPM dependency tests + +Unit tests for `firebase_spm.rb` — the shared helper that declares Firebase dependencies with SPM support and CocoaPods fallback. + +### How to run + +```bash +ruby __tests__/firebase_spm_test.rb +``` + +### What is `Pod::Specification` and why is it mocked? + +`Pod::Specification` is the core CocoaPods class — it's the `s` object used inside every `.podspec` file to declare things like `s.dependency`, `s.name`, `s.version`, etc. We mock it with a simple class that records `dependency` calls, so we can run the tests without installing CocoaPods. + +### Test flow + +1. A mock `Pod::Specification` captures `dependency` calls. +2. `firebase_dependency()` is called with known inputs (version, SPM products, CocoaPods pods). +3. Assertions verify which path executed and with what arguments. + +### Tests + +| Test | What it verifies | Path | Flow | +|------|-----------------|------|------| +| `test_cocoapods_single_pod` | When SPM is not available, a single Firebase pod (like Auth) is added as a CocoaPods dependency with the correct name and version. | CocoaPods | `spm_dependency` undefined → `spec.dependency('Firebase/Auth', '12.10.0')` called once | +| `test_cocoapods_multiple_pods` | When SPM is not available and a module needs multiple pods (like Crashlytics + CoreExtension), all of them are added as CocoaPods dependencies. | CocoaPods | `spm_dependency` undefined → `spec.dependency` called twice (Crashlytics + CoreExtension) | +| `test_spm_single_product` | When SPM is available, the Firebase dependency is declared via Swift Package Manager instead of CocoaPods, using the correct URL, version, and product name. | SPM | `spm_dependency` defined → called with `['FirebaseAuth']`, `spec.dependency` not called | +| `test_spm_multiple_products_ignores_cocoapods_extras` | When SPM is available, only the SPM product names are used. Extra CocoaPods-only dependencies (like FirebaseCoreExtension) are correctly ignored because SPM resolves them automatically as transitive dependencies. | SPM | `spm_dependency` defined → called with `['FirebaseCrashlytics']` only, ignores CocoaPods extras | +| `test_reads_spm_url_from_package_json` | The Firebase SPM repository URL is read from `package.json` instead of being hardcoded, ensuring a single source of truth for the SDK location. | Config | `$firebase_spm_url` reads from `package.json` → `https://github.com/firebase/firebase-ios-sdk.git` | diff --git a/packages/app/__tests__/firebase_spm_test.rb b/packages/app/__tests__/firebase_spm_test.rb new file mode 100644 index 0000000000..e56a7163e9 --- /dev/null +++ b/packages/app/__tests__/firebase_spm_test.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require 'json' + +# Mock Pod::Specification to capture dependency calls +class MockSpec + attr_reader :dependencies + + def initialize + @dependencies = [] + end + + def dependency(name, version) + @dependencies << { name: name, version: version } + end +end + +class FirebaseSpmTest < Minitest::Test + def setup + # Reset global state before each test + $firebase_spm_url = nil + # Remove spm_dependency if defined from a previous test + if defined?(spm_dependency) + Object.send(:remove_method, :spm_dependency) + end + end + + def load_firebase_spm + # Force re-evaluation of the file + load File.join(__dir__, '..', 'firebase_spm.rb') + end + + # ── CocoaPods path (spm_dependency NOT defined) ── + + def test_cocoapods_single_pod + load_firebase_spm + + spec = MockSpec.new + firebase_dependency(spec, '12.10.0', ['FirebaseAuth'], 'Firebase/Auth') + + assert_equal 1, spec.dependencies.length + assert_equal 'Firebase/Auth', spec.dependencies[0][:name] + assert_equal '12.10.0', spec.dependencies[0][:version] + end + + def test_cocoapods_multiple_pods + load_firebase_spm + + spec = MockSpec.new + firebase_dependency(spec, '12.10.0', + ['FirebaseCrashlytics'], + ['Firebase/Crashlytics', 'FirebaseCoreExtension'] + ) + + assert_equal 2, spec.dependencies.length + assert_equal 'Firebase/Crashlytics', spec.dependencies[0][:name] + assert_equal 'FirebaseCoreExtension', spec.dependencies[1][:name] + spec.dependencies.each do |dep| + assert_equal '12.10.0', dep[:version] + end + end + + # ── SPM path (spm_dependency IS defined) ── + + def test_spm_single_product + # Define spm_dependency mock to capture the call + spm_calls = [] + Object.define_method(:spm_dependency) do |spec, **kwargs| + spm_calls << { spec: spec, **kwargs } + end + + load_firebase_spm + + spec = MockSpec.new + firebase_dependency(spec, '12.10.0', ['FirebaseAuth'], 'Firebase/Auth') + + # CocoaPods dependency should NOT be called + assert_equal 0, spec.dependencies.length + + # SPM dependency should be called with correct params + assert_equal 1, spm_calls.length + call = spm_calls[0] + assert_equal spec, call[:spec] + assert_equal 'https://github.com/firebase/firebase-ios-sdk.git', call[:url] + assert_equal({ kind: 'upToNextMajorVersion', minimumVersion: '12.10.0' }, call[:requirement]) + assert_equal ['FirebaseAuth'], call[:products] + end + + def test_spm_multiple_products_ignores_cocoapods_extras + spm_calls = [] + Object.define_method(:spm_dependency) do |spec, **kwargs| + spm_calls << { spec: spec, **kwargs } + end + + load_firebase_spm + + spec = MockSpec.new + # Crashlytics: SPM only needs FirebaseCrashlytics, CocoaPods needs 2 pods + firebase_dependency(spec, '12.10.0', + ['FirebaseCrashlytics'], + ['Firebase/Crashlytics', 'FirebaseCoreExtension'] + ) + + # CocoaPods not called + assert_equal 0, spec.dependencies.length + + # SPM called with only the SPM products (no FirebaseCoreExtension) + assert_equal 1, spm_calls.length + assert_equal ['FirebaseCrashlytics'], spm_calls[0][:products] + end + + # ── URL from package.json ── + + def test_reads_spm_url_from_package_json + load_firebase_spm + + assert_equal 'https://github.com/firebase/firebase-ios-sdk.git', $firebase_spm_url + end +end diff --git a/packages/app/firebase_spm.rb b/packages/app/firebase_spm.rb new file mode 100644 index 0000000000..51ef891c47 --- /dev/null +++ b/packages/app/firebase_spm.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +# +# Copyright (c) 2016-present Invertase Limited & Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this library except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'json' + +# Read Firebase SPM URL from app package.json (single source of truth) +$firebase_spm_url ||= begin + app_package_path = File.join(__dir__, 'package.json') + app_package = JSON.parse(File.read(app_package_path)) + app_package['sdkVersions']['ios']['firebaseSpmUrl'] +end + +# Helper to declare Firebase dependencies with SPM support and CocoaPods fallback. +# +# When `spm_dependency` is available (React Native >= 0.75), it declares the +# Firebase iOS SDK as a Swift Package dependency. Otherwise, it falls back to +# the traditional CocoaPods `s.dependency` declaration. +# +# Set `$RNFirebaseDisableSPM = true` in your Podfile to force CocoaPods-only +# dependency resolution. This is required when using `use_frameworks! :linkage => :static` +# because static frameworks cause each pod to embed Firebase SPM products, +# resulting in duplicate symbol linker errors. +# +# @param spec [Pod::Specification] The podspec object (the `s` in podspec DSL) +# @param version [String] Firebase SDK version (e.g., '12.10.0') +# @param spm_products [Array] SPM product names (e.g., ['FirebaseAuth']) +# @param pods [Array, String] CocoaPods dependency names with optional version +# Can be a single string like 'Firebase/Auth' or an array like ['Firebase/Messaging', 'FirebaseCoreExtension'] +def firebase_dependency(spec, version, spm_products, pods) + if defined?(spm_dependency) && !defined?($RNFirebaseDisableSPM) + if defined?(Pod) && defined?(Pod::UI) + Pod::UI.puts "[react-native-firebase] #{spec.name}: ".yellow + + "Using SPM for Firebase dependency resolution (products: #{spm_products.join(', ')})" + end + spm_dependency(spec, + url: $firebase_spm_url, + requirement: { kind: 'upToNextMajorVersion', minimumVersion: version }, + products: spm_products + ) + else + if defined?(Pod) && defined?(Pod::UI) + if defined?($RNFirebaseDisableSPM) + Pod::UI.puts "[react-native-firebase] #{spec.name}: ".yellow + + "SPM disabled ($RNFirebaseDisableSPM = true), using CocoaPods for Firebase dependencies" + elsif !defined?(spm_dependency) + Pod::UI.puts "[react-native-firebase] #{spec.name}: ".yellow + + "SPM not available (React Native < 0.75), using CocoaPods for Firebase dependencies" + end + end + pods = [pods] unless pods.is_a?(Array) + pods.each do |pod| + spec.dependency pod, version + end + end +end diff --git a/packages/app/ios/RNFBApp/RCTConvert+FIRApp.h b/packages/app/ios/RNFBApp/RCTConvert+FIRApp.h index 73c0982f24..8c9d722fa7 100644 --- a/packages/app/ios/RNFBApp/RCTConvert+FIRApp.h +++ b/packages/app/ios/RNFBApp/RCTConvert+FIRApp.h @@ -15,7 +15,11 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +#endif #import @interface RCTConvert (FIRApp) diff --git a/packages/app/ios/RNFBApp/RCTConvert+FIROptions.h b/packages/app/ios/RNFBApp/RCTConvert+FIROptions.h index 470ac3573e..d1e5f221b2 100644 --- a/packages/app/ios/RNFBApp/RCTConvert+FIROptions.h +++ b/packages/app/ios/RNFBApp/RCTConvert+FIROptions.h @@ -15,7 +15,11 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +#endif #import @interface RCTConvert (FIROptions) diff --git a/packages/app/ios/RNFBApp/RNFBAppModule.m b/packages/app/ios/RNFBApp/RNFBAppModule.m index f679daad1b..fbc4a8486e 100644 --- a/packages/app/ios/RNFBApp/RNFBAppModule.m +++ b/packages/app/ios/RNFBApp/RNFBAppModule.m @@ -15,7 +15,11 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +#endif #import #import "RNFBAppModule.h" diff --git a/packages/app/ios/RNFBApp/RNFBSharedUtils.h b/packages/app/ios/RNFBApp/RNFBSharedUtils.h index ebfe513e94..c15ac36dc7 100644 --- a/packages/app/ios/RNFBApp/RNFBSharedUtils.h +++ b/packages/app/ios/RNFBApp/RNFBSharedUtils.h @@ -18,7 +18,11 @@ #ifndef RNFBSharedUtils_h #define RNFBSharedUtils_h +#if __has_include() #import +#else +@import FirebaseCore; +#endif #import #ifdef DEBUG diff --git a/packages/app/package.json b/packages/app/package.json index 983ba5998e..c352f78c06 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -106,6 +106,7 @@ "sdkVersions": { "ios": { "firebase": "12.10.0", + "firebaseSpmUrl": "https://github.com/firebase/firebase-ios-sdk.git", "iosTarget": "15.0", "macosTarget": "10.15", "tvosTarget": "15.0" diff --git a/packages/auth/RNFBAuth.podspec b/packages/auth/RNFBAuth.podspec index 8e9b9d5065..fbeed65979 100644 --- a/packages/auth/RNFBAuth.podspec +++ b/packages/auth/RNFBAuth.podspec @@ -1,4 +1,5 @@ require 'json' +require '../app/firebase_spm' package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) appPackage = JSON.parse(File.read(File.join('..', 'app', 'package.json'))) @@ -44,7 +45,7 @@ Pod::Spec.new do |s| end # Firebase dependencies - s.dependency 'Firebase/Auth', firebase_sdk_version + firebase_dependency(s, firebase_sdk_version, ['FirebaseAuth'], 'Firebase/Auth') if defined?($RNFirebaseAsStaticFramework) Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'" diff --git a/packages/auth/ios/RNFBAuth/RNFBAuthModule.h b/packages/auth/ios/RNFBAuth/RNFBAuthModule.h index dd7b0774c1..214058f487 100644 --- a/packages/auth/ios/RNFBAuth/RNFBAuthModule.h +++ b/packages/auth/ios/RNFBAuth/RNFBAuthModule.h @@ -15,7 +15,13 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseAuth; +@import FirebaseAuthInternal; +#endif #import #import diff --git a/packages/auth/ios/RNFBAuth/RNFBAuthModule.m b/packages/auth/ios/RNFBAuth/RNFBAuthModule.m index 40ee9aa2e4..efc14dcd3c 100644 --- a/packages/auth/ios/RNFBAuth/RNFBAuthModule.m +++ b/packages/auth/ios/RNFBAuth/RNFBAuthModule.m @@ -15,7 +15,13 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseAuth; +@import FirebaseAuthInternal; +#endif #import #import "RNFBApp/RCTConvert+FIRApp.h" diff --git a/packages/crashlytics/RNFBCrashlytics.podspec b/packages/crashlytics/RNFBCrashlytics.podspec index d33b3f729f..65579350fe 100644 --- a/packages/crashlytics/RNFBCrashlytics.podspec +++ b/packages/crashlytics/RNFBCrashlytics.podspec @@ -1,4 +1,5 @@ require 'json' +require '../app/firebase_spm' package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) appPackage = JSON.parse(File.read(File.join('..', 'app', 'package.json'))) @@ -46,8 +47,12 @@ Pod::Spec.new do |s| end # Firebase dependencies - s.dependency 'Firebase/Crashlytics', firebase_sdk_version - s.dependency 'FirebaseCoreExtension' + # FirebaseCoreExtension is a transitive dependency of FirebaseCrashlytics in SPM, + # so it only needs to be declared explicitly for CocoaPods. + firebase_dependency(s, firebase_sdk_version, + ['FirebaseCrashlytics'], + ['Firebase/Crashlytics', 'FirebaseCoreExtension'] + ) if defined?($RNFirebaseAsStaticFramework) Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'" diff --git a/packages/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsInitProvider.h b/packages/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsInitProvider.h index 2a546f06d0..9d0ea6281c 100644 --- a/packages/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsInitProvider.h +++ b/packages/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsInitProvider.h @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseCoreExtension; +#endif #import @interface RNFBCrashlyticsInitProvider : NSObject diff --git a/packages/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsInitProvider.m b/packages/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsInitProvider.m index 4e4961d7d4..d9ae42fbdb 100644 --- a/packages/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsInitProvider.m +++ b/packages/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsInitProvider.m @@ -16,13 +16,25 @@ */ #import "RNFBCrashlyticsInitProvider.h" +#if __has_include() #import +#else +@import FirebaseCore; +#endif +#if __has_include() #import #import #import #import #import +#else +@import FirebaseCoreExtension; +#endif +#if __has_include() #import +#else +@import FirebaseCrashlytics; +#endif #import "RNFBJSON.h" #import "RNFBMeta.h" #import "RNFBPreferences.h" diff --git a/packages/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsModule.m b/packages/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsModule.m index ada982b7a1..3ff80ba314 100644 --- a/packages/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsModule.m +++ b/packages/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsModule.m @@ -22,7 +22,12 @@ #import #import +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseCrashlytics; +#endif #import "RNFBApp/RNFBSharedUtils.h" #import "RNFBCrashlyticsInitProvider.h" #import "RNFBCrashlyticsModule.h" diff --git a/packages/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsNativeHelper.m b/packages/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsNativeHelper.m index 8abd8cec52..a09522d325 100644 --- a/packages/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsNativeHelper.m +++ b/packages/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsNativeHelper.m @@ -17,7 +17,12 @@ */ #import "RNFBCrashlyticsNativeHelper.h" +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseCrashlytics; +#endif @implementation RNFBCrashlyticsNativeHelper diff --git a/packages/crashlytics/ios_config.sh b/packages/crashlytics/ios_config.sh index c13498f3b7..ff93dd2c7f 100755 --- a/packages/crashlytics/ios_config.sh +++ b/packages/crashlytics/ios_config.sh @@ -16,10 +16,19 @@ # set -e -if [[ ${PODS_ROOT} ]]; then +if [[ ${PODS_ROOT} && -f "${PODS_ROOT}/FirebaseCrashlytics/run" ]]; then echo "info: Exec FirebaseCrashlytics Run from Pods" "${PODS_ROOT}/FirebaseCrashlytics/run" -else +elif [[ -f "${PROJECT_DIR}/FirebaseCrashlytics.framework/run" ]]; then echo "info: Exec FirebaseCrashlytics Run from framework" "${PROJECT_DIR}/FirebaseCrashlytics.framework/run" +else + # SPM: upload-symbols is in the SourcePackages checkout + SPM_UPLOAD_SYMBOLS=$(find "${BUILD_DIR%Build/*}SourcePackages/checkouts/firebase-ios-sdk/Crashlytics" -name "upload-symbols" -type f 2>/dev/null | head -1) + if [[ -n "${SPM_UPLOAD_SYMBOLS}" ]]; then + echo "info: Exec FirebaseCrashlytics upload-symbols from SPM" + "${SPM_UPLOAD_SYMBOLS}" -gsp "${PROJECT_DIR}/GoogleService-Info.plist" -p ios "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}" + else + echo "warning: FirebaseCrashlytics run script not found, skipping dSYM upload" + fi fi diff --git a/packages/database/RNFBDatabase.podspec b/packages/database/RNFBDatabase.podspec index cc37ff2a6b..0138721664 100644 --- a/packages/database/RNFBDatabase.podspec +++ b/packages/database/RNFBDatabase.podspec @@ -1,4 +1,5 @@ require 'json' +require '../app/firebase_spm' package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) appPackage = JSON.parse(File.read(File.join('..', 'app', 'package.json'))) @@ -45,7 +46,7 @@ Pod::Spec.new do |s| end # Firebase dependencies - s.dependency 'Firebase/Database', firebase_sdk_version + firebase_dependency(s, firebase_sdk_version, ['FirebaseDatabase'], 'Firebase/Database') if defined?($RNFirebaseAsStaticFramework) Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'" diff --git a/packages/database/ios/RNFBDatabase/RNFBDatabaseCommon.h b/packages/database/ios/RNFBDatabase/RNFBDatabaseCommon.h index 39563a12c6..800c0dc635 100644 --- a/packages/database/ios/RNFBDatabase/RNFBDatabaseCommon.h +++ b/packages/database/ios/RNFBDatabase/RNFBDatabaseCommon.h @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseDatabaseInternal; +#endif #import @interface RNFBDatabaseCommon : NSObject diff --git a/packages/database/ios/RNFBDatabase/RNFBDatabaseModule.m b/packages/database/ios/RNFBDatabase/RNFBDatabaseModule.m index b4789169e6..6c2b2c55f4 100644 --- a/packages/database/ios/RNFBDatabase/RNFBDatabaseModule.m +++ b/packages/database/ios/RNFBDatabase/RNFBDatabaseModule.m @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseDatabaseInternal; +#endif #import #import "RNFBDatabaseCommon.h" diff --git a/packages/database/ios/RNFBDatabase/RNFBDatabaseOnDisconnectModule.m b/packages/database/ios/RNFBDatabase/RNFBDatabaseOnDisconnectModule.m index c1454dded9..6ad1974305 100644 --- a/packages/database/ios/RNFBDatabase/RNFBDatabaseOnDisconnectModule.m +++ b/packages/database/ios/RNFBDatabase/RNFBDatabaseOnDisconnectModule.m @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseDatabaseInternal; +#endif #import #import "RNFBDatabaseCommon.h" diff --git a/packages/database/ios/RNFBDatabase/RNFBDatabaseQuery.h b/packages/database/ios/RNFBDatabase/RNFBDatabaseQuery.h index 7fc70cbb29..8a374c46fe 100644 --- a/packages/database/ios/RNFBDatabase/RNFBDatabaseQuery.h +++ b/packages/database/ios/RNFBDatabase/RNFBDatabaseQuery.h @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseDatabaseInternal; +#endif #import @interface RNFBDatabaseQuery : NSObject diff --git a/packages/database/ios/RNFBDatabase/RNFBDatabaseQueryModule.h b/packages/database/ios/RNFBDatabase/RNFBDatabaseQueryModule.h index 4f31472c18..cb7c9c9a12 100644 --- a/packages/database/ios/RNFBDatabase/RNFBDatabaseQueryModule.h +++ b/packages/database/ios/RNFBDatabase/RNFBDatabaseQueryModule.h @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseDatabaseInternal; +#endif #import #import diff --git a/packages/database/ios/RNFBDatabase/RNFBDatabaseReferenceModule.m b/packages/database/ios/RNFBDatabase/RNFBDatabaseReferenceModule.m index 0c7350f88a..b559caca9b 100644 --- a/packages/database/ios/RNFBDatabase/RNFBDatabaseReferenceModule.m +++ b/packages/database/ios/RNFBDatabase/RNFBDatabaseReferenceModule.m @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseDatabaseInternal; +#endif #import #import "RNFBDatabaseCommon.h" diff --git a/packages/database/ios/RNFBDatabase/RNFBDatabaseTransactionModule.m b/packages/database/ios/RNFBDatabase/RNFBDatabaseTransactionModule.m index bd2f1ea614..d271128043 100644 --- a/packages/database/ios/RNFBDatabase/RNFBDatabaseTransactionModule.m +++ b/packages/database/ios/RNFBDatabase/RNFBDatabaseTransactionModule.m @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseDatabaseInternal; +#endif #import #import "RNFBDatabaseCommon.h" diff --git a/packages/firestore/RNFBFirestore.podspec b/packages/firestore/RNFBFirestore.podspec index 25ac1a307b..ce1dcc4628 100644 --- a/packages/firestore/RNFBFirestore.podspec +++ b/packages/firestore/RNFBFirestore.podspec @@ -1,4 +1,5 @@ require 'json' +require '../app/firebase_spm' package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) appPackage = JSON.parse(File.read(File.join('..', 'app', 'package.json'))) @@ -44,7 +45,7 @@ Pod::Spec.new do |s| end # Firebase dependencies - s.dependency 'Firebase/Firestore', firebase_sdk_version + firebase_dependency(s, firebase_sdk_version, ['FirebaseFirestore'], 'Firebase/Firestore') if defined?($RNFirebaseAsStaticFramework) Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'" diff --git a/packages/firestore/ios/RNFBFirestore/RCTConvert+FIRLoggerLevel.h b/packages/firestore/ios/RNFBFirestore/RCTConvert+FIRLoggerLevel.h index 7f5d782fcf..ab89763df2 100644 --- a/packages/firestore/ios/RNFBFirestore/RCTConvert+FIRLoggerLevel.h +++ b/packages/firestore/ios/RNFBFirestore/RCTConvert+FIRLoggerLevel.h @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseFirestore; +#endif #import @interface RCTConvert (FIRLoggerLevel) diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCollectionModule.h b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCollectionModule.h index 5df82d5275..5727d364c3 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCollectionModule.h +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCollectionModule.h @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseFirestore; +#endif #import #import #import diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.h b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.h index b2cd6d2fac..dc0cc71f25 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.h +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.h @@ -16,7 +16,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseFirestore; +#endif #import @interface RNFBFirestoreCommon : NSObject diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreDocumentModule.h b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreDocumentModule.h index 8a314bdce1..fab5c496ec 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreDocumentModule.h +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreDocumentModule.h @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseFirestore; +#endif #import #import #import diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.h b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.h index a989534e02..3c67ea7023 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.h +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.h @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseFirestore; +#endif #import #import #import diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreQuery.h b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreQuery.h index 952190063f..b681519f6e 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreQuery.h +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreQuery.h @@ -16,7 +16,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseFirestore; +#endif #import #import diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.h b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.h index 6c601af7af..923237d1db 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.h +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.h @@ -16,7 +16,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseFirestore; +#endif #import @interface RNFBFirestoreSerialize : NSObject diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.m b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.m index 70dd25e085..b296ce0fad 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.m +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.m @@ -16,7 +16,11 @@ * */ +#if __has_include("FirebaseFirestore/FIRVectorValue.h") #import "FirebaseFirestore/FIRVectorValue.h" +#elif __has_include() +#import +#endif #import "RNFBFirestoreCommon.h" #import "RNFBFirestoreSerialize.h" diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreTransactionModule.h b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreTransactionModule.h index 7fb14bb357..ae5d608e44 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreTransactionModule.h +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreTransactionModule.h @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseFirestore; +#endif #import #import #import diff --git a/packages/functions/RNFBFunctions.podspec b/packages/functions/RNFBFunctions.podspec index fe0174fadc..8755621334 100644 --- a/packages/functions/RNFBFunctions.podspec +++ b/packages/functions/RNFBFunctions.podspec @@ -1,4 +1,5 @@ require 'json' +require '../app/firebase_spm' package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) appPackage = JSON.parse(File.read(File.join('..', 'app', 'package.json'))) @@ -48,7 +49,7 @@ Pod::Spec.new do |s| end # Firebase dependencies - s.dependency 'Firebase/Functions', firebase_sdk_version + firebase_dependency(s, firebase_sdk_version, ['FirebaseFunctions'], 'Firebase/Functions') if defined?($RNFirebaseAsStaticFramework) Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'" diff --git a/packages/functions/ios/RNFBFunctions/RNFBFunctionsCallHandler.swift b/packages/functions/ios/RNFBFunctions/RNFBFunctionsCallHandler.swift index 73c81dd02c..0f71c7f331 100644 --- a/packages/functions/ios/RNFBFunctions/RNFBFunctionsCallHandler.swift +++ b/packages/functions/ios/RNFBFunctions/RNFBFunctionsCallHandler.swift @@ -22,7 +22,31 @@ import FirebaseCore /// Swift wrapper for Firebase Functions callable methods that's accessible from Objective-C /// This encapsulates the logic for calling Firebase Functions (both by name and URL) @objcMembers public class RNFBFunctionsCallHandler: NSObject { - + + // MARK: - Factory method for FIRFunctions (used by ObjC++ to avoid @import FirebaseFunctions) + + /// Creates and configures a Functions instance. + /// This factory exists because FirebaseFunctions is a pure-Swift SPM module + /// and cannot be imported from .mm files without -fcxx-modules (which breaks JSI). + @objc public static func createFunctions( + forApp app: FirebaseApp, + customUrlOrRegion: String, + emulatorHost: String?, + emulatorPort: Int + ) -> Functions { + let functions: Functions + if let url = URL(string: customUrlOrRegion), + url.scheme != nil, url.host != nil { + functions = Functions.functions(app: app, customDomain: customUrlOrRegion) + } else { + functions = Functions.functions(app: app, region: customUrlOrRegion) + } + if let host = emulatorHost { + functions.useEmulator(withHost: host, port: emulatorPort) + } + return functions + } + /// Call a Firebase Function by name /// - Parameters: /// - app: Firebase App instance diff --git a/packages/functions/ios/RNFBFunctions/RNFBFunctionsModule.mm b/packages/functions/ios/RNFBFunctions/RNFBFunctionsModule.mm index 3fe5fbd1ab..aa0db6b8a8 100644 --- a/packages/functions/ios/RNFBFunctions/RNFBFunctionsModule.mm +++ b/packages/functions/ios/RNFBFunctions/RNFBFunctionsModule.mm @@ -15,7 +15,19 @@ * */ +#if __has_include() #import +#elif __has_include() +#import +#import +#elif __has_include() +#import +// SPM: FirebaseFunctions is a pure-Swift module — no ObjC headers available. +// FIRFunctions instances are created via +// RNFBFunctionsCallHandler.createFunctions() factory. +#else +@import FirebaseCore; +#endif #import #import "NativeRNFBTurboFunctions.h" @@ -30,9 +42,9 @@ #elif __has_include("RNFBFunctions-Swift.h") // If `use_frameworks!` is not in use (for example, while using pre-built // react-native core) then header imports based on frameworks assumptions fail. -// So, if frameworks are not available, fall back to importing the header directly, it -// should be findable from a header search path pointing to the build -// directory. See firebase-ios-sdk#12611 for more context. +// So, if frameworks are not available, fall back to importing the header +// directly, it should be findable from a header search path pointing to the +// build directory. See firebase-ios-sdk#12611 for more context. #import "RNFBFunctions-Swift.h" #endif @@ -82,30 +94,28 @@ - (void)httpsCallable:(NSString *)appName options:(JS::NativeRNFBTurboFunctions::SpecHttpsCallableOptions &)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - NSURL *url = [NSURL URLWithString:customUrlOrRegion]; FIRApp *firebaseApp = [RCTConvert firAppFromString:appName]; - FIRFunctions *functions = - (url && url.scheme && url.host) - ? [FIRFunctions functionsForApp:firebaseApp customDomain:customUrlOrRegion] - : [FIRFunctions functionsForApp:firebaseApp region:customUrlOrRegion]; + // Use Swift factory — FirebaseFunctions is a pure-Swift SPM module, + // cannot be imported directly from .mm files. + id functions = [RNFBFunctionsCallHandler createFunctionsForApp:firebaseApp + customUrlOrRegion:customUrlOrRegion + emulatorHost:emulatorHost + emulatorPort:(int)emulatorPort]; id callableData = data.data(); - // In reality, this value is always null, because we always call it with null data - // on the javascript side for some reason. Check for that case (which should be 100% of the time) - // and set it to an `NSNull` (versus the `Optional` Swift will see from `valueForKey` so that - // FirebaseFunctions serializer won't have a validation failure for an unknown type. + // In reality, this value is always null, because we always call it with null + // data on the javascript side for some reason. Check for that case (which + // should be 100% of the time) and set it to an `NSNull` (versus the + // `Optional` Swift will see from `valueForKey` so that FirebaseFunctions + // serializer won't have a validation failure for an unknown type. if (callableData == nil) { callableData = [NSNull null]; } std::optional timeout = options.timeout(); - if (emulatorHost != nil) { - [functions useEmulatorWithHost:emulatorHost port:(int)emulatorPort]; - } - RNFBFunctionsCallHandler *handler = [[RNFBFunctionsCallHandler alloc] init]; double timeoutValue = timeout.has_value() ? timeout.value() : 0; @@ -140,20 +150,20 @@ - (void)httpsCallableFromUrl:(NSString *)appName (JS::NativeRNFBTurboFunctions::SpecHttpsCallableFromUrlOptions &)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - NSURL *customUrl = [NSURL URLWithString:customUrlOrRegion]; FIRApp *firebaseApp = [RCTConvert firAppFromString:appName]; - FIRFunctions *functions = - (customUrl && customUrl.scheme && customUrl.host) - ? [FIRFunctions functionsForApp:firebaseApp customDomain:customUrlOrRegion] - : [FIRFunctions functionsForApp:firebaseApp region:customUrlOrRegion]; + id functions = [RNFBFunctionsCallHandler createFunctionsForApp:firebaseApp + customUrlOrRegion:customUrlOrRegion + emulatorHost:emulatorHost + emulatorPort:(int)emulatorPort]; id callableData = data.data(); - // In reality, this value is always null, because we always call it with null data - // on the javascript side for some reason. Check for that case (which should be 100% of the time) - // and set it to an `NSNull` (versus the `Optional` Swift will see from `valueForKey` so that - // FirebaseFunctions serializer won't have a validation failure for an unknown type. + // In reality, this value is always null, because we always call it with null + // data on the javascript side for some reason. Check for that case (which + // should be 100% of the time) and set it to an `NSNull` (versus the + // `Optional` Swift will see from `valueForKey` so that FirebaseFunctions + // serializer won't have a validation failure for an unknown type. if (callableData == nil) { callableData = [NSNull null]; } @@ -161,10 +171,6 @@ - (void)httpsCallableFromUrl:(NSString *)appName std::optional timeout = options.timeout(); std::optional limitedUseAppCheckToken = options.limitedUseAppCheckTokens(); - if (emulatorHost != nil) { - [functions useEmulatorWithHost:emulatorHost port:(int)emulatorPort]; - } - RNFBFunctionsCallHandler *handler = [[RNFBFunctionsCallHandler alloc] init]; double timeoutValue = timeout.has_value() ? timeout.value() : 0; @@ -246,22 +252,17 @@ - (void)streamSetup:(NSString *)appName NSNumber *listenerIdNumber = @((int)listenerId); if (@available(iOS 15.0, macOS 12.0, *)) { - NSURL *customUrl = [NSURL URLWithString:customUrlOrRegion]; FIRApp *firebaseApp = [RCTConvert firAppFromString:appName]; - FIRFunctions *functions = - (customUrl && customUrl.scheme && customUrl.host) - ? [FIRFunctions functionsForApp:firebaseApp customDomain:customUrlOrRegion] - : [FIRFunctions functionsForApp:firebaseApp region:customUrlOrRegion]; + id functions = [RNFBFunctionsCallHandler createFunctionsForApp:firebaseApp + customUrlOrRegion:customUrlOrRegion + emulatorHost:emulatorHost + emulatorPort:(int)emulatorPort]; if (data == nil) { data = [NSNull null]; } - if (emulatorHost != nil) { - [functions useEmulatorWithHost:emulatorHost port:(int)emulatorPort]; - } - RNFBFunctionsStreamHandler *handler = [[RNFBFunctionsStreamHandler alloc] init]; double timeoutValue = timeout.has_value() ? timeout.value() : 0; diff --git a/packages/in-app-messaging/RNFBInAppMessaging.podspec b/packages/in-app-messaging/RNFBInAppMessaging.podspec index aa72f52e42..015484d4bf 100644 --- a/packages/in-app-messaging/RNFBInAppMessaging.podspec +++ b/packages/in-app-messaging/RNFBInAppMessaging.podspec @@ -1,4 +1,5 @@ require 'json' +require '../app/firebase_spm' package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) appPackage = JSON.parse(File.read(File.join('..', 'app', 'package.json'))) @@ -45,7 +46,7 @@ Pod::Spec.new do |s| end # Firebase dependencies - s.dependency 'Firebase/InAppMessaging', firebase_sdk_version + firebase_dependency(s, firebase_sdk_version, ['FirebaseInAppMessaging-Beta'], 'Firebase/InAppMessaging') if defined?($RNFirebaseAsStaticFramework) Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'" diff --git a/packages/in-app-messaging/ios/RNFBFiam/RNFBFiamModule.m b/packages/in-app-messaging/ios/RNFBFiam/RNFBFiamModule.m index 2b860c389e..12eacbebe8 100644 --- a/packages/in-app-messaging/ios/RNFBFiam/RNFBFiamModule.m +++ b/packages/in-app-messaging/ios/RNFBFiam/RNFBFiamModule.m @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseInAppMessagingInternal; +#endif #import #import diff --git a/packages/installations/RNFBInstallations.podspec b/packages/installations/RNFBInstallations.podspec index c4f480878e..97b07a009e 100644 --- a/packages/installations/RNFBInstallations.podspec +++ b/packages/installations/RNFBInstallations.podspec @@ -1,4 +1,5 @@ require 'json' +require '../app/firebase_spm' package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) appPackage = JSON.parse(File.read(File.join('..', 'app', 'package.json'))) @@ -45,7 +46,7 @@ Pod::Spec.new do |s| end # Firebase dependencies - s.dependency 'Firebase/Installations', firebase_sdk_version + firebase_dependency(s, firebase_sdk_version, ['FirebaseInstallations'], 'Firebase/Installations') if defined?($RNFirebaseAsStaticFramework) Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'" diff --git a/packages/installations/ios/RNFBInstallations/RNFBInstallationsModule.m b/packages/installations/ios/RNFBInstallations/RNFBInstallationsModule.m index 7b07b36a0b..69ebec034d 100644 --- a/packages/installations/ios/RNFBInstallations/RNFBInstallationsModule.m +++ b/packages/installations/ios/RNFBInstallations/RNFBInstallationsModule.m @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseInstallations; +#endif #import #import "RNFBApp/RNFBSharedUtils.h" diff --git a/packages/messaging/RNFBMessaging.podspec b/packages/messaging/RNFBMessaging.podspec index db9ae77d22..470eb76373 100644 --- a/packages/messaging/RNFBMessaging.podspec +++ b/packages/messaging/RNFBMessaging.podspec @@ -1,4 +1,5 @@ require 'json' +require '../app/firebase_spm' package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) appPackage = JSON.parse(File.read(File.join('..', 'app', 'package.json'))) @@ -42,8 +43,12 @@ Pod::Spec.new do |s| end # Firebase dependencies - s.dependency 'Firebase/Messaging', firebase_sdk_version - s.dependency 'FirebaseCoreExtension' + # FirebaseCoreExtension is a transitive dependency of FirebaseMessaging in SPM, + # so it only needs to be declared explicitly for CocoaPods. + firebase_dependency(s, firebase_sdk_version, + ['FirebaseMessaging'], + ['Firebase/Messaging', 'FirebaseCoreExtension'] + ) if defined?($RNFirebaseAsStaticFramework) Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'" diff --git a/packages/messaging/ios/RNFBMessaging/RNFBMessaging+AppDelegate.m b/packages/messaging/ios/RNFBMessaging/RNFBMessaging+AppDelegate.m index 9c3651d51b..76469e0a9e 100644 --- a/packages/messaging/ios/RNFBMessaging/RNFBMessaging+AppDelegate.m +++ b/packages/messaging/ios/RNFBMessaging/RNFBMessaging+AppDelegate.m @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseMessaging; +#endif #import #import diff --git a/packages/messaging/ios/RNFBMessaging/RNFBMessaging+FIRMessagingDelegate.h b/packages/messaging/ios/RNFBMessaging/RNFBMessaging+FIRMessagingDelegate.h index fb83927d51..007f6431c9 100644 --- a/packages/messaging/ios/RNFBMessaging/RNFBMessaging+FIRMessagingDelegate.h +++ b/packages/messaging/ios/RNFBMessaging/RNFBMessaging+FIRMessagingDelegate.h @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseMessaging; +#endif #import NS_ASSUME_NONNULL_BEGIN diff --git a/packages/messaging/ios/RNFBMessaging/RNFBMessaging+NSNotificationCenter.m b/packages/messaging/ios/RNFBMessaging/RNFBMessaging+NSNotificationCenter.m index 1867f14a40..759e5fd601 100644 --- a/packages/messaging/ios/RNFBMessaging/RNFBMessaging+NSNotificationCenter.m +++ b/packages/messaging/ios/RNFBMessaging/RNFBMessaging+NSNotificationCenter.m @@ -14,7 +14,12 @@ * limitations under the License. * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseMessaging; +#endif #import #import #import diff --git a/packages/messaging/ios/RNFBMessaging/RNFBMessagingModule.m b/packages/messaging/ios/RNFBMessaging/RNFBMessagingModule.m index 657ac96b30..94faefa898 100644 --- a/packages/messaging/ios/RNFBMessaging/RNFBMessagingModule.m +++ b/packages/messaging/ios/RNFBMessaging/RNFBMessagingModule.m @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseMessaging; +#endif #import #import #import diff --git a/packages/messaging/ios/RNFBMessaging/RNFBMessagingSerializer.h b/packages/messaging/ios/RNFBMessaging/RNFBMessagingSerializer.h index 502da9aecf..e3768b4715 100644 --- a/packages/messaging/ios/RNFBMessaging/RNFBMessagingSerializer.h +++ b/packages/messaging/ios/RNFBMessaging/RNFBMessagingSerializer.h @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseMessaging; +#endif #import #import diff --git a/packages/ml/RNFBML.podspec b/packages/ml/RNFBML.podspec index 4ab7f0a7df..a90e3a00cd 100644 --- a/packages/ml/RNFBML.podspec +++ b/packages/ml/RNFBML.podspec @@ -1,5 +1,6 @@ require 'json' require '../app/firebase_json' +require '../app/firebase_spm' package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) appPackage = JSON.parse(File.read(File.join('..', 'app', 'package.json'))) @@ -46,6 +47,9 @@ Pod::Spec.new do |s| end # Firebase dependencies + # NOTE: Firebase/MLModelDownloader dependency is currently disabled + # When re-enabled, use: firebase_dependency(s, firebase_sdk_version, + # ['FirebaseMLModelDownloader'], 'Firebase/MLModelDownloader') # s.dependency 'Firebase/MLModelDownloader', firebase_sdk_version if defined?($RNFirebaseAsStaticFramework) diff --git a/packages/perf/RNFBPerf.podspec b/packages/perf/RNFBPerf.podspec index 8456941ddb..b81ef8b2a1 100644 --- a/packages/perf/RNFBPerf.podspec +++ b/packages/perf/RNFBPerf.podspec @@ -1,4 +1,5 @@ require 'json' +require '../app/firebase_spm' package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) appPackage = JSON.parse(File.read(File.join('..', 'app', 'package.json'))) @@ -45,7 +46,7 @@ Pod::Spec.new do |s| end # Firebase dependencies - s.dependency 'Firebase/Performance', firebase_sdk_version + firebase_dependency(s, firebase_sdk_version, ['FirebasePerformance'], 'Firebase/Performance') if defined?($RNFirebaseAsStaticFramework) Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'" diff --git a/packages/perf/ios/RNFBPerf/RNFBPerfModule.m b/packages/perf/ios/RNFBPerf/RNFBPerfModule.m index 96570231a3..18b91c32ec 100644 --- a/packages/perf/ios/RNFBPerf/RNFBPerfModule.m +++ b/packages/perf/ios/RNFBPerf/RNFBPerfModule.m @@ -18,7 +18,12 @@ #import #import +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebasePerformance; +#endif #import "RNFBPerfModule.h" static __strong NSMutableDictionary *traces; diff --git a/packages/remote-config/RNFBRemoteConfig.podspec b/packages/remote-config/RNFBRemoteConfig.podspec index 68661bf39c..384381d91d 100644 --- a/packages/remote-config/RNFBRemoteConfig.podspec +++ b/packages/remote-config/RNFBRemoteConfig.podspec @@ -1,4 +1,5 @@ require 'json' +require '../app/firebase_spm' package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) appPackage = JSON.parse(File.read(File.join('..', 'app', 'package.json'))) @@ -45,7 +46,7 @@ Pod::Spec.new do |s| end # Firebase dependencies - s.dependency 'Firebase/RemoteConfig', firebase_sdk_version + firebase_dependency(s, firebase_sdk_version, ['FirebaseRemoteConfig'], 'Firebase/RemoteConfig') if defined?($RNFirebaseAsStaticFramework) Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'" diff --git a/packages/remote-config/ios/RNFBConfig/RNFBConfigModule.m b/packages/remote-config/ios/RNFBConfig/RNFBConfigModule.m index bbef694970..848423dae6 100644 --- a/packages/remote-config/ios/RNFBConfig/RNFBConfigModule.m +++ b/packages/remote-config/ios/RNFBConfig/RNFBConfigModule.m @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseRemoteConfigInternal; +#endif #import #import diff --git a/packages/storage/RNFBStorage.podspec b/packages/storage/RNFBStorage.podspec index 01d5952835..a0dced6466 100644 --- a/packages/storage/RNFBStorage.podspec +++ b/packages/storage/RNFBStorage.podspec @@ -1,4 +1,5 @@ require 'json' +require '../app/firebase_spm' package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) appPackage = JSON.parse(File.read(File.join('..', 'app', 'package.json'))) @@ -44,7 +45,7 @@ Pod::Spec.new do |s| end # Firebase dependencies - s.dependency 'Firebase/Storage', firebase_sdk_version + firebase_dependency(s, firebase_sdk_version, ['FirebaseStorage'], 'Firebase/Storage') if defined?($RNFirebaseAsStaticFramework) Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'" diff --git a/packages/storage/ios/RNFBStorage/RNFBStorageCommon.m b/packages/storage/ios/RNFBStorage/RNFBStorageCommon.m index 0c90d3c83a..aa9401a02a 100644 --- a/packages/storage/ios/RNFBStorage/RNFBStorageCommon.m +++ b/packages/storage/ios/RNFBStorage/RNFBStorageCommon.m @@ -16,7 +16,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseStorage; +#endif #import #import "RNFBSharedUtils.h" diff --git a/packages/storage/ios/RNFBStorage/RNFBStorageModule.m b/packages/storage/ios/RNFBStorage/RNFBStorageModule.m index a3695b6ecf..cc472438fc 100644 --- a/packages/storage/ios/RNFBStorage/RNFBStorageModule.m +++ b/packages/storage/ios/RNFBStorage/RNFBStorageModule.m @@ -15,7 +15,12 @@ * */ +#if __has_include() #import +#else +@import FirebaseCore; +@import FirebaseStorage; +#endif #import #import "RNFBRCTEventEmitter.h" diff --git a/tests/ios/Podfile b/tests/ios/Podfile index 6d2b7392a5..2472823574 100644 --- a/tests/ios/Podfile +++ b/tests/ios/Podfile @@ -14,12 +14,16 @@ Pod::UI.puts "react-native-firebase/tests: Using Firebase SDK version '#{$Fireba # Everything will be static with `use_frameworks!`, but you can set it manually for testing if desired # $RNFirebaseAsStaticFramework = true # toggle this to true (and set 'use_frameworks!' below to test static frameworks) +# SPM mode: SPM dependency resolution enabled (no $RNFirebaseDisableSPM flag) +# Works with dynamic linkage. For static linkage, set $RNFirebaseDisableSPM = true + # Toggle this to true for the no-ad-tracking Analytics subspec. Useful at minimum for Kids category apps. # See: https://firebase.google.com/support/release-notes/ios#analytics - requires firebase-ios-sdk 7.11.0+ #$RNFirebaseAnalyticsWithoutAdIdSupport = true # toggle this to true for the no-ad-tracking Analytics subspec # Toggle this to true if you want to include support for on device conversion measurement APIs -$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true +# Disabled for SPM test: GoogleAdsOnDeviceConversion is a static xcframework incompatible with dynamic linkage +# $RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true # Toggle this to true if you want to include optional support for extended analytics features $RNFirebaseAnalyticsEnableAdSupport = true @@ -41,7 +45,7 @@ platform :ios, min_ios_version_supported prepare_react_native_project! # set this to static and toggle '$RNFirebaseAsStaticFramework' above to test static frameworks) -linkage = 'static' # ENV['USE_FRAMEWORKS'] +linkage = 'dynamic' # SPM test: dynamic linkage avoids duplicate symbols if linkage != nil Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green use_frameworks! :linkage => linkage.to_sym @@ -93,10 +97,15 @@ target 'testing' do end # Bumps minimum deploy target to ours (which is >12.4): https://github.com/facebook/react-native/issues/34106 + # Xcode 26 enables explicit modules by default, but Firebase SPM internal targets + # (FirebaseCoreInternal, FirebaseSharedSwift) are not exposed as public products. + # 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 found automatically. installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings["GCC_WARN_INHIBIT_ALL_WARNINGS"] = "YES" config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = min_ios_version_supported + config.build_settings['SWIFT_ENABLE_EXPLICIT_MODULES'] = 'NO' end end end diff --git a/tests/local-tests/index.js b/tests/local-tests/index.js index 06e32d17ad..3e18345736 100644 --- a/tests/local-tests/index.js +++ b/tests/local-tests/index.js @@ -30,9 +30,11 @@ import { VertexAITestComponent } from './vertexai/vertexai'; import { AuthMFADemonstrator } from './auth/auth-mfa-demonstrator'; import { HttpsCallableTestComponent } from './functions/https-callable'; import { StreamingCallableTestComponent } from './functions/streaming-callable'; +import { SPMVerificationComponent } from './spm-verification'; const testComponents = { // List your imported components here... + 'SPM Dependency Verification': SPMVerificationComponent, 'Crashlytics Test Crash': CrashTestComponent, 'AI Generation Example': AITestComponent, 'Database onChildMoved Test': DatabaseOnChildMovedTest, diff --git a/tests/local-tests/spm-verification.jsx b/tests/local-tests/spm-verification.jsx new file mode 100644 index 0000000000..d33d3be797 --- /dev/null +++ b/tests/local-tests/spm-verification.jsx @@ -0,0 +1,131 @@ +/* eslint-disable react/react-in-jsx-scope */ +import { useState } from 'react'; +import { Button, ScrollView, StyleSheet, Text, View } from 'react-native'; +import { getApp, SDK_VERSION } from '@react-native-firebase/app'; +import { getAuth } from '@react-native-firebase/auth'; +import { getFirestore } from '@react-native-firebase/firestore'; +import { getDatabase } from '@react-native-firebase/database'; +import { getFunctions } from '@react-native-firebase/functions'; +import { getStorage } from '@react-native-firebase/storage'; +import { getCrashlytics, setAttribute } from '@react-native-firebase/crashlytics'; +import { getAnalytics, logEvent } from '@react-native-firebase/analytics'; +import { getRemoteConfig } from '@react-native-firebase/remote-config'; +import { getInstallations } from '@react-native-firebase/installations'; +import { getPerformance } from '@react-native-firebase/perf'; + +const MODULES_TO_CHECK = [ + { name: 'App', check: () => getApp().name }, + { name: 'Auth', check: () => getAuth().app.name }, + { name: 'Firestore', check: () => getFirestore().app.name }, + { name: 'Database', check: () => getDatabase().app.name }, + { name: 'Functions', check: () => getFunctions().app.name }, + { name: 'Storage', check: () => getStorage().app.name }, + { name: 'Crashlytics', check: () => getCrashlytics().app.name }, + { name: 'Analytics', check: () => getAnalytics().app.name }, + { name: 'RemoteConfig', check: () => getRemoteConfig().app.name }, + { name: 'Installations', check: () => getInstallations().app.name }, + { name: 'Perf', check: () => getPerformance().app.name }, +]; + +export function SPMVerificationComponent() { + const [results, setResults] = useState(null); + const [running, setRunning] = useState(false); + + const runVerification = async () => { + setRunning(true); + const moduleResults = []; + + const sdkVersion = SDK_VERSION; + + for (const mod of MODULES_TO_CHECK) { + try { + const result = mod.check(); + moduleResults.push({ name: mod.name, status: 'OK', detail: String(result) }); + } catch (e) { + moduleResults.push({ name: mod.name, status: 'FAIL', detail: e.message }); + } + } + + // Verify app options are populated (proves native initialization worked) + try { + const app = getApp(); + const opts = app.options; + moduleResults.push({ + name: 'App Options', + status: opts.appId ? 'OK' : 'FAIL', + detail: opts.appId ? `appId: ${opts.appId.substring(0, 15)}...` : 'No appId', + }); + } catch (e) { + moduleResults.push({ name: 'App Options', status: 'FAIL', detail: e.message }); + } + + // Verify Crashlytics setAttribute (proves native bridge works) + try { + await setAttribute(getCrashlytics(), 'spm_test', 'true'); + moduleResults.push({ name: 'Crashlytics setAttribute', status: 'OK', detail: 'Set OK' }); + } catch (e) { + moduleResults.push({ name: 'Crashlytics setAttribute', status: 'FAIL', detail: e.message }); + } + + // Verify Analytics logEvent (proves native bridge works) + try { + await logEvent(getAnalytics(), 'spm_verification_test', { timestamp: Date.now() }); + moduleResults.push({ name: 'Analytics logEvent', status: 'OK', detail: 'Logged OK' }); + } catch (e) { + moduleResults.push({ name: 'Analytics logEvent', status: 'FAIL', detail: e.message }); + } + + const passed = moduleResults.filter(r => r.status === 'OK').length; + const failed = moduleResults.filter(r => r.status === 'FAIL').length; + + setResults({ sdkVersion, modules: moduleResults, passed, failed }); + setRunning(false); + }; + + return ( + + SPM Dependency Verification + + Verifies all Firebase native modules are loaded and functional. + + +