diff --git a/CHANGELOG.md b/CHANGELOG.md index 41c86bf4d3..6a37c0ab42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Bitcoin: display zero amounts without decimal places - Add option to navigate to "Used addresses" in sign-message workflow - Floating mobile bottom navigation bar +- Mobile: Move settings into bottom navigation ## v4.51.4 - Bundle BitBox02 and BitBox02 Nova firmware version v9.26.5 diff --git a/frontends/web/src/app.tsx b/frontends/web/src/app.tsx index 61dd5c5742..fb56144713 100644 --- a/frontends/web/src/app.tsx +++ b/frontends/web/src/app.tsx @@ -92,6 +92,7 @@ const AppFrame = ({ activeAccounts={activeAccounts} devices={devices} devicesKey={devicesKey} + showBottomNavigation={showMobileBottomNavigation} /> @@ -150,9 +151,6 @@ export const App = () => { || currentURL.startsWith('/add-account') || currentURL.startsWith('/settings/manage-accounts') || currentURL.startsWith('/accounts/') - // Workaround on mobile where the bottom menu is not shown when there are no devices/accounts. - // If one is on "More" and the bottom menu disappears, one is stuck. - || currentURL === '/settings/more' )) { navigate('/'); return; diff --git a/frontends/web/src/components/bottom-navigation/bottom-navigation.module.css b/frontends/web/src/components/bottom-navigation/bottom-navigation.module.css index 9cc2296fca..2e8a8141c3 100644 --- a/frontends/web/src/components/bottom-navigation/bottom-navigation.module.css +++ b/frontends/web/src/components/bottom-navigation/bottom-navigation.module.css @@ -93,7 +93,7 @@ position: relative; } -.moreLabel { +.settingsLabel { align-items: center; display: inline-flex; position: relative; diff --git a/frontends/web/src/components/bottom-navigation/bottom-navigation.tsx b/frontends/web/src/components/bottom-navigation/bottom-navigation.tsx index d942e87ae8..17a398237b 100644 --- a/frontends/web/src/components/bottom-navigation/bottom-navigation.tsx +++ b/frontends/web/src/components/bottom-navigation/bottom-navigation.tsx @@ -4,11 +4,12 @@ import { useTranslation } from 'react-i18next'; import { Link, useLocation } from 'react-router-dom'; import type { TAccount } from '@/api/account'; import type { TDevices } from '@/api/devices'; -import { AccountIconSVG, MarketIconSVG, MoreIconSVG, PortfolioIconSVG } from '@/components/bottom-navigation/menu-icons'; +import { AccountIconSVG, MarketIconSVG, PortfolioIconSVG } from '@/components/bottom-navigation/menu-icons'; import { useLoad } from '@/hooks/api'; import { getVersion } from '@/api/bitbox02'; -import { RedDot } from '@/components/icon'; +import { CogBlue, CogDark, CogLight, RedDot } from '@/components/icon'; import { NewBadge } from '@/components/new-badge/new-badge'; +import { useDarkmode } from '@/hooks/darkmode'; import { useAndroidKeyboardVisible } from './use-android-keyboard-visible'; import { useSlidingIndicator } from './use-sliding-indicator'; import { getBottomNavIndex, getBottomNavKey } from './utils'; @@ -24,6 +25,7 @@ export const BottomNavigation = ({ devices, }: Props) => { const { t } = useTranslation(); + const { isDarkMode } = useDarkmode(); const { pathname } = useLocation(); const deviceID = Object.keys(devices)[0]; const isBitBox02 = deviceID && devices[deviceID] === 'bitbox02'; @@ -35,19 +37,21 @@ export const BottomNavigation = ({ const accountLabel = onlyHasOneAccount ? t('account.account') : t('account.accounts'); const portfolioLabel = t('accountSummary.portfolio'); const marketLabel = t('generic.buySell'); - const moreLabel = t('settings.more'); + const settingsLabel = t('sidebar.settings'); const bottomNavKey = getBottomNavKey(pathname); const portfolioActive = bottomNavKey === 'portfolio'; const accountsActive = bottomNavKey === 'accounts'; const marketActive = bottomNavKey === 'market'; - const moreActive = bottomNavKey === 'more'; + const settingsActive = bottomNavKey === 'settings'; + const InactiveSettingsIcon = isDarkMode ? CogLight : CogDark; + const SettingsIcon = settingsActive ? CogBlue : InactiveSettingsIcon; const activeIndex = getBottomNavIndex(bottomNavKey); const { containerRef, indicatorStyle, labelRefs, - } = useSlidingIndicator(activeIndex, `${portfolioLabel}:${accountLabel}:${marketLabel}:${moreLabel}`); + } = useSlidingIndicator(activeIndex, `${portfolioLabel}:${accountLabel}:${marketLabel}:${settingsLabel}`); const androidKeyboardVisible = useAndroidKeyboardVisible(); if (androidKeyboardVisible) { @@ -110,14 +114,14 @@ export const BottomNavigation = ({ - - + + labelRefs.current[3] = element}> - {moreLabel} + {settingsLabel} {canUpgrade && ( ( ); - -export const MoreIconSVG = () => ( - - - - -); diff --git a/frontends/web/src/components/bottom-navigation/utils.test.ts b/frontends/web/src/components/bottom-navigation/utils.test.ts index 4436113fbc..860cc212b8 100644 --- a/frontends/web/src/components/bottom-navigation/utils.test.ts +++ b/frontends/web/src/components/bottom-navigation/utils.test.ts @@ -14,6 +14,12 @@ describe('getBottomNavKey', () => { expect(getBottomNavKey('/market/bitrefill/spend/btc')).toBe('market'); expect(getBottomNavKey('/market/pocket/buy/btc')).toBe('market'); }); + + it('maps settings routes to the settings tab', () => { + expect(getBottomNavKey('/settings')).toBe('settings'); + expect(getBottomNavKey('/settings/general')).toBe('settings'); + expect(getBottomNavKey('/settings/device-settings/deviceID')).toBe('settings'); + }); }); describe('getBottomNavIndex', () => { @@ -21,7 +27,7 @@ describe('getBottomNavIndex', () => { expect(getBottomNavIndex('portfolio')).toBe(0); expect(getBottomNavIndex('accounts')).toBe(1); expect(getBottomNavIndex('market')).toBe(2); - expect(getBottomNavIndex('more')).toBe(3); + expect(getBottomNavIndex('settings')).toBe(3); expect(getBottomNavIndex('other')).toBeUndefined(); }); }); diff --git a/frontends/web/src/components/bottom-navigation/utils.ts b/frontends/web/src/components/bottom-navigation/utils.ts index 214a267a15..2f01f154a0 100644 --- a/frontends/web/src/components/bottom-navigation/utils.ts +++ b/frontends/web/src/components/bottom-navigation/utils.ts @@ -3,7 +3,7 @@ import type { TAccount } from '@/api/account'; import type { TDevices } from '@/api/devices'; -const bottomNavKeys = ['portfolio', 'accounts', 'market', 'more'] as const; +const bottomNavKeys = ['portfolio', 'accounts', 'market', 'settings'] as const; export type TBottomNavItem = typeof bottomNavKeys[number]; export type TBottomNavKey = TBottomNavItem | 'other'; @@ -23,7 +23,7 @@ export const getBottomNavKey = (pathname: string): TBottomNavKey => { return 'market'; } if (pathname.startsWith('/settings')) { - return 'more'; + return 'settings'; } return 'other'; }; diff --git a/frontends/web/src/routes/router.tsx b/frontends/web/src/routes/router.tsx index 2dbf4a31c1..530b33b35a 100644 --- a/frontends/web/src/routes/router.tsx +++ b/frontends/web/src/routes/router.tsx @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 import React, { ReactChild } from 'react'; -import { Route, Routes, useParams } from 'react-router-dom'; +import { Navigate, Route, Routes, useParams } from 'react-router-dom'; import { TAccount } from '@/api/account'; import { TDevices } from '@/api/devices'; import { AddAccount } from './account/add/add-account'; @@ -42,13 +42,13 @@ import { BitsuranceDashboard } from './bitsurance/dashboard'; import { ConnectScreenWalletConnect } from './account/walletconnect/connect'; import { DashboardWalletConnect } from './account/walletconnect/dashboard'; import { AllAccounts } from '@/routes/accounts/all-accounts'; -import { More } from '@/routes/settings/more'; type TAppRouterProps = { devices: TDevices; accounts: TAccount[]; activeAccounts: TAccount[]; devicesKey: ((input: string) => string); + showBottomNavigation: boolean; }; type TInjectParamsProps = { @@ -60,7 +60,13 @@ const InjectParams = ({ children }: TInjectParamsProps) => { return React.cloneElement(children as React.ReactElement, params); }; -export const AppRouter = ({ devices, devicesKey, accounts, activeAccounts }: TAppRouterProps) => { +export const AppRouter = ({ + devices, + devicesKey, + accounts, + activeAccounts, + showBottomNavigation, +}: TAppRouterProps) => { const hasAccounts = accounts.length > 0; const Homepage = ( ); - const MoreEl = ( - - ); - const GeneralEl = ( - + } /> diff --git a/frontends/web/src/routes/settings/mobile-settings.tsx b/frontends/web/src/routes/settings/mobile-settings.tsx index d2ec726446..376e7dd4e2 100644 --- a/frontends/web/src/routes/settings/mobile-settings.tsx +++ b/frontends/web/src/routes/settings/mobile-settings.tsx @@ -7,10 +7,14 @@ import { Tabs, WithSettingsTabs } from './components/tabs'; import { TPagePropsWithSettingsTabs } from './types'; import { ContentWrapper } from '@/components/contentwrapper/contentwrapper'; import { GlobalBanners } from '@/components/banners'; -import { useBackNavigation } from '@/contexts/BackNavigationContext'; import { useOnlyVisitableOnMobile } from '@/hooks/onlyvisitableonmobile'; import { MobileHeader } from '@/routes/settings/components/mobile-header'; import { useNavigate } from 'react-router-dom'; + +type TProps = TPagePropsWithSettingsTabs & { + showBottomNavigation: boolean; +}; + /** * The "index" page of the settings * that will only be shown on Mobile. @@ -19,22 +23,10 @@ import { useNavigate } from 'react-router-dom'; * we see on Desktop, as it's the equivalent * of "tabs" on Mobile. **/ -export const MobileSettings = ({ devices, hasAccounts }: TPagePropsWithSettingsTabs) => { +export const MobileSettings = ({ devices, hasAccounts, showBottomNavigation }: TProps) => { const { t } = useTranslation(); const navigate = useNavigate(); - const { goBack } = useBackNavigation(); useOnlyVisitableOnMobile('/settings/general'); - const handleClick = () => { - if (goBack()) { - return; - } - // go to home page if no devices or accounts (waiting.tsx will be shown) - if (Object.keys(devices).length === 0 && !hasAccounts) { - navigate('/'); - } else { - navigate('/settings/more'); - } - }; return ( @@ -42,7 +34,11 @@ export const MobileSettings = ({ devices, hasAccounts }: TPagePropsWithSettingsT + navigate('/')} + title={t('settings.title')} + variant={showBottomNavigation ? 'titleOnly' : 'back'} + /> } /> diff --git a/frontends/web/src/routes/settings/more.module.css b/frontends/web/src/routes/settings/more.module.css deleted file mode 100644 index 5752c89b6e..0000000000 --- a/frontends/web/src/routes/settings/more.module.css +++ /dev/null @@ -1,11 +0,0 @@ -.container { - display: flex; - flex-direction: column; -} - -.item { - display: flex; - align-items: center; - gap: 10px; - position: relative; -} \ No newline at end of file diff --git a/frontends/web/src/routes/settings/more.tsx b/frontends/web/src/routes/settings/more.tsx deleted file mode 100644 index 47be5a5519..0000000000 --- a/frontends/web/src/routes/settings/more.tsx +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; -import { View, ViewContent } from '@/components/view/view'; -import { GuidedContent, GuideWrapper, Header, Main } from '@/components/layout'; -import { ContentWrapper } from '@/components/contentwrapper/contentwrapper'; -import { GlobalBanners } from '@/components/banners'; -import { SettingsItem } from '@/routes/settings/components/settingsItem/settingsItem'; -import { useOnlyVisitableOnMobile } from '@/hooks/onlyvisitableonmobile'; -import { useDarkmode } from '@/hooks/darkmode'; -import { CogDark, CogLight } from '@/components/icon'; -import { TDevices } from '@/api/devices'; -import { useLoad } from '@/hooks/api'; -import { getVersion } from '@/api/bitbox02'; -import styles from './more.module.css'; - -/** - * This component will only be shown on mobile. - **/ - -type Props = { - devices: TDevices; -}; - -export const More = ({ devices }: Props) => { - const navigate = useNavigate(); - const { t } = useTranslation(); - const { isDarkMode } = useDarkmode(); - useOnlyVisitableOnMobile('/settings/general'); - const deviceID = Object.keys(devices)[0]; - const isBitBox02 = deviceID && devices[deviceID] === 'bitbox02'; - const versionInfo = useLoad(isBitBox02 ? () => getVersion(deviceID) : null, [deviceID, isBitBox02]); - const canUpgrade = versionInfo ? versionInfo.canUpgrade : false; - - return ( - - - - - - - {t('settings.more')}} /> - - - - - {isDarkMode - ? - : } - {t('sidebar.settings')} - - } - onClick={() => navigate('/settings')} - canUpgrade={canUpgrade} - /> - - - - - - - ); -};