mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-03-01 21:15:13 +01:00
feat: UI facelift of the Affected transactions in the RBF flow
This commit is contained in:
committed by
Peter Sanderson
parent
bd273e8b46
commit
49fd2cba42
@@ -0,0 +1,38 @@
|
||||
import { WalletAccountTransaction } from '@suite-common/wallet-types';
|
||||
import { Transaction } from '@trezor/blockchain-link-types';
|
||||
import { Icon, InfoSegments, Row, Text } from '@trezor/components';
|
||||
import { spacings } from '@trezor/theme';
|
||||
|
||||
import { Address, FormattedDate, HiddenPlaceholder } from 'src/components/suite';
|
||||
|
||||
type RowIcon = {
|
||||
txType: Transaction['type'];
|
||||
isAccountOwned: boolean | undefined;
|
||||
};
|
||||
|
||||
const RowIcon = ({ txType, isAccountOwned }: RowIcon) => {
|
||||
const iconType = txType === 'recv' ? 'receive' : 'send';
|
||||
|
||||
return <Icon size={16} variant="disabled" name={isAccountOwned ? iconType : 'clock'} />;
|
||||
};
|
||||
|
||||
type AffectedTransactionItemProps = {
|
||||
tx: WalletAccountTransaction;
|
||||
isAccountOwned?: boolean;
|
||||
};
|
||||
|
||||
export const AffectedTransactionItem = ({ tx, isAccountOwned }: AffectedTransactionItemProps) => (
|
||||
<Row gap={spacings.sm}>
|
||||
<RowIcon isAccountOwned={isAccountOwned} txType={tx.type} />
|
||||
|
||||
<InfoSegments>
|
||||
{tx.blockTime && <FormattedDate value={new Date(tx.blockTime * 1000)} date time />}
|
||||
|
||||
<Text typographyStyle="hint" variant="tertiary">
|
||||
<HiddenPlaceholder>
|
||||
<Address value={tx.txid} isTruncated />
|
||||
</HiddenPlaceholder>
|
||||
</Text>
|
||||
</InfoSegments>
|
||||
</Row>
|
||||
);
|
||||
@@ -0,0 +1,57 @@
|
||||
import { ChainedTransactions } from '@suite-common/wallet-types';
|
||||
import { Banner, Card, Column, Divider, Link, Row, Table, Text } from '@trezor/components';
|
||||
import { spacings } from '@trezor/theme';
|
||||
|
||||
import { Translation } from 'src/components/suite';
|
||||
|
||||
import { AffectedTransactionItem } from './AffectedTransactionItem';
|
||||
|
||||
type AffectedTransactionsProps = {
|
||||
chainedTxs?: ChainedTransactions;
|
||||
showChained: () => void;
|
||||
};
|
||||
|
||||
export const AffectedTransactions = ({ chainedTxs, showChained }: AffectedTransactionsProps) => {
|
||||
if (chainedTxs === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card fillType="flat" paddingType="none">
|
||||
<Row justifyContent="space-between" alignItems="center" padding={spacings.md}>
|
||||
<Text typographyStyle="body">
|
||||
<Translation id="TR_CHAINED_TXS" />
|
||||
</Text>
|
||||
<Text variant="primary" typographyStyle="hint">
|
||||
<Link onClick={showChained} icon="arrowUpRight" variant="nostyle">
|
||||
<Translation id="TR_SEE_DETAILS" />
|
||||
</Link>
|
||||
</Text>
|
||||
</Row>
|
||||
<Divider margin={spacings.zero} />
|
||||
<Column padding={spacings.md} gap={spacings.md}>
|
||||
<Banner variant="warning">
|
||||
<Translation id="TR_AFFECTED_TXS" />
|
||||
</Banner>
|
||||
<Table>
|
||||
<Table.Body>
|
||||
{chainedTxs.own.map(tx => (
|
||||
<Table.Row key={tx.txid}>
|
||||
<Table.Cell>
|
||||
<AffectedTransactionItem tx={tx} isAccountOwned />
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
{chainedTxs.others.map(tx => (
|
||||
<Table.Row key={tx.txid}>
|
||||
<Table.Cell>
|
||||
<AffectedTransactionItem tx={tx} />
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</Column>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
import { NetworkSymbol } from '@suite-common/wallet-config';
|
||||
import { Account, FormState } from '@suite-common/wallet-types';
|
||||
import { formatNetworkAmount } from '@suite-common/wallet-utils';
|
||||
import {
|
||||
Banner,
|
||||
Card,
|
||||
Column,
|
||||
Divider,
|
||||
Icon,
|
||||
Link,
|
||||
RadioCard,
|
||||
Row,
|
||||
Text,
|
||||
} from '@trezor/components';
|
||||
import { spacings } from '@trezor/theme';
|
||||
import { HELP_CENTER_REPLACE_BY_FEE_BITCOIN } from '@trezor/urls';
|
||||
|
||||
import { Address, FormattedCryptoAmount, HiddenPlaceholder } from 'src/components/suite';
|
||||
import { Translation, TranslationKey } from 'src/components/suite/Translation';
|
||||
import { RbfContextValues, useRbfContext } from 'src/hooks/wallet/useRbfForm';
|
||||
|
||||
type AmountRowProps = {
|
||||
labelTranslationKey: TranslationKey;
|
||||
shouldSendInSats: boolean | undefined;
|
||||
amount: string;
|
||||
symbol: NetworkSymbol;
|
||||
};
|
||||
|
||||
const AmountItem = ({ labelTranslationKey, shouldSendInSats, amount, symbol }: AmountRowProps) => {
|
||||
const value = shouldSendInSats ? formatNetworkAmount(amount, symbol) : amount;
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<Text variant="tertiary" typographyStyle="label">
|
||||
<Translation id={labelTranslationKey} />
|
||||
</Text>
|
||||
<FormattedCryptoAmount value={value} symbol={symbol} />
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
type ReducedAmount = {
|
||||
composedLevels: RbfContextValues['composedLevels'];
|
||||
setMaxOutputId: number;
|
||||
account: Account;
|
||||
selectedFee: FormState['selectedFee'];
|
||||
};
|
||||
|
||||
const ReducedAmount = ({ composedLevels, setMaxOutputId, account, selectedFee }: ReducedAmount) => {
|
||||
if (!composedLevels) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const precomposedTx = composedLevels[selectedFee || 'normal'];
|
||||
|
||||
if (precomposedTx.type !== 'final') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Icon name="arrowRightLong" />
|
||||
<AmountItem
|
||||
labelTranslationKey="TR_RBF_NEW_AMOUNT"
|
||||
amount={precomposedTx.outputs[setMaxOutputId].amount.toString()}
|
||||
symbol={account.symbol}
|
||||
shouldSendInSats={true} // precomposedTx.outputs is always in Sats
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const DecreasedOutputs = () => {
|
||||
const {
|
||||
showDecreasedOutputs,
|
||||
formValues,
|
||||
account,
|
||||
coinjoinRegisteredUtxos,
|
||||
getValues,
|
||||
setValue,
|
||||
composedLevels,
|
||||
composeRequest,
|
||||
shouldSendInSats,
|
||||
} = useRbfContext();
|
||||
const { selectedFee, setMaxOutputId } = getValues();
|
||||
|
||||
// no set-max means that no output was decreased
|
||||
if (!showDecreasedOutputs || typeof setMaxOutputId !== 'number') return null;
|
||||
|
||||
// find all outputs possible to reduce
|
||||
const useRadio = formValues.outputs.filter(o => typeof o.address === 'string').length > 1;
|
||||
|
||||
const getDecreaseWarring = (): TranslationKey => {
|
||||
if (account.accountType === 'coinjoin') {
|
||||
if (coinjoinRegisteredUtxos.length > 0) {
|
||||
return 'TR_UTXO_REGISTERED_IN_COINJOIN_RBF_WARNING';
|
||||
} else {
|
||||
return 'TR_NOT_ENOUGH_ANONYMIZED_FUNDS_RBF_WARNING';
|
||||
}
|
||||
}
|
||||
|
||||
return 'TR_DECREASE_TX';
|
||||
};
|
||||
|
||||
return (
|
||||
<Card fillType="flat" paddingType="none">
|
||||
<Row justifyContent="space-between" alignItems="center" padding={spacings.md}>
|
||||
<Text typographyStyle="body">
|
||||
<Translation id="TR_AMOUNT_REDUCED_TXS" />
|
||||
</Text>
|
||||
<Text variant="primary" typographyStyle="hint">
|
||||
<Link
|
||||
icon="arrowUpRight"
|
||||
variant="nostyle"
|
||||
href={HELP_CENTER_REPLACE_BY_FEE_BITCOIN}
|
||||
>
|
||||
<Translation id="TR_LEARN_MORE" />
|
||||
</Link>
|
||||
</Text>
|
||||
</Row>
|
||||
|
||||
<Divider margin={spacings.zero} />
|
||||
<Column margin={spacings.md} gap={spacings.md}>
|
||||
<Banner variant="warning" data-testid="@send/decreased-outputs" icon="warning">
|
||||
<Translation id={getDecreaseWarring()} />
|
||||
</Banner>
|
||||
{useRadio && (
|
||||
<Text>
|
||||
<Translation id="TR_DECREASED_AMOUNT_SELECTION_EXPLANATION" />
|
||||
</Text>
|
||||
)}
|
||||
<Column gap={spacings.md} alignItems="center">
|
||||
{formValues.outputs.flatMap((output, i) => {
|
||||
if (typeof output.address !== 'string') return null;
|
||||
const isChecked = setMaxOutputId === i;
|
||||
|
||||
return (
|
||||
// it's safe to use array index as key since outputs do not change
|
||||
<RadioCard
|
||||
key={i}
|
||||
onClick={() => {
|
||||
if (useRadio) {
|
||||
setValue('setMaxOutputId', i);
|
||||
composeRequest();
|
||||
}
|
||||
}}
|
||||
isActive={useRadio && isChecked}
|
||||
>
|
||||
<Row gap={spacings.sm}>
|
||||
<AmountItem
|
||||
labelTranslationKey="TR_RBF_ORIGINAL_AMOUNT"
|
||||
amount={output.amount}
|
||||
symbol={account.symbol}
|
||||
shouldSendInSats={shouldSendInSats}
|
||||
/>
|
||||
{isChecked && (
|
||||
<ReducedAmount
|
||||
account={account}
|
||||
selectedFee={selectedFee}
|
||||
composedLevels={composedLevels}
|
||||
setMaxOutputId={setMaxOutputId}
|
||||
/>
|
||||
)}
|
||||
<Column margin={{ left: 'auto' }}>
|
||||
<Text variant="tertiary" typographyStyle="label">
|
||||
<Translation id="TR_RECIPIENT_ADDRESS" />
|
||||
</Text>
|
||||
<HiddenPlaceholder>
|
||||
<Address value={output.address} isTruncated />
|
||||
</HiddenPlaceholder>
|
||||
</Column>
|
||||
</Row>
|
||||
</RadioCard>
|
||||
);
|
||||
})}
|
||||
</Column>
|
||||
</Column>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import { variables } from '@trezor/components';
|
||||
import { Translation, TrezorLink } from 'src/components/suite';
|
||||
import { TransactionItem } from 'src/components/wallet/TransactionItem/TransactionItem';
|
||||
|
||||
import { AffectedTransactionItem } from './ChangeFee/AffectedTransactionItem';
|
||||
import { AffectedTransactionItem } from './AffectedTransactions/AffectedTransactionItem';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
text-align: left;
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import styled, { useTheme } from 'styled-components';
|
||||
|
||||
import { WalletAccountTransaction } from '@suite-common/wallet-types';
|
||||
import { Icon, variables } from '@trezor/components';
|
||||
import { truncateMiddle } from '@trezor/utils';
|
||||
|
||||
import { FormattedDate } from 'src/components/suite';
|
||||
import { useLayoutSize } from 'src/hooks/suite/useLayoutSize';
|
||||
|
||||
const TxRow = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
`;
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
padding-right: 24px;
|
||||
`;
|
||||
|
||||
const Text = styled.span`
|
||||
color: ${({ theme }) => theme.legacy.TYPE_LIGHT_GREY};
|
||||
font-weight: ${variables.FONT_WEIGHT.MEDIUM};
|
||||
font-size: ${variables.FONT_SIZE.SMALL};
|
||||
font-variant-numeric: tabular-nums;
|
||||
`;
|
||||
|
||||
const Txid = styled(Text)`
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
font-variant-numeric: slashed-zero tabular-nums;
|
||||
`;
|
||||
|
||||
const Timestamp = styled(Text)`
|
||||
white-space: nowrap;
|
||||
margin-left: 4px;
|
||||
`;
|
||||
|
||||
const Bullet = styled.div`
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
color: ${({ theme }) => theme.legacy.TYPE_LIGHT_GREY};
|
||||
`;
|
||||
|
||||
export const AffectedTransactionItem = ({
|
||||
tx,
|
||||
isAccountOwned,
|
||||
className,
|
||||
}: {
|
||||
tx: WalletAccountTransaction;
|
||||
isAccountOwned?: boolean;
|
||||
className?: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { isMobileLayout } = useLayoutSize();
|
||||
const shownTxidChars = isMobileLayout ? 4 : 8;
|
||||
const iconType = tx.type === 'recv' ? 'receive' : 'send';
|
||||
|
||||
return (
|
||||
<TxRow className={className}>
|
||||
{!isMobileLayout && (
|
||||
<IconWrapper>
|
||||
<Icon
|
||||
size={16}
|
||||
color={theme.legacy.TYPE_LIGHT_GREY}
|
||||
name={isAccountOwned ? iconType : 'clock'}
|
||||
/>
|
||||
</IconWrapper>
|
||||
)}
|
||||
|
||||
{tx.blockTime && (
|
||||
<>
|
||||
<Timestamp>
|
||||
<FormattedDate value={new Date(tx.blockTime * 1000)} time />
|
||||
</Timestamp>
|
||||
<Bullet>•</Bullet>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Txid>{truncateMiddle(tx.txid, shownTxidChars, shownTxidChars + 2)}</Txid>
|
||||
</TxRow>
|
||||
);
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Button } from '@trezor/components';
|
||||
|
||||
import { Translation } from 'src/components/suite';
|
||||
import { useRbfContext } from 'src/hooks/wallet/useRbfForm';
|
||||
|
||||
import { AffectedTransactionItem } from './AffectedTransactionItem';
|
||||
import { GreyCard } from './GreyCard';
|
||||
import { WarnHeader } from './WarnHeader';
|
||||
|
||||
const ChainedTxs = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 24px;
|
||||
margin-top: 24px;
|
||||
border-top: 1px solid ${({ theme }) => theme.legacy.STROKE_GREY};
|
||||
`;
|
||||
|
||||
export const AffectedTransactions = ({ showChained }: { showChained: () => void }) => {
|
||||
const { chainedTxs } = useRbfContext();
|
||||
|
||||
if (!chainedTxs) return null;
|
||||
|
||||
return (
|
||||
<GreyCard>
|
||||
<WarnHeader
|
||||
action={
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={showChained}
|
||||
icon="caretRight"
|
||||
iconAlignment="right"
|
||||
>
|
||||
<Translation id="TR_SEE_DETAILS" />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Translation id="TR_AFFECTED_TXS" />
|
||||
</WarnHeader>
|
||||
<ChainedTxs>
|
||||
{chainedTxs.own.map(tx => (
|
||||
<AffectedTransactionItem key={tx.txid} tx={tx} isAccountOwned />
|
||||
))}
|
||||
{chainedTxs.others.map(tx => (
|
||||
<AffectedTransactionItem key={tx.txid} tx={tx} />
|
||||
))}
|
||||
</ChainedTxs>
|
||||
</GreyCard>
|
||||
);
|
||||
};
|
||||
@@ -2,16 +2,16 @@ import { ReactNode } from 'react';
|
||||
|
||||
import { WalletAccountTransaction } from '@suite-common/wallet-types';
|
||||
import { formatNetworkAmount, getFeeUnits } from '@suite-common/wallet-utils';
|
||||
import { Card, Divider, InfoItem, Row, Text } from '@trezor/components';
|
||||
import { Card, Column, Divider, InfoItem, Row, Text } from '@trezor/components';
|
||||
import { spacings } from '@trezor/theme';
|
||||
|
||||
import { FiatValue, FormattedCryptoAmount, Translation } from 'src/components/suite';
|
||||
import { useSelector } from 'src/hooks/suite';
|
||||
import { UseRbfProps, useRbfContext } from 'src/hooks/wallet/useRbfForm';
|
||||
|
||||
import { AffectedTransactions } from './AffectedTransactions';
|
||||
import { DecreasedOutputs } from './DecreasedOutputs';
|
||||
import { RbfFees } from './RbfFees';
|
||||
import { AffectedTransactions } from '../AffectedTransactions/AffectedTransactions';
|
||||
import { DecreasedOutputs } from '../AffectedTransactions/DecreasedOutputs';
|
||||
|
||||
/* children are only for test purposes, this prop is not available in regular build */
|
||||
interface ChangeFeeProps extends UseRbfProps {
|
||||
@@ -24,6 +24,7 @@ const ChangeFeeLoaded = (props: ChangeFeeProps) => {
|
||||
const { tx, showChained, children } = props;
|
||||
const {
|
||||
account: { networkType },
|
||||
chainedTxs,
|
||||
} = useRbfContext();
|
||||
|
||||
const feeRate =
|
||||
@@ -31,43 +32,52 @@ const ChangeFeeLoaded = (props: ChangeFeeProps) => {
|
||||
const fee = formatNetworkAmount(tx.fee, tx.symbol);
|
||||
|
||||
return (
|
||||
<Card fillType="flat">
|
||||
<InfoItem
|
||||
direction="row"
|
||||
label={
|
||||
<>
|
||||
<Translation id="TR_CURRENT_FEE" />
|
||||
{feeRate && ` (${feeRate})`}
|
||||
</>
|
||||
}
|
||||
typographyStyle="body"
|
||||
>
|
||||
<Row gap={spacings.md} alignItems="baseline">
|
||||
<FormattedCryptoAmount
|
||||
disableHiddenPlaceholder
|
||||
value={fee}
|
||||
symbol={tx.symbol}
|
||||
/>
|
||||
<Text variant="tertiary" typographyStyle="label">
|
||||
<FiatValue
|
||||
disableHiddenPlaceholder
|
||||
amount={fee}
|
||||
symbol={tx.symbol}
|
||||
showApproximationIndicator
|
||||
/>
|
||||
</Text>
|
||||
</Row>
|
||||
</InfoItem>
|
||||
<>
|
||||
<Card fillType="flat" paddingType="none">
|
||||
<Text typographyStyle="body" margin={spacings.md}>
|
||||
<Translation id="TR_BUMP_FEE_SUBTEXT" />
|
||||
</Text>
|
||||
<Divider margin={spacings.zero} />
|
||||
<Column margin={spacings.md}>
|
||||
<InfoItem
|
||||
direction="row"
|
||||
label={
|
||||
<>
|
||||
<Translation id="TR_CURRENT_FEE" />
|
||||
{feeRate && ` (${feeRate})`}
|
||||
</>
|
||||
}
|
||||
typographyStyle="body"
|
||||
>
|
||||
<Row gap={spacings.md} alignItems="baseline">
|
||||
<FormattedCryptoAmount
|
||||
disableHiddenPlaceholder
|
||||
value={fee}
|
||||
symbol={tx.symbol}
|
||||
/>
|
||||
<Text variant="tertiary" typographyStyle="label">
|
||||
<FiatValue
|
||||
disableHiddenPlaceholder
|
||||
amount={fee}
|
||||
symbol={tx.symbol}
|
||||
showApproximationIndicator
|
||||
/>
|
||||
</Text>
|
||||
</Row>
|
||||
</InfoItem>
|
||||
|
||||
<Divider />
|
||||
<Divider />
|
||||
|
||||
<RbfFees />
|
||||
<RbfFees />
|
||||
|
||||
{children}
|
||||
</Column>
|
||||
</Card>
|
||||
|
||||
<DecreasedOutputs />
|
||||
<AffectedTransactions showChained={showChained} />
|
||||
|
||||
{children}
|
||||
</Card>
|
||||
<AffectedTransactions chainedTxs={chainedTxs} showChained={showChained} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { formatNetworkAmount } from '@suite-common/wallet-utils';
|
||||
import { Icon, Radio, motionAnimation, variables } from '@trezor/components';
|
||||
import { spacings } from '@trezor/theme';
|
||||
|
||||
import { FormattedCryptoAmount, HiddenPlaceholder } from 'src/components/suite';
|
||||
import { Translation, TranslationKey } from 'src/components/suite/Translation';
|
||||
import { useRbfContext } from 'src/hooks/wallet/useRbfForm';
|
||||
|
||||
import { GreyCard } from './GreyCard';
|
||||
import { WarnHeader } from './WarnHeader';
|
||||
|
||||
const OutputsWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 12px;
|
||||
margin-top: 24px;
|
||||
border-top: 1px solid ${({ theme }) => theme.legacy.STROKE_GREY};
|
||||
`;
|
||||
|
||||
const Output = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
`;
|
||||
|
||||
const OutputInner = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const OutputLabel = styled.div<{ $isChecked: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: ${variables.FONT_SIZE.NORMAL};
|
||||
line-height: 24px; /* icon height */
|
||||
font-weight: ${$props =>
|
||||
$props.$isChecked ? variables.FONT_WEIGHT.DEMI_BOLD : variables.FONT_WEIGHT.MEDIUM};
|
||||
color: ${({ $isChecked, theme }) => ($isChecked ? theme.legacy.TYPE_GREEN : 'inherit')};
|
||||
`;
|
||||
|
||||
const OutputAddress = styled.div<{ $isChecked: boolean }>`
|
||||
font-size: ${variables.FONT_SIZE.TINY};
|
||||
font-weight: ${$props =>
|
||||
$props.$isChecked ? variables.FONT_WEIGHT.DEMI_BOLD : variables.FONT_WEIGHT.MEDIUM};
|
||||
color: ${({ $isChecked, theme }) => ($isChecked ? theme.legacy.TYPE_DARK_GREY : 'inherit')};
|
||||
padding-top: 2px;
|
||||
`;
|
||||
|
||||
const ReducedAmount = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const DecreasedOutputs = () => {
|
||||
const {
|
||||
showDecreasedOutputs,
|
||||
formValues,
|
||||
account,
|
||||
coinjoinRegisteredUtxos,
|
||||
getValues,
|
||||
setValue,
|
||||
composedLevels,
|
||||
composeRequest,
|
||||
shouldSendInSats,
|
||||
} = useRbfContext();
|
||||
const { selectedFee, setMaxOutputId } = getValues();
|
||||
|
||||
// no set-max means that no output was decreased
|
||||
if (!showDecreasedOutputs || typeof setMaxOutputId !== 'number') return null;
|
||||
|
||||
let reducedAmount: ReactNode = null;
|
||||
if (composedLevels) {
|
||||
const precomposedTx = composedLevels[selectedFee || 'normal'];
|
||||
if (precomposedTx.type === 'final') {
|
||||
reducedAmount = (
|
||||
<ReducedAmount>
|
||||
<Icon
|
||||
name="arrowRightLong"
|
||||
margin={{ left: spacings.sm, right: spacings.sm }}
|
||||
variant="primary"
|
||||
/>
|
||||
<FormattedCryptoAmount
|
||||
value={formatNetworkAmount(
|
||||
precomposedTx.outputs[setMaxOutputId].amount.toString(),
|
||||
account.symbol,
|
||||
)}
|
||||
symbol={account.symbol}
|
||||
/>
|
||||
</ReducedAmount>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// find all outputs possible to reduce
|
||||
const useRadio = formValues.outputs.filter(o => typeof o.address === 'string').length > 1;
|
||||
|
||||
let decreaseWarning: TranslationKey = 'TR_DECREASE_TX';
|
||||
if (account.accountType === 'coinjoin') {
|
||||
if (coinjoinRegisteredUtxos.length > 0) {
|
||||
decreaseWarning = 'TR_UTXO_REGISTERED_IN_COINJOIN_RBF_WARNING';
|
||||
} else {
|
||||
decreaseWarning = 'TR_NOT_ENOUGH_ANONYMIZED_FUNDS_RBF_WARNING';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatePresence initial>
|
||||
<motion.div {...motionAnimation.expand}>
|
||||
<GreyCard>
|
||||
<WarnHeader data-testid="@send/decreased-outputs">
|
||||
<Translation id={decreaseWarning} />
|
||||
</WarnHeader>
|
||||
<OutputsWrapper>
|
||||
{formValues.outputs.flatMap((o, i) => {
|
||||
if (typeof o.address !== 'string') return null;
|
||||
const isChecked = setMaxOutputId === i;
|
||||
|
||||
return (
|
||||
// it's safe to use array index as key since outputs do not change
|
||||
|
||||
<Output key={i}>
|
||||
{useRadio && (
|
||||
<Radio
|
||||
onClick={() => {
|
||||
setValue('setMaxOutputId', i);
|
||||
composeRequest();
|
||||
}}
|
||||
isChecked={isChecked}
|
||||
margin={{ right: spacings.xs }}
|
||||
/>
|
||||
)}
|
||||
<OutputInner>
|
||||
<OutputLabel $isChecked={isChecked}>
|
||||
<Translation
|
||||
id="TR_REDUCE_FROM"
|
||||
values={{
|
||||
value: (
|
||||
<FormattedCryptoAmount
|
||||
value={
|
||||
shouldSendInSats
|
||||
? formatNetworkAmount(
|
||||
o.amount,
|
||||
account.symbol,
|
||||
)
|
||||
: o.amount
|
||||
}
|
||||
symbol={account.symbol}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{isChecked && reducedAmount}
|
||||
</OutputLabel>
|
||||
<OutputAddress $isChecked={isChecked}>
|
||||
<HiddenPlaceholder>{o.address}</HiddenPlaceholder>
|
||||
</OutputAddress>
|
||||
</OutputInner>
|
||||
</Output>
|
||||
);
|
||||
})}
|
||||
</OutputsWrapper>
|
||||
</GreyCard>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Card, Column } from '@trezor/components';
|
||||
|
||||
// eslint-disable-next-line local-rules/no-override-ds-component
|
||||
const Wrapper = styled(Card)`
|
||||
text-align: left;
|
||||
background-color: ${({ theme }) => theme.legacy.BG_GREY};
|
||||
margin-left: -10px;
|
||||
margin-right: -10px;
|
||||
margin-top: 12px;
|
||||
`;
|
||||
|
||||
export const GreyCard = (props: { children?: ReactNode }) => (
|
||||
<Wrapper>
|
||||
<Column alignItems="center">{props.children}</Column>
|
||||
</Wrapper>
|
||||
);
|
||||
@@ -1,37 +0,0 @@
|
||||
import { HTMLAttributes, ReactNode } from 'react';
|
||||
|
||||
import styled, { useTheme } from 'styled-components';
|
||||
|
||||
import { Icon, variables } from '@trezor/components';
|
||||
|
||||
const Header = styled.div`
|
||||
display: flex;
|
||||
margin-top: 4px;
|
||||
color: ${({ theme }) => theme.legacy.TYPE_ORANGE};
|
||||
font-weight: ${variables.FONT_WEIGHT.DEMI_BOLD};
|
||||
font-size: ${variables.FONT_SIZE.TINY};
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Body = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding: 0 16px;
|
||||
`;
|
||||
|
||||
interface WarnHeaderProps extends HTMLAttributes<HTMLDivElement> {
|
||||
action?: ReactNode;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const WarnHeader = ({ action, children, ...rest }: WarnHeaderProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Header {...rest}>
|
||||
<Icon size={16} name="warningTriangle" color={theme.legacy.TYPE_ORANGE} />
|
||||
<Body>{children}</Body>
|
||||
{action}
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
@@ -55,7 +55,7 @@ export const TxDetailModalBase = ({
|
||||
bottomContent={bottomContent}
|
||||
onBackClick={onBackClick}
|
||||
>
|
||||
<Column gap={spacings.lg}>
|
||||
<Column gap={spacings.md}>
|
||||
<BasicTxDetails
|
||||
explorerUrl={blockchain.explorer.tx}
|
||||
explorerUrlQueryString={blockchain.explorer.queryString}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from '@suite-common/wallet-utils';
|
||||
import { Button, Card, Column, Link, Row, Tooltip } from '@trezor/components';
|
||||
import { spacings } from '@trezor/theme';
|
||||
import { HELP_CENTER_REPLACE_BY_FEE } from '@trezor/urls';
|
||||
import { HELP_CENTER_REPLACE_BY_FEE_ETHEREUM } from '@trezor/urls';
|
||||
|
||||
import { openModal } from 'src/actions/suite/modalActions';
|
||||
import { OutlineHighlight } from 'src/components/OutlineHighlight';
|
||||
@@ -193,7 +193,7 @@ export const TransactionItem = memo(
|
||||
values={{
|
||||
a: chunks => (
|
||||
<Link
|
||||
href={HELP_CENTER_REPLACE_BY_FEE}
|
||||
href={HELP_CENTER_REPLACE_BY_FEE_ETHEREUM}
|
||||
variant="nostyle"
|
||||
icon="arrowUpRight"
|
||||
typographyStyle="hint"
|
||||
@@ -477,6 +477,7 @@ export const TransactionItem = memo(
|
||||
flex="1"
|
||||
alignItems="flex-start"
|
||||
margin={{ bottom: spacings.xxs }}
|
||||
gap={spacings.sm}
|
||||
>
|
||||
{disableBumpFee ? (
|
||||
<DisabledBumpFeeButtonWithTooltip />
|
||||
|
||||
@@ -285,7 +285,8 @@ export const useRbf = (props: UseRbfProps) => {
|
||||
};
|
||||
|
||||
// context accepts only valid state (non-nullable account)
|
||||
type RbfContextValues = ReturnType<typeof useRbf> & NonNullable<ReturnType<typeof useRbfState>>;
|
||||
export type RbfContextValues = ReturnType<typeof useRbf> &
|
||||
NonNullable<ReturnType<typeof useRbfState>>;
|
||||
|
||||
export const RbfContext = createContext<RbfContextValues | null>(null);
|
||||
RbfContext.displayName = 'RbfContext';
|
||||
|
||||
@@ -6150,6 +6150,10 @@ export default defineMessages({
|
||||
id: 'TR_CHAINED_TXS',
|
||||
defaultMessage: 'Chained transactions',
|
||||
},
|
||||
TR_AMOUNT_REDUCED_TXS: {
|
||||
id: 'TR_AMOUNT_REDUCED_TXS',
|
||||
defaultMessage: 'Amount reduced',
|
||||
},
|
||||
TR_DATA: {
|
||||
id: 'TR_DATA',
|
||||
defaultMessage: 'Data',
|
||||
@@ -6402,6 +6406,10 @@ export default defineMessages({
|
||||
id: 'TR_BUMP_FEE',
|
||||
defaultMessage: 'Bump fee',
|
||||
},
|
||||
TR_BUMP_FEE_SUBTEXT: {
|
||||
id: 'TR_BUMP_FEE_SUBTEXT',
|
||||
defaultMessage: 'Speed up this transaction confirmation by paying a higher fee.',
|
||||
},
|
||||
TR_REPLACE_TX: {
|
||||
id: 'TR_REPLACE_TX',
|
||||
defaultMessage: 'Replace transaction',
|
||||
@@ -6430,9 +6438,17 @@ export default defineMessages({
|
||||
id: 'TR_DECREASE_TX',
|
||||
defaultMessage: 'No funds left for fee. Final amount needs to be reduced to bump fee.',
|
||||
},
|
||||
TR_REDUCE_FROM: {
|
||||
id: 'TR_REDUCE_FROM',
|
||||
defaultMessage: 'Reduce from {value}',
|
||||
TR_RBF_ORIGINAL_AMOUNT: {
|
||||
id: 'TR_RBF_ORIGINAL_AMOUNT',
|
||||
defaultMessage: 'Original amount',
|
||||
},
|
||||
TR_RBF_NEW_AMOUNT: {
|
||||
id: 'TR_RBF_NEW_AMOUNT',
|
||||
defaultMessage: 'New amount',
|
||||
},
|
||||
TR_DECREASED_AMOUNT_SELECTION_EXPLANATION: {
|
||||
id: 'TR_DECREASED_AMOUNT_SELECTION_EXPLANATION',
|
||||
defaultMessage: 'Select the amount that should be reduced to pay for the increased fee.',
|
||||
},
|
||||
TR_DECREASE_AMOUNT_BY: {
|
||||
id: 'TR_DECREASE_AMOUNT_BY',
|
||||
|
||||
@@ -113,8 +113,10 @@ export const HELP_CENTER_EVM_SEND_TO_CONTRACT_URL =
|
||||
'https://trezor.io/support/a/where-is-my-ethereum';
|
||||
export const HELP_CENTER_FIRMWARE_REVISION_CHECK: Url =
|
||||
'https://trezor.io/learn/a/trezor-firmware-authenticity-check';
|
||||
export const HELP_CENTER_REPLACE_BY_FEE: Url =
|
||||
export const HELP_CENTER_REPLACE_BY_FEE_ETHEREUM: Url =
|
||||
'https://trezor.io/learn/a/replace-by-fee-rbf-ethereum';
|
||||
export const HELP_CENTER_REPLACE_BY_FEE_BITCOIN =
|
||||
'https://trezor.io/learn/a/replace-by-fee-rbf-bitcoin';
|
||||
|
||||
export const INVITY_URL: Url = 'https://invity.io/';
|
||||
export const INVITY_SCHEDULE_OF_FEES: Url = 'https://blog.invity.io/schedule-of-fees';
|
||||
|
||||
@@ -51,7 +51,6 @@ export * from './splitStringEveryNCharacters';
|
||||
export * from './throttler';
|
||||
export * from './throwError';
|
||||
export * from './topologicalSort';
|
||||
export * from './truncateMiddle';
|
||||
export * from './typedEventEmitter';
|
||||
export * from './urlToOnion';
|
||||
export * from './zip';
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export const truncateMiddle = (text: string, startChars: number, endChars: number) => {
|
||||
if (text.length <= startChars + endChars) return text;
|
||||
const start = text.substring(0, startChars);
|
||||
const end = text.substring(text.length - endChars, text.length);
|
||||
|
||||
return `${start}…${end}`;
|
||||
};
|
||||
Reference in New Issue
Block a user