diff --git a/.mx/known_dependencies.json b/.mx/known_dependencies.json index ea74c9c72..6be9983b8 100644 --- a/.mx/known_dependencies.json +++ b/.mx/known_dependencies.json @@ -1,7 +1,7 @@ { - "@react-native-firebase/app": "20.1.0", - "@react-native-firebase/messaging": "20.1.0", - "@react-native-firebase/crashlytics": "20.1.0", + "@react-native-firebase/app": "21.14.0", + "@react-native-firebase/messaging": "21.14.0", + "@react-native-firebase/crashlytics": "21.14.0", "react-native-notify-kit": "10.4.7", "react-native-maps": "1.20.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 703536b0d..feee93b1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Changes + +- We updated react-native-firebase (`@react-native-firebase/app`, `@react-native-firebase/messaging` and `@react-native-firebase/crashlytics`) from v20.1.0 to v21.14.0, which uses Firebase iOS SDK v11.11.0. This resolves a CocoaPods dependency conflict with react-native-vision-camera-barcode-scanner v5 (GoogleMLKit v9.0.0) on iOS. +- We updated the Android Firebase BoM from v33.1.1 to v33.12.0 to match react-native-firebase v21.14.0. +- We made this Native Template version the minimum required version for Barcode Scanner widget v4.6.0 or above (Native Mobile Resources version 12.6.0 or above) on iOS when push notifications are enabled. Older Native Template versions will fail to build in this setup. + ## [19.1.7] - 2026-09-23 - We removed some unused Android Permissions coming in from Manifest mergers. diff --git a/android/build.gradle b/android/build.gradle index e08132800..39843172b 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -44,13 +44,10 @@ buildscript { project.ext { set('react-native', [ versions: [ - // Override Firebase BOM version to avoid updating to react-native-firebase 20.2.0+, - // which adds many new node_modules increasing the size significantly. - // Affected dependencies include the entire react-native-firebase suite e.g.: - // @react-native-firebase/app, @react-native-firebase/analytics, @react-native-firebase/crashlytics, @react-native-firebase/messaging - // NOTE: If updating to react-native-firebase 20.2.0+ is done, this override should be removed. + // Firebase BOM used by react-native-firebase and android/app/build.gradle (firebaseBomVersion). + // Keep in sync with sdkVersions.android.firebase of the @react-native-firebase/app version in .mx/known_dependencies.json. firebase: [ - bom: "33.1.1" + bom: "33.12.0" ], ], ]) diff --git a/ios/Podfile b/ios/Podfile index 30b56d1bf..0889b2f35 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -24,6 +24,7 @@ node_require('react-native/scripts/react_native_pods.rb') node_require('react-native-permissions/scripts/setup.rb') require_relative './mendix_utils' +require_relative './mlkit_simulator_support' deployment_target = "15.5" platform :ios, deployment_target @@ -155,4 +156,7 @@ post_install do |installer| end end + + # ML Kit has no arm64 simulator slice; see mlkit_simulator_support.rb. + MLKitSimulatorSupport.apply(installer) end diff --git a/ios/mlkit_simulator_support.rb b/ios/mlkit_simulator_support.rb new file mode 100644 index 000000000..8319ae059 --- /dev/null +++ b/ios/mlkit_simulator_support.rb @@ -0,0 +1,170 @@ +# Google ML Kit (pulled in by react-native-vision-camera-barcode-scanner) ships fat static +# frameworks with only an x86_64 simulator slice and an arm64 *device* slice. Linking an arm64 +# simulator build (Apple Silicon) fails with: +# building for 'iOS-simulator', but linking in object file ... built for 'iOS' +# +# A fat binary can't hold two arm64 slices, so we keep the original frameworks untouched for +# device builds and archives, and create a copy next to them ("Frameworks-simulator") whose arm64 +# objects are re-tagged as iOS-simulator (LC_BUILD_VERSION platform 2 -> 7). The generated +# xcconfigs then point FRAMEWORK_SEARCH_PATHS at "$(MLKIT_FRAMEWORKS_DIR)", which only resolves to +# the copy for sdk=iphonesimulator*, so only simulator builds ever link the patched binaries. +# +# The ML Kit podspecs also set EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64 (forcing Rosetta), +# which leaves the Pods aggregate target with no architectures on an arm64 simulator, so that +# arm64 exclusion is dropped as well unless another pod asks for it too. +require 'fileutils' + +module MLKitSimulatorSupport + POD_NAME_PATTERN = /\A(GoogleMLKit|MLKit|MLImage)/ + SIMULATOR_FRAMEWORKS_DIR = "Frameworks-simulator" + SIMULATOR_EXCLUDED_ARCHS = "EXCLUDED_ARCHS[sdk=iphonesimulator*]" + FRAMEWORKS_DIR_SETTING = "MLKIT_FRAMEWORKS_DIR" + + FAT_MAGIC = 0xcafebabe + MH_MAGIC_64 = 0xfeedfacf + CPU_TYPE_ARM64 = 0x0100000c + LC_BUILD_VERSION = 0x32 + PLATFORM_IOS = 2 + PLATFORM_IOSSIMULATOR = 7 + + module_function + + def apply(installer) + pods_root = installer.sandbox.root + pods = installer.pod_targets.map(&:pod_name).uniq.select { |name| name =~ POD_NAME_PATTERN } + pods = pods.select { |pod| create_simulator_frameworks(pods_root + pod) } + return if pods.empty? + + Pod::UI.puts "Adding arm64 simulator support for #{pods.join(', ')}".green + include_arm64 = !other_pods_exclude_arm64?(installer) + xcconfig_paths(installer).each { |path| update_xcconfig(path, pods, include_arm64) } + end + + def other_pods_exclude_arm64?(installer) + others = installer.pod_targets.reject { |target| target.pod_name =~ POD_NAME_PATTERN } + names = others.select do |target| + target.spec_consumers.any? do |consumer| + [consumer.user_target_xcconfig, consumer.pod_target_xcconfig].any? do |xcconfig| + xcconfig[SIMULATOR_EXCLUDED_ARCHS].to_s.split.include?("arm64") + end + end + end.map(&:pod_name) + return false if names.empty? + + Pod::UI.warn "#{names.join(', ')} exclude arm64 for the simulator, so arm64 simulator builds will not work." + true + end + + def create_simulator_frameworks(pod_dir) + frameworks_dir = pod_dir + "Frameworks" + return false unless frameworks_dir.directory? + + simulator_dir = pod_dir + SIMULATOR_FRAMEWORKS_DIR + FileUtils.rm_rf(simulator_dir) + FileUtils.mkdir_p(simulator_dir) + Dir.glob(frameworks_dir + "*.framework").each do |framework| + FileUtils.cp_r(framework, simulator_dir) + name = File.basename(framework, ".framework") + patch_binary(simulator_dir + "#{name}.framework" + name) + end + true + end + + def xcconfig_paths(installer) + aggregate = installer.aggregate_targets.flat_map do |target| + target.xcconfigs.keys.map { |variant| target.xcconfig_path(variant) } + end + pod_targets = installer.pod_targets.flat_map do |target| + target.build_settings.keys.map { |variant| target.xcconfig_path(variant) } + end + (aggregate + pod_targets).uniq.select { |path| File.exist?(path) } + end + + def update_xcconfig(path, pods, include_arm64) + original = File.read(path) + lines = original.lines.reject { |line| line.start_with?(FRAMEWORKS_DIR_SETTING) } + + search_paths = lines.index { |line| line.start_with?("FRAMEWORK_SEARCH_PATHS =") } + if search_paths + line = pods.reduce(lines[search_paths]) do |value, pod| + value.gsub("\"${PODS_ROOT}/#{pod}/Frameworks\"", "\"${PODS_ROOT}/#{pod}/$(#{FRAMEWORKS_DIR_SETTING})\"") + end + if line != lines[search_paths] + lines[search_paths] = line + lines << "\n" unless lines.last.end_with?("\n") + lines << "#{FRAMEWORKS_DIR_SETTING} = Frameworks\n" + lines << "#{FRAMEWORKS_DIR_SETTING}[sdk=iphonesimulator*] = #{SIMULATOR_FRAMEWORKS_DIR}\n" + end + end + + if include_arm64 + lines = lines.map do |line| + next line unless line.start_with?("#{SIMULATOR_EXCLUDED_ARCHS} =") + + archs = line.delete_prefix("#{SIMULATOR_EXCLUDED_ARCHS} =").split - ["arm64"] + archs.empty? ? nil : "#{SIMULATOR_EXCLUDED_ARCHS} = #{archs.join(' ')}\n" + end.compact + end + + File.write(path, lines.join) if lines.join != original + end + + def patch_binary(path) + buf = File.binread(path) + patched = 0 + if be32(buf, 0) == FAT_MAGIC + be32(buf, 4).times do |i| + arch = 8 + i * 20 + next unless be32(buf, arch) == CPU_TYPE_ARM64 + + start = be32(buf, arch + 8) + patched += patch_slice(buf, start, start + be32(buf, arch + 12)) + end + else + patched += patch_slice(buf, 0, buf.bytesize) + end + File.binwrite(path, buf) if patched > 0 + end + + def patch_slice(buf, start, finish) + buf.byteslice(start, 8) == "!\n" ? patch_archive(buf, start, finish) : patch_mach_o(buf, start) + end + + def patch_archive(buf, start, finish) + offset = start + 8 + patched = 0 + while offset + 60 <= finish + name = buf.byteslice(offset, 16).strip + size = buf.byteslice(offset + 48, 10).strip.to_i + data = offset + 60 + # BSD long names: "#1/" means the name is stored at the start of the member data. + data += name[3..].to_i if name.start_with?("#1/") + patched += patch_mach_o(buf, data) + offset += 60 + size + (size % 2) + end + patched + end + + def be32(buf, offset) + buf.byteslice(offset, 4).unpack("N").first + end + + def le32(buf, offset) + buf.byteslice(offset, 4).unpack("V").first + end + + def patch_mach_o(buf, offset) + return 0 unless le32(buf, offset) == MH_MAGIC_64 && le32(buf, offset + 4) == CPU_TYPE_ARM64 + + cmd_offset = offset + 32 + patched = 0 + le32(buf, offset + 16).times do + if le32(buf, cmd_offset) == LC_BUILD_VERSION && le32(buf, cmd_offset + 8) == PLATFORM_IOS + buf[cmd_offset + 8, 4] = [PLATFORM_IOSSIMULATOR].pack("V") + patched += 1 + end + cmd_offset += le32(buf, cmd_offset + 4) + end + patched + end +end