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: add new Review component and apply it to SendConfirmation #6402

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

Conversation

sviderock
Copy link
Contributor

@sviderock sviderock commented Jan 9, 2025

This PR adds a new reusable Review component to be used for all the review screens and applies it to the SendConfirmation screen. (design)

Review component is implemented with a bunch of composable components which might look too abstract (e.g. a bunch of components are just a single View with styles and children) but they provide a few benefits:

  • all the styles are incapsulated inside of the Review.tsx file so there are no need to copy-paste all the styles (even small ones like gap: 16) to each review screen
  • the whole structure of the Review screen is purely JSX so there is a clear vision of how many things is rendered and what those things are specifically
  • no need to move items to an array using useMemo (e.g. if we were to implement ReviewDetails as a component that accepts an array prop for all the children items)
  • we can remove a lot of tests from each individual screen as all the review-related things will be tested from within Review.test.tsx

The Review component also re-uses formatting functions from TokenEnterAmount.tsx for better consistency.

P.S. Actual implementation (Review component + SendConfirmation changes) takes ~400 lines. Most of the other changes are necessary snapshot changes and new test file Review.test.tsx.
Also, Knip forced me to remove a component and move the function out of it to the different file which also trigger tiny changes in some extra unrelated files.

Test plan

Simulator.Screen.Recording.-.iPhone.15.Pro.Max.-.2025-01-09.at.14.02.46.mp4

Related issues

Relates to RET-1295

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)

Copy link

codecov bot commented Jan 9, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 89.11%. Comparing base (ee71b25) to head (a09bf64).
Report is 3 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##             main    #6402    +/-   ##
========================================
  Coverage   89.10%   89.11%            
========================================
  Files         737      737            
  Lines       31881    31956    +75     
  Branches     5743     6060   +317     
========================================
+ Hits        28409    28478    +69     
- Misses       3424     3431     +7     
+ Partials       48       47     -1     
Files with missing lines Coverage Δ
src/account/utils.ts 100.00% <100.00%> (ø)
src/components/Avatar.tsx 100.00% <100.00%> (ø)
src/components/ContactCircle.tsx 100.00% <100.00%> (ø)
src/components/Review.tsx 100.00% <100.00%> (ø)
src/recipients/recipient.ts 78.90% <100.00%> (ø)
src/send/SendConfirmation.tsx 96.55% <100.00%> (+0.71%) ⬆️
src/tokens/hooks.ts 98.03% <100.00%> (+0.42%) ⬆️

... and 4 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update ee71b25...a09bf64. Read the comment docs.

@sviderock sviderock marked this pull request as ready for review January 9, 2025 12:04
return <DefaultIcon foregroundColor={fontColor} backgroundColor={iconBackgroundColor} />
return (
<DefaultIcon
size={iconSize / 1.625}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't like using magic numbers but in this case this is the exact proportion as per the new designs (which I think must be applied across the codebase for consistency)

Comment on lines -170 to -218

it('renders address for phone recipients with multiple addresses', () => {
const screenProps = getMockStackScreenProps(Screens.SendConfirmation, {
transactionData: {
...mockTokenTransactionData,
recipient: mockRecipient, // recipient that includes a PN
},
origin: SendOrigin.AppSendFlow,
isFromScan: false,
})
const { getByTestId } = renderScreen(
{
identity: {
e164NumberToAddress: {
[mockE164Number]: [mockAccount3, mockAccount2],
},
},
},
screenProps
)

expect(getByTestId('RecipientAddress')).toBeTruthy()
})

it.each([
{ testSuffix: 'non phone number recipients', recipient: mockAddressRecipient },
{ testSuffix: 'phone number recipient with one address', recipient: mockRecipient },
])('does not render address for $testSuffix', ({ recipient }) => {
const screenProps = getMockStackScreenProps(Screens.SendConfirmation, {
transactionData: {
...mockTokenTransactionData,
recipient,
},
origin: SendOrigin.AppSendFlow,
isFromScan: false,
})
const { queryByTestId } = renderScreen(
{
identity: {
e164NumberToAddress: {
[mockE164Number]: [mockAccount3],
},
},
},
screenProps
)

expect(queryByTestId('RecipientAddress')).toBeFalsy()
})
Copy link
Contributor Author

@sviderock sviderock Jan 9, 2025

Choose a reason for hiding this comment

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

These seem to be redundant with the new way of showing the contact and considering that the contact view itself is tested in Review.test.tsx

Comment on lines +218 to +219
const displayTokenAmount = getDisplayTokenAmount(tokenAmount, token)
const displayLocalAmount = getDisplayLocalAmount(tokenToLocal, localCurrencySymbol)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it might make sense to move getDisplayTokenAmount/getDisplayLocalAmount out of TokenEnterAmount.tsx if we're gonna use this across the board for the consistency?

@@ -76,7 +76,7 @@ export default Send = () => {
})

