From 07877161fd9bca2d7c84a05f614ab82f1e38ba30 Mon Sep 17 00:00:00 2001 From: marcosrdz Date: Thu, 19 Nov 2020 20:16:11 -0500 Subject: [PATCH] REF: PSBTWithHardware screen uses Hooks --- BlueComponents.js | 69 ++- Navigation.js | 13 +- loc/en.json | 1 + screen/send/psbtWithHardwareWallet.js | 606 ++++++++++++-------------- screen/send/success.js | 42 +- 5 files changed, 346 insertions(+), 385 deletions(-) diff --git a/BlueComponents.js b/BlueComponents.js index c68c0c4d8..3d6d57306 100644 --- a/BlueComponents.js +++ b/BlueComponents.js @@ -144,42 +144,41 @@ export const BlueButtonHook = props => { ); }; -export class SecondButton extends Component { - render() { - let backgroundColor = this.props.backgroundColor ? this.props.backgroundColor : BlueCurrentTheme.colors.buttonBlueBackgroundColor; - let fontColor = BlueCurrentTheme.colors.buttonTextColor; - if (this.props.disabled === true) { - backgroundColor = BlueCurrentTheme.colors.buttonDisabledBackgroundColor; - fontColor = BlueCurrentTheme.colors.buttonDisabledTextColor; - } - // let buttonWidth = this.props.width ? this.props.width : width / 1.5; - // if ('noMinWidth' in this.props) { - // buttonWidth = 0; - // } - return ( - - - {this.props.icon && } - {this.props.title && {this.props.title}} - - - ); +export const SecondButton = props => { + const { colors } = useTheme(); + let backgroundColor = props.backgroundColor ? props.backgroundColor : colors.buttonBlueBackgroundColor; + let fontColor = colors.buttonTextColor; + if (props.disabled === true) { + backgroundColor = colors.buttonDisabledBackgroundColor; + fontColor = colors.buttonDisabledTextColor; } -} + // let buttonWidth = this.props.width ? this.props.width : width / 1.5; + // if ('noMinWidth' in this.props) { + // buttonWidth = 0; + // } + return ( + + + {props.icon && } + {props.title && {props.title}} + + + ); +}; export const BitcoinButton = props => { const { colors } = useTheme(); diff --git a/Navigation.js b/Navigation.js index 27850e71f..16670c709 100644 --- a/Navigation.js +++ b/Navigation.js @@ -146,7 +146,7 @@ const WalletsRoot = () => ( component={LNDViewInvoice} options={LNDViewInvoice.navigationOptions} swipeEnabled={false} - gesturesEnabled={false} + gestureEnabled={false} /> ( /> - + ); @@ -205,7 +212,7 @@ const LNDCreateInvoiceRoot = () => ( component={LNDViewInvoice} options={LNDViewInvoice.navigationOptions} swipeEnabled={false} - gesturesEnabled={false} + gestureEnabled={false} /> { + const { txMetadata, fetchAndSaveWalletTransactions } = useContext(BlueStorageContext); + const navigation = useNavigation(); + const route = useRoute(); + const { fromWallet, memo, psbt, deepLinkPSBT } = route.params; + const routeParamsPSBT = useRef(route.params.psbt); + const routeParamsTXHex = route.params.txhex; + const { colors } = useTheme(); + const [isLoading, setIsLoading] = useState(false); + const [txHex, setTxHex] = useState(route.params.txhex); + + const stylesHook = StyleSheet.create({ + root: { + backgroundColor: colors.elevated, + }, + rootPadding: { + backgroundColor: colors.elevated, + }, + hexWrap: { + backgroundColor: colors.elevated, + }, + hexLabel: { + color: colors.foregroundColor, + }, + hexInput: { + borderColor: colors.formBorder, + backgroundColor: colors.inputBackgroundColor, + color: colors.foregroundColor, + }, + hexText: { + color: colors.foregroundColor, + }, + }); + + const _combinePSBT = receivedPSBT => { + return fromWallet.combinePsbt(psbt, receivedPSBT); + }; + + const onBarScanned = ret => { + if (ret && !ret.data) ret = { data: ret }; + if (ret.data.toUpperCase().startsWith('UR')) { + alert('BC-UR not decoded. This should never happen'); + } + if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) { + // this looks like NOT base64, so maybe its transaction's hex + setTxHex(ret.data); + return; + } + try { + const Tx = _combinePSBT(ret.data); + setTxHex(Tx.toHex()); + } catch (Err) { + alert(Err); + } + }; + + useEffect(() => { + if (!psbt && !route.params.txhex) { + alert(loc.send.no_tx_signing_in_progress); + } + + if (deepLinkPSBT) { + const psbt = bitcoin.Psbt.fromBase64(deepLinkPSBT); + try { + const Tx = fromWallet.combinePsbt(routeParamsPSBT.current, psbt); + setTxHex(Tx.toHex()); + } catch (Err) { + alert(Err); + } + } else if (routeParamsTXHex) { + setTxHex(routeParamsTXHex); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [deepLinkPSBT, routeParamsTXHex]); + + const broadcast = async () => { + setIsLoading(true); + try { + await BlueElectrum.ping(); + await BlueElectrum.waitTillConnected(); + const result = await fromWallet.broadcastTx(txHex); + if (result) { + setIsLoading(false); + const txDecoded = bitcoin.Transaction.fromHex(txHex); + const txid = txDecoded.getId(); + Notifications.majorTomToGroundControl([], [], [txid]); + if (memo) { + txMetadata[txid] = { memo }; + } + fetchAndSaveWalletTransactions(fromWallet.getID()); + navigation.navigate('Success', { amount: undefined }); + } else { + ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); + setIsLoading(false); + alert(loc.errors.broadcast); + } + } catch (error) { + ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); + setIsLoading(false); + alert(error.message); + } + }; + + const _renderBroadcastHex = () => { + return ( + + + {loc.send.create_this_is_hex} + + + + {loc.send.create_copy} + + Linking.openURL('https://coinb.in/?verify=' + txHex)}> + {loc.send.create_verify} + + + + + + ); + }; + + const exportPSBT = async () => { + const fileName = `${Date.now()}.psbt`; + if (Platform.OS === 'ios') { + const filePath = RNFS.TemporaryDirectoryPath + `/${fileName}`; + await RNFS.writeFile(filePath, typeof psbt === 'string' ? psbt : psbt.toBase64()); + Share.open({ + url: 'file://' + filePath, + saveToFiles: isDesktop, + }) + .catch(error => { + console.log(error); + }) + .finally(() => { + RNFS.unlink(filePath); + }); + } else if (Platform.OS === 'android') { + const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, { + title: loc.send.permission_storage_title, + message: loc.send.permission_storage_message, + buttonNeutral: loc.send.permission_storage_later, + buttonNegative: loc._.cancel, + buttonPositive: loc._.ok, + }); + + if (granted === PermissionsAndroid.RESULTS.GRANTED) { + console.log('Storage Permission: Granted'); + const filePath = RNFS.DownloadDirectoryPath + `/${fileName}`; + await RNFS.writeFile(filePath, typeof psbt === 'string' ? psbt : psbt.toBase64()); + alert(loc.formatString(loc.send.txSaved, { filePath: fileName })); + } else { + console.log('Storage Permission: Denied'); + } + } + }; + + const openSignedTransaction = async () => { + try { + const res = await DocumentPicker.pick({ + type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallt.psbt.txn'] : [DocumentPicker.types.allFiles], + }); + const file = await RNFS.readFile(res.uri); + if (file) { + onBarScanned({ data: file }); + } else { + throw new Error(); + } + } catch (err) { + if (!DocumentPicker.isCancel(err)) { + alert(loc.send.details_no_signed_tx); + } + } + }; + + const openScanner = () => { + if (isDesktop) { + ImagePicker.launchCamera( + { + title: null, + mediaType: 'photo', + takePhotoButtonTitle: null, + }, + response => { + if (response.uri) { + const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.path.toString(); + LocalQRCode.decode(uri, (error, result) => { + if (!error) { + onBarScanned(result); + } else { + alert(loc.send.qr_error_no_qrcode); + } + }); + } else if (response.error) { + ScanQRCode.presentCameraNotAuthorizedAlert(response.error); + } + }, + ); + } else { + navigation.navigate('ScanQRCodeRoot', { + screen: 'ScanQRCode', + params: { + launchedBy: route.name, + showFileImportButton: false, + onBarScanned, + }, + }); + } + }; + + if (txHex) return _renderBroadcastHex(); + + return isLoading ? ( + + + + ) : ( + + + + + {loc.send.psbt_this_is_psbt} + + + + + + + + + + + + + + + + + ); +}; + +export default PsbtWithHardwareWallet; + +PsbtWithHardwareWallet.navigationOptions = () => ({ + ...BlueNavigationStyle(null, false), + title: loc.send.header, +}); + const styles = StyleSheet.create({ root: { flex: 1, - backgroundColor: BlueCurrentTheme.colors.elevated, }, scrollViewContent: { flexGrow: 1, @@ -66,7 +338,6 @@ const styles = StyleSheet.create({ rootPadding: { flex: 1, paddingTop: 20, - backgroundColor: BlueCurrentTheme.colors.elevated, }, closeCamera: { width: 40, @@ -88,18 +359,13 @@ const styles = StyleSheet.create({ hexWrap: { alignItems: 'center', flex: 1, - backgroundColor: BlueCurrentTheme.colors.elevated, }, hexLabel: { - color: BlueCurrentTheme.colors.foregroundColor, fontWeight: '500', }, hexInput: { - borderColor: BlueCurrentTheme.colors.formBorder, - backgroundColor: BlueCurrentTheme.colors.inputBackgroundColor, borderRadius: 4, marginTop: 20, - color: BlueCurrentTheme.colors.foregroundColor, fontWeight: '500', fontSize: 14, paddingHorizontal: 16, @@ -110,7 +376,6 @@ const styles = StyleSheet.create({ marginVertical: 24, }, hexText: { - color: BlueCurrentTheme.colors.foregroundColor, fontSize: 15, fontWeight: '500', alignSelf: 'center', @@ -120,314 +385,3 @@ const styles = StyleSheet.create({ alignItems: 'center', }, }); - -export default class PsbtWithHardwareWallet extends Component { - cameraRef = null; - static contextType = BlueStorageContext; - - _combinePSBT = receivedPSBT => { - return this.state.fromWallet.combinePsbt(this.state.psbt, receivedPSBT); - }; - - onBarScanned = ret => { - if (ret && !ret.data) ret = { data: ret }; - if (ret.data.toUpperCase().startsWith('UR')) { - alert('BC-UR not decoded. This should never happen'); - } - if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) { - // this looks like NOT base64, so maybe its transaction's hex - this.setState({ txhex: ret.data }); - return; - } - try { - const Tx = this._combinePSBT(ret.data); - this.setState({ txhex: Tx.toHex() }); - } catch (Err) { - alert(Err); - } - }; - - constructor(props) { - super(props); - this.state = { - isLoading: false, - qrCodeHeight: height > width ? width - 40 : width / 3, - memo: props.route.params.memo, - psbt: props.route.params.psbt, - fromWallet: props.route.params.fromWallet, - isSecondPSBTAlreadyBase64: false, - deepLinkPSBT: undefined, - txhex: props.route.params.txhex || undefined, - animatedQRCodeData: [], - }; - this.fileName = `${Date.now()}.psbt`; - } - - static getDerivedStateFromProps(nextProps, prevState) { - if (!prevState.psbt && !nextProps.route.params.txhex) { - alert('There is no transaction signing in progress'); - return { - ...prevState, - isLoading: true, - }; - } - - const deepLinkPSBT = nextProps.route.params.deepLinkPSBT; - const txhex = nextProps.route.params.txhex; - if (deepLinkPSBT) { - const psbt = bitcoin.Psbt.fromBase64(deepLinkPSBT); - try { - const Tx = prevState.fromWallet.combinePsbt(prevState.psbt, psbt); - return { - ...prevState, - txhex: Tx.toHex(), - }; - } catch (Err) { - alert(Err); - } - } else if (txhex) { - return { - ...prevState, - txhex: txhex, - }; - } - return prevState; - } - - componentDidMount() { - console.log('send/psbtWithHardwareWallet - componentDidMount'); - } - - broadcast = () => { - this.setState({ isLoading: true }, async () => { - try { - await BlueElectrum.ping(); - await BlueElectrum.waitTillConnected(); - const result = await this.state.fromWallet.broadcastTx(this.state.txhex); - if (result) { - this.setState({ success: true, isLoading: false }); - const txDecoded = bitcoin.Transaction.fromHex(this.state.txhex); - const txid = txDecoded.getId(); - Notifications.majorTomToGroundControl([], [], [txid]); - if (this.state.memo) { - this.context.txMetadata[txid] = { memo: this.state.memo }; - } - } else { - ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); - this.setState({ isLoading: false }); - alert(loc.errors.broadcast); - } - } catch (error) { - ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); - this.setState({ isLoading: false }); - alert(error.message); - } - }); - }; - - _renderSuccess() { - return ( - - - - this.props.navigation.dangerouslyGetParent().pop()} title={loc.send.success_done} /> - - - ); - } - - _renderBroadcastHex() { - return ( - - - {loc.send.create_this_is_hex} - - - Clipboard.setString(this.state.txhex)}> - {loc.send.create_copy} - - Linking.openURL('https://coinb.in/?verify=' + this.state.txhex)}> - {loc.send.create_verify} - - - - - - ); - } - - exportPSBT = async () => { - if (Platform.OS === 'ios') { - const filePath = RNFS.TemporaryDirectoryPath + `/${this.fileName}`; - await RNFS.writeFile(filePath, typeof this.state.psbt === 'string' ? this.state.psbt : this.state.psbt.toBase64()); - Share.open({ - url: 'file://' + filePath, - saveToFiles: isDesktop, - }) - .catch(error => { - console.log(error); - alert(error.message); - }) - .finally(() => { - RNFS.unlink(filePath); - }); - } else if (Platform.OS === 'android') { - const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, { - title: loc.send.permission_storage_title, - message: loc.send.permission_storage_message, - buttonNeutral: loc.send.permission_storage_later, - buttonNegative: loc._.cancel, - buttonPositive: loc._.ok, - }); - - if (granted === PermissionsAndroid.RESULTS.GRANTED) { - console.log('Storage Permission: Granted'); - const filePath = RNFS.DownloadDirectoryPath + `/${this.fileName}`; - await RNFS.writeFile(filePath, typeof this.state.psbt === 'string' ? this.state.psbt : this.state.psbt.toBase64()); - alert(loc.formatString(loc.send.txSaved, { filePath: this.fileName })); - } else { - console.log('Storage Permission: Denied'); - } - } - }; - - openSignedTransaction = async () => { - try { - const res = await DocumentPicker.pick({ - type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallt.psbt.txn'] : [DocumentPicker.types.allFiles], - }); - const file = await RNFS.readFile(res.uri); - if (file) { - this.setState({ isSecondPSBTAlreadyBase64: true }, () => this.onBarScanned({ data: file })); - } else { - this.setState({ isSecondPSBTAlreadyBase64: false }); - throw new Error(); - } - } catch (err) { - if (!DocumentPicker.isCancel(err)) { - alert(loc.send.details_no_signed_tx); - } - } - }; - - openScanner = () => { - if (isDesktop) { - ImagePicker.launchCamera( - { - title: null, - mediaType: 'photo', - takePhotoButtonTitle: null, - }, - response => { - if (response.uri) { - const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.path.toString(); - LocalQRCode.decode(uri, (error, result) => { - if (!error) { - this.onBarScanned(result); - } else { - alert(loc.send.qr_error_no_qrcode); - } - }); - } else if (response.error) { - ScanQRCode.presentCameraNotAuthorizedAlert(response.error); - } - }, - ); - } else { - this.props.navigation.navigate('ScanQRCodeRoot', { - screen: 'ScanQRCode', - params: { - launchedBy: this.props.route.name, - showFileImportButton: false, - onBarScanned: this.onBarScanned, - }, - }); - } - }; - - render() { - if (this.state.isLoading) { - return ( - - - - ); - } - - if (this.state.success) return this._renderSuccess(); - if (this.state.txhex) return this._renderBroadcastHex(); - - return ( - - - - - {loc.send.psbt_this_is_psbt} - - - - - - - - - - - - - - - - - ); - } -} - -PsbtWithHardwareWallet.propTypes = { - navigation: PropTypes.shape({ - goBack: PropTypes.func, - navigate: PropTypes.func, - dangerouslyGetParent: PropTypes.func, - }), - route: PropTypes.shape({ - params: PropTypes.object, - name: PropTypes.string, - }), -}; - -PsbtWithHardwareWallet.navigationOptions = () => ({ - ...BlueNavigationStyle(null, false), - title: loc.send.header, -}); diff --git a/screen/send/success.js b/screen/send/success.js index adad08ac5..a6d115a47 100644 --- a/screen/send/success.js +++ b/screen/send/success.js @@ -1,9 +1,9 @@ import React, { useEffect, useRef } from 'react'; import LottieView from 'lottie-react-native'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; -import { View, StyleSheet } from 'react-native'; +import { View, StyleSheet, SafeAreaView } from 'react-native'; import { Text } from 'react-native-elements'; -import { BlueButton, SafeBlueArea, BlueCard } from '../../BlueComponents'; +import { BlueButton, BlueCard } from '../../BlueComponents'; import { BitcoinUnit } from '../../models/bitcoinUnits'; import loc from '../../loc'; import { useNavigation, useRoute, useTheme } from '@react-navigation/native'; @@ -11,7 +11,7 @@ import { useNavigation, useRoute, useTheme } from '@react-navigation/native'; const Success = () => { const { colors } = useTheme(); const { dangerouslyGetParent } = useNavigation(); - const { amount, fee = 0, amountUnit = BitcoinUnit.BTC, invoiceDescription = '' } = useRoute().params; + const { amount = 0, fee = 0, amountUnit = BitcoinUnit.BTC, invoiceDescription = '' } = useRoute().params; const animationRef = useRef(); const stylesHook = StyleSheet.create({ root: { @@ -39,14 +39,16 @@ const Success = () => { }, [colors]); return ( - - - {amount > 0 && ( - - {amount} - {' ' + amountUnit} - - )} + + + + {amount > 0 && ( + <> + {amount} + {' ' + amountUnit} + + )} + {fee > 0 && ( {loc.send.create_fee}: {fee} {BitcoinUnit.BTC} @@ -81,28 +83,26 @@ const Success = () => { ]} /> - + - - + + ); }; -Success.navigationOptions = { - headerShown: false, - gesturesEnabled: false, -}; - export default Success; const styles = StyleSheet.create({ root: { flex: 1, paddingTop: 19, + justifyContent: 'space-between', }, - amout: { + buttonContainer: { + padding: 58, + }, + amount: { alignItems: 'center', - flex: 1, }, view: { flexDirection: 'row',