diff --git a/.github/workflows/tests_e2e_ios.yml b/.github/workflows/tests_e2e_ios.yml index 7d78a895f7..e383e3c651 100644 --- a/.github/workflows/tests_e2e_ios.yml +++ b/.github/workflows/tests_e2e_ios.yml @@ -218,8 +218,8 @@ jobs: continue-on-error: true with: path: tests/ios/Pods - key: ${{ runner.os }}-ios-pods-v3-${{ matrix.dep-resolution }}-${{ hashFiles('tests/ios/Podfile.lock') }} - restore-keys: ${{ runner.os }}-ios-pods-v3-${{ matrix.dep-resolution }} + key: ${{ runner.os }}-${{ matrix.dep-resolution }}-ios-pods-v3-${{ hashFiles('tests/ios/Podfile.lock') }} + restore-keys: ${{ runner.os }}-${{ matrix.dep-resolution }}-ios-pods-v3 - name: Configure Dependency Resolution Mode run: | diff --git a/.gitignore b/.gitignore index 738419a9c8..5a3873c8cd 100644 --- a/.gitignore +++ b/.gitignore @@ -237,6 +237,8 @@ typings/ # Output of 'npm pack' *.tgz +# Exception: tarballs/ is a versioned folder for distributing prebuilt packages +!tarballs/*.tgz # Yarn Integrity file .yarn-integrity diff --git a/DOCUMENTACION_SPM_IMPLEMENTACION.md b/DOCUMENTACION_SPM_IMPLEMENTACION.md deleted file mode 100644 index 0e5bc74da9..0000000000 --- a/DOCUMENTACION_SPM_IMPLEMENTACION.md +++ /dev/null @@ -1,1135 +0,0 @@ -# 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/packages/analytics/RNFBAnalytics.podspec b/packages/analytics/RNFBAnalytics.podspec index 0fcf3c1557..2c82be7437 100644 --- a/packages/analytics/RNFBAnalytics.podspec +++ b/packages/analytics/RNFBAnalytics.podspec @@ -47,10 +47,11 @@ Pod::Spec.new do |s| # Firebase dependencies # Analytics has conditional dependencies that vary between SPM and CocoaPods. - # SPM: use FirebaseAnalyticsWithoutAdIdSupport when $RNFirebaseAnalyticsWithoutAdIdSupport = true - # to avoid GoogleAppMeasurement APM symbols that require FirebaseRemoteConfig (linker error). + # SPM: use FirebaseAnalyticsCore when $RNFirebaseAnalyticsWithoutAdIdSupport = true + # to avoid GoogleAppMeasurement APM symbols (APMETaskManager, APMMeasurement) + # that require FirebasePerformance at link time. # CocoaPods: IdentitySupport is a separate subspec controlled by $RNFirebaseAnalyticsWithoutAdIdSupport. - if defined?(spm_dependency) && !defined?($RNFirebaseDisableSPM) && + if defined?(spm_dependency) && !rnfirebase_spm_disabled? && defined?($RNFirebaseAnalyticsWithoutAdIdSupport) && $RNFirebaseAnalyticsWithoutAdIdSupport # FirebaseAnalyticsCore uses GoogleAppMeasurementCore (no IDFA, no APM objects). # FirebaseAnalytics uses GoogleAppMeasurement which has APMETaskManager/APMMeasurement @@ -61,7 +62,7 @@ Pod::Spec.new do |s| firebase_dependency(s, firebase_sdk_version, ['FirebaseAnalytics'], 'FirebaseAnalytics/Core') end - unless defined?(spm_dependency) + unless defined?(spm_dependency) && !rnfirebase_spm_disabled? # CocoaPods-only: conditional IdentitySupport subspec if defined?($RNFirebaseAnalyticsWithoutAdIdSupport) && ($RNFirebaseAnalyticsWithoutAdIdSupport == true) Pod::UI.puts "#{s.name}: Not installing FirebaseAnalytics/IdentitySupport Pod, no IDFA will be collected." @@ -80,10 +81,21 @@ Pod::Spec.new do |s| s.frameworks = 'AdSupport' end - # GoogleAdsOnDeviceConversion (CocoaPods only, not available in firebase-ios-sdk SPM) + # GoogleAdsOnDeviceConversion (CocoaPods only) + # This is a static xcframework distributed separately from firebase-ios-sdk. + # It is NOT available as an SPM product in the firebase-ios-sdk Package.swift. + # When using SPM (dynamic linkage), this static xcframework causes duplicate + # symbol errors. Use CocoaPods mode ($RNFirebaseDisableSPM = true) if you need + # on-device conversion measurement. + # See: https://developers.google.com/google-ads/api/docs/conversions/upload-identifiers if defined?($RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion) && ($RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion == true) - Pod::UI.puts "#{s.name}: GoogleAdsOnDeviceConversion pod added" - s.dependency 'GoogleAdsOnDeviceConversion' + if defined?(spm_dependency) && !rnfirebase_spm_disabled? + Pod::UI.warn "#{s.name}: GoogleAdsOnDeviceConversion is not available in SPM mode. " \ + "Set $RNFirebaseDisableSPM = true in your Podfile to use this feature." + else + Pod::UI.puts "#{s.name}: GoogleAdsOnDeviceConversion pod added" + s.dependency 'GoogleAdsOnDeviceConversion' + end end if defined?($RNFirebaseAsStaticFramework) diff --git a/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckModule.m b/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckModule.m index c1706d6b5c..ec7f5a142a 100644 --- a/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckModule.m +++ b/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckModule.m @@ -60,8 +60,7 @@ + (instancetype)sharedInstance { : (BOOL)isTokenAutoRefreshEnabled : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) { - DLog(@"deprecated API, provider will be deviceCheck / token refresh %d for " - @"app %@", + DLog(@"deprecated API, provider will be deviceCheck / token refresh %d for app %@", isTokenAutoRefreshEnabled, firebaseApp.name); [[RNFBAppCheckModule sharedInstance].providerFactory configure:firebaseApp providerName:@"deviceCheck" @@ -94,8 +93,8 @@ + (instancetype)sharedInstance { appCheck.isTokenAutoRefreshEnabled = isTokenAutoRefreshEnabled; } -// Not present in JS or Android - it is iOS-specific so we only call this in -// testing - it is not in index.d.ts +// Not present in JS or Android - it is iOS-specific so we only call this in testing - it is not in +// index.d.ts RCT_EXPORT_METHOD(isTokenAutoRefreshEnabled : (FIRApp *)firebaseApp : (RCTPromiseResolveBlock)resolve rejecter @@ -113,36 +112,33 @@ + (instancetype)sharedInstance { : (RCTPromiseRejectBlock)reject) { FIRAppCheck *appCheck = [FIRAppCheck appCheckWithApp:firebaseApp]; DLog(@"appName %@", firebaseApp.name); - [appCheck tokenForcingRefresh:forceRefresh - completion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { - if (error != nil) { - // Handle any errors if the token was not retrieved. - DLog(@"RNFBAppCheck - getToken - Unable to retrieve App " - @"Check token: %@", - error); - [RNFBSharedUtils - rejectPromiseWithUserInfo:reject - userInfo:(NSMutableDictionary *)@{ - @"code" : @"token-error", - @"message" : [error localizedDescription], - }]; - return; - } - if (token == nil) { - DLog(@"RNFBAppCheck - getToken - Unable to retrieve App " - @"Check token."); - [RNFBSharedUtils rejectPromiseWithUserInfo:reject - userInfo:(NSMutableDictionary *)@{ - @"code" : @"token-null", - @"message" : @"no token fetched", - }]; - return; - } - - NSMutableDictionary *tokenResultDictionary = [NSMutableDictionary new]; - tokenResultDictionary[@"token"] = token.token; - resolve(tokenResultDictionary); - }]; + [appCheck + tokenForcingRefresh:forceRefresh + completion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { + if (error != nil) { + // Handle any errors if the token was not retrieved. + DLog(@"RNFBAppCheck - getToken - Unable to retrieve App Check token: %@", error); + [RNFBSharedUtils rejectPromiseWithUserInfo:reject + userInfo:(NSMutableDictionary *)@{ + @"code" : @"token-error", + @"message" : [error localizedDescription], + }]; + return; + } + if (token == nil) { + DLog(@"RNFBAppCheck - getToken - Unable to retrieve App Check token."); + [RNFBSharedUtils rejectPromiseWithUserInfo:reject + userInfo:(NSMutableDictionary *)@{ + @"code" : @"token-null", + @"message" : @"no token fetched", + }]; + return; + } + + NSMutableDictionary *tokenResultDictionary = [NSMutableDictionary new]; + tokenResultDictionary[@"token"] = token.token; + resolve(tokenResultDictionary); + }]; } RCT_EXPORT_METHOD(getLimitedUseToken @@ -151,35 +147,32 @@ + (instancetype)sharedInstance { : (RCTPromiseRejectBlock)reject) { FIRAppCheck *appCheck = [FIRAppCheck appCheckWithApp:firebaseApp]; DLog(@"appName %@", firebaseApp.name); - [appCheck - limitedUseTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { - if (error != nil) { - // Handle any errors if the token was not retrieved. - DLog(@"RNFBAppCheck - getLimitedUseToken - Unable to retrieve App Check " - @"token: %@", - error); - [RNFBSharedUtils rejectPromiseWithUserInfo:reject - userInfo:(NSMutableDictionary *)@{ - @"code" : @"token-error", - @"message" : [error localizedDescription], - }]; - return; - } - if (token == nil) { - DLog(@"RNFBAppCheck - getLimitedUseToken - Unable to retrieve App Check " - @"token."); - [RNFBSharedUtils rejectPromiseWithUserInfo:reject - userInfo:(NSMutableDictionary *)@{ - @"code" : @"token-null", - @"message" : @"no token fetched", - }]; - return; - } - - NSMutableDictionary *tokenResultDictionary = [NSMutableDictionary new]; - tokenResultDictionary[@"token"] = token.token; - resolve(tokenResultDictionary); - }]; + [appCheck limitedUseTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, + NSError *_Nullable error) { + if (error != nil) { + // Handle any errors if the token was not retrieved. + DLog(@"RNFBAppCheck - getLimitedUseToken - Unable to retrieve App Check token: %@", error); + [RNFBSharedUtils rejectPromiseWithUserInfo:reject + userInfo:(NSMutableDictionary *)@{ + @"code" : @"token-error", + @"message" : [error localizedDescription], + }]; + return; + } + if (token == nil) { + DLog(@"RNFBAppCheck - getLimitedUseToken - Unable to retrieve App Check token."); + [RNFBSharedUtils rejectPromiseWithUserInfo:reject + userInfo:(NSMutableDictionary *)@{ + @"code" : @"token-null", + @"message" : @"no token fetched", + }]; + return; + } + + NSMutableDictionary *tokenResultDictionary = [NSMutableDictionary new]; + tokenResultDictionary[@"token"] = token.token; + resolve(tokenResultDictionary); + }]; } @end diff --git a/packages/app/.npmignore b/packages/app/.npmignore index 757d1f0ef2..be4862f316 100644 --- a/packages/app/.npmignore +++ b/packages/app/.npmignore @@ -69,5 +69,8 @@ type-test.ts scripts __tests__ -# Force include generated version file (needed for linking) +# Force include generated version file (needed for linking). +# This file is in .gitignore but must be in the published npm package. +# Note: this package does not use a "files" array in package.json, +# so .npmignore is the sole mechanism controlling published file set. !ios/RNFBApp/RNFBVersion.m diff --git a/packages/app/README.md b/packages/app/README.md index 873f65b6e9..2f16b3e597 100644 --- a/packages/app/README.md +++ b/packages/app/README.md @@ -38,7 +38,7 @@ yarn add @react-native-firebase/app ## iOS Dependency Resolution: SPM vs CocoaPods -Starting with React Native >= 0.75, `@react-native-firebase` supports **Swift Package Manager (SPM)** for resolving Firebase iOS SDK dependencies. SPM is enabled by default — no configuration needed. +Starting with React Native 0.75+, `@react-native-firebase` supports **Swift Package Manager (SPM)** for resolving Firebase iOS SDK dependencies. SPM is enabled by default when the `spm_dependency` macro is available (injected by React Native >= 0.75) — no configuration needed. ### How it works @@ -46,13 +46,15 @@ Each RNFB module uses `firebase_dependency()` (defined in `firebase_spm.rb`) to | Condition | Resolution | When to use | |-----------|-----------|-------------| -| React Native >= 0.75 and `$RNFirebaseDisableSPM` **not set** | **SPM** (default) | Dynamic linkage (`use_frameworks! :linkage => :dynamic`) | -| `$RNFirebaseDisableSPM = true` in Podfile | **CocoaPods** | Static linkage or projects that need CocoaPods-only resolution | -| React Native < 0.75 | **CocoaPods** (automatic fallback) | Older React Native versions without `spm_dependency` support | +| RN >= 0.75 and `$RNFirebaseDisableSPM` **not set** | **SPM** (default) | Dynamic linkage / pre-built RN core (`use_frameworks! :linkage => :dynamic`) | +| `$RNFirebaseDisableSPM = true` in Podfile | **CocoaPods** | Static linkage / no pre-built RN core (`use_frameworks! :linkage => :static`) | +| RN < 0.75 | **CocoaPods** (automatic fallback) | Older React Native versions without `spm_dependency` support | + +> **Note on linkage:** firebase-ios-sdk SPM products use dynamic linkage. When using `use_frameworks! :linkage => :static`, each pod embeds its own copy of Firebase SPM products, causing duplicate symbol errors. Use CocoaPods mode (`$RNFirebaseDisableSPM = true`) with static linkage. ### Configuration -**Option A — SPM (default, recommended for Xcode 26+)** +#### Option A — SPM (default, recommended for Xcode 26+) No changes needed. Just make sure your Podfile uses dynamic linkage: @@ -69,7 +71,7 @@ use_frameworks! :linkage => :dynamic > This does NOT disable SPM — it only tells the Swift compiler to use implicit module discovery > (the Xcode 16 default) so transitive SPM targets are resolved automatically. -**Option B — CocoaPods only** +#### Option B — CocoaPods only Add this line at the top of your Podfile (before any `target` block): @@ -81,6 +83,52 @@ $RNFirebaseDisableSPM = true This forces all RNFB modules to use traditional `s.dependency` CocoaPods declarations. You can use either static or dynamic linkage with this option. +#### Expo + +For Expo managed projects, use `expo-build-properties` to configure linkage and Podfile directives: + +```json +// app.json +{ + "expo": { + "plugins": [ + [ + "expo-build-properties", + { + "ios": { + "useFrameworks": "dynamic" + } + } + ] + ] + } +} +``` + +To disable SPM in Expo, add a Podfile directive via a config plugin or `app.json`: + +```json +// app.json +{ + "expo": { + "plugins": [ + [ + "expo-build-properties", + { + "ios": { + "useFrameworks": "static", + "extraPods": [] + } + } + ] + ] + } +} +``` + +Then create a small [config plugin](https://docs.expo.dev/config-plugins/introduction/) to prepend +`$RNFirebaseDisableSPM = true` to the generated Podfile, or add it manually if you have ejected. + ### How to verify During `pod install`, you will see messages indicating which resolution mode is active: @@ -94,6 +142,14 @@ During `pod install`, you will see messages indicating which resolution mode is [react-native-firebase] RNFBApp: SPM disabled ($RNFirebaseDisableSPM = true), using CocoaPods for Firebase dependencies ``` +### Monorepo / pnpm notes + +The `firebase_spm.rb` helper is loaded by each RNFB podspec via `require '../app/firebase_spm'`. +This relative path assumes the standard `node_modules/@react-native-firebase/` layout. If your +package manager hoists dependencies differently (e.g., pnpm strict mode), you may need to verify +that the require path resolves correctly. The SPM URL is read from +`@react-native-firebase/app/package.json` at the location of `firebase_spm.rb`. + ## License - See [LICENSE](/LICENSE) diff --git a/packages/app/RNFBApp.podspec b/packages/app/RNFBApp.podspec index 9a24fa02f1..f787460550 100644 --- a/packages/app/RNFBApp.podspec +++ b/packages/app/RNFBApp.podspec @@ -44,6 +44,16 @@ Pod::Spec.new do |s| "DEFINES_MODULE" => "YES", } + # RNFBUtilsModule.mm uses PHAsset (Photos.framework) to resolve local asset paths. + # Not declaring this explicitly used to work by luck (CocoaPods normally relies on + # this declaration -- not Clang autolinking -- to populate OTHER_LDFLAGS), but with + # use_frameworks! each pod is a standalone dynamic framework that must resolve its + # own symbols at its own link step, so the missing declaration now surfaces as + # "Undefined symbols ... _OBJC_CLASS_$_PHAsset". iOS/macOS only -- PhotoKit doesn't + # exist on tvOS. + s.ios.frameworks = 'Photos' + s.osx.frameworks = 'Photos' + # React Native dependencies if defined?(install_modules_dependencies()) != nil install_modules_dependencies(s); @@ -62,7 +72,7 @@ Pod::Spec.new do |s| end # Firebase dependencies - firebase_dependency(s, firebase_sdk_version, ['FirebaseCore'], 'Firebase/CoreOnly') + firebase_dependency(s, firebase_sdk_version, ['FirebaseCore', 'FirebaseInstallations'], 'Firebase/CoreOnly') if defined?($RNFirebaseAsStaticFramework) Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'" diff --git a/packages/app/__tests__/firebase_spm_test.rb b/packages/app/__tests__/firebase_spm_test.rb index e56a7163e9..4fa383ec02 100644 --- a/packages/app/__tests__/firebase_spm_test.rb +++ b/packages/app/__tests__/firebase_spm_test.rb @@ -24,6 +24,12 @@ def setup if defined?(spm_dependency) Object.send(:remove_method, :spm_dependency) end + # Note: a Ruby global variable can't be "undefined" again once assigned in this + # process, so `defined?($RNFirebaseDisableSPM)` stays true for the rest of the + # suite after the first test that touches it. Resetting the value to nil (rather + # than relying on `defined?` alone) is exactly the behavior rnfirebase_spm_disabled? + # is meant to guard against, so tests below assert on it explicitly. + $RNFirebaseDisableSPM = nil end def load_firebase_spm @@ -110,6 +116,55 @@ def test_spm_multiple_products_ignores_cocoapods_extras assert_equal ['FirebaseCrashlytics'], spm_calls[0][:products] end + # ── $RNFirebaseDisableSPM semantics (must check truthiness, not defined?) ── + + def test_disable_spm_true_forces_cocoapods + Object.define_method(:spm_dependency) { |*| raise 'spm_dependency should not be called' } + + load_firebase_spm + $RNFirebaseDisableSPM = true + + 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] + end + + def test_disable_spm_false_still_uses_spm + spm_calls = [] + Object.define_method(:spm_dependency) do |spec, **kwargs| + spm_calls << { spec: spec, **kwargs } + end + + load_firebase_spm + # Config generators / Expo plugins / env-templated Podfiles may emit `false` + # rather than omitting the assignment entirely. This must NOT disable SPM. + $RNFirebaseDisableSPM = false + + spec = MockSpec.new + firebase_dependency(spec, '12.10.0', ['FirebaseAuth'], 'Firebase/Auth') + + assert_equal 0, spec.dependencies.length + assert_equal 1, spm_calls.length + assert_equal ['FirebaseAuth'], spm_calls[0][:products] + end + + def test_disable_spm_unset_uses_spm + 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') + + assert_equal 0, spec.dependencies.length + assert_equal 1, spm_calls.length + end + # ── URL from package.json ── def test_reads_spm_url_from_package_json diff --git a/packages/app/firebase_spm.rb b/packages/app/firebase_spm.rb index 51ef891c47..ef24bf6cf2 100644 --- a/packages/app/firebase_spm.rb +++ b/packages/app/firebase_spm.rb @@ -18,7 +18,14 @@ require 'json' -# Read Firebase SPM URL from app package.json (single source of truth) +RNFIREBASE_SPM_EMBED_PHASE_NAME = '[RNFB] Embed Firebase SPM Frameworks' + +# Read Firebase SPM URL from app package.json (single source of truth). +# __dir__ resolves to the directory of this file (packages/app/). +# In monorepos with hoisted dependencies or pnpm, the path from other packages +# (e.g., `require '../app/firebase_spm'`) must resolve correctly to this location. +# If your package manager hoists differently, you may need to adjust the require +# path in individual podspecs. $firebase_spm_url ||= begin app_package_path = File.join(__dir__, 'package.json') app_package = JSON.parse(File.read(app_package_path)) @@ -32,17 +39,104 @@ # 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` +# dependency resolution. You must disable SPM when using `use_frameworks! :linkage => :static` # because static frameworks cause each pod to embed Firebase SPM products, # resulting in duplicate symbol linker errors. # +# firebase-ios-sdk SPM requires dynamic linkage. There is no upstream statement +# from Google that SPM+static is supported. See: +# https://github.com/firebase/firebase-ios-sdk/blob/main/Package.swift +# (all products use .library(type: .dynamic)) +# +# Returns true only when `$RNFirebaseDisableSPM` has been explicitly set to `true`. +# +# We deliberately check the value (not just `defined?`), so that config generators, +# Expo plugins, or env-templated Podfiles that emit `$RNFirebaseDisableSPM = false` +# don't silently switch to CocoaPods. +def rnfirebase_spm_disabled? + defined?($RNFirebaseDisableSPM) && $RNFirebaseDisableSPM == true +end + +def rnfirebase_spm_embed_script + <<~'SCRIPT' + set -euo pipefail + + package_frameworks_dir="${BUILT_PRODUCTS_DIR}/PackageFrameworks" + app_frameworks_dir="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + + if [ ! -d "${package_frameworks_dir}" ]; then + exit 0 + fi + + mkdir -p "${app_frameworks_dir}" + + find "${package_frameworks_dir}" -maxdepth 1 -type d -name "*.framework" -print0 | while IFS= read -r -d '' framework; do + framework_name="$(basename "${framework}")" + destination="${app_frameworks_dir}/${framework_name}" + + if [ -e "${destination}" ]; then + continue + fi + + echo "Embedding Firebase SPM framework ${framework_name}" + rsync -av --delete \ + --filter "- Headers" \ + --filter "- PrivateHeaders" \ + --filter "- Modules" \ + "${framework}" \ + "${app_frameworks_dir}" + + if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" ] && [ "${CODE_SIGNING_REQUIRED:-}" != "NO" ] && [ "${CODE_SIGNING_ALLOWED:-}" != "NO" ]; then + /usr/bin/codesign --force --sign "${EXPANDED_CODE_SIGN_IDENTITY}" ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements "${destination}" + fi + done + SCRIPT +end + +def rnfirebase_add_spm_embed_phase(installer) + return unless defined?(SPM) + + dependencies_by_pod = SPM.instance_variable_get(:@dependencies_by_pod) || {} + firebase_spm_enabled = dependencies_by_pod.values.flatten.any? do |dependency| + dependency[:url] == $firebase_spm_url + end + return unless firebase_spm_enabled + + installer.aggregate_targets.each do |aggregate_target| + aggregate_target.user_project.native_targets.each do |target| + next unless target.respond_to?(:shell_script_build_phases) + next unless target.shell_script_build_phases.any? { |phase| phase.name == '[CP] Embed Pods Frameworks' } + + phase = target.shell_script_build_phases.find { |candidate| candidate.name == RNFIREBASE_SPM_EMBED_PHASE_NAME } + phase ||= target.new_shell_script_build_phase(RNFIREBASE_SPM_EMBED_PHASE_NAME) + + phase.shell_script = rnfirebase_spm_embed_script + phase.shell_path = '/bin/bash' + phase.always_out_of_date = '1' + phase.input_paths = ['${BUILT_PRODUCTS_DIR}/PackageFrameworks'] + phase.output_paths = ['${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}'] + end + + aggregate_target.user_project.save + end +end + +if defined?(SPM) && SPM.respond_to?(:apply_on_post_install) && !SPM.singleton_class.method_defined?(:rnfirebase_original_apply_on_post_install) + SPM.singleton_class.alias_method :rnfirebase_original_apply_on_post_install, :apply_on_post_install + + def SPM.apply_on_post_install(installer) + rnfirebase_original_apply_on_post_install(installer) + rnfirebase_add_spm_embed_phase(installer) + end +end + # @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?(spm_dependency) && !rnfirebase_spm_disabled? 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(', ')})" @@ -54,7 +148,7 @@ def firebase_dependency(spec, version, spm_products, pods) ) else if defined?(Pod) && defined?(Pod::UI) - if defined?($RNFirebaseDisableSPM) + if rnfirebase_spm_disabled? Pod::UI.puts "[react-native-firebase] #{spec.name}: ".yellow + "SPM disabled ($RNFirebaseDisableSPM = true), using CocoaPods for Firebase dependencies" elsif !defined?(spm_dependency) diff --git a/packages/crashlytics/ios_config.sh b/packages/crashlytics/ios_config.sh index ff93dd2c7f..5c88b45e53 100755 --- a/packages/crashlytics/ios_config.sh +++ b/packages/crashlytics/ios_config.sh @@ -23,12 +23,20 @@ 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 + # SPM: upload-symbols is at a known path in the SourcePackages checkout. + # BUILD_DIR is typically DerivedData/Project-hash/Build/Products — strip from /Build onward + # to get the DerivedData project root where SourcePackages lives. + SPM_CRASHLYTICS_DIR="${BUILD_DIR%Build/*}SourcePackages/checkouts/firebase-ios-sdk/Crashlytics" + SPM_UPLOAD_SYMBOLS="${SPM_CRASHLYTICS_DIR}/upload-symbols" + if [[ -x "${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}" + elif [[ -f "${SPM_UPLOAD_SYMBOLS}" ]]; then + echo "info: Exec FirebaseCrashlytics upload-symbols from SPM (chmod +x)" + chmod +x "${SPM_UPLOAD_SYMBOLS}" + "${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" + echo "warning: FirebaseCrashlytics run script not found at CocoaPods, framework, or SPM paths. Skipping dSYM upload." + echo "warning: Checked: \${PODS_ROOT}/FirebaseCrashlytics/run, \${PROJECT_DIR}/FirebaseCrashlytics.framework/run, ${SPM_UPLOAD_SYMBOLS}" fi fi diff --git a/packages/installations/ios/RNFBInstallations/RNFBInstallationsModule.m b/packages/installations/ios/RNFBInstallations/RNFBInstallationsModule.m index 69ebec034d..66a7d35efe 100644 --- a/packages/installations/ios/RNFBInstallations/RNFBInstallationsModule.m +++ b/packages/installations/ios/RNFBInstallations/RNFBInstallationsModule.m @@ -26,8 +26,6 @@ #import "RNFBApp/RNFBSharedUtils.h" #import "RNFBInstallationsModule.h" -#import "FirebaseInstallations/FIRInstallations.h" - @implementation RNFBInstallationsModule #pragma mark - #pragma mark Module Setup diff --git a/packages/ml/RNFBML.podspec b/packages/ml/RNFBML.podspec index a90e3a00cd..382c6ad854 100644 --- a/packages/ml/RNFBML.podspec +++ b/packages/ml/RNFBML.podspec @@ -47,16 +47,15 @@ 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 + # NOTE: Firebase/MLModelDownloader dependency is currently disabled. + # When re-enabled, use: + # firebase_dependency(s, firebase_sdk_version, + # ['FirebaseMLModelDownloader'], 'Firebase/MLModelDownloader') if defined?($RNFirebaseAsStaticFramework) Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'" s.static_framework = $RNFirebaseAsStaticFramework else - # raise "#{s.name}: Underlying Firebase/MLModelDownloader requires $RNFirebaseAsStaticFrameworks = true and !use_frameworks in your Podfile" s.static_framework = false end end diff --git a/packages/storage/RNFBStorage.podspec b/packages/storage/RNFBStorage.podspec index a0dced6466..382ac8b698 100644 --- a/packages/storage/RNFBStorage.podspec +++ b/packages/storage/RNFBStorage.podspec @@ -47,6 +47,13 @@ Pod::Spec.new do |s| # Firebase dependencies firebase_dependency(s, firebase_sdk_version, ['FirebaseStorage'], 'Firebase/Storage') + # RNFBStorageCommon.m uses PHAsset/PHAssetResource (Photos.framework) for local + # asset uploads. See RNFBApp.podspec for why this must be declared explicitly + # rather than relying on Clang autolinking. iOS/macOS only -- PhotoKit doesn't + # exist on tvOS. + s.ios.frameworks = 'Photos' + s.osx.frameworks = 'Photos' + if defined?($RNFirebaseAsStaticFramework) Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'" s.static_framework = $RNFirebaseAsStaticFramework diff --git a/tarballs/react-native-firebase-analytics-23.8.8.tgz b/tarballs/react-native-firebase-analytics-23.8.8.tgz new file mode 100644 index 0000000000..4721a8ebf7 Binary files /dev/null and b/tarballs/react-native-firebase-analytics-23.8.8.tgz differ diff --git a/tarballs/react-native-firebase-app-23.8.8.tgz b/tarballs/react-native-firebase-app-23.8.8.tgz new file mode 100644 index 0000000000..d0c8e638ab Binary files /dev/null and b/tarballs/react-native-firebase-app-23.8.8.tgz differ diff --git a/tarballs/react-native-firebase-crashlytics-23.8.8.tgz b/tarballs/react-native-firebase-crashlytics-23.8.8.tgz new file mode 100644 index 0000000000..8820620727 Binary files /dev/null and b/tarballs/react-native-firebase-crashlytics-23.8.8.tgz differ diff --git a/tarballs/react-native-firebase-database-23.8.8.tgz b/tarballs/react-native-firebase-database-23.8.8.tgz new file mode 100644 index 0000000000..5ac753c04a Binary files /dev/null and b/tarballs/react-native-firebase-database-23.8.8.tgz differ diff --git a/tarballs/react-native-firebase-messaging-23.8.8.tgz b/tarballs/react-native-firebase-messaging-23.8.8.tgz new file mode 100644 index 0000000000..a51de041dc Binary files /dev/null and b/tarballs/react-native-firebase-messaging-23.8.8.tgz differ