diff --git a/MainBottomTabs.js b/MainBottomTabs.js index da9df33ef..0c6613321 100644 --- a/MainBottomTabs.js +++ b/MainBottomTabs.js @@ -21,6 +21,8 @@ import receiveDetails from './screen/receive/details'; import sendDetails from './screen/send/details'; import sendScanQrAddress from './screen/send/scanQrAddress'; import sendCreate from './screen/send/create'; +import Confirm from './screen/send/confirm'; +import Success from './screen/send/success'; import ManageFunds from './screen/lnd/manageFunds'; import ScanLndInvoice from './screen/lnd/scanLndInvoice'; @@ -55,10 +57,22 @@ const WalletsStackNavigator = createStackNavigator({ }, }); +const SuccessTransactionStackNavigation = createStackNavigator( + { + Success: { + screen: Success, + }, + }, + { mode: 'modal', headerMode: 'none' }, +); + const CreateTransactionStackNavigator = createStackNavigator({ SendDetails: { screen: sendDetails, }, + Confirm: { + screen: Confirm, + }, CreateTransaction: { screen: sendCreate, navigationOptions: { @@ -109,6 +123,13 @@ const Tabs = createStackNavigator( TransactionDetails: { screen: details, }, + Success: { + screen: SuccessTransactionStackNavigation, + navigationOptions: { + header: null, + gesturesEnabled: false, + }, + }, RBF: { screen: rbf, }, diff --git a/loc/en.js b/loc/en.js index 63be1761f..ae8e12f2e 100644 --- a/loc/en.js +++ b/loc/en.js @@ -89,7 +89,7 @@ module.exports = { conf: 'conf', }, details: { - title: 'transaction details', + title: 'Transaction details', from: 'Input', to: 'Output', copy: 'Copy', @@ -103,6 +103,7 @@ module.exports = { fee_field_is_not_valid: 'Fee field is not valid', address_field_is_not_valid: 'Address field is not valid', total_exceeds_balance: 'The sending amount exceeds the available balance.', + create_tx_error: 'There was an error creating the transaction. Please, make sure the address is valid.', address: 'address', amount_placeholder: 'amount to send (in BTC)', fee_placeholder: 'plus transaction fee (in BTC)', @@ -113,7 +114,15 @@ module.exports = { create: 'Create', remaining_balance: 'Remaining balance', }, + confirm: { + header: 'Confirm', + sendNow: 'Send now', + }, + success: { + done: 'Done', + }, create: { + details: 'Details', title: 'create transaction', error: 'Error creating transaction. Invalid address or send amount?', go_back: 'Go Back', diff --git a/loc/es.js b/loc/es.js index 15f6d9311..b0385c10d 100644 --- a/loc/es.js +++ b/loc/es.js @@ -97,6 +97,13 @@ module.exports = { }, send: { header: 'enviar', + confirm: { + header: 'Confirm', + sendNow: 'Send now', + }, + success: { + done: 'Done', + }, details: { title: 'Crear Transaccion', amount_field_is_not_valid: 'La cantidad no es válida', @@ -106,6 +113,7 @@ module.exports = { amount_placeholder: 'cantidad para enviar (in BTC)', fee_placeholder: 'más tasa de transaccion (in BTC)', note_placeholder: 'comentario (para ti mismo)', + create_tx_error: 'Se ha producido un error al crear la transacción. Por favor, asegúrese de que la dirección es válida.', cancel: 'Cancelar', scan: 'Escaniar', address: 'Direccion', @@ -116,6 +124,7 @@ module.exports = { }, create: { title: 'Crear una Transaccion', + details: 'Detalles', error: 'Error al crear una transacción. ¿Dirección o cantidad estan invalidas?', go_back: 'Regresar', this_is_hex: 'Este es representacion hex de transacción, firmado y listo para ser transmitido a la red. ¿Continuar?', diff --git a/loc/index.js b/loc/index.js index 60e041e37..25f22f411 100644 --- a/loc/index.js +++ b/loc/index.js @@ -79,6 +79,8 @@ strings.formatBalance = (balance, unit) => { return b.multipliedBy(1000000).toString() + ' ' + BitcoinUnit.BITS; } else if (unit === BitcoinUnit.SATOSHIS) { return (b.times(conversion).toString() + ' ' + BitcoinUnit.SATOSHIS).replace(/\./g, ''); + } else if (unit === BitcoinUnit.SATS) { + return (b.times(conversion).toString() + ' ' + BitcoinUnit.SATS).replace(/\./g, ''); } } return balance + ' ' + BitcoinUnit.BTC; diff --git a/loc/pt_BR.js b/loc/pt_BR.js index 9a7bc9bab..5c63fa022 100644 --- a/loc/pt_BR.js +++ b/loc/pt_BR.js @@ -98,6 +98,13 @@ module.exports = { }, send: { header: 'Enviar', + confirm: { + header: 'Confirm', + sendNow: 'Send now', + }, + success: { + done: 'Done', + }, details: { title: 'Criar Transacção', amount_field_is_not_valid: 'Campo de quantia não é válido', @@ -106,6 +113,7 @@ module.exports = { receiver_placeholder: 'endereço de envio aqui', amount_placeholder: 'quantia a enviar (em BTC)', fee_placeholder: 'mais a taxa de transacção (em BTC)', + create_tx_error: 'There was an error creating the transaction. Please, make sure the address is valid.', note_placeholder: 'Nota pessoal', cancel: 'Cancelar', scan: 'Scanear', @@ -117,6 +125,7 @@ module.exports = { }, create: { title: 'Criar Transacção', + details: 'Details', error: 'Erro ao criar transação. Endereço inválido ou quantia?', go_back: 'Voltar', this_is_hex: 'Este é o hex da transação, assinado e pronto para ser difundido para a network. Continuar?', diff --git a/loc/pt_PT.js b/loc/pt_PT.js index a725f317c..ed58d75ca 100644 --- a/loc/pt_PT.js +++ b/loc/pt_PT.js @@ -97,6 +97,13 @@ module.exports = { }, send: { header: 'Enviar', + confirm: { + header: 'Confirm', + sendNow: 'Send now', + }, + success: { + done: 'Done', + }, details: { title: 'Criar Transacção', amount_field_is_not_valid: 'Campo de quantia não é válido', @@ -105,6 +112,7 @@ module.exports = { receiver_placeholder: 'endereço de envio aqui', amount_placeholder: 'quantia a enviar (em BTC)', fee_placeholder: 'mais a taxa de transacção (em BTC)', + create_tx_error: 'There was an error creating the transaction. Please, make sure the address is valid.', note_placeholder: 'Nota pessoal', total_exceeds_balance: 'Total amount exceeds available balance.', cancel: 'Cancelar', @@ -116,6 +124,7 @@ module.exports = { }, create: { title: 'Criar Transacção', + details: 'Details', error: 'Erro ao criar transacção. Endereço inválido ou quantia?', go_back: 'Voltar', this_is_hex: 'Este é o hex da transacção, assinado e pronto para ser difundido para a network. Continuar?', diff --git a/loc/ru.js b/loc/ru.js index f9b36c6aa..89d53fd47 100644 --- a/loc/ru.js +++ b/loc/ru.js @@ -96,6 +96,13 @@ module.exports = { }, send: { header: 'Отправить', + confirm: { + header: 'Confirm', + sendNow: 'Send now', + }, + success: { + done: 'Done', + }, details: { title: 'Создать Транзакцию', amount_field_is_not_valid: 'Поле не валидно', @@ -105,6 +112,7 @@ module.exports = { amount_placeholder: 'сколько отправить (в BTC)', fee_placeholder: 'плюс комиссия за перевод (в BTC)', note_placeholder: 'примечание платежа', + create_tx_error: 'There was an error creating the transaction. Please, make sure the address is valid.', cancel: 'Отмена', scan: 'Скан QR', create: 'Создать', @@ -115,6 +123,7 @@ module.exports = { }, create: { title: 'Создать Транзакцию', + details: 'Details', error: 'Ошибка при создании транзакции. Неправильный адрес назначения или недостаточно средств?', go_back: 'Назад', this_is_hex: 'Это данные транзакции. Транзакция подписана и готова быть транслирована в сеть. Продолжить?', diff --git a/loc/ua.js b/loc/ua.js index 7f57577ae..9e0c41d32 100644 --- a/loc/ua.js +++ b/loc/ua.js @@ -104,6 +104,7 @@ module.exports = { receiver_placeholder: 'Адреса одержувача', amount_placeholder: 'скільки відправити (в BTC)', fee_placeholder: 'плюс комісія за переказ (в BTC)', + create_tx_error: 'There was an error creating the transaction. Please, make sure the address is valid.', note_placeholder: 'примітка платежу', cancel: 'Відміна', scan: 'Скан QR', @@ -113,8 +114,16 @@ module.exports = { create: 'Створити', remaining_balance: 'Залишок балансу', }, + confirm: { + header: 'Confirm', + sendNow: 'Send now', + }, + success: { + done: 'Done', + }, create: { title: 'Створити Транзакцію', + details: 'Details', error: 'Помилка при створенні транзакції. Невiрна адреса призначення або недостатньо коштiв?', go_back: 'Назад', this_is_hex: 'Це дані транзакції. Транзакція підписана і готова бути трансльована в мережу. Продовжити?', diff --git a/models/bitcoinUnits.js b/models/bitcoinUnits.js index 982792a65..2f2ee6b02 100644 --- a/models/bitcoinUnits.js +++ b/models/bitcoinUnits.js @@ -3,4 +3,5 @@ export const BitcoinUnit = Object.freeze({ MBTC: 'mBTC', BITS: 'bits', SATOSHIS: 'satoshis', + SATS: 'sats', }); diff --git a/package-lock.json b/package-lock.json index 34918dfc6..9cd267a61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "BlueWallet", - "version": "3.0.0", + "version": "3.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/screen/send/confirm.js b/screen/send/confirm.js new file mode 100644 index 000000000..48bc9cd9c --- /dev/null +++ b/screen/send/confirm.js @@ -0,0 +1,173 @@ +/* global alert */ +import React, { Component } from 'react'; +import { ActivityIndicator, TouchableOpacity, StyleSheet, View } from 'react-native'; +import { Text } from 'react-native-elements'; +import { BlueButton, SafeBlueArea, BlueCard, BlueSpacing40, BlueHeaderDefaultSub } from '../../BlueComponents'; +import { BitcoinUnit } from '../../models/bitcoinUnits'; +import PropTypes from 'prop-types'; +let loc = require('../../loc'); +let EV = require('../../events'); + +export default class Confirm extends Component { + static navigationOptions = { + headerStyle: { + backgroundColor: '#FFFFFF', + borderBottomWidth: 0, + }, + headerTintColor: '#0c2550', + }; + + constructor(props) { + super(props); + console.log('send/create constructor'); + + this.state = { + isLoading: false, + amount: props.navigation.getParam('amount'), + fee: props.navigation.getParam('fee'), + address: props.navigation.getParam('address'), + memo: props.navigation.getParam('memo'), + size: Math.round(props.navigation.getParam('tx').length / 2), + tx: props.navigation.getParam('tx'), + satoshiPerByte: props.navigation.getParam('satoshiPerByte'), + fromWallet: props.navigation.getParam('fromWallet'), + }; + } + + async componentDidMount() { + console.log('send/create - componentDidMount'); + console.log('address = ', this.state.address); + } + + broadcast() { + this.setState({ isLoading: true }, async () => { + let result = await this.state.fromWallet.broadcastTx(this.state.tx); + console.log('broadcast result = ', result); + if (typeof result === 'string') { + result = JSON.parse(result); + } + this.setState({ isLoading: false }); + if (result && result.error) { + alert(JSON.stringify(result.error)); + } else { + EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs + this.props.navigation.navigate('Success', { + fee: Number(this.state.fee), + amount: this.state.amount, + address: this.state.address, + dismissModal: () => this.props.navigation.dismiss(), + }); + } + }); + } + + render() { + return ( + + + + + + {this.state.amount} + + + {' ' + BitcoinUnit.BTC} + + + + {loc.send.create.fee}: {loc.formatBalance(this.state.fee, BitcoinUnit.SATS)} + + + + {loc.send.create.to} + {this.state.address} + + {this.state.isLoading ? ( + + ) : ( + this.broadcast()} + title={loc.send.confirm.sendNow} + style={{ maxWidth: 263, paddingHorizontal: 56 }} + /> + )} + + 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, + }) + } + > + {loc.transactions.details.title} + + + + ); + } +} + +const styles = StyleSheet.create({ + transactionDetailsTitle: { + color: '#0c2550', + fontWeight: '500', + fontSize: 17, + marginBottom: 2, + }, + transactionDetailsSubtitle: { + color: '#9aa0aa', + fontWeight: '500', + fontSize: 15, + marginBottom: 20, + }, +}); + +Confirm.propTypes = { + navigation: PropTypes.shape({ + goBack: PropTypes.function, + getParam: PropTypes.function, + navigate: PropTypes.function, + dismiss: PropTypes.function, + state: PropTypes.shape({ + params: PropTypes.shape({ + amount: PropTypes.string, + fee: PropTypes.number, + address: PropTypes.string, + memo: PropTypes.string, + fromWallet: PropTypes.shape({ + fromAddress: PropTypes.string, + fromSecret: PropTypes.string, + }), + }), + }), + }), +}; diff --git a/screen/send/create.js b/screen/send/create.js index b3793cf2c..c0ba8925a 100644 --- a/screen/send/create.js +++ b/screen/send/create.js @@ -1,13 +1,9 @@ -/* global alert */ import React, { Component } from 'react'; -import { TextInput, ActivityIndicator, TouchableOpacity, Clipboard, StyleSheet, ScrollView } from 'react-native'; +import { TextInput, TouchableOpacity, Clipboard, StyleSheet, ScrollView } from 'react-native'; import { Text } from 'react-native-elements'; -import { BlueButton, SafeBlueArea, BlueCard, BlueText } from '../../BlueComponents'; +import { BlueHeaderDefaultSub, SafeBlueArea, BlueCard, BlueText } from '../../BlueComponents'; import PropTypes from 'prop-types'; -/** @type {AppStorage} */ -// let BlueApp = require('../../BlueApp'); let loc = require('../../loc'); -let EV = require('../../events'); export default class SendCreate extends Component { constructor(props) { @@ -16,14 +12,13 @@ export default class SendCreate extends Component { this.state = { isLoading: false, - amount: props.navigation.state.params.amount, - fee: props.navigation.state.params.fee, - address: props.navigation.state.params.address, - memo: props.navigation.state.params.memo, + amount: props.navigation.getParam('amount'), + fee: props.navigation.getParam('fee'), + address: props.navigation.getParam('address'), + memo: props.navigation.getParam('memo'), size: Math.round(props.navigation.getParam('tx').length / 2), tx: props.navigation.getParam('tx'), satoshiPerByte: props.navigation.getParam('satoshiPerByte'), - fromWallet: props.navigation.getParam('fromWallet'), }; } @@ -32,27 +27,10 @@ export default class SendCreate extends Component { console.log('address = ', this.state.address); } - broadcast() { - this.setState({ isLoading: true }, async () => { - let result = await this.state.fromWallet.broadcastTx(this.state.tx); - console.log('broadcast result = ', result); - if (typeof result === 'string') { - result = JSON.parse(result); - } - this.setState({ isLoading: false }); - if (result && result.error) { - alert(JSON.stringify(result.error)); - } else { - EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs - alert('Transaction has been successfully broadcasted. Your transaction ID is: ' + JSON.stringify(result.result)); - this.props.navigation.navigate('Wallets'); - } - }); - } - render() { return ( + {loc.send.create.this_is_hex} @@ -79,11 +57,6 @@ export default class SendCreate extends Component { Clipboard.setString(this.state.tx)}> Copy and broadcast later - {this.state.isLoading ? ( - - ) : ( - this.broadcast()} title={loc.send.details.send} style={{ maxWidth: 263, paddingHorizontal: 56 }} /> - )} {loc.send.create.to} @@ -130,16 +103,13 @@ SendCreate.propTypes = { goBack: PropTypes.function, getParam: PropTypes.function, navigate: PropTypes.function, + dismiss: PropTypes.function, state: PropTypes.shape({ params: PropTypes.shape({ amount: PropTypes.string, fee: PropTypes.number, address: PropTypes.string, memo: PropTypes.string, - fromWallet: PropTypes.shape({ - fromAddress: PropTypes.string, - fromSecret: PropTypes.string, - }), }), }), }), diff --git a/screen/send/details.js b/screen/send/details.js index 8ff971754..aa132dd71 100644 --- a/screen/send/details.js +++ b/screen/send/details.js @@ -25,6 +25,7 @@ let BigNumber = require('bignumber.js'); let BlueApp = require('../../BlueApp'); let loc = require('../../loc'); let bitcoin = require('bitcoinjs-lib'); +let currency = require('../../currency'); const btcAddressRx = /^[a-zA-Z0-9]{26,35}$/; @@ -46,7 +47,7 @@ export default class SendDetails extends Component { let fromAddress; if (props.navigation.state.params) fromAddress = props.navigation.state.params.fromAddress; let fromSecret; - if (props.navigation.state.params.fromSecret) fromSecret = props.navigation.state.params.fromSecret; + if (props.navigation.state.params) fromSecret = props.navigation.state.params.fromSecret; let fromWallet = {}; let startTime2 = Date.now(); @@ -204,7 +205,14 @@ export default class SendDetails extends Component { } let startTime = Date.now(); - tx = this.state.fromWallet.createTx(utxo, this.state.amount, fee, this.state.address, this.state.memo); + try { + tx = this.state.fromWallet.createTx(utxo, this.state.amount, fee, this.state.address, this.state.memo); + } catch (error) { + console.log(error); + alert(loc.send.details.create_tx_error); + this.setState({ isLoading: false }); + return; + } let endTime = Date.now(); console.log('create tx ', (endTime - startTime) / 1000, 'sec'); @@ -238,15 +246,15 @@ export default class SendDetails extends Component { await BlueApp.saveToDisk(); } catch (err) { console.log(err); - alert(err); + alert(loc.send.details.create_tx_error); this.setState({ isLoading: false }); return; } this.setState({ isLoading: false }, () => - this.props.navigation.navigate('CreateTransaction', { + this.props.navigation.navigate('Confirm', { amount: this.state.amount, - fee: fee.toFixed(8), + fee: Number(fee.toFixed(8)), address: this.state.address, memo: this.state.memo, fromWallet: this.state.fromWallet, @@ -376,6 +384,11 @@ export default class SendDetails extends Component { {' ' + BitcoinUnit.BTC} + + + {currency.satoshiToLocalCurrency(loc.formatBalanceWithoutSuffix(this.state.amount || 0, BitcoinUnit.SATOSHIS))} + + + + + {this.state.amount} + + + {' ' + BitcoinUnit.BTC} + + + + {loc.send.create.fee}: {loc.formatBalance(this.state.fee, BitcoinUnit.SATS)} + + + + + + + { + this.props.navigation.getParam('dismissModal')(); + }} + title={loc.send.success.done} + style={{ maxWidth: 263, paddingHorizontal: 56 }} + /> + + + ); + } +} + +Success.propTypes = { + navigation: PropTypes.shape({ + goBack: PropTypes.function, + getParam: PropTypes.function, + navigate: PropTypes.function, + state: PropTypes.shape({ + params: PropTypes.shape({ + amount: PropTypes.string, + fee: PropTypes.number, + address: PropTypes.string, + }), + }), + }), +}; diff --git a/screen/transactions/details.js b/screen/transactions/details.js index 61df31baa..f671ef9fb 100644 --- a/screen/transactions/details.js +++ b/screen/transactions/details.js @@ -39,7 +39,7 @@ function formatTime(time) { export default class TransactionsDetails extends Component { static navigationOptions = { header: ({ navigation }) => { - return navigation.goBack(null)} />; + return navigation.goBack(null)} />; }, }; @@ -78,7 +78,7 @@ export default class TransactionsDetails extends Component { } render() { - if (this.state.isLoading) { + if (this.state.isLoading || !this.state.hasOwnProperty('tx')) { return ; } @@ -99,51 +99,82 @@ export default class TransactionsDetails extends Component { } })()} - - {loc.transactions.details.from} - - - {this.state.from.filter(onlyUnique).join(', ')} + {this.state.hasOwnProperty('from') && ( + + + {loc.transactions.details.from} + + + {this.state.from.filter(onlyUnique).join(', ')} + + )} - - {loc.transactions.details.to} - - - - {arrDiff(this.state.from, this.state.to.filter(onlyUnique)).join(', ')} - + {this.state.hasOwnProperty('to') && ( + + + {loc.transactions.details.to} + + + + {arrDiff(this.state.from, this.state.to.filter(onlyUnique)).join(', ')} + + + )} - - Txid - - - { - const url = `https://live.blockcypher.com/btc/tx/${this.state.tx.hash}`; - Linking.canOpenURL(url).then(supported => { - if (supported) { - Linking.openURL(url); - } - }); - }} - > - {this.state.tx.hash} - + {this.state.tx.hasOwnProperty('hash') && ( + + + Txid + + + { + const url = `https://live.blockcypher.com/btc/tx/${this.state.tx.hash}`; + Linking.canOpenURL(url).then(supported => { + if (supported) { + Linking.openURL(url); + } + }); + }} + > + {this.state.tx.hash} + + + )} + {this.state.tx.hasOwnProperty('received') && ( + + Received + {formatTime(this.state.tx.received)} + + )} - Received - {formatTime(this.state.tx.received)} + {this.state.tx.hasOwnProperty('block_height') && ( + + Block Height + {formatTime(this.state.tx.block_height)} + + )} - Block Height - {formatTime(this.state.tx.block_height)} + {this.state.tx.hasOwnProperty('confirmations') && ( + + Confirmations + {this.state.tx.confirmations} + + )} - Confirmations - {this.state.tx.confirmations} + {this.state.tx.hasOwnProperty('inputs') && ( + + Inputs + {this.state.tx.inputs.length} + + )} - Inputs - {this.state.tx.inputs.length} - - Outputs - {this.state.tx.outputs.length} + {this.state.tx.hasOwnProperty('outputs') && ( + + Outputs + {this.state.tx.outputs.length} + + )} {(() => { diff --git a/screen/wallets/export.js b/screen/wallets/export.js index 6bd55cc5a..cfaeb67fb 100644 --- a/screen/wallets/export.js +++ b/screen/wallets/export.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import { Dimensions, ActivityIndicator, View } from 'react-native'; import QRCode from 'react-native-qrcode'; -import { BlueSpacing, BlueSpacing40, SafeBlueArea, BlueCard, BlueText, BlueHeaderDefaultSub } from '../../BlueComponents'; +import { BlueSpacing40, SafeBlueArea, BlueCard, BlueText, BlueHeaderDefaultSub } from '../../BlueComponents'; import PropTypes from 'prop-types'; /** @type {AppStorage} */ let BlueApp = require('../../BlueApp'); @@ -48,6 +48,13 @@ export default class WalletExport extends Component { }); } + determineSize = () => { + if (width > 312) { + return width - 48; + } + return 312; + }; + render() { if (this.state.isLoading) { return ( @@ -72,8 +79,6 @@ export default class WalletExport extends Component { {(() => { if (isIpad) { return ; - } else { - return ; } })()} @@ -88,11 +93,11 @@ export default class WalletExport extends Component { })()} - {this.state.wallet.getSecret()} + {this.state.wallet.getSecret()} );