@react-native-firebase/app 26.0.0 → 26.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +35 -0
  3. package/RNFBApp.podspec +13 -2
  4. package/android/src/reactnative/java/io/invertase/firebase/app/ReactNativeFirebaseVersion.java +1 -1
  5. package/dist/module/internal/web/RNFBAppModule.js +16 -3
  6. package/dist/module/internal/web/RNFBAppModule.js.map +1 -1
  7. package/dist/module/internal/web/utils.js +6 -1
  8. package/dist/module/internal/web/utils.js.map +1 -1
  9. package/dist/module/version.js +1 -1
  10. package/dist/typescript/lib/internal/web/RNFBAppModule.d.ts.map +1 -1
  11. package/dist/typescript/lib/internal/web/utils.d.ts.map +1 -1
  12. package/dist/typescript/lib/version.d.ts +1 -1
  13. package/firebase_json.rb +11 -10
  14. package/firebase_spm.rb +1092 -0
  15. package/ios/RNFBApp/RCTConvert+FIRApp.h +4 -0
  16. package/ios/RNFBApp/RCTConvert+FIROptions.h +4 -0
  17. package/ios/RNFBApp/RNFBAppModule.mm +6 -0
  18. package/ios/RNFBApp/RNFBSharedUtils.h +4 -0
  19. package/ios/RNFBApp/RNFBVersion.m +1 -1
  20. package/lib/internal/web/RNFBAppModule.ts +16 -3
  21. package/lib/internal/web/utils.ts +6 -1
  22. package/lib/version.ts +1 -1
  23. package/package.json +9 -8
  24. package/plugin/build/index.d.ts +2 -1
  25. package/plugin/build/index.js +2 -1
  26. package/plugin/build/ios/index.d.ts +2 -1
  27. package/plugin/build/ios/index.js +3 -1
  28. package/plugin/build/ios/podfile.d.ts +4 -0
  29. package/plugin/build/ios/podfile.js +32 -0
  30. package/plugin/build/pluginConfig.d.ts +12 -0
  31. package/plugin/build/pluginConfig.js +2 -0
  32. package/plugin/src/index.ts +4 -2
  33. package/plugin/src/ios/index.ts +2 -1
  34. package/plugin/src/ios/podfile.ts +40 -0
  35. package/plugin/src/pluginConfig.ts +13 -0
  36. package/plugin/tsconfig.tsbuildinfo +1 -1
  37. package/ios/generated/RCTModuleProviders.h +0 -16
  38. package/ios/generated/RCTModuleProviders.mm +0 -52
  39. package/ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.h +0 -14
  40. package/ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.mm +0 -19
  41. package/ios/generated/ReactCodegen.podspec +0 -111
