From c18e5ef04f16a56e8dcb622887ccfdd910d668a2 Mon Sep 17 00:00:00 2001 From: Cyrus Gray <144336577+graycyrus@users.noreply.github.com> Date: Sat, 20 Jun 2026 00:06:42 +0530 Subject: [PATCH] feat(shell): surface wallet at a prominent location (#3836) --- .../layout/shell/CollapsedNavRail.test.tsx | 23 ++++++- .../layout/shell/CollapsedNavRail.tsx | 18 +++++ .../layout/shell/SidebarHeader.test.tsx | 69 +++++++++++++++++++ .../components/layout/shell/SidebarHeader.tsx | 18 +++++ app/src/components/layout/shell/navIcons.tsx | 11 +++ app/src/lib/i18n/ar.ts | 1 + app/src/lib/i18n/bn.ts | 1 + app/src/lib/i18n/de.ts | 1 + app/src/lib/i18n/en.ts | 1 + app/src/lib/i18n/es.ts | 1 + app/src/lib/i18n/fr.ts | 1 + app/src/lib/i18n/hi.ts | 1 + app/src/lib/i18n/id.ts | 1 + app/src/lib/i18n/it.ts | 1 + app/src/lib/i18n/ko.ts | 1 + app/src/lib/i18n/pl.ts | 1 + app/src/lib/i18n/pt.ts | 1 + app/src/lib/i18n/ru.ts | 1 + app/src/lib/i18n/zh-CN.ts | 1 + 19 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 app/src/components/layout/shell/SidebarHeader.test.tsx diff --git a/app/src/components/layout/shell/CollapsedNavRail.test.tsx b/app/src/components/layout/shell/CollapsedNavRail.test.tsx index 93ac0c8ed..dd33aac00 100644 --- a/app/src/components/layout/shell/CollapsedNavRail.test.tsx +++ b/app/src/components/layout/shell/CollapsedNavRail.test.tsx @@ -19,10 +19,11 @@ vi.mock('../../../services/analytics', () => ({ trackEvent: vi.fn() })); describe('CollapsedNavRail', () => { beforeEach(() => vi.clearAllMocks()); - it('renders Home plus every primary nav destination as an icon button', () => { + it('renders Home, Wallet, and every primary nav destination as icon buttons', () => { renderWithProviders(, { initialEntries: ['/home'] }); for (const key of [ 'nav.home', + 'nav.wallet', 'nav.chat', 'nav.human', 'nav.brain', @@ -33,6 +34,26 @@ describe('CollapsedNavRail', () => { } }); + it('wallet button navigates to /settings/wallet-balances', () => { + renderWithProviders(, { initialEntries: ['/home'] }); + fireEvent.click(screen.getByRole('button', { name: 'nav.wallet' })); + expect(mockNavigate).toHaveBeenCalledWith('/settings/wallet-balances'); + }); + + it('wallet button has correct data-analytics-id', () => { + renderWithProviders(, { initialEntries: ['/home'] }); + expect(screen.getByRole('button', { name: 'nav.wallet' })).toHaveAttribute( + 'data-analytics-id', + 'collapsed-rail-wallet' + ); + }); + + it('wallet button is marked active when on /settings/wallet-balances', () => { + renderWithProviders(, { initialEntries: ['/settings/wallet-balances'] }); + const btn = screen.getByRole('button', { name: 'nav.wallet' }); + expect(btn.className).toMatch(/bg-white|dark:bg-neutral-800/); + }); + it('navigates to a destination path when its icon is clicked', () => { renderWithProviders(, { initialEntries: ['/home'] }); fireEvent.click(screen.getByRole('button', { name: 'nav.brain' })); diff --git a/app/src/components/layout/shell/CollapsedNavRail.tsx b/app/src/components/layout/shell/CollapsedNavRail.tsx index 4a5ce1a87..a828f4f04 100644 --- a/app/src/components/layout/shell/CollapsedNavRail.tsx +++ b/app/src/components/layout/shell/CollapsedNavRail.tsx @@ -67,6 +67,24 @@ export default function CollapsedNavRail() { + {/* Wallet shortcut — mirrors SidebarHeader wallet button for collapsed state. */} + navigate('/settings/wallet-balances')} + title={t('nav.wallet')} + aria-label={t('nav.wallet')} + aria-current={ + matchActive('/settings/wallet-balances', location.pathname) ? 'page' : undefined + } + data-analytics-id="collapsed-rail-wallet" + className={`${RAIL_BTN} ${ + matchActive('/settings/wallet-balances', location.pathname) + ? 'bg-white text-stone-900 shadow-sm dark:bg-neutral-800 dark:text-neutral-100' + : 'text-stone-500 hover:bg-stone-100 hover:text-stone-700 dark:text-neutral-400 dark:hover:bg-neutral-800/60 dark:hover:text-neutral-200' + }`}> + + + {/* Primary nav destinations */} {tabs.map(tab => { const active = matchActive(tab.path, location.pathname); diff --git a/app/src/components/layout/shell/SidebarHeader.test.tsx b/app/src/components/layout/shell/SidebarHeader.test.tsx new file mode 100644 index 000000000..e8ba41253 --- /dev/null +++ b/app/src/components/layout/shell/SidebarHeader.test.tsx @@ -0,0 +1,69 @@ +import { fireEvent, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { renderWithProviders } from '../../../test/test-utils'; +import SidebarHeader from './SidebarHeader'; + +const mockNavigate = vi.fn(); +const mockHome = vi.fn(); +const mockHide = vi.fn(); + +vi.mock('react-router-dom', async importOriginal => { + const actual = await importOriginal(); + return { ...actual, useNavigate: () => mockNavigate }; +}); +vi.mock('./useHomeNav', () => ({ useHomeNav: () => mockHome })); +vi.mock('./RootShellLayout', () => ({ useRootSidebar: () => ({ hide: mockHide }) })); +// Return i18n keys verbatim so queries don't depend on locale. +vi.mock('../../../lib/i18n/I18nContext', () => ({ useT: () => ({ t: (k: string) => k }) })); + +describe('SidebarHeader', () => { + beforeEach(() => vi.clearAllMocks()); + + it('renders Home, Wallet, Settings, and Collapse buttons', () => { + renderWithProviders(, { initialEntries: ['/home'] }); + expect(screen.getByRole('button', { name: 'nav.home' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'nav.wallet' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'nav.settings' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'chat.hideSidebar' })).toBeInTheDocument(); + }); + + it('wallet button navigates to /settings/wallet-balances', () => { + renderWithProviders(, { initialEntries: ['/home'] }); + fireEvent.click(screen.getByRole('button', { name: 'nav.wallet' })); + expect(mockNavigate).toHaveBeenCalledWith('/settings/wallet-balances'); + }); + + it('wallet button has correct data-analytics-id', () => { + renderWithProviders(, { initialEntries: ['/home'] }); + expect(screen.getByRole('button', { name: 'nav.wallet' })).toHaveAttribute( + 'data-analytics-id', + 'sidebar-header-wallet' + ); + }); + + it('wallet button has matching aria-label and title', () => { + renderWithProviders(, { initialEntries: ['/home'] }); + const btn = screen.getByRole('button', { name: 'nav.wallet' }); + expect(btn).toHaveAttribute('aria-label', 'nav.wallet'); + expect(btn).toHaveAttribute('title', 'nav.wallet'); + }); + + it('settings button navigates to /settings', () => { + renderWithProviders(, { initialEntries: ['/home'] }); + fireEvent.click(screen.getByRole('button', { name: 'nav.settings' })); + expect(mockNavigate).toHaveBeenCalledWith('/settings'); + }); + + it('Home button invokes the shared Home action', () => { + renderWithProviders(, { initialEntries: ['/home'] }); + fireEvent.click(screen.getByRole('button', { name: 'nav.home' })); + expect(mockHome).toHaveBeenCalledTimes(1); + }); + + it('Collapse button calls hide()', () => { + renderWithProviders(, { initialEntries: ['/home'] }); + fireEvent.click(screen.getByRole('button', { name: 'chat.hideSidebar' })); + expect(mockHide).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app/src/components/layout/shell/SidebarHeader.tsx b/app/src/components/layout/shell/SidebarHeader.tsx index c5901588b..9b7ab3075 100644 --- a/app/src/components/layout/shell/SidebarHeader.tsx +++ b/app/src/components/layout/shell/SidebarHeader.tsx @@ -37,6 +37,24 @@ export default function SidebarHeader() { + {/* Wallet shortcut — one-click access to wallet balances. */} + navigate('/settings/wallet-balances')} + className={ICON_BTN} + data-analytics-id="sidebar-header-wallet" + aria-label={t('nav.wallet')} + title={t('nav.wallet')}> + + + + + navigate('/settings')} diff --git a/app/src/components/layout/shell/navIcons.tsx b/app/src/components/layout/shell/navIcons.tsx index 0f0421d50..34e211413 100644 --- a/app/src/components/layout/shell/navIcons.tsx +++ b/app/src/components/layout/shell/navIcons.tsx @@ -97,6 +97,17 @@ export function NavIcon({ id, className = 'w-5 h-5' }: NavIconProps) { /> ); + case 'wallet': + return ( + + + + ); case 'brain': return ( diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index 700516307..8fb0a83b5 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -40,6 +40,7 @@ const messages: TranslationMap = { 'nav.activity': 'النشاط', 'nav.brain': 'الدماغ', 'nav.agentWorld': 'Tiny.Place', + 'nav.wallet': 'المحفظة', 'agentWorld.description': 'Tiny.Place شبكة اجتماعية لوكلاء الذكاء الاصطناعي. استخدم OpenHuman للتفاعل والعثور على الوظائف ونشرها والتداول والنمو معًا.', 'agentWorld.feed': 'التغذية', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index e4b398c00..982ca063d 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -40,6 +40,7 @@ const messages: TranslationMap = { 'nav.activity': 'কার্যকলাপ', 'nav.brain': 'ব্রেইন', 'nav.agentWorld': 'Tiny.Place', + 'nav.wallet': 'ওয়ালেট', 'agentWorld.description': 'Tiny.Place হলো এআই এজেন্টদের জন্য একটি সোশ্যাল নেটওয়ার্ক। যোগাযোগ করতে, কাজ খুঁজতে ও পোস্ট করতে, লেনদেন করতে এবং একসাথে বেড়ে উঠতে OpenHuman ব্যবহার করুন।', 'agentWorld.feed': 'ফিড', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index 7eb73eb01..0ed17f678 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -40,6 +40,7 @@ const messages: TranslationMap = { 'nav.activity': 'Aktivität', 'nav.brain': 'Gehirn', 'nav.agentWorld': 'Tiny.Place', + 'nav.wallet': 'Wallet', 'agentWorld.description': 'Tiny.Place ist ein soziales Netzwerk für KI-Agenten. Nutze OpenHuman, um zu interagieren, Jobs zu finden und zu veröffentlichen, zu handeln und gemeinsam zu wachsen.', 'agentWorld.feed': 'Feed', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index 0a32882c0..00f2c59ac 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -24,6 +24,7 @@ const en: TranslationMap = { 'nav.activity': 'Activity', 'nav.brain': 'Brain', 'nav.agentWorld': 'Tiny.Place', + 'nav.wallet': 'Wallet', // Agent World section sub-navigation labels 'agentWorld.description': 'Tiny.Place is a social network for AI agents. Use OpenHuman to interact, find and post jobs, trade, and grow together.', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index 85abe6ea9..e88461abf 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -40,6 +40,7 @@ const messages: TranslationMap = { 'nav.activity': 'Actividad', 'nav.brain': 'Cerebro', 'nav.agentWorld': 'Tiny.Place', + 'nav.wallet': 'Cartera', 'agentWorld.description': 'Tiny.Place es una red social para agentes de IA. Usa OpenHuman para interactuar, encontrar y publicar trabajos, comerciar y crecer juntos.', 'agentWorld.feed': 'Noticias', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 998636d0f..3b7242df8 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -40,6 +40,7 @@ const messages: TranslationMap = { 'nav.activity': 'Activité', 'nav.brain': 'Cerveau', 'nav.agentWorld': 'Tiny.Place', + 'nav.wallet': 'Portefeuille', 'agentWorld.description': 'Tiny.Place est un réseau social pour les agents IA. Utilisez OpenHuman pour interagir, trouver et publier des missions, échanger et grandir ensemble.', 'agentWorld.feed': 'Fil', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index a9e9a40b6..a99a077e6 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -40,6 +40,7 @@ const messages: TranslationMap = { 'nav.activity': 'गतिविधि', 'nav.brain': 'ब्रेन', 'nav.agentWorld': 'Tiny.Place', + 'nav.wallet': 'वॉलेट', 'agentWorld.description': 'Tiny.Place एआई एजेंट्स के लिए एक सोशल नेटवर्क है। बातचीत करने, काम खोजने और पोस्ट करने, व्यापार करने और साथ मिलकर आगे बढ़ने के लिए OpenHuman का उपयोग करें।', 'agentWorld.feed': 'फ़ीड', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 35780dcf0..355ea6920 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -40,6 +40,7 @@ const messages: TranslationMap = { 'nav.activity': 'Aktivitas', 'nav.brain': 'Otak', 'nav.agentWorld': 'Tiny.Place', + 'nav.wallet': 'Dompet', 'agentWorld.description': 'Tiny.Place adalah jejaring sosial untuk agen AI. Gunakan OpenHuman untuk berinteraksi, menemukan dan memasang pekerjaan, berdagang, serta tumbuh bersama.', 'agentWorld.feed': 'Feed', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index cc2a0c55a..be3a5a795 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -40,6 +40,7 @@ const messages: TranslationMap = { 'nav.activity': 'Attività', 'nav.brain': 'Cervello', 'nav.agentWorld': 'Tiny.Place', + 'nav.wallet': 'Portafoglio', 'agentWorld.description': 'Tiny.Place è un social network per agenti IA. Usa OpenHuman per interagire, trovare e pubblicare lavori, scambiare e crescere insieme.', 'agentWorld.feed': 'Feed', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 063c66ba1..dadae98a2 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -40,6 +40,7 @@ const messages: TranslationMap = { 'nav.activity': '활동', 'nav.brain': '브레인', 'nav.agentWorld': 'Tiny.Place', + 'nav.wallet': '지갑', 'agentWorld.description': 'Tiny.Place는 AI 에이전트를 위한 소셜 네트워크입니다. OpenHuman을 사용해 소통하고, 일자리를 찾고 올리며, 거래하고 함께 성장하세요.', 'agentWorld.feed': '피드', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 0b870ff97..d7ff4abdf 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -40,6 +40,7 @@ const messages: TranslationMap = { 'nav.activity': 'Aktywność', 'nav.brain': 'Mózg', 'nav.agentWorld': 'Tiny.Place', + 'nav.wallet': 'Portfel', 'agentWorld.description': 'Tiny.Place to sieć społecznościowa dla agentów AI. Używaj OpenHuman, aby wchodzić w interakcje, znajdować i publikować zlecenia, handlować i wspólnie się rozwijać.', 'agentWorld.feed': 'Kanał', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 65e87b0df..d39a594e5 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -40,6 +40,7 @@ const messages: TranslationMap = { 'nav.activity': 'Atividade', 'nav.brain': 'Cérebro', 'nav.agentWorld': 'Tiny.Place', + 'nav.wallet': 'Carteira', 'agentWorld.description': 'Tiny.Place é uma rede social para agentes de IA. Use o OpenHuman para interagir, encontrar e publicar trabalhos, negociar e crescer juntos.', 'agentWorld.feed': 'Feed', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index fcf0fb255..544fd3648 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -40,6 +40,7 @@ const messages: TranslationMap = { 'nav.activity': 'Активность', 'nav.brain': 'Мозг', 'nav.agentWorld': 'Tiny.Place', + 'nav.wallet': 'Кошелёк', 'agentWorld.description': 'Tiny.Place — это социальная сеть для ИИ-агентов. Используйте OpenHuman, чтобы взаимодействовать, находить и публиковать задания, торговать и расти вместе.', 'agentWorld.feed': 'Лента', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 6ee3304b3..f83cfb6f7 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -40,6 +40,7 @@ const messages: TranslationMap = { 'nav.activity': '动态', 'nav.brain': '大脑', 'nav.agentWorld': 'Tiny.Place', + 'nav.wallet': '钱包', 'agentWorld.description': 'Tiny.Place 是面向 AI 智能体的社交网络。使用 OpenHuman 来互动、查找和发布工作、交易并共同成长。', 'agentWorld.feed': '动态',