From 83c802b4aee9258f343e9ad6def16dcdb9776dae Mon Sep 17 00:00:00 2001 From: Sebastian Sutter Date: Tue, 18 Aug 2026 18:25:35 +0200 Subject: [PATCH] frontend: move mobile settings into bottom navigation After moving Insurance to the Marketplace, "Settings" was now the only item inside "More", causing unnecessary friction to enter the actual settings on mobile. This moves settings directly into the menu bar, removing the redundant intermediate step. --- CHANGELOG.md | 1 + frontends/web/src/app.tsx | 4 +- .../bottom-navigation.module.css | 2 +- .../bottom-navigation/bottom-navigation.tsx | 24 ++++--- .../bottom-navigation/menu-icons.tsx | 7 -- .../bottom-navigation/utils.test.ts | 8 ++- .../src/components/bottom-navigation/utils.ts | 4 +- frontends/web/src/routes/router.tsx | 19 +++--- .../src/routes/settings/mobile-settings.tsx | 26 +++---- .../web/src/routes/settings/more.module.css | 11 --- frontends/web/src/routes/settings/more.tsx | 67 ------------------- 11 files changed, 48 insertions(+), 125 deletions(-) delete mode 100644 frontends/web/src/routes/settings/more.module.css delete mode 100644 frontends/web/src/routes/settings/more.tsx 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} - /> - -
-
-
-
-
- ); -};