diff --git a/BlueApp.js b/BlueApp.js
index 8ede27e90..5b59e6c01 100644
--- a/BlueApp.js
+++ b/BlueApp.js
@@ -5,7 +5,7 @@ import { AppStorage } from './class';
import DeviceQuickActions from './class/quick-actions';
const prompt = require('./prompt');
const EV = require('./events');
-const currency = require('./currency');
+const currency = require('./blue_modules/currency');
const loc = require('./loc');
const BlueElectrum = require('./BlueElectrum'); // eslint-disable-line no-unused-vars
diff --git a/BlueComponents.js b/BlueComponents.js
index 8d3141233..bd81f7661 100644
--- a/BlueComponents.js
+++ b/BlueComponents.js
@@ -40,6 +40,7 @@ const BlueApp = require('./BlueApp');
const { height, width } = Dimensions.get('window');
const aspectRatio = height / width;
const BigNumber = require('bignumber.js');
+const currency = require('./blue_modules/currency');
let isIpad;
if (aspectRatio > 1.6) {
isIpad = false;
@@ -579,7 +580,6 @@ export class BlueText extends Component {
);
}
}
-
export class BlueTextCentered extends Component {
render() {
return ;
@@ -828,6 +828,10 @@ export class BlueUseAllFundsButton extends Component {
onUseAllPressed: PropTypes.func.isRequired,
};
+ static defaultProps = {
+ unit: BitcoinUnit.BTC,
+ };
+
render() {
const inputView = (
{
return (
{
+ onPressedOut();
onPress(index);
+ onPressedOut();
}}
/>
);
@@ -1828,7 +1834,9 @@ const WalletCarouselItem = ({ item, index, onPress, handleLongPress }) => {
onPressOut={item.getIsFailure() ? onPressedOut : null}
onPress={() => {
if (item.getIsFailure()) {
+ onPressedOut();
onPress(index);
+ onPressedOut();
}
}}
>
@@ -1896,7 +1904,9 @@ const WalletCarouselItem = ({ item, index, onPress, handleLongPress }) => {
onPressOut={onPressedOut}
onLongPress={handleLongPress}
onPress={() => {
+ onPressedOut();
onPress(index);
+ onPressedOut();
}}
>
@@ -2240,14 +2250,104 @@ export class BlueReplaceFeeSuggestions extends Component {
export class BlueBitcoinAmount extends Component {
static propTypes = {
isLoading: PropTypes.bool,
+ /**
+ * amount is a sting thats always in current unit denomination, e.g. '0.001' or '9.43' or '10000'
+ */
amount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ /**
+ * callback that returns currently typed amount, in current denomination, e.g. 0.001 or 10000 or $9.34
+ * (btc, sat, fiat)
+ */
onChangeText: PropTypes.func,
+ /**
+ * callback thats fired to notify of currently selected denomination, returns
+ */
+ onAmountUnitChange: PropTypes.func,
disabled: PropTypes.bool,
- unit: PropTypes.string,
};
- static defaultProps = {
- unit: BitcoinUnit.BTC,
+ /**
+ * cache of conversions fiat amount => satoshi
+ * @type {{}}
+ */
+ static conversionCache = {};
+
+ static getCachedSatoshis(amount) {
+ return BlueBitcoinAmount.conversionCache[amount + BitcoinUnit.LOCAL_CURRENCY] || false;
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = { unit: props.unit || BitcoinUnit.BTC, previousUnit: BitcoinUnit.SATS };
+ }
+
+ /**
+ * 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
+ *
+ * @param previousUnit {string} one of {BitcoinUnit.*}
+ * @param newUnit {string} one of {BitcoinUnit.*}
+ */
+ onAmountUnitChange(previousUnit, newUnit) {
+ const amount = this.props.amount || 0;
+ console.log('was:', amount, previousUnit, '; converting to', newUnit);
+ let sats = 0;
+ switch (previousUnit) {
+ case BitcoinUnit.BTC:
+ sats = new BigNumber(amount).multipliedBy(100000000).toString();
+ break;
+ case BitcoinUnit.SATS:
+ sats = amount;
+ break;
+ case BitcoinUnit.LOCAL_CURRENCY:
+ sats = new BigNumber(currency.fiatToBTC(amount)).multipliedBy(100000000).toString();
+ break;
+ }
+ if (previousUnit === BitcoinUnit.LOCAL_CURRENCY && BlueBitcoinAmount.conversionCache[amount + previousUnit]) {
+ // cache hit! we reuse old value that supposedly doesnt have rounding errors
+ sats = BlueBitcoinAmount.conversionCache[amount + previousUnit];
+ }
+ console.log('so, in sats its', sats);
+
+ const newInputValue = loc.formatBalancePlain(sats, newUnit, false);
+ console.log('and in', newUnit, 'its', newInputValue);
+
+ if (newUnit === BitcoinUnit.LOCAL_CURRENCY && previousUnit === BitcoinUnit.SATS) {
+ // we cache conversion, so when we will need reverse conversion there wont be a rounding error
+ BlueBitcoinAmount.conversionCache[newInputValue + newUnit] = amount;
+ }
+ this.props.onChangeText(newInputValue);
+ if (this.props.onAmountUnitChange) this.props.onAmountUnitChange(newUnit);
+ }
+
+ /**
+ * responsible for cycling currently selected denomination, BTC->SAT->LOCAL_CURRENCY->BTC
+ */
+ changeAmountUnit = () => {
+ let previousUnit = this.state.unit;
+ let newUnit;
+ if (previousUnit === BitcoinUnit.BTC) {
+ newUnit = BitcoinUnit.SATS;
+ } else if (previousUnit === BitcoinUnit.SATS) {
+ newUnit = BitcoinUnit.LOCAL_CURRENCY;
+ } else if (previousUnit === BitcoinUnit.LOCAL_CURRENCY) {
+ newUnit = BitcoinUnit.BTC;
+ } else {
+ newUnit = BitcoinUnit.BTC;
+ previousUnit = BitcoinUnit.SATS;
+ }
+ this.setState({ unit: newUnit, previousUnit }, () => this.onAmountUnitChange(previousUnit, newUnit));
+ };
+
+ maxLength = () => {
+ switch (this.state.unit) {
+ case BitcoinUnit.BTC:
+ return 10;
+ case BitcoinUnit.SATS:
+ return 15;
+ default:
+ return 15;
+ }
};
textInput = React.createRef();
@@ -2257,85 +2357,155 @@ export class BlueBitcoinAmount extends Component {
};
render() {
- const amount = this.props.amount || '0';
- let localCurrency = loc.formatBalanceWithoutSuffix(amount, BitcoinUnit.LOCAL_CURRENCY, false);
- if (this.props.unit === BitcoinUnit.BTC) {
- let sat = new BigNumber(amount);
- sat = sat.multipliedBy(100000000).toString();
- localCurrency = loc.formatBalanceWithoutSuffix(sat, BitcoinUnit.LOCAL_CURRENCY, false);
- } else {
- localCurrency = loc.formatBalanceWithoutSuffix(amount.toString(), BitcoinUnit.LOCAL_CURRENCY, false);
- }
- if (amount === BitcoinUnit.MAX) localCurrency = ''; // we dont want to display NaN
- return (
-
-
-
- {
- text = text.trim();
- text = text.replace(',', '.');
- const split = text.split('.');
- if (split.length >= 2) {
- text = `${parseInt(split[0], 10)}.${split[1]}`;
- } else {
- text = `${parseInt(split[0], 10)}`;
- }
- text = this.props.unit === BitcoinUnit.BTC ? text.replace(/[^0-9.]/g, '') : text.replace(/[^0-9]/g, '');
- text = text.replace(/(\..*)\./g, '$1');
+ const amount = this.props.amount || 0;
+ let secondaryDisplayCurrency = loc.formatBalanceWithoutSuffix(amount, BitcoinUnit.LOCAL_CURRENCY, false);
- if (text.startsWith('.')) {
- text = '0.';
- }
- text = text.replace(/(0{1,}.)\./g, '$1');
- if (this.props.unit !== BitcoinUnit.BTC) {
- text = text.replace(/[^0-9.]/g, '');
- }
- this.props.onChangeText(text);
- }}
- onBlur={() => {
- if (this.props.onBlur) this.props.onBlur();
- }}
- onFocus={() => {
- if (this.props.onFocus) this.props.onFocus();
- }}
- placeholder="0"
- maxLength={10}
- ref={this.textInput}
- editable={!this.props.isLoading && !this.props.disabled}
- value={amount}
- placeholderTextColor={this.props.disabled ? BlueApp.settings.buttonDisabledTextColor : BlueApp.settings.alternativeTextColor2}
- style={{
- color: this.props.disabled ? BlueApp.settings.buttonDisabledTextColor : BlueApp.settings.alternativeTextColor2,
- fontSize: 36,
- fontWeight: '600',
- }}
- />
- this.textInput.focus()}>
+
+ {!this.props.disabled && }
+
+
- {' ' + this.props.unit}
-
-
-
- {localCurrency}
+ {this.state.unit === BitcoinUnit.LOCAL_CURRENCY && (
+
+ {currency.getCurrencySymbol() + ' '}
+
+ )}
+ {
+ text = text.trim();
+ if (this.state.unit !== BitcoinUnit.LOCAL_CURRENCY) {
+ text = text.replace(',', '.');
+ const split = text.split('.');
+ if (split.length >= 2) {
+ text = `${parseInt(split[0], 10)}.${split[1]}`;
+ } else {
+ text = `${parseInt(split[0], 10)}`;
+ }
+ text = this.state.unit === BitcoinUnit.BTC ? text.replace(/[^0-9.]/g, '') : text.replace(/[^0-9]/g, '');
+ text = text.replace(/(\..*)\./g, '$1');
+
+ if (text.startsWith('.')) {
+ text = '0.';
+ }
+ text = text.replace(/(0{1,}.)\./g, '$1');
+ if (this.state.unit !== BitcoinUnit.BTC) {
+ text = text.replace(/[^0-9.]/g, '');
+ }
+ } else if (this.state.unit === BitcoinUnit.LOCAL_CURRENCY) {
+ text = text.replace(/,/gi, '');
+ if (text.split('.').length > 2) {
+ // too many dots. stupid code to remove all but first dot:
+ let rez = '';
+ let first = true;
+ for (const part of text.split('.')) {
+ rez += part;
+ if (first) {
+ rez += '.';
+ first = false;
+ }
+ }
+ text = rez;
+ }
+ text = text.replace(/[^\d.,-]/g, ''); // remove all but numberd, dots & commas
+ }
+
+ this.props.onChangeText(text);
+ }}
+ 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 && !this.props.disabled}
+ value={parseFloat(amount) > 0 || amount === BitcoinUnit.MAX ? amount : undefined}
+ placeholderTextColor={
+ this.props.disabled ? BlueApp.settings.buttonDisabledTextColor : BlueApp.settings.alternativeTextColor2
+ }
+ style={{
+ color: this.props.disabled ? BlueApp.settings.buttonDisabledTextColor : BlueApp.settings.alternativeTextColor2,
+ fontWeight: 'bold',
+ fontSize: amount.length > 10 ? 20 : 36,
+ }}
+ />
+ {this.state.unit !== BitcoinUnit.LOCAL_CURRENCY && (
+
+ {' ' + this.state.unit}
+
+ )}
+
+
+
+ {this.state.unit === BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX
+ ? loc.removeTrailingZeros(secondaryDisplayCurrency)
+ : secondaryDisplayCurrency}
+ {this.state.unit === BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX ? ` ${BitcoinUnit.BTC}` : null}
+
+
+ {!this.props.disabled && (
+
+
+
+ )}
);
}
}
-
const styles = StyleSheet.create({
balanceBlur: {
height: 30,
diff --git a/currency.js b/blue_modules/currency.js
similarity index 79%
rename from currency.js
rename to blue_modules/currency.js
index ac75b0e8f..7e1545ed4 100644
--- a/currency.js
+++ b/blue_modules/currency.js
@@ -1,9 +1,9 @@
import Frisbee from 'frisbee';
import AsyncStorage from '@react-native-community/async-storage';
-import { AppStorage } from './class';
-import { FiatUnit } from './models/fiatUnit';
+import { AppStorage } from '../class';
+import { FiatUnit } from '../models/fiatUnit';
import DefaultPreference from 'react-native-default-preference';
-import DeviceQuickActions from './class/quick-actions';
+import DeviceQuickActions from '../class/quick-actions';
const BigNumber = require('bignumber.js');
let preferredFiatCurrency = FiatUnit.USD;
const exchangeRates = {};
@@ -129,13 +129,49 @@ function satoshiToBTC(satoshi) {
return b.toString(10);
}
+function btcToSatoshi(btc) {
+ return new BigNumber(btc).multipliedBy(100000000).toNumber();
+}
+
+function fiatToBTC(fiatFloat) {
+ let b = new BigNumber(fiatFloat);
+ b = b.dividedBy(exchangeRates['BTC_' + preferredFiatCurrency.endPointKey]).toFixed(8);
+ return b;
+}
+
+function getCurrencySymbol() {
+ return preferredFiatCurrency.symbol;
+}
+
+/**
+ * Used to mock data in tests
+ *
+ * @param {object} currency, one of FiatUnit.*
+ */
+function _setPreferredFiatCurrency(currency) {
+ preferredFiatCurrency = currency;
+}
+
+/**
+ * Used to mock data in tests
+ *
+ * @param {string} pair as expected by rest of this module, e.g 'BTC_JPY' or 'BTC_USD'
+ * @param {number} rate exchange rate
+ */
+function _setExchangeRate(pair, rate) {
+ exchangeRates[pair] = rate;
+}
+
module.exports.updateExchangeRate = updateExchangeRate;
module.exports.startUpdater = startUpdater;
module.exports.STRUCT = STRUCT;
module.exports.satoshiToLocalCurrency = satoshiToLocalCurrency;
+module.exports.fiatToBTC = fiatToBTC;
module.exports.satoshiToBTC = satoshiToBTC;
module.exports.BTCToLocalCurrency = BTCToLocalCurrency;
module.exports.setPrefferedCurrency = setPrefferedCurrency;
module.exports.getPreferredCurrency = getPreferredCurrency;
-module.exports.exchangeRates = exchangeRates; // export it to mock data in tests
-module.exports.preferredFiatCurrency = preferredFiatCurrency; // export it to mock data in tests
+module.exports.btcToSatoshi = btcToSatoshi;
+module.exports.getCurrencySymbol = getCurrencySymbol;
+module.exports._setPreferredFiatCurrency = _setPreferredFiatCurrency; // export it to mock data in tests
+module.exports._setExchangeRate = _setExchangeRate; // export it to mock data in tests
diff --git a/img/round-compare-arrows-24-px.png b/img/round-compare-arrows-24-px.png
new file mode 100755
index 000000000..c081c2a4d
Binary files /dev/null and b/img/round-compare-arrows-24-px.png differ
diff --git a/img/round-compare-arrows-24-px@2x.png b/img/round-compare-arrows-24-px@2x.png
new file mode 100755
index 000000000..c0d35bcd6
Binary files /dev/null and b/img/round-compare-arrows-24-px@2x.png differ
diff --git a/img/round-compare-arrows-24-px@3x.png b/img/round-compare-arrows-24-px@3x.png
new file mode 100755
index 000000000..6296b84e7
Binary files /dev/null and b/img/round-compare-arrows-24-px@3x.png differ
diff --git a/loc/index.js b/loc/index.js
index 536d6b362..1ea4e45d4 100644
--- a/loc/index.js
+++ b/loc/index.js
@@ -4,7 +4,7 @@ import { AppStorage } from '../class';
import { BitcoinUnit } from '../models/bitcoinUnits';
import relativeTime from 'dayjs/plugin/relativeTime';
const dayjs = require('dayjs');
-const currency = require('../currency');
+const currency = require('../blue_modules/currency');
const BigNumber = require('bignumber.js');
dayjs.extend(relativeTime);
@@ -149,7 +149,7 @@ strings.transactionTimeToReadable = time => {
return ret;
};
-function removeTrailingZeros(value) {
+strings.removeTrailingZeros = value => {
value = value.toString();
if (value.indexOf('.') === -1) {
@@ -159,21 +159,22 @@ function removeTrailingZeros(value) {
value = value.substr(0, value.length - 1);
}
return value;
-}
+};
/**
*
- * @param balance {Number} Float amount of bitcoins
+ * @param balance {number} Satoshis
* @param toUnit {String} Value from models/bitcoinUnits.js
+ * @param withFormatting {boolean} Works only with `BitcoinUnit.SATS`, makes spaces wetween groups of 000
* @returns {string}
*/
-strings.formatBalance = (balance, toUnit, withFormatting = false) => {
+function formatBalance(balance, toUnit, withFormatting = false) {
if (toUnit === undefined) {
return balance + ' ' + BitcoinUnit.BTC;
}
if (toUnit === BitcoinUnit.BTC) {
const value = new BigNumber(balance).dividedBy(100000000).toFixed(8);
- return removeTrailingZeros(value) + ' ' + BitcoinUnit.BTC;
+ return strings.removeTrailingZeros(value) + ' ' + BitcoinUnit.BTC;
} else if (toUnit === BitcoinUnit.SATS) {
return (
(balance < 0 ? '-' : '') +
@@ -184,22 +185,23 @@ strings.formatBalance = (balance, toUnit, withFormatting = false) => {
} else if (toUnit === BitcoinUnit.LOCAL_CURRENCY) {
return currency.satoshiToLocalCurrency(balance);
}
-};
+}
/**
*
* @param balance {Integer} Satoshis
- * @param toUnit {String} Value from models/bitcoinUnits.js
+ * @param toUnit {String} Value from models/bitcoinUnits.js, for example `BitcoinUnit.SATS`
+ * @param withFormatting {boolean} Works only with `BitcoinUnit.SATS`, makes spaces wetween groups of 000
* @returns {string}
*/
-strings.formatBalanceWithoutSuffix = (balance = 0, toUnit, withFormatting = false) => {
+function formatBalanceWithoutSuffix(balance = 0, toUnit, withFormatting = false) {
if (toUnit === undefined) {
return balance;
}
if (balance !== 0) {
if (toUnit === BitcoinUnit.BTC) {
const value = new BigNumber(balance).dividedBy(100000000).toFixed(8);
- return removeTrailingZeros(value);
+ return strings.removeTrailingZeros(value);
} else if (toUnit === BitcoinUnit.SATS) {
return (balance < 0 ? '-' : '') + (withFormatting ? new Intl.NumberFormat().format(balance).replace(/[^0-9]/g, ' ') : balance);
} else if (toUnit === BitcoinUnit.LOCAL_CURRENCY) {
@@ -207,6 +209,36 @@ strings.formatBalanceWithoutSuffix = (balance = 0, toUnit, withFormatting = fals
}
}
return balance.toString();
-};
+}
+
+/**
+ * Should be used when we need a simple string to be put in text input, for example
+ *
+ * @param balance {integer} Satoshis
+ * @param toUnit {String} Value from models/bitcoinUnits.js, for example `BitcoinUnit.SATS`
+ * @param withFormatting {boolean} Works only with `BitcoinUnit.SATS`, makes spaces wetween groups of 000
+ * @returns {string}
+ */
+function formatBalancePlain(balance = 0, toUnit, withFormatting = false) {
+ const newInputValue = formatBalanceWithoutSuffix(balance, toUnit, withFormatting);
+ return _leaveNumbersAndDots(newInputValue);
+}
+
+function _leaveNumbersAndDots(newInputValue) {
+ newInputValue = newInputValue.replace(/[^\d.,-]/g, ''); // filtering, leaving only numbers, dots & commas
+ if (newInputValue.endsWith('.00') || newInputValue.endsWith(',00')) newInputValue = newInputValue.substring(0, newInputValue.length - 3);
+
+ if (newInputValue[newInputValue.length - 3] === ',') {
+ // this is a fractional value, lets replace comma to dot so it represents actual fractional value for normal people
+ newInputValue = newInputValue.substring(0, newInputValue.length - 3) + '.' + newInputValue.substring(newInputValue.length - 2);
+ }
+ newInputValue = newInputValue.replace(/,/gi, '');
+
+ return newInputValue;
+}
module.exports = strings;
+module.exports.formatBalanceWithoutSuffix = formatBalanceWithoutSuffix;
+module.exports.formatBalance = formatBalance;
+module.exports.formatBalancePlain = formatBalancePlain;
+module.exports._leaveNumbersAndDots = _leaveNumbersAndDots;
diff --git a/models/bitcoinTransactionInfo.js b/models/bitcoinTransactionInfo.js
index f5daf18e0..9eb4e097f 100644
--- a/models/bitcoinTransactionInfo.js
+++ b/models/bitcoinTransactionInfo.js
@@ -1,6 +1,13 @@
export class BitcoinTransaction {
- constructor(address = '', amount) {
+ /**
+ *
+ * @param address
+ * @param amount {number}
+ * @param amountSats {integer} satoshi
+ */
+ constructor(address = '', amount, amountSats) {
this.address = address;
this.amount = amount;
+ this.amountSats = amountSats;
}
}
diff --git a/screen/lnd/lndCreateInvoice.js b/screen/lnd/lndCreateInvoice.js
index 13941f77b..e4421bb81 100644
--- a/screen/lnd/lndCreateInvoice.js
+++ b/screen/lnd/lndCreateInvoice.js
@@ -25,6 +25,7 @@ import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import NavigationService from '../../NavigationService';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { Icon } from 'react-native-elements';
+const currency = require('../../blue_modules/currency');
const BlueApp = require('../../BlueApp');
const EV = require('../../events');
const loc = require('../../loc');
@@ -137,6 +138,7 @@ export default class LNDCreateInvoice extends Component {
super(props);
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow);
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
+ /** @type LightningCustodianWallet */
let fromWallet;
if (props.route.params.fromWallet) fromWallet = props.route.params.fromWallet;
@@ -152,6 +154,7 @@ export default class LNDCreateInvoice extends Component {
this.state = {
fromWallet,
amount: '',
+ unit: fromWallet.preferredBalanceUnit,
description: '',
lnurl: '',
lnurlParams: null,
@@ -170,6 +173,7 @@ export default class LNDCreateInvoice extends Component {
};
componentDidMount() {
+ console.log('lnd/lndCreateInvoice mounted');
if (this.state.fromWallet.getUserHasSavedExport()) {
this.renderReceiveDetails();
} else {
@@ -201,7 +205,22 @@ export default class LNDCreateInvoice extends Component {
async createInvoice() {
this.setState({ isLoading: true }, async () => {
try {
- const invoiceRequest = await this.state.fromWallet.addInvoice(this.state.amount, this.state.description);
+ this.setState({ isLoading: false });
+ let amount = this.state.amount;
+ switch (this.state.unit) {
+ case BitcoinUnit.SATS:
+ amount = parseInt(amount); // basically nop
+ break;
+ case BitcoinUnit.BTC:
+ amount = currency.btcToSatoshi(amount);
+ break;
+ case BitcoinUnit.LOCAL_CURRENCY:
+ // trying to fetch cached sat equivalent for this fiat amount
+ amount = BlueBitcoinAmount.getCachedSatoshis(amount) || currency.btcToSatoshi(currency.fiatToBTC(amount));
+ break;
+ }
+
+ const invoiceRequest = await this.state.fromWallet.addInvoice(amount, this.state.description);
EV(EV.enum.TRANSACTIONS_COUNT_CHANGED);
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
@@ -379,6 +398,9 @@ export default class LNDCreateInvoice extends Component {
{
+ this.setState({ unit });
+ }}
onChangeText={text => {
if (this.state.lnurlParams) {
// in this case we prevent the user from changing the amount to < min or > max
@@ -394,7 +416,7 @@ export default class LNDCreateInvoice extends Component {
this.setState({ amount: text });
}}
disabled={this.state.isLoading || (this.state.lnurlParams && this.state.lnurlParams.fixed)}
- unit={BitcoinUnit.SATS}
+ unit={this.state.unit}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
diff --git a/screen/lnd/scanLndInvoice.js b/screen/lnd/scanLndInvoice.js
index cfc01755b..cc4c0beb2 100644
--- a/screen/lnd/scanLndInvoice.js
+++ b/screen/lnd/scanLndInvoice.js
@@ -21,6 +21,7 @@ import Biometric from '../../class/biometrics';
const BlueApp = require('../../BlueApp');
const EV = require('../../events');
const loc = require('../../loc');
+const currency = require('../../blue_modules/currency');
const styles = StyleSheet.create({
walletSelectRoot: {
@@ -138,6 +139,7 @@ export default class ScanLndInvoice extends React.Component {
this.state = {
fromWallet,
fromSecret,
+ unit: BitcoinUnit.SATS,
destination: '',
};
}
@@ -174,6 +176,8 @@ export default class ScanLndInvoice extends React.Component {
return {
invoice: data,
decoded,
+ unit: state.unit,
+ amount: decoded.num_satoshis,
expiresIn,
destination: data,
isAmountInitiallyEmpty: decoded.num_satoshis === '0',
@@ -220,6 +224,19 @@ export default class ScanLndInvoice extends React.Component {
}
}
+ let amountSats = this.state.amount;
+ switch (this.state.unit) {
+ case BitcoinUnit.SATS:
+ amountSats = parseInt(amountSats); // nop
+ break;
+ case BitcoinUnit.BTC:
+ amountSats = currency.btcToSatoshi(amountSats);
+ break;
+ case BitcoinUnit.LOCAL_CURRENCY:
+ amountSats = currency.btcToSatoshi(currency.fiatToBTC(amountSats));
+ break;
+ }
+
this.setState(
{
isLoading: true,
@@ -245,7 +262,7 @@ export default class ScanLndInvoice extends React.Component {
}
try {
- await fromWallet.payInvoice(this.state.invoice, this.state.decoded.num_satoshis);
+ await fromWallet.payInvoice(this.state.invoice, amountSats);
} catch (Err) {
console.log(Err.message);
this.setState({ isLoading: false });
@@ -255,7 +272,7 @@ export default class ScanLndInvoice extends React.Component {
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs
this.props.navigation.navigate('Success', {
- amount: this.state.decoded.num_satoshis,
+ amount: amountSats,
amountUnit: BitcoinUnit.SATS,
invoiceDescription: this.state.decoded.description,
});
@@ -275,11 +292,12 @@ export default class ScanLndInvoice extends React.Component {
if (typeof this.state.decoded !== 'object') {
return true;
} else {
- if (!this.state.decoded.num_satoshis) {
+ if (!this.state.amount) {
return true;
}
}
- return this.state.decoded.num_satoshis <= 0 || this.state.isLoading || isNaN(this.state.decoded.num_satoshis);
+ return !(this.state.amount > 0);
+ // return this.state.decoded.num_satoshis <= 0 || this.state.isLoading || isNaN(this.state.decoded.num_satoshis);
};
renderWalletSelectionButton = () => {
@@ -327,6 +345,10 @@ export default class ScanLndInvoice extends React.Component {
});
};
+ async componentDidMount() {
+ console.log('scanLndInvoice did mount');
+ }
+
render() {
if (!this.state.fromWallet) {
return ;
@@ -340,16 +362,25 @@ export default class ScanLndInvoice extends React.Component {
{
+ this.setState({ unit });
+ }}
onChangeText={text => {
- if (typeof this.state.decoded === 'object') {
+ this.setState({ amount: text });
+
+ /* if (typeof this.state.decoded === 'object') {
text = parseInt(text || 0);
const decoded = this.state.decoded;
decoded.num_satoshis = text;
this.setState({ decoded: decoded });
- }
+ } */
}}
- disabled={typeof this.state.decoded !== 'object' || this.state.isLoading}
+ disabled={
+ typeof this.state.decoded !== 'object' ||
+ this.state.isLoading ||
+ (this.state.decoded && this.state.decoded.num_satoshis > 0)
+ }
unit={BitcoinUnit.SATS}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
diff --git a/screen/receive/details.js b/screen/receive/details.js
index aed52bd6c..8c3c7e41b 100644
--- a/screen/receive/details.js
+++ b/screen/receive/details.js
@@ -1,7 +1,7 @@
import React, { useEffect, useState, useCallback } from 'react';
import { View, InteractionManager, Platform, TextInput, KeyboardAvoidingView, Keyboard, StyleSheet, ScrollView } from 'react-native';
import QRCode from 'react-native-qrcode-svg';
-import { useNavigation, useRoute, useIsFocused } from '@react-navigation/native';
+import { useNavigation, useRoute } from '@react-navigation/native';
import {
BlueLoading,
SafeBlueArea,
@@ -25,20 +25,21 @@ import Handoff from 'react-native-handoff';
/** @type {AppStorage} */
const BlueApp = require('../../BlueApp');
const loc = require('../../loc');
+const currency = require('../../blue_modules/currency');
const ReceiveDetails = () => {
const { secret } = useRoute().params;
- const [wallet, setWallet] = useState();
+ const wallet = BlueApp.getWallets().find(w => w.getSecret() === secret);
const [isHandOffUseEnabled, setIsHandOffUseEnabled] = useState(false);
const [address, setAddress] = useState('');
const [customLabel, setCustomLabel] = useState();
const [customAmount, setCustomAmount] = useState(0);
+ const [customUnit, setCustomUnit] = useState(BitcoinUnit.BTC);
const [bip21encoded, setBip21encoded] = useState();
const [qrCodeSVG, setQrCodeSVG] = useState();
const [isCustom, setIsCustom] = useState(false);
const [isCustomModalVisible, setIsCustomModalVisible] = useState(false);
const { navigate, goBack } = useNavigation();
- const isFocused = useIsFocused();
const renderReceiveDetails = useCallback(async () => {
console.log('receive/details - componentDidMount');
@@ -82,10 +83,6 @@ const ReceiveDetails = () => {
}, [wallet]);
useEffect(() => {
- Privacy.enableBlur();
-
- setWallet(BlueApp.getWallets().find(w => w.getSecret() === secret));
-
if (wallet) {
if (!wallet.getUserHasSavedExport()) {
BlueAlertWalletExportReminder({
@@ -102,6 +99,7 @@ const ReceiveDetails = () => {
}
}
HandoffSettings.isHandoffUseEnabled().then(setIsHandOffUseEnabled);
+ Privacy.enableBlur();
return () => Privacy.disableBlur();
}, [goBack, navigate, renderReceiveDetails, secret, wallet]);
@@ -117,7 +115,24 @@ const ReceiveDetails = () => {
const createCustomAmountAddress = () => {
setIsCustom(true);
setIsCustomModalVisible(false);
- setBip21encoded(DeeplinkSchemaMatch.bip21encode(address, { amount: customAmount, label: customLabel }));
+ let amount = customAmount;
+ switch (customUnit) {
+ case BitcoinUnit.BTC:
+ // nop
+ break;
+ case BitcoinUnit.SATS:
+ amount = currency.satoshiToBTC(customAmount);
+ break;
+ case BitcoinUnit.LOCAL_CURRENCY:
+ if (BlueBitcoinAmount.conversionCache[amount + BitcoinUnit.LOCAL_CURRENCY]) {
+ // cache hit! we reuse old value that supposedly doesnt have rounding errors
+ amount = currency.satoshiToBTC(BlueBitcoinAmount.conversionCache[amount + BitcoinUnit.LOCAL_CURRENCY]);
+ } else {
+ amount = currency.fiatToBTC(customAmount);
+ }
+ break;
+ }
+ setBip21encoded(DeeplinkSchemaMatch.bip21encode(address, { amount, label: customLabel }));
};
const clearCustomAmount = () => {
@@ -133,7 +148,12 @@ const ReceiveDetails = () => {
-
+
{
}
};
+ /**
+ * @returns {string} BTC amount, accounting for current `customUnit` and `customUnit`
+ */
+ const getDisplayAmount = () => {
+ switch (customUnit) {
+ case BitcoinUnit.BTC:
+ return customAmount + ' BTC';
+ case BitcoinUnit.SATS:
+ return currency.satoshiToBTC(customAmount) + ' BTC';
+ case BitcoinUnit.LOCAL_CURRENCY:
+ return currency.fiatToBTC(customAmount) + ' BTC';
+ }
+ return customAmount + ' ' + customUnit;
+ };
+
return (
{isHandOffUseEnabled && address !== undefined && (
@@ -181,19 +216,19 @@ const ReceiveDetails = () => {
url={`https://blockstream.info/address/${address}`}
/>
)}
-
+
{isCustom && (
<>
- {customAmount} {BitcoinUnit.BTC}
+ {getDisplayAmount()}
{customLabel}
>
)}
- {bip21encoded === undefined && isFocused ? (
+ {bip21encoded === undefined ? (
diff --git a/screen/send/confirm.js b/screen/send/confirm.js
index 006b58ec4..cba6a3ee8 100644
--- a/screen/send/confirm.js
+++ b/screen/send/confirm.js
@@ -19,7 +19,7 @@ import {
} from '../../class';
const loc = require('../../loc');
const EV = require('../../events');
-const currency = require('../../currency');
+const currency = require('../../blue_modules/currency');
const BlueElectrum = require('../../BlueElectrum');
const Bignumber = require('bignumber.js');
/** @type {AppStorage} */
@@ -125,6 +125,11 @@ export default class Confirm extends Component {
{' ' + BitcoinUnit.BTC}
+
+ {item.value !== BitcoinUnit.MAX && item.value
+ ? currency.satoshiToLocalCurrency(item.value)
+ : currency.satoshiToLocalCurrency(this.state.fromWallet.getBalance() - this.state.feeSatoshi)}
+
{loc.send.create.to}
{item.address}
@@ -212,6 +217,13 @@ const styles = StyleSheet.create({
fontSize: 15,
marginBottom: 20,
},
+ transactionAmountFiat: {
+ color: '#9aa0aa',
+ fontWeight: '500',
+ fontSize: 15,
+ marginVertical: 20,
+ textAlign: 'center',
+ },
valueWrap: {
flexDirection: 'row',
justifyContent: 'center',
@@ -247,6 +259,7 @@ const styles = StyleSheet.create({
marginTop: 16,
alignItems: 'center',
justifyContent: 'space-between',
+ backgroundColor: '#FFFFFF',
},
flat: {
maxHeight: '55%',
@@ -256,6 +269,7 @@ const styles = StyleSheet.create({
justifyContent: 'center',
paddingTop: 16,
paddingBottom: 16,
+ backgroundColor: '#FFFFFF',
},
cardText: {
color: '#37c0a1',
diff --git a/screen/send/create.js b/screen/send/create.js
index e46c5dbf8..52f289d8a 100644
--- a/screen/send/create.js
+++ b/screen/send/create.js
@@ -25,7 +25,7 @@ import RNFS from 'react-native-fs';
/** @type {AppStorage} */
const BlueApp = require('../../BlueApp');
const loc = require('../../loc');
-const currency = require('../../currency');
+const currency = require('../../blue_modules/currency');
export default class SendCreate extends Component {
static navigationOptions = ({ navigation, route }) => {
diff --git a/screen/send/details.js b/screen/send/details.js
index fb5bdd615..1dea1b51d 100644
--- a/screen/send/details.js
+++ b/screen/send/details.js
@@ -1,6 +1,7 @@
/* global alert */
import React, { Component } from 'react';
import {
+ ActivityIndicator,
View,
TextInput,
Alert,
@@ -13,7 +14,6 @@ import {
Dimensions,
Platform,
ScrollView,
- ActivityIndicator,
Text,
} from 'react-native';
import { Icon } from 'react-native-elements';
@@ -24,6 +24,7 @@ import {
BlueBitcoinAmount,
BlueAddressInput,
BlueDismissKeyboardInputAccessory,
+ BlueLoading,
BlueUseAllFundsButton,
BlueListItem,
BlueText,
@@ -41,6 +42,7 @@ import DocumentPicker from 'react-native-document-picker';
import RNFS from 'react-native-fs';
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
const bitcoin = require('bitcoinjs-lib');
+const currency = require('../../blue_modules/currency');
const BigNumber = require('bignumber.js');
const { width } = Dimensions.get('window');
const BlueApp: AppStorage = require('../../BlueApp');
@@ -52,6 +54,7 @@ const styles = StyleSheet.create({
loading: {
flex: 1,
paddingTop: 20,
+ backgroundColor: '#FFFFFF',
},
root: {
flex: 1,
@@ -254,10 +257,12 @@ export default class SendDetails extends Component {
recipientsScrollIndex: 0,
fromWallet,
addresses: [],
+ units: [],
memo: '',
networkTransactionFees: new NetworkTransactionFee(1, 1, 1),
fee: 1,
feeSliderValue: 1,
+ amountUnit: fromWallet.preferredBalanceUnit, // default for whole screen
bip70TransactionExpiration: null,
renderWalletSelectionButtonHidden: false,
};
@@ -289,18 +294,22 @@ export default class SendDetails extends Component {
feeSliderValue: bip70.feeSliderValue,
fee: bip70.fee,
isLoading: false,
+ amountUnit: BitcoinUnit.BTC,
bip70TransactionExpiration: bip70.bip70TransactionExpiration,
});
} else {
- console.warn('2');
const recipients = this.state.addresses;
const dataWithoutSchema = data.replace('bitcoin:', '').replace('BITCOIN:', '');
if (this.state.fromWallet.isAddressValid(dataWithoutSchema)) {
recipients[[this.state.recipientsScrollIndex]].address = dataWithoutSchema;
+ const units = this.state.units;
+ units[this.state.recipientsScrollIndex] = BitcoinUnit.BTC; // also resetting current unit to BTC
this.setState({
address: recipients,
bip70TransactionExpiration: null,
isLoading: false,
+ amountUnit: BitcoinUnit.BTC,
+ units,
});
} else {
let address = '';
@@ -322,6 +331,8 @@ export default class SendDetails extends Component {
}
console.log(options);
if (btcAddressRx.test(address) || address.indexOf('bc1') === 0 || address.indexOf('BC1') === 0) {
+ const units = this.state.units;
+ units[this.state.recipientsScrollIndex] = BitcoinUnit.BTC; // also resetting current unit to BTC
recipients[[this.state.recipientsScrollIndex]].address = address;
recipients[[this.state.recipientsScrollIndex]].amount = options.amount;
this.setState({
@@ -329,6 +340,8 @@ export default class SendDetails extends Component {
memo: options.label || options.message,
bip70TransactionExpiration: null,
isLoading: false,
+ amountUnit: BitcoinUnit.BTC,
+ units,
});
} else {
this.setState({ isLoading: false });
@@ -342,6 +355,7 @@ export default class SendDetails extends Component {
this.renderNavigationHeader();
console.log('send/details - componentDidMount');
StatusBar.setBarStyle('dark-content');
+ /** @type {BitcoinTransaction[]} */
const addresses = [];
let initialMemo = '';
if (this.props.route.params.uri) {
@@ -350,13 +364,13 @@ export default class SendDetails extends Component {
const { recipient, memo, fee, feeSliderValue } = await this.processBIP70Invoice(uri);
addresses.push(recipient);
initialMemo = memo;
- this.setState({ addresses, memo: initialMemo, fee, feeSliderValue, isLoading: false });
+ this.setState({ addresses, memo: initialMemo, fee, feeSliderValue, isLoading: false, amountUnit: BitcoinUnit.BTC });
} else {
try {
const { address, amount, memo } = this.decodeBitcoinUri(uri);
- addresses.push(new BitcoinTransaction(address, amount));
+ addresses.push(new BitcoinTransaction(address, amount, currency.btcToSatoshi(amount)));
initialMemo = memo;
- this.setState({ addresses, memo: initialMemo, isLoading: false });
+ this.setState({ addresses, memo: initialMemo, isLoading: false, amountUnit: BitcoinUnit.BTC });
} catch (error) {
console.log(error);
alert('Error: Unable to decode Bitcoin address');
@@ -365,7 +379,7 @@ export default class SendDetails extends Component {
} else if (this.props.route.params.address) {
addresses.push(new BitcoinTransaction(this.props.route.params.address));
if (this.props.route.params.memo) initialMemo = this.props.route.params.memo;
- this.setState({ addresses, memo: initialMemo, isLoading: false });
+ this.setState({ addresses, memo: initialMemo, isLoading: false, amountUnit: BitcoinUnit.BTC });
} else {
this.setState({ addresses: [new BitcoinTransaction()], isLoading: false });
}
@@ -446,33 +460,13 @@ export default class SendDetails extends Component {
return { address, amount, memo };
}
- recalculateAvailableBalance(balance, amount, fee) {
- if (!amount) amount = 0;
- if (!fee) fee = 0;
- let availableBalance;
- try {
- availableBalance = new BigNumber(balance);
- availableBalance = availableBalance.div(100000000); // sat2btc
- availableBalance = availableBalance.minus(amount);
- availableBalance = availableBalance.minus(fee);
- availableBalance = availableBalance.toString(10);
- } catch (err) {
- return balance;
- }
-
- return (availableBalance === 'NaN' && balance) || availableBalance;
- }
-
async processBIP70Invoice(text) {
try {
if (BitcoinBIP70TransactionDecode.matchesPaymentURL(text)) {
Keyboard.dismiss();
return BitcoinBIP70TransactionDecode.decode(text)
.then(response => {
- const recipient = new BitcoinTransaction(
- response.address,
- loc.formatBalanceWithoutSuffix(response.amount, BitcoinUnit.BTC, false),
- );
+ const recipient = new BitcoinTransaction(response.address, currency.satoshiToBTC(response.amount), response.amount);
return {
recipient,
memo: response.memo,
@@ -507,7 +501,7 @@ export default class SendDetails extends Component {
} else if (!transaction.address) {
error = loc.send.details.address_field_is_not_valid;
console.log('validation error');
- } else if (this.recalculateAvailableBalance(this.state.fromWallet.getBalance(), transaction.amount, 0) < 0) {
+ } else if (this.state.fromWallet.getBalance() - transaction.amountSats < 0) {
// first sanity check is that sending amount is not bigger than available balance
error = loc.send.details.total_exceeds_balance;
console.log('validation error');
@@ -577,7 +571,7 @@ export default class SendDetails extends Component {
targets = [{ address: transaction.address }];
break;
}
- const value = new BigNumber(transaction.amount).multipliedBy(100000000).toNumber();
+ const value = parseInt(transaction.amountSats);
if (value > 0) {
targets.push({ address: transaction.address, value });
}
@@ -787,7 +781,7 @@ export default class SendDetails extends Component {
const isSendMaxUsed = this.state.addresses.some(element => element.amount === BitcoinUnit.MAX);
return (
{
Keyboard.dismiss();
@@ -835,6 +829,8 @@ export default class SendDetails extends Component {
() => {
this.scrollView.scrollToEnd();
if (this.state.addresses.length > 1) this.scrollView.flashScrollIndicators();
+ // after adding recipient it automatically scrolls to the last one
+ this.setState({ recipientsScrollIndex: this.state.addresses.length - 1 });
},
);
}}
@@ -854,7 +850,8 @@ export default class SendDetails extends Component {
},
() => {
if (this.state.addresses.length > 1) this.scrollView.flashScrollIndicators();
- this.setState({ recipientsScrollIndex: this.scrollViewCurrentIndex });
+ // after deletion it automatically scrolls to the last one
+ this.setState({ recipientsScrollIndex: this.state.addresses.length - 1 });
},
);
}}
@@ -941,12 +938,49 @@ export default class SendDetails extends Component {
{
+ const units = this.state.units;
+ units[index] = unit;
+
+ const addresses = this.state.addresses;
+ const item = addresses[index];
+
+ switch (unit) {
+ case BitcoinUnit.SATS:
+ item.amountSats = parseInt(item.amount);
+ break;
+ case BitcoinUnit.BTC:
+ item.amountSats = currency.btcToSatoshi(item.amount);
+ break;
+ case BitcoinUnit.LOCAL_CURRENCY:
+ // also accounting for cached fiat->sat conversion to avoid rounding error
+ item.amountSats =
+ BlueBitcoinAmount.getCachedSatoshis(item.amount) || currency.btcToSatoshi(currency.fiatToBTC(item.amount));
+ break;
+ }
+
+ addresses[index] = item;
+ this.setState({ units, addresses });
+ }}
onChangeText={text => {
item.amount = text;
- const transactions = this.state.addresses;
- transactions[index] = item;
- this.setState({ addresses: transactions });
+ switch (this.state.units[index] || this.state.amountUnit) {
+ case BitcoinUnit.BTC:
+ item.amountSats = currency.btcToSatoshi(item.amount);
+ break;
+ case BitcoinUnit.LOCAL_CURRENCY:
+ item.amountSats = currency.btcToSatoshi(currency.fiatToBTC(item.amount));
+ break;
+ default:
+ case BitcoinUnit.SATS:
+ item.amountSats = parseInt(text);
+ break;
+ }
+ const addresses = this.state.addresses;
+ addresses[index] = item;
+ this.setState({ addresses });
}}
+ unit={this.state.units[index] || this.state.amountUnit}
inputAccessoryViewID={this.state.fromWallet.allowSendMax() ? BlueUseAllFundsButton.InputAccessoryViewID : null}
/>
+
+
+ );
+ }
return (
-
+
-
-
- (this.scrollView = ref)}
- onContentSizeChange={() => this.scrollView.scrollToEnd()}
- onLayout={() => this.scrollView.scrollToEnd()}
- onMomentumScrollEnd={this.handlePageChange}
- scrollEnabled={this.state.addresses.length > 1}
- scrollIndicatorInsets={{ top: 0, left: 8, bottom: 0, right: 8 }}
- >
- {this.renderBitcoinTransactionInfoFields()}
-
-
- this.setState({ memo: text })}
- placeholder={loc.send.details.note_placeholder}
- placeholderTextColor="#81868e"
- value={this.state.memo}
- numberOfLines={1}
- style={styles.memoText}
- editable={!this.state.isLoading}
- onSubmitEditing={Keyboard.dismiss}
- inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
- />
+
+ (this.scrollView = ref)}
+ keyboardShouldPersistTaps="always"
+ onContentSizeChange={() => this.scrollView.scrollToEnd()}
+ onLayout={() => this.scrollView.scrollToEnd()}
+ onMomentumScrollEnd={this.handlePageChange}
+ scrollEnabled={this.state.addresses.length > 1}
+ scrollIndicatorInsets={{ top: 0, left: 8, bottom: 0, right: 8 }}
+ >
+ {this.renderBitcoinTransactionInfoFields()}
+
+
+ this.setState({ memo: text })}
+ placeholder={loc.send.details.note_placeholder}
+ placeholderTextColor="#81868e"
+ value={this.state.memo}
+ numberOfLines={1}
+ style={styles.memoText}
+ editable={!this.state.isLoading}
+ onSubmitEditing={Keyboard.dismiss}
+ inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
+ />
+
+ this.setState({ isFeeSelectionModalVisible: true })}
+ disabled={this.state.isLoading}
+ style={styles.fee}
+ >
+ Fee
+
+ {this.state.fee}
+ sat/b
- this.setState({ isFeeSelectionModalVisible: true })}
- disabled={this.state.isLoading}
- style={styles.fee}
- >
- Fee
-
- {this.state.fee}
- sat/b
-
-
- {this.renderFeeSelectionModal()}
- {this.renderAdvancedTransactionOptionsModal()}
-
-
- {this.renderCreateButton()}
+
+ {this.renderCreateButton()}
+ {this.renderFeeSelectionModal()}
+ {this.renderAdvancedTransactionOptionsModal()}
+
{Platform.select({
- ios: ,
+ ios: (
+
+ ),
android: this.state.isAmountToolbarVisibleForAndroid && (
-
+
),
})}
diff --git a/screen/settings/currency.js b/screen/settings/currency.js
index 154e3f1d6..5db59df8e 100644
--- a/screen/settings/currency.js
+++ b/screen/settings/currency.js
@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import { Icon } from 'react-native-elements';
import { FiatUnit } from '../../models/fiatUnit';
const loc = require('../../loc');
-const currency = require('../../currency');
+const currency = require('../../blue_modules/currency');
const data = Object.values(FiatUnit);
diff --git a/screen/wallets/buyBitcoin.js b/screen/wallets/buyBitcoin.js
index 50b7f10b8..2f1ba7deb 100644
--- a/screen/wallets/buyBitcoin.js
+++ b/screen/wallets/buyBitcoin.js
@@ -4,7 +4,7 @@ import { BlueNavigationStyle, BlueLoading, SafeBlueArea } from '../../BlueCompon
import PropTypes from 'prop-types';
import { WebView } from 'react-native-webview';
import { AppStorage, LightningCustodianWallet, WatchOnlyWallet } from '../../class';
-const currency = require('../../currency');
+const currency = require('../../blue_modules/currency');
const BlueApp: AppStorage = require('../../BlueApp');
const loc = require('../../loc');
diff --git a/screen/wallets/reorderWallets.js b/screen/wallets/reorderWallets.js
index a981f9fd4..fdc6a2717 100644
--- a/screen/wallets/reorderWallets.js
+++ b/screen/wallets/reorderWallets.js
@@ -86,6 +86,8 @@ export default class ReorderWallets extends Component {
};
}
+ sortableList = React.createRef();
+
componentDidMount() {
this.props.navigation.setParams({
customCloseButtonFunction: async () => {
@@ -160,7 +162,7 @@ export default class ReorderWallets extends Component {
return (
(this.sortableList = ref)}
+ ref={this.sortableList}
style={styles.root}
data={this.state.data}
renderRow={this._renderItem}
diff --git a/tests/integration/Currency.test.js b/tests/integration/Currency.test.js
index 861af96c0..5fadbffca 100644
--- a/tests/integration/Currency.test.js
+++ b/tests/integration/Currency.test.js
@@ -8,7 +8,7 @@ jest.useFakeTimers();
describe('currency', () => {
it('fetches exchange rate and saves to AsyncStorage', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000;
- const currency = require('../../currency');
+ const currency = require('../../blue_modules/currency');
await currency.startUpdater();
let cur = await AsyncStorage.getItem(AppStorage.EXCHANGE_RATES);
cur = JSON.parse(cur);
diff --git a/tests/unit/Currency.test.js b/tests/unit/Currency.test.js
index 978b3e5ba..bec396f8d 100644
--- a/tests/unit/Currency.test.js
+++ b/tests/unit/Currency.test.js
@@ -4,8 +4,8 @@ const assert = require('assert');
describe('currency', () => {
it('formats everything correctly', async () => {
- const currency = require('../../currency');
- currency.exchangeRates.BTC_USD = 10000;
+ const currency = require('../../blue_modules/currency');
+ currency._setExchangeRate('BTC_USD', 10000);
assert.strictEqual(currency.satoshiToLocalCurrency(1), '$0.0001');
assert.strictEqual(currency.satoshiToLocalCurrency(-1), '-$0.0001');
@@ -26,10 +26,8 @@ describe('currency', () => {
assert.strictEqual(currency.satoshiToBTC(100000000), '1');
assert.strictEqual(currency.satoshiToBTC(123456789123456789), '1234567891.2345678');
- currency.preferredFiatCurrency.endPointKey = FiatUnit.JPY.endPointKey;
- currency.preferredFiatCurrency.symbol = FiatUnit.JPY.symbol;
- currency.preferredFiatCurrency.locale = FiatUnit.JPY.locale;
- currency.exchangeRates.BTC_JPY = 1043740.8614;
+ currency._setPreferredFiatCurrency(FiatUnit.JPY);
+ currency._setExchangeRate('BTC_JPY', 1043740.8614);
assert.strictEqual(currency.satoshiToLocalCurrency(1), '¥0.01');
});
diff --git a/tests/unit/Loc.test.js b/tests/unit/Loc.test.js
index 824962a3a..6b96ed00b 100644
--- a/tests/unit/Loc.test.js
+++ b/tests/unit/Loc.test.js
@@ -1,8 +1,88 @@
/* global it, describe */
+import { BitcoinUnit } from '../../models/bitcoinUnits';
+import { FiatUnit } from '../../models/fiatUnit';
const assert = require('assert');
const fs = require('fs');
+const loc = require('../../loc/');
+const currency = require('../../blue_modules/currency');
describe('Localization', () => {
+ it('internal formatter', () => {
+ assert.strictEqual(loc._leaveNumbersAndDots('1,00 ₽'), '1');
+ assert.strictEqual(loc._leaveNumbersAndDots('0,50 ₽"'), '0.50');
+ assert.strictEqual(loc._leaveNumbersAndDots('RUB 1,00'), '1');
+ });
+
+ it('formatBalancePlain() && formatBalancePlain()', () => {
+ currency._setExchangeRate('BTC_RUB', 660180.143);
+ currency._setPreferredFiatCurrency(FiatUnit.RUB);
+ let newInputValue = loc.formatBalanceWithoutSuffix(152, BitcoinUnit.LOCAL_CURRENCY, false);
+ assert.strictEqual(newInputValue, 'RUB 1.00');
+ newInputValue = loc.formatBalancePlain(152, BitcoinUnit.LOCAL_CURRENCY, false);
+ assert.strictEqual(newInputValue, '1');
+
+ newInputValue = loc.formatBalanceWithoutSuffix(1515, BitcoinUnit.LOCAL_CURRENCY, false);
+ assert.strictEqual(newInputValue, 'RUB 10.00');
+ newInputValue = loc.formatBalancePlain(1515, BitcoinUnit.LOCAL_CURRENCY, false);
+ assert.strictEqual(newInputValue, '10');
+
+ newInputValue = loc.formatBalanceWithoutSuffix(16793829, BitcoinUnit.LOCAL_CURRENCY, false);
+ assert.strictEqual(newInputValue, 'RUB 110,869.52');
+ newInputValue = loc.formatBalancePlain(16793829, BitcoinUnit.LOCAL_CURRENCY, false);
+ assert.strictEqual(newInputValue, '110869.52');
+
+ newInputValue = loc.formatBalancePlain(76, BitcoinUnit.LOCAL_CURRENCY, false);
+ assert.strictEqual(newInputValue, '0.50');
+
+ currency._setExchangeRate('BTC_USD', 10000);
+ currency._setPreferredFiatCurrency(FiatUnit.USD);
+ newInputValue = loc.formatBalanceWithoutSuffix(16793829, BitcoinUnit.LOCAL_CURRENCY, false);
+ assert.strictEqual(newInputValue, '$1,679.38');
+ newInputValue = loc.formatBalancePlain(16793829, BitcoinUnit.LOCAL_CURRENCY, false);
+ assert.strictEqual(newInputValue, '1679.38');
+
+ newInputValue = loc.formatBalancePlain(16000000, BitcoinUnit.LOCAL_CURRENCY, false);
+ assert.strictEqual(newInputValue, '1600');
+ });
+
+ it.each([
+ [123000000, BitcoinUnit.SATS, false, '123000000', false],
+ [123000000, BitcoinUnit.SATS, true, '123 000 000', false],
+ [123456000, BitcoinUnit.BTC, true, '1.23456', false],
+ ['123456000', BitcoinUnit.BTC, true, '1.23456', false], // can handle strings
+ [100000000, BitcoinUnit.BTC, true, '1', false],
+ [10000000, BitcoinUnit.BTC, true, '0.1', false],
+ [1, BitcoinUnit.BTC, true, '0.00000001', false],
+ [10000000, BitcoinUnit.LOCAL_CURRENCY, true, '...', true], // means unknown since we did not receive exchange rate
+ ])(
+ 'can formatBalanceWithoutSuffix',
+ async (balance, toUnit, withFormatting, expectedResult, shouldResetRate) => {
+ currency._setExchangeRate('BTC_USD', 1);
+ currency._setPreferredFiatCurrency(FiatUnit.USD);
+ if (shouldResetRate) {
+ currency._setExchangeRate('BTC_USD', false);
+ }
+ const actualResult = loc.formatBalanceWithoutSuffix(balance, toUnit, withFormatting);
+ assert.strictEqual(actualResult, expectedResult);
+ },
+ 240000,
+ );
+
+ it.each([
+ [123000000, BitcoinUnit.SATS, false, '123000000 sats'],
+ [123000000, BitcoinUnit.BTC, false, '1.23 BTC'],
+ [123000000, BitcoinUnit.LOCAL_CURRENCY, false, '$1.23'],
+ ])(
+ 'can formatBalance',
+ async (balance, toUnit, withFormatting, expectedResult) => {
+ currency._setExchangeRate('BTC_USD', 1);
+ currency._setPreferredFiatCurrency(FiatUnit.USD);
+ const actualResult = loc.formatBalance(balance, toUnit, withFormatting);
+ assert.strictEqual(actualResult, expectedResult);
+ },
+ 240000,
+ );
+
it('has all keys in all locales', async () => {
const en = require('../../loc/en');
let issues = 0;