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,