diff --git a/openspec/changes/archive/2026-05-11-fix-server-settings-warning-overlap/.openspec.yaml b/openspec/changes/archive/2026-05-11-fix-server-settings-warning-overlap/.openspec.yaml new file mode 100644 index 0000000000..81cd71fe0b --- /dev/null +++ b/openspec/changes/archive/2026-05-11-fix-server-settings-warning-overlap/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-11 diff --git a/openspec/changes/archive/2026-05-11-fix-server-settings-warning-overlap/design.md b/openspec/changes/archive/2026-05-11-fix-server-settings-warning-overlap/design.md new file mode 100644 index 0000000000..0c11a553d6 --- /dev/null +++ b/openspec/changes/archive/2026-05-11-fix-server-settings-warning-overlap/design.md @@ -0,0 +1,38 @@ +## Context + +`src/screens/AppSettings/Node/NodeSettings.tsx` renders saved Electrum servers in a `FlatList` and renders the offline `WarningNote` inside the footer area that also holds the "Add New Node" button. When the list content becomes tall, the fixed footer placement allows the note to visually collide with the last `ServerItem` card. This change only affects screen layout and does not involve Redux slices, sagas, Realm schema changes, MMKV keys, PSBT flows, or hardware signers. + +## Goals / Non-Goals + +**Goals:** +- Ensure the offline warning note is displayed after the saved server list content instead of on top of the last server row +- Preserve the existing add-node button placement and warning behavior for empty and non-empty node lists +- Keep the implementation limited to the Server Settings UI and focused validation + +**Non-Goals:** +- Modify any Electrum connection or deletion behavior +- Introduce store, saga, migration, or persistence changes +- Change warning copy, navigation, or modal behavior + +## Decisions + +- Render the non-empty offline warning inside the `FlatList` flow as a footer component so it scrolls with the list content and always appears after the final `ServerItem` + - Alternative considered: increase outer footer spacing or add absolute bottom padding. Rejected because it depends on device height and does not guarantee separation from the last card. +- Keep the empty-list warning in the footer area above the add button + - Alternative considered: move all warnings into the list area. Rejected because the empty-state screen already uses a dedicated illustration area and the warning/button grouping is readable there. +- Affected files: + - `src/screens/AppSettings/Node/NodeSettings.tsx` + - `tests/components/NodeSettings.test.tsx` (if the focused test is added) + +## Risks / Trade-offs + +- [List footer spacing differs slightly from current footer spacing] → Match existing spacing with a small wrapper style around the footer warning +- [Focused screen test may require heavy mocks] → Keep the test scope narrow and skip it only if existing screen dependencies make it disproportionately invasive + +## Migration Plan + +No migration is required. This is a presentation-only change with no persisted data updates, no store version bump, and no rollback complexity beyond reverting the UI diff. + +## Open Questions + +- None diff --git a/openspec/changes/archive/2026-05-11-fix-server-settings-warning-overlap/proposal.md b/openspec/changes/archive/2026-05-11-fix-server-settings-warning-overlap/proposal.md new file mode 100644 index 0000000000..2e5f0565a1 --- /dev/null +++ b/openspec/changes/archive/2026-05-11-fix-server-settings-warning-overlap/proposal.md @@ -0,0 +1,31 @@ +## Why + +The Server Settings screen can place the offline warning note on top of the last saved Electrum server card when the list grows, which makes the Delete and Connect actions hard to read and use. This needs to be fixed now so saved server management remains readable and tappable on both mainnet and testnet screens. + +## What Changes + +- Keep the saved server warning content visually separated from server cards when no Electrum server is connected +- Update the Server Settings layout so the warning note flows after the list content instead of overlapping list actions +- Add focused validation for the Server Settings warning placement if practical within the existing Jest setup + +## Capabilities + +### New Capabilities +- `server-settings-layout`: Defines how the Server Settings screen lays out saved Electrum servers and offline guidance without overlapping actionable controls + +### Modified Capabilities + +- None + +## Impact + +- Affects both mainnet and testnet server settings flows +- No hardware signer compatibility impact +- No subscription tier gating impact +- Security/privacy impact is unchanged because this is a presentation-only change with no new key, storage, or network handling + +## Non-goals + +- Changing Electrum connection logic or saved server persistence +- Redesigning the Server Settings screen beyond the overlap fix +- Adding new node management capabilities or copy changes unrelated to spacing diff --git a/openspec/changes/archive/2026-05-11-fix-server-settings-warning-overlap/specs/server-settings-layout/spec.md b/openspec/changes/archive/2026-05-11-fix-server-settings-warning-overlap/specs/server-settings-layout/spec.md new file mode 100644 index 0000000000..f202fd1cf4 --- /dev/null +++ b/openspec/changes/archive/2026-05-11-fix-server-settings-warning-overlap/specs/server-settings-layout/spec.md @@ -0,0 +1,14 @@ +## ADDED Requirements + +### Requirement: Server settings warning placement +The Server Settings screen SHALL render offline guidance after the saved Electrum server list content so that `NodeDetail` entries and their actions remain readable and tappable when no server is connected. + +#### Scenario: Warning follows saved server cards +- **GIVEN** a user has one or more saved Electrum servers and no server is currently connected +- **WHEN** the Server Settings screen renders the saved server list +- **THEN** the offline warning SHALL appear after the final server card instead of overlapping any Delete or Connect action + +#### Scenario: Empty server state still shows guidance +- **GIVEN** a user has no saved Electrum servers and no server is currently connected +- **WHEN** the Server Settings screen renders +- **THEN** the screen SHALL continue showing offline guidance separately from the add-node action without overlapping other content diff --git a/openspec/changes/archive/2026-05-11-fix-server-settings-warning-overlap/tasks.md b/openspec/changes/archive/2026-05-11-fix-server-settings-warning-overlap/tasks.md new file mode 100644 index 0000000000..74a3cb08e5 --- /dev/null +++ b/openspec/changes/archive/2026-05-11-fix-server-settings-warning-overlap/tasks.md @@ -0,0 +1,9 @@ +## 1. UI Components + +- [x] 1.1 Update the Server Settings screen so the non-empty offline warning renders after the server list content instead of in the fixed footer +- [x] 1.2 Keep the empty-state warning and add-node button behavior intact after the layout adjustment + +## 2. Tests + +- [x] 2.1 Add or update focused validation for the Server Settings warning placement if it can be done with the existing Jest test setup +- [x] 2.2 Run targeted validation for the changed screen and record any pre-existing failures that are unrelated diff --git a/openspec/specs/server-settings-layout/spec.md b/openspec/specs/server-settings-layout/spec.md new file mode 100644 index 0000000000..40df4b1945 --- /dev/null +++ b/openspec/specs/server-settings-layout/spec.md @@ -0,0 +1,17 @@ +# server-settings-layout Specification + +## Purpose +Define how the Server Settings screen lays out saved Electrum servers and offline guidance so informational warnings do not overlap server actions. +## Requirements +### Requirement: Server settings warning placement +The Server Settings screen SHALL render offline guidance after the saved Electrum server list content so that `NodeDetail` entries and their actions remain readable and tappable when no server is connected. + +#### Scenario: Warning follows saved server cards +- **GIVEN** a user has one or more saved Electrum servers and no server is currently connected +- **WHEN** the Server Settings screen renders the saved server list +- **THEN** the offline warning SHALL appear after the final server card instead of overlapping any Delete or Connect action + +#### Scenario: Empty server state still shows guidance +- **GIVEN** a user has no saved Electrum servers and no server is currently connected +- **WHEN** the Server Settings screen renders +- **THEN** the screen SHALL continue showing offline guidance separately from the add-node action without overlapping other content diff --git a/src/screens/AppSettings/Node/NodeSettings.tsx b/src/screens/AppSettings/Node/NodeSettings.tsx index cfb104a830..34bb891981 100644 --- a/src/screens/AppSettings/Node/NodeSettings.tsx +++ b/src/screens/AppSettings/Node/NodeSettings.tsx @@ -5,7 +5,6 @@ import { hp, wp } from 'src/constants/responsive'; import { LocalizationContext } from 'src/context/Localization/LocContext'; import { useAppDispatch } from 'src/store/hooks'; import { NodeDetail } from 'src/services/wallets/interfaces'; -import KeeperHeader from 'src/components/KeeperHeader'; import ScreenWrapper from 'src/components/ScreenWrapper'; import KeeperModal from 'src/components/KeeperModal'; import useToastMessage from 'src/hooks/useToastMessage'; @@ -21,12 +20,12 @@ import DowngradeToPlebDark from 'src/assets/images/downgradetoplebDark.svg'; import Buttons from 'src/components/Buttons'; import EmptyListIllustration from 'src/components/EmptyListIllustration'; import { CommonActions, useNavigation } from '@react-navigation/native'; -import ServerItem from './components/ServerItem'; import WarningNote from 'src/components/WarningNote'; import ActivityIndicatorView from 'src/components/AppActivityIndicator/ActivityIndicatorView'; import { updateAppImage } from 'src/store/sagaActions/bhr'; import { ELECTRUM_CLIENT } from 'src/services/electrum/client'; import WalletHeader from 'src/components/WalletHeader'; +import ServerItem from './components/ServerItem'; function ElectrumDisconnectWarningContent() { const { colorMode } = useColorMode(); @@ -130,6 +129,16 @@ function NodeSettings() { } }; + const renderNodeListFooter = () => { + if (!isNoNodeConnected || isNodeListEmpty) return null; + + return ( + + + + ); + }; + return ( @@ -143,6 +152,8 @@ function NodeSettings() { item.id} + ListFooterComponent={renderNodeListFooter} renderItem={({ item }) => ( - {isNoNodeConnected ? ( - isNodeListEmpty ? ( - - ) : ( - - ) - ) : null} + {isNoNodeConnected && isNodeListEmpty ? : null} navigation.dispatch(CommonActions.navigate('NodeSelection'))} @@ -251,6 +256,9 @@ const styles = StyleSheet.create({ footerContainer: { gap: hp(30), }, + listFooterWarning: { + marginTop: hp(10), + }, }); export default NodeSettings; diff --git a/tests/components/NodeSettings.test.tsx b/tests/components/NodeSettings.test.tsx new file mode 100644 index 0000000000..446b498131 --- /dev/null +++ b/tests/components/NodeSettings.test.tsx @@ -0,0 +1,205 @@ +jest.mock('src/components/KeeperText', () => { + return ({ children }) => {children}; +}); + +import React from 'react'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import NodeSettings from '../../src/screens/AppSettings/Node/NodeSettings'; +import { render, waitFor, within } from '@testing-library/react-native'; + +let mockNodes = []; +let mockNavigationDispatch = jest.fn(); +let mockAppDispatch = jest.fn(); +const mockTranslations = { + common: { + disconnectingFromServer: 'Disconnect', + disconnectingFromServerText: 'Disconnect text', + cancel: 'Cancel', + }, + settings: { + nodeSettings: 'Server Settings', + manageElectrumServersSubtitle: 'Manage your saved Electrum servers', + noNodeWarning1: 'Please add a server to use the app.', + noNodeWarning2: 'Please connect to any server to use the app.', + addNewNode: 'Add New Node', + }, + error: { + ConnectedTo: 'Connected to', + disconnectedFrom: 'Disconnected from', + failedToDiConnect: 'Disconnect failed', + }, +}; + +jest.mock('src/context/Localization/LocContext', () => { + const React = require('react'); + + return { + LocalizationContext: React.createContext({}), + }; +}); + +jest.mock('@gluestack-ui/themed-native-base', () => { + const React = require('react'); + const { View } = require('react-native'); + + return { + Box: ({ children, ...props }) => {children}, + useColorMode: () => ({ colorMode: 'light', toggleColorMode: jest.fn() }), + }; +}); + +jest.mock('@react-navigation/native', () => ({ + CommonActions: { + navigate: jest.fn((screen) => screen), + }, + useNavigation: () => ({ + dispatch: mockNavigationDispatch, + }), +})); + +jest.mock('src/store/hooks', () => ({ + useAppDispatch: () => mockAppDispatch, +})); + +jest.mock('src/hooks/useToastMessage', () => () => ({ + showToast: jest.fn(), +})); + +jest.mock('src/store/reducers/login', () => ({ + electrumClientConnectionExecuted: jest.fn((payload) => payload), + electrumClientConnectionInitiated: jest.fn(() => ({ type: 'INIT' })), +})); + +jest.mock('src/store/sagaActions/bhr', () => ({ + updateAppImage: jest.fn((payload) => payload), +})); + +jest.mock('src/services/electrum/client', () => ({ + ELECTRUM_CLIENT: { + isClientConnected: false, + }, +})); + +jest.mock('src/services/electrum/node', () => ({ + __esModule: true, + default: { + getAllNodes: jest.fn(() => mockNodes), + nodeConnectionStatus: jest.fn((node) => node.isConnected), + disconnect: jest.fn(), + delete: jest.fn(), + update: jest.fn(), + connectToSelectedNode: jest.fn(), + }, +})); + +jest.mock('src/components/ScreenWrapper', () => ({ + __esModule: true, + default: ({ children }) => <>{children}, +})); + +jest.mock('src/components/KeeperModal', () => ({ + __esModule: true, + default: () => null, +})); + +jest.mock('src/components/WalletHeader', () => ({ + __esModule: true, + default: ({ title, subTitle }) => { + const { Text, View } = require('react-native'); + + return ( + + {title} + {subTitle} + + ); + }, +})); + +jest.mock('src/components/Buttons', () => ({ + __esModule: true, + default: ({ primaryText }) => { + const { Text } = require('react-native'); + + return {primaryText}; + }, +})); + +jest.mock('src/components/EmptyListIllustration', () => ({ + __esModule: true, + default: () => { + const { Text } = require('react-native'); + + return EmptyList; + }, +})); + +jest.mock('src/components/WarningNote', () => ({ + __esModule: true, + default: ({ noteText }) => { + const { Text } = require('react-native'); + + return {noteText}; + }, +})); + +jest.mock('src/components/AppActivityIndicator/ActivityIndicatorView', () => ({ + __esModule: true, + default: () => null, +})); + +jest.mock('../../src/screens/AppSettings/Node/components/ServerItem', () => ({ + __esModule: true, + default: ({ item }) => { + const { Text } = require('react-native'); + + return {item.host}; + }, +})); + +jest.mock('src/assets/images/toast_error.svg', () => () => null); +jest.mock('src/assets/images/icon_tick.svg', () => () => null); +jest.mock('src/assets/images/downgradetopleb.svg', () => () => null); +jest.mock('src/assets/images/downgradetoplebDark.svg', () => () => null); + +const { ELECTRUM_CLIENT: mockElectrumClient } = jest.requireMock('src/services/electrum/client'); + +const renderNodeSettings = () => + render( + + + + ); + +describe('NodeSettings', () => { + beforeEach(() => { + mockNodes = []; + mockElectrumClient.isClientConnected = false; + mockNavigationDispatch.mockClear(); + mockAppDispatch.mockClear(); + }); + + it('renders the disconnected warning inside the list footer when saved servers exist', async () => { + mockNodes = [{ id: '1', host: 'electrum.example.com', port: '50002', isConnected: false }]; + + const screen = renderNodeSettings(); + + await waitFor(() => expect(screen.getByText('electrum.example.com')).toBeTruthy()); + + expect( + within(screen.getByTestId('server-settings-warning-footer')).getByText( + mockTranslations.settings.noNodeWarning2 + ) + ).toBeTruthy(); + expect(screen.queryByText(mockTranslations.settings.noNodeWarning1)).toBeNull(); + }); + + it('keeps the empty-state warning outside the list footer when no servers exist', async () => { + const screen = renderNodeSettings(); + + await waitFor(() => expect(screen.getByText(mockTranslations.settings.noNodeWarning1)).toBeTruthy()); + + expect(screen.queryByTestId('server-settings-warning-footer')).toBeNull(); + expect(screen.queryByText(mockTranslations.settings.noNodeWarning2)).toBeNull(); + }); +});