From 96a59666b7bd81d998201dc55f6c126f2d0412d7 Mon Sep 17 00:00:00 2001 From: Ivan Vershigora Date: Sat, 9 Nov 2024 15:14:13 +0000 Subject: [PATCH] fix: CoinControl Typescript --- navigation/LazyLoadSendDetailsStack.tsx | 2 +- .../send/{coinControl.js => CoinControl.tsx} | 219 ++++++++++-------- 2 files changed, 118 insertions(+), 103 deletions(-) rename screen/send/{coinControl.js => CoinControl.tsx} (72%) diff --git a/navigation/LazyLoadSendDetailsStack.tsx b/navigation/LazyLoadSendDetailsStack.tsx index 34ef8b0c4..57cb5896b 100644 --- a/navigation/LazyLoadSendDetailsStack.tsx +++ b/navigation/LazyLoadSendDetailsStack.tsx @@ -10,7 +10,7 @@ const PsbtMultisig = lazy(() => import('../screen/send/psbtMultisig')); const PsbtMultisigQRCode = lazy(() => import('../screen/send/psbtMultisigQRCode')); const Success = lazy(() => import('../screen/send/success')); const SelectWallet = lazy(() => import('../screen/wallets/SelectWallet')); -const CoinControl = lazy(() => import('../screen/send/coinControl')); +const CoinControl = lazy(() => import('../screen/send/CoinControl')); const PaymentCodesList = lazy(() => import('../screen/wallets/PaymentCodesList')); // Export each component with its lazy loader and optional configurations diff --git a/screen/send/coinControl.js b/screen/send/CoinControl.tsx similarity index 72% rename from screen/send/coinControl.js rename to screen/send/CoinControl.tsx index 9811dc019..e111993fe 100644 --- a/screen/send/coinControl.js +++ b/screen/send/CoinControl.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { useRoute } from '@react-navigation/native'; -import PropTypes from 'prop-types'; +import { RouteProp, useRoute } from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { Avatar, Badge, Icon, ListItem as RNElementsListItem } from '@rneui/themed'; import { ActivityIndicator, FlatList, @@ -15,41 +16,59 @@ import { useWindowDimensions, View, } from 'react-native'; -import { Avatar, Badge, Icon, ListItem as RNElementsListItem } from '@rneui/themed'; import * as RNLocalize from 'react-native-localize'; + import debounce from '../../blue_modules/debounce'; import { BlueSpacing10, BlueSpacing20 } from '../../BlueComponents'; -import BottomModal from '../../components/BottomModal'; +import { TWallet, Utxo } from '../../class/wallets/types'; +import BottomModal, { BottomModalHandle } from '../../components/BottomModal'; import Button from '../../components/Button'; import { FButton, FContainer } from '../../components/FloatButtons'; import ListItem from '../../components/ListItem'; import SafeArea from '../../components/SafeArea'; import { useTheme } from '../../components/themes'; -import loc, { formatBalance } from '../../loc'; -import { BitcoinUnit } from '../../models/bitcoinUnits'; import { useStorage } from '../../hooks/context/useStorage'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; +import loc, { formatBalance } from '../../loc'; +import { BitcoinUnit } from '../../models/bitcoinUnits'; +import { SendDetailsStackParamList } from '../../navigation/SendDetailsStackParamList'; -const FrozenBadge = () => { +type NavigationProps = NativeStackNavigationProp; +type RouteProps = RouteProp; + +const FrozenBadge: React.FC = () => { const { colors } = useTheme(); const oStyles = StyleSheet.create({ - freeze: { backgroundColor: colors.redBG, borderWidth: 0 }, - freezeText: { color: colors.redText }, + freeze: { backgroundColor: colors.redBG, borderWidth: 0, marginLeft: 4 }, + freezeText: { color: colors.redText, marginTop: -1 }, }); return ; }; -const ChangeBadge = () => { +const ChangeBadge: React.FC = () => { const { colors } = useTheme(); const oStyles = StyleSheet.create({ - change: { backgroundColor: colors.buttonDisabledBackgroundColor, borderWidth: 0 }, - changeText: { color: colors.alternativeTextColor }, + change: { backgroundColor: colors.buttonDisabledBackgroundColor, borderWidth: 0, marginLeft: 4 }, + changeText: { color: colors.alternativeTextColor, marginTop: -1 }, }); return ; }; -const OutputList = ({ - item: { address, txid, value, vout, confirmations = 0 }, +type TOutputListProps = { + item: Utxo; + balanceUnit: string; + oMemo?: string; + frozen: boolean; + change: boolean; + onOpen: () => void; + selected: boolean; + selectionStarted: boolean; + onSelect: () => void; + onDeSelect: () => void; +}; + +const OutputList: React.FC = ({ + item: { address, txid, value }, balanceUnit = BitcoinUnit.BTC, oMemo, frozen, @@ -59,7 +78,7 @@ const OutputList = ({ selectionStarted, onSelect, onDeSelect, -}) => { +}: TOutputListProps) => { const { colors } = useTheme(); const { txMetadata } = useStorage(); const memo = oMemo || txMetadata[txid]?.memo || ''; @@ -84,12 +103,12 @@ const OutputList = ({ return ( - {amount} @@ -97,32 +116,25 @@ const OutputList = ({ {memo || address} - {change && } - {frozen && } + + {frozen && } + {change && } + ); }; -OutputList.propTypes = { - item: PropTypes.shape({ - address: PropTypes.string.isRequired, - txid: PropTypes.string.isRequired, - value: PropTypes.number.isRequired, - vout: PropTypes.number.isRequired, - confirmations: PropTypes.number, - }), - balanceUnit: PropTypes.string, - oMemo: PropTypes.string, - frozen: PropTypes.bool, - change: PropTypes.bool, - onOpen: PropTypes.func, - selected: PropTypes.bool, - selectionStarted: PropTypes.bool, - onSelect: PropTypes.func, - onDeSelect: PropTypes.func, +type TOutputModalProps = { + item: Utxo; + balanceUnit: string; + oMemo?: string; }; -const OutputModal = ({ item: { address, txid, value, vout, confirmations = 0 }, balanceUnit = BitcoinUnit.BTC, oMemo }) => { +const OutputModal: React.FC = ({ + item: { address, txid, value, vout, confirmations = 0 }, + balanceUnit = BitcoinUnit.BTC, + oMemo, +}) => { const { colors } = useTheme(); const { txMetadata } = useStorage(); const memo = oMemo || txMetadata[txid]?.memo || ''; @@ -144,7 +156,7 @@ const OutputModal = ({ item: { address, txid, value, vout, confirmations = 0 }, return ( - + {amount} @@ -166,18 +178,6 @@ const OutputModal = ({ item: { address, txid, value, vout, confirmations = 0 }, ); }; -OutputModal.propTypes = { - item: PropTypes.shape({ - address: PropTypes.string.isRequired, - txid: PropTypes.string.isRequired, - value: PropTypes.number.isRequired, - vout: PropTypes.number.isRequired, - confirmations: PropTypes.number, - }), - balanceUnit: PropTypes.string, - oMemo: PropTypes.string, -}; - const mStyles = StyleSheet.create({ memoTextInput: { flexDirection: 'row', @@ -198,13 +198,22 @@ const mStyles = StyleSheet.create({ }, }); +type TOutputModalContentProps = { + output: Utxo; + wallet: TWallet; + onUseCoin: (u: Utxo[]) => void; + frozen: boolean; + setFrozen: (value: boolean) => void; +}; + const transparentBackground = { backgroundColor: 'transparent' }; -const OutputModalContent = ({ output, wallet, onUseCoin, frozen, setFrozen }) => { +const OutputModalContent: React.FC = ({ output, wallet, onUseCoin, frozen, setFrozen }) => { const { colors } = useTheme(); const { txMetadata, saveToDisk } = useStorage(); - const [memo, setMemo] = useState(wallet.getUTXOMetadata(output.txid, output.vout).memo || txMetadata[output.txid]?.memo || ''); - const onMemoChange = value => setMemo(value); - const switchValue = useMemo(() => ({ value: frozen, onValueChange: value => setFrozen(value) }), [frozen, setFrozen]); + const [memo, setMemo] = useState(wallet.getUTXOMetadata(output.txid, output.vout).memo || txMetadata[output.txid]?.memo || ''); + const switchValue = useMemo(() => ({ value: frozen, onValueChange: (value: boolean) => setFrozen(value) }), [frozen, setFrozen]); + + const onMemoChange = (value: string) => setMemo(value); // save on form change. Because effect called on each event, debounce it. const debouncedSaveMemo = useRef( @@ -218,7 +227,7 @@ const OutputModalContent = ({ output, wallet, onUseCoin, frozen, setFrozen }) => }, [memo]); return ( - + ); }; -OutputModalContent.propTypes = { - output: PropTypes.object, - wallet: PropTypes.object, - onUseCoin: PropTypes.func.isRequired, - frozen: PropTypes.bool.isRequired, - setFrozen: PropTypes.func.isRequired, -}; - -const CoinControl = () => { +const CoinControl: React.FC = () => { const { colors } = useTheme(); - const navigation = useExtendedNavigation(); + const navigation = useExtendedNavigation(); const { width } = useWindowDimensions(); - const bottomModalRef = useRef(null); - const { walletID } = useRoute().params; + const bottomModalRef = useRef(null); + const { walletID } = useRoute().params; const { wallets, saveToDisk, sleep } = useStorage(); - const wallet = wallets.find(w => w.getID() === walletID); + const wallet = wallets.find(w => w.getID() === walletID) as TWallet; // sort by height ascending, txid , vout ascending - const utxo = wallet.getUtxo(true).sort((a, b) => a.height - b.height || a.txid.localeCompare(b.txid) || a.vout - b.vout); - const [output, setOutput] = useState(); - const [loading, setLoading] = useState(true); - const [selected, setSelected] = useState([]); - const [frozen, setFrozen] = useState( - utxo.filter(out => wallet.getUTXOMetadata(out.txid, out.vout).frozen).map(({ txid, vout }) => `${txid}:${vout}`), + const utxos: Utxo[] = wallet.getUtxo(true).sort((a, b) => a.height - b.height || a.txid.localeCompare(b.txid) || a.vout - b.vout); + const [output, setOutput] = useState(); + const [loading, setLoading] = useState(true); + const [selected, setSelected] = useState([]); + const [frozen, setFrozen] = useState( + utxos.filter(out => wallet.getUTXOMetadata(out.txid, out.vout).frozen).map(({ txid, vout }) => `${txid}:${vout}`), ); // save frozen status. Because effect called on each event, debounce it. const debouncedSaveFronen = useRef( debounce(async frzn => { - utxo.forEach(({ txid, vout }) => { + utxos.forEach(({ txid, vout }) => { wallet.setUTXOMetadata(txid, vout, { frozen: frzn.includes(`${txid}:${vout}`) }); }); await saveToDisk(); @@ -305,13 +306,13 @@ const CoinControl = () => { }); const tipCoins = () => { - if (utxo.length === 0) return null; + if (utxos.length === 0) return null; let text = loc.cc.tip; if (selected.length > 0) { // show summ of coins if any selected const summ = selected.reduce((prev, curr) => { - return prev + utxo.find(({ txid, vout }) => `${txid}:${vout}` === curr).value; + return prev + (utxos.find(({ txid, vout }) => `${txid}:${vout}` === curr) as Utxo).value; }, 0); const value = formatBalance(summ, wallet.getPreferredBalanceUnit(), true); @@ -325,10 +326,11 @@ const CoinControl = () => { ); }; - const handleChoose = item => setOutput(item); + const handleChoose = (item: Utxo) => setOutput(item); - const handleUseCoin = async u => { - setOutput(null); + const handleUseCoin = async (u: Utxo[]) => { + setOutput(undefined); + // @ts-ignore navigation WTF navigation.navigate('SendDetailsRoot', { screen: 'SendDetails', params: { @@ -347,7 +349,7 @@ const CoinControl = () => { }; const handleMassUse = () => { - const fUtxo = utxo.filter(({ txid, vout }) => selected.includes(`${txid}:${vout}`)); + const fUtxo = utxos.filter(({ txid, vout }) => selected.includes(`${txid}:${vout}`)); handleUseCoin(fUtxo); }; @@ -357,7 +359,7 @@ const CoinControl = () => { const allFrozen = selectionStarted && selected.reduce((prev, curr) => (prev ? frozen.includes(curr) : false), true); const buttonFontSize = PixelRatio.roundToNearestPixel(width / 26) > 22 ? 22 : PixelRatio.roundToNearestPixel(width / 26); - const renderItem = p => { + const renderItem = (p: { item: Utxo }) => { const { memo } = wallet.getUTXOMetadata(p.item.txid, p.item.vout); const change = wallet.addressIsChange(p.item.address); const oFrozen = frozen.includes(`${p.item.txid}:${p.item.vout}`); @@ -372,27 +374,39 @@ const CoinControl = () => { selected={selected.includes(`${p.item.txid}:${p.item.vout}`)} selectionStarted={selectionStarted} onSelect={() => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); // animate buttons show - setSelected(s => [...s, `${p.item.txid}:${p.item.vout}`]); + setSelected(s => { + if (s.length === 0) { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); // animate buttons show + } + return [...s, `${p.item.txid}:${p.item.vout}`]; + }); }} onDeSelect={() => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); // animate buttons show - setSelected(s => s.filter(i => i !== `${p.item.txid}:${p.item.vout}`)); + setSelected(s => { + const newValue = s.filter(i => i !== `${p.item.txid}:${p.item.vout}`); + if (newValue.length === 0) { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); // animate buttons show + } + return newValue; + }); }} /> ); }; - const renderOutputModalContent = () => { - const oFrozen = frozen.includes(`${output.txid}:${output.vout}`); - const setOFrozen = value => { + const renderOutputModalContent = (o: Utxo | undefined) => { + if (!o) { + return null; + } + const oFrozen = frozen.includes(`${o.txid}:${o.vout}`); + const setOFrozen = (value: boolean) => { if (value) { - setFrozen(f => [...f, `${output.txid}:${output.vout}`]); + setFrozen(f => [...f, `${o.txid}:${o.vout}`]); } else { - setFrozen(f => f.filter(i => i !== `${output.txid}:${output.vout}`)); + setFrozen(f => f.filter(i => i !== `${o.txid}:${o.vout}`)); } }; - return ; + return ; }; useEffect(() => { @@ -411,7 +425,7 @@ const CoinControl = () => { return ( - {utxo.length === 0 && ( + {utxos.length === 0 && ( {loc.cc.empty} @@ -421,7 +435,7 @@ const CoinControl = () => { ref={bottomModalRef} onClose={() => { Keyboard.dismiss(); - setOutput(false); + setOutput(undefined); }} backgroundColor={colors.elevated} footer={ @@ -430,6 +444,7 @@ const CoinControl = () => { testID="UseCoin" title={loc.cc.use_coin} onPress={async () => { + if (!output) throw new Error('output is not set'); await bottomModalRef.current?.dismiss(); handleUseCoin([output]); }} @@ -438,11 +453,11 @@ const CoinControl = () => { } contentContainerStyle={styles.modalMinHeight} > - {output && renderOutputModalContent()} + {renderOutputModalContent(output)} `${item.txid}:${item.vout}`} contentInset={{ top: 0, left: 0, bottom: 70, right: 0 }} @@ -478,9 +493,6 @@ const styles = StyleSheet.create({ justifyContent: 'center', alignItems: 'center', }, - padding: { - padding: 16, - }, modalMinHeight: Platform.OS === 'android' ? { minHeight: 530 } : {}, empty: { flex: 1, @@ -497,6 +509,9 @@ const styles = StyleSheet.create({ sendIcon: { transform: [{ rotate: '225deg' }], }, + badges: { + flexDirection: 'row', + }, }); export default CoinControl;