diff --git a/App.js b/App.js index 6ad2cfb11..4530804c8 100644 --- a/App.js +++ b/App.js @@ -117,9 +117,10 @@ export default class App extends React.Component { wallet.chain === Chain.ONCHAIN ? wallet.weOwnAddress(clipboard) : wallet.isInvoiceGeneratedByWallet(clipboard), ); if ( - !isAddressFromStoredWallet && - this.state.clipboardContent !== clipboard && - (this.isBitcoinAddress(clipboard) || this.isLightningInvoice(clipboard) || this.isLnUrl(clipboard)) + (!isAddressFromStoredWallet && + this.state.clipboardContent !== clipboard && + (this.isBitcoinAddress(clipboard) || this.isLightningInvoice(clipboard) || this.isLnUrl(clipboard))) || + this.isBothBitcoinAndLightning(clipboard) ) { this.setState({ isClipboardContentModalVisible: true }); } @@ -142,6 +143,10 @@ export default class App extends React.Component { } isBitcoinAddress(address) { + address = address + .replace('bitcoin:', '') + .replace('bitcoin=', '') + .split('?')[0]; let isValidBitcoinAddress = false; try { bitcoin.address.toOutputScript(address); @@ -150,12 +155,6 @@ export default class App extends React.Component { } catch (err) { isValidBitcoinAddress = false; } - if (!isValidBitcoinAddress) { - if (address.indexOf('bitcoin:') === 0 || address.indexOf('BITCOIN:') === 0) { - isValidBitcoinAddress = true; - this.setState({ clipboardContentModalAddressType: bitcoinModalString }); - } - } return isValidBitcoinAddress; } @@ -175,12 +174,77 @@ export default class App extends React.Component { return false; } + isBothBitcoinAndLightning(url) { + if (url.includes('lightning') && url.includes('bitcoin')) { + const txInfo = url.split(/(bitcoin:|lightning:|lightning=|bitcoin=)+/); + let bitcoin; + let lndInvoice; + for (const [index, value] of txInfo.entries()) { + try { + // Inside try-catch. We dont wan't to crash in case of an out-of-bounds error. + if (value.startsWith('bitcoin')) { + bitcoin = `bitcoin:${txInfo[index + 1]}`; + if (!this.isBitcoinAddress(bitcoin)) { + bitcoin = false; + break; + } + } else if (value.startsWith('lightning')) { + lndInvoice = `lightning:${txInfo[index + 1]}`; + if (!this.isLightningInvoice(lndInvoice)) { + lndInvoice = false; + break; + } + } + } catch (e) { + console.log(e); + } + if (bitcoin && lndInvoice) break; + } + if (bitcoin && lndInvoice) { + this.setState({ + clipboardContent: { bitcoin, lndInvoice }, + }); + return { bitcoin, lndInvoice }; + } else { + return undefined; + } + } + return undefined; + } + isSafelloRedirect(event) { let urlObject = url.parse(event.url, true) // eslint-disable-line return !!urlObject.query['safello-state-token']; } + isBothBitcoinAndLightningWalletSelect = wallet => { + const clipboardContent = this.state.clipboardContent; + if (wallet.chain === Chain.ONCHAIN) { + this.navigator && + this.navigator.dispatch( + NavigationActions.navigate({ + routeName: 'SendDetails', + params: { + uri: clipboardContent.bitcoin, + fromWallet: wallet, + }, + }), + ); + } else if (wallet.chain === Chain.OFFCHAIN) { + this.navigator && + this.navigator.dispatch( + NavigationActions.navigate({ + routeName: 'ScanLndInvoice', + params: { + uri: clipboardContent.lndInvoice, + fromSecret: wallet.getSecret(), + }, + }), + ); + } + }; + handleOpenURL = event => { if (event.url === null) { return; @@ -188,7 +252,23 @@ export default class App extends React.Component { if (typeof event.url !== 'string') { return; } - if (this.isBitcoinAddress(event.url)) { + let isBothBitcoinAndLightning; + try { + isBothBitcoinAndLightning = this.isBothBitcoinAndLightning(event.url); + } catch (e) { + console.log(e); + } + if (isBothBitcoinAndLightning) { + this.navigator && + this.navigator.dispatch( + NavigationActions.navigate({ + routeName: 'HandleOffchainAndOnChain', + params: { + onWalletSelect: this.isBothBitcoinAndLightningWalletSelect, + }, + }), + ); + } else if (this.isBitcoinAddress(event.url)) { this.navigator && this.navigator.dispatch( NavigationActions.navigate({ diff --git a/BlueComponents.js b/BlueComponents.js index b0f169471..5a13c26ff 100644 --- a/BlueComponents.js +++ b/BlueComponents.js @@ -53,7 +53,7 @@ export class BlueButton extends Component { backgroundColor = BlueApp.settings.buttonDisabledBackgroundColor; fontColor = BlueApp.settings.buttonDisabledTextColor; } - let buttonWidth = width / 1.5; + let buttonWidth = this.props.width ? this.props.width : width / 1.5; if (this.props.hasOwnProperty('noMinWidth')) { buttonWidth = 0; } diff --git a/MainBottomTabs.js b/MainBottomTabs.js index 2d1365a3a..9e4af6d1e 100644 --- a/MainBottomTabs.js +++ b/MainBottomTabs.js @@ -35,7 +35,6 @@ import rbfBumpFee from './screen/transactions/RBFBumpFee'; import rbfCancel from './screen/transactions/RBFCancel'; import receiveDetails from './screen/receive/details'; -import setReceiveAmount from './screen/receive/receiveAmount'; import sendDetails from './screen/send/details'; import ScanQRCode from './screen/send/scanQrAddress'; @@ -228,6 +227,32 @@ const LightningScanInvoiceStackNavigator = createStackNavigator({ }, }); +const HandleOffchainAndOnChainStackNavigator = createStackNavigator( + { + SelectWallet: { + screen: SelectWallet, + }, + // LND: + + ScanLndInvoice: { + screen: LightningScanInvoiceStackNavigator, + navigationOptions: { + header: null, + }, + }, + ScanQrAddress: { + screen: ScanQRCode, + }, + SendDetails: { + screen: CreateTransactionStackNavigator, + navigationOptions: { + header: null, + }, + }, + }, + { headerBackTitleVisible: false }, +); + const MainBottomTabs = createStackNavigator( { Wallets: { @@ -278,10 +303,6 @@ const MainBottomTabs = createStackNavigator( screen: receiveDetails, }, - ReceiveAmount: { - screen: setReceiveAmount, - }, - // // LND: @@ -311,6 +332,12 @@ const MainBottomTabs = createStackNavigator( header: null, }, }, + HandleOffchainAndOnChain: { + screen: HandleOffchainAndOnChainStackNavigator, + navigationOptions: { + header: null, + }, + }, }, { mode: 'modal', diff --git a/android/app/build.gradle b/android/app/build.gradle index 77257b908..37ac1334f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -119,7 +119,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 - versionName "4.9.0" + versionName "4.9.1" multiDexEnabled true missingDimensionStrategy 'react-native-camera', 'general' } diff --git a/ios/BlueWallet/Info.plist b/ios/BlueWallet/Info.plist index b3534f6ee..e1375e790 100644 --- a/ios/BlueWallet/Info.plist +++ b/ios/BlueWallet/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 4.9.0 + 4.9.1 CFBundleSignature ???? CFBundleURLTypes diff --git a/ios/BlueWalletWatch Extension/Info.plist b/ios/BlueWalletWatch Extension/Info.plist index b9b8ee2ad..34dedd1e6 100644 --- a/ios/BlueWalletWatch Extension/Info.plist +++ b/ios/BlueWalletWatch Extension/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 4.9.0 + 4.9.1 CFBundleVersion 239 CLKComplicationPrincipalClass diff --git a/ios/BlueWalletWatch/Info.plist b/ios/BlueWalletWatch/Info.plist index 26f0c6790..925b3b3fc 100644 --- a/ios/BlueWalletWatch/Info.plist +++ b/ios/BlueWalletWatch/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 4.9.0 + 4.9.1 CFBundleVersion 239 UISupportedInterfaceOrientations diff --git a/ios/TodayExtension/Info.plist b/ios/TodayExtension/Info.plist index eebccd34a..3fca81fb5 100644 --- a/ios/TodayExtension/Info.plist +++ b/ios/TodayExtension/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 4.9.0 + 4.9.1 CFBundleVersion 1 NSExtension diff --git a/package-lock.json b/package-lock.json index e3fe32f6d..1fc24ea3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "BlueWallet", - "version": "4.9.0", + "version": "4.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 8a3617722..d37a2b88a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "BlueWallet", - "version": "4.9.0", + "version": "4.9.1", "devDependencies": { "@babel/core": "^7.5.0", "@babel/runtime": "^7.5.1", diff --git a/screen/receive/details.js b/screen/receive/details.js index eda7c6e0c..2976229da 100644 --- a/screen/receive/details.js +++ b/screen/receive/details.js @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import { View, InteractionManager, ScrollView } from 'react-native'; +import { View, InteractionManager, Platform, TextInput, KeyboardAvoidingView, Keyboard, StyleSheet, ScrollView } from 'react-native'; import QRCode from 'react-native-qrcode-svg'; import bip21 from 'bip21'; import { @@ -10,14 +10,18 @@ import { BlueButtonLink, BlueNavigationStyle, is, + BlueBitcoinAmount, + BlueText, + BlueSpacing20, } from '../../BlueComponents'; import PropTypes from 'prop-types'; import Privacy from '../../Privacy'; import Share from 'react-native-share'; -import { Chain } from '../../models/bitcoinUnits'; +import { Chain, BitcoinUnit } from '../../models/bitcoinUnits'; +import Modal from 'react-native-modal'; /** @type {AppStorage} */ -let BlueApp = require('../../BlueApp'); -let loc = require('../../loc'); +const BlueApp = require('../../BlueApp'); +const loc = require('../../loc'); export default class ReceiveDetails extends Component { static navigationOptions = ({ navigation }) => ({ @@ -33,7 +37,11 @@ export default class ReceiveDetails extends Component { this.state = { secret: secret, addressText: '', + customLabel: '', + customAmount: 0, bip21encoded: undefined, + isCustom: false, + isCustomModalVisible: false, }; } @@ -82,12 +90,10 @@ export default class ReceiveDetails extends Component { } this.setState({ address: address, - addressText: address, }); } else if (wallet.getAddress) { this.setState({ address: wallet.getAddress(), - addressText: wallet.getAddress(), }); } } @@ -99,15 +105,102 @@ export default class ReceiveDetails extends Component { }); } - async componentWillUnmount() { + componentWillUnmount() { Privacy.disableBlur(); } + renderCustomAmountModal = () => { + return ( + { + Keyboard.dismiss(); + this.setState({ isCustomModalVisible: false }); + }} + > + + + this.setState({ customAmount: text })} /> + + this.setState({ customLabel: text })} + placeholder={loc.receive.details.label} + value={this.state.customLabel || ''} + numberOfLines={1} + style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }} + /> + + + + { + this.setState({ + isCustom: true, + isCustomModalVisible: false, + bip21encoded: bip21.encode(this.state.address, { amount: this.state.customAmount, label: this.state.customLabel }), + }); + }} + /> + + { + this.setState({ + isCustom: false, + isCustomModalVisible: false, + customAmount: '', + customLabel: '', + bip21encoded: bip21.encode(this.state.addresss), + }); + }} + /> + + + + + + ); + }; + + showCustomAmountModal = () => { + this.setState({ isCustomModalVisible: true }); + }; + render() { return ( + {this.state.isCustom && ( + <> + + {this.state.customAmount} {BitcoinUnit.BTC} + + + {this.state.customLabel} + + + )} {this.state.bip21encoded === undefined ? ( @@ -124,17 +217,10 @@ export default class ReceiveDetails extends Component { getRef={c => (this.qrCodeSVG = c)} /> )} - + - { - this.props.navigation.navigate('ReceiveAmount', { - address: this.state.address, - }); - }} - /> + { if (this.qrCodeSVG === undefined) { - Share.open({ message: `bitcoin:${this.state.address}` }).catch(error => console.log(error)); + Share.open({ message: this.state.bip21encoded }).catch(error => console.log(error)); } else { InteractionManager.runAfterInteractions(async () => { this.qrCodeSVG.toDataURL(data => { let shareImageBase64 = { - message: `bitcoin:${this.state.address}`, + message: this.state.bip21encoded, url: `data:image/png;base64,${data}`, }; Share.open(shareImageBase64).catch(error => console.log(error)); @@ -161,12 +247,31 @@ export default class ReceiveDetails extends Component { /> + {this.renderCustomAmountModal()} ); } } +const styles = StyleSheet.create({ + modalContent: { + backgroundColor: '#FFFFFF', + padding: 22, + justifyContent: 'center', + alignItems: 'center', + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + borderColor: 'rgba(0, 0, 0, 0.1)', + minHeight: 350, + height: 350, + }, + bottomModal: { + justifyContent: 'flex-end', + margin: 0, + }, +}); + ReceiveDetails.propTypes = { navigation: PropTypes.shape({ goBack: PropTypes.func, diff --git a/screen/receive/receiveAmount.js b/screen/receive/receiveAmount.js deleted file mode 100644 index abfa1195f..000000000 --- a/screen/receive/receiveAmount.js +++ /dev/null @@ -1,168 +0,0 @@ -import React, { Component } from 'react'; -import { View, Share, TextInput, KeyboardAvoidingView, Dimensions, ScrollView } from 'react-native'; -import QRCode from 'react-native-qrcode-svg'; -import bip21 from 'bip21'; -import { - SafeBlueArea, - BlueCard, - BlueButton, - BlueNavigationStyle, - BlueBitcoinAmount, - BlueText, - BlueCopyTextToClipboard, -} from '../../BlueComponents'; -import PropTypes from 'prop-types'; -import Privacy from '../../Privacy'; -/** @type {AppStorage} */ -let BlueApp = require('../../BlueApp'); -let loc = require('../../loc'); -const { width } = Dimensions.get('window'); - -export default class ReceiveAmount extends Component { - static navigationOptions = ({ navigation }) => ({ - ...BlueNavigationStyle(navigation, true), - title: loc.receive.header, - headerLeft: null, - }); - - static propTypes = { - navigation: PropTypes.shape({ - state: PropTypes.shape({ - params: PropTypes.shape({ - address: PropTypes.string, - }), - }), - }), - }; - - constructor(props) { - super(props); - let address = props.navigation.state.params.address; - - this.state = { - address: address, - addressText: address, - amount: undefined, - label: undefined, - amountSet: false, - }; - } - - async componentDidMount() { - Privacy.enableBlur(); - } - - async componentWillUnmount() { - Privacy.disableBlur(); - } - - determineSize = () => { - if (width > 312) { - return width - 48; - } - return 312; - }; - - renderDefault() { - return ( - - - this.setState({ label: text })} - placeholder={loc.receive.details.label} - value={this.state.label || ''} - numberOfLines={1} - style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }} - editable={!this.state.isLoading} - /> - - - { - this.setState({ - amountSet: true, - bip21: bip21.encode(this.state.address, { amount: this.state.amount, label: this.state.label }), - }); - }} - /> - - - ); - } - - renderWithSetAmount() { - return ( - - - {this.state.label} - - - - - - - - - ); - } - - render() { - return ( - - - - - this.setState({ amount: text })} - disabled={this.state.amountSet} - /> - {this.state.amountSet ? this.renderWithSetAmount() : this.renderDefault()} - - {this.state.amountSet && ( - - { - Share.share({ - message: this.state.bip21, - }); - }} - title={loc.receive.details.share} - /> - - )} - - - - ); - } -} diff --git a/screen/wallets/selectWallet.js b/screen/wallets/selectWallet.js index a5c81f37e..e409c7834 100644 --- a/screen/wallets/selectWallet.js +++ b/screen/wallets/selectWallet.js @@ -33,7 +33,7 @@ export default class SelectWallet extends Component { componentDidMount() { const wallets = this.chainType ? BlueApp.getWallets().filter(item => item.chain === this.chainType && item.allowSend()) - : BlueApp.getWallets(); + : BlueApp.getWallets().filter(item => item.allowSend()); this.setState({ data: wallets, isLoading: false,