Skip to content

Commit

Permalink
feat(suite-native): send fees selection screen
Browse files Browse the repository at this point in the history
  • Loading branch information
PeKne committed Aug 14, 2024
1 parent 9f6678f commit 4d39c4c
Show file tree
Hide file tree
Showing 24 changed files with 650 additions and 120 deletions.
11 changes: 11 additions & 0 deletions suite-common/wallet-core/src/accounts/accountsReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,17 @@ export const selectAccountNetworkSymbol = (
return account.symbol;
};

export const selectAccountAvailableBalance = (
state: AccountsRootState,
accountKey?: AccountKey,
): string | null => {
const account = selectAccountByKey(state, accountKey);

if (!account) return null;

return account.availableBalance;
};

export const selectFormattedAccountType = (
state: AccountsRootState,
accountKey: AccountKey,
Expand Down
12 changes: 12 additions & 0 deletions suite-common/wallet-core/src/send/sendFormReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
AccountKey,
FormState,
GeneralPrecomposedTransactionFinal,
Output,
} from '@suite-common/wallet-types';
import { cloneObject } from '@trezor/utils';
import { createReducerWithExtraDeps } from '@suite-common/redux-utils';
Expand Down Expand Up @@ -109,6 +110,17 @@ export const selectSendFormDraftByAccountKey = (
return state.wallet.send.drafts[accountKey] ?? null;
};

export const selectSendFormDraftOutputsByAccountKey = (
state: SendRootState,
accountKey?: AccountKey,
): Output[] | null => {
if (G.isUndefined(accountKey)) return null;

const draft = selectSendFormDraftByAccountKey(state, accountKey);

return draft?.outputs ?? null;
};

