-
Notifications
You must be signed in to change notification settings - Fork 96
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
feat(TokenEnterAmount): add new component to Swap flow #6247
base: main
Are you sure you want to change the base?
Changes from all commits
e5ab0a5
2fbc1b2
b12d50f
8af7b8d
44c0aab
1aa17d6
4ed89fc
d0c7099
98d6552
b7e5107
6ca6f92
f2185bc
8762401
98938bc
5a3f776
9360626
f322e23
a54d71f
b3f3c67
8ce68a4
c2773fa
37455cb
8357b18
f5db952
130b620
10fb5bd
112e5f9
bce9c2b
7048f8e
a08f3a9
22f4a44
78218f6
cea53aa
060a81a
8d80b3d
f9b5e31
3db7a64
621490d
c7a5951
9b2a578
c150c32
ad7f467
0d64a5b
2031bbc
d7b5f0a
6d738e4
4c9ed39
e1dbbec
e9fc844
ec1f0a8
96bc9b3
2c6f88b
1a0c2bc
6921dc5
bcb4c6f
5d85bbd
da1652b
f57fac6
39c6766
cdd8396
6bb7c89
1f40e7c
5cec1fb
b9f21de
3858035
4eb45ba
639e612
080de6d
27f8458
8e06da5
3b00fec
bbd01b0
e0de08b
9e54474
35100c4
80b919e
54aae6e
289d9e7
44597bb
fe52e47
7234e80
9e07831
5825753
8001e05
5357212
9c5c31d
3f37e64
8c500b7
5822b22
6a2b347
3463640
97e0a1a
08b540a
574ba00
a1e834c
0fde6a6
5dd36f4
e576d26
1dbd98e
95cf72b
7cdaac1
fd40bc6
98ccb17
ce11582
39664fe
1fa2209
52a37a0
57e4e01
aa79667
76b4dba
8c6c71a
8599b18
c3ba81b
930a919
5e780d9
98c06f9
726153d
9c6886d
27a7f58
67c69ba
68319ed
bd8999c
e6b5971
7caeb77
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ import { | |
View, | ||
} from 'react-native' | ||
import { getNumberFormatSettings } from 'react-native-localize' | ||
import SkeletonPlaceholder from 'react-native-skeleton-placeholder' | ||
import TextInput from 'src/components/TextInput' | ||
import TokenDisplay from 'src/components/TokenDisplay' | ||
import TokenIcon, { IconSize } from 'src/components/TokenIcon' | ||
|
@@ -105,12 +106,10 @@ export function getDisplayLocalAmount( | |
* variables and handlers that manage "enter amount" functionality, including rate calculations. | ||
*/ | ||
export function useEnterAmount(props: { | ||
token: TokenBalance | ||
token: TokenBalance | undefined | ||
inputRef: React.RefObject<RNTextInput> | ||
onHandleAmountInputChange?(amount: string): void | ||
}) { | ||
const { decimalSeparator } = getNumberFormatSettings() | ||
|
||
/** | ||
* This field is formatted for processing purpose. It is a lot easier to process a number formatted | ||
* in a single format, rather than writing different logic for various combinations of decimal | ||
|
@@ -139,6 +138,15 @@ export function useEnterAmount(props: { | |
* - `local.displayAmount` -> `localDisplayAmount` | ||
*/ | ||
const processedAmounts = useMemo(() => { | ||
const { decimalSeparator } = getNumberFormatSettings() | ||
|
||
if (!props.token) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it okay that this variable is not listed in the dependecies list? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bakoushin accidentally missed it! Will add it to the list. P.S. Sometimes I really wish we had eslint rule for this 😄 |
||
return { | ||
token: { bignum: null, displayAmount: '' }, | ||
local: { bignum: null, displayAmount: '' }, | ||
} | ||
} | ||
|
||
if (amountType === 'token') { | ||
const parsedTokenAmount = amount === '' ? null : parseInputAmount(amount) | ||
|
||
|
@@ -201,9 +209,11 @@ export function useEnterAmount(props: { | |
displayAmount: getDisplayLocalAmount(parsedLocalAmount, localCurrencySymbol), | ||
}, | ||
} | ||
}, [amount, amountType, localCurrencySymbol]) | ||
}, [amount, amountType, localCurrencySymbol, usdToLocalRate, props.token]) | ||
|
||
function handleToggleAmountType() { | ||
if (!props.token) return | ||
|
||
const newAmountType = amountType === 'local' ? 'token' : 'local' | ||
setAmountType(newAmountType) | ||
setAmount( | ||
|
@@ -218,7 +228,7 @@ export function useEnterAmount(props: { | |
value = unformatNumberForProcessing(value) | ||
value = value.startsWith('.') ? `0${value}` : value | ||
|
||
if (!value) { | ||
if (!value || !props.token) { | ||
setAmount('') | ||
props.onHandleAmountInputChange?.('') | ||
return | ||
|
@@ -231,17 +241,30 @@ export function useEnterAmount(props: { | |
`^(?:\\d+[.]?\\d{0,${props.token.decimals}}|[.]\\d{0,${props.token.decimals}}|[.])$` | ||
) | ||
|
||
if ( | ||
(amountType === 'token' && value.match(tokenAmountRegex)) || | ||
(amountType === 'local' && value.match(localAmountRegex)) | ||
) { | ||
const isValidTokenAmount = amountType === 'token' && value.match(tokenAmountRegex) | ||
const isValidLocalAmount = amountType === 'local' && value.match(localAmountRegex) | ||
if (isValidTokenAmount || isValidLocalAmount) { | ||
setAmount(value) | ||
props.onHandleAmountInputChange?.(value) | ||
return | ||
} | ||
} | ||
|
||
function replaceAmount(value: string) { | ||
if (!props.token) return | ||
|
||
if (value === '') { | ||
setAmount('') | ||
return | ||
} | ||
|
||
const rawValue = unformatNumberForProcessing(value) | ||
const roundedAmount = new BigNumber(rawValue).decimalPlaces(props.token?.decimals).toString() | ||
setAmount(roundedAmount) | ||
} | ||
|
||
function handleSelectPercentageAmount(percentage: number) { | ||
if (!props.token) return | ||
if (percentage <= 0 || percentage > 1) return | ||
|
||
const percentageAmount = props.token.balance.multipliedBy(percentage) | ||
|
@@ -265,7 +288,7 @@ export function useEnterAmount(props: { | |
amount, | ||
amountType, | ||
processedAmounts, | ||
replaceAmount: setAmount, | ||
replaceAmount, | ||
handleToggleAmountType, | ||
handleAmountInputChange, | ||
handleSelectPercentageAmount, | ||
|
@@ -281,30 +304,31 @@ export default function TokenEnterAmount({ | |
inputRef, | ||
inputStyle, | ||
autoFocus, | ||
editable = true, | ||
testID, | ||
onInputChange, | ||
toggleAmountType, | ||
onOpenTokenPicker, | ||
loading, | ||
}: { | ||
token?: TokenBalance | ||
inputValue: string | ||
tokenAmount: string | ||
localAmount: string | ||
amountType: AmountEnteredIn | ||
inputRef: React.MutableRefObject<RNTextInput | null> | ||
loading?: boolean | ||
inputStyle?: StyleProp<TextStyle> | ||
autoFocus?: boolean | ||
editable?: boolean | ||
testID?: string | ||
onInputChange(value: string): void | ||
onInputChange?(value: string): void | ||
toggleAmountType?(): void | ||
onOpenTokenPicker?(): void | ||
}) { | ||
const { t } = useTranslation() | ||
// the startPosition and inputRef variables exist to ensure TextInput | ||
// displays the start of the value for long values on Android | ||
// https://github.com/facebook/react-native/issues/14845 | ||
/** | ||
* startPosition and inputRef variables exist to ensure TextInput displays the start of the value | ||
* for long values on Android: https://github.com/facebook/react-native/issues/14845 | ||
*/ | ||
const [startPosition, setStartPosition] = useState<number | undefined>(0) | ||
// this should never be null, just adding a default to make TS happy | ||
const localCurrencySymbol = useSelector(getLocalCurrencySymbol) ?? LocalCurrencySymbol.USD | ||
|
@@ -359,7 +383,10 @@ export default function TokenEnterAmount({ | |
|
||
<View style={styles.tokenNameAndAvailable}> | ||
<Text style={styles.tokenName} testID={`${testID}/TokenName`}> | ||
{token.symbol} on {NETWORK_NAMES[token.networkId]} | ||
{t('tokenEnterAmount.tokenDescription', { | ||
tokenName: token.symbol, | ||
tokenNetwork: NETWORK_NAMES[token.networkId], | ||
})} | ||
</Text> | ||
<Text style={styles.tokenBalance} testID={`${testID}/TokenBalance`}> | ||
<Trans i18nKey="tokenEnterAmount.availableBalance"> | ||
|
@@ -381,81 +408,95 @@ export default function TokenEnterAmount({ | |
</View> | ||
</Touchable> | ||
{token && ( | ||
<View | ||
style={[ | ||
styles.rowContainer, | ||
{ borderTopLeftRadius: 0, borderTopRightRadius: 0, borderTopWidth: 0 }, | ||
]} | ||
> | ||
<TextInput | ||
forwardedRef={inputRef} | ||
onChangeText={(value) => { | ||
handleSetStartPosition(undefined) | ||
onInputChange(value) | ||
}} | ||
value={formattedInputValue} | ||
placeholderTextColor={Colors.gray3} | ||
placeholder={amountType === 'token' ? placeholder.token : placeholder.local} | ||
keyboardType="decimal-pad" | ||
// Work around for RN issue with Samsung keyboards | ||
// https://github.com/facebook/react-native/issues/22005 | ||
autoCapitalize="words" | ||
autoFocus={autoFocus} | ||
// unset lineHeight to allow ellipsis on long inputs on iOS. For | ||
// android, ellipses doesn't work and unsetting line height causes | ||
// height changes when amount is entered | ||
inputStyle={[ | ||
styles.primaryAmountText, | ||
inputStyle, | ||
Platform.select({ ios: { lineHeight: undefined } }), | ||
<View> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This addition/removal is purely a result of adding extra |
||
<View | ||
style={[ | ||
styles.rowContainer, | ||
{ borderTopLeftRadius: 0, borderTopRightRadius: 0, borderTopWidth: 0 }, | ||
]} | ||
onBlur={() => { | ||
handleSetStartPosition(0) | ||
}} | ||
onFocus={() => { | ||
const withCurrency = amountType === 'local' ? 1 : 0 | ||
handleSetStartPosition((inputValue?.length ?? 0) + withCurrency) | ||
}} | ||
onSelectionChange={() => { | ||
handleSetStartPosition(undefined) | ||
}} | ||
selection={ | ||
Platform.OS === 'android' && typeof startPosition === 'number' | ||
? { start: startPosition } | ||
: undefined | ||
} | ||
showClearButton={false} | ||
editable={editable} | ||
testID={`${testID}/TokenAmountInput`} | ||
/> | ||
|
||
{token.priceUsd ? ( | ||
<> | ||
{toggleAmountType && ( | ||
<Touchable | ||
onPress={toggleAmountType} | ||
style={styles.swapArrowContainer} | ||
testID={`${testID}/SwitchTokens`} | ||
hitSlop={variables.iconHitslop} | ||
> | ||
<TextInput | ||
forwardedRef={inputRef} | ||
onChangeText={(value) => { | ||
handleSetStartPosition(undefined) | ||
onInputChange?.(value) | ||
}} | ||
value={formattedInputValue} | ||
placeholderTextColor={Colors.gray3} | ||
placeholder={amountType === 'token' ? placeholder.token : placeholder.local} | ||
keyboardType="decimal-pad" | ||
// Work around for RN issue with Samsung keyboards | ||
// https://github.com/facebook/react-native/issues/22005 | ||
autoCapitalize="words" | ||
autoFocus={autoFocus} | ||
// unset lineHeight to allow ellipsis on long inputs on iOS. For | ||
// android, ellipses doesn't work and unsetting line height causes | ||
// height changes when amount is entered | ||
inputStyle={[ | ||
styles.primaryAmountText, | ||
inputStyle, | ||
Platform.select({ ios: { lineHeight: undefined } }), | ||
]} | ||
onBlur={() => { | ||
handleSetStartPosition(0) | ||
}} | ||
onFocus={() => { | ||
const withCurrency = amountType === 'local' ? 1 : 0 | ||
handleSetStartPosition((inputValue?.length ?? 0) + withCurrency) | ||
}} | ||
onSelectionChange={() => { | ||
handleSetStartPosition(undefined) | ||
}} | ||
selection={ | ||
Platform.OS === 'android' && typeof startPosition === 'number' | ||
? { start: startPosition } | ||
: undefined | ||
} | ||
showClearButton={false} | ||
editable={!!onInputChange} | ||
testID={`${testID}/TokenAmountInput`} | ||
/> | ||
|
||
{token.priceUsd ? ( | ||
<> | ||
{toggleAmountType && ( | ||
<Touchable | ||
onPress={toggleAmountType} | ||
style={styles.swapArrowContainer} | ||
testID={`${testID}/SwitchTokens`} | ||
hitSlop={variables.iconHitslop} | ||
> | ||
<SwapArrows color={Colors.gray3} size={24} /> | ||
</Touchable> | ||
)} | ||
|
||
<Text | ||
numberOfLines={1} | ||
style={[styles.secondaryAmountText, { flex: 0, textAlign: 'right' }]} | ||
testID={`${testID}/ExchangeAmount`} | ||
> | ||
<SwapArrows color={Colors.gray3} size={24} /> | ||
</Touchable> | ||
)} | ||
|
||
<Text | ||
numberOfLines={1} | ||
style={[styles.secondaryAmountText, { flex: 0, textAlign: 'right' }]} | ||
testID={`${testID}/ExchangeAmount`} | ||
> | ||
{amountType === 'token' | ||
? `${APPROX_SYMBOL} ${localAmount ? localAmount : placeholder.local}` | ||
: `${APPROX_SYMBOL} ${tokenAmount ? tokenAmount : placeholder.token}`} | ||
{amountType === 'token' | ||
? `${APPROX_SYMBOL} ${localAmount ? localAmount : placeholder.local}` | ||
: `${APPROX_SYMBOL} ${tokenAmount ? tokenAmount : placeholder.token}`} | ||
</Text> | ||
</> | ||
) : ( | ||
<Text style={styles.secondaryAmountText}> | ||
{t('tokenEnterAmount.fiatPriceUnavailable')} | ||
</Text> | ||
</> | ||
) : ( | ||
<Text style={styles.secondaryAmountText}> | ||
{t('tokenEnterAmount.fiatPriceUnavailable')} | ||
</Text> | ||
)} | ||
</View> | ||
|
||
{loading && ( | ||
<View testID={`${testID}/Loader`} style={styles.loader}> | ||
<SkeletonPlaceholder | ||
borderRadius={100} // ensure rounded corners with font scaling | ||
backgroundColor={Colors.gray2} | ||
highlightColor={Colors.white} | ||
> | ||
<View style={{ height: '100%', width: '100%' }} /> | ||
</SkeletonPlaceholder> | ||
</View> | ||
)} | ||
</View> | ||
)} | ||
|
@@ -511,4 +552,12 @@ const styles = StyleSheet.create({ | |
swapArrowContainer: { | ||
transform: [{ rotate: '90deg' }], | ||
}, | ||
loader: { | ||
padding: Spacing.Regular16, | ||
position: 'absolute', | ||
top: 0, | ||
left: 0, | ||
height: '100%', | ||
width: '100%', | ||
}, | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i guess the translation of the word "on" is highly dependent on context. perhaps we can use some template to provide it? kind of like this:
and, with that, shall we include this item in the
tokenEnterAmount
group above?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need to keep the "on"?