mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-01-19 05:45:15 +01:00
ADD: Batch send UI
WIP: Create batch TX WIP: Continuing... WIP: More work WIP: More work... WIP WIP Update signer.js FIX: Fix Send Max ADD: Warn user if they want to make a max transaction but then switch to a wallet without support. FIX: Fixed BIP70 processing FIX: Removed parameter FIX: Fixed multiple UI recipient bugs. WIP WIP ADD: Scroll the problematic transaction Update details.js Update details.js FIX: Fix crash when switching wallets FIX: Success message
This commit is contained in:
parent
23ebcd4987
commit
6f2280ab0e
@ -382,6 +382,36 @@ export const BlueNavigationStyle = (navigation, withNavigationCloseButton = fals
|
|||||||
headerBackTitle: null,
|
headerBackTitle: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const BlueCreateTxNavigationStyle = (navigation, withAdvancedOptionsMenuButton = false, advancedOptionsMenuButtonAction) => ({
|
||||||
|
headerStyle: {
|
||||||
|
backgroundColor: BlueApp.settings.brandingColor,
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
elevation: 0,
|
||||||
|
},
|
||||||
|
headerTitleStyle: {
|
||||||
|
fontWeight: '600',
|
||||||
|
color: BlueApp.settings.foregroundColor,
|
||||||
|
},
|
||||||
|
headerTintColor: BlueApp.settings.foregroundColor,
|
||||||
|
headerLeft: (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{ minWwidth: 40, height: 40, padding: 14 }}
|
||||||
|
onPress={() => {
|
||||||
|
Keyboard.dismiss();
|
||||||
|
navigation.goBack(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image style={{ alignSelf: 'center' }} source={require('./img/close.png')} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
),
|
||||||
|
headerRight: withAdvancedOptionsMenuButton ? (
|
||||||
|
<TouchableOpacity style={{ minWidth: 40, height: 40, padding: 14 }} onPress={advancedOptionsMenuButtonAction}>
|
||||||
|
<Icon size={22} name="kebab-horizontal" type="octicon" color={BlueApp.settings.foregroundColor} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : null,
|
||||||
|
headerBackTitle: null,
|
||||||
|
});
|
||||||
|
|
||||||
export const BluePrivateBalance = () => {
|
export const BluePrivateBalance = () => {
|
||||||
return Platform.select({
|
return Platform.select({
|
||||||
ios: (
|
ios: (
|
||||||
@ -815,7 +845,7 @@ export class BlueUseAllFundsButton extends Component {
|
|||||||
<BlueButtonLink
|
<BlueButtonLink
|
||||||
style={{ paddingRight: 8, paddingLeft: 0, paddingTop: 12, paddingBottom: 12 }}
|
style={{ paddingRight: 8, paddingLeft: 0, paddingTop: 12, paddingBottom: 12 }}
|
||||||
title="Done"
|
title="Done"
|
||||||
onPress={Keyboard.dismiss}
|
onPress={() => Keyboard.dismiss()}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -1942,7 +1972,7 @@ export class BlueAddressInput extends Component {
|
|||||||
value={this.props.address}
|
value={this.props.address}
|
||||||
style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }}
|
style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }}
|
||||||
editable={!this.props.isLoading}
|
editable={!this.props.isLoading}
|
||||||
onSubmitEditing={Keyboard.dismiss}
|
onSubmitEditing={() => Keyboard.dismiss()}
|
||||||
{...this.props}
|
{...this.props}
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
@ -111,7 +111,7 @@ function BTCToLocalCurrency(bitcoin) {
|
|||||||
function satoshiToBTC(satoshi) {
|
function satoshiToBTC(satoshi) {
|
||||||
let b = new BigNumber(satoshi);
|
let b = new BigNumber(satoshi);
|
||||||
b = b.dividedBy(100000000);
|
b = b.dividedBy(100000000);
|
||||||
return b.toString(10) + ' BTC';
|
return b.toString(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.updateExchangeRate = updateExchangeRate;
|
module.exports.updateExchangeRate = updateExchangeRate;
|
||||||
|
@ -72,7 +72,7 @@ target 'BlueWalletWatch Extension' do
|
|||||||
# Comment the next line if you're not using Swift and don't want to use dynamic frameworks
|
# Comment the next line if you're not using Swift and don't want to use dynamic frameworks
|
||||||
# use_frameworks!
|
# use_frameworks!
|
||||||
platform :watchos, '5.1'
|
platform :watchos, '5.1'
|
||||||
pod 'EFQRCode', '~> 5.0.0'
|
pod 'EFQRCode', '5.0.0'
|
||||||
# Pods for BlueWalletWatch Extension
|
# Pods for BlueWalletWatch Extension
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -162,7 +162,7 @@ DEPENDENCIES:
|
|||||||
- appcenter-crashes (from `../node_modules/appcenter-crashes`)
|
- appcenter-crashes (from `../node_modules/appcenter-crashes`)
|
||||||
- BVLinearGradient (from `../node_modules/react-native-linear-gradient`)
|
- BVLinearGradient (from `../node_modules/react-native-linear-gradient`)
|
||||||
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
||||||
- EFQRCode (~> 5.0.0)
|
- EFQRCode (= 5.0.0)
|
||||||
- Folly (from `../node_modules/react-native/third-party-podspecs/Folly.podspec`)
|
- Folly (from `../node_modules/react-native/third-party-podspecs/Folly.podspec`)
|
||||||
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
||||||
- RCTSystemSetting (from `../node_modules/react-native-system-setting`)
|
- RCTSystemSetting (from `../node_modules/react-native-system-setting`)
|
||||||
@ -362,6 +362,6 @@ SPEC CHECKSUMS:
|
|||||||
ToolTipMenu: c158702a26154d892bc9e6eaa7d7382f0f1ee16e
|
ToolTipMenu: c158702a26154d892bc9e6eaa7d7382f0f1ee16e
|
||||||
yoga: 312528f5bbbba37b4dcea5ef00e8b4033fdd9411
|
yoga: 312528f5bbbba37b4dcea5ef00e8b4033fdd9411
|
||||||
|
|
||||||
PODFILE CHECKSUM: 0367389d52d7644ba1974734bda6b2a9b29d2311
|
PODFILE CHECKSUM: 1a39cb1ff2ae237255b84fcbe9415cb089e659c7
|
||||||
|
|
||||||
COCOAPODS: 1.7.5
|
COCOAPODS: 1.7.5
|
||||||
|
6
models/bitcoinTransactionInfo.js
Normal file
6
models/bitcoinTransactionInfo.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export class BitcoinTransaction {
|
||||||
|
constructor(address = '', amount) {
|
||||||
|
this.address = address;
|
||||||
|
this.amount = amount;
|
||||||
|
}
|
||||||
|
}
|
@ -31,7 +31,6 @@ exports.createHDTransaction = function(utxos, toAddress, amount, fixedFee, chang
|
|||||||
}
|
}
|
||||||
outputNum++;
|
outputNum++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unspentAmountSatoshi < amountToOutputSatoshi + feeInSatoshis) {
|
if (unspentAmountSatoshi < amountToOutputSatoshi + feeInSatoshis) {
|
||||||
throw new Error('Not enough balance. Please, try sending a smaller amount.');
|
throw new Error('Not enough balance. Please, try sending a smaller amount.');
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
/* global alert */
|
/* global alert */
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { ActivityIndicator, TouchableOpacity, StyleSheet, View } from 'react-native';
|
import { ActivityIndicator, FlatList, TouchableOpacity, StyleSheet, View } from 'react-native';
|
||||||
import { Text } from 'react-native-elements';
|
import { Text } from 'react-native-elements';
|
||||||
import { BlueButton, SafeBlueArea, BlueCard, BlueSpacing40, BlueNavigationStyle } from '../../BlueComponents';
|
import { BlueButton, BlueText, SafeBlueArea, BlueCard, BlueSpacing40, BlueNavigationStyle } from '../../BlueComponents';
|
||||||
import { BitcoinUnit } from '../../models/bitcoinUnits';
|
import { BitcoinUnit } from '../../models/bitcoinUnits';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||||
@ -11,6 +11,8 @@ let EV = require('../../events');
|
|||||||
let currency = require('../../currency');
|
let currency = require('../../currency');
|
||||||
let BlueElectrum = require('../../BlueElectrum');
|
let BlueElectrum = require('../../BlueElectrum');
|
||||||
let Bignumber = require('bignumber.js');
|
let Bignumber = require('bignumber.js');
|
||||||
|
/** @type {AppStorage} */
|
||||||
|
const BlueApp = require('../../BlueApp');
|
||||||
|
|
||||||
export default class Confirm extends Component {
|
export default class Confirm extends Component {
|
||||||
static navigationOptions = () => ({
|
static navigationOptions = () => ({
|
||||||
@ -23,11 +25,10 @@ export default class Confirm extends Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
amount: props.navigation.getParam('amount'),
|
|
||||||
fee: props.navigation.getParam('fee'),
|
fee: props.navigation.getParam('fee'),
|
||||||
feeSatoshi: new Bignumber(props.navigation.getParam('fee')).multipliedBy(100000000).toNumber(),
|
feeSatoshi: new Bignumber(props.navigation.getParam('fee')).multipliedBy(100000000).toNumber(),
|
||||||
address: props.navigation.getParam('address'),
|
|
||||||
memo: props.navigation.getParam('memo'),
|
memo: props.navigation.getParam('memo'),
|
||||||
|
recipients: props.navigation.getParam('recipients'),
|
||||||
size: Math.round(props.navigation.getParam('tx').length / 2),
|
size: Math.round(props.navigation.getParam('tx').length / 2),
|
||||||
tx: props.navigation.getParam('tx'),
|
tx: props.navigation.getParam('tx'),
|
||||||
satoshiPerByte: props.navigation.getParam('satoshiPerByte'),
|
satoshiPerByte: props.navigation.getParam('satoshiPerByte'),
|
||||||
@ -37,7 +38,7 @@ export default class Confirm extends Component {
|
|||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
console.log('send/confirm - componentDidMount');
|
console.log('send/confirm - componentDidMount');
|
||||||
console.log('address = ', this.state.address);
|
console.log('address = ', this.state.addresses);
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcast() {
|
broadcast() {
|
||||||
@ -54,12 +55,17 @@ export default class Confirm extends Component {
|
|||||||
} else {
|
} else {
|
||||||
console.log('broadcast result = ', result);
|
console.log('broadcast result = ', result);
|
||||||
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs
|
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs
|
||||||
|
let amount = 0;
|
||||||
|
for (const recipient of this.state.recipients) {
|
||||||
|
amount += recipient.value;
|
||||||
|
}
|
||||||
|
amount = loc.formatBalanceWithoutSuffix(amount, BitcoinUnit.BTC, false);
|
||||||
this.props.navigation.navigate('Success', {
|
this.props.navigation.navigate('Success', {
|
||||||
fee: Number(this.state.fee),
|
fee: Number(this.state.fee),
|
||||||
amount: this.state.amount,
|
amount,
|
||||||
address: this.state.address,
|
|
||||||
dismissModal: () => this.props.navigation.dismiss(),
|
dismissModal: () => this.props.navigation.dismiss(),
|
||||||
});
|
});
|
||||||
|
this.setState({ isLoading: false });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
|
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
|
||||||
@ -69,70 +75,107 @@ export default class Confirm extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_renderItem = ({ index, item }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View style={{ flexDirection: 'row', justifyContent: 'center' }}>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: '#0f5cc0',
|
||||||
|
fontSize: 36,
|
||||||
|
fontWeight: '600',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.amount === BitcoinUnit.MAX
|
||||||
|
? currency.satoshiToBTC(this.state.fromWallet.getBalance() - this.state.feeSatoshi)
|
||||||
|
: currency.satoshiToBTC(item.value)}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: '#0f5cc0',
|
||||||
|
fontSize: 16,
|
||||||
|
marginHorizontal: 4,
|
||||||
|
paddingBottom: 6,
|
||||||
|
fontWeight: '600',
|
||||||
|
alignSelf: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{' ' + BitcoinUnit.BTC}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<BlueCard>
|
||||||
|
<Text style={styles.transactionDetailsTitle}>{loc.send.create.to}</Text>
|
||||||
|
<Text style={styles.transactionDetailsSubtitle}>{item.address}</Text>
|
||||||
|
</BlueCard>
|
||||||
|
{this.state.recipients.length > 1 && (
|
||||||
|
<BlueText style={{ alignSelf: 'flex-end', marginRight: 18, marginVertical: 8 }}>
|
||||||
|
{index + 1} of {this.state.recipients.length}
|
||||||
|
</BlueText>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderSeparator = () => {
|
||||||
|
return <View style={{ backgroundColor: BlueApp.settings.inputBorderColor, height: 0.5, margin: 16 }} />;
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<SafeBlueArea style={{ flex: 1, paddingTop: 19 }}>
|
<SafeBlueArea style={{ flex: 1, paddingTop: 19 }}>
|
||||||
<BlueCard style={{ alignItems: 'center', flex: 1 }}>
|
<View style={{ marginTop: 16, alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<FlatList
|
||||||
|
scrollEnabled={this.state.recipients.length > 1}
|
||||||
|
extraData={this.state.recipients}
|
||||||
|
data={this.state.recipients}
|
||||||
|
renderItem={this._renderItem}
|
||||||
|
keyExtractor={(_item, index) => `${index}`}
|
||||||
|
ItemSeparatorComponent={this.renderSeparator}
|
||||||
|
style={{ maxHeight: '55%' }}
|
||||||
|
/>
|
||||||
<View style={{ flexDirection: 'row', justifyContent: 'center', paddingTop: 16, paddingBottom: 16 }}>
|
<View style={{ flexDirection: 'row', justifyContent: 'center', paddingTop: 16, paddingBottom: 16 }}>
|
||||||
<Text
|
<BlueCard>
|
||||||
style={{
|
<Text
|
||||||
color: '#0f5cc0',
|
style={{
|
||||||
fontSize: 36,
|
color: '#37c0a1',
|
||||||
fontWeight: '600',
|
fontSize: 14,
|
||||||
}}
|
marginHorizontal: 4,
|
||||||
>
|
paddingBottom: 6,
|
||||||
{this.state.amount}
|
fontWeight: '500',
|
||||||
</Text>
|
alignSelf: 'center',
|
||||||
<Text
|
}}
|
||||||
style={{
|
>
|
||||||
color: '#0f5cc0',
|
{loc.send.create.fee}: {loc.formatBalance(this.state.feeSatoshi, BitcoinUnit.BTC)} (
|
||||||
fontSize: 16,
|
{currency.satoshiToLocalCurrency(this.state.feeSatoshi)})
|
||||||
marginHorizontal: 4,
|
</Text>
|
||||||
paddingBottom: 6,
|
<BlueSpacing40 />
|
||||||
fontWeight: '600',
|
{this.state.isLoading ? (
|
||||||
alignSelf: 'flex-end',
|
<ActivityIndicator />
|
||||||
}}
|
) : (
|
||||||
>
|
<BlueButton onPress={() => this.broadcast()} title={loc.send.confirm.sendNow} />
|
||||||
{' ' + BitcoinUnit.BTC}
|
)}
|
||||||
</Text>
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{ marginVertical: 24 }}
|
||||||
|
onPress={() =>
|
||||||
|
this.props.navigation.navigate('CreateTransaction', {
|
||||||
|
fee: this.state.fee,
|
||||||
|
recipients: this.state.recipients,
|
||||||
|
memo: this.state.memo,
|
||||||
|
tx: this.state.tx,
|
||||||
|
satoshiPerByte: this.state.satoshiPerByte,
|
||||||
|
wallet: this.state.fromWallet,
|
||||||
|
feeSatoshi: this.state.feeSatoshi,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text style={{ color: '#0c2550', fontSize: 15, fontWeight: '500', alignSelf: 'center' }}>
|
||||||
|
{loc.transactions.details.transaction_details}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</BlueCard>
|
||||||
</View>
|
</View>
|
||||||
<Text
|
</View>
|
||||||
style={{
|
|
||||||
color: '#37c0a1',
|
|
||||||
fontSize: 14,
|
|
||||||
marginHorizontal: 4,
|
|
||||||
paddingBottom: 6,
|
|
||||||
fontWeight: '500',
|
|
||||||
alignSelf: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{loc.send.create.fee}: {loc.formatBalance(this.state.feeSatoshi, BitcoinUnit.BTC)} (
|
|
||||||
{currency.satoshiToLocalCurrency(this.state.feeSatoshi)})
|
|
||||||
</Text>
|
|
||||||
</BlueCard>
|
|
||||||
<BlueCard>
|
|
||||||
<Text style={styles.transactionDetailsTitle}>{loc.send.create.to}</Text>
|
|
||||||
<Text style={styles.transactionDetailsSubtitle}>{this.state.address}</Text>
|
|
||||||
<BlueSpacing40 />
|
|
||||||
{this.state.isLoading ? <ActivityIndicator /> : <BlueButton onPress={() => this.broadcast()} title={loc.send.confirm.sendNow} />}
|
|
||||||
<TouchableOpacity
|
|
||||||
style={{ marginVertical: 24 }}
|
|
||||||
onPress={() =>
|
|
||||||
this.props.navigation.navigate('CreateTransaction', {
|
|
||||||
amount: this.state.amount,
|
|
||||||
fee: this.state.fee,
|
|
||||||
address: this.state.address,
|
|
||||||
memo: this.state.memo,
|
|
||||||
tx: this.state.tx,
|
|
||||||
satoshiPerByte: this.state.satoshiPerByte,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text style={{ color: '#0c2550', fontSize: 15, fontWeight: '500', alignSelf: 'center' }}>
|
|
||||||
{loc.transactions.details.transaction_details}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</BlueCard>
|
|
||||||
</SafeBlueArea>
|
</SafeBlueArea>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,25 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { TextInput, ScrollView, Linking, TouchableOpacity, Clipboard, StyleSheet, TouchableWithoutFeedback, Keyboard } from 'react-native';
|
import {
|
||||||
import { Text } from 'react-native-elements';
|
TextInput,
|
||||||
|
FlatList,
|
||||||
|
ScrollView,
|
||||||
|
Linking,
|
||||||
|
TouchableOpacity,
|
||||||
|
Clipboard,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableWithoutFeedback,
|
||||||
|
Keyboard,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
} from 'react-native';
|
||||||
import { BlueNavigationStyle, SafeBlueArea, BlueCard, BlueText } from '../../BlueComponents';
|
import { BlueNavigationStyle, SafeBlueArea, BlueCard, BlueText } from '../../BlueComponents';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Privacy from '../../Privacy';
|
import Privacy from '../../Privacy';
|
||||||
|
import { BitcoinUnit } from '../../models/bitcoinUnits';
|
||||||
let loc = require('../../loc');
|
/** @type {AppStorage} */
|
||||||
|
const BlueApp = require('../../BlueApp');
|
||||||
|
const loc = require('../../loc');
|
||||||
|
const currency = require('../../currency');
|
||||||
|
|
||||||
export default class SendCreate extends Component {
|
export default class SendCreate extends Component {
|
||||||
static navigationOptions = () => ({
|
static navigationOptions = () => ({
|
||||||
@ -19,26 +33,53 @@ export default class SendCreate extends Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
amount: props.navigation.getParam('amount'),
|
|
||||||
fee: props.navigation.getParam('fee'),
|
fee: props.navigation.getParam('fee'),
|
||||||
address: props.navigation.getParam('address'),
|
recipients: props.navigation.getParam('recipients'),
|
||||||
memo: props.navigation.getParam('memo'),
|
memo: props.navigation.getParam('memo') || '',
|
||||||
size: Math.round(props.navigation.getParam('tx').length / 2),
|
size: Math.round(props.navigation.getParam('tx').length / 2),
|
||||||
tx: props.navigation.getParam('tx'),
|
tx: props.navigation.getParam('tx'),
|
||||||
satoshiPerByte: props.navigation.getParam('satoshiPerByte'),
|
satoshiPerByte: props.navigation.getParam('satoshiPerByte'),
|
||||||
|
wallet: props.navigation.getParam('wallet'),
|
||||||
|
feeSatoshi: props.navigation.getParam('feeSatoshi'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
Privacy.enableBlur();
|
Privacy.enableBlur();
|
||||||
console.log('send/create - componentDidMount');
|
console.log('send/create - componentDidMount');
|
||||||
console.log('address = ', this.state.address);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
Privacy.disableBlur();
|
Privacy.disableBlur();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_renderItem = ({ index, item }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.transactionDetailsTitle}>{loc.send.create.to}</Text>
|
||||||
|
<Text style={styles.transactionDetailsSubtitle}>{item.address}</Text>
|
||||||
|
<Text style={styles.transactionDetailsTitle}>{loc.send.create.amount}</Text>
|
||||||
|
<Text style={styles.transactionDetailsSubtitle}>
|
||||||
|
{item.amount === BitcoinUnit.MAX
|
||||||
|
? currency.satoshiToBTC(this.state.wallet.getBalance()) - this.state.fee
|
||||||
|
: currency.satoshiToBTC(item.value)}{' '}
|
||||||
|
{BitcoinUnit.BTC}
|
||||||
|
</Text>
|
||||||
|
{this.state.recipients.length > 1 && (
|
||||||
|
<BlueText style={{ alignSelf: 'flex-end' }}>
|
||||||
|
{index + 1} of {this.state.recipients.length}
|
||||||
|
</BlueText>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderSeparator = () => {
|
||||||
|
return <View style={{ backgroundColor: BlueApp.settings.inputBorderColor, height: 0.5, marginVertical: 16 }} />;
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<SafeBlueArea style={{ flex: 1, paddingTop: 19 }}>
|
<SafeBlueArea style={{ flex: 1, paddingTop: 19 }}>
|
||||||
@ -73,23 +114,30 @@ export default class SendCreate extends Component {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</BlueCard>
|
</BlueCard>
|
||||||
<BlueCard>
|
<BlueCard>
|
||||||
<Text style={styles.transactionDetailsTitle}>{loc.send.create.to}</Text>
|
<FlatList
|
||||||
<Text style={styles.transactionDetailsSubtitle}>{this.state.address}</Text>
|
scrollEnabled={this.state.recipients.length > 1}
|
||||||
|
extraData={this.state.recipients}
|
||||||
<Text style={styles.transactionDetailsTitle}>{loc.send.create.amount}</Text>
|
data={this.state.recipients}
|
||||||
<Text style={styles.transactionDetailsSubtitle}>{this.state.amount} BTC</Text>
|
renderItem={this._renderItem}
|
||||||
|
keyExtractor={(_item, index) => `${index}`}
|
||||||
|
ItemSeparatorComponent={this.renderSeparator}
|
||||||
|
/>
|
||||||
<Text style={styles.transactionDetailsTitle}>{loc.send.create.fee}</Text>
|
<Text style={styles.transactionDetailsTitle}>{loc.send.create.fee}</Text>
|
||||||
<Text style={styles.transactionDetailsSubtitle}>{this.state.fee} BTC</Text>
|
<Text style={styles.transactionDetailsSubtitle}>
|
||||||
|
{this.state.fee} {BitcoinUnit.BTC}
|
||||||
|
</Text>
|
||||||
|
|
||||||
<Text style={styles.transactionDetailsTitle}>{loc.send.create.tx_size}</Text>
|
<Text style={styles.transactionDetailsTitle}>{loc.send.create.tx_size}</Text>
|
||||||
<Text style={styles.transactionDetailsSubtitle}>{this.state.size} bytes</Text>
|
<Text style={styles.transactionDetailsSubtitle}>{this.state.size} bytes</Text>
|
||||||
|
|
||||||
<Text style={styles.transactionDetailsTitle}>{loc.send.create.satoshi_per_byte}</Text>
|
<Text style={styles.transactionDetailsTitle}>{loc.send.create.satoshi_per_byte}</Text>
|
||||||
<Text style={styles.transactionDetailsSubtitle}>{this.state.satoshiPerByte} Sat/B</Text>
|
<Text style={styles.transactionDetailsSubtitle}>{this.state.satoshiPerByte} Sat/B</Text>
|
||||||
|
{this.state.memo.length > 0 && (
|
||||||
<Text style={styles.transactionDetailsTitle}>{loc.send.create.memo}</Text>
|
<>
|
||||||
<Text style={styles.transactionDetailsSubtitle}>{this.state.memo}</Text>
|
<Text style={styles.transactionDetailsTitle}>{loc.send.create.memo}</Text>
|
||||||
|
<Text style={styles.transactionDetailsSubtitle}>{this.state.memo}</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</BlueCard>
|
</BlueCard>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
|
@ -11,19 +11,23 @@ import {
|
|||||||
Keyboard,
|
Keyboard,
|
||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
|
Dimensions,
|
||||||
Platform,
|
Platform,
|
||||||
|
ScrollView,
|
||||||
Text,
|
Text,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Icon } from 'react-native-elements';
|
import { Icon } from 'react-native-elements';
|
||||||
import AsyncStorage from '@react-native-community/async-storage';
|
import AsyncStorage from '@react-native-community/async-storage';
|
||||||
import {
|
import {
|
||||||
BlueNavigationStyle,
|
BlueCreateTxNavigationStyle,
|
||||||
BlueButton,
|
BlueButton,
|
||||||
BlueBitcoinAmount,
|
BlueBitcoinAmount,
|
||||||
BlueAddressInput,
|
BlueAddressInput,
|
||||||
BlueDismissKeyboardInputAccessory,
|
BlueDismissKeyboardInputAccessory,
|
||||||
BlueLoading,
|
BlueLoading,
|
||||||
BlueUseAllFundsButton,
|
BlueUseAllFundsButton,
|
||||||
|
BlueListItem,
|
||||||
|
BlueText,
|
||||||
} from '../../BlueComponents';
|
} from '../../BlueComponents';
|
||||||
import Slider from '@react-native-community/slider';
|
import Slider from '@react-native-community/slider';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@ -33,8 +37,10 @@ import BitcoinBIP70TransactionDecode from '../../bip70/bip70';
|
|||||||
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
||||||
import { HDLegacyP2PKHWallet, HDSegwitBech32Wallet, HDSegwitP2SHWallet, LightningCustodianWallet } from '../../class';
|
import { HDLegacyP2PKHWallet, HDSegwitBech32Wallet, HDSegwitP2SHWallet, LightningCustodianWallet } from '../../class';
|
||||||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||||
|
import { BitcoinTransaction } from '../../models/bitcoinTransactionInfo';
|
||||||
const bip21 = require('bip21');
|
const bip21 = require('bip21');
|
||||||
let BigNumber = require('bignumber.js');
|
let BigNumber = require('bignumber.js');
|
||||||
|
const { width } = Dimensions.get('window');
|
||||||
/** @type {AppStorage} */
|
/** @type {AppStorage} */
|
||||||
let BlueApp = require('../../BlueApp');
|
let BlueApp = require('../../BlueApp');
|
||||||
let loc = require('../../loc');
|
let loc = require('../../loc');
|
||||||
@ -44,16 +50,17 @@ const btcAddressRx = /^[a-zA-Z0-9]{26,35}$/;
|
|||||||
|
|
||||||
export default class SendDetails extends Component {
|
export default class SendDetails extends Component {
|
||||||
static navigationOptions = ({ navigation }) => ({
|
static navigationOptions = ({ navigation }) => ({
|
||||||
...BlueNavigationStyle(navigation, true),
|
...BlueCreateTxNavigationStyle(
|
||||||
|
navigation,
|
||||||
|
navigation.state.params.withAdvancedOptionsMenuButton,
|
||||||
|
navigation.state.params.advancedOptionsMenuButtonAction,
|
||||||
|
),
|
||||||
title: loc.send.header,
|
title: loc.send.header,
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
let address;
|
|
||||||
let memo;
|
|
||||||
if (props.navigation.state.params) address = props.navigation.state.params.address;
|
|
||||||
if (props.navigation.state.params) memo = props.navigation.state.params.memo;
|
|
||||||
let fromAddress;
|
let fromAddress;
|
||||||
if (props.navigation.state.params) fromAddress = props.navigation.state.params.fromAddress;
|
if (props.navigation.state.params) fromAddress = props.navigation.state.params.fromAddress;
|
||||||
let fromSecret;
|
let fromSecret;
|
||||||
@ -76,13 +83,15 @@ export default class SendDetails extends Component {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
showSendMax: false,
|
showSendMax: false,
|
||||||
isFeeSelectionModalVisible: false,
|
isFeeSelectionModalVisible: false,
|
||||||
|
isAdvancedTransactionOptionsVisible: false,
|
||||||
|
recipientsScrollIndex: 0,
|
||||||
fromAddress,
|
fromAddress,
|
||||||
fromWallet,
|
fromWallet,
|
||||||
fromSecret,
|
fromSecret,
|
||||||
address,
|
addresses: [],
|
||||||
memo,
|
memo: '',
|
||||||
fee: 1,
|
|
||||||
networkTransactionFees: new NetworkTransactionFee(1, 1, 1),
|
networkTransactionFees: new NetworkTransactionFee(1, 1, 1),
|
||||||
|
fee: 1,
|
||||||
feeSliderValue: 1,
|
feeSliderValue: 1,
|
||||||
bip70TransactionExpiration: null,
|
bip70TransactionExpiration: null,
|
||||||
renderWalletSelectionButtonHidden: false,
|
renderWalletSelectionButtonHidden: false,
|
||||||
@ -90,65 +99,111 @@ export default class SendDetails extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderNavigationHeader() {
|
||||||
|
this.props.navigation.setParams({
|
||||||
|
withAdvancedOptionsMenuButton: this.state.fromWallet.allowBatchSend(),
|
||||||
|
advancedOptionsMenuButtonAction: () => {
|
||||||
|
Keyboard.dismiss();
|
||||||
|
this.setState({ isAdvancedTransactionOptionsVisible: true });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: refactor this mess, get rid of regexp, use https://github.com/bitcoinjs/bitcoinjs-lib/issues/890 etc etc
|
* TODO: refactor this mess, get rid of regexp, use https://github.com/bitcoinjs/bitcoinjs-lib/issues/890 etc etc
|
||||||
*
|
*
|
||||||
* @param data {String} Can be address or `bitcoin:xxxxxxx` uri scheme, or invalid garbage
|
* @param data {String} Can be address or `bitcoin:xxxxxxx` uri scheme, or invalid garbage
|
||||||
*/
|
*/
|
||||||
processAddressData = data => {
|
processAddressData = data => {
|
||||||
this.setState(
|
this.setState({ isLoading: true }, async () => {
|
||||||
{ isLoading: true },
|
if (BitcoinBIP70TransactionDecode.matchesPaymentURL(data)) {
|
||||||
() => {
|
const bip70 = await this.processBIP70Invoice(data);
|
||||||
if (BitcoinBIP70TransactionDecode.matchesPaymentURL(data)) {
|
this.setState({
|
||||||
this.processBIP70Invoice(data);
|
addresses: [bip70.recipient],
|
||||||
|
memo: bip70.memo,
|
||||||
|
feeSliderValue: bip70.feeSliderValue,
|
||||||
|
fee: bip70.fee,
|
||||||
|
bip70TransactionExpiration: bip70.bip70TransactionExpiration,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let recipients = this.state.addresses;
|
||||||
|
const dataWithoutSchema = data.replace('bitcoin:', '');
|
||||||
|
if (btcAddressRx.test(dataWithoutSchema) || (dataWithoutSchema.indexOf('bc1') === 0 && dataWithoutSchema.indexOf('?') === -1)) {
|
||||||
|
recipients[[this.state.recipientsScrollIndex]].address = dataWithoutSchema;
|
||||||
|
this.setState({
|
||||||
|
address: recipients,
|
||||||
|
bip70TransactionExpiration: null,
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const dataWithoutSchema = data.replace('bitcoin:', '');
|
let address = '';
|
||||||
if (btcAddressRx.test(dataWithoutSchema) || (dataWithoutSchema.indexOf('bc1') === 0 && dataWithoutSchema.indexOf('?') === -1)) {
|
let options;
|
||||||
|
try {
|
||||||
|
if (!data.toLowerCase().startsWith('bitcoin:')) {
|
||||||
|
data = `bitcoin:${data}`;
|
||||||
|
}
|
||||||
|
const decoded = bip21.decode(data);
|
||||||
|
address = decoded.address;
|
||||||
|
options = decoded.options;
|
||||||
|
} catch (error) {
|
||||||
|
data = data.replace(/(amount)=([^&]+)/g, '').replace(/(amount)=([^&]+)&/g, '');
|
||||||
|
const decoded = bip21.decode(data);
|
||||||
|
decoded.options.amount = 0;
|
||||||
|
address = decoded.address;
|
||||||
|
options = decoded.options;
|
||||||
|
this.setState({ isLoading: false });
|
||||||
|
}
|
||||||
|
console.log(options);
|
||||||
|
if (btcAddressRx.test(address) || address.indexOf('bc1') === 0) {
|
||||||
|
recipients[[this.state.recipientsScrollIndex]].address = address;
|
||||||
|
recipients[[this.state.recipientsScrollIndex]].amount = options.amount;
|
||||||
this.setState({
|
this.setState({
|
||||||
address: dataWithoutSchema,
|
addresses: recipients,
|
||||||
|
memo: options.label || options.message,
|
||||||
bip70TransactionExpiration: null,
|
bip70TransactionExpiration: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
let address = '';
|
|
||||||
let options;
|
|
||||||
try {
|
|
||||||
if (!data.toLowerCase().startsWith('bitcoin:')) {
|
|
||||||
data = `bitcoin:${data}`;
|
|
||||||
}
|
|
||||||
const decoded = bip21.decode(data);
|
|
||||||
address = decoded.address;
|
|
||||||
options = decoded.options;
|
|
||||||
} catch (error) {
|
|
||||||
data = data.replace(/(amount)=([^&]+)/g, '').replace(/(amount)=([^&]+)&/g, '');
|
|
||||||
const decoded = bip21.decode(data);
|
|
||||||
decoded.options.amount = 0;
|
|
||||||
address = decoded.address;
|
|
||||||
options = decoded.options;
|
|
||||||
this.setState({ isLoading: false });
|
|
||||||
}
|
|
||||||
console.log(options);
|
|
||||||
if (btcAddressRx.test(address) || address.indexOf('bc1') === 0) {
|
|
||||||
this.setState({
|
|
||||||
address,
|
|
||||||
amount: options.amount,
|
|
||||||
memo: options.label || options.message,
|
|
||||||
bip70TransactionExpiration: null,
|
|
||||||
isLoading: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
true,
|
});
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
|
this.renderNavigationHeader();
|
||||||
console.log('send/details - componentDidMount');
|
console.log('send/details - componentDidMount');
|
||||||
StatusBar.setBarStyle('dark-content');
|
StatusBar.setBarStyle('dark-content');
|
||||||
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow);
|
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow);
|
||||||
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
|
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
|
||||||
|
|
||||||
|
let addresses = [];
|
||||||
|
let initialMemo = '';
|
||||||
|
if (this.props.navigation.state.params.uri) {
|
||||||
|
const uri = this.props.navigation.state.params.uri;
|
||||||
|
if (BitcoinBIP70TransactionDecode.matchesPaymentURL(uri)) {
|
||||||
|
const { recipient, memo, fee, feeSliderValue } = await this.processBIP70Invoice(uri);
|
||||||
|
addresses.push(recipient);
|
||||||
|
initialMemo = memo;
|
||||||
|
this.setState({ addresses, memo: initialMemo, fee, feeSliderValue, isLoading: false });
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const { address, amount, memo } = this.decodeBitcoinUri(uri);
|
||||||
|
addresses.push(new BitcoinTransaction(address, amount));
|
||||||
|
initialMemo = memo;
|
||||||
|
this.setState({ addresses, memo: initialMemo, isLoading: false });
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
alert('Error: Unable to decode Bitcoin address');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.props.navigation.state.params.address) {
|
||||||
|
addresses.push(new BitcoinTransaction(this.props.navigation.state.params.address));
|
||||||
|
if (this.props.navigation.state.params.memo) initialMemo = this.props.navigation.state.params.memo;
|
||||||
|
this.setState({ addresses, memo: initialMemo, isLoading: false });
|
||||||
|
} else {
|
||||||
|
this.setState({ addresses: [new BitcoinTransaction()], isLoading: false });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cachedNetworkTransactionFees = JSON.parse(await AsyncStorage.getItem(NetworkTransactionFee.StorageKey));
|
const cachedNetworkTransactionFees = JSON.parse(await AsyncStorage.getItem(NetworkTransactionFee.StorageKey));
|
||||||
|
|
||||||
@ -161,34 +216,17 @@ export default class SendDetails extends Component {
|
|||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
let recommendedFees = await NetworkTransactionFees.recommendedFees();
|
try {
|
||||||
if (recommendedFees && recommendedFees.hasOwnProperty('halfHourFee')) {
|
let recommendedFees = await NetworkTransactionFees.recommendedFees();
|
||||||
await AsyncStorage.setItem(NetworkTransactionFee.StorageKey, JSON.stringify(recommendedFees));
|
if (recommendedFees && recommendedFees.hasOwnProperty('halfHourFee')) {
|
||||||
this.setState({
|
await AsyncStorage.setItem(NetworkTransactionFee.StorageKey, JSON.stringify(recommendedFees));
|
||||||
fee: recommendedFees.halfHourFee,
|
this.setState({
|
||||||
networkTransactionFees: recommendedFees,
|
fee: recommendedFees.halfHourFee,
|
||||||
feeSliderValue: recommendedFees.halfHourFee,
|
networkTransactionFees: recommendedFees,
|
||||||
});
|
feeSliderValue: recommendedFees.halfHourFee,
|
||||||
|
});
|
||||||
if (this.props.navigation.state.params.uri) {
|
|
||||||
if (BitcoinBIP70TransactionDecode.matchesPaymentURL(this.props.navigation.state.params.uri)) {
|
|
||||||
this.processBIP70Invoice(this.props.navigation.state.params.uri);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const { address, amount, memo } = this.decodeBitcoinUri(this.props.navigation.getParam('uri'));
|
|
||||||
this.setState({ address, amount, memo, isLoading: false });
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
this.setState({ isLoading: false });
|
|
||||||
alert('Error: Unable to decode Bitcoin address');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.setState({ isLoading: false });
|
|
||||||
}
|
}
|
||||||
} else {
|
} catch (_e) {}
|
||||||
this.setState({ isLoading: false });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@ -268,46 +306,33 @@ export default class SendDetails extends Component {
|
|||||||
return new BigNumber(totalInput - totalOutput).dividedBy(100000000).toNumber();
|
return new BigNumber(totalInput - totalOutput).dividedBy(100000000).toNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
processBIP70Invoice(text) {
|
async processBIP70Invoice(text) {
|
||||||
try {
|
try {
|
||||||
if (BitcoinBIP70TransactionDecode.matchesPaymentURL(text)) {
|
if (BitcoinBIP70TransactionDecode.matchesPaymentURL(text)) {
|
||||||
this.setState(
|
Keyboard.dismiss();
|
||||||
{
|
return BitcoinBIP70TransactionDecode.decode(text)
|
||||||
isLoading: true,
|
.then(response => {
|
||||||
},
|
const recipient = new BitcoinTransaction(
|
||||||
() => {
|
response.address,
|
||||||
Keyboard.dismiss();
|
loc.formatBalanceWithoutSuffix(response.amount, BitcoinUnit.BTC, false),
|
||||||
BitcoinBIP70TransactionDecode.decode(text)
|
);
|
||||||
.then(response => {
|
return {
|
||||||
let networkTransactionFees = this.state.networkTransactionFees;
|
recipient,
|
||||||
if (response.fee > networkTransactionFees.fastestFee) {
|
memo: response.memo,
|
||||||
networkTransactionFees.fastestFee = response.fee;
|
fee: response.fee,
|
||||||
} else {
|
feeSliderValue: response.fee,
|
||||||
networkTransactionFees.halfHourFee = response.fee;
|
bip70TransactionExpiration: response.expires,
|
||||||
}
|
};
|
||||||
this.setState({
|
})
|
||||||
address: response.address,
|
.catch(error => {
|
||||||
amount: loc.formatBalanceWithoutSuffix(response.amount, BitcoinUnit.BTC, false),
|
alert(error.errorMessage);
|
||||||
memo: response.memo,
|
throw error;
|
||||||
networkTransactionFees,
|
});
|
||||||
fee: networkTransactionFees.fastestFee,
|
|
||||||
feeSliderValue: networkTransactionFees.fastestFee,
|
|
||||||
bip70TransactionExpiration: response.expires,
|
|
||||||
isLoading: false,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
alert(error.errorMessage);
|
|
||||||
this.setState({ isLoading: false, bip70TransactionExpiration: null });
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setState({ address: text.replace(' ', ''), isLoading: false, bip70TransactionExpiration: null, amount: 0 });
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
throw new Error('BIP70: Unable to process.');
|
||||||
}
|
}
|
||||||
|
|
||||||
async createTransaction() {
|
async createTransaction() {
|
||||||
@ -315,46 +340,58 @@ export default class SendDetails extends Component {
|
|||||||
this.setState({ isLoading: true });
|
this.setState({ isLoading: true });
|
||||||
let error = false;
|
let error = false;
|
||||||
let requestedSatPerByte = this.state.fee.toString().replace(/\D/g, '');
|
let requestedSatPerByte = this.state.fee.toString().replace(/\D/g, '');
|
||||||
|
for (const [index, transaction] of this.state.addresses.entries()) {
|
||||||
if (!this.state.amount || this.state.amount === '0' || parseFloat(this.state.amount) === 0) {
|
if (!transaction.amount || transaction.amount < 0 || parseFloat(transaction.amount) === 0) {
|
||||||
error = loc.send.details.amount_field_is_not_valid;
|
error = loc.send.details.amount_field_is_not_valid;
|
||||||
console.log('validation error');
|
|
||||||
} else if (!this.state.fee || !requestedSatPerByte || parseFloat(requestedSatPerByte) < 1) {
|
|
||||||
error = loc.send.details.fee_field_is_not_valid;
|
|
||||||
console.log('validation error');
|
|
||||||
} else if (!this.state.address) {
|
|
||||||
error = loc.send.details.address_field_is_not_valid;
|
|
||||||
console.log('validation error');
|
|
||||||
} else if (this.recalculateAvailableBalance(this.state.fromWallet.getBalance(), this.state.amount, 0) < 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');
|
|
||||||
} else if (BitcoinBIP70TransactionDecode.isExpired(this.state.bip70TransactionExpiration)) {
|
|
||||||
error = 'Transaction has expired.';
|
|
||||||
console.log('validation error');
|
|
||||||
} else if (this.state.address) {
|
|
||||||
const address = this.state.address.trim().toLowerCase();
|
|
||||||
if (address.startsWith('lnb') || address.startsWith('lightning:lnb')) {
|
|
||||||
error =
|
|
||||||
'This address appears to be for a Lightning invoice. Please, go to your Lightning wallet in order to make a payment for this invoice.';
|
|
||||||
console.log('validation error');
|
console.log('validation error');
|
||||||
}
|
} else if (!this.state.fee || !requestedSatPerByte || parseFloat(requestedSatPerByte) < 1) {
|
||||||
}
|
error = loc.send.details.fee_field_is_not_valid;
|
||||||
|
|
||||||
if (!error) {
|
|
||||||
try {
|
|
||||||
bitcoin.address.toOutputScript(this.state.address);
|
|
||||||
} catch (err) {
|
|
||||||
console.log('validation error');
|
console.log('validation error');
|
||||||
console.log(err);
|
} else if (!transaction.address) {
|
||||||
error = loc.send.details.address_field_is_not_valid;
|
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) {
|
||||||
|
// first sanity check is that sending amount is not bigger than available balance
|
||||||
|
error = loc.send.details.total_exceeds_balance;
|
||||||
|
console.log('validation error');
|
||||||
|
} else if (BitcoinBIP70TransactionDecode.isExpired(this.state.bip70TransactionExpiration)) {
|
||||||
|
error = 'Transaction has expired.';
|
||||||
|
console.log('validation error');
|
||||||
|
} else if (transaction.address) {
|
||||||
|
const address = transaction.address.trim().toLowerCase();
|
||||||
|
if (address.startsWith('lnb') || address.startsWith('lightning:lnb')) {
|
||||||
|
error =
|
||||||
|
'This address appears to be for a Lightning invoice. Please, go to your Lightning wallet in order to make a payment for this invoice.';
|
||||||
|
console.log('validation error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
try {
|
||||||
|
bitcoin.address.toOutputScript(transaction.address);
|
||||||
|
} catch (err) {
|
||||||
|
console.log('validation error');
|
||||||
|
console.log(err);
|
||||||
|
error = loc.send.details.address_field_is_not_valid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
if (index === 0) {
|
||||||
|
this.scrollView.scrollTo();
|
||||||
|
} else if (index === this.state.addresses.length - 1) {
|
||||||
|
this.scrollView.scrollToEnd();
|
||||||
|
} else {
|
||||||
|
const page = Math.round(width * (this.state.addresses.length - 2));
|
||||||
|
this.scrollView.scrollTo({ x: page, y: 0, animated: true });
|
||||||
|
}
|
||||||
|
this.setState({ isLoading: false, recipientsScrollIndex: index });
|
||||||
|
alert(error);
|
||||||
|
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
this.setState({ isLoading: false });
|
|
||||||
alert(error);
|
|
||||||
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -378,7 +415,7 @@ export default class SendDetails extends Component {
|
|||||||
let tx, txid;
|
let tx, txid;
|
||||||
let tries = 1;
|
let tries = 1;
|
||||||
let fee = 0.000001; // initial fee guess
|
let fee = 0.000001; // initial fee guess
|
||||||
|
const firstTransaction = this.state.addresses[0];
|
||||||
try {
|
try {
|
||||||
await this.state.fromWallet.fetchUtxo();
|
await this.state.fromWallet.fetchUtxo();
|
||||||
if (this.state.fromWallet.getChangeAddressAsync) {
|
if (this.state.fromWallet.getChangeAddressAsync) {
|
||||||
@ -392,13 +429,13 @@ export default class SendDetails extends Component {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
console.log('try #', tries, 'fee=', fee);
|
console.log('try #', tries, 'fee=', fee);
|
||||||
if (this.recalculateAvailableBalance(this.state.fromWallet.getBalance(), this.state.amount, fee) < 0) {
|
if (this.recalculateAvailableBalance(this.state.fromWallet.getBalance(), firstTransaction.amount, fee) < 0) {
|
||||||
// we could not add any fee. user is trying to send all he's got. that wont work
|
// we could not add any fee. user is trying to send all he's got. that wont work
|
||||||
throw new Error(loc.send.details.total_exceeds_balance);
|
throw new Error(loc.send.details.total_exceeds_balance);
|
||||||
}
|
}
|
||||||
|
|
||||||
let startTime = Date.now();
|
let startTime = Date.now();
|
||||||
tx = this.state.fromWallet.createTx(utxo, this.state.amount, fee, this.state.address, this.state.memo);
|
tx = this.state.fromWallet.createTx(utxo, firstTransaction.amount, fee, firstTransaction.address, this.state.memo);
|
||||||
let endTime = Date.now();
|
let endTime = Date.now();
|
||||||
console.log('create tx ', (endTime - startTime) / 1000, 'sec');
|
console.log('create tx ', (endTime - startTime) / 1000, 'sec');
|
||||||
|
|
||||||
@ -440,14 +477,13 @@ export default class SendDetails extends Component {
|
|||||||
|
|
||||||
this.setState({ isLoading: false }, () =>
|
this.setState({ isLoading: false }, () =>
|
||||||
this.props.navigation.navigate('Confirm', {
|
this.props.navigation.navigate('Confirm', {
|
||||||
amount: this.state.amount,
|
recipients: [firstTransaction],
|
||||||
// HD wallet's utxo is in sats, classic segwit wallet utxos are in btc
|
// HD wallet's utxo is in sats, classic segwit wallet utxos are in btc
|
||||||
fee: this.calculateFee(
|
fee: this.calculateFee(
|
||||||
utxo,
|
utxo,
|
||||||
tx,
|
tx,
|
||||||
this.state.fromWallet.type === HDSegwitP2SHWallet.type || this.state.fromWallet.type === HDLegacyP2PKHWallet.type,
|
this.state.fromWallet.type === HDSegwitP2SHWallet.type || this.state.fromWallet.type === HDLegacyP2PKHWallet.type,
|
||||||
),
|
),
|
||||||
address: this.state.address,
|
|
||||||
memo: this.state.memo,
|
memo: this.state.memo,
|
||||||
fromWallet: this.state.fromWallet,
|
fromWallet: this.state.fromWallet,
|
||||||
tx: tx,
|
tx: tx,
|
||||||
@ -461,15 +497,23 @@ export default class SendDetails extends Component {
|
|||||||
/** @type {HDSegwitBech32Wallet} */
|
/** @type {HDSegwitBech32Wallet} */
|
||||||
const wallet = this.state.fromWallet;
|
const wallet = this.state.fromWallet;
|
||||||
await wallet.fetchUtxo();
|
await wallet.fetchUtxo();
|
||||||
|
const firstTransaction = this.state.addresses[0];
|
||||||
const changeAddress = await wallet.getChangeAddressAsync();
|
const changeAddress = await wallet.getChangeAddressAsync();
|
||||||
let satoshis = new BigNumber(this.state.amount).multipliedBy(100000000).toNumber();
|
let satoshis = new BigNumber(firstTransaction.amount).multipliedBy(100000000).toNumber();
|
||||||
const requestedSatPerByte = +this.state.fee.toString().replace(/\D/g, '');
|
const requestedSatPerByte = +this.state.fee.toString().replace(/\D/g, '');
|
||||||
console.log({ satoshis, requestedSatPerByte, utxo: wallet.getUtxo() });
|
console.log({ satoshis, requestedSatPerByte, utxo: wallet.getUtxo() });
|
||||||
|
|
||||||
let targets = [];
|
let targets = [];
|
||||||
targets.push({ address: this.state.address, value: satoshis });
|
for (const transaction of this.state.addresses) {
|
||||||
if (this.state.amount === BitcoinUnit.MAX) {
|
const amount =
|
||||||
targets = [{ address: this.state.address }];
|
transaction.amount === BitcoinUnit.MAX ? BitcoinUnit.MAX : new BigNumber(transaction.amount).multipliedBy(100000000).toNumber();
|
||||||
|
if (amount > 0.0 || amount === BitcoinUnit.MAX) {
|
||||||
|
targets.push({ address: transaction.address, value: amount });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstTransaction.amount === BitcoinUnit.MAX) {
|
||||||
|
targets = [{ address: firstTransaction.address, amount: BitcoinUnit.MAX }];
|
||||||
}
|
}
|
||||||
|
|
||||||
let { tx, fee } = wallet.createTransaction(wallet.getUtxo(), targets, requestedSatPerByte, changeAddress);
|
let { tx, fee } = wallet.createTransaction(wallet.getUtxo(), targets, requestedSatPerByte, changeAddress);
|
||||||
@ -480,24 +524,72 @@ export default class SendDetails extends Component {
|
|||||||
memo: this.state.memo,
|
memo: this.state.memo,
|
||||||
};
|
};
|
||||||
await BlueApp.saveToDisk();
|
await BlueApp.saveToDisk();
|
||||||
|
|
||||||
this.setState({ isLoading: false }, () =>
|
this.setState({ isLoading: false }, () =>
|
||||||
this.props.navigation.navigate('Confirm', {
|
this.props.navigation.navigate('Confirm', {
|
||||||
amount: this.state.amount,
|
|
||||||
fee: new BigNumber(fee).dividedBy(100000000).toNumber(),
|
fee: new BigNumber(fee).dividedBy(100000000).toNumber(),
|
||||||
address: this.state.address,
|
|
||||||
memo: this.state.memo,
|
memo: this.state.memo,
|
||||||
fromWallet: wallet,
|
fromWallet: wallet,
|
||||||
tx: tx.toHex(),
|
tx: tx.toHex(),
|
||||||
|
recipients: targets,
|
||||||
satoshiPerByte: requestedSatPerByte,
|
satoshiPerByte: requestedSatPerByte,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onWalletSelect = wallet => {
|
onWalletSelect = wallet => {
|
||||||
this.setState({ fromAddress: wallet.getAddress(), fromSecret: wallet.getSecret(), fromWallet: wallet }, () => {
|
const changeWallet = () => {
|
||||||
this.props.navigation.pop();
|
this.setState({ fromAddress: wallet.getAddress(), fromSecret: wallet.getSecret(), fromWallet: wallet }, () => {
|
||||||
});
|
this.renderNavigationHeader();
|
||||||
|
this.props.navigation.pop();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (this.state.addresses.length > 1 && !wallet.allowBatchSend()) {
|
||||||
|
ReactNativeHapticFeedback.trigger('notificationWarning');
|
||||||
|
Alert.alert(
|
||||||
|
'Wallet Selection',
|
||||||
|
`The selected wallet does not support sending Bitcoin to multiple recipients. Are you sure to want to select this wallet?`,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: loc._.ok,
|
||||||
|
onPress: async () => {
|
||||||
|
const firstTransaction =
|
||||||
|
this.state.addresses.find(element => {
|
||||||
|
const feeSatoshi = new BigNumber(element.amount).multipliedBy(100000000);
|
||||||
|
return element.address.length > 0 && feeSatoshi > 0;
|
||||||
|
}) || this.state.addresses[0];
|
||||||
|
this.setState({ addresses: [firstTransaction], recipientsScrollIndex: 0 }, () => changeWallet());
|
||||||
|
},
|
||||||
|
style: 'default',
|
||||||
|
},
|
||||||
|
{ text: loc.send.details.cancel, onPress: () => {}, style: 'cancel' },
|
||||||
|
],
|
||||||
|
{ cancelable: false },
|
||||||
|
);
|
||||||
|
} else if (this.state.addresses.some(element => element.amount === BitcoinUnit.MAX) && !wallet.allowSendMax()) {
|
||||||
|
ReactNativeHapticFeedback.trigger('notificationWarning');
|
||||||
|
Alert.alert(
|
||||||
|
'Wallet Selection',
|
||||||
|
`The selected wallet does not support automatic maximum balance calculation. Are you sure to want to select this wallet?`,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: loc._.ok,
|
||||||
|
onPress: async () => {
|
||||||
|
const firstTransaction =
|
||||||
|
this.state.addresses.find(element => {
|
||||||
|
return element.amount === BitcoinUnit.MAX;
|
||||||
|
}) || this.state.addresses[0];
|
||||||
|
firstTransaction.amount = 0;
|
||||||
|
this.setState({ addresses: [firstTransaction], recipientsScrollIndex: 0 }, () => changeWallet());
|
||||||
|
},
|
||||||
|
style: 'default',
|
||||||
|
},
|
||||||
|
{ text: loc.send.details.cancel, onPress: () => {}, style: 'cancel' },
|
||||||
|
],
|
||||||
|
{ cancelable: false },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
changeWallet();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
renderFeeSelectionModal = () => {
|
renderFeeSelectionModal = () => {
|
||||||
@ -575,14 +667,71 @@ export default class SendDetails extends Component {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
renderAdvancedTransactionOptionsModal = () => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isVisible={this.state.isAdvancedTransactionOptionsVisible}
|
||||||
|
style={styles.bottomModal}
|
||||||
|
onBackdropPress={() => {
|
||||||
|
Keyboard.dismiss();
|
||||||
|
this.setState({ isAdvancedTransactionOptionsVisible: false });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'position' : null}>
|
||||||
|
<View style={styles.advancedTransactionOptionsModalContent}>
|
||||||
|
<TouchableOpacity
|
||||||
|
disabled={this.state.addresses.some(element => element.amount === BitcoinUnit.MAX)}
|
||||||
|
onPress={() => {
|
||||||
|
const addresses = this.state.addresses;
|
||||||
|
addresses.push(new BitcoinTransaction());
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
addresses,
|
||||||
|
isAdvancedTransactionOptionsVisible: false,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.scrollView.scrollToEnd();
|
||||||
|
if (this.state.addresses.length > 1) this.scrollView.flashScrollIndicators();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BlueListItem
|
||||||
|
disabled={this.state.addresses.some(element => element.amount === BitcoinUnit.MAX)}
|
||||||
|
title="Add Recipient"
|
||||||
|
hideChevron
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
disabled={this.state.addresses.length < 2}
|
||||||
|
onPress={() => {
|
||||||
|
const addresses = this.state.addresses;
|
||||||
|
addresses.splice(this.state.recipientsScrollIndex, 1);
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
addresses,
|
||||||
|
isAdvancedTransactionOptionsVisible: false,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (this.state.addresses.length > 1) this.scrollView.flashScrollIndicators();
|
||||||
|
this.setState({ recipientsScrollIndex: this.scrollViewCurrentIndex });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BlueListItem disabled={this.state.addresses.length < 2} title="Remove Recipient" hideChevron />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
renderCreateButton = () => {
|
renderCreateButton = () => {
|
||||||
return (
|
return (
|
||||||
<View style={{ marginHorizontal: 56, marginVertical: 16, alignContent: 'center', backgroundColor: '#FFFFFF', minHeight: 44 }}>
|
<View style={{ marginHorizontal: 56, marginVertical: 16, alignContent: 'center', backgroundColor: '#FFFFFF', minHeight: 44 }}>
|
||||||
{this.state.isLoading ? (
|
{this.state.isLoading ? <ActivityIndicator /> : <BlueButton onPress={() => this.createTransaction()} title={'Next'} />}
|
||||||
<ActivityIndicator />
|
|
||||||
) : (
|
|
||||||
<BlueButton onPress={() => this.createTransaction()} title={loc.send.details.create} />
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -610,18 +759,89 @@ export default class SendDetails extends Component {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Text style={{ color: '#0c2550', fontSize: 14 }}>{this.state.fromWallet.getLabel()}</Text>
|
<Text style={{ color: '#0c2550', fontSize: 14 }}>{this.state.fromWallet.getLabel()}</Text>
|
||||||
<Text style={{ color: '#0c2550', fontSize: 14, fontWeight: '600', marginLeft: 8, marginRight: 4 }}>
|
|
||||||
{loc.formatBalanceWithoutSuffix(this.state.fromWallet.getBalance(), BitcoinUnit.BTC, false)}
|
|
||||||
</Text>
|
|
||||||
<Text style={{ color: '#0c2550', fontSize: 11, fontWeight: '600', textAlignVertical: 'bottom', marginTop: 2 }}>
|
|
||||||
{BitcoinUnit.BTC}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handlePageChange = e => {
|
||||||
|
Keyboard.dismiss();
|
||||||
|
var offset = e.nativeEvent.contentOffset;
|
||||||
|
if (offset) {
|
||||||
|
const page = Math.round(offset.x / width);
|
||||||
|
if (this.state.recipientsScrollIndex !== page) {
|
||||||
|
this.setState({ recipientsScrollIndex: page });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
scrollViewCurrentIndex = () => {
|
||||||
|
Keyboard.dismiss();
|
||||||
|
var offset = this.scrollView.contentOffset;
|
||||||
|
if (offset) {
|
||||||
|
const page = Math.round(offset.x / width);
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
renderBitcoinTransactionInfoFields = () => {
|
||||||
|
let rows = [];
|
||||||
|
for (let [index, item] of this.state.addresses.entries()) {
|
||||||
|
rows.push(
|
||||||
|
<View style={{ minWidth: width, maxWidth: width, width: width }}>
|
||||||
|
<BlueBitcoinAmount
|
||||||
|
isLoading={this.state.isLoading}
|
||||||
|
amount={item.amount ? item.amount.toString() : null}
|
||||||
|
onChangeText={text => {
|
||||||
|
item.amount = text;
|
||||||
|
const transactions = this.state.addresses;
|
||||||
|
transactions[index] = item;
|
||||||
|
this.setState({ addresses: transactions });
|
||||||
|
}}
|
||||||
|
inputAccessoryViewID={this.state.fromWallet.allowSendMax() ? BlueUseAllFundsButton.InputAccessoryViewID : null}
|
||||||
|
onFocus={() => this.setState({ isAmountToolbarVisibleForAndroid: true })}
|
||||||
|
onBlur={() => this.setState({ isAmountToolbarVisibleForAndroid: false })}
|
||||||
|
/>
|
||||||
|
<BlueAddressInput
|
||||||
|
onChangeText={async text => {
|
||||||
|
text = text.trim();
|
||||||
|
let transactions = this.state.addresses;
|
||||||
|
try {
|
||||||
|
const { recipient, memo, fee, feeSliderValue } = await this.processBIP70Invoice(text);
|
||||||
|
transactions[index].address = recipient.address;
|
||||||
|
transactions[index].amount = recipient.amount;
|
||||||
|
this.setState({ addresses: transactions, memo: memo, fee, feeSliderValue, isLoading: false });
|
||||||
|
} catch (_e) {
|
||||||
|
const { address, amount, memo } = this.decodeBitcoinUri(text);
|
||||||
|
item.address = address || text;
|
||||||
|
item.amount = amount || item.amount;
|
||||||
|
transactions[index] = item;
|
||||||
|
this.setState({
|
||||||
|
addresses: transactions,
|
||||||
|
memo: memo || this.state.memo,
|
||||||
|
isLoading: false,
|
||||||
|
bip70TransactionExpiration: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBarScanned={this.processAddressData}
|
||||||
|
address={item.address}
|
||||||
|
isLoading={this.state.isLoading}
|
||||||
|
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
|
||||||
|
/>
|
||||||
|
{this.state.addresses.length > 1 && (
|
||||||
|
<BlueText style={{ alignSelf: 'flex-end', marginRight: 18, marginVertical: 8 }}>
|
||||||
|
{index + 1} of {this.state.addresses.length}
|
||||||
|
</BlueText>
|
||||||
|
)}
|
||||||
|
</View>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.isLoading || typeof this.state.fromWallet === 'undefined') {
|
if (this.state.isLoading || typeof this.state.fromWallet === 'undefined') {
|
||||||
return (
|
return (
|
||||||
@ -633,44 +853,21 @@ export default class SendDetails extends Component {
|
|||||||
return (
|
return (
|
||||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
|
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
|
||||||
<View style={{ flex: 1, justifyContent: 'space-between' }}>
|
<View style={{ flex: 1, justifyContent: 'space-between' }}>
|
||||||
<View style={{ flex: 1, backgroundColor: '#FFFFFF' }}>
|
<View>
|
||||||
<KeyboardAvoidingView behavior="position">
|
<KeyboardAvoidingView behavior="position">
|
||||||
<BlueBitcoinAmount
|
<ScrollView
|
||||||
isLoading={this.state.isLoading}
|
pagingEnabled
|
||||||
amount={this.state.amount ? this.state.amount.toString() : null}
|
horizontal
|
||||||
onChangeText={text => this.setState({ amount: text })}
|
contentContainerStyle={{ flexWrap: 'wrap', flexDirection: 'row' }}
|
||||||
inputAccessoryViewID={this.state.fromWallet.allowSendMax() ? BlueUseAllFundsButton.InputAccessoryViewID : null}
|
ref={ref => (this.scrollView = ref)}
|
||||||
onFocus={() => this.setState({ isAmountToolbarVisibleForAndroid: true })}
|
onContentSizeChange={() => this.scrollView.scrollToEnd()}
|
||||||
onBlur={() => this.setState({ isAmountToolbarVisibleForAndroid: false })}
|
onLayout={() => this.scrollView.scrollToEnd()}
|
||||||
/>
|
onMomentumScrollEnd={this.handlePageChange}
|
||||||
<BlueAddressInput
|
scrollEnabled={this.state.addresses.length > 1}
|
||||||
onChangeText={text => {
|
scrollIndicatorInsets={{ top: 0, left: 8, bottom: 0, right: 8 }}
|
||||||
if (!this.processBIP70Invoice(text)) {
|
>
|
||||||
this.setState({
|
{this.renderBitcoinTransactionInfoFields()}
|
||||||
address: text.trim().replace('bitcoin:', ''),
|
</ScrollView>
|
||||||
isLoading: false,
|
|
||||||
bip70TransactionExpiration: null,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const { address, amount, memo } = this.decodeBitcoinUri(text);
|
|
||||||
this.setState({
|
|
||||||
address: address || this.state.address,
|
|
||||||
amount: amount || this.state.amount,
|
|
||||||
memo: memo || this.state.memo,
|
|
||||||
isLoading: false,
|
|
||||||
bip70TransactionExpiration: null,
|
|
||||||
});
|
|
||||||
} catch (_) {
|
|
||||||
this.setState({ address: text.trim(), isLoading: false, bip70TransactionExpiration: null });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBarScanned={this.processAddressData}
|
|
||||||
address={this.state.address}
|
|
||||||
isLoading={this.state.isLoading}
|
|
||||||
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
|
|
||||||
/>
|
|
||||||
<View
|
<View
|
||||||
hide={!this.state.showMemoRow}
|
hide={!this.state.showMemoRow}
|
||||||
style={{
|
style={{
|
||||||
@ -723,6 +920,7 @@ export default class SendDetails extends Component {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
{this.renderCreateButton()}
|
{this.renderCreateButton()}
|
||||||
{this.renderFeeSelectionModal()}
|
{this.renderFeeSelectionModal()}
|
||||||
|
{this.renderAdvancedTransactionOptionsModal()}
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
</View>
|
</View>
|
||||||
<BlueDismissKeyboardInputAccessory />
|
<BlueDismissKeyboardInputAccessory />
|
||||||
@ -733,13 +931,17 @@ export default class SendDetails extends Component {
|
|||||||
ReactNativeHapticFeedback.trigger('notificationWarning');
|
ReactNativeHapticFeedback.trigger('notificationWarning');
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
'Use full balance',
|
'Use full balance',
|
||||||
`Are you sure you want to use your wallet's full balance for this transaction?`,
|
`Are you sure you want to use your wallet's full balance for this transaction? ${
|
||||||
|
this.state.addresses.length > 1 ? 'Your other recipients will be removed from this transaction.' : ''
|
||||||
|
}`,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
text: loc._.ok,
|
text: loc._.ok,
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
Keyboard.dismiss();
|
Keyboard.dismiss();
|
||||||
this.setState({ amount: BitcoinUnit.MAX });
|
const recipient = this.state.addresses[this.state.recipientsScrollIndex];
|
||||||
|
recipient.amount = BitcoinUnit.MAX;
|
||||||
|
this.setState({ addresses: [recipient], recipientsScrollIndex: 0 });
|
||||||
},
|
},
|
||||||
style: 'default',
|
style: 'default',
|
||||||
},
|
},
|
||||||
@ -795,6 +997,14 @@ const styles = StyleSheet.create({
|
|||||||
minHeight: 200,
|
minHeight: 200,
|
||||||
height: 200,
|
height: 200,
|
||||||
},
|
},
|
||||||
|
advancedTransactionOptionsModalContent: {
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
padding: 22,
|
||||||
|
borderTopLeftRadius: 16,
|
||||||
|
borderTopRightRadius: 16,
|
||||||
|
borderColor: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
minHeight: 130,
|
||||||
|
},
|
||||||
bottomModal: {
|
bottomModal: {
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
margin: 0,
|
margin: 0,
|
||||||
@ -816,6 +1026,7 @@ SendDetails.propTypes = {
|
|||||||
goBack: PropTypes.func,
|
goBack: PropTypes.func,
|
||||||
navigate: PropTypes.func,
|
navigate: PropTypes.func,
|
||||||
getParam: PropTypes.func,
|
getParam: PropTypes.func,
|
||||||
|
setParams: PropTypes.func,
|
||||||
state: PropTypes.shape({
|
state: PropTypes.shape({
|
||||||
params: PropTypes.shape({
|
params: PropTypes.shape({
|
||||||
amount: PropTypes.number,
|
amount: PropTypes.number,
|
||||||
|
@ -10,6 +10,7 @@ let loc = require('../../loc');
|
|||||||
export default class Success extends Component {
|
export default class Success extends Component {
|
||||||
static navigationOptions = {
|
static navigationOptions = {
|
||||||
header: null,
|
header: null,
|
||||||
|
gesturesEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -67,7 +68,7 @@ export default class Success extends Component {
|
|||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{loc.send.create.fee}: {loc.formatBalance(this.state.fee, BitcoinUnit.SATS)}
|
{loc.send.create.fee}: {this.state.fee} {BitcoinUnit.BTC}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{this.state.fee <= 0 && (
|
{this.state.fee <= 0 && (
|
||||||
|
Loading…
Reference in New Issue
Block a user