export const selectSendFormReviewButtonRequestsCount = (
state: DeviceRootState,
networkSymbol?: NetworkSymbol,
Expand Down
3 changes: 3 additions & 0 deletions suite-common/wallet-types/src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export type GeneralPrecomposedTransactionFinal =

export type PrecomposedLevels = { [key: string]: PrecomposedTransaction };
export type PrecomposedLevelsCardano = { [key: string]: PrecomposedTransactionCardano };
export type GeneralPrecomposedLevels = PrecomposedLevels | PrecomposedLevelsCardano;

export interface RbfTransactionParams {
txid: string;
Expand Down Expand Up @@ -284,3 +285,5 @@ export type ReviewOutputType = ReviewOutput['type'];
export type ReviewOutputState = 'active' | 'success' | undefined;

export type ExcludedUtxos = Record<string, 'low-anonymity' | 'dust' | undefined>;

export type FeeLevelLabel = FeeLevel['label'];
13 changes: 6 additions & 7 deletions suite-native/atoms/src/Radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ import { NativeStyleObject, prepareNativeStyle, useNativeStyles } from '@trezor/

import { ACCESSIBILITY_FONTSIZE_MULTIPLIER } from './Text';

type RadioValue = string | number;
export interface RadioProps extends Omit<TouchableOpacityProps, 'style' | 'onPress'> {
value: RadioValue;
export type RadioProps<TValue> = Omit<TouchableOpacityProps, 'style' | 'onPress'> & {
value: TValue;
isChecked?: boolean;
isDisabled?: boolean;
onPress: (value: RadioValue) => void;
onPress: (value: TValue) => void;
style?: NativeStyleObject;
}
};

type RadioStyleProps = {
isChecked: boolean;
Expand Down Expand Up @@ -50,14 +49,14 @@ const radioCheckStyle = prepareNativeStyle<Omit<RadioStyleProps, 'isChecked'>>(
}),
);

export const Radio = ({
export const Radio = <TValue extends string | number>({
value,
isChecked = false,
onPress,
isDisabled = false,
style,
...props
}: RadioProps) => {
}: RadioProps<TValue>) => {
const { applyStyle } = useNativeStyles();

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ type useFiatFromCryptoValueParams = {
};

export const useFiatFromCryptoValue = ({
// TODO: is balance
cryptoValue,
network,
tokenAddress,
Expand Down
25 changes: 25 additions & 0 deletions suite-native/intl/src/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,31 @@ export const en = {
title: 'Enter passphrase to continue',
},
},
moduleSend: {
fees: {
recipient: { singular: 'Recipient' },
description: {
title: 'Mining fee',
body: 'Fees are paid directly to network miners for processing your transactions.',
},
levels: {
low: {
label: 'Low',
timeEstimate: '~ 1 hour',
},
medium: {
label: 'Medium',
timeEstimate: '~ 20 minutes',
},
high: {
label: 'High',
timeEstimate: '~ 10 minutes',
},
},
totalAmount: 'Total amount',
submitButton: 'Review and sign',
},
},
};

export type Translations = typeof en;
5 changes: 4 additions & 1 deletion suite-native/module-send/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@react-navigation/native": "6.1.17",
"@react-navigation/native-stack": "6.9.26",
"@reduxjs/toolkit": "1.9.5",
"@suite-common/icons": "workspace:*",
"@suite-common/redux-utils": "workspace:*",
"@suite-common/validators": "workspace:*",
"@suite-common/wallet-config": "workspace:*",
Expand All @@ -27,14 +28,16 @@
"@suite-native/device-mutex": "workspace:*",
"@suite-native/formatters": "workspace:*",
"@suite-native/forms": "workspace:*",
"@suite-native/intl": "workspace:*",
"@suite-native/navigation": "workspace:*",
"@suite-native/toasts": "workspace:*",
"@trezor/blockchain-link-types": "workspace:*",
"@trezor/connect": "workspace:*",
"@trezor/react-utils": "workspace:*",
"@trezor/styles": "workspace:*",
"@trezor/utils": "workspace:*",
"react": "18.2.0",
"react-hook-form": "^7.50.1",
"react-native": "0.74.1",
"react-redux": "8.0.7"
}
}
86 changes: 86 additions & 0 deletions suite-native/module-send/src/components/FeeOption.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { useContext } from 'react';
import { TouchableOpacity } from 'react-native';

import { NetworkSymbol } from '@suite-common/wallet-config';
import { GeneralPrecomposedTransactionFinal } from '@suite-common/wallet-types';
import { Text, HStack, VStack, Radio } from '@suite-native/atoms';
import { CryptoToFiatAmountFormatter, CryptoAmountFormatter } from '@suite-native/formatters';
import { FormContext } from '@suite-native/forms';
import { TxKeyPath, Translation } from '@suite-native/intl';

import { SendFeesFormValues } from '../sendFeesFormSchema';
import { NativeSupportedFeeLevel } from '../types';

const feeLabelsMap = {
economy: {
label: 'moduleSend.fees.levels.low.label',
timeEstimate: 'moduleSend.fees.levels.low.timeEstimate',
},
normal: {
label: 'moduleSend.fees.levels.medium.label',
timeEstimate: 'moduleSend.fees.levels.medium.timeEstimate',
},
high: {
label: 'moduleSend.fees.levels.high.label',
timeEstimate: 'moduleSend.fees.levels.high.timeEstimate',
},
} as const satisfies Record<NativeSupportedFeeLevel, { label: TxKeyPath; timeEstimate: TxKeyPath }>;

export const FeeOption = ({
feeKey,
feeLevel,
networkSymbol,
}: {
feeKey: SendFeesFormValues['feeLevel'];
feeLevel: GeneralPrecomposedTransactionFinal;
networkSymbol: NetworkSymbol;
}) => {
const { watch, setValue } = useContext(FormContext);
const selectedLevel = watch('feeLevel');

const isChecked = selectedLevel === feeKey;

const { label, timeEstimate } = feeLabelsMap[feeKey];

const handleSelectFeeLevel = () => {
setValue('feeLevel', feeKey, {
shouldValidate: true,
});
};

return (
<TouchableOpacity onPress={handleSelectFeeLevel}>
<HStack spacing="large" justifyContent="space-between" flex={1} alignItems="center">
<VStack alignItems="flex-start" spacing="extraSmall">
<Text variant="highlight">
<Translation id={label} />
</Text>
<Text variant="hint" color="textSubdued">
<Translation id={timeEstimate} />
</Text>
</VStack>
<VStack flex={1} alignItems="flex-end" spacing="extraSmall">
<CryptoToFiatAmountFormatter
variant="body"
color="textDefault"
value={feeLevel.fee}
network={networkSymbol}
/>
<CryptoAmountFormatter
variant="hint"
color="textSubdued"
value={feeLevel.fee}
network={networkSymbol}
isBalance={false}
/>
</VStack>
<Radio
isChecked={isChecked}
value={feeKey}
onPress={handleSelectFeeLevel}
testID={`@send/fees-level-${feeKey}`}
/>
</HStack>
</TouchableOpacity>
);
};
34 changes: 34 additions & 0 deletions suite-native/module-send/src/components/FeeOptionsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { D } from '@mobily/ts-belt';

import { NetworkSymbol } from '@suite-common/wallet-config';
import { GeneralPrecomposedLevels } from '@suite-common/wallet-types';
import { Card, VStack } from '@suite-native/atoms';

import { FeeOption } from './FeeOption';
import { NativeSupportedFeeLevel } from '../types';

export const FeeOptionsList = ({
feeLevels,
networkSymbol,
}: {
feeLevels: GeneralPrecomposedLevels;
networkSymbol: NetworkSymbol;
}) => {
// Remove custom fee level from the list. It is not supported in the first version of the send flow.
const predefinedFeeLevels = D.filterWithKey(feeLevels, key => key !== 'custom');

return (
<Card>
<VStack spacing="extraLarge">
{Object.entries(predefinedFeeLevels).map(([feeKey, feeLevel]) => (
<FeeOption
key={feeKey}
feeKey={feeKey as NativeSupportedFeeLevel}
feeLevel={feeLevel}
networkSymbol={networkSymbol}
/>
))}
</VStack>
</Card>
);
};
81 changes: 81 additions & 0 deletions suite-native/module-send/src/components/FeesFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useContext } from 'react';

