ui: Keypad currency toggle modal
This commit is contained in:
@@ -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 = () => (
|
||||
<View
|
||||
style={{
|
||||
height: 1,
|
||||
backgroundColor: themeColor('separator')
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
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 (
|
||||
<View style={{ marginBottom: 8 }}>
|
||||
{BITCOIN_UNITS.map((item, index) => (
|
||||
<React.Fragment key={item.value}>
|
||||
<ListItem
|
||||
containerStyle={{
|
||||
borderBottomWidth: 0,
|
||||
backgroundColor: 'transparent'
|
||||
}}
|
||||
onPress={() =>
|
||||
this.handleSelect(item.value, 'unit')
|
||||
}
|
||||
>
|
||||
<View style={{ marginRight: 8 }}>
|
||||
<BitcoinIcon height={20} width={20} />
|
||||
</View>
|
||||
<ListItem.Content>
|
||||
<ListItem.Title
|
||||
style={{
|
||||
color:
|
||||
currentUnit === item.value
|
||||
? themeColor('highlight')
|
||||
: themeColor('text'),
|
||||
fontFamily: 'PPNeueMontreal-Book'
|
||||
}}
|
||||
>
|
||||
{item.key}
|
||||
</ListItem.Title>
|
||||
{item.value === 'sats' && (
|
||||
<ListItem.Subtitle
|
||||
style={{
|
||||
color: themeColor('secondaryText'),
|
||||
fontFamily: 'PPNeueMontreal-Book',
|
||||
fontSize: 12
|
||||
}}
|
||||
>
|
||||
100,000,000 sats = 1 BTC
|
||||
</ListItem.Subtitle>
|
||||
)}
|
||||
</ListItem.Content>
|
||||
{currentUnit === item.value && (
|
||||
<Icon
|
||||
name="check"
|
||||
color={themeColor('highlight')}
|
||||
/>
|
||||
)}
|
||||
</ListItem>
|
||||
{index < BITCOIN_UNITS.length - 1 &&
|
||||
this.renderSeparator()}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<View style={{ flex: 1 }}>
|
||||
<SearchBar
|
||||
placeholder={localeString('general.search')}
|
||||
onChangeText={this.updateSearch}
|
||||
value={search}
|
||||
inputStyle={{
|
||||
color: themeColor('text'),
|
||||
fontFamily: 'PPNeueMontreal-Book'
|
||||
}}
|
||||
placeholderTextColor={themeColor('secondaryText')}
|
||||
containerStyle={{
|
||||
backgroundColor: 'transparent',
|
||||
borderTopWidth: 0,
|
||||
borderBottomWidth: 0
|
||||
}}
|
||||
inputContainerStyle={{
|
||||
borderRadius: 15,
|
||||
backgroundColor: themeColor('secondary')
|
||||
}}
|
||||
autoCorrect={false}
|
||||
/>
|
||||
|
||||
{!search && this.renderBitcoinUnits()}
|
||||
|
||||
{!search && (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
backgroundColor: themeColor('secondary')
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: themeColor('secondaryText'),
|
||||
fontFamily: 'PPNeueMontreal-Medium',
|
||||
fontSize: 14
|
||||
}}
|
||||
>
|
||||
{localeString('views.Settings.Currency.title')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<FlatList
|
||||
data={currencies}
|
||||
renderItem={({ item }) => (
|
||||
<ListItem
|
||||
containerStyle={{
|
||||
borderBottomWidth: 0,
|
||||
backgroundColor: 'transparent'
|
||||
}}
|
||||
onPress={() =>
|
||||
this.handleSelect(item.value, 'fiat')
|
||||
}
|
||||
>
|
||||
<ListItem.Content>
|
||||
<ListItem.Title
|
||||
style={{
|
||||
color:
|
||||
currentUnit === 'fiat' &&
|
||||
currentFiat === item.value
|
||||
? themeColor('highlight')
|
||||
: themeColor('text'),
|
||||
fontFamily: 'PPNeueMontreal-Book'
|
||||
}}
|
||||
>
|
||||
{`${item.flag ? item.flag : ''} ${
|
||||
item.key
|
||||
} (${item.value})`}
|
||||
</ListItem.Title>
|
||||
{(() => {
|
||||
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 (
|
||||
<ListItem.Subtitle
|
||||
style={{
|
||||
color: themeColor(
|
||||
'secondaryText'
|
||||
),
|
||||
fontFamily:
|
||||
'PPNeueMontreal-Book',
|
||||
fontSize: 12
|
||||
}}
|
||||
>
|
||||
{rateDisplay}
|
||||
</ListItem.Subtitle>
|
||||
);
|
||||
})()}
|
||||
</ListItem.Content>
|
||||
{currentUnit === 'fiat' &&
|
||||
currentFiat === item.value && (
|
||||
<Icon
|
||||
name="check"
|
||||
color={themeColor('highlight')}
|
||||
/>
|
||||
)}
|
||||
</ListItem>
|
||||
)}
|
||||
keyExtractor={(item) => item.value}
|
||||
ItemSeparatorComponent={this.renderSeparator}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<any, any>;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
interface CurrencySelectorModalState {
|
||||
activeTab: 'currencies' | 'converter';
|
||||
}
|
||||
|
||||
export default class CurrencySelectorModal extends React.Component<
|
||||
CurrencySelectorModalProps,
|
||||
CurrencySelectorModalState
|
||||
> {
|
||||
private modalRef = React.createRef<ModalBox>();
|
||||
|
||||
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 (
|
||||
<ModalBox
|
||||
ref={this.modalRef}
|
||||
style={{
|
||||
height: SCREEN_HEIGHT * 0.9,
|
||||
backgroundColor: themeColor('background'),
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20
|
||||
}}
|
||||
swipeToClose={true}
|
||||
swipeArea={60}
|
||||
backButtonClose={true}
|
||||
backdropPressToClose={true}
|
||||
backdrop={true}
|
||||
position="bottom"
|
||||
onClosed={this.handleClose}
|
||||
coverScreen={true}
|
||||
>
|
||||
<View style={{ flex: 1, paddingTop: 8 }}>
|
||||
<View
|
||||
style={{
|
||||
width: 40,
|
||||
height: 4,
|
||||
backgroundColor: themeColor('secondaryText'),
|
||||
borderRadius: 2,
|
||||
alignSelf: 'center',
|
||||
marginBottom: 16
|
||||
}}
|
||||
/>
|
||||
|
||||
<ToggleButton
|
||||
options={TAB_OPTIONS}
|
||||
value={activeTab}
|
||||
onToggle={this.handleTabToggle}
|
||||
/>
|
||||
|
||||
<View style={{ flex: 1, marginTop: 16 }}>
|
||||
<CurrencyList onSelect={this.handleCurrencySelect} />
|
||||
</View>
|
||||
</View>
|
||||
</ModalBox>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<UnitToggleProps, {}> {
|
||||
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 (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'column',
|
||||
marginRight: 4
|
||||
}}
|
||||
>
|
||||
{flags.slice(0, 3).map((f, idx) => (
|
||||
<Text
|
||||
key={idx}
|
||||
style={{
|
||||
fontSize: 8,
|
||||
lineHeight: 10
|
||||
}}
|
||||
>
|
||||
{f}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
<Text
|
||||
style={{ color: themeColor('buttonText') }}
|
||||
>
|
||||
{fiat}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return `${flag} ${fiat}`;
|
||||
}
|
||||
return fiat;
|
||||
}
|
||||
return units;
|
||||
};
|
||||
|
||||
const handlePress = () => {
|
||||
if (fiatEnabled && onOpenModal) {
|
||||
onOpenModal();
|
||||
} else {
|
||||
if (onToggle) onToggle();
|
||||
changeUnits();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Button
|
||||
title={units === 'fiat' ? fiat : units}
|
||||
title={getDisplayTitle()}
|
||||
icon={{
|
||||
name: 'import-export',
|
||||
size: 25,
|
||||
@@ -35,9 +106,12 @@ export default class UnitToggle extends React.Component<UnitToggleProps, {}> {
|
||||
adaptiveWidth
|
||||
quaternary
|
||||
noUppercase
|
||||
onPress={() => {
|
||||
if (onToggle) onToggle();
|
||||
changeUnits();
|
||||
onPress={handlePress}
|
||||
buttonStyle={{
|
||||
height: 38,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 0
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<ModalBox>();
|
||||
private currencySelectorModalRef = React.createRef<CurrencySelectorModal>();
|
||||
|
||||
handleOpenCurrencyModal = () => {
|
||||
this.currencySelectorModalRef.current?.open();
|
||||
};
|
||||
|
||||
handleCurrencyModalClose = () => {
|
||||
this.clearValue();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
@@ -503,7 +513,10 @@ export default class KeypadPane extends React.PureComponent<
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<UnitToggle onToggle={this.clearValue} />
|
||||
<UnitToggle
|
||||
onToggle={this.clearValue}
|
||||
onOpenModal={this.handleOpenCurrencyModal}
|
||||
/>
|
||||
</Row>
|
||||
</Animated.View>
|
||||
</View>
|
||||
@@ -784,6 +797,11 @@ export default class KeypadPane extends React.PureComponent<
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ModalBox>
|
||||
<CurrencySelectorModal
|
||||
ref={this.currencySelectorModalRef}
|
||||
navigation={navigation}
|
||||
onClose={this.handleCurrencyModalClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user