feat: UI facelift of the Affected transactions in the RBF flow

This commit is contained in:
Peter Sanderson
2025-01-23 15:08:18 +01:00
committed by Peter Sanderson
parent bd273e8b46
commit 49fd2cba42
17 changed files with 349 additions and 414 deletions

View File

@@ -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>
);

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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>&bull;</Bullet>
</>
)}
<Txid>{truncateMiddle(tx.txid, shownTxidChars, shownTxidChars + 2)}</Txid>
</TxRow>
);
};

View File

@@ -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>
);
};

View File

@@ -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} />
</>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);

View File

@@ -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>
);
};

View File

@@ -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}

View File

@@ -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 />

View File

@@ -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';

View File

@@ -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',

View File

@@ -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';

View File

@@ -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';

View File

@@ -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}`;
};