|
1 |
| -import React, { useEffect, useState } from 'react'; |
| 1 | +import React from 'react'; |
| 2 | + |
| 3 | +import styled from 'styled-components'; |
2 | 4 |
|
3 | 5 | import { formatDuration } from '@suite-common/suite-utils';
|
4 |
| -import { NetworkType } from '@suite-common/wallet-config'; |
| 6 | +import { NetworkSymbol, NetworkType } from '@suite-common/wallet-config'; |
5 | 7 | import {
|
6 | 8 | FeeInfo,
|
7 | 9 | PrecomposedTransaction,
|
8 | 10 | PrecomposedTransactionCardano,
|
9 | 11 | } from '@suite-common/wallet-types';
|
10 | 12 | 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'; |
12 | 23 | import { FeeLevel } from '@trezor/connect';
|
13 | 24 | import { spacings } from '@trezor/theme';
|
14 | 25 |
|
15 |
| -import { Translation } from 'src/components/suite/Translation'; |
| 26 | +import { FiatValue } from 'src/components/suite/FiatValue'; |
| 27 | + |
| 28 | +import { FeeOption } from './Fees'; |
16 | 29 |
|
17 | 30 | type DetailsProps = {
|
18 | 31 | networkType: NetworkType;
|
| 32 | + symbol: NetworkSymbol; |
19 | 33 | selectedLevel: FeeLevel;
|
20 | 34 | // fields below are validated as false-positives, eslint claims that they are not used...
|
21 |
| - |
| 35 | + feeOptions: FeeOption[]; |
22 | 36 | feeInfo: FeeInfo;
|
23 |
| - |
| 37 | + selectedLevelOption: string; |
| 38 | + setSelectedLevelOption: (option: string) => void; |
| 39 | + changeFeeLevel: (level: FeeLevel['label']) => void; |
24 | 40 | transactionInfo?: PrecomposedTransaction | PrecomposedTransactionCardano;
|
25 | 41 |
|
26 | 42 | showFee: boolean;
|
27 | 43 | };
|
28 | 44 |
|
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; |
32 | 59 | };
|
33 | 60 |
|
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> |
39 | 93 | );
|
40 | 94 |
|
41 | 95 | const BitcoinDetails = ({
|
42 | 96 | networkType,
|
43 | 97 | feeInfo,
|
44 |
| - selectedLevel, |
45 | 98 | transactionInfo,
|
| 99 | + feeOptions, |
46 | 100 | showFee,
|
| 101 | + setSelectedLevelOption, |
| 102 | + selectedLevelOption, |
| 103 | + changeFeeLevel, |
| 104 | + symbol, |
47 | 105 | }: DetailsProps) => {
|
48 | 106 | const hasInfo = transactionInfo && transactionInfo.type !== 'error';
|
49 | 107 |
|
50 | 108 | return (
|
51 | 109 | 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> |
63 | 141 | )
|
64 | 142 | );
|
65 | 143 | };
|
66 | 144 |
|
67 | 145 | const EthereumDetails = ({
|
68 |
| - networkType, |
69 |
| - selectedLevel, |
70 |
| - transactionInfo, |
71 | 146 | showFee,
|
| 147 | + feeOptions, |
| 148 | + setSelectedLevelOption, |
| 149 | + selectedLevelOption, |
| 150 | + changeFeeLevel, |
| 151 | + symbol, |
| 152 | + networkType, |
72 | 153 | }: 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})`); |
78 | 155 |
|
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); |
85 | 159 |
|
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 | + }; |
92 | 162 |
|
93 | 163 | return (
|
94 | 164 | 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> |
102 | 201 | )
|
103 | 202 | );
|
104 | 203 | };
|
105 | 204 |
|
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 | + ); |
108 | 235 |
|
109 | 236 | export const FeeDetails = (props: DetailsProps) => {
|
110 | 237 | const { networkType } = props;
|
111 | 238 |
|
| 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 | + |
112 | 250 | 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 /> |
118 | 254 | </Row>
|
119 | 255 | </Text>
|
120 | 256 | );
|
|
0 commit comments