Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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} />
adamhavel marked this conversation as resolved.
Show resolved Hide resolved

<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>
);
Original file line number Diff line number Diff line change
@@ -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">
adamhavel marked this conversation as resolved.
Show resolved Hide resolved
<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>
);
};
Original file line number Diff line number Diff line change
@@ -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>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

This file was deleted.

Loading
Loading