BlueWallet/screen/wallets/transactions.js

672 lines
22 KiB
JavaScript
Raw Normal View History

/* global alert */
import React, { Component } from 'react';
import { Chain } from '../../models/bitcoinUnits';
2019-09-19 01:26:28 +02:00
import {
Text,
Platform,
2019-09-29 22:01:27 +02:00
StyleSheet,
2019-09-19 01:26:28 +02:00
View,
2019-09-29 22:01:27 +02:00
Keyboard,
2019-09-19 01:26:28 +02:00
ActivityIndicator,
InteractionManager,
FlatList,
2019-11-02 21:58:55 +01:00
ScrollView,
2019-09-19 01:26:28 +02:00
RefreshControl,
TouchableOpacity,
StatusBar,
2019-09-29 22:01:27 +02:00
Linking,
KeyboardAvoidingView,
2020-01-01 04:31:04 +01:00
Alert,
2019-09-19 01:26:28 +02:00
} from 'react-native';
import PropTypes from 'prop-types';
import { NavigationEvents } from 'react-navigation';
2019-09-29 22:01:27 +02:00
import {
BlueSendButtonIcon,
BlueListItem,
BlueReceiveButtonIcon,
BlueTransactionListItem,
BlueWalletNavigationHeader,
BlueAlertWalletExportReminder,
2019-09-29 22:01:27 +02:00
} from '../../BlueComponents';
import WalletGradient from '../../class/walletGradient';
import { Icon } from 'react-native-elements';
2020-02-26 15:39:19 +01:00
import { LightningCustodianWallet, WatchOnlyWallet } from '../../class';
import Handoff from 'react-native-handoff';
2019-09-29 22:01:27 +02:00
import Modal from 'react-native-modal';
import NavigationService from '../../NavigationService';
/** @type {AppStorage} */
let BlueApp = require('../../BlueApp');
let loc = require('../../loc');
let EV = require('../../events');
let BlueElectrum = require('../../BlueElectrum');
export default class WalletTransactions extends Component {
static navigationOptions = ({ navigation }) => {
2019-10-06 13:40:21 +02:00
return {
headerRight: (
<TouchableOpacity
disabled={navigation.getParam('isLoading') === true}
2019-02-19 01:03:32 +01:00
style={{ marginHorizontal: 16, minWidth: 150, justifyContent: 'center', alignItems: 'flex-end' }}
onPress={() =>
navigation.navigate('WalletDetails', {
wallet: navigation.state.params.wallet,
})
}
>
<Icon name="kebab-horizontal" type="octicon" size={22} color="#FFFFFF" />
</TouchableOpacity>
),
headerStyle: {
backgroundColor: WalletGradient.headerColorFor(navigation.state.params.wallet.type),
borderBottomWidth: 0,
elevation: 0,
shadowRadius: 0,
},
headerTintColor: '#FFFFFF',
2019-10-06 13:40:21 +02:00
};
};
walletBalanceText = null;
constructor(props) {
super(props);
// here, when we receive REMOTE_TRANSACTIONS_COUNT_CHANGED we fetch TXs and balance for current wallet
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED, this.refreshTransactionsFunction.bind(this));
const wallet = props.navigation.getParam('wallet');
this.props.navigation.setParams({ wallet: wallet, isLoading: true });
this.state = {
isLoading: true,
2019-09-29 22:01:27 +02:00
isManageFundsModalVisible: false,
2019-02-17 02:22:14 +01:00
showShowFlatListRefreshControl: false,
wallet: wallet,
dataSource: this.getTransactions(15),
limit: 15,
pageSize: 20,
2019-12-28 17:22:43 +01:00
timeElapsed: 0, // this is to force a re-render for FlatList items.
};
}
componentDidMount() {
this.props.navigation.setParams({ isLoading: false });
2019-12-28 17:22:43 +01:00
this.interval = setInterval(() => {
this.setState(prev => ({ timeElapsed: prev.timeElapsed + 1 }));
}, 60000);
}
/**
* Forcefully fetches TXs and balance for wallet
*/
refreshTransactionsFunction() {
let that = this;
setTimeout(function() {
that.refreshTransactions();
}, 4000); // giving a chance to remote server to propagate
}
/**
* Simple wrapper for `wallet.getTransactions()`, where `wallet` is current wallet.
* Sorts. Provides limiting.
*
* @param limit {Integer} How many txs return, starting from the earliest. Default: all of them.
* @returns {Array}
*/
getTransactions(limit = Infinity) {
let wallet = this.props.navigation.getParam('wallet');
2019-12-25 21:53:53 +01:00
let txs = wallet.getTransactions();
for (let tx of txs) {
tx.sort_ts = +new Date(tx.received);
}
txs = txs.sort(function(a, b) {
return b.sort_ts - a.sort_ts;
});
return txs.slice(0, limit);
}
2019-02-17 02:22:14 +01:00
redrawScreen() {
InteractionManager.runAfterInteractions(async () => {
console.log('wallets/transactions redrawScreen()');
this.setState({
isLoading: false,
2019-02-17 02:22:14 +01:00
showShowFlatListRefreshControl: false,
dataSource: this.getTransactions(this.state.limit),
});
2019-02-17 02:22:14 +01:00
});
}
isLightning() {
let w = this.state.wallet;
2019-12-25 21:53:53 +01:00
if (w && w.chain === Chain.OFFCHAIN) {
return true;
}
return false;
}
/**
* Forcefully fetches TXs and balance for wallet
*/
refreshTransactions() {
2019-02-17 02:22:14 +01:00
if (this.state.isLoading) return;
this.setState(
{
2019-02-17 02:22:14 +01:00
showShowFlatListRefreshControl: true,
isLoading: true,
},
2019-02-17 02:22:14 +01:00
async () => {
let noErr = true;
let smthChanged = false;
try {
2019-07-13 17:21:03 +02:00
await BlueElectrum.ping();
await BlueElectrum.waitTillConnected();
2019-02-17 02:22:14 +01:00
/** @type {LegacyWallet} */
let wallet = this.state.wallet;
let balanceStart = +new Date();
const oldBalance = wallet.getBalance();
await wallet.fetchBalance();
if (oldBalance !== wallet.getBalance()) smthChanged = true;
let balanceEnd = +new Date();
console.log(wallet.getLabel(), 'fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
let start = +new Date();
const oldTxLen = wallet.getTransactions().length;
await wallet.fetchTransactions();
if (wallet.fetchPendingTransactions) {
await wallet.fetchPendingTransactions();
}
2019-02-17 02:22:14 +01:00
if (wallet.fetchUserInvoices) {
await wallet.fetchUserInvoices();
}
if (oldTxLen !== wallet.getTransactions().length) smthChanged = true;
2019-02-17 02:22:14 +01:00
let end = +new Date();
console.log(wallet.getLabel(), 'fetch tx took', (end - start) / 1000, 'sec');
} catch (err) {
noErr = false;
alert(err.message);
2019-02-17 02:22:14 +01:00
this.setState({
isLoading: false,
showShowFlatListRefreshControl: false,
});
}
if (noErr && smthChanged) {
console.log('saving to disk');
await BlueApp.saveToDisk(); // caching
EV(EV.enum.TRANSACTIONS_COUNT_CHANGED); // let other components know they should redraw
}
this.redrawScreen();
},
);
}
_keyExtractor = (_item, index) => index.toString();
renderListFooterComponent = () => {
// if not all txs rendered - display indicator
return (this.getTransactions(Infinity).length > this.state.limit && <ActivityIndicator style={{ marginVertical: 20 }} />) || <View />;
};
renderListHeaderComponent = () => {
return (
2019-12-25 21:53:53 +01:00
<View style={{ flex: 1 }}>
<View style={{ flexDirection: 'row', margin: 16, justifyContent: 'space-evenly' }}>
{/*
So the idea here, due to Apple banning native Lapp marketplace, is:
On Android everythins works as it worked before. Single "Marketplace" button that leads to LappBrowser that
opens /marketplace/ url of offchain wallet type, and /marketplace-btc/ for onchain.
On iOS its more complicated - we have one button that opens same page _externally_ (in Safari), and second
button that opens actual LappBrowser but with _blank_ page. This is important to not trigger Apple.
Blank page is also the way Trust Wallet does it with Dapp Browser.
For ONCHAIN wallet type no LappBrowser button should be displayed, its Lightning-network specific.
*/}
{this.renderMarketplaceButton()}
{this.state.wallet.type === LightningCustodianWallet.type && Platform.OS === 'ios' && this.renderLappBrowserButton()}
</View>
<Text
style={{
2019-02-16 07:16:08 +01:00
flex: 1,
2019-09-19 11:16:12 +02:00
marginLeft: 16,
marginTop: 24,
marginBottom: 8,
fontWeight: 'bold',
fontSize: 24,
color: BlueApp.settings.foregroundColor,
}}
>
{loc.transactions.list.title}
</Text>
</View>
);
};
2019-09-29 22:01:27 +02:00
renderManageFundsModal = () => {
return (
<Modal
isVisible={this.state.isManageFundsModalVisible}
style={styles.bottomModal}
onBackdropPress={() => {
Keyboard.dismiss();
this.setState({ isManageFundsModalVisible: false });
}}
>
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'position' : null}>
<View style={styles.advancedTransactionOptionsModalContent}>
<BlueListItem
hideChevron
component={TouchableOpacity}
onPress={a => {
const wallets = [...BlueApp.getWallets().filter(item => item.chain === Chain.ONCHAIN && item.allowSend())];
if (wallets.length === 0) {
alert('In order to proceed, please create a Bitcoin wallet to refill with.');
} else {
this.setState({ isManageFundsModalVisible: false });
2019-10-01 00:13:22 +02:00
this.props.navigation.navigate('SelectWallet', { onWalletSelect: this.onWalletSelect, chainType: Chain.ONCHAIN });
2019-09-29 22:01:27 +02:00
}
}}
2019-10-01 00:13:22 +02:00
title={loc.lnd.refill}
2019-09-29 22:01:27 +02:00
/>
<BlueListItem
hideChevron
component={TouchableOpacity}
onPress={a => {
2019-10-01 00:13:22 +02:00
this.setState({ isManageFundsModalVisible: false }, () =>
2019-09-29 22:01:27 +02:00
this.props.navigation.navigate('ReceiveDetails', {
secret: this.state.wallet.getSecret(),
2019-10-01 00:13:22 +02:00
}),
);
2019-09-29 22:01:27 +02:00
}}
2019-10-01 00:13:22 +02:00
title={'Refill with External Wallet'}
2019-09-29 22:01:27 +02:00
/>
<BlueListItem
2019-10-01 00:13:22 +02:00
title={loc.lnd.withdraw}
2019-09-29 22:01:27 +02:00
hideChevron
component={TouchableOpacity}
onPress={a => {
this.setState({ isManageFundsModalVisible: false });
Linking.openURL('https://zigzag.io/?utm_source=integration&utm_medium=bluewallet&utm_campaign=withdrawLink');
}}
/>
</View>
</KeyboardAvoidingView>
</Modal>
);
};
2019-10-17 03:51:22 +02:00
renderMarketplaceButton = () => {
return Platform.select({
android: (
<TouchableOpacity
onPress={() => {
if (this.state.wallet.type === LightningCustodianWallet.type) {
this.props.navigation.navigate('LappBrowser', { fromSecret: this.state.wallet.getSecret(), fromWallet: this.state.wallet });
} else {
this.props.navigation.navigate('Marketplace', { fromWallet: this.state.wallet });
}
}}
2019-10-18 16:22:49 +02:00
style={{
backgroundColor: '#f2f2f2',
borderRadius: 9,
minHeight: 49,
flex: 1,
paddingHorizontal: 8,
justifyContent: 'center',
flexDirection: 'row',
alignItems: 'center',
}}
2019-10-17 03:51:22 +02:00
>
2019-10-18 16:22:49 +02:00
<Text style={{ color: '#062453', fontSize: 18 }}>marketplace</Text>
2019-10-17 03:51:22 +02:00
</TouchableOpacity>
),
ios:
this.state.wallet.getBalance() > 0 ? (
<TouchableOpacity
onPress={async () => {
if (this.state.wallet.type === LightningCustodianWallet.type) {
Linking.openURL('https://bluewallet.io/marketplace/');
} else {
let address = await this.state.wallet.getAddressAsync();
Linking.openURL('https://bluewallet.io/marketplace-btc/?address=' + address);
}
}}
2019-10-17 04:08:09 +02:00
style={{
backgroundColor: '#f2f2f2',
borderRadius: 9,
minHeight: 49,
flex: 1,
paddingHorizontal: 8,
justifyContent: 'center',
flexDirection: 'row',
alignItems: 'center',
}}
2019-10-17 03:51:22 +02:00
>
2019-10-17 04:08:09 +02:00
<Icon name="external-link" size={18} type="font-awesome" color="#9aa0aa" />
<Text style={{ color: '#062453', fontSize: 18, marginHorizontal: 8 }}>marketplace</Text>
2019-10-17 03:51:22 +02:00
</TouchableOpacity>
) : null,
});
};
2019-10-21 13:52:38 +02:00
renderLappBrowserButton = () => {
return (
<TouchableOpacity
onPress={() => {
this.props.navigation.navigate('LappBrowser', {
fromSecret: this.state.wallet.getSecret(),
fromWallet: this.state.wallet,
2019-10-23 23:54:09 +02:00
url: 'https://duckduckgo.com',
2019-10-21 13:52:38 +02:00
});
}}
style={{
marginLeft: 5,
backgroundColor: '#f2f2f2',
borderRadius: 9,
minHeight: 49,
flex: 1,
paddingHorizontal: 8,
justifyContent: 'center',
flexDirection: 'row',
alignItems: 'center',
}}
>
<Text style={{ color: '#062453', fontSize: 18 }}>LApp Browser</Text>
</TouchableOpacity>
);
};
2019-09-29 22:01:27 +02:00
onWalletSelect = async wallet => {
if (wallet) {
NavigationService.navigate('WalletTransactions', {
key: `WalletTransactions-${wallet.getID()}`,
});
/** @type {LightningCustodianWallet} */
let toAddress = false;
if (this.state.wallet.refill_addressess.length > 0) {
2019-09-29 22:01:27 +02:00
toAddress = this.state.wallet.refill_addressess[0];
} else {
try {
await this.state.wallet.fetchBtcAddress();
toAddress = this.state.wallet.refill_addressess[0];
} catch (Err) {
return alert(Err.message);
}
2019-09-29 22:01:27 +02:00
}
this.props.navigation.navigate('SendDetails', {
memo: loc.lnd.refill_lnd_balance,
fromSecret: wallet.getSecret(),
address: toAddress,
fromWallet: wallet,
});
}
};
2020-01-01 04:31:04 +01:00
onWillBlur() {
StatusBar.setBarStyle('dark-content');
}
componentWillUnmount() {
this.onWillBlur();
2019-12-28 17:22:43 +01:00
clearInterval(this.interval);
}
2020-01-01 04:31:04 +01:00
navigateToSendScreen = () => {
this.props.navigation.navigate('SendDetails', {
fromAddress: this.state.wallet.getAddress(),
fromSecret: this.state.wallet.getSecret(),
fromWallet: this.state.wallet,
});
};
2019-01-30 03:13:45 +01:00
renderItem = item => {
2019-12-28 17:22:43 +01:00
return (
<BlueTransactionListItem
item={item.item}
itemPriceUnit={this.state.wallet.getPreferredBalanceUnit()}
shouldRefresh={this.state.timeElapsed}
/>
);
};
render() {
const { navigate } = this.props.navigation;
return (
<View style={{ flex: 1 }}>
{this.state.wallet.chain === Chain.ONCHAIN && (
<Handoff
title={`Bitcoin Wallet ${this.state.wallet.getLabel()}`}
type="io.bluewallet.bluewallet"
url={`https://blockpath.com/search/addr?q=${this.state.wallet.getXpub()}`}
/>
)}
2018-12-12 04:33:28 +01:00
<NavigationEvents
onWillFocus={() => {
StatusBar.setBarStyle('light-content');
2019-02-17 02:22:14 +01:00
this.redrawScreen();
2018-12-12 04:33:28 +01:00
}}
onWillBlur={() => this.onWillBlur()}
onDidFocus={() => this.props.navigation.setParams({ isLoading: false })}
2018-12-12 04:33:28 +01:00
/>
2019-08-04 08:42:05 +02:00
<BlueWalletNavigationHeader
wallet={this.state.wallet}
onWalletUnitChange={wallet =>
InteractionManager.runAfterInteractions(async () => {
2019-08-31 08:37:34 +02:00
this.setState({ wallet }, () => InteractionManager.runAfterInteractions(() => BlueApp.saveToDisk()));
2019-08-04 08:42:05 +02:00
})
}
onManageFundsPressed={() => {
if (this.state.wallet.getUserHasSavedExport()) {
this.setState({ isManageFundsModalVisible: true });
} else {
BlueAlertWalletExportReminder({
onSuccess: async () => {
this.state.wallet.setUserHasSavedExport(true);
await BlueApp.saveToDisk();
this.setState({ isManageFundsModalVisible: true });
},
onFailure: () =>
this.props.navigation.navigate('WalletExport', {
address: this.state.wallet.getAddress(),
secret: this.state.wallet.getSecret(),
}),
});
}
}}
2019-08-04 08:42:05 +02:00
/>
2019-12-25 21:53:53 +01:00
<View style={{ backgroundColor: '#FFFFFF', flex: 1 }}>
<FlatList
2019-12-25 21:53:53 +01:00
ListHeaderComponent={this.renderListHeaderComponent}
onEndReachedThreshold={0.3}
2019-12-25 21:53:53 +01:00
onEndReached={async () => {
// pagination in works. in this block we will add more txs to flatlist
// so as user scrolls closer to bottom it will render mode transactions
if (this.getTransactions(Infinity).length < this.state.limit) {
// all list rendered. nop
return;
}
this.setState({
dataSource: this.getTransactions(this.state.limit + this.state.pageSize),
limit: this.state.limit + this.state.pageSize,
pageSize: this.state.pageSize * 2,
});
}}
ListFooterComponent={this.renderListFooterComponent}
ListEmptyComponent={
2019-09-19 01:26:28 +02:00
<ScrollView style={{ minHeight: 100 }} contentContainerStyle={{ flex: 1, justifyContent: 'center', paddingHorizontal: 16 }}>
<Text
numberOfLines={0}
style={{
fontSize: 18,
color: '#9aa0aa',
textAlign: 'center',
}}
>
2019-03-30 20:32:58 +01:00
{(this.isLightning() && loc.wallets.list.empty_txs1_lightning) || loc.wallets.list.empty_txs1}
</Text>
<Text
style={{
fontSize: 18,
color: '#9aa0aa',
textAlign: 'center',
}}
>
2019-03-30 20:32:58 +01:00
{(this.isLightning() && loc.wallets.list.empty_txs2_lightning) || loc.wallets.list.empty_txs2}
</Text>
<Text />
<Text />
{!this.isLightning() && (
2018-10-10 21:36:32 +02:00
<Text
style={{
fontSize: 18,
color: '#9aa0aa',
textAlign: 'center',
textDecorationLine: 'underline',
2018-10-10 21:36:32 +02:00
}}
onPress={() =>
this.props.navigation.navigate('BuyBitcoin', {
address: this.state.wallet.getAddress(),
secret: this.state.wallet.getSecret(),
})
}
2018-10-10 21:36:32 +02:00
>
{loc.wallets.list.tap_here_to_buy}
2018-10-10 21:36:32 +02:00
</Text>
)}
</ScrollView>
}
2019-02-17 02:22:14 +01:00
refreshControl={
<RefreshControl onRefresh={() => this.refreshTransactions()} refreshing={this.state.showShowFlatListRefreshControl} />
}
extraData={this.state.dataSource}
data={this.state.dataSource}
keyExtractor={this._keyExtractor}
2019-01-30 03:13:45 +01:00
renderItem={this.renderItem}
contentInset={{ top: 0, left: 0, bottom: 90, right: 0 }}
/>
2019-09-29 22:01:27 +02:00
{this.renderManageFundsModal()}
</View>
<View
style={{
flexDirection: 'row',
alignSelf: 'center',
backgroundColor: 'transparent',
position: 'absolute',
bottom: 30,
2019-01-05 03:14:23 +01:00
borderRadius: 30,
minHeight: 48,
overflow: 'hidden',
}}
>
{(() => {
if (this.state.wallet.allowReceive()) {
return (
<BlueReceiveButtonIcon
onPress={() => {
2019-12-25 21:53:53 +01:00
if (this.state.wallet.chain === Chain.OFFCHAIN) {
2018-12-25 17:34:51 +01:00
navigate('LNDCreateInvoice', { fromWallet: this.state.wallet });
} else {
2019-09-27 16:49:56 +02:00
navigate('ReceiveDetails', { secret: this.state.wallet.getSecret() });
}
}}
/>
);
}
})()}
{(() => {
2020-01-01 04:31:04 +01:00
if (
this.state.wallet.allowSend() ||
2020-02-26 15:39:19 +01:00
(this.state.wallet.type === WatchOnlyWallet.type &&
this.state.wallet.isHd() &&
this.state.wallet.getSecret().startsWith('zpub'))
2020-01-01 04:31:04 +01:00
) {
return (
<BlueSendButtonIcon
onPress={() => {
2019-12-25 21:53:53 +01:00
if (this.state.wallet.chain === Chain.OFFCHAIN) {
navigate('ScanLndInvoice', { fromSecret: this.state.wallet.getSecret() });
} else {
2020-01-01 04:31:04 +01:00
if (
2020-02-26 15:39:19 +01:00
this.state.wallet.type === WatchOnlyWallet.type &&
this.state.wallet.isHd() &&
this.state.wallet.getSecret().startsWith('zpub')
2020-01-01 04:31:04 +01:00
) {
2020-02-26 15:39:19 +01:00
if (this.state.wallet.useWithHardwareWalletEnabled()) {
2020-01-01 04:31:04 +01:00
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,
2020-01-05 02:03:02 +01:00
onPress: () => {
const wallet = this.state.wallet;
2020-02-26 15:39:19 +01:00
wallet.setUseWithHardwareWalletEnabled(true);
2020-01-05 02:03:02 +01:00
this.setState({ wallet }, async () => {
await BlueApp.saveToDisk();
this.navigateToSendScreen();
2020-02-24 22:45:14 +01:00
});
2020-01-01 04:31:04 +01:00
},
style: 'default',
},
{ text: loc.send.details.cancel, onPress: () => {}, style: 'cancel' },
],
{ cancelable: false },
);
}
} else {
this.navigateToSendScreen();
}
}
}}
/>
);
}
})()}
</View>
</View>
);
}
}
2019-09-29 22:01:27 +02:00
const styles = StyleSheet.create({
modalContent: {
backgroundColor: '#FFFFFF',
padding: 22,
justifyContent: 'center',
alignItems: 'center',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
borderColor: 'rgba(0, 0, 0, 0.1)',
minHeight: 200,
height: 200,
},
advancedTransactionOptionsModalContent: {
backgroundColor: '#FFFFFF',
padding: 22,
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
borderColor: 'rgba(0, 0, 0, 0.1)',
minHeight: 130,
},
bottomModal: {
justifyContent: 'flex-end',
margin: 0,
},
});
WalletTransactions.propTypes = {
navigation: PropTypes.shape({
navigate: PropTypes.func,
goBack: PropTypes.func,
getParam: PropTypes.func,
setParams: PropTypes.func,
}),
};