it('Then should display correct recipient', async () => {
await expect(element(by.text(recipientAddressDisplay))).toBeVisible()
await expect(element(by.text(DEFAULT_RECIPIENT_ADDRESS))).toBeVisible()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addresses are now showed in full, not shortened

Copy link
Collaborator

@kathaypacific kathaypacific left a comment

Choose a reason for hiding this comment

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

sorry i haven't actually finished reviewing but need to go offline for a bit so i thought i'd submit the partial review for now in case the early feedback is helpful. i'll continue this afternoon.

one thought i had is that now the enter amount screens are all consistent, and they all prepare transactions (since we need to show the network fees). so we should pass the prepared transactions to the review screens, and there should be no need to prepare the transactions again and therefore no need to cater for a loading state right? i know the prepare transactions logic is existing but perhaps it's not needed anymore and can be simplified?

@@ -30,7 +34,7 @@ function ContactCircle({
backgroundColor,
foregroundColor,
borderColor,
DefaultIcon = ({ foregroundColor }) => <User color={foregroundColor} />,
DefaultIcon = ({ color, size = 20 }) => <User size={size} color={color} />,
Copy link
Collaborator

Choose a reason for hiding this comment

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

why apply a size to User when it is overwritten when the DefaultIcon is rendered later?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that's indeed redundant, will remove!

@@ -187,3 +194,32 @@ export function useAmountAsUsd(amount: BigNumber, tokenId: string | undefined) {
}
return amount.multipliedBy(tokenInfo.priceUsd)
}

export function useDisplayAmount({
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we please add a comment about what the expected resulting amount looks like? i think that there is a tendency for these generically named "toDisplayFormat" type of functions to become duplicated because it is not obvious what it is trying to do and when someone needs something similar, it's difficult to know that it already exists.

along this line of thinking, what do you think about accepting the local currency symbol and usd to local rate as props so that it can be a pure function rather than a hook? then we can start to have a collection of these formatting functions in one place, so that it is easier to find what you need?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree, that does make a lot of sense. One of my intentions with this specific function was to also memoize formatting of the string as it invokes 3 functions just to format that string (even though it's definitely not a performance bottleneck). This was more of a "if it takes basically no effort to reduce recalculations - I'll just do it" type of thing.

But I totally agree with the point of having it as a pure function and having an explanatory comment for better clarity. Will change the implementation!

@@ -187,3 +194,32 @@ export function useAmountAsUsd(amount: BigNumber, tokenId: string | undefined) {
}
return amount.multipliedBy(tokenInfo.priceUsd)
}

export function useDisplayAmount({
Copy link
Collaborator

Choose a reason for hiding this comment

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

it'd also be great to add a test for this

import { Spacing } from 'src/styles/styles'
import variables from 'src/styles/variables'

export function Review(props: {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
export function Review(props: {
export function ReviewTransaction(props: {

title: string
children: ReactNode
headerAction?: ReactNode
isModal?: boolean
Copy link
Collaborator

Choose a reason for hiding this comment

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

will this be used? i thought we were going to make any review modals into screens?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought that it might still be used (noticed usage of Screens.SendConfirmationModal from within bidali exchange flow?) but I'll double check it. There wasn't a particular screen in design where that particular modal changes (Screens.SendConfirmation was a separate screen from the start, not a modal) so I've kept it unchanged but I should've clarified this with Kayla. I'll talk to her about this one!

</Text>
<View style={styles.reviewSummaryItemContent}>
{props.icon}
<View style={{ flexShrink: 1 }}>
Copy link
Collaborator

Choose a reason for hiding this comment

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

should use stylesheet instead?

Copy link
Contributor Author

@sviderock sviderock Jan 10, 2025

Choose a reason for hiding this comment

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

I've noticed throughout the codebase that there are sometimes inline styles present and I assumed that this is fine for some occasions. It feels like sometimes (and specifically in this case) the style is so rudementary that it makes it clearer to understand what it does if it stays inline. That way it's clear that it's a "patch-like" kind of style. Otherwise, I'll need to provide a classname like "reviewSummaryItemContentTitlesWrapper` that doesn't provide any clarity or context on what is does (and even contextually the name doesn't explain what it does) and only consists of a single property that just fixes a certain quirk/aspect.

I do acknowledge the downside of inline styles being an object that re-creates on every re-render (therefore not keeping the same reference as it would be if it was created within the styles object) but my understanding is that's not a performance issue at all as of now, especially with the components that don't tend to re-render much.

Copy link
Collaborator

Choose a reason for hiding this comment

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

i think any inline styles that are in the codebase that aren't actively using dynamic variables are unintentional...IMO consistency makes it worthwhile. i'd also argue that it's additional mental energy to think "is there only one style? if so, can i name it easily? if so, i'll put it in the stylesheet and if not, i'll put it inline" - it's also less scalable because it makes it harder to add new styles (would need to decide to add to the inline styles, or when it's worth moving to the stylesheet). the readability topic is interesting, i can see why seeing the style inline makes it easy to understand, however i think seeing one style in isolation doesn't necessarily give you the whole picture...

flexShrink: 1,
},
reviewSummaryItem: {
gap: 4,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
gap: 4,
gap: Spacing.Tiny4,

},
reviewSummaryItemContent: {
flexDirection: 'row',
gap: 8,
Copy link
Collaborator

Choose a reason for hiding this comment

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

should use Spacing

reviewDetailsItemLabel: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
Copy link
Collaborator

Choose a reason for hiding this comment

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

please use one of the Spacing values

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 one was specifically 6.5 in designs which we don't have a direct spacing value for so I've just kept it like this. I don't think 4/8 will make much bigger visual difference but I feel like I see it and it nagged me that it wasn't pixel-perfect 😄 (even though I made it 6 and not 6.5 as in design but I'm too used to web px units disregarding decimal points)

Copy link
Collaborator

Choose a reason for hiding this comment

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

oh for cases like this where the designs are using variables outside of our predefined values, please tell Kayla to update the designs - we need to hold each other accountable to the design system :)

@sviderock
Copy link
Contributor Author

sviderock commented Jan 10, 2025

@kathaypacific it is indeed helpful! My understanding was that it was necessary to reload the quote on review screen just to make sure that it is up to date with what actually is gonna happen as I've noticed multiple times that in those few seconds when you go from enter amount screen to review - it might have different fee amount as it fluctuates a lot.

I do understand that it looks inconsistent to have one data on the enter amount and then slightly different data on the review but at the same time it is also true if we show the same data on both enter amount and review but then show different data on the actual transaction details one it's completed. Does it make sense?

But I'm happy to remove that loading state in case this indeed is unnecessary and it sure can be simplified!

@kathaypacific
Copy link
Collaborator

@kathaypacific it is indeed helpful! My understanding was that it was necessary to reload the quote on review screen just to make sure that it is up to date with what actually is gonna happen as I've noticed multiple times that in those few seconds when you go from enter amount screen to review - it might have different fee amount as it fluctuates a lot.

I do understand that it looks inconsistent to have one data on the enter amount and then slightly different data on the review but at the same time it is also true if we show the same data on both enter amount and review but then show different data on the actual transaction details one it's completed. Does it make sense?

But I'm happy to remove that loading state in case this indeed is unnecessary and it sure can be simplified!

@kathaypacific it is indeed helpful! My understanding was that it was necessary to reload the quote on review screen just to make sure that it is up to date with what actually is gonna happen as I've noticed multiple times that in those few seconds when you go from enter amount screen to review - it might have different fee amount as it fluctuates a lot.

I do understand that it looks inconsistent to have one data on the enter amount and then slightly different data on the review but at the same time it is also true if we show the same data on both enter amount and review but then show different data on the actual transaction details one it's completed. Does it make sense?

But I'm happy to remove that loading state in case this indeed is unnecessary and it sure can be simplified!

hum, can i leave this with you to decide with Kayla and Laura? i only started thinking about this because i think that the send flow used to start with an enter amount screen that didn't prepare the transactions / show the gas price. this made it a necessity to do it on the review screen. with the new enter amount components, we'll make the user wait once on the enter amount step, and then make them wait again on the next screen. removing any redundant wait time would be a nice UX improvement.

i hear the feedback that preparing transactions again would give the most recent gas estimate in case of fluctuations. if we re-prepared the transaction on the review screen, the edge case we prevent is a user enters an amount -> waits some time before proceeding to the next screen, during that time the gas price changes dramatically where re-calculating the gas value on the review screen may prevent the user from executing a transaction that costs more in gas than they want.

however, i think the edge case still remains - even if we re-prepare the transaction on the review screen, the user could still wait some time before pressing the confirm button. during this time, the gas price could have changed and we are once again in the above scenario. so some part of me thinks that if we really cared about this scenario of the gas price changing between the time of send flow start and the time of transaction execution, we could solve the problem more holistically (e.g. refresh the prepared transaction every block time, no matter which screen you are on)

i don't have a strong preference for the outcome of this topic and it's all good if we want to keep it like this but i wanted to raise it to make sure we have thought about it and are not carrying forward complexities that existed due to constraints which are no longer there by default :)

@sviderock
Copy link
Contributor Author

@kathaypacific I totally agree and now lean more to your explanation! I'll check it out with Kayla and Laura just to make sure but I think we are gonna land on the same conclusion as you've just described. I'll keep you posted!

Copy link
Collaborator

@kathaypacific kathaypacific left a comment

Choose a reason for hiding this comment

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

yay great! i like the compound components approach, it's neat 💅🏼

left={<BackButton eventName={SendEvents.send_confirm_back} />}
/>
<DisconnectBanner />
<ReviewFrame
Copy link
Collaborator

Choose a reason for hiding this comment

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

🥳 you might be able to remove ReviewFrame now too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh yeah, I can remove it! I thought it would've been caught by Knip but probably the sole reason that its own test file is importing it tricks Knip into considering it "used"

testID="SendConfirmationNetwork"
label={t('transactionDetails.network')}
value={fees.networkName}
isLoading={prepareTransactionLoading}
Copy link
Collaborator

Choose a reason for hiding this comment

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

i think that you can always show the network name (no loader needed)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yeah, me inattentive 🤦

<ReviewSummaryItem
testID="SendConfirmationToken"
header="Sending"
icon={<TokenIcon token={tokenInfo!} />}
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we avoid tokenInfo!? i'm not sure what we should do if there is no tokenInfo and it seems unlikely, but perhaps we can log an error (so at least we know about it) and render null instead of risking an app crash? or perhaps the components here already handle no tokenInfo gracefully and we just need to make the type optional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I'll put proper guardrails


// "t('unknown')" returns "mockedTranslation:unknown" as per our mock
expect(tree.getByTestId('ContactItem/Unknown/Title')).toHaveTextContent('unknown')
})
Copy link
Collaborator

Choose a reason for hiding this comment

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

i wonder if these tests would benefit from a it.each, since the tests content is the same but only the recipient and text content changes? also fine to leave it like this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They differ ever so slightly but I might change it. I'll look into it.

<ReviewSummaryItemContact header="Contact" recipient={recipient} testID="ContactItem" />
)

// "t('unknown')" returns "mockedTranslation:unknown" as per our mock
Copy link
Collaborator

Choose a reason for hiding this comment

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

wait is this true? i don't see the string mockedTranslation in any searches...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kathaypacific ah sorry, that was initial chatgpt generated comment string that I missed to remove 😅

return { title: recipient.address, icon: WalletIcon, testID: 'Address' }
}

return { title: t('unknown'), icon: UserIcon, testID: 'Unknown' }
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is probably carried over logic but i don't think it's necessary - the recipient should always have a phone number or address, so it should never reach this line. (although the main thing i wanted to call out is that "Unknown" is very unfriendly and we probably don't want this as a default, but then i realised we don't need a default at all)

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