import React, { useContext, useEffect, useLayoutEffect, useState } from 'react'; import { View, ScrollView, TouchableOpacity, Text, TextInput, Linking, StyleSheet, Keyboard } from 'react-native'; import { useNavigation, useRoute } from '@react-navigation/native'; import Clipboard from '@react-native-clipboard/clipboard'; import { BlueCard, BlueCopyToClipboardButton, BlueLoading, BlueSpacing20, BlueText } from '../../BlueComponents'; import navigationStyle from '../../components/navigationStyle'; import HandoffComponent from '../../components/handoff'; import loc from '../../loc'; import { BlueStorageContext } from '../../blue_modules/storage-context'; import ToolTipMenu from '../../components/TooltipMenu'; import alert from '../../components/Alert'; import { useTheme } from '../../components/themes'; const dayjs = require('dayjs'); function onlyUnique(value, index, self) { return self.indexOf(value) === index; } function arrDiff(a1, a2) { const ret = []; for (const v of a2) { if (a1.indexOf(v) === -1) { ret.push(v); } } return ret; } const TransactionsDetails = () => { const { setOptions, navigate } = useNavigation(); const { hash } = useRoute().params; const { saveToDisk, txMetadata, wallets, getTransactions } = useContext(BlueStorageContext); const [from, setFrom] = useState(); const [to, setTo] = useState(); const [isLoading, setIsLoading] = useState(true); const [tx, setTX] = useState(); const [memo, setMemo] = useState(); const { colors } = useTheme(); const stylesHooks = StyleSheet.create({ memoTextInput: { borderColor: colors.formBorder, borderBottomColor: colors.formBorder, backgroundColor: colors.inputBackgroundColor, }, greyButton: { backgroundColor: colors.lightButton, }, Link: { color: colors.buttonTextColor, }, save: { backgroundColor: colors.lightButton, }, saveText: { color: colors.buttonTextColor, }, }); useLayoutEffect(() => { setOptions({ // eslint-disable-next-line react/no-unstable-nested-components headerRight: () => ( {loc.wallets.details_save} ), }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [colors, isLoading, memo]); useEffect(() => { let foundTx = {}; let newFrom = []; let newTo = []; for (const transaction of getTransactions(null, Infinity, true)) { if (transaction.hash === hash) { foundTx = transaction; for (const input of foundTx.inputs) { newFrom = newFrom.concat(input.addresses); } for (const output of foundTx.outputs) { if (output.addresses) newTo = newTo.concat(output.addresses); if (output.scriptPubKey && output.scriptPubKey.addresses) newTo = newTo.concat(output.scriptPubKey.addresses); } } } for (const w of wallets) { for (const t of w.getTransactions()) { if (t.hash === hash) { console.log('tx', hash, 'belongs to', w.getLabel()); } } } if (txMetadata[foundTx.hash]) { if (txMetadata[foundTx.hash].memo) { setMemo(txMetadata[foundTx.hash].memo); } } setTX(foundTx); setFrom(newFrom); setTo(newTo); setIsLoading(false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [hash, wallets]); const handleOnSaveButtonTapped = () => { Keyboard.dismiss(); txMetadata[tx.hash] = { memo }; saveToDisk().then(_success => alert(loc.transactions.transaction_note_saved)); }; const handleOnOpenTransactionOnBlockExporerTapped = () => { const url = `https://mempool.space/tx/${tx.hash}`; Linking.canOpenURL(url) .then(supported => { if (supported) { Linking.openURL(url).catch(e => { console.log('openURL failed in handleOnOpenTransactionOnBlockExporerTapped'); console.log(e.message); alert(e.message); }); } else { console.log('canOpenURL supported is false in handleOnOpenTransactionOnBlockExporerTapped'); alert(loc.transactions.open_url_error); } }) .catch(e => { console.log('canOpenURL failed in handleOnOpenTransactionOnBlockExporerTapped'); console.log(e.message); alert(e.message); }); }; const handleCopyPress = stringToCopy => { Clipboard.setString( stringToCopy !== TransactionsDetails.actionKeys.CopyToClipboard ? stringToCopy : `https://mempool.space/tx/${tx.hash}`, ); }; if (isLoading || !tx) { return ; } const weOwnAddress = address => { for (const w of wallets) { if (w.weOwnAddress(address)) { return w; } } return null; }; const navigateToWallet = wallet => { const walletID = wallet.getID(); navigate('WalletTransactions', { walletID, walletType: wallet.type, }); }; const renderSection = array => { const fromArray = []; for (const [index, address] of array.entries()) { const actions = []; actions.push({ id: TransactionsDetails.actionKeys.CopyToClipboard, text: loc.transactions.details_copy, icon: TransactionsDetails.actionIcons.Clipboard, }); const isWeOwnAddress = weOwnAddress(address); if (isWeOwnAddress) { actions.push({ id: TransactionsDetails.actionKeys.GoToWallet, text: loc.formatString(loc.transactions.view_wallet, { walletLabel: isWeOwnAddress.getLabel() }), icon: TransactionsDetails.actionIcons.GoToWallet, }); } fromArray.push( { if (id === TransactionsDetails.actionKeys.CopyToClipboard) { handleCopyPress(address); } else if (id === TransactionsDetails.actionKeys.GoToWallet) { navigateToWallet(isWeOwnAddress); } }} > {address} {index === array.length - 1 ? null : ','} , ); } return fromArray; }; return ( {from && ( <> {loc.transactions.details_from} {renderSection(from.filter(onlyUnique))} )} {to && ( <> {loc.transactions.details_to} {renderSection(arrDiff(from, to.filter(onlyUnique)))} )} {tx.fee && ( <> {loc.send.create_fee} {tx.fee + ' sats'} )} {tx.hash && ( <> {loc.transactions.txid} {tx.hash} )} {tx.received && ( <> {loc.transactions.details_received} {dayjs(tx.received).format('LLL')} )} {tx.block_height > 0 && ( <> {loc.transactions.details_block} {tx.block_height} )} {tx.inputs && ( <> {loc.transactions.details_inputs} {tx.inputs.length} )} {tx.outputs?.length > 0 && ( <> {loc.transactions.details_outputs} {tx.outputs.length} )} {loc.transactions.details_show_in_block_explorer} ); }; TransactionsDetails.actionKeys = { CopyToClipboard: 'copyToClipboard', GoToWallet: 'goToWallet', }; TransactionsDetails.actionIcons = { Clipboard: { iconType: 'SYSTEM', iconValue: 'doc.on.doc', }, GoToWallet: { iconType: 'SYSTEM', iconValue: 'wallet.pass', }, }; const styles = StyleSheet.create({ scroll: { flex: 1, }, rowHeader: { flex: 1, flexDirection: 'row', marginBottom: 4, justifyContent: 'space-between', }, rowCaption: { fontSize: 16, fontWeight: '500', marginBottom: 4, }, rowValue: { color: 'grey', }, marginBottom18: { marginBottom: 18, }, txId: { fontSize: 16, fontWeight: '500', }, Link: { fontWeight: '600', fontSize: 15, }, weOwnAddress: { fontWeight: '600', }, save: { alignItems: 'center', justifyContent: 'center', width: 80, borderRadius: 8, height: 34, }, saveText: { fontSize: 15, fontWeight: '600', }, memoTextInput: { flexDirection: 'row', borderWidth: 1, borderBottomWidth: 0.5, minHeight: 44, height: 44, alignItems: 'center', marginVertical: 8, borderRadius: 4, paddingHorizontal: 8, color: '#81868e', }, greyButton: { borderRadius: 9, minHeight: 49, paddingHorizontal: 8, justifyContent: 'center', alignItems: 'center', flexDirection: 'row', alignSelf: 'auto', flexGrow: 1, marginHorizontal: 4, }, }); export default TransactionsDetails; TransactionsDetails.navigationOptions = navigationStyle({ headerTitle: loc.transactions.details_title }, (options, { theme }) => { return { ...options, statusBarStyle: 'auto', headerStyle: { backgroundColor: theme.colors.customHeader, }, }; });