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 (
diff --git a/stores/UnitsStore.ts b/stores/UnitsStore.ts
index c81aaed5a..04ee38a2f 100644
--- a/stores/UnitsStore.ts
+++ b/stores/UnitsStore.ts
@@ -29,6 +29,11 @@ export default class UnitsStore {
await Storage.setItem(UNIT_KEY, this.units);
};
+ public setUnits = async (unit: Units | string) => {
+ this.units = unit;
+ await Storage.setItem(UNIT_KEY, this.units);
+ };
+
public getNextUnit = () => {
const { settings } = this.settingsStore;
const { fiatEnabled } = settings;
diff --git a/views/Wallet/KeypadPane.tsx b/views/Wallet/KeypadPane.tsx
index 3bdaea48e..a035a9af1 100644
--- a/views/Wallet/KeypadPane.tsx
+++ b/views/Wallet/KeypadPane.tsx
@@ -17,6 +17,7 @@ import EcashMintPicker from '../../components/EcashMintPicker';
import EcashToggle from '../../components/EcashToggle';
import ModalBox from '../../components/ModalBox';
import UnitToggle from '../../components/UnitToggle';
+import CurrencySelectorModal from '../../components/CurrencySelectorModal';
import WalletHeader from '../../components/WalletHeader';
import { getSatAmount } from '../../components/AmountInput';
import { Row } from '../../components/layout/Row';
@@ -337,6 +338,15 @@ export default class KeypadPane extends React.PureComponent<
};
private modalBoxRef = React.createRef();
+ private currencySelectorModalRef = React.createRef();
+
+ handleOpenCurrencyModal = () => {
+ this.currencySelectorModalRef.current?.open();
+ };
+
+ handleCurrencyModalClose = () => {
+ this.clearValue();
+ };
render() {
const {
@@ -503,7 +513,10 @@ export default class KeypadPane extends React.PureComponent<
)}
>
)}
-
+
@@ -784,6 +797,11 @@ export default class KeypadPane extends React.PureComponent<
+
>
);
}