diff --git a/components/CurrencyList.tsx b/components/CurrencyList.tsx new file mode 100644 index 000000000..bdb95bfa7 --- /dev/null +++ b/components/CurrencyList.tsx @@ -0,0 +1,282 @@ +import * as React from 'react'; +import { FlatList, View, Text } from 'react-native'; +import { Icon, ListItem, SearchBar } from '@rneui/themed'; +import { inject, observer } from 'mobx-react'; + +import SettingsStore, { + CURRENCY_KEYS, + DEFAULT_FIAT, + DEFAULT_FIAT_RATES_SOURCE +} from '../stores/SettingsStore'; +import UnitsStore from '../stores/UnitsStore'; +import FiatStore from '../stores/FiatStore'; + +import { localeString } from '../utils/LocaleUtils'; +import { themeColor } from '../utils/ThemeUtils'; +import { numberWithCommas, numberWithDecimals } from '../utils/UnitsUtils'; + +import BitcoinIcon from '../assets/images/SVG/bitcoin-icon.svg'; + +interface CurrencyListProps { + SettingsStore?: SettingsStore; + UnitsStore?: UnitsStore; + FiatStore?: FiatStore; + onSelect: (currency: string, type: 'unit' | 'fiat') => void; +} + +interface CurrencyListState { + search: string; + currencies: typeof CURRENCY_KEYS; + fiatRatesSource: string; +} + +const BITCOIN_UNITS = [ + { key: 'Satoshis (sats)', value: 'sats' }, + { key: 'Bitcoin (BTC)', value: 'BTC' } +]; + +@inject('SettingsStore', 'UnitsStore', 'FiatStore') +@observer +export default class CurrencyList extends React.Component< + CurrencyListProps, + CurrencyListState +> { + constructor(props: CurrencyListProps) { + super(props); + this.state = { + search: '', + currencies: CURRENCY_KEYS, + fiatRatesSource: DEFAULT_FIAT_RATES_SOURCE + }; + } + + renderSeparator = () => ( + + ); + + updateSearch = (value: string) => { + const result = CURRENCY_KEYS.filter((item: any) => { + const currencyString = `${item.flag ? item.flag : ''} ${item.key} ${ + item.value ? `(${item.value})` : '' + }`; + return currencyString.toLowerCase().includes(value.toLowerCase()); + }); + this.setState({ + search: value, + currencies: result + }); + }; + + handleSelect = async (value: string, type: 'unit' | 'fiat') => { + const { SettingsStore, UnitsStore, onSelect } = this.props; + + if (type === 'unit') { + await UnitsStore!.setUnits(value); + } else { + await SettingsStore!.updateSettings({ fiat: value }); + await UnitsStore!.setUnits('fiat'); + } + + onSelect(value, type); + }; + + renderBitcoinUnits = () => { + const { UnitsStore } = this.props; + const currentUnit = UnitsStore!.units; + + return ( + + {BITCOIN_UNITS.map((item, index) => ( + + + this.handleSelect(item.value, 'unit') + } + > + + + + + + {item.key} + + {item.value === 'sats' && ( + + 100,000,000 sats = 1 BTC + + )} + + {currentUnit === item.value && ( + + )} + + {index < BITCOIN_UNITS.length - 1 && + this.renderSeparator()} + + ))} + + ); + }; + + render() { + const { SettingsStore, UnitsStore, FiatStore } = this.props; + const { search, fiatRatesSource } = this.state; + const currentUnit = UnitsStore!.units; + const currentFiat = SettingsStore!.settings.fiat || DEFAULT_FIAT; + const fiatRates = FiatStore?.fiatRates; + + const currencies = [...this.state.currencies] + .sort((a, b) => a.key.localeCompare(b.key)) + .filter((c) => c.supportedSources?.includes(fiatRatesSource)); + + return ( + + + + {!search && this.renderBitcoinUnits()} + + {!search && ( + + + {localeString('views.Settings.Currency.title')} + + + )} + + ( + + this.handleSelect(item.value, 'fiat') + } + > + + + {`${item.flag ? item.flag : ''} ${ + item.key + } (${item.value})`} + + {(() => { + const rateEntry = fiatRates?.find( + (r) => r.code === item.value + ); + if (!rateEntry) return null; + const { + symbol, + space, + rtl, + separatorSwap + } = FiatStore!.symbolLookup(item.value); + const formattedRate = separatorSwap + ? numberWithDecimals(rateEntry.rate) + : numberWithCommas(rateEntry.rate); + const rateDisplay = rtl + ? `${formattedRate}${ + space ? ' ' : '' + }${symbol} BTC/${item.value}` + : `${symbol}${ + space ? ' ' : '' + }${formattedRate} BTC/${item.value}`; + return ( + + {rateDisplay} + + ); + })()} + + {currentUnit === 'fiat' && + currentFiat === item.value && ( + + )} + + )} + keyExtractor={(item) => item.value} + ItemSeparatorComponent={this.renderSeparator} + /> + + ); + } +} diff --git a/components/CurrencySelectorModal.tsx b/components/CurrencySelectorModal.tsx new file mode 100644 index 000000000..e349c6370 --- /dev/null +++ b/components/CurrencySelectorModal.tsx @@ -0,0 +1,124 @@ +import * as React from 'react'; +import { View, Dimensions } from 'react-native'; +import { StackNavigationProp } from '@react-navigation/stack'; + +import ModalBox from './ModalBox'; +import ToggleButton from './ToggleButton'; +import CurrencyList from './CurrencyList'; + +import { localeString } from '../utils/LocaleUtils'; +import { themeColor } from '../utils/ThemeUtils'; + +const { height: SCREEN_HEIGHT } = Dimensions.get('window'); + +interface CurrencySelectorModalProps { + navigation?: StackNavigationProp; + onClose?: () => void; +} + +interface CurrencySelectorModalState { + activeTab: 'currencies' | 'converter'; +} + +export default class CurrencySelectorModal extends React.Component< + CurrencySelectorModalProps, + CurrencySelectorModalState +> { + private modalRef = React.createRef(); + + constructor(props: CurrencySelectorModalProps) { + super(props); + this.state = { + activeTab: 'currencies' + }; + } + + open = () => { + this.modalRef.current?.open(); + }; + + close = () => { + this.modalRef.current?.close(); + }; + + handleClose = () => { + const { onClose } = this.props; + if (onClose) onClose(); + this.setState({ activeTab: 'currencies' }); + }; + + handleTabToggle = (key: string) => { + if (key === 'converter') { + // Navigate to the existing CurrencyConverter screen + const { navigation } = this.props; + if (navigation) { + this.close(); + navigation.navigate('CurrencyConverter'); + } + } else { + this.setState({ activeTab: key as 'currencies' | 'converter' }); + } + }; + + handleCurrencySelect = (_currency: string, _type: 'unit' | 'fiat') => { + this.close(); + }; + + render() { + const { activeTab } = this.state; + + const TAB_OPTIONS = [ + { + key: 'currencies', + label: localeString('views.Settings.Currency.title') + }, + { + key: 'converter', + label: localeString('views.Settings.CurrencyConverter.title') + } + ]; + + return ( + + + + + + + + + + + + ); + } +} diff --git a/components/UnitToggle.tsx b/components/UnitToggle.tsx index 79c054054..8a0224c0b 100644 --- a/components/UnitToggle.tsx +++ b/components/UnitToggle.tsx @@ -1,10 +1,11 @@ import * as React from 'react'; +import { View, Text } from 'react-native'; import { inject, observer } from 'mobx-react'; import Button from '../components/Button'; import UnitsStore from '../stores/UnitsStore'; -import SettingsStore from '../stores/SettingsStore'; +import SettingsStore, { CURRENCY_KEYS } from '../stores/SettingsStore'; import { themeColor } from '../utils/ThemeUtils'; @@ -12,21 +13,91 @@ interface UnitToggleProps { UnitsStore?: UnitsStore; SettingsStore?: SettingsStore; onToggle?: () => void; + onOpenModal?: () => void; } +const getFlagForFiat = (fiatCode: string): string => { + const currency = CURRENCY_KEYS.find((c) => c.value === fiatCode); + return currency?.flag || ''; +}; + +// Split a string of multiple flag emojis into individual flags +const splitFlags = (flags: string): string[] => { + if (!flags) return []; + const flagRegex = /\p{Regional_Indicator}{2}/gu; + const matches = flags.match(flagRegex); + return matches || [flags]; +}; + @inject('UnitsStore', 'SettingsStore') @observer export default class UnitToggle extends React.Component { render() { - const { UnitsStore, SettingsStore, onToggle } = this.props; + const { UnitsStore, SettingsStore, onToggle, onOpenModal } = this.props; const { changeUnits, units } = UnitsStore!; const { settings } = SettingsStore!; - const { fiat } = settings; + const { fiat, fiatEnabled } = settings; + + const getDisplayTitle = (): string | React.ReactElement => { + if (units === 'fiat' && fiat) { + const flag = getFlagForFiat(fiat); + if (flag) { + const flags = splitFlags(flag); + if (flags.length > 1) { + // Multiple flags - stack them vertically + return ( + + + {flags.slice(0, 3).map((f, idx) => ( + + {f} + + ))} + + + {fiat} + + + ); + } + return `${flag} ${fiat}`; + } + return fiat; + } + return units; + }; + + const handlePress = () => { + if (fiatEnabled && onOpenModal) { + onOpenModal(); + } else { + if (onToggle) onToggle(); + changeUnits(); + } + }; return (