BlueWallet/screen/receive/details.js

548 lines
18 KiB
JavaScript
Raw Normal View History

import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import {
BackHandler,
InteractionManager,
Keyboard,
2020-11-17 09:43:38 +01:00
KeyboardAvoidingView,
Platform,
ScrollView,
2020-11-17 09:43:38 +01:00
StatusBar,
StyleSheet,
TextInput,
View,
} from 'react-native';
import { useNavigation, useRoute, useTheme, useFocusEffect } from '@react-navigation/native';
2020-11-17 09:43:38 +01:00
import Share from 'react-native-share';
import QRCodeComponent from '../../components/QRCodeComponent';
2019-01-21 14:55:39 +01:00
import {
2020-11-30 05:18:54 +01:00
BlueLoading,
2019-01-21 14:55:39 +01:00
BlueCopyTextToClipboard,
BlueButton,
2020-11-30 05:18:54 +01:00
BlueButtonLink,
2019-12-15 07:10:22 +01:00
BlueText,
BlueSpacing20,
BlueAlertWalletExportReminder,
2021-04-21 12:50:51 +02:00
BlueCard,
BlueSpacing40,
2019-01-21 14:55:39 +01:00
} from '../../BlueComponents';
2020-12-25 17:09:53 +01:00
import navigationStyle from '../../components/navigationStyle';
2020-11-17 09:43:38 +01:00
import BottomModal from '../../components/BottomModal';
2019-12-15 07:10:22 +01:00
import { Chain, BitcoinUnit } from '../../models/bitcoinUnits';
2021-01-22 17:34:47 +01:00
import HandoffComponent from '../../components/handoff';
import AmountInput from '../../components/AmountInput';
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
import loc, { formatBalance } from '../../loc';
import { BlueStorageContext } from '../../blue_modules/storage-context';
import Notifications from '../../blue_modules/notifications';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { TransactionPendingIconBig } from '../../components/TransactionPendingIconBig';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
2022-02-23 17:13:20 +01:00
import { SuccessView } from '../send/success';
2020-06-09 16:08:18 +02:00
const currency = require('../../blue_modules/currency');
2018-01-30 23:42:38 +01:00
2020-04-20 06:03:36 +02:00
const ReceiveDetails = () => {
2021-04-17 19:43:39 +02:00
const { walletID, address } = useRoute().params;
const { wallets, saveToDisk, sleep, isElectrumDisabled, fetchAndSaveWalletTransactions } = useContext(BlueStorageContext);
2020-11-03 21:23:34 +01:00
const wallet = wallets.find(w => w.getID() === walletID);
2020-04-20 06:03:36 +02:00
const [customLabel, setCustomLabel] = useState();
const [customAmount, setCustomAmount] = useState();
2020-06-09 16:08:18 +02:00
const [customUnit, setCustomUnit] = useState(BitcoinUnit.BTC);
2020-04-20 06:03:36 +02:00
const [bip21encoded, setBip21encoded] = useState();
const [isCustom, setIsCustom] = useState(false);
const [isCustomModalVisible, setIsCustomModalVisible] = useState(false);
const [showPendingBalance, setShowPendingBalance] = useState(false);
const [showConfirmedBalance, setShowConfirmedBalance] = useState(false);
const [showAddress, setShowAddress] = useState(false);
2021-04-17 19:43:39 +02:00
const { navigate, goBack, setParams } = useNavigation();
2020-07-15 19:32:59 +02:00
const { colors } = useTheme();
const [intervalMs, setIntervalMs] = useState(5000);
const [eta, setEta] = useState('');
const [initialConfirmed, setInitialConfirmed] = useState(0);
const [initialUnconfirmed, setInitialUnconfirmed] = useState(0);
const [displayBalance, setDisplayBalance] = useState('');
const fetchAddressInterval = useRef();
2021-08-29 18:28:56 +02:00
const stylesHook = StyleSheet.create({
2020-07-15 19:32:59 +02:00
modalContent: {
backgroundColor: colors.modal,
2020-07-15 19:32:59 +02:00
borderTopColor: colors.foregroundColor,
borderWidth: colors.borderWidth,
},
customAmount: {
borderColor: colors.formBorder,
borderBottomColor: colors.formBorder,
backgroundColor: colors.inputBackgroundColor,
2020-07-15 19:32:59 +02:00
},
customAmountText: {
color: colors.foregroundColor,
2020-07-15 19:32:59 +02:00
},
root: {
backgroundColor: colors.elevated,
2020-07-15 19:32:59 +02:00
},
2021-08-29 18:28:56 +02:00
rootBackgroundColor: {
backgroundColor: colors.elevated,
},
2020-07-15 19:32:59 +02:00
amount: {
color: colors.foregroundColor,
2020-07-15 19:32:59 +02:00
},
label: {
color: colors.foregroundColor,
2020-07-15 19:32:59 +02:00
},
modalButton: {
backgroundColor: colors.modalButton,
2020-07-15 19:32:59 +02:00
},
});
2018-01-30 23:42:38 +01:00
2021-08-29 18:28:56 +02:00
useEffect(() => {
if (showConfirmedBalance) {
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
}
}, [showConfirmedBalance]);
// re-fetching address balance periodically
useEffect(() => {
console.log('receive/defails - useEffect');
if (fetchAddressInterval.current) {
// interval already exists, lets cleanup it and recreate, so theres no duplicate intervals
clearInterval(fetchAddressInterval.current);
fetchAddressInterval.current = undefined;
}
fetchAddressInterval.current = setInterval(async () => {
try {
const decoded = DeeplinkSchemaMatch.bip21decode(bip21encoded);
const address2use = address || decoded.address;
if (!address2use) return;
console.log('checking address', address2use, 'for balance...');
const balance = await BlueElectrum.getBalanceByAddress(address2use);
console.log('...got', balance);
if (balance.unconfirmed > 0) {
if (initialConfirmed === 0 && initialUnconfirmed === 0) {
// saving initial values for later (when tx gets confirmed)
setInitialConfirmed(balance.confirmed);
setInitialUnconfirmed(balance.unconfirmed);
setIntervalMs(25000);
2021-08-29 18:28:56 +02:00
ReactNativeHapticFeedback.trigger('impactHeavy', { ignoreAndroidSystemSettings: false });
}
const txs = await BlueElectrum.getMempoolTransactionsByAddress(address2use);
const tx = txs.pop();
if (tx) {
const rez = await BlueElectrum.multiGetTransactionByTxid([tx.tx_hash], 10, true);
if (rez && rez[tx.tx_hash] && rez[tx.tx_hash].vsize) {
const satPerVbyte = Math.round(tx.fee / rez[tx.tx_hash].vsize);
const fees = await BlueElectrum.estimateFees();
if (satPerVbyte >= fees.fast) {
setEta(loc.formatString(loc.transactions.eta_10m));
}
if (satPerVbyte >= fees.medium && satPerVbyte < fees.fast) {
setEta(loc.formatString(loc.transactions.eta_3h));
}
if (satPerVbyte < fees.medium) {
setEta(loc.formatString(loc.transactions.eta_1d));
}
}
}
setDisplayBalance(
loc.formatString(loc.transactions.pending_with_amount, {
amt1: formatBalance(balance.unconfirmed, BitcoinUnit.LOCAL_CURRENCY, true).toString(),
amt2: formatBalance(balance.unconfirmed, BitcoinUnit.BTC, true).toString(),
}),
);
setShowPendingBalance(true);
setShowAddress(false);
} else if (balance.unconfirmed === 0 && initialUnconfirmed !== 0) {
// now, handling a case when unconfirmed == 0, but in past it wasnt (i.e. it changed while user was
// staring at the screen)
const balanceToShow = balance.confirmed - initialConfirmed;
if (balanceToShow > 0) {
// address has actually more coins then initially, so we definately gained something
setShowConfirmedBalance(true);
setShowPendingBalance(false);
setShowAddress(false);
clearInterval(fetchAddressInterval.current);
fetchAddressInterval.current = undefined;
setDisplayBalance(
loc.formatString(loc.transactions.received_with_amount, {
amt1: formatBalance(balanceToShow, BitcoinUnit.LOCAL_CURRENCY, true).toString(),
amt2: formatBalance(balanceToShow, BitcoinUnit.BTC, true).toString(),
}),
);
fetchAndSaveWalletTransactions(walletID);
} else {
// rare case, but probable. transaction evicted from mempool (maybe cancelled by the sender)
setShowConfirmedBalance(false);
setShowPendingBalance(false);
setShowAddress(true);
}
}
} catch (error) {
console.log(error);
}
}, intervalMs);
}, [bip21encoded, address, initialConfirmed, initialUnconfirmed, intervalMs, fetchAndSaveWalletTransactions, walletID]);
const renderConfirmedBalance = () => {
return (
2021-08-29 18:28:56 +02:00
<ScrollView style={stylesHook.rootBackgroundColors} centerContent keyboardShouldPersistTaps="always">
<View style={styles.scrollBody}>
{isCustom && (
<>
<BlueText style={[styles.label, stylesHook.label]} numberOfLines={1}>
{customLabel}
</BlueText>
</>
)}
2022-02-23 17:13:20 +01:00
<SuccessView />
<BlueText style={[styles.label, stylesHook.label]} numberOfLines={1}>
{displayBalance}
</BlueText>
</View>
</ScrollView>
);
};
const renderPendingBalance = () => {
return (
2021-08-29 18:28:56 +02:00
<ScrollView contentContainerStyle={stylesHook.rootBackgroundColor} centerContent keyboardShouldPersistTaps="always">
<View style={styles.scrollBody}>
{isCustom && (
<>
<BlueText style={[styles.label, stylesHook.label]} numberOfLines={1}>
{customLabel}
</BlueText>
</>
)}
2021-08-29 18:28:56 +02:00
<TransactionPendingIconBig />
<BlueSpacing40 />
<BlueText style={[styles.label, stylesHook.label]} numberOfLines={1}>
{displayBalance}
</BlueText>
<BlueText style={[styles.label, stylesHook.label]} numberOfLines={1}>
{eta}
</BlueText>
</View>
</ScrollView>
);
};
const handleBackButton = () => {
goBack(null);
return true;
};
useEffect(() => {
BackHandler.addEventListener('hardwareBackPress', handleBackButton);
return () => {
BackHandler.removeEventListener('hardwareBackPress', handleBackButton);
clearInterval(fetchAddressInterval.current);
fetchAddressInterval.current = undefined;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const renderReceiveDetails = () => {
return (
<ScrollView contentContainerStyle={[styles.root, stylesHook.root]} keyboardShouldPersistTaps="always">
<View style={styles.scrollBody}>
{isCustom && (
<>
2021-11-08 16:31:25 +01:00
{getDisplayAmount() && (
<BlueText testID="CustomAmountText" style={[styles.amount, stylesHook.amount]} numberOfLines={1}>
2021-11-08 16:31:25 +01:00
{getDisplayAmount()}
</BlueText>
)}
{customLabel?.length > 0 && (
<BlueText testID="CustomAmountDescriptionText" style={[styles.label, stylesHook.label]} numberOfLines={1}>
2021-11-08 16:31:25 +01:00
{customLabel}
</BlueText>
)}
</>
)}
2021-08-27 03:31:36 +02:00
<QRCodeComponent value={bip21encoded} />
<BlueCopyTextToClipboard text={isCustom ? bip21encoded : address} />
</View>
<View style={styles.share}>
2021-04-21 14:25:00 +02:00
<BlueCard>
<BlueButtonLink
style={styles.link}
2021-04-21 14:25:00 +02:00
testID="SetCustomAmountButton"
title={loc.receive.details_setAmount}
onPress={showCustomAmountModal}
/>
<BlueButton onPress={handleShareButtonPressed} title={loc.receive.details_share} />
2021-04-21 12:50:51 +02:00
</BlueCard>
</View>
{renderCustomAmountModal()}
</ScrollView>
);
};
const obtainWalletAddress = useCallback(async () => {
2020-04-20 06:03:36 +02:00
console.log('receive/details - componentDidMount');
wallet.setUserHasSavedExport(true);
await saveToDisk();
2021-04-17 19:43:39 +02:00
let newAddress;
if (address) {
setAddressBIP21Encoded(address);
await Notifications.tryToObtainPermissions();
Notifications.majorTomToGroundControl([address], [], []);
} else {
2020-04-20 06:03:36 +02:00
if (wallet.chain === Chain.ONCHAIN) {
try {
2021-08-24 07:00:57 +02:00
if (!isElectrumDisabled) newAddress = await Promise.race([wallet.getAddressAsync(), sleep(1000)]);
} catch (_) {}
2021-04-19 18:25:20 +02:00
if (newAddress === undefined) {
// either sleep expired or getAddressAsync threw an exception
console.warn('either sleep expired or getAddressAsync threw an exception');
2021-04-17 19:43:39 +02:00
newAddress = wallet._getExternalAddressByIndex(wallet.getNextFreeAddressIndex());
} else {
saveToDisk(); // caching whatever getAddressAsync() generated internally
2019-10-01 00:13:22 +02:00
}
2020-04-20 06:03:36 +02:00
} else if (wallet.chain === Chain.OFFCHAIN) {
try {
await Promise.race([wallet.getAddressAsync(), sleep(1000)]);
2021-04-17 19:43:39 +02:00
newAddress = wallet.getAddress();
} catch (_) {}
2021-04-19 18:25:20 +02:00
if (newAddress === undefined) {
// either sleep expired or getAddressAsync threw an exception
console.warn('either sleep expired or getAddressAsync threw an exception');
2021-04-17 19:43:39 +02:00
newAddress = wallet.getAddress();
} else {
saveToDisk(); // caching whatever getAddressAsync() generated internally
2019-09-12 04:05:01 +02:00
}
2019-05-02 22:33:03 +02:00
}
2021-04-17 19:43:39 +02:00
setAddressBIP21Encoded(newAddress);
await Notifications.tryToObtainPermissions();
2021-04-17 19:43:39 +02:00
Notifications.majorTomToGroundControl([newAddress], [], []);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const setAddressBIP21Encoded = address => {
const bip21encoded = DeeplinkSchemaMatch.bip21encode(address);
2021-04-17 19:43:39 +02:00
setParams({ address });
setBip21encoded(bip21encoded);
setShowAddress(true);
};
useFocusEffect(
useCallback(() => {
const task = InteractionManager.runAfterInteractions(async () => {
if (wallet) {
if (!wallet.getUserHasSavedExport()) {
BlueAlertWalletExportReminder({
onSuccess: obtainWalletAddress,
onFailure: () => {
goBack();
navigate('WalletExportRoot', {
screen: 'WalletExport',
params: {
walletID: wallet.getID(),
},
});
},
});
} else {
obtainWalletAddress();
}
} else if (!wallet && address) {
setAddressBIP21Encoded(address);
}
});
return () => {
task.cancel();
};
2021-04-17 19:43:39 +02:00
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [wallet]),
);
2018-01-30 23:42:38 +01:00
2020-04-20 06:03:36 +02:00
const dismissCustomAmountModal = () => {
Keyboard.dismiss();
setIsCustomModalVisible(false);
};
2020-04-20 06:03:36 +02:00
const showCustomAmountModal = () => {
setIsCustomModalVisible(true);
};
const createCustomAmountAddress = () => {
setIsCustom(true);
setIsCustomModalVisible(false);
2020-06-09 16:08:18 +02:00
let amount = customAmount;
switch (customUnit) {
case BitcoinUnit.BTC:
// nop
break;
case BitcoinUnit.SATS:
amount = currency.satoshiToBTC(customAmount);
break;
case BitcoinUnit.LOCAL_CURRENCY:
if (AmountInput.conversionCache[amount + BitcoinUnit.LOCAL_CURRENCY]) {
2020-06-09 16:08:18 +02:00
// cache hit! we reuse old value that supposedly doesnt have rounding errors
amount = currency.satoshiToBTC(AmountInput.conversionCache[amount + BitcoinUnit.LOCAL_CURRENCY]);
2020-06-09 16:08:18 +02:00
} else {
amount = currency.fiatToBTC(customAmount);
}
break;
}
2021-11-08 16:31:25 +01:00
setBip21encoded(DeeplinkSchemaMatch.bip21encode(address, { amount, label: customLabel }));
2020-07-23 15:28:22 +02:00
setShowAddress(true);
2020-04-20 06:03:36 +02:00
};
const renderCustomAmountModal = () => {
2019-12-15 07:10:22 +01:00
return (
2020-11-17 09:43:38 +01:00
<BottomModal isVisible={isCustomModalVisible} onClose={dismissCustomAmountModal}>
2021-02-25 02:56:06 +01:00
<KeyboardAvoidingView enabled={!Platform.isPad} behavior={Platform.OS === 'ios' ? 'position' : null}>
<View style={[styles.modalContent, stylesHook.modalContent]}>
<AmountInput unit={customUnit} amount={customAmount || ''} onChangeText={setCustomAmount} onAmountUnitChange={setCustomUnit} />
<View style={[styles.customAmount, stylesHook.customAmount]}>
2019-12-15 07:10:22 +01:00
<TextInput
2020-04-20 06:03:36 +02:00
onChangeText={setCustomLabel}
2020-06-09 17:55:19 +02:00
placeholderTextColor="#81868e"
2020-07-20 15:38:46 +02:00
placeholder={loc.receive.details_label}
2020-04-20 06:03:36 +02:00
value={customLabel || ''}
2019-12-15 07:10:22 +01:00
numberOfLines={1}
style={[styles.customAmountText, stylesHook.customAmountText]}
testID="CustomAmountDescription"
2019-12-15 07:10:22 +01:00
/>
</View>
<BlueSpacing20 />
<View>
<BlueButton
testID="CustomAmountSaveButton"
style={[styles.modalButton, stylesHook.modalButton]}
title={loc.receive.details_create}
onPress={createCustomAmountAddress}
/>
2019-12-15 07:10:22 +01:00
<BlueSpacing20 />
</View>
<BlueSpacing20 />
</View>
</KeyboardAvoidingView>
2020-11-17 09:43:38 +01:00
</BottomModal>
2019-12-15 07:10:22 +01:00
);
};
2020-04-20 06:03:36 +02:00
const handleShareButtonPressed = () => {
2020-07-06 23:53:08 +02:00
Share.open({ message: bip21encoded }).catch(error => console.log(error));
2019-12-15 07:10:22 +01:00
};
2020-06-09 16:08:18 +02:00
/**
* @returns {string} BTC amount, accounting for current `customUnit` and `customUnit`
*/
const getDisplayAmount = () => {
if (Number(customAmount) > 0) {
switch (customUnit) {
case BitcoinUnit.BTC:
return customAmount + ' BTC';
case BitcoinUnit.SATS:
return currency.satoshiToBTC(customAmount) + ' BTC';
case BitcoinUnit.LOCAL_CURRENCY:
return currency.fiatToBTC(customAmount) + ' BTC';
}
return customAmount + ' ' + customUnit;
} else {
return null;
2020-06-09 16:08:18 +02:00
}
};
2020-04-20 06:03:36 +02:00
return (
<View style={[styles.root, stylesHook.root]}>
<StatusBar barStyle="light-content" />
2021-01-19 04:40:11 +01:00
{address !== undefined && showAddress && (
2022-10-31 13:25:26 +01:00
<HandoffComponent title={loc.send.details_address} type={HandoffComponent.activityTypes.ReceiveOnchain} userInfo={{ address }} />
2020-04-20 06:03:36 +02:00
)}
{showConfirmedBalance ? renderConfirmedBalance() : null}
{showPendingBalance ? renderPendingBalance() : null}
{showAddress ? renderReceiveDetails() : null}
{!showAddress && !showPendingBalance && !showConfirmedBalance ? <BlueLoading /> : null}
2020-07-15 19:32:59 +02:00
</View>
2020-04-20 06:03:36 +02:00
);
};
2021-08-29 18:28:56 +02:00
const styles = StyleSheet.create({
modalContent: {
padding: 22,
justifyContent: 'center',
alignItems: 'center',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
minHeight: 350,
height: 350,
},
customAmount: {
flexDirection: 'row',
borderWidth: 1.0,
borderBottomWidth: 0.5,
minHeight: 44,
height: 44,
marginHorizontal: 20,
alignItems: 'center',
marginVertical: 8,
borderRadius: 4,
},
root: {
flexGrow: 1,
justifyContent: 'space-between',
},
scrollBody: {
marginTop: 32,
flexGrow: 1,
alignItems: 'center',
paddingHorizontal: 16,
},
share: {
justifyContent: 'flex-end',
paddingVertical: 16,
alignItems: 'center',
marginBottom: 8,
},
link: {
marginVertical: 16,
paddingHorizontal: 32,
},
amount: {
fontWeight: '600',
fontSize: 36,
textAlign: 'center',
},
label: {
fontWeight: '600',
textAlign: 'center',
paddingBottom: 24,
},
modalButton: {
paddingVertical: 14,
paddingHorizontal: 70,
maxWidth: '80%',
borderRadius: 50,
fontWeight: '700',
},
customAmountText: {
flex: 1,
marginHorizontal: 8,
minHeight: 33,
},
2021-08-29 18:28:56 +02:00
});
2021-02-15 09:03:54 +01:00
ReceiveDetails.navigationOptions = navigationStyle(
{
closeButton: true,
2021-09-13 19:43:26 +02:00
headerHideBackButton: true,
2021-02-15 09:03:54 +01:00
},
opts => ({ ...opts, title: loc.receive.header }),
);
2020-04-20 06:03:36 +02:00
export default ReceiveDetails;