REF: Reworked Import wallet flow

This commit is contained in:
Marcos Rodriguez 2019-12-26 20:21:07 -06:00 committed by Overtorment
parent ec5bc4a3c6
commit c975347b6f
11 changed files with 581 additions and 750 deletions

View file

@ -23,7 +23,7 @@ import {
TextInput, TextInput,
} from 'react-native'; } from 'react-native';
import LinearGradient from 'react-native-linear-gradient'; import LinearGradient from 'react-native-linear-gradient';
import { LightningCustodianWallet } from './class'; import { LightningCustodianWallet, PlaceholderWallet } from './class';
import Carousel from 'react-native-snap-carousel'; import Carousel from 'react-native-snap-carousel';
import { BitcoinUnit } from './models/bitcoinUnits'; import { BitcoinUnit } from './models/bitcoinUnits';
import NavigationService from './NavigationService'; import NavigationService from './NavigationService';
@ -748,6 +748,7 @@ export class BlueHeaderDefaultMain extends Component {
</Text> </Text>
} }
rightComponent={ rightComponent={
this.props.onNewWalletPress && (
<TouchableOpacity <TouchableOpacity
onPress={this.props.onNewWalletPress} onPress={this.props.onNewWalletPress}
style={{ style={{
@ -757,6 +758,7 @@ export class BlueHeaderDefaultMain extends Component {
> >
<BluePlusIcon /> <BluePlusIcon />
</TouchableOpacity> </TouchableOpacity>
)
} }
/> />
</SafeAreaView> </SafeAreaView>
@ -957,7 +959,7 @@ export class BlueLoading extends Component {
render() { render() {
return ( return (
<SafeBlueArea> <SafeBlueArea>
<View style={{ flex: 1, paddingTop: 200 }}> <View style={{ flex: 1, paddingTop: 200 }} {...this.props}>
<ActivityIndicator /> <ActivityIndicator />
</View> </View>
</SafeBlueArea> </SafeBlueArea>
@ -1844,6 +1846,74 @@ export class WalletsCarousel extends Component {
); );
} }
if (item.type === PlaceholderWallet.type) {
return (
<Animated.View
style={{ paddingRight: 10, marginVertical: 17, transform: [{ scale: scaleValue }] }}
shadowOpacity={40 / 100}
shadowOffset={{ width: 0, height: 0 }}
shadowRadius={5}
>
<TouchableWithoutFeedback
onPressIn={item.getIsFailure() ? this.onPressedIn : null}
onPressOut={item.getIsFailure() ? this.onPressedOut : null}
onPress={() => {
if (item.getIsFailure() && WalletsCarousel.handleClick) {
WalletsCarousel.handleClick(index);
}
}}
>
<LinearGradient
shadowColor={BlueApp.settings.shadowColor}
colors={WalletGradient.gradientsFor(item.type)}
style={{
padding: 15,
borderRadius: 10,
minHeight: 164,
elevation: 5,
}}
>
<Image
source={require('./img/btc-shape.png')}
style={{
width: 99,
height: 94,
position: 'absolute',
bottom: 0,
right: 0,
}}
/>
<Text style={{ backgroundColor: 'transparent' }} />
<Text
numberOfLines={1}
style={{
backgroundColor: 'transparent',
fontSize: 19,
color: BlueApp.settings.inverseForegroundColor,
}}
>
{item.getLabel()}
</Text>
{item.getIsFailure() ? (
<Text
numberOfLines={0}
style={{
backgroundColor: 'transparent',
fontSize: 19,
marginTop: 40,
color: BlueApp.settings.inverseForegroundColor,
}}
>
An error was encountered when attempting to import this wallet.
</Text>
) : (
<ActivityIndicator style={{ marginTop: 40 }} />
)}
</LinearGradient>
</TouchableWithoutFeedback>
</Animated.View>
);
} else {
return ( return (
<Animated.View <Animated.View
style={{ paddingRight: 10, marginVertical: 17, transform: [{ scale: scaleValue }] }} style={{ paddingRight: 10, marginVertical: 17, transform: [{ scale: scaleValue }] }}
@ -1936,6 +2006,7 @@ export class WalletsCarousel extends Component {
</Animated.View> </Animated.View>
); );
} }
}
snapToItem = item => { snapToItem = item => {
this.walletsCarousel.current.snapToItem(item); this.walletsCarousel.current.snapToItem(item);

View file

@ -23,7 +23,6 @@ import WalletExport from './screen/wallets/export';
import WalletXpub from './screen/wallets/xpub'; import WalletXpub from './screen/wallets/xpub';
import BuyBitcoin from './screen/wallets/buyBitcoin'; import BuyBitcoin from './screen/wallets/buyBitcoin';
import Marketplace from './screen/wallets/marketplace'; import Marketplace from './screen/wallets/marketplace';
import scanQrWif from './screen/wallets/scanQrWif';
import ReorderWallets from './screen/wallets/reorderWallets'; import ReorderWallets from './screen/wallets/reorderWallets';
import SelectWallet from './screen/wallets/selectWallet'; import SelectWallet from './screen/wallets/selectWallet';
@ -277,9 +276,6 @@ const MainBottomTabs = createStackNavigator(
header: null, header: null,
}, },
}, },
ScanQrWif: {
screen: scanQrWif,
},
WalletExport: { WalletExport: {
screen: WalletExport, screen: WalletExport,
}, },

View file

@ -9,8 +9,9 @@ import {
SegwitP2SHWallet, SegwitP2SHWallet,
SegwitBech32Wallet, SegwitBech32Wallet,
HDSegwitBech32Wallet, HDSegwitBech32Wallet,
PlaceholderWallet,
LightningCustodianWallet,
} from './'; } from './';
import { LightningCustodianWallet } from './lightning-custodian-wallet';
import WatchConnectivity from '../WatchConnectivity'; import WatchConnectivity from '../WatchConnectivity';
import DeviceQuickActions from './quickActions'; import DeviceQuickActions from './quickActions';
const encryption = require('../encryption'); const encryption = require('../encryption');
@ -192,6 +193,8 @@ export class AppStorage {
let tempObj = JSON.parse(key); let tempObj = JSON.parse(key);
let unserializedWallet; let unserializedWallet;
switch (tempObj.type) { switch (tempObj.type) {
case PlaceholderWallet.type:
continue;
case SegwitBech32Wallet.type: case SegwitBech32Wallet.type:
unserializedWallet = SegwitBech32Wallet.fromJson(key); unserializedWallet = SegwitBech32Wallet.fromJson(key);
break; break;
@ -298,7 +301,7 @@ export class AppStorage {
async saveToDisk() { async saveToDisk() {
let walletsToSave = []; let walletsToSave = [];
for (let key of this.wallets) { for (let key of this.wallets) {
if (typeof key === 'boolean') continue; if (typeof key === 'boolean' || key.type === PlaceholderWallet.type) continue;
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 }));
} }
@ -349,13 +352,13 @@ export class AppStorage {
console.log('fetchWalletBalances for wallet#', index); console.log('fetchWalletBalances for wallet#', index);
if (index || index === 0) { if (index || index === 0) {
let c = 0; let c = 0;
for (let wallet of this.wallets) { for (let wallet of this.wallets.filter(wallet => wallet.type !== PlaceholderWallet.type)) {
if (c++ === index) { if (c++ === index) {
await wallet.fetchBalance(); await wallet.fetchBalance();
} }
} }
} else { } else {
for (let wallet of this.wallets) { for (let wallet of this.wallets.filter(wallet => wallet.type !== PlaceholderWallet.type)) {
await wallet.fetchBalance(); await wallet.fetchBalance();
} }
} }
@ -375,7 +378,7 @@ export class AppStorage {
console.log('fetchWalletTransactions for wallet#', index); console.log('fetchWalletTransactions for wallet#', index);
if (index || index === 0) { if (index || index === 0) {
let c = 0; let c = 0;
for (let wallet of this.wallets) { for (let wallet of this.wallets.filter(wallet => wallet.type !== PlaceholderWallet.type)) {
if (c++ === index) { if (c++ === index) {
await wallet.fetchTransactions(); await wallet.fetchTransactions();
if (wallet.fetchPendingTransactions) { if (wallet.fetchPendingTransactions) {

View file

@ -12,3 +12,4 @@ export * from './lightning-custodian-wallet';
export * from './abstract-hd-wallet'; export * from './abstract-hd-wallet';
export * from './hd-segwit-bech32-wallet'; export * from './hd-segwit-bech32-wallet';
export * from './hd-segwit-bech32-transaction'; export * from './hd-segwit-bech32-transaction';
export * from './placeholder-wallet';

View file

@ -0,0 +1,31 @@
import { AbstractWallet } from './abstract-wallet';
export class PlaceholderWallet extends AbstractWallet {
static type = 'placeholder';
static typeReadable = 'Placeholder';
constructor() {
super();
this._isFailure = false;
}
allowSend() {
return false;
}
getLabel() {
return this.getIsFailure() ? 'Wallet Import' : 'Importing Wallet...';
}
allowReceive() {
return false;
}
getIsFailure() {
return this._isFailure;
}
setIsFailure(value) {
this._isFailure = value;
}
}

View file

@ -5,6 +5,7 @@ import { HDLegacyBreadwalletWallet } from './hd-legacy-breadwallet-wallet';
import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet'; import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet';
import { WatchOnlyWallet } from './watch-only-wallet'; import { WatchOnlyWallet } from './watch-only-wallet';
import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet'; import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet';
import { PlaceholderWallet } from './placeholder-wallet';
export default class WalletGradient { export default class WalletGradient {
static hdSegwitP2SHWallet = ['#65ceef', '#68bbe1']; static hdSegwitP2SHWallet = ['#65ceef', '#68bbe1'];
@ -41,6 +42,9 @@ export default class WalletGradient {
case LightningCustodianWallet.type: case LightningCustodianWallet.type:
gradient = WalletGradient.lightningCustodianWallet; gradient = WalletGradient.lightningCustodianWallet;
break; break;
case PlaceholderWallet.type:
gradient = WalletGradient.watchOnlyWallet;
break;
case 'CreateWallet': case 'CreateWallet':
gradient = WalletGradient.createWallet; gradient = WalletGradient.createWallet;
break; break;

228
class/walletImport.js Normal file
View file

@ -0,0 +1,228 @@
/* global alert */
import {
SegwitP2SHWallet,
LegacyWallet,
WatchOnlyWallet,
HDLegacyBreadwalletWallet,
HDSegwitP2SHWallet,
HDLegacyP2PKHWallet,
HDSegwitBech32Wallet,
LightningCustodianWallet,
PlaceholderWallet,
} from '../class';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
const EV = require('../events');
const A = require('../analytics');
/** @type {AppStorage} */
const BlueApp = require('../BlueApp');
const loc = require('../loc');
export default class WalletImport {
static async _saveWallet(w) {
try {
const wallet = BlueApp.getWallets().some(wallet => wallet.getSecret() === w.secret && wallet.type !== PlaceholderWallet.type);
if (wallet) {
alert('This wallet has been previously imported.');
WalletImport.removePlaceholderWallet();
} else {
alert(loc.wallets.import.success);
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
w.setLabel(loc.wallets.import.imported + ' ' + w.typeReadable);
WalletImport.removePlaceholderWallet();
BlueApp.wallets.push(w);
await BlueApp.saveToDisk();
A(A.ENUM.CREATED_WALLET);
}
EV(EV.enum.WALLETS_COUNT_CHANGED);
} catch (_e) {}
}
static removePlaceholderWallet() {
const placeholderWalletIndex = BlueApp.wallets.findIndex(wallet => wallet.type === PlaceholderWallet.type);
if (placeholderWalletIndex > -1) {
BlueApp.wallets.splice(placeholderWalletIndex, 1);
}
}
static addPlaceholderWallet(importText, isFailure = false) {
const wallet = new PlaceholderWallet();
wallet.setSecret(importText);
wallet.setIsFailure(isFailure);
BlueApp.wallets.push(wallet);
EV(EV.enum.WALLETS_COUNT_CHANGED);
return wallet;
}
static isCurrentlyImportingWallet() {
return BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type);
}
static async processImportText(importText) {
if (WalletImport.isCurrentlyImportingWallet()) {
return;
}
const placeholderWallet = WalletImport.addPlaceholderWallet(importText);
// Plan:
// 0. check if its HDSegwitBech32Wallet (BIP84)
// 1. check if its HDSegwitP2SHWallet (BIP49)
// 2. check if its HDLegacyP2PKHWallet (BIP44)
// 3. check if its HDLegacyBreadwalletWallet (no BIP, just "m/0")
// 4. check if its Segwit WIF (P2SH)
// 5. check if its Legacy WIF
// 6. check if its address (watch-only wallet)
// 7. check if its private key (segwit address P2SH) TODO
// 7. check if its private key (legacy address) TODO
try {
// is it lightning custodian?
if (importText.indexOf('blitzhub://') !== -1 || importText.indexOf('lndhub://') !== -1) {
let lnd = new LightningCustodianWallet();
if (importText.includes('@')) {
const split = importText.split('@');
lnd.setBaseURI(split[1]);
lnd.setSecret(split[0]);
} else {
lnd.setBaseURI(LightningCustodianWallet.defaultBaseUri);
lnd.setSecret(importText);
}
lnd.init();
await lnd.authorize();
await lnd.fetchTransactions();
await lnd.fetchUserInvoices();
await lnd.fetchPendingTransactions();
await lnd.fetchBalance();
return WalletImport._saveWallet(lnd);
}
// trying other wallet types
let hd4 = new HDSegwitBech32Wallet();
hd4.setSecret(importText);
if (hd4.validateMnemonic()) {
await hd4.fetchBalance();
if (hd4.getBalance() > 0) {
await hd4.fetchTransactions();
return WalletImport._saveWallet(hd4);
}
}
let segwitWallet = new SegwitP2SHWallet();
segwitWallet.setSecret(importText);
if (segwitWallet.getAddress()) {
// ok its a valid WIF
let legacyWallet = new LegacyWallet();
legacyWallet.setSecret(importText);
await legacyWallet.fetchBalance();
if (legacyWallet.getBalance() > 0) {
// yep, its legacy we're importing
await legacyWallet.fetchTransactions();
return WalletImport._saveWallet(legacyWallet);
} else {
// by default, we import wif as Segwit P2SH
await segwitWallet.fetchBalance();
await segwitWallet.fetchTransactions();
return WalletImport._saveWallet(segwitWallet);
}
}
// case - WIF is valid, just has uncompressed pubkey
let legacyWallet = new LegacyWallet();
legacyWallet.setSecret(importText);
if (legacyWallet.getAddress()) {
await legacyWallet.fetchBalance();
await legacyWallet.fetchTransactions();
return WalletImport._saveWallet(legacyWallet);
}
// if we're here - nope, its not a valid WIF
let hd1 = new HDLegacyBreadwalletWallet();
hd1.setSecret(importText);
if (hd1.validateMnemonic()) {
await hd1.fetchBalance();
if (hd1.getBalance() > 0) {
await hd1.fetchTransactions();
return WalletImport._saveWallet(hd1);
}
}
let hd2 = new HDSegwitP2SHWallet();
hd2.setSecret(importText);
if (hd2.validateMnemonic()) {
await hd2.fetchBalance();
if (hd2.getBalance() > 0) {
await hd2.fetchTransactions();
return WalletImport._saveWallet(hd2);
}
}
let hd3 = new HDLegacyP2PKHWallet();
hd3.setSecret(importText);
if (hd3.validateMnemonic()) {
await hd3.fetchBalance();
if (hd3.getBalance() > 0) {
await hd3.fetchTransactions();
return WalletImport._saveWallet(hd3);
}
}
// no balances? how about transactions count?
if (hd1.validateMnemonic()) {
await hd1.fetchTransactions();
if (hd1.getTransactions().length !== 0) {
return WalletImport._saveWallet(hd1);
}
}
if (hd2.validateMnemonic()) {
await hd2.fetchTransactions();
if (hd2.getTransactions().length !== 0) {
return WalletImport._saveWallet(hd2);
}
}
if (hd3.validateMnemonic()) {
await hd3.fetchTransactions();
if (hd3.getTransactions().length !== 0) {
return WalletImport._saveWallet(hd3);
}
}
if (hd4.validateMnemonic()) {
await hd4.fetchTransactions();
if (hd4.getTransactions().length !== 0) {
return WalletImport._saveWallet(hd4);
}
}
// is it even valid? if yes we will import as:
if (hd4.validateMnemonic()) {
return WalletImport._saveWallet(hd4);
}
// not valid? maybe its a watch-only address?
let watchOnly = new WatchOnlyWallet();
watchOnly.setSecret(importText);
if (watchOnly.valid()) {
await watchOnly.fetchTransactions();
await watchOnly.fetchBalance();
return WalletImport._saveWallet(watchOnly);
}
// nope?
// TODO: try a raw private key
} catch (Err) {
WalletImport.removePlaceholderWallet(placeholderWallet);
EV(EV.enum.WALLETS_COUNT_CHANGED);
console.warn(Err);
}
WalletImport.removePlaceholderWallet();
WalletImport.addPlaceholderWallet(importText, true);
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
EV(EV.enum.WALLETS_COUNT_CHANGED);
alert(loc.wallets.import.error);
}
}

View file

@ -1,253 +1,54 @@
/* global alert */ /* global alert */
import { import React, { useEffect, useState } from 'react';
SegwitP2SHWallet,
LegacyWallet,
WatchOnlyWallet,
HDLegacyBreadwalletWallet,
HDSegwitP2SHWallet,
HDLegacyP2PKHWallet,
HDSegwitBech32Wallet,
} from '../../class';
import React, { Component } from 'react';
import { KeyboardAvoidingView, Platform, Dimensions, View, TouchableWithoutFeedback, Keyboard } from 'react-native'; import { KeyboardAvoidingView, Platform, Dimensions, View, TouchableWithoutFeedback, Keyboard } from 'react-native';
import { import {
BlueFormMultiInput, BlueFormMultiInput,
BlueButtonLink, BlueButtonLink,
BlueFormLabel, BlueFormLabel,
BlueLoading,
BlueDoneAndDismissKeyboardInputAccessory, BlueDoneAndDismissKeyboardInputAccessory,
BlueButton, BlueButton,
SafeBlueArea, SafeBlueArea,
BlueSpacing20, BlueSpacing20,
BlueNavigationStyle, BlueNavigationStyle,
} from '../../BlueComponents'; } from '../../BlueComponents';
import PropTypes from 'prop-types';
import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import Privacy from '../../Privacy'; import Privacy from '../../Privacy';
let EV = require('../../events'); import { useNavigation, useNavigationParam } from 'react-navigation-hooks';
let A = require('../../analytics'); import WalletImport from '../../class/walletImport';
/** @type {AppStorage} */
let BlueApp = require('../../BlueApp');
let loc = require('../../loc'); let loc = require('../../loc');
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
export default class WalletsImport extends Component { const WalletsImport = () => {
static navigationOptions = { const [isToolbarVisibleForAndroid, setIsToolbarVisibleForAndroid] = useState(false);
...BlueNavigationStyle(), const [importText, setImportText] = useState(useNavigationParam('label') || '');
title: loc.wallets.import.title, const { navigate, dismiss } = useNavigation();
};
constructor(props) { useEffect(() => {
super(props);
this.state = {
isLoading: true,
isToolbarVisibleForAndroid: false,
};
}
componentDidMount() {
this.setState({
isLoading: false,
label: '',
});
Privacy.enableBlur(); Privacy.enableBlur();
} return () => Privacy.disableBlur();
});
componentWillUnmount() { const importButtonPressed = () => {
Privacy.disableBlur(); if (importText.trim().length === 0) {
return;
} }
importMnemonic(importText);
};
async _saveWallet(w) { const importMnemonic = importText => {
if (BlueApp.getWallets().some(wallet => wallet.getSecret() === w.secret)) {
alert('This wallet has been previously imported.');
} else {
alert(loc.wallets.import.success);
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
w.setLabel(loc.wallets.import.imported + ' ' + w.typeReadable);
w.setUserHasSavedExport(true);
BlueApp.wallets.push(w);
await BlueApp.saveToDisk();
EV(EV.enum.WALLETS_COUNT_CHANGED);
A(A.ENUM.CREATED_WALLET);
this.props.navigation.dismiss();
}
}
async importMnemonic(text) {
try { try {
// is it lightning custodian? WalletImport.processImportText(importText);
if (text.indexOf('blitzhub://') !== -1 || text.indexOf('lndhub://') !== -1) { dismiss();
let lnd = new LightningCustodianWallet(); } catch (error) {
if (text.includes('@')) {
const split = text.split('@');
lnd.setBaseURI(split[1]);
lnd.setSecret(split[0]);
} else {
lnd.setBaseURI(LightningCustodianWallet.defaultBaseUri);
lnd.setSecret(text);
}
lnd.init();
await lnd.authorize();
await lnd.fetchTransactions();
await lnd.fetchUserInvoices();
await lnd.fetchPendingTransactions();
await lnd.fetchBalance();
return this._saveWallet(lnd);
}
// trying other wallet types
let hd4 = new HDSegwitBech32Wallet();
hd4.setSecret(text);
if (hd4.validateMnemonic()) {
await hd4.fetchBalance();
if (hd4.getBalance() > 0) {
await hd4.fetchTransactions();
return this._saveWallet(hd4);
}
}
let segwitWallet = new SegwitP2SHWallet();
segwitWallet.setSecret(text);
if (segwitWallet.getAddress()) {
// ok its a valid WIF
let legacyWallet = new LegacyWallet();
legacyWallet.setSecret(text);
await legacyWallet.fetchBalance();
if (legacyWallet.getBalance() > 0) {
// yep, its legacy we're importing
await legacyWallet.fetchTransactions();
return this._saveWallet(legacyWallet);
} else {
// by default, we import wif as Segwit P2SH
await segwitWallet.fetchBalance();
await segwitWallet.fetchTransactions();
return this._saveWallet(segwitWallet);
}
}
// case - WIF is valid, just has uncompressed pubkey
let legacyWallet = new LegacyWallet();
legacyWallet.setSecret(text);
if (legacyWallet.getAddress()) {
await legacyWallet.fetchBalance();
await legacyWallet.fetchTransactions();
return this._saveWallet(legacyWallet);
}
// if we're here - nope, its not a valid WIF
let hd1 = new HDLegacyBreadwalletWallet();
hd1.setSecret(text);
if (hd1.validateMnemonic()) {
await hd1.fetchBalance();
if (hd1.getBalance() > 0) {
await hd1.fetchTransactions();
return this._saveWallet(hd1);
}
}
let hd2 = new HDSegwitP2SHWallet();
hd2.setSecret(text);
if (hd2.validateMnemonic()) {
await hd2.fetchBalance();
if (hd2.getBalance() > 0) {
await hd2.fetchTransactions();
return this._saveWallet(hd2);
}
}
let hd3 = new HDLegacyP2PKHWallet();
hd3.setSecret(text);
if (hd3.validateMnemonic()) {
await hd3.fetchBalance();
if (hd3.getBalance() > 0) {
await hd3.fetchTransactions();
return this._saveWallet(hd3);
}
}
// no balances? how about transactions count?
if (hd1.validateMnemonic()) {
await hd1.fetchTransactions();
if (hd1.getTransactions().length !== 0) {
return this._saveWallet(hd1);
}
}
if (hd2.validateMnemonic()) {
await hd2.fetchTransactions();
if (hd2.getTransactions().length !== 0) {
return this._saveWallet(hd2);
}
}
if (hd3.validateMnemonic()) {
await hd3.fetchTransactions();
if (hd3.getTransactions().length !== 0) {
return this._saveWallet(hd3);
}
}
if (hd4.validateMnemonic()) {
await hd4.fetchTransactions();
if (hd4.getTransactions().length !== 0) {
return this._saveWallet(hd4);
}
}
// is it even valid? if yes we will import as:
if (hd4.validateMnemonic()) {
return this._saveWallet(hd4);
}
// not valid? maybe its a watch-only address?
let watchOnly = new WatchOnlyWallet();
watchOnly.setSecret(text);
if (watchOnly.valid()) {
await watchOnly.fetchTransactions();
await watchOnly.fetchBalance();
return this._saveWallet(watchOnly);
}
// nope?
// TODO: try a raw private key
} catch (Err) {
console.warn(Err);
}
alert(loc.wallets.import.error); alert(loc.wallets.import.error);
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
// Plan:
// 0. check if its HDSegwitBech32Wallet (BIP84)
// 1. check if its HDSegwitP2SHWallet (BIP49)
// 2. check if its HDLegacyP2PKHWallet (BIP44)
// 3. check if its HDLegacyBreadwalletWallet (no BIP, just "m/0")
// 4. check if its Segwit WIF (P2SH)
// 5. check if its Legacy WIF
// 6. check if its address (watch-only wallet)
// 7. check if its private key (segwit address P2SH) TODO
// 7. check if its private key (legacy address) TODO
} }
};
setLabel(text) { const onBarScanned = value => {
this.setState({ setImportText(value);
label: text, importMnemonic(value);
}); /* also, a hack to make screen update new typed text */ };
}
render() {
if (this.state.isLoading) {
return (
<View style={{ flex: 1, paddingTop: 20 }}>
<BlueLoading />
</View>
);
}
return ( return (
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1, paddingTop: 40 }}> <SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1, paddingTop: 40 }}>
@ -256,27 +57,36 @@ export default class WalletsImport extends Component {
<BlueFormLabel>{loc.wallets.import.explanation}</BlueFormLabel> <BlueFormLabel>{loc.wallets.import.explanation}</BlueFormLabel>
<BlueSpacing20 /> <BlueSpacing20 />
<BlueFormMultiInput <BlueFormMultiInput
value={this.state.label} value={importText}
placeholder=""
contextMenuHidden contextMenuHidden
onChangeText={text => { onChangeText={setImportText}
this.setLabel(text);
}}
inputAccessoryViewID={BlueDoneAndDismissKeyboardInputAccessory.InputAccessoryViewID} inputAccessoryViewID={BlueDoneAndDismissKeyboardInputAccessory.InputAccessoryViewID}
onFocus={() => this.setState({ isToolbarVisibleForAndroid: true })} onFocus={() => setIsToolbarVisibleForAndroid(true)}
onBlur={() => this.setState({ isToolbarVisibleForAndroid: false })} onBlur={() => setIsToolbarVisibleForAndroid(false)}
/> />
{Platform.select({ {Platform.select({
ios: ( ios: (
<BlueDoneAndDismissKeyboardInputAccessory <BlueDoneAndDismissKeyboardInputAccessory
onClearTapped={() => this.setState({ label: '' }, () => Keyboard.dismiss())} onClearTapped={() => {
onPasteTapped={text => this.setState({ label: text }, () => Keyboard.dismiss())} setImportText('');
Keyboard.dismiss();
}}
onPasteTapped={text => {
setImportText(text);
Keyboard.dismiss();
}}
/> />
), ),
android: this.state.isToolbarVisibleForAndroid && ( android: isToolbarVisibleForAndroid && (
<BlueDoneAndDismissKeyboardInputAccessory <BlueDoneAndDismissKeyboardInputAccessory
onClearTapped={() => this.setState({ label: '' }, () => Keyboard.dismiss())} onClearTapped={() => {
onPasteTapped={text => this.setState({ label: text }, () => Keyboard.dismiss())} setImportText('');
Keyboard.dismiss();
}}
onPasteTapped={text => {
setImportText(text);
Keyboard.dismiss();
}}
/> />
), ),
})} })}
@ -290,37 +100,26 @@ export default class WalletsImport extends Component {
}} }}
> >
<BlueButton <BlueButton
disabled={!this.state.label} disabled={importText.trim().length === 0}
title={loc.wallets.import.do_import} title={loc.wallets.import.do_import}
buttonStyle={{ buttonStyle={{
width: width / 1.5, width: width / 1.5,
}} }}
onPress={async () => { onPress={importButtonPressed}
if (!this.state.label) {
return;
}
this.setState({ isLoading: true }, async () => {
await this.importMnemonic(this.state.label.trim());
this.setState({ isLoading: false });
});
}}
/> />
<BlueButtonLink <BlueButtonLink
title={loc.wallets.import.scan_qr} title={loc.wallets.import.scan_qr}
onPress={() => { onPress={() => {
this.props.navigation.navigate('ScanQrWif'); navigate('ScanQrAddress', { onBarScanned });
}} }}
/> />
</View> </View>
</SafeBlueArea> </SafeBlueArea>
); );
}
}
WalletsImport.propTypes = {
navigation: PropTypes.shape({
navigate: PropTypes.func,
goBack: PropTypes.func,
dismiss: PropTypes.func,
}),
}; };
WalletsImport.navigationOptions = {
...BlueNavigationStyle(),
title: loc.wallets.import.title,
};
export default WalletsImport;

View file

@ -1,11 +1,13 @@
/* global alert */ /* global alert */
import React, { Component } from 'react'; import React, { Component } from 'react';
import { View, TouchableOpacity, Text, FlatList, InteractionManager, RefreshControl, ScrollView } from 'react-native'; import { View, TouchableOpacity, Text, 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';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { PlaceholderWallet } from '../../class';
import WalletImport from '../../class/walletImport';
let EV = require('../../events'); let EV = require('../../events');
let A = require('../../analytics'); let A = require('../../analytics');
/** @type {AppStorage} */ /** @type {AppStorage} */
@ -135,7 +137,7 @@ export default class WalletsList extends Component {
}, },
() => { () => {
if (scrollToEnd) { if (scrollToEnd) {
this.walletsCarousel.snapToItem(this.state.wallets.length - 1); this.walletsCarousel.snapToItem(this.state.wallets.length - 2);
} }
}, },
); );
@ -152,15 +154,44 @@ export default class WalletsList extends Component {
console.log('click', index); console.log('click', index);
let wallet = BlueApp.wallets[index]; let wallet = BlueApp.wallets[index];
if (wallet) { if (wallet) {
if (wallet.type === PlaceholderWallet.type) {
Alert.alert(
loc.wallets.add.details,
'There was a problem importing this wallet.',
[
{
text: loc.wallets.details.delete,
onPress: () => {
WalletImport.removePlaceholderWallet();
EV(EV.enum.WALLETS_COUNT_CHANGED);
},
style: 'destructive',
},
{
text: 'Try Again',
onPress: () => {
this.props.navigation.navigate('ImportWallet', { label: wallet.getSecret() });
WalletImport.removePlaceholderWallet();
EV(EV.enum.WALLETS_COUNT_CHANGED);
},
style: 'default',
},
],
{ cancelable: false },
);
} else {
this.props.navigation.navigate('WalletTransactions', { this.props.navigation.navigate('WalletTransactions', {
wallet: wallet, wallet: wallet,
key: `WalletTransactions-${wallet.getID()}`, key: `WalletTransactions-${wallet.getID()}`,
}); });
}
} else { } else {
// if its out of index - this must be last card with incentive to create wallet // if its out of index - this must be last card with incentive to create wallet
if (!BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type)) {
this.props.navigation.navigate('AddWallet'); this.props.navigation.navigate('AddWallet');
} }
} }
}
onSnapToItem(index) { onSnapToItem(index) {
console.log('onSnapToItem', index); console.log('onSnapToItem', index);
@ -171,6 +202,10 @@ export default class WalletsList extends Component {
// not the last // not the last
} }
if (this.state.wallets[index].type === PlaceholderWallet.type) {
return;
}
// now, lets try to fetch balance and txs for this wallet in case it has changed // now, lets try to fetch balance and txs for this wallet in case it has changed
this.lazyRefreshWallet(index); this.lazyRefreshWallet(index);
} }
@ -193,7 +228,7 @@ export default class WalletsList extends Component {
let didRefresh = false; let didRefresh = false;
try { try {
if (wallets && wallets[index] && wallets[index].timeToRefreshBalance()) { if (wallets && wallets[index] && wallets[index].type !== PlaceholderWallet.type && wallets[index].timeToRefreshBalance()) {
console.log('snapped to, and now its time to refresh wallet #', index); console.log('snapped to, and now its time to refresh wallet #', index);
await wallets[index].fetchBalance(); await wallets[index].fetchBalance();
if (oldBalance !== wallets[index].getBalance() || wallets[index].getUnconfirmedBalance() !== 0) { if (oldBalance !== wallets[index].getBalance() || wallets[index].getUnconfirmedBalance() !== 0) {
@ -250,7 +285,7 @@ export default class WalletsList extends Component {
}; };
handleLongPress = () => { handleLongPress = () => {
if (BlueApp.getWallets().length > 1) { if (BlueApp.getWallets().length > 1 && !BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type)) {
this.props.navigation.navigate('ReorderWallets'); this.props.navigation.navigate('ReorderWallets');
} else { } else {
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
@ -276,7 +311,14 @@ export default class WalletsList extends Component {
<RefreshControl onRefresh={() => this.refreshTransactions()} refreshing={!this.state.isFlatListRefreshControlHidden} /> <RefreshControl onRefresh={() => this.refreshTransactions()} refreshing={!this.state.isFlatListRefreshControlHidden} />
} }
> >
<BlueHeaderDefaultMain leftText={loc.wallets.list.title} onNewWalletPress={() => this.props.navigation.navigate('AddWallet')} /> <BlueHeaderDefaultMain
leftText={loc.wallets.list.title}
onNewWalletPress={
!BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type)
? () => this.props.navigation.navigate('AddWallet')
: null
}
/>
<WalletsCarousel <WalletsCarousel
removeClippedSubviews={false} removeClippedSubviews={false}
data={this.state.wallets} data={this.state.wallets}

View file

@ -4,7 +4,7 @@ import { SafeBlueArea, BlueNavigationStyle } from '../../BlueComponents';
import SortableList from 'react-native-sortable-list'; import SortableList from 'react-native-sortable-list';
import LinearGradient from 'react-native-linear-gradient'; import LinearGradient from 'react-native-linear-gradient';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet'; import { PlaceholderWallet, LightningCustodianWallet } from '../../class';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import WalletGradient from '../../class/walletGradient'; import WalletGradient from '../../class/walletGradient';
let EV = require('../../events'); let EV = require('../../events');
@ -51,7 +51,7 @@ export default class ReorderWallets extends Component {
}, },
}); });
const wallets = BlueApp.getWallets(); const wallets = BlueApp.getWallets().filter(wallet => wallet.type !== PlaceholderWallet.type);
this.setState({ this.setState({
data: wallets, data: wallets,
isLoading: false, isLoading: false,

View file

@ -1,344 +0,0 @@
/* global alert */
import React from 'react';
import { ActivityIndicator, Image, View, TouchableOpacity } from 'react-native';
import { BlueText, SafeBlueArea, BlueButton } from '../../BlueComponents';
import { RNCamera } from 'react-native-camera';
import { SegwitP2SHWallet, LegacyWallet, WatchOnlyWallet, HDLegacyP2PKHWallet, HDSegwitBech32Wallet } from '../../class';
import PropTypes from 'prop-types';
import { HDSegwitP2SHWallet } from '../../class/hd-segwit-p2sh-wallet';
import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet';
import bip21 from 'bip21';
/** @type {AppStorage} */
let BlueApp = require('../../BlueApp');
let EV = require('../../events');
let bip38 = require('../../bip38');
let wif = require('wif');
let prompt = require('../../prompt');
let loc = require('../../loc');
export default class ScanQrWif extends React.Component {
static navigationOptions = {
header: null,
};
state = { isLoading: false };
onBarCodeScanned = async ret => {
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.pausePreview();
if (+new Date() - this.lastTimeIveBeenHere < 6000) {
this.lastTimeIveBeenHere = +new Date();
return;
}
this.lastTimeIveBeenHere = +new Date();
this.setState({ isLoading: true });
if (ret.data[0] === '6') {
// password-encrypted, need to ask for password and decrypt
console.log('trying to decrypt...');
this.setState({
message: loc.wallets.scanQrWif.decoding,
});
shold_stop_bip38 = undefined; // eslint-disable-line
let password = await prompt(loc.wallets.scanQrWif.input_password, loc.wallets.scanQrWif.password_explain);
if (!password) {
return;
}
let that = this;
try {
let decryptedKey = await bip38.decrypt(ret.data, password, function(status) {
that.setState({
message: loc.wallets.scanQrWif.decoding + '... ' + status.percent.toString().substr(0, 4) + ' %',
});
});
ret.data = wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed);
} catch (e) {
console.log(e.message);
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview();
this.setState({ message: false, isLoading: false });
return alert(loc.wallets.scanQrWif.bad_password);
}
this.setState({ message: false, isLoading: false });
}
for (let w of BlueApp.wallets) {
if (w.getSecret() === ret.data) {
// lookig for duplicates
this.setState({ isLoading: false });
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview();
return alert(loc.wallets.scanQrWif.wallet_already_exists); // duplicate, not adding
}
}
// is it HD BIP49 mnemonic?
let hd = new HDSegwitP2SHWallet();
hd.setSecret(ret.data);
if (hd.validateMnemonic()) {
for (let w of BlueApp.wallets) {
if (w.getSecret() === hd.getSecret()) {
// lookig for duplicates
this.setState({ isLoading: false });
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview();
return alert(loc.wallets.scanQrWif.wallet_already_exists); // duplicate, not adding
}
}
this.setState({ isLoading: true });
hd.setLabel(loc.wallets.import.imported + ' ' + hd.typeReadable);
await hd.fetchBalance();
if (hd.getBalance() !== 0) {
await hd.fetchTransactions();
BlueApp.wallets.push(hd);
await BlueApp.saveToDisk();
alert(loc.wallets.import.success);
this.props.navigation.popToTop();
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500);
this.setState({ isLoading: false });
return;
}
}
// nope
// is it HD legacy (BIP44) mnemonic?
hd = new HDLegacyP2PKHWallet();
hd.setSecret(ret.data);
if (hd.validateMnemonic()) {
for (let w of BlueApp.wallets) {
if (w.getSecret() === hd.getSecret()) {
// lookig for duplicates
this.setState({ isLoading: false });
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview();
return alert(loc.wallets.scanQrWif.wallet_already_exists); // duplicate, not adding
}
}
await hd.fetchTransactions();
if (hd.getTransactions().length !== 0) {
await hd.fetchBalance();
hd.setLabel(loc.wallets.import.imported + ' ' + hd.typeReadable);
BlueApp.wallets.push(hd);
await BlueApp.saveToDisk();
alert(loc.wallets.import.success);
this.props.navigation.popToTop();
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500);
this.setState({ isLoading: false });
return;
}
}
// nope
// is it HD BIP49 mnemonic?
hd = new HDSegwitBech32Wallet();
hd.setSecret(ret.data);
if (hd.validateMnemonic()) {
for (let w of BlueApp.wallets) {
if (w.getSecret() === hd.getSecret()) {
// lookig for duplicates
this.setState({ isLoading: false });
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview();
return alert(loc.wallets.scanQrWif.wallet_already_exists); // duplicate, not adding
}
}
this.setState({ isLoading: true });
hd.setLabel(loc.wallets.import.imported + ' ' + hd.typeReadable);
BlueApp.wallets.push(hd);
await hd.fetchBalance();
await hd.fetchTransactions();
await BlueApp.saveToDisk();
alert(loc.wallets.import.success);
this.props.navigation.popToTop();
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500);
this.setState({ isLoading: false });
return;
}
// nope
// is it lndhub?
if (ret.data.indexOf('blitzhub://') !== -1 || ret.data.indexOf('lndhub://') !== -1) {
this.setState({ isLoading: true });
let lnd = new LightningCustodianWallet();
lnd.setSecret(ret.data);
if (ret.data.includes('@')) {
const split = ret.data.split('@');
lnd.setBaseURI(split[1]);
lnd.init();
lnd.setSecret(split[0]);
}
try {
await lnd.authorize();
await lnd.fetchTransactions();
await lnd.fetchUserInvoices();
await lnd.fetchPendingTransactions();
await lnd.fetchBalance();
} catch (Err) {
console.log(Err);
this.setState({ isLoading: false });
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview();
alert(Err.message);
return;
}
BlueApp.wallets.push(lnd);
lnd.setLabel(loc.wallets.import.imported + ' ' + lnd.typeReadable);
this.props.navigation.popToTop();
alert(loc.wallets.import.success);
await BlueApp.saveToDisk();
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500);
this.setState({ isLoading: false });
return;
}
// nope
// is it just address..?
let watchOnly = new WatchOnlyWallet();
let watchAddr = ret.data;
// Is it BIP21 format?
if (ret.data.indexOf('bitcoin:') === 0 || ret.data.indexOf('BITCOIN:') === 0) {
try {
watchAddr = bip21.decode(ret.data).address;
} catch (err) {
console.log(err);
}
}
if (watchOnly.setSecret(watchAddr) && watchOnly.valid()) {
watchOnly.setLabel(loc.wallets.scanQrWif.imported_watchonly);
BlueApp.wallets.push(watchOnly);
alert(loc.wallets.scanQrWif.imported_watchonly + loc.wallets.scanQrWif.with_address + watchOnly.getAddress());
await watchOnly.fetchBalance();
await watchOnly.fetchTransactions();
await BlueApp.saveToDisk();
this.props.navigation.popToTop();
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500);
this.setState({ isLoading: false });
return;
}
// nope
let newWallet = new SegwitP2SHWallet();
newWallet.setSecret(ret.data);
let newLegacyWallet = new LegacyWallet();
newLegacyWallet.setSecret(ret.data);
if (newWallet.getAddress() === false && newLegacyWallet.getAddress() === false) {
alert(loc.wallets.scanQrWif.bad_wif);
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview();
this.setState({ isLoading: false });
return;
}
if (newWallet.getAddress() === false && newLegacyWallet.getAddress() !== false) {
// case - WIF is valid, just has uncompressed pubkey
newLegacyWallet.setLabel(loc.wallets.scanQrWif.imported_legacy);
BlueApp.wallets.push(newLegacyWallet);
alert(loc.wallets.scanQrWif.imported_wif + ret.data + loc.wallets.scanQrWif.with_address + newLegacyWallet.getAddress());
await newLegacyWallet.fetchBalance();
await newLegacyWallet.fetchTransactions();
await BlueApp.saveToDisk();
this.props.navigation.popToTop();
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500);
return;
}
this.setState({ isLoading: true });
await newLegacyWallet.fetchBalance();
console.log('newLegacyWallet == ', newLegacyWallet.getBalance());
if (newLegacyWallet.getBalance()) {
newLegacyWallet.setLabel(loc.wallets.scanQrWif.imported_legacy);
BlueApp.wallets.push(newLegacyWallet);
alert(loc.wallets.scanQrWif.imported_wif + ret.data + loc.wallets.scanQrWif.with_address + newLegacyWallet.getAddress());
await newLegacyWallet.fetchTransactions();
} else {
await newWallet.fetchBalance();
await newWallet.fetchTransactions();
newWallet.setLabel(loc.wallets.scanQrWif.imported_segwit);
BlueApp.wallets.push(newWallet);
alert(loc.wallets.scanQrWif.imported_wif + ret.data + loc.wallets.scanQrWif.with_address + newWallet.getAddress());
}
await BlueApp.saveToDisk();
this.props.navigation.popToTop();
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500);
}; // end
render() {
if (this.state.isLoading) {
return (
<View style={{ flex: 1, paddingTop: 20, justifyContent: 'center', alignContent: 'center' }}>
<ActivityIndicator />
</View>
);
}
return (
<View style={{ flex: 1 }}>
{(() => {
if (this.state.message) {
return (
<SafeBlueArea>
<View
style={{
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<BlueText>{this.state.message}</BlueText>
<BlueButton
icon={{ name: 'ban', type: 'font-awesome' }}
onPress={async () => {
this.setState({ message: false });
shold_stop_bip38 = true; // eslint-disable-line
}}
title={loc.wallets.scanQrWif.cancel}
/>
</View>
</SafeBlueArea>
);
} else {
return (
<SafeBlueArea style={{ flex: 1 }}>
<RNCamera
captureAudio={false}
androidCameraPermissionOptions={{
title: 'Permission to use camera',
message: 'We need your permission to use your camera',
buttonPositive: 'OK',
buttonNegative: 'Cancel',
}}
style={{ flex: 1, justifyContent: 'space-between' }}
onBarCodeRead={this.onBarCodeScanned}
ref={ref => (this.cameraRef = ref)}
barCodeTypes={[RNCamera.Constants.BarCodeType.qr]}
/>
<TouchableOpacity
style={{
width: 40,
height: 40,
marginLeft: 24,
backgroundColor: '#FFFFFF',
justifyContent: 'center',
borderRadius: 20,
position: 'absolute',
top: 64,
}}
onPress={() => this.props.navigation.goBack(null)}
>
<Image style={{ alignSelf: 'center' }} source={require('../../img/close.png')} />
</TouchableOpacity>
</SafeBlueArea>
);
}
})()}
</View>
);
}
}
ScanQrWif.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.func,
popToTop: PropTypes.func,
navigate: PropTypes.func,
}),
};