diff --git a/.changeset/tidy-tools-smile.md b/.changeset/tidy-tools-smile.md new file mode 100644 index 00000000..4d2f14ef --- /dev/null +++ b/.changeset/tidy-tools-smile.md @@ -0,0 +1,5 @@ +--- +"react-native-bottom-tabs": major +--- + +Disable Apple-platform SVG tab icon support by default and add the `$RNBottomTabsEnableSVG` Podfile opt-in. The optional CoreSVG decoder is now scoped to bottom-tab icons instead of being registered app-wide with React Native's image loader. diff --git a/apps/example/ios/Podfile b/apps/example/ios/Podfile index 09683801..5eff6406 100644 --- a/apps/example/ios/Podfile +++ b/apps/example/ios/Podfile @@ -1,4 +1,5 @@ ENV['RCT_NEW_ARCH_ENABLED'] = '1' +$RNBottomTabsEnableSVG = ENV['RN_BOTTOM_TABS_ENABLE_SVG'] == '1' ws_dir = Pathname.new(__dir__) ws_dir = ws_dir.parent until diff --git a/apps/example/src/Examples/NativeBottomTabsEmbeddedStacks.tsx b/apps/example/src/Examples/NativeBottomTabsEmbeddedStacks.tsx index 63fe64aa..a974ffb5 100644 --- a/apps/example/src/Examples/NativeBottomTabsEmbeddedStacks.tsx +++ b/apps/example/src/Examples/NativeBottomTabsEmbeddedStacks.tsx @@ -94,8 +94,7 @@ function NativeBottomTabsEmbeddedStacks() { name="Chat" component={ChatStackScreen} options={{ - tabBarIcon: () => - require('../../assets/icons/message-circle-code.svg'), + tabBarIcon: () => require('../../assets/icons/chat_dark.png'), }} /> diff --git a/apps/example/src/Examples/TintColors.tsx b/apps/example/src/Examples/TintColors.tsx index 62c87340..bce3cd0c 100644 --- a/apps/example/src/Examples/TintColors.tsx +++ b/apps/example/src/Examples/TintColors.tsx @@ -44,9 +44,7 @@ export default function TintColorsExample() { }, { key: 'chat', - focusedIcon: { - uri: 'https://upload.wikimedia.org/wikipedia/commons/f/fa/Apple_logo_black.svg', - }, + focusedIcon: require('../../assets/icons/chat_dark.png'), title: 'Chat', }, ]); diff --git a/artifacts/issue-565/README.md b/artifacts/issue-565/README.md new file mode 100644 index 00000000..3c976675 --- /dev/null +++ b/artifacts/issue-565/README.md @@ -0,0 +1,32 @@ +# Issue #565 verification + +These artifacts were captured from the example app on an iPhone 17 Pro simulator running iOS 26.5. The native builds used Xcode 27.0, CocoaPods 1.17.0, React Native 0.81.4, and the new architecture. Device automation used `agent-device` 0.20.10. + +## Default configuration + +`pod install` ran without `RN_BOTTOM_TABS_ENABLE_SVG`. The generated Pods project contained no `ios/SVG` source references and React Native codegen generated an empty `imageDataDecoderClassNames` array. The built app, app dylib, and `libreact-native-bottom-tabs.a` contained none of these strings: + +- `CoreSVGWrapper` +- `SvgDecoder` +- `CGSVGDocument` +- `_imageWithCGSVGDocument` +- the CoreSVG symbol-name base64 literals + +`RCTTabViewComponentView` was present as a positive control. The app was then exercised with SF Symbol and ordinary PNG icons: + +- [SF Symbols](default/sf-symbols.png) +- [Selected SF Symbol tab](default/sf-symbols-albums.png) +- [PNG icons](default/png-icons.png) +- [Non-SVG interaction recording](default/non-svg-icons.mp4) + +## SVG opt-in configuration + +`pod install` ran with `RN_BOTTOM_TABS_ENABLE_SVG=1`. The generated Pods project included exactly `CoreSVG.{h,mm}` and `SvgDecoder.{h,mm}` and set `RN_BOTTOM_TABS_ENABLE_SVG` for Objective-C++ and Swift compilation. React Native codegen still generated an empty `imageDataDecoderClassNames` array because the decoder is library-scoped. + +The built static library exported `RNBottomTabsDecodeSVGData` and contained `CoreSVGWrapper`, `CGSVGDocument`, and the CoreSVG symbol-name base64 literals. Both local and remote SVG tab sources rendered, and tab selection was verified through accessibility state: + +- [Local SVG icons](opt-in/local-svg-icons.png) +- [Remote SVG icons](opt-in/remote-svg-icons.png) +- [SVG interaction recording](opt-in/svg-icons.mp4) + +The same podspec configuration checks also passed with React Native 0.71.19, 0.76.9, 0.79.7, 0.81.6, 0.84.1, and 0.87.1 under CocoaPods 1.17.0, for both default and opted-in evaluation. diff --git a/artifacts/issue-565/default/non-svg-icons.mp4 b/artifacts/issue-565/default/non-svg-icons.mp4 new file mode 100644 index 00000000..52a5bb87 Binary files /dev/null and b/artifacts/issue-565/default/non-svg-icons.mp4 differ diff --git a/artifacts/issue-565/default/png-icons.png b/artifacts/issue-565/default/png-icons.png new file mode 100644 index 00000000..ed144f2f Binary files /dev/null and b/artifacts/issue-565/default/png-icons.png differ diff --git a/artifacts/issue-565/default/sf-symbols-albums.png b/artifacts/issue-565/default/sf-symbols-albums.png new file mode 100644 index 00000000..f9b69e3b Binary files /dev/null and b/artifacts/issue-565/default/sf-symbols-albums.png differ diff --git a/artifacts/issue-565/default/sf-symbols.png b/artifacts/issue-565/default/sf-symbols.png new file mode 100644 index 00000000..99798135 Binary files /dev/null and b/artifacts/issue-565/default/sf-symbols.png differ diff --git a/artifacts/issue-565/opt-in/local-svg-icons.png b/artifacts/issue-565/opt-in/local-svg-icons.png new file mode 100644 index 00000000..50ced4c7 Binary files /dev/null and b/artifacts/issue-565/opt-in/local-svg-icons.png differ diff --git a/artifacts/issue-565/opt-in/remote-svg-icons.png b/artifacts/issue-565/opt-in/remote-svg-icons.png new file mode 100644 index 00000000..d6c2dc7b Binary files /dev/null and b/artifacts/issue-565/opt-in/remote-svg-icons.png differ diff --git a/artifacts/issue-565/opt-in/svg-icons.mp4 b/artifacts/issue-565/opt-in/svg-icons.mp4 new file mode 100644 index 00000000..459343df Binary files /dev/null and b/artifacts/issue-565/opt-in/svg-icons.mp4 differ diff --git a/docs/docs/docs/guides/usage-with-react-navigation.mdx b/docs/docs/docs/guides/usage-with-react-navigation.mdx index 78e49d55..b6085726 100644 --- a/docs/docs/docs/guides/usage-with-react-navigation.mdx +++ b/docs/docs/docs/guides/usage-with-react-navigation.mdx @@ -272,8 +272,6 @@ Function that given `{ focused: boolean }` returns `ImageSource` or `AppleIcon` component={Albums} options={{ tabBarIcon: () => require('person.png'), - // SVG is also supported - tabBarIcon: () => require('person.svg'), // or tabBarIcon: () => ({ sfSymbol: 'person' }), // You can also pass a URL @@ -286,6 +284,36 @@ Function that given `{ focused: boolean }` returns `ImageSource` or `AppleIcon` SF Symbols are only supported on Apple platforms. ::: +#### SVG icons on Apple platforms + +SVG tab icons remain available by default on Android. On Apple platforms, SVG support is disabled by default because its native decoder uses private CoreSVG APIs. PNG, JPEG, and SF Symbol icons do not need this decoder. + +To opt in, set the flag before React Native configures pods in your `Podfile`, then run `pod install` and rebuild the native app: + +```ruby +$RNBottomTabsEnableSVG = true + +# require React Native's pod helpers and call use_react_native! below +``` + +With the decoder enabled, local and remote SVG sources can be used like other image sources: + +```tsx +tabBarIcon: () => require('person.svg') +``` + +Enabling this flag compiles the optional CoreSVG implementation into the Apple-platform pod. The decoder is scoped to bottom-tab icons and is not registered with React Native's app-wide image loader. + +If you are upgrading from a version where Apple-platform SVG support was enabled automatically, add this flag before upgrading to preserve your existing SVG tab icons. Expo prebuild projects can use the package's `enableSVG` plugin option instead: + +```json +{ + "expo": { + "plugins": [["react-native-bottom-tabs", { "enableSVG": true }]] + } +} +``` + #### `tabBarIconRenderingMode` Rendering mode for image icons. diff --git a/packages/react-native-bottom-tabs/README.md b/packages/react-native-bottom-tabs/README.md index 236b1b52..93abdd67 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://callstackincubator.github.io/react-native-bottom-tabs/). +SVG tab icons on Apple platforms are disabled by default. See the [icon configuration guide](https://oss.callstack.com/react-native-bottom-tabs/docs/guides/usage-with-react-navigation#svg-icons-on-apple-platforms) for the Podfile and Expo opt-ins. + ## 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/SvgDecoder.h b/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.h index 5e7fde1d..0ba14eea 100644 --- a/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.h +++ b/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.h @@ -1,10 +1,26 @@ -#ifdef __cplusplus +#import +#import + +#if TARGET_OS_OSX +#import +#else +#import +#endif -#import -#import +NS_ASSUME_NONNULL_BEGIN -@interface SvgDecoder : NSObject +#ifdef __cplusplus +extern "C" { +#endif -@end +#if TARGET_OS_OSX +NSImage *_Nullable RNBottomTabsDecodeSVGData(NSData *data); +#else +UIImage *_Nullable RNBottomTabsDecodeSVGData(NSData *data); +#endif +#ifdef __cplusplus +} #endif + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm b/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm index 1bbddd04..819bdf00 100644 --- a/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm +++ b/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm @@ -1,38 +1,11 @@ #import "SvgDecoder.h" #import "CoreSVG.h" -@implementation SvgDecoder - -RCT_EXPORT_MODULE() - -- (BOOL)canDecodeImageData:(NSData *)imageData -{ - return [CoreSVGWrapper isSVGData:imageData]; -} - -- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData - size:(CGSize)size - scale:(CGFloat)scale - resizeMode:(RCTResizeMode)resizeMode - completionHandler:(RCTImageLoaderCompletionBlock)completionHandler +PlatformImage *RNBottomTabsDecodeSVGData(NSData *data) { - UIImage *image = [CoreSVGWrapper.shared imageFromSVGData:imageData]; - - if (image) { - completionHandler(nil, image); - } else { - NSError *error = [NSError errorWithDomain:@"SVGDecoderErrorDomain" - code:2 - userInfo:@{NSLocalizedDescriptionKey: @"Failed to render SVG to image"}]; - completionHandler(error, nil); + if (![CoreSVGWrapper isSVGData:data]) { + return nil; } - return ^{}; -} -- (std::shared_ptr)getTurboModule: -(const facebook::react::ObjCTurboModule::InitParams &)params -{ - return std::make_shared(params); + return [CoreSVGWrapper.shared imageFromSVGData:data]; } - -@end diff --git a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift index e5663453..63963ff8 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift @@ -264,6 +264,21 @@ public final class TabInfo: NSObject { if let imageSources = icons as? [RCTImageSource?] { for (index, imageSource) in imageSources.enumerated() { guard let imageSource else { continue } + + #if RN_BOTTOM_TABS_ENABLE_SVG + if isSVGRequest(imageSource.request) { + loadSVGIcon(with: imageSource.request) { [weak self] image in + guard let self else { return } + if let image { + updateIcon(image, at: index, focused: focused) + } else { + print("[TabView] Error loading SVG icon") + } + } + continue + } + #endif + imageLoader.loadImage( with: imageSource.request, size: imageSource.size, @@ -272,41 +287,93 @@ public final class TabInfo: NSObject { resizeMode: RCTResizeMode.contain, progressBlock: { _, _ in }, partialLoad: { _ in }, - completionBlock: { error, image in - if error != nil { - print("[TabView] Error loading image: \(error!.localizedDescription)") - return - } - guard let image else { return } - DispatchQueue.main.async { [weak self] in - guard let self else { return } - let icon = image.resizeImageTo(size: iconSize) - #if os(iOS) - if props.experimentalBakedTintColors { - if focused { - props.focusedIcons[index] = icon?.withRenderingMode(.alwaysTemplate) - } else { - props.icons[index] = icon?.withRenderingMode(.alwaysTemplate) - } - } else { - if focused { - props.focusedIcons[index] = icon + completionBlock: { [weak self] error, image in + guard let self else { return } + if let error { + #if RN_BOTTOM_TABS_ENABLE_SVG + loadSVGIcon(with: imageSource.request) { [weak self] svgImage in + guard let self else { return } + if let svgImage { + updateIcon(svgImage, at: index, focused: focused) } else { - props.icons[index] = icon + print("[TabView] Error loading image: \(error.localizedDescription)") } } - props.iconsRevision += 1 #else - if focused { - props.focusedIcons[index] = icon - } else { - props.icons[index] = icon - } - props.iconsRevision += 1 + print("[TabView] Error loading image: \(error.localizedDescription)") #endif + return } + guard let image else { return } + updateIcon(image, at: index, focused: focused) }) } } } + + private func updateIcon(_ image: PlatformImage, at index: Int, focused: Bool) { + DispatchQueue.main.async { [weak self] in + guard let self else { return } + let icon = image.resizeImageTo(size: iconSize) + #if os(iOS) + if props.experimentalBakedTintColors { + if focused { + props.focusedIcons[index] = icon?.withRenderingMode(.alwaysTemplate) + } else { + props.icons[index] = icon?.withRenderingMode(.alwaysTemplate) + } + } else { + if focused { + props.focusedIcons[index] = icon + } else { + props.icons[index] = icon + } + } + props.iconsRevision += 1 + #else + if focused { + props.focusedIcons[index] = icon + } else { + props.icons[index] = icon + } + props.iconsRevision += 1 + #endif + } + } + + #if RN_BOTTOM_TABS_ENABLE_SVG + private func isSVGRequest(_ request: URLRequest) -> Bool { + guard let url = request.url else { return false } + return url.pathExtension.lowercased() == "svg" + || url.absoluteString.lowercased().hasPrefix("data:image/svg+xml") + } + + private func loadSVGIcon( + with request: URLRequest, + completion: @escaping (PlatformImage?) -> Void + ) { + guard let url = request.url else { + completion(nil) + return + } + + let decode: (Data?) -> Void = { data in + guard let data else { + completion(nil) + return + } + completion(RNBottomTabsDecodeSVGData(data)) + } + + if url.scheme == "http" || url.scheme == "https" { + URLSession.shared.dataTask(with: request) { data, _, _ in + decode(data) + }.resume() + } else { + DispatchQueue.global(qos: .userInitiated).async { + decode(try? Data(contentsOf: url)) + } + } + } + #endif } 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..da9a4a6a --- /dev/null +++ b/packages/react-native-bottom-tabs/ios/__tests__/svg_configuration_test.rb @@ -0,0 +1,38 @@ +require "minitest/autorun" +require_relative "../svg_configuration" + +class SVGConfigurationTest < Minitest::Test + def test_svg_support_is_disabled_by_default + $RNBottomTabsEnableSVG = nil + + refute RNBottomTabs::SVGConfiguration.enabled? + assert_equal "ios/SVG/**/*", RNBottomTabs::SVGConfiguration::SVG_SOURCES + assert_equal( + { "DEFINES_MODULE" => "YES" }, + RNBottomTabs::SVGConfiguration.xcconfig({ "DEFINES_MODULE" => "YES" }) + ) + end + + def test_global_flag_enables_svg_support + $RNBottomTabsEnableSVG = true + + assert RNBottomTabs::SVGConfiguration.enabled? + end + + def test_opt_in_sets_compilation_conditions + xcconfig = RNBottomTabs::SVGConfiguration.xcconfig( + { "DEFINES_MODULE" => "YES" }, + enabled: true + ) + + assert_equal "YES", xcconfig["DEFINES_MODULE"] + assert_equal( + "$(inherited) RN_BOTTOM_TABS_ENABLE_SVG=1", + xcconfig["GCC_PREPROCESSOR_DEFINITIONS"] + ) + assert_equal( + "$(inherited) RN_BOTTOM_TABS_ENABLE_SVG", + xcconfig["SWIFT_ACTIVE_COMPILATION_CONDITIONS"] + ) + end +end diff --git a/packages/react-native-bottom-tabs/ios/react-native-bottom-tabs-Bridging-Header.h b/packages/react-native-bottom-tabs/ios/react-native-bottom-tabs-Bridging-Header.h index b4a33181..956e0ce0 100644 --- a/packages/react-native-bottom-tabs/ios/react-native-bottom-tabs-Bridging-Header.h +++ b/packages/react-native-bottom-tabs/ios/react-native-bottom-tabs-Bridging-Header.h @@ -2,3 +2,7 @@ #import #import #import + +#if RN_BOTTOM_TABS_ENABLE_SVG +#import "SVG/SvgDecoder.h" +#endif 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..517a580d --- /dev/null +++ b/packages/react-native-bottom-tabs/ios/svg_configuration.rb @@ -0,0 +1,19 @@ +module RNBottomTabs + module SVGConfiguration + SVG_SOURCES = "ios/SVG/**/*" + COMPILATION_CONDITION = "RN_BOTTOM_TABS_ENABLE_SVG" + + def self.enabled? + defined?($RNBottomTabsEnableSVG) && $RNBottomTabsEnableSVG == true + end + + def self.xcconfig(base, enabled: enabled?) + return base unless enabled + + base.merge( + "GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) #{COMPILATION_CONDITION}=1", + "SWIFT_ACTIVE_COMPILATION_CONDITIONS" => "$(inherited) #{COMPILATION_CONDITION}" + ) + end + end +end diff --git a/packages/react-native-bottom-tabs/package.json b/packages/react-native-bottom-tabs/package.json index 683e719f..c9c7c073 100644 --- a/packages/react-native-bottom-tabs/package.json +++ b/packages/react-native-bottom-tabs/package.json @@ -37,7 +37,7 @@ "!**/.*" ], "scripts": { - "test": "jest", + "test": "ruby ios/__tests__/svg_configuration_test.rb && jest", "typecheck": "tsc -b", "lint": "eslint \"**/*.{js,ts,tsx}\"", "build": "bob build" @@ -105,7 +105,7 @@ }, "codegenConfig": { "name": "RNCTabView", - "type": "all", + "type": "components", "jsSrcsDir": "./src", "android": { "javaPackageName": "com.rcttabview" @@ -114,11 +114,6 @@ "componentProvider": { "RNCTabView": "RCTTabViewComponentView", "BottomAccessoryView": "RCTBottomAccessoryComponentView" - }, - "modulesConformingToProtocol": { - "RCTImageDataDecoder": [ - "SvgDecoder" - ] } } } 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..9ab72772 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' - } + s.exclude_files = RNBottomTabs::SVGConfiguration::SVG_SOURCES unless RNBottomTabs::SVGConfiguration.enabled? + + s.pod_target_xcconfig = RNBottomTabs::SVGConfiguration.xcconfig( + { 'DEFINES_MODULE' => 'YES' } + ) install_modules_dependencies(s) end diff --git a/packages/react-native-bottom-tabs/src/NativeSVGDecoder.ts b/packages/react-native-bottom-tabs/src/NativeSVGDecoder.ts deleted file mode 100644 index dad9f8f7..00000000 --- a/packages/react-native-bottom-tabs/src/NativeSVGDecoder.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { TurboModuleRegistry, type TurboModule } from 'react-native'; - -export interface Spec extends TurboModule {} - -export default TurboModuleRegistry.getEnforcing('SvgDecoder'); diff --git a/packages/react-native-bottom-tabs/src/expo.ts b/packages/react-native-bottom-tabs/src/expo.ts index d3375070..4a6252e0 100644 --- a/packages/react-native-bottom-tabs/src/expo.ts +++ b/packages/react-native-bottom-tabs/src/expo.ts @@ -4,9 +4,12 @@ import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const ConfigPlugins = require('@expo/config-plugins'); +const GenerateCode = require('@expo/config-plugins/build/utils/generateCode'); -const { createRunOncePlugin, withAndroidStyles } = +const { createRunOncePlugin, withAndroidStyles, withPodfile } = ConfigPlugins as typeof import('@expo/config-plugins'); +const { mergeContents, removeContents } = + GenerateCode as typeof import('@expo/config-plugins/build/utils/generateCode'); const MATERIAL3_THEME_DYANMIC = 'Theme.Material3.DynamicColors.DayNight.NoActionBar'; @@ -20,13 +23,21 @@ type ConfigProps = { * Define theme that should be used. * @default 'material3' */ - theme: + theme?: | 'material2' | 'material3' | 'material3-dynamic' | 'material3-expressive'; + /** + * Enable SVG tab icons on Apple platforms. + * @default false + */ + enableSVG?: boolean; }; +const SVG_PODFILE_TAG = 'react-native-bottom-tabs-svg'; +const SVG_PODFILE_FLAG = '$RNBottomTabsEnableSVG = true'; + const withMaterial3Theme: ConfigPlugin = (config, options) => { const theme = options?.theme; @@ -52,7 +63,31 @@ const withMaterial3Theme: ConfigPlugin = (config, options) => { }); }; -export default createRunOncePlugin( - withMaterial3Theme, - 'react-native-bottom-tabs' -); +const withSVGSupport: ConfigPlugin = (config, options) => { + return withPodfile(config, (podfileConfig) => { + const contents = podfileConfig.modResults.contents; + + podfileConfig.modResults.contents = options?.enableSVG + ? mergeContents({ + src: contents, + newSrc: SVG_PODFILE_FLAG, + tag: SVG_PODFILE_TAG, + anchor: /^platform :ios/, + offset: 1, + comment: '#', + }).contents + : removeContents({ + src: contents, + tag: SVG_PODFILE_TAG, + }).contents; + + return podfileConfig; + }); +}; + +const withBottomTabs: ConfigPlugin = (config, options) => { + config = withMaterial3Theme(config, options); + return withSVGSupport(config, options); +}; + +export default createRunOncePlugin(withBottomTabs, 'react-native-bottom-tabs');