import { NetworkSymbol } from '@suite-common/wallet-config';
import { Text, Button, Box, Card, HStack, VStack } from '@suite-native/atoms';
import { CryptoToFiatAmountFormatter, CryptoAmountFormatter } from '@suite-native/formatters';
import { FormContext } from '@suite-native/forms';
import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
import { Translation } from '@suite-native/intl';

type FeesFooterProps = {
onSubmit: () => void;
networkSymbol: NetworkSymbol;
totalAmount: string;
};

const CARD_BOTTOM_PADDING = 40;

const footerWrapperStyle = prepareNativeStyle(utils => ({
marginBottom: utils.spacings.small,
}));

const cardStyle = prepareNativeStyle(utils => ({
paddingTop: utils.spacings.small,
paddingBottom: CARD_BOTTOM_PADDING, // ensures that nothing is hidden behind the absolute confirm button
backgroundColor: utils.colors.backgroundSurfaceElevationNegative,
borderColor: utils.colors.borderElevation0,
borderWidth: utils.borders.widths.small,
}));

const buttonStyle = prepareNativeStyle(utils => ({
position: 'absolute',
width: '100%',
// Offset so the button overlaps the adjacent card (as design demands).
top: -utils.spacings.extraLarge,
}));

export const FeesFooter = ({ onSubmit, totalAmount, networkSymbol }: FeesFooterProps) => {
const { applyStyle } = useNativeStyles();

const form = useContext(FormContext);
const {
formState: { isSubmitting },
} = form;

return (
<Box style={applyStyle(footerWrapperStyle)}>
<Card style={applyStyle(cardStyle)}>
<HStack justifyContent="space-between" alignItems="center">
<Text variant="callout">
<Translation id="moduleSend.fees.totalAmount" />
</Text>
<VStack spacing={2} alignItems="flex-end">
<CryptoToFiatAmountFormatter
variant="callout"
color="textDefault"
value={totalAmount}
network={networkSymbol}
/>
<CryptoAmountFormatter
variant="hint"
color="textSubdued"
value={totalAmount}
network={networkSymbol}
isBalance={false}
/>
</VStack>
</HStack>
</Card>
<Button
style={applyStyle(buttonStyle)}
accessibilityRole="button"
accessibilityLabel="validate send form"
testID="@send/fees-submit-button"
onPress={onSubmit}
disabled={isSubmitting}
>
<Translation id="moduleSend.fees.submitButton" />
</Button>
</Box>
);
};
Loading

0 comments on commit 4d39c4c

Please sign in to comment.