@@ -0,0 +1,1092 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics, Style/GlobalVars
4
+ #
5
+ # Copyright (c) 2016-present Invertase Limited & Contributors
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this library except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ require 'json'
21
+
22
+ RNFIREBASE_SPM_EMBED_PHASE_NAME = '[RNFB] Embed Firebase SPM Frameworks'
23
+ RNFIREBASE_SPM_SIGNATURE_FIX_PHASE_NAME = '[RNFB] Remove duplicate Firebase/Google SPM binary xcframework signature files' # rubocop:disable Layout/LineLength
24
+
25
+ # Every `.binaryTarget` xcframework name reachable in the resolved SPM package
26
+ # graph for the RNFB test app (firebase-ios-sdk 12.16.0, full module set --
27
+ # Analytics with ad support, Firestore, etc). Enumerated from
28
+ # `SourcePackages/workspace-state.json`'s `artifacts` list after a clean
29
+ # `-resolvePackageDependencies` run rather than guessed, since none of these
30
+ # show up as a reference in our own podspecs or pbxprojs (see comment on
31
+ # `rnfirebase_fix_spm_archive_signature_collision` below for why). Any of
32
+ # these can be staged into more than one target's build directory and hit the
33
+ # Archive signature-copy collision below, not just the Analytics-related
34
+ # ones we hit first.
35
+ RNFIREBASE_SPM_SIGNATURE_FIX_ARTIFACT_NAMES = %w[
36
+ GoogleAppMeasurement
37
+ GoogleAppMeasurementIdentitySupport
38
+ GoogleAdsOnDeviceConversion
39
+ FirebaseAnalytics
40
+ FirebaseFirestoreInternal
41
+ absl
42
+ grpc
43
+ grpcpp
44
+ openssl_grpc
45
+ ].freeze
46
+
47
+ # Encapsulates the SPM-related state that has to survive across CocoaPods'
48
+ # per-podspec evaluation (where `firebase_dependency` runs -- see `activate!`
49
+ # below) and the later, single `post_install` phase (where every
50
+ # `rnfirebase_*` helper in this file reads it back via `active?`/`version`/
51
+ # `url`). This works today because CocoaPods always finishes evaluating every
52
+ # podspec before running `post_install`, so a single, process-wide place to
53
+ # stash this is safe -- but wrapping the three pieces of state in a module
54
+ # (rather than three bare globals) keeps every read/write site in this file
55
+ # explicit about what it's touching, and gives `active?` a place to
56
+ # self-check for a state that should never happen instead of every
57
+ # downstream caller trusting a bare boolean blindly.
58
+ module RNFirebaseSPM
59
+ class << self
60
+ # Firebase SPM package URL, read from the app's own package.json (single
61
+ # source of truth) the first time it's needed, then cached for the rest
62
+ # of this `pod install` process.
63
+ #
64
+ # __dir__ resolves to the directory of this file (packages/app/).
65
+ # Every other podspec loads this file via `require_relative` (e.g.
66
+ # `require_relative '../app/firebase_spm'`), which resolves the path
67
+ # relative to the requiring file's own location rather than the
68
+ # process's current working directory -- so this always resolves
69
+ # correctly regardless of monorepo hoisting layout (hoisted
70
+ # dependencies, pnpm, etc.), with no adjustment needed.
71
+ def url
72
+ @url ||= begin
73
+ app_package_path = File.join(__dir__, 'package.json')
74
+ app_package = JSON.parse(File.read(app_package_path))
75
+ app_package['sdkVersions']['ios']['firebaseSpmUrl']
76
+ end
77
+ end
78
+
79
+ # Records that `firebase_dependency` (below) took the SPM path for at
80
+ # least one podspec in this install, and which Firebase SDK `version` it
81
+ # resolved with. Called once, from `firebase_dependency` itself, the
82
+ # first time it successfully takes the SPM path. `version` is stored so
83
+ # `rnfirebase_add_spm_core_to_app_target` can declare the same minimum
84
+ # version requirement on the app target's own FirebaseCore product
85
+ # dependency, without needing its own separate copy of it.
86
+ def activate!(version)
87
+ @active = true
88
+ @version = version
89
+ end
90
+
91
+ # Whether SPM is active for this install -- read by every `rnfirebase_*`
92
+ # post-install helper in this file to decide whether to act at all.
93
+ #
94
+ # Self-checks internal consistency before returning: if `@active` is
95
+ # `true` but no real `version` was ever recorded, something set the flag
96
+ # without going through `activate!` above (e.g. a future refactor that
97
+ # assigns the flag directly instead of calling it), and every downstream
98
+ # helper that trusts this return value -- including one that links
99
+ # FirebaseCore into the app target at a specific minimum version -- would
100
+ # silently operate on incomplete state instead. Raising `Pod::Informative`
101
+ # here (this file's own user-facing `pod install`-time error class, same
102
+ # as `rnfirebase_fail_if_spm_static_linkage!` below) turns that into a
103
+ # loud, immediate failure instead of a confusing downstream symptom.
104
+ def active?
105
+ return false unless @active
106
+
107
+ if @version.nil? || @version.to_s.strip.empty?
108
+ raise Pod::Informative, <<~MESSAGE
109
+ [react-native-firebase] Internal error: Firebase SPM was marked active without a recorded version -- `RNFirebaseSPM.activate!` was either never called, or was called with a nil/empty version. This indicates a bug in react-native-firebase's own Podfile integration, not a problem with your project.
110
+
111
+ Please open an issue at https://github.com/invertase/react-native-firebase/issues, including your full `pod install` output.
112
+ MESSAGE
113
+ end
114
+
115
+ true
116
+ end
117
+
118
+ # The Firebase SDK version `firebase_dependency` resolved with, recorded
119
+ # by `activate!` above -- used by `rnfirebase_add_spm_core_to_app_target`
120
+ # to declare the same minimum version requirement on the app target's own
121
+ # FirebaseCore product dependency.
122
+ attr_reader :version
123
+
124
+ # Clears active/version/url back to their unset defaults. Not needed in
125
+ # production Podfile code paths -- each `pod install` process is
126
+ # short-lived, so there's nothing to reset between installs -- but the
127
+ # test suite `load`s this file fresh for every test and needs a
128
+ # deliberate way to reset this module's state in between, rather than
129
+ # relying on Ruby's one-way `defined?` (a global variable, once assigned,
130
+ # can't become "undefined" again) the way `$RNFirebaseDisableSPM` still
131
+ # does in the test suite's `setup`.
132
+ def reset!
133
+ @active = nil
134
+ @version = nil
135
+ @url = nil
136
+ end
137
+ end
138
+ end
139
+
140
+ # Helper to declare Firebase dependencies with SPM support and CocoaPods fallback.
141
+ #
142
+ # When `spm_dependency` is available (React Native >= 0.75), it declares the
143
+ # Firebase iOS SDK as a Swift Package dependency. Otherwise, it falls back to
144
+ # the traditional CocoaPods `s.dependency` declaration.
145
+ #
146
+ # Set `$RNFirebaseDisableSPM = true` in your Podfile to force CocoaPods-only
147
+ # dependency resolution. You must disable SPM when using `use_frameworks! :linkage => :static`
148
+ # because static frameworks cause each pod to embed Firebase SPM products,
149
+ # resulting in duplicate symbol linker errors.
150
+ #
151
+ # firebase-ios-sdk SPM products are plain `.library(...)` (automatic linkage),
152
+ # not `.library(type: .dynamic)`. See:
153
+ # https://github.com/firebase/firebase-ios-sdk/blob/main/Package.swift
154
+ # Automatic products are linked statically into each consumer. RNFB SPM mode
155
+ # therefore requires `use_frameworks! :linkage => :dynamic` so the app can
156
+ # build and run; static pod linkage collides those per-pod copies at link time.
157
+ # Dynamic pods avoid the link failure but still embed a private copy per pod,
158
+ # so third-party pods cannot share one FirebaseCore/FirebaseApp with RNFB
159
+ # under SPM (use `$RNFirebaseDisableSPM = true` + CocoaPods for that).
160
+ #
161
+ # Returns true only when `$RNFirebaseDisableSPM` has been explicitly set to `true`.
162
+ #
163
+ # We deliberately check the value (not just `defined?`), so that config generators,
164
+ # Expo plugins, or env-templated Podfiles that emit `$RNFirebaseDisableSPM = false`
165
+ # don't silently switch to CocoaPods.
166
+ def rnfirebase_spm_disabled?
167
+ defined?($RNFirebaseDisableSPM) && $RNFirebaseDisableSPM == true
168
+ end
169
+
170
+ # Normalizes a build setting value that Xcode/Xcodeproj may represent as
171
+ # `nil`, a whitespace-separated `String`, or an `Array` into a plain `Array`
172
+ # safe to call `include?`/`push`/`<<` on. Settings like `OTHER_LDFLAGS` or
173
+ # `SWIFT_INCLUDE_PATHS` can legitimately arrive as any of the three depending
174
+ # on how a consumer's project/build settings were authored -- treating them
175
+ # as always-already-an-Array (e.g. a bare `||= []` default, which only
176
+ # covers the `nil` case) raises `NoMethodError` the moment a consumer's
177
+ # target already has one of these set as a `String`.
178
+ def rnfirebase_build_setting_list(current)
179
+ if current.nil? || (current.is_a?(String) && current.strip.empty?) || (current.is_a?(Array) && current.empty?)
180
+ ['$(inherited)']
181
+ elsif current.is_a?(String)
182
+ current.split
183
+ else
184
+ current.dup
185
+ end
186
+ end
187
+
188
+ def rnfirebase_spm_embed_script
189
+ <<~'SCRIPT'
190
+ set -euo pipefail
191
+
192
+ app_frameworks_dir="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
193
+ mkdir -p "${app_frameworks_dir}"
194
+
195
+ embed_frameworks_from() {
196
+ local source_dir="$1"
197
+ [ -d "${source_dir}" ] || return 0
198
+
199
+ find "${source_dir}" -maxdepth 1 -type d -name "*.framework" -print0 | while IFS= read -r -d '' framework; do
200
+ framework_name="$(basename "${framework}")"
201
+ destination="${app_frameworks_dir}/${framework_name}"
202
+
203
+ if [ -e "${destination}" ]; then
204
+ continue
205
+ fi
206
+
207
+ # Xcode's Archive action writes EVERY SKIP_INSTALL=YES product into
208
+ # ${OBJROOT}/UninstalledProducts/${PLATFORM_NAME} -- not just Swift
209
+ # Package products. With CocoaPods + `use_frameworks!` (required for
210
+ # SPM mode), that folder also contains every pod's .framework,
211
+ # including STATIC ones (Expo modules, the Pods_<target> umbrella,
212
+ # ...). Embedding a static framework into the app bundle makes App
213
+ # Store validation fail with "Invalid bundle structure ... binary
214
+ # file is not permitted". Only ever embed real dynamic libraries.
215
+ #
216
+ # Internal binary name matches the .framework folder name minus the
217
+ # extension for every CocoaPods/SPM-built framework (all this script
218
+ # handles).
219
+ binary_name="${framework_name%.framework}"
220
+ # Deliberate tradeoff: match `file -b` prose for "dynamically linked"
221
+ # rather than inspecting Mach-O magic bytes. Locale-independent in
222
+ # practice on Apple's `file`, but still a CLI string match.
223
+ if [ ! -f "${framework}/${binary_name}" ] || \
224
+ ! file -b "${framework}/${binary_name}" | grep -q 'dynamically linked'; then
225
+ echo "Skipping ${framework_name}: binary missing or not dynamically linked"
226
+ continue
227
+ fi
228
+
229
+ echo "Embedding Firebase SPM framework ${framework_name} (from ${source_dir})"
230
+ rsync -av --delete \
231
+ --filter "- Headers" \
232
+ --filter "- PrivateHeaders" \
233
+ --filter "- Modules" \
234
+ "${framework}" \
235
+ "${app_frameworks_dir}"
236
+
237
+ if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" ] && [ "${CODE_SIGNING_REQUIRED:-}" != "NO" ] && [ "${CODE_SIGNING_ALLOWED:-}" != "NO" ]; then
238
+ /usr/bin/codesign --force --sign "${EXPANDED_CODE_SIGN_IDENTITY}" ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements "${destination}"
239
+ fi
240
+ done
241
+ }
242
+
243
+ # Regular (simulator/device) builds put every Swift Package product for
244
+ # the whole scheme's dependency graph into one shared folder.
245
+ embed_frameworks_from "${BUILT_PRODUCTS_DIR}/PackageFrameworks"
246
+
247
+ # Xcode's Archive action (ONLY_ACTIVE_ARCH=NO, DEPLOYMENT_POSTPROCESSING=YES)
248
+ # never populates PackageFrameworks. It stages build products under
249
+ # ${OBJROOT}/UninstalledProducts/${PLATFORM_NAME} instead -- and that
250
+ # folder is NOT SPM-only: Archive writes every SKIP_INSTALL=YES product
251
+ # there, which under CocoaPods + use_frameworks! (required for SPM mode)
252
+ # includes every pod .framework (dynamic Firebase SPM products and static
253
+ # pods alike). Without this fallback, Archive embeds zero Firebase SPM
254
+ # frameworks and the app crashes at launch (missing-library dyld). The
255
+ # filter inside embed_frameworks_from keeps only dynamically linked
256
+ # binaries so static CocoaPods frameworks are not copied into the app
257
+ # bundle (App Store "Invalid bundle structure").
258
+ embed_frameworks_from "${OBJROOT}/UninstalledProducts/${PLATFORM_NAME}"
259
+ SCRIPT
260
+ end
261
+
262
+ # Creates (or updates in place) a shell-script build phase named `name` on
263
+ # `target`, only reporting a change when something about it actually
264
+ # differs from what's already there.
265
+ #
266
+ # Without this, callers that unconditionally assign every property and then
267
+ # unconditionally set their own "did I touch anything" flag end up rewriting
268
+ # -- and re-saving -- the consumer's `.pbxproj` on every single `pod install`,
269
+ # even when the phase's script/paths are byte-for-byte identical to the last
270
+ # install. That's needless diff churn on a file consumers commit to source
271
+ # control.
272
+ #
273
+ # Returns `true` if the phase was newly created or any of its properties
274
+ # changed; `false` if it already matched and nothing was touched.
275
+ def rnfirebase_upsert_shell_script_phase!(target, name, shell_script:, shell_path:, input_paths: nil, output_paths: nil)
276
+ existing = target.shell_script_build_phases.find { |candidate| candidate.name == name }
277
+ phase = existing || target.new_shell_script_build_phase(name)
278
+
279
+ changed = existing.nil?
280
+ changed ||= phase.shell_script != shell_script
281
+ changed ||= phase.shell_path != shell_path
282
+ changed ||= phase.always_out_of_date != '1'
283
+ changed ||= !input_paths.nil? && phase.input_paths != input_paths
284
+ changed ||= !output_paths.nil? && phase.output_paths != output_paths
285
+
286
+ phase.shell_script = shell_script
287
+ phase.shell_path = shell_path
288
+ phase.always_out_of_date = '1'
289
+ phase.input_paths = input_paths if input_paths
290
+ phase.output_paths = output_paths if output_paths
291
+
292
+ changed
293
+ end
294
+
295
+ # Adds a build phase that copies Firebase's SPM-built dynamic frameworks
296
+ # into the app bundle. Runs automatically on every `pod install`/`pod update`
297
+ # -- see `rnfirebase_hook_cocoapods_post_install!` below -- so you normally
298
+ # never need to call this yourself.
299
+ #
300
+ # Only needed when Firebase is resolved via SPM (the RN >= 0.75 default) with
301
+ # dynamic linkage. It's a no-op (returns immediately) when Firebase used
302
+ # CocoaPods instead, so it's always safe to leave in your Podfile if you're
303
+ # calling it manually as a fallback (see below).
304
+ #
305
+ # Why this needs to exist at all: React Native's SPM integration
306
+ # (`spm_dependency` -> `SPM.apply_on_post_install`, in RN's own bundled
307
+ # `react_native_pods.rb`) adds Swift package product dependencies to pod
308
+ # targets, but never teaches the app target's CocoaPods embed script about
309
+ # the dynamic frameworks Xcode's SPM build produces -- so without this, apps
310
+ # crash at launch with a missing-library dyld error.
311
+ #
312
+ # If the automatic hook below ever fails to install (e.g. a future CocoaPods
313
+ # release restructures `Pod::Installer`), it prints a `pod install`-time
314
+ # warning telling you to call this explicitly instead:
315
+ #
316
+ # post_install do |installer|
317
+ # react_native_post_install(installer, ...)
318
+ # rnfirebase_add_spm_embed_phase(installer)
319
+ # end
320
+ def rnfirebase_add_spm_embed_phase(installer)
321
+ return unless RNFirebaseSPM.active?
322
+
323
+ installer.aggregate_targets.each do |aggregate_target|
324
+ project_modified = false
325
+
326
+ aggregate_target.user_project.native_targets.each do |target|
327
+ next unless target.respond_to?(:shell_script_build_phases)
328
+ next unless target.shell_script_build_phases.any? { |phase| phase.name == '[CP] Embed Pods Frameworks' }
329
+
330
+ changed = rnfirebase_upsert_shell_script_phase!(
331
+ target,
332
+ RNFIREBASE_SPM_EMBED_PHASE_NAME,
333
+ shell_script: rnfirebase_spm_embed_script,
334
+ shell_path: '/bin/bash',
335
+ input_paths: ['${BUILT_PRODUCTS_DIR}/PackageFrameworks'],
336
+ output_paths: ['${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}']
337
+ )
338
+ project_modified ||= changed
339
+ end
340
+
341
+ # Only rewrite the pbxproj when we actually created or changed a phase --
342
+ # avoids an unconditional save (and consumer-visible pbxproj diff) on
343
+ # every single `pod install`, even when nothing about the phase differs
344
+ # from the last install.
345
+ aggregate_target.user_project.save if project_modified
346
+ end
347
+ end
348
+
349
+ # Safety net for `rnfirebase_add_spm_embed_phase`: confirms its embed build
350
+ # phase actually landed on every target that needs it, immediately after it
351
+ # runs. "Needs it" uses the exact same target-selection criterion
352
+ # `rnfirebase_add_spm_embed_phase` itself uses (SPM active, target has a
353
+ # `'[CP] Embed Pods Frameworks'` phase) -- so this is only ever checking
354
+ # targets that function was actually supposed to have touched.
355
+ #
356
+ # Without this, a future Xcodeproj/Xcode-project shape that
357
+ # `rnfirebase_add_spm_embed_phase` doesn't anticipate could leave it
358
+ # silently doing nothing for a target, with no `pod install`-time signal --
359
+ # the first sign of trouble would be that app crashing at launch with a
360
+ # missing-library dyld error, with nothing pointing back at this file.
361
+ #
362
+ # Raises `Pod::Informative` -- CocoaPods' own user-facing error class,
363
+ # same as `rnfirebase_fail_if_spm_static_linkage!` -- rather than warning
364
+ # and continuing like the softer checks in this file: there's no safe
365
+ # fallback for "the app you're about to build will crash at launch," so
366
+ # `pod install` itself must fail loudly here instead.
367
+ def rnfirebase_verify_spm_embed_phase_applied!(installer)
368
+ return unless RNFirebaseSPM.active?
369
+
370
+ missing_target_names = []
371
+
372
+ installer.aggregate_targets.each do |aggregate_target|
373
+ aggregate_target.user_project.native_targets.each do |target|
374
+ next unless target.respond_to?(:shell_script_build_phases)
375
+ next unless target.shell_script_build_phases.any? { |phase| phase.name == '[CP] Embed Pods Frameworks' }
376
+ next if target.shell_script_build_phases.any? { |phase| phase.name == RNFIREBASE_SPM_EMBED_PHASE_NAME }
377
+
378
+ missing_target_names << target.name
379
+ end
380
+ end
381
+
382
+ return if missing_target_names.empty?
383
+
384
+ raise Pod::Informative, <<~MESSAGE
385
+ [react-native-firebase] Failed to add the Firebase SPM embed build phase to target(s): #{missing_target_names.join(', ')}.
386
+
387
+ Without it, this app will crash at launch with a missing-library dyld error, because Firebase's Swift Package frameworks never get copied into the app bundle.
388
+
389
+ Add `rnfirebase_add_spm_embed_phase(installer)` to your Podfile's post_install block as a fallback, then run `pod install` again. # rubocop:disable Layout/LineLength
390
+ MESSAGE
391
+ end
392
+
393
+ # Fails `pod install` fast, with a clear explanation, when RNFB's SPM mode is
394
+ # combined with static linkage (`use_frameworks! :linkage => :static`) --
395
+ # instead of letting consumers hit a confusing "duplicate symbols for
396
+ # architecture ..." linker error later, at Xcode build time, with no obvious
397
+ # link back to the Podfile setting that caused it.
398
+ #
399
+ # firebase-ios-sdk's SPM products are automatic libraries (plain `.library(...)`,
400
+ # not `type: .dynamic`) -- see
401
+ # https://github.com/firebase/firebase-ios-sdk/blob/main/Package.swift -- so
402
+ # each pod that resolves Firebase via SPM statically embeds its own copy.
403
+ # Under static pod linkage those copies collide when the app links
404
+ # (`duplicate symbol`). Under dynamic pod linkage the app builds, but each
405
+ # dynamic framework still keeps a private copy (runtime class duplication /
406
+ # split FirebaseApp registries). There is no supported combination of RNFB's
407
+ # SPM mode with static linkage; the fix is always to switch to dynamic
408
+ # linkage or opt out of SPM entirely.
409
+ #
410
+ # Raises `Pod::Informative` -- CocoaPods' own user-facing error class, which
411
+ # `pod install` prints as a plain, readable message with no Ruby backtrace --
412
+ # rather than warning-and-continuing like the other checks in this file.
413
+ def rnfirebase_fail_if_spm_static_linkage!(installer)
414
+ return unless RNFirebaseSPM.active?
415
+
416
+ # `AggregateTarget#build_as_static?` (and `#build_type` it delegates to) is
417
+ # NOT what it sounds like: CocoaPods always constructs the aggregate
418
+ # "Pods-<target>" umbrella target itself as `static_framework`/
419
+ # `static_library` -- see `Installer::Analyzer#generate_aggregate_target`,
420
+ # which hardcodes `target_definition.uses_frameworks? ? BuildType.static_framework
421
+ # : BuildType.static_library` regardless of `use_frameworks!`'s `:linkage`.
422
+ # Checking it here made this fail unconditionally for every SPM install,
423
+ # dynamic linkage included, because it's true either way.
424
+ #
425
+ # The actual `:linkage => :dynamic` vs `:static` choice from the Podfile is
426
+ # recorded on each aggregate target's own `target_definition.build_type`
427
+ # instead (`TargetDefinition#build_type`, which individual `PodTarget`s
428
+ # also inherit from via `Analyzer#determine_build_type`) -- check that.
429
+ static_targets = installer.aggregate_targets.select do |target|
430
+ target.target_definition.build_type.static?
431
+ end
432
+ return if static_targets.empty?
433
+
434
+ target_names = static_targets.map(&:name).join(', ')
435
+
436
+ raise Pod::Informative, <<~MESSAGE
437
+ [react-native-firebase] SPM + static linkage is not supported (target(s): #{target_names}).
438
+
439
+ firebase-ios-sdk's Swift Package products are automatic libraries (not `type: .dynamic`), so each react-native-firebase pod that resolves Firebase via SPM embeds its own copy. With `use_frameworks! :linkage => :static` those copies collide at link time as duplicate-symbol errors.
440
+
441
+ Fix one of the following in your Podfile, then run `pod install` again:
442
+ - Use dynamic linkage: `use_frameworks! :linkage => :dynamic`
443
+ - Opt out of SPM: set `$RNFirebaseDisableSPM = true` before any target block, then use the static or dynamic linkage your project requires.
444
+ MESSAGE
445
+ end
446
+
447
+ # CocoaPods `TargetUUIDGenerator` replaces `@generated_uuids` with leftover
448
+ # `@available_uuids` before Podfile `post_install`. RN's SPM integration then
449
+ # calls `project.new` for Firebase package product deps; with an empty/short
450
+ # counter that restarts near index 0 and **overwrites** `rootObject`
451
+ # (`PBXProject` at `PREFIX0000000`). Xcode 26 then refuses to open Pods
452
+ # (`-[XCSwiftPackageProductDependency _setSavedArchiveVersion:]`) and the
453
+ # workspace builds only SPM app targets — bridging headers can't see RNFB
454
+ # frameworks (reproduced on RN 0.85.3 × prebuilt RNCore). Raise the sequential
455
+ # UUID high-water mark to past every existing CocoaPods-format object before
456
+ # any `post_install` body (including `react_native_post_install` → SPM) runs.
457
+ def rnfirebase_ensure_pods_uuid_counter_safe!(installer)
458
+ project = installer.pods_project
459
+ return unless project
460
+
461
+ prefix = project.instance_variable_get(:@uuid_prefix)
462
+ return unless prefix.is_a?(String) && prefix.length >= 6
463
+
464
+ pfx = prefix[0, 6]
465
+ max_idx = -1
466
+ project.objects_by_uuid.each_key do |uuid|
467
+ next unless uuid.is_a?(String) && uuid.length == 14 && uuid.start_with?(pfx) && uuid.end_with?('0')
468
+
469
+ idx = uuid[6, 7].to_i(16)
470
+ max_idx = idx if idx > max_idx
471
+ end
472
+ return if max_idx.negative?
473
+
474
+ generated = project.instance_variable_get(:@generated_uuids)
475
+ generated = [] unless generated.is_a?(Array)
476
+ already_high = generated.size > max_idx
477
+ # CocoaPods UUID layout (prefix + 7 hex digits + trailing 0) — positional tokens are intentional.
478
+ generated << format('%.6s%07X0', prefix, generated.size) while generated.size <= max_idx # rubocop:disable Style/FormatStringToken
479
+ project.instance_variable_set(:@generated_uuids, generated)
480
+ project.instance_variable_set(:@available_uuids, [])
481
+
482
+ # Only log when SPM is active and we actually padded -- non-SPM installs
483
+ # still get the counter raise (cheap insurance) but must not spam every
484
+ # `pod install` with a success line.
485
+ return unless !already_high && defined?(Pod::UI) && RNFirebaseSPM.active?
486
+
487
+ Pod::UI.puts '[react-native-firebase] Raised CocoaPods Pods UUID counter ' \
488
+ "past index #{max_idx} before RN SPM mutates Pods.xcodeproj."
489
+ end
490
+
491
+ # Hard integrity check for the UUID-collision failure class above -- mirrors
492
+ # `rnfirebase_verify_spm_embed_phase_applied!` (soft attempt, then fail closed).
493
+ # After RN's SPM `post_install` mutates Pods.xcodeproj, the Pods project's
494
+ # `rootObject` UUID must still resolve to the same `PBXProject` instance. If a
495
+ # later `project.new` reused `PREFIX0000000`, `objects_by_uuid` holds a
496
+ # different object at that UUID while `@root_object` still points at the
497
+ # original `PBXProject`; saving then writing that UUID as `rootObject` leaves
498
+ # Xcode unable to open Pods.
499
+ #
500
+ # Raises `Pod::Informative` rather than warning-and-continuing: there is no
501
+ # safe fallback once the project graph is corrupted.
502
+ def rnfirebase_verify_pods_project_uuid_integrity!(installer)
503
+ return unless RNFirebaseSPM.active?
504
+ return unless installer.respond_to?(:pods_project)
505
+
506
+ project = installer.pods_project
507
+ return unless project
508
+ return unless project.respond_to?(:root_object) && project.respond_to?(:objects_by_uuid)
509
+
510
+ root = project.root_object
511
+ resolved = root && project.objects_by_uuid[root.uuid]
512
+ return if root &&
513
+ resolved.equal?(root) &&
514
+ resolved.respond_to?(:isa) &&
515
+ resolved.isa == 'PBXProject'
516
+
517
+ raise Pod::Informative, <<~MESSAGE
518
+ [react-native-firebase] Pods.xcodeproj rootObject / PBXProject UUID integrity check failed after post_install.
519
+
520
+ CocoaPods' sequential UUID counter was likely reset before React Native's SPM integration called `project.new`, overwriting the Pods `PBXProject` (`rootObject`). Xcode then refuses to open Pods (e.g. `-[XCSwiftPackageProductDependency _setSavedArchiveVersion:]`), and bridging headers cannot see React Native Firebase frameworks.
521
+
522
+ Delete `ios/Pods` and `ios/Podfile.lock`, upgrade `@react-native-firebase/app`, then run `pod install` again. If this persists, report it with your React Native and CocoaPods versions.
523
+ MESSAGE
524
+ end
525
+
526
+ # Hooks CocoaPods itself (not React Native) so `rnfirebase_add_spm_embed_phase`
527
+ # runs automatically on every `pod install`/`pod update`, without requiring
528
+ # any Podfile change from consumers.
529
+ #
530
+ # We wrap `Pod::Installer#run_podfile_post_install_hooks` -- the method
531
+ # CocoaPods calls, unconditionally, on every install (it's what runs the
532
+ # Podfile's own `post_install do |installer| ... end` block, if any, but the
533
+ # *wrapper* method itself always runs even when the Podfile defines no
534
+ # `post_install` at all). This is the same point in the install lifecycle
535
+ # where consumers previously called `rnfirebase_add_spm_embed_phase`
536
+ # manually, so behavior is unchanged -- only *how* it gets invoked differs.
537
+ #
538
+ # This file is `require`d from each RNFB podspec, which CocoaPods evaluates
539
+ # early, during dependency resolution (itself one of the first steps inside
540
+ # `Installer#install!`). That's early enough for the patch installed here to
541
+ # affect the *later*, fresh call to `run_podfile_post_install_hooks` made
542
+ # further down in that same `install!` run.
543
+ #
544
+ # Why hook CocoaPods instead of React Native: `Pod::Installer` is a stable,
545
+ # semantically-versioned public class that the wider CocoaPods plugin
546
+ # ecosystem already depends on directly, and its shape hasn't materially
547
+ # changed in years. That makes it a meaningfully safer patch target than
548
+ # RN's private, unversioned `react-native/scripts/cocoapods/spm.rb` helper,
549
+ # which isn't part of any documented RN contract. If CocoaPods ever
550
+ # renames/removes this method, the guards below no-op instead of raising,
551
+ # and print a `pod install`-time warning (a visible integration error,
552
+ # rather than a silent runtime dyld crash) telling you to call
553
+ # `rnfirebase_add_spm_embed_phase(installer)` from your own Podfile as a
554
+ # fallback.
555
+ #
556
+ # `installer_class` is only ever overridden by tests -- there's no real
557
+ # `Pod::Installer` outside of a full CocoaPods environment.
558
+ def rnfirebase_hook_cocoapods_post_install!(installer_class = (Pod::Installer if defined?(Pod::Installer)))
559
+ hook_method = :run_podfile_post_install_hooks
560
+ original_method = :rnfirebase_original_run_podfile_post_install_hooks
561
+
562
+ unless installer_class
563
+ if defined?(Pod::UI)
564
+ Pod::UI.warn '[react-native-firebase] `Pod::Installer` isn\'t defined -- automatic Firebase SPM setup ' \
565
+ '(dynamic framework embedding, etc.) was not hooked into `pod install`. Add ' \
566
+ '`rnfirebase_add_spm_embed_phase(installer)` to your Podfile\'s post_install block as a fallback.'
567
+ end
568
+ return
569
+ end
570
+
571
+ was_private = installer_class.private_method_defined?(hook_method)
572
+ unless was_private || installer_class.method_defined?(hook_method)
573
+ if defined?(Pod::UI)
574
+ Pod::UI.warn "[react-native-firebase] `Pod::Installer##{hook_method}` doesn't exist (a CocoaPods " \
575
+ 'release may have renamed or removed it) -- automatic Firebase SPM setup was not hooked into ' \
576
+ '`pod install`. Add `rnfirebase_add_spm_embed_phase(installer)` to your Podfile\'s post_install ' \
577
+ 'block as a fallback.'
578
+ end
579
+ return
580
+ end
581
+
582
+ # Already hooked -- e.g. a second RNFB podspec also `require`d this same
583
+ # file within one `pod install` process (this file is required by path,
584
+ # and a hoisted/symlinked dependency layout can resolve to the "same"
585
+ # file more than once). This is expected and idempotent, not a failure --
586
+ # every multi-podspec RNFB install hits this exact path -- so it's
587
+ # deliberately silent rather than warning on every normal install.
588
+ return if installer_class.method_defined?(original_method) || installer_class.private_method_defined?(original_method)
589
+
590
+ installer_class.class_eval do
591
+ alias_method original_method, hook_method
592
+
593
+ define_method(hook_method) do
594
+ # Deliberately not wrapped in a rescue-and-warn like the checks below:
595
+ # there's no working fallback for this combination, so letting `pod
596
+ # install` continue would only delay the same failure to Xcode's
597
+ # build/link step, with a far more confusing error and no pointer back
598
+ # to the actual misconfiguration.
599
+ rnfirebase_fail_if_spm_static_linkage!(self)
600
+ # Soft ensure (warn on unexpected errors) -- paired with the hard
601
+ # `rnfirebase_verify_pods_project_uuid_integrity!` after original
602
+ # post_install, same pattern as embed-phase add + verify below.
603
+ begin
604
+ rnfirebase_ensure_pods_uuid_counter_safe!(self)
605
+ rescue StandardError => e
606
+ if defined?(Pod::UI)
607
+ Pod::UI.warn '[react-native-firebase] Couldn\'t raise Pods UUID counter before ' \
608
+ "RN SPM (#{e.class}: #{e.message}). If `pod install` leaves Pods.xcodeproj " \
609
+ 'damaged (missing PBXProject / Xcode `_setSavedArchiveVersion`), upgrade ' \
610
+ 'react-native-firebase or patch CocoaPods UUID generation.'
611
+ end
612
+ end
613
+ result = send(original_method)
614
+ # Deliberately not rescued: if RN SPM overwrote `rootObject`, continuing
615
+ # would only delay the failure to Xcode with a worse diagnostic.
616
+ rnfirebase_verify_pods_project_uuid_integrity!(self)
617
+ begin
618
+ rnfirebase_add_spm_embed_phase(self)
619
+ rescue StandardError => e
620
+ if defined?(Pod::UI)
621
+ Pod::UI.warn "[react-native-firebase] Couldn't embed Firebase SPM frameworks " \
622
+ "automatically (#{e.class}: #{e.message}). Add `rnfirebase_add_spm_embed_phase(installer)` " \
623
+ 'to your Podfile\'s post_install block as a fallback.'
624
+ end
625
+ end
626
+ # Deliberately outside the `rescue` above, and not itself wrapped in a
627
+ # rescue-and-warn like the softer checks in this method: dynamic
628
+ # framework embedding is load-bearing (without it, the app crashes at
629
+ # launch with a missing-library dyld error), so if the phase still
630
+ # isn't actually on a target that needs it after the call above --
631
+ # whether that call raised, silently no-opped, or only partially
632
+ # applied -- this must abort `pod install` with a clear message
633
+ # rather than let a broken install continue, the same way
634
+ # `rnfirebase_fail_if_spm_static_linkage!` above is deliberately not
635
+ # rescued either.
636
+ rnfirebase_verify_spm_embed_phase_applied!(self)
637
+ begin
638
+ rnfirebase_add_spm_core_to_app_target(self)
639
+ rescue StandardError => e
640
+ if defined?(Pod::UI)
641
+ Pod::UI.warn "[react-native-firebase] Couldn't link FirebaseCore into the app target " \
642
+ "automatically (#{e.class}: #{e.message}). " \
643
+ 'Add `rnfirebase_add_spm_core_to_app_target(installer)` ' \
644
+ 'to your Podfile\'s post_install block as a fallback if your own native code calls ' \
645
+ 'FIRApp/FIROptions APIs directly.'
646
+ end
647
+ end
648
+ begin
649
+ rnfirebase_remove_spm_core_from_app_target(self)
650
+ rescue StandardError => e
651
+ if defined?(Pod::UI)
652
+ Pod::UI.warn "[react-native-firebase] Couldn't remove a stale FirebaseCore SPM link from the " \
653
+ "app target automatically (#{e.class}: #{e.message}). If you previously used SPM and have " \
654
+ 'since set `$RNFirebaseDisableSPM = true`, remove the "firebase-ios-sdk" Swift Package ' \
655
+ 'dependency from your app target manually in Xcode.'
656
+ end
657
+ end
658
+ begin
659
+ rnfirebase_fix_spm_archive_signature_collision(self)
660
+ rescue StandardError => e
661
+ if defined?(Pod::UI)
662
+ Pod::UI.warn '[react-native-firebase] Couldn\'t add the Firebase/Google SPM binary ' \
663
+ "xcframework signature workaround automatically (#{e.class}: #{e.message}). If your " \
664
+ 'Release archive fails with `"...xcframework-ios.signature" couldn\'t be copied to ' \
665
+ '"Signatures" because an item with the same name already exists`, add a Run Script ' \
666
+ 'build phase to your app target that runs `rm -f ' \
667
+ '"\\${CONFIGURATION_BUILD_DIR}"/<TheNameFromTheErrorMessage>.xcframework-ios.signature`.'
668
+ end
669
+ end
670
+ begin
671
+ rnfirebase_apply_spm_build_settings(self)
672
+ rescue StandardError => e
673
+ if defined?(Pod::UI)
674
+ Pod::UI.warn "[react-native-firebase] Couldn't apply Firebase SPM build settings " \
675
+ "automatically (#{e.class}: #{e.message}). " \
676
+ 'Add `rnfirebase_apply_spm_build_settings(installer)` ' \
677
+ 'to your Podfile\'s post_install block as a fallback if Release builds crash at launch with ' \
678
+ 'missing FIRComponent registrations, or Xcode reports that a Firebase module such as ' \
679
+ '`FirebaseCoreInternal`/`FirebaseSharedSwift` cannot be resolved.'
680
+ end
681
+ end
682
+ result
683
+ end
684
+ end
685
+ installer_class.send(:private, hook_method) if was_private
686
+ rescue StandardError => e
687
+ if defined?(Pod::UI)
688
+ Pod::UI.warn "[react-native-firebase] Couldn't hook CocoaPods to auto-embed Firebase SPM " \
689
+ "frameworks (#{e.class}: #{e.message}). Add `rnfirebase_add_spm_embed_phase(installer)` " \
690
+ 'to your Podfile\'s post_install block as a fallback.'
691
+ end
692
+ end
693
+
694
+ # Adds a direct SPM product dependency on `FirebaseCore` to the *app's own*
695
+ # native target(s) -- not just RNFB's pod targets. Runs automatically on every
696
+ # `pod install`/`pod update` alongside `rnfirebase_add_spm_embed_phase` -- see
697
+ # `rnfirebase_hook_cocoapods_post_install!` below -- so you normally never
698
+ # need to call this yourself.
699
+ #
700
+ # Why this needs to exist: every react-native-firebase app is required to
701
+ # `import Firebase` and call `FirebaseApp.configure()` (Swift) /
702
+ # `[FIRApp configure]` (Objective-C) itself -- this isn't an optional pattern
703
+ # for a secondary app instance, it's a strict requirement for all RNFB apps.
704
+ # With CocoaPods-only Firebase dependency resolution, every RNFB pod declares
705
+ # a regular `s.dependency 'Firebase/CoreOnly'`, and CocoaPods automatically
706
+ # propagates the resulting framework/header search paths all the way up to
707
+ # the app's own target -- so that required `FIRApp configure` call has always
708
+ # been able to link against FirebaseCore for free, without the app declaring
709
+ # anything itself. Xcode's own SPM package product dependencies don't
710
+ # propagate the same way: each target needs its own *explicit* product
711
+ # dependency in order to link a package product. Without this, apps using
712
+ # SPM+dynamic linkage fail at Archive time with "Undefined symbols ...
713
+ # _OBJC_CLASS_$_FIRApp", even though the same code links fine under
714
+ # CocoaPods-only resolution.
715
+ def rnfirebase_add_spm_core_to_app_target(installer)
716
+ return unless RNFirebaseSPM.active?
717
+
718
+ pkg_class = Xcodeproj::Project::Object::XCRemoteSwiftPackageReference
719
+ ref_class = Xcodeproj::Project::Object::XCSwiftPackageProductDependency
720
+
721
+ installer.aggregate_targets.each do |aggregate_target|
722
+ project = aggregate_target.user_project
723
+ project_modified = false
724
+
725
+ project.native_targets.each do |target|
726
+ next unless target.respond_to?(:package_product_dependencies)
727
+ next unless target.respond_to?(:shell_script_build_phases)
728
+ next unless target.shell_script_build_phases.any? { |phase| phase.name == '[CP] Embed Pods Frameworks' }
729
+
730
+ # A `FirebaseCore` product dependency already being declared on the
731
+ # target does *not* by itself mean this target is a genuine no-op:
732
+ # a pre-fix RNFB version could have committed that dependency into
733
+ # the consumer's `.pbxproj` without ever linking it (see the
734
+ # PBXBuildFile comment below) -- that's the exact broken state #9158
735
+ # reports, and it's already sitting in every affected consumer's
736
+ # project today. So checking `package_product_dependencies` alone
737
+ # can't tell that already-affected state apart from a healthy,
738
+ # already-linked one -- it has to check the build phase itself.
739
+ existing_ref = target.package_product_dependencies.find { |dep| dep.product_name == 'FirebaseCore' }
740
+ if existing_ref
741
+ next if target.frameworks_build_phase.files.any? { |bf| bf.product_ref == existing_ref }
742
+
743
+ # Healing path: reuse the dependency (and its package reference)
744
+ # that's already declared -- only the link (PBXBuildFile) is
745
+ # missing, so don't create a duplicate dependency/package reference.
746
+ ref = existing_ref
747
+ else
748
+ pkg = project.root_object.package_references.find do |candidate|
749
+ candidate.instance_of?(pkg_class) && candidate.repositoryURL == RNFirebaseSPM.url
750
+ end
751
+ unless pkg
752
+ pkg = project.new(pkg_class)
753
+ pkg.repositoryURL = RNFirebaseSPM.url
754
+ pkg.requirement = { kind: 'upToNextMajorVersion', minimumVersion: RNFirebaseSPM.version }
755
+ project.root_object.package_references << pkg
756
+ end
757
+
758
+ ref = project.new(ref_class)
759
+ ref.package = pkg
760
+ ref.product_name = 'FirebaseCore'
761
+ target.package_product_dependencies << ref
762
+ end
763
+
764
+ if defined?(Pod::UI)
765
+ message = if existing_ref
766
+ 'Repairing FirebaseCore SPM link on the app target (dependency was already declared but ' \
767
+ 'never linked) so native code that calls FIRApp/FIROptions APIs directly can resolve ' \
768
+ 'those symbols.'
769
+ else
770
+ 'Linking FirebaseCore directly into the app target (SPM) so native code that calls ' \
771
+ 'FIRApp/FIROptions APIs directly can resolve those symbols.'
772
+ end
773
+ Pod::UI.puts "[react-native-firebase] #{target.name}: ".yellow + message
774
+ end
775
+
776
+ # Declaring the product dependency (above, or in a prior install for
777
+ # the healing path) only tells Xcode the target *depends* on it -- it
778
+ # doesn't actually link it. Linking a package product (same as
779
+ # CocoaPods pod dependencies, and the same as adding
780
+ # one via Xcode's own "Frameworks, Libraries, and Embedded Content" UI)
781
+ # requires a matching PBXBuildFile, with its product_ref pointed at
782
+ # this same dependency, in the target's Frameworks build phase. Without
783
+ # this, the app fails at the link step with "Undefined symbols ...
784
+ # _OBJC_CLASS_$_FIRApp" for any native code that calls FIRApp/FIROptions
785
+ # directly (e.g. Expo's generated AppDelegate) -- even though `pod
786
+ # install` itself appears to succeed.
787
+ build_file = project.new(Xcodeproj::Project::Object::PBXBuildFile)
788
+ build_file.product_ref = ref
789
+ target.frameworks_build_phase.files << build_file
790
+
791
+ target.build_configurations.each do |config|
792
+ build_settings = target.build_settings(config.name)
793
+ # Normalize first: Xcode/Xcodeproj may already represent
794
+ # SWIFT_INCLUDE_PATHS as a whitespace-separated String rather than an
795
+ # Array, depending on how the consumer's project was authored. A bare
796
+ # `||= ['$(inherited)']` only covers the nil case -- calling `.push`
797
+ # on an existing String value raises NoMethodError and crashes `pod
798
+ # install` for that target.
799
+ paths = rnfirebase_build_setting_list(build_settings['SWIFT_INCLUDE_PATHS'])
800
+ search_path = '${SYMROOT}/${CONFIGURATION}${EFFECTIVE_PLATFORM_NAME}/'
801
+ paths << search_path unless paths.include?(search_path)
802
+ build_settings['SWIFT_INCLUDE_PATHS'] = paths
803
+ end
804
+
805
+ project_modified = true
806
+ end
807
+
808
+ project.save if project_modified
809
+ end
810
+ end
811
+
812
+ # Undoes `rnfirebase_add_spm_core_to_app_target` -- removes the direct SPM
813
+ # `FirebaseCore` product dependency (and, once nothing else references it,
814
+ # the "firebase-ios-sdk" package reference itself) from the app's own native
815
+ # target(s). Runs automatically on every `pod install`/`pod update` alongside
816
+ # `rnfirebase_add_spm_core_to_app_target` -- see
817
+ # `rnfirebase_hook_cocoapods_post_install!` above -- so you normally never
818
+ # need to call this yourself.
819
+ #
820
+ # Why this needs to exist: `rnfirebase_add_spm_core_to_app_target` writes into
821
+ # the *app's own* Xcode project (`aggregate_target.user_project`, e.g.
822
+ # `testing.xcodeproj`) -- a different project than the one React Native's own
823
+ # SPM integration manages (`installer.pods_project`, i.e. `Pods.xcodeproj`).
824
+ # RN's `SPMManager#clean_spm_dependencies_from_target` (in
825
+ # `react-native/scripts/cocoapods/spm.rb`) only ever clears package
826
+ # references from `pods_project` on every `pod install` -- it has no
827
+ # knowledge of, and never touches, the app-project-level reference added
828
+ # above. So once SPM has been active at least once and the resulting
829
+ # `FirebaseCore` product dependency has been committed into the app's
830
+ # `.pbxproj` (as it normally would be), switching to
831
+ # `$RNFirebaseDisableSPM = true` and reinstalling left that stale SPM wiring
832
+ # in place forever: the app target ended up simultaneously linked against
833
+ # Xcode's SPM-resolved `firebase-ios-sdk` package graph *and* the freshly
834
+ # CocoaPods-resolved `Firebase/CoreOnly` pod, and the two copies of
835
+ # Firebase's module graph collided -- surfacing as `redefinition of module
836
+ # 'Firebase'` at compile time, and as duplicate App-Intents-metadata build
837
+ # commands at Archive time.
838
+ def rnfirebase_remove_spm_core_from_app_target(installer)
839
+ return if RNFirebaseSPM.active?
840
+
841
+ pkg_class = Xcodeproj::Project::Object::XCRemoteSwiftPackageReference
842
+ ref_class = Xcodeproj::Project::Object::XCSwiftPackageProductDependency
843
+
844
+ installer.aggregate_targets.each do |aggregate_target|
845
+ project = aggregate_target.user_project
846
+ project_modified = false
847
+
848
+ project.native_targets.each do |target|
849
+ next unless target.respond_to?(:package_product_dependencies)
850
+
851
+ stale_refs = target.package_product_dependencies.select do |dep|
852
+ dep.instance_of?(ref_class) && dep.product_name == 'FirebaseCore' && dep.package&.repositoryURL == RNFirebaseSPM.url # rubocop:disable Layout/LineLength
853
+ end
854
+ next if stale_refs.empty?
855
+
856
+ if defined?(Pod::UI)
857
+ Pod::UI.puts "#{"[react-native-firebase] #{target.name}: ".yellow}SPM disabled -- removing the stale FirebaseCore Swift Package link left on the app target." # rubocop:disable Layout/LineLength
858
+ end
859
+
860
+ stale_refs.each do |ref|
861
+ target.package_product_dependencies.delete(ref)
862
+ # `ref.remove_from_project` below only nils out the matching PBXBuildFile's
863
+ # product_ref -- it doesn't remove the now-empty PBXBuildFile itself from the
864
+ # Frameworks build phase -- so look it up first, while product_ref is still set.
865
+ stale_build_file = target.frameworks_build_phase.files.find { |file| file.product_ref == ref }
866
+ target.frameworks_build_phase.files.delete(stale_build_file) if stale_build_file
867
+ ref.remove_from_project
868
+ end
869
+ project_modified = true
870
+ end
871
+
872
+ project.root_object.package_references
873
+ .select { |pkg| pkg.instance_of?(pkg_class) && pkg.repositoryURL == RNFirebaseSPM.url }
874
+ .each do |pkg|
875
+ next if pkg.referrers.any?(ref_class)
876
+
877
+ project.root_object.package_references.delete(pkg)
878
+ pkg.remove_from_project
879
+ project_modified = true
880
+ end
881
+
882
+ project.save if project_modified
883
+ end
884
+ end
885
+
886
+ # Works around a long-standing Xcode Archive bug (present since Xcode 15,
887
+ # still reproducing on Xcode 26) where a Swift Package binary target's
888
+ # `.signature` provenance file gets staged into more than one target's build
889
+ # directory when multiple targets in the workspace transitively depend on the
890
+ # same binary artifact. Xcode's Archive action then tries to copy every
891
+ # staged copy into the shared `<Archive>.xcarchive/Signatures/` directory,
892
+ # and the second copy collides with the first:
893
+ #
894
+ # "GoogleAppMeasurementIdentitySupport.xcframework-ios.signature" couldn't
895
+ # be copied to "Signatures" because an item with the same name already
896
+ # exists.
897
+ #
898
+ # This isn't specific to react-native-firebase -- the same class of bug, with
899
+ # the same fix, has been reported for other CocoaPods+SPM binary xcframeworks
900
+ # (Mapbox: CocoaPods/CocoaPods#12022; MapLibre: maplibre-react-native#1489;
901
+ # Lottie).
902
+ #
903
+ # It can hit *any* binary xcframework in the resolved graph, not just
904
+ # Analytics-related ones -- e.g. Google's own `google/GoogleAppMeasurement.git`
905
+ # SPM package unconditionally links `GoogleAdsOnDeviceConversion` (from the
906
+ # *separate* `googleads/google-ads-on-device-conversion-ios-sdk` package) as a
907
+ # dependency of `GoogleAppMeasurementTarget`, completely independent of
908
+ # RNFBAnalytics's own *optional* `spm_dependency` call for it (gated behind
909
+ # `$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion`, which turns
910
+ # out to only matter for CocoaPods-only resolution) -- none of that showed up
911
+ # as a reference in our own podspecs or pbxprojs; it only turned up by
912
+ # inspecting the actual checked-out Package.swift manifests under
913
+ # DerivedData/.../SourcePackages/checkouts. Confirmed locally: fixing one
914
+ # binary artifact just surfaces the collision on the next one on a subsequent
915
+ # archive run, so `RNFIREBASE_SPM_SIGNATURE_FIX_ARTIFACT_NAMES` above lists
916
+ # every `.binaryTarget` xcframework in the resolved graph (enumerated from
917
+ # `SourcePackages/workspace-state.json`, not guessed) so they're all covered
918
+ # in one pass.
919
+ #
920
+ # Deliberately scoped to this known artifact-name list rather than a bare
921
+ # `*.signature` glob -- broad enough to cover this whole binary family without
922
+ # also silently masking an unrelated, legitimate "file already exists"
923
+ # failure from some other SPM package in a consumer's own app.
924
+ def rnfirebase_fix_spm_archive_signature_collision(installer)
925
+ return unless RNFirebaseSPM.active?
926
+
927
+ installer.aggregate_targets.each do |aggregate_target|
928
+ project = aggregate_target.user_project
929
+ project_modified = false
930
+
931
+ project.native_targets.each do |target|
932
+ next unless target.respond_to?(:shell_script_build_phases)
933
+ next unless target.shell_script_build_phases.any? { |phase| phase.name == '[CP] Embed Pods Frameworks' }
934
+
935
+ rm_lines = RNFIREBASE_SPM_SIGNATURE_FIX_ARTIFACT_NAMES.map do |name|
936
+ "rm -f \"${CONFIGURATION_BUILD_DIR}\"/#{name}.xcframework-ios.signature"
937
+ end
938
+ # Trailing newline required for Xcode Run Script phase content.
939
+ shell_script = "#{rm_lines.join("\n")}\n"
940
+
941
+ changed = rnfirebase_upsert_shell_script_phase!(
942
+ target,
943
+ RNFIREBASE_SPM_SIGNATURE_FIX_PHASE_NAME,
944
+ shell_script: shell_script,
945
+ shell_path: '/bin/sh'
946
+ )
947
+ project_modified ||= changed
948
+ end
949
+
950
+ project.save if project_modified
951
+ end
952
+ end
953
+
954
+ # Applies Release/module-build-system settings that Firebase SPM + dynamic
955
+ # linkage requires on the app's own native target(s) and on the Pods
956
+ # project. Runs automatically on every `pod install`/`pod update` -- see
957
+ # `rnfirebase_hook_cocoapods_post_install!` above -- so you normally never
958
+ # need to call this yourself.
959
+ #
960
+ # 1. `-ObjC` in `OTHER_LDFLAGS` (app target, every configuration): under SPM
961
+ # + dynamic linkage, dead-code stripping can drop Objective-C
962
+ # classes/categories that are only ever discovered via runtime reflection
963
+ # rather than a direct static reference -- e.g. Firebase's
964
+ # FIRLibrary/FIRComponent registration used by RNFBCrashlyticsInitProvider
965
+ # -- which otherwise crashes the app at launch, but only in Release/
966
+ # Archive builds (a TestFlight-only failure that's hard to reproduce from
967
+ # a local Debug build). `-ObjC` forces the linker to keep any object file
968
+ # that defines an ObjC class/category, without disabling dead-code
969
+ # stripping or optimizations for anything else, so it doesn't meaningfully
970
+ # grow the binary or slow down Release builds.
971
+ #
972
+ # 2. `SWIFT_ENABLE_EXPLICIT_MODULES = 'NO'` and `CLANG_ENABLE_EXPLICIT_MODULES
973
+ # = 'NO'` (app target and Pods project, every configuration): Xcode 26
974
+ # enables explicit modules -- separately for Swift and for Clang -- by
975
+ # default, but Firebase's SPM internal targets (`FirebaseCoreInternal`,
976
+ # `FirebaseSharedSwift`) aren't exposed as public products. Explicit
977
+ # modules is a build-system-wide setting for Swift Package products
978
+ # resolved via the app's own project/scheme (SPM packages don't have
979
+ # their own toggle for it), so both settings have to be disabled on the
980
+ # app project too, not just the Pods project -- otherwise pure-Swift
981
+ # Firebase SPM products (Storage, RemoteConfig, Database, InAppMessaging)
982
+ # intermittently fail to have their generated ObjC interop header
983
+ # (*-Swift.h) available when the consuming RNFB Pods target starts
984
+ # compiling. This does NOT disable SPM -- it only makes Swift and Clang
985
+ # use implicit module discovery (the Xcode 16 default) uniformly across
986
+ # the app, CocoaPods, and SPM build boundary. See
987
+ # okf-bundle/ios-spm-native-imports.md.
988
+ #
989
+ # NOT applied (tried and reverted): `-fmodules -fcxx-modules` in
990
+ # `OTHER_CPLUSPLUSFLAGS`, to let `.mm` files use `@import FirebaseCore;`/
991
+ # `@import <ProductName>;` -- every RNFB module's Objective-C header falls
992
+ # back to that Clang module-import syntax (as opposed to a plain `#import
993
+ # <Header.h>`) when `__has_include(<ProductName/Header.h>)` fails to find a
994
+ # classic `<Module/Header.h>`-style include path. It turns out that fallback
995
+ # is never actually exercised for the app target: `rnfirebase_add_spm_core_to_app_target`
996
+ # above already links `FirebaseCore` into the app target as a direct SPM
997
+ # product dependency, and Xcode's SPM integration then adds header search
998
+ # paths for *every* product in that resolved package graph (not just
999
+ # `FirebaseCore`) to any target with at least one product dependency on it
1000
+ # -- so `__has_include(<FirebaseAppCheck/FirebaseAppCheck.h>)` (etc.)
1001
+ # already succeeds for the app target's own files, and the `@import`
1002
+ # fallback branch is dead code there. Forcing C++ modules on anyway just
1003
+ # breaks things: Xcode then tries to build Clang modules for anything the
1004
+ # app `#import`s, including React Native's own `use_frameworks!` products,
1005
+ # and several of those (`glog`, `cxxreact` -- via `folly`) aren't clean
1006
+ # under `-fcxx-modules` (e.g. "import of module 'glog.log_severity' appears
1007
+ # within namespace 'google'", "no type named 'is_dynamic' in namespace
1008
+ # 'facebook::xplat::detail'"), failing the Archive build with "could not
1009
+ # build module 'glog'"/`'cxxreact'`. Confirmed via a real `xcodebuild
1010
+ # archive`: with this flag, the build fails on React Native's own C++ pods;
1011
+ # without it, `@import` is never reached and the archive succeeds cleanly.
1012
+ def rnfirebase_apply_spm_build_settings(installer)
1013
+ return unless RNFirebaseSPM.active?
1014
+
1015
+ explicit_modules_settings = %w[SWIFT_ENABLE_EXPLICIT_MODULES CLANG_ENABLE_EXPLICIT_MODULES]
1016
+
1017
+ add_flag = lambda do |build_settings, key, flag|
1018
+ current = rnfirebase_build_setting_list(build_settings[key])
1019
+
1020
+ next false if current.include?(flag)
1021
+
1022
+ build_settings[key] = (current << flag).join(' ')
1023
+ true
1024
+ end
1025
+
1026
+ installer.aggregate_targets.each do |aggregate_target|
1027
+ project = aggregate_target.user_project
1028
+ project_modified = false
1029
+
1030
+ project.native_targets.each do |target|
1031
+ target.build_configurations.each do |config|
1032
+ ldflags_changed = add_flag.call(config.build_settings, 'OTHER_LDFLAGS', '-ObjC')
1033
+ project_modified ||= ldflags_changed
1034
+
1035
+ explicit_modules_settings.each do |setting|
1036
+ unless config.build_settings[setting] == 'NO'
1037
+ config.build_settings[setting] = 'NO'
1038
+ project_modified = true
1039
+ end
1040
+ end
1041
+ end
1042
+ end
1043
+
1044
+ project.save if project_modified
1045
+ end
1046
+
1047
+ installer.pods_project.targets.each do |target|
1048
+ target.build_configurations.each do |config|
1049
+ explicit_modules_settings.each do |setting|
1050
+ config.build_settings[setting] = 'NO'
1051
+ end
1052
+ end
1053
+ end
1054
+ end
1055
+
1056
+ rnfirebase_hook_cocoapods_post_install!
1057
+
1058
+ # @param spec [Pod::Specification] The podspec object (the `s` in podspec DSL)
1059
+ # @param version [String] Firebase SDK version (e.g., '12.10.0')
1060
+ # @param spm_products [Array<String>] SPM product names (e.g., ['FirebaseAuth'])
1061
+ # @param pods [Array<String>, String] CocoaPods dependency names with optional version
1062
+ # Can be a single string like 'Firebase/Auth' or an array like ['Firebase/Messaging', 'FirebaseCoreExtension']
1063
+ def firebase_dependency(spec, version, spm_products, pods)
1064
+ if defined?(spm_dependency) && !rnfirebase_spm_disabled?
1065
+ # Tracked ourselves (rather than inspecting RN's internal `SPM` object's
1066
+ # dependency list) so `rnfirebase_add_spm_embed_phase` doesn't depend on
1067
+ # any RN-internal state shape -- only on whether *we* ever took this path.
1068
+ RNFirebaseSPM.activate!(version)
1069
+ if defined?(Pod::UI)
1070
+ Pod::UI.puts "[react-native-firebase] #{spec.name}: ".yellow +
1071
+ "Using SPM for Firebase dependency resolution (products: #{spm_products.join(', ')})"
1072
+ end
1073
+ spm_dependency(spec,
1074
+ url: RNFirebaseSPM.url,
1075
+ requirement: { kind: 'upToNextMajorVersion', minimumVersion: version },
1076
+ products: spm_products)
1077
+ else
1078
+ if defined?(Pod::UI)
1079
+ if rnfirebase_spm_disabled?
1080
+ Pod::UI.puts "#{"[react-native-firebase] #{spec.name}: ".yellow}SPM disabled ($RNFirebaseDisableSPM = true), using CocoaPods for Firebase dependencies" # rubocop:disable Layout/LineLength
1081
+ elsif !defined?(spm_dependency)
1082
+ Pod::UI.puts "#{"[react-native-firebase] #{spec.name}: ".yellow}SPM not available (React Native < 0.75), using CocoaPods for Firebase dependencies" # rubocop:disable Layout/LineLength
1083
+ end
1084
+ end
1085
+ pods = [pods] unless pods.is_a?(Array)
1086
+ pods.each do |pod|
1087
+ spec.dependency pod, version
1088
+ end
1089
+ end
1090
+ end
1091
+
1092
+ # rubocop:enable Metrics, Style/GlobalVars