diff --git a/app/src/components/settings/SettingsHome.tsx b/app/src/components/settings/SettingsHome.tsx
index 66a6b526b..97b4f507d 100644
--- a/app/src/components/settings/SettingsHome.tsx
+++ b/app/src/components/settings/SettingsHome.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { ReactNode, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useCoreState } from '../../providers/CoreStateProvider';
@@ -15,6 +15,29 @@ import SettingsHeader from './components/SettingsHeader';
import SettingsMenuItem from './components/SettingsMenuItem';
import { useSettingsNavigation } from './hooks/useSettingsNavigation';
+interface SettingsSection {
+ label: string;
+ items: SettingsItem[];
+}
+
+interface SettingsItem {
+ id: string;
+ title: string;
+ description: string;
+ icon: ReactNode;
+ onClick: () => void;
+ dangerous?: boolean;
+}
+
+// Subtle uppercase section header label separating settings groups
+const SectionHeader = ({ label }: { label: string }) => (
+
+
+ {label}
+
+
+);
+
const SettingsHome = () => {
const navigate = useNavigate();
const { navigateToSettings } = useSettingsNavigation();
@@ -93,157 +116,185 @@ const SettingsHome = () => {
}
};
- // const handleViewEncryptionKey = () => {
- // // TODO: Show encryption key in a secure modal
- // console.log('View encryption key');
- // };
-
- const groupedMenuItems = [
+ const settingsSections: SettingsSection[] = [
{
- id: 'account',
- title: 'Account',
- description: 'Recovery phrase, team, connections, and privacy',
- icon: (
-
- ),
- onClick: () => navigateToSettings('account'),
- dangerous: false,
+ label: 'General',
+ items: [
+ {
+ id: 'account',
+ title: 'Account',
+ description: 'Recovery phrase, team, connections, and privacy',
+ icon: (
+
+ ),
+ onClick: () => navigateToSettings('account'),
+ },
+ {
+ id: 'notifications',
+ title: 'Notifications',
+ description: 'Do Not Disturb and per-account notification controls',
+ icon: (
+
+ ),
+ onClick: () => navigateToSettings('notifications'),
+ },
+ ],
},
{
- id: 'billing',
- title: 'Billing & Usage',
- description: 'Subscription plan, credits, and payment methods',
- icon: (
-
- ),
- onClick: () => {
- void openUrl(BILLING_DASHBOARD_URL);
- },
- dangerous: false,
+ label: 'Features & AI',
+ items: [
+ {
+ id: 'features',
+ title: 'Features',
+ description: 'Screen awareness, messaging, and tools',
+ icon: (
+
+ ),
+ onClick: () => navigateToSettings('features'),
+ },
+ {
+ id: 'ai-models',
+ title: 'AI & Models',
+ description: 'Local AI model setup and downloads',
+ icon: (
+
+ ),
+ onClick: () => navigateToSettings('ai-models'),
+ },
+ ],
},
{
- id: 'features',
- title: 'Features',
- description: 'Screen awareness, messaging, and tools',
- icon: (
-
- ),
- onClick: () => navigateToSettings('features'),
- dangerous: false,
+ label: 'Billing & Rewards',
+ items: [
+ {
+ id: 'billing',
+ title: 'Billing & Usage',
+ description: 'Subscription plan, credits, and payment methods',
+ icon: (
+
+ ),
+ onClick: () => {
+ void openUrl(BILLING_DASHBOARD_URL);
+ },
+ },
+ {
+ id: 'rewards',
+ title: 'Rewards',
+ description: 'Referrals, coupons, and earned credits',
+ icon: (
+
+ ),
+ onClick: () => navigate('/rewards'),
+ },
+ ],
},
{
- id: 'ai-models',
- title: 'AI & Models',
- description: 'Local AI model setup and downloads',
- icon: (
-
- ),
- onClick: () => navigateToSettings('ai-models'),
- dangerous: false,
+ label: 'Support',
+ items: [
+ {
+ id: 'restart-tour',
+ title: 'Restart Tour',
+ description: 'Replay the product walkthrough from the beginning',
+ icon: (
+
+ ),
+ onClick: () => {
+ resetWalkthrough();
+ navigate('/home');
+ },
+ },
+ {
+ id: 'about',
+ title: 'About',
+ description: 'App version and software updates',
+ icon: (
+
+ ),
+ onClick: () => navigateToSettings('about'),
+ },
+ ],
},
{
- id: 'notifications',
- title: 'Notifications',
- description: 'Do Not Disturb and per-account notification controls',
- icon: (
-
- ),
- onClick: () => navigateToSettings('notifications'),
- dangerous: false,
- },
- {
- id: 'restart-tour',
- title: 'Restart Tour',
- description: 'Replay the product walkthrough from the beginning',
- icon: (
-
- ),
- onClick: () => {
- resetWalkthrough();
- navigate('/home');
- },
- dangerous: false,
- },
- {
- id: 'about',
- title: 'About',
- description: 'App version and software updates',
- icon: (
-
- ),
- onClick: () => navigateToSettings('about'),
- dangerous: false,
- },
- {
- id: 'developer-options',
- title: 'Developer Options',
- description: 'Diagnostics, debug panels, webhooks, and memory inspection',
- icon: (
-
- ),
- onClick: () => navigateToSettings('developer-options'),
- dangerous: false,
+ label: 'Advanced',
+ items: [
+ {
+ id: 'developer-options',
+ title: 'Developer Options',
+ description: 'Diagnostics, debug panels, webhooks, and memory inspection',
+ icon: (
+
+ ),
+ onClick: () => navigateToSettings('developer-options'),
+ },
+ ],
},
];
- // Destructive actions menu items
- const destructiveMenuItems = [
+ // Destructive actions — rendered separately under "Danger Zone" heading
+ const destructiveItems: SettingsItem[] = [
{
id: 'logout-and-clear',
title: 'Clear App Data',
@@ -287,22 +338,28 @@ const SettingsHome = () => {
- {/* Grouped Settings */}
- {groupedMenuItems.map((item, index) => (
-
+ {/* Grouped sections with section headers */}
+ {settingsSections.map(section => (
+
+
+ {section.items.map((item, index) => (
+
+ ))}
+
))}
- {/* Destructive Actions */}
- {destructiveMenuItems.map((item, index) => (
+ {/* Danger Zone */}
+
+ {destructiveItems.map((item, index) => (
{
onClick={item.onClick}
dangerous={item.dangerous}
isFirst={index === 0}
- isLast={index === destructiveMenuItems.length - 1}
+ isLast={index === destructiveItems.length - 1}
/>
))}
diff --git a/app/src/components/settings/__tests__/SettingsHome.test.tsx b/app/src/components/settings/__tests__/SettingsHome.test.tsx
new file mode 100644
index 000000000..560e57203
--- /dev/null
+++ b/app/src/components/settings/__tests__/SettingsHome.test.tsx
@@ -0,0 +1,248 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { MemoryRouter } from 'react-router-dom';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import SettingsHome from '../SettingsHome';
+
+// --- hoisted mocks ---
+
+const { mockNavigate, mockNavigateToSettings } = vi.hoisted(() => ({
+ mockNavigate: vi.fn(),
+ mockNavigateToSettings: vi.fn(),
+}));
+
+vi.mock('react-router-dom', async importOriginal => {
+ const actual = await importOriginal();
+ return { ...actual, useNavigate: () => mockNavigate };
+});
+
+vi.mock('../hooks/useSettingsNavigation', () => ({
+ useSettingsNavigation: () => ({ navigateToSettings: mockNavigateToSettings }),
+}));
+
+vi.mock('../../../providers/CoreStateProvider', () => ({
+ useCoreState: () => ({
+ clearSession: vi.fn().mockResolvedValue(undefined),
+ snapshot: { auth: { userId: null }, currentUser: null },
+ }),
+}));
+
+vi.mock('../../../store', () => ({ persistor: { purge: vi.fn().mockResolvedValue(undefined) } }));
+
+vi.mock('../../../utils/links', () => ({ BILLING_DASHBOARD_URL: 'https://billing.example.com' }));
+
+vi.mock('../../../utils/openUrl', () => ({ openUrl: vi.fn() }));
+
+vi.mock('../../../utils/tauriCommands', () => ({
+ resetOpenHumanDataAndRestartCore: vi.fn().mockResolvedValue(undefined),
+ restartApp: vi.fn().mockResolvedValue(undefined),
+ scheduleCefProfilePurge: vi.fn().mockResolvedValue(undefined),
+}));
+
+vi.mock('../../walkthrough/AppWalkthrough', () => ({ resetWalkthrough: vi.fn() }));
+
+// --- helpers ---
+
+function renderSettingsHome() {
+ return render(
+
+
+
+ );
+}
+
+// --- tests ---
+
+describe('SettingsHome', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('section headers', () => {
+ it('renders the General section header', () => {
+ renderSettingsHome();
+ expect(screen.getByText('General')).toBeInTheDocument();
+ });
+
+ it('renders the Features & AI section header', () => {
+ renderSettingsHome();
+ expect(screen.getByText('Features & AI')).toBeInTheDocument();
+ });
+
+ it('renders the Billing & Rewards section header', () => {
+ renderSettingsHome();
+ expect(screen.getByText('Billing & Rewards')).toBeInTheDocument();
+ });
+
+ it('renders the Support section header', () => {
+ renderSettingsHome();
+ expect(screen.getByText('Support')).toBeInTheDocument();
+ });
+
+ it('renders the Advanced section header', () => {
+ renderSettingsHome();
+ expect(screen.getByText('Advanced')).toBeInTheDocument();
+ });
+
+ it('renders the Danger Zone section header', () => {
+ renderSettingsHome();
+ expect(screen.getByText('Danger Zone')).toBeInTheDocument();
+ });
+ });
+
+ describe('item grouping order', () => {
+ it('places Account and Notifications under General', () => {
+ renderSettingsHome();
+ const generalHeader = screen.getByText('General');
+ const accountItem = screen.getByText('Account');
+ const notificationsItem = screen.getByText('Notifications');
+
+ // All should appear after the General header in DOM order
+ expect(generalHeader.compareDocumentPosition(accountItem)).toBe(
+ Node.DOCUMENT_POSITION_FOLLOWING
+ );
+ expect(generalHeader.compareDocumentPosition(notificationsItem)).toBe(
+ Node.DOCUMENT_POSITION_FOLLOWING
+ );
+ });
+
+ it('places Features, AI & Models under Features & AI', () => {
+ renderSettingsHome();
+ const header = screen.getByText('Features & AI');
+ expect(header.compareDocumentPosition(screen.getByText('Features'))).toBe(
+ Node.DOCUMENT_POSITION_FOLLOWING
+ );
+ expect(header.compareDocumentPosition(screen.getByText('AI & Models'))).toBe(
+ Node.DOCUMENT_POSITION_FOLLOWING
+ );
+ });
+
+ it('places Billing & Usage and Rewards under Billing & Rewards', () => {
+ renderSettingsHome();
+ const header = screen.getByText('Billing & Rewards');
+ expect(header.compareDocumentPosition(screen.getByText('Billing & Usage'))).toBe(
+ Node.DOCUMENT_POSITION_FOLLOWING
+ );
+ expect(header.compareDocumentPosition(screen.getByText('Rewards'))).toBe(
+ Node.DOCUMENT_POSITION_FOLLOWING
+ );
+ });
+
+ it('places Restart Tour and About under Support', () => {
+ renderSettingsHome();
+ const header = screen.getByText('Support');
+ expect(header.compareDocumentPosition(screen.getByText('Restart Tour'))).toBe(
+ Node.DOCUMENT_POSITION_FOLLOWING
+ );
+ expect(header.compareDocumentPosition(screen.getByText('About'))).toBe(
+ Node.DOCUMENT_POSITION_FOLLOWING
+ );
+ });
+
+ it('places Developer Options under Advanced', () => {
+ renderSettingsHome();
+ const header = screen.getByText('Advanced');
+ expect(header.compareDocumentPosition(screen.getByText('Developer Options'))).toBe(
+ Node.DOCUMENT_POSITION_FOLLOWING
+ );
+ });
+
+ it('places Clear App Data and Log out under Danger Zone', () => {
+ renderSettingsHome();
+ const header = screen.getByText('Danger Zone');
+ expect(header.compareDocumentPosition(screen.getByText('Clear App Data'))).toBe(
+ Node.DOCUMENT_POSITION_FOLLOWING
+ );
+ expect(header.compareDocumentPosition(screen.getByText('Log out'))).toBe(
+ Node.DOCUMENT_POSITION_FOLLOWING
+ );
+ });
+ });
+
+ describe('Rewards menu item', () => {
+ it('renders the Rewards item', () => {
+ renderSettingsHome();
+ expect(screen.getByText('Rewards')).toBeInTheDocument();
+ });
+
+ it('navigates to /rewards when clicked', async () => {
+ const user = userEvent.setup();
+ renderSettingsHome();
+
+ // The Rewards item description is used to find the right button
+ const rewardsButton = screen.getByText('Rewards').closest('button');
+ expect(rewardsButton).toBeTruthy();
+ await user.click(rewardsButton!);
+
+ expect(mockNavigate).toHaveBeenCalledWith('/rewards');
+ });
+ });
+
+ describe('existing navigation items', () => {
+ it('navigates to account settings when Account is clicked', async () => {
+ const user = userEvent.setup();
+ renderSettingsHome();
+
+ await user.click(screen.getByText('Account').closest('button')!);
+ expect(mockNavigateToSettings).toHaveBeenCalledWith('account');
+ });
+
+ it('navigates to notifications settings when Notifications is clicked', async () => {
+ const user = userEvent.setup();
+ renderSettingsHome();
+
+ await user.click(screen.getByText('Notifications').closest('button')!);
+ expect(mockNavigateToSettings).toHaveBeenCalledWith('notifications');
+ });
+
+ it('navigates to /home when Restart Tour is clicked', async () => {
+ const user = userEvent.setup();
+ renderSettingsHome();
+
+ await user.click(screen.getByText('Restart Tour').closest('button')!);
+ expect(mockNavigate).toHaveBeenCalledWith('/home');
+ });
+
+ it('navigates to features settings when Features is clicked', async () => {
+ const user = userEvent.setup();
+ renderSettingsHome();
+
+ await user.click(screen.getByText('Features').closest('button')!);
+ expect(mockNavigateToSettings).toHaveBeenCalledWith('features');
+ });
+
+ it('navigates to ai-models settings when AI & Models is clicked', async () => {
+ const user = userEvent.setup();
+ renderSettingsHome();
+
+ await user.click(screen.getByText('AI & Models').closest('button')!);
+ expect(mockNavigateToSettings).toHaveBeenCalledWith('ai-models');
+ });
+
+ it('opens billing URL when Billing & Usage is clicked', async () => {
+ const { openUrl } = await import('../../../utils/openUrl');
+ const user = userEvent.setup();
+ renderSettingsHome();
+
+ await user.click(screen.getByText('Billing & Usage').closest('button')!);
+ expect(openUrl).toHaveBeenCalledWith('https://billing.example.com');
+ });
+
+ it('navigates to about settings when About is clicked', async () => {
+ const user = userEvent.setup();
+ renderSettingsHome();
+
+ await user.click(screen.getByText('About').closest('button')!);
+ expect(mockNavigateToSettings).toHaveBeenCalledWith('about');
+ });
+
+ it('navigates to developer-options settings when Developer Options is clicked', async () => {
+ const user = userEvent.setup();
+ renderSettingsHome();
+
+ await user.click(screen.getByText('Developer Options').closest('button')!);
+ expect(mockNavigateToSettings).toHaveBeenCalledWith('developer-options');
+ });
+ });
+});