mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-03-12 02:08:22 +01:00
ADD: Export/Import PSBTs
eipiji
This commit is contained in:
parent
f5f5e787b8
commit
9e302df75d
22 changed files with 971 additions and 414 deletions
252
App.js
252
App.js
|
@ -1,18 +1,17 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Linking, DeviceEventEmitter, AppState, Clipboard, StyleSheet, KeyboardAvoidingView, Platform, View } from 'react-native';
|
import { Linking, DeviceEventEmitter, AppState, Clipboard, StyleSheet, KeyboardAvoidingView, Platform, View } from 'react-native';
|
||||||
import AsyncStorage from '@react-native-community/async-storage';
|
|
||||||
import Modal from 'react-native-modal';
|
import Modal from 'react-native-modal';
|
||||||
import { NavigationActions } from 'react-navigation';
|
import { NavigationActions } from 'react-navigation';
|
||||||
import MainBottomTabs from './MainBottomTabs';
|
import MainBottomTabs from './MainBottomTabs';
|
||||||
import NavigationService from './NavigationService';
|
import NavigationService from './NavigationService';
|
||||||
import { BlueTextCentered, BlueButton } from './BlueComponents';
|
import { BlueTextCentered, BlueButton } from './BlueComponents';
|
||||||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||||
import url from 'url';
|
|
||||||
import { AppStorage, LightningCustodianWallet } from './class';
|
|
||||||
import { Chain } from './models/bitcoinUnits';
|
import { Chain } from './models/bitcoinUnits';
|
||||||
import QuickActions from 'react-native-quick-actions';
|
import QuickActions from 'react-native-quick-actions';
|
||||||
import * as Sentry from '@sentry/react-native';
|
import * as Sentry from '@sentry/react-native';
|
||||||
import OnAppLaunch from './class/onAppLaunch';
|
import OnAppLaunch from './class/onAppLaunch';
|
||||||
|
import DeeplinkSchemaMatch from './class/deeplinkSchemaMatch';
|
||||||
|
import BitcoinBIP70TransactionDecode from './bip70/bip70';
|
||||||
const A = require('./analytics');
|
const A = require('./analytics');
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'development') {
|
if (process.env.NODE_ENV !== 'development') {
|
||||||
|
@ -21,11 +20,9 @@ if (process.env.NODE_ENV !== 'development') {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const bitcoin = require('bitcoinjs-lib');
|
|
||||||
const bitcoinModalString = 'Bitcoin address';
|
const bitcoinModalString = 'Bitcoin address';
|
||||||
const lightningModalString = 'Lightning Invoice';
|
const lightningModalString = 'Lightning Invoice';
|
||||||
const loc = require('./loc');
|
const loc = require('./loc');
|
||||||
/** @type {AppStorage} */
|
|
||||||
const BlueApp = require('./BlueApp');
|
const BlueApp = require('./BlueApp');
|
||||||
|
|
||||||
export default class App extends React.Component {
|
export default class App extends React.Component {
|
||||||
|
@ -62,7 +59,7 @@ export default class App extends React.Component {
|
||||||
} else {
|
} else {
|
||||||
const url = await Linking.getInitialURL();
|
const url = await Linking.getInitialURL();
|
||||||
if (url) {
|
if (url) {
|
||||||
if (this.hasSchema(url)) {
|
if (DeeplinkSchemaMatch.hasSchema(url)) {
|
||||||
this.handleOpenURL({ url });
|
this.handleOpenURL({ url });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -116,12 +113,23 @@ export default class App extends React.Component {
|
||||||
const isAddressFromStoredWallet = BlueApp.getWallets().some(wallet =>
|
const isAddressFromStoredWallet = BlueApp.getWallets().some(wallet =>
|
||||||
wallet.chain === Chain.ONCHAIN ? wallet.weOwnAddress(clipboard) : wallet.isInvoiceGeneratedByWallet(clipboard),
|
wallet.chain === Chain.ONCHAIN ? wallet.weOwnAddress(clipboard) : wallet.isInvoiceGeneratedByWallet(clipboard),
|
||||||
);
|
);
|
||||||
|
const isBitcoinAddress =
|
||||||
|
DeeplinkSchemaMatch.isBitcoinAddress(clipboard) || BitcoinBIP70TransactionDecode.matchesPaymentURL(clipboard);
|
||||||
|
const isLightningInvoice = DeeplinkSchemaMatch.isLightningInvoice(clipboard);
|
||||||
|
const isLNURL = DeeplinkSchemaMatch.isLnUrl(clipboard);
|
||||||
|
const isBothBitcoinAndLightning = DeeplinkSchemaMatch.isBothBitcoinAndLightning(clipboard);
|
||||||
if (
|
if (
|
||||||
(!isAddressFromStoredWallet &&
|
!isAddressFromStoredWallet &&
|
||||||
this.state.clipboardContent !== clipboard &&
|
this.state.clipboardContent !== clipboard &&
|
||||||
(this.isBitcoinAddress(clipboard) || this.isLightningInvoice(clipboard) || this.isLnUrl(clipboard))) ||
|
(isBitcoinAddress || isLightningInvoice || isLNURL || isBothBitcoinAndLightning)
|
||||||
this.isBothBitcoinAndLightning(clipboard)
|
|
||||||
) {
|
) {
|
||||||
|
if (isBitcoinAddress) {
|
||||||
|
this.setState({ clipboardContentModalAddressType: bitcoinModalString });
|
||||||
|
} else if (isLightningInvoice || isLNURL) {
|
||||||
|
this.setState({ clipboardContentModalAddressType: lightningModalString });
|
||||||
|
} else if (isBothBitcoinAndLightning) {
|
||||||
|
this.setState({ clipboardContentModalAddressType: bitcoinModalString });
|
||||||
|
}
|
||||||
this.setState({ isClipboardContentModalVisible: true });
|
this.setState({ isClipboardContentModalVisible: true });
|
||||||
}
|
}
|
||||||
this.setState({ clipboardContent: clipboard });
|
this.setState({ clipboardContent: clipboard });
|
||||||
|
@ -130,94 +138,6 @@ export default class App extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
hasSchema(schemaString) {
|
|
||||||
if (typeof schemaString !== 'string' || schemaString.length <= 0) return false;
|
|
||||||
const lowercaseString = schemaString.trim().toLowerCase();
|
|
||||||
return (
|
|
||||||
lowercaseString.startsWith('bitcoin:') ||
|
|
||||||
lowercaseString.startsWith('lightning:') ||
|
|
||||||
lowercaseString.startsWith('blue:') ||
|
|
||||||
lowercaseString.startsWith('bluewallet:') ||
|
|
||||||
lowercaseString.startsWith('lapp:')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
isBitcoinAddress(address) {
|
|
||||||
address = address
|
|
||||||
.replace('bitcoin:', '')
|
|
||||||
.replace('bitcoin=', '')
|
|
||||||
.split('?')[0];
|
|
||||||
let isValidBitcoinAddress = false;
|
|
||||||
try {
|
|
||||||
bitcoin.address.toOutputScript(address);
|
|
||||||
isValidBitcoinAddress = true;
|
|
||||||
this.setState({ clipboardContentModalAddressType: bitcoinModalString });
|
|
||||||
} catch (err) {
|
|
||||||
isValidBitcoinAddress = false;
|
|
||||||
}
|
|
||||||
return isValidBitcoinAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLightningInvoice(invoice) {
|
|
||||||
let isValidLightningInvoice = false;
|
|
||||||
if (invoice.toLowerCase().startsWith('lightning:lnb') || invoice.toLowerCase().startsWith('lnb')) {
|
|
||||||
this.setState({ clipboardContentModalAddressType: lightningModalString });
|
|
||||||
isValidLightningInvoice = true;
|
|
||||||
}
|
|
||||||
return isValidLightningInvoice;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLnUrl(text) {
|
|
||||||
if (text.toLowerCase().startsWith('lightning:lnurl') || text.toLowerCase().startsWith('lnurl')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
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 => {
|
isBothBitcoinAndLightningWalletSelect = wallet => {
|
||||||
const clipboardContent = this.state.clipboardContent;
|
const clipboardContent = this.state.clipboardContent;
|
||||||
if (wallet.chain === Chain.ONCHAIN) {
|
if (wallet.chain === Chain.ONCHAIN) {
|
||||||
|
@ -246,141 +166,7 @@ export default class App extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
handleOpenURL = event => {
|
handleOpenURL = event => {
|
||||||
if (event.url === null) {
|
DeeplinkSchemaMatch.navigationRouteFor(event, value => this.navigator && this.navigator.dispatch(NavigationActions.navigate(value)));
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof event.url !== 'string') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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({
|
|
||||||
routeName: 'SendDetails',
|
|
||||||
params: {
|
|
||||||
uri: event.url,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else if (this.isLightningInvoice(event.url)) {
|
|
||||||
this.navigator &&
|
|
||||||
this.navigator.dispatch(
|
|
||||||
NavigationActions.navigate({
|
|
||||||
routeName: 'ScanLndInvoice',
|
|
||||||
params: {
|
|
||||||
uri: event.url,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else if (this.isLnUrl(event.url)) {
|
|
||||||
this.navigator &&
|
|
||||||
this.navigator.dispatch(
|
|
||||||
NavigationActions.navigate({
|
|
||||||
routeName: 'LNDCreateInvoice',
|
|
||||||
params: {
|
|
||||||
uri: event.url,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else if (this.isSafelloRedirect(event)) {
|
|
||||||
let urlObject = url.parse(event.url, true) // eslint-disable-line
|
|
||||||
|
|
||||||
const safelloStateToken = urlObject.query['safello-state-token'];
|
|
||||||
|
|
||||||
this.navigator &&
|
|
||||||
this.navigator.dispatch(
|
|
||||||
NavigationActions.navigate({
|
|
||||||
routeName: 'BuyBitcoin',
|
|
||||||
params: {
|
|
||||||
uri: event.url,
|
|
||||||
safelloStateToken,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
let urlObject = url.parse(event.url, true); // eslint-disable-line
|
|
||||||
console.log('parsed', urlObject);
|
|
||||||
(async () => {
|
|
||||||
if (urlObject.protocol === 'bluewallet:' || urlObject.protocol === 'lapp:' || urlObject.protocol === 'blue:') {
|
|
||||||
switch (urlObject.host) {
|
|
||||||
case 'openlappbrowser':
|
|
||||||
console.log('opening LAPP', urlObject.query.url);
|
|
||||||
// searching for LN wallet:
|
|
||||||
let haveLnWallet = false;
|
|
||||||
for (let w of BlueApp.getWallets()) {
|
|
||||||
if (w.type === LightningCustodianWallet.type) {
|
|
||||||
haveLnWallet = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!haveLnWallet) {
|
|
||||||
// need to create one
|
|
||||||
let w = new LightningCustodianWallet();
|
|
||||||
w.setLabel(this.state.label || w.typeReadable);
|
|
||||||
|
|
||||||
try {
|
|
||||||
let lndhub = await AsyncStorage.getItem(AppStorage.LNDHUB);
|
|
||||||
if (lndhub) {
|
|
||||||
w.setBaseURI(lndhub);
|
|
||||||
w.init();
|
|
||||||
}
|
|
||||||
await w.createAccount();
|
|
||||||
await w.authorize();
|
|
||||||
} catch (Err) {
|
|
||||||
// giving up, not doing anything
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
BlueApp.wallets.push(w);
|
|
||||||
await BlueApp.saveToDisk();
|
|
||||||
}
|
|
||||||
|
|
||||||
// now, opening lapp browser and navigating it to URL.
|
|
||||||
// looking for a LN wallet:
|
|
||||||
let lnWallet;
|
|
||||||
for (let w of BlueApp.getWallets()) {
|
|
||||||
if (w.type === LightningCustodianWallet.type) {
|
|
||||||
lnWallet = w;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!lnWallet) {
|
|
||||||
// something went wrong
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.navigator &&
|
|
||||||
this.navigator.dispatch(
|
|
||||||
NavigationActions.navigate({
|
|
||||||
routeName: 'LappBrowser',
|
|
||||||
params: {
|
|
||||||
fromSecret: lnWallet.getSecret(),
|
|
||||||
fromWallet: lnWallet,
|
|
||||||
url: urlObject.query.url,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
renderClipboardContentModal = () => {
|
renderClipboardContentModal = () => {
|
||||||
|
|
|
@ -1381,7 +1381,7 @@ export class NewWalletPanel extends Component {
|
||||||
style={{
|
style={{
|
||||||
padding: 15,
|
padding: 15,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
minHeight: 164,
|
minHeight: Platform.OS === 'ios' ? 164 : 181,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}}
|
}}
|
||||||
|
@ -1837,7 +1837,9 @@ export class WalletsCarousel extends Component {
|
||||||
<NewWalletPanel
|
<NewWalletPanel
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (WalletsCarousel.handleClick) {
|
if (WalletsCarousel.handleClick) {
|
||||||
|
this.onPressedOut();
|
||||||
WalletsCarousel.handleClick(index);
|
WalletsCarousel.handleClick(index);
|
||||||
|
this.onPressedOut();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -1857,7 +1859,9 @@ export class WalletsCarousel extends Component {
|
||||||
onPressOut={item.getIsFailure() ? this.onPressedOut : null}
|
onPressOut={item.getIsFailure() ? this.onPressedOut : null}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (item.getIsFailure() && WalletsCarousel.handleClick) {
|
if (item.getIsFailure() && WalletsCarousel.handleClick) {
|
||||||
|
this.onPressedOut();
|
||||||
WalletsCarousel.handleClick(index);
|
WalletsCarousel.handleClick(index);
|
||||||
|
this.onPressedOut();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -1925,7 +1929,9 @@ export class WalletsCarousel extends Component {
|
||||||
onLongPress={WalletsCarousel.handleLongPress}
|
onLongPress={WalletsCarousel.handleLongPress}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (WalletsCarousel.handleClick) {
|
if (WalletsCarousel.handleClick) {
|
||||||
|
this.onPressedOut();
|
||||||
WalletsCarousel.handleClick(index);
|
WalletsCarousel.handleClick(index);
|
||||||
|
this.onPressedOut();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -2037,7 +2043,8 @@ export class BlueAddressInput extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
onChangeText: PropTypes.func,
|
onChangeText: PropTypes.func,
|
||||||
onBarScanned: PropTypes.func,
|
onBarScanned: PropTypes.func.isRequired,
|
||||||
|
launchedBy: PropTypes.string.isRequired,
|
||||||
address: PropTypes.string,
|
address: PropTypes.string,
|
||||||
placeholder: PropTypes.string,
|
placeholder: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
@ -2081,7 +2088,7 @@ export class BlueAddressInput extends Component {
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
disabled={this.props.isLoading}
|
disabled={this.props.isLoading}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
NavigationService.navigate('ScanQrAddress', { onBarScanned: this.props.onBarScanned });
|
NavigationService.navigate('ScanQrAddress', { onBarScanned: this.props.onBarScanned, launchedBy: this.props.launchedBy });
|
||||||
Keyboard.dismiss();
|
Keyboard.dismiss();
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
|
|
|
@ -60,6 +60,9 @@ const WalletsStackNavigator = createStackNavigator(
|
||||||
Wallets: {
|
Wallets: {
|
||||||
screen: WalletsList,
|
screen: WalletsList,
|
||||||
path: 'wallets',
|
path: 'wallets',
|
||||||
|
navigationOptions: {
|
||||||
|
header: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
WalletTransactions: {
|
WalletTransactions: {
|
||||||
screen: WalletTransactions,
|
screen: WalletTransactions,
|
||||||
|
@ -157,6 +160,7 @@ const WalletsStackNavigator = createStackNavigator(
|
||||||
|
|
||||||
const CreateTransactionStackNavigator = createStackNavigator({
|
const CreateTransactionStackNavigator = createStackNavigator({
|
||||||
SendDetails: {
|
SendDetails: {
|
||||||
|
routeName: 'SendDetails',
|
||||||
screen: sendDetails,
|
screen: sendDetails,
|
||||||
},
|
},
|
||||||
Confirm: {
|
Confirm: {
|
||||||
|
@ -206,6 +210,7 @@ const CreateWalletStackNavigator = createStackNavigator({
|
||||||
},
|
},
|
||||||
ImportWallet: {
|
ImportWallet: {
|
||||||
screen: ImportWallet,
|
screen: ImportWallet,
|
||||||
|
routeName: 'ImportWallet',
|
||||||
},
|
},
|
||||||
PleaseBackup: {
|
PleaseBackup: {
|
||||||
screen: PleaseBackup,
|
screen: PleaseBackup,
|
||||||
|
@ -290,6 +295,7 @@ const MainBottomTabs = createStackNavigator(
|
||||||
},
|
},
|
||||||
//
|
//
|
||||||
SendDetails: {
|
SendDetails: {
|
||||||
|
routeName: 'SendDetails',
|
||||||
screen: CreateTransactionStackNavigator,
|
screen: CreateTransactionStackNavigator,
|
||||||
navigationOptions: {
|
navigationOptions: {
|
||||||
header: null,
|
header: null,
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
<application
|
<application
|
||||||
android:name=".MainApplication"
|
android:name=".MainApplication"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
@ -34,6 +35,26 @@
|
||||||
<data android:scheme="lapp" />
|
<data android:scheme="lapp" />
|
||||||
<data android:scheme="blue" />
|
<data android:scheme="blue" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.intent.action.EDIT" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data
|
||||||
|
android:mimeType="application/octet-stream"
|
||||||
|
android:host="*"
|
||||||
|
android:pathPattern=".*\\.psbt"
|
||||||
|
/>
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.intent.action.EDIT" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data
|
||||||
|
android:mimeType="text/plain"
|
||||||
|
android:host="*"
|
||||||
|
android:pathPattern=".*\\.psbt"
|
||||||
|
/>
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||||
</application>
|
</application>
|
||||||
|
|
|
@ -8,6 +8,7 @@ const BlueElectrum = require('../BlueElectrum');
|
||||||
const HDNode = require('bip32');
|
const HDNode = require('bip32');
|
||||||
const coinSelectAccumulative = require('coinselect/accumulative');
|
const coinSelectAccumulative = require('coinselect/accumulative');
|
||||||
const coinSelectSplit = require('coinselect/split');
|
const coinSelectSplit = require('coinselect/split');
|
||||||
|
const reverse = require('buffer-reverse');
|
||||||
|
|
||||||
const { RNRandomBytes } = NativeModules;
|
const { RNRandomBytes } = NativeModules;
|
||||||
|
|
||||||
|
@ -719,7 +720,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||||
* @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case
|
* @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case
|
||||||
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}}
|
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}}
|
||||||
*/
|
*/
|
||||||
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false) {
|
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false, masterFingerprint) {
|
||||||
if (!changeAddress) throw new Error('No change address provided');
|
if (!changeAddress) throw new Error('No change address provided');
|
||||||
sequence = sequence || AbstractHDElectrumWallet.defaultRBFSequence;
|
sequence = sequence || AbstractHDElectrumWallet.defaultRBFSequence;
|
||||||
|
|
||||||
|
@ -756,7 +757,13 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||||
if (!input.address || !this._getWifForAddress(input.address)) throw new Error('Internal error: no address or WIF to sign input');
|
if (!input.address || !this._getWifForAddress(input.address)) throw new Error('Internal error: no address or WIF to sign input');
|
||||||
}
|
}
|
||||||
let pubkey = this._getPubkeyByAddress(input.address);
|
let pubkey = this._getPubkeyByAddress(input.address);
|
||||||
let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]);
|
let masterFingerprintBuffer;
|
||||||
|
if (masterFingerprint) {
|
||||||
|
const hexBuffer = Buffer.from(Number(masterFingerprint).toString(16), 'hex');
|
||||||
|
masterFingerprintBuffer = Buffer.from(reverse(hexBuffer));
|
||||||
|
} else {
|
||||||
|
masterFingerprintBuffer = Buffer.from([0x00, 0x00, 0x00, 0x00]);
|
||||||
|
}
|
||||||
// this is not correct fingerprint, as we dont know real fingerprint - we got zpub with 84/0, but fingerpting
|
// this is not correct fingerprint, as we dont know real fingerprint - we got zpub with 84/0, but fingerpting
|
||||||
// should be from root. basically, fingerprint should be provided from outside by user when importing zpub
|
// should be from root. basically, fingerprint should be provided from outside by user when importing zpub
|
||||||
let path = this._getDerivationPathByAddress(input.address);
|
let path = this._getDerivationPathByAddress(input.address);
|
||||||
|
@ -767,7 +774,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||||
sequence,
|
sequence,
|
||||||
bip32Derivation: [
|
bip32Derivation: [
|
||||||
{
|
{
|
||||||
masterFingerprint,
|
masterFingerprint: masterFingerprintBuffer,
|
||||||
path,
|
path,
|
||||||
pubkey,
|
pubkey,
|
||||||
},
|
},
|
||||||
|
@ -789,7 +796,15 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||||
|
|
||||||
let path = this._getDerivationPathByAddress(output.address);
|
let path = this._getDerivationPathByAddress(output.address);
|
||||||
let pubkey = this._getPubkeyByAddress(output.address);
|
let pubkey = this._getPubkeyByAddress(output.address);
|
||||||
let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]);
|
let masterFingerprintBuffer;
|
||||||
|
|
||||||
|
if (masterFingerprint) {
|
||||||
|
const hexBuffer = Buffer.from(Number(masterFingerprint).toString(16), 'hex')
|
||||||
|
masterFingerprintBuffer = Buffer.from(reverse(hexBuffer));
|
||||||
|
} else {
|
||||||
|
masterFingerprintBuffer = Buffer.from([0x00, 0x00, 0x00, 0x00]);
|
||||||
|
}
|
||||||
|
|
||||||
// this is not correct fingerprint, as we dont know realfingerprint - we got zpub with 84/0, but fingerpting
|
// this is not correct fingerprint, as we dont know realfingerprint - we got zpub with 84/0, but fingerpting
|
||||||
// should be from root. basically, fingerprint should be provided from outside by user when importing zpub
|
// should be from root. basically, fingerprint should be provided from outside by user when importing zpub
|
||||||
|
|
||||||
|
@ -801,7 +816,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||||
if (change) {
|
if (change) {
|
||||||
outputData['bip32Derivation'] = [
|
outputData['bip32Derivation'] = [
|
||||||
{
|
{
|
||||||
masterFingerprint,
|
masterFingerprint: masterFingerprintBuffer,
|
||||||
path,
|
path,
|
||||||
pubkey,
|
pubkey,
|
||||||
},
|
},
|
||||||
|
|
|
@ -305,7 +305,6 @@ export class AppStorage {
|
||||||
if (key.prepareForSerialization) key.prepareForSerialization();
|
if (key.prepareForSerialization) key.prepareForSerialization();
|
||||||
walletsToSave.push(JSON.stringify({ ...key, type: key.type }));
|
walletsToSave.push(JSON.stringify({ ...key, type: key.type }));
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = {
|
let data = {
|
||||||
wallets: walletsToSave,
|
wallets: walletsToSave,
|
||||||
tx_metadata: this.tx_metadata,
|
tx_metadata: this.tx_metadata,
|
||||||
|
|
221
class/deeplinkSchemaMatch.js
Normal file
221
class/deeplinkSchemaMatch.js
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
import { AppStorage, LightningCustodianWallet } from './';
|
||||||
|
import AsyncStorage from '@react-native-community/async-storage';
|
||||||
|
import BitcoinBIP70TransactionDecode from '../bip70/bip70';
|
||||||
|
const bitcoin = require('bitcoinjs-lib');
|
||||||
|
const BlueApp = require('../BlueApp');
|
||||||
|
class DeeplinkSchemaMatch {
|
||||||
|
static hasSchema(schemaString) {
|
||||||
|
if (typeof schemaString !== 'string' || schemaString.length <= 0) return false;
|
||||||
|
const lowercaseString = schemaString.trim().toLowerCase();
|
||||||
|
return (
|
||||||
|
lowercaseString.startsWith('bitcoin:') ||
|
||||||
|
lowercaseString.startsWith('lightning:') ||
|
||||||
|
lowercaseString.startsWith('blue:') ||
|
||||||
|
lowercaseString.startsWith('bluewallet:') ||
|
||||||
|
lowercaseString.startsWith('lapp:')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Examines the content of the event parameter.
|
||||||
|
* If the content is recognizable, create a dictionary with the respective
|
||||||
|
* navigation dictionary required by react-navigation
|
||||||
|
* @param {Object} event
|
||||||
|
* @param {void} completionHandler
|
||||||
|
*/
|
||||||
|
static navigationRouteFor(event, completionHandler) {
|
||||||
|
if (event.url === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof event.url !== 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let isBothBitcoinAndLightning;
|
||||||
|
try {
|
||||||
|
isBothBitcoinAndLightning = DeeplinkSchemaMatch.isBothBitcoinAndLightning(event.url);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
if (isBothBitcoinAndLightning) {
|
||||||
|
completionHandler({
|
||||||
|
routeName: 'HandleOffchainAndOnChain',
|
||||||
|
params: {
|
||||||
|
onWalletSelect: this.isBothBitcoinAndLightningWalletSelect,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (DeeplinkSchemaMatch.isBitcoinAddress(event.url) || BitcoinBIP70TransactionDecode.matchesPaymentURL(event.url)) {
|
||||||
|
completionHandler({
|
||||||
|
routeName: 'SendDetails',
|
||||||
|
params: {
|
||||||
|
uri: event.url,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (DeeplinkSchemaMatch.isLightningInvoice(event.url)) {
|
||||||
|
completionHandler({
|
||||||
|
routeName: 'ScanLndInvoice',
|
||||||
|
params: {
|
||||||
|
uri: event.url,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (DeeplinkSchemaMatch.isLnUrl(event.url)) {
|
||||||
|
completionHandler({
|
||||||
|
routeName: 'LNDCreateInvoice',
|
||||||
|
params: {
|
||||||
|
uri: event.url,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (DeeplinkSchemaMatch.isSafelloRedirect(event)) {
|
||||||
|
let urlObject = url.parse(event.url, true) // eslint-disable-line
|
||||||
|
|
||||||
|
const safelloStateToken = urlObject.query['safello-state-token'];
|
||||||
|
|
||||||
|
completionHandler({
|
||||||
|
routeName: 'BuyBitcoin',
|
||||||
|
params: {
|
||||||
|
uri: event.url,
|
||||||
|
safelloStateToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let urlObject = url.parse(event.url, true); // eslint-disable-line
|
||||||
|
console.log('parsed', urlObject);
|
||||||
|
(async () => {
|
||||||
|
if (urlObject.protocol === 'bluewallet:' || urlObject.protocol === 'lapp:' || urlObject.protocol === 'blue:') {
|
||||||
|
switch (urlObject.host) {
|
||||||
|
case 'openlappbrowser':
|
||||||
|
console.log('opening LAPP', urlObject.query.url);
|
||||||
|
// searching for LN wallet:
|
||||||
|
let haveLnWallet = false;
|
||||||
|
for (let w of BlueApp.getWallets()) {
|
||||||
|
if (w.type === LightningCustodianWallet.type) {
|
||||||
|
haveLnWallet = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!haveLnWallet) {
|
||||||
|
// need to create one
|
||||||
|
let w = new LightningCustodianWallet();
|
||||||
|
w.setLabel(this.state.label || w.typeReadable);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let lndhub = await AsyncStorage.getItem(AppStorage.LNDHUB);
|
||||||
|
if (lndhub) {
|
||||||
|
w.setBaseURI(lndhub);
|
||||||
|
w.init();
|
||||||
|
}
|
||||||
|
await w.createAccount();
|
||||||
|
await w.authorize();
|
||||||
|
} catch (Err) {
|
||||||
|
// giving up, not doing anything
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BlueApp.wallets.push(w);
|
||||||
|
await BlueApp.saveToDisk();
|
||||||
|
}
|
||||||
|
|
||||||
|
// now, opening lapp browser and navigating it to URL.
|
||||||
|
// looking for a LN wallet:
|
||||||
|
let lnWallet;
|
||||||
|
for (let w of BlueApp.getWallets()) {
|
||||||
|
if (w.type === LightningCustodianWallet.type) {
|
||||||
|
lnWallet = w;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lnWallet) {
|
||||||
|
// something went wrong
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.navigator &&
|
||||||
|
this.navigator.dispatch(
|
||||||
|
completionHandler({
|
||||||
|
routeName: 'LappBrowser',
|
||||||
|
params: {
|
||||||
|
fromSecret: lnWallet.getSecret(),
|
||||||
|
fromWallet: lnWallet,
|
||||||
|
url: urlObject.query.url,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static isBitcoinAddress(address) {
|
||||||
|
address = address
|
||||||
|
.replace('bitcoin:', '')
|
||||||
|
.replace('bitcoin=', '')
|
||||||
|
.split('?')[0];
|
||||||
|
let isValidBitcoinAddress = false;
|
||||||
|
try {
|
||||||
|
bitcoin.address.toOutputScript(address);
|
||||||
|
isValidBitcoinAddress = true;
|
||||||
|
} catch (err) {
|
||||||
|
isValidBitcoinAddress = false;
|
||||||
|
}
|
||||||
|
return isValidBitcoinAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
static isLightningInvoice(invoice) {
|
||||||
|
let isValidLightningInvoice = false;
|
||||||
|
if (invoice.toLowerCase().startsWith('lightning:lnb') || invoice.toLowerCase().startsWith('lnb')) {
|
||||||
|
isValidLightningInvoice = true;
|
||||||
|
}
|
||||||
|
return isValidLightningInvoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
static isLnUrl(text) {
|
||||||
|
if (text.toLowerCase().startsWith('lightning:lnurl') || text.toLowerCase().startsWith('lnurl')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static isSafelloRedirect(event) {
|
||||||
|
let urlObject = url.parse(event.url, true) // eslint-disable-line
|
||||||
|
|
||||||
|
return !!urlObject.query['safello-state-token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
static 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 (!DeeplinkSchemaMatch.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) {
|
||||||
|
return { bitcoin, lndInvoice };
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DeeplinkSchemaMatch;
|
|
@ -18,24 +18,34 @@ const BlueApp = require('../BlueApp');
|
||||||
const loc = require('../loc');
|
const loc = require('../loc');
|
||||||
|
|
||||||
export default class WalletImport {
|
export default class WalletImport {
|
||||||
static async _saveWallet(w) {
|
static async _saveWallet(w, additionalProperties) {
|
||||||
try {
|
try {
|
||||||
const wallet = BlueApp.getWallets().some(wallet => wallet.getSecret() === w.secret && wallet.type !== PlaceholderWallet.type);
|
const wallet = BlueApp.getWallets().some(wallet => wallet.getSecret() === w.secret && wallet.type !== PlaceholderWallet.type);
|
||||||
if (wallet) {
|
if (wallet) {
|
||||||
alert('This wallet has been previously imported.');
|
alert('This wallet has been previously imported.');
|
||||||
WalletImport.removePlaceholderWallet();
|
WalletImport.removePlaceholderWallet();
|
||||||
} else {
|
} else {
|
||||||
alert(loc.wallets.import.success);
|
|
||||||
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
|
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
|
||||||
w.setLabel(loc.wallets.import.imported + ' ' + w.typeReadable);
|
w.setLabel(loc.wallets.import.imported + ' ' + w.typeReadable);
|
||||||
w.setUserHasSavedExport(true);
|
w.setUserHasSavedExport(true);
|
||||||
|
if (additionalProperties) {
|
||||||
|
for (const [key, value] of Object.entries(additionalProperties)) {
|
||||||
|
w[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
WalletImport.removePlaceholderWallet();
|
WalletImport.removePlaceholderWallet();
|
||||||
BlueApp.wallets.push(w);
|
BlueApp.wallets.push(w);
|
||||||
await BlueApp.saveToDisk();
|
await BlueApp.saveToDisk();
|
||||||
A(A.ENUM.CREATED_WALLET);
|
A(A.ENUM.CREATED_WALLET);
|
||||||
|
alert(loc.wallets.import.success);
|
||||||
}
|
}
|
||||||
EV(EV.enum.WALLETS_COUNT_CHANGED);
|
EV(EV.enum.WALLETS_COUNT_CHANGED);
|
||||||
} catch (_e) {}
|
} catch (e) {
|
||||||
|
alert(e);
|
||||||
|
console.log(e);
|
||||||
|
WalletImport.removePlaceholderWallet();
|
||||||
|
EV(EV.enum.WALLETS_COUNT_CHANGED);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static removePlaceholderWallet() {
|
static removePlaceholderWallet() {
|
||||||
|
@ -58,7 +68,7 @@ export default class WalletImport {
|
||||||
return BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type);
|
return BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async processImportText(importText) {
|
static async processImportText(importText, additionalProperties) {
|
||||||
if (WalletImport.isCurrentlyImportingWallet()) {
|
if (WalletImport.isCurrentlyImportingWallet()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -209,7 +219,7 @@ export default class WalletImport {
|
||||||
if (watchOnly.valid()) {
|
if (watchOnly.valid()) {
|
||||||
await watchOnly.fetchTransactions();
|
await watchOnly.fetchTransactions();
|
||||||
await watchOnly.fetchBalance();
|
await watchOnly.fetchBalance();
|
||||||
return WalletImport._saveWallet(watchOnly);
|
return WalletImport._saveWallet(watchOnly, additionalProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
// nope?
|
// nope?
|
||||||
|
|
|
@ -11,6 +11,7 @@ export class WatchOnlyWallet extends LegacyWallet {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.use_with_hardware_wallet = false;
|
this.use_with_hardware_wallet = false;
|
||||||
|
this.masterFingerprint = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
allowSend() {
|
allowSend() {
|
||||||
|
@ -146,7 +147,7 @@ export class WatchOnlyWallet extends LegacyWallet {
|
||||||
*/
|
*/
|
||||||
createTransaction(utxos, targets, feeRate, changeAddress, sequence) {
|
createTransaction(utxos, targets, feeRate, changeAddress, sequence) {
|
||||||
if (this._hdWalletInstance instanceof HDSegwitBech32Wallet) {
|
if (this._hdWalletInstance instanceof HDSegwitBech32Wallet) {
|
||||||
return this._hdWalletInstance.createTransaction(utxos, targets, feeRate, changeAddress, sequence, true);
|
return this._hdWalletInstance.createTransaction(utxos, targets, feeRate, changeAddress, sequence, true, this.masterFingerprint);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Not a zpub watch-only wallet, cant create PSBT (or just not initialized)');
|
throw new Error('Not a zpub watch-only wallet, cant create PSBT (or just not initialized)');
|
||||||
}
|
}
|
||||||
|
|
|
@ -164,6 +164,7 @@
|
||||||
3271B0BA236E329400DA766F /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = "<group>"; };
|
3271B0BA236E329400DA766F /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = "<group>"; };
|
||||||
32B5A3282334450100F8D608 /* BlueWallet-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BlueWallet-Bridging-Header.h"; sourceTree = "<group>"; };
|
32B5A3282334450100F8D608 /* BlueWallet-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BlueWallet-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
32B5A3292334450100F8D608 /* Bridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bridge.swift; sourceTree = "<group>"; };
|
32B5A3292334450100F8D608 /* Bridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bridge.swift; sourceTree = "<group>"; };
|
||||||
|
32C7944323B8879D00BE2AFA /* BlueWalletRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = BlueWalletRelease.entitlements; path = BlueWallet/BlueWalletRelease.entitlements; sourceTree = "<group>"; };
|
||||||
32F0A24F2310B0700095C559 /* BlueWalletWatch Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "BlueWalletWatch Extension.entitlements"; sourceTree = "<group>"; };
|
32F0A24F2310B0700095C559 /* BlueWalletWatch Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "BlueWalletWatch Extension.entitlements"; sourceTree = "<group>"; };
|
||||||
32F0A2502310B0910095C559 /* BlueWallet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = BlueWallet.entitlements; path = BlueWallet/BlueWallet.entitlements; sourceTree = "<group>"; };
|
32F0A2502310B0910095C559 /* BlueWallet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = BlueWallet.entitlements; path = BlueWallet/BlueWallet.entitlements; sourceTree = "<group>"; };
|
||||||
32F0A2992311DBB20095C559 /* ComplicationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComplicationController.swift; sourceTree = "<group>"; };
|
32F0A2992311DBB20095C559 /* ComplicationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComplicationController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -332,6 +333,7 @@
|
||||||
13B07FAE1A68108700A75B9A /* BlueWallet */ = {
|
13B07FAE1A68108700A75B9A /* BlueWallet */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
32C7944323B8879D00BE2AFA /* BlueWalletRelease.entitlements */,
|
||||||
32F0A2502310B0910095C559 /* BlueWallet.entitlements */,
|
32F0A2502310B0910095C559 /* BlueWallet.entitlements */,
|
||||||
008F07F21AC5B25A0029DE68 /* main.jsbundle */,
|
008F07F21AC5B25A0029DE68 /* main.jsbundle */,
|
||||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
|
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
|
||||||
|
@ -1266,7 +1268,7 @@
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = BlueWallet/BlueWallet.entitlements;
|
CODE_SIGN_ENTITLEMENTS = BlueWallet/BlueWalletRelease.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
|
18
ios/BlueWallet/BlueWalletRelease.entitlements
Normal file
18
ios/BlueWallet/BlueWalletRelease.entitlements
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||||
|
<array/>
|
||||||
|
<key>com.apple.developer.icloud-services</key>
|
||||||
|
<array>
|
||||||
|
<string>CloudDocuments</string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.developer.ubiquity-container-identifiers</key>
|
||||||
|
<array/>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.io.bluewallet.bluewallet</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -2,12 +2,29 @@
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIUserInterfaceStyle</key>
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
<string>Light</string>
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>en</string>
|
<string>en</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>BlueWallet</string>
|
<string>BlueWallet</string>
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeIconFiles</key>
|
||||||
|
<array/>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>PSBT</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Owner</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>io.bluewallet.psbt</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
|
@ -58,18 +75,18 @@
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSAppleMusicUsageDescription</key>
|
<key>NSAppleMusicUsageDescription</key>
|
||||||
<string>This alert should not show up as we do not require this data</string>
|
<string>This alert should not show up as we do not require this data</string>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
|
||||||
<string>In order to confirm your identity, we need your permission to use FaceID.</string>
|
|
||||||
<key>NSBluetoothPeripheralUsageDescription</key>
|
<key>NSBluetoothPeripheralUsageDescription</key>
|
||||||
<string>This alert should not show up as we do not require this data</string>
|
<string>This alert should not show up as we do not require this data</string>
|
||||||
<key>NSCalendarsUsageDescription</key>
|
<key>NSCalendarsUsageDescription</key>
|
||||||
<string>This alert should not show up as we do not require this data</string>
|
<string>This alert should not show up as we do not require this data</string>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>In order to quickly scan the recipient's address, we need your permission to use the camera to scan their QR Code.</string>
|
<string>In order to quickly scan the recipient's address, we need your permission to use the camera to scan their QR Code.</string>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
<string>This alert should not show up as we do not require this data</string>
|
<string>In order to confirm your identity, we need your permission to use FaceID.</string>
|
||||||
<key>NSLocationAlwaysUsageDescription</key>
|
<key>NSLocationAlwaysUsageDescription</key>
|
||||||
<string>This alert should not show up as we do not require this data</string>
|
<string>This alert should not show up as we do not require this data</string>
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string>This alert should not show up as we do not require this data</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>This alert should not show up as we do not require this data</string>
|
<string>This alert should not show up as we do not require this data</string>
|
||||||
<key>NSMotionUsageDescription</key>
|
<key>NSMotionUsageDescription</key>
|
||||||
|
@ -116,7 +133,53 @@
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UIUserInterfaceStyle</key>
|
||||||
|
<string>Light</string>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<false/>
|
<false/>
|
||||||
|
<key>UTExportedTypeDeclarations</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UTTypeConformsTo</key>
|
||||||
|
<array>
|
||||||
|
<string>public.data</string>
|
||||||
|
</array>
|
||||||
|
<key>UTTypeDescription</key>
|
||||||
|
<string>PSBT</string>
|
||||||
|
<key>UTTypeIconFiles</key>
|
||||||
|
<array/>
|
||||||
|
<key>UTTypeIdentifier</key>
|
||||||
|
<string>io.bluewallet.psbt</string>
|
||||||
|
<key>UTTypeTagSpecification</key>
|
||||||
|
<dict>
|
||||||
|
<key>public.filename-extension</key>
|
||||||
|
<array>
|
||||||
|
<string>psbt</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>UTImportedTypeDeclarations</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UTTypeConformsTo</key>
|
||||||
|
<array>
|
||||||
|
<string>public.data</string>
|
||||||
|
</array>
|
||||||
|
<key>UTTypeDescription</key>
|
||||||
|
<string>PSBT</string>
|
||||||
|
<key>UTTypeIconFiles</key>
|
||||||
|
<array/>
|
||||||
|
<key>UTTypeIdentifier</key>
|
||||||
|
<string>io.bluewallet.psbt</string>
|
||||||
|
<key>UTTypeTagSpecification</key>
|
||||||
|
<dict>
|
||||||
|
<key>public.filename-extension</key>
|
||||||
|
<array>
|
||||||
|
<string>psbt</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -93,6 +93,8 @@
|
||||||
"react-native-camera": "3.4.0",
|
"react-native-camera": "3.4.0",
|
||||||
"react-native-default-preference": "1.4.1",
|
"react-native-default-preference": "1.4.1",
|
||||||
"react-native-device-info": "4.0.1",
|
"react-native-device-info": "4.0.1",
|
||||||
|
"react-native-directory-picker": "git+https://github.com/BlueWallet/react-native-directory-picker.git#63307e646f72444ab83b619e579c55ee38cd162a",
|
||||||
|
"react-native-document-picker": "git+https://github.com/BlueWallet/react-native-document-picker.git#9ce83792db340d01b1361d24b19613658abef4aa",
|
||||||
"react-native-elements": "0.19.0",
|
"react-native-elements": "0.19.0",
|
||||||
"react-native-flexi-radio-button": "0.2.2",
|
"react-native-flexi-radio-button": "0.2.2",
|
||||||
"react-native-fs": "2.13.3",
|
"react-native-fs": "2.13.3",
|
||||||
|
@ -116,6 +118,7 @@
|
||||||
"react-native-snap-carousel": "3.8.4",
|
"react-native-snap-carousel": "3.8.4",
|
||||||
"react-native-sortable-list": "0.0.23",
|
"react-native-sortable-list": "0.0.23",
|
||||||
"react-native-svg": "9.5.1",
|
"react-native-svg": "9.5.1",
|
||||||
|
"react-native-swiper": "git+https://github.com/BlueWallet/react-native-swiper.git#1.5.14",
|
||||||
"react-native-tcp": "git+https://github.com/aprock/react-native-tcp.git",
|
"react-native-tcp": "git+https://github.com/aprock/react-native-tcp.git",
|
||||||
"react-native-tooltip": "git+https://github.com/marcosrdz/react-native-tooltip.git",
|
"react-native-tooltip": "git+https://github.com/marcosrdz/react-native-tooltip.git",
|
||||||
"react-native-vector-icons": "6.6.0",
|
"react-native-vector-icons": "6.6.0",
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
BlueNavigationStyle,
|
BlueNavigationStyle,
|
||||||
BlueAddressInput,
|
BlueAddressInput,
|
||||||
BlueBitcoinAmount,
|
BlueBitcoinAmount,
|
||||||
|
BlueLoading,
|
||||||
} from '../../BlueComponents';
|
} from '../../BlueComponents';
|
||||||
import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet';
|
import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet';
|
||||||
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
||||||
|
@ -45,7 +46,8 @@ export default class ScanLndInvoice extends React.Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow);
|
||||||
|
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
|
||||||
if (!BlueApp.getWallets().some(item => item.type === LightningCustodianWallet.type)) {
|
if (!BlueApp.getWallets().some(item => item.type === LightningCustodianWallet.type)) {
|
||||||
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
|
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
|
||||||
alert('Before paying a Lightning invoice, you must first add a Lightning wallet.');
|
alert('Before paying a Lightning invoice, you must first add a Lightning wallet.');
|
||||||
|
@ -78,9 +80,7 @@ export default class ScanLndInvoice extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
componentDidMount() {
|
||||||
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow);
|
|
||||||
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
|
|
||||||
if (this.props.navigation.state.params.uri) {
|
if (this.props.navigation.state.params.uri) {
|
||||||
this.processTextForInvoice(this.props.navigation.getParam('uri'));
|
this.processTextForInvoice(this.props.navigation.getParam('uri'));
|
||||||
}
|
}
|
||||||
|
@ -265,6 +265,9 @@ export default class ScanLndInvoice extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (!this.state.fromWallet) {
|
||||||
|
return <BlueLoading />;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
|
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
|
||||||
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1 }}>
|
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1 }}>
|
||||||
|
@ -300,6 +303,7 @@ export default class ScanLndInvoice extends React.Component {
|
||||||
isLoading={this.state.isLoading}
|
isLoading={this.state.isLoading}
|
||||||
placeholder={loc.lnd.placeholder}
|
placeholder={loc.lnd.placeholder}
|
||||||
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
|
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
|
||||||
|
launchedBy={this.props.navigation.state.routeName}
|
||||||
/>
|
/>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
|
@ -353,6 +357,7 @@ ScanLndInvoice.propTypes = {
|
||||||
getParam: PropTypes.func,
|
getParam: PropTypes.func,
|
||||||
dismiss: PropTypes.func,
|
dismiss: PropTypes.func,
|
||||||
state: PropTypes.shape({
|
state: PropTypes.shape({
|
||||||
|
routeName: PropTypes.string,
|
||||||
params: PropTypes.shape({
|
params: PropTypes.shape({
|
||||||
uri: PropTypes.string,
|
uri: PropTypes.string,
|
||||||
fromSecret: PropTypes.string,
|
fromSecret: PropTypes.string,
|
||||||
|
|
|
@ -38,6 +38,8 @@ import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
||||||
import { HDLegacyP2PKHWallet, HDSegwitBech32Wallet, HDSegwitP2SHWallet, LightningCustodianWallet, WatchOnlyWallet } from '../../class';
|
import { HDLegacyP2PKHWallet, HDSegwitBech32Wallet, HDSegwitP2SHWallet, LightningCustodianWallet, WatchOnlyWallet } from '../../class';
|
||||||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||||
import { BitcoinTransaction } from '../../models/bitcoinTransactionInfo';
|
import { BitcoinTransaction } from '../../models/bitcoinTransactionInfo';
|
||||||
|
import DocumentPicker from 'react-native-document-picker';
|
||||||
|
import RNFS from 'react-native-fs';
|
||||||
const bitcoin = require('bitcoinjs-lib');
|
const bitcoin = require('bitcoinjs-lib');
|
||||||
const bip21 = require('bip21');
|
const bip21 = require('bip21');
|
||||||
let BigNumber = require('bignumber.js');
|
let BigNumber = require('bignumber.js');
|
||||||
|
@ -58,9 +60,14 @@ export default class SendDetails extends Component {
|
||||||
title: loc.send.header,
|
title: loc.send.header,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
state = { isLoading: true };
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow);
|
||||||
|
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
|
||||||
|
|
||||||
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;
|
||||||
|
@ -177,9 +184,6 @@ export default class SendDetails extends Component {
|
||||||
this.renderNavigationHeader();
|
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.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
|
|
||||||
|
|
||||||
let addresses = [];
|
let addresses = [];
|
||||||
let initialMemo = '';
|
let initialMemo = '';
|
||||||
if (this.props.navigation.state.params.uri) {
|
if (this.props.navigation.state.params.uri) {
|
||||||
|
@ -705,6 +709,36 @@ export default class SendDetails extends Component {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
importTransaction = async () => {
|
||||||
|
try {
|
||||||
|
const res = await DocumentPicker.pick();
|
||||||
|
const file = await RNFS.readFile(res.uri, 'ascii');
|
||||||
|
const bufferDecoded = Buffer.from(file, 'ascii').toString('base64');
|
||||||
|
if (bufferDecoded) {
|
||||||
|
if (this.state.fromWallet.type === WatchOnlyWallet.type) {
|
||||||
|
// watch-only wallets with enabled HW wallet support have different flow. we have to show PSBT to user as QR code
|
||||||
|
// so he can scan it and sign it. then we have to scan it back from user (via camera and QR code), and ask
|
||||||
|
// user whether he wants to broadcast it
|
||||||
|
this.props.navigation.navigate('PsbtWithHardwareWallet', {
|
||||||
|
memo: this.state.memo,
|
||||||
|
fromWallet: this.state.fromWallet,
|
||||||
|
psbt: bufferDecoded,
|
||||||
|
isFirstPSBTAlreadyBase64: true,
|
||||||
|
});
|
||||||
|
this.setState({ isLoading: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!DocumentPicker.isCancel(err)) {
|
||||||
|
alert('The selected file does not contain a signed transaction that can be imported.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setState({ isAdvancedTransactionOptionsVisible: false });
|
||||||
|
};
|
||||||
|
|
||||||
renderAdvancedTransactionOptionsModal = () => {
|
renderAdvancedTransactionOptionsModal = () => {
|
||||||
const isSendMaxUsed = this.state.addresses.some(element => element.amount === BitcoinUnit.MAX);
|
const isSendMaxUsed = this.state.addresses.some(element => element.amount === BitcoinUnit.MAX);
|
||||||
return (
|
return (
|
||||||
|
@ -736,6 +770,9 @@ export default class SendDetails extends Component {
|
||||||
onSwitch={this.onReplaceableFeeSwitchValueChanged}
|
onSwitch={this.onReplaceableFeeSwitchValueChanged}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{this.state.fromWallet.use_with_hardware_wallet && (
|
||||||
|
<BlueListItem title="Import Transaction" hideChevron component={TouchableOpacity} onPress={this.importTransaction} />
|
||||||
|
)}
|
||||||
{this.state.fromWallet.allowBatchSend() && (
|
{this.state.fromWallet.allowBatchSend() && (
|
||||||
<>
|
<>
|
||||||
<BlueListItem
|
<BlueListItem
|
||||||
|
@ -892,6 +929,7 @@ export default class SendDetails extends Component {
|
||||||
address={item.address}
|
address={item.address}
|
||||||
isLoading={this.state.isLoading}
|
isLoading={this.state.isLoading}
|
||||||
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
|
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
|
||||||
|
launchedBy={this.props.navigation.state.routeName}
|
||||||
/>
|
/>
|
||||||
{this.state.addresses.length > 1 && (
|
{this.state.addresses.length > 1 && (
|
||||||
<BlueText style={{ alignSelf: 'flex-end', marginRight: 18, marginVertical: 8 }}>
|
<BlueText style={{ alignSelf: 'flex-end', marginRight: 18, marginVertical: 8 }}>
|
||||||
|
@ -1067,6 +1105,7 @@ SendDetails.propTypes = {
|
||||||
getParam: PropTypes.func,
|
getParam: PropTypes.func,
|
||||||
setParams: PropTypes.func,
|
setParams: PropTypes.func,
|
||||||
state: PropTypes.shape({
|
state: PropTypes.shape({
|
||||||
|
routeName: PropTypes.string,
|
||||||
params: PropTypes.shape({
|
params: PropTypes.shape({
|
||||||
amount: PropTypes.number,
|
amount: PropTypes.number,
|
||||||
address: PropTypes.string,
|
address: PropTypes.string,
|
||||||
|
|
|
@ -1,6 +1,17 @@
|
||||||
/* global alert */
|
/* global alert */
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { ActivityIndicator, TouchableOpacity, View, Dimensions, Image, TextInput, Clipboard, Linking } from 'react-native';
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
TouchableOpacity,
|
||||||
|
ScrollView,
|
||||||
|
View,
|
||||||
|
Dimensions,
|
||||||
|
Image,
|
||||||
|
TextInput,
|
||||||
|
Clipboard,
|
||||||
|
Linking,
|
||||||
|
Platform,
|
||||||
|
} from 'react-native';
|
||||||
import QRCode from 'react-native-qrcode-svg';
|
import QRCode from 'react-native-qrcode-svg';
|
||||||
import { Icon, Text } from 'react-native-elements';
|
import { Icon, Text } from 'react-native-elements';
|
||||||
import {
|
import {
|
||||||
|
@ -13,8 +24,12 @@ import {
|
||||||
BlueCopyToClipboardButton,
|
BlueCopyToClipboardButton,
|
||||||
} from '../../BlueComponents';
|
} from '../../BlueComponents';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import Share from 'react-native-share';
|
||||||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||||
import { RNCamera } from 'react-native-camera';
|
import { RNCamera } from 'react-native-camera';
|
||||||
|
import RNFS from 'react-native-fs';
|
||||||
|
import DocumentPicker from 'react-native-document-picker';
|
||||||
|
import DirectoryPickerManager from 'react-native-directory-picker';
|
||||||
let loc = require('../../loc');
|
let loc = require('../../loc');
|
||||||
let EV = require('../../events');
|
let EV = require('../../events');
|
||||||
let BlueElectrum = require('../../BlueElectrum');
|
let BlueElectrum = require('../../BlueElectrum');
|
||||||
|
@ -36,7 +51,10 @@ export default class PsbtWithHardwareWallet extends Component {
|
||||||
this.setState({ renderScanner: false }, () => {
|
this.setState({ renderScanner: false }, () => {
|
||||||
console.log(ret.data);
|
console.log(ret.data);
|
||||||
try {
|
try {
|
||||||
let Tx = this.state.fromWallet.combinePsbt(this.state.psbt.toBase64(), ret.data);
|
let Tx = this.state.fromWallet.combinePsbt(
|
||||||
|
this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64(),
|
||||||
|
this.state.isSecondPSBTAlreadyBase64 ? ret.data : ret.data.toBase64(),
|
||||||
|
);
|
||||||
this.setState({ txhex: Tx.toHex() });
|
this.setState({ txhex: Tx.toHex() });
|
||||||
} catch (Err) {
|
} catch (Err) {
|
||||||
alert(Err);
|
alert(Err);
|
||||||
|
@ -46,18 +64,19 @@ export default class PsbtWithHardwareWallet extends Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
renderScanner: false,
|
renderScanner: false,
|
||||||
qrCodeHeight: height > width ? width - 40 : width / 2,
|
qrCodeHeight: height > width ? width - 40 : width / 3,
|
||||||
memo: props.navigation.getParam('memo'),
|
memo: props.navigation.getParam('memo'),
|
||||||
psbt: props.navigation.getParam('psbt'),
|
psbt: props.navigation.getParam('psbt'),
|
||||||
fromWallet: props.navigation.getParam('fromWallet'),
|
fromWallet: props.navigation.getParam('fromWallet'),
|
||||||
|
isFirstPSBTAlreadyBase64: props.navigation.getParam('isFirstPSBTAlreadyBase64'),
|
||||||
|
isSecondPSBTAlreadyBase64: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
componentDidMount() {
|
||||||
console.log('send/psbtWithHardwareWallet - componentDidMount');
|
console.log('send/psbtWithHardwareWallet - componentDidMount');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,6 +204,60 @@ export default class PsbtWithHardwareWallet extends Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exportPSBT = async () => {
|
||||||
|
const fileName = `${Date.now()}.psbt`;
|
||||||
|
if (Platform.OS === 'ios') {
|
||||||
|
const filePath = RNFS.TemporaryDirectoryPath + `/${fileName}`;
|
||||||
|
await RNFS.writeFile(filePath, this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64(), 'ascii');
|
||||||
|
Share.open({
|
||||||
|
url: 'file://' + filePath,
|
||||||
|
})
|
||||||
|
.catch(error => console.log(error))
|
||||||
|
.finally(() => {
|
||||||
|
RNFS.unlink(filePath);
|
||||||
|
});
|
||||||
|
} else if (Platform.OS === 'android') {
|
||||||
|
DirectoryPickerManager.showDirectoryPicker(null, async response => {
|
||||||
|
if (response.didCancel) {
|
||||||
|
console.log('User cancelled directory picker');
|
||||||
|
} else if (response.error) {
|
||||||
|
console.log('DirectoryPickerManager Error: ', response.error);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await RNFS.writeFile(
|
||||||
|
response.path + `/${fileName}`,
|
||||||
|
this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64(),
|
||||||
|
'ascii',
|
||||||
|
);
|
||||||
|
alert('Successfully exported.');
|
||||||
|
RNFS.unlink(response.path + `/${fileName}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
alert(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
openSignedTransaction = async () => {
|
||||||
|
try {
|
||||||
|
const res = await DocumentPicker.pick();
|
||||||
|
const file = await RNFS.readFile(res.uri, 'ascii');
|
||||||
|
const bufferDecoded = Buffer.from(file, 'ascii').toString('base64');
|
||||||
|
if (bufferDecoded) {
|
||||||
|
this.setState({ isSecondPSBTAlreadyBase64: true }, () => this.onBarCodeRead({ data: bufferDecoded }));
|
||||||
|
} else {
|
||||||
|
this.setState({ isSecondPSBTAlreadyBase64: false });
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!DocumentPicker.isCancel(err)) {
|
||||||
|
alert('The selected file does not contain a signed transaction that can be imported.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.isLoading) {
|
if (this.state.isLoading) {
|
||||||
return (
|
return (
|
||||||
|
@ -200,27 +273,58 @@ export default class PsbtWithHardwareWallet extends Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeBlueArea style={{ flex: 1 }}>
|
<SafeBlueArea style={{ flex: 1 }}>
|
||||||
<View style={{ alignItems: 'center', justifyContent: 'space-between' }}>
|
<ScrollView centerContent contentContainerStyle={{ flexGrow: 1, justifyContent: 'space-between' }}>
|
||||||
<View style={{ flexDirection: 'row', justifyContent: 'center', paddingTop: 16, paddingBottom: 16 }}>
|
<View style={{ flexDirection: 'row', justifyContent: 'center', paddingTop: 16, paddingBottom: 16 }}>
|
||||||
<BlueCard>
|
<BlueCard>
|
||||||
<BlueText>This is partially signed bitcoin transaction (PSBT). Please finish signing it with your hardware wallet.</BlueText>
|
<BlueText>This is partially signed bitcoin transaction (PSBT). Please finish signing it with your hardware wallet.</BlueText>
|
||||||
<BlueSpacing20 />
|
<BlueSpacing20 />
|
||||||
<QRCode
|
<QRCode
|
||||||
value={this.state.psbt.toBase64()}
|
value={this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64()}
|
||||||
size={this.state.qrCodeHeight}
|
size={this.state.qrCodeHeight}
|
||||||
color={BlueApp.settings.foregroundColor}
|
color={BlueApp.settings.foregroundColor}
|
||||||
logoBackgroundColor={BlueApp.settings.brandingColor}
|
logoBackgroundColor={BlueApp.settings.brandingColor}
|
||||||
ecl={'L'}
|
ecl={'L'}
|
||||||
/>
|
/>
|
||||||
<BlueSpacing20 />
|
<BlueSpacing20 />
|
||||||
<BlueButton onPress={() => this.setState({ renderScanner: true })} title={'Scan signed transaction'} />
|
<BlueButton
|
||||||
|
icon={{
|
||||||
|
name: 'qrcode',
|
||||||
|
type: 'font-awesome',
|
||||||
|
color: BlueApp.settings.buttonTextColor,
|
||||||
|
}}
|
||||||
|
onPress={() => this.setState({ renderScanner: true })}
|
||||||
|
title={'Scan Signed Transaction'}
|
||||||
|
/>
|
||||||
|
<BlueSpacing20 />
|
||||||
|
<BlueButton
|
||||||
|
icon={{
|
||||||
|
name: 'file-import',
|
||||||
|
type: 'material-community',
|
||||||
|
color: BlueApp.settings.buttonTextColor,
|
||||||
|
}}
|
||||||
|
onPress={this.openSignedTransaction}
|
||||||
|
title={'Open Signed Transaction'}
|
||||||
|
/>
|
||||||
|
<BlueSpacing20 />
|
||||||
|
<BlueButton
|
||||||
|
icon={{
|
||||||
|
name: 'share-alternative',
|
||||||
|
type: 'entypo',
|
||||||
|
color: BlueApp.settings.buttonTextColor,
|
||||||
|
}}
|
||||||
|
onPress={this.exportPSBT}
|
||||||
|
title={'Export'}
|
||||||
|
/>
|
||||||
<BlueSpacing20 />
|
<BlueSpacing20 />
|
||||||
<View style={{ justifyContent: 'center', alignItems: 'center' }}>
|
<View style={{ justifyContent: 'center', alignItems: 'center' }}>
|
||||||
<BlueCopyToClipboardButton stringToCopy={this.state.psbt.toBase64()} displayText={'Copy to Clipboard'} />
|
<BlueCopyToClipboardButton
|
||||||
|
stringToCopy={this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64()}
|
||||||
|
displayText={'Copy to Clipboard'}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</BlueCard>
|
</BlueCard>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</ScrollView>
|
||||||
</SafeBlueArea>
|
</SafeBlueArea>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,69 @@
|
||||||
/* global alert */
|
/* global alert */
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Image, TouchableOpacity, Platform } from 'react-native';
|
import { Image, View, TouchableOpacity, Platform } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { RNCamera } from 'react-native-camera';
|
import { RNCamera } from 'react-native-camera';
|
||||||
import { SafeBlueArea } from '../../BlueComponents';
|
|
||||||
import { Icon } from 'react-native-elements';
|
import { Icon } from 'react-native-elements';
|
||||||
import ImagePicker from 'react-native-image-picker';
|
import ImagePicker from 'react-native-image-picker';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useNavigationParam, useNavigation } from 'react-navigation-hooks';
|
||||||
|
import DocumentPicker from 'react-native-document-picker';
|
||||||
|
import RNFS from 'react-native-fs';
|
||||||
const LocalQRCode = require('@remobile/react-native-qrcode-local-image');
|
const LocalQRCode = require('@remobile/react-native-qrcode-local-image');
|
||||||
|
|
||||||
export default class ScanQRCode extends React.Component {
|
const ScanQRCode = ({
|
||||||
static navigationOptions = {
|
onBarScanned = useNavigationParam('onBarScanned'),
|
||||||
header: null,
|
cameraPreviewIsPaused = false,
|
||||||
|
showCloseButton = true,
|
||||||
|
showFileImportButton = useNavigationParam('showFileImportButton') || false,
|
||||||
|
launchedBy = useNavigationParam('launchedBy'),
|
||||||
|
}) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { navigate } = useNavigation();
|
||||||
|
|
||||||
|
const onBarCodeRead = ret => {
|
||||||
|
if (!isLoading && !cameraPreviewIsPaused) {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
if (showCloseButton && launchedBy) {
|
||||||
|
navigate(launchedBy);
|
||||||
|
}
|
||||||
|
if (ret.additionalProperties) {
|
||||||
|
onBarScanned(ret.data, ret.additionalProperties);
|
||||||
|
} else {
|
||||||
|
onBarScanned(ret.data);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
cameraRef = null;
|
const showFilePicker = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await DocumentPicker.pick();
|
||||||
|
const file = await RNFS.readFile(res.uri);
|
||||||
|
const fileParsed = JSON.parse(file);
|
||||||
|
if (fileParsed.keystore.xpub) {
|
||||||
|
onBarCodeRead({ data: fileParsed.keystore.xpub, additionalProperties: { masterFingerprint: fileParsed.keystore.ckcc_xfp } });
|
||||||
|
} else {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!DocumentPicker.isCancel(err)) {
|
||||||
|
alert('The selected file does not contain a wallet that can be imported.');
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
onBarCodeRead = ret => {
|
useEffect(() => {}, [cameraPreviewIsPaused]);
|
||||||
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.pausePreview();
|
|
||||||
const onBarScannedProp = this.props.navigation.getParam('onBarScanned');
|
|
||||||
this.props.navigation.goBack();
|
|
||||||
onBarScannedProp(ret.data);
|
|
||||||
}; // end
|
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<View style={{ flex: 1, backgroundColor: '#000000' }}>
|
||||||
<SafeBlueArea style={{ flex: 1 }}>
|
{!cameraPreviewIsPaused && !isLoading && (
|
||||||
<RNCamera
|
<RNCamera
|
||||||
captureAudio={false}
|
captureAudio={false}
|
||||||
androidCameraPermissionOptions={{
|
androidCameraPermissionOptions={{
|
||||||
|
@ -33,11 +72,12 @@ export default class ScanQRCode extends React.Component {
|
||||||
buttonPositive: 'OK',
|
buttonPositive: 'OK',
|
||||||
buttonNegative: 'Cancel',
|
buttonNegative: 'Cancel',
|
||||||
}}
|
}}
|
||||||
ref={ref => (this.cameraRef = ref)}
|
style={{ flex: 1, justifyContent: 'space-between', backgroundColor: '#000000' }}
|
||||||
style={{ flex: 1, justifyContent: 'space-between' }}
|
onBarCodeRead={onBarCodeRead}
|
||||||
onBarCodeRead={this.onBarCodeRead}
|
|
||||||
barCodeTypes={[RNCamera.Constants.BarCodeType.qr]}
|
barCodeTypes={[RNCamera.Constants.BarCodeType.qr]}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{showCloseButton && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={{
|
style={{
|
||||||
width: 40,
|
width: 40,
|
||||||
|
@ -49,23 +89,25 @@ export default class ScanQRCode extends React.Component {
|
||||||
right: 16,
|
right: 16,
|
||||||
top: 64,
|
top: 64,
|
||||||
}}
|
}}
|
||||||
onPress={() => this.props.navigation.goBack(null)}
|
onPress={() => navigate(launchedBy)}
|
||||||
>
|
>
|
||||||
<Image style={{ alignSelf: 'center' }} source={require('../../img/close-white.png')} />
|
<Image style={{ alignSelf: 'center' }} source={require('../../img/close-white.png')} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
)}
|
||||||
style={{
|
<TouchableOpacity
|
||||||
width: 40,
|
style={{
|
||||||
height: 40,
|
width: 40,
|
||||||
backgroundColor: '#FFFFFF',
|
height: 40,
|
||||||
justifyContent: 'center',
|
backgroundColor: '#FFFFFF',
|
||||||
borderRadius: 20,
|
justifyContent: 'center',
|
||||||
position: 'absolute',
|
borderRadius: 20,
|
||||||
left: 24,
|
position: 'absolute',
|
||||||
bottom: 48,
|
left: 24,
|
||||||
}}
|
bottom: 48,
|
||||||
onPress={() => {
|
}}
|
||||||
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.pausePreview();
|
onPress={() => {
|
||||||
|
if (!isLoading) {
|
||||||
|
setIsLoading(true);
|
||||||
ImagePicker.launchImageLibrary(
|
ImagePicker.launchImageLibrary(
|
||||||
{
|
{
|
||||||
title: null,
|
title: null,
|
||||||
|
@ -77,30 +119,49 @@ export default class ScanQRCode extends React.Component {
|
||||||
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.path.toString();
|
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.path.toString();
|
||||||
LocalQRCode.decode(uri, (error, result) => {
|
LocalQRCode.decode(uri, (error, result) => {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
this.onBarCodeRead({ data: result });
|
onBarCodeRead({ data: result });
|
||||||
} else {
|
} else {
|
||||||
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview();
|
|
||||||
alert('The selected image does not contain a QR Code.');
|
alert('The selected image does not contain a QR Code.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview();
|
|
||||||
}
|
}
|
||||||
|
setIsLoading(false);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="image" type="font-awesome" color="#0c2550" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
{showFileImportButton && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: 20,
|
||||||
|
position: 'absolute',
|
||||||
|
left: 96,
|
||||||
|
bottom: 48,
|
||||||
}}
|
}}
|
||||||
|
onPress={showFilePicker}
|
||||||
>
|
>
|
||||||
<Icon name="image" type="font-awesome" color="#0c2550" />
|
<Icon name="file-import" type="material-community" color="#0c2550" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</SafeBlueArea>
|
)}
|
||||||
);
|
</View>
|
||||||
}
|
);
|
||||||
}
|
|
||||||
|
|
||||||
ScanQRCode.propTypes = {
|
|
||||||
navigation: PropTypes.shape({
|
|
||||||
goBack: PropTypes.func,
|
|
||||||
dismiss: PropTypes.func,
|
|
||||||
getParam: PropTypes.func,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ScanQRCode.navigationOptions = {
|
||||||
|
header: null,
|
||||||
|
};
|
||||||
|
ScanQRCode.propTypes = {
|
||||||
|
launchedBy: PropTypes.string,
|
||||||
|
onBarScanned: PropTypes.func,
|
||||||
|
cameraPreviewIsPaused: PropTypes.bool,
|
||||||
|
showFileImportButton: PropTypes.bool,
|
||||||
|
showCloseButton: PropTypes.bool,
|
||||||
|
};
|
||||||
|
export default ScanQRCode;
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { HDSegwitP2SHWallet } from '../../class/hd-segwit-p2sh-wallet';
|
||||||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||||
import Biometric from '../../class/biometrics';
|
import Biometric from '../../class/biometrics';
|
||||||
import { HDSegwitBech32Wallet, WatchOnlyWallet } from '../../class';
|
import { HDSegwitBech32Wallet, WatchOnlyWallet } from '../../class';
|
||||||
|
import { ScrollView } from 'react-native-gesture-handler';
|
||||||
let EV = require('../../events');
|
let EV = require('../../events');
|
||||||
let prompt = require('../../prompt');
|
let prompt = require('../../prompt');
|
||||||
/** @type {AppStorage} */
|
/** @type {AppStorage} */
|
||||||
|
@ -55,6 +56,7 @@ export default class WalletDetails extends Component {
|
||||||
walletName: wallet.getLabel(),
|
walletName: wallet.getLabel(),
|
||||||
wallet,
|
wallet,
|
||||||
useWithHardwareWallet: !!wallet.use_with_hardware_wallet,
|
useWithHardwareWallet: !!wallet.use_with_hardware_wallet,
|
||||||
|
masterFingerprint: wallet.masterFingerprint ? String(wallet.masterFingerprint) : '',
|
||||||
};
|
};
|
||||||
this.props.navigation.setParams({ isLoading, saveAction: () => this.setLabel() });
|
this.props.navigation.setParams({ isLoading, saveAction: () => this.setLabel() });
|
||||||
}
|
}
|
||||||
|
@ -71,6 +73,7 @@ export default class WalletDetails extends Component {
|
||||||
this.props.navigation.setParams({ isLoading: true });
|
this.props.navigation.setParams({ isLoading: true });
|
||||||
this.setState({ isLoading: true }, async () => {
|
this.setState({ isLoading: true }, async () => {
|
||||||
this.state.wallet.setLabel(this.state.walletName);
|
this.state.wallet.setLabel(this.state.walletName);
|
||||||
|
this.state.wallet.masterFingerprint = Number(this.state.masterFingerprint);
|
||||||
BlueApp.saveToDisk();
|
BlueApp.saveToDisk();
|
||||||
alert('Wallet updated.');
|
alert('Wallet updated.');
|
||||||
this.props.navigation.goBack(null);
|
this.props.navigation.goBack(null);
|
||||||
|
@ -122,7 +125,7 @@ export default class WalletDetails extends Component {
|
||||||
return (
|
return (
|
||||||
<SafeBlueArea style={{ flex: 1 }}>
|
<SafeBlueArea style={{ flex: 1 }}>
|
||||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
|
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
|
||||||
<View style={{ flex: 1 }}>
|
<ScrollView contentContainerStyle={{ flexGrow: 1 }}>
|
||||||
<BlueCard style={{ alignItems: 'center', flex: 1 }}>
|
<BlueCard style={{ alignItems: 'center', flex: 1 }}>
|
||||||
{(() => {
|
{(() => {
|
||||||
if (this.state.wallet.getAddress()) {
|
if (this.state.wallet.getAddress()) {
|
||||||
|
@ -158,18 +161,20 @@ export default class WalletDetails extends Component {
|
||||||
placeholder={loc.send.details.note_placeholder}
|
placeholder={loc.send.details.note_placeholder}
|
||||||
value={this.state.walletName}
|
value={this.state.walletName}
|
||||||
onChangeText={text => {
|
onChangeText={text => {
|
||||||
if (text.trim().length === 0) {
|
|
||||||
text = this.state.wallet.getLabel();
|
|
||||||
}
|
|
||||||
this.setState({ walletName: text });
|
this.setState({ walletName: text });
|
||||||
}}
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
if (this.state.walletName.trim().length === 0) {
|
||||||
|
this.setState({ walletName: this.state.wallet.getLabel() });
|
||||||
|
}
|
||||||
|
}}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }}
|
style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }}
|
||||||
editable={!this.state.isLoading}
|
editable={!this.state.isLoading}
|
||||||
underlineColorAndroid="transparent"
|
underlineColorAndroid="transparent"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
<BlueSpacing20 />
|
||||||
<Text style={{ color: '#0c2550', fontWeight: '500', fontSize: 14, marginVertical: 12 }}>
|
<Text style={{ color: '#0c2550', fontWeight: '500', fontSize: 14, marginVertical: 12 }}>
|
||||||
{loc.wallets.details.type.toLowerCase()}
|
{loc.wallets.details.type.toLowerCase()}
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -182,15 +187,48 @@ export default class WalletDetails extends Component {
|
||||||
)}
|
)}
|
||||||
<View>
|
<View>
|
||||||
<BlueSpacing20 />
|
<BlueSpacing20 />
|
||||||
|
|
||||||
{this.state.wallet.type === WatchOnlyWallet.type && this.state.wallet.getSecret().startsWith('zpub') && (
|
{this.state.wallet.type === WatchOnlyWallet.type && this.state.wallet.getSecret().startsWith('zpub') && (
|
||||||
<React.Fragment>
|
<>
|
||||||
|
<Text style={{ color: '#0c2550', fontWeight: '500', fontSize: 14, marginVertical: 16 }}>{'advanced'}</Text>
|
||||||
|
<BlueText>Master Fingerprint</BlueText>
|
||||||
|
<BlueSpacing20 />
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
borderColor: '#d2d2d2',
|
||||||
|
borderBottomColor: '#d2d2d2',
|
||||||
|
borderWidth: 1.0,
|
||||||
|
borderBottomWidth: 0.5,
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
minHeight: 44,
|
||||||
|
height: 44,
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Master Fingerprint"
|
||||||
|
value={this.state.masterFingerprint}
|
||||||
|
onChangeText={text => {
|
||||||
|
if (isNaN(text)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({ masterFingerprint: text });
|
||||||
|
}}
|
||||||
|
numberOfLines={1}
|
||||||
|
style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }}
|
||||||
|
editable={!this.state.isLoading}
|
||||||
|
keyboardType="decimal-pad"
|
||||||
|
underlineColorAndroid="transparent"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<BlueSpacing20 />
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<BlueText>{'Use with hardware wallet'}</BlueText>
|
<BlueText>{'Use with hardware wallet'}</BlueText>
|
||||||
<Switch value={this.state.useWithHardwareWallet} onValueChange={value => this.onUseWithHardwareWalletSwitch(value)} />
|
<Switch value={this.state.useWithHardwareWallet} onValueChange={value => this.onUseWithHardwareWalletSwitch(value)} />
|
||||||
</View>
|
</View>
|
||||||
<BlueSpacing20 />
|
<BlueSpacing20 />
|
||||||
</React.Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<BlueButton
|
<BlueButton
|
||||||
|
@ -285,7 +323,7 @@ export default class WalletDetails extends Component {
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</BlueCard>
|
</BlueCard>
|
||||||
</View>
|
</ScrollView>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
</SafeBlueArea>
|
</SafeBlueArea>
|
||||||
);
|
);
|
||||||
|
|
|
@ -35,9 +35,9 @@ const WalletsImport = () => {
|
||||||
importMnemonic(importText);
|
importMnemonic(importText);
|
||||||
};
|
};
|
||||||
|
|
||||||
const importMnemonic = importText => {
|
const importMnemonic = (importText, additionalProperties) => {
|
||||||
try {
|
try {
|
||||||
WalletImport.processImportText(importText);
|
WalletImport.processImportText(importText, additionalProperties);
|
||||||
dismiss();
|
dismiss();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(loc.wallets.import.error);
|
alert(loc.wallets.import.error);
|
||||||
|
@ -45,9 +45,9 @@ const WalletsImport = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onBarScanned = value => {
|
const onBarScanned = (value, additionalProperties) => {
|
||||||
setImportText(value);
|
setImportText(value);
|
||||||
importMnemonic(value);
|
importMnemonic(value, additionalProperties);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -110,7 +110,7 @@ const WalletsImport = () => {
|
||||||
<BlueButtonLink
|
<BlueButtonLink
|
||||||
title={loc.wallets.import.scan_qr}
|
title={loc.wallets.import.scan_qr}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
navigate('ScanQrAddress', { onBarScanned });
|
navigate('ScanQrAddress', { launchedBy: 'ImportWallet', onBarScanned, showFileImportButton: true });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -1,6 +1,17 @@
|
||||||
/* global alert */
|
/* global alert */
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { View, TouchableOpacity, Text, FlatList, InteractionManager, RefreshControl, ScrollView, Alert } from 'react-native';
|
import {
|
||||||
|
View,
|
||||||
|
StatusBar,
|
||||||
|
TouchableOpacity,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
FlatList,
|
||||||
|
InteractionManager,
|
||||||
|
RefreshControl,
|
||||||
|
ScrollView,
|
||||||
|
Alert,
|
||||||
|
} from 'react-native';
|
||||||
import { BlueLoading, SafeBlueArea, WalletsCarousel, BlueList, BlueHeaderDefaultMain, BlueTransactionListItem } from '../../BlueComponents';
|
import { BlueLoading, SafeBlueArea, WalletsCarousel, BlueList, BlueHeaderDefaultMain, BlueTransactionListItem } from '../../BlueComponents';
|
||||||
import { Icon } from 'react-native-elements';
|
import { Icon } from 'react-native-elements';
|
||||||
import { NavigationEvents } from 'react-navigation';
|
import { NavigationEvents } from 'react-navigation';
|
||||||
|
@ -8,6 +19,9 @@ import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PlaceholderWallet } from '../../class';
|
import { PlaceholderWallet } from '../../class';
|
||||||
import WalletImport from '../../class/walletImport';
|
import WalletImport from '../../class/walletImport';
|
||||||
|
import Swiper from 'react-native-swiper';
|
||||||
|
import ScanQRCode from '../send/scanQrAddress';
|
||||||
|
import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch';
|
||||||
let EV = require('../../events');
|
let EV = require('../../events');
|
||||||
let A = require('../../analytics');
|
let A = require('../../analytics');
|
||||||
/** @type {AppStorage} */
|
/** @type {AppStorage} */
|
||||||
|
@ -16,23 +30,8 @@ let loc = require('../../loc');
|
||||||
let BlueElectrum = require('../../BlueElectrum');
|
let BlueElectrum = require('../../BlueElectrum');
|
||||||
|
|
||||||
export default class WalletsList extends Component {
|
export default class WalletsList extends Component {
|
||||||
static navigationOptions = ({ navigation }) => ({
|
|
||||||
headerStyle: {
|
|
||||||
backgroundColor: '#FFFFFF',
|
|
||||||
borderBottomWidth: 0,
|
|
||||||
elevation: 0,
|
|
||||||
},
|
|
||||||
headerRight: (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={{ marginHorizontal: 16, width: 40, height: 40, justifyContent: 'center', alignItems: 'flex-end' }}
|
|
||||||
onPress={() => navigation.navigate('Settings')}
|
|
||||||
>
|
|
||||||
<Icon size={22} name="kebab-horizontal" type="octicon" color={BlueApp.settings.foregroundColor} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
walletsCarousel = React.createRef();
|
walletsCarousel = React.createRef();
|
||||||
|
swiperRef = React.createRef();
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -299,21 +298,47 @@ export default class WalletsList extends Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onSwiperIndexChanged = index => {
|
||||||
|
StatusBar.setBarStyle(index === 1 ? 'dark-content' : 'light-content');
|
||||||
|
this.setState({ cameraPreviewIsPaused: index === 1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
onBarScanned = value => {
|
||||||
|
DeeplinkSchemaMatch.navigationRouteFor({ url: value }, completionValue => {
|
||||||
|
ReactNativeHapticFeedback.trigger('impactLight', { ignoreAndroidSystemSettings: false });
|
||||||
|
this.props.navigation.navigate(completionValue);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
_renderItem = data => {
|
_renderItem = data => {
|
||||||
return <BlueTransactionListItem item={data.item} itemPriceUnit={data.item.walletPreferredBalanceUnit} />;
|
return <BlueTransactionListItem item={data.item} itemPriceUnit={data.item.walletPreferredBalanceUnit} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
renderNavigationHeader = () => {
|
||||||
|
return (
|
||||||
|
<View style={{ height: 44, alignItems: 'flex-end', justifyContent: 'center' }}>
|
||||||
|
<TouchableOpacity style={{ marginHorizontal: 16 }} onPress={() => this.props.navigation.navigate('Settings')}>
|
||||||
|
<Icon size={22} name="kebab-horizontal" type="octicon" color={BlueApp.settings.foregroundColor} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.isLoading) {
|
if (this.state.isLoading) {
|
||||||
return <BlueLoading />;
|
return <BlueLoading />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<SafeBlueArea style={{ flex: 1, backgroundColor: '#FFFFFF' }}>
|
<View style={{ flex: 1, backgroundColor: '#000000' }}>
|
||||||
<NavigationEvents
|
<NavigationEvents
|
||||||
onWillFocus={() => {
|
onDidFocus={() => {
|
||||||
this.redrawScreen();
|
this.redrawScreen();
|
||||||
|
this.setState({ cameraPreviewIsPaused: this.swiperRef.current.index === 1 });
|
||||||
}}
|
}}
|
||||||
|
onWillBlur={() => this.setState({ cameraPreviewIsPaused: true })}
|
||||||
/>
|
/>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
contentContainerStyle={{ flex: 1 }}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
onRefresh={() => this.refreshTransactions()}
|
onRefresh={() => this.refreshTransactions()}
|
||||||
|
@ -322,65 +347,112 @@ export default class WalletsList extends Component {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<BlueHeaderDefaultMain
|
<Swiper
|
||||||
leftText={loc.wallets.list.title}
|
style={styles.wrapper}
|
||||||
onNewWalletPress={
|
onIndexChanged={this.onSwiperIndexChanged}
|
||||||
!BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type)
|
index={1}
|
||||||
? () => this.props.navigation.navigate('AddWallet')
|
ref={this.swiperRef}
|
||||||
: null
|
showsPagination={false}
|
||||||
}
|
showsButtons={false}
|
||||||
/>
|
loop={false}
|
||||||
<WalletsCarousel
|
>
|
||||||
removeClippedSubviews={false}
|
<View style={styles.scanQRWrapper}>
|
||||||
data={this.state.wallets}
|
<ScanQRCode
|
||||||
handleClick={index => {
|
cameraPreviewIsPaused={this.state.cameraPreviewIsPaused}
|
||||||
this.handleClick(index);
|
onBarScanned={this.onBarScanned}
|
||||||
}}
|
showCloseButton={false}
|
||||||
handleLongPress={this.handleLongPress}
|
initialCameraStatusReady={false}
|
||||||
onSnapToItem={index => {
|
launchedBy={this.props.navigation.state.routeName}
|
||||||
this.onSnapToItem(index);
|
/>
|
||||||
}}
|
</View>
|
||||||
ref={c => (this.walletsCarousel = c)}
|
<SafeBlueArea>
|
||||||
/>
|
<View style={styles.walletsListWrapper}>
|
||||||
<BlueList>
|
{this.renderNavigationHeader()}
|
||||||
<FlatList
|
<ScrollView
|
||||||
ListHeaderComponent={this.renderListHeaderComponent}
|
refreshControl={
|
||||||
ListEmptyComponent={
|
<RefreshControl onRefresh={() => this.refreshTransactions()} refreshing={!this.state.isFlatListRefreshControlHidden} />
|
||||||
<View style={{ top: 50, height: 100 }}>
|
}
|
||||||
<Text
|
>
|
||||||
style={{
|
<BlueHeaderDefaultMain
|
||||||
fontSize: 18,
|
leftText={loc.wallets.list.title}
|
||||||
color: '#9aa0aa',
|
onNewWalletPress={
|
||||||
textAlign: 'center',
|
!BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type)
|
||||||
|
? () => this.props.navigation.navigate('AddWallet')
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<WalletsCarousel
|
||||||
|
removeClippedSubviews={false}
|
||||||
|
data={this.state.wallets}
|
||||||
|
handleClick={index => {
|
||||||
|
this.handleClick(index);
|
||||||
}}
|
}}
|
||||||
>
|
handleLongPress={this.handleLongPress}
|
||||||
{loc.wallets.list.empty_txs1}
|
onSnapToItem={index => {
|
||||||
</Text>
|
this.onSnapToItem(index);
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
fontSize: 18,
|
|
||||||
color: '#9aa0aa',
|
|
||||||
textAlign: 'center',
|
|
||||||
}}
|
}}
|
||||||
>
|
ref={c => (this.walletsCarousel = c)}
|
||||||
{loc.wallets.list.empty_txs2}
|
/>
|
||||||
</Text>
|
<BlueList>
|
||||||
</View>
|
<FlatList
|
||||||
}
|
ListHeaderComponent={this.renderListHeaderComponent}
|
||||||
data={this.state.dataSource}
|
ListEmptyComponent={
|
||||||
extraData={this.state.dataSource}
|
<View style={{ top: 50, height: 100 }}>
|
||||||
keyExtractor={this._keyExtractor}
|
<Text
|
||||||
renderItem={this._renderItem}
|
style={{
|
||||||
/>
|
fontSize: 18,
|
||||||
</BlueList>
|
color: '#9aa0aa',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loc.wallets.list.empty_txs1}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: 18,
|
||||||
|
color: '#9aa0aa',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loc.wallets.list.empty_txs2}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
data={this.state.dataSource}
|
||||||
|
extraData={this.state.dataSource}
|
||||||
|
keyExtractor={this._keyExtractor}
|
||||||
|
renderItem={this._renderItem}
|
||||||
|
/>
|
||||||
|
</BlueList>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</SafeBlueArea>
|
||||||
|
</Swiper>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeBlueArea>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
wrapper: {
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
},
|
||||||
|
walletsListWrapper: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
},
|
||||||
|
scanQRWrapper: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#000000',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
WalletsList.propTypes = {
|
WalletsList.propTypes = {
|
||||||
navigation: PropTypes.shape({
|
navigation: PropTypes.shape({
|
||||||
|
state: PropTypes.shape({
|
||||||
|
routeName: PropTypes.string,
|
||||||
|
}),
|
||||||
navigate: PropTypes.func,
|
navigate: PropTypes.func,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
StatusBar,
|
StatusBar,
|
||||||
Linking,
|
Linking,
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
|
Alert,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { NavigationEvents } from 'react-navigation';
|
import { NavigationEvents } from 'react-navigation';
|
||||||
|
@ -29,7 +30,7 @@ import {
|
||||||
} from '../../BlueComponents';
|
} from '../../BlueComponents';
|
||||||
import WalletGradient from '../../class/walletGradient';
|
import WalletGradient from '../../class/walletGradient';
|
||||||
import { Icon } from 'react-native-elements';
|
import { Icon } from 'react-native-elements';
|
||||||
import { LightningCustodianWallet } from '../../class';
|
import { LightningCustodianWallet, HDSegwitBech32Wallet } from '../../class';
|
||||||
import Handoff from 'react-native-handoff';
|
import Handoff from 'react-native-handoff';
|
||||||
import Modal from 'react-native-modal';
|
import Modal from 'react-native-modal';
|
||||||
import NavigationService from '../../NavigationService';
|
import NavigationService from '../../NavigationService';
|
||||||
|
@ -400,7 +401,7 @@ export default class WalletTransactions extends Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async onWillBlur() {
|
onWillBlur() {
|
||||||
StatusBar.setBarStyle('dark-content');
|
StatusBar.setBarStyle('dark-content');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -409,6 +410,14 @@ export default class WalletTransactions extends Component {
|
||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navigateToSendScreen = () => {
|
||||||
|
this.props.navigation.navigate('SendDetails', {
|
||||||
|
fromAddress: this.state.wallet.getAddress(),
|
||||||
|
fromSecret: this.state.wallet.getSecret(),
|
||||||
|
fromWallet: this.state.wallet,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
renderItem = item => {
|
renderItem = item => {
|
||||||
return (
|
return (
|
||||||
<BlueTransactionListItem
|
<BlueTransactionListItem
|
||||||
|
@ -569,18 +578,45 @@ export default class WalletTransactions extends Component {
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{(() => {
|
{(() => {
|
||||||
if (this.state.wallet.allowSend()) {
|
if (
|
||||||
|
this.state.wallet.allowSend() ||
|
||||||
|
(this.state.wallet._hdWalletInstance instanceof HDSegwitBech32Wallet && this.state.wallet._hdWalletInstance.allowSend())
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<BlueSendButtonIcon
|
<BlueSendButtonIcon
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (this.state.wallet.chain === Chain.OFFCHAIN) {
|
if (this.state.wallet.chain === Chain.OFFCHAIN) {
|
||||||
navigate('ScanLndInvoice', { fromSecret: this.state.wallet.getSecret() });
|
navigate('ScanLndInvoice', { fromSecret: this.state.wallet.getSecret() });
|
||||||
} else {
|
} else {
|
||||||
navigate('SendDetails', {
|
if (
|
||||||
fromAddress: this.state.wallet.getAddress(),
|
this.state.wallet._hdWalletInstance instanceof HDSegwitBech32Wallet &&
|
||||||
fromSecret: this.state.wallet.getSecret(),
|
this.state.wallet._hdWalletInstance.allowSend()
|
||||||
fromWallet: this.state.wallet,
|
) {
|
||||||
});
|
if (this.state.wallet.use_with_hardware_wallet) {
|
||||||
|
this.navigateToSendScreen();
|
||||||
|
} else {
|
||||||
|
Alert.alert(
|
||||||
|
'Wallet',
|
||||||
|
'This wallet is not being used in conjunction with a hardwarde wallet. Would you like to enable hardware wallet use?',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: loc._.ok,
|
||||||
|
onPress: async () => {
|
||||||
|
this.state.wallet.use_with_hardware_wallet = true;
|
||||||
|
await BlueApp.saveToDisk();
|
||||||
|
this.navigateToSendScreen();
|
||||||
|
},
|
||||||
|
style: 'default',
|
||||||
|
},
|
||||||
|
|
||||||
|
{ text: loc.send.details.cancel, onPress: () => {}, style: 'cancel' },
|
||||||
|
],
|
||||||
|
{ cancelable: false },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.navigateToSendScreen();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
50
tests/integration/deepLinkSchemaMatch.test.js
Normal file
50
tests/integration/deepLinkSchemaMatch.test.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/* global describe, it, expect */
|
||||||
|
import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch';
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
describe('unit - DeepLinkSchemaMatch', function() {
|
||||||
|
it('hasSchema', () => {
|
||||||
|
const hasSchema = DeeplinkSchemaMatch.hasSchema('bitcoin:12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG');
|
||||||
|
assert.ok(hasSchema);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isBitcoin Address', () => {
|
||||||
|
assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isLighting Invoice', () => {
|
||||||
|
assert.ok(
|
||||||
|
DeeplinkSchemaMatch.isLightningInvoice(
|
||||||
|
'lightning:lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isBoth Bitcoin & Invoice', () => {
|
||||||
|
assert.ok(
|
||||||
|
DeeplinkSchemaMatch.isBothBitcoinAndLightning(
|
||||||
|
'bitcoin:1DamianM2k8WfNEeJmyqSe2YW1upB7UATx?amount=0.000001&lightning=lnbc1u1pwry044pp53xlmkghmzjzm3cljl6729cwwqz5hhnhevwfajpkln850n7clft4sdqlgfy4qv33ypmj7sj0f32rzvfqw3jhxaqcqzysxq97zvuq5zy8ge6q70prnvgwtade0g2k5h2r76ws7j2926xdjj2pjaq6q3r4awsxtm6k5prqcul73p3atveljkn6wxdkrcy69t6k5edhtc6q7lgpe4m5k4',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isLnurl', () => {
|
||||||
|
assert.ok(
|
||||||
|
DeeplinkSchemaMatch.isLnUrl(
|
||||||
|
'LNURL1DP68GURN8GHJ7UM9WFMXJCM99E3K7MF0V9CXJ0M385EKVCENXC6R2C35XVUKXEFCV5MKVV34X5EKZD3EV56NYD3HXQURZEPEXEJXXEPNXSCRVWFNV9NXZCN9XQ6XYEFHVGCXXCMYXYMNSERXFQ5FNS',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('navigationForRoute', () => {
|
||||||
|
const event = { uri: '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG' };
|
||||||
|
DeeplinkSchemaMatch.navigationRouteFor(event, navValue => {
|
||||||
|
assert.strictEqual(navValue, {
|
||||||
|
routeName: 'SendDetails',
|
||||||
|
params: {
|
||||||
|
uri: event.url,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Reference in a new issue