Skip to content

chore(suite-native): Mobile Trade: types renaming, generic trade input #19087

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

Closed
wants to merge 1 commit into from
Closed
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
2 changes: 1 addition & 1 deletion suite-native/forms/src/Form.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ReactNode, createContext } from 'react';
import { FieldValues, UseFormReturn } from 'react-hook-form';

export type { FieldValues, UseFormReturn } from 'react-hook-form';
export type { FieldPath, FieldValues, UseFormReturn } from 'react-hook-form';

export interface FormProps<TFieldValues extends FieldValues> {
children?: ReactNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { CryptoId } from 'invity-api';

import { TokenAddress } from '@suite-common/wallet-types';

import { TradeableAsset } from '../types';
import { TradeableAsset } from '../types/general';

export const btcAsset: TradeableAsset = {
symbol: 'btc',
Expand Down
187 changes: 12 additions & 175 deletions suite-native/module-trading/src/components/buy/BuyAmountInput.tsx
Original file line number Diff line number Diff line change
@@ -1,183 +1,20 @@
import { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
import { LayoutChangeEvent, Pressable, TextInput, TextInputProps } from 'react-native';
import { forwardRef } from 'react';
import { TextInput } from 'react-native';

import { useField } from '@suite-native/forms';
import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
import type { UseFormReturn } from '@suite-native/forms';

import { useBuyFormContext } from '../../hooks/buy/useBuyFormContext';
import { truncateDecimals } from '../../utils/general/amountUtils';
import { FocusableFormValues } from '../../types/general';
import { AmountInput, AmountInputProps } from '../general/AmountInput';

export type TradingAmountInputProps = {
name: 'fiatValue' | 'cryptoValue';
inputTransformer: (value: string) => string;
maxDecimals?: number;
} & Omit<
TextInputProps,
'value' | 'style' | 'onBlur' | 'onFocus' | 'onChangeText' | 'onLayout' | 'onContentSizeChange'
>;
type InputKey = 'fiatValue' | 'cryptoValue';

const MAX_FONT_SIZE = 34;
const MIN_FONT_SIZE = Math.ceil(MAX_FONT_SIZE / 2);
const FONT_TO_LINE_HEIGHT_RATIO = 1.235;
const FONT_SIZE_SHRINK_THRESHOLD = 20;
const FONT_SIZE_GROW_HYSTERESIS = 20;
export type BuyAmountInputProps = Omit<AmountInputProps<InputKey>, 'form'>;

export const MIN_INPUT_WIDTH = 70;
export const MAX_INPUT_HEIGHT = Math.floor(MAX_FONT_SIZE * FONT_TO_LINE_HEIGHT_RATIO);
export { MAX_INPUT_HEIGHT, MIN_INPUT_WIDTH } from '../general/AmountInput';

const boxStyle = prepareNativeStyle(() => ({
flex: 1,
alignItems: 'flex-end',
paddingLeft: 0,
marginLeft: 0,
overflow: 'visible',
}));
export const BuyAmountInput = forwardRef<TextInput, BuyAmountInputProps>((props, ref) => {
const form = useBuyFormContext() as unknown as UseFormReturn<FocusableFormValues<string>>;

const inputStyle = prepareNativeStyle<{ hasError: boolean; fontSize: number }>(
({ colors, typography }, { hasError, fontSize }) => ({
...typography.body,
color: hasError ? colors.textAlertRed : colors.textDefault,
textAlign: 'right',
fontSize,
lineHeight: Math.floor(fontSize * FONT_TO_LINE_HEIGHT_RATIO),
minWidth: MIN_INPUT_WIDTH,
}),
);

const useInputLayoutControls = () => {
const [availableWidth, setAvailableWidth] = useState(
MIN_INPUT_WIDTH + FONT_SIZE_SHRINK_THRESHOLD,
);
const [fontSize, setFontSize] = useState(MAX_FONT_SIZE);

const handleAvailableWith = useCallback(({ nativeEvent }: LayoutChangeEvent) => {
const { width } = nativeEvent.layout;
setAvailableWidth(width);
}, []);

const handleFontSizeOnContentChange = useCallback(
({ nativeEvent }: LayoutChangeEvent) => {
const contentWidth = nativeEvent.layout.width;

if (contentWidth === 0 || availableWidth === 0) {
setFontSize(MAX_FONT_SIZE);

return;
}

const shrinkThreshold = availableWidth - FONT_SIZE_SHRINK_THRESHOLD;
if (contentWidth > shrinkThreshold) {
const newFontSize = Math.max(
Math.floor((shrinkThreshold / contentWidth) * fontSize),
MIN_FONT_SIZE,
);
setFontSize(newFontSize);
}

const growThreshold = shrinkThreshold - FONT_SIZE_GROW_HYSTERESIS;
if (contentWidth < growThreshold) {
const newFontSize = Math.min(
Math.floor((shrinkThreshold / contentWidth) * fontSize),
MAX_FONT_SIZE,
);
setFontSize(newFontSize);
}
},
[availableWidth, fontSize],
);

return {
fontSize,
onBoxLayout: handleAvailableWith,
onInputLayout: handleFontSizeOnContentChange,
};
};

const useInputFormControls = (
name: 'fiatValue' | 'cryptoValue',
inputTransformer: (value: string) => string,
maxLength: number | undefined,
maxDecimals: number | undefined,
) => {
const { getValues, setValue } = useBuyFormContext();
// do not use `value` from `useField` here, because it does not work properly with `undefined`
const value = getValues(name);
const { onChange, onBlur, hasError } = useField({ name });

const setFocusedValue = useCallback(() => {
setValue('focusedValue', name);
}, [name, setValue]);

const handleTextChange = useCallback(
(text: string) => {
let transformedText = inputTransformer(text);
transformedText = truncateDecimals(transformedText, maxDecimals);
transformedText = transformedText.slice(0, maxLength);

return onChange(transformedText === '' ? undefined : transformedText);
},
[maxLength, maxDecimals, inputTransformer, onChange],
);

const clearFocusedValueAndBlur = useCallback(() => {
onBlur();
setValue('focusedValue', undefined);
}, [onBlur, setValue]);

return {
value,
hasError,
onFocus: setFocusedValue,
onChangeText: handleTextChange,
onBlur: clearFocusedValueAndBlur,
};
};

export const BuyAmountInput = forwardRef<TextInput, TradingAmountInputProps>(
({ name, inputTransformer, maxLength, maxDecimals, onPress, ...inputProps }, ref) => {
const innerRef = useRef<TextInput>(null);
useImperativeHandle(ref, () => innerRef.current!, []);

const { applyStyle, utils } = useNativeStyles();
const { fontSize, onBoxLayout, onInputLayout } = useInputLayoutControls();
const { value, hasError, onFocus, onChangeText, onBlur } = useInputFormControls(
name,
inputTransformer,
maxLength,
maxDecimals,
);

const focusInputCallback = useCallback(() => {
innerRef.current?.focus();
}, [innerRef]);
const wrapperOnPress = onPress ?? focusInputCallback;

// Note: it would be nice to use `onContentSizeChange` instead of `onLayout` on `<Pressable />` once this bug is fixed https://github.com/facebook/react-native/issues/29702.
// It would also allow us to remove `innerRef` and `useImperativeHandle` logic.
return (
<Pressable
style={applyStyle(boxStyle)}
onLayout={onBoxLayout}
testID="@trading/amountInput/wrapper"
onPress={wrapperOnPress}
>
<TextInput
ref={innerRef}
style={applyStyle(inputStyle, { hasError, fontSize })}
keyboardType="decimal-pad"
inputMode="decimal"
placeholder="0.0"
placeholderTextColor={utils.colors.textDisabled}
value={value ?? ''}
maxLength={maxLength}
onChangeText={onChangeText}
onFocus={onFocus}
onBlur={onBlur}
onLayout={onInputLayout}
onPress={onPress}
{...inputProps}
/>
</Pressable>
);
},
);
return <AmountInput ref={ref} form={form} {...props} />;
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useTranslate } from '@suite-native/intl';

import { useBuyFormContext } from '../../hooks/buy/useBuyFormContext';
import { useSheetControls } from '../../hooks/general/useSheetControls';
import { Country } from '../../types';
import { Country } from '../../types/general';
import { CountrySheet } from '../general/CountrySheet/CountrySheet';
import { OverviewRow } from '../general/OverviewRow';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Badge } from '@suite-native/atoms';
import { useField } from '@suite-native/forms';

import { TradingBuyFormValues } from '../../types';
import { BuyFormValues } from '../../types/buy';

export type BuyFormFieldErrorBadgeProps = {
fieldName: keyof TradingBuyFormValues;
fieldName: keyof BuyFormValues;
};

export const BuyFormFieldErrorBadge = ({ fieldName }: BuyFormFieldErrorBadgeProps) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { BuyTradeableAssetsSheet } from './BuyTradeableAssetsSheet';
import { useBuyFormContext } from '../../hooks/buy/useBuyFormContext';
import { useSheetControls } from '../../hooks/general/useSheetControls';
import { selectBuyTradeableAssetsSorted } from '../../selectors/buySelectors';
import { TradeableAsset } from '../../types';
import { TradeableAsset } from '../../types/general';
import { SelectTradeableAssetButton } from '../general/SelectTradeableAssetButton';

const ASSET_PICKER_TEST_ID = '@trading/buy/asset-button';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import {
} from '@suite-native/test-utils';

import { useBuyForm } from '../../../hooks/buy/useBuyForm';
import { TradingBuyForm } from '../../../types';
import { BuyForm } from '../../../types/buy';
import { BuyAlert } from '../BuyAlert';

describe('BuyAlert', () => {
let form: TradingBuyForm;
let form: BuyForm;

const renderFormHook = () => renderHookWithStoreProviderAsync(() => useBuyForm());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,12 @@ import {
import { paletteV1 } from '@trezor/theme';

import { useBuyForm } from '../../../hooks/buy/useBuyForm';
import { TradingBuyForm } from '../../../types';
import { BuyAmountInput, TradingAmountInputProps } from '../BuyAmountInput';
import { BuyForm } from '../../../types/buy';
import { BuyAmountInput, BuyAmountInputProps } from '../BuyAmountInput';

describe('BuyAmountInput', () => {
const renderBuyFormHook = () => renderHookWithStoreProviderAsync(() => useBuyForm());
const renderTradingAmountInput = (
props: Partial<TradingAmountInputProps>,
form: TradingBuyForm,
) =>
const renderTradingAmountInput = (props: Partial<BuyAmountInputProps>, form: BuyForm) =>
renderWithBasicProvider(
<Form form={form}>
<BuyAmountInput
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import {

import { btcAsset, ethOnBaseAsset, usdcAsset } from '../../../__fixtures__/tradeableAssets';
import { useBuyForm } from '../../../hooks/buy/useBuyForm';
import { TradingBuyForm } from '../../../types';
import { BuyForm } from '../../../types/buy';
import { BuyAssetNetworkInfo } from '../BuyAssetNetworkInfo';

describe('BuyAssetNetworkInfo', () => {
let form: TradingBuyForm;
let form: BuyForm;

const renderNetworkIconForToken = () =>
renderWithBasicProvider(
Expand Down
Loading