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