-
Notifications
You must be signed in to change notification settings - Fork 97
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 flow to EnterAmount.tsx #6245
Changes from 63 commits
e5ab0a5
2fbc1b2
b12d50f
8af7b8d
44c0aab
1aa17d6
4ed89fc
d0c7099
b7e5107
6ca6f92
f2185bc
8762401
98938bc
5a3f776
9360626
b3f3c67
8ce68a4
c2773fa
37455cb
8357b18
f5db952
130b620
10fb5bd
112e5f9
bce9c2b
7048f8e
a08f3a9
22f4a44
78218f6
cea53aa
060a81a
8d80b3d
f9b5e31
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
e0de08b
8001e05
ae82212
7714b4d
b91f09d
fbcd741
757a759
8aed766
ac325bd
c5bc604
c0b5ab2
81991b5
7c746d6
12b4e47
6d6ae26
e54e741
1aa565d
5e2dbee
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 | ||||
---|---|---|---|---|---|---|
|
@@ -32,6 +32,7 @@ | |||||
export const APPROX_SYMBOL = '≈' | ||||||
|
||||||
const BORDER_RADIUS = 12 | ||||||
export const FETCH_UPDATED_TRANSACTIONS_DEBOUNCE_TIME_MS = 250 | ||||||
|
||||||
/** | ||||||
* This function formats numbers in a "1234.5678" format into the correct format according to the | ||||||
|
@@ -48,6 +49,16 @@ | |||||
.replaceAll('_', groupingSeparator) | ||||||
} | ||||||
|
||||||
export function unformatNumberForProcessing(value: string) { | ||||||
const { decimalSeparator, groupingSeparator } = getNumberFormatSettings() | ||||||
return value.replaceAll(groupingSeparator, '').replaceAll(decimalSeparator, '.') | ||||||
} | ||||||
|
||||||
export function roundLocalNumber(value: BigNumber | null) { | ||||||
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.
Suggested change
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. side thought, no need to address now but i wonder if this will be handy to have as a general function that can be used in other places like the transaction feed.... 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. @kathaypacific I think it definitely will be. Considering that it is currently only used in this file - there's no point in moving it to a general utility yet file but we should keep this in mind for when its gonna be useful. |
||||||
if (!value) return '' | ||||||
return value.isLessThan(0.01) ? value.toPrecision(1) : value.toFixed(2) | ||||||
} | ||||||
|
||||||
/** | ||||||
* This function returns complete formatted value for a token (crypto) amount when it is being a | ||||||
* converted value (in "ExchangeAmount" element). | ||||||
|
@@ -83,7 +94,7 @@ | |||||
return `<${localCurrencySymbol}0${decimalSeparator}000001` | ||||||
} | ||||||
|
||||||
const roundedAmount = bignum.isLessThan(0.01) ? bignum.toPrecision(1) : bignum.toFixed(2) | ||||||
const roundedAmount = roundLocalNumber(bignum) | ||||||
const formattedAmount = formatNumber(roundedAmount.toString()) | ||||||
return `${localCurrencySymbol}${formattedAmount}` | ||||||
} | ||||||
|
@@ -95,62 +106,53 @@ | |||||
export function useEnterAmount(props: { | ||||||
token: TokenBalance | ||||||
inputRef: React.RefObject<RNTextInput> | ||||||
onHandleAmountInputChange?(value: string): void | ||||||
}) { | ||||||
const { decimalSeparator, groupingSeparator } = 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 | ||||||
* and grouping separators. The format is "1234.5678". | ||||||
*/ | ||||||
const [amount, setAmount] = useState('') | ||||||
const [amountType, setAmountType] = useState<AmountEnteredIn>('token') | ||||||
|
||||||
// this should never be null, just adding a default to make TS happy | ||||||
// This should never be null, just adding a default to make TS happy | ||||||
const localCurrencySymbol = useSelector(getLocalCurrencySymbol) ?? LocalCurrencySymbol.USD | ||||||
const usdToLocalRate = useSelector(usdToLocalCurrencyRateSelector) | ||||||
|
||||||
/** | ||||||
* This field is for processing purposes only. It is a lot easier to process a number formatted | ||||||
* in a single format, rather than writing different logic for various combinations of decimal | ||||||
* and grouping separators. The format is "1234.5678" | ||||||
*/ | ||||||
const amountRaw = useMemo(() => { | ||||||
return formatNumber(amount).replaceAll(groupingSeparator, '').replaceAll(decimalSeparator, '.') | ||||||
}, [amount]) | ||||||
|
||||||
/** | ||||||
* This field includes all the necessary processed derived state. It is recalculated once whenever | ||||||
* the input amount changes. Please, add new processed/calculated values to this variable. | ||||||
* | ||||||
* This field consists of two values: token and local. Both values represent calculated values for the | ||||||
* corresponding amount type. Whenever we change token amount - we need to recalculate both token and | ||||||
* local amounts. Each object consists of: | ||||||
* - "amount" - this is the actual value, formatted to the unified format without any groupings | ||||||
* and with a point as a decimal separator, format example: "1234.5678" (as per "amountRaw" value above) | ||||||
* - "bignum" - this is a BigNumber representation of the "amount" field. Necessary for easier | ||||||
* - `bignum` - this is a BigNumber representation of the "amount" field. Necessary for easier | ||||||
* condition checks and various processing things. | ||||||
* - "displayAmount" - this is a read-only component-friendly value that contains all of the necessary | ||||||
* - `displayAmount` - this is a read-only component-friendly value that contains all of the necessary | ||||||
* formatting, including: grouping, decimals, token symbol/fiat sign, small amounts format. This | ||||||
* value is only necessary to be passed to TokenEnterAmount component fields as: | ||||||
* - token.displayAmount -> tokenAmount | ||||||
* - local.displayAmount -> localAmount | ||||||
* - `token.displayAmount` -> `tokenDisplayAmount` | ||||||
* - `local.displayAmount` -> `localDisplayAmount` | ||||||
*/ | ||||||
const processedAmounts = useMemo(() => { | ||||||
if (amountType === 'token') { | ||||||
const parsedTokenAmount = amountRaw === '' ? null : parseInputAmount(amountRaw) | ||||||
const parsedTokenAmount = amount === '' ? null : parseInputAmount(amount) | ||||||
|
||||||
const tokenToLocal = convertTokenToLocalAmount({ | ||||||
tokenAmount: parsedTokenAmount, | ||||||
tokenInfo: props.token, | ||||||
usdToLocalRate, | ||||||
}) | ||||||
|
||||||
const convertedTokenToLocal = | ||||||
tokenToLocal && tokenToLocal.gt(0) ? tokenToLocal.toFixed(2) : '' | ||||||
|
||||||
return { | ||||||
token: { | ||||||
amount: amountRaw, | ||||||
bignum: parsedTokenAmount, | ||||||
displayAmount: getDisplayTokenAmount(parsedTokenAmount, props.token), | ||||||
}, | ||||||
local: { | ||||||
amount: convertedTokenToLocal, | ||||||
bignum: tokenToLocal, | ||||||
displayAmount: getDisplayLocalAmount(tokenToLocal, localCurrencySymbol), | ||||||
}, | ||||||
|
@@ -160,7 +162,7 @@ | |||||
/** | ||||||
* At this point, we can be sure that we are processing local (fiat) input. | ||||||
*/ | ||||||
const parsedLocalAmount = amountRaw === '' ? null : parseInputAmount(amountRaw) | ||||||
const parsedLocalAmount = amount === '' ? null : parseInputAmount(amount) | ||||||
|
||||||
const localToToken = convertLocalToTokenAmount({ | ||||||
localAmount: parsedLocalAmount, | ||||||
|
@@ -177,66 +179,94 @@ | |||||
: '' | ||||||
|
||||||
const parsedTokenAmount = parseInputAmount(convertedLocalToToken, decimalSeparator) | ||||||
const balanceInLocal = convertTokenToLocalAmount({ | ||||||
tokenAmount: props.token.balance, | ||||||
tokenInfo: props.token, | ||||||
usdToLocalRate, | ||||||
})?.decimalPlaces(2) | ||||||
|
||||||
const isMaxLocalAmount = | ||||||
parsedLocalAmount && balanceInLocal && balanceInLocal.eq(parsedLocalAmount) | ||||||
const tokenAmount = isMaxLocalAmount ? props.token.balance : parsedTokenAmount | ||||||
|
||||||
return { | ||||||
token: { | ||||||
amount: convertedLocalToToken, | ||||||
bignum: parsedTokenAmount, | ||||||
displayAmount: getDisplayTokenAmount(parsedTokenAmount, props.token), | ||||||
bignum: tokenAmount, | ||||||
displayAmount: getDisplayTokenAmount(tokenAmount, props.token), | ||||||
}, | ||||||
local: { | ||||||
amount: parsedLocalAmount?.toFixed(2) ?? '', | ||||||
bignum: parsedLocalAmount, | ||||||
displayAmount: getDisplayLocalAmount(parsedLocalAmount, localCurrencySymbol), | ||||||
}, | ||||||
} | ||||||
}, [amountRaw, amountType, localCurrencySymbol]) | ||||||
}, [amount, amountType, localCurrencySymbol]) | ||||||
|
||||||
function handleToggleAmountType() { | ||||||
setAmountType((prev) => (prev === 'local' ? 'token' : 'local')) | ||||||
const newAmountType = amountType === 'local' ? 'token' : 'local' | ||||||
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 is an existing issue and i haven't been able to think of a better approach that doesn't introduce a bunch of issues, but setting state using the existing value of a state variable like this is unreliable because of the batched state updates 🙈 probably low risk for this use case, unless someone is toggling the amounts many times really quickly. maybe this component would have been a good one for 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. @kathaypacific considering that the state setting is indeed batched since React 18 and the whole scope of And according to React docs: So even if user clicks multiple time - we will be sure that both states will always be in sync with one another as it will only execute everything in scope as a single update. P.S. I do remember that they also whether had this stated at the react docs previously or it was just some Abramov's post but there was a statement that using state setter without callback is completely fine if the use-case is simple enough and it will benefit from batching (which I believe applies in this case). |
||||||
setAmountType(newAmountType) | ||||||
setAmount( | ||||||
amountType === 'token' ? processedAmounts.local.amount || '' : processedAmounts.token.amount | ||||||
newAmountType === 'local' | ||||||
? processedAmounts.local.bignum?.toFixed(2) || '' | ||||||
: processedAmounts.token.bignum?.decimalPlaces(6).toString() || '' | ||||||
) | ||||||
props.inputRef.current?.blur() | ||||||
} | ||||||
|
||||||
function handleAmountInputChange(val: string) { | ||||||
let value = val.replaceAll(groupingSeparator, '') | ||||||
let value = val.replaceAll(groupingSeparator, '').replaceAll(decimalSeparator, '.') | ||||||
value = value.startsWith('.') ? `0${value}` : value | ||||||
|
||||||
if (!value) { | ||||||
setAmount('') | ||||||
props.onHandleAmountInputChange?.('') | ||||||
return | ||||||
} | ||||||
|
||||||
if (value.startsWith(decimalSeparator)) { | ||||||
value = `0${value}` | ||||||
} | ||||||
|
||||||
// only allow numbers, one decimal separator and 2 decimals | ||||||
const localAmountRegex = new RegExp(`^(\\d+([.])?\\d{0,2}|[.]\\d{0,2}|[.])$`) | ||||||
// only allow numbers, one decimal separator and amount of decimals equal to token.decimals | ||||||
const tokenAmountRegex = new RegExp( | ||||||
`^(?:\\d+[${decimalSeparator}]?\\d{0,${props.token.decimals}}|[${decimalSeparator}]\\d{0,${props.token.decimals}}|[${decimalSeparator}])$` | ||||||
) | ||||||
|
||||||
// only allow numbers, one decimal separator and 2 decimals | ||||||
const localAmountRegex = new RegExp( | ||||||
`^(\\d+([${decimalSeparator}])?\\d{0,2}|[${decimalSeparator}]\\d{0,2}|[${decimalSeparator}])$` | ||||||
`^(?:\\d+[.]?\\d{0,${props.token.decimals}}|[.]\\d{0,${props.token.decimals}}|[.])$` | ||||||
) | ||||||
|
||||||
if ( | ||||||
(amountType === 'token' && value.match(tokenAmountRegex)) || | ||||||
(amountType === 'local' && value.match(localAmountRegex)) | ||||||
) { | ||||||
setAmount(value) | ||||||
props.onHandleAmountInputChange?.(value) | ||||||
return | ||||||
} | ||||||
} | ||||||
|
||||||
function handlePercentage(percentage: number) { | ||||||
if (percentage <= 0 || percentage > 1) return | ||||||
|
||||||
const percentageAmount = props.token.balance.multipliedBy(percentage) | ||||||
const newAmount = | ||||||
amountType === 'token' | ||||||
? percentageAmount.decimalPlaces(6).toString() | ||||||
: roundLocalNumber( | ||||||
convertTokenToLocalAmount({ | ||||||
tokenAmount: percentageAmount, | ||||||
tokenInfo: props.token, | ||||||
usdToLocalRate, | ||||||
}) | ||||||
) | ||||||
|
||||||
if (!newAmount) return | ||||||
|
||||||
setAmount(newAmount) | ||||||
} | ||||||
|
||||||
return { | ||||||
amount: amountRaw, | ||||||
amount, | ||||||
amountType, | ||||||
processedAmounts, | ||||||
setAmount, | ||||||
handleToggleAmountType, | ||||||
handleAmountInputChange, | ||||||
handlePercentage, | ||||||
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.
Suggested change
|
||||||
} | ||||||
} | ||||||
|
||||||
|
@@ -276,9 +306,14 @@ | |||||
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 | ||||||
const { decimalSeparator } = getNumberFormatSettings() | ||||||
const tokenPlaceholder = new BigNumber(0).toFormat(2) | ||||||
const localPlaceholder = `${localCurrencySymbol}${new BigNumber(0).toFormat(2).replaceAll('.', decimalSeparator)}` | ||||||
|
||||||
const placeholder = useMemo(() => { | ||||||
const { decimalSeparator } = getNumberFormatSettings() | ||||||
return { | ||||||
token: new BigNumber(0).toFormat(2), | ||||||
local: `${localCurrencySymbol}${new BigNumber(0).toFormat(2).replaceAll('.', decimalSeparator)}`, | ||||||
} | ||||||
}, [localCurrencySymbol]) | ||||||
|
||||||
const formattedInputValue = useMemo(() => { | ||||||
const formattedNumber = formatNumber(inputValue) | ||||||
|
@@ -358,7 +393,7 @@ | |||||
}} | ||||||
value={formattedInputValue} | ||||||
placeholderTextColor={Colors.gray3} | ||||||
placeholder={amountType === 'token' ? tokenPlaceholder : localPlaceholder} | ||||||
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 | ||||||
|
@@ -406,12 +441,12 @@ | |||||
|
||||||
<Text | ||||||
numberOfLines={1} | ||||||
style={[styles.secondaryAmountText, { maxWidth: '35%' }]} | ||||||
style={[styles.secondaryAmountText, { flexShrink: 0, maxWidth: '45%' }]} | ||||||
testID={`${testID}/ExchangeAmount`} | ||||||
> | ||||||
{amountType === 'token' | ||||||
? `${APPROX_SYMBOL} ${localAmount ? localAmount : localPlaceholder}` | ||||||
: `${APPROX_SYMBOL} ${tokenAmount ? tokenAmount : tokenPlaceholder}`} | ||||||
? `${APPROX_SYMBOL} ${localAmount ? localAmount : placeholder.local}` | ||||||
: `${APPROX_SYMBOL} ${tokenAmount ? tokenAmount : placeholder.token}`} | ||||||
</Text> | ||||||
</> | ||||||
) : ( | ||||||
|
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.
or
parseNumberToStandardFormat
orapplyUSNumberFormatting
?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.
@kathaypacific hmm, I'm still unsure on this one. It is only used in a single place so far to actually un-format already formatted number that comes from the input and format it for the processing purposes. I think it makes a more clear distinction for the specific purpose of the function:
I've only made is a separate function to simplify description as otherwise I'd need to write a comment explaining what it does and why, which feels more concise in a form of a clear function name.
In case of
formatNumberToStandard/parseNumberToStandardFormat
- we need to define what we mean by "standard". And in case ofapplyUSNumberFormatting
- we need to understand why we specifically useUS
number formatting (even though I'm not sure it's US specific as up to this point I thought this was a general format that everyone uses and was always using it myself 😄)At least, that's my train of though. What do you think about it?