BlueWallet/screen/send/CoinControlOutputSheet.tsx
Nuno 9e907566f0
feat: redesigned wallet details (#8301)
* feat: redesign transaction detail screen with unified layout and Lottie pending animation

* ADD: decode OP_RETURN payload as UTF-8 text in transaction detail

Co-authored-by: Cursor <cursoragent@cursor.com>

* REF: transaction detail redesign (themes, pending icon, loc)

Co-authored-by: Cursor <cursoragent@cursor.com>

* REF: remove deprecated TransactionDetails, TransactionStatus and getTransactionStatusOptions

Co-authored-by: Cursor <cursoragent@cursor.com>

* FIX: resolve lint errors (unused vars, styles, loc keys, no-bitwise, inline styles)

Co-authored-by: Cursor <cursoragent@cursor.com>

* FIX: remove redundant !tx check in transaction detail guard

Co-authored-by: Cursor <cursoragent@cursor.com>

* FIX: show transaction not available when tx not found after load

Co-authored-by: Cursor <cursoragent@cursor.com>

* FIX: remove unused transaction prop type from TransactionDetail

Co-authored-by: Cursor <cursoragent@cursor.com>

* TST: update UTXO note E2E to use new transaction detail note prompt UI

Co-authored-by: Cursor <cursoragent@cursor.com>

* simplify changes on the PR for review

* remove unused loc

* remove unchanged colors

* better offline support for tx details

* remove unused key loc

* fix code review issues

* fix balance

* fix tests

* REF: address PR #8289 review feedback

* redesigned wallets details

* fix lint

* fix lint

* fix bip84 test

* fix test

* fix tests

* fix tests

* fix: truncation and sendTo logic display

* fix: new arch fixes

* fix: lint

* fix: crash on status update

* fix: lint and tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* Potential fix for pull request finding 'Identical operands'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix style

* fix merge master

* Merge branch 'wallet-details' of https://github.com/BlueWallet/BlueWallet into wallet-details

* fix loc

* fix loc

* fix style

* improve coin control from wallet details

* fix: e2e

* fix: WalletDetails

* fix: flat

* fix: e2e

* fix: e2e

* Potential fix for pull request finding 'Unused variable, import, function or class'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* fix: remove notifications dialogs

* fix: second button title

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Ivan Vershigora <ivan.vershigora@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Overtorment <overtorment@gmail.com>
2026-05-21 09:12:49 +02:00

227 lines
7.8 KiB
TypeScript

import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { RouteProp, useRoute } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { StyleSheet, Text, TextInput, View } from 'react-native';
import debounce from '../../blue_modules/debounce';
import Avatar from '../../components/Avatar';
import ListItem from '../../components/ListItem';
import { BlueSpacing10 } from '../../components/BlueSpacing';
import Button from '../../components/Button';
import { useTheme } from '../../components/themes';
import loc, { formatBalance } from '../../loc';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import { goFromCoinControlToSendDetails } from '../../navigation/goFromCoinControlToSendDetails';
import { SendDetailsStackParamList } from '../../navigation/SendDetailsStackParamList';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { useStorage } from '../../hooks/context/useStorage';
import * as RNLocalize from 'react-native-localize';
import { useKeyboard } from '../../hooks/useKeyboard';
import HeaderRightButton from '../../components/HeaderRightButton';
type RouteProps = RouteProp<SendDetailsStackParamList, 'CoinControlOutput'>;
type NavigationProps = NativeStackNavigationProp<SendDetailsStackParamList, 'CoinControlOutput'>;
const CoinControlOutputSheet: React.FC = () => {
const navigation = useExtendedNavigation<NavigationProps>();
const route = useRoute<RouteProps>();
const { walletID, utxo } = route.params;
const { wallets, txMetadata, saveToDisk } = useStorage();
const wallet = useMemo(() => wallets.find(w => w.getID() === walletID), [walletID, wallets]);
const { colors } = useTheme();
const { isVisible } = useKeyboard();
const [memo, setMemo] = useState<string>('');
const [frozen, setFrozen] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
if (!wallet) return;
const meta = wallet.getUTXOMetadata(utxo.txid, utxo.vout);
setMemo(meta.memo || txMetadata[utxo.txid]?.memo || '');
setFrozen(Boolean(meta.frozen));
setLoading(false);
}, [txMetadata, utxo.txid, utxo.vout, wallet]);
const switchValue = useMemo(
() => ({
value: frozen,
testID: 'FreezeSwitch',
onValueChange: async (value: boolean) => {
if (!wallet) return;
setFrozen(value);
wallet.setUTXOMetadata(utxo.txid, utxo.vout, { frozen: value });
await saveToDisk();
},
}),
[frozen, saveToDisk, utxo.txid, utxo.vout, wallet],
);
const onMemoChange = (value: string) => setMemo(value);
const debouncedSaveMemo = useRef(
debounce(async m => {
if (!wallet) return;
wallet.setUTXOMetadata(utxo.txid, utxo.vout, { memo: m });
await saveToDisk();
}, 500),
);
useEffect(() => {
debouncedSaveMemo.current(memo);
}, [memo]);
const amount = formatBalance(utxo.value, wallet?.getPreferredBalanceUnit?.() ?? BitcoinUnit.BTC, true);
const color = `#${utxo.txid.substring(0, 6)}`;
const confirmationsFormatted = useMemo(
() => new Intl.NumberFormat(RNLocalize.getLocales()[0].languageCode, { maximumSignificantDigits: 3 }).format(utxo.confirmations ?? 0),
[utxo.confirmations],
);
const handleUseCoin = useCallback(async () => {
if (!wallet) return;
debouncedSaveMemo.current.cancel();
wallet.setUTXOMetadata(utxo.txid, utxo.vout, { memo });
await saveToDisk();
goFromCoinControlToSendDetails(navigation, walletID, [utxo]);
}, [memo, navigation, saveToDisk, utxo, wallet, walletID]);
const applyChangesAndClose = useCallback(async () => {
if (!wallet) return;
debouncedSaveMemo.current.cancel();
wallet.setUTXOMetadata(utxo.txid, utxo.vout, { memo });
await saveToDisk();
navigation.goBack();
}, [memo, navigation, saveToDisk, utxo.txid, utxo.vout, wallet]);
if (!wallet) {
return (
<View style={[styles.center, { backgroundColor: colors.elevated }]}>
<Text style={{ color: colors.foregroundColor }}>{loc.wallets.import_discovery_no_wallets}</Text>
</View>
);
}
return (
<View style={[styles.root, { backgroundColor: colors.elevated }]}>
<View style={styles.floatingDoneButtonContainer}>
<HeaderRightButton testID="CoinControlOutputDone" title={loc.send.input_done} onPress={applyChangesAndClose} disabled={loading} />
</View>
<View style={styles.flex}>
<View style={styles.headerContainer}>
<View style={styles.rowContent}>
<Avatar rounded size={40} containerStyle={[styles.avatar, { backgroundColor: color }]} />
<View style={styles.listContent}>
<Text numberOfLines={1} style={[styles.amount, { color: colors.foregroundColor }]}>
{amount}
</Text>
<View style={styles.tranContainer}>
<Text style={[styles.tranText, { color: colors.alternativeTextColor }]}>
{loc.formatString(loc.transactions.list_conf, { number: confirmationsFormatted })}
</Text>
</View>
{memo ? (
<>
<Text style={[styles.memo, { color: colors.alternativeTextColor }]}>{memo}</Text>
<BlueSpacing10 />
</>
) : null}
<Text style={[styles.memo, { color: colors.alternativeTextColor }]}>{utxo.address}</Text>
<BlueSpacing10 />
<Text style={[styles.memo, { color: colors.alternativeTextColor }]}>{`${utxo.txid}:${utxo.vout}`}</Text>
</View>
</View>
</View>
<View style={styles.content}>
<TextInput
testID="OutputMemo"
placeholder={loc.send.details_note_placeholder}
value={memo}
placeholderTextColor="#81868e"
editable={!loading}
style={[
styles.memoTextInput,
{
borderColor: colors.formBorder,
borderBottomColor: colors.formBorder,
backgroundColor: colors.inputBackgroundColor,
color: colors.foregroundColor,
},
]}
onChangeText={onMemoChange}
/>
<ListItem title={loc.cc.freezeLabel} switch={switchValue} bottomDivider={false} />
</View>
<View style={styles.buttonContainer}>
{!isVisible && <Button testID="UseCoin" title={loc.cc.use_coin} onPress={handleUseCoin} disabled={loading} />}
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
root: {
flex: 1,
paddingHorizontal: 24,
},
flex: {
flex: 1,
},
center: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
headerContainer: {
paddingHorizontal: 0,
borderBottomColor: 'transparent',
backgroundColor: 'transparent',
},
rowContent: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'transparent',
gap: 10,
},
listContent: {
flex: 1,
},
avatar: { borderColor: 'white', borderWidth: 1 },
amount: { fontWeight: 'bold' },
tranContainer: { paddingLeft: 20 },
tranText: { fontWeight: 'normal', fontSize: 13 },
memo: { fontSize: 13, marginTop: 3 },
content: {
paddingTop: 12,
flex: 1,
},
memoTextInput: {
flexDirection: 'row',
borderWidth: 1,
borderBottomWidth: 0.5,
minHeight: 44,
height: 44,
alignItems: 'center',
marginVertical: 8,
borderRadius: 4,
paddingHorizontal: 8,
},
buttonContainer: {
height: 45,
marginBottom: 36,
},
floatingDoneButtonContainer: {
position: 'absolute',
top: 8,
right: 0,
zIndex: 10,
elevation: 10,
},
});
export default CoinControlOutputSheet;