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 124 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
/>
</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?

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's indeed redundant, will remove!

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?

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 As I understand - it's purely for type narrowing. It shouldn't ever evaluate to true as warnings.showDecreaseSpendForGasWarning directly contradicts it. But with this condition quote.preparedTransactions is evaluated to PreparedTransactionsNeedDecreaseSpendAmountForGas and doesn't need type casting with as (even thought the actual warnings.showDecreaseSpendForGasWarning check already ensures the type is right but typescript cannot determine it as it considers it a "different variable" 🤷 )

Copy link
Contributor

Choose a reason for hiding this comment

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

ah, yes! it makes sense, thanks! maybe we can add the // Should never happen comment here -- up to your judjement.
i noticed they are often used in our code base to mark those type guards and allow to just skip them when reading without thinking too much. at least, that's how it works for me 😅

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

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! It was like that initially so I assume that it was accidentally left out from clean up.

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

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 Yes. Per design, UK disclaimer has to be right above the button and this View is a direct child of ScrollView component with styles.contentContainer which has justifyContent: 'space-between'. Without this view disclaimer and button will be evenly separated from other blocks (inputs and warnings) across the whole height of the screen instead of sticking together.

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?

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

  1. It's just me not doing my due diligence with the styles cleanup after making layout changes. Will move this to a separate class!
  2. We do need it as it separates block with inputs and warnings from the block with the disclaimer and button which ensures the first block to stay at the very top and the second one - at the very bottom.

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?

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 it's indeed redundant, will remove!

}

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

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 hard agree on both!


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?

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 this log was present there from the start. I would keep it just in case somehow in the future we actually hit it for any miraculous reason (even though it should be impossible but who knows 🤷 )

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

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'll add a TODO comment for this one. This useEffect initially lacked some dependencies which contradicts the rules of hooks but if I include all used variables/functions - it causes unnecessary re-runs which breaks a bunch of tests. So in order to make this optimization more clear - I'd prefer to make it in a separate PR, especially considering that currently the flow works as intended (just not following the rules of hooks).

So I've just left those commented there to not forget that they should also be acknowledged once I get to the point of optimizing it so I'll add a comment explaining that.

@bakoushin
Copy link
Contributor

Epic work! 🎉

Left few nits/questions

Copy link

emerge-tools bot commented Jan 2, 2025

📸 Snapshot Test

Base build not found

No build was found for the base commit 8891d00. This is required to generate a snapshot diff for your pull request.

It's possible that you created a branch off the base commit before all of the CI steps have finished processing, e.g. the one that uploads a build to our system. If that's the case, no problem! Just wait and this will eventually resolve.


🛸 Powered by Emerge Tools

@@ -201,9 +215,11 @@ export function useEnterAmount(props: {
displayAmount: getDisplayLocalAmount(parsedLocalAmount, localCurrencySymbol),
},
}
}, [amount, amountType, localCurrencySymbol])
}, [amount, amountType, localCurrencySymbol, props.token])
Copy link
Contributor

Choose a reason for hiding this comment

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

not directly related to this PR, but i'm trying to better understand the logic behind the inclusion/exclusion some variables to the dependency list.

for instance, we included the localCurrencySymbol, but not usdToLocalRate. yeah, it is highly unlikely that the rate could suddenly change while this component is rendered. but what is the reason for choosing one over another? it seems equally unlikely that the currency symbol will suddenly change.

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 that's just again my inattentiveness! Everything used in the scope should be included in the deps list. Will fix this now!

@@ -2823,5 +2823,6 @@
"availableBalance": "Available: <0></0>",
"selectToken": "Select token",
"fiatPriceUnavailable": "Price unavailable"
}
},
"on": "on"
Copy link
Contributor

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:

"tokenDescription": "{{tokenName}} on {{tokenNetwork}}"

and, with that, shall we include this item in the tokenEnterAmount group above?

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 keep the "on"?

@@ -283,6 +285,9 @@ export default function BeforeDepositBottomSheet({
const { t } = useTranslation()

const { availableShortcutIds } = pool
Copy link
Contributor

Choose a reason for hiding this comment

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

not sure if those files should appear in the diff

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 oops, that's a merge going wrong.

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 this one's resolved!

@@ -127,7 +127,7 @@ describe(PointsHistoryBottomSheet, () => {
).toBeTruthy()
expect(tree.getByText('points.history.cards.createWallet.subtitle')).toBeTruthy()

expect(tree.getByText('March')).toBeTruthy()
expect(tree.getByText('March', { exact: false })).toBeTruthy()
Copy link
Contributor

Choose a reason for hiding this comment

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

perhaps this change is worth its own (yet tiny) PR, so other team members could benefit from the fix before this epic PR is merged.
also, it will provide more context as to why it is fixed.

Copy link
Contributor Author

@sviderock sviderock Jan 2, 2025

Choose a reason for hiding this comment

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

@bakoushin addressed in this PR: #6392

@sviderock sviderock force-pushed the slava/change-swap-flow branch from 0130a21 to 9c6886d Compare January 2, 2025 14:21
Comment on lines 450 to 452
[Field.FROM]: processedAmountsFrom.token.bignum
? processedAmountsFrom.token.bignum.toString()
: '',
Copy link
Contributor

Choose a reason for hiding this comment

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

supernit: perhaps we can make use of a nullish coalescing operator and save 2 lines here? 😅

processedAmountsFrom.token.bignum?.toString() ?? ''

Copy link
Contributor Author

@sviderock sviderock Jan 2, 2025

Choose a reason for hiding this comment

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

@bakoushin it's even gonna be 4 lines cause it has to be done for both FROM and TO 😄

let newSwitchedToNetwork: typeof switchedToNetworkId | null = null

switch (true) {
// If were selecting a field that was already selected in the other input then switch inputs
Copy link
Contributor

Choose a reason for hiding this comment

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

looks like typo: "we're"

Comment on lines 645 to 669
if (!fromToken) {
// Should never happen
return
}
AppAnalytics.track(SwapEvents.swap_screen_percentage_selected, {
tokenSymbol: fromToken.symbol,
tokenId: fromToken.tokenId,
tokenNetworkId: fromToken.networkId,
percentage,
})
}

function onPressLearnMore() {
AppAnalytics.track(SwapEvents.swap_learn_more)
navigate(Screens.WebViewScreen, { uri: links.swapLearnMore })
}

function onPressLearnMoreFees() {
AppAnalytics.track(SwapEvents.swap_gas_fees_learn_more)
navigate(Screens.WebViewScreen, { uri: links.transactionFeesLearnMore })
}

return (
<SafeAreaView style={styles.safeAreaContainer} testID="SwapScreen">
<CustomHeader
Copy link
Contributor

Choose a reason for hiding this comment

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

stylistic nit: most of the handlers above start with handle-, but these two start with on-. it would be nice to have a consistent naming (my preference is to keep using handle-)

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 fixed!

Copy link
Contributor

@bakoushin bakoushin left a comment

Choose a reason for hiding this comment

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

🚀

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