From 303f9e356c572ecd9f83a7a6afdbe68d97f06b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Rodriguez=20V=C3=A9lez?= Date: Wed, 27 Oct 2021 00:19:06 -0400 Subject: [PATCH] ADD: Update Outdated Rate --- blue_modules/currency.js | 18 +++- components/AmountInput.js | 181 +++++++++++++++++++++++++----------- loc/en.json | 2 + screen/settings/currency.js | 22 +++-- 4 files changed, 156 insertions(+), 67 deletions(-) diff --git a/blue_modules/currency.js b/blue_modules/currency.js index 2b77ae220..100df61dd 100644 --- a/blue_modules/currency.js +++ b/blue_modules/currency.js @@ -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; diff --git a/components/AmountInput.js b/components/AmountInput.js index 4333040da..3f051cafb 100644 --- a/components/AmountInput.js +++ b/components/AmountInput.js @@ -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 ( this.textInput.focus()}> - - {!disabled && } - - - {unit === BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX && ( - {currency.getCurrencySymbol() + ' '} - )} - {amount !== BitcoinUnit.MAX ? ( - { - 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]} - /> - ) : ( - - {BitcoinUnit.MAX} - - )} - {unit !== BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX && ( - {' ' + loc.units[unit]} - )} - - - - {unit === BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX - ? removeTrailingZeros(secondaryDisplayCurrency) - : secondaryDisplayCurrency} - {unit === BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX ? ` ${loc.units[BitcoinUnit.BTC]}` : null} - + <> + + {!disabled && } + + + {unit === BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX && ( + {currency.getCurrencySymbol() + ' '} + )} + {amount !== BitcoinUnit.MAX ? ( + { + 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]} + /> + ) : ( + + {BitcoinUnit.MAX} + + )} + {unit !== BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX && ( + {' ' + loc.units[unit]} + )} + + + + {unit === BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX + ? removeTrailingZeros(secondaryDisplayCurrency) + : secondaryDisplayCurrency} + {unit === BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX ? ` ${loc.units[BitcoinUnit.BTC]}` : null} + + + {!disabled && amount !== BitcoinUnit.MAX && ( + + + + )} - {!disabled && amount !== BitcoinUnit.MAX && ( - - - + {this.state.isRateOutdated && ( + + + + + {loc.formatString(loc.send.outdated_rate, { date: dayjs(this.state.mostRecentFetchedRate.LastUpdated).format('LT') })} + + + + + + )} - + ); } @@ -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', diff --git a/loc/en.json b/loc/en.json index 6eb5e93ef..06d638b2a 100644 --- a/loc/en.json +++ b/loc/en.json @@ -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", diff --git a/screen/settings/currency.js b/screen/settings/currency.js index ff3d62e31..69adb942e 100644 --- a/screen/settings/currency.js +++ b/screen/settings/currency.js @@ -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); + } }} /> );