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/DOCUMENTACION_SPM_IMPLEMENTACION.md b/DOCUMENTACION_SPM_IMPLEMENTACION.md new file mode 100644 index 0000000000..0e5bc74da9 --- /dev/null +++ b/DOCUMENTACION_SPM_IMPLEMENTACION.md @@ -0,0 +1,1135 @@ +# Documentacion Completa: Soporte Dual SPM + CocoaPods para Firebase en React Native + +> **Fecha:** 2026-03-17 +> **Rama:** `feature/spm-dependency-support` +> **Repositorio:** `jsnavarroc/react-native-firebase` (fork de `invertase/react-native-firebase`) +> **Firebase SDK:** 12.10.0 +> **React Native minimo para SPM:** 0.75+ + +--- + +## Tabla de Contenido + +1. [Resumen Ejecutivo](#1-resumen-ejecutivo) +2. [Arquitectura de la Solucion](#2-arquitectura-de-la-solucion) +3. [Cambios Realizados](#3-cambios-realizados) +4. [Referencia de Funciones y Parametros](#4-referencia-de-funciones-y-parametros) +5. [Guia de Integracion — Proyecto Legacy](#5-guia-de-integracion--proyecto-legacy) +6. [Guia de Integracion — Proyectos que requieren actualizar SPM](#6-guia-de-integracion--proyectos-que-requieren-actualizar-spm) +7. [Glosario](#7-glosario) + +--- + +## 1. Resumen Ejecutivo + +### Que problema resuelve + +Cuando Apple lanzo **Xcode 26** (2026), introdujo un cambio importante: el compilador Swift ahora usa **"modulos explicitos"** por defecto. Esto significa que el compilador necesita saber exactamente donde esta cada modulo (cada libreria) antes de compilar. + +El problema es que **Firebase iOS SDK**, cuando se instala a traves de **CocoaPods** (el gestor de dependencias tradicional de iOS), tiene modulos internos (`FirebaseCoreInternal`, `FirebaseSharedSwift`) que **no estan expuestos como productos publicos**. En Xcode 16, esto no era un problema porque el compilador los encontraba automaticamente. En Xcode 26, el compilador ya no los busca por su cuenta y lanza errores de compilacion. + +**La solucion:** Usar **Swift Package Manager (SPM)** como metodo principal para resolver dependencias de Firebase. SPM es el gestor de paquetes nativo de Apple y maneja correctamente la visibilidad de modulos internos. Como alternativa, se mantiene CocoaPods para proyectos que lo necesiten, con un workaround (`SWIFT_ENABLE_EXPLICIT_MODULES=NO`). + +### Que se implemento + +Un **sistema de resolucion dual de dependencias** que permite elegir entre SPM y CocoaPods para Firebase, de forma transparente, sin cambiar el codigo de la app. El sistema: + +1. **Detecta automaticamente** si SPM esta disponible (React Native >= 0.75) +2. **Usa SPM por defecto** cuando esta disponible +3. **Cae a CocoaPods** cuando SPM no esta disponible o cuando se desactiva explicitamente +4. **No requiere cambios** en el codigo JavaScript/TypeScript de la app +5. **No requiere cambios** en el codigo nativo (Objective-C/Swift) de la app + +### Puntos criticos antes de integrar + +| Punto | Detalle | +|-------|---------| +| **Linkage** | SPM requiere **dynamic linkage**. CocoaPods requiere **static linkage**. No se pueden mezclar. | +| **Xcode 26** | Si usas CocoaPods con Xcode 26, DEBES agregar `SWIFT_ENABLE_EXPLICIT_MODULES = 'NO'` en tu Podfile post_install. | +| **React Native < 0.75** | Solo funciona con CocoaPods (SPM no esta disponible en versiones anteriores). | +| **Simbolos duplicados** | Si usas SPM con `static linkage`, cada pod embebe los productos SPM de Firebase → linker error por simbolos duplicados. Por eso SPM = dynamic. | +| **FirebaseCoreExtension** | Algunos paquetes (Messaging, Crashlytics) necesitan `FirebaseCoreExtension` como dependencia explicita en CocoaPods, pero SPM lo resuelve automaticamente como dependencia transitiva. | + +--- + +## 2. Arquitectura de la Solucion + +### 2.1 Diagrama del Flujo de Decision + +``` + ┌─────────────────────────┐ + │ pod install / build │ + └────────────┬────────────┘ + │ + ┌────────────▼────────────┐ + │ Podspec carga │ + │ firebase_spm.rb │ + └────────────┬────────────┘ + │ + ┌────────────▼────────────┐ + │ ¿spm_dependency() esta │ + │ definida? (RN >= 0.75) │ + └──────┬──────────┬───────┘ + │ │ + SI NO + │ │ + ┌────────────▼──┐ ┌──▼─────────────────┐ + │ ¿$RNFirebase │ │ Usar CocoaPods │ + │ DisableSPM │ │ spec.dependency() │ + │ esta activo? │ └────────────────────┘ + └───┬───────┬───┘ + │ │ + SI NO + │ │ + ┌────────────▼──┐ ┌─▼──────────────────┐ + │ Usar CocoaPods│ │ Usar SPM │ + │ (forzado) │ │ spm_dependency() │ + └───────────────┘ └────────────────────┘ +``` + +### 2.2 Componentes del Sistema + +El sistema tiene **5 componentes** que interactuan entre si: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ COMPONENTE CENTRAL │ +│ packages/app/firebase_spm.rb │ +│ → Define la funcion firebase_dependency() │ +│ → Lee la URL de SPM desde package.json │ +│ → Decide automaticamente: SPM o CocoaPods │ +└────────────────────────────┬────────────────────────────────────┘ + │ es requerido por + ┌──────────────────┼────────────────┐ + │ │ │ + ┌───────▼──────┐ ┌───────▼──────┐ ┌───────▼──────┐ + │ 16 Podspecs │ │ package.json │ │ 43 archivos │ + │ (*.podspec) │ │ sdkVersions │ │ nativos iOS │ + │ │ │ │ │ (.h, .m) │ + │ Cada uno │ │ Define: │ │ │ + │ llama a │ │ - version │ │ Usan #if │ + │ firebase_ │ │ Firebase │ │ __has_include│ + │ dependency() │ │ - URL SPM │ │ para imports │ + └──────────────┘ └──────────────┘ │ duales │ + │ └──────────────┘ + │ + ┌───────▼──────────────────────────────────────────┐ + │ CI/CD: tests_e2e_ios.yml │ + │ → Prueba AMBOS modos en cada PR │ + │ → Matriz: spm × cocoapods × debug × release │ + └──────────────────────────────────────────────────┘ +``` + +### 2.3 Decisiones de Diseno y su Justificacion + +| Decision | Justificacion | +|----------|--------------| +| **SPM como default** | Apple impulsa SPM como el estandar. Xcode 26 funciona mejor con SPM. La comunidad iOS esta migrando a SPM. | +| **Mantener CocoaPods como fallback** | Muchos proyectos legacy dependen de CocoaPods. React Native < 0.75 no soporta SPM. Algunos setups (static frameworks) necesitan CocoaPods. | +| **Una sola funcion helper** | En lugar de modificar cada podspec individualmente, se centraliza la logica en `firebase_dependency()`. Si cambia la logica, se cambia en un solo lugar. | +| **URL de SPM en package.json** | Single source of truth: la version de Firebase y la URL de SPM estan en un solo archivo. Evita desincronizacion entre paquetes. | +| **Flag `$RNFirebaseDisableSPM`** | Escape hatch: si algo falla con SPM, el usuario puede volver a CocoaPods con una sola linea en el Podfile. | +| **Dynamic linkage para SPM** | Evita que cada pod embeba una copia de los productos SPM de Firebase (lo que causa "duplicate symbols" en static). | +| **`SWIFT_ENABLE_EXPLICIT_MODULES=NO` para CocoaPods** | Permite que el compilador Swift use descubrimiento implicito de modulos (como Xcode 16), evitando errores con modulos internos de Firebase. | +| **`#if __has_include` en codigo nativo** | Permite que el mismo archivo .m/.h compile tanto con SPM (headers de framework) como con CocoaPods (@import). Sin cambios en el codigo de la app. | + +--- + +## 3. Cambios Realizados + +### 3.1 `packages/app/firebase_spm.rb` — El Cerebro del Sistema + +- **Que es:** Un archivo Ruby que define una funcion helper +- **Donde:** `packages/app/firebase_spm.rb` +- **Por que existe:** Porque cada paquete de react-native-firebase (auth, analytics, messaging, etc.) tiene un archivo `.podspec` que declara sus dependencias iOS. Antes, cada uno hacia `s.dependency 'Firebase/Auth', version` directamente. Ahora, todos llaman a `firebase_dependency()` que decide si usar SPM o CocoaPods. +- **Para que sirve:** Centralizar la logica de decision SPM vs CocoaPods en un solo lugar +- **Como funciona:** + +```ruby +# PASO 1: Lee la URL del repositorio SPM de Firebase desde package.json +# Esto se ejecuta UNA sola vez cuando se carga el archivo +$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'] + # Resultado: "https://github.com/firebase/firebase-ios-sdk.git" +end + +# PASO 2: Funcion que cada podspec llama +def firebase_dependency(spec, version, spm_products, pods) + + # Condicion 1: ¿Existe la funcion spm_dependency? + # → SI existe si React Native >= 0.75 (ellos la agregaron) + # → NO existe si React Native < 0.75 + # + # Condicion 2: ¿El usuario NO definio $RNFirebaseDisableSPM? + # → Si el usuario puso $RNFirebaseDisableSPM = true en su Podfile, + # esta variable EXISTE y la condicion es falsa + + if defined?(spm_dependency) && !defined?($RNFirebaseDisableSPM) + # RUTA SPM: Registra la dependencia via Swift Package Manager + spm_dependency(spec, + url: $firebase_spm_url, + requirement: { kind: 'upToNextMajorVersion', minimumVersion: version }, + products: spm_products + ) + else + # RUTA COCOAPODS: Registra la dependencia via CocoaPods tradicional + pods = [pods] unless pods.is_a?(Array) # Normaliza a array + pods.each do |pod| + spec.dependency pod, version + end + end +end +``` + +**Detalle linea por linea:** + +| Linea | Codigo | Que hace | +|-------|--------|----------| +| 22 | `$firebase_spm_url \|\|= begin` | Variable global Ruby. El `\|\|=` significa "asigna solo si no tiene valor". Se ejecuta una vez. | +| 23 | `File.join(__dir__, 'package.json')` | Construye la ruta al package.json del paquete `app`. `__dir__` es el directorio donde esta este archivo. | +| 24 | `JSON.parse(File.read(...))` | Lee y parsea el JSON del package.json | +| 25 | `['sdkVersions']['ios']['firebaseSpmUrl']` | Extrae la URL: `https://github.com/firebase/firebase-ios-sdk.git` | +| 44 | `def firebase_dependency(spec, version, spm_products, pods)` | Define la funcion con 4 parametros | +| 45 | `if defined?(spm_dependency) && !defined?($RNFirebaseDisableSPM)` | Doble condicion: SPM disponible Y no deshabilitado | +| 46 | `if defined?(Pod) && defined?(Pod::UI)` | Guard: solo imprime log si estamos dentro de CocoaPods (no en tests) | +| 50-54 | `spm_dependency(spec, url:..., requirement:..., products:...)` | Llama a la funcion de React Native para registrar dependencia SPM | +| 65 | `pods = [pods] unless pods.is_a?(Array)` | Si `pods` es un string, lo convierte a array de un elemento | +| 66-68 | `pods.each { \|pod\| spec.dependency pod, version }` | Registra cada pod como dependencia CocoaPods | + +### 3.2 `packages/app/__tests__/firebase_spm_test.rb` — Tests Unitarios + +- **Que es:** Archivo de tests en Ruby usando el framework Minitest +- **Donde:** `packages/app/__tests__/firebase_spm_test.rb` +- **Por que existe:** Para verificar que la logica de decision SPM vs CocoaPods funciona correctamente en CI sin necesidad de un proyecto iOS real +- **Para que sirve:** Detectar regresiones si alguien modifica `firebase_spm.rb` +- **Como funciona:** + +```ruby +# MockSpec simula un Pod::Specification de CocoaPods +# Captura las llamadas a .dependency() para poder verificarlas +class MockSpec + attr_reader :dependencies # Array donde se guardan las dependencias registradas + + def dependency(name, version) + @dependencies << { name: name, version: version } + end +end +``` + +**Los 5 tests:** + +| Test | Que verifica | Como lo verifica | +|------|-------------|-----------------| +| `test_cocoapods_single_pod` | Que cuando SPM NO esta disponible, se usa CocoaPods con un solo pod | Llama a `firebase_dependency` sin definir `spm_dependency`. Verifica que `spec.dependencies` tiene 1 entrada: `Firebase/Auth` | +| `test_cocoapods_multiple_pods` | Que cuando SPM NO esta disponible, se registran multiples pods | Pasa un array `['Firebase/Crashlytics', 'FirebaseCoreExtension']`. Verifica que ambos se registran. | +| `test_spm_single_product` | Que cuando SPM SI esta disponible, se llama a `spm_dependency` | Define un mock de `spm_dependency` como metodo global. Verifica que se llama con los parametros correctos (URL, version, products). | +| `test_spm_multiple_products_ignores_cocoapods_extras` | Que SPM solo usa los productos SPM, no los pods extra | Pasa `['FirebaseCrashlytics']` como SPM y `['Firebase/Crashlytics', 'FirebaseCoreExtension']` como CocoaPods. Verifica que solo `FirebaseCrashlytics` se registra via SPM. | +| `test_reads_spm_url_from_package_json` | Que la URL se lee correctamente del package.json | Verifica que `$firebase_spm_url == 'https://github.com/firebase/firebase-ios-sdk.git'` | + +### 3.3 `packages/app/package.json` — Fuente de Verdad + +- **Que es:** El package.json del paquete `@react-native-firebase/app` +- **Donde:** `packages/app/package.json` +- **Que se agrego:** Un campo `firebaseSpmUrl` dentro de `sdkVersions.ios` +- **Por que:** Para que la URL del repositorio SPM de Firebase este en un solo lugar, no hardcodeada en multiples archivos +- **Para que sirve:** `firebase_spm.rb` lee este campo para saber de donde descargar Firebase via SPM + +```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" + } + } +} +``` + +| Campo | Tipo | Que es | +|-------|------|--------| +| `firebase` | String | Version del Firebase iOS SDK. Usada por todos los podspecs. | +| `firebaseSpmUrl` | String | URL del repositorio git de Firebase para SPM. | +| `iosTarget` | String | Version minima de iOS soportada. | +| `macosTarget` | String | Version minima de macOS soportada. | +| `tvosTarget` | String | Version minima de tvOS soportada. | + +### 3.4 Los 16 Archivos `.podspec` — Consumidores del Helper + +Cada paquete de react-native-firebase tiene un archivo `.podspec` que le dice a CocoaPods como instalarlo. **Todos fueron modificados** para usar `firebase_dependency()` en lugar de `s.dependency` directo. + +**Antes (metodo original):** +```ruby +# En RNFBAuth.podspec +s.dependency 'Firebase/Auth', firebase_sdk_version +``` + +**Despues (con soporte SPM):** +```ruby +# En RNFBAuth.podspec +require '../app/firebase_spm' # Carga el helper +firebase_dependency(s, firebase_sdk_version, ['FirebaseAuth'], 'Firebase/Auth') +``` + +**Tabla completa de los 16 paquetes:** + +| Paquete | Podspec | Productos SPM | Pods CocoaPods | Notas | +|---------|---------|---------------|----------------|-------| +| **app** | `RNFBApp.podspec` | `['FirebaseCore']` | `'Firebase/CoreOnly'` | Paquete base, requerido por todos | +| **auth** | `RNFBAuth.podspec` | `['FirebaseAuth']` | `'Firebase/Auth'` | Autenticacion | +| **analytics** | `RNFBAnalytics.podspec` | `['FirebaseAnalytics']` | `'FirebaseAnalytics/Core'` | Tiene logica extra para IdentitySupport | +| **messaging** | `RNFBMessaging.podspec` | `['FirebaseMessaging']` | `['Firebase/Messaging', 'FirebaseCoreExtension']` | Necesita 2 pods en CocoaPods | +| **crashlytics** | `RNFBCrashlytics.podspec` | `['FirebaseCrashlytics']` | `['Firebase/Crashlytics', 'FirebaseCoreExtension']` | Necesita 2 pods en CocoaPods | +| **firestore** | `RNFBFirestore.podspec` | `['FirebaseFirestore']` | `'Firebase/Firestore'` | Base de datos NoSQL | +| **database** | `RNFBDatabase.podspec` | `['FirebaseDatabase']` | `'Firebase/Database'` | Realtime Database | +| **storage** | `RNFBStorage.podspec` | `['FirebaseStorage']` | `'Firebase/Storage'` | Almacenamiento de archivos | +| **functions** | `RNFBFunctions.podspec` | `['FirebaseFunctions']` | `'Firebase/Functions'` | Cloud Functions | +| **perf** | `RNFBPerf.podspec` | `['FirebasePerformance']` | `'Firebase/Performance'` | Performance Monitoring | +| **app-check** | `RNFBAppCheck.podspec` | `['FirebaseAppCheck']` | `'Firebase/AppCheck'` | Verificacion de integridad | +| **installations** | `RNFBInstallations.podspec` | `['FirebaseInstallations']` | `'Firebase/Installations'` | IDs de instalacion | +| **remote-config** | `RNFBRemoteConfig.podspec` | `['FirebaseRemoteConfig']` | `'Firebase/RemoteConfig'` | Configuracion remota | +| **in-app-messaging** | `RNFBInAppMessaging.podspec` | `['FirebaseInAppMessaging-Beta']` | `'Firebase/InAppMessaging'` | Mensajes in-app | +| **app-distribution** | `RNFBAppDistribution.podspec` | `['FirebaseAppDistribution-Beta']` | `'Firebase/AppDistribution'` | Distribucion de apps | +| **ml** | `RNFBML.podspec` | *(deshabilitado)* | *(deshabilitado)* | Machine Learning (comentado) | + +**¿Por que Messaging y Crashlytics necesitan 2 pods en CocoaPods pero solo 1 producto en SPM?** + +Porque `FirebaseCoreExtension` es una dependencia **transitiva** en SPM — cuando instalas `FirebaseMessaging` via SPM, SPM automaticamente incluye `FirebaseCoreExtension`. Pero en CocoaPods, cada dependencia debe declararse explicitamente. + +### 3.5 Los 43 Archivos Nativos iOS — Imports Duales + +- **Que son:** Archivos `.h` (headers) y `.m`/`.mm` (implementacion) en Objective-C +- **Donde:** Dentro de `packages/*/ios/RNFB*/` +- **Por que se modificaron:** Porque SPM y CocoaPods exponen los headers de Firebase de formas diferentes +- **Para que sirve:** Para que el mismo codigo compile tanto con SPM como con CocoaPods + +**Patron de import dual:** + +```objc +// ANTES (solo CocoaPods): +#import // Header umbrella que incluye todo + +// DESPUES (SPM + CocoaPods): +#if __has_include() + // Ruta 1: CocoaPods — el header umbrella existe + #import +#elif __has_include() + // Ruta 2: SPM — cada modulo tiene su propio header + #import + #import +#else + // Ruta 3: @import (modulos Clang) — fallback final + @import FirebaseCore; + @import FirebaseAuth; +#endif +``` + +**Explicacion del patron:** + +| Directiva | Que hace | Cuando se usa | +|-----------|----------|--------------| +| `#if __has_include()` | Pregunta al compilador: "¿existe este header en el proyecto?" | En compilacion. Si CocoaPods instalo Firebase, este header existe. | +| `#import ` | Importa el header umbrella de Firebase (incluye TODO) | Solo con CocoaPods, porque CocoaPods crea este header que agrupa todo. | +| `#elif __has_include()` | Pregunta: "¿existe el header individual del modulo?" | En compilacion. Si SPM instalo FirebaseAuth, este header existe. | +| `#import ` | Importa el header especifico del modulo | Con SPM, porque cada producto SPM tiene su propio namespace. | +| `@import FirebaseAuth;` | Import de modulo Clang (Objective-C modules) | Fallback: funciona en ambos modos pero requiere que modules este habilitado. | + +**Archivos modificados por paquete:** + +| Paquete | Archivos | Headers importados | +|---------|----------|-------------------| +| 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 con Matriz Dual + +- **Que es:** Archivo de GitHub Actions que define los tests end-to-end de iOS +- **Donde:** `.github/workflows/tests_e2e_ios.yml` +- **Por que se modifico:** Para probar AMBOS modos (SPM y CocoaPods) en cada Pull Request +- **Para que sirve:** Garantizar que ningun cambio rompa ninguno de los dos modos + +**Cambio clave 1 — Matriz ampliada:** + +```yaml +# ANTES: solo probaba debug y release +let buildmode = ['debug', 'release']; + +# DESPUES: tambien prueba SPM y CocoaPods +let buildmode = ['debug', 'release']; +let depResolution = ['spm', 'cocoapods']; # NUEVO +``` + +Esto genera **4 combinaciones** de jobs E2E: +- `iOS (debug, spm, 0)` +- `iOS (debug, cocoapods, 0)` +- `iOS (release, spm, 0)` +- `iOS (release, cocoapods, 0)` + +**Cambio clave 2 — Step "Configure Dependency Resolution Mode":** + +```yaml +- name: Configure Dependency Resolution Mode + run: | + if [[ "${{ matrix.dep-resolution }}" == "cocoapods" ]]; then + echo "Configuring CocoaPods-only mode (disabling SPM)" + cd tests/ios + + # 1. Cambia linkage de dynamic a static + sed -i '' "s/^linkage = 'dynamic'/linkage = 'static'/" Podfile + + # 2. Inyecta la flag $RNFirebaseDisableSPM = true al inicio del Podfile + printf '%s\n' '$RNFirebaseDisableSPM = true' | cat - Podfile > Podfile.tmp && mv Podfile.tmp Podfile + + # 3. Remueve SWIFT_ENABLE_EXPLICIT_MODULES (no necesario sin SPM en 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 +``` + +**Explicacion paso a paso de cada comando:** + +| # | Comando | Que hace | Por que | +|---|---------|----------|---------| +| 1 | `sed -i '' "s/^linkage = 'dynamic'/linkage = 'static'/"` | Busca la linea `linkage = 'dynamic'` y la reemplaza por `linkage = 'static'` | CocoaPods necesita static linkage para evitar problemas con frameworks | +| 2 | `printf '%s\n' '$RNFirebaseDisableSPM = true' \| cat - Podfile > Podfile.tmp && mv Podfile.tmp Podfile` | Prepone `$RNFirebaseDisableSPM = true` al inicio del Podfile | Activa el flag que hace que `firebase_dependency()` use CocoaPods | +| 3 | `sed -i '' "/SWIFT_ENABLE_EXPLICIT_MODULES/d"` | Elimina cualquier linea que contenga `SWIFT_ENABLE_EXPLICIT_MODULES` | En modo CocoaPods en CI, no necesitamos este workaround | + +**¿Por que `printf | cat` en lugar de `sed -i`?** + +El comando `sed -i '' "/^platform :ios/i\\\n$RNFirebaseDisableSPM = true\n"` (insertar antes de una linea) requiere texto multi-linea. Dentro de un bloque YAML `run: |`, las lineas sin indentacion rompen el YAML. `printf | cat` es un workaround portable que funciona dentro de YAML sin problemas de indentacion. + +### 3.7 `tests/ios/Podfile` — Podfile de Tests con Workaround Xcode 26 + +- **Que es:** El Podfile del proyecto de tests E2E +- **Donde:** `tests/ios/Podfile` +- **Por que se modifico:** Para soportar modo SPM por defecto y agregar el workaround de Xcode 26 +- **Para que sirve:** Es el archivo que CocoaPods lee para saber que dependencias instalar en el proyecto de tests + +**Lineas clave:** + +```ruby +# Linea 48 — Linkage dinamico para SPM +linkage = 'dynamic' # En modo CocoaPods, CI lo cambia a 'static' + +# Lineas 100-110 — Workaround Xcode 26 +# Xcode 26 habilita modulos explicitos por defecto, pero los targets +# internos de Firebase SPM (FirebaseCoreInternal, FirebaseSharedSwift) +# no estan expuestos como productos publicos. +# Esto NO desactiva SPM — solo le dice al compilador Swift que use +# descubrimiento implicito de modulos (comportamiento de Xcode 16). +installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['SWIFT_ENABLE_EXPLICIT_MODULES'] = 'NO' + end +end +``` + +**¿Que es `SWIFT_ENABLE_EXPLICIT_MODULES`?** + +Es un build setting de Xcode que controla como el compilador Swift encuentra los modulos: +- `YES` (default en Xcode 26): El compilador SOLO encuentra modulos que estan explicitamente declarados. Si un modulo no esta listado como "publico", no lo encuentra. +- `NO` (default en Xcode 16): El compilador busca modulos automaticamente en todas las rutas de busqueda. Esto es mas permisivo pero menos estricto. + +Firebase tiene modulos internos que no son publicos. Con `YES`, Xcode 26 no los encuentra → error de compilacion. Con `NO`, funciona como antes. + +--- + +## 4. Referencia de Funciones y Parametros + +### 4.1 `firebase_dependency(spec, version, spm_products, pods)` + +**Proposito:** Registra una dependencia de Firebase en un podspec, eligiendo automaticamente entre SPM y CocoaPods. + +**Parametros:** + +| Parametro | Tipo Ruby | Requerido | Descripcion | Ejemplo | +|-----------|-----------|-----------|-------------|---------| +| `spec` | `Pod::Specification` | Si | El objeto podspec (el `s` en el DSL de podspec). Representa al paquete que se esta configurando. | `s` (viene del contexto del podspec) | +| `version` | `String` | Si | Version del Firebase iOS SDK a usar. Debe coincidir con la version en package.json. | `'12.10.0'` | +| `spm_products` | `Array` | Si | Lista de nombres de productos SPM de Firebase. Estos nombres son los que aparecen en el `Package.swift` del firebase-ios-sdk. | `['FirebaseAuth']` o `['FirebaseCrashlytics']` | +| `pods` | `String` o `Array` | Si | Nombre(s) de las dependencias CocoaPods. Puede ser un string (1 dependencia) o un array (multiples). Estos nombres son los que aparecen en el Podspec de Firebase. | `'Firebase/Auth'` o `['Firebase/Messaging', 'FirebaseCoreExtension']` | + +**Valor de retorno:** `nil` — La funcion no retorna valor. Su efecto es lateral: registra la dependencia en el sistema (SPM o CocoaPods). + +**Ejemplo de uso en un podspec:** + +```ruby +# RNFBAuth.podspec +require '../app/firebase_spm' + +Pod::Spec.new do |s| + # ... configuracion del podspec ... + + firebase_sdk_version = appPackage['sdkVersions']['ios']['firebase'] + + # Registra FirebaseAuth como dependencia + # - Si SPM: llama spm_dependency(s, url: "...", products: ['FirebaseAuth']) + # - Si CocoaPods: llama s.dependency('Firebase/Auth', '12.10.0') + firebase_dependency(s, firebase_sdk_version, ['FirebaseAuth'], 'Firebase/Auth') +end +``` + +**Ejemplo con multiples dependencias CocoaPods:** + +```ruby +# RNFBCrashlytics.podspec +firebase_dependency(s, firebase_sdk_version, + ['FirebaseCrashlytics'], # SPM: solo necesita este + ['Firebase/Crashlytics', 'FirebaseCoreExtension'] # CocoaPods: necesita ambos +) +``` + +### 4.2 Variable Global `$firebase_spm_url` + +**Proposito:** Almacena la URL del repositorio git de Firebase iOS SDK para SPM. + +| Propiedad | Valor | +|-----------|-------| +| **Tipo** | `String` (variable global Ruby) | +| **Valor default** | `nil` (se asigna al cargar `firebase_spm.rb`) | +| **Valor despues de cargar** | `'https://github.com/firebase/firebase-ios-sdk.git'` | +| **Se puede sobreescribir** | Si. Si defines `$firebase_spm_url = 'otra-url'` ANTES de cargar `firebase_spm.rb`, usara tu URL. | + +**Caso de uso para sobreescribir:** + +```ruby +# En tu Podfile, antes de cualquier pod install: +$firebase_spm_url = 'https://github.com/mi-empresa/firebase-ios-sdk-fork.git' +# Ahora todos los paquetes RNFB usaran tu fork de Firebase +``` + +### 4.3 Variable Global `$RNFirebaseDisableSPM` + +**Proposito:** Flag para forzar el uso de CocoaPods y deshabilitar SPM. + +| Propiedad | Valor | +|-----------|-------| +| **Tipo** | Cualquiera (se checa con `defined?()`, no con valor) | +| **Valor default** | No definida (SPM habilitado) | +| **Como activar** | `$RNFirebaseDisableSPM = true` en tu Podfile | +| **Efecto** | `firebase_dependency()` siempre usara CocoaPods | + +**IMPORTANTE:** La funcion checa `defined?($RNFirebaseDisableSPM)`, NO el valor. Esto significa que incluso `$RNFirebaseDisableSPM = false` DESACTIVA SPM, porque la variable esta "definida". Para habilitar SPM, simplemente no definas esta variable. + +### 4.4 Funcion `spm_dependency` (proporcionada por React Native) + +**NO esta definida en este proyecto.** Es una funcion que React Native (>= 0.75) inyecta durante el proceso de `pod install`. Si existe, significa que el entorno soporta SPM. + +| Parametro | Tipo | Descripcion | +|-----------|------|-------------| +| `spec` | `Pod::Specification` | Podspec al que agregar la dependencia | +| `url:` | `String` | URL del repositorio git del paquete Swift | +| `requirement:` | `Hash` | Restriccion de version. Formato: `{ kind: 'upToNextMajorVersion', minimumVersion: '12.10.0' }` | +| `products:` | `Array` | Lista de productos SPM a incluir | + +--- + +## 5. Guia de Integracion — Proyecto Legacy + +> **Proyecto legacy** = Un proyecto que usa React Native con CocoaPods y NO tiene soporte SPM. + +### 5.1 Requisitos previos + +| Requisito | Minimo | Recomendado | +|-----------|--------|-------------| +| React Native | 0.73+ | 0.75+ (para SPM) | +| Xcode | 15.0 | 26+ | +| CocoaPods | 1.14+ | 1.16+ | +| iOS target | 15.0+ | 15.1+ | +| Ruby | 2.7+ | 3.0+ | + +### 5.2 Paso a paso + +#### Paso 1: Actualizar `@react-native-firebase` a la version con soporte SPM + +```bash +# En tu proyecto React Native +yarn add @react-native-firebase/app@latest +yarn add @react-native-firebase/auth@latest +# ... repite para cada modulo que uses +``` + +#### Paso 2: Decidir — ¿SPM o CocoaPods? + +**Usa SPM si:** +- React Native >= 0.75 +- Xcode 26+ +- No tienes dependencias que requieran static linkage +- Quieres el modo recomendado por Apple + +**Usa CocoaPods si:** +- React Native < 0.75 +- Tienes `use_frameworks! :linkage => :static` en tu Podfile +- Tienes otras dependencias incompatibles con SPM +- Prefieres no cambiar nada (modo legacy) + +#### Paso 3A: Configuracion para SPM (recomendado) + +```ruby +# ios/Podfile + +# Asegurate de tener dynamic linkage +linkage = 'dynamic' +use_frameworks! :linkage => linkage.to_sym + +target 'TuApp' do + # ... tus pods ... + + post_install do |installer| + # OBLIGATORIO para 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 +``` + +Luego: +```bash +cd ios && pod install +``` + +Deberias ver mensajes como: +``` +[react-native-firebase] RNFBApp: Using SPM for Firebase dependency resolution (products: FirebaseCore) +[react-native-firebase] RNFBAuth: Using SPM for Firebase dependency resolution (products: FirebaseAuth) +``` + +#### Paso 3B: Configuracion para CocoaPods (legacy) + +```ruby +# ios/Podfile — ANTES de las declaraciones de target + +$RNFirebaseDisableSPM = true # Fuerza CocoaPods + +# Static linkage (requerido para CocoaPods) +linkage = 'static' +use_frameworks! :linkage => linkage.to_sym + +target 'TuApp' do + # ... tus pods ... + + post_install do |installer| + # OBLIGATORIO si usas 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 +``` + +Deberias ver: +``` +[react-native-firebase] RNFBApp: SPM disabled ($RNFirebaseDisableSPM = true), using CocoaPods for Firebase dependencies +``` + +#### Paso 4: Verificar que compila + +```bash +cd ios && xcodebuild -workspace TuApp.xcworkspace -scheme TuApp -sdk iphonesimulator build +``` + +### 5.3 Posibles conflictos + +| Conflicto | Sintoma | Solucion | +|-----------|---------|----------| +| **Duplicate symbols** | `duplicate symbol '_FIRApp' in ...` | Estas usando SPM con static linkage. Cambia a `dynamic` o activa `$RNFirebaseDisableSPM` | +| **Module not found** | `No such module 'FirebaseAuth'` | Falta `SWIFT_ENABLE_EXPLICIT_MODULES = 'NO'` en Xcode 26 | +| **Header not found** | `'Firebase/Firebase.h' file not found` | El modo actual (SPM) no genera ese header umbrella. Los archivos nativos ya usan `#if __has_include` para manejar esto. | +| **Pod::UI undefined** | `NameError: uninitialized constant Pod` | Estas ejecutando `firebase_spm.rb` fuera de CocoaPods (en tests). El guard `defined?(Pod)` ya lo maneja. | +| **Version mismatch** | `unable to satisfy version requirement` | Asegurate que la version en `package.json` de `@react-native-firebase/app` coincide con tus pods. | + +### 5.4 Checklist post-integracion + +- [ ] `pod install` completa sin errores +- [ ] Los mensajes de log muestran el modo correcto (SPM o CocoaPods) +- [ ] El proyecto compila en Xcode sin errores +- [ ] La app inicia y `Firebase.configure()` se ejecuta +- [ ] Las funciones de Firebase (auth, analytics, etc.) funcionan +- [ ] Los tests existentes pasan + +--- + +## 6. Guia de Integracion — Proyectos que requieren actualizar SPM + +### 6.1 ¿Que es SPM? + +**Swift Package Manager (SPM)** es el gestor de paquetes nativo de Apple, integrado en Xcode. A diferencia de CocoaPods (que es una herramienta de terceros), SPM esta construido dentro de Xcode y Swift. + +| Aspecto | CocoaPods | SPM | +|---------|-----------|-----| +| **Instalacion** | `gem install cocoapods` | Ya viene con Xcode | +| **Archivo config** | `Podfile` | `Package.swift` | +| **Lock file** | `Podfile.lock` | `Package.resolved` | +| **Resolucion** | Centralizada (trunk server) | Descentralizada (git repos) | +| **Tipo** | Ruby gem externo | Herramienta nativa Apple | + +### 6.2 Dependencias y versiones minimas + +| Dependencia | Version Minima | Razon | +|-------------|---------------|-------| +| `react-native` / `react-native-tvos` | 0.75.0 | Primera version que expone `spm_dependency()` en el runtime de CocoaPods | +| `@react-native-firebase/app` | Version con soporte SPM (esta PR) | Necesita `firebase_spm.rb` | +| Xcode | 15.0 (funcional), 26+ (recomendado) | SPM esta integrado desde Xcode 11, pero Xcode 26 cambia el compilador | +| Firebase iOS SDK | 12.10.0+ | Version testeada con este sistema | +| CocoaPods | 1.14+ | Para que `spm_dependency()` funcione correctamente en el contexto de pod install | + +### 6.3 Instrucciones paso a paso + +#### Paso 1: Verificar version de React Native + +```bash +node -p "require('./package.json').dependencies['react-native']" +# o para tvOS: +node -p "require('./package.json').dependencies['react-native-tvos']" +``` + +Si es < 0.75, necesitas hacer upgrade de React Native primero. SPM no esta disponible en versiones anteriores. + +#### Paso 2: Verificar linkage en tu Podfile + +Abre `ios/Podfile` y busca: +```ruby +use_frameworks! :linkage => :static +``` + +Si tienes `:static`, necesitas cambiarlo a `:dynamic` para SPM: +```ruby +use_frameworks! :linkage => :dynamic +``` + +**ADVERTENCIA:** Cambiar de static a dynamic puede afectar otras dependencias. Verifica que todas tus dependencias soporten dynamic linkage. + +#### Paso 3: Remover `$RNFirebaseDisableSPM` si existe + +Si tu Podfile tiene esta linea, eliminala: +```ruby +$RNFirebaseDisableSPM = true # ELIMINAR ESTA LINEA +``` + +#### Paso 4: Agregar workaround Xcode 26 (si aplica) + +En tu 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 +``` + +#### Paso 5: Limpiar y reinstalar + +```bash +cd ios +rm -rf Pods +rm Podfile.lock +pod install +``` + +#### Paso 6: Verificar en la salida de pod install + +Busca estas lineas: +``` +[react-native-firebase] RNFBApp: Using SPM for Firebase dependency resolution (products: FirebaseCore) +``` + +Si ves "Using SPM", el modo SPM esta activo. + +Si ves "SPM not available", tu version de React Native no soporta SPM. + +### 6.4 Errores comunes y soluciones + +| Error | Causa | Solucion | +|-------|-------|----------| +| `duplicate symbol` durante linkeo | SPM + static linkage | Cambiar a `dynamic` linkage | +| `No such module 'FirebaseCore'` | Xcode 26 con explicit modules | Agregar `SWIFT_ENABLE_EXPLICIT_MODULES = 'NO'` | +| `spm_dependency is not defined` | RN < 0.75 | Actualizar React Native a >= 0.75 o usar CocoaPods con `$RNFirebaseDisableSPM = true` | +| `multiple commands produce Firebase.framework` | Conflicto entre SPM y CocoaPods para Firebase | Asegurate de NO tener `pod 'Firebase/Core'` manual en tu Podfile si SPM esta activo | +| `unable to resolve package` | URL de SPM incorrecta | Verificar `firebaseSpmUrl` en `packages/app/package.json` | +| `trueplatform :ios` en CI | Bug del sed multiline en YAML | Ya resuelto. Usar `printf \| cat` en lugar de `sed -i insert` | +| Pod install loop / conflicto de versiones | Version de Firebase no coincide entre SPM y CocoaPods | Asegurate de usar la misma version en `package.json` y cualquier pod manual | + +### 6.5 Rollback a CocoaPods + +Si SPM no funciona, puedes volver a CocoaPods en 30 segundos: + +```ruby +# Agrega al inicio de tu Podfile: +$RNFirebaseDisableSPM = true + +# Cambia linkage a static: +use_frameworks! :linkage => :static +``` + +```bash +cd ios && rm -rf Pods && pod install +``` + +--- + +## 7. Glosario + +| Termino | Definicion | +|---------|-----------| +| **SPM (Swift Package Manager)** | Herramienta de Apple para gestionar dependencias en proyectos iOS/macOS. Viene integrada en Xcode. Es el reemplazo moderno de CocoaPods. | +| **CocoaPods** | Herramienta de terceros (escrita en Ruby) para gestionar dependencias en iOS. Fue el estandar durante anos. Usa un archivo `Podfile` para declarar dependencias. | +| **Podspec (.podspec)** | Archivo de configuracion que describe una libreria distribuida via CocoaPods. Define nombre, version, archivos fuente, dependencias, etc. | +| **Podfile** | Archivo en la raiz del directorio `ios/` que declara que dependencias necesita tu proyecto. CocoaPods lo lee durante `pod install`. | +| **Linkage (static vs dynamic)** | Define como se enlazan las librerias al binario final. **Static**: el codigo de la libreria se copia dentro de tu app. **Dynamic**: la libreria es un archivo separado que se carga en runtime. | +| **Framework** | En iOS, una forma de empaquetar una libreria con sus headers, recursos y metadatos. Puede ser static o dynamic. | +| **Header (.h)** | Archivo que declara la interfaz publica de una libreria en Objective-C/C. Le dice al compilador que funciones/clases existen. | +| **Implementation (.m, .mm)** | Archivo con el codigo real (implementacion) en Objective-C (.m) o Objective-C++ (.mm). | +| **`#if __has_include`** | Directiva del preprocesador de C/Objective-C que pregunta: "¿existe este archivo en las rutas de busqueda?" Retorna true/false. Se evalua en tiempo de compilacion. | +| **`@import`** | Forma moderna de importar un modulo en Objective-C. Equivalente a `#import` pero mas eficiente (usa modulos Clang). | +| **Modulos explicitos** | Feature de Xcode 26: el compilador solo reconoce modulos que estan explicitamente declarados. Modulos internos/transitivos no se encuentran automaticamente. | +| **Modulos implicitos** | Comportamiento anterior a Xcode 26: el compilador busca modulos en todas las rutas de busqueda, incluyendo transitivos. | +| **Dependencia transitiva** | Una dependencia que no declaras directamente, pero que es requerida por una dependencia que si declaraste. Ejemplo: si usas `FirebaseMessaging` y este necesita `FirebaseCoreExtension`, entonces `FirebaseCoreExtension` es transitiva. | +| **`defined?()` (Ruby)** | Operador Ruby que verifica si una expresion esta definida. Retorna una descripcion de la expresion (string) o `nil`. NO lanza error si no esta definida. | +| **Variable global Ruby (`$var`)** | Variable que comienza con `$` en Ruby. Es accesible desde cualquier parte del programa. Se usa aqui para configuracion compartida entre archivos. | +| **`spm_dependency()`** | Funcion que React Native (>= 0.75) inyecta en el contexto de CocoaPods durante `pod install`. Permite que un podspec declare una dependencia SPM. | +| **YAML block scalar (`\|`)** | En archivos YAML, `\|` indica un bloque de texto multi-linea donde se preservan los saltos de linea. Usado en GitHub Actions para scripts multi-linea. | +| **Umbrella header** | Un archivo `.h` que importa todos los headers de un framework. CocoaPods crea `Firebase/Firebase.h` que incluye todo. SPM no genera este archivo. | +| **Xcode build setting** | Configuracion que controla como Xcode compila tu proyecto. Se define en el archivo `.xcodeproj` o via CocoaPods `post_install`. Ejemplo: `SWIFT_ENABLE_EXPLICIT_MODULES`. | +| **CI/CD** | Continuous Integration / Continuous Deployment. Sistema automatizado que compila, testea y despliega codigo en cada cambio. Aqui se usa GitHub Actions. | +| **Matrix (CI)** | Estrategia de GitHub Actions para ejecutar el mismo job con diferentes combinaciones de parametros. Ejemplo: `buildmode: [debug, release]` × `dep-resolution: [spm, cocoapods]` = 4 ejecuciones. | +| **Interop layer** | Capa de compatibilidad que permite que codigo antiguo funcione con APIs nuevas. React Native 0.81 con Old Architecture usa interop para que los bridges Objective-C funcionen con el nuevo sistema. | +| **react-native-firebase** | Libreria open-source que proporciona modulos de Firebase para React Native. Cada servicio de Firebase (Auth, Analytics, etc.) es un paquete separado. Repo original: `invertase/react-native-firebase`. | +| **Fork** | Copia de un repositorio git en otra cuenta. Se usa para hacer cambios sin afectar el original. Aqui: `jsnavarroc/react-native-firebase` es fork de `invertase/react-native-firebase`. | +| **PR (Pull Request)** | Solicitud para integrar cambios de una rama a otra. Aqui: PR #1 de `feature/spm-dependency-support` → `main` en el fork. | +| **clang-format** | Herramienta de formateo automatico para codigo C/C++/Objective-C. El CI la usa para verificar que el codigo sigue el estilo del proyecto. | +| **Old Architecture (React Native)** | Arquitectura original de React Native que usa bridge Objective-C (`RCT_EXTERN_MODULE`), `NativeEventEmitter`, y `setNativeProps`. La "New Architecture" usa TurboModules y Fabric. | + +--- + +## Apendice A: Commits en la Rama + +| Hash | Mensaje | Que cambio | +|------|---------|-----------| +| `ca5604939` | `feat(ios): add SPM dependency resolution support alongside CocoaPods` | Commit principal: firebase_spm.rb, tests, 16 podspecs, 43 archivos nativos, CI matrix | +| `d5e423bec` | `fix(ios): guard Pod::UI.puts calls for test environments` | Agrego `if defined?(Pod::UI)` para evitar error en tests Ruby | +| `dcb618b41` | `fix(ios): resolve CI failures — clang-format, sed indentation, Pod constant guard` | Formateo clang-format, fix sed en workflow, guard `defined?(Pod)` | +| `d21651adb` | `fix(ci): use printf instead of sed multiline for Podfile flag insertion` | Reemplazo sed multiline por printf+cat para compatibilidad YAML | + +## Apendice B: Estructura de Archivos del Cambio + +``` +react-native-firebase/ +├── packages/ +│ ├── app/ +│ │ ├── firebase_spm.rb ← NUEVO: Helper central +│ │ ├── __tests__/ +│ │ │ └── firebase_spm_test.rb ← NUEVO: Tests unitarios +│ │ ├── package.json ← MODIFICADO: agrego firebaseSpmUrl +│ │ └── RNFBApp.podspec ← MODIFICADO: usa firebase_dependency() +│ ├── auth/ +│ │ ├── RNFBAuth.podspec ← MODIFICADO +│ │ └── ios/RNFBAuth/ +│ │ ├── RNFBAuthModule.h ← MODIFICADO: #if __has_include +│ │ └── RNFBAuthModule.m ← MODIFICADO: #if __has_include +│ ├── analytics/ +│ │ ├── RNFBAnalytics.podspec ← MODIFICADO +│ │ └── ios/RNFBAnalytics/ +│ │ └── RNFBAnalyticsModule.m ← MODIFICADO +│ ├── messaging/ +│ │ ├── RNFBMessaging.podspec ← MODIFICADO +│ │ └── ios/RNFBMessaging/ +│ │ ├── RNFBMessagingModule.m ← MODIFICADO +│ │ └── RNFBMessagingSerializer.m← MODIFICADO +│ ├── crashlytics/ +│ │ ├── RNFBCrashlytics.podspec ← MODIFICADO +│ │ └── ios/RNFBCrashlytics/ +│ │ ├── RNFBCrashlyticsModule.m ← MODIFICADO +│ │ ├── RNFBCrashlyticsInitProvider.h ← MODIFICADO +│ │ ├── RNFBCrashlyticsInitProvider.m ← MODIFICADO +│ │ └── RNFBCrashlyticsNativeHelper.m ← MODIFICADO +│ ├── firestore/ ← MODIFICADO (5 archivos) +│ ├── database/ ← MODIFICADO (5 archivos) +│ ├── storage/ ← MODIFICADO (2 archivos) +│ ├── functions/ ← MODIFICADO (1 archivo .mm) +│ ├── perf/ ← MODIFICADO +│ ├── app-check/ ← MODIFICADO (2 archivos) +│ ├── installations/ ← MODIFICADO +│ ├── remote-config/ ← MODIFICADO +│ ├── in-app-messaging/ ← MODIFICADO +│ ├── app-distribution/ ← MODIFICADO +│ └── ml/ ← MODIFICADO (podspec deshabilitado) +├── tests/ +│ └── ios/ +│ └── Podfile ← MODIFICADO: linkage dynamic + SWIFT_ENABLE_EXPLICIT_MODULES +└── .github/ + └── workflows/ + └── tests_e2e_ios.yml ← MODIFICADO: matriz SPM/CocoaPods + step configuracion +``` + +--- + +## 8. Bugs Encontrados Durante la Integracion y Sus Soluciones + +> Esta seccion documenta bugs reales encontrados al integrar esta solucion en un proyecto tvOS con React Native 0.77 → 0.81 (react-native-tvos). Util para diagnosticar problemas similares. + +--- + +### Bug 1: Linker error con `APMETaskManager` / `APMMeasurement` al usar `$RNFirebaseAnalyticsWithoutAdIdSupport = true` con SPM + +**Sintoma:** + +``` +Undefined symbols for architecture arm64: + "_OBJC_CLASS_$_APMETaskManager" + "_OBJC_CLASS_$_APMMeasurement" +``` + +**Cuando ocurre:** Solo cuando se usan las tres condiciones simultaneamente: +1. SPM habilitado (no `$RNFirebaseDisableSPM`) +2. `$RNFirebaseAnalyticsWithoutAdIdSupport = true` en el Podfile +3. `FirebasePerformance` NO esta instalado + +**Causa raiz:** + +El producto SPM `FirebaseAnalytics` incluye `GoogleAppMeasurement`, que contiene referencias cruzadas a `APMETaskManager` y `APMMeasurement` (clases de Firebase Performance Monitoring). Cuando `FirebasePerformance` no esta instalado, esas clases no existen → error de linker. + +El producto SPM `FirebaseAnalyticsCore` usa `GoogleAppMeasurementCore` en su lugar, que es la version sin IDFA y sin las referencias APM. Es exactamente lo que se necesita cuando se quiere analytics sin Ad ID support. + +**Archivo afectado:** `packages/analytics/RNFBAnalytics.podspec` + +**Fix aplicado:** + +```ruby +# ANTES (solo FirebaseAnalytics, siempre): +firebase_dependency(s, firebase_sdk_version, ['FirebaseAnalytics'], 'FirebaseAnalytics/Core') + +# DESPUES (condicional segun $RNFirebaseAnalyticsWithoutAdIdSupport + SPM): +if defined?(spm_dependency) && !defined?($RNFirebaseDisableSPM) && + defined?($RNFirebaseAnalyticsWithoutAdIdSupport) && $RNFirebaseAnalyticsWithoutAdIdSupport + # FirebaseAnalyticsCore → GoogleAppMeasurementCore (sin IDFA, sin APM objects) + Pod::UI.puts "#{s.name}: Using FirebaseAnalyticsCore SPM product (no IDFA, uses GoogleAppMeasurementCore)." + firebase_dependency(s, firebase_sdk_version, ['FirebaseAnalyticsCore'], 'FirebaseAnalytics/Core') +else + firebase_dependency(s, firebase_sdk_version, ['FirebaseAnalytics'], 'FirebaseAnalytics/Core') +end +``` + +**Cuando aplicar este fix:** Siempre que se use `$RNFirebaseAnalyticsWithoutAdIdSupport = true` con SPM y sin `FirebasePerformance`. + +--- + +### Bug 2: Error "Packages are not supported when using legacy build locations" en Xcode 26 + +**Sintoma:** + +``` +error: Packages are not supported when using legacy build locations. +``` + +El build falla inmediatamente al abrir el workspace o durante `xcodebuild`. + +**Cuando ocurre:** Al usar SPM (con `spm_dependency`) en un proyecto que tiene CocoaPods. El proyecto de Pods (`.xcodeproj` generado por CocoaPods) usa "legacy build locations" por defecto, pero Xcode 26 no permite paquetes SPM con ese modo. + +**Causa raiz:** + +CocoaPods genera proyectos `.xcodeproj` que no tienen `WorkspaceSettings.xcsettings` con `BuildSystemType = Latest`. Sin este archivo, Xcode usa "legacy build locations". Xcode 26 agregó la restricción de que los paquetes SPM no pueden usarse con ese modo. + +**Archivo afectado:** `node_modules/react-native/scripts/cocoapods/spm.rb` (parte de `react-native`, no de este fork) + +**Fix aplicado en `spm.rb` (metodo `apply_on_post_install`):** + +```ruby +# Crear WorkspaceSettings.xcsettings para optar por modern build system +unless @dependencies_by_pod.empty? + shared_data_dir = File.join(project.path, 'project.xcworkspace', 'xcshareddata') + FileUtils.mkdir_p(shared_data_dir) + settings_path = File.join(shared_data_dir, 'WorkspaceSettings.xcsettings') + unless File.exist?(settings_path) + File.write(settings_path, <<~PLIST) + + + + + BuildSystemType + Latest + + + PLIST + end +end +``` + +**Nota importante:** Este fix esta en `react-native` / `react-native-tvos`, no en este fork. Hay que verificar si esta incluido en react-native >= 0.81 o aplicarlo via post_install hook en el Podfile del proyecto. + +**Workaround alternativo (en Podfile del proyecto):** + +```ruby +post_install do |installer| + shared_data_dir = File.join(installer.pods_project.path, 'project.xcworkspace', 'xcshareddata') + FileUtils.mkdir_p(shared_data_dir) + settings_path = File.join(shared_data_dir, 'WorkspaceSettings.xcsettings') + unless File.exist?(settings_path) + File.write(settings_path, <<~PLIST) + + + + + BuildSystemType + Latest + + + PLIST + end +end +``` + +--- + +### Bug 3: Crash "XCSwiftPackageProductDependency _setSavedArchiveVersion" o "Type checking error: got XCSwiftPackageProductDependency for mainGroup" + +**Sintoma:** + +Xcode crashea al abrir el workspace o el build falla con uno de estos errores: +``` +XCSwiftPackageProductDependency _setSavedArchiveVersion: unrecognized selector +``` +``` +Type checking error: got XCSwiftPackageProductDependency for mainGroup +``` + +El `Pods.xcodeproj` resulta corrupto. + +**Cuando ocurre:** En proyectos que ya tuvieron dependencias SPM y luego se ejecuta `pod install` de nuevo (por ejemplo, al cambiar versiones de Firebase o al hacer `rm -rf Pods && pod install`). + +**Causa raiz:** + +CocoaPods usa un contador secuencial para generar UUIDs de objetos en el `.pbxproj`. Cuando `clean_spm_dependencies_from_target` elimina los objetos SPM del proyecto, sus UUIDs vuelven al pool disponible. Al agregar nuevos objetos SPM, el contador puede reutilizar esos UUIDs, que coinciden con UUIDs de objetos no-SPM ya existentes (como `rootObject` o `mainGroup`). + +En `objects_by_uuid`, el ultimo que escribe gana. El objeto SPM sobreescribe la entrada del objeto no-SPM → el proyecto queda corrupto. + +**Archivo afectado:** `node_modules/react-native/scripts/cocoapods/spm.rb` (metodo `apply_on_post_install`) + +**Fix aplicado en `spm.rb`:** + +```ruby +# ANTES de agregar objetos SPM, tomar snapshot de objects_by_uuid +pre_spm_snapshot = project.objects_by_uuid.dup + +# ... agregar objetos SPM ... + +# DESPUES, detectar y corregir colisiones +fix_spm_uuid_collisions(project, pre_spm_snapshot) +``` + +El metodo `fix_spm_uuid_collisions` detecta objetos SPM cuyo UUID existia en el snapshot pre-SPM, les asigna un UUID aleatorio seguro via `SecureRandom.hex`, y restaura el objeto original en su UUID original. + +**Nota importante:** Este fix esta en `react-native` / `react-native-tvos`, no en este fork. Reportar upstream o aplicar manualmente si se usa react-native < 0.81. + +--- + +### Bug 4: `RNFBAppModule not found` — crash al iniciar la app en tvOS + +**Sintoma:** + +``` +RNFBAppModule not found. Did you forget to link the native module? +``` + +La app crashea inmediatamente al iniciar en tvOS. + +**Cuando ocurre:** Especificamente en el target tvOS. En iOS el mismo build funciona correctamente. + +**Causa raiz:** + +El target tvOS en `project.pbxproj` tenia `OTHER_LDFLAGS` incompleto: +``` +OTHER_LDFLAGS = ("$(inherited)", " "); // tvOS — FALTABA -ObjC +``` + +Mientras el target iOS tenia: +``` +OTHER_LDFLAGS = ("$(inherited)", "-ObjC", "-lc++"); // iOS — correcto +``` + +Sin `-ObjC`, el linker no carga las categorias y clases Objective-C definidas en librerias estaticas (como los modulos nativos de Firebase). El modulo `RNFBAppModule` no se registra en el bridge de React Native → crash. + +**Archivo afectado:** `ios/NextPlay.xcodeproj/project.pbxproj` (en el proyecto cliente, no en el fork) + +**Fix:** Agregar `-ObjC` y `-lc++` a `OTHER_LDFLAGS` de los targets tvOS (Debug y Release): + +``` +OTHER_LDFLAGS = ("$(inherited)", "-ObjC", "-lc++"); +``` + +Despues del fix: **Clean Build Folder** (`Cmd+Shift+K`) y rebuild. + +**Como diagnosticar:** Buscar en `project.pbxproj` todas las ocurrencias de `OTHER_LDFLAGS` y verificar que los targets tvOS (`SDKROOT = appletvos`) tengan `-ObjC`. + +--- + +### Bug 5: Firebase DebugView no muestra eventos (demora de hasta 1 hora) + +**Sintoma:** + +Los eventos de Firebase Analytics no aparecen en Firebase DebugView. Si aparecen, lo hacen con 30-60 minutos de retraso. + +**Cuando ocurre:** Sin las flags de debug, Firebase Analytics agrupa los eventos y los envia en batch cada ~30-60 minutos para optimizar bateria/red. + +**Causa raiz:** + +Dos problemas combinados: +1. `AppDelegate.swift` solo tenia `FirebaseApp.configure()` sin activar el modo debug +2. Se usaba `-FIRDebugEnabled` en lugar de `-FIRAnalyticsDebugEnabled` (flag especifica para Analytics DebugView) +3. `CommandLine.arguments.append(...)` solo funciona si la app se lanza desde Xcode. Si se lanza manualmente en el simulador, no tiene efecto + +**Fix aplicado en `AppDelegate.swift` (ANTES de `FirebaseApp.configure()`):** + +```swift +#if DEBUG +// Metodo 1: funciona cuando se lanza desde Xcode +CommandLine.arguments.append("-FIRAnalyticsDebugEnabled") + +// Metodo 2: funciona cuando se lanza manualmente en el simulador +UserDefaults.standard.set(true, forKey: "/google/firebase/debug_mode") +UserDefaults.standard.set(true, forKey: "/google/measurement/debug_mode") +UserDefaults.standard.synchronize() +#endif + +FirebaseApp.configure() +``` + +**Nota:** Para tvOS, usar el simulador "Apple TV 4K (2nd generation)" o posterior. El simulador "Apple TV" original no reporta eventos a Firebase DebugView. + +--- + +### Resumen de Bugs por Archivo + +| Bug | Archivo afectado | Repositorio | Fix incluido en este PR | +|-----|-----------------|-------------|------------------------| +| APM linker error con `WithoutAdIdSupport` | `packages/analytics/RNFBAnalytics.podspec` | **este fork** | Si | +| Legacy build locations Xcode 26 | `node_modules/react-native/scripts/cocoapods/spm.rb` | react-native/react-native-tvos | No (upstream) | +| UUID collision en pbxproj | `node_modules/react-native/scripts/cocoapods/spm.rb` | react-native/react-native-tvos | No (upstream) | +| `RNFBAppModule not found` tvOS | `ios/*.xcodeproj/project.pbxproj` | proyecto cliente | No (fix manual) | +| Firebase DebugView sin eventos | `ios/NextPlay/AppDelegate.swift` | proyecto cliente | No (fix manual) | 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..0fcf3c1557 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,41 @@ 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." + # Analytics has conditional dependencies that vary between SPM and CocoaPods. + # SPM: use FirebaseAnalyticsWithoutAdIdSupport when $RNFirebaseAnalyticsWithoutAdIdSupport = true + # to avoid GoogleAppMeasurement APM symbols that require FirebaseRemoteConfig (linker error). + # CocoaPods: IdentitySupport is a separate subspec controlled by $RNFirebaseAnalyticsWithoutAdIdSupport. + if defined?(spm_dependency) && !defined?($RNFirebaseDisableSPM) && + defined?($RNFirebaseAnalyticsWithoutAdIdSupport) && $RNFirebaseAnalyticsWithoutAdIdSupport + # FirebaseAnalyticsCore uses GoogleAppMeasurementCore (no IDFA, no APM objects). + # FirebaseAnalytics uses GoogleAppMeasurement which has APMETaskManager/APMMeasurement + # cross-references that cause linker errors when FirebasePerformance is not linked. + Pod::UI.puts "#{s.name}: Using FirebaseAnalyticsCore SPM product (no IDFA, uses GoogleAppMeasurementCore)." + firebase_dependency(s, firebase_sdk_version, ['FirebaseAnalyticsCore'], 'FirebaseAnalytics/Core') 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 + firebase_dependency(s, firebase_sdk_version, ['FirebaseAnalytics'], 'FirebaseAnalytics/Core') + end - # 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. + + +