-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
feat: add Quick Trade button to each crypto token row #31805
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
Merged
Merged
Changes from 8 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
2827956
feat(explore-search): add Quick Trade button to each crypto token row
salimtb fba4831
feat(explore-search): gate Quick Trade button behind A/B test ASSETS-…
salimtb 24b5911
fix: clean up
salimtb 4176a0f
fix: clean up
salimtb 19de026
fix: clean up
salimtb 585d816
fix: fix bottom sheet close
salimtb 15baed2
fix: design review comments
salimtb 71b77b9
fix: fix unit test
salimtb d993515
feat: extend Quick Buy flash button to all Explore surfaces
salimtb fe37cdf
fix: remove BottomSheetOverlay from QuickBuyRoot
salimtb c7dcd03
chore: merge main into feat/explore-search-quick-trade-button
salimtb db2640e
fix: put under ab testing
salimtb 7cd1a25
fix: clean up
salimtb f520a31
fix: put quick buy under ab test
salimtb File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
190 changes: 190 additions & 0 deletions
190
app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| import React from 'react'; | ||
| import { render } from '@testing-library/react-native'; | ||
| import type { TrendingAsset } from '@metamask/assets-controllers'; | ||
| import TrendingQuickBuy from './TrendingQuickBuy'; | ||
| import { MetaMetricsEvents } from '../../../../../core/Analytics'; | ||
| import { SocialLeaderboardEventProperties } from '../../../../Views/SocialLeaderboard/analytics'; | ||
|
|
||
| const mockTrack = jest.fn(); | ||
| jest.mock( | ||
| '../../../../Views/SocialLeaderboard/analytics/useSocialLeaderboardAnalytics', | ||
| () => ({ | ||
| useSocialLeaderboardAnalytics: () => ({ track: mockTrack }), | ||
| }), | ||
| ); | ||
|
|
||
| const mockQuickBuyRoot = jest.fn(); | ||
| jest.mock( | ||
| '../../../../Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/quickBuy', | ||
| () => ({ | ||
| QuickBuy: { | ||
| Root: (props: Record<string, unknown>) => { | ||
| mockQuickBuyRoot(props); | ||
| return null; | ||
| }, | ||
| }, | ||
| }), | ||
| ); | ||
|
|
||
| jest.mock( | ||
| '../../../../Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/features', | ||
| () => ({ | ||
| TOP_TRADERS_QUICK_BUY_FEATURES: { tradeModes: ['buy', 'sell'] }, | ||
| }), | ||
| ); | ||
|
|
||
| // ─── Helpers ───────────────────────────────────────────────────────────────── | ||
|
|
||
| const makeToken = (overrides: Partial<TrendingAsset> = {}): TrendingAsset => ({ | ||
| assetId: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', | ||
| name: 'USD Coin', | ||
| symbol: 'USDC', | ||
| decimals: 6, | ||
| price: '1.00', | ||
| marketCap: 75_000_000_000, | ||
| aggregatedUsdVolume: 900_000_000, | ||
| ...overrides, | ||
| }); | ||
|
|
||
| describe('TrendingQuickBuy', () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| it('renders QuickBuy.Root with isVisible=false when token is null', () => { | ||
| render(<TrendingQuickBuy token={null} onClose={jest.fn()} />); | ||
|
|
||
| expect(mockQuickBuyRoot).toHaveBeenCalledWith( | ||
| expect.objectContaining({ isVisible: false }), | ||
| ); | ||
| }); | ||
|
|
||
| it('renders QuickBuy.Root with isVisible=true when token is provided', () => { | ||
| render(<TrendingQuickBuy token={makeToken()} onClose={jest.fn()} />); | ||
|
|
||
| expect(mockQuickBuyRoot).toHaveBeenCalledWith( | ||
| expect.objectContaining({ isVisible: true }), | ||
| ); | ||
| }); | ||
|
|
||
| it('maps an ERC-20 TrendingAsset to the correct QuickBuyTarget', () => { | ||
| const token = makeToken({ | ||
| assetId: 'eip155:1/erc20:0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', | ||
| symbol: 'SNX', | ||
| name: 'Synthetix', | ||
| }); | ||
|
|
||
| render(<TrendingQuickBuy token={token} onClose={jest.fn()} />); | ||
|
|
||
| expect(mockQuickBuyRoot).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| target: { | ||
| chain: 'eip155:1', | ||
| tokenAddress: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', | ||
| tokenSymbol: 'SNX', | ||
| tokenName: 'Synthetix', | ||
| }, | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it('maps a native (slip44) TrendingAsset to the zero address', () => { | ||
| const token = makeToken({ | ||
| assetId: 'eip155:1/slip44:60', | ||
| symbol: 'ETH', | ||
| name: 'Ethereum', | ||
| }); | ||
|
|
||
| render(<TrendingQuickBuy token={token} onClose={jest.fn()} />); | ||
|
|
||
| expect(mockQuickBuyRoot).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| target: expect.objectContaining({ | ||
| tokenAddress: '0x0000000000000000000000000000000000000000', | ||
| tokenSymbol: 'ETH', | ||
| }), | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it('passes analyticsContext with source=explore_search to QuickBuy.Root', () => { | ||
| render(<TrendingQuickBuy token={makeToken()} onClose={jest.fn()} />); | ||
|
|
||
| expect(mockQuickBuyRoot).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| analyticsContext: expect.objectContaining({ source: 'explore_search' }), | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it('fires SOCIAL_QUICK_BUY_SHEET_VIEWED when token transitions from null to non-null', () => { | ||
| const token = makeToken(); | ||
| render(<TrendingQuickBuy token={token} onClose={jest.fn()} />); | ||
|
|
||
| expect(mockTrack).toHaveBeenCalledTimes(1); | ||
| expect(mockTrack).toHaveBeenCalledWith( | ||
| MetaMetricsEvents.SOCIAL_QUICK_BUY_SHEET_VIEWED, | ||
| expect.objectContaining({ | ||
| [SocialLeaderboardEventProperties.SOURCE]: 'explore_search', | ||
| [SocialLeaderboardEventProperties.CAIP19]: token.assetId, | ||
| [SocialLeaderboardEventProperties.MARKET_CAP]: token.marketCap, | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it('does not fire SOCIAL_QUICK_BUY_SHEET_VIEWED when token is null', () => { | ||
| render(<TrendingQuickBuy token={null} onClose={jest.fn()} />); | ||
| expect(mockTrack).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('does not re-fire the event while the same token stays visible', () => { | ||
| const token = makeToken(); | ||
| const { rerender } = render( | ||
| <TrendingQuickBuy token={token} onClose={jest.fn()} />, | ||
| ); | ||
|
|
||
| // Simulate a re-render with the same token (e.g. parent re-renders) | ||
| rerender(<TrendingQuickBuy token={token} onClose={jest.fn()} />); | ||
|
|
||
| expect(mockTrack).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('fires the event again when a new token is opened after closing', () => { | ||
| const tokenA = makeToken({ | ||
| assetId: 'eip155:1/erc20:0xaaa', | ||
| symbol: 'AAA', | ||
| name: 'Token A', | ||
| }); | ||
| const tokenB = makeToken({ | ||
| assetId: 'eip155:1/erc20:0xbbb', | ||
| symbol: 'BBB', | ||
| name: 'Token B', | ||
| }); | ||
|
|
||
| const { rerender } = render( | ||
| <TrendingQuickBuy token={tokenA} onClose={jest.fn()} />, | ||
| ); | ||
| // Close sheet | ||
| rerender(<TrendingQuickBuy token={null} onClose={jest.fn()} />); | ||
| // Open with a new token | ||
| rerender(<TrendingQuickBuy token={tokenB} onClose={jest.fn()} />); | ||
|
|
||
| expect(mockTrack).toHaveBeenCalledTimes(2); | ||
| expect(mockTrack).toHaveBeenNthCalledWith( | ||
| 2, | ||
| MetaMetricsEvents.SOCIAL_QUICK_BUY_SHEET_VIEWED, | ||
| expect.objectContaining({ | ||
| [SocialLeaderboardEventProperties.CAIP19]: tokenB.assetId, | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it('forwards onClose to QuickBuy.Root', () => { | ||
| const onClose = jest.fn(); | ||
| render(<TrendingQuickBuy token={makeToken()} onClose={onClose} />); | ||
|
|
||
| expect(mockQuickBuyRoot).toHaveBeenCalledWith( | ||
| expect.objectContaining({ onClose }), | ||
| ); | ||
| }); | ||
| }); |
65 changes: 65 additions & 0 deletions
65
app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import type { TrendingAsset } from '@metamask/assets-controllers'; | ||
| import React, { useEffect, useMemo, useRef } from 'react'; | ||
| import { QuickBuy } from '../../../../Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/quickBuy'; | ||
| import { TOP_TRADERS_QUICK_BUY_FEATURES } from '../../../../Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/features'; | ||
| import type { QuickBuyTarget } from '../../../../Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/types'; | ||
| import { NATIVE_SWAPS_TOKEN_ADDRESS } from '../../../../../constants/bridge'; | ||
| import { MetaMetricsEvents } from '../../../../../core/Analytics'; | ||
| import { | ||
| useSocialLeaderboardAnalytics, | ||
| SocialLeaderboardEventProperties, | ||
| } from '../../../../Views/SocialLeaderboard/analytics'; | ||
|
|
||
| interface TrendingQuickBuyProps { | ||
| token: TrendingAsset | null; | ||
| onClose: () => void; | ||
| } | ||
|
|
||
| const TrendingQuickBuy: React.FC<TrendingQuickBuyProps> = ({ | ||
| token, | ||
| onClose, | ||
| }) => { | ||
| const { track } = useSocialLeaderboardAnalytics(); | ||
| const prevTokenRef = useRef<TrendingAsset | null>(null); | ||
|
|
||
| const target = useMemo((): QuickBuyTarget | null => { | ||
| if (!token) return null; | ||
| const [caipChainId, assetIdentifier] = token.assetId.split('/'); | ||
| if (!caipChainId) return null; | ||
| const isNative = assetIdentifier?.startsWith('slip44:'); | ||
| const tokenAddress = isNative | ||
| ? NATIVE_SWAPS_TOKEN_ADDRESS | ||
| : (assetIdentifier?.split(':')[1] ?? ''); | ||
| return { | ||
| chain: caipChainId as QuickBuyTarget['chain'], | ||
| tokenAddress, | ||
| tokenSymbol: token.symbol ?? '', | ||
| tokenName: token.name ?? '', | ||
| }; | ||
| }, [token]); | ||
|
|
||
| useEffect(() => { | ||
| if (token && !prevTokenRef.current) { | ||
| track(MetaMetricsEvents.SOCIAL_QUICK_BUY_SHEET_VIEWED, { | ||
| [SocialLeaderboardEventProperties.SOURCE]: 'explore_search', | ||
| [SocialLeaderboardEventProperties.CAIP19]: token.assetId, | ||
| ...(typeof token.marketCap === 'number' | ||
| ? { [SocialLeaderboardEventProperties.MARKET_CAP]: token.marketCap } | ||
| : {}), | ||
| }); | ||
| } | ||
| prevTokenRef.current = token; | ||
| }, [token, track]); | ||
|
|
||
| return ( | ||
| <QuickBuy.Root | ||
| isVisible={!!token} | ||
| target={target} | ||
| onClose={onClose} | ||
| features={TOP_TRADERS_QUICK_BUY_FEATURES} | ||
| analyticsContext={{ source: 'explore_search' }} | ||
| /> | ||
| ); | ||
| }; | ||
|
|
||
| export default TrendingQuickBuy; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.