ADD: fiat input (#1190)

This commit is contained in:
Overtorment 2020-06-09 15:08:18 +01:00 committed by GitHub
parent b159674411
commit 2243a19a2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 683 additions and 210 deletions

View file

@ -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

View file

@ -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 <Text {...this.props} style={{ color: BlueApp.settings.foregroundColor, textAlign: 'center' }} />;
@ -828,6 +828,10 @@ export class BlueUseAllFundsButton extends Component {
onUseAllPressed: PropTypes.func.isRequired,
};
static defaultProps = {
unit: BitcoinUnit.BTC,
};
render() {
const inputView = (
<View
@ -1809,7 +1813,9 @@ const WalletCarouselItem = ({ item, index, onPress, handleLongPress }) => {
return (
<NewWalletPanel
onPress={() => {
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();
}}
>
<LinearGradient
@ -2016,7 +2026,7 @@ export class WalletsCarousel extends Component {
itemWidth={itemWidth}
inactiveSlideScale={1}
inactiveSlideOpacity={0.7}
initialNumToRender={4}
initialNumToRender={20}
onLayout={this.onLayout}
contentContainerCustomStyle={{ left: -20 }}
/>
@ -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 <BitcoinUnit.*>
*/
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 (
<TouchableWithoutFeedback disabled={this.props.pointerEvents === 'none'} onPress={this.handleTextInputOnPress}>
<View>
<View style={{ flexDirection: 'row', justifyContent: 'center', paddingTop: 16, paddingBottom: 2 }}>
<TextInput
{...this.props}
testID="BitcoinAmountInput"
keyboardType="numeric"
onChangeText={text => {
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',
}}
/>
<Text
style={{
color: this.props.disabled ? BlueApp.settings.buttonDisabledTextColor : BlueApp.settings.alternativeTextColor2,
fontSize: 16,
marginHorizontal: 4,
paddingBottom: 6,
fontWeight: '600',
alignSelf: 'flex-end',
}}
// if main display is sat or btc - secondary display is fiat
// if main display is fiat - secondary dislay is btc
let sat;
switch (this.state.unit) {
case BitcoinUnit.BTC:
sat = new BigNumber(amount).multipliedBy(100000000).toString();
secondaryDisplayCurrency = loc.formatBalanceWithoutSuffix(sat, BitcoinUnit.LOCAL_CURRENCY, false);
break;
case BitcoinUnit.SATS:
secondaryDisplayCurrency = loc.formatBalanceWithoutSuffix(amount.toString(), BitcoinUnit.LOCAL_CURRENCY, false);
break;
case BitcoinUnit.LOCAL_CURRENCY:
secondaryDisplayCurrency = currency.fiatToBTC(parseFloat(amount));
if (BlueBitcoinAmount.conversionCache[amount + BitcoinUnit.LOCAL_CURRENCY]) {
// cache hit! we reuse old value that supposedly doesnt have rounding errors
const sats = BlueBitcoinAmount.conversionCache[amount + BitcoinUnit.LOCAL_CURRENCY];
secondaryDisplayCurrency = currency.satoshiToBTC(sats);
}
break;
}
if (amount === BitcoinUnit.MAX) secondaryDisplayCurrency = ''; // we dont want to display NaN
return (
<TouchableWithoutFeedback disabled={this.props.pointerEvents === 'none'} onPress={() => this.textInput.focus()}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
{!this.props.disabled && <View style={{ alignSelf: 'center', marginLeft: 16, padding: 15 }} />}
<View style={{ flex: 1 }}>
<View
style={{ flexDirection: 'row', alignContent: 'space-between', justifyContent: 'center', paddingTop: 16, paddingBottom: 2 }}
>
{' ' + this.props.unit}
</Text>
</View>
<View style={{ alignItems: 'center', marginBottom: 22, marginTop: 4 }}>
<Text style={{ fontSize: 18, color: '#d4d4d4', fontWeight: '600' }}>{localCurrency}</Text>
{this.state.unit === BitcoinUnit.LOCAL_CURRENCY && (
<Text
style={{
color: this.props.disabled ? BlueApp.settings.buttonDisabledTextColor : BlueApp.settings.alternativeTextColor2,
fontSize: 18,
marginHorizontal: 4,
fontWeight: 'bold',
alignSelf: 'center',
justifyContent: 'center',
}}
>
{currency.getCurrencySymbol() + ' '}
</Text>
)}
<TextInput
{...this.props}
testID="BitcoinAmountInput"
keyboardType="numeric"
adjustsFontSizeToFit
onChangeText={text => {
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 && (
<Text
style={{
color: this.props.disabled ? BlueApp.settings.buttonDisabledTextColor : BlueApp.settings.alternativeTextColor2,
fontSize: 15,
marginHorizontal: 4,
fontWeight: '600',
alignSelf: 'center',
justifyContent: 'center',
}}
>
{' ' + this.state.unit}
</Text>
)}
</View>
<View style={{ alignItems: 'center', marginBottom: 22 }}>
<Text style={{ fontSize: 16, color: '#9BA0A9', fontWeight: '600' }}>
{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}
</Text>
</View>
</View>
{!this.props.disabled && (
<TouchableOpacity
style={{ alignSelf: 'center', marginRight: 16, paddingLeft: 16, paddingVertical: 16 }}
onPress={this.changeAmountUnit}
>
<Image source={require('./img/round-compare-arrows-24-px.png')} />
</TouchableOpacity>
)}
</View>
</TouchableWithoutFeedback>
);
}
}
const styles = StyleSheet.create({
balanceBlur: {
height: 30,

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 B

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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 {
<BlueBitcoinAmount
isLoading={this.state.isLoading}
amount={this.state.amount}
onAmountUnitChange={unit => {
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}
/>
<View style={styles.fiat}>

View file

@ -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 <BlueLoading />;
@ -340,16 +362,25 @@ export default class ScanLndInvoice extends React.Component {
<BlueBitcoinAmount
pointerEvents={this.state.isAmountInitiallyEmpty ? 'auto' : 'none'}
isLoading={this.state.isLoading}
amount={typeof this.state.decoded === 'object' ? this.state.decoded.num_satoshis : 0}
amount={this.state.amount}
onAmountUnitChange={unit => {
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}
/>

View file

@ -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 = () => {
<Modal isVisible={isCustomModalVisible} style={styles.bottomModal} onBackdropPress={dismissCustomAmountModal}>
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'position' : null}>
<View style={styles.modalContent}>
<BlueBitcoinAmount amount={customAmount || ''} onChangeText={setCustomAmount} />
<BlueBitcoinAmount
unit={customUnit}
amount={customAmount || ''}
onChangeText={setCustomAmount}
onAmountUnitChange={setCustomUnit}
/>
<View style={styles.customAmount}>
<TextInput
onChangeText={setCustomLabel}
@ -172,6 +192,21 @@ 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 (
<SafeBlueArea style={styles.root}>
{isHandOffUseEnabled && address !== undefined && (
@ -181,19 +216,19 @@ const ReceiveDetails = () => {
url={`https://blockstream.info/address/${address}`}
/>
)}
<ScrollView contentContainerStyle={styles.scroll}>
<ScrollView contentContainerStyle={styles.scroll} keyboardShouldPersistTaps="always">
<View style={styles.scrollBody}>
{isCustom && (
<>
<BlueText style={styles.amount} numberOfLines={1}>
{customAmount} {BitcoinUnit.BTC}
{getDisplayAmount()}
</BlueText>
<BlueText style={styles.label} numberOfLines={1}>
{customLabel}
</BlueText>
</>
)}
{bip21encoded === undefined && isFocused ? (
{bip21encoded === undefined ? (
<View style={styles.loading}>
<BlueLoading />
</View>

View file

@ -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 {
</Text>
<Text style={styles.valueUnit}>{' ' + BitcoinUnit.BTC}</Text>
</View>
<Text style={styles.transactionAmountFiat}>
{item.value !== BitcoinUnit.MAX && item.value
? currency.satoshiToLocalCurrency(item.value)
: currency.satoshiToLocalCurrency(this.state.fromWallet.getBalance() - this.state.feeSatoshi)}
</Text>
<BlueCard>
<Text style={styles.transactionDetailsTitle}>{loc.send.create.to}</Text>
<Text style={styles.transactionDetailsSubtitle}>{item.address}</Text>
@ -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',

View file

@ -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 }) => {

View file

@ -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 (
<Modal
isVisible={this.state.isAdvancedTransactionOptionsVisible && !this.state.isLoading}
isVisible={this.state.isAdvancedTransactionOptionsVisible}
style={styles.bottomModal}
onBackdropPress={() => {
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 {
<BlueBitcoinAmount
isLoading={this.state.isLoading}
amount={item.amount ? item.amount.toString() : null}
onAmountUnitChange={unit => {
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}
/>
<BlueAddressInput
@ -1002,7 +1036,12 @@ export default class SendDetails extends Component {
Keyboard.dismiss();
const recipient = this.state.addresses[this.state.recipientsScrollIndex];
recipient.amount = BitcoinUnit.MAX;
this.setState({ addresses: [recipient], recipientsScrollIndex: 0, isAdvancedTransactionOptionsVisible: false });
this.setState({
addresses: [recipient],
units: [BitcoinUnit.BTC],
recipientsScrollIndex: 0,
isAdvancedTransactionOptionsVisible: false,
});
},
style: 'default',
},
@ -1013,61 +1052,68 @@ export default class SendDetails extends Component {
};
render() {
const opacity = this.state.isLoading ? 0.5 : 1.0;
if (this.state.isLoading || typeof this.state.fromWallet === 'undefined') {
return (
<View style={styles.loading}>
<BlueLoading />
</View>
);
}
return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
<View style={styles.root} pointerEvents={this.state.isLoading ? 'none' : 'auto'}>
<View style={styles.root}>
<View>
<View style={{ opacity: opacity }}>
<KeyboardAvoidingView behavior="position">
<ScrollView
pagingEnabled
horizontal
contentContainerStyle={styles.scrollViewContent}
ref={ref => (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()}
</ScrollView>
<View hide={!this.state.showMemoRow} style={styles.memo}>
<TextInput
onChangeText={text => 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}
/>
<KeyboardAvoidingView behavior="position">
<ScrollView
pagingEnabled
horizontal
contentContainerStyle={styles.scrollViewContent}
ref={ref => (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()}
</ScrollView>
<View hide={!this.state.showMemoRow} style={styles.memo}>
<TextInput
onChangeText={text => 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}
/>
</View>
<TouchableOpacity
onPress={() => this.setState({ isFeeSelectionModalVisible: true })}
disabled={this.state.isLoading}
style={styles.fee}
>
<Text style={styles.feeLabel}>Fee</Text>
<View style={styles.feeRow}>
<Text style={styles.feeValue}>{this.state.fee}</Text>
<Text style={styles.feeUnit}>sat/b</Text>
</View>
<TouchableOpacity
onPress={() => this.setState({ isFeeSelectionModalVisible: true })}
disabled={this.state.isLoading}
style={styles.fee}
>
<Text style={styles.feeLabel}>Fee</Text>
<View style={styles.feeRow}>
<Text style={styles.feeValue}>{this.state.fee}</Text>
<Text style={styles.feeUnit}>sat/b</Text>
</View>
</TouchableOpacity>
{this.renderFeeSelectionModal()}
{this.renderAdvancedTransactionOptionsModal()}
</KeyboardAvoidingView>
</View>
{this.renderCreateButton()}
</TouchableOpacity>
{this.renderCreateButton()}
{this.renderFeeSelectionModal()}
{this.renderAdvancedTransactionOptionsModal()}
</KeyboardAvoidingView>
</View>
<BlueDismissKeyboardInputAccessory />
{Platform.select({
ios: <BlueUseAllFundsButton onUseAllPressed={this.onUseAllPressed} wallet={this.state.fromWallet} />,
ios: (
<BlueUseAllFundsButton unit={this.state.amountUnit} onUseAllPressed={this.onUseAllPressed} wallet={this.state.fromWallet} />
),
android: this.state.isAmountToolbarVisibleForAndroid && (
<BlueUseAllFundsButton onUseAllPressed={this.onUseAllPressed} wallet={this.state.fromWallet} />
<BlueUseAllFundsButton unit={this.state.amountUnit} onUseAllPressed={this.onUseAllPressed} wallet={this.state.fromWallet} />
),
})}

View file

@ -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);

View file

@ -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');

View file

@ -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 (
<SafeBlueArea>
<SortableList
ref={ref => (this.sortableList = ref)}
ref={this.sortableList}
style={styles.root}
data={this.state.data}
renderRow={this._renderItem}

View file

@ -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);

View file

@ -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');
});

View file

@ -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;