Skip to content

Commit 1549e50

Browse files
committed
feat(suite): fees redesign
1 parent b646159 commit 1549e50

File tree

6 files changed

+525
-224
lines changed

6 files changed

+525
-224
lines changed

Diff for: packages/suite/src/components/wallet/Fees/CustomFee.tsx

+31-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,18 @@ import {
1111
import { NetworkType } from '@suite-common/wallet-config';
1212
import { FeeInfo, FormState } from '@suite-common/wallet-types';
1313
import { getFeeUnits, getInputState, isInteger } from '@suite-common/wallet-utils';
14-
import { Banner, Column, Grid, Note, Text, useMediaQuery, variables } from '@trezor/components';
14+
import {
15+
Banner,
16+
Column,
17+
Grid,
18+
Icon,
19+
Note,
20+
Row,
21+
Text,
22+
useMediaQuery,
23+
variables,
24+
} from '@trezor/components';
25+
import { FeeLevel } from '@trezor/connect';
1526
import { NumberInput } from '@trezor/product-components';
1627
import { spacings } from '@trezor/theme';
1728
import { HELP_CENTER_TRANSACTION_FEES_URL } from '@trezor/urls';
@@ -36,16 +47,16 @@ interface CustomFeeProps<TFieldValues extends FormState> {
3647
control: Control;
3748
setValue: UseFormSetValue<TFieldValues>;
3849
getValues: UseFormGetValues<TFieldValues>;
39-
changeFeeLimit?: (value: string) => void;
4050
composedFeePerByte: string;
4151
}
4252

53+
const getCurrentFee = (levels: FeeLevel[]) => `${levels[levels.length > 2 ? 1 : 0].feePerUnit}`;
54+
4355
export const CustomFee = <TFieldValues extends FormState>({
4456
networkType,
4557
feeInfo,
4658
register,
4759
control,
48-
changeFeeLimit,
4960
composedFeePerByte,
5061
...props
5162
}: CustomFeeProps<TFieldValues>) => {
@@ -146,7 +157,7 @@ export const CustomFee = <TFieldValues extends FormState>({
146157
feeLimitError?.type === 'feeLimit' ? feeLimitValidationProps : undefined;
147158

148159
return (
149-
<Column gap={spacings.xs}>
160+
<Column gap={spacings.md} margin={{ bottom: spacings.md }}>
150161
<Banner
151162
icon
152163
variant="warning"
@@ -160,6 +171,22 @@ export const CustomFee = <TFieldValues extends FormState>({
160171
>
161172
<Translation id="TR_CUSTOM_FEE_WARNING" />
162173
</Banner>
174+
<Row justifyContent="space-between">
175+
<Text variant="tertiary">
176+
<Translation id="TR_CURRENT_FEE_CUSTOM_FEES" />
177+
</Text>
178+
<Text variant="tertiary">
179+
<Row alignItems="center" gap={spacings.xxs}>
180+
<Text>
181+
{getCurrentFee(feeInfo.levels)} {getFeeUnits(networkType)}
182+
</Text>
183+
<Icon
184+
name={networkType === 'ethereum' ? 'gasPump' : 'receipt'}
185+
size="mediumLarge"
186+
/>
187+
</Row>
188+
</Text>
189+
</Row>
163190
<Grid gap={spacings.xs} columns={useFeeLimit && !isBelowLaptop ? 2 : 1}>
164191
{useFeeLimit ? (
165192
<NumberInput
@@ -169,7 +196,6 @@ export const CustomFee = <TFieldValues extends FormState>({
169196
inputState={getInputState(feeLimitError)}
170197
name={FEE_LIMIT}
171198
data-testid={FEE_LIMIT}
172-
onChange={changeFeeLimit}
173199
bottomText={
174200
feeLimitError?.message ? (
175201
<InputError

Diff for: packages/suite/src/components/wallet/Fees/FeeDetails.tsx

+196-60
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,256 @@
1-
import React, { useEffect, useState } from 'react';
1+
import React from 'react';
2+
3+
import styled from 'styled-components';
24

35
import { formatDuration } from '@suite-common/suite-utils';
4-
import { NetworkType } from '@suite-common/wallet-config';
6+
import { NetworkSymbol, NetworkType } from '@suite-common/wallet-config';
57
import {
68
FeeInfo,
79
PrecomposedTransaction,
810
PrecomposedTransactionCardano,
911
} from '@suite-common/wallet-types';
1012
import { getFeeUnits } from '@suite-common/wallet-utils';
11-
import { Row, Text } from '@trezor/components';
13+
import {
14+
Box,
15+
Column,
16+
Grid,
17+
RadioCard,
18+
Row,
19+
Text,
20+
useMediaQuery,
21+
variables,
22+
} from '@trezor/components';
1223
import { FeeLevel } from '@trezor/connect';
1324
import { spacings } from '@trezor/theme';
1425

15-
import { Translation } from 'src/components/suite/Translation';
26+
import { FiatValue } from 'src/components/suite/FiatValue';
27+
28+
import { FeeOption } from './Fees';
1629

1730
type DetailsProps = {
1831
networkType: NetworkType;
32+
symbol: NetworkSymbol;
1933
selectedLevel: FeeLevel;
2034
// fields below are validated as false-positives, eslint claims that they are not used...
21-
35+
feeOptions: FeeOption[];
2236
feeInfo: FeeInfo;
23-
37+
selectedLevelOption: string;
38+
setSelectedLevelOption: (option: string) => void;
39+
changeFeeLevel: (level: FeeLevel['label']) => void;
2440
transactionInfo?: PrecomposedTransaction | PrecomposedTransactionCardano;
2541

2642
showFee: boolean;
2743
};
2844

29-
type ItemProps = {
30-
label: React.ReactNode;
31-
children: React.ReactNode;
45+
export const FeeCardsWrapper = styled.div`
46+
width: 100%;
47+
display: grid;
48+
`;
49+
50+
type FeeCardProps = {
51+
value: FeeLevel['label'];
52+
setSelectedLevelOption: (option: string) => void;
53+
isSelected: boolean;
54+
changeFeeLevel: (level: FeeLevel['label']) => void;
55+
topLeftChild: React.ReactNode;
56+
topRightChild: React.ReactNode;
57+
bottomLeftChild: React.ReactNode;
58+
bottomRightChild: React.ReactNode;
3259
};
3360

34-
const Item = ({ label, children }: ItemProps) => (
35-
<Row gap={spacings.xxs}>
36-
<Text variant="tertiary">{label}:</Text>
37-
{children}
38-
</Row>
61+
const FeeCard = ({
62+
value,
63+
setSelectedLevelOption,
64+
isSelected,
65+
changeFeeLevel,
66+
topLeftChild,
67+
topRightChild,
68+
bottomLeftChild,
69+
bottomRightChild,
70+
}: FeeCardProps) => (
71+
<Box minWidth={170} margin={spacings.xxs}>
72+
<RadioCard
73+
onClick={() => {
74+
setSelectedLevelOption(value);
75+
changeFeeLevel(value);
76+
}}
77+
isActive={isSelected}
78+
>
79+
<Column>
80+
<Row justifyContent="space-between">
81+
<Text typographyStyle="highlight">{topLeftChild}</Text>
82+
<Text variant="tertiary">{topRightChild}</Text>
83+
</Row>
84+
<Row justifyContent="space-between">
85+
<Text>{bottomLeftChild}</Text>
86+
<Text variant="tertiary" typographyStyle="hint">
87+
{bottomRightChild}
88+
</Text>
89+
</Row>
90+
</Column>
91+
</RadioCard>
92+
</Box>
3993
);
4094

4195
const BitcoinDetails = ({
4296
networkType,
4397
feeInfo,
44-
selectedLevel,
4598
transactionInfo,
99+
feeOptions,
46100
showFee,
101+
setSelectedLevelOption,
102+
selectedLevelOption,
103+
changeFeeLevel,
104+
symbol,
47105
}: DetailsProps) => {
48106
const hasInfo = transactionInfo && transactionInfo.type !== 'error';
49107

50108
return (
51109
showFee && (
52-
<>
53-
<Item label={<Translation id="ESTIMATED_TIME" />}>
54-
{formatDuration(feeInfo.blockTime * selectedLevel.blocks * 60)}
55-
</Item>
56-
57-
<Item label={<Translation id="TR_FEE_RATE" />}>
58-
{hasInfo ? transactionInfo.feePerByte : selectedLevel.feePerUnit}{' '}
59-
{getFeeUnits(networkType)}
60-
{hasInfo ? ` (${transactionInfo.bytes} B)` : ''}
61-
</Item>
62-
</>
110+
<FeeCardsWrapper>
111+
{feeOptions &&
112+
feeOptions.map((fee, index) => (
113+
<FeeCard
114+
key={index}
115+
value={fee.value}
116+
setSelectedLevelOption={setSelectedLevelOption}
117+
isSelected={selectedLevelOption === fee.value}
118+
changeFeeLevel={changeFeeLevel}
119+
topLeftChild={
120+
<span data-testid={`fee-card/${fee.value}`}>{fee.label}</span>
121+
}
122+
topRightChild={
123+
<>~{formatDuration(feeInfo.blockTime * (fee?.blocks || 0) * 60)}</>
124+
}
125+
bottomLeftChild={
126+
<FiatValue
127+
disableHiddenPlaceholder
128+
amount={fee?.networkAmount || ''}
129+
symbol={symbol}
130+
/>
131+
}
132+
bottomRightChild={
133+
<>
134+
{fee?.feePerUnit} {getFeeUnits(networkType)}
135+
{hasInfo ? ` (${transactionInfo.bytes} B)` : ''}
136+
</>
137+
}
138+
/>
139+
))}
140+
</FeeCardsWrapper>
63141
)
64142
);
65143
};
66144

67145
const EthereumDetails = ({
68-
networkType,
69-
selectedLevel,
70-
transactionInfo,
71146
showFee,
147+
feeOptions,
148+
setSelectedLevelOption,
149+
selectedLevelOption,
150+
changeFeeLevel,
151+
symbol,
152+
networkType,
72153
}: DetailsProps) => {
73-
// States to remember the last known values of feeLimit and feePerByte when isComposedTx was true.
74-
const [lastKnownFeeLimit, setLastKnownFeeLimit] = useState('');
75-
const [lastKnownFeePerByte, setLastKnownFeePerByte] = useState('');
76-
77-
const isComposedTx = transactionInfo && transactionInfo.type !== 'error';
154+
const isBelowTablet = useMediaQuery(`(max-width: ${variables.SCREEN_SIZE.MD})`);
78155

79-
useEffect(() => {
80-
if (isComposedTx && transactionInfo.feeLimit) {
81-
setLastKnownFeeLimit(transactionInfo.feeLimit);
82-
setLastKnownFeePerByte(transactionInfo.feePerByte);
83-
}
84-
}, [isComposedTx, transactionInfo]);
156+
const formatFeePerUnit = (feePerUnit?: string) => {
157+
if (!feePerUnit) return '0.00';
158+
const num = Number(feePerUnit);
85159

86-
const gasLimit = isComposedTx
87-
? transactionInfo.feeLimit
88-
: lastKnownFeeLimit || selectedLevel.feeLimit;
89-
const gasPrice = isComposedTx
90-
? transactionInfo.feePerByte
91-
: lastKnownFeePerByte || selectedLevel.feePerUnit;
160+
return (Math.ceil(num * 100) / 100).toFixed(2);
161+
};
92162

93163
return (
94164
showFee && (
95-
<>
96-
<Item label={<Translation id="TR_GAS_LIMIT" />}>{gasLimit}</Item>
97-
98-
<Item label={<Translation id="TR_GAS_PRICE" />}>
99-
{gasPrice} {getFeeUnits(networkType)}
100-
</Item>
101-
</>
165+
<Column width="100%">
166+
<Grid
167+
margin={{ top: spacings.xxxs }}
168+
columns={isBelowTablet ? 1 : feeOptions.length}
169+
gap={spacings.xs}
170+
>
171+
{feeOptions &&
172+
feeOptions.map((fee, index) => (
173+
<FeeCard
174+
key={index}
175+
value={fee.value}
176+
setSelectedLevelOption={setSelectedLevelOption}
177+
isSelected={selectedLevelOption === fee.value}
178+
changeFeeLevel={changeFeeLevel}
179+
topLeftChild={
180+
<span data-testid={`fee-card/${fee.value}`}>{fee.label}</span>
181+
}
182+
topRightChild=""
183+
bottomLeftChild={
184+
<FiatValue
185+
disableHiddenPlaceholder
186+
amount={fee.networkAmount || ''}
187+
symbol={symbol}
188+
showApproximationIndicator
189+
/>
190+
}
191+
bottomRightChild={
192+
<>
193+
{formatFeePerUnit(fee?.feePerUnit)}{' '}
194+
{getFeeUnits(networkType)}
195+
</>
196+
}
197+
/>
198+
))}
199+
</Grid>
200+
</Column>
102201
)
103202
);
104203
};
105204

106-
const RippleDetails = ({ networkType, selectedLevel, showFee }: DetailsProps) =>
107-
showFee && <Item label={getFeeUnits(networkType)}>{selectedLevel.feePerUnit}</Item>;
205+
// Solana, Ripple, Cardano and other networks with only one option
206+
const MiscDetails = ({ networkType, showFee, feeOptions, symbol }: DetailsProps) =>
207+
showFee && (
208+
<Column padding={spacings.xxxs} width="100%">
209+
<FeeCard
210+
value={feeOptions[0].value}
211+
setSelectedLevelOption={() => {}}
212+
isSelected={true}
213+
changeFeeLevel={() => {}}
214+
topLeftChild={
215+
<span data-testid={`fee-card/${feeOptions[0].value}`}>
216+
{feeOptions[0].label}
217+
</span>
218+
}
219+
topRightChild=""
220+
bottomLeftChild={
221+
<FiatValue
222+
disableHiddenPlaceholder
223+
amount={feeOptions[0].networkAmount || ''}
224+
symbol={symbol}
225+
/>
226+
}
227+
bottomRightChild={
228+
<Text variant="tertiary">
229+
{feeOptions[0].feePerUnit} {getFeeUnits(networkType)}
230+
</Text>
231+
}
232+
/>
233+
</Column>
234+
);
108235

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

239+
const DetailsComponent = () => {
240+
switch (networkType) {
241+
case 'bitcoin':
242+
return <BitcoinDetails {...props} />;
243+
case 'ethereum':
244+
return <EthereumDetails {...props} />;
245+
default:
246+
return <MiscDetails {...props} />;
247+
}
248+
};
249+
112250
return (
113-
<Text data-testid="@wallet/fee-details" as="div" typographyStyle="hint">
114-
<Row gap={spacings.md}>
115-
{networkType === 'bitcoin' && <BitcoinDetails {...props} />}
116-
{networkType === 'ethereum' && <EthereumDetails {...props} />}
117-
{networkType === 'ripple' && <RippleDetails {...props} />}
251+
<Text data-testid="@wallet/fee-details" as="div">
252+
<Row gap={spacings.md} justifyContent="space-evenly">
253+
<DetailsComponent />
118254
</Row>
119255
</Text>
120256
);

0 commit comments

Comments
 (0)