Skip to content

Commit

Permalink
feat(suite): fees redesign
Browse files Browse the repository at this point in the history
  • Loading branch information
enjojoy committed Feb 11, 2025
1 parent b646159 commit 92b1582
Show file tree
Hide file tree
Showing 6 changed files with 525 additions and 224 deletions.
36 changes: 31 additions & 5 deletions packages/suite/src/components/wallet/Fees/CustomFee.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,18 @@ import {
import { NetworkType } from '@suite-common/wallet-config';
import { FeeInfo, FormState } from '@suite-common/wallet-types';
import { getFeeUnits, getInputState, isInteger } from '@suite-common/wallet-utils';
import { Banner, Column, Grid, Note, Text, useMediaQuery, variables } from '@trezor/components';
import {
Banner,
Column,
Grid,
Icon,
Note,
Row,
Text,
useMediaQuery,
variables,
} from '@trezor/components';
import { FeeLevel } from '@trezor/connect';
import { NumberInput } from '@trezor/product-components';
import { spacings } from '@trezor/theme';
import { HELP_CENTER_TRANSACTION_FEES_URL } from '@trezor/urls';
Expand All @@ -36,16 +47,16 @@ interface CustomFeeProps<TFieldValues extends FormState> {
control: Control;
setValue: UseFormSetValue<TFieldValues>;
getValues: UseFormGetValues<TFieldValues>;
changeFeeLimit?: (value: string) => void;
composedFeePerByte: string;
}

const getCurrentFee = (levels: FeeLevel[]) => `${levels[levels.length > 2 ? 1 : 0].feePerUnit}`;

export const CustomFee = <TFieldValues extends FormState>({
networkType,
feeInfo,
register,
control,
changeFeeLimit,
composedFeePerByte,
...props
}: CustomFeeProps<TFieldValues>) => {
Expand Down Expand Up @@ -146,7 +157,7 @@ export const CustomFee = <TFieldValues extends FormState>({
feeLimitError?.type === 'feeLimit' ? feeLimitValidationProps : undefined;

return (
<Column gap={spacings.xs}>
<Column gap={spacings.md} margin={{ bottom: spacings.md }}>
<Banner
icon
variant="warning"
Expand All @@ -160,6 +171,22 @@ export const CustomFee = <TFieldValues extends FormState>({
>
<Translation id="TR_CUSTOM_FEE_WARNING" />
</Banner>
<Row justifyContent="space-between">
<Text variant="tertiary">
<Translation id="TR_CURRENT_FEE_CUSTOM_FEES" />
</Text>
<Text variant="tertiary">
<Row alignItems="center" gap={spacings.xxs}>
<Text>
{getCurrentFee(feeInfo.levels)} {getFeeUnits(networkType)}
</Text>
<Icon
name={networkType === 'ethereum' ? 'gasPump' : 'receipt'}
size="mediumLarge"
/>
</Row>
</Text>
</Row>
<Grid gap={spacings.xs} columns={useFeeLimit && !isBelowLaptop ? 2 : 1}>
{useFeeLimit ? (
<NumberInput
Expand All @@ -169,7 +196,6 @@ export const CustomFee = <TFieldValues extends FormState>({
inputState={getInputState(feeLimitError)}
name={FEE_LIMIT}
data-testid={FEE_LIMIT}
onChange={changeFeeLimit}
bottomText={
feeLimitError?.message ? (
<InputError
Expand Down
256 changes: 196 additions & 60 deletions packages/suite/src/components/wallet/Fees/FeeDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,120 +1,256 @@
import React, { useEffect, useState } from 'react';
import React from 'react';

import styled from 'styled-components';

import { formatDuration } from '@suite-common/suite-utils';
import { NetworkType } from '@suite-common/wallet-config';
import { NetworkSymbol, NetworkType } from '@suite-common/wallet-config';
import {
FeeInfo,
PrecomposedTransaction,
PrecomposedTransactionCardano,
} from '@suite-common/wallet-types';
import { getFeeUnits } from '@suite-common/wallet-utils';
import { Row, Text } from '@trezor/components';
import {
Box,
Column,
Grid,
RadioCard,
Row,
Text,
useMediaQuery,
variables,
} from '@trezor/components';
import { FeeLevel } from '@trezor/connect';
import { spacings } from '@trezor/theme';

import { Translation } from 'src/components/suite/Translation';
import { FiatValue } from 'src/components/suite/FiatValue';

import { FeeOption } from './Fees';

type DetailsProps = {
networkType: NetworkType;
symbol: NetworkSymbol;
selectedLevel: FeeLevel;
// fields below are validated as false-positives, eslint claims that they are not used...

feeOptions: FeeOption[];
feeInfo: FeeInfo;

selectedLevelOption: string;
setSelectedLevelOption: (option: string) => void;
changeFeeLevel: (level: FeeLevel['label']) => void;
transactionInfo?: PrecomposedTransaction | PrecomposedTransactionCardano;

showFee: boolean;
};

type ItemProps = {
label: React.ReactNode;
children: React.ReactNode;
export const FeeCardsWrapper = styled.div`
width: 100%;
display: grid;
`;

type FeeCardProps = {
value: FeeLevel['label'];
setSelectedLevelOption: (option: string) => void;
isSelected: boolean;
changeFeeLevel: (level: FeeLevel['label']) => void;
topLeftChild: React.ReactNode;
topRightChild: React.ReactNode;
bottomLeftChild: React.ReactNode;
bottomRightChild: React.ReactNode;
};

const Item = ({ label, children }: ItemProps) => (
<Row gap={spacings.xxs}>
<Text variant="tertiary">{label}:</Text>
{children}
</Row>
const FeeCard = ({
value,
setSelectedLevelOption,
isSelected,
changeFeeLevel,
topLeftChild,
topRightChild,
bottomLeftChild,
bottomRightChild,
}: FeeCardProps) => (
<Box minWidth={170} margin={spacings.xxs}>
<RadioCard
onClick={() => {
setSelectedLevelOption(value);
changeFeeLevel(value);
}}
isActive={isSelected}
>
<Column>
<Row justifyContent="space-between">
<Text typographyStyle="highlight">{topLeftChild}</Text>
<Text variant="tertiary">{topRightChild}</Text>
</Row>
<Row justifyContent="space-between">
<Text>{bottomLeftChild}</Text>
<Text variant="tertiary" typographyStyle="hint">
{bottomRightChild}
</Text>
</Row>
</Column>
</RadioCard>
</Box>
);

const BitcoinDetails = ({
networkType,
feeInfo,
selectedLevel,
transactionInfo,
feeOptions,
showFee,
setSelectedLevelOption,
selectedLevelOption,
changeFeeLevel,
symbol,
}: DetailsProps) => {
const hasInfo = transactionInfo && transactionInfo.type !== 'error';

return (
showFee && (
<>
<Item label={<Translation id="ESTIMATED_TIME" />}>
{formatDuration(feeInfo.blockTime * selectedLevel.blocks * 60)}
</Item>

<Item label={<Translation id="TR_FEE_RATE" />}>
{hasInfo ? transactionInfo.feePerByte : selectedLevel.feePerUnit}{' '}
{getFeeUnits(networkType)}
{hasInfo ? ` (${transactionInfo.bytes} B)` : ''}
</Item>
</>
<FeeCardsWrapper>
{feeOptions &&
feeOptions.map((fee, index) => (
<FeeCard
key={index}
value={fee.value}
setSelectedLevelOption={setSelectedLevelOption}
isSelected={selectedLevelOption === fee.value}
changeFeeLevel={changeFeeLevel}
topLeftChild={
<span data-testid={`fee-card/${fee.value}`}>{fee.label}</span>
}
topRightChild={
<>~{formatDuration(feeInfo.blockTime * (fee?.blocks || 0) * 60)}</>
}
bottomLeftChild={
<FiatValue
disableHiddenPlaceholder
amount={fee?.networkAmount || ''}
symbol={symbol}
/>
}
bottomRightChild={
<>
{fee?.feePerUnit} {getFeeUnits(networkType)}
{hasInfo ? ` (${transactionInfo.bytes} B)` : ''}
</>
}
/>
))}
</FeeCardsWrapper>
)
);
};

const EthereumDetails = ({
networkType,
selectedLevel,
transactionInfo,
showFee,
feeOptions,
setSelectedLevelOption,
selectedLevelOption,
changeFeeLevel,
symbol,
networkType,
}: DetailsProps) => {
// States to remember the last known values of feeLimit and feePerByte when isComposedTx was true.
const [lastKnownFeeLimit, setLastKnownFeeLimit] = useState('');
const [lastKnownFeePerByte, setLastKnownFeePerByte] = useState('');

const isComposedTx = transactionInfo && transactionInfo.type !== 'error';
const isBelowTablet = useMediaQuery(`(max-width: ${variables.SCREEN_SIZE.MD})`);

useEffect(() => {
if (isComposedTx && transactionInfo.feeLimit) {
setLastKnownFeeLimit(transactionInfo.feeLimit);
setLastKnownFeePerByte(transactionInfo.feePerByte);
}
}, [isComposedTx, transactionInfo]);
const formatFeePerUnit = (feePerUnit?: string) => {
if (!feePerUnit) return '0.00';
const num = Number(feePerUnit);

const gasLimit = isComposedTx
? transactionInfo.feeLimit
: lastKnownFeeLimit || selectedLevel.feeLimit;
const gasPrice = isComposedTx
? transactionInfo.feePerByte
: lastKnownFeePerByte || selectedLevel.feePerUnit;
return (Math.ceil(num * 100) / 100).toFixed(2);
};

return (
showFee && (
<>
<Item label={<Translation id="TR_GAS_LIMIT" />}>{gasLimit}</Item>

<Item label={<Translation id="TR_GAS_PRICE" />}>
{gasPrice} {getFeeUnits(networkType)}
</Item>
</>
<Column width="100%">
<Grid
margin={{ top: spacings.xxxs }}
columns={isBelowTablet ? 1 : feeOptions.length}
gap={spacings.xs}
>
{feeOptions &&
feeOptions.map((fee, index) => (
<FeeCard
key={index}
value={fee.value}
setSelectedLevelOption={setSelectedLevelOption}
isSelected={selectedLevelOption === fee.value}
changeFeeLevel={changeFeeLevel}
topLeftChild={
<span data-testid={`fee-card/${fee.value}`}>{fee.label}</span>
}
topRightChild=""
bottomLeftChild={
<FiatValue
disableHiddenPlaceholder
amount={fee.networkAmount || ''}
symbol={symbol}
showApproximationIndicator
/>
}
bottomRightChild={
<>
{formatFeePerUnit(fee?.feePerUnit)}{' '}
{getFeeUnits(networkType)}
</>
}
/>
))}
</Grid>
</Column>
)
);
};

const RippleDetails = ({ networkType, selectedLevel, showFee }: DetailsProps) =>
showFee && <Item label={getFeeUnits(networkType)}>{selectedLevel.feePerUnit}</Item>;
// Solana, Ripple, Cardano and other networks with only one option
const MiscDetails = ({ networkType, showFee, feeOptions, symbol }: DetailsProps) =>
showFee && (
<Column padding={spacings.xxxs} width="100%">
<FeeCard
value={feeOptions[0].value}
setSelectedLevelOption={() => {}}
isSelected={true}
changeFeeLevel={() => {}}
topLeftChild={
<span data-testid={`fee-card/${feeOptions[0].value}`}>
{feeOptions[0].label}
</span>
}
topRightChild=""
bottomLeftChild={
<FiatValue
disableHiddenPlaceholder
amount={feeOptions[0].networkAmount || ''}
symbol={symbol}
/>
}
bottomRightChild={
<Text variant="tertiary">
{feeOptions[0].feePerUnit} {getFeeUnits(networkType)}
</Text>
}
/>
</Column>
);

export const FeeDetails = (props: DetailsProps) => {
const { networkType } = props;

const DetailsComponent = () => {
switch (networkType) {
case 'bitcoin':
return <BitcoinDetails {...props} />;
case 'ethereum':
return <EthereumDetails {...props} />;
default:
return <MiscDetails {...props} />;
}
};

return (
<Text data-testid="@wallet/fee-details" as="div" typographyStyle="hint">
<Row gap={spacings.md}>
{networkType === 'bitcoin' && <BitcoinDetails {...props} />}
{networkType === 'ethereum' && <EthereumDetails {...props} />}
{networkType === 'ripple' && <RippleDetails {...props} />}
<Text data-testid="@wallet/fee-details" as="div">
<Row gap={spacings.md} justifyContent="space-evenly">
<DetailsComponent />
</Row>
</Text>
);
Expand Down
Loading

0 comments on commit 92b1582

Please sign in to comment.