import React, { useEffect, useId, useMemo, useState } from "react"; import { Box, Text } from "ink"; import { ScrollableList, type ListItemData } from "./List.js"; import TextInput from "./TextInput.js"; import { DialogWrapper } from "./Dialog.js"; import { useInputLayer, useLayeredInput } from "../hooks/useInputLayer.js"; import { colors } from "../theme.js"; /** * Props for the currency selection dialog. */ interface CurrencySelectionDialogProps { /** Current wallet currency from persisted settings. */ currentCurrency: string; /** Available fiat numerator symbols that can be paired with BCH. */ currencies: string[]; /** True while the dialog is loading available pairs. */ isLoading: boolean; /** Optional loading/error message for pair discovery. */ errorMessage: string | null; /** Called when the user chooses a currency and confirms. */ onSelectCurrency: (currencyCode: string) => void; /** Called when the dialog should close without applying changes. */ onCancel: () => void; } /** * Currency picker dialog. * * UX requirements: * - Arrow keys move the highlighted item. * - Typing immediately filters results. * - Enter applies current selection. * - Escape closes without saving. */ export function CurrencySelectionDialog({ currentCurrency, currencies, isLoading, errorMessage, onSelectCurrency, onCancel, }: CurrencySelectionDialogProps): React.ReactElement { const layerId = useId(); const [filterText, setFilterText] = useState(""); const [selectedIndex, setSelectedIndex] = useState(0); // Mount this as a capturing input layer so background screens stop handling keys. useInputLayer(layerId); /** * Applies the currently selected filtered result. */ const applySelection = (): void => { const selectedCurrency = filteredCurrencies[selectedIndex]; if (!selectedCurrency) { return; } onSelectCurrency(selectedCurrency); }; useLayeredInput(layerId, (_input, key) => { if (key.escape) { onCancel(); return; } if (key.upArrow) { setSelectedIndex((prev) => prev <= 0 ? Math.max(filteredCurrencies.length - 1, 0) : prev - 1, ); return; } if (key.downArrow) { setSelectedIndex((prev) => filteredCurrencies.length === 0 ? 0 : prev >= filteredCurrencies.length - 1 ? 0 : prev + 1, ); return; } }); /** * Filter currencies as the user types. */ const filteredCurrencies = useMemo(() => { const normalizedFilter = filterText.trim().toUpperCase(); if (!normalizedFilter) { return currencies; } return currencies.filter((currencyCode) => currencyCode.toUpperCase().includes(normalizedFilter), ); }, [currencies, filterText]); /** * Keep selected index valid whenever filtering shrinks the result set. */ useEffect(() => { if (filteredCurrencies.length === 0) { setSelectedIndex(0); return; } if (selectedIndex >= filteredCurrencies.length) { setSelectedIndex(filteredCurrencies.length - 1); } }, [filteredCurrencies, selectedIndex]); /** * When the dialog opens or the list updates, default to current currency. */ useEffect(() => { if (filterText.trim().length > 0) { return; } const currentIndex = filteredCurrencies.findIndex( (currencyCode) => currencyCode.toUpperCase() === currentCurrency.toUpperCase(), ); if (currentIndex >= 0) { setSelectedIndex(currentIndex); } }, [filteredCurrencies, currentCurrency, filterText]); const listItems: ListItemData[] = filteredCurrencies.map( (currencyCode) => ({ key: currencyCode, label: currencyCode, description: currencyCode.toUpperCase() === currentCurrency.toUpperCase() ? "(current)" : undefined, value: currencyCode, }), ); return ( Available BCH quote pairs are loaded from the live rates adapter. Filter: applySelection()} placeholder="Type currency code (e.g. USD, AUD)..." focus /> {isLoading ? ( Loading available pairs... ) : errorMessage ? ( {errorMessage} ) : ( applySelection()} focus={false} maxVisible={8} emptyMessage="No BCH quote pairs match this filter." /> )} Type to filter • ↑↓ navigate • Enter apply • Esc cancel ); }