diff --git a/Navigation.ts b/Navigation.ts
index ef4e0aed1..5e84f0de8 100644
--- a/Navigation.ts
+++ b/Navigation.ts
@@ -104,6 +104,8 @@ import AddNotes from './views/AddNotes';
import Contacts from './views/Settings/Contacts';
import AddContact from './views/Settings/AddContact';
import ContactDetails from './views/ContactDetails';
+import CurrencyConverter from './views/Settings/CurrencyConverter';
+
// POS
import Order from './views/Order';
@@ -423,6 +425,9 @@ const AppScenes = {
ContactQR: {
screen: ContactQR
},
+ CurrencyConverter: {
+ screen: CurrencyConverter
+ },
ChannelsSettings: {
screen: ChannelsSettings
},
diff --git a/assets/images/SVG/bitcoin-icon.svg b/assets/images/SVG/bitcoin-icon.svg
new file mode 100644
index 000000000..68f758d5d
--- /dev/null
+++ b/assets/images/SVG/bitcoin-icon.svg
@@ -0,0 +1,6 @@
+
diff --git a/locales/en.json b/locales/en.json
index bc9939dc6..7ad696424 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -1001,6 +1001,8 @@
"views.Settings.Attestations.invalidAttestation": "Invalid attestation",
"views.Settings.Nostr.addRelay": "Add relay",
"views.Settings.Nostr.relays": "Relays",
+ "views.Settings.CurrencyConverter.title": "Currency Converter",
+ "views.Settings.CurrencyConverter.enterAmount": "Enter amount",
"views.LspExplanation.title": "What are these fees?",
"views.LspExplanation.text1": "Zeus is a self-custodial lightning wallet. In order to send or receive a lightning payment, you must open a lightning payment channel, which has a setup fee.",
"views.LspExplanation.text2": "Once the channel is set up, you'll only have to pay normal network fees until your channel exhausts its capacity.",
@@ -1035,4 +1037,4 @@
"time.1H": "1H",
"time.1D": "1D",
"time.1W": "1W"
-}
+}
\ No newline at end of file
diff --git a/stores/FiatStore.ts b/stores/FiatStore.ts
index 311231f19..249d08263 100644
--- a/stores/FiatStore.ts
+++ b/stores/FiatStore.ts
@@ -24,6 +24,13 @@ export default class FiatStore {
@observable public loading = false;
@observable public error = false;
+ @observable public numberWithCommas = (x: string | number) =>
+ x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
+
+ @observable public numberWithDecimals = (x: string | number) =>
+ this.numberWithCommas(x).replace(/[,.]/g, (y: string) =>
+ y === ',' ? '.' : ','
+ );
private sourceOfCurrentFiatRates: string | undefined;
getFiatRatesToken: any;
@@ -34,17 +41,11 @@ export default class FiatStore {
this.settingsStore = settingsStore;
}
- numberWithCommas = (x: string | number) =>
- x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
-
- numberWithDecimals = (x: string | number) =>
- this.numberWithCommas(x).replace(/[,.]/g, (y: string) =>
- y === ',' ? '.' : ','
- );
-
// Resource below may be helpful for formatting
// https://fastspring.com/blog/how-to-format-30-currencies-from-countries-all-over-the-world/
- symbolLookup = (symbol: string): CurrencyDisplayRules => {
+ @observable public symbolLookup = (
+ symbol: string
+ ): CurrencyDisplayRules => {
const symbolPairs: any = {
USD: {
symbol: '$',
diff --git a/views/Settings/Currency.tsx b/views/Settings/Currency.tsx
index 4bf9896bf..c87763afe 100644
--- a/views/Settings/Currency.tsx
+++ b/views/Settings/Currency.tsx
@@ -183,6 +183,29 @@ export default class Currency extends React.Component<
color={themeColor('secondaryText')}
/>
+ navigation.navigate('CurrencyConverter')}
+ >
+
+
+ {localeString(
+ 'views.Settings.CurrencyConverter.title'
+ )}
+
+
+
+
);
diff --git a/views/Settings/CurrencyConverter.tsx b/views/Settings/CurrencyConverter.tsx
new file mode 100644
index 000000000..984fb8a56
--- /dev/null
+++ b/views/Settings/CurrencyConverter.tsx
@@ -0,0 +1,649 @@
+import * as React from 'react';
+import { observer, inject } from 'mobx-react';
+import {
+ TouchableOpacity,
+ View,
+ StyleSheet,
+ Animated,
+ Easing
+} from 'react-native';
+import Svg, { Text } from 'react-native-svg';
+import DragList, { DragListRenderItemInfo } from 'react-native-draglist';
+import { Icon } from 'react-native-elements';
+import EncryptedStorage from 'react-native-encrypted-storage';
+
+import Screen from '../../components/Screen';
+import Header from '../../components/Header';
+import TextInput from '../../components/TextInput';
+import { ErrorMessage } from '../../components/SuccessErrorMessage';
+import { Row } from '../../components/layout/Row';
+
+import { themeColor } from '../../utils/ThemeUtils';
+import { localeString } from '../../utils/LocaleUtils';
+import FiatStore from '../../stores/FiatStore';
+import SettingsStore, { CURRENCY_KEYS } from '../../stores/SettingsStore';
+
+import Add from '../../assets/images/SVG/Add.svg';
+import Edit from '../../assets/images/SVG/Pen.svg';
+import DragDots from '../../assets/images/SVG/DragDots.svg';
+import BitcoinIcon from '../../assets/images/SVG/bitcoin-icon.svg';
+
+interface CurrencyConverterProps {
+ navigation: any;
+ FiatStore?: FiatStore;
+ SettingsStore?: SettingsStore;
+}
+
+interface CurrencyConverterState {
+ inputValues: { [key: string]: string };
+ selectedCurrency: string;
+ editMode: boolean;
+ fadeAnim: Animated.Value;
+}
+
+@inject('FiatStore', 'SettingsStore')
+@observer
+export default class CurrencyConverter extends React.Component<
+ CurrencyConverterProps,
+ CurrencyConverterState
+> {
+ constructor(props: CurrencyConverterProps) {
+ super(props);
+ this.state = {
+ inputValues: {
+ BTC: '',
+ sats: ''
+ },
+ selectedCurrency: '',
+ editMode: false,
+ fadeAnim: new Animated.Value(0)
+ };
+ }
+
+ componentDidMount() {
+ const { navigation } = this.props;
+ this.retrieveInputValues();
+ this.addDefaultCurrenciesToStorage();
+ const selectedCurrency = navigation.getParam('selectedCurrency', '');
+ if (selectedCurrency) {
+ this.handleCurrencySelect(selectedCurrency);
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ const { navigation } = this.props;
+ const selectedCurrency = navigation.getParam('selectedCurrency', '');
+ const prevSelectedCurrency = prevProps.navigation.getParam(
+ 'selectedCurrency',
+ ''
+ );
+
+ // Check if the selected currency prop has changed
+ if (selectedCurrency !== prevSelectedCurrency) {
+ this.handleCurrencySelect(selectedCurrency);
+ }
+ }
+
+ addDefaultCurrenciesToStorage = async () => {
+ try {
+ const { inputValues } = this.state;
+
+ const inputValuesString = await EncryptedStorage.getItem(
+ 'currency-codes'
+ );
+ const existingInputValues = inputValuesString
+ ? JSON.parse(inputValuesString)
+ : {};
+
+ // Add default currencies from state to existing inputValues
+ for (const currency of Object.keys(inputValues)) {
+ if (!existingInputValues.hasOwnProperty(currency)) {
+ existingInputValues[currency] = '';
+ }
+ }
+
+ // Save updated inputValues to storage
+ await EncryptedStorage.setItem(
+ 'currency-codes',
+ JSON.stringify(existingInputValues)
+ );
+
+ // Update the state with the updated inputValues
+ this.setState({ inputValues: existingInputValues });
+ } catch (error) {
+ console.error('Error adding default currencies:', error);
+ }
+ };
+
+ saveInputValues = async () => {
+ try {
+ await EncryptedStorage.setItem(
+ 'currency-codes',
+ JSON.stringify(this.state.inputValues)
+ );
+ } catch (error) {
+ console.error('Error saving input values:', error);
+ }
+ };
+
+ retrieveInputValues = async () => {
+ try {
+ const inputValuesString = await EncryptedStorage.getItem(
+ 'currency-codes'
+ );
+ if (inputValuesString) {
+ const inputValues = JSON.parse(inputValuesString);
+ this.setState({ inputValues });
+ }
+ } catch (error) {
+ console.error('Error retrieving input values:', error);
+ }
+ };
+
+ handleCurrencySelect = (currency: string) => {
+ const { inputValues } = this.state;
+
+ if (!inputValues.hasOwnProperty(currency)) {
+ const updatedInputValues = { ...inputValues, [currency]: '' };
+ this.setState(
+ {
+ inputValues: updatedInputValues,
+ selectedCurrency: currency
+ },
+ () => {
+ this.saveInputValues();
+ }
+ );
+ } else {
+ this.setState({ selectedCurrency: currency });
+ }
+ };
+
+ handleInputChange = (value: string, currency: string) => {
+ const sanitizedValue = value.replace(/,/g, '').replace(/[^\d.]/g, '');
+
+ const { inputValues } = this.state;
+ const FiatStore = this.props.FiatStore!;
+ const fiatRates = FiatStore?.fiatRates || [];
+
+ const formatNumber = (value: string, currency: string) => {
+ // Check if the currency is BTC
+ if (currency === 'BTC') {
+ // If BTC and the value is greater than 1, apply formatting with numberWithCommas
+ if (parseFloat(value) > 1) {
+ return FiatStore.numberWithCommas(value);
+ } else {
+ // Otherwise, return the value as it is
+ return value;
+ }
+ } else {
+ // For other currencies, apply formatting with numberWithCommas
+ return FiatStore.numberWithCommas(value);
+ }
+ };
+
+ const convertedValues: { [key: string]: string } = { ...inputValues };
+
+ // Apply formatting to the input value before conversion for all currencies
+ const formattedValue = formatNumber(sanitizedValue, currency);
+
+ // Set the input value
+ convertedValues[currency] = formattedValue;
+
+ // Convert the value to other currencies and BTC
+ Object.keys(convertedValues).forEach((key) => {
+ if (key !== currency) {
+ let convertedValue = '';
+
+ // Check if the value is empty
+ if (sanitizedValue === '') {
+ convertedValues[key] = ''; // Set the converted value to empty string
+ } else {
+ // Conversion from sats to currency
+ if (currency === 'sats') {
+ // Convert sats to BTC first
+ const btcValue = (
+ parseFloat(sanitizedValue) / 100000000
+ ).toFixed(8);
+
+ if (key === 'BTC') {
+ convertedValue = btcValue;
+ } else {
+ // Then convert BTC to the target currency
+ const btcConversionRate = fiatRates.find(
+ (rate) => rate.currencyPair === `BTC_${key}`
+ )?.rate;
+
+ if (btcConversionRate) {
+ convertedValue = (
+ parseFloat(btcValue) * btcConversionRate
+ ).toFixed(2);
+ }
+ }
+ } else if (currency === 'BTC') {
+ // Conversion from BTC to currency
+ const directRate = fiatRates.find(
+ (rate) => rate.currencyPair === `${currency}_${key}`
+ );
+ if (key === 'sats') {
+ convertedValue = (
+ parseFloat(sanitizedValue) * 100000000
+ ).toFixed(0);
+ }
+ if (directRate) {
+ convertedValue = (
+ parseFloat(sanitizedValue) * directRate.rate
+ ).toFixed(2);
+ }
+ } else {
+ // Conversion from currency to currency
+ const btcToRate = fiatRates.find(
+ (rate) => rate.currencyPair === `BTC_${key}`
+ )?.rate;
+ const btcFromRate: any = fiatRates.find(
+ (rate) => rate.currencyPair === `BTC_${currency}`
+ )?.rate;
+
+ if (btcFromRate) {
+ const btcValue = (
+ parseFloat(sanitizedValue) / btcFromRate
+ ).toFixed(8);
+ if (key === 'BTC') {
+ convertedValue =
+ sanitizedValue === '' ? '' : btcValue;
+ } else if (key === 'sats') {
+ convertedValue =
+ sanitizedValue === ''
+ ? ''
+ : (
+ parseFloat(btcValue) * 100000000
+ ).toFixed(0); // Convert BTC to sats if sats is present
+ }
+ }
+
+ if (btcToRate && btcFromRate) {
+ const btcValue =
+ parseFloat(sanitizedValue) / btcFromRate;
+ convertedValue = (btcValue * btcToRate).toFixed(2);
+ }
+ }
+ }
+
+ // Apply formatting on currencies
+ convertedValues[key] = formatNumber(convertedValue, key);
+ }
+ });
+
+ // Update the state with the converted values
+ this.setState({ inputValues: convertedValues });
+ };
+
+ handleDeleteCurrency = async (currency: string) => {
+ try {
+ // Retrieve inputValues from storage
+ const inputValuesString = await EncryptedStorage.getItem(
+ 'currency-codes'
+ );
+ const existingInputValues = inputValuesString
+ ? JSON.parse(inputValuesString)
+ : {};
+
+ // Create a copy of the inputValues object
+ const updatedInputValues = { ...existingInputValues };
+
+ // Remove the currency code from the inputValues object
+ delete updatedInputValues[currency];
+
+ // Save updated inputValues to storage
+ await EncryptedStorage.setItem(
+ 'currency-codes',
+ JSON.stringify(updatedInputValues)
+ );
+
+ // Update the component state with the updated inputValues
+ this.setState({ inputValues: updatedInputValues });
+ } catch (error) {
+ console.error('Error deleting currency:', error);
+ }
+ };
+
+ onReordered = async (fromIndex: number, toIndex: number) => {
+ const { inputValues } = this.state;
+ const keys = Object.keys(inputValues);
+ const copy: { [key: string]: string } = {};
+
+ // Create a copy of inputValues object
+ keys.forEach((key) => {
+ copy[key] = inputValues[key];
+ });
+
+ // Reorder keys array
+ keys.splice(toIndex, 0, keys.splice(fromIndex, 1)[0]);
+
+ // Create a new object with reordered inputValues
+ const reorderedValues: { [key: string]: string } = {};
+ keys.forEach((key) => {
+ reorderedValues[key] = copy[key];
+ });
+
+ // Update state with reordered inputValues
+ try {
+ await EncryptedStorage.setItem(
+ 'currency-codes',
+ JSON.stringify(reorderedValues)
+ );
+ } catch (error) {
+ console.error('Error saving input values:', error);
+ }
+ this.setState({
+ inputValues: reorderedValues
+ });
+ };
+
+ toggleEditMode = () => {
+ const { editMode, fadeAnim } = this.state;
+ this.setState({ editMode: !editMode });
+
+ Animated.parallel([
+ Animated.timing(fadeAnim, {
+ toValue: editMode ? 0 : 1,
+ duration: 350,
+ easing: Easing.ease,
+ useNativeDriver: false
+ })
+ ]).start();
+ };
+
+ render() {
+ const { navigation, SettingsStore } = this.props;
+ const { inputValues, editMode, fadeAnim } = this.state;
+ const { settings }: any = SettingsStore;
+ const { fiatEnabled } = settings;
+
+ const AddButton = () => (
+
+ navigation.navigate('SelectCurrency', {
+ currencyConverter: true
+ })
+ }
+ accessibilityLabel={localeString('general.add')}
+ >
+
+
+ );
+
+ const EditButton = () => (
+
+
+
+ );
+
+ const getFlagEmoji = (currencyValue: string) => {
+ const currency = CURRENCY_KEYS.find(
+ (currency) => currency.value === currencyValue
+ );
+ if (currency) {
+ return currency.key.split(' ')[0];
+ }
+ return '';
+ };
+
+ const inputBoxWidth = fadeAnim.interpolate({
+ inputRange: [0, 1],
+ outputRange: [1, editMode ? 1 : 0.8]
+ });
+
+ const slideInLeft = fadeAnim.interpolate({
+ inputRange: [0, 1],
+ outputRange: [editMode ? -50 : 0, 0]
+ });
+
+ const slideInRight = fadeAnim.interpolate({
+ inputRange: [0, 1],
+ outputRange: [editMode ? 50 : 0, 0]
+ });
+
+ return (
+
+
+ {Object.keys(inputValues).length > 2 && (
+
+ )}
+
+
+ )
+ }
+ centerComponent={{
+ text: localeString(
+ 'views.Settings.CurrencyConverter.title'
+ ),
+ style: {
+ color: themeColor('text'),
+ fontFamily: 'PPNeueMontreal-Book'
+ }
+ }}
+ navigation={navigation}
+ />
+ {!fiatEnabled ? (
+
+
+
+ ) : (
+
+ item}
+ renderItem={({
+ item,
+ onDragStart,
+ onDragEnd
+ }: DragListRenderItemInfo) => {
+ const { inputValues } = this.state;
+ return (
+
+
+ {editMode &&
+ item !== 'BTC' &&
+ item !== 'sats' && (
+
+ this.handleDeleteCurrency(
+ item
+ )
+ }
+ >
+
+
+
+
+ )}
+
+
+
+ {['BTC', 'sats'].includes(
+ item
+ ) ? (
+
+ ) : (
+
+ )}
+
+
+
+ this.handleInputChange(
+ value,
+ item
+ )
+ }
+ autoCapitalize="none"
+ />
+
+
+ {editMode &&
+ item !== 'BTC' &&
+ item !== 'sats' && (
+
+
+
+
+
+ )}
+
+
+ );
+ }}
+ />
+
+ )}
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ inputContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center'
+ },
+ inputBox: {
+ flex: 1,
+ flexDirection: 'row-reverse',
+ alignItems: 'center'
+ },
+ deleteIcon: {
+ marginRight: 16,
+ marginLeft: -6
+ },
+ draggableItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between'
+ },
+ dragHandle: {
+ marginLeft: 16,
+ marginRight: -6
+ }
+});
diff --git a/views/Settings/SelectCurrency.tsx b/views/Settings/SelectCurrency.tsx
index 75efc8aef..5eb98c1f4 100644
--- a/views/Settings/SelectCurrency.tsx
+++ b/views/Settings/SelectCurrency.tsx
@@ -78,6 +78,11 @@ export default class SelectCurrency extends React.Component<
this.state;
const { updateSettings, getSettings }: any = SettingsStore;
+ const currencyConverter = navigation.getParam(
+ 'currencyConverter',
+ null
+ );
+
return (
@@ -128,22 +133,32 @@ export default class SelectCurrency extends React.Component<
backgroundColor: 'transparent'
}}
onPress={async () => {
- await updateSettings({
- fiat: item.value
- }).then(() => {
- getSettings();
- navigation.navigate('Currency', {
- refresh: true
+ if (currencyConverter) {
+ navigation.navigate(
+ 'CurrencyConverter',
+ {
+ selectedCurrency: item.value
+ }
+ );
+ } else {
+ await updateSettings({
+ fiat: item.value
+ }).then(() => {
+ getSettings();
+ navigation.navigate('Currency', {
+ refresh: true
+ });
});
- });
+ }
}}
>
{(selectedCurrency === item.value ||
(!selectedCurrency &&
- item.value === DEFAULT_FIAT)) && (
-
-
-
- )}
+ item.value === DEFAULT_FIAT)) &&
+ !currencyConverter && (
+
+
+
+ )}
)}
keyExtractor={(item, index) => `${item.host}-${index}`}