diff --git a/.changeset/tidy-tools-smile.md b/.changeset/tidy-tools-smile.md new file mode 100644 index 00000000..2d1122c1 --- /dev/null +++ b/.changeset/tidy-tools-smile.md @@ -0,0 +1,5 @@ +--- +"react-native-bottom-tabs": minor +--- + +Add the `$RNBottomTabsEnableSVG` Podfile flag, the `enableSVG` Expo plugin option, and the `RN_BOTTOM_TABS_ENABLE_SVG` environment variable for Swift Package Manager. Set the flag to `false` to remove the private CoreSVG code from Apple builds. When the flag is not set, SVG decoding stays enabled and `pod install` shows a deprecation warning. The next major version disables SVG decoding by default. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index afe3d690..f23f60a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,9 @@ jobs: - name: Typecheck files run: yarn typecheck + - name: Test SVG configuration + run: yarn workspace react-native-bottom-tabs test:ruby + swift-lint: runs-on: ubuntu-latest steps: @@ -157,8 +160,19 @@ jobs: yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" build-ios-swiftpm: + name: ${{ matrix.svg.name == 'default' && 'build-ios-swiftpm' || 'build-ios-swiftpm-svg-disabled' }} runs-on: macos-26 timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + svg: + - name: default + value: '' + - name: disabled + value: '0' + env: + RN_BOTTOM_TABS_ENABLE_SVG: ${{ matrix.svg.value }} steps: - name: Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 @@ -181,3 +195,57 @@ jobs: ARCHS=arm64 \ CODE_SIGNING_ALLOWED=NO \ build + + - name: Check that no private CoreSVG code is linked + if: matrix.svg.name == 'disabled' + run: | + app="$RUNNER_TEMP/example-swiftpm-build/Build/Products/Release-iphonesimulator/ExampleSwiftPM.app" + if find "$app" -type f -perm +111 -exec strings -a {} + | grep -E 'CoreSVGWrapper|Q0dTVkdEb2N1bWVud'; then + echo "::error::The SVG-disabled build still contains private CoreSVG code." + exit 1 + fi + + build-ios-svg-disabled: + runs-on: macos-26 + timeout-minutes: 45 + env: + RN_BOTTOM_TABS_ENABLE_SVG: '0' + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Setup + uses: ./.github/actions/setup + + - name: Build package + run: yarn build + + - name: Install cocoapods and plugins + run: | + gem install cocoapods-swift-modular-headers + cd apps/example + pod install --project-directory=ios + env: + RCT_NEW_ARCH_ENABLED: 1 + NO_FLIPPER: 1 + + - name: Build example for iOS with SVG disabled + run: | + xcodebuild \ + -workspace apps/example/ios/ReactNativeBottomTabsExample.xcworkspace \ + -scheme ReactTestApp \ + -configuration Debug \ + -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath "$RUNNER_TEMP/example-svg-disabled-build" \ + ARCHS=arm64 \ + CODE_SIGNING_ALLOWED=NO \ + build + + - name: Check that no private CoreSVG code is linked + run: | + app="$RUNNER_TEMP/example-svg-disabled-build/Build/Products/Debug-iphonesimulator/ReactTestApp.app" + if find "$app" -type f -perm +111 -exec strings -a {} + | grep -E 'CoreSVGWrapper|Q0dTVkdEb2N1bWVud'; then + echo "::error::The SVG-disabled build still contains private CoreSVG code." + exit 1 + fi diff --git a/docs/docs/docs/guides/usage-with-react-navigation.mdx b/docs/docs/docs/guides/usage-with-react-navigation.mdx index 8ff246ac..8a2eb0e2 100644 --- a/docs/docs/docs/guides/usage-with-react-navigation.mdx +++ b/docs/docs/docs/guides/usage-with-react-navigation.mdx @@ -286,6 +286,56 @@ Function that given `{ focused: boolean }` returns `ImageSource` or `AppleIcon` SF Symbols are only supported on Apple platforms. ::: +#### SVG images on Apple platforms + +On Apple platforms, this library adds an SVG decoder to React Native's image loader. The decoder handles SVG tab icons. It also handles SVG sources in every React Native `` in your app. It uses private CoreSVG APIs. Android does not use this decoder. + +:::warning +The next major version disables the SVG decoder by default. Set the flag below now to choose the behavior that you want. +::: + +To control the decoder, set `$RNBottomTabsEnableSVG` in your `Podfile`, before React Native configures pods. Then run `pod install` and rebuild the native app: + +```ruby +# Keep SVG tab icons and SVG images: +$RNBottomTabsEnableSVG = true + +# Or remove the private CoreSVG code from your app: +$RNBottomTabsEnableSVG = false +``` + +| Value | This version | Next major version | +| --- | --- | --- | +| not set | Enabled, with a warning during `pod install` | Disabled | +| `true` | Enabled | Enabled | +| `false` | Disabled. No private CoreSVG code is compiled. | Disabled | + +When the decoder is disabled, SVG sources do not show, and development builds log a warning to the JavaScript console. React Native DevTools shows the full message, and LogBox shows that a warning exists. PNG, JPEG, and SF Symbol icons do not need the decoder. + +You can also set the `RN_BOTTOM_TABS_ENABLE_SVG` environment variable to `1` or `0`. If the `Podfile` also sets `$RNBottomTabsEnableSVG`, the `Podfile` value is used. + +If you use Expo prebuild, set the `enableSVG` option of the config plugin to `true` or `false`: + +```json +{ + "expo": { + "plugins": [["react-native-bottom-tabs", { "enableSVG": false }]] + } +} +``` + +If your app uses Swift Package Manager instead of CocoaPods, set `RN_BOTTOM_TABS_ENABLE_SVG` to `1` or `0` in the environment when SwiftPM resolves packages. For example, run the sync and the build from the same shell: + +```sh +export RN_BOTTOM_TABS_ENABLE_SVG=0 +npx react-native spm +npx react-native run-ios +``` + +Xcode reads this variable only when it is set in Xcode's own environment. If you open the project from the Dock or Finder, the variable from your shell does not reach it. + +When the flag is not set, development builds also log the deprecation warning to the JavaScript console the first time that the app decodes an SVG image. With Swift Package Manager, this is the only place where the warning shows, because there is no `pod install` step. + #### `tabBarIconRenderingMode` Rendering mode for image icons. diff --git a/packages/react-native-bottom-tabs/Package.swift b/packages/react-native-bottom-tabs/Package.swift index 70720fff..1a1adf89 100644 --- a/packages/react-native-bottom-tabs/Package.swift +++ b/packages/react-native-bottom-tabs/Package.swift @@ -2,6 +2,35 @@ import PackageDescription +// The SVG image decoder uses private CoreSVG APIs. RN_BOTTOM_TABS_ENABLE_SVG controls it +// when SwiftPM resolves the package: +// unset -> enabled, with a deprecation notice in dev builds +// 1 / true / yes -> enabled +// 0 / false / no -> disabled, no private CoreSVG code is compiled +let svgRawSetting = Context.environment["RN_BOTTOM_TABS_ENABLE_SVG"] ?? "" +let svgSetting = String( + svgRawSetting.drop(while: \.isWhitespace).reversed().drop(while: \.isWhitespace).reversed() +).lowercased() +let svgSettingIsUnset = svgSetting.isEmpty +// TODO(next major): disable SVG when RN_BOTTOM_TABS_ENABLE_SVG is unset. +let enableSVG: Bool +switch svgSetting { +case "", "1", "true", "yes": + enableSVG = true +case "0", "false", "no": + enableSVG = false +default: + fatalError( + "[react-native-bottom-tabs] Unsupported value \"\(svgRawSetting)\" for RN_BOTTOM_TABS_ENABLE_SVG. " + + "Use true or false (or 1 or 0)." + ) +} +// CSetting and CXXSetting are different types, so both lists map these names. +let svgDefines = [ + ("RN_BOTTOM_TABS_ENABLE_SVG", enableSVG), + ("RN_BOTTOM_TABS_SVG_DEFAULTED", enableSVG && svgSettingIsUnset), +].filter(\.1).map(\.0) + let reactHeaders: [Target.Dependency] = [ .product(name: "ReactHeaders", package: "ReactNative"), .product(name: "ReactNativeHeaders", package: "ReactNative"), @@ -64,7 +93,8 @@ let package = Package( "common/cpp", ], publicHeadersPath: "ios/include", - cxxSettings: [ + cSettings: svgDefines.map { .define($0, to: "1") }, + cxxSettings: svgDefines.map { .define($0, to: "1") } + [ .headerSearchPath("common/cpp"), .define("RCT_NEW_ARCH_ENABLED", to: "1"), // Fabric's C++ layout must match the selected React framework. diff --git a/packages/react-native-bottom-tabs/README.md b/packages/react-native-bottom-tabs/README.md index f4fc5785..b1531d19 100644 --- a/packages/react-native-bottom-tabs/README.md +++ b/packages/react-native-bottom-tabs/README.md @@ -19,6 +19,8 @@ The full documentation can be found on our [website](https://oss.callstack.com/react-native-bottom-tabs/). +On Apple platforms, the SVG image decoder will be disabled by default in the next major version. See [SVG images on Apple platforms](https://oss.callstack.com/react-native-bottom-tabs/docs/guides/usage-with-react-navigation#svg-images-on-apple-platforms) to keep it or to remove it now. + ## Contributing See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. diff --git a/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.mm b/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.mm index 44a554d8..03059e98 100644 --- a/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.mm +++ b/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.mm @@ -1,3 +1,7 @@ +// The private CoreSVG code is compiled only when SVG support is enabled. +// See ios/svg_configuration.rb and Package.swift. +#if RN_BOTTOM_TABS_ENABLE_SVG + #import "CoreSVG.h" #import #import @@ -175,3 +179,5 @@ + (BOOL)supportsVectorSVGImage { } @end + +#endif diff --git a/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm b/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm index 1bbddd04..1d48f199 100644 --- a/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm +++ b/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm @@ -1,13 +1,60 @@ #import "SvgDecoder.h" +#import + +#if RN_BOTTOM_TABS_ENABLE_SVG #import "CoreSVG.h" +#endif + +#if !RN_BOTTOM_TABS_ENABLE_SVG && RCT_DEV +static BOOL RNBottomTabsLooksLikeSVGData(NSData *data) +{ + if (!data) { + return NO; + } + NSData *tag = [@"" dataUsingEncoding:NSUTF8StringEncoding]; + NSUInteger length = MIN(100, data.length); + return [data rangeOfData:tag options:NSDataSearchBackwards range:NSMakeRange(data.length - length, length)].location != NSNotFound; +} +#endif @implementation SvgDecoder RCT_EXPORT_MODULE() +@synthesize callableJSModules = _callableJSModules; + +#if RCT_DEV +// RCTLogWarn alone does not show the message in React Native DevTools, so the +// warning goes through the JS console, which also shows it in LogBox. +- (void)logDevWarning:(NSString *)message +{ + RCTCallableJSModules *jsModules = self.callableJSModules; + if (jsModules) { + NSLog(@"%@", message); + [jsModules invokeModule:@"RCTLog" method:@"logToConsole" withArgs:@[ @"warn", message ]]; + } else { + RCTLogWarn(@"%@", message); + } +} +#endif + - (BOOL)canDecodeImageData:(NSData *)imageData { +#if RN_BOTTOM_TABS_ENABLE_SVG return [CoreSVGWrapper isSVGData:imageData]; +#else +#if RCT_DEV + if (RNBottomTabsLooksLikeSVGData(imageData)) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self logDevWarning:@"[react-native-bottom-tabs] SVG decoding is disabled, so SVG images are not shown. " + @"To enable it, set `$RNBottomTabsEnableSVG = true` in your Podfile, " + @"or `RN_BOTTOM_TABS_ENABLE_SVG=1` for Swift Package Manager."]; + }); + } +#endif + return NO; +#endif } - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData @@ -16,7 +63,20 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData resizeMode:(RCTResizeMode)resizeMode completionHandler:(RCTImageLoaderCompletionBlock)completionHandler { +#if RN_BOTTOM_TABS_ENABLE_SVG +#if RCT_DEV && RN_BOTTOM_TABS_SVG_DEFAULTED + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self logDevWarning:@"[react-native-bottom-tabs] SVG decoding will be disabled by default in the next major version. " + @"To keep it, set `$RNBottomTabsEnableSVG = true` in your Podfile, " + @"or `RN_BOTTOM_TABS_ENABLE_SVG=1` for Swift Package Manager. " + @"Set it to `false` (or `0`) to remove the private CoreSVG code now."]; + }); +#endif UIImage *image = [CoreSVGWrapper.shared imageFromSVGData:imageData]; +#else + UIImage *image = nil; +#endif if (image) { completionHandler(nil, image); diff --git a/packages/react-native-bottom-tabs/ios/__tests__/svg_configuration_test.rb b/packages/react-native-bottom-tabs/ios/__tests__/svg_configuration_test.rb new file mode 100644 index 00000000..3b4c8552 --- /dev/null +++ b/packages/react-native-bottom-tabs/ios/__tests__/svg_configuration_test.rb @@ -0,0 +1,61 @@ +require "minitest/autorun" +require_relative "../svg_configuration" + +class SVGConfigurationTest < Minitest::Test + Config = RNBottomTabs::SVGConfiguration + BASE = { "DEFINES_MODULE" => "YES" }.freeze + + def test_unset_is_enabled_and_marked_as_defaulted + assert_nil Config.setting(global: nil, env: nil) + assert_nil Config.setting(global: nil, env: " ") + assert Config.enabled?(nil) + assert_equal( + "$(inherited) RN_BOTTOM_TABS_ENABLE_SVG=1 RN_BOTTOM_TABS_SVG_DEFAULTED=1", + Config.xcconfig(BASE, value: nil)["GCC_PREPROCESSOR_DEFINITIONS"] + ) + end + + def test_true_values_enable_svg + [true, "true", "1", "YES"].each do |value| + assert_equal true, Config.setting(global: value, env: nil), value.inspect + end + assert_equal true, Config.setting(global: nil, env: "1") + assert_equal( + "$(inherited) RN_BOTTOM_TABS_ENABLE_SVG=1", + Config.xcconfig(BASE, value: true)["GCC_PREPROCESSOR_DEFINITIONS"] + ) + end + + def test_false_values_disable_svg + [false, "false", "0", "no"].each do |value| + assert_equal false, Config.setting(global: value, env: nil), value.inspect + end + assert_equal false, Config.setting(global: nil, env: "0") + refute Config.enabled?(false) + assert_equal BASE, Config.xcconfig(BASE, value: false) + end + + def test_podfile_value_wins_over_environment + assert_equal false, Config.setting(global: false, env: "1") + assert_equal true, Config.setting(global: true, env: "0") + end + + def test_unsupported_value_raises + assert_raises(ArgumentError) { Config.setting(global: "maybe", env: nil) } + end + + def test_warns_only_when_unset + Config.instance_variable_set(:@warned, false) + _, err = capture_io do + Config.warn_if_unset(nil) + Config.warn_if_unset(nil) + end + assert_equal 1, err.scan(/next major version/).size + + _, err = capture_io do + Config.warn_if_unset(true) + Config.warn_if_unset(false) + end + assert_empty err + end +end diff --git a/packages/react-native-bottom-tabs/ios/svg_configuration.rb b/packages/react-native-bottom-tabs/ios/svg_configuration.rb new file mode 100644 index 00000000..03f7399e --- /dev/null +++ b/packages/react-native-bottom-tabs/ios/svg_configuration.rb @@ -0,0 +1,63 @@ +module RNBottomTabs + # Controls the SVG image decoder, which uses private CoreSVG APIs. + # + # The app sets `$RNBottomTabsEnableSVG` in its Podfile, or the + # `RN_BOTTOM_TABS_ENABLE_SVG` environment variable. The Podfile value wins. + # + # unset -> enabled, with a deprecation notice + # true / "1" / "yes" -> enabled + # false / "0" / "no" -> disabled, no private CoreSVG code is compiled + module SVGConfiguration + COMPILATION_CONDITION = "RN_BOTTOM_TABS_ENABLE_SVG" + DEFAULTED_CONDITION = "RN_BOTTOM_TABS_SVG_DEFAULTED" + ENV_NAME = "RN_BOTTOM_TABS_ENABLE_SVG" + TRUE_VALUES = ["true", "1", "yes"].freeze + FALSE_VALUES = ["false", "0", "no"].freeze + DEPRECATION_MESSAGE = + "[react-native-bottom-tabs] SVG tab icons and SVG images will be disabled by default in the next major version. " \ + "Set `$RNBottomTabsEnableSVG = true` in your Podfile to keep them, " \ + "or `$RNBottomTabsEnableSVG = false` to remove the private CoreSVG code now." + + # Returns true or false when the app chose, or nil when it did not. + def self.setting(global: defined?($RNBottomTabsEnableSVG) ? $RNBottomTabsEnableSVG : nil, env: ENV[ENV_NAME]) + value = global.nil? ? env : global + return value if value == true || value == false + + normalized = value.to_s.strip.downcase + return nil if normalized.empty? + return true if TRUE_VALUES.include?(normalized) + return false if FALSE_VALUES.include?(normalized) + + raise ArgumentError, + "[react-native-bottom-tabs] Unsupported value #{value.inspect} for `$RNBottomTabsEnableSVG` " \ + "or `#{ENV_NAME}`. Use true or false (or 1 or 0)." + end + + # TODO(next major): disable SVG when the app does not choose (`setting` is nil). + def self.enabled?(value = setting) + value != false + end + + def self.xcconfig(base, value: setting) + definitions = [] + definitions << "#{COMPILATION_CONDITION}=1" if enabled?(value) + definitions << "#{DEFAULTED_CONDITION}=1" if value.nil? + return base if definitions.empty? + + base.merge("GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) #{definitions.join(' ')}") + end + + # CocoaPods can evaluate the podspec more than once, so warn once per process. + def self.warn_if_unset(value = setting) + return if !value.nil? || @warned + + @warned = true + + if defined?(Pod::UI) + Pod::UI.warn(DEPRECATION_MESSAGE) + else + Kernel.warn(DEPRECATION_MESSAGE) + end + end + end +end diff --git a/packages/react-native-bottom-tabs/package.json b/packages/react-native-bottom-tabs/package.json index 8a25bc6d..19aca29b 100644 --- a/packages/react-native-bottom-tabs/package.json +++ b/packages/react-native-bottom-tabs/package.json @@ -39,6 +39,7 @@ ], "scripts": { "test": "jest", + "test:ruby": "ruby ios/__tests__/svg_configuration_test.rb", "typecheck": "tsc -b", "lint": "eslint \"**/*.{js,ts,tsx}\"", "build": "bob build" diff --git a/packages/react-native-bottom-tabs/react-native-bottom-tabs.podspec b/packages/react-native-bottom-tabs/react-native-bottom-tabs.podspec index 15fddf33..a364fb86 100644 --- a/packages/react-native-bottom-tabs/react-native-bottom-tabs.podspec +++ b/packages/react-native-bottom-tabs/react-native-bottom-tabs.podspec @@ -1,4 +1,5 @@ require "json" +require_relative "ios/svg_configuration" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) @@ -27,9 +28,11 @@ Pod::Spec.new do |s| s.dependency "SwiftUIIntrospect", '~> 1.0' - s.pod_target_xcconfig = { - 'DEFINES_MODULE' => 'YES' - } + RNBottomTabs::SVGConfiguration.warn_if_unset + + s.pod_target_xcconfig = RNBottomTabs::SVGConfiguration.xcconfig( + { 'DEFINES_MODULE' => 'YES' } + ) install_modules_dependencies(s) end diff --git a/packages/react-native-bottom-tabs/src/__tests__/expoPodfile.test.ts b/packages/react-native-bottom-tabs/src/__tests__/expoPodfile.test.ts new file mode 100644 index 00000000..faeab3e4 --- /dev/null +++ b/packages/react-native-bottom-tabs/src/__tests__/expoPodfile.test.ts @@ -0,0 +1,59 @@ +import { setPodfileSVGFlag } from '../expoPodfile'; + +const podfile = [ + "require 'json'", + "platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'", + '', + "target 'HelloWorld' do", + 'end', +].join('\n'); + +describe('setPodfileSVGFlag', () => { + it('adds the flag after the platform line', () => { + const result = setPodfileSVGFlag(podfile, true).split('\n'); + + expect(result[2]).toMatch( + /^# @generated begin react-native-bottom-tabs-svg/ + ); + expect(result[3]).toBe('$RNBottomTabsEnableSVG = true'); + expect(result[4]).toBe('# @generated end react-native-bottom-tabs-svg'); + }); + + it('writes false to disable SVG', () => { + expect(setPodfileSVGFlag(podfile, false)).toContain( + '$RNBottomTabsEnableSVG = false' + ); + }); + + it('replaces an existing flag instead of adding a second one', () => { + const once = setPodfileSVGFlag(podfile, true); + const twice = setPodfileSVGFlag(setPodfileSVGFlag(once, true), false); + + expect(twice.match(/\$RNBottomTabsEnableSVG/g)).toHaveLength(1); + expect(twice).toContain('$RNBottomTabsEnableSVG = false'); + }); + + it('replaces a block that lost its end marker', () => { + const broken = setPodfileSVGFlag(podfile, false) + .split('\n') + .filter((line) => !line.startsWith('# @generated end')) + .join('\n'); + const result = setPodfileSVGFlag(broken, true); + + expect(result.match(/\$RNBottomTabsEnableSVG/g)).toHaveLength(1); + expect(result).toContain('$RNBottomTabsEnableSVG = true'); + }); + + it('removes the flag when the option is unset', () => { + expect(setPodfileSVGFlag(setPodfileSVGFlag(podfile, true), undefined)).toBe( + podfile + ); + }); + + it('adds the flag at the top when there is no platform line', () => { + const result = setPodfileSVGFlag("target 'App' do\nend", true).split('\n'); + + expect(result[1]).toBe('$RNBottomTabsEnableSVG = true'); + expect(result[3]).toBe("target 'App' do"); + }); +}); diff --git a/packages/react-native-bottom-tabs/src/expo.ts b/packages/react-native-bottom-tabs/src/expo.ts index 71c3b0d3..4fec979e 100644 --- a/packages/react-native-bottom-tabs/src/expo.ts +++ b/packages/react-native-bottom-tabs/src/expo.ts @@ -1,11 +1,12 @@ import { type ConfigPlugin } from '@expo/config-plugins'; import { createRequire } from 'node:module'; +import { setPodfileSVGFlag } from './expoPodfile'; const require = createRequire(import.meta.url); const ConfigPlugins = require('@expo/config-plugins'); -const { createRunOncePlugin, withAndroidStyles } = +const { createRunOncePlugin, withAndroidStyles, withPodfile } = ConfigPlugins as typeof import('@expo/config-plugins'); const MATERIAL3_THEME_DYNAMIC = @@ -20,11 +21,18 @@ type ConfigProps = { * Define theme that should be used. * @default 'material3' */ - theme: + theme?: | 'material2' | 'material3' | 'material3-dynamic' | 'material3-expressive'; + /** + * Controls the SVG image decoder on Apple platforms, which uses private + * CoreSVG APIs. `true` keeps it, `false` removes it from the build. + * When unset, it stays enabled with a deprecation warning during + * `pod install`, because the next major version disables it by default. + */ + enableSVG?: boolean; }; const withMaterial3Theme: ConfigPlugin = (config, options) => { @@ -52,7 +60,20 @@ const withMaterial3Theme: ConfigPlugin = (config, options) => { }); }; -export default createRunOncePlugin( - withMaterial3Theme, - 'react-native-bottom-tabs' -); +const withSVGSupport: ConfigPlugin = (config, options) => { + return withPodfile(config, (podfileConfig) => { + podfileConfig.modResults.contents = setPodfileSVGFlag( + podfileConfig.modResults.contents, + options?.enableSVG + ); + + return podfileConfig; + }); +}; + +const withBottomTabs: ConfigPlugin = (config, options) => { + config = withMaterial3Theme(config, options); + return withSVGSupport(config, options); +}; + +export default createRunOncePlugin(withBottomTabs, 'react-native-bottom-tabs'); diff --git a/packages/react-native-bottom-tabs/src/expoPodfile.ts b/packages/react-native-bottom-tabs/src/expoPodfile.ts new file mode 100644 index 00000000..660ff9d8 --- /dev/null +++ b/packages/react-native-bottom-tabs/src/expoPodfile.ts @@ -0,0 +1,38 @@ +const BLOCK_START = '# @generated begin react-native-bottom-tabs-svg'; +const BLOCK_END = '# @generated end react-native-bottom-tabs-svg'; +const PLATFORM_LINE = /^\s*platform\s+:ios\b/; + +/** + * Writes `$RNBottomTabsEnableSVG` into a Podfile, or removes it when + * `enableSVG` is undefined. The flag goes after the `platform :ios` line, + * or at the top of the file when the Podfile has no such line. + */ +export function setPodfileSVGFlag( + contents: string, + enableSVG: boolean | undefined +): string { + const lines = contents.split('\n'); + const start = lines.findIndex((line) => line.startsWith(BLOCK_START)); + const end = lines.findIndex( + (line, index) => index > start && line.startsWith(BLOCK_END) + ); + + // A block without its end marker is removed up to the end of the file. + if (start !== -1) { + lines.splice(start, (end === -1 ? lines.length : end + 1) - start); + } + + if (enableSVG !== undefined) { + const platformLine = lines.findIndex((line) => PLATFORM_LINE.test(line)); + + lines.splice( + platformLine + 1, + 0, + `${BLOCK_START} - expo prebuild (DO NOT MODIFY)`, + `$RNBottomTabsEnableSVG = ${enableSVG}`, + BLOCK_END + ); + } + + return lines.join('\n'); +}