From d2ae03d09bd9824fcdbfcfd6b7a0c781ae1ecf23 Mon Sep 17 00:00:00 2001 From: Tom McGuire Date: Fri, 8 Mar 2024 13:11:31 -0800 Subject: [PATCH] feat: add tab navigator (#5027) ### Description Adds the tab navigator and associated icons. Behavior is enabled with the `use_tab_navigator` feature gate. Please disregard the header display as this will be covered in ACT-1108. | Android | iOS 15 Pro Max - Large Font | iOS SE (3rd Generation) | | ----- | ----- | ----- | | ![](https://github.com/valora-inc/wallet/assets/26950305/e65dbbd2-c449-4b17-8b7b-904bfa3463f9 "Android Assets") | ![](https://github.com/valora-inc/wallet/assets/26950305/d74ae00e-4ae8-4ae2-9a4b-bb54377da375 "iOS 15 Pro Max Assets") | ![](https://github.com/valora-inc/wallet/assets/26950305/ee815632-29ee-46de-9145-8a9c420ac01a "iOS SE (3rd Generation) Assets") | ### Test plan - [x] Tested locally on iOS with feature flag enabled and disabled - [x] Tested locally on Android with feature flag enabled and disabled - [x] Unit tests updated ### Related issues - Fixes ACT-1103 ### Backwards compatibility Yes - behind the `use_tab_navigator` feature gate. ### Network scalability N/A --------- Co-authored-by: Satish Ravi --- locales/base/translation.json | 12 ++++ package.json | 1 + src/icons/navigator/Discover.tsx | 17 ++++++ src/icons/navigator/Home.tsx | 17 ++++++ src/icons/navigator/Wallet.tsx | 17 ++++++ src/navigator/NavigationService.ts | 8 ++- src/navigator/Navigator.tsx | 2 + src/navigator/Screens.tsx | 4 ++ src/navigator/TabNavigator.tsx | 85 +++++++++++++++++++++++++++++ src/navigator/initialRoute.test.tsx | 16 +++++- src/navigator/initialRoute.tsx | 6 +- src/navigator/types.tsx | 11 ++++ src/statsig/constants.ts | 1 + src/statsig/types.ts | 1 + src/tokens/Assets.tsx | 2 +- test/RootStateSchema.json | 4 ++ yarn.lock | 9 +++ 17 files changed, 207 insertions(+), 6 deletions(-) create mode 100644 src/icons/navigator/Discover.tsx create mode 100644 src/icons/navigator/Home.tsx create mode 100644 src/icons/navigator/Wallet.tsx create mode 100644 src/navigator/TabNavigator.tsx diff --git a/locales/base/translation.json b/locales/base/translation.json index d799cbed127..3184a144359 100644 --- a/locales/base/translation.json +++ b/locales/base/translation.json @@ -2167,5 +2167,17 @@ "description": "No more messy wallet addresses! Send crypto to other Celo users with just a phone number", "startButtonLabel": "Link Phone Number", "later": "I'll do this later" + }, + "bottomTabsNavigator": { + "wallet": { + "tabName": "Wallet", + "title": "My Wallet" + }, + "home": { + "tabName": "Home" + }, + "discover": { + "tabName": "Discover" + } } } diff --git a/package.json b/package.json index 1a9c323f82f..6d5950a1484 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "@react-native-firebase/remote-config": "^17.5.0", "@react-native-masked-view/masked-view": "^0.3.1", "@react-native-picker/picker": "^2.6.1", + "@react-navigation/bottom-tabs": "^6.5.16", "@react-navigation/devtools": "^6.0.23", "@react-navigation/drawer": "^6.6.11", "@react-navigation/elements": "^1.3.26", diff --git a/src/icons/navigator/Discover.tsx b/src/icons/navigator/Discover.tsx new file mode 100644 index 00000000000..c465d80c739 --- /dev/null +++ b/src/icons/navigator/Discover.tsx @@ -0,0 +1,17 @@ +import * as React from 'react' +import Svg, { Path } from 'react-native-svg' + +interface Props { + color?: string + size?: number +} + +const Discover = ({ color, size }: Props) => ( + + + +) +export default Discover diff --git a/src/icons/navigator/Home.tsx b/src/icons/navigator/Home.tsx new file mode 100644 index 00000000000..77309358050 --- /dev/null +++ b/src/icons/navigator/Home.tsx @@ -0,0 +1,17 @@ +import * as React from 'react' +import Svg, { Path } from 'react-native-svg' + +interface Props { + color?: string + size?: number +} + +const Home = ({ color, size }: Props) => ( + + + +) +export default Home diff --git a/src/icons/navigator/Wallet.tsx b/src/icons/navigator/Wallet.tsx new file mode 100644 index 00000000000..5d7c1db0f88 --- /dev/null +++ b/src/icons/navigator/Wallet.tsx @@ -0,0 +1,17 @@ +import * as React from 'react' +import Svg, { Path } from 'react-native-svg' + +interface Props { + color?: string + size?: number +} + +const Wallet = ({ color, size }: Props) => ( + + + +) +export default Wallet diff --git a/src/navigator/NavigationService.ts b/src/navigator/NavigationService.ts index c9a080e9967..fcb9b104ea4 100644 --- a/src/navigator/NavigationService.ts +++ b/src/navigator/NavigationService.ts @@ -20,6 +20,8 @@ import { requestPincodeInput, } from 'src/pincode/authentication' import { store } from 'src/redux/store' +import { getFeatureGate } from 'src/statsig' +import { StatsigFeatureGates } from 'src/statsig/types' import { isUserCancelledError } from 'src/storage/keychain' import { ensureError } from 'src/utils/ensureError' import Logger from 'src/utils/Logger' @@ -224,7 +226,7 @@ export async function isBottomSheetVisible(screen: Screens) { } interface NavigateHomeOptions { - params?: StackParamList[Screens.DrawerNavigator] + params?: StackParamList[Screens.DrawerNavigator] | StackParamList[Screens.TabNavigator] } /*** @@ -238,7 +240,9 @@ export function navigateHome(options?: NavigateHomeOptions) { setTimeout(() => { navigationRef.current?.reset({ index: 0, - routes: [{ name: Screens.DrawerNavigator, params }], + routes: getFeatureGate(StatsigFeatureGates.USE_TAB_NAVIGATOR) + ? [{ name: Screens.TabNavigator, params }] + : [{ name: Screens.DrawerNavigator, params }], }) }, timeout) } diff --git a/src/navigator/Navigator.tsx b/src/navigator/Navigator.tsx index 637c5bc565a..af9d0f63362 100644 --- a/src/navigator/Navigator.tsx +++ b/src/navigator/Navigator.tsx @@ -77,6 +77,7 @@ import { } from 'src/navigator/Headers' import QRNavigator from 'src/navigator/QRNavigator' import { Screens } from 'src/navigator/Screens' +import TabNavigator from 'src/navigator/TabNavigator' import { getInitialRoute } from 'src/navigator/initialRoute' import { StackParamList } from 'src/navigator/types' import NftsInfoCarousel from 'src/nfts/NftsInfoCarousel' @@ -602,6 +603,7 @@ export function MainStackScreen() { return ( + {commonScreens(Stack)} {sendScreens(Stack)} diff --git a/src/navigator/Screens.tsx b/src/navigator/Screens.tsx index 8072e09229f..bfff00273ce 100644 --- a/src/navigator/Screens.tsx +++ b/src/navigator/Screens.tsx @@ -83,6 +83,10 @@ export enum Screens { Support = 'Support', SupportContact = 'SupportContact', SwapScreenWithBack = 'SwapScreenWithBack', + TabNavigator = 'TabNavigator', + TabDiscover = 'TabDiscover', + TabHome = 'TabHome', + TabWallet = 'TabWallet', TokenDetails = 'TokenDetails', TokenDetailsMoreActions = 'TokenDetailsMoreActions', TokenImport = 'TokenImport', diff --git a/src/navigator/TabNavigator.tsx b/src/navigator/TabNavigator.tsx new file mode 100644 index 00000000000..fb8d7fa5519 --- /dev/null +++ b/src/navigator/TabNavigator.tsx @@ -0,0 +1,85 @@ +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { StyleSheet } from 'react-native' +import DAppsExplorerScreenSearchFilter from 'src/dappsExplorer/DAppsExplorerScreenSearchFilter' +import WalletHome from 'src/home/WalletHome' +import Discover from 'src/icons/navigator/Discover' +import Home from 'src/icons/navigator/Home' +import Wallet from 'src/icons/navigator/Wallet' +import { Screens } from 'src/navigator/Screens' +import { StackParamList } from 'src/navigator/types' +import Colors from 'src/styles/colors' +import { typeScale } from 'src/styles/fonts' +import { Spacing } from 'src/styles/styles' +import variables from 'src/styles/variables' +import AssetsScreen from 'src/tokens/Assets' + +const Tab = createBottomTabNavigator() + +type Props = NativeStackScreenProps + +export default function TabNavigator({ route }: Props) { + const initialScreen = route.params?.initialScreen ?? Screens.TabHome + const { t } = useTranslation() + + return ( + + + + + + ) +} + +const styles = StyleSheet.create({ + label: { + ...typeScale.labelSemiBoldSmall, + }, + tabBarItem: { + paddingVertical: Spacing.Smallest8, + }, +}) diff --git a/src/navigator/initialRoute.test.tsx b/src/navigator/initialRoute.test.tsx index d9cce7848ae..35bc35f7f98 100644 --- a/src/navigator/initialRoute.test.tsx +++ b/src/navigator/initialRoute.test.tsx @@ -3,6 +3,7 @@ import { MultichainBetaStatus } from 'src/app/actions' import { getInitialRoute } from 'src/navigator/initialRoute' import { Screens } from 'src/navigator/Screens' import { getFeatureGate } from 'src/statsig' +import { StatsigFeatureGates } from 'src/statsig/types' jest.mock('src/statsig/index') @@ -98,16 +99,27 @@ describe('initialRoute', () => { }) it('returns drawer navigator if all onboarding complete, multichain beta is opted in and feature gate is on', () => { - jest.mocked(getFeatureGate).mockReturnValue(true) + jest + .mocked(getFeatureGate) + .mockImplementation((featureGate) => featureGate !== StatsigFeatureGates.USE_TAB_NAVIGATOR) expect( getInitialRoute({ ...defaultArgs, multichainBetaStatus: MultichainBetaStatus.OptedIn }) ).toEqual(Screens.DrawerNavigator) }) it('returns drawer navigator if all onboarding complete, multichain beta is opted out and feature gate is on', () => { - jest.mocked(getFeatureGate).mockReturnValue(true) + jest + .mocked(getFeatureGate) + .mockImplementation((featureGate) => featureGate !== StatsigFeatureGates.USE_TAB_NAVIGATOR) expect( getInitialRoute({ ...defaultArgs, multichainBetaStatus: MultichainBetaStatus.OptedOut }) ).toEqual(Screens.DrawerNavigator) }) + + it('returns tab navigator if all onboarding complete, multichain beta is opted in and use_tab_navigator feature gate is on', () => { + jest.mocked(getFeatureGate).mockReturnValue(true) + expect( + getInitialRoute({ ...defaultArgs, multichainBetaStatus: MultichainBetaStatus.OptedIn }) + ).toEqual(Screens.TabNavigator) + }) }) diff --git a/src/navigator/initialRoute.tsx b/src/navigator/initialRoute.tsx index 1e2a18f32ed..4fe5ee394ff 100644 --- a/src/navigator/initialRoute.tsx +++ b/src/navigator/initialRoute.tsx @@ -45,6 +45,10 @@ export function getInitialRoute({ ) { return Screens.MultichainBeta } else { - return Screens.DrawerNavigator + if (getFeatureGate(StatsigFeatureGates.USE_TAB_NAVIGATOR)) { + return Screens.TabNavigator + } else { + return Screens.DrawerNavigator + } } } diff --git a/src/navigator/types.tsx b/src/navigator/types.tsx index 2816b84a12c..0d33798edb2 100644 --- a/src/navigator/types.tsx +++ b/src/navigator/types.tsx @@ -272,6 +272,17 @@ export type StackParamList = { } | undefined [Screens.SwapScreenWithBack]: { fromTokenId: string } | undefined + [Screens.TabDiscover]: undefined + [Screens.TabHome]: undefined + [Screens.TabWallet]: + | { + activeTab: AssetTabType + } + | undefined + [Screens.TabNavigator]: { + initialScreen?: Screens + fromModal?: boolean + } [Screens.TokenDetails]: { tokenId: string } [Screens.TokenImport]: undefined [Screens.TransactionDetailsScreen]: { diff --git a/src/statsig/constants.ts b/src/statsig/constants.ts index 6d72f0a9375..38442fed24d 100644 --- a/src/statsig/constants.ts +++ b/src/statsig/constants.ts @@ -23,6 +23,7 @@ export const FeatureGates = { [StatsigFeatureGates.SHOW_NFT_CELEBRATION]: false, [StatsigFeatureGates.SHOW_NFT_REWARD]: false, [StatsigFeatureGates.SHOW_JUMPSTART_SEND]: false, + [StatsigFeatureGates.USE_TAB_NAVIGATOR]: false, } export const ExperimentConfigs = { diff --git a/src/statsig/types.ts b/src/statsig/types.ts index c02ffa9b7e3..9b0f9d5e011 100644 --- a/src/statsig/types.ts +++ b/src/statsig/types.ts @@ -30,6 +30,7 @@ export enum StatsigFeatureGates { SHOW_NFT_CELEBRATION = 'show_nft_celebration', SHOW_NFT_REWARD = 'show_nft_reward', SHOW_JUMPSTART_SEND = 'show_jumpstart_send', + USE_TAB_NAVIGATOR = 'use_tab_navigator', } export enum StatsigExperiments { diff --git a/src/tokens/Assets.tsx b/src/tokens/Assets.tsx index dece772dca7..4c2fc02b420 100644 --- a/src/tokens/Assets.tsx +++ b/src/tokens/Assets.tsx @@ -39,7 +39,7 @@ import { useTokenPricesAreStale, useTotalTokenBalance } from 'src/tokens/hooks' import { AssetTabType } from 'src/tokens/types' import { getSupportedNetworkIdsForTokenBalances } from 'src/tokens/utils' -type Props = NativeStackScreenProps +type Props = NativeStackScreenProps // offset relative to the bottom of the non sticky header component, where the // screen header opacity animation starts diff --git a/test/RootStateSchema.json b/test/RootStateSchema.json index e4d0d44ee70..eb86427d845 100644 --- a/test/RootStateSchema.json +++ b/test/RootStateSchema.json @@ -2671,6 +2671,10 @@ "Support", "SupportContact", "SwapScreenWithBack", + "TabDiscover", + "TabHome", + "TabNavigator", + "TabWallet", "TokenDetails", "TokenDetailsMoreActions", "TokenImport", diff --git a/yarn.lock b/yarn.lock index 859e32bb44e..07987b28ce7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2986,6 +2986,15 @@ resolved "https://registry.yarnpkg.com/@react-native/polyfills/-/polyfills-2.0.0.tgz#4c40b74655c83982c8cf47530ee7dc13d957b6aa" integrity sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ== +"@react-navigation/bottom-tabs@^6.5.16": + version "6.5.16" + resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-6.5.16.tgz#8b9b2fb482cdcdfcad5146a77878a48090fb64b5" + integrity sha512-zw6hlP1YI4APbOoeJSg1JS9h3OPZIp4O2ccAkiIPgw7T6wyKs8dGJyrkkUtTryXoRUqL1D14xp6K1Etgqm7F2A== + dependencies: + "@react-navigation/elements" "^1.3.26" + color "^4.2.3" + warn-once "^0.1.0" + "@react-navigation/core@^6.4.13": version "6.4.13" resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-6.4.13.tgz#27cf33d963f59aeeb36f27514a9744ab818097b3"