import React, { useEffect, useState, useCallback, useContext, useRef, useMemo, useLayoutEffect } from 'react';
import {
View,
Text,
TextInput,
Alert,
KeyboardAvoidingView,
TouchableOpacity,
Keyboard,
TouchableWithoutFeedback,
Switch,
Platform,
Linking,
StyleSheet,
StatusBar,
ScrollView,
PermissionsAndroid,
InteractionManager,
ActivityIndicator,
I18nManager,
} from 'react-native';
import { BlueCard, BlueLoading, BlueSpacing10, BlueSpacing20, BlueText, SecondButton, BlueListItem } from '../../BlueComponents';
import navigationStyle from '../../components/navigationStyle';
import { LightningCustodianWallet } from '../../class/wallets/lightning-custodian-wallet';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import Biometric from '../../class/biometrics';
import {
HDSegwitBech32Wallet,
SegwitP2SHWallet,
LegacyWallet,
SegwitBech32Wallet,
WatchOnlyWallet,
MultisigHDWallet,
HDAezeedWallet,
LightningLdkWallet,
} from '../../class';
import loc, { formatBalanceWithoutSuffix } from '../../loc';
import { useTheme, useRoute, useNavigation } from '@react-navigation/native';
import RNFS from 'react-native-fs';
import Share from 'react-native-share';
import { BlueStorageContext } from '../../blue_modules/storage-context';
import Notifications from '../../blue_modules/notifications';
import { isDesktop } from '../../blue_modules/environment';
import { AbstractHDElectrumWallet } from '../../class/wallets/abstract-hd-electrum-wallet';
import alert from '../../components/Alert';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { writeFileAndExport } from '../../blue_modules/fs';
const prompt = require('../../helpers/prompt');
const styles = StyleSheet.create({
scrollViewContent: {
flexGrow: 1,
},
address: {
alignItems: 'center',
flex: 1,
},
textLabel1: {
fontWeight: '500',
fontSize: 14,
marginVertical: 12,
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
},
textLabel2: {
fontWeight: '500',
fontSize: 14,
marginVertical: 16,
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
},
textValue: {
fontWeight: '500',
fontSize: 14,
},
input: {
flexDirection: 'row',
borderWidth: 1,
borderBottomWidth: 0.5,
minHeight: 44,
height: 44,
alignItems: 'center',
borderRadius: 4,
},
inputText: {
flex: 1,
marginHorizontal: 8,
minHeight: 33,
color: '#81868e',
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
},
hardware: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
delete: {
color: '#d0021b',
fontSize: 15,
fontWeight: '500',
textAlign: 'center',
},
row: {
flexDirection: 'row',
},
marginRight16: {
marginRight: 16,
},
save: {
alignItems: 'center',
justifyContent: 'center',
width: 80,
borderRadius: 8,
height: 34,
},
saveText: {
fontSize: 15,
fontWeight: '600',
},
});
const WalletDetails = () => {
const { saveToDisk, wallets, deleteWallet, setSelectedWallet, txMetadata } = useContext(BlueStorageContext);
const { walletID } = useRoute().params;
const [isLoading, setIsLoading] = useState(false);
const [backdoorPressed, setBackdoorPressed] = useState(0);
const [backdoorBip47Pressed, setBackdoorBip47Pressed] = useState(0);
const wallet = useRef(wallets.find(w => w.getID() === walletID)).current;
const [walletName, setWalletName] = useState(wallet.getLabel());
const [useWithHardwareWallet, setUseWithHardwareWallet] = useState(wallet.useWithHardwareWalletEnabled());
const { isAdvancedModeEnabled } = useContext(BlueStorageContext);
const [isAdvancedModeEnabledRender, setIsAdvancedModeEnabledRender] = useState(false);
const [isBIP47Enabled, setIsBIP47Enabled] = useState(wallet.isBIP47Enabled());
const [hideTransactionsInWalletsList, setHideTransactionsInWalletsList] = useState(!wallet.getHideTransactionsInWalletsList());
const { goBack, navigate, setOptions, popToTop } = useNavigation();
const { colors } = useTheme();
const [masterFingerprint, setMasterFingerprint] = useState();
const walletTransactionsLength = useMemo(() => wallet.getTransactions().length, [wallet]);
const derivationPath = useMemo(() => {
try {
const path = wallet.getDerivationPath();
return path.length > 0 ? path : null;
} catch (e) {
return null;
}
}, [wallet]);
const [lightningWalletInfo, setLightningWalletInfo] = useState({});
useEffect(() => {
if (isAdvancedModeEnabledRender && wallet.allowMasterFingerprint()) {
InteractionManager.runAfterInteractions(() => {
setMasterFingerprint(wallet.getMasterFingerprintHex());
});
}
}, [isAdvancedModeEnabledRender, wallet]);
const stylesHook = StyleSheet.create({
textLabel1: {
color: colors.feeText,
},
textLabel2: {
color: colors.feeText,
},
textValue: {
color: colors.outputValue,
},
input: {
borderColor: colors.formBorder,
borderBottomColor: colors.formBorder,
backgroundColor: colors.inputBackgroundColor,
},
save: {
backgroundColor: colors.lightButton,
},
saveText: {
color: colors.buttonTextColor,
},
});
useEffect(() => {
if (wallet.type === LightningLdkWallet.type) {
wallet.getInfo().then(setLightningWalletInfo);
}
}, [wallet]);
const save = () => {
setIsLoading(true);
if (walletName.trim().length > 0) {
wallet.setLabel(walletName.trim());
if (wallet.type === WatchOnlyWallet.type && wallet.isHd()) {
wallet.setUseWithHardwareWalletEnabled(useWithHardwareWallet);
}
wallet.setHideTransactionsInWalletsList(!hideTransactionsInWalletsList);
if (wallet.allowBIP47()) {
wallet.switchBIP47(isBIP47Enabled);
}
}
saveToDisk()
.then(() => {
alert(loc.wallets.details_wallet_updated);
goBack();
})
.catch(error => {
console.log(error.message);
setIsLoading(false);
});
};
useLayoutEffect(() => {
isAdvancedModeEnabled().then(setIsAdvancedModeEnabledRender);
setOptions({
// eslint-disable-next-line react/no-unstable-nested-components
headerRight: () => (
{loc.wallets.details_save}
),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading, colors, walletName, useWithHardwareWallet, hideTransactionsInWalletsList, isBIP47Enabled]);
useEffect(() => {
if (wallets.some(w => w.getID() === walletID)) {
setSelectedWallet(walletID);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [walletID]);
const navigateToOverviewAndDeleteWallet = () => {
setIsLoading(true);
let externalAddresses = [];
try {
externalAddresses = wallet.getAllExternalAddresses();
} catch (_) {}
Notifications.unsubscribe(externalAddresses, [], []);
popToTop();
deleteWallet(wallet);
saveToDisk(true);
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
};
const presentWalletHasBalanceAlert = useCallback(async () => {
ReactNativeHapticFeedback.trigger('notificationWarning', { ignoreAndroidSystemSettings: false });
try {
const walletBalanceConfirmation = await prompt(
loc.wallets.details_delete_wallet,
loc.formatString(loc.wallets.details_del_wb_q, { balance: wallet.getBalance() }),
true,
'plain-text',
true,
loc.wallets.details_delete,
);
if (Number(walletBalanceConfirmation) === wallet.getBalance()) {
navigateToOverviewAndDeleteWallet();
} else {
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
setIsLoading(false);
alert(loc.wallets.details_del_wb_err);
}
} catch (_) {}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const navigateToWalletExport = () => {
navigate('WalletExportRoot', {
screen: 'WalletExport',
params: {
walletID: wallet.getID(),
},
});
};
const navigateToMultisigCoordinationSetup = () => {
navigate('ExportMultisigCoordinationSetupRoot', {
screen: 'ExportMultisigCoordinationSetup',
params: {
walletId: wallet.getID(),
},
});
};
const navigateToViewEditCosigners = () => {
navigate('ViewEditMultisigCosignersRoot', {
screen: 'ViewEditMultisigCosigners',
params: {
walletId: wallet.getID(),
},
});
};
const navigateToXPub = () =>
navigate('WalletXpubRoot', {
screen: 'WalletXpub',
params: {
walletID,
},
});
const navigateToSignVerify = () =>
navigate('SignVerifyRoot', {
screen: 'SignVerify',
params: {
walletID: wallet.getID(),
address: wallet.getAllExternalAddresses()[0], // works for both single address and HD wallets
},
});
const navigateToLdkViewLogs = () => {
navigate('LdkViewLogs', {
walletID,
});
};
const navigateToAddresses = () =>
navigate('WalletAddresses', {
walletID: wallet.getID(),
});
const navigateToPaymentCodes = () =>
navigate('PaymentCodeRoot', {
screen: 'PaymentCodesList',
params: {
walletID: wallet.getID(),
},
});
const exportInternals = async () => {
if (backdoorPressed < 10) return setBackdoorPressed(backdoorPressed + 1);
setBackdoorPressed(0);
if (wallet.type !== HDSegwitBech32Wallet.type) return;
const fileName = 'wallet-externals.json';
const contents = JSON.stringify(
{
_balances_by_external_index: wallet._balances_by_external_index,
_balances_by_internal_index: wallet._balances_by_internal_index,
_txs_by_external_index: wallet._txs_by_external_index,
_txs_by_internal_index: wallet._txs_by_internal_index,
_utxo: wallet._utxo,
next_free_address_index: wallet.next_free_address_index,
next_free_change_address_index: wallet.next_free_change_address_index,
internal_addresses_cache: wallet.internal_addresses_cache,
external_addresses_cache: wallet.external_addresses_cache,
_xpub: wallet._xpub,
gap_limit: wallet.gap_limit,
label: wallet.label,
_lastTxFetch: wallet._lastTxFetch,
_lastBalanceFetch: wallet._lastBalanceFetch,
},
null,
2,
);
if (Platform.OS === 'ios') {
const filePath = RNFS.TemporaryDirectoryPath + `/${fileName}`;
await RNFS.writeFile(filePath, contents);
Share.open({
url: 'file://' + filePath,
saveToFiles: isDesktop,
})
.catch(error => {
console.log(error);
alert(error.message);
})
.finally(() => {
RNFS.unlink(filePath);
});
} else if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, {
title: loc.send.permission_storage_title,
message: loc.send.permission_storage_message,
buttonNeutral: loc.send.permission_storage_later,
buttonNegative: loc._.cancel,
buttonPositive: loc._.ok,
});
if (granted === PermissionsAndroid.RESULTS.GRANTED || Platform.Version >= 33) {
console.log('Storage Permission: Granted');
const filePath = RNFS.DownloadDirectoryPath + `/${fileName}`;
try {
await RNFS.writeFile(filePath, contents);
alert(loc.formatString(loc.send.txSaved, { filePath: fileName }));
} catch (e) {
console.log(e);
alert(e.message);
}
} else {
console.log('Storage Permission: Denied');
Alert.alert(loc.send.permission_storage_title, loc.send.permission_storage_denied_message, [
{
text: loc.send.open_settings,
onPress: () => {
Linking.openSettings();
},
style: 'default',
},
{ text: loc._.cancel, onPress: () => {}, style: 'cancel' },
]);
}
}
};
const purgeTransactions = async () => {
if (backdoorPressed < 10) return setBackdoorPressed(backdoorPressed + 1);
setBackdoorPressed(0);
const msg = 'Transactions purged. Pls go to main screen and back to rerender screen';
if (wallet.type === HDSegwitBech32Wallet.type) {
wallet._txs_by_external_index = {};
wallet._txs_by_internal_index = {};
alert(msg);
}
if (wallet._hdWalletInstance) {
wallet._hdWalletInstance._txs_by_external_index = {};
wallet._hdWalletInstance._txs_by_internal_index = {};
alert(msg);
}
};
const walletNameTextInputOnBlur = () => {
if (walletName.trim().length === 0) {
const walletLabel = wallet.getLabel();
setWalletName(walletLabel);
}
};
const onExportHistoryPressed = async () => {
let csvFile = [
loc.transactions.date,
loc.transactions.txid,
`${loc.send.create_amount} (${BitcoinUnit.BTC})`,
loc.send.create_memo,
].join(','); // CSV header
const transactions = wallet.getTransactions();
for (const transaction of transactions) {
const value = formatBalanceWithoutSuffix(transaction.value, BitcoinUnit.BTC, true);
let hash = transaction.hash;
let memo = txMetadata[transaction.hash]?.memo?.trim() ?? '';
if (wallet.chain === Chain.OFFCHAIN) {
hash = transaction.payment_hash;
memo = transaction.description;
if (hash?.type === 'Buffer' && hash?.data) {
const bb = Buffer.from(hash);
hash = bb.toString('hex');
}
}
csvFile += '\n' + [new Date(transaction.received).toString(), hash, value, memo].join(','); // CSV line
}
await writeFileAndExport(`${wallet.label.replace(' ', '-')}-history.csv`, csvFile);
};
const handleDeleteButtonTapped = () => {
ReactNativeHapticFeedback.trigger('notificationWarning', { ignoreAndroidSystemSettings: false });
Alert.alert(
loc.wallets.details_delete_wallet,
loc.wallets.details_are_you_sure,
[
{
text: loc.wallets.details_yes_delete,
onPress: async () => {
const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled();
if (isBiometricsEnabled) {
if (!(await Biometric.unlockWithBiometrics())) {
return;
}
}
if (wallet.getBalance() > 0 && wallet.allowSend()) {
presentWalletHasBalanceAlert();
} else {
navigateToOverviewAndDeleteWallet();
}
},
style: 'destructive',
},
{ text: loc.wallets.details_no_cancel, onPress: () => {}, style: 'cancel' },
],
{ cancelable: false },
);
};
return (
{isLoading ? (
) : (
{(() => {
if (
[LegacyWallet.type, SegwitBech32Wallet.type, SegwitP2SHWallet.type].includes(wallet.type) ||
(wallet.type === WatchOnlyWallet.type && !wallet.isHd())
) {
return (
<>
{loc.wallets.details_address.toLowerCase()}
{wallet.getAddress()}
>
);
}
})()}
{loc.wallets.add_wallet_name.toLowerCase()}
{loc.wallets.details_type.toLowerCase()}
{wallet.typeReadable}
{wallet.type === LightningLdkWallet.type && (
<>
{loc.wallets.identity_pubkey}
{lightningWalletInfo?.identityPubkey ? (
<>
{lightningWalletInfo.identityPubkey}
>
) : (
)}
>
)}
{wallet.type === MultisigHDWallet.type && (
<>
{loc.wallets.details_multisig_type}
{`${wallet.getM()} / ${wallet.getN()} (${
wallet.isNativeSegwit() ? 'native segwit' : wallet.isWrappedSegwit() ? 'wrapped segwit' : 'legacy'
})`}
>
)}
{wallet.type === MultisigHDWallet.type && (
<>
{loc.multisig.how_many_signatures_can_bluewallet_make}
{wallet.howManySignaturesCanWeMake()}
>
)}
{wallet.type === LightningCustodianWallet.type && (
<>
{loc.wallets.details_connected_to.toLowerCase()}
{wallet.getBaseURI()}
>
)}
{wallet.type === HDAezeedWallet.type && (
<>
{loc.wallets.identity_pubkey.toLowerCase()}
{wallet.getIdentityPubkey()}
>
)}
<>
{loc.transactions.list_title.toLowerCase()}
setBackdoorBip47Pressed(prevState => prevState + 1)}>{loc.wallets.details_display}
>
<>
{loc.transactions.transactions_count.toLowerCase()}
{wallet.getTransactions().length}
>
{backdoorBip47Pressed >= 10 && wallet.allowBIP47() ? (
<>
{loc.bip47.payment_code}
{loc.bip47.purpose}
>
) : null}
{wallet.type === WatchOnlyWallet.type && wallet.isHd() && (
<>
{loc.wallets.details_advanced.toLowerCase()}
{loc.wallets.details_use_with_hardware_wallet}
>
)}
{isAdvancedModeEnabledRender && (
{wallet.allowMasterFingerprint() && (
{loc.wallets.details_master_fingerprint.toLowerCase()}
{masterFingerprint ?? }
)}
{derivationPath && (
{loc.wallets.details_derivation_path}
{derivationPath}
)}
)}
{(wallet instanceof AbstractHDElectrumWallet || (wallet.type === WatchOnlyWallet.type && wallet.isHd())) && (
)}
{wallet.allowBIP47() && isBIP47Enabled && }
{walletTransactionsLength > 0 && (
<>
>
)}
{wallet.type === MultisigHDWallet.type && (
<>
c.toUpperCase())}
/>
>
)}
{wallet.type === MultisigHDWallet.type && (
<>
>
)}
{wallet.allowXpub() && (
<>
>
)}
{wallet.allowSignVerifyMessage() && (
<>
>
)}
{wallet.type === LightningLdkWallet.type && (
<>
>
)}
{`${loc.wallets.details_delete}${' '}`}
)}
);
};
WalletDetails.navigationOptions = navigationStyle({}, opts => ({ ...opts, headerTitle: loc.wallets.details_title }));
export default WalletDetails;