Skip to content
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

Open
wants to merge 116 commits into
base: main
Choose a base branch
from

Conversation

sviderock
Copy link
Contributor

@sviderock sviderock commented Nov 19, 2024

Final PR for new Enter Amount component. This page implements a 2nd version of SwapScreen that uses new Enter Amount component. It is hidden behind the feature flag to mitigate the risks of such a big rework.

SwapScreenV2.test.tsx file is an exact copy of the existing SwapScreen.test.tsx. It seems like a huge addition (2k lines) BUT it is a 99% copy-paste just because the new flow is implemented as a new component. The only changes in that file are testID props and some translations excerpts. To make life A LOT easier in reviewing this specific file, I suggest doing the following in VSCode to only see the changed lines:

  • Right click SwapScreen.test.tsx and click "Select for Compare"
  • Right click SwapScreenV2.test.tsx and click "Compare with Selected"

Details of the implementation for clarity:

  • The correct functioning of the new component has been ensured by implementing it to pass all the existing tests from SwapScreen.test.tsx.
  • Removed local Redux slice. As a new Enter Amount component is using useState and it would be a mess to keep multiple useStates and Redux slice in sync - all the other fields from the slice were moved to the corresponding useStates.
  • The business logic of the component is intact. Only the state implementation (useState instead of Redux slice) has changed to follow the existing logic therefore some functions look different but they do identical things in terms of business logic.
  • Code has been order in the following order:
    1. variables
    2. derived variables (useMemo)
    3. useEffects
    4. functions

Test plan

Simulator.Screen.Recording.-.iPhone.15.Pro.Max.-.2024-12-19.at.14.56.55.mp4

Related issues

Relates to RET-1208

Backwards compatibility

Yes

Network scalability

If a new NetworkId and/or Network are added in the future, the changes in this PR will:

  • Continue to work without code changes, OR trigger a compilation error (guaranteeing we find it when a new network is added)

@sviderock sviderock changed the base branch from main to slava/change-earn-flow November 19, 2024 12:44
styles.primaryAmountText,
inputStyle,
Platform.select({ ios: { lineHeight: undefined } }),
<View>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This addition/removal is purely a result of adding extra View wrapper around the input as it is needed for the new loading check below. The actual TextField props are unchanged.

Comment on lines +40 to +43
export function getDisplayTokenName<T extends { symbol: string; networkId: NetworkId }>(token: T) {
return `${token.symbol} on ${NETWORK_NAMES[token.networkId]}`
}

Copy link
Contributor Author

@sviderock sviderock Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was moved to a function just for the sake of being able to use it in the SwapScreenV2.test.tsx file as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not related to the actual change, but it will show "on" without translation no matter what language is used? are we okay with that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bakoushin You're right, my bad! Will add it to the translations.

networkId: fromToken?.networkId || networkConfig.defaultNetworkId,
slippagePercentage: maxSlippagePercentage,
enableAppFee: enableAppFee,
onError: (error) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onError and onSuccess are the same as identical useEffects from SwapScreen.tsx

})
}

function onSelectTokenFrom(selectedToken: TokenBalance, tokenPositionInList: number) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In SwapScreen.tsx functionality of onSelectTokenFrom and onSelectTokenTo functions is implemented in handleConfirmSelectToken. These two functions execute the same logic but as a two separate handlers. When I was trying to understand the flow of the data in the existing component it seemed like separating this function into separate handlers with straightforward actions could make it more understandable.

Obviosuly, that's subjective so please let me know if you have any comments or objections!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shall we keep the handle- prefix?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps we can keep 2 handlers, but extract most of the logic into a separate function?
the main difference i noticed is the field prop, the rest seems identical.
it feels not quite right to introduce that amount of ducplication

@sviderock sviderock marked this pull request as ready for review December 19, 2024 13:32
@@ -139,6 +145,13 @@ export function useEnterAmount(props: {
* - `local.displayAmount` -> `localDisplayAmount`
*/
const processedAmounts = useMemo(() => {
if (!props.token) {
Copy link
Contributor

@bakoushin bakoushin Dec 23, 2024

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 😄

@@ -183,7 +187,7 @@ function useSwapQuote({
return null
}

if (!swapAmount[updatedField].gt(0)) {
if (!swapAmount[updatedField] || !swapAmount[updatedField].gt(0)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wonder if we need this condition: !swapAmount[updatedField].gt(0)?
i noticed that we do almost the same check against the swpapAmountInWay just below

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bakoushin I've just adjusted the existing logic to reduce the amount of potential changes. But I can remove if needed!

onInputChange={handleAmountInputChange}
amountType={amountType}
toggleAmountType={handleToggleAmountType}
onOpenTokenPicker={onOpenTokenPickerFrom}
Copy link
Contributor

@bakoushin bakoushin Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

supernit: shall we prefix this handler with handle- instead of on- for naming consistency?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bakoushin I think it makes sense to keep props of components with on to follow the regular naming convention of event handlers for components (e.g. onClick/onBlur etc).

Naming function handlers with handle totally makes sense to me though. Will change that!

amountType={amountType}
onOpenTokenPicker={onOpenTokenPickerTo}
loading={shouldShowSkeletons}
testID="SwapAmountInput"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this testID is identical to the "from" component. perhaps a typo?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bakoushin its per design. It was like that before rework and I've kept it the same cause in the test files it searches for the same testIDs but in different containers (from/to)

>
<View style={{ display: 'flex', justifyContent: 'space-between', flexGrow: 1 }}>
<View style={{ flexShrink: 0 }}>
<View style={[styles.warningsContainer, { gap: 4 }]}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"warnings" sounds kind of confusing: there is no any warning inside this view

/>
</View>

<View>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this <View> wrapper?

Comment on lines +853 to +857
if (
!quote ||
quote.preparedTransactions.type !== 'need-decrease-spend-amount-for-gas'
)
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why we need this condition? it seems to lead to a confusing ux.

if i correctly understand its behavior:

  1. the warning is displayed
  2. but the CTA press does nothing

why it does nothing? maybe we shoudn't display the warning in the first place?

feeCurrencies,
})}
style={styles.warning}
onPressCta={onPressLearnMoreFees}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

either the cta label is missing or this handler is redundant

</View>
</View>

<View>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this <View> wrapper?

contentContainerStyle={styles.contentContainer}
keyboardShouldPersistTaps="handled"
>
<View style={{ display: 'flex', justifyContent: 'space-between', flexGrow: 1 }}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the reason of inlining styles vs including them in the StyleSheet?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and do we need that View at all?

keyboardShouldPersistTaps="handled"
>
<View style={{ display: 'flex', justifyContent: 'space-between', flexGrow: 1 }}>
<View style={{ flexShrink: 0 }}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this style?

}

if (fromToken?.tokenId === selectedToken.tokenId) {
setFromToken(toToken)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to set "from" token in this handler?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please ignore, got it 😅

// if in "from" we select the same token as in "to" then just swap
if (toToken?.tokenId === selectedToken.tokenId) {
setFromToken(toToken)
setToToken(fromToken)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to set "to" token in this handler?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please ignore, got it 😅

Comment on lines +416 to +421
AppAnalytics.track(SwapEvents.swap_screen_select_token, { fieldType: Field.FROM })
// use requestAnimationFrame so that the bottom sheet open animation is done
// after the selectingField value is updated, so that the title of the
// bottom sheet (which depends on selectingField) does not change on the
// screen
requestAnimationFrame(() => tokenBottomSheetFromRef.current?.snapToIndex(0))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. shall we consistenly use the hadndle prefix?
  2. perhaps we can extract this code to a separate function e.g. handleOpenTokenPicker(fieldType: Field) to emphasize that hanlding is mostly identical


if (!fromToken || !toToken) {
// Should never happen
Logger.error(TAG, 'fromToken or toToken not found')
Copy link
Contributor

@bakoushin bakoushin Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if that is a type guard (as "should never happen" comment suggests), do we need to log anything?

Comment on lines +370 to +371
// clearQuote,
// replaceAmountTo,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shall we remove it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or uncomment? 😅

@bakoushin
Copy link
Contributor

Epic work! 🎉

Left few nits/questions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants