ADD: Update Outdated Rate

This commit is contained in:
Marcos Rodriguez Vélez 2021-10-27 00:19:06 -04:00
parent c1dfe8906a
commit 303f9e356c
No known key found for this signature in database
GPG Key ID: 0D64671698D11C5C
4 changed files with 156 additions and 67 deletions

View File

@ -9,10 +9,11 @@ const PREFERRED_CURRENCY_STORAGE_KEY = 'preferredCurrency';
const EXCHANGE_RATES_STORAGE_KEY = 'currency';
let preferredFiatCurrency = FiatUnit.USD;
let exchangeRates = {};
let exchangeRates = { LAST_UPDATED_ERROR: false };
let lastTimeUpdateExchangeRateWasCalled = 0;
const LAST_UPDATED = 'LAST_UPDATED';
const LAST_UPDATED_ERROR = false;
/**
* Saves to storage preferred currency, whole object
@ -40,9 +41,10 @@ async function getPreferredCurrency() {
async function _restoreSavedExchangeRatesFromStorage() {
try {
exchangeRates = JSON.parse(await AsyncStorage.getItem(EXCHANGE_RATES_STORAGE_KEY));
if (!exchangeRates) exchangeRates = {};
exchangeRates.LAST_UPDATED_ERROR = false;
if (!exchangeRates) exchangeRates = { LAST_UPDATED_ERROR: false };
} catch (_) {
exchangeRates = {};
exchangeRates = { LAST_UPDATED_ERROR: false };
}
}
@ -88,12 +90,19 @@ async function updateExchangeRate() {
exchangeRates[LAST_UPDATED] = +new Date();
exchangeRates['BTC_' + preferredFiatCurrency.endPointKey] = rate;
await AsyncStorage.setItem(EXCHANGE_RATES_STORAGE_KEY, JSON.stringify(exchangeRates));
exchangeRates[LAST_UPDATED_ERROR] = false;
} catch (Err) {
console.log('Error encountered when attempting to update exchange rate...');
console.warn(Err.message);
exchangeRates[LAST_UPDATED_ERROR] = true;
throw Err;
}
}
function isRateOutdated() {
return exchangeRates[LAST_UPDATED_ERROR] || new Date() - exchangeRates[LAST_UPDATED] >= 31 * 60 * 1000;
}
/**
* this function reads storage and restores current preferred fiat currency & last saved exchange rate, then calls
* updateExchangeRate() to update rates.
@ -167,7 +176,7 @@ async function mostRecentFetchedRate() {
currency: preferredFiatCurrency.endPointKey,
});
return {
LastUpdated: currencyInformation[LAST_UPDATED],
LastUpdated: isRateOutdated() ? exchangeRates[LAST_UPDATED] : currencyInformation[LAST_UPDATED],
Rate: formatter.format(JSON.parse(currencyInformation)[`BTC_${preferredFiatCurrency.endPointKey}`]),
};
}
@ -227,3 +236,4 @@ module.exports.PREFERRED_CURRENCY = PREFERRED_CURRENCY_STORAGE_KEY;
module.exports.EXCHANGE_RATES = EXCHANGE_RATES_STORAGE_KEY;
module.exports.LAST_UPDATED = LAST_UPDATED;
module.exports.mostRecentFetchedRate = mostRecentFetchedRate;
module.exports.isRateOutdated = isRateOutdated;

View File

@ -1,13 +1,16 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import BigNumber from 'bignumber.js';
import { Text } from 'react-native-elements';
import { Image, Pressable, StyleSheet, TextInput, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native';
import { Badge, Icon, Text } from 'react-native-elements';
import { Image, LayoutAnimation, Pressable, StyleSheet, TextInput, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native';
import { useTheme } from '@react-navigation/native';
import confirm from '../helpers/confirm';
import { BitcoinUnit } from '../models/bitcoinUnits';
import loc, { formatBalanceWithoutSuffix, formatBalancePlain, removeTrailingZeros } from '../loc';
import { BlueText } from '../BlueComponents';
import dayjs from 'dayjs';
const currency = require('../blue_modules/currency');
dayjs.extend(require('dayjs/plugin/localizedFormat'));
class AmountInput extends Component {
static propTypes = {
@ -47,6 +50,22 @@ class AmountInput extends Component {
AmountInput.conversionCache[amount + BitcoinUnit.LOCAL_CURRENCY] = sats;
};
constructor() {
super();
this.state = { mostRecentFetchedRate: Date(), isRateOutdated: false, isRateBeingUpdated: false };
}
componentDidMount() {
currency
.mostRecentFetchedRate()
.then(mostRecentFetchedRate => {
this.setState({ mostRecentFetchedRate });
})
.finally(() => {
this.setState({ isRateOutdated: currency.isRateOutdated() });
});
}
/**
* here we must recalculate old amont value (which was denominated in `previousUnit`) to new denomination `newUnit`
* and fill this value in input box, so user can switch between, for example, 0.001 BTC <=> 100000 sats
@ -168,6 +187,22 @@ class AmountInput extends Component {
}
};
updateRate = () => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
this.setState({ isRateBeingUpdated: true }, async () => {
try {
await currency.updateExchangeRate();
currency.mostRecentFetchedRate().then(mostRecentFetchedRate => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
this.setState({ mostRecentFetchedRate });
});
} finally {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
this.setState({ isRateBeingUpdated: false, isRateOutdated: currency.isRateOutdated() });
}
});
};
render() {
const { colors, disabled, unit } = this.props;
const amount = this.props.amount || 0;
@ -205,63 +240,82 @@ class AmountInput extends Component {
return (
<TouchableWithoutFeedback disabled={this.props.pointerEvents === 'none'} onPress={() => this.textInput.focus()}>
<View style={styles.root}>
{!disabled && <View style={[styles.center, stylesHook.center]} />}
<View style={styles.flex}>
<View style={styles.container}>
{unit === BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX && (
<Text style={[styles.localCurrency, stylesHook.localCurrency]}>{currency.getCurrencySymbol() + ' '}</Text>
)}
{amount !== BitcoinUnit.MAX ? (
<TextInput
{...this.props}
testID="BitcoinAmountInput"
keyboardType="numeric"
adjustsFontSizeToFit
onChangeText={this.handleChangeText}
onBlur={() => {
if (this.props.onBlur) this.props.onBlur();
}}
onFocus={() => {
if (this.props.onFocus) this.props.onFocus();
}}
placeholder="0"
maxLength={this.maxLength()}
ref={textInput => (this.textInput = textInput)}
editable={!this.props.isLoading && !disabled}
value={amount === BitcoinUnit.MAX ? loc.units.MAX : parseFloat(amount) >= 0 ? String(amount) : undefined}
placeholderTextColor={disabled ? colors.buttonDisabledTextColor : colors.alternativeTextColor2}
style={[styles.input, stylesHook.input]}
/>
) : (
<Pressable onPress={this.resetAmount}>
<Text style={[styles.input, stylesHook.input]}>{BitcoinUnit.MAX}</Text>
</Pressable>
)}
{unit !== BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX && (
<Text style={[styles.cryptoCurrency, stylesHook.cryptoCurrency]}>{' ' + loc.units[unit]}</Text>
)}
</View>
<View style={styles.secondaryRoot}>
<Text style={styles.secondaryText}>
{unit === BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX
? removeTrailingZeros(secondaryDisplayCurrency)
: secondaryDisplayCurrency}
{unit === BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX ? ` ${loc.units[BitcoinUnit.BTC]}` : null}
</Text>
<>
<View style={styles.root}>
{!disabled && <View style={[styles.center, stylesHook.center]} />}
<View style={styles.flex}>
<View style={styles.container}>
{unit === BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX && (
<Text style={[styles.localCurrency, stylesHook.localCurrency]}>{currency.getCurrencySymbol() + ' '}</Text>
)}
{amount !== BitcoinUnit.MAX ? (
<TextInput
{...this.props}
testID="BitcoinAmountInput"
keyboardType="numeric"
adjustsFontSizeToFit
onChangeText={this.handleChangeText}
onBlur={() => {
if (this.props.onBlur) this.props.onBlur();
}}
onFocus={() => {
if (this.props.onFocus) this.props.onFocus();
}}
placeholder="0"
maxLength={this.maxLength()}
ref={textInput => (this.textInput = textInput)}
editable={!this.props.isLoading && !disabled}
value={amount === BitcoinUnit.MAX ? loc.units.MAX : parseFloat(amount) >= 0 ? String(amount) : undefined}
placeholderTextColor={disabled ? colors.buttonDisabledTextColor : colors.alternativeTextColor2}
style={[styles.input, stylesHook.input]}
/>
) : (
<Pressable onPress={this.resetAmount}>
<Text style={[styles.input, stylesHook.input]}>{BitcoinUnit.MAX}</Text>
</Pressable>
)}
{unit !== BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX && (
<Text style={[styles.cryptoCurrency, stylesHook.cryptoCurrency]}>{' ' + loc.units[unit]}</Text>
)}
</View>
<View style={styles.secondaryRoot}>
<Text style={styles.secondaryText}>
{unit === BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX
? removeTrailingZeros(secondaryDisplayCurrency)
: secondaryDisplayCurrency}
{unit === BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX ? ` ${loc.units[BitcoinUnit.BTC]}` : null}
</Text>
</View>
</View>
{!disabled && amount !== BitcoinUnit.MAX && (
<TouchableOpacity
accessibilityRole="button"
testID="changeAmountUnitButton"
style={styles.changeAmountUnit}
onPress={this.changeAmountUnit}
>
<Image source={require('../img/round-compare-arrows-24-px.png')} />
</TouchableOpacity>
)}
</View>
{!disabled && amount !== BitcoinUnit.MAX && (
<TouchableOpacity
accessibilityRole="button"
testID="changeAmountUnitButton"
style={styles.changeAmountUnit}
onPress={this.changeAmountUnit}
>
<Image source={require('../img/round-compare-arrows-24-px.png')} />
</TouchableOpacity>
{this.state.isRateOutdated && (
<View style={styles.outdatedRateContainer}>
<Badge status="warning" />
<View style={styles.spacing8} />
<BlueText>
{loc.formatString(loc.send.outdated_rate, { date: dayjs(this.state.mostRecentFetchedRate.LastUpdated).format('LT') })}
</BlueText>
<View style={styles.spacing8} />
<TouchableOpacity
onPress={this.updateRate}
disabled={this.state.isRateBeingUpdated}
style={this.state.isRateBeingUpdated ? styles.disabledButton : styles.enabledButon}
>
<Icon name="sync" type="font-awesome-5" size={16} color={colors.buttonAlternativeTextColor} />
</TouchableOpacity>
</View>
)}
</View>
</>
</TouchableWithoutFeedback>
);
}
@ -278,6 +332,21 @@ const styles = StyleSheet.create({
flex: {
flex: 1,
},
spacing8: {
width: 8,
},
disabledButton: {
opacity: 0.5,
},
enabledButton: {
opacity: 1,
},
outdatedRateContainer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginVertical: 8,
},
container: {
flexDirection: 'row',
alignContent: 'space-between',

View File

@ -257,6 +257,7 @@
"psbt_this_is_psbt": "This is a Partially Signed Bitcoin Transaction (PSBT). Please finish signing it with your hardware wallet.",
"psbt_tx_export": "Export to file",
"no_tx_signing_in_progress": "There is no transaction signing in progress.",
"outdated_rate": "Rate was last updated at {date}",
"psbt_tx_open": "Open Signed Transaction",
"psbt_tx_scan": "Scan Signed Transaction",
"qr_error_no_qrcode": "We were unable to find a QR Code in the selected image. Make sure the image contains only a QR Code and no additional content such as text, or buttons.",
@ -290,6 +291,7 @@
"biom_remove_decrypt": "All your wallets will be removed and your storage will be decrypted. Are you sure you want to proceed?",
"currency": "Currency",
"currency_source": "Prices are obtained from",
"currency_fetch_error": "There was an error while obtaining the rate for the selected currency.",
"default_desc": "When disabled, BlueWallet will immediately open the selected wallet at launch.",
"default_info": "Default info",
"default_title": "On Launch",

View File

@ -4,10 +4,11 @@ import { useTheme } from '@react-navigation/native';
import navigationStyle from '../../components/navigationStyle';
import { SafeBlueArea, BlueListItem, BlueText, BlueCard, BlueSpacing10 } from '../../BlueComponents';
import { FiatUnit, FiatUnitSource } from '../../models/fiatUnit';
import { FiatUnit, FiatUnitSource, getFiatRate } from '../../models/fiatUnit';
import loc from '../../loc';
import { BlueStorageContext } from '../../blue_modules/storage-context';
import dayjs from 'dayjs';
import alert from '../../components/Alert';
dayjs.extend(require('dayjs/plugin/calendar'));
const currency = require('../../blue_modules/currency');
const data = Object.values(FiatUnit);
@ -67,12 +68,19 @@ const Currency = () => {
checkmark={selectedCurrency.endPointKey === item.endPointKey}
onPress={async () => {
setIsSavingNewPreferredCurrency(true);
setSelectedCurrency(item);
await currency.setPrefferedCurrency(item);
await currency.init(true);
setIsSavingNewPreferredCurrency(false);
setPreferredFiatCurrency();
fetchCurrency();
try {
await getFiatRate(item.endPointKey);
await currency.setPrefferedCurrency(item);
await currency.init(true);
await fetchCurrency();
setSelectedCurrency(item);
setPreferredFiatCurrency();
} catch (error) {
console.log(error);
alert(loc.settings.currency_fetch_error);
} finally {
setIsSavingNewPreferredCurrency(false);
}
}}
/>
);