From 640e1f9fce640435d450694e6f53cdc7b172deb8 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 2 Jan 2025 15:16:50 -0800 Subject: [PATCH 001/150] initial walletisland ux --- src/internal/components/Draggable.tsx | 2 +- src/wallet/components/WalletIsland.test.tsx | 66 ++++ src/wallet/components/WalletIsland.tsx | 128 ++++++++ .../WalletIslandAddressDetails.test.tsx | 151 +++++++++ .../components/WalletIslandAddressDetails.tsx | 89 ++++++ .../components/WalletIslandContent.test.tsx | 230 ++++++++++++++ .../components/WalletIslandDefault.test.tsx | 101 ++++++ src/wallet/components/WalletIslandDefault.tsx | 27 ++ .../components/WalletIslandProvider.test.tsx | 141 +++++++++ .../components/WalletIslandProvider.tsx | 138 +++++++++ .../components/WalletIslandQrReceive.test.tsx | 287 ++++++++++++++++++ .../components/WalletIslandQrReceive.tsx | 140 +++++++++ .../components/WalletIslandSwap.test.tsx | 264 ++++++++++++++++ src/wallet/components/WalletIslandSwap.tsx | 113 +++++++ .../WalletIslandTokenHoldings.test.tsx | 88 ++++++ .../components/WalletIslandTokenHoldings.tsx | 78 +++++ .../WalletIslandTransactionActions.test.tsx | 82 +++++ .../WalletIslandTransactionActions.tsx | 80 +++++ .../WalletIslandWalletActions.test.tsx | 137 +++++++++ .../components/WalletIslandWalletActions.tsx | 98 ++++++ 20 files changed, 2439 insertions(+), 1 deletion(-) create mode 100644 src/wallet/components/WalletIsland.test.tsx create mode 100644 src/wallet/components/WalletIsland.tsx create mode 100644 src/wallet/components/WalletIslandAddressDetails.test.tsx create mode 100644 src/wallet/components/WalletIslandAddressDetails.tsx create mode 100644 src/wallet/components/WalletIslandContent.test.tsx create mode 100644 src/wallet/components/WalletIslandDefault.test.tsx create mode 100644 src/wallet/components/WalletIslandDefault.tsx create mode 100644 src/wallet/components/WalletIslandProvider.test.tsx create mode 100644 src/wallet/components/WalletIslandProvider.tsx create mode 100644 src/wallet/components/WalletIslandQrReceive.test.tsx create mode 100644 src/wallet/components/WalletIslandQrReceive.tsx create mode 100644 src/wallet/components/WalletIslandSwap.test.tsx create mode 100644 src/wallet/components/WalletIslandSwap.tsx create mode 100644 src/wallet/components/WalletIslandTokenHoldings.test.tsx create mode 100644 src/wallet/components/WalletIslandTokenHoldings.tsx create mode 100644 src/wallet/components/WalletIslandTransactionActions.test.tsx create mode 100644 src/wallet/components/WalletIslandTransactionActions.tsx create mode 100644 src/wallet/components/WalletIslandWalletActions.test.tsx create mode 100644 src/wallet/components/WalletIslandWalletActions.tsx diff --git a/src/internal/components/Draggable.tsx b/src/internal/components/Draggable.tsx index 5f6c61711a..57414deedf 100644 --- a/src/internal/components/Draggable.tsx +++ b/src/internal/components/Draggable.tsx @@ -9,7 +9,7 @@ type DraggableProps = { snapToGrid?: boolean; }; -export default function Draggable({ +export function Draggable({ children, gridSize = 1, startingPosition = { x: 20, y: 20 }, diff --git a/src/wallet/components/WalletIsland.test.tsx b/src/wallet/components/WalletIsland.test.tsx new file mode 100644 index 0000000000..a302c725ab --- /dev/null +++ b/src/wallet/components/WalletIsland.test.tsx @@ -0,0 +1,66 @@ +import { render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { ConnectWallet } from './ConnectWallet'; +import { Wallet } from './Wallet'; +import { WalletIsland } from './WalletIsland'; +import { useWalletContext } from './WalletProvider'; + +vi.mock('../../../core-react/internal/hooks/useTheme', () => ({ + useTheme: vi.fn(), +})); + +vi.mock('../ConnectWallet', () => ({ + ConnectWallet: () =>
Connect Wallet
, +})); + +vi.mock('../WalletProvider', () => ({ + useWalletContext: vi.fn(), + WalletProvider: ({ children }) => <>{children}, +})); + +vi.mock('./WalletIslandContent', () => ({ + WalletIslandContent: ({ children }) => ( +
{children}
+ ), +})); + +describe('WalletIsland', () => { + const mockUseWalletContext = useWalletContext as ReturnType; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders connect-wallet when isOpen is false', () => { + mockUseWalletContext.mockReturnValue({ isOpen: false }); + + render( + + + +
Some content
+
+
, + ); + + expect(screen.getByTestId('connect-wallet')).toBeDefined(); + expect(screen.queryByTestId('wallet-island-content')).toBeNull(); + expect(screen.queryByTestId('child-content')).toBeNull(); + }); + + it('renders wallet-island-content when isOpen is true', () => { + mockUseWalletContext.mockReturnValue({ isOpen: true }); + + render( + + + +
Some content
+
+
, + ); + + expect(screen.getByTestId('wallet-island-content')).toBeDefined(); + expect(screen.getByTestId('child-content')).toBeDefined(); + }); +}); diff --git a/src/wallet/components/WalletIsland.tsx b/src/wallet/components/WalletIsland.tsx new file mode 100644 index 0000000000..a8c3639ca8 --- /dev/null +++ b/src/wallet/components/WalletIsland.tsx @@ -0,0 +1,128 @@ +import { useMemo } from 'react'; +import { useTheme } from '../../core-react/internal/hooks/useTheme'; +import { Draggable } from '@/internal/components/Draggable'; +import { background, border, cn, text } from '../../styles/theme'; +import type { WalletIslandProps } from '../types'; +import { useWalletIslandContext } from './WalletIslandProvider'; +import { WalletIslandQrReceive } from './WalletIslandQrReceive'; +import { WalletIslandSwap } from './WalletIslandSwap'; +import { useWalletContext } from './WalletProvider'; +import { WalletIslandProvider } from './WalletIslandProvider'; + +export function WalletIsland({ children }: WalletIslandProps) { + const { isOpen } = useWalletContext(); + + if (!isOpen) { + return null; + } + + return ( + + {children} + + ); +} + + + +const WALLET_ISLAND_WIDTH = 384; +const WALLET_ISLAND_HEIGHT = 394; + +export function WalletIslandContent({ children }: WalletIslandProps) { + const { containerRef } = useWalletContext(); + const { showQr, showSwap, tokenHoldings, animationClasses } = + useWalletIslandContext(); + const componentTheme = useTheme(); + + const position = useMemo(() => { + if (containerRef?.current) { + const rect = containerRef.current.getBoundingClientRect(); + const windowWidth = window.innerWidth; + const windowHeight = window.innerHeight; + + let xPos: number; + let yPos: number; + + if (windowWidth - rect.right < WALLET_ISLAND_WIDTH) { + xPos = rect.right - WALLET_ISLAND_WIDTH; + } else { + xPos = rect.left; + } + + if (windowHeight - rect.bottom < WALLET_ISLAND_HEIGHT) { + yPos = rect.bottom - WALLET_ISLAND_HEIGHT - rect.height - 10; + } else { + yPos = rect.bottom + 10; + } + + return { + x: xPos, + y: yPos, + }; + } + + return { + x: 20, + y: 20, + }; + }, [containerRef]); + + return ( + +
+
+ +
+
+ + Swap +
+ } + to={tokenHoldings.map((token) => token.token)} + from={tokenHoldings.map((token) => token.token)} + className="w-full p-2" + /> +
+
+ {children} +
+ +
+ ); +} diff --git a/src/wallet/components/WalletIslandAddressDetails.test.tsx b/src/wallet/components/WalletIslandAddressDetails.test.tsx new file mode 100644 index 0000000000..a49005b99c --- /dev/null +++ b/src/wallet/components/WalletIslandAddressDetails.test.tsx @@ -0,0 +1,151 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useIdentityContext } from '../../core-react/identity/providers/IdentityProvider'; +import { AddressDetails } from './WalletIslandAddressDetails'; +import { useWalletIslandContext } from './WalletIslandProvider'; +import { useWalletContext } from './WalletProvider'; + +vi.mock('../../../core-react/internal/hooks/useTheme', () => ({ + useTheme: vi.fn(), +})); + +vi.mock('../../../identity/components/IdentityProvider', () => ({ + useIdentityContext: vi.fn().mockReturnValue({ + schemaId: '1', + }), +})); + +vi.mock('../../../identity/hooks/useAttestations', () => ({ + useAttestations: () => [{ testAttestation: 'Test Attestation' }], +})); + +vi.mock('../../../identity/hooks/useAvatar', () => ({ + useAvatar: () => ({ data: null, isLoading: false }), +})); + +vi.mock('../../../identity/hooks/useName', () => ({ + useName: () => ({ data: null, isLoading: false }), +})); + +vi.mock('../WalletProvider', () => ({ + useWalletContext: vi.fn(), + WalletProvider: ({ children }) => <>{children}, +})); + +vi.mock('./WalletIslandProvider', () => ({ + useWalletIslandContext: vi.fn(), + WalletIslandProvider: ({ children }) => <>{children}, +})); + +describe('WalletIslandAddressDetails', () => { + const mockUseWalletContext = useWalletContext as ReturnType; + const mockUseIdentityContext = useIdentityContext as ReturnType; + const mockUseWalletIslandContext = useWalletIslandContext as ReturnType< + typeof vi.fn + >; + + const mockClipboard = { + writeText: vi.fn(), + }; + Object.assign(navigator, { clipboard: mockClipboard }); + + beforeEach(() => { + vi.clearAllMocks(); + mockUseWalletIslandContext.mockReturnValue({ + animationClasses: { + addressDetails: 'animate-walletIslandContainerItem2', + }, + }); + }); + + it('renders null when isClosing is true', () => { + mockUseWalletContext.mockReturnValue({ isClosing: true }); + + render(); + + expect(screen.queryByTestId('address-details')).toBeNull(); + }); + + it('renders Avatar, Badge, Name, and AddressBalance when isClosing is false', () => { + mockUseWalletContext.mockReturnValue({ + isClosing: false, + address: '0x1234567890', + chain: { id: 8453 }, + }); + + mockUseIdentityContext.mockReturnValue({ + schemaId: '1', + }); + + render(); + + expect(screen.getByTestId('ockAvatar_ImageContainer')).toBeDefined(); + expect(screen.getByTestId('ockAvatar_BadgeContainer')).toBeDefined(); + expect(screen.getByTestId('ockIdentity_Text')).toBeDefined(); + expect(screen.getByTestId('ockWalletIsland_AddressBalance')).toBeDefined(); + }); + + it('copies address to clipboard and shows tooltip when Name group is clicked', async () => { + mockUseWalletContext.mockReturnValue({ + isClosing: false, + address: '0x1234567890', + chain: { id: 8453 }, + }); + + render(); + + const nameButton = screen.getByTestId('ockWalletIsland_NameButton'); + const tooltip = screen.getByTestId('ockWalletIsland_NameTooltip'); + expect(tooltip).toHaveClass('opacity-0'); + expect(tooltip).toHaveClass('group-hover:opacity-100'); + + fireEvent.click(nameButton); + + await waitFor(() => { + expect(tooltip?.textContent).toBe('Copied'); + }); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith('0x1234567890'); + expect(screen.getByText('Copied')).toBeInTheDocument(); + }); + + it('shows error state when clipboard fails', async () => { + mockUseWalletContext.mockReturnValue({ + isClosing: false, + address: '0x1234567890', + chain: { id: 8453 }, + }); + + const mockClipboard = { + writeText: vi.fn().mockRejectedValue(new Error('Clipboard failed')), + }; + Object.assign(navigator, { clipboard: mockClipboard }); + + render(); + + const nameButton = screen.getByTestId('ockWalletIsland_NameButton'); + const tooltip = screen.getByTestId('ockWalletIsland_NameTooltip'); + + fireEvent.click(nameButton); + + await waitFor(() => { + expect(tooltip.textContent).toBe('Failed to copy'); + }); + }); + + it('copies empty string when address is null', () => { + mockUseWalletContext.mockReturnValue({ + isClosing: false, + address: null, + chain: { id: 8453 }, + }); + + render(); + + const nameButton = screen.getByTestId('ockWalletIsland_NameButton'); + + fireEvent.click(nameButton); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith(''); + }); +}); diff --git a/src/wallet/components/WalletIslandAddressDetails.tsx b/src/wallet/components/WalletIslandAddressDetails.tsx new file mode 100644 index 0000000000..45da7e12aa --- /dev/null +++ b/src/wallet/components/WalletIslandAddressDetails.tsx @@ -0,0 +1,89 @@ +import { useCallback, useState } from 'react'; +import type { Address, Chain } from 'viem'; +import { Avatar, Badge, Name } from '../../ui/react/identity'; +import { border, cn, color, pressable, text } from '../../styles/theme'; +import { useWalletIslandContext } from './WalletIslandProvider'; +import { useWalletContext } from './WalletProvider'; + +export function AddressDetails() { + const { address, chain, isClosing } = useWalletContext(); + const { animationClasses } = useWalletIslandContext(); + const [copyText, setCopyText] = useState('Copy'); + + const handleCopyAddress = useCallback(async () => { + try { + await navigator.clipboard.writeText(address ?? ''); + setCopyText('Copied'); + setTimeout(() => setCopyText('Copy'), 2000); + } catch (err) { + console.error('Failed to copy address:', err); + setCopyText('Failed to copy'); + setTimeout(() => setCopyText('Copy'), 2000); + } + }, [address]); + + if (isClosing || !chain) { + return null; + } + + return ( +
+
+ + + +
+
+ + +
+
+ +
+
+ ); +} + +type AddressBalanceProps = { + address?: Address | null; + chain?: Chain | null; +}; + +function AddressBalance({ address, chain }: AddressBalanceProps) { + const data = { address, chain }; // temp linter fix + console.log({ data }); // temp linter fix + return $690.42; +} diff --git a/src/wallet/components/WalletIslandContent.test.tsx b/src/wallet/components/WalletIslandContent.test.tsx new file mode 100644 index 0000000000..f2ef1074b5 --- /dev/null +++ b/src/wallet/components/WalletIslandContent.test.tsx @@ -0,0 +1,230 @@ +import { render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useWalletContext } from './WalletProvider'; +import { WalletIslandContent } from './WalletIslandContent'; +import { useWalletIslandContext } from './WalletIslandProvider'; + +vi.mock('../../../core-react/internal/hooks/useTheme', () => ({ + useTheme: vi.fn(), +})); + +vi.mock('../WalletProvider', () => ({ + useWalletContext: vi.fn(), + WalletProvider: ({ children }) => <>{children}, +})); + +vi.mock('./WalletIslandProvider', () => ({ + useWalletIslandContext: vi.fn(), + WalletIslandProvider: ({ children }) => <>{children}, +})); + +vi.mock('./WalletIslandQrReceive', () => ({ + WalletIslandQrReceive: () => ( +
WalletIslandQrReceive
+ ), +})); + +vi.mock('./WalletIslandSwap', () => ({ + WalletIslandSwap: () => ( +
WalletIslandSwap
+ ), +})); + +describe('WalletIslandContent', () => { + const mockUseWalletContext = useWalletContext as ReturnType; + const mockUseWalletIslandContext = useWalletIslandContext as ReturnType< + typeof vi.fn + >; + + const defaultMockUseWalletIslandContext = { + showSwap: false, + isSwapClosing: false, + showQr: false, + isQrClosing: false, + tokenHoldings: [], + animationClasses: { + content: 'animate-walletIslandContainerIn', + }, + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockUseWalletIslandContext.mockReturnValue( + defaultMockUseWalletIslandContext, + ); + }); + + it('renders WalletIslandContent when isClosing is false', () => { + mockUseWalletContext.mockReturnValue({ isClosing: false }); + + render(); + + expect(screen.getByTestId('ockWalletIslandContent')).toBeDefined(); + expect(screen.queryByTestId('ockWalletIslandContent')).toHaveClass( + 'animate-walletIslandContainerIn', + ); + expect( + screen.queryByTestId('ockWalletIslandQrReceive')?.parentElement, + ).toHaveClass('hidden'); + expect( + screen.queryByTestId('ockWalletIslandSwap')?.parentElement, + ).toHaveClass('hidden'); + }); + + it('closes WalletIslandContent when isClosing is true', () => { + mockUseWalletContext.mockReturnValue({ isClosing: true }); + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + animationClasses: { + content: 'animate-walletIslandContainerOut', + }, + }); + + render(); + + expect(screen.getByTestId('ockWalletIslandContent')).toBeDefined(); + expect(screen.queryByTestId('ockWalletIslandContent')).toHaveClass( + 'animate-walletIslandContainerOut', + ); + expect( + screen.queryByTestId('ockWalletIslandQrReceive')?.parentElement, + ).toHaveClass('hidden'); + expect( + screen.queryByTestId('ockWalletIslandSwap')?.parentElement, + ).toHaveClass('hidden'); + }); + + it('renders WalletIslandQrReceive when showQr is true', () => { + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + showQr: true, + }); + + render(); + + expect(screen.getByTestId('ockWalletIslandQrReceive')).toBeDefined(); + expect( + screen.queryByTestId('ockWalletIslandQrReceive')?.parentElement, + ).not.toHaveClass('hidden'); + expect( + screen.queryByTestId('ockWalletIslandSwap')?.parentElement, + ).toHaveClass('hidden'); + }); + + it('renders WalletIslandSwap when showSwap is true', () => { + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + showSwap: true, + }); + + render(); + + expect(screen.getByTestId('ockWalletIslandSwap')).toBeDefined(); + expect( + screen.queryByTestId('ockWalletIslandSwap')?.parentElement, + ).not.toHaveClass('hidden'); + expect( + screen.queryByTestId('ockWalletIslandQrReceive')?.parentElement, + ).toHaveClass('hidden'); + }); + + it('correctly positions WalletIslandContent when there is enough space on the right', () => { + const mockRect = { + left: 100, + right: 200, + bottom: 450, + height: 400, + }; + const mockRef = { current: { getBoundingClientRect: () => mockRect } }; + + window.innerWidth = 1000; + window.innerHeight = 1000; + + mockUseWalletContext.mockReturnValue({ + isClosing: false, + containerRef: mockRef, + }); + + render(); + + const draggable = screen.getByTestId('ockDraggable'); + expect(draggable).toHaveStyle({ + left: '100px', + }); + }); + + it('correctly positions WalletIslandContent when there is not enough space on the right', () => { + const mockRect = { + left: 300, + right: 400, + bottom: 450, + height: 400, + }; + const mockRef = { current: { getBoundingClientRect: () => mockRect } }; + + window.innerWidth = 550; + window.innerHeight = 1000; + + mockUseWalletContext.mockReturnValue({ + isClosing: false, + containerRef: mockRef, + }); + + render(); + + const draggable = screen.getByTestId('ockDraggable'); + expect(draggable).toHaveStyle({ + left: '16px', + }); + }); + + it('correctly positions WalletIslandContent when there is enough space on the bottom', () => { + const mockRect = { + left: 300, + right: 400, + bottom: 450, + height: 400, + }; + const mockRef = { current: { getBoundingClientRect: () => mockRect } }; + + window.innerWidth = 550; + window.innerHeight = 1000; + + mockUseWalletContext.mockReturnValue({ + isClosing: false, + containerRef: mockRef, + }); + + render(); + + const draggable = screen.getByTestId('ockDraggable'); + expect(draggable).toHaveStyle({ + top: '460px', + }); + }); + + it('correctly positions WalletIslandContent when there is not enough space on the bottom', () => { + const mockRect = { + left: 300, + right: 400, + bottom: 850, + height: 400, + }; + const mockRef = { current: { getBoundingClientRect: () => mockRect } }; + + window.innerWidth = 550; + window.innerHeight = 1000; + + mockUseWalletContext.mockReturnValue({ + isClosing: false, + containerRef: mockRef, + }); + + render(); + + const draggable = screen.getByTestId('ockDraggable'); + expect(draggable).toHaveStyle({ + top: '46px', + }); + }); +}); diff --git a/src/wallet/components/WalletIslandDefault.test.tsx b/src/wallet/components/WalletIslandDefault.test.tsx new file mode 100644 index 0000000000..1cd6edf890 --- /dev/null +++ b/src/wallet/components/WalletIslandDefault.test.tsx @@ -0,0 +1,101 @@ +import { render, screen } from '@testing-library/react'; +import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useAccount, useConnect } from 'wagmi'; +import { WalletIslandDefault } from './WalletIslandDefault'; +import { useWalletContext } from './WalletProvider'; + +vi.mock('wagmi', () => ({ + useAccount: vi.fn(), + useConnect: vi.fn(), + useConfig: vi.fn(), +})); + +vi.mock('../WalletProvider', () => ({ + useWalletContext: vi.fn(), + WalletProvider: ({ children }) => <>{children}, +})); + +vi.mock('../../../core-react/internal/hooks/useTheme', () => ({ + useTheme: vi.fn(), +})); + +vi.mock('./WalletIslandProvider', () => ({ + useWalletIslandContext: vi.fn(), + WalletIslandProvider: ({ children }) => <>{children}, +})); + +vi.mock('./WalletIslandContent', () => ({ + WalletIslandContent: () => ( +
WalletIslandContent
+ ), +})); + +vi.mock('../../../identity/hooks/useAvatar', () => ({ + useAvatar: () => ({ data: null, isLoading: false }), +})); + +vi.mock('../../../identity/hooks/useName', () => ({ + useName: () => ({ data: null, isLoading: false }), +})); + +describe('WalletIslandDefault', () => { + const mockUseWalletContext = useWalletContext as ReturnType; + + beforeEach(() => { + vi.clearAllMocks(); + (useConnect as ReturnType).mockReturnValue({ + connectors: [], + status: 'disconnected', + }); + (useAccount as ReturnType).mockReturnValue({ + status: 'disconnected', + address: '', + }); + (useWalletContext as Mock).mockReturnValue({ + isOpen: false, + }); + }); + + it('renders ConnectWallet in disconnected state', () => { + mockUseWalletContext.mockReturnValue({ isOpen: false }); + + render(); + + expect(screen.getByTestId('ockConnectWallet_Container')).toBeDefined(); + }); + + it('renders Avatar and Name in connected state and isOpen is false', () => { + (useConnect as ReturnType).mockReturnValue({ + connectors: [], + status: 'connected', + }); + (useAccount as ReturnType).mockReturnValue({ + status: 'connected', + address: '0x123', + }); + + mockUseWalletContext.mockReturnValue({ isOpen: false }); + + render(); + + expect(screen.getByTestId('ockAvatar_ImageContainer')).toBeDefined(); + expect(screen.getByTestId('ockIdentity_Text')).toBeDefined(); + }); + + it('renders WalletIslandContent in connected state and isOpen is true', () => { + (useConnect as ReturnType).mockReturnValue({ + connectors: [], + status: 'connected', + }); + (useAccount as ReturnType).mockReturnValue({ + status: 'connected', + address: '0x123', + }); + + mockUseWalletContext.mockReturnValue({ isOpen: true }); + + render(); + + expect(screen.getByTestId('ockWalletIslandContent')).toBeDefined(); + }); +}); diff --git a/src/wallet/components/WalletIslandDefault.tsx b/src/wallet/components/WalletIslandDefault.tsx new file mode 100644 index 0000000000..06b0a36721 --- /dev/null +++ b/src/wallet/components/WalletIslandDefault.tsx @@ -0,0 +1,27 @@ +import { Avatar, Name } from '../../ui/react/identity'; +import { ConnectWallet } from './ConnectWallet'; +import { ConnectWalletText } from './ConnectWalletText'; +import { Wallet } from './Wallet'; +import { WalletIsland } from './WalletIsland'; +import { AddressDetails } from './WalletIslandAddressDetails'; +import { WalletIslandTokenHoldings } from './WalletIslandTokenHoldings'; +import { WalletIslandTransactionActions } from './WalletIslandTransactionActions'; +import { WalletIslandWalletActions } from './WalletIslandWalletActions'; + +export function WalletIslandDefault() { + return ( + + + Connect Wallet + + + + + + + + + + + ); +} diff --git a/src/wallet/components/WalletIslandProvider.test.tsx b/src/wallet/components/WalletIslandProvider.test.tsx new file mode 100644 index 0000000000..8f95795590 --- /dev/null +++ b/src/wallet/components/WalletIslandProvider.test.tsx @@ -0,0 +1,141 @@ +import { act, renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useAccount } from 'wagmi'; +import { + WalletIslandProvider, + useWalletIslandContext, +} from './WalletIslandProvider'; +import { WalletProvider, useWalletContext } from './WalletProvider'; + +vi.mock('wagmi', () => ({ + useAccount: vi.fn(), +})); + +vi.mock('../WalletProvider', () => ({ + useWalletContext: vi.fn(), + WalletProvider: ({ children }) => <>{children}, +})); + +describe('useWalletIslandContext', () => { + const mockUseAccount = useAccount as ReturnType; + const mockUseWalletContext = useWalletContext as ReturnType; + const defaultWalletContext = { + address: '0x123', + isClosing: false, + }; + + beforeEach(() => { + mockUseAccount.mockReturnValue({ + address: '0x123', + }); + mockUseWalletContext.mockReturnValue(defaultWalletContext); + }); + + it('should provide wallet island context', () => { + const { result } = renderHook(() => useWalletIslandContext(), { + wrapper: WalletIslandProvider, + }); + + expect(result.current).toEqual({ + showSwap: false, + setShowSwap: expect.any(Function), + isSwapClosing: false, + setIsSwapClosing: expect.any(Function), + showQr: false, + setShowQr: expect.any(Function), + isQrClosing: false, + setIsQrClosing: expect.any(Function), + tokenHoldings: expect.any(Array), + animationClasses: { + content: expect.any(String), + qr: expect.any(String), + swap: expect.any(String), + walletActions: expect.any(String), + addressDetails: expect.any(String), + transactionActions: expect.any(String), + tokenHoldings: expect.any(String), + }, + setHasContentAnimated: expect.any(Function), + }); + }); + + describe('animation classes', () => { + it('should show slide out animations when QR is closing', async () => { + const { result } = renderHook(() => useWalletIslandContext(), { + wrapper: WalletIslandProvider, + }); + + await act(async () => { + result.current.setIsQrClosing(true); + }); + + expect(result.current.animationClasses).toEqual({ + content: '', + qr: 'animate-slideOutToLeft', + swap: 'animate-slideOutToLeft', + walletActions: 'animate-slideInFromRight', + addressDetails: 'animate-slideInFromRight', + transactionActions: 'animate-slideInFromRight', + tokenHoldings: 'animate-slideInFromRight', + }); + }); + + it('should show slide out animations when Swap is closing', async () => { + const { result } = renderHook(() => useWalletIslandContext(), { + wrapper: WalletIslandProvider, + }); + + await act(async () => { + result.current.setIsSwapClosing(true); + }); + + expect(result.current.animationClasses).toEqual({ + content: '', + qr: 'animate-slideOutToLeft', + swap: 'animate-slideOutToLeft', + walletActions: 'animate-slideInFromRight', + addressDetails: 'animate-slideInFromRight', + transactionActions: 'animate-slideInFromRight', + tokenHoldings: 'animate-slideInFromRight', + }); + }); + + it('should show wallet container out animation when closing', async () => { + mockUseAccount.mockReturnValue({ + address: '0x123', + }); + mockUseWalletContext.mockReturnValue({ + ...defaultWalletContext, + isClosing: true, + }); + + const { result } = renderHook(() => useWalletIslandContext(), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + expect(result.current.animationClasses.content).toBe( + 'animate-walletIslandContainerOut', + ); + }); + + it('should show default animations when not closing', () => { + const { result } = renderHook(() => useWalletIslandContext(), { + wrapper: WalletIslandProvider, + }); + + expect(result.current.animationClasses).toEqual({ + content: 'animate-walletIslandContainerIn', + qr: 'animate-slideInFromLeft', + swap: 'animate-slideInFromRight', + walletActions: 'animate-walletIslandContainerItem1', + addressDetails: 'animate-walletIslandContainerItem2', + transactionActions: 'animate-walletIslandContainerItem3', + tokenHoldings: 'animate-walletIslandContainerItem4', + }); + }); + }); +}); diff --git a/src/wallet/components/WalletIslandProvider.tsx b/src/wallet/components/WalletIslandProvider.tsx new file mode 100644 index 0000000000..3c9c8337f2 --- /dev/null +++ b/src/wallet/components/WalletIslandProvider.tsx @@ -0,0 +1,138 @@ +import { + type Dispatch, + type ReactNode, + type SetStateAction, + createContext, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { useValue } from '@/core-react/internal/hooks/useValue'; +import { getAddressTokenBalances } from '@/core-react/internal/utils/getAddressTokenBalances'; +import type { TokenBalanceWithFiatValue } from './WalletIslandTokenHoldings'; +import { useWalletContext } from './WalletProvider'; + +export type WalletIslandContextType = { + showSwap: boolean; + setShowSwap: Dispatch>; + isSwapClosing: boolean; + setIsSwapClosing: Dispatch>; + showQr: boolean; + setShowQr: Dispatch>; + isQrClosing: boolean; + setIsQrClosing: Dispatch>; + tokenHoldings: TokenBalanceWithFiatValue[]; + animationClasses: WalletIslandAnimations; + setHasContentAnimated: Dispatch>; +}; + +type WalletIslandAnimations = { + content: `animate-${string}` | ''; + qr: `animate-${string}`; + swap: `animate-${string}`; + walletActions: `animate-${string}`; + addressDetails: `animate-${string}`; + transactionActions: `animate-${string}`; + tokenHoldings: `animate-${string}`; +}; + +type WalletIslandProviderReact = { + children: ReactNode; +}; + +const WalletIslandContext = createContext( + {} as WalletIslandContextType, +); + +export function WalletIslandProvider({ children }: WalletIslandProviderReact) { + const { address, isClosing } = useWalletContext(); + const [showSwap, setShowSwap] = useState(false); + const [isSwapClosing, setIsSwapClosing] = useState(false); + const [showQr, setShowQr] = useState(false); + const [isQrClosing, setIsQrClosing] = useState(false); + const [hasContentAnimated, setHasContentAnimated] = useState(false); + const [tokenHoldings, setTokenHoldings] = useState< + TokenBalanceWithFiatValue[] + >([]); + + useEffect(() => { + if (isQrClosing || isSwapClosing) { + setHasContentAnimated(true); + } + }, [isQrClosing, isSwapClosing]); + + useEffect(() => { + if (isClosing) { + setHasContentAnimated(false); + } + }, [isClosing]); + + useEffect(() => { + async function fetchTokens() { + if (address) { + const fetchedTokens = await getAddressTokenBalances(address); + setTokenHoldings(fetchedTokens); + } + } + + fetchTokens(); + }, [address]); + + const animations = { + content: hasContentAnimated ? '' : 'animate-walletIslandContainerIn', + qr: 'animate-slideInFromLeft', + swap: 'animate-slideInFromRight', + walletActions: hasContentAnimated + ? 'opacity-100' + : 'animate-walletIslandContainerItem1', + addressDetails: hasContentAnimated + ? 'opacity-100' + : 'animate-walletIslandContainerItem2', + transactionActions: hasContentAnimated + ? 'opacity-100' + : 'animate-walletIslandContainerItem3', + tokenHoldings: hasContentAnimated + ? 'opacity-100' + : 'animate-walletIslandContainerItem4', + } as WalletIslandAnimations; + + const animationClasses = useMemo(() => { + if (isQrClosing || isSwapClosing) { + animations.content = ''; + animations.qr = 'animate-slideOutToLeft'; + animations.swap = 'animate-slideOutToLeft'; + animations.walletActions = 'animate-slideInFromRight'; + animations.addressDetails = 'animate-slideInFromRight'; + animations.transactionActions = 'animate-slideInFromRight'; + animations.tokenHoldings = 'animate-slideInFromRight'; + } else if (isClosing) { + animations.content = 'animate-walletIslandContainerOut'; + } + return animations; + }, [isClosing, isQrClosing, isSwapClosing, animations]); + + const value = useValue({ + showSwap, + setShowSwap, + isSwapClosing, + setIsSwapClosing, + showQr, + setShowQr, + isQrClosing, + setIsQrClosing, + tokenHoldings, + animationClasses, + setHasContentAnimated, + }); + + return ( + + {children} + + ); +} + +export function useWalletIslandContext() { + return useContext(WalletIslandContext); +} diff --git a/src/wallet/components/WalletIslandQrReceive.test.tsx b/src/wallet/components/WalletIslandQrReceive.test.tsx new file mode 100644 index 0000000000..29f87535c1 --- /dev/null +++ b/src/wallet/components/WalletIslandQrReceive.test.tsx @@ -0,0 +1,287 @@ +import { act, fireEvent, render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useWalletIslandContext } from './WalletIslandProvider'; +import { WalletIslandQrReceive } from './WalletIslandQrReceive'; +import { useWalletContext } from './WalletProvider'; + +vi.mock('../../../useTheme', () => ({ + useTheme: vi.fn(), +})); + +vi.mock('../WalletProvider', () => ({ + useWalletContext: vi.fn(), + WalletProvider: ({ children }) => <>{children}, +})); + +vi.mock('./WalletIslandProvider', () => ({ + useWalletIslandContext: vi.fn(), + WalletIslandProvider: ({ children }) => <>{children}, +})); + +const mockSetCopyText = vi.fn(); +const mockSetCopyButtonText = vi.fn(); + +vi.mock('react', async () => { + const actual = await vi.importActual('react'); + return { + ...actual, + useState: vi + .fn() + .mockImplementation((init) => [ + init, + init === 'Copy' ? mockSetCopyText : mockSetCopyButtonText, + ]), + }; +}); + +const mockClipboard = { + writeText: vi.fn().mockResolvedValue(undefined), +}; + +Object.defineProperty(navigator, 'clipboard', { + value: mockClipboard, + configurable: true, +}); + +describe('WalletIslandQrReceive', () => { + const mockUseWalletContext = useWalletContext as ReturnType; + const mockUseWalletIslandContext = useWalletIslandContext as ReturnType< + typeof vi.fn + >; + + const defaultMockUseWalletIslandContext = { + showQr: false, + setShowQr: vi.fn(), + animationClasses: { + qr: 'animate-slideInFromLeft', + }, + }; + + beforeEach(() => { + mockUseWalletContext.mockReturnValue({ + isOpen: true, + isClosing: false, + }); + mockUseWalletIslandContext.mockReturnValue( + defaultMockUseWalletIslandContext, + ); + mockSetCopyText.mockClear(); + mockSetCopyButtonText.mockClear(); + mockClipboard.writeText.mockReset(); + }); + + it('should render correctly based on isClosing state', () => { + mockUseWalletContext.mockReturnValue({ + isClosing: true, + }); + + const { rerender } = render(); + expect(screen.queryByTestId('ockWalletIslandQrReceive')).toBeNull(); + + mockUseWalletContext.mockReturnValue({ + isClosing: false, + }); + rerender(); + expect(screen.getByTestId('ockWalletIslandQrReceive')).toBeInTheDocument(); + }); + + it('should focus backButtonRef when showQr is true', () => { + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + showQr: true, + setShowQr: vi.fn(), + }); + + render(); + const backButton = screen.getByRole('button', { name: /back/i }); + expect(backButton).toHaveFocus(); + }); + + it('should close when the back button is clicked', () => { + vi.useFakeTimers(); + + const setShowQrMock = vi.fn(); + const setIsQrClosingMock = vi.fn(); + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + showQr: true, + setShowQr: setShowQrMock, + setIsQrClosing: setIsQrClosingMock, + }); + + render(); + const backButton = screen.getByRole('button', { name: /back/i }); + fireEvent.click(backButton); + expect(setIsQrClosingMock).toHaveBeenCalledWith(true); + + vi.advanceTimersByTime(200); + expect(setShowQrMock).toHaveBeenCalledWith(false); + + vi.advanceTimersByTime(200); + expect(setIsQrClosingMock).toHaveBeenCalledWith(false); + + vi.useRealTimers(); + }); + + it('should copy address when the copy icon is clicked', async () => { + vi.useFakeTimers(); + + mockClipboard.writeText.mockResolvedValueOnce(undefined); + + mockUseWalletContext.mockReturnValue({ + address: '0x1234567890', + }); + + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + showQr: true, + }); + + render(); + + const copyIcon = screen.getByRole('button', { name: /copy icon/i }); + + await act(async () => { + fireEvent.click(copyIcon); + await Promise.resolve(); + await vi.runAllTimersAsync(); + }); + + expect(mockClipboard.writeText).toHaveBeenCalledWith('0x1234567890'); + expect(mockSetCopyText).toHaveBeenCalledWith('Copied'); + + const tooltip = screen.getByRole('button', { name: /copy tooltip/i }); + expect(tooltip).toBeInTheDocument(); + + vi.advanceTimersByTime(2000); + expect(mockSetCopyText).toHaveBeenCalledWith('Copy'); + + expect(mockSetCopyButtonText.mock.calls.length).toBe(1); // setCopyButtonText is only called at initial render + + vi.useRealTimers(); + }); + + it('should copy address when the copy tooltip is clicked', async () => { + vi.useFakeTimers(); + + mockClipboard.writeText.mockResolvedValueOnce(undefined); + + mockUseWalletContext.mockReturnValue({ + address: '0x1234567890', + }); + + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + showQr: true, + }); + + render(); + + const copyTooltip = screen.getByRole('button', { name: /copy tooltip/i }); + + await act(async () => { + fireEvent.click(copyTooltip); + await Promise.resolve(); + await vi.runAllTimersAsync(); + }); + + expect(mockClipboard.writeText).toHaveBeenCalledWith('0x1234567890'); + expect(mockSetCopyText).toHaveBeenCalledWith('Copied'); + + const tooltip = screen.getByRole('button', { name: /copy tooltip/i }); + expect(tooltip).toBeInTheDocument(); + + vi.advanceTimersByTime(2000); + expect(mockSetCopyText).toHaveBeenCalledWith('Copy'); + + expect(mockSetCopyButtonText.mock.calls.length).toBe(1); // setCopyButtonText is only called at initial render + + vi.useRealTimers(); + }); + + it('should copy address when the copy button is clicked', async () => { + vi.useFakeTimers(); + + mockClipboard.writeText.mockResolvedValueOnce(undefined); + + mockUseWalletContext.mockReturnValue({ + address: '0x1234567890', + }); + + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + showQr: true, + }); + + render(); + + const copyButton = screen.getByRole('button', { name: /copy button/i }); + + await act(async () => { + fireEvent.click(copyButton); + await Promise.resolve(); + await vi.runAllTimersAsync(); + }); + + expect(mockClipboard.writeText).toHaveBeenCalledWith('0x1234567890'); + expect(mockSetCopyButtonText).toHaveBeenCalledWith('Address copied'); + + vi.advanceTimersByTime(2000); + expect(mockSetCopyButtonText).toHaveBeenCalledWith('Copy address'); + + expect(mockSetCopyText.mock.calls.length).toBe(1); // setCopyText is only called at initial render + + vi.useRealTimers(); + }); + + it('should handle clipboard errors gracefully', async () => { + vi.useFakeTimers(); + + mockClipboard.writeText.mockRejectedValue(new Error('Clipboard error')); + + mockUseWalletContext.mockReturnValue({ + address: '0x1234567890', + }); + + render(); + + mockSetCopyText.mockClear(); + const copyIcon = screen.getByRole('button', { name: /copy icon/i }); + await act(async () => { + fireEvent.click(copyIcon); + await Promise.resolve(); + await vi.runAllTimersAsync(); + }); + + expect(mockSetCopyText).toHaveBeenCalledWith('Failed to copy'); + + vi.advanceTimersByTime(2000); + + mockSetCopyButtonText.mockClear(); + const copyButton = screen.getByRole('button', { name: /copy button/i }); + await act(async () => { + fireEvent.click(copyButton); + await Promise.resolve(); + await vi.runAllTimersAsync(); + }); + + expect(mockSetCopyButtonText).toHaveBeenCalledWith( + 'Failed to copy address', + ); + + vi.useRealTimers(); + }); + + it('should copy empty string when address is empty', () => { + mockUseWalletContext.mockReturnValue({ + address: undefined, + }); + + render(); + + const copyIcon = screen.getByRole('button', { name: /copy icon/i }); + fireEvent.click(copyIcon); + + expect(mockClipboard.writeText).toHaveBeenCalledWith(''); + }); +}); diff --git a/src/wallet/components/WalletIslandQrReceive.tsx b/src/wallet/components/WalletIslandQrReceive.tsx new file mode 100644 index 0000000000..de150da2bb --- /dev/null +++ b/src/wallet/components/WalletIslandQrReceive.tsx @@ -0,0 +1,140 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { QRCodeComponent } from '@/internal/components/QrCode/QrCode'; +import { backArrowSvg } from '@/internal/svg/backArrowSvg'; +import { copySvg } from '@/internal/svg/copySvg'; +import { border, cn, color, pressable, text } from '@/styles/theme'; +import { useWalletIslandContext } from './WalletIslandProvider'; +import { useWalletContext } from './WalletProvider'; + +export function WalletIslandQrReceive() { + const { address, isClosing } = useWalletContext(); + const { showQr, setShowQr, setIsQrClosing, animationClasses } = + useWalletIslandContext(); + const backButtonRef = useRef(null); + const [copyText, setCopyText] = useState('Copy'); + const [copyButtonText, setCopyButtonText] = useState('Copy address'); + + useEffect(() => { + if (showQr) { + backButtonRef.current?.focus(); + } + }, [showQr]); + + const handleCloseQr = useCallback(() => { + setIsQrClosing(true); + + setTimeout(() => { + setShowQr(false); + }, 200); + + setTimeout(() => { + setIsQrClosing(false); + }, 400); + }, [setShowQr, setIsQrClosing]); + + const handleCopyAddress = useCallback( + async (element: 'button' | 'icon') => { + try { + await navigator.clipboard.writeText(address ?? ''); + if (element === 'button') { + setCopyButtonText('Address copied'); + } else { + setCopyText('Copied'); + } + setTimeout(() => { + setCopyText('Copy'); + setCopyButtonText('Copy address'); + }, 2000); + } catch (err) { + console.error('Failed to copy address:', err); + if (element === 'button') { + setCopyButtonText('Failed to copy address'); + } else { + setCopyText('Failed to copy'); + } + setTimeout(() => { + setCopyText('Copy'); + setCopyButtonText('Copy address'); + }, 2000); + } + }, + [address], + ); + + if (isClosing) { + return null; + } + + return ( +
+
+ + Scan to receive +
+ + +
+
+ + + + +
+ ); +} diff --git a/src/wallet/components/WalletIslandSwap.test.tsx b/src/wallet/components/WalletIslandSwap.test.tsx new file mode 100644 index 0000000000..ca7a5dc4a9 --- /dev/null +++ b/src/wallet/components/WalletIslandSwap.test.tsx @@ -0,0 +1,264 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useAccount } from 'wagmi'; +import { useSwapContext } from '@/swap/components/SwapProvider'; +import type { Token } from '@/token'; +import { useWalletIslandContext } from './WalletIslandProvider'; +import { WalletIslandSwap } from './WalletIslandSwap'; +import { useWalletContext } from './WalletProvider'; + +const tokens = [ + { + name: 'Ether', + address: '', + symbol: 'ETH', + decimals: 18, + image: + 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: 8453, + balance: 0.42, + valueInFiat: 1386, + }, + { + name: 'USD Coin', + address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', + symbol: 'USDC', + decimals: 6, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjMZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', + chainId: 8453, + balance: 69, + valueInFiat: 69, + }, +]; + +vi.mock('wagmi', () => ({ + useAccount: vi.fn(), +})); + +vi.mock(import('../../../swap'), async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + SwapAmountInput: () =>
, + SwapButton: () =>
, + SwapMessage: () =>
, + SwapSettings: ({ children }) => ( +
{children}
+ ), + SwapSettingsSlippageDescription: () => ( +
+ ), + SwapSettingsSlippageInput: () => ( +
+ ), + SwapSettingsSlippageTitle: () => ( +
+ ), + SwapToast: () =>
, + SwapToggleButton: () =>
, + }; +}); + +vi.mock('../../../swap/components/SwapProvider', () => ({ + useSwapContext: vi.fn(), + SwapProvider: ({ children }) => <>{children}, +})); + +vi.mock('../WalletProvider', () => ({ + useWalletContext: vi.fn(), +})); + +vi.mock('./WalletIslandProvider', () => ({ + useWalletIslandContext: vi.fn(), +})); + +describe('WalletIslandSwap', () => { + const mockUseWalletContext = useWalletContext as ReturnType; + const mockUseWalletIslandContext = useWalletIslandContext as ReturnType< + typeof vi.fn + >; + const mockUseSwapContext = useSwapContext as ReturnType; + const mockUseAccount = useAccount as ReturnType; + + const defaultMockUseWalletIslandContext = { + showSwap: false, + setShowSwap: vi.fn(), + setIsSwapClosing: vi.fn(), + animationClasses: { + swap: 'animate-slideInFromLeft', + }, + }; + + const defaultMockUseSwapContext = { + address: '0x123', + config: {}, + from: [tokens[0]], + to: [tokens[1]], + handleAmountChange: vi.fn(), + handleToggle: vi.fn(), + handleSubmit: vi.fn(), + lifecycleStatus: { + statusName: '', + statusData: { + isMissingRequiredField: false, + maxSlippage: 1, + }, + }, + updateLifecycleStatus: vi.fn(), + isToastVisible: false, + setIsToastVisible: vi.fn(), + setTransactionHash: vi.fn(), + transactionHash: null, + }; + + beforeEach(() => { + mockUseWalletContext.mockReturnValue({ + isOpen: true, + isClosing: false, + }); + mockUseWalletIslandContext.mockReturnValue( + defaultMockUseWalletIslandContext, + ); + mockUseSwapContext.mockReturnValue(defaultMockUseSwapContext); + mockUseAccount.mockReturnValue({ + address: '0x123', + chainId: 8453, + }); + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), + }); + }); + + it('should render correctly', () => { + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + showSwap: true, + }); + + render( + , + ); + expect(screen.getByTestId('ockWalletIslandSwap')).toBeInTheDocument(); + }); + + it('should focus swapDivRef when showSwap is true', () => { + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + showSwap: true, + }); + + render( + , + ); + const swapDiv = screen.getByTestId('ockWalletIslandSwap'); + expect(swapDiv).toHaveFocus(); + }); + + it('should close swap when back button is clicked', () => { + vi.useFakeTimers(); + + const mockSetShowSwap = vi.fn(); + const mockSetIsSwapClosing = vi.fn(); + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + showSwap: true, + tokenHoldings: [tokens], + setShowSwap: mockSetShowSwap, + setIsSwapClosing: mockSetIsSwapClosing, + }); + + render( + , + ); + + expect(screen.getByTestId('ockWalletIslandSwap')).toBeInTheDocument(); + + const backButton = screen.getByRole('button', { name: /back button/i }); + fireEvent.click(backButton); + expect(mockSetIsSwapClosing).toHaveBeenCalledWith(true); + + vi.advanceTimersByTime(200); + expect(mockSetShowSwap).toHaveBeenCalledWith(false); + + vi.advanceTimersByTime(200); + expect(mockSetIsSwapClosing).toHaveBeenCalledWith(false); + + vi.useRealTimers(); + }); + + it('should set tabIndex to -1 when showSwap is true', () => { + const mockSetShowSwap = vi.fn(); + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + showSwap: true, + }); + + const { rerender } = render( + , + ); + + expect(screen.getByTestId('ockWalletIslandSwap')).toHaveAttribute( + 'tabindex', + '-1', + ); + + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + showSwap: false, + setShowSwap: mockSetShowSwap, + }); + + rerender( + , + ); + expect(screen.getByTestId('ockWalletIslandSwap')).not.toHaveAttribute( + 'tabIndex', + ); + }); +}); diff --git a/src/wallet/components/WalletIslandSwap.tsx b/src/wallet/components/WalletIslandSwap.tsx new file mode 100644 index 0000000000..8708d031ff --- /dev/null +++ b/src/wallet/components/WalletIslandSwap.tsx @@ -0,0 +1,113 @@ +import { useCallback, useEffect, useRef } from 'react'; +import { backArrowSvg } from '@/internal/svg/backArrowSvg'; +import { border, cn, pressable } from '@/styles/theme'; +import { + Swap, + SwapAmountInput, + SwapButton, + SwapMessage, + SwapSettings, + SwapSettingsSlippageDescription, + SwapSettingsSlippageInput, + SwapSettingsSlippageTitle, + SwapToast, + SwapToggleButton, +} from '@/swap'; +import type { SwapDefaultReact } from '@/swap/types'; +import { useWalletIslandContext } from './WalletIslandProvider'; + +export function WalletIslandSwap({ + config, + className, + disabled, + experimental, + from, + isSponsored = false, + onError, + onStatus, + onSuccess, + title, + to, +}: SwapDefaultReact) { + const { showSwap, setShowSwap, setIsSwapClosing, animationClasses } = + useWalletIslandContext(); + const swapDivRef = useRef(null); + + const handleCloseSwap = useCallback(() => { + setIsSwapClosing(true); + + setTimeout(() => { + setShowSwap(false); + }, 200); + + setTimeout(() => { + setIsSwapClosing(false); + }, 400); + }, [setShowSwap, setIsSwapClosing]); + + useEffect(() => { + if (showSwap) { + swapDivRef.current?.focus(); + } + }, [showSwap]); + + return ( +
+ + + + Max. slippage + + Your swap will revert if the prices change by more than the selected + percentage. + + + + + + + + + + +
+ ); +} diff --git a/src/wallet/components/WalletIslandTokenHoldings.test.tsx b/src/wallet/components/WalletIslandTokenHoldings.test.tsx new file mode 100644 index 0000000000..8d06623f77 --- /dev/null +++ b/src/wallet/components/WalletIslandTokenHoldings.test.tsx @@ -0,0 +1,88 @@ +import { render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useWalletIslandContext } from './WalletIslandProvider'; +import { WalletIslandTokenHoldings } from './WalletIslandTokenHoldings'; + +vi.mock('./WalletIslandProvider', () => ({ + useWalletIslandContext: vi.fn(), + WalletIslandProvider: ({ children }) => <>{children}, +})); + +describe('WalletIslandTokenHoldings', () => { + const mockUseWalletIslandContext = useWalletIslandContext as ReturnType< + typeof vi.fn + >; + + const defaultMockUseWalletIslandContext = { + animationClasses: { + tokenHoldings: 'animate-walletIslandContainerItem4', + }, + tokenHoldings: [], + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockUseWalletIslandContext.mockReturnValue( + defaultMockUseWalletIslandContext, + ); + }); + + it('renders the WalletIslandTokenHoldings component with tokens', () => { + const tokens = [ + { + token: { + name: 'Ether', + address: '', + symbol: 'ETH', + decimals: 18, + image: + 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: 8453, + }, + balance: 0.42, + valueInFiat: 1386, + }, + { + token: { + name: 'USD Coin', + address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', + symbol: 'USDC', + decimals: 6, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', + chainId: 8453, + }, + balance: 69, + valueInFiat: 69, + }, + { + token: { + name: 'Ether', + address: '', + symbol: 'ETH', + decimals: 18, + image: + 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: 8453, + }, + balance: 0.42, + valueInFiat: 1386, + }, + ]; + + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + tokenHoldings: tokens, + }); + + render(); + + expect(screen.getByTestId('ockWalletIsland_TokenHoldings')).toBeDefined(); + }); + + it('does not render token lists with zero tokens', () => { + render(); + + expect(screen.queryByTestId('ockWalletIsland_TokenHoldings')).toBeNull(); + }); +}); diff --git a/src/wallet/components/WalletIslandTokenHoldings.tsx b/src/wallet/components/WalletIslandTokenHoldings.tsx new file mode 100644 index 0000000000..61174109ab --- /dev/null +++ b/src/wallet/components/WalletIslandTokenHoldings.tsx @@ -0,0 +1,78 @@ +import { cn, color, text } from '../../styles/theme'; +import { type Token, TokenImage } from '../../token'; +import { useWalletIslandContext } from './WalletIslandProvider'; + +export type TokenBalanceWithFiatValue = { + token: Token; + /** Token: + * address: Address | ""; + * chainId: number; + * decimals: number; + * image: string | null; + * name: string; + * symbol: string; + */ + balance: number; + valueInFiat: number; +}; + +// TODO: handle loading state +export function WalletIslandTokenHoldings() { + const { animationClasses, tokenHoldings } = useWalletIslandContext(); + + if (tokenHoldings.length === 0) { + return null; + } + + return ( +
+ {tokenHoldings.map((tokenBalance, index) => ( + + ))} +
+ ); +} + +type TokenDetailsProps = { + token: Token; + balance: number; + valueInFiat: number; +}; + +function TokenDetails({ token, balance, valueInFiat }: TokenDetailsProps) { + const currencySymbol = '$'; // TODO: get from user settings + + return ( +
+
+ +
+ + {token.name} + + + {`${balance} ${token.symbol}`} + +
+
+ + {`${currencySymbol}${valueInFiat}`} + +
+ ); +} diff --git a/src/wallet/components/WalletIslandTransactionActions.test.tsx b/src/wallet/components/WalletIslandTransactionActions.test.tsx new file mode 100644 index 0000000000..8dde826173 --- /dev/null +++ b/src/wallet/components/WalletIslandTransactionActions.test.tsx @@ -0,0 +1,82 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useWalletIslandContext } from './WalletIslandProvider'; +import { WalletIslandTransactionActions } from './WalletIslandTransactionActions'; + +vi.mock('./WalletIslandProvider', () => ({ + useWalletIslandContext: vi.fn(), + WalletIslandProvider: ({ children }) => <>{children}, +})); + +describe('WalletIslandTransactionActons', () => { + const mockUseWalletIslandContext = useWalletIslandContext as ReturnType< + typeof vi.fn + >; + + const defaultMockUseWalletIslandContext = { + setShowSwap: vi.fn(), + animationClasses: { + transactionActions: 'animate-walletIslandContainerItem3', + }, + }; + + beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(window, 'open').mockImplementation(() => null); + mockUseWalletIslandContext.mockReturnValue( + defaultMockUseWalletIslandContext, + ); + }); + + it('renders the WalletIslandTransactionActions component', () => { + render(); + + const buyButton = screen.getByRole('button', { name: 'Buy' }); + const sendButton = screen.getByRole('button', { name: 'Send' }); + const swapButton = screen.getByRole('button', { name: 'Swap' }); + + expect(buyButton).toBeDefined(); + expect(sendButton).toBeDefined(); + expect(swapButton).toBeDefined(); + }); + + it('opens the buy page when the buy button is clicked', () => { + render(); + + const buyButton = screen.getByRole('button', { name: 'Buy' }); + fireEvent.click(buyButton); + + expect(window.open).toHaveBeenCalledWith( + 'https://pay.coinbase.com', + '_blank', + ); + }); + + it('opens the send page when the send button is clicked', () => { + render(); + + const sendButton = screen.getByRole('button', { name: 'Send' }); + fireEvent.click(sendButton); + + expect(window.open).toHaveBeenCalledWith( + 'https://wallet.coinbase.com', + '_blank', + ); + }); + + it('sets showSwap to true when the swap button is clicked', () => { + const setShowSwapMock = vi.fn(); + + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + setShowSwap: setShowSwapMock, + }); + + render(); + + const swapButton = screen.getByRole('button', { name: 'Swap' }); + fireEvent.click(swapButton); + + expect(setShowSwapMock).toHaveBeenCalledWith(true); + }); +}); diff --git a/src/wallet/components/WalletIslandTransactionActions.tsx b/src/wallet/components/WalletIslandTransactionActions.tsx new file mode 100644 index 0000000000..7b79f5a53a --- /dev/null +++ b/src/wallet/components/WalletIslandTransactionActions.tsx @@ -0,0 +1,80 @@ +import { addSvgForeground } from '@/internal/svg/addForegroundSvg'; +import { arrowUpRightSvg } from '@/internal/svg/arrowUpRightSvg'; +import { toggleSvg } from '@/internal/svg/toggleSvg'; +import { border, cn, color, pressable, text } from '@/styles/theme'; +import { useWalletIslandContext } from './WalletIslandProvider'; + +type TransactionActionProps = { + icon: React.ReactNode; + label: string; + action: () => void; +}; + +export function WalletIslandTransactionActions() { + const { setShowSwap, animationClasses } = useWalletIslandContext(); + + return ( +
+ { + window.open('https://pay.coinbase.com', '_blank'); + }} + /> + { + window.open('https://wallet.coinbase.com', '_blank'); + }} + /> + { + setShowSwap(true); + }} + /> +
+ ); +} + +function WalletIslandTransactionAction({ + icon, + label, + action, +}: TransactionActionProps) { + return ( + + ); +} diff --git a/src/wallet/components/WalletIslandWalletActions.test.tsx b/src/wallet/components/WalletIslandWalletActions.test.tsx new file mode 100644 index 0000000000..96d2849a0a --- /dev/null +++ b/src/wallet/components/WalletIslandWalletActions.test.tsx @@ -0,0 +1,137 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useDisconnect } from 'wagmi'; +import { useWalletIslandContext } from './WalletIslandProvider'; +import { WalletIslandWalletActions } from './WalletIslandWalletActions'; +import { useWalletContext } from './WalletProvider'; + +vi.mock('wagmi', () => ({ + useDisconnect: vi.fn(), +})); + +vi.mock('wagmi/actions', () => ({ + disconnect: vi.fn(), +})); + +vi.mock('../WalletProvider', () => ({ + useWalletContext: vi.fn(), + WalletProvider: ({ children }) => <>{children}, +})); + +vi.mock('./WalletIslandProvider', () => ({ + useWalletIslandContext: vi.fn(), + WalletIslandProvider: ({ children }) => <>{children}, +})); + +describe('WalletIslandWalletActions', () => { + const mockUseWalletContext = useWalletContext as ReturnType; + const mockUseWalletIslandContext = useWalletIslandContext as ReturnType< + typeof vi.fn + >; + + const defaultMockUseWalletIslandContext = { + animationClasses: { + walletActions: 'animate-walletIslandContainerItem1', + }, + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockUseWalletIslandContext.mockReturnValue( + defaultMockUseWalletIslandContext, + ); + }); + + it('renders the WalletIslandWalletActions component', () => { + const handleCloseMock = vi.fn(); + mockUseWalletContext.mockReturnValue({ handleClose: handleCloseMock }); + + const setShowQrMock = vi.fn(); + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + setShowQr: setShowQrMock, + }); + + (useDisconnect as Mock).mockReturnValue({ + disconnect: vi.fn(), + connectors: [], + }); + + render(); + + expect( + screen.getByTestId('ockWalletIsland_TransactionsButton'), + ).toBeDefined(); + expect(screen.getByTestId('ockWalletIsland_QrButton')).toBeDefined(); + expect( + screen.getByTestId('ockWalletIsland_DisconnectButton'), + ).toBeDefined(); + expect(screen.getByTestId('ockWalletIsland_CollapseButton')).toBeDefined(); + }); + + it('disconnects connectors and closes when disconnect button is clicked', () => { + const handleCloseMock = vi.fn(); + mockUseWalletContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + handleClose: handleCloseMock, + }); + + const setShowQrMock = vi.fn(); + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + setShowQr: setShowQrMock, + }); + + const disconnectMock = vi.fn(); + (useDisconnect as Mock).mockReturnValue({ + disconnect: disconnectMock, + connectors: [{ id: 'mock-connector' }], + }); + + render(); + + const disconnectButton = screen.getByTestId( + 'ockWalletIsland_DisconnectButton', + ); + fireEvent.click(disconnectButton); + + expect(disconnectMock).toHaveBeenCalled(); + expect(handleCloseMock).toHaveBeenCalled(); + }); + + it('sets showQr to true when qr button is clicked', () => { + const setShowQrMock = vi.fn(); + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + setShowQr: setShowQrMock, + }); + + render(); + + const qrButton = screen.getByTestId('ockWalletIsland_QrButton'); + fireEvent.click(qrButton); + + expect(setShowQrMock).toHaveBeenCalled(); + }); + + it('closes when collapse button is clicked', () => { + const handleCloseMock = vi.fn(); + mockUseWalletContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + handleClose: handleCloseMock, + }); + + const setShowQrMock = vi.fn(); + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + setShowQr: setShowQrMock, + }); + + render(); + + const collapseButton = screen.getByTestId('ockWalletIsland_CollapseButton'); + fireEvent.click(collapseButton); + + expect(handleCloseMock).toHaveBeenCalled(); + }); +}); diff --git a/src/wallet/components/WalletIslandWalletActions.tsx b/src/wallet/components/WalletIslandWalletActions.tsx new file mode 100644 index 0000000000..ee6b8f1b19 --- /dev/null +++ b/src/wallet/components/WalletIslandWalletActions.tsx @@ -0,0 +1,98 @@ +import { useCallback } from 'react'; +import { useDisconnect } from 'wagmi'; +import { clockSvg } from '@/internal/svg/clockSvg'; +import { collapseSvg } from '@/internal/svg/collapseSvg'; +import { disconnectSvg } from '@/internal/svg/disconnectSvg'; +import { qrIconSvg } from '@/internal/svg/qrIconSvg'; +import { border, cn, pressable } from '../../styles/theme'; +import { useWalletContext } from './WalletProvider'; +import { useWalletIslandContext } from './WalletIslandProvider'; + +export function WalletIslandWalletActions() { + const { handleClose } = useWalletContext(); + const { setShowQr, animationClasses } = useWalletIslandContext(); + const { disconnect, connectors } = useDisconnect(); + + const handleDisconnect = useCallback(() => { + handleClose(); + for (const connector of connectors) { + disconnect({ connector }); + } + }, [disconnect, connectors, handleClose]); + + const handleQr = useCallback(() => { + setShowQr(true); + }, [setShowQr]); + + const handleCollapse = useCallback(() => { + handleClose(); + }, [handleClose]); + + return ( +
+
+ + {clockSvg} + + +
+
+ + +
+
+ ); +} From 2d381be377c3a97e9468527a69092f1ba5c0d641 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 2 Jan 2025 15:27:42 -0800 Subject: [PATCH 002/150] svgs and util placeholder --- .../utils/getAddressTokenBalances.test.tsx | 130 +++++++++++++++++ .../utils/getAddressTokenBalances.tsx | 135 ++++++++++++++++++ src/internal/svg/addForegroundSvg.tsx | 20 +++ src/internal/svg/arrowUpRightSvg.tsx | 17 +++ src/internal/svg/backArrowSvg.tsx | 17 +++ src/internal/svg/clockSvg.tsx | 18 +++ src/internal/svg/collapseSvg.tsx | 22 +++ src/internal/svg/copySvg.tsx | 20 +++ src/internal/svg/qrIconSvg.tsx | 35 +++++ 9 files changed, 414 insertions(+) create mode 100644 src/core-react/internal/utils/getAddressTokenBalances.test.tsx create mode 100644 src/core-react/internal/utils/getAddressTokenBalances.tsx create mode 100644 src/internal/svg/addForegroundSvg.tsx create mode 100644 src/internal/svg/arrowUpRightSvg.tsx create mode 100644 src/internal/svg/backArrowSvg.tsx create mode 100644 src/internal/svg/clockSvg.tsx create mode 100644 src/internal/svg/collapseSvg.tsx create mode 100644 src/internal/svg/copySvg.tsx create mode 100644 src/internal/svg/qrIconSvg.tsx diff --git a/src/core-react/internal/utils/getAddressTokenBalances.test.tsx b/src/core-react/internal/utils/getAddressTokenBalances.test.tsx new file mode 100644 index 0000000000..ba14a4e3a6 --- /dev/null +++ b/src/core-react/internal/utils/getAddressTokenBalances.test.tsx @@ -0,0 +1,130 @@ +import { describe, expect, it } from 'vitest'; +import type { TokenBalanceWithFiatValue } from '../../wallet/components/island/WalletIslandTokenHoldings'; +import { getAddressTokenBalances } from './getAddressTokenBalances'; + +describe('getAddressTokenBalances', () => { + it('should return an empty array for an invalid address', async () => { + const result = await getAddressTokenBalances('invalid-address'); + expect(result).toEqual([]); + }); + + it('should return an empty array for a null address', async () => { + const result = await getAddressTokenBalances(null); + expect(result).toEqual([]); + }); + + it('should return an array of token balances for a valid address', async () => { + const tokenBalances: TokenBalanceWithFiatValue[] = [ + { + token: { + name: 'Ether', + address: '', + symbol: 'ETH', + decimals: 18, + image: + 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: 8453, + }, + balance: 0.42, + valueInFiat: 1386, + }, + { + token: { + name: 'USD Coin', + address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', + symbol: 'USDC', + decimals: 6, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', + chainId: 8453, + }, + balance: 69, + valueInFiat: 69, + }, + { + token: { + name: 'Ether', + address: '', + symbol: 'ETH', + decimals: 18, + image: + 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: 8453, + }, + balance: 0.42, + valueInFiat: 1386, + }, + { + token: { + name: 'USD Coin', + address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', + symbol: 'USDC', + decimals: 6, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', + chainId: 8453, + }, + balance: 69, + valueInFiat: 69, + }, + { + token: { + name: 'Ether', + address: '', + symbol: 'ETH', + decimals: 18, + image: + 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: 8453, + }, + balance: 0.42, + valueInFiat: 1386, + }, + { + token: { + name: 'USD Coin', + address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', + symbol: 'USDC', + decimals: 6, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', + chainId: 8453, + }, + balance: 69, + valueInFiat: 69, + }, + { + token: { + name: 'Ether', + address: '', + symbol: 'ETH', + decimals: 18, + image: + 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: 8453, + }, + balance: 0.42, + valueInFiat: 1386, + }, + { + token: { + name: 'USD Coin', + address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', + symbol: 'USDC', + decimals: 6, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', + chainId: 8453, + }, + balance: 69, + valueInFiat: 69, + }, + ]; + const result = await getAddressTokenBalances( + '0x0000000000000000000000000000000000000000', + ); + expect(result).toEqual( + tokenBalances.sort((a, b) => b.valueInFiat - a.valueInFiat), + ); + }); +}); diff --git a/src/core-react/internal/utils/getAddressTokenBalances.tsx b/src/core-react/internal/utils/getAddressTokenBalances.tsx new file mode 100644 index 0000000000..9deae4b635 --- /dev/null +++ b/src/core-react/internal/utils/getAddressTokenBalances.tsx @@ -0,0 +1,135 @@ +// import type { Address } from 'viem'; +// import { base } from 'viem/chains'; +// import { sendRequest } from '../../network/request'; +import type { Token } from '@/token'; + +export type TokenBalanceWithFiatValue = { + token: Token; + /** Token: + * address: Address | ""; + * chainId: number; + * decimals: number; + * image: string | null; + * name: string; + * symbol: string; + */ + balance: number; + valueInFiat: number; +}; + +export async function getAddressTokenBalances( + address: `0x${string}`, +): Promise { + if (!address || address.slice(0, 2) !== '0x' || address.length !== 42) { + return []; + } + + const tokenBalances: TokenBalanceWithFiatValue[] = [ + { + token: { + name: 'Ether', + address: '', + symbol: 'ETH', + decimals: 18, + image: + 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: 8453, + }, + balance: 0.42, + valueInFiat: 1386, + }, + { + token: { + name: 'USD Coin', + address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', + symbol: 'USDC', + decimals: 6, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', + chainId: 8453, + }, + balance: 69, + valueInFiat: 69, + }, + { + token: { + name: 'Ether', + address: '', + symbol: 'ETH', + decimals: 18, + image: + 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: 8453, + }, + balance: 0.42, + valueInFiat: 1386, + }, + { + token: { + name: 'USD Coin', + address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', + symbol: 'USDC', + decimals: 6, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', + chainId: 8453, + }, + balance: 69, + valueInFiat: 69, + }, + { + token: { + name: 'Ether', + address: '', + symbol: 'ETH', + decimals: 18, + image: + 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: 8453, + }, + balance: 0.42, + valueInFiat: 1386, + }, + { + token: { + name: 'USD Coin', + address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', + symbol: 'USDC', + decimals: 6, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', + chainId: 8453, + }, + balance: 69, + valueInFiat: 69, + }, + { + token: { + name: 'Ether', + address: '', + symbol: 'ETH', + decimals: 18, + image: + 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: 8453, + }, + balance: 0.42, + valueInFiat: 1386, + }, + { + token: { + name: 'USD Coin', + address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', + symbol: 'USDC', + decimals: 6, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', + chainId: 8453, + }, + balance: 69, + valueInFiat: 69, + }, + ]; + + return tokenBalances.sort((a, b) => b.valueInFiat - a.valueInFiat); +} diff --git a/src/internal/svg/addForegroundSvg.tsx b/src/internal/svg/addForegroundSvg.tsx new file mode 100644 index 0000000000..0b8ed9d309 --- /dev/null +++ b/src/internal/svg/addForegroundSvg.tsx @@ -0,0 +1,20 @@ +import { icon } from '../../styles/theme'; + +export const addSvgForeground = ( + + Add + + +); diff --git a/src/internal/svg/arrowUpRightSvg.tsx b/src/internal/svg/arrowUpRightSvg.tsx new file mode 100644 index 0000000000..5e2de94a3a --- /dev/null +++ b/src/internal/svg/arrowUpRightSvg.tsx @@ -0,0 +1,17 @@ +import { icon } from '../../styles/theme'; + +export const arrowUpRightSvg = ( + + Arrow Up Right + + +); diff --git a/src/internal/svg/backArrowSvg.tsx b/src/internal/svg/backArrowSvg.tsx new file mode 100644 index 0000000000..45cfa71102 --- /dev/null +++ b/src/internal/svg/backArrowSvg.tsx @@ -0,0 +1,17 @@ +import { icon } from '../../styles/theme'; + +export const backArrowSvg = ( + + Back Arrow + + +); diff --git a/src/internal/svg/clockSvg.tsx b/src/internal/svg/clockSvg.tsx new file mode 100644 index 0000000000..3bc6b21740 --- /dev/null +++ b/src/internal/svg/clockSvg.tsx @@ -0,0 +1,18 @@ +import { background, icon } from '../../styles/theme'; + +export const clockSvg = ( + + Clock Icon + + + +); diff --git a/src/internal/svg/collapseSvg.tsx b/src/internal/svg/collapseSvg.tsx new file mode 100644 index 0000000000..e8bd964f17 --- /dev/null +++ b/src/internal/svg/collapseSvg.tsx @@ -0,0 +1,22 @@ +import { background, icon } from '../../styles/theme'; + +export const collapseSvg = ( + + Collapse + + + + +); diff --git a/src/internal/svg/copySvg.tsx b/src/internal/svg/copySvg.tsx new file mode 100644 index 0000000000..e029a521fe --- /dev/null +++ b/src/internal/svg/copySvg.tsx @@ -0,0 +1,20 @@ +import { icon } from '../../styles/theme'; + +export const copySvg = ( + + Copy Icon + + + +); diff --git a/src/internal/svg/qrIconSvg.tsx b/src/internal/svg/qrIconSvg.tsx new file mode 100644 index 0000000000..0a969c8b1e --- /dev/null +++ b/src/internal/svg/qrIconSvg.tsx @@ -0,0 +1,35 @@ +import { background, icon } from '../../styles/theme'; + +export const qrIconSvg = ( + + QR Code Icon + + + + + + + + + + + +); From 0603d86de9b530eb8a4847ad8249abbcf2a51a30 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 2 Jan 2025 15:27:53 -0800 Subject: [PATCH 003/150] fix: imports --- .../components/WalletIslandQrReceive.tsx | 4 ++-- .../components/WalletIslandSwap.test.tsx | 2 +- .../components/WalletIslandTokenHoldings.tsx | 18 ++---------------- .../components/WalletIslandWalletActions.tsx | 2 +- 4 files changed, 6 insertions(+), 20 deletions(-) diff --git a/src/wallet/components/WalletIslandQrReceive.tsx b/src/wallet/components/WalletIslandQrReceive.tsx index de150da2bb..0c6457ffc9 100644 --- a/src/wallet/components/WalletIslandQrReceive.tsx +++ b/src/wallet/components/WalletIslandQrReceive.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import { QRCodeComponent } from '@/internal/components/QrCode/QrCode'; +import { QrCodeSvg } from '@/internal/components/QrCode/QrCodeSvg'; import { backArrowSvg } from '@/internal/svg/backArrowSvg'; import { copySvg } from '@/internal/svg/copySvg'; import { border, cn, color, pressable, text } from '@/styles/theme'; @@ -125,7 +125,7 @@ export function WalletIslandQrReceive() {
- +
- + + ); } From 7be24c7524923da6b75decdc8169195daa2e5f9a Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 2 Jan 2025 17:01:26 -0800 Subject: [PATCH 010/150] temp animations --- src/wallet/components/WalletIslandAddressDetails.tsx | 6 +++--- src/wallet/components/WalletIslandTokenHoldings.tsx | 4 ++-- src/wallet/components/WalletIslandTransactionActions.tsx | 4 ++-- src/wallet/components/WalletIslandWalletActions.tsx | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/wallet/components/WalletIslandAddressDetails.tsx b/src/wallet/components/WalletIslandAddressDetails.tsx index 45da7e12aa..0378b7b6f5 100644 --- a/src/wallet/components/WalletIslandAddressDetails.tsx +++ b/src/wallet/components/WalletIslandAddressDetails.tsx @@ -23,7 +23,7 @@ export function AddressDetails() { }, [address]); if (isClosing || !chain) { - return null; + return
; } return ( @@ -32,8 +32,8 @@ export function AddressDetails() { 'mt-2 flex flex-col items-center justify-center', color.foreground, text.body, - 'opacity-0', - animationClasses.addressDetails, + // 'opacity-0', + // animationClasses.addressDetails, )} >
diff --git a/src/wallet/components/WalletIslandTokenHoldings.tsx b/src/wallet/components/WalletIslandTokenHoldings.tsx index b9bc221061..b324988cc0 100644 --- a/src/wallet/components/WalletIslandTokenHoldings.tsx +++ b/src/wallet/components/WalletIslandTokenHoldings.tsx @@ -16,8 +16,8 @@ export function WalletIslandTokenHoldings() { 'max-h-44 overflow-y-auto', 'flex w-full flex-col items-center gap-4', 'mt-2 mb-2 px-2', - 'opacity-0', - animationClasses.tokenHoldings, + // 'opacity-0', + // animationClasses.tokenHoldings, 'shadow-[inset_0_-15px_10px_-10px_rgba(0,0,0,0.05)]', )} data-testid="ockWalletIsland_TokenHoldings" diff --git a/src/wallet/components/WalletIslandTransactionActions.tsx b/src/wallet/components/WalletIslandTransactionActions.tsx index 7b79f5a53a..2d585737ec 100644 --- a/src/wallet/components/WalletIslandTransactionActions.tsx +++ b/src/wallet/components/WalletIslandTransactionActions.tsx @@ -17,8 +17,8 @@ export function WalletIslandTransactionActions() {
@@ -77,7 +77,7 @@ export function WalletIslandWalletActions() { 'flex items-center justify-center p-2', )} > - {disconnectSvg} +
{disconnectSvg}
+ + ); +} diff --git a/src/wallet/components/WalletIslandProvider.tsx b/src/wallet/components/WalletIslandProvider.tsx index c805f9e451..f8a69f7fcd 100644 --- a/src/wallet/components/WalletIslandProvider.tsx +++ b/src/wallet/components/WalletIslandProvider.tsx @@ -136,5 +136,9 @@ export function WalletIslandProvider({ children }: WalletIslandProviderReact) { } export function useWalletIslandContext() { - return useContext(WalletIslandContext); + const walletIslandContext = useContext(WalletIslandContext); + if (!walletIslandContext) { + throw new Error('useWalletIslandContext must be used within a WalletIslandProvider'); + } + return walletIslandContext; } From 06b33bfeafb4937d570f3d7b21d8ea17ef647713 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 3 Jan 2025 11:17:31 -0800 Subject: [PATCH 013/150] qr animations --- src/wallet/components/WalletIslandQrReceive.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/wallet/components/WalletIslandQrReceive.tsx b/src/wallet/components/WalletIslandQrReceive.tsx index 1c5e6c9cb9..d4de05615e 100644 --- a/src/wallet/components/WalletIslandQrReceive.tsx +++ b/src/wallet/components/WalletIslandQrReceive.tsx @@ -8,7 +8,7 @@ import { useWalletContext } from './WalletProvider'; export function WalletIslandQrReceive() { const { address, isClosing } = useWalletContext(); - const { showQr, setShowQr, setIsQrClosing, animationClasses } = + const { showQr, setShowQr, isQrClosing, setIsQrClosing } = useWalletIslandContext(); const backButtonRef = useRef(null); const [copyText, setCopyText] = useState('Copy'); @@ -73,7 +73,9 @@ export function WalletIslandQrReceive() { text.headline, 'flex flex-col items-center justify-center gap-12', 'w-full', - animationClasses.qr, + isQrClosing + ? 'fade-out slide-out-to-left-5 animate-out fill-mode-forwards ease-in-out' + : 'fade-in slide-in-from-right-5 animate-in duration-150 ease-out', )} >
From bf8eca7d7b8a539aebda355cea042c2520f8e25d Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 3 Jan 2025 11:24:53 -0800 Subject: [PATCH 014/150] update animations --- .../components/WalletIslandProvider.tsx | 67 ++----------------- src/wallet/components/WalletIslandSwap.tsx | 9 ++- 2 files changed, 11 insertions(+), 65 deletions(-) diff --git a/src/wallet/components/WalletIslandProvider.tsx b/src/wallet/components/WalletIslandProvider.tsx index f8a69f7fcd..91a137f32a 100644 --- a/src/wallet/components/WalletIslandProvider.tsx +++ b/src/wallet/components/WalletIslandProvider.tsx @@ -10,7 +10,6 @@ import { createContext, useContext, useEffect, - useMemo, useState, } from 'react'; import { useWalletContext } from './WalletProvider'; @@ -25,18 +24,6 @@ export type WalletIslandContextType = { isQrClosing: boolean; setIsQrClosing: Dispatch>; tokenHoldings: TokenBalanceWithFiatValue[]; - animationClasses: WalletIslandAnimations; - setHasContentAnimated: Dispatch>; -}; - -type WalletIslandAnimations = { - content: `animate-${string}` | ''; - qr: `animate-${string}`; - swap: `animate-${string}`; - walletActions: `animate-${string}`; - addressDetails: `animate-${string}`; - transactionActions: `animate-${string}`; - tokenHoldings: `animate-${string}`; }; type WalletIslandProviderReact = { @@ -48,28 +35,15 @@ const WalletIslandContext = createContext( ); export function WalletIslandProvider({ children }: WalletIslandProviderReact) { - const { address, isClosing } = useWalletContext(); + const { address } = useWalletContext(); const [showSwap, setShowSwap] = useState(false); const [isSwapClosing, setIsSwapClosing] = useState(false); const [showQr, setShowQr] = useState(false); const [isQrClosing, setIsQrClosing] = useState(false); - const [hasContentAnimated, setHasContentAnimated] = useState(false); const [tokenHoldings, setTokenHoldings] = useState< TokenBalanceWithFiatValue[] >([]); - useEffect(() => { - if (isQrClosing || isSwapClosing) { - setHasContentAnimated(true); - } - }, [isQrClosing, isSwapClosing]); - - useEffect(() => { - if (isClosing) { - setHasContentAnimated(false); - } - }, [isClosing]); - useEffect(() => { async function fetchTokens() { if (address) { @@ -81,39 +55,6 @@ export function WalletIslandProvider({ children }: WalletIslandProviderReact) { fetchTokens(); }, [address]); - const animations = { - content: hasContentAnimated ? '' : 'animate-walletIslandContainerIn', - qr: 'animate-slideInFromLeft', - swap: 'animate-slideInFromRight', - walletActions: hasContentAnimated - ? 'opacity-100' - : 'animate-walletIslandContainerItem1', - addressDetails: hasContentAnimated - ? 'opacity-100' - : 'animate-walletIslandContainerItem2', - transactionActions: hasContentAnimated - ? 'opacity-100' - : 'animate-walletIslandContainerItem3', - tokenHoldings: hasContentAnimated - ? 'opacity-100' - : 'animate-walletIslandContainerItem4', - } as WalletIslandAnimations; - - const animationClasses = useMemo(() => { - if (isQrClosing || isSwapClosing) { - animations.content = ''; - animations.qr = 'animate-slideOutToLeft'; - animations.swap = 'animate-slideOutToLeft'; - animations.walletActions = 'animate-slideInFromRight'; - animations.addressDetails = 'animate-slideInFromRight'; - animations.transactionActions = 'animate-slideInFromRight'; - animations.tokenHoldings = 'animate-slideInFromRight'; - } else if (isClosing) { - animations.content = 'animate-walletIslandContainerOut'; - } - return animations; - }, [isClosing, isQrClosing, isSwapClosing, animations]); - const value = useValue({ showSwap, setShowSwap, @@ -124,8 +65,6 @@ export function WalletIslandProvider({ children }: WalletIslandProviderReact) { isQrClosing, setIsQrClosing, tokenHoldings, - animationClasses, - setHasContentAnimated, }); return ( @@ -138,7 +77,9 @@ export function WalletIslandProvider({ children }: WalletIslandProviderReact) { export function useWalletIslandContext() { const walletIslandContext = useContext(WalletIslandContext); if (!walletIslandContext) { - throw new Error('useWalletIslandContext must be used within a WalletIslandProvider'); + throw new Error( + 'useWalletIslandContext must be used within a WalletIslandProvider', + ); } return walletIslandContext; } diff --git a/src/wallet/components/WalletIslandSwap.tsx b/src/wallet/components/WalletIslandSwap.tsx index 9bdd5433a7..0bf84e90ad 100644 --- a/src/wallet/components/WalletIslandSwap.tsx +++ b/src/wallet/components/WalletIslandSwap.tsx @@ -29,7 +29,7 @@ export function WalletIslandSwap({ title, to, }: SwapDefaultReact) { - const { showSwap, setShowSwap, setIsSwapClosing, animationClasses } = + const { showSwap, setShowSwap, isSwapClosing, setIsSwapClosing } = useWalletIslandContext(); const swapDivRef = useRef(null); @@ -55,7 +55,12 @@ export function WalletIslandSwap({
Scan to receive
@@ -106,7 +106,7 @@ export function WalletIslandQrReceive() { )} aria-label="Copy icon" > -
{copySvg}
+
{copySvg}
+ ); + return (
- Max. slippage From a01056ba4e89e0c3578eb4764b33e29bc0b00fb6 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 3 Jan 2025 12:03:52 -0800 Subject: [PATCH 016/150] content block animations --- src/wallet/components/WalletIslandAddressDetails.tsx | 8 ++++---- src/wallet/components/WalletIslandTokenHoldings.tsx | 9 ++++++--- src/wallet/components/WalletIslandTransactionActions.tsx | 9 ++++++--- src/wallet/components/WalletIslandWalletActions.tsx | 9 +++++---- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/wallet/components/WalletIslandAddressDetails.tsx b/src/wallet/components/WalletIslandAddressDetails.tsx index 20b45410f8..71e9590e15 100644 --- a/src/wallet/components/WalletIslandAddressDetails.tsx +++ b/src/wallet/components/WalletIslandAddressDetails.tsx @@ -2,12 +2,10 @@ import { border, cn, color, pressable, text } from '@/styles/theme'; import { Avatar, Badge, Name } from '@/ui/react/identity'; import { useCallback, useState } from 'react'; import type { Address, Chain } from 'viem'; -// import { useWalletIslandContext } from './WalletIslandProvider'; import { useWalletContext } from './WalletProvider'; export function AddressDetails() { const { address, chain, isClosing } = useWalletContext(); - // const { animationClasses } = useWalletIslandContext(); const [copyText, setCopyText] = useState('Copy'); const handleCopyAddress = useCallback(async () => { @@ -32,8 +30,10 @@ export function AddressDetails() { 'mt-2 flex flex-col items-center justify-center', color.foreground, text.body, - // 'opacity-0', - // animationClasses.addressDetails, + { + 'fade-in slide-in-from-top-2.5 animate-in fill-mode-forwards duration-300 ease-out': + !isClosing, + }, )} >
diff --git a/src/wallet/components/WalletIslandTokenHoldings.tsx b/src/wallet/components/WalletIslandTokenHoldings.tsx index 62106ba70e..c50726a664 100644 --- a/src/wallet/components/WalletIslandTokenHoldings.tsx +++ b/src/wallet/components/WalletIslandTokenHoldings.tsx @@ -1,10 +1,11 @@ import { cn, color, text } from '@/styles/theme'; import { type Token, TokenImage } from '@/token'; import { useWalletIslandContext } from './WalletIslandProvider'; +import { useWalletContext } from './WalletProvider'; // TODO: handle loading state export function WalletIslandTokenHoldings() { - // const { animationClasses, tokenHoldings } = useWalletIslandContext(); + const { isClosing } = useWalletContext(); const { tokenHoldings } = useWalletIslandContext(); if (tokenHoldings.length === 0) { @@ -17,8 +18,10 @@ export function WalletIslandTokenHoldings() { 'max-h-44 overflow-y-auto', 'flex w-full flex-col items-center gap-4', 'mt-2 mb-2 px-2', - // 'opacity-0', - // animationClasses.tokenHoldings, + { + 'fade-in slide-in-from-top-2.5 animate-in fill-mode-forwards duration-300 ease-out': + !isClosing, + }, 'shadow-[inset_0_-15px_10px_-10px_rgba(0,0,0,0.05)]', )} data-testid="ockWalletIsland_TokenHoldings" diff --git a/src/wallet/components/WalletIslandTransactionActions.tsx b/src/wallet/components/WalletIslandTransactionActions.tsx index 817f2aa108..ae1434d20a 100644 --- a/src/wallet/components/WalletIslandTransactionActions.tsx +++ b/src/wallet/components/WalletIslandTransactionActions.tsx @@ -3,6 +3,7 @@ import { arrowUpRightSvg } from '@/internal/svg/arrowUpRightSvg'; import { toggleSvg } from '@/internal/svg/toggleSvg'; import { border, cn, color, pressable, text } from '@/styles/theme'; import { useWalletIslandContext } from './WalletIslandProvider'; +import { useWalletContext } from '@/wallet/components/WalletProvider'; type TransactionActionProps = { icon: React.ReactNode; @@ -11,15 +12,17 @@ type TransactionActionProps = { }; export function WalletIslandTransactionActions() { - // const { setShowSwap, animationClasses } = useWalletIslandContext(); + const { isClosing } = useWalletContext(); const { setShowSwap } = useWalletIslandContext(); return (
From 8f8f80cb4b7b40d2c3e9f9c660a0dbe7ff8181d6 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 3 Jan 2025 12:05:25 -0800 Subject: [PATCH 017/150] fix: linters --- src/wallet/components/WalletIsland.tsx | 2 +- .../components/WalletIslandTransactionActions.tsx | 13 +++++-------- src/wallet/components/WalletIslandWalletActions.tsx | 11 ++++------- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/wallet/components/WalletIsland.tsx b/src/wallet/components/WalletIsland.tsx index 561bd67d07..fe63b15416 100644 --- a/src/wallet/components/WalletIsland.tsx +++ b/src/wallet/components/WalletIsland.tsx @@ -1,6 +1,6 @@ import type { WalletIslandProps } from '../types'; -import { WalletIslandProvider } from './WalletIslandProvider'; import { WalletIslandContent } from './WalletIslandContent'; +import { WalletIslandProvider } from './WalletIslandProvider'; export function WalletIsland({ children, diff --git a/src/wallet/components/WalletIslandTransactionActions.tsx b/src/wallet/components/WalletIslandTransactionActions.tsx index ae1434d20a..e823d97b5f 100644 --- a/src/wallet/components/WalletIslandTransactionActions.tsx +++ b/src/wallet/components/WalletIslandTransactionActions.tsx @@ -3,7 +3,7 @@ import { arrowUpRightSvg } from '@/internal/svg/arrowUpRightSvg'; import { toggleSvg } from '@/internal/svg/toggleSvg'; import { border, cn, color, pressable, text } from '@/styles/theme'; import { useWalletIslandContext } from './WalletIslandProvider'; -import { useWalletContext } from '@/wallet/components/WalletProvider'; +import { useWalletContext } from './WalletProvider'; type TransactionActionProps = { icon: React.ReactNode; @@ -17,13 +17,10 @@ export function WalletIslandTransactionActions() { return (
Date: Fri, 3 Jan 2025 12:10:56 -0800 Subject: [PATCH 018/150] fix: test: --- src/internal/components/Draggable.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internal/components/Draggable.test.tsx b/src/internal/components/Draggable.test.tsx index 97ffa9ab07..4a09e99178 100644 --- a/src/internal/components/Draggable.test.tsx +++ b/src/internal/components/Draggable.test.tsx @@ -1,7 +1,7 @@ import { fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import Draggable from './Draggable'; +import { Draggable } from './Draggable'; describe('Draggable', () => { beforeEach(() => { From 84c4b9de81c63616f7f07baefef5ffb82dcdf2f6 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 3 Jan 2025 12:52:27 -0800 Subject: [PATCH 019/150] fix: tests --- src/wallet/components/WalletIsland.test.tsx | 14 +-- .../WalletIslandAddressDetails.test.tsx | 20 ++-- .../components/WalletIslandContent.test.tsx | 39 +++----- .../components/WalletIslandDefault.test.tsx | 21 ++-- .../components/WalletIslandProvider.test.tsx | 96 +------------------ .../components/WalletIslandQrReceive.test.tsx | 15 +-- 6 files changed, 53 insertions(+), 152 deletions(-) diff --git a/src/wallet/components/WalletIsland.test.tsx b/src/wallet/components/WalletIsland.test.tsx index a302c725ab..ba225040f1 100644 --- a/src/wallet/components/WalletIsland.test.tsx +++ b/src/wallet/components/WalletIsland.test.tsx @@ -5,25 +5,25 @@ import { Wallet } from './Wallet'; import { WalletIsland } from './WalletIsland'; import { useWalletContext } from './WalletProvider'; -vi.mock('../../../core-react/internal/hooks/useTheme', () => ({ +vi.mock('../../core-react/internal/hooks/useTheme', () => ({ useTheme: vi.fn(), })); -vi.mock('../ConnectWallet', () => ({ +vi.mock('./ConnectWallet', () => ({ ConnectWallet: () =>
Connect Wallet
, })); -vi.mock('../WalletProvider', () => ({ - useWalletContext: vi.fn(), - WalletProvider: ({ children }) => <>{children}, -})); - vi.mock('./WalletIslandContent', () => ({ WalletIslandContent: ({ children }) => (
{children}
), })); +vi.mock('./WalletProvider', () => ({ + useWalletContext: vi.fn(), + WalletProvider: ({ children }) => <>{children}, +})); + describe('WalletIsland', () => { const mockUseWalletContext = useWalletContext as ReturnType; diff --git a/src/wallet/components/WalletIslandAddressDetails.test.tsx b/src/wallet/components/WalletIslandAddressDetails.test.tsx index a49005b99c..c86c3843e4 100644 --- a/src/wallet/components/WalletIslandAddressDetails.test.tsx +++ b/src/wallet/components/WalletIslandAddressDetails.test.tsx @@ -5,38 +5,38 @@ import { AddressDetails } from './WalletIslandAddressDetails'; import { useWalletIslandContext } from './WalletIslandProvider'; import { useWalletContext } from './WalletProvider'; -vi.mock('../../../core-react/internal/hooks/useTheme', () => ({ +vi.mock('../../core-react/internal/hooks/useTheme', () => ({ useTheme: vi.fn(), })); -vi.mock('../../../identity/components/IdentityProvider', () => ({ +vi.mock('../../core-react/identity/providers/IdentityProvider', () => ({ useIdentityContext: vi.fn().mockReturnValue({ schemaId: '1', }), })); -vi.mock('../../../identity/hooks/useAttestations', () => ({ +vi.mock('../../core-react/identity/hooks/useAttestations', () => ({ useAttestations: () => [{ testAttestation: 'Test Attestation' }], })); -vi.mock('../../../identity/hooks/useAvatar', () => ({ +vi.mock('../../core-react/identity/hooks/useAvatar', () => ({ useAvatar: () => ({ data: null, isLoading: false }), })); -vi.mock('../../../identity/hooks/useName', () => ({ +vi.mock('../../core-react/identity/hooks/useName', () => ({ useName: () => ({ data: null, isLoading: false }), })); -vi.mock('../WalletProvider', () => ({ - useWalletContext: vi.fn(), - WalletProvider: ({ children }) => <>{children}, -})); - vi.mock('./WalletIslandProvider', () => ({ useWalletIslandContext: vi.fn(), WalletIslandProvider: ({ children }) => <>{children}, })); +vi.mock('./WalletProvider', () => ({ + useWalletContext: vi.fn(), + WalletProvider: ({ children }) => <>{children}, +})); + describe('WalletIslandAddressDetails', () => { const mockUseWalletContext = useWalletContext as ReturnType; const mockUseIdentityContext = useIdentityContext as ReturnType; diff --git a/src/wallet/components/WalletIslandContent.test.tsx b/src/wallet/components/WalletIslandContent.test.tsx index 3b70ac3e52..24cc07801a 100644 --- a/src/wallet/components/WalletIslandContent.test.tsx +++ b/src/wallet/components/WalletIslandContent.test.tsx @@ -4,15 +4,10 @@ import { WalletIslandContent } from './WalletIslandContent'; import { useWalletIslandContext } from './WalletIslandProvider'; import { useWalletContext } from './WalletProvider'; -vi.mock('../../../core-react/internal/hooks/useTheme', () => ({ +vi.mock('../../core-react/internal/hooks/useTheme', () => ({ useTheme: vi.fn(), })); -vi.mock('../WalletProvider', () => ({ - useWalletContext: vi.fn(), - WalletProvider: ({ children }) => <>{children}, -})); - vi.mock('./WalletIslandProvider', () => ({ useWalletIslandContext: vi.fn(), WalletIslandProvider: ({ children }) => <>{children}, @@ -30,6 +25,11 @@ vi.mock('./WalletIslandSwap', () => ({ ), })); +vi.mock('./WalletProvider', () => ({ + useWalletContext: vi.fn(), + WalletProvider: ({ children }) => <>{children}, +})); + describe('WalletIslandContent', () => { const mockUseWalletContext = useWalletContext as ReturnType; const mockUseWalletIslandContext = useWalletIslandContext as ReturnType< @@ -42,9 +42,6 @@ describe('WalletIslandContent', () => { showQr: false, isQrClosing: false, tokenHoldings: [], - animationClasses: { - content: 'animate-walletIslandContainerIn', - }, }; beforeEach(() => { @@ -61,7 +58,7 @@ describe('WalletIslandContent', () => { expect(screen.getByTestId('ockWalletIslandContent')).toBeDefined(); expect(screen.queryByTestId('ockWalletIslandContent')).toHaveClass( - 'animate-walletIslandContainerIn', + 'fade-in slide-in-from-top-1.5 animate-in duration-300 ease-out', ); expect( screen.queryByTestId('ockWalletIslandQrReceive')?.parentElement, @@ -73,18 +70,12 @@ describe('WalletIslandContent', () => { it('closes WalletIslandContent when isClosing is true', () => { mockUseWalletContext.mockReturnValue({ isClosing: true }); - mockUseWalletIslandContext.mockReturnValue({ - ...defaultMockUseWalletIslandContext, - animationClasses: { - content: 'animate-walletIslandContainerOut', - }, - }); render(); expect(screen.getByTestId('ockWalletIslandContent')).toBeDefined(); expect(screen.queryByTestId('ockWalletIslandContent')).toHaveClass( - 'animate-walletIslandContainerOut', + 'fade-out slide-out-to-top-1.5 animate-out fill-mode-forwards ease-in-out', ); expect( screen.queryByTestId('ockWalletIslandQrReceive')?.parentElement, @@ -142,10 +133,9 @@ describe('WalletIslandContent', () => { mockUseWalletContext.mockReturnValue({ isClosing: false, - containerRef: mockRef, }); - render(); + render(); const draggable = screen.getByTestId('ockDraggable'); expect(draggable).toHaveStyle({ @@ -167,14 +157,13 @@ describe('WalletIslandContent', () => { mockUseWalletContext.mockReturnValue({ isClosing: false, - containerRef: mockRef, }); - render(); + render(); const draggable = screen.getByTestId('ockDraggable'); expect(draggable).toHaveStyle({ - left: '16px', + left: '48px', }); }); @@ -192,10 +181,9 @@ describe('WalletIslandContent', () => { mockUseWalletContext.mockReturnValue({ isClosing: false, - containerRef: mockRef, }); - render(); + render(); const draggable = screen.getByTestId('ockDraggable'); expect(draggable).toHaveStyle({ @@ -217,10 +205,9 @@ describe('WalletIslandContent', () => { mockUseWalletContext.mockReturnValue({ isClosing: false, - containerRef: mockRef, }); - render(); + render(); const draggable = screen.getByTestId('ockDraggable'); expect(draggable).toHaveStyle({ diff --git a/src/wallet/components/WalletIslandDefault.test.tsx b/src/wallet/components/WalletIslandDefault.test.tsx index 1cd6edf890..5c13602429 100644 --- a/src/wallet/components/WalletIslandDefault.test.tsx +++ b/src/wallet/components/WalletIslandDefault.test.tsx @@ -10,13 +10,16 @@ vi.mock('wagmi', () => ({ useConfig: vi.fn(), })); -vi.mock('../WalletProvider', () => ({ - useWalletContext: vi.fn(), - WalletProvider: ({ children }) => <>{children}, +vi.mock('../../core-react/internal/hooks/useTheme', () => ({ + useTheme: vi.fn(), })); -vi.mock('../../../core-react/internal/hooks/useTheme', () => ({ - useTheme: vi.fn(), +vi.mock('../../core-react/identity/hooks/useAvatar', () => ({ + useAvatar: () => ({ data: null, isLoading: false }), +})); + +vi.mock('../../core-react/identity/hooks/useName', () => ({ + useName: () => ({ data: null, isLoading: false }), })); vi.mock('./WalletIslandProvider', () => ({ @@ -30,12 +33,10 @@ vi.mock('./WalletIslandContent', () => ({ ), })); -vi.mock('../../../identity/hooks/useAvatar', () => ({ - useAvatar: () => ({ data: null, isLoading: false }), -})); -vi.mock('../../../identity/hooks/useName', () => ({ - useName: () => ({ data: null, isLoading: false }), +vi.mock('./WalletProvider', () => ({ + useWalletContext: vi.fn(), + WalletProvider: ({ children }) => <>{children}, })); describe('WalletIslandDefault', () => { diff --git a/src/wallet/components/WalletIslandProvider.test.tsx b/src/wallet/components/WalletIslandProvider.test.tsx index 8f95795590..75028cdccf 100644 --- a/src/wallet/components/WalletIslandProvider.test.tsx +++ b/src/wallet/components/WalletIslandProvider.test.tsx @@ -1,17 +1,17 @@ -import { act, renderHook } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useAccount } from 'wagmi'; import { WalletIslandProvider, useWalletIslandContext, } from './WalletIslandProvider'; -import { WalletProvider, useWalletContext } from './WalletProvider'; +import { useWalletContext } from './WalletProvider'; vi.mock('wagmi', () => ({ useAccount: vi.fn(), })); -vi.mock('../WalletProvider', () => ({ +vi.mock('./WalletProvider', () => ({ useWalletContext: vi.fn(), WalletProvider: ({ children }) => <>{children}, })); @@ -46,96 +46,6 @@ describe('useWalletIslandContext', () => { isQrClosing: false, setIsQrClosing: expect.any(Function), tokenHoldings: expect.any(Array), - animationClasses: { - content: expect.any(String), - qr: expect.any(String), - swap: expect.any(String), - walletActions: expect.any(String), - addressDetails: expect.any(String), - transactionActions: expect.any(String), - tokenHoldings: expect.any(String), - }, - setHasContentAnimated: expect.any(Function), - }); - }); - - describe('animation classes', () => { - it('should show slide out animations when QR is closing', async () => { - const { result } = renderHook(() => useWalletIslandContext(), { - wrapper: WalletIslandProvider, - }); - - await act(async () => { - result.current.setIsQrClosing(true); - }); - - expect(result.current.animationClasses).toEqual({ - content: '', - qr: 'animate-slideOutToLeft', - swap: 'animate-slideOutToLeft', - walletActions: 'animate-slideInFromRight', - addressDetails: 'animate-slideInFromRight', - transactionActions: 'animate-slideInFromRight', - tokenHoldings: 'animate-slideInFromRight', - }); - }); - - it('should show slide out animations when Swap is closing', async () => { - const { result } = renderHook(() => useWalletIslandContext(), { - wrapper: WalletIslandProvider, - }); - - await act(async () => { - result.current.setIsSwapClosing(true); - }); - - expect(result.current.animationClasses).toEqual({ - content: '', - qr: 'animate-slideOutToLeft', - swap: 'animate-slideOutToLeft', - walletActions: 'animate-slideInFromRight', - addressDetails: 'animate-slideInFromRight', - transactionActions: 'animate-slideInFromRight', - tokenHoldings: 'animate-slideInFromRight', - }); - }); - - it('should show wallet container out animation when closing', async () => { - mockUseAccount.mockReturnValue({ - address: '0x123', - }); - mockUseWalletContext.mockReturnValue({ - ...defaultWalletContext, - isClosing: true, - }); - - const { result } = renderHook(() => useWalletIslandContext(), { - wrapper: ({ children }) => ( - - {children} - - ), - }); - - expect(result.current.animationClasses.content).toBe( - 'animate-walletIslandContainerOut', - ); - }); - - it('should show default animations when not closing', () => { - const { result } = renderHook(() => useWalletIslandContext(), { - wrapper: WalletIslandProvider, - }); - - expect(result.current.animationClasses).toEqual({ - content: 'animate-walletIslandContainerIn', - qr: 'animate-slideInFromLeft', - swap: 'animate-slideInFromRight', - walletActions: 'animate-walletIslandContainerItem1', - addressDetails: 'animate-walletIslandContainerItem2', - transactionActions: 'animate-walletIslandContainerItem3', - tokenHoldings: 'animate-walletIslandContainerItem4', - }); }); }); }); diff --git a/src/wallet/components/WalletIslandQrReceive.test.tsx b/src/wallet/components/WalletIslandQrReceive.test.tsx index 29f87535c1..a3539bdc14 100644 --- a/src/wallet/components/WalletIslandQrReceive.test.tsx +++ b/src/wallet/components/WalletIslandQrReceive.test.tsx @@ -1,23 +1,24 @@ +import { useTheme } from '@/core-react/internal/hooks/useTheme'; import { act, fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useWalletIslandContext } from './WalletIslandProvider'; import { WalletIslandQrReceive } from './WalletIslandQrReceive'; import { useWalletContext } from './WalletProvider'; -vi.mock('../../../useTheme', () => ({ +vi.mock('../../core-react/internal/hooks/useTheme', () => ({ useTheme: vi.fn(), })); -vi.mock('../WalletProvider', () => ({ - useWalletContext: vi.fn(), - WalletProvider: ({ children }) => <>{children}, -})); - vi.mock('./WalletIslandProvider', () => ({ useWalletIslandContext: vi.fn(), WalletIslandProvider: ({ children }) => <>{children}, })); +vi.mock('./WalletProvider', () => ({ + useWalletContext: vi.fn(), + WalletProvider: ({ children }) => <>{children}, +})); + const mockSetCopyText = vi.fn(); const mockSetCopyButtonText = vi.fn(); @@ -44,6 +45,7 @@ Object.defineProperty(navigator, 'clipboard', { }); describe('WalletIslandQrReceive', () => { + const mockUseTheme = useTheme as ReturnType; const mockUseWalletContext = useWalletContext as ReturnType; const mockUseWalletIslandContext = useWalletIslandContext as ReturnType< typeof vi.fn @@ -58,6 +60,7 @@ describe('WalletIslandQrReceive', () => { }; beforeEach(() => { + mockUseTheme.mockReturnValue(''); mockUseWalletContext.mockReturnValue({ isOpen: true, isClosing: false, From c4d6164c8a632e0b4df82f476b8c36a9a290e0a7 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 3 Jan 2025 12:52:48 -0800 Subject: [PATCH 020/150] improve defaults, fix variable spelling --- src/internal/components/QrCode/QrCodeSvg.tsx | 14 +++++++------- .../components/QrCode/gradientConstants.test.ts | 4 ++-- .../components/QrCode/gradientConstants.ts | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/internal/components/QrCode/QrCodeSvg.tsx b/src/internal/components/QrCode/QrCodeSvg.tsx index e525ab72fa..35f2025b32 100644 --- a/src/internal/components/QrCode/QrCodeSvg.tsx +++ b/src/internal/components/QrCode/QrCodeSvg.tsx @@ -9,7 +9,7 @@ import { QR_LOGO_SIZE, linearGradientStops, ockThemeToLinearGradientColorMap, - ockThemeToRadiamGradientColorMap, + ockThemeToRadialGradientColorMap, presetGradients, } from './gradientConstants'; import { useCorners } from './useCorners'; @@ -66,18 +66,18 @@ export function QrCodeSvg({ const linearGradientColor = ockThemeToLinearGradientColorMap[ themeName as keyof typeof ockThemeToLinearGradientColorMap - ]; + ] ?? 'blue'; const linearColors = [ linearGradientStops[linearGradientColor].startColor, linearGradientStops[linearGradientColor].endColor, ]; + const radialGradientColor = + ockThemeToRadialGradientColorMap[ + themeName as keyof typeof ockThemeToLinearGradientColorMap + ] ?? 'default'; const presetGradientForColor = - presetGradients[ - ockThemeToRadiamGradientColorMap[ - themeName as keyof typeof ockThemeToLinearGradientColorMap - ] as keyof typeof presetGradients - ]; + presetGradients[radialGradientColor as keyof typeof presetGradients]; const matrix = useMatrix(value, ecl); const corners = useCorners(size, matrix.length, bgColor, fillColor, uid); diff --git a/src/internal/components/QrCode/gradientConstants.test.ts b/src/internal/components/QrCode/gradientConstants.test.ts index 8167f35bf5..b9165b6942 100644 --- a/src/internal/components/QrCode/gradientConstants.test.ts +++ b/src/internal/components/QrCode/gradientConstants.test.ts @@ -9,7 +9,7 @@ import { QR_LOGO_SIZE, linearGradientStops, ockThemeToLinearGradientColorMap, - ockThemeToRadiamGradientColorMap, + ockThemeToRadialGradientColorMap, presetGradients, } from './gradientConstants'; @@ -45,7 +45,7 @@ describe('Theme Maps', () => { }); it('should have correct radial gradient theme mappings', () => { - expect(ockThemeToRadiamGradientColorMap).toEqual({ + expect(ockThemeToRadialGradientColorMap).toEqual({ default: 'default', base: 'blue', cyberpunk: 'magenta', diff --git a/src/internal/components/QrCode/gradientConstants.ts b/src/internal/components/QrCode/gradientConstants.ts index c56b89a0ac..3ba54b8ebe 100644 --- a/src/internal/components/QrCode/gradientConstants.ts +++ b/src/internal/components/QrCode/gradientConstants.ts @@ -15,7 +15,7 @@ export const ockThemeToLinearGradientColorMap = { hacker: 'black', }; -export const ockThemeToRadiamGradientColorMap = { +export const ockThemeToRadialGradientColorMap = { default: 'default', base: 'blue', cyberpunk: 'magenta', From eca0fb8ac7aec29f401621c66a3f58ca2f0c8c10 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 3 Jan 2025 12:55:53 -0800 Subject: [PATCH 021/150] fix: tests --- src/wallet/components/WalletIslandSwap.test.tsx | 10 +++++----- .../components/WalletIslandWalletActions.test.tsx | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/wallet/components/WalletIslandSwap.test.tsx b/src/wallet/components/WalletIslandSwap.test.tsx index 9e5b182f2a..8927090de2 100644 --- a/src/wallet/components/WalletIslandSwap.test.tsx +++ b/src/wallet/components/WalletIslandSwap.test.tsx @@ -60,19 +60,19 @@ vi.mock(import('../../swap'), async (importOriginal) => { }; }); -vi.mock('../../../swap/components/SwapProvider', () => ({ +vi.mock('../../swap/components/SwapProvider', () => ({ useSwapContext: vi.fn(), SwapProvider: ({ children }) => <>{children}, })); -vi.mock('../WalletProvider', () => ({ - useWalletContext: vi.fn(), -})); - vi.mock('./WalletIslandProvider', () => ({ useWalletIslandContext: vi.fn(), })); +vi.mock('./WalletProvider', () => ({ + useWalletContext: vi.fn(), +})); + describe('WalletIslandSwap', () => { const mockUseWalletContext = useWalletContext as ReturnType; const mockUseWalletIslandContext = useWalletIslandContext as ReturnType< diff --git a/src/wallet/components/WalletIslandWalletActions.test.tsx b/src/wallet/components/WalletIslandWalletActions.test.tsx index 96d2849a0a..cf98d1e808 100644 --- a/src/wallet/components/WalletIslandWalletActions.test.tsx +++ b/src/wallet/components/WalletIslandWalletActions.test.tsx @@ -13,16 +13,16 @@ vi.mock('wagmi/actions', () => ({ disconnect: vi.fn(), })); -vi.mock('../WalletProvider', () => ({ - useWalletContext: vi.fn(), - WalletProvider: ({ children }) => <>{children}, -})); - vi.mock('./WalletIslandProvider', () => ({ useWalletIslandContext: vi.fn(), WalletIslandProvider: ({ children }) => <>{children}, })); +vi.mock('./WalletProvider', () => ({ + useWalletContext: vi.fn(), + WalletProvider: ({ children }) => <>{children}, +})); + describe('WalletIslandWalletActions', () => { const mockUseWalletContext = useWalletContext as ReturnType; const mockUseWalletIslandContext = useWalletIslandContext as ReturnType< From 8f7edb219a8a0755a38a9b48cc4d80e6dbcc68be Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 3 Jan 2025 13:27:18 -0800 Subject: [PATCH 022/150] fixed error handling for useWalletIslandContext --- .../components/WalletIslandProvider.tsx | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/wallet/components/WalletIslandProvider.tsx b/src/wallet/components/WalletIslandProvider.tsx index 91a137f32a..66bdd55afb 100644 --- a/src/wallet/components/WalletIslandProvider.tsx +++ b/src/wallet/components/WalletIslandProvider.tsx @@ -30,9 +30,20 @@ type WalletIslandProviderReact = { children: ReactNode; }; -const WalletIslandContext = createContext( - {} as WalletIslandContextType, -); +const emptyContext = {} as WalletIslandContextType; + +const WalletIslandContext = + createContext(emptyContext); + +export function useWalletIslandContext() { + const walletIslandContext = useContext(WalletIslandContext); + if (walletIslandContext === emptyContext) { + throw new Error( + 'useWalletIslandContext must be used within a WalletIslandProvider', + ); + } + return walletIslandContext; +} export function WalletIslandProvider({ children }: WalletIslandProviderReact) { const { address } = useWalletContext(); @@ -73,13 +84,3 @@ export function WalletIslandProvider({ children }: WalletIslandProviderReact) { ); } - -export function useWalletIslandContext() { - const walletIslandContext = useContext(WalletIslandContext); - if (!walletIslandContext) { - throw new Error( - 'useWalletIslandContext must be used within a WalletIslandProvider', - ); - } - return walletIslandContext; -} From 9a65a1d2132669091144100f3bd6790fbe3a39bd Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 3 Jan 2025 13:27:48 -0800 Subject: [PATCH 023/150] optional containerRef --- src/wallet/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wallet/types.ts b/src/wallet/types.ts index 7bbed1e1b6..691cbb78c6 100644 --- a/src/wallet/types.ts +++ b/src/wallet/types.ts @@ -152,5 +152,5 @@ export type WalletDropdownLinkReact = { export type WalletIslandProps = { children: React.ReactNode; - walletContainerRef: React.RefObject; + walletContainerRef?: React.RefObject; }; From a81d2bac51dd61d76c32a9c8442c8fa46a580213 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 3 Jan 2025 13:32:26 -0800 Subject: [PATCH 024/150] fix: tests --- src/wallet/components/Wallet.test.tsx | 43 ++++++++++++++++--- .../components/WalletIslandContent.test.tsx | 20 ++++++++- .../components/WalletIslandProvider.test.tsx | 19 +++++++- .../components/WalletIslandQrReceive.test.tsx | 20 +++++++++ .../components/WalletIslandSwap.test.tsx | 20 +++++++++ 5 files changed, 112 insertions(+), 10 deletions(-) diff --git a/src/wallet/components/Wallet.test.tsx b/src/wallet/components/Wallet.test.tsx index 204e452dbf..136c854e0e 100644 --- a/src/wallet/components/Wallet.test.tsx +++ b/src/wallet/components/Wallet.test.tsx @@ -4,11 +4,15 @@ import { useOutsideClick } from '../../ui/react/internal/hooks/useOutsideClick'; import { ConnectWallet } from './ConnectWallet'; import { Wallet } from './Wallet'; import { WalletDropdown } from './WalletDropdown'; +import { WalletIsland } from './WalletIsland'; import { type WalletProviderReact, useWalletContext } from './WalletProvider'; -vi.mock('./WalletProvider', () => ({ - useWalletContext: vi.fn(), - WalletProvider: ({ children }: WalletProviderReact) => <>{children}, +vi.mock('../../core-react/internal/hooks/useTheme', () => ({ + useTheme: vi.fn(), +})); + +vi.mock('../../ui/react/internal/hooks/useOutsideClick', () => ({ + useOutsideClick: vi.fn(), })); vi.mock('./ConnectWallet', () => ({ @@ -21,12 +25,13 @@ vi.mock('./WalletDropdown', () => ({ ), })); -vi.mock('../../core-react/internal/hooks/useTheme', () => ({ - useTheme: vi.fn(), +vi.mock('./WalletIsland', () => ({ + WalletIsland: () =>
Wallet Island
, })); -vi.mock('../../ui/react/internal/hooks/useOutsideClick', () => ({ - useOutsideClick: vi.fn(), +vi.mock('./WalletProvider', () => ({ + useWalletContext: vi.fn(), + WalletProvider: ({ children }: WalletProviderReact) => <>{children}, })); describe('Wallet Component', () => { @@ -119,4 +124,28 @@ describe('Wallet Component', () => { expect(mockHandleClose).not.toHaveBeenCalled(); }); + + it('should log error and default to WalletDropdown when both WalletDropdown and WalletIsland are provided', () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + render( + + + +
Wallet Dropdown
+
+ +
Wallet Island
+
+
, + ); + + expect(consoleSpy).toHaveBeenCalledWith( + 'Defaulted to WalletDropdown. Wallet cannot have both WalletDropdown and WalletIsland as children.', + ); + expect(screen.getByTestId('wallet-dropdown')).toBeDefined(); + expect(screen.queryByTestId('wallet-island')).toBeNull(); + + consoleSpy.mockRestore(); + }); }); diff --git a/src/wallet/components/WalletIslandContent.test.tsx b/src/wallet/components/WalletIslandContent.test.tsx index 24cc07801a..6a19575fa3 100644 --- a/src/wallet/components/WalletIslandContent.test.tsx +++ b/src/wallet/components/WalletIslandContent.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { WalletIslandContent } from './WalletIslandContent'; import { useWalletIslandContext } from './WalletIslandProvider'; @@ -85,6 +85,24 @@ describe('WalletIslandContent', () => { ).toHaveClass('hidden'); }); + it('handles animation end when closing', () => { + const setIsOpen = vi.fn(); + const setIsClosing = vi.fn(); + mockUseWalletContext.mockReturnValue({ + isClosing: true, + setIsOpen, + setIsClosing + }); + + render(); + + const content = screen.getByTestId('ockWalletIslandContent'); + fireEvent.animationEnd(content); + + expect(setIsOpen).toHaveBeenCalledWith(false); + expect(setIsClosing).toHaveBeenCalledWith(false); + }); + it('renders WalletIslandQrReceive when showQr is true', () => { mockUseWalletIslandContext.mockReturnValue({ ...defaultMockUseWalletIslandContext, diff --git a/src/wallet/components/WalletIslandProvider.test.tsx b/src/wallet/components/WalletIslandProvider.test.tsx index 75028cdccf..a601034a83 100644 --- a/src/wallet/components/WalletIslandProvider.test.tsx +++ b/src/wallet/components/WalletIslandProvider.test.tsx @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react'; +import { render, renderHook } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useAccount } from 'wagmi'; import { @@ -31,7 +31,7 @@ describe('useWalletIslandContext', () => { mockUseWalletContext.mockReturnValue(defaultWalletContext); }); - it('should provide wallet island context', () => { + it('should provide wallet island context when used within provider', () => { const { result } = renderHook(() => useWalletIslandContext(), { wrapper: WalletIslandProvider, }); @@ -48,4 +48,19 @@ describe('useWalletIslandContext', () => { tokenHoldings: expect.any(Array), }); }); + + it('should throw an error when used outside of WalletIslandProvider', () => { + const TestComponent = () => { + useWalletIslandContext(); + return null; + }; + // Suppress console.error for this test to avoid noisy output + const originalError = console.error; + console.error = vi.fn(); + expect(() => { + render(); + }).toThrow('useWalletIslandContext must be used within a WalletIslandProvider'); + // Restore console.error + console.error = originalError; + }); }); diff --git a/src/wallet/components/WalletIslandQrReceive.test.tsx b/src/wallet/components/WalletIslandQrReceive.test.tsx index a3539bdc14..80663d67a7 100644 --- a/src/wallet/components/WalletIslandQrReceive.test.tsx +++ b/src/wallet/components/WalletIslandQrReceive.test.tsx @@ -88,6 +88,26 @@ describe('WalletIslandQrReceive', () => { expect(screen.getByTestId('ockWalletIslandQrReceive')).toBeInTheDocument(); }); + it('should render correctly based on isQrClosing state', () => { + mockUseWalletIslandContext.mockReturnValue({ + isQrClosing: false, + }); + + const { rerender } = render(); + expect(screen.getByTestId('ockWalletIslandQrReceive')).toBeInTheDocument(); + expect(screen.getByTestId('ockWalletIslandQrReceive')).toHaveClass( + 'fade-in slide-in-from-right-5 animate-in duration-150 ease-out', + ); + + mockUseWalletIslandContext.mockReturnValue({ + isQrClosing: true, + }); + rerender(); + expect(screen.getByTestId('ockWalletIslandQrReceive')).toHaveClass( + 'fade-out slide-out-to-left-5 animate-out fill-mode-forwards ease-in-out', + ); + }); + it('should focus backButtonRef when showQr is true', () => { mockUseWalletIslandContext.mockReturnValue({ ...defaultMockUseWalletIslandContext, diff --git a/src/wallet/components/WalletIslandSwap.test.tsx b/src/wallet/components/WalletIslandSwap.test.tsx index 8927090de2..9875db8087 100644 --- a/src/wallet/components/WalletIslandSwap.test.tsx +++ b/src/wallet/components/WalletIslandSwap.test.tsx @@ -159,6 +159,26 @@ describe('WalletIslandSwap', () => { expect(screen.getByTestId('ockWalletIslandSwap')).toBeInTheDocument(); }); + it('should render correctly based on isSwapClosing state', () => { + mockUseWalletIslandContext.mockReturnValue({ + isSwapClosing: false, + }); + + const { rerender } = render(); + expect(screen.getByTestId('ockWalletIslandSwap')).toBeInTheDocument(); + expect(screen.getByTestId('ockWalletIslandSwap')).toHaveClass( + 'fade-in slide-in-from-right-5 animate-in duration-150 ease-out', + ); + + mockUseWalletIslandContext.mockReturnValue({ + isSwapClosing: true, + }); + rerender(); + expect(screen.getByTestId('ockWalletIslandSwap')).toHaveClass( + 'fade-out slide-out-to-left-5 animate-out fill-mode-forwards ease-in-out', + ); + }); + it('should focus swapDivRef when showSwap is true', () => { mockUseWalletIslandContext.mockReturnValue({ ...defaultMockUseWalletIslandContext, From 5cb60c5c031b6030fab9aa0aea6aadf583a60d0f Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 3 Jan 2025 13:34:19 -0800 Subject: [PATCH 025/150] fix: linters --- src/wallet/components/WalletIslandContent.test.tsx | 2 +- src/wallet/components/WalletIslandDefault.test.tsx | 1 - src/wallet/components/WalletIslandProvider.test.tsx | 4 +++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/wallet/components/WalletIslandContent.test.tsx b/src/wallet/components/WalletIslandContent.test.tsx index 6a19575fa3..5c025460b5 100644 --- a/src/wallet/components/WalletIslandContent.test.tsx +++ b/src/wallet/components/WalletIslandContent.test.tsx @@ -91,7 +91,7 @@ describe('WalletIslandContent', () => { mockUseWalletContext.mockReturnValue({ isClosing: true, setIsOpen, - setIsClosing + setIsClosing, }); render(); diff --git a/src/wallet/components/WalletIslandDefault.test.tsx b/src/wallet/components/WalletIslandDefault.test.tsx index 5c13602429..2b193deb26 100644 --- a/src/wallet/components/WalletIslandDefault.test.tsx +++ b/src/wallet/components/WalletIslandDefault.test.tsx @@ -33,7 +33,6 @@ vi.mock('./WalletIslandContent', () => ({ ), })); - vi.mock('./WalletProvider', () => ({ useWalletContext: vi.fn(), WalletProvider: ({ children }) => <>{children}, diff --git a/src/wallet/components/WalletIslandProvider.test.tsx b/src/wallet/components/WalletIslandProvider.test.tsx index a601034a83..d18a59ef4a 100644 --- a/src/wallet/components/WalletIslandProvider.test.tsx +++ b/src/wallet/components/WalletIslandProvider.test.tsx @@ -59,7 +59,9 @@ describe('useWalletIslandContext', () => { console.error = vi.fn(); expect(() => { render(); - }).toThrow('useWalletIslandContext must be used within a WalletIslandProvider'); + }).toThrow( + 'useWalletIslandContext must be used within a WalletIslandProvider', + ); // Restore console.error console.error = originalError; }); From 7934bcbaa95d56fe847becca3621626f65b331d0 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 3 Jan 2025 13:38:04 -0800 Subject: [PATCH 026/150] fix: test --- src/core-react/internal/utils/getAddressTokenBalances.test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core-react/internal/utils/getAddressTokenBalances.test.tsx b/src/core-react/internal/utils/getAddressTokenBalances.test.tsx index ba14a4e3a6..25db4f845e 100644 --- a/src/core-react/internal/utils/getAddressTokenBalances.test.tsx +++ b/src/core-react/internal/utils/getAddressTokenBalances.test.tsx @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest'; -import type { TokenBalanceWithFiatValue } from '../../wallet/components/island/WalletIslandTokenHoldings'; -import { getAddressTokenBalances } from './getAddressTokenBalances'; +import { type TokenBalanceWithFiatValue, getAddressTokenBalances } from './getAddressTokenBalances'; describe('getAddressTokenBalances', () => { it('should return an empty array for an invalid address', async () => { From 4c1b9ead61f714ecbf993dd3a158a533547e51c9 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 3 Jan 2025 13:40:11 -0800 Subject: [PATCH 027/150] fix: lint --- .../internal/utils/getAddressTokenBalances.test.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core-react/internal/utils/getAddressTokenBalances.test.tsx b/src/core-react/internal/utils/getAddressTokenBalances.test.tsx index 25db4f845e..d41dab6844 100644 --- a/src/core-react/internal/utils/getAddressTokenBalances.test.tsx +++ b/src/core-react/internal/utils/getAddressTokenBalances.test.tsx @@ -1,5 +1,8 @@ import { describe, expect, it } from 'vitest'; -import { type TokenBalanceWithFiatValue, getAddressTokenBalances } from './getAddressTokenBalances'; +import { + type TokenBalanceWithFiatValue, + getAddressTokenBalances, +} from './getAddressTokenBalances'; describe('getAddressTokenBalances', () => { it('should return an empty array for an invalid address', async () => { From 484c88676e3a73a57a74072758b03f67dc7be7f2 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 3 Jan 2025 13:45:24 -0800 Subject: [PATCH 028/150] fix: lint, test --- .../internal/utils/getAddressTokenBalances.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core-react/internal/utils/getAddressTokenBalances.test.tsx b/src/core-react/internal/utils/getAddressTokenBalances.test.tsx index d41dab6844..84d21dbc15 100644 --- a/src/core-react/internal/utils/getAddressTokenBalances.test.tsx +++ b/src/core-react/internal/utils/getAddressTokenBalances.test.tsx @@ -6,12 +6,12 @@ import { describe('getAddressTokenBalances', () => { it('should return an empty array for an invalid address', async () => { - const result = await getAddressTokenBalances('invalid-address'); + const result = await getAddressTokenBalances('invalid-address' as `0x${string}`); expect(result).toEqual([]); }); it('should return an empty array for a null address', async () => { - const result = await getAddressTokenBalances(null); + const result = await getAddressTokenBalances(null as unknown as `0x${string}`); expect(result).toEqual([]); }); From 8b7b61bc056719c599b5fd6d622c168669f02035 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 3 Jan 2025 13:47:56 -0800 Subject: [PATCH 029/150] fix: linters --- .../internal/utils/getAddressTokenBalances.test.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/core-react/internal/utils/getAddressTokenBalances.test.tsx b/src/core-react/internal/utils/getAddressTokenBalances.test.tsx index 84d21dbc15..a199ece8ab 100644 --- a/src/core-react/internal/utils/getAddressTokenBalances.test.tsx +++ b/src/core-react/internal/utils/getAddressTokenBalances.test.tsx @@ -6,12 +6,16 @@ import { describe('getAddressTokenBalances', () => { it('should return an empty array for an invalid address', async () => { - const result = await getAddressTokenBalances('invalid-address' as `0x${string}`); + const result = await getAddressTokenBalances( + 'invalid-address' as `0x${string}`, + ); expect(result).toEqual([]); }); it('should return an empty array for a null address', async () => { - const result = await getAddressTokenBalances(null as unknown as `0x${string}`); + const result = await getAddressTokenBalances( + null as unknown as `0x${string}`, + ); expect(result).toEqual([]); }); From 54a49a53ed956b0a0ab20844cd7c43c98814e353 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 3 Jan 2025 13:53:52 -0800 Subject: [PATCH 030/150] add types to tests --- src/wallet/components/WalletIsland.test.tsx | 6 +- .../WalletIslandAddressDetails.test.tsx | 8 ++- .../components/WalletIslandContent.test.tsx | 71 ++++++++++++++++--- .../components/WalletIslandDefault.test.tsx | 8 ++- .../components/WalletIslandProvider.test.tsx | 4 +- .../components/WalletIslandQrReceive.test.tsx | 8 ++- .../components/WalletIslandSwap.test.tsx | 6 +- .../WalletIslandTokenHoldings.test.tsx | 4 +- .../WalletIslandTransactionActions.test.tsx | 4 +- .../WalletIslandWalletActions.test.tsx | 8 ++- 10 files changed, 101 insertions(+), 26 deletions(-) diff --git a/src/wallet/components/WalletIsland.test.tsx b/src/wallet/components/WalletIsland.test.tsx index ba225040f1..b36ea50d1a 100644 --- a/src/wallet/components/WalletIsland.test.tsx +++ b/src/wallet/components/WalletIsland.test.tsx @@ -14,14 +14,16 @@ vi.mock('./ConnectWallet', () => ({ })); vi.mock('./WalletIslandContent', () => ({ - WalletIslandContent: ({ children }) => ( + WalletIslandContent: ({ children }: { children: React.ReactNode }) => (
{children}
), })); vi.mock('./WalletProvider', () => ({ useWalletContext: vi.fn(), - WalletProvider: ({ children }) => <>{children}, + WalletProvider: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), })); describe('WalletIsland', () => { diff --git a/src/wallet/components/WalletIslandAddressDetails.test.tsx b/src/wallet/components/WalletIslandAddressDetails.test.tsx index c86c3843e4..40fa8eea60 100644 --- a/src/wallet/components/WalletIslandAddressDetails.test.tsx +++ b/src/wallet/components/WalletIslandAddressDetails.test.tsx @@ -29,12 +29,16 @@ vi.mock('../../core-react/identity/hooks/useName', () => ({ vi.mock('./WalletIslandProvider', () => ({ useWalletIslandContext: vi.fn(), - WalletIslandProvider: ({ children }) => <>{children}, + WalletIslandProvider: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), })); vi.mock('./WalletProvider', () => ({ useWalletContext: vi.fn(), - WalletProvider: ({ children }) => <>{children}, + WalletProvider: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), })); describe('WalletIslandAddressDetails', () => { diff --git a/src/wallet/components/WalletIslandContent.test.tsx b/src/wallet/components/WalletIslandContent.test.tsx index 5c025460b5..1d26821105 100644 --- a/src/wallet/components/WalletIslandContent.test.tsx +++ b/src/wallet/components/WalletIslandContent.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react'; +import type { RefObject } from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { WalletIslandContent } from './WalletIslandContent'; import { useWalletIslandContext } from './WalletIslandProvider'; @@ -10,7 +11,9 @@ vi.mock('../../core-react/internal/hooks/useTheme', () => ({ vi.mock('./WalletIslandProvider', () => ({ useWalletIslandContext: vi.fn(), - WalletIslandProvider: ({ children }) => <>{children}, + WalletIslandProvider: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), })); vi.mock('./WalletIslandQrReceive', () => ({ @@ -27,7 +30,9 @@ vi.mock('./WalletIslandSwap', () => ({ vi.mock('./WalletProvider', () => ({ useWalletContext: vi.fn(), - WalletProvider: ({ children }) => <>{children}, + WalletProvider: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), })); describe('WalletIslandContent', () => { @@ -54,7 +59,11 @@ describe('WalletIslandContent', () => { it('renders WalletIslandContent when isClosing is false', () => { mockUseWalletContext.mockReturnValue({ isClosing: false }); - render(); + render( + +
WalletIslandContent
+
, + ); expect(screen.getByTestId('ockWalletIslandContent')).toBeDefined(); expect(screen.queryByTestId('ockWalletIslandContent')).toHaveClass( @@ -71,7 +80,11 @@ describe('WalletIslandContent', () => { it('closes WalletIslandContent when isClosing is true', () => { mockUseWalletContext.mockReturnValue({ isClosing: true }); - render(); + render( + +
WalletIslandContent
+
, + ); expect(screen.getByTestId('ockWalletIslandContent')).toBeDefined(); expect(screen.queryByTestId('ockWalletIslandContent')).toHaveClass( @@ -94,7 +107,11 @@ describe('WalletIslandContent', () => { setIsClosing, }); - render(); + render( + +
WalletIslandContent
+
, + ); const content = screen.getByTestId('ockWalletIslandContent'); fireEvent.animationEnd(content); @@ -109,7 +126,11 @@ describe('WalletIslandContent', () => { showQr: true, }); - render(); + render( + +
WalletIslandContent
+
, + ); expect(screen.getByTestId('ockWalletIslandQrReceive')).toBeDefined(); expect( @@ -126,7 +147,11 @@ describe('WalletIslandContent', () => { showSwap: true, }); - render(); + render( + +
WalletIslandContent
+
, + ); expect(screen.getByTestId('ockWalletIslandSwap')).toBeDefined(); expect( @@ -153,7 +178,13 @@ describe('WalletIslandContent', () => { isClosing: false, }); - render(); + render( + } + > +
WalletIslandContent
+
, + ); const draggable = screen.getByTestId('ockDraggable'); expect(draggable).toHaveStyle({ @@ -177,7 +208,13 @@ describe('WalletIslandContent', () => { isClosing: false, }); - render(); + render( + } + > +
WalletIslandContent
+
, + ); const draggable = screen.getByTestId('ockDraggable'); expect(draggable).toHaveStyle({ @@ -201,7 +238,13 @@ describe('WalletIslandContent', () => { isClosing: false, }); - render(); + render( + } + > +
WalletIslandContent
+
, + ); const draggable = screen.getByTestId('ockDraggable'); expect(draggable).toHaveStyle({ @@ -225,7 +268,13 @@ describe('WalletIslandContent', () => { isClosing: false, }); - render(); + render( + } + > +
WalletIslandContent
+
, + ); const draggable = screen.getByTestId('ockDraggable'); expect(draggable).toHaveStyle({ diff --git a/src/wallet/components/WalletIslandDefault.test.tsx b/src/wallet/components/WalletIslandDefault.test.tsx index 2b193deb26..d7b86bc370 100644 --- a/src/wallet/components/WalletIslandDefault.test.tsx +++ b/src/wallet/components/WalletIslandDefault.test.tsx @@ -24,7 +24,9 @@ vi.mock('../../core-react/identity/hooks/useName', () => ({ vi.mock('./WalletIslandProvider', () => ({ useWalletIslandContext: vi.fn(), - WalletIslandProvider: ({ children }) => <>{children}, + WalletIslandProvider: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), })); vi.mock('./WalletIslandContent', () => ({ @@ -35,7 +37,9 @@ vi.mock('./WalletIslandContent', () => ({ vi.mock('./WalletProvider', () => ({ useWalletContext: vi.fn(), - WalletProvider: ({ children }) => <>{children}, + WalletProvider: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), })); describe('WalletIslandDefault', () => { diff --git a/src/wallet/components/WalletIslandProvider.test.tsx b/src/wallet/components/WalletIslandProvider.test.tsx index d18a59ef4a..59ff7bf425 100644 --- a/src/wallet/components/WalletIslandProvider.test.tsx +++ b/src/wallet/components/WalletIslandProvider.test.tsx @@ -13,7 +13,9 @@ vi.mock('wagmi', () => ({ vi.mock('./WalletProvider', () => ({ useWalletContext: vi.fn(), - WalletProvider: ({ children }) => <>{children}, + WalletProvider: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), })); describe('useWalletIslandContext', () => { diff --git a/src/wallet/components/WalletIslandQrReceive.test.tsx b/src/wallet/components/WalletIslandQrReceive.test.tsx index 80663d67a7..aa77971da1 100644 --- a/src/wallet/components/WalletIslandQrReceive.test.tsx +++ b/src/wallet/components/WalletIslandQrReceive.test.tsx @@ -11,12 +11,16 @@ vi.mock('../../core-react/internal/hooks/useTheme', () => ({ vi.mock('./WalletIslandProvider', () => ({ useWalletIslandContext: vi.fn(), - WalletIslandProvider: ({ children }) => <>{children}, + WalletIslandProvider: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), })); vi.mock('./WalletProvider', () => ({ useWalletContext: vi.fn(), - WalletProvider: ({ children }) => <>{children}, + WalletProvider: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), })); const mockSetCopyText = vi.fn(); diff --git a/src/wallet/components/WalletIslandSwap.test.tsx b/src/wallet/components/WalletIslandSwap.test.tsx index 9875db8087..a6476809d5 100644 --- a/src/wallet/components/WalletIslandSwap.test.tsx +++ b/src/wallet/components/WalletIslandSwap.test.tsx @@ -43,7 +43,7 @@ vi.mock(import('../../swap'), async (importOriginal) => { SwapAmountInput: () =>
, SwapButton: () =>
, SwapMessage: () =>
, - SwapSettings: ({ children }) => ( + SwapSettings: ({ children }: { children: React.ReactNode }) => (
{children}
), SwapSettingsSlippageDescription: () => ( @@ -62,7 +62,9 @@ vi.mock(import('../../swap'), async (importOriginal) => { vi.mock('../../swap/components/SwapProvider', () => ({ useSwapContext: vi.fn(), - SwapProvider: ({ children }) => <>{children}, + SwapProvider: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), })); vi.mock('./WalletIslandProvider', () => ({ diff --git a/src/wallet/components/WalletIslandTokenHoldings.test.tsx b/src/wallet/components/WalletIslandTokenHoldings.test.tsx index 8d06623f77..a9f5daac98 100644 --- a/src/wallet/components/WalletIslandTokenHoldings.test.tsx +++ b/src/wallet/components/WalletIslandTokenHoldings.test.tsx @@ -5,7 +5,9 @@ import { WalletIslandTokenHoldings } from './WalletIslandTokenHoldings'; vi.mock('./WalletIslandProvider', () => ({ useWalletIslandContext: vi.fn(), - WalletIslandProvider: ({ children }) => <>{children}, + WalletIslandProvider: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), })); describe('WalletIslandTokenHoldings', () => { diff --git a/src/wallet/components/WalletIslandTransactionActions.test.tsx b/src/wallet/components/WalletIslandTransactionActions.test.tsx index 8dde826173..63b1ff5455 100644 --- a/src/wallet/components/WalletIslandTransactionActions.test.tsx +++ b/src/wallet/components/WalletIslandTransactionActions.test.tsx @@ -5,7 +5,9 @@ import { WalletIslandTransactionActions } from './WalletIslandTransactionActions vi.mock('./WalletIslandProvider', () => ({ useWalletIslandContext: vi.fn(), - WalletIslandProvider: ({ children }) => <>{children}, + WalletIslandProvider: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), })); describe('WalletIslandTransactionActons', () => { diff --git a/src/wallet/components/WalletIslandWalletActions.test.tsx b/src/wallet/components/WalletIslandWalletActions.test.tsx index cf98d1e808..d994943389 100644 --- a/src/wallet/components/WalletIslandWalletActions.test.tsx +++ b/src/wallet/components/WalletIslandWalletActions.test.tsx @@ -15,12 +15,16 @@ vi.mock('wagmi/actions', () => ({ vi.mock('./WalletIslandProvider', () => ({ useWalletIslandContext: vi.fn(), - WalletIslandProvider: ({ children }) => <>{children}, + WalletIslandProvider: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), })); vi.mock('./WalletProvider', () => ({ useWalletContext: vi.fn(), - WalletProvider: ({ children }) => <>{children}, + WalletProvider: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), })); describe('WalletIslandWalletActions', () => { From 96df8e779a9c9d1336d2716467d9748598ca7266 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 3 Jan 2025 14:06:10 -0800 Subject: [PATCH 031/150] more types in swaptests --- .../components/WalletIslandSwap.test.tsx | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/wallet/components/WalletIslandSwap.test.tsx b/src/wallet/components/WalletIslandSwap.test.tsx index a6476809d5..5ca9eb156e 100644 --- a/src/wallet/components/WalletIslandSwap.test.tsx +++ b/src/wallet/components/WalletIslandSwap.test.tsx @@ -150,7 +150,7 @@ describe('WalletIslandSwap', () => { render( { isSwapClosing: false, }); - const { rerender } = render(); + const { rerender } = render( + , + ); expect(screen.getByTestId('ockWalletIslandSwap')).toBeInTheDocument(); expect(screen.getByTestId('ockWalletIslandSwap')).toHaveClass( 'fade-in slide-in-from-right-5 animate-in duration-150 ease-out', @@ -175,7 +184,16 @@ describe('WalletIslandSwap', () => { mockUseWalletIslandContext.mockReturnValue({ isSwapClosing: true, }); - rerender(); + rerender( + , + ); expect(screen.getByTestId('ockWalletIslandSwap')).toHaveClass( 'fade-out slide-out-to-left-5 animate-out fill-mode-forwards ease-in-out', ); @@ -189,9 +207,9 @@ describe('WalletIslandSwap', () => { render( { render( { const { rerender } = render( { rerender( Date: Sat, 4 Jan 2025 11:44:55 -0800 Subject: [PATCH 032/150] portfolio api method and hook --- .../wallet/usePortfolioTokenBalances.ts | 31 ++++++++++ src/core/api/getPortfolioTokenBalances.ts | 57 +++++++++++++++++++ src/core/network/definitions/wallet.ts | 1 + src/core/utils/isApiResponseError.ts | 7 +++ 4 files changed, 96 insertions(+) create mode 100644 src/core-react/wallet/usePortfolioTokenBalances.ts create mode 100644 src/core/api/getPortfolioTokenBalances.ts create mode 100644 src/core/network/definitions/wallet.ts create mode 100644 src/core/utils/isApiResponseError.ts diff --git a/src/core-react/wallet/usePortfolioTokenBalances.ts b/src/core-react/wallet/usePortfolioTokenBalances.ts new file mode 100644 index 0000000000..55d1a987b9 --- /dev/null +++ b/src/core-react/wallet/usePortfolioTokenBalances.ts @@ -0,0 +1,31 @@ +import { + type GetPortfolioTokenBalancesParams, + type Portfolio, + getPortfolioTokenBalances, +} from '@/core/api/getPortfolioTokenBalances'; +import { isApiError } from '@/core/utils/isApiResponseError'; +import { type UseQueryResult, useQuery } from '@tanstack/react-query'; + +export function usePortfolioTokenBalances({ + addresses, +}: GetPortfolioTokenBalancesParams): UseQueryResult { + const actionKey = `usePortfolioTokenBalances-${addresses}`; + return useQuery({ + queryKey: ['usePortfolioTokenBalances', actionKey], + queryFn: async () => { + const response = await getPortfolioTokenBalances({ + addresses, + }); + + if (isApiError(response)) { + throw new Error(response.message); + } + + return response.tokens; + }, + retry: false, + refetchOnWindowFocus: false, + enabled: !!addresses && addresses.length > 0, + refetchInterval: 1000 * 60 * 15, // 15 minutes + }); +} diff --git a/src/core/api/getPortfolioTokenBalances.ts b/src/core/api/getPortfolioTokenBalances.ts new file mode 100644 index 0000000000..1fd199c254 --- /dev/null +++ b/src/core/api/getPortfolioTokenBalances.ts @@ -0,0 +1,57 @@ +import type { APIError } from '@/core/api/types'; +import type { Token } from '@/token'; +import type { Address } from 'viem'; +import { CDP_GET_PORTFOLIO_TOKEN_BALANCES } from '../network/definitions/wallet'; +import { sendRequest } from '../network/request'; + +export type GetPortfolioTokenBalancesParams = { + addresses: Address[] | null | undefined; +}; + +/** Token: + * address: Address | ""; + * chainId: number; + * decimals: number; + * image: string | null; + * name: string; + * symbol: string; + */ +export type PortfolioTokenWithFiatValue = Token & { + crypto_balance: number; + fiat_balance: number; +}; + +export type Portfolio = { + address: Address; + token_balances: PortfolioTokenWithFiatValue[]; + portfolio_balance_usd: number; +}; + +export type GetPortfolioTokenBalancesResponse = { + tokens: Portfolio[] | APIError; // TODO: rename the response key to portfolio +}; + +export async function getPortfolioTokenBalances({ + addresses, +}: GetPortfolioTokenBalancesParams) { + try { + const res = await sendRequest< + GetPortfolioTokenBalancesParams, + GetPortfolioTokenBalancesResponse + >(CDP_GET_PORTFOLIO_TOKEN_BALANCES, [{ addresses }]); + if (res.error) { + return { + code: `${res.error.code}`, + error: 'Error fetching portfolio token balances', + message: res.error.message, + }; + } + return res.result; + } catch (_error) { + return { + code: 'uncaught-portfolio', + error: 'Something went wrong', + message: `Error fetching portfolio token balances: ${_error}`, + }; + } +} diff --git a/src/core/network/definitions/wallet.ts b/src/core/network/definitions/wallet.ts new file mode 100644 index 0000000000..129cd6573e --- /dev/null +++ b/src/core/network/definitions/wallet.ts @@ -0,0 +1 @@ +export const CDP_GET_PORTFOLIO_TOKEN_BALANCES = 'cdp_getTokensForAddresses'; diff --git a/src/core/utils/isApiResponseError.ts b/src/core/utils/isApiResponseError.ts new file mode 100644 index 0000000000..d91218dcce --- /dev/null +++ b/src/core/utils/isApiResponseError.ts @@ -0,0 +1,7 @@ +import type { APIError } from '@/core/api'; + +export function isApiError(response: unknown): response is APIError { + return ( + response !== null && typeof response === 'object' && 'error' in response + ); +} From 18f4e2fc7df9418ef5e87c3f884761c0a42042ab Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sat, 4 Jan 2025 11:45:18 -0800 Subject: [PATCH 033/150] portfolio data --- .../components/WalletIslandAddressDetails.tsx | 18 +++------- .../components/WalletIslandProvider.tsx | 34 ++++++++----------- .../components/WalletIslandTokenHoldings.tsx | 25 +++++++++----- 3 files changed, 35 insertions(+), 42 deletions(-) diff --git a/src/wallet/components/WalletIslandAddressDetails.tsx b/src/wallet/components/WalletIslandAddressDetails.tsx index 71e9590e15..7a42e69dab 100644 --- a/src/wallet/components/WalletIslandAddressDetails.tsx +++ b/src/wallet/components/WalletIslandAddressDetails.tsx @@ -1,12 +1,13 @@ import { border, cn, color, pressable, text } from '@/styles/theme'; import { Avatar, Badge, Name } from '@/ui/react/identity'; import { useCallback, useState } from 'react'; -import type { Address, Chain } from 'viem'; import { useWalletContext } from './WalletProvider'; +import { useWalletIslandContext } from '@/wallet/components/WalletIslandProvider'; export function AddressDetails() { const { address, chain, isClosing } = useWalletContext(); const [copyText, setCopyText] = useState('Copy'); + const { portfolioFiatValue } = useWalletIslandContext(); const handleCopyAddress = useCallback(async () => { try { @@ -71,19 +72,10 @@ export function AddressDetails() {
- + + {portfolioFiatValue && `$${Number(portfolioFiatValue)?.toFixed(2)}`} +
); } - -type AddressBalanceProps = { - address?: Address | null; - chain?: Chain | null; -}; - -function AddressBalance({ address, chain }: AddressBalanceProps) { - const data = { address, chain }; // temp linter fix - console.log({ data }); // temp linter fix - return $690.42; -} diff --git a/src/wallet/components/WalletIslandProvider.tsx b/src/wallet/components/WalletIslandProvider.tsx index 66bdd55afb..638486bbda 100644 --- a/src/wallet/components/WalletIslandProvider.tsx +++ b/src/wallet/components/WalletIslandProvider.tsx @@ -1,18 +1,15 @@ import { useValue } from '@/core-react/internal/hooks/useValue'; -import { - type TokenBalanceWithFiatValue, - getAddressTokenBalances, -} from '@/core-react/internal/utils/getAddressTokenBalances'; import { type Dispatch, type ReactNode, type SetStateAction, createContext, useContext, - useEffect, useState, } from 'react'; import { useWalletContext } from './WalletProvider'; +import { usePortfolioTokenBalances } from '@/core-react/wallet/usePortfolioTokenBalances'; +import type { PortfolioTokenWithFiatValue } from '@/core/api/getPortfolioTokenBalances'; export type WalletIslandContextType = { showSwap: boolean; @@ -23,7 +20,8 @@ export type WalletIslandContextType = { setShowQr: Dispatch>; isQrClosing: boolean; setIsQrClosing: Dispatch>; - tokenHoldings: TokenBalanceWithFiatValue[]; + tokenBalances: PortfolioTokenWithFiatValue[] | undefined; + portfolioFiatValue: number | undefined; }; type WalletIslandProviderReact = { @@ -51,20 +49,15 @@ export function WalletIslandProvider({ children }: WalletIslandProviderReact) { const [isSwapClosing, setIsSwapClosing] = useState(false); const [showQr, setShowQr] = useState(false); const [isQrClosing, setIsQrClosing] = useState(false); - const [tokenHoldings, setTokenHoldings] = useState< - TokenBalanceWithFiatValue[] - >([]); - - useEffect(() => { - async function fetchTokens() { - if (address) { - const fetchedTokens = await getAddressTokenBalances(address); - setTokenHoldings(fetchedTokens); - } - } + const { + data: portfolioData, + // isLoading: isLoadingPortfolioData, + // isError, + // error, + } = usePortfolioTokenBalances({ addresses: [address ?? '0x000'] }); - fetchTokens(); - }, [address]); + const portfolioFiatValue = portfolioData?.[0]?.portfolio_balance_usd; + const tokenBalances = portfolioData?.[0]?.token_balances; const value = useValue({ showSwap, @@ -75,7 +68,8 @@ export function WalletIslandProvider({ children }: WalletIslandProviderReact) { setShowQr, isQrClosing, setIsQrClosing, - tokenHoldings, + tokenBalances, + portfolioFiatValue, }); return ( diff --git a/src/wallet/components/WalletIslandTokenHoldings.tsx b/src/wallet/components/WalletIslandTokenHoldings.tsx index c50726a664..3181c043ca 100644 --- a/src/wallet/components/WalletIslandTokenHoldings.tsx +++ b/src/wallet/components/WalletIslandTokenHoldings.tsx @@ -6,9 +6,9 @@ import { useWalletContext } from './WalletProvider'; // TODO: handle loading state export function WalletIslandTokenHoldings() { const { isClosing } = useWalletContext(); - const { tokenHoldings } = useWalletIslandContext(); + const { tokenBalances } = useWalletIslandContext(); - if (tokenHoldings.length === 0) { + if (!tokenBalances || tokenBalances.length === 0) { return null; } @@ -26,12 +26,19 @@ export function WalletIslandTokenHoldings() { )} data-testid="ockWalletIsland_TokenHoldings" > - {tokenHoldings.map((tokenBalance, index) => ( + {tokenBalances.map((tokenBalance, index) => ( ))}
@@ -56,12 +63,12 @@ function TokenDetails({ token, balance, valueInFiat }: TokenDetailsProps) { {token.name} - {`${balance} ${token.symbol}`} + {`${balance.toFixed(5)} ${token.symbol}`}
- {`${currencySymbol}${valueInFiat}`} + {`${currencySymbol}${valueInFiat.toFixed(2)}`}
); From 7e8fbab43193d4331e7e4842e6f0c3c5cc54d6cc Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sat, 4 Jan 2025 12:19:18 -0800 Subject: [PATCH 034/150] portfolio refresh, svg sizing --- .../components/WalletIslandWalletActions.tsx | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/wallet/components/WalletIslandWalletActions.tsx b/src/wallet/components/WalletIslandWalletActions.tsx index 1382fc4c55..447048e848 100644 --- a/src/wallet/components/WalletIslandWalletActions.tsx +++ b/src/wallet/components/WalletIslandWalletActions.tsx @@ -1,7 +1,7 @@ import { clockSvg } from '@/internal/svg/clockSvg'; -import { collapseSvg } from '@/internal/svg/collapseSvg'; import { disconnectSvg } from '@/internal/svg/disconnectSvg'; import { qrIconSvg } from '@/internal/svg/qrIconSvg'; +import { refreshSvg } from '@/internal/svg/refreshSvg'; import { border, cn, pressable } from '@/styles/theme'; import { useCallback } from 'react'; import { useDisconnect } from 'wagmi'; @@ -10,7 +10,8 @@ import { useWalletContext } from './WalletProvider'; export function WalletIslandWalletActions() { const { isClosing, handleClose } = useWalletContext(); - const { setShowQr } = useWalletIslandContext(); + const { setShowQr, refetchPortfolioData, portfolioDataUpdatedAt } = + useWalletIslandContext(); const { disconnect, connectors } = useDisconnect(); const handleDisconnect = useCallback(() => { @@ -24,9 +25,15 @@ export function WalletIslandWalletActions() { setShowQr(true); }, [setShowQr]); - const handleCollapse = useCallback(() => { - handleClose(); - }, [handleClose]); + const handleRefreshPortfolioData = useCallback(async () => { + if ( + portfolioDataUpdatedAt && + Date.now() - portfolioDataUpdatedAt < 1000 * 15 + ) { + return; // TODO: Add toast + } + await refetchPortfolioData(); + }, [refetchPortfolioData, portfolioDataUpdatedAt]); return (
-
{disconnectSvg}
+
{disconnectSvg}
From 19f69759edca4a79991e9c9bb5f879dc199cc43c Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sat, 4 Jan 2025 12:20:13 -0800 Subject: [PATCH 035/150] refresh svg --- src/internal/svg/refreshSvg.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/internal/svg/refreshSvg.tsx diff --git a/src/internal/svg/refreshSvg.tsx b/src/internal/svg/refreshSvg.tsx new file mode 100644 index 0000000000..4ea667a5c8 --- /dev/null +++ b/src/internal/svg/refreshSvg.tsx @@ -0,0 +1,17 @@ +import { icon } from '../../styles/theme'; + +export const refreshSvg = ( + + Refresh SVG + + +); From 317f74c1e468bf76dc19e0f186ea8512a869deb8 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sat, 4 Jan 2025 12:21:04 -0800 Subject: [PATCH 036/150] loading state --- .../components/WalletIslandAddressDetails.tsx | 21 +++++++++++++++---- .../components/WalletIslandTokenHoldings.tsx | 7 ++++++- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/wallet/components/WalletIslandAddressDetails.tsx b/src/wallet/components/WalletIslandAddressDetails.tsx index 7a42e69dab..0a3a647b01 100644 --- a/src/wallet/components/WalletIslandAddressDetails.tsx +++ b/src/wallet/components/WalletIslandAddressDetails.tsx @@ -3,11 +3,11 @@ import { Avatar, Badge, Name } from '@/ui/react/identity'; import { useCallback, useState } from 'react'; import { useWalletContext } from './WalletProvider'; import { useWalletIslandContext } from '@/wallet/components/WalletIslandProvider'; +import { Spinner } from '@/internal/components/Spinner'; export function AddressDetails() { const { address, chain, isClosing } = useWalletContext(); const [copyText, setCopyText] = useState('Copy'); - const { portfolioFiatValue } = useWalletIslandContext(); const handleCopyAddress = useCallback(async () => { try { @@ -72,10 +72,23 @@ export function AddressDetails() {
- - {portfolioFiatValue && `$${Number(portfolioFiatValue)?.toFixed(2)}`} - +
); } + +function AddressBalanceInFiat() { + const { portfolioFiatValue, isFetchingPortfolioData } = + useWalletIslandContext(); + + if (isFetchingPortfolioData) { + return ; + } + + return ( + + {portfolioFiatValue && `$${Number(portfolioFiatValue)?.toFixed(2)}`} + + ); +} diff --git a/src/wallet/components/WalletIslandTokenHoldings.tsx b/src/wallet/components/WalletIslandTokenHoldings.tsx index 3181c043ca..f0c5ce0f3f 100644 --- a/src/wallet/components/WalletIslandTokenHoldings.tsx +++ b/src/wallet/components/WalletIslandTokenHoldings.tsx @@ -1,3 +1,4 @@ +import { Spinner } from '@/internal/components/Spinner'; import { cn, color, text } from '@/styles/theme'; import { type Token, TokenImage } from '@/token'; import { useWalletIslandContext } from './WalletIslandProvider'; @@ -6,7 +7,11 @@ import { useWalletContext } from './WalletProvider'; // TODO: handle loading state export function WalletIslandTokenHoldings() { const { isClosing } = useWalletContext(); - const { tokenBalances } = useWalletIslandContext(); + const { tokenBalances, isFetchingPortfolioData } = useWalletIslandContext(); + + if (isFetchingPortfolioData) { + return ; + } if (!tokenBalances || tokenBalances.length === 0) { return null; From d197df6472610f18161bbf8b00581f6076c19cac Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sat, 4 Jan 2025 12:21:25 -0800 Subject: [PATCH 037/150] refetch and loading state --- src/wallet/components/WalletIslandProvider.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/wallet/components/WalletIslandProvider.tsx b/src/wallet/components/WalletIslandProvider.tsx index 638486bbda..df6aa3c187 100644 --- a/src/wallet/components/WalletIslandProvider.tsx +++ b/src/wallet/components/WalletIslandProvider.tsx @@ -9,7 +9,8 @@ import { } from 'react'; import { useWalletContext } from './WalletProvider'; import { usePortfolioTokenBalances } from '@/core-react/wallet/usePortfolioTokenBalances'; -import type { PortfolioTokenWithFiatValue } from '@/core/api/getPortfolioTokenBalances'; +import type { Portfolio, PortfolioTokenWithFiatValue } from '@/core/api/getPortfolioTokenBalances'; +import type { QueryObserverResult } from '@tanstack/react-query'; export type WalletIslandContextType = { showSwap: boolean; @@ -22,6 +23,9 @@ export type WalletIslandContextType = { setIsQrClosing: Dispatch>; tokenBalances: PortfolioTokenWithFiatValue[] | undefined; portfolioFiatValue: number | undefined; + refetchPortfolioData: () => Promise>; + isFetchingPortfolioData: boolean; + portfolioDataUpdatedAt: number | undefined; }; type WalletIslandProviderReact = { @@ -51,9 +55,9 @@ export function WalletIslandProvider({ children }: WalletIslandProviderReact) { const [isQrClosing, setIsQrClosing] = useState(false); const { data: portfolioData, - // isLoading: isLoadingPortfolioData, - // isError, - // error, + refetch: refetchPortfolioData, + isFetching: isFetchingPortfolioData, + dataUpdatedAt: portfolioDataUpdatedAt, } = usePortfolioTokenBalances({ addresses: [address ?? '0x000'] }); const portfolioFiatValue = portfolioData?.[0]?.portfolio_balance_usd; @@ -70,6 +74,9 @@ export function WalletIslandProvider({ children }: WalletIslandProviderReact) { setIsQrClosing, tokenBalances, portfolioFiatValue, + refetchPortfolioData, + isFetchingPortfolioData, + portfolioDataUpdatedAt, }); return ( From 1d126397c5bd16d3274a55db3dcdd11bec89ebcc Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sat, 4 Jan 2025 12:31:09 -0800 Subject: [PATCH 038/150] update query config --- src/core-react/wallet/usePortfolioTokenBalances.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core-react/wallet/usePortfolioTokenBalances.ts b/src/core-react/wallet/usePortfolioTokenBalances.ts index 55d1a987b9..a572168d37 100644 --- a/src/core-react/wallet/usePortfolioTokenBalances.ts +++ b/src/core-react/wallet/usePortfolioTokenBalances.ts @@ -24,8 +24,11 @@ export function usePortfolioTokenBalances({ return response.tokens; }, retry: false, - refetchOnWindowFocus: false, enabled: !!addresses && addresses.length > 0, - refetchInterval: 1000 * 60 * 15, // 15 minutes + refetchOnWindowFocus: false, + staleTime: 1000 * 60 * 5, // refresh on mount every 5 minutes + refetchOnMount: true, + refetchInterval: 1000 * 60 * 15, // refresh in background every 15 minutes + refetchIntervalInBackground: true, }); } From 258af8bfba4692274848a12214eaad08ee8c01ff Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sat, 4 Jan 2025 12:38:25 -0800 Subject: [PATCH 039/150] fix: linters --- src/wallet/components/WalletIslandAddressDetails.tsx | 4 ++-- src/wallet/components/WalletIslandProvider.tsx | 9 ++++++--- src/wallet/components/WalletIslandTokenHoldings.tsx | 5 ++++- src/wallet/components/WalletIslandWalletActions.tsx | 2 +- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/wallet/components/WalletIslandAddressDetails.tsx b/src/wallet/components/WalletIslandAddressDetails.tsx index 0a3a647b01..9b9825989a 100644 --- a/src/wallet/components/WalletIslandAddressDetails.tsx +++ b/src/wallet/components/WalletIslandAddressDetails.tsx @@ -1,9 +1,9 @@ +import { Spinner } from '@/internal/components/Spinner'; import { border, cn, color, pressable, text } from '@/styles/theme'; import { Avatar, Badge, Name } from '@/ui/react/identity'; +import { useWalletIslandContext } from '@/wallet/components/WalletIslandProvider'; import { useCallback, useState } from 'react'; import { useWalletContext } from './WalletProvider'; -import { useWalletIslandContext } from '@/wallet/components/WalletIslandProvider'; -import { Spinner } from '@/internal/components/Spinner'; export function AddressDetails() { const { address, chain, isClosing } = useWalletContext(); diff --git a/src/wallet/components/WalletIslandProvider.tsx b/src/wallet/components/WalletIslandProvider.tsx index df6aa3c187..d0b7b188c7 100644 --- a/src/wallet/components/WalletIslandProvider.tsx +++ b/src/wallet/components/WalletIslandProvider.tsx @@ -1,4 +1,10 @@ import { useValue } from '@/core-react/internal/hooks/useValue'; +import { usePortfolioTokenBalances } from '@/core-react/wallet/usePortfolioTokenBalances'; +import type { + Portfolio, + PortfolioTokenWithFiatValue, +} from '@/core/api/getPortfolioTokenBalances'; +import type { QueryObserverResult } from '@tanstack/react-query'; import { type Dispatch, type ReactNode, @@ -8,9 +14,6 @@ import { useState, } from 'react'; import { useWalletContext } from './WalletProvider'; -import { usePortfolioTokenBalances } from '@/core-react/wallet/usePortfolioTokenBalances'; -import type { Portfolio, PortfolioTokenWithFiatValue } from '@/core/api/getPortfolioTokenBalances'; -import type { QueryObserverResult } from '@tanstack/react-query'; export type WalletIslandContextType = { showSwap: boolean; diff --git a/src/wallet/components/WalletIslandTokenHoldings.tsx b/src/wallet/components/WalletIslandTokenHoldings.tsx index f0c5ce0f3f..b2040a9214 100644 --- a/src/wallet/components/WalletIslandTokenHoldings.tsx +++ b/src/wallet/components/WalletIslandTokenHoldings.tsx @@ -42,7 +42,10 @@ export function WalletIslandTokenHoldings() { name: tokenBalance.name, symbol: tokenBalance.symbol, }} - balance={(Number(tokenBalance.crypto_balance) / 10 ** Number(tokenBalance.decimals))} + balance={ + Number(tokenBalance.crypto_balance) / + 10 ** Number(tokenBalance.decimals) + } valueInFiat={Number(tokenBalance.fiat_balance)} /> ))} diff --git a/src/wallet/components/WalletIslandWalletActions.tsx b/src/wallet/components/WalletIslandWalletActions.tsx index 447048e848..d754c978c0 100644 --- a/src/wallet/components/WalletIslandWalletActions.tsx +++ b/src/wallet/components/WalletIslandWalletActions.tsx @@ -96,7 +96,7 @@ export function WalletIslandWalletActions() { 'flex items-center justify-center p-2', )} > -
{refreshSvg}
+
{refreshSvg}
From 64289b68579edfff2a400f1444158f9e1f6280f5 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sat, 4 Jan 2025 12:46:37 -0800 Subject: [PATCH 040/150] refactored types --- .../wallet/hooks/usePortfolioTokenBalances.ts | 34 +++++++++++++++++++ src/core/api/getPortfolioTokenBalances.ts | 34 +++---------------- src/core/api/types.ts | 31 +++++++++++++++++ .../components/WalletIslandProvider.tsx | 8 ++--- 4 files changed, 73 insertions(+), 34 deletions(-) create mode 100644 src/core-react/wallet/hooks/usePortfolioTokenBalances.ts diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts new file mode 100644 index 0000000000..660b415a1f --- /dev/null +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts @@ -0,0 +1,34 @@ +import { getPortfolioTokenBalances } from '@/core/api/getPortfolioTokenBalances'; +import type { + GetPortfolioTokenBalancesParams, + PortfolioTokenBalances, +} from '@/core/api/types'; +import { isApiError } from '@/core/utils/isApiResponseError'; +import { type UseQueryResult, useQuery } from '@tanstack/react-query'; + +export function usePortfolioTokenBalances({ + addresses, +}: GetPortfolioTokenBalancesParams): UseQueryResult { + const actionKey = `usePortfolioTokenBalances-${addresses}`; + return useQuery({ + queryKey: ['usePortfolioTokenBalances', actionKey], + queryFn: async () => { + const response = await getPortfolioTokenBalances({ + addresses, + }); + + if (isApiError(response)) { + throw new Error(response.message); + } + + return response.tokens; + }, + retry: false, + enabled: !!addresses && addresses.length > 0, + refetchOnWindowFocus: false, + staleTime: 1000 * 60 * 5, // refresh on mount every 5 minutes + refetchOnMount: true, + refetchInterval: 1000 * 60 * 15, // refresh in background every 15 minutes + refetchIntervalInBackground: true, + }); +} diff --git a/src/core/api/getPortfolioTokenBalances.ts b/src/core/api/getPortfolioTokenBalances.ts index 1fd199c254..20d0bd7684 100644 --- a/src/core/api/getPortfolioTokenBalances.ts +++ b/src/core/api/getPortfolioTokenBalances.ts @@ -1,35 +1,9 @@ -import type { APIError } from '@/core/api/types'; -import type { Token } from '@/token'; -import type { Address } from 'viem'; import { CDP_GET_PORTFOLIO_TOKEN_BALANCES } from '../network/definitions/wallet'; import { sendRequest } from '../network/request'; - -export type GetPortfolioTokenBalancesParams = { - addresses: Address[] | null | undefined; -}; - -/** Token: - * address: Address | ""; - * chainId: number; - * decimals: number; - * image: string | null; - * name: string; - * symbol: string; - */ -export type PortfolioTokenWithFiatValue = Token & { - crypto_balance: number; - fiat_balance: number; -}; - -export type Portfolio = { - address: Address; - token_balances: PortfolioTokenWithFiatValue[]; - portfolio_balance_usd: number; -}; - -export type GetPortfolioTokenBalancesResponse = { - tokens: Portfolio[] | APIError; // TODO: rename the response key to portfolio -}; +import type { + GetPortfolioTokenBalancesParams, + GetPortfolioTokenBalancesResponse, +} from './types'; export async function getPortfolioTokenBalances({ addresses, diff --git a/src/core/api/types.ts b/src/core/api/types.ts index 281fc3bc7b..fadcc16e67 100644 --- a/src/core/api/types.ts +++ b/src/core/api/types.ts @@ -247,3 +247,34 @@ type MintTransaction = { * Note: exported as public Type */ export type BuildMintTransactionResponse = MintTransaction | APIError; + +/** + * Note: exported as public Type + */ +export type GetPortfolioTokenBalancesParams = { + addresses: Address[] | null | undefined; +}; + +/** + * Note: exported as public Type + */ +export type PortfolioTokenWithFiatValue = Token & { + crypto_balance: number; + fiat_balance: number; +}; + +/** + * Note: exported as public Type + */ +export type PortfolioTokenBalances = { + address: Address; + token_balances: PortfolioTokenWithFiatValue[]; + portfolio_balance_usd: number; +}; + +/** + * Note: exported as public Type + */ +export type GetPortfolioTokenBalancesResponse = { + tokens: PortfolioTokenBalances[] | APIError; // TODO: rename the response key to portfolio +}; diff --git a/src/wallet/components/WalletIslandProvider.tsx b/src/wallet/components/WalletIslandProvider.tsx index d0b7b188c7..2e31160100 100644 --- a/src/wallet/components/WalletIslandProvider.tsx +++ b/src/wallet/components/WalletIslandProvider.tsx @@ -1,9 +1,9 @@ import { useValue } from '@/core-react/internal/hooks/useValue'; -import { usePortfolioTokenBalances } from '@/core-react/wallet/usePortfolioTokenBalances'; +import { usePortfolioTokenBalances } from '@/core-react/wallet/hooks/usePortfolioTokenBalances'; import type { - Portfolio, + PortfolioTokenBalances, PortfolioTokenWithFiatValue, -} from '@/core/api/getPortfolioTokenBalances'; +} from '@/core/api/types'; import type { QueryObserverResult } from '@tanstack/react-query'; import { type Dispatch, @@ -26,7 +26,7 @@ export type WalletIslandContextType = { setIsQrClosing: Dispatch>; tokenBalances: PortfolioTokenWithFiatValue[] | undefined; portfolioFiatValue: number | undefined; - refetchPortfolioData: () => Promise>; + refetchPortfolioData: () => Promise>; isFetchingPortfolioData: boolean; portfolioDataUpdatedAt: number | undefined; }; From 3888b574b1b77f593758debf9ec3ceeacfb30ac2 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sat, 4 Jan 2025 12:46:54 -0800 Subject: [PATCH 041/150] removing unused files, adding tests --- .../utils/getAddressTokenBalances.test.tsx | 136 ------------------ .../utils/getAddressTokenBalances.tsx | 135 ----------------- .../hooks/usePortfolioTokenBalances.test.tsx | 0 .../wallet/usePortfolioTokenBalances.ts | 34 ----- .../api/getPortfolioTokenBalances.test.ts | 0 5 files changed, 305 deletions(-) delete mode 100644 src/core-react/internal/utils/getAddressTokenBalances.test.tsx delete mode 100644 src/core-react/internal/utils/getAddressTokenBalances.tsx create mode 100644 src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx delete mode 100644 src/core-react/wallet/usePortfolioTokenBalances.ts create mode 100644 src/core/api/getPortfolioTokenBalances.test.ts diff --git a/src/core-react/internal/utils/getAddressTokenBalances.test.tsx b/src/core-react/internal/utils/getAddressTokenBalances.test.tsx deleted file mode 100644 index a199ece8ab..0000000000 --- a/src/core-react/internal/utils/getAddressTokenBalances.test.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - type TokenBalanceWithFiatValue, - getAddressTokenBalances, -} from './getAddressTokenBalances'; - -describe('getAddressTokenBalances', () => { - it('should return an empty array for an invalid address', async () => { - const result = await getAddressTokenBalances( - 'invalid-address' as `0x${string}`, - ); - expect(result).toEqual([]); - }); - - it('should return an empty array for a null address', async () => { - const result = await getAddressTokenBalances( - null as unknown as `0x${string}`, - ); - expect(result).toEqual([]); - }); - - it('should return an array of token balances for a valid address', async () => { - const tokenBalances: TokenBalanceWithFiatValue[] = [ - { - token: { - name: 'Ether', - address: '', - symbol: 'ETH', - decimals: 18, - image: - 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', - chainId: 8453, - }, - balance: 0.42, - valueInFiat: 1386, - }, - { - token: { - name: 'USD Coin', - address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', - symbol: 'USDC', - decimals: 6, - image: - 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', - chainId: 8453, - }, - balance: 69, - valueInFiat: 69, - }, - { - token: { - name: 'Ether', - address: '', - symbol: 'ETH', - decimals: 18, - image: - 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', - chainId: 8453, - }, - balance: 0.42, - valueInFiat: 1386, - }, - { - token: { - name: 'USD Coin', - address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', - symbol: 'USDC', - decimals: 6, - image: - 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', - chainId: 8453, - }, - balance: 69, - valueInFiat: 69, - }, - { - token: { - name: 'Ether', - address: '', - symbol: 'ETH', - decimals: 18, - image: - 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', - chainId: 8453, - }, - balance: 0.42, - valueInFiat: 1386, - }, - { - token: { - name: 'USD Coin', - address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', - symbol: 'USDC', - decimals: 6, - image: - 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', - chainId: 8453, - }, - balance: 69, - valueInFiat: 69, - }, - { - token: { - name: 'Ether', - address: '', - symbol: 'ETH', - decimals: 18, - image: - 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', - chainId: 8453, - }, - balance: 0.42, - valueInFiat: 1386, - }, - { - token: { - name: 'USD Coin', - address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', - symbol: 'USDC', - decimals: 6, - image: - 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', - chainId: 8453, - }, - balance: 69, - valueInFiat: 69, - }, - ]; - const result = await getAddressTokenBalances( - '0x0000000000000000000000000000000000000000', - ); - expect(result).toEqual( - tokenBalances.sort((a, b) => b.valueInFiat - a.valueInFiat), - ); - }); -}); diff --git a/src/core-react/internal/utils/getAddressTokenBalances.tsx b/src/core-react/internal/utils/getAddressTokenBalances.tsx deleted file mode 100644 index 9deae4b635..0000000000 --- a/src/core-react/internal/utils/getAddressTokenBalances.tsx +++ /dev/null @@ -1,135 +0,0 @@ -// import type { Address } from 'viem'; -// import { base } from 'viem/chains'; -// import { sendRequest } from '../../network/request'; -import type { Token } from '@/token'; - -export type TokenBalanceWithFiatValue = { - token: Token; - /** Token: - * address: Address | ""; - * chainId: number; - * decimals: number; - * image: string | null; - * name: string; - * symbol: string; - */ - balance: number; - valueInFiat: number; -}; - -export async function getAddressTokenBalances( - address: `0x${string}`, -): Promise { - if (!address || address.slice(0, 2) !== '0x' || address.length !== 42) { - return []; - } - - const tokenBalances: TokenBalanceWithFiatValue[] = [ - { - token: { - name: 'Ether', - address: '', - symbol: 'ETH', - decimals: 18, - image: - 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', - chainId: 8453, - }, - balance: 0.42, - valueInFiat: 1386, - }, - { - token: { - name: 'USD Coin', - address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', - symbol: 'USDC', - decimals: 6, - image: - 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', - chainId: 8453, - }, - balance: 69, - valueInFiat: 69, - }, - { - token: { - name: 'Ether', - address: '', - symbol: 'ETH', - decimals: 18, - image: - 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', - chainId: 8453, - }, - balance: 0.42, - valueInFiat: 1386, - }, - { - token: { - name: 'USD Coin', - address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', - symbol: 'USDC', - decimals: 6, - image: - 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', - chainId: 8453, - }, - balance: 69, - valueInFiat: 69, - }, - { - token: { - name: 'Ether', - address: '', - symbol: 'ETH', - decimals: 18, - image: - 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', - chainId: 8453, - }, - balance: 0.42, - valueInFiat: 1386, - }, - { - token: { - name: 'USD Coin', - address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', - symbol: 'USDC', - decimals: 6, - image: - 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', - chainId: 8453, - }, - balance: 69, - valueInFiat: 69, - }, - { - token: { - name: 'Ether', - address: '', - symbol: 'ETH', - decimals: 18, - image: - 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', - chainId: 8453, - }, - balance: 0.42, - valueInFiat: 1386, - }, - { - token: { - name: 'USD Coin', - address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', - symbol: 'USDC', - decimals: 6, - image: - 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', - chainId: 8453, - }, - balance: 69, - valueInFiat: 69, - }, - ]; - - return tokenBalances.sort((a, b) => b.valueInFiat - a.valueInFiat); -} diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/core-react/wallet/usePortfolioTokenBalances.ts b/src/core-react/wallet/usePortfolioTokenBalances.ts deleted file mode 100644 index a572168d37..0000000000 --- a/src/core-react/wallet/usePortfolioTokenBalances.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - type GetPortfolioTokenBalancesParams, - type Portfolio, - getPortfolioTokenBalances, -} from '@/core/api/getPortfolioTokenBalances'; -import { isApiError } from '@/core/utils/isApiResponseError'; -import { type UseQueryResult, useQuery } from '@tanstack/react-query'; - -export function usePortfolioTokenBalances({ - addresses, -}: GetPortfolioTokenBalancesParams): UseQueryResult { - const actionKey = `usePortfolioTokenBalances-${addresses}`; - return useQuery({ - queryKey: ['usePortfolioTokenBalances', actionKey], - queryFn: async () => { - const response = await getPortfolioTokenBalances({ - addresses, - }); - - if (isApiError(response)) { - throw new Error(response.message); - } - - return response.tokens; - }, - retry: false, - enabled: !!addresses && addresses.length > 0, - refetchOnWindowFocus: false, - staleTime: 1000 * 60 * 5, // refresh on mount every 5 minutes - refetchOnMount: true, - refetchInterval: 1000 * 60 * 15, // refresh in background every 15 minutes - refetchIntervalInBackground: true, - }); -} diff --git a/src/core/api/getPortfolioTokenBalances.test.ts b/src/core/api/getPortfolioTokenBalances.test.ts new file mode 100644 index 0000000000..e69de29bb2 From bc98a5b6482cbd4e9ca88333a987f17c90519722 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sat, 4 Jan 2025 13:20:59 -0800 Subject: [PATCH 042/150] add: tests --- .../hooks/usePortfolioTokenBalances.test.tsx | 106 ++++++++++++++++++ .../api/getPortfolioTokenBalances.test.ts | 93 +++++++++++++++ 2 files changed, 199 insertions(+) diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx index e69de29bb2..a680f14945 100644 --- a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx @@ -0,0 +1,106 @@ +import { getPortfolioTokenBalances } from '@/core/api/getPortfolioTokenBalances'; +import type { + PortfolioTokenWithFiatValue, + PortfolioTokenBalances, +} from '@/core/api/types'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { renderHook, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { usePortfolioTokenBalances } from './usePortfolioTokenBalances'; + +vi.mock('@/core/api/getPortfolioTokenBalances'); + +const mockAddresses: `0x${string}`[] = ['0x123']; +const mockTokens: PortfolioTokenWithFiatValue[] = [ + { + address: '0x123', + chainId: 8453, + decimals: 6, + image: '', + name: 'Token', + symbol: 'TOKEN', + crypto_balance: 100, + fiat_balance: 100, + }, +]; +const mockPortfolioTokenBalances: PortfolioTokenBalances[] = [ + { + address: mockAddresses[0], + token_balances: mockTokens, + portfolio_balance_usd: 100, + }, +]; + +const createWrapper = () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + return ({ children }: { children: React.ReactNode }) => ( + {children} + ); +}; + +describe('usePortfolioTokenBalances', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should fetch token balances successfully', async () => { + vi.mocked(getPortfolioTokenBalances).mockResolvedValueOnce({ + tokens: mockPortfolioTokenBalances, + }); + + const { result } = renderHook( + () => usePortfolioTokenBalances({ addresses: mockAddresses }), + { wrapper: createWrapper() }, + ); + + expect(result.current.isLoading).toBe(true); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toEqual(mockPortfolioTokenBalances); + expect(getPortfolioTokenBalances).toHaveBeenCalledWith({ + addresses: mockAddresses, + }); + }); + + it('should handle API errors', async () => { + vi.mocked(getPortfolioTokenBalances).mockResolvedValueOnce({ + code: 'API Error', + error: 'API Error', + message: 'API Error', + }); + + const { result } = renderHook( + () => usePortfolioTokenBalances({ addresses: mockAddresses }), + { wrapper: createWrapper() }, + ); + + await waitFor(() => expect(result.current.isError).toBe(true)); + + expect(result.current.error).toBeInstanceOf(Error); + expect(result.current.error?.message).toBe('API Error'); + }); + + it('should not fetch when addresses is empty', () => { + renderHook( + () => usePortfolioTokenBalances({ addresses: [] }), + { wrapper: createWrapper() }, + ); + + expect(getPortfolioTokenBalances).not.toHaveBeenCalled(); + }); + + it('should not fetch when addresses is undefined', () => { + renderHook(() => usePortfolioTokenBalances({ addresses: undefined }), { + wrapper: createWrapper(), + }); + + expect(getPortfolioTokenBalances).not.toHaveBeenCalled(); + }); +}); diff --git a/src/core/api/getPortfolioTokenBalances.test.ts b/src/core/api/getPortfolioTokenBalances.test.ts index e69de29bb2..d883406e14 100644 --- a/src/core/api/getPortfolioTokenBalances.test.ts +++ b/src/core/api/getPortfolioTokenBalances.test.ts @@ -0,0 +1,93 @@ +import type { + PortfolioTokenBalances, + PortfolioTokenWithFiatValue, +} from '@/core/api/types'; +import { type Mock, describe, expect, it, vi } from 'vitest'; +import { getPortfolioTokenBalances } from './getPortfolioTokenBalances'; +import { sendRequest } from '../network/request'; +import { CDP_GET_PORTFOLIO_TOKEN_BALANCES } from '../network/definitions/wallet'; + +vi.mock('../network/request', () => ({ + sendRequest: vi.fn(), +})); + +const mockAddresses: `0x${string}`[] = ['0x123']; +const mockTokens: PortfolioTokenWithFiatValue[] = [ + { + address: '0x123', + chainId: 8453, + decimals: 6, + image: '', + name: 'Token', + symbol: 'TOKEN', + crypto_balance: 100, + fiat_balance: 100, + }, +]; +const mockPortfolioTokenBalances: PortfolioTokenBalances[] = [ + { + address: mockAddresses[0], + token_balances: mockTokens, + portfolio_balance_usd: 100, + }, +]; + +describe('getPortfolioTokenBalances', () => { + const mockSendRequest = sendRequest as Mock; + + const mockSuccessResponse = { + tokens: mockPortfolioTokenBalances, + }; + + it('should return token balances on successful request', async () => { + mockSendRequest.mockResolvedValueOnce({ + result: mockSuccessResponse, + }); + + const result = await getPortfolioTokenBalances({ + addresses: mockAddresses as `0x${string}`[], + }); + + expect(result).toEqual(mockSuccessResponse); + expect(mockSendRequest).toHaveBeenCalledWith(CDP_GET_PORTFOLIO_TOKEN_BALANCES, [ + { addresses: mockAddresses }, + ]); + }); + + it('should handle API error response', async () => { + const mockError = { + code: 500, + error: 'Internal Server Error', + message: 'Internal Server Error', + }; + + mockSendRequest.mockResolvedValueOnce({ + error: mockError, + }); + + const result = await getPortfolioTokenBalances({ + addresses: mockAddresses, + }); + + expect(result).toEqual({ + code: '500', + error: 'Error fetching portfolio token balances', + message: 'Internal Server Error', + }); + }); + + it('should handle unexpected errors', async () => { + const errorMessage = 'Network Error'; + mockSendRequest.mockRejectedValueOnce(new Error(errorMessage)); + + const result = await getPortfolioTokenBalances({ + addresses: mockAddresses, + }); + + expect(result).toEqual({ + code: 'uncaught-portfolio', + error: 'Something went wrong', + message: `Error fetching portfolio token balances: Error: ${errorMessage}`, + }); + }); +}); From 731cddb59ac913e187b509d28e2dc7c4cfeed96a Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sat, 4 Jan 2025 13:38:32 -0800 Subject: [PATCH 043/150] fix: tests --- src/wallet/components/WalletIsland.test.tsx | 8 ++++++ .../components/WalletIslandProvider.test.tsx | 25 ++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/wallet/components/WalletIsland.test.tsx b/src/wallet/components/WalletIsland.test.tsx index b36ea50d1a..1728e2a695 100644 --- a/src/wallet/components/WalletIsland.test.tsx +++ b/src/wallet/components/WalletIsland.test.tsx @@ -19,6 +19,13 @@ vi.mock('./WalletIslandContent', () => ({ ), })); +vi.mock('./WalletIslandProvider', () => ({ + useWalletIslandContext: vi.fn(), + WalletIslandProvider: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), +})); + vi.mock('./WalletProvider', () => ({ useWalletContext: vi.fn(), WalletProvider: ({ children }: { children: React.ReactNode }) => ( @@ -26,6 +33,7 @@ vi.mock('./WalletProvider', () => ({ ), })); + describe('WalletIsland', () => { const mockUseWalletContext = useWalletContext as ReturnType; diff --git a/src/wallet/components/WalletIslandProvider.test.tsx b/src/wallet/components/WalletIslandProvider.test.tsx index 59ff7bf425..629f2e230a 100644 --- a/src/wallet/components/WalletIslandProvider.test.tsx +++ b/src/wallet/components/WalletIslandProvider.test.tsx @@ -1,3 +1,4 @@ +import { usePortfolioTokenBalances } from '@/core-react/wallet/hooks/usePortfolioTokenBalances'; import { render, renderHook } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useAccount } from 'wagmi'; @@ -11,6 +12,10 @@ vi.mock('wagmi', () => ({ useAccount: vi.fn(), })); +vi.mock('../../core-react/wallet/hooks/usePortfolioTokenBalances', () => ({ + usePortfolioTokenBalances: vi.fn(), +})); + vi.mock('./WalletProvider', () => ({ useWalletContext: vi.fn(), WalletProvider: ({ children }: { children: React.ReactNode }) => ( @@ -26,11 +31,25 @@ describe('useWalletIslandContext', () => { isClosing: false, }; + const mockUsePortfolioTokenBalances = usePortfolioTokenBalances as ReturnType< + typeof vi.fn + >; + beforeEach(() => { mockUseAccount.mockReturnValue({ address: '0x123', }); mockUseWalletContext.mockReturnValue(defaultWalletContext); + mockUsePortfolioTokenBalances.mockReturnValue({ + data: [{ + address: '0x123', + token_balances: [], + portfolio_balance_usd: 0, + }], + refetch: vi.fn(), + isFetching: false, + dataUpdatedAt: new Date(), + }); }); it('should provide wallet island context when used within provider', () => { @@ -47,7 +66,11 @@ describe('useWalletIslandContext', () => { setShowQr: expect.any(Function), isQrClosing: false, setIsQrClosing: expect.any(Function), - tokenHoldings: expect.any(Array), + tokenBalances: expect.any(Array), + portfolioFiatValue: expect.any(Number), + refetchPortfolioData: expect.any(Function), + isFetchingPortfolioData: false, + portfolioDataUpdatedAt: expect.any(Date), }); }); From d347a65c5551fce37a9e9d5bd7e3a8cd6812d898 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sat, 4 Jan 2025 13:55:56 -0800 Subject: [PATCH 044/150] fix: tests --- .../WalletIslandTokenHoldings.test.tsx | 36 ++++++++++++------- .../WalletIslandWalletActions.test.tsx | 31 ++++++++++------ 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/src/wallet/components/WalletIslandTokenHoldings.test.tsx b/src/wallet/components/WalletIslandTokenHoldings.test.tsx index a9f5daac98..ed3db8ad94 100644 --- a/src/wallet/components/WalletIslandTokenHoldings.test.tsx +++ b/src/wallet/components/WalletIslandTokenHoldings.test.tsx @@ -16,10 +16,11 @@ describe('WalletIslandTokenHoldings', () => { >; const defaultMockUseWalletIslandContext = { - animationClasses: { - tokenHoldings: 'animate-walletIslandContainerItem4', - }, - tokenHoldings: [], + tokenBalances: [], + portfolioFiatValue: 0, + refetchPortfolioData: vi.fn(), + isFetchingPortfolioData: false, + portfolioDataUpdatedAt: new Date(), }; beforeEach(() => { @@ -29,7 +30,24 @@ describe('WalletIslandTokenHoldings', () => { ); }); - it('renders the WalletIslandTokenHoldings component with tokens', () => { + it('does not render token lists with zero tokens', () => { + render(); + + expect(screen.queryByTestId('ockWalletIsland_TokenHoldings')).toBeNull(); + }); + + it('renders a spinner when fetcher is loading', () => { + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + isFetchingPortfolioData: true, + }); + + render(); + + expect(screen.getByTestId('ockSpinner')).toBeDefined(); + }); + + it('renders the WalletIslandTokenHoldings component with tokens when user has tokens and fetcher is not loading', () => { const tokens = [ { token: { @@ -74,17 +92,11 @@ describe('WalletIslandTokenHoldings', () => { mockUseWalletIslandContext.mockReturnValue({ ...defaultMockUseWalletIslandContext, - tokenHoldings: tokens, + tokenBalances: tokens, }); render(); expect(screen.getByTestId('ockWalletIsland_TokenHoldings')).toBeDefined(); }); - - it('does not render token lists with zero tokens', () => { - render(); - - expect(screen.queryByTestId('ockWalletIsland_TokenHoldings')).toBeNull(); - }); }); diff --git a/src/wallet/components/WalletIslandWalletActions.test.tsx b/src/wallet/components/WalletIslandWalletActions.test.tsx index d994943389..f0c9bdb8d0 100644 --- a/src/wallet/components/WalletIslandWalletActions.test.tsx +++ b/src/wallet/components/WalletIslandWalletActions.test.tsx @@ -70,7 +70,7 @@ describe('WalletIslandWalletActions', () => { expect( screen.getByTestId('ockWalletIsland_DisconnectButton'), ).toBeDefined(); - expect(screen.getByTestId('ockWalletIsland_CollapseButton')).toBeDefined(); + expect(screen.getByTestId('ockWalletIsland_RefreshButton')).toBeDefined(); }); it('disconnects connectors and closes when disconnect button is clicked', () => { @@ -118,24 +118,35 @@ describe('WalletIslandWalletActions', () => { expect(setShowQrMock).toHaveBeenCalled(); }); - it('closes when collapse button is clicked', () => { - const handleCloseMock = vi.fn(); - mockUseWalletContext.mockReturnValue({ + it('refreshes portfolio data when refresh button is clicked and data is not stale', () => { + const refetchPortfolioDataMock = vi.fn(); + mockUseWalletIslandContext.mockReturnValue({ ...defaultMockUseWalletIslandContext, - handleClose: handleCloseMock, + refetchPortfolioData: refetchPortfolioDataMock, + portfolioDataUpdatedAt: Date.now() - 1000 * 15 - 1, }); - const setShowQrMock = vi.fn(); + render(); + + const refreshButton = screen.getByTestId('ockWalletIsland_RefreshButton'); + fireEvent.click(refreshButton); + + expect(refetchPortfolioDataMock).toHaveBeenCalled(); + }); + + it('does not refresh portfolio data when data is not stale', () => { + const refetchPortfolioDataMock = vi.fn(); mockUseWalletIslandContext.mockReturnValue({ ...defaultMockUseWalletIslandContext, - setShowQr: setShowQrMock, + refetchPortfolioData: refetchPortfolioDataMock, + portfolioDataUpdatedAt: Date.now() - 1000 * 14, }); render(); - const collapseButton = screen.getByTestId('ockWalletIsland_CollapseButton'); - fireEvent.click(collapseButton); + const refreshButton = screen.getByTestId('ockWalletIsland_RefreshButton'); + fireEvent.click(refreshButton); - expect(handleCloseMock).toHaveBeenCalled(); + expect(refetchPortfolioDataMock).not.toHaveBeenCalled(); }); }); From 71c6b5753049bc4f1adac0983d881a4bfb1ad0fb Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sat, 4 Jan 2025 14:00:44 -0800 Subject: [PATCH 045/150] add test --- .../components/WalletIslandProvider.test.tsx | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/wallet/components/WalletIslandProvider.test.tsx b/src/wallet/components/WalletIslandProvider.test.tsx index 629f2e230a..3adc98c9d2 100644 --- a/src/wallet/components/WalletIslandProvider.test.tsx +++ b/src/wallet/components/WalletIslandProvider.test.tsx @@ -41,11 +41,13 @@ describe('useWalletIslandContext', () => { }); mockUseWalletContext.mockReturnValue(defaultWalletContext); mockUsePortfolioTokenBalances.mockReturnValue({ - data: [{ - address: '0x123', - token_balances: [], - portfolio_balance_usd: 0, - }], + data: [ + { + address: '0x123', + token_balances: [], + portfolio_balance_usd: 0, + }, + ], refetch: vi.fn(), isFetching: false, dataUpdatedAt: new Date(), @@ -90,4 +92,30 @@ describe('useWalletIslandContext', () => { // Restore console.error console.error = originalError; }); + + it('should call usePortfolioTokenBalances with the correct address', () => { + mockUseWalletContext.mockReturnValue({ + address: null, + isClosing: false, + }); + + const { rerender } = renderHook(() => useWalletIslandContext(), { + wrapper: WalletIslandProvider, + }); + + expect(mockUsePortfolioTokenBalances).toHaveBeenCalledWith({ + addresses: ['0x000'], + }); + + mockUseWalletContext.mockReturnValue({ + address: '0x123', + isClosing: false, + }); + + rerender(); + + expect(mockUsePortfolioTokenBalances).toHaveBeenCalledWith({ + addresses: ['0x123'], + }); + }); }); From 11d8dc723ec0102765c117c7eb83c828726f5248 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sat, 4 Jan 2025 14:07:31 -0800 Subject: [PATCH 046/150] add tests --- .../WalletIslandAddressDetails.test.tsx | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/wallet/components/WalletIslandAddressDetails.test.tsx b/src/wallet/components/WalletIslandAddressDetails.test.tsx index 40fa8eea60..fa963d923d 100644 --- a/src/wallet/components/WalletIslandAddressDetails.test.tsx +++ b/src/wallet/components/WalletIslandAddressDetails.test.tsx @@ -152,4 +152,34 @@ describe('WalletIslandAddressDetails', () => { expect(navigator.clipboard.writeText).toHaveBeenCalledWith(''); }); + + it('should show spinner when fetching portfolio data', () => { + mockUseWalletIslandContext.mockReturnValue({ + isFetchingPortfolioData: true, + }); + + render(); + + expect(screen.getByTestId('ockSpinner')).toBeInTheDocument(); + }); + + it('should display formatted portfolio value when available', () => { + mockUseWalletIslandContext.mockReturnValue({ + isFetchingPortfolioData: false, + portfolioFiatValue: null, + }); + + const { rerender } = render(); + + expect(screen.getByTestId('ockWalletIsland_AddressBalance')).toHaveTextContent(''); + + mockUseWalletIslandContext.mockReturnValue({ + isFetchingPortfolioData: false, + portfolioFiatValue: '1234.567' + }); + + rerender(); + + expect(screen.getByTestId('ockWalletIsland_AddressBalance')).toHaveTextContent('$1234.57'); + }); }); From 8f3665aeb64b8a609ffe8f8bf23f691828bd7f90 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sat, 4 Jan 2025 14:11:10 -0800 Subject: [PATCH 047/150] fix: linters --- .../wallet/hooks/usePortfolioTokenBalances.test.tsx | 9 ++++----- src/core/api/getPortfolioTokenBalances.test.ts | 11 ++++++----- src/wallet/components/WalletIsland.test.tsx | 1 - .../components/WalletIslandAddressDetails.test.tsx | 10 +++++++--- src/wallet/components/WalletIslandProvider.tsx | 6 ++++-- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx index a680f14945..87a1169ae6 100644 --- a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx @@ -1,7 +1,7 @@ import { getPortfolioTokenBalances } from '@/core/api/getPortfolioTokenBalances'; import type { - PortfolioTokenWithFiatValue, PortfolioTokenBalances, + PortfolioTokenWithFiatValue, } from '@/core/api/types'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { renderHook, waitFor } from '@testing-library/react'; @@ -88,10 +88,9 @@ describe('usePortfolioTokenBalances', () => { }); it('should not fetch when addresses is empty', () => { - renderHook( - () => usePortfolioTokenBalances({ addresses: [] }), - { wrapper: createWrapper() }, - ); + renderHook(() => usePortfolioTokenBalances({ addresses: [] }), { + wrapper: createWrapper(), + }); expect(getPortfolioTokenBalances).not.toHaveBeenCalled(); }); diff --git a/src/core/api/getPortfolioTokenBalances.test.ts b/src/core/api/getPortfolioTokenBalances.test.ts index d883406e14..064e01c314 100644 --- a/src/core/api/getPortfolioTokenBalances.test.ts +++ b/src/core/api/getPortfolioTokenBalances.test.ts @@ -3,9 +3,9 @@ import type { PortfolioTokenWithFiatValue, } from '@/core/api/types'; import { type Mock, describe, expect, it, vi } from 'vitest'; -import { getPortfolioTokenBalances } from './getPortfolioTokenBalances'; -import { sendRequest } from '../network/request'; import { CDP_GET_PORTFOLIO_TOKEN_BALANCES } from '../network/definitions/wallet'; +import { sendRequest } from '../network/request'; +import { getPortfolioTokenBalances } from './getPortfolioTokenBalances'; vi.mock('../network/request', () => ({ sendRequest: vi.fn(), @@ -49,9 +49,10 @@ describe('getPortfolioTokenBalances', () => { }); expect(result).toEqual(mockSuccessResponse); - expect(mockSendRequest).toHaveBeenCalledWith(CDP_GET_PORTFOLIO_TOKEN_BALANCES, [ - { addresses: mockAddresses }, - ]); + expect(mockSendRequest).toHaveBeenCalledWith( + CDP_GET_PORTFOLIO_TOKEN_BALANCES, + [{ addresses: mockAddresses }], + ); }); it('should handle API error response', async () => { diff --git a/src/wallet/components/WalletIsland.test.tsx b/src/wallet/components/WalletIsland.test.tsx index 1728e2a695..6eaf60987d 100644 --- a/src/wallet/components/WalletIsland.test.tsx +++ b/src/wallet/components/WalletIsland.test.tsx @@ -33,7 +33,6 @@ vi.mock('./WalletProvider', () => ({ ), })); - describe('WalletIsland', () => { const mockUseWalletContext = useWalletContext as ReturnType; diff --git a/src/wallet/components/WalletIslandAddressDetails.test.tsx b/src/wallet/components/WalletIslandAddressDetails.test.tsx index fa963d923d..3cd8d03c1c 100644 --- a/src/wallet/components/WalletIslandAddressDetails.test.tsx +++ b/src/wallet/components/WalletIslandAddressDetails.test.tsx @@ -171,15 +171,19 @@ describe('WalletIslandAddressDetails', () => { const { rerender } = render(); - expect(screen.getByTestId('ockWalletIsland_AddressBalance')).toHaveTextContent(''); + expect( + screen.getByTestId('ockWalletIsland_AddressBalance'), + ).toHaveTextContent(''); mockUseWalletIslandContext.mockReturnValue({ isFetchingPortfolioData: false, - portfolioFiatValue: '1234.567' + portfolioFiatValue: '1234.567', }); rerender(); - expect(screen.getByTestId('ockWalletIsland_AddressBalance')).toHaveTextContent('$1234.57'); + expect( + screen.getByTestId('ockWalletIsland_AddressBalance'), + ).toHaveTextContent('$1234.57'); }); }); diff --git a/src/wallet/components/WalletIslandProvider.tsx b/src/wallet/components/WalletIslandProvider.tsx index 2e31160100..0d9b7d16e9 100644 --- a/src/wallet/components/WalletIslandProvider.tsx +++ b/src/wallet/components/WalletIslandProvider.tsx @@ -26,9 +26,11 @@ export type WalletIslandContextType = { setIsQrClosing: Dispatch>; tokenBalances: PortfolioTokenWithFiatValue[] | undefined; portfolioFiatValue: number | undefined; - refetchPortfolioData: () => Promise>; isFetchingPortfolioData: boolean; portfolioDataUpdatedAt: number | undefined; + refetchPortfolioData: () => Promise< + QueryObserverResult + >; }; type WalletIslandProviderReact = { @@ -77,9 +79,9 @@ export function WalletIslandProvider({ children }: WalletIslandProviderReact) { setIsQrClosing, tokenBalances, portfolioFiatValue, - refetchPortfolioData, isFetchingPortfolioData, portfolioDataUpdatedAt, + refetchPortfolioData, }); return ( From b333c34231cab7cf29172dce4568b266e6c7a2b0 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sat, 4 Jan 2025 14:56:03 -0800 Subject: [PATCH 048/150] specify swap tokens --- .../components/WalletIslandContent.test.tsx | 61 ++++++++++++++++++- src/wallet/components/WalletIslandContent.tsx | 38 +++++++++++- src/wallet/types.ts | 4 +- 3 files changed, 97 insertions(+), 6 deletions(-) diff --git a/src/wallet/components/WalletIslandContent.test.tsx b/src/wallet/components/WalletIslandContent.test.tsx index 1d26821105..2831870ccb 100644 --- a/src/wallet/components/WalletIslandContent.test.tsx +++ b/src/wallet/components/WalletIslandContent.test.tsx @@ -1,3 +1,4 @@ +import type { SwapDefaultReact } from '@/swap/types'; import { fireEvent, render, screen } from '@testing-library/react'; import type { RefObject } from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -23,8 +24,13 @@ vi.mock('./WalletIslandQrReceive', () => ({ })); vi.mock('./WalletIslandSwap', () => ({ - WalletIslandSwap: () => ( -
WalletIslandSwap
+ WalletIslandSwap: ({ from, to }: SwapDefaultReact) => ( +
+ WalletIslandSwap +
), })); @@ -162,6 +168,57 @@ describe('WalletIslandContent', () => { ).toHaveClass('hidden'); }); + it('correctly maps token balances to the swap component', () => { + const mockTokenBalances = [ + { + address: '0x123', + chainId: 1, + symbol: 'TEST', + decimals: 18, + image: 'test.png', + name: 'Test Token', + }, + { + address: '0x456', + chainId: 2, + symbol: 'TEST2', + decimals: 6, + image: 'test2.png', + name: 'Test Token 2', + }, + ]; + + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + showSwap: true, + tokenBalances: mockTokenBalances, + }); + + mockUseWalletContext.mockReturnValue({ isClosing: false }); + + render( + +
WalletIslandContent
+
, + ); + + const swapComponent = screen.getByTestId('ockWalletIslandSwap'); + const props = JSON.parse( + swapComponent.getAttribute('data-props') as string, + ); + + expect(props.from).toEqual( + mockTokenBalances.map((token) => ({ + address: token.address, + chainId: token.chainId, + symbol: token.symbol, + decimals: token.decimals, + image: token.image, + name: token.name, + })), + ); + }); + it('correctly positions WalletIslandContent when there is enough space on the right', () => { const mockRect = { left: 100, diff --git a/src/wallet/components/WalletIslandContent.tsx b/src/wallet/components/WalletIslandContent.tsx index 63356cd635..276b40ecb6 100644 --- a/src/wallet/components/WalletIslandContent.tsx +++ b/src/wallet/components/WalletIslandContent.tsx @@ -1,7 +1,9 @@ import { useTheme } from '@/core-react/internal/hooks/useTheme'; import { Draggable } from '@/internal/components/Draggable'; import { background, border, cn, text } from '@/styles/theme'; +import type { Token } from '@/token'; import { useMemo } from 'react'; +import { base } from 'viem/chains'; import type { WalletIslandProps } from '../types'; import { useWalletIslandContext } from './WalletIslandProvider'; import { WalletIslandQrReceive } from './WalletIslandQrReceive'; @@ -10,13 +12,34 @@ import { useWalletContext } from './WalletProvider'; const WALLET_ISLAND_WIDTH = 352; const WALLET_ISLAND_HEIGHT = 394; +const WALLET_ISLAND_DEFAULT_SWAPPABLE_TOKENS: Token[] = [ + { + name: 'ETH', + address: '', + symbol: 'ETH', + decimals: 18, + image: + 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: base.id, + }, + { + name: 'USDC', + address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', + symbol: 'USDC', + decimals: 6, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', + chainId: base.id, + }, +]; export function WalletIslandContent({ children, + swappableTokens, walletContainerRef, }: WalletIslandProps) { const { isClosing, setIsOpen, setIsClosing } = useWalletContext(); - const { showQr, showSwap, tokenHoldings } = useWalletIslandContext(); + const { showQr, showSwap, tokenBalances } = useWalletIslandContext(); const componentTheme = useTheme(); const position = useMemo(() => { @@ -100,8 +123,17 @@ export function WalletIslandContent({ Swap
} - to={tokenHoldings?.map((token) => token.token)} - from={tokenHoldings?.map((token) => token.token)} + to={swappableTokens ?? WALLET_ISLAND_DEFAULT_SWAPPABLE_TOKENS} + from={ + tokenBalances?.map((token) => ({ + address: token.address, + chainId: token.chainId, + symbol: token.symbol, + decimals: token.decimals, + image: token.image, + name: token.name, + })) ?? [] + } className="w-full p-2" />
diff --git a/src/wallet/types.ts b/src/wallet/types.ts index 691cbb78c6..63c563f4cb 100644 --- a/src/wallet/types.ts +++ b/src/wallet/types.ts @@ -1,8 +1,9 @@ +import type { SwapError } from '@/swap'; +import type { Token } from '@/token'; import type { Dispatch, ReactNode, SetStateAction } from 'react'; import type { Address, Chain, PublicClient } from 'viem'; import type { UserOperation } from 'viem/_types/account-abstraction'; import type { UseBalanceReturnType, UseReadContractReturnType } from 'wagmi'; -import type { SwapError } from '../swap'; export type ConnectButtonReact = { className?: string; // Optional className override for button element @@ -152,5 +153,6 @@ export type WalletDropdownLinkReact = { export type WalletIslandProps = { children: React.ReactNode; + swappableTokens?: Token[]; walletContainerRef?: React.RefObject; }; From 125a2f88d8c7006d9691a9ef86c42791f8e000fd Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 11:05:42 -0800 Subject: [PATCH 049/150] updated type definitions --- src/core/api/types.ts | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/core/api/types.ts b/src/core/api/types.ts index fadcc16e67..a4ecac619f 100644 --- a/src/core/api/types.ts +++ b/src/core/api/types.ts @@ -255,26 +255,42 @@ export type GetPortfolioTokenBalancesParams = { addresses: Address[] | null | undefined; }; +/** + * Note: exported as public Type +*/ +export type PortfolioTokenBalances = { + address: Address; + portfolioBalanceUsd: number; + tokenBalances: PortfolioTokenWithFiatValue[]; +}; + /** * Note: exported as public Type */ export type PortfolioTokenWithFiatValue = Token & { - crypto_balance: number; - fiat_balance: number; + cryptoBalance: number; + fiatBalance: number; }; /** * Note: exported as public Type */ -export type PortfolioTokenBalances = { +export type PortfolioAPIResponse = { address: Address; - token_balances: PortfolioTokenWithFiatValue[]; portfolio_balance_usd: number; + token_balances: PortfolioTokenBalanceAPIResponse[]; }; /** * Note: exported as public Type */ -export type GetPortfolioTokenBalancesResponse = { - tokens: PortfolioTokenBalances[] | APIError; // TODO: rename the response key to portfolio +export type PortfolioTokenBalanceAPIResponse = { + address: Address; + chain_id: number; + decimals: number; + image: string; + name: string; + symbol: string; + crypto_balance: number; + fiat_balance: number; }; From e3afa9c607126b333ce000ba8210e27159e25e4b Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 11:06:40 -0800 Subject: [PATCH 050/150] update type usage --- .../hooks/usePortfolioTokenBalances.test.tsx | 8 ++++---- .../wallet/hooks/usePortfolioTokenBalances.ts | 17 ++++++++++++++++- src/wallet/components/WalletIslandProvider.tsx | 4 ++-- .../components/WalletIslandTokenHoldings.tsx | 4 ++-- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx index 87a1169ae6..f9334fa7e2 100644 --- a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx @@ -19,15 +19,15 @@ const mockTokens: PortfolioTokenWithFiatValue[] = [ image: '', name: 'Token', symbol: 'TOKEN', - crypto_balance: 100, - fiat_balance: 100, + cryptoBalance: 100, + fiatBalance: 100, }, ]; const mockPortfolioTokenBalances: PortfolioTokenBalances[] = [ { address: mockAddresses[0], - token_balances: mockTokens, - portfolio_balance_usd: 100, + tokenBalances: mockTokens, + portfolioBalanceUsd: 100, }, ]; diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts index 660b415a1f..ba825a846a 100644 --- a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts @@ -2,6 +2,8 @@ import { getPortfolioTokenBalances } from '@/core/api/getPortfolioTokenBalances' import type { GetPortfolioTokenBalancesParams, PortfolioTokenBalances, + PortfolioAPIResponse, + PortfolioTokenBalanceAPIResponse, } from '@/core/api/types'; import { isApiError } from '@/core/utils/isApiResponseError'; import { type UseQueryResult, useQuery } from '@tanstack/react-query'; @@ -21,7 +23,20 @@ export function usePortfolioTokenBalances({ throw new Error(response.message); } - return response.tokens; + return response.tokens.map((token: PortfolioAPIResponse) => ({ + address: token.address, + portfolioBalanceUsd: token.portfolio_balance_usd, + tokenBalances: token.token_balances.map((tokenBalance: PortfolioTokenBalanceAPIResponse) => ({ + address: tokenBalance.symbol === 'ETH' ? '' : tokenBalance.address, + chainId: tokenBalance.chain_id, + decimals: tokenBalance.decimals, + image: tokenBalance.image, + name: tokenBalance.name, + symbol: tokenBalance.symbol, + cryptoBalance: tokenBalance.crypto_balance, + fiatBalance: tokenBalance.fiat_balance, + })), + })); }, retry: false, enabled: !!addresses && addresses.length > 0, diff --git a/src/wallet/components/WalletIslandProvider.tsx b/src/wallet/components/WalletIslandProvider.tsx index 0d9b7d16e9..7ba60b1e87 100644 --- a/src/wallet/components/WalletIslandProvider.tsx +++ b/src/wallet/components/WalletIslandProvider.tsx @@ -65,8 +65,8 @@ export function WalletIslandProvider({ children }: WalletIslandProviderReact) { dataUpdatedAt: portfolioDataUpdatedAt, } = usePortfolioTokenBalances({ addresses: [address ?? '0x000'] }); - const portfolioFiatValue = portfolioData?.[0]?.portfolio_balance_usd; - const tokenBalances = portfolioData?.[0]?.token_balances; + const portfolioFiatValue = portfolioData?.[0]?.portfolioBalanceUsd; + const tokenBalances = portfolioData?.[0]?.tokenBalances; const value = useValue({ showSwap, diff --git a/src/wallet/components/WalletIslandTokenHoldings.tsx b/src/wallet/components/WalletIslandTokenHoldings.tsx index b2040a9214..5374a1fbcd 100644 --- a/src/wallet/components/WalletIslandTokenHoldings.tsx +++ b/src/wallet/components/WalletIslandTokenHoldings.tsx @@ -43,10 +43,10 @@ export function WalletIslandTokenHoldings() { symbol: tokenBalance.symbol, }} balance={ - Number(tokenBalance.crypto_balance) / + Number(tokenBalance.cryptoBalance) / 10 ** Number(tokenBalance.decimals) } - valueInFiat={Number(tokenBalance.fiat_balance)} + valueInFiat={Number(tokenBalance.fiatBalance)} /> ))}
From 283c6f777428c9b774a6d3de67ac4931d8a56d4f Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 11:08:03 -0800 Subject: [PATCH 051/150] fix: linters --- .../wallet/hooks/usePortfolioTokenBalances.ts | 24 ++++++++++--------- src/core/api/types.ts | 2 +- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts index ba825a846a..9c56155ecd 100644 --- a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts @@ -1,9 +1,9 @@ import { getPortfolioTokenBalances } from '@/core/api/getPortfolioTokenBalances'; import type { GetPortfolioTokenBalancesParams, - PortfolioTokenBalances, PortfolioAPIResponse, PortfolioTokenBalanceAPIResponse, + PortfolioTokenBalances, } from '@/core/api/types'; import { isApiError } from '@/core/utils/isApiResponseError'; import { type UseQueryResult, useQuery } from '@tanstack/react-query'; @@ -26,16 +26,18 @@ export function usePortfolioTokenBalances({ return response.tokens.map((token: PortfolioAPIResponse) => ({ address: token.address, portfolioBalanceUsd: token.portfolio_balance_usd, - tokenBalances: token.token_balances.map((tokenBalance: PortfolioTokenBalanceAPIResponse) => ({ - address: tokenBalance.symbol === 'ETH' ? '' : tokenBalance.address, - chainId: tokenBalance.chain_id, - decimals: tokenBalance.decimals, - image: tokenBalance.image, - name: tokenBalance.name, - symbol: tokenBalance.symbol, - cryptoBalance: tokenBalance.crypto_balance, - fiatBalance: tokenBalance.fiat_balance, - })), + tokenBalances: token.token_balances.map( + (tokenBalance: PortfolioTokenBalanceAPIResponse) => ({ + address: tokenBalance.symbol === 'ETH' ? '' : tokenBalance.address, + chainId: tokenBalance.chain_id, + decimals: tokenBalance.decimals, + image: tokenBalance.image, + name: tokenBalance.name, + symbol: tokenBalance.symbol, + cryptoBalance: tokenBalance.crypto_balance, + fiatBalance: tokenBalance.fiat_balance, + }), + ), })); }, retry: false, diff --git a/src/core/api/types.ts b/src/core/api/types.ts index a4ecac619f..dc322c18c8 100644 --- a/src/core/api/types.ts +++ b/src/core/api/types.ts @@ -257,7 +257,7 @@ export type GetPortfolioTokenBalancesParams = { /** * Note: exported as public Type -*/ + */ export type PortfolioTokenBalances = { address: Address; portfolioBalanceUsd: number; From 19fba385274929e3f09f2c52d93b9fc10956286d Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 12:03:39 -0800 Subject: [PATCH 052/150] fix tests --- .../hooks/usePortfolioTokenBalances.test.tsx | 28 +++++++++++++++++-- .../components/WalletIslandProvider.test.tsx | 4 +-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx index f9334fa7e2..7e7137e422 100644 --- a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx @@ -1,5 +1,7 @@ import { getPortfolioTokenBalances } from '@/core/api/getPortfolioTokenBalances'; import type { + PortfolioAPIResponse, + PortfolioTokenBalanceAPIResponse, PortfolioTokenBalances, PortfolioTokenWithFiatValue, } from '@/core/api/types'; @@ -11,6 +13,25 @@ import { usePortfolioTokenBalances } from './usePortfolioTokenBalances'; vi.mock('@/core/api/getPortfolioTokenBalances'); const mockAddresses: `0x${string}`[] = ['0x123']; +const mockTokensAPIResponse: PortfolioTokenBalanceAPIResponse[] = [ + { + address: '0x123', + chain_id: 8453, + decimals: 6, + image: '', + name: 'Token', + symbol: 'TOKEN', + crypto_balance: 100, + fiat_balance: 100, + }, +]; +const mockPortfolioTokenBalancesAPIResponse: PortfolioAPIResponse[] = [ + { + address: mockAddresses[0], + portfolio_balance_usd: 100, + token_balances: mockTokensAPIResponse, + }, +]; const mockTokens: PortfolioTokenWithFiatValue[] = [ { address: '0x123', @@ -26,8 +47,8 @@ const mockTokens: PortfolioTokenWithFiatValue[] = [ const mockPortfolioTokenBalances: PortfolioTokenBalances[] = [ { address: mockAddresses[0], - tokenBalances: mockTokens, portfolioBalanceUsd: 100, + tokenBalances: mockTokens, }, ]; @@ -51,7 +72,7 @@ describe('usePortfolioTokenBalances', () => { it('should fetch token balances successfully', async () => { vi.mocked(getPortfolioTokenBalances).mockResolvedValueOnce({ - tokens: mockPortfolioTokenBalances, + tokens: mockPortfolioTokenBalancesAPIResponse, }); const { result } = renderHook( @@ -63,10 +84,11 @@ describe('usePortfolioTokenBalances', () => { await waitFor(() => expect(result.current.isSuccess).toBe(true)); - expect(result.current.data).toEqual(mockPortfolioTokenBalances); expect(getPortfolioTokenBalances).toHaveBeenCalledWith({ addresses: mockAddresses, }); + + expect(result.current.data).toEqual(mockPortfolioTokenBalances); }); it('should handle API errors', async () => { diff --git a/src/wallet/components/WalletIslandProvider.test.tsx b/src/wallet/components/WalletIslandProvider.test.tsx index 3adc98c9d2..b0e8bcb04b 100644 --- a/src/wallet/components/WalletIslandProvider.test.tsx +++ b/src/wallet/components/WalletIslandProvider.test.tsx @@ -44,8 +44,8 @@ describe('useWalletIslandContext', () => { data: [ { address: '0x123', - token_balances: [], - portfolio_balance_usd: 0, + tokenBalances: [], + portfolioBalanceUsd: 0, }, ], refetch: vi.fn(), From a5ebef50ba94f29dc42057f7595ac1d365cf213b Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 12:12:04 -0800 Subject: [PATCH 053/150] fix: types --- src/core/api/getPortfolioTokenBalances.test.ts | 12 ++++++------ src/core/api/getPortfolioTokenBalances.ts | 4 ++-- src/core/api/types.ts | 7 +++++++ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/core/api/getPortfolioTokenBalances.test.ts b/src/core/api/getPortfolioTokenBalances.test.ts index 064e01c314..4f46753d3b 100644 --- a/src/core/api/getPortfolioTokenBalances.test.ts +++ b/src/core/api/getPortfolioTokenBalances.test.ts @@ -1,6 +1,6 @@ import type { - PortfolioTokenBalances, - PortfolioTokenWithFiatValue, + PortfolioAPIResponse, + PortfolioTokenBalanceAPIResponse, } from '@/core/api/types'; import { type Mock, describe, expect, it, vi } from 'vitest'; import { CDP_GET_PORTFOLIO_TOKEN_BALANCES } from '../network/definitions/wallet'; @@ -12,10 +12,10 @@ vi.mock('../network/request', () => ({ })); const mockAddresses: `0x${string}`[] = ['0x123']; -const mockTokens: PortfolioTokenWithFiatValue[] = [ +const mockTokens: PortfolioTokenBalanceAPIResponse[] = [ { address: '0x123', - chainId: 8453, + chain_id: 8453, decimals: 6, image: '', name: 'Token', @@ -24,11 +24,11 @@ const mockTokens: PortfolioTokenWithFiatValue[] = [ fiat_balance: 100, }, ]; -const mockPortfolioTokenBalances: PortfolioTokenBalances[] = [ +const mockPortfolioTokenBalances: PortfolioAPIResponse[] = [ { address: mockAddresses[0], - token_balances: mockTokens, portfolio_balance_usd: 100, + token_balances: mockTokens, }, ]; diff --git a/src/core/api/getPortfolioTokenBalances.ts b/src/core/api/getPortfolioTokenBalances.ts index 20d0bd7684..1d33ec08cb 100644 --- a/src/core/api/getPortfolioTokenBalances.ts +++ b/src/core/api/getPortfolioTokenBalances.ts @@ -2,7 +2,7 @@ import { CDP_GET_PORTFOLIO_TOKEN_BALANCES } from '../network/definitions/wallet' import { sendRequest } from '../network/request'; import type { GetPortfolioTokenBalancesParams, - GetPortfolioTokenBalancesResponse, + GetPortfoliosAPIResponse, } from './types'; export async function getPortfolioTokenBalances({ @@ -11,7 +11,7 @@ export async function getPortfolioTokenBalances({ try { const res = await sendRequest< GetPortfolioTokenBalancesParams, - GetPortfolioTokenBalancesResponse + GetPortfoliosAPIResponse >(CDP_GET_PORTFOLIO_TOKEN_BALANCES, [{ addresses }]); if (res.error) { return { diff --git a/src/core/api/types.ts b/src/core/api/types.ts index dc322c18c8..91bf8a8696 100644 --- a/src/core/api/types.ts +++ b/src/core/api/types.ts @@ -272,6 +272,13 @@ export type PortfolioTokenWithFiatValue = Token & { fiatBalance: number; }; +/** + * Note: exported as public Type + */ +export type GetPortfoliosAPIResponse = { + tokens: PortfolioAPIResponse[]; +}; + /** * Note: exported as public Type */ From 52f75493b464527cd02b8031b62762e481ff7f94 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 12:26:35 -0800 Subject: [PATCH 054/150] add test --- .../hooks/usePortfolioTokenBalances.test.tsx | 62 +++++++++++++++++++ src/core/api/types.ts | 2 +- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx index 7e7137e422..108f486913 100644 --- a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx @@ -91,6 +91,68 @@ describe('usePortfolioTokenBalances', () => { expect(result.current.data).toEqual(mockPortfolioTokenBalances); }); + it('should transform the address for ETH to an empty string', async () => { + const mockTokensAPIResponseWithEth: PortfolioTokenBalanceAPIResponse[] = [ + { + address: 'native', + chain_id: 8453, + decimals: 6, + image: '', + name: 'Ethereum', + symbol: 'ETH', + crypto_balance: 100, + fiat_balance: 100, + }, + ]; + const mockPortfolioTokenBalancesAPIResponseWithEth: PortfolioAPIResponse[] = + [ + { + address: mockAddresses[0], + portfolio_balance_usd: 100, + token_balances: mockTokensAPIResponseWithEth, + }, + ]; + + const mockTokensWithEth: PortfolioTokenWithFiatValue[] = [ + { + address: '', + chainId: 8453, + decimals: 6, + image: '', + name: 'Ethereum', + symbol: 'ETH', + cryptoBalance: 100, + fiatBalance: 100, + }, + ]; + const mockPortfolioTokenBalancesWithEth: PortfolioTokenBalances[] = [ + { + address: mockAddresses[0], + portfolioBalanceUsd: 100, + tokenBalances: mockTokensWithEth, + }, + ]; + + vi.mocked(getPortfolioTokenBalances).mockResolvedValueOnce({ + tokens: mockPortfolioTokenBalancesAPIResponseWithEth, + }); + + const { result } = renderHook( + () => usePortfolioTokenBalances({ addresses: mockAddresses }), + { wrapper: createWrapper() }, + ); + + expect(result.current.isLoading).toBe(true); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(getPortfolioTokenBalances).toHaveBeenCalledWith({ + addresses: mockAddresses, + }); + + expect(result.current.data).toEqual(mockPortfolioTokenBalancesWithEth); + }); + it('should handle API errors', async () => { vi.mocked(getPortfolioTokenBalances).mockResolvedValueOnce({ code: 'API Error', diff --git a/src/core/api/types.ts b/src/core/api/types.ts index 91bf8a8696..cbb34416ea 100644 --- a/src/core/api/types.ts +++ b/src/core/api/types.ts @@ -292,7 +292,7 @@ export type PortfolioAPIResponse = { * Note: exported as public Type */ export type PortfolioTokenBalanceAPIResponse = { - address: Address; + address: Address | 'native'; chain_id: number; decimals: number; image: string; From 9ddd27a2e8b1c4f4736caf9d9e6a5ee99f4a777c Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 13:50:32 -0800 Subject: [PATCH 055/150] update spacing --- src/wallet/components/WalletIslandTokenHoldings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wallet/components/WalletIslandTokenHoldings.tsx b/src/wallet/components/WalletIslandTokenHoldings.tsx index 5374a1fbcd..5e4156d4dc 100644 --- a/src/wallet/components/WalletIslandTokenHoldings.tsx +++ b/src/wallet/components/WalletIslandTokenHoldings.tsx @@ -22,7 +22,7 @@ export function WalletIslandTokenHoldings() { className={cn( 'max-h-44 overflow-y-auto', 'flex w-full flex-col items-center gap-4', - 'mt-2 mb-2 px-2', + 'mt-2 mb-2', { 'fade-in slide-in-from-top-2.5 animate-in fill-mode-forwards duration-300 ease-out': !isClosing, From 2ffd2a15391d2d7d120c071cda598db33afcc91e Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 13:50:52 -0800 Subject: [PATCH 056/150] filter zero-balance tokens --- .../wallet/hooks/usePortfolioTokenBalances.ts | 31 +++++++++++++------ .../components/WalletIslandProvider.tsx | 6 ++-- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts index 9c56155ecd..0bd914c57b 100644 --- a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts @@ -1,16 +1,16 @@ import { getPortfolioTokenBalances } from '@/core/api/getPortfolioTokenBalances'; import type { GetPortfolioTokenBalancesParams, - PortfolioAPIResponse, PortfolioTokenBalanceAPIResponse, PortfolioTokenBalances, + PortfolioTokenWithFiatValue, } from '@/core/api/types'; import { isApiError } from '@/core/utils/isApiResponseError'; import { type UseQueryResult, useQuery } from '@tanstack/react-query'; export function usePortfolioTokenBalances({ addresses, -}: GetPortfolioTokenBalancesParams): UseQueryResult { +}: GetPortfolioTokenBalancesParams): UseQueryResult { const actionKey = `usePortfolioTokenBalances-${addresses}`; return useQuery({ queryKey: ['usePortfolioTokenBalances', actionKey], @@ -23,12 +23,15 @@ export function usePortfolioTokenBalances({ throw new Error(response.message); } - return response.tokens.map((token: PortfolioAPIResponse) => ({ - address: token.address, - portfolioBalanceUsd: token.portfolio_balance_usd, - tokenBalances: token.token_balances.map( + const userPortfolio = response.tokens[0]; + + const transformedPortfolio: PortfolioTokenBalances = { + address: userPortfolio.address, + portfolioBalanceUsd: userPortfolio.portfolio_balance_usd, + tokenBalances: userPortfolio.token_balances.map( (tokenBalance: PortfolioTokenBalanceAPIResponse) => ({ - address: tokenBalance.symbol === 'ETH' ? '' : tokenBalance.address, + address: + tokenBalance.symbol === 'ETH' ? '' : tokenBalance.address, chainId: tokenBalance.chain_id, decimals: tokenBalance.decimals, image: tokenBalance.image, @@ -36,9 +39,19 @@ export function usePortfolioTokenBalances({ symbol: tokenBalance.symbol, cryptoBalance: tokenBalance.crypto_balance, fiatBalance: tokenBalance.fiat_balance, - }), + }) as PortfolioTokenWithFiatValue, + ), + }; + + const filteredPortfolio = { + ...transformedPortfolio, + tokenBalances: transformedPortfolio.tokenBalances.filter( + (tokenBalance: PortfolioTokenWithFiatValue) => + tokenBalance.cryptoBalance > 0, ), - })); + }; + + return filteredPortfolio; }, retry: false, enabled: !!addresses && addresses.length > 0, diff --git a/src/wallet/components/WalletIslandProvider.tsx b/src/wallet/components/WalletIslandProvider.tsx index 7ba60b1e87..9778c212f2 100644 --- a/src/wallet/components/WalletIslandProvider.tsx +++ b/src/wallet/components/WalletIslandProvider.tsx @@ -29,7 +29,7 @@ export type WalletIslandContextType = { isFetchingPortfolioData: boolean; portfolioDataUpdatedAt: number | undefined; refetchPortfolioData: () => Promise< - QueryObserverResult + QueryObserverResult >; }; @@ -65,8 +65,8 @@ export function WalletIslandProvider({ children }: WalletIslandProviderReact) { dataUpdatedAt: portfolioDataUpdatedAt, } = usePortfolioTokenBalances({ addresses: [address ?? '0x000'] }); - const portfolioFiatValue = portfolioData?.[0]?.portfolioBalanceUsd; - const tokenBalances = portfolioData?.[0]?.tokenBalances; + const portfolioFiatValue = portfolioData?.portfolioBalanceUsd; + const tokenBalances = portfolioData?.tokenBalances; const value = useValue({ showSwap, From b5804768782b6155b987e329009c842738e30683 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 14:19:04 -0800 Subject: [PATCH 057/150] standard icon treatment --- src/internal/components/PressableIcon.tsx | 24 +++++ .../components/WalletIslandQrReceive.tsx | 43 ++++----- src/wallet/components/WalletIslandSwap.tsx | 21 ++--- .../components/WalletIslandWalletActions.tsx | 93 ++++++++----------- 4 files changed, 88 insertions(+), 93 deletions(-) create mode 100644 src/internal/components/PressableIcon.tsx diff --git a/src/internal/components/PressableIcon.tsx b/src/internal/components/PressableIcon.tsx new file mode 100644 index 0000000000..99700014bc --- /dev/null +++ b/src/internal/components/PressableIcon.tsx @@ -0,0 +1,24 @@ +import { border, cn, pressable } from '@/styles/theme'; +import type { ReactNode } from 'react'; + +export function PressableIcon({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) { + return ( +
+ {children} +
+ ); +} diff --git a/src/wallet/components/WalletIslandQrReceive.tsx b/src/wallet/components/WalletIslandQrReceive.tsx index 54383a4a58..38c7b264a8 100644 --- a/src/wallet/components/WalletIslandQrReceive.tsx +++ b/src/wallet/components/WalletIslandQrReceive.tsx @@ -1,3 +1,4 @@ +import { PressableIcon } from '@/internal/components/PressableIcon'; import { QrCodeSvg } from '@/internal/components/QrCode/QrCodeSvg'; import { backArrowSvg } from '@/internal/svg/backArrowSvg'; import { copySvg } from '@/internal/svg/copySvg'; @@ -79,35 +80,27 @@ export function WalletIslandQrReceive() { )} >
- - Scan to receive -
+ + + Scan to receive +
+ + + + + + ); return ( diff --git a/src/wallet/components/WalletIslandWalletActions.tsx b/src/wallet/components/WalletIslandWalletActions.tsx index d754c978c0..293762488b 100644 --- a/src/wallet/components/WalletIslandWalletActions.tsx +++ b/src/wallet/components/WalletIslandWalletActions.tsx @@ -1,8 +1,9 @@ +import { PressableIcon } from '@/internal/components/PressableIcon'; import { clockSvg } from '@/internal/svg/clockSvg'; import { disconnectSvg } from '@/internal/svg/disconnectSvg'; import { qrIconSvg } from '@/internal/svg/qrIconSvg'; import { refreshSvg } from '@/internal/svg/refreshSvg'; -import { border, cn, pressable } from '@/styles/theme'; +import { cn } from '@/styles/theme'; import { useCallback } from 'react'; import { useDisconnect } from 'wagmi'; import { useWalletIslandContext } from './WalletIslandProvider'; @@ -43,61 +44,45 @@ export function WalletIslandWalletActions() { })} >
- - {clockSvg} - - + + + {clockSvg} + + + + +
- - + + + + + +
); From 26c8c958aaa8c991c596354c047704442ae952fc Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 14:22:56 -0800 Subject: [PATCH 058/150] add test --- .../components/PressableIcon.test.tsx | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/internal/components/PressableIcon.test.tsx diff --git a/src/internal/components/PressableIcon.test.tsx b/src/internal/components/PressableIcon.test.tsx new file mode 100644 index 0000000000..162c32996d --- /dev/null +++ b/src/internal/components/PressableIcon.test.tsx @@ -0,0 +1,43 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { PressableIcon } from './PressableIcon'; + +describe('PressableIcon', () => { + it('renders children correctly', () => { + render( + + Icon + , + ); + + expect(screen.getByTestId('test-child')).toBeInTheDocument(); + }); + + it('applies default classes', () => { + render( + + Icon + , + ); + + const container = screen.getByText('Icon').parentElement; + expect(container).toHaveClass('flex', 'items-center', 'justify-center'); + }); + + it('merges custom className with default classes', () => { + const customClass = 'custom-class'; + render( + + Icon + , + ); + + const container = screen.getByText('Icon').parentElement; + expect(container).toHaveClass( + 'flex', + 'items-center', + 'justify-center', + customClass, + ); + }); +}); From e0b83757cebd0925ef7a7750097870222790b3c7 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 14:23:07 -0800 Subject: [PATCH 059/150] fix: linters --- .../wallet/hooks/usePortfolioTokenBalances.ts | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts index 0bd914c57b..50acb5d314 100644 --- a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts @@ -29,17 +29,18 @@ export function usePortfolioTokenBalances({ address: userPortfolio.address, portfolioBalanceUsd: userPortfolio.portfolio_balance_usd, tokenBalances: userPortfolio.token_balances.map( - (tokenBalance: PortfolioTokenBalanceAPIResponse) => ({ - address: - tokenBalance.symbol === 'ETH' ? '' : tokenBalance.address, - chainId: tokenBalance.chain_id, - decimals: tokenBalance.decimals, - image: tokenBalance.image, - name: tokenBalance.name, - symbol: tokenBalance.symbol, - cryptoBalance: tokenBalance.crypto_balance, - fiatBalance: tokenBalance.fiat_balance, - }) as PortfolioTokenWithFiatValue, + (tokenBalance: PortfolioTokenBalanceAPIResponse) => + ({ + address: + tokenBalance.symbol === 'ETH' ? '' : tokenBalance.address, + chainId: tokenBalance.chain_id, + decimals: tokenBalance.decimals, + image: tokenBalance.image, + name: tokenBalance.name, + symbol: tokenBalance.symbol, + cryptoBalance: tokenBalance.crypto_balance, + fiatBalance: tokenBalance.fiat_balance, + }) as PortfolioTokenWithFiatValue, ), }; From 9bc678bedbc5226496c61d7eab36c580669f093f Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 15:29:18 -0800 Subject: [PATCH 060/150] fix: tests --- .../hooks/usePortfolioTokenBalances.test.tsx | 24 ++++++++----------- .../components/WalletIslandProvider.test.tsx | 12 ++++------ src/wallet/components/WalletIslandSwap.tsx | 2 +- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx index 108f486913..bb1651a6fc 100644 --- a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx @@ -44,13 +44,11 @@ const mockTokens: PortfolioTokenWithFiatValue[] = [ fiatBalance: 100, }, ]; -const mockPortfolioTokenBalances: PortfolioTokenBalances[] = [ - { - address: mockAddresses[0], - portfolioBalanceUsd: 100, - tokenBalances: mockTokens, - }, -]; +const mockPortfolioTokenBalances: PortfolioTokenBalances = { + address: mockAddresses[0], + portfolioBalanceUsd: 100, + tokenBalances: mockTokens, +}; const createWrapper = () => { const queryClient = new QueryClient({ @@ -125,13 +123,11 @@ describe('usePortfolioTokenBalances', () => { fiatBalance: 100, }, ]; - const mockPortfolioTokenBalancesWithEth: PortfolioTokenBalances[] = [ - { - address: mockAddresses[0], - portfolioBalanceUsd: 100, - tokenBalances: mockTokensWithEth, - }, - ]; + const mockPortfolioTokenBalancesWithEth: PortfolioTokenBalances = { + address: mockAddresses[0], + portfolioBalanceUsd: 100, + tokenBalances: mockTokensWithEth, + }; vi.mocked(getPortfolioTokenBalances).mockResolvedValueOnce({ tokens: mockPortfolioTokenBalancesAPIResponseWithEth, diff --git a/src/wallet/components/WalletIslandProvider.test.tsx b/src/wallet/components/WalletIslandProvider.test.tsx index b0e8bcb04b..4085313596 100644 --- a/src/wallet/components/WalletIslandProvider.test.tsx +++ b/src/wallet/components/WalletIslandProvider.test.tsx @@ -41,13 +41,11 @@ describe('useWalletIslandContext', () => { }); mockUseWalletContext.mockReturnValue(defaultWalletContext); mockUsePortfolioTokenBalances.mockReturnValue({ - data: [ - { - address: '0x123', - tokenBalances: [], - portfolioBalanceUsd: 0, - }, - ], + data: { + address: '0x123', + tokenBalances: [], + portfolioBalanceUsd: 0, + }, refetch: vi.fn(), isFetching: false, dataUpdatedAt: new Date(), diff --git a/src/wallet/components/WalletIslandSwap.tsx b/src/wallet/components/WalletIslandSwap.tsx index 73ef05b8c8..0855828235 100644 --- a/src/wallet/components/WalletIslandSwap.tsx +++ b/src/wallet/components/WalletIslandSwap.tsx @@ -54,7 +54,7 @@ export function WalletIslandSwap({ const backButton = ( - From 6464e36d037e9f7a4cc68525cd58d269aed94a97 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 15:40:44 -0800 Subject: [PATCH 061/150] address comments --- src/wallet/components/WalletIslandQrReceive.tsx | 4 +--- src/wallet/components/WalletIslandWalletActions.tsx | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/wallet/components/WalletIslandQrReceive.tsx b/src/wallet/components/WalletIslandQrReceive.tsx index 38c7b264a8..015edcea20 100644 --- a/src/wallet/components/WalletIslandQrReceive.tsx +++ b/src/wallet/components/WalletIslandQrReceive.tsx @@ -119,16 +119,14 @@ export function WalletIslandQrReceive() {
- -
); diff --git a/src/wallet/components/WalletIslandWalletActions.tsx b/src/wallet/components/WalletIslandWalletActions.tsx index 293762488b..7318be55b1 100644 --- a/src/wallet/components/WalletIslandWalletActions.tsx +++ b/src/wallet/components/WalletIslandWalletActions.tsx @@ -31,7 +31,7 @@ export function WalletIslandWalletActions() { portfolioDataUpdatedAt && Date.now() - portfolioDataUpdatedAt < 1000 * 15 ) { - return; // TODO: Add toast + return; } await refetchPortfolioData(); }, [refetchPortfolioData, portfolioDataUpdatedAt]); From 4061c4fe3432a042cb695cdec73259bd343d9376 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 19:24:29 -0800 Subject: [PATCH 062/150] change default playground position --- playground/nextjs-app-router/components/AppProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/nextjs-app-router/components/AppProvider.tsx b/playground/nextjs-app-router/components/AppProvider.tsx index c4e5f829c8..9f49672807 100644 --- a/playground/nextjs-app-router/components/AppProvider.tsx +++ b/playground/nextjs-app-router/components/AppProvider.tsx @@ -51,7 +51,7 @@ export const defaultState: State = { setComponentMode: () => {}, setNFTToken: () => {}, setIsSponsored: () => {}, - anchorPosition: 'top-left', + anchorPosition: 'top-center', setAnchorPosition: () => {}, }; From a7cbabb4366c7f9edad1be7c190ef4931c1e05fc Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 21:12:27 -0800 Subject: [PATCH 063/150] draggable prop on wallet --- src/wallet/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wallet/types.ts b/src/wallet/types.ts index 63c563f4cb..438d3d8a20 100644 --- a/src/wallet/types.ts +++ b/src/wallet/types.ts @@ -86,6 +86,8 @@ export type WalletContextType = { export type WalletReact = { children: React.ReactNode; className?: string; + draggable?: boolean; + startingPosition?: { x: number; y: number }; }; /** From 4d71fc0d01a3914b5743def3510e2d1a432c13e0 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 21:12:59 -0800 Subject: [PATCH 064/150] wallet island default to-tokens, max-height --- src/wallet/constants.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/wallet/constants.ts b/src/wallet/constants.ts index 8ee1b17120..849c7eb0f8 100644 --- a/src/wallet/constants.ts +++ b/src/wallet/constants.ts @@ -1,3 +1,6 @@ +import type { Token } from '@/token'; +import { base } from 'viem/chains'; + // The bytecode for the Coinbase Smart Wallet proxy contract. export const CB_SW_PROXY_BYTECODE = '0x363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6038573d6000fd5b3d6000f3'; @@ -10,3 +13,25 @@ export const ERC_1967_PROXY_IMPLEMENTATION_SLOT = // The Coinbase Smart Wallet factory address. export const CB_SW_FACTORY_ADDRESS = '0x0BA5ED0c6AA8c49038F819E587E2633c4A9F428a'; +export const WALLET_ISLAND_MAX_HEIGHT = 400; +// The default tokens that will be shown in the WalletIslandSwap sub-component. +export const WALLET_ISLAND_DEFAULT_SWAPPABLE_TOKENS: Token[] = [ + { + name: 'ETH', + address: '', + symbol: 'ETH', + decimals: 18, + image: + 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: base.id, + }, + { + name: 'USDC', + address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', + symbol: 'USDC', + decimals: 6, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', + chainId: base.id, + }, + ]; From dcf885c4246cd01d30dca12de1e1052d8ab27626 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 21:14:24 -0800 Subject: [PATCH 065/150] remove ref from walletisland --- src/wallet/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wallet/types.ts b/src/wallet/types.ts index 438d3d8a20..886375bdcd 100644 --- a/src/wallet/types.ts +++ b/src/wallet/types.ts @@ -156,5 +156,4 @@ export type WalletDropdownLinkReact = { export type WalletIslandProps = { children: React.ReactNode; swappableTokens?: Token[]; - walletContainerRef?: React.RefObject; }; From af42881ce2b293c62d114265ef9522ba1d74931f Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 21:16:59 -0800 Subject: [PATCH 066/150] refactor draggable component to entire wallet --- src/wallet/components/WalletIsland.tsx | 9 +- src/wallet/components/WalletIslandContent.tsx | 190 ++++++------------ src/wallet/components/WalletIslandDefault.tsx | 4 +- 3 files changed, 68 insertions(+), 135 deletions(-) diff --git a/src/wallet/components/WalletIsland.tsx b/src/wallet/components/WalletIsland.tsx index fe63b15416..8e35f6fd8d 100644 --- a/src/wallet/components/WalletIsland.tsx +++ b/src/wallet/components/WalletIsland.tsx @@ -2,15 +2,10 @@ import type { WalletIslandProps } from '../types'; import { WalletIslandContent } from './WalletIslandContent'; import { WalletIslandProvider } from './WalletIslandProvider'; -export function WalletIsland({ - children, - walletContainerRef, -}: WalletIslandProps) { +export function WalletIsland({ children }: WalletIslandProps) { return ( - - {children} - + {children} ); } diff --git a/src/wallet/components/WalletIslandContent.tsx b/src/wallet/components/WalletIslandContent.tsx index 276b40ecb6..80cb8595b6 100644 --- a/src/wallet/components/WalletIslandContent.tsx +++ b/src/wallet/components/WalletIslandContent.tsx @@ -1,153 +1,89 @@ import { useTheme } from '@/core-react/internal/hooks/useTheme'; -import { Draggable } from '@/internal/components/Draggable'; import { background, border, cn, text } from '@/styles/theme'; -import type { Token } from '@/token'; -import { useMemo } from 'react'; -import { base } from 'viem/chains'; +import { WALLET_ISLAND_DEFAULT_SWAPPABLE_TOKENS } from '../constants'; import type { WalletIslandProps } from '../types'; import { useWalletIslandContext } from './WalletIslandProvider'; import { WalletIslandQrReceive } from './WalletIslandQrReceive'; import { WalletIslandSwap } from './WalletIslandSwap'; import { useWalletContext } from './WalletProvider'; -const WALLET_ISLAND_WIDTH = 352; -const WALLET_ISLAND_HEIGHT = 394; -const WALLET_ISLAND_DEFAULT_SWAPPABLE_TOKENS: Token[] = [ - { - name: 'ETH', - address: '', - symbol: 'ETH', - decimals: 18, - image: - 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', - chainId: base.id, - }, - { - name: 'USDC', - address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', - symbol: 'USDC', - decimals: 6, - image: - 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', - chainId: base.id, - }, -]; - export function WalletIslandContent({ children, swappableTokens, - walletContainerRef, }: WalletIslandProps) { const { isClosing, setIsOpen, setIsClosing } = useWalletContext(); const { showQr, showSwap, tokenBalances } = useWalletIslandContext(); const componentTheme = useTheme(); - const position = useMemo(() => { - if (walletContainerRef?.current) { - const rect = walletContainerRef.current.getBoundingClientRect(); - const windowWidth = window.innerWidth; - const windowHeight = window.innerHeight; - - let xPos: number; - let yPos: number; - - if (windowWidth - rect.right < WALLET_ISLAND_WIDTH) { - xPos = rect.right - WALLET_ISLAND_WIDTH; - } else { - xPos = rect.left; - } - - if (windowHeight - rect.bottom < WALLET_ISLAND_HEIGHT) { - yPos = rect.bottom - WALLET_ISLAND_HEIGHT - rect.height - 10; - } else { - yPos = rect.bottom + 10; - } - - return { - x: xPos, - y: yPos, - }; - } - - return { - x: 20, - y: 20, - }; - }, [walletContainerRef]); - return ( - +
{ + if (isClosing) { + setIsOpen(false); + setIsClosing(false); + } + }} + > +
+ +
{ - if (isClosing) { - setIsOpen(false); - setIsClosing(false); + > + + Swap +
+ } + to={swappableTokens ?? WALLET_ISLAND_DEFAULT_SWAPPABLE_TOKENS} + from={ + tokenBalances?.map((token) => ({ + address: token.address, + chainId: token.chainId, + symbol: token.symbol, + decimals: token.decimals, + image: token.image, + name: token.name, + })) ?? [] } - }} + className="w-full p-2" + /> +
+
-
- -
-
- - Swap -
- } - to={swappableTokens ?? WALLET_ISLAND_DEFAULT_SWAPPABLE_TOKENS} - from={ - tokenBalances?.map((token) => ({ - address: token.address, - chainId: token.chainId, - symbol: token.symbol, - decimals: token.decimals, - image: token.image, - name: token.name, - })) ?? [] - } - className="w-full p-2" - /> -
-
- {children} -
+ {children}
- +
); } diff --git a/src/wallet/components/WalletIslandDefault.tsx b/src/wallet/components/WalletIslandDefault.tsx index 06b0a36721..72b6a0fdf0 100644 --- a/src/wallet/components/WalletIslandDefault.tsx +++ b/src/wallet/components/WalletIslandDefault.tsx @@ -10,7 +10,9 @@ import { WalletIslandWalletActions } from './WalletIslandWalletActions'; export function WalletIslandDefault() { return ( - + Connect Wallet From 28695340ad77905f3acc9cf2d7d78b23fb4ca0ca Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 21:17:34 -0800 Subject: [PATCH 067/150] prevent click functionality on drag --- src/internal/components/Draggable.tsx | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/internal/components/Draggable.tsx b/src/internal/components/Draggable.tsx index 57414deedf..ea82caa64a 100644 --- a/src/internal/components/Draggable.tsx +++ b/src/internal/components/Draggable.tsx @@ -17,6 +17,7 @@ export function Draggable({ }: DraggableProps) { const [position, setPosition] = useState(startingPosition); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); + const [dragStartPosition, setDragStartPosition] = useState({ x: 0, y: 0 }); const [isDragging, setIsDragging] = useState(false); const calculateSnapToGrid = useCallback( @@ -27,8 +28,10 @@ export function Draggable({ ); const handleDragStart = (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); setIsDragging(true); - + setDragStartPosition({ x: e.clientX, y: e.clientY }); setDragOffset({ x: e.clientX - position.x, y: e.clientY - position.y, @@ -47,7 +50,23 @@ export function Draggable({ }); }; - const handleGlobalEnd = () => { + const handleGlobalEnd = (e: PointerEvent) => { + const moveDistance = Math.hypot( + e.clientX - dragStartPosition.x, + e.clientY - dragStartPosition.y, + ); + + if (moveDistance > 2) { + e.preventDefault(); + e.stopPropagation(); + const clickEvent = (e2: MouseEvent) => { + e2.preventDefault(); + e2.stopPropagation(); + document.removeEventListener('click', clickEvent, true); + }; + document.addEventListener('click', clickEvent, true); + } + setPosition((prev) => ({ x: snapToGrid ? calculateSnapToGrid(prev.x) : prev.x, y: snapToGrid ? calculateSnapToGrid(prev.y) : prev.y, @@ -62,7 +81,7 @@ export function Draggable({ document.removeEventListener('pointermove', handleGlobalMove); document.removeEventListener('pointerup', handleGlobalEnd); }; - }, [isDragging, dragOffset, snapToGrid, calculateSnapToGrid]); + }, [isDragging, dragOffset, snapToGrid, calculateSnapToGrid, dragStartPosition]); return (
Date: Mon, 6 Jan 2025 22:19:24 -0800 Subject: [PATCH 068/150] fix: lint --- src/internal/components/Draggable.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/internal/components/Draggable.tsx b/src/internal/components/Draggable.tsx index ea82caa64a..8dc549db91 100644 --- a/src/internal/components/Draggable.tsx +++ b/src/internal/components/Draggable.tsx @@ -81,7 +81,13 @@ export function Draggable({ document.removeEventListener('pointermove', handleGlobalMove); document.removeEventListener('pointerup', handleGlobalEnd); }; - }, [isDragging, dragOffset, snapToGrid, calculateSnapToGrid, dragStartPosition]); + }, [ + isDragging, + dragOffset, + snapToGrid, + calculateSnapToGrid, + dragStartPosition, + ]); return (
Date: Mon, 6 Jan 2025 22:19:49 -0800 Subject: [PATCH 069/150] fix: types --- src/wallet/components/WalletIsland.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wallet/components/WalletIsland.tsx b/src/wallet/components/WalletIsland.tsx index 8e35f6fd8d..7a7598c4eb 100644 --- a/src/wallet/components/WalletIsland.tsx +++ b/src/wallet/components/WalletIsland.tsx @@ -1,8 +1,8 @@ -import type { WalletIslandProps } from '../types'; +import type { WalletIslandReact } from '../types'; import { WalletIslandContent } from './WalletIslandContent'; import { WalletIslandProvider } from './WalletIslandProvider'; -export function WalletIsland({ children }: WalletIslandProps) { +export function WalletIsland({ children }: WalletIslandReact) { return ( {children} From 815db402822bca37257ee1dff951de1ee1c93309 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 22:20:12 -0800 Subject: [PATCH 070/150] fix: types --- src/wallet/components/WalletIslandContent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wallet/components/WalletIslandContent.tsx b/src/wallet/components/WalletIslandContent.tsx index 80cb8595b6..cda9ce292c 100644 --- a/src/wallet/components/WalletIslandContent.tsx +++ b/src/wallet/components/WalletIslandContent.tsx @@ -1,7 +1,7 @@ import { useTheme } from '@/core-react/internal/hooks/useTheme'; import { background, border, cn, text } from '@/styles/theme'; import { WALLET_ISLAND_DEFAULT_SWAPPABLE_TOKENS } from '../constants'; -import type { WalletIslandProps } from '../types'; +import type { WalletIslandReact } from '../types'; import { useWalletIslandContext } from './WalletIslandProvider'; import { WalletIslandQrReceive } from './WalletIslandQrReceive'; import { WalletIslandSwap } from './WalletIslandSwap'; @@ -10,7 +10,7 @@ import { useWalletContext } from './WalletProvider'; export function WalletIslandContent({ children, swappableTokens, -}: WalletIslandProps) { +}: WalletIslandReact) { const { isClosing, setIsOpen, setIsClosing } = useWalletContext(); const { showQr, showSwap, tokenBalances } = useWalletIslandContext(); const componentTheme = useTheme(); From 7ba6cb7eb20bf2e596ebd5fb9518773fce790393 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 22:20:30 -0800 Subject: [PATCH 071/150] fix: lint --- src/wallet/components/WalletIslandDefault.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/wallet/components/WalletIslandDefault.tsx b/src/wallet/components/WalletIslandDefault.tsx index 72b6a0fdf0..8a908d879b 100644 --- a/src/wallet/components/WalletIslandDefault.tsx +++ b/src/wallet/components/WalletIslandDefault.tsx @@ -10,9 +10,7 @@ import { WalletIslandWalletActions } from './WalletIslandWalletActions'; export function WalletIslandDefault() { return ( - + Connect Wallet From 8951289ceb5576df38dc1f07d632798f8cfdb2df Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 22:21:53 -0800 Subject: [PATCH 072/150] remove comment, fix lint --- src/wallet/components/WalletIslandTokenHoldings.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/wallet/components/WalletIslandTokenHoldings.tsx b/src/wallet/components/WalletIslandTokenHoldings.tsx index 5e4156d4dc..45d518f48f 100644 --- a/src/wallet/components/WalletIslandTokenHoldings.tsx +++ b/src/wallet/components/WalletIslandTokenHoldings.tsx @@ -4,7 +4,12 @@ import { type Token, TokenImage } from '@/token'; import { useWalletIslandContext } from './WalletIslandProvider'; import { useWalletContext } from './WalletProvider'; -// TODO: handle loading state +type TokenDetailsProps = { + token: Token; + balance: number; + valueInFiat: number; +}; + export function WalletIslandTokenHoldings() { const { isClosing } = useWalletContext(); const { tokenBalances, isFetchingPortfolioData } = useWalletIslandContext(); @@ -53,12 +58,6 @@ export function WalletIslandTokenHoldings() { ); } -type TokenDetailsProps = { - token: Token; - balance: number; - valueInFiat: number; -}; - function TokenDetails({ token, balance, valueInFiat }: TokenDetailsProps) { const currencySymbol = '$'; // TODO: get from user settings From 8140fe65952947f3b9c392014da983f761472d7c Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 22:22:27 -0800 Subject: [PATCH 073/150] move walletisland types to wallet/types --- .../components/WalletIslandProvider.tsx | 33 +--------------- src/wallet/types.ts | 38 ++++++++++++++++++- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/wallet/components/WalletIslandProvider.tsx b/src/wallet/components/WalletIslandProvider.tsx index 9778c212f2..5b847656a5 100644 --- a/src/wallet/components/WalletIslandProvider.tsx +++ b/src/wallet/components/WalletIslandProvider.tsx @@ -1,38 +1,9 @@ import { useValue } from '@/core-react/internal/hooks/useValue'; import { usePortfolioTokenBalances } from '@/core-react/wallet/hooks/usePortfolioTokenBalances'; -import type { - PortfolioTokenBalances, - PortfolioTokenWithFiatValue, -} from '@/core/api/types'; -import type { QueryObserverResult } from '@tanstack/react-query'; -import { - type Dispatch, - type ReactNode, - type SetStateAction, - createContext, - useContext, - useState, -} from 'react'; +import { type ReactNode, createContext, useContext, useState } from 'react'; +import type { WalletIslandContextType } from '../types'; import { useWalletContext } from './WalletProvider'; -export type WalletIslandContextType = { - showSwap: boolean; - setShowSwap: Dispatch>; - isSwapClosing: boolean; - setIsSwapClosing: Dispatch>; - showQr: boolean; - setShowQr: Dispatch>; - isQrClosing: boolean; - setIsQrClosing: Dispatch>; - tokenBalances: PortfolioTokenWithFiatValue[] | undefined; - portfolioFiatValue: number | undefined; - isFetchingPortfolioData: boolean; - portfolioDataUpdatedAt: number | undefined; - refetchPortfolioData: () => Promise< - QueryObserverResult - >; -}; - type WalletIslandProviderReact = { children: ReactNode; }; diff --git a/src/wallet/types.ts b/src/wallet/types.ts index 886375bdcd..61699b09ed 100644 --- a/src/wallet/types.ts +++ b/src/wallet/types.ts @@ -1,5 +1,7 @@ +import type { PortfolioTokenBalances, PortfolioTokenWithFiatValue } from '@/core/api/types'; import type { SwapError } from '@/swap'; import type { Token } from '@/token'; +import type { QueryObserverResult } from '@tanstack/react-query'; import type { Dispatch, ReactNode, SetStateAction } from 'react'; import type { Address, Chain, PublicClient } from 'viem'; import type { UserOperation } from 'viem/_types/account-abstraction'; @@ -90,6 +92,16 @@ export type WalletReact = { startingPosition?: { x: number; y: number }; }; +export type WalletSubComponentReact = { + connect: React.ReactNode; + connectRef: React.RefObject; + dropdown: React.ReactNode; + island: React.ReactNode; + isOpen: boolean; + alignSubComponentRight: boolean; + showSubComponentAbove: boolean; +}; + /** * Note: exported as public Type */ @@ -153,7 +165,31 @@ export type WalletDropdownLinkReact = { target?: string; }; -export type WalletIslandProps = { +/** + * Note: exported as public Type + */ +export type WalletIslandReact = { children: React.ReactNode; swappableTokens?: Token[]; }; + +/** + * Note: exported as public Type + */ +export type WalletIslandContextType = { + showSwap: boolean; + setShowSwap: Dispatch>; + isSwapClosing: boolean; + setIsSwapClosing: Dispatch>; + showQr: boolean; + setShowQr: Dispatch>; + isQrClosing: boolean; + setIsQrClosing: Dispatch>; + tokenBalances: PortfolioTokenWithFiatValue[] | undefined; + portfolioFiatValue: number | undefined; + isFetchingPortfolioData: boolean; + portfolioDataUpdatedAt: number | undefined; + refetchPortfolioData: () => Promise< + QueryObserverResult + >; +}; From 2c84581051d78772f11475996c960e9b9d5d2fa8 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 22:23:00 -0800 Subject: [PATCH 074/150] walletisland constants --- src/wallet/constants.ts | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/wallet/constants.ts b/src/wallet/constants.ts index 849c7eb0f8..cba4bf1386 100644 --- a/src/wallet/constants.ts +++ b/src/wallet/constants.ts @@ -14,24 +14,24 @@ export const ERC_1967_PROXY_IMPLEMENTATION_SLOT = export const CB_SW_FACTORY_ADDRESS = '0x0BA5ED0c6AA8c49038F819E587E2633c4A9F428a'; export const WALLET_ISLAND_MAX_HEIGHT = 400; -// The default tokens that will be shown in the WalletIslandSwap sub-component. +export const WALLET_ISLAND_MAX_WIDTH = 352; export const WALLET_ISLAND_DEFAULT_SWAPPABLE_TOKENS: Token[] = [ - { - name: 'ETH', - address: '', - symbol: 'ETH', - decimals: 18, - image: - 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', - chainId: base.id, - }, - { - name: 'USDC', - address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', - symbol: 'USDC', - decimals: 6, - image: - 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', - chainId: base.id, - }, - ]; + { + name: 'ETH', + address: '', + symbol: 'ETH', + decimals: 18, + image: + 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: base.id, + }, + { + name: 'USDC', + address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', + symbol: 'USDC', + decimals: 6, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', + chainId: base.id, + }, +]; From 5cad2fd3e1526f5c4dc3766e6bd40057c337a009 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 22:23:10 -0800 Subject: [PATCH 075/150] add walletisland exports --- src/wallet/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wallet/index.ts b/src/wallet/index.ts index 3299f1cc93..5912ec2c88 100644 --- a/src/wallet/index.ts +++ b/src/wallet/index.ts @@ -30,4 +30,6 @@ export type { WalletDropdownLinkReact, WalletDropdownReact, WalletReact, + WalletIslandReact, + WalletIslandContextType, } from './types'; From d898f4a51331ebac5866a5e583442181562ba59f Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 22:23:52 -0800 Subject: [PATCH 076/150] draggable functionality on entire wallet --- src/wallet/components/Wallet.tsx | 153 ++++++++++++++++++++++++++----- 1 file changed, 129 insertions(+), 24 deletions(-) diff --git a/src/wallet/components/Wallet.tsx b/src/wallet/components/Wallet.tsx index 5c117fb250..fa8480f889 100644 --- a/src/wallet/components/Wallet.tsx +++ b/src/wallet/components/Wallet.tsx @@ -1,18 +1,61 @@ import { useIsMounted } from '@/core-react/internal/hooks/useIsMounted'; import { useTheme } from '@/core-react/internal/hooks/useTheme'; import { findComponent } from '@/core-react/internal/utils/findComponent'; +import { Draggable } from '@/internal/components/Draggable'; import { cn } from '@/styles/theme'; import { useOutsideClick } from '@/ui-react/internal/hooks/useOutsideClick'; -import { Children, cloneElement, useMemo, useRef } from 'react'; +import { Children, useEffect, useMemo, useRef, useState } from 'react'; +import { + WALLET_ISLAND_MAX_HEIGHT, + WALLET_ISLAND_MAX_WIDTH, +} from '../constants'; import type { WalletReact } from '../types'; import { ConnectWallet } from './ConnectWallet'; import { WalletDropdown } from './WalletDropdown'; import { WalletIsland } from './WalletIsland'; import { WalletProvider, useWalletContext } from './WalletProvider'; -function WalletContent({ children, className }: WalletReact) { +export const Wallet = ({ + children, + className, + draggable = false, + startingPosition = { + x: window.innerWidth - 250, + y: window.innerHeight - 100, + }, +}: WalletReact) => { + const componentTheme = useTheme(); + const isMounted = useIsMounted(); + + // prevents SSR hydration issue + if (!isMounted) { + return null; + } + + return ( + + + {children} + + + ); +}; + +function WalletContent({ + children, + className, + draggable, + startingPosition, +}: WalletReact) { + const [showSubComponentAbove, setShowSubComponentAbove] = useState(false); + const [alignSubComponentRight, setAlignSubComponentRight] = useState(false); const { isOpen, handleClose } = useWalletContext(); const walletContainerRef = useRef(null); + const connectRef = useRef(null); useOutsideClick(walletContainerRef, handleClose); @@ -21,47 +64,109 @@ function WalletContent({ children, className }: WalletReact) { return { connect: childrenArray.find(findComponent(ConnectWallet)), dropdown: childrenArray.find(findComponent(WalletDropdown)), - island: (() => { - const islandComponent = childrenArray.find(findComponent(WalletIsland)); - return islandComponent - ? cloneElement(islandComponent, { walletContainerRef }) - : null; - })(), + island: childrenArray.find(findComponent(WalletIsland)), }; }, [children]); - const walletSubComponent = dropdown || island; if (dropdown && island) { console.error( 'Defaulted to WalletDropdown. Wallet cannot have both WalletDropdown and WalletIsland as children.', ); } + useEffect(() => { + if (isOpen && connectRef.current) { + const connectRect = connectRef.current.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + const viewportWidth = window.innerWidth; + + const spaceAvailableBelow = viewportHeight - connectRect.bottom; + const spaceAvailableRight = viewportWidth - connectRect.left; + + setShowSubComponentAbove(spaceAvailableBelow < WALLET_ISLAND_MAX_HEIGHT); + setAlignSubComponentRight(spaceAvailableRight < WALLET_ISLAND_MAX_WIDTH); + } + }, [isOpen]); + + if (draggable) { + return ( +
+ + + +
+ ); + } + return (
- {connect} - {isOpen && walletSubComponent} +
); } -export const Wallet = ({ children, className }: WalletReact) => { - const componentTheme = useTheme(); - const isMounted = useIsMounted(); - - // prevents SSR hydration issue - if (!isMounted) { - return null; +function WalletSubComponent({ + connect, + connectRef, + dropdown, + island, + isOpen, + alignSubComponentRight, + showSubComponentAbove, +}: { + connect: React.ReactNode; + connectRef: React.RefObject; + dropdown: React.ReactNode; + island: React.ReactNode; + isOpen: boolean; + alignSubComponentRight: boolean; + showSubComponentAbove: boolean; +}) { + if (dropdown) { + return ( + <> + {connect} + {isOpen && dropdown} + + ); } return ( - - - {children} - - + <> +
{connect}
+ {isOpen && ( +
+ {island} +
+ )} + ); -}; +} From ce71d3a5db5e957deca4c716568c9c0600a4f90b Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 6 Jan 2025 22:24:20 -0800 Subject: [PATCH 077/150] fix: lint --- src/wallet/types.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/wallet/types.ts b/src/wallet/types.ts index 61699b09ed..5e42d62499 100644 --- a/src/wallet/types.ts +++ b/src/wallet/types.ts @@ -1,4 +1,7 @@ -import type { PortfolioTokenBalances, PortfolioTokenWithFiatValue } from '@/core/api/types'; +import type { + PortfolioTokenBalances, + PortfolioTokenWithFiatValue, +} from '@/core/api/types'; import type { SwapError } from '@/swap'; import type { Token } from '@/token'; import type { QueryObserverResult } from '@tanstack/react-query'; From d9da5618fc243671dcf9f6a1d26a072c649d7670 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 7 Jan 2025 10:32:35 -0800 Subject: [PATCH 078/150] fix walletisland-connectwallet gap --- src/wallet/components/Wallet.tsx | 6 +++--- src/wallet/components/WalletIslandContent.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wallet/components/Wallet.tsx b/src/wallet/components/Wallet.tsx index fa8480f889..993a717e75 100644 --- a/src/wallet/components/Wallet.tsx +++ b/src/wallet/components/Wallet.tsx @@ -75,7 +75,7 @@ function WalletContent({ } useEffect(() => { - if (isOpen && connectRef.current) { + if (draggable && isOpen && connectRef.current) { const connectRect = connectRef.current.getBoundingClientRect(); const viewportHeight = window.innerHeight; const viewportWidth = window.innerWidth; @@ -86,7 +86,7 @@ function WalletContent({ setShowSubComponentAbove(spaceAvailableBelow < WALLET_ISLAND_MAX_HEIGHT); setAlignSubComponentRight(spaceAvailableRight < WALLET_ISLAND_MAX_WIDTH); } - }, [isOpen]); + }, [draggable, isOpen]); if (draggable) { return ( @@ -160,7 +160,7 @@ function WalletSubComponent({
diff --git a/src/wallet/components/WalletIslandContent.tsx b/src/wallet/components/WalletIslandContent.tsx index cda9ce292c..3f489c9e68 100644 --- a/src/wallet/components/WalletIslandContent.tsx +++ b/src/wallet/components/WalletIslandContent.tsx @@ -23,7 +23,7 @@ export function WalletIslandContent({ background.default, border.radius, border.lineDefault, - 'mt-2.5 h-auto w-88', + 'my-1.5 h-auto w-88', 'flex items-center justify-center', isClosing ? 'fade-out slide-out-to-top-1.5 animate-out fill-mode-forwards ease-in-out' From a3b40bc58a9fbd8e2ea343eea1ee18d8b3e9ed56 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 7 Jan 2025 10:32:47 -0800 Subject: [PATCH 079/150] fix icon sizes --- src/wallet/components/WalletIslandWalletActions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wallet/components/WalletIslandWalletActions.tsx b/src/wallet/components/WalletIslandWalletActions.tsx index 7318be55b1..5c8af8e6d8 100644 --- a/src/wallet/components/WalletIslandWalletActions.tsx +++ b/src/wallet/components/WalletIslandWalletActions.tsx @@ -80,7 +80,7 @@ export function WalletIslandWalletActions() { type="button" onClick={handleRefreshPortfolioData} > -
{refreshSvg}
+
{refreshSvg}
From 4e34b83f777c0a50beb59913d4a9ce321b3779d5 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 7 Jan 2025 12:53:13 -0800 Subject: [PATCH 080/150] update wallet tests --- src/wallet/components/Wallet.test.tsx | 187 ++++++++++++++++++ src/wallet/components/Wallet.tsx | 5 +- .../components/WalletIslandContent.test.tsx | 121 ------------ 3 files changed, 190 insertions(+), 123 deletions(-) diff --git a/src/wallet/components/Wallet.test.tsx b/src/wallet/components/Wallet.test.tsx index 136c854e0e..5a46dd3495 100644 --- a/src/wallet/components/Wallet.test.tsx +++ b/src/wallet/components/Wallet.test.tsx @@ -148,4 +148,191 @@ describe('Wallet Component', () => { consoleSpy.mockRestore(); }); + + it('should render WalletIsland when WalletIsland is provided', () => { + (useWalletContext as ReturnType).mockReturnValue({ + isOpen: true, + handleClose: mockHandleClose, + containerRef: { current: document.createElement('div') }, + }); + + render( + + + +
Wallet Island
+
+
, + ); + + expect(screen.getByTestId('wallet-island')).toBeDefined(); + expect(screen.queryByTestId('wallet-dropdown')).toBeNull(); + }); + + it('should render Draggable when draggable prop is true', () => { + (useWalletContext as ReturnType).mockReturnValue({ + isOpen: true, + handleClose: mockHandleClose, + containerRef: { current: document.createElement('div') }, + }); + + render( + + + +
Wallet Island
+
+
, + ); + + expect(screen.getByTestId('ockDraggable')).toBeDefined(); + }); + + it('should render WalletIsland right-aligned when there is not enough space on the right', () => { + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: 500, + }); + + (useWalletContext as ReturnType).mockReturnValue({ + isOpen: true, + }); + + const mockGetBoundingClientRect = vi.fn().mockReturnValue({ + left: 400, + right: 450, + bottom: 100, + top: 0, + width: 50, + height: 100, + }); + + // Mock Element.prototype.getBoundingClientRect called on the ConnectWallet ref + Element.prototype.getBoundingClientRect = mockGetBoundingClientRect; + + render( + + + +
Wallet Island
+
+
, + ); + + expect(screen.getByTestId('ockWalletIslandContainer')).toHaveClass( + 'right-0', + ); + }); + + it('should render WalletIsland left-aligned when there is enough space on the right', () => { + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: 1000, + }); + + (useWalletContext as ReturnType).mockReturnValue({ + isOpen: true, + }); + + const mockGetBoundingClientRect = vi.fn().mockReturnValue({ + left: 400, + right: 450, + bottom: 100, + top: 0, + width: 50, + height: 100, + }); + + // Mock Element.prototype.getBoundingClientRect called on the ConnectWallet ref + Element.prototype.getBoundingClientRect = mockGetBoundingClientRect; + + render( + + + +
Wallet Island
+
+
, + ); + + expect(screen.getByTestId('ockWalletIslandContainer')).toHaveClass( + 'left-0', + ); + }); + + it('should render WalletIsland above ConnectWallet when there is not enough space on the bottom', () => { + Object.defineProperty(window, 'innerHeight', { + writable: true, + configurable: true, + value: 1000, + }); + + (useWalletContext as ReturnType).mockReturnValue({ + isOpen: true, + }); + + const mockGetBoundingClientRect = vi.fn().mockReturnValue({ + left: 400, + right: 450, + bottom: 100, + top: 0, + width: 50, + height: 100, + }); + + // Mock Element.prototype.getBoundingClientRect called on the ConnectWallet ref + Element.prototype.getBoundingClientRect = mockGetBoundingClientRect; + + render( + + + +
Wallet Island
+
+
, + ); + + expect(screen.getByTestId('ockWalletIslandContainer')).toHaveClass( + 'top-full', + ); + }); + + it('should render WalletIsland above ConnectWallet when there is not enough space on the bottom', () => { + Object.defineProperty(window, 'innerHeight', { + writable: true, + configurable: true, + value: 1000, + }); + + (useWalletContext as ReturnType).mockReturnValue({ + isOpen: true, + }); + + const mockGetBoundingClientRect = vi.fn().mockReturnValue({ + left: 400, + right: 450, + bottom: 800, + top: 0, + width: 50, + height: 100, + }); + + // Mock Element.prototype.getBoundingClientRect called on the ConnectWallet ref + Element.prototype.getBoundingClientRect = mockGetBoundingClientRect; + + render( + + + +
Wallet Island
+
+
, + ); + + expect(screen.getByTestId('ockWalletIslandContainer')).toHaveClass( + 'bottom-full', + ); + }); }); diff --git a/src/wallet/components/Wallet.tsx b/src/wallet/components/Wallet.tsx index 993a717e75..9683f5903c 100644 --- a/src/wallet/components/Wallet.tsx +++ b/src/wallet/components/Wallet.tsx @@ -75,7 +75,7 @@ function WalletContent({ } useEffect(() => { - if (draggable && isOpen && connectRef.current) { + if (isOpen && connectRef?.current) { const connectRect = connectRef.current.getBoundingClientRect(); const viewportHeight = window.innerHeight; const viewportWidth = window.innerWidth; @@ -86,7 +86,7 @@ function WalletContent({ setShowSubComponentAbove(spaceAvailableBelow < WALLET_ISLAND_MAX_HEIGHT); setAlignSubComponentRight(spaceAvailableRight < WALLET_ISLAND_MAX_WIDTH); } - }, [draggable, isOpen]); + }, [isOpen]); if (draggable) { return ( @@ -158,6 +158,7 @@ function WalletSubComponent({
{connect}
{isOpen && (
{ })), ); }); - - it('correctly positions WalletIslandContent when there is enough space on the right', () => { - const mockRect = { - left: 100, - right: 200, - bottom: 450, - height: 400, - }; - const mockRef = { current: { getBoundingClientRect: () => mockRect } }; - - window.innerWidth = 1000; - window.innerHeight = 1000; - - mockUseWalletContext.mockReturnValue({ - isClosing: false, - }); - - render( - } - > -
WalletIslandContent
-
, - ); - - const draggable = screen.getByTestId('ockDraggable'); - expect(draggable).toHaveStyle({ - left: '100px', - }); - }); - - it('correctly positions WalletIslandContent when there is not enough space on the right', () => { - const mockRect = { - left: 300, - right: 400, - bottom: 450, - height: 400, - }; - const mockRef = { current: { getBoundingClientRect: () => mockRect } }; - - window.innerWidth = 550; - window.innerHeight = 1000; - - mockUseWalletContext.mockReturnValue({ - isClosing: false, - }); - - render( - } - > -
WalletIslandContent
-
, - ); - - const draggable = screen.getByTestId('ockDraggable'); - expect(draggable).toHaveStyle({ - left: '48px', - }); - }); - - it('correctly positions WalletIslandContent when there is enough space on the bottom', () => { - const mockRect = { - left: 300, - right: 400, - bottom: 450, - height: 400, - }; - const mockRef = { current: { getBoundingClientRect: () => mockRect } }; - - window.innerWidth = 550; - window.innerHeight = 1000; - - mockUseWalletContext.mockReturnValue({ - isClosing: false, - }); - - render( - } - > -
WalletIslandContent
-
, - ); - - const draggable = screen.getByTestId('ockDraggable'); - expect(draggable).toHaveStyle({ - top: '460px', - }); - }); - - it('correctly positions WalletIslandContent when there is not enough space on the bottom', () => { - const mockRect = { - left: 300, - right: 400, - bottom: 850, - height: 400, - }; - const mockRef = { current: { getBoundingClientRect: () => mockRect } }; - - window.innerWidth = 550; - window.innerHeight = 1000; - - mockUseWalletContext.mockReturnValue({ - isClosing: false, - }); - - render( - } - > -
WalletIslandContent
-
, - ); - - const draggable = screen.getByTestId('ockDraggable'); - expect(draggable).toHaveStyle({ - top: '46px', - }); - }); }); From 5adb8820ca42fd7f754b7abb2c5318f30b6e507b Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 7 Jan 2025 12:59:22 -0800 Subject: [PATCH 081/150] add test cleanup --- src/wallet/components/Wallet.test.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/wallet/components/Wallet.test.tsx b/src/wallet/components/Wallet.test.tsx index 5a46dd3495..680c05b82e 100644 --- a/src/wallet/components/Wallet.test.tsx +++ b/src/wallet/components/Wallet.test.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { useOutsideClick } from '../../ui/react/internal/hooks/useOutsideClick'; import { ConnectWallet } from './ConnectWallet'; import { Wallet } from './Wallet'; @@ -189,6 +189,8 @@ describe('Wallet Component', () => { }); it('should render WalletIsland right-aligned when there is not enough space on the right', () => { + const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect; + Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, @@ -223,6 +225,8 @@ describe('Wallet Component', () => { expect(screen.getByTestId('ockWalletIslandContainer')).toHaveClass( 'right-0', ); + + Element.prototype.getBoundingClientRect = originalGetBoundingClientRect; }); it('should render WalletIsland left-aligned when there is enough space on the right', () => { From 2039a944fbaae38c36aaba9a23e9c21634b793cc Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 7 Jan 2025 12:59:53 -0800 Subject: [PATCH 082/150] fix: lint --- src/wallet/components/Wallet.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wallet/components/Wallet.test.tsx b/src/wallet/components/Wallet.test.tsx index 680c05b82e..5361ee8981 100644 --- a/src/wallet/components/Wallet.test.tsx +++ b/src/wallet/components/Wallet.test.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useOutsideClick } from '../../ui/react/internal/hooks/useOutsideClick'; import { ConnectWallet } from './ConnectWallet'; import { Wallet } from './Wallet'; From 7c730e4c1370d8e5d804d866868a0de9059db5ff Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 7 Jan 2025 13:03:49 -0800 Subject: [PATCH 083/150] stronger test cleanup --- src/wallet/components/Wallet.test.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/wallet/components/Wallet.test.tsx b/src/wallet/components/Wallet.test.tsx index 5361ee8981..452376a9d0 100644 --- a/src/wallet/components/Wallet.test.tsx +++ b/src/wallet/components/Wallet.test.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { useOutsideClick } from '../../ui/react/internal/hooks/useOutsideClick'; import { ConnectWallet } from './ConnectWallet'; import { Wallet } from './Wallet'; @@ -34,6 +34,8 @@ vi.mock('./WalletProvider', () => ({ WalletProvider: ({ children }: WalletProviderReact) => <>{children}, })); +const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect; + describe('Wallet Component', () => { let mockHandleClose: ReturnType; @@ -48,6 +50,10 @@ describe('Wallet Component', () => { vi.clearAllMocks(); }); + afterEach(() => { + Element.prototype.getBoundingClientRect = originalGetBoundingClientRect; + }); + it('should render the Wallet component with ConnectWallet', () => { (useWalletContext as ReturnType).mockReturnValue({ isOpen: false, @@ -189,8 +195,6 @@ describe('Wallet Component', () => { }); it('should render WalletIsland right-aligned when there is not enough space on the right', () => { - const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect; - Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, @@ -225,8 +229,6 @@ describe('Wallet Component', () => { expect(screen.getByTestId('ockWalletIslandContainer')).toHaveClass( 'right-0', ); - - Element.prototype.getBoundingClientRect = originalGetBoundingClientRect; }); it('should render WalletIsland left-aligned when there is enough space on the right', () => { From 979980d40b301d4dafacf45458f04b79142a6727 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 7 Jan 2025 13:12:50 -0800 Subject: [PATCH 084/150] add test for click prevention on drag --- src/internal/components/Draggable.test.tsx | 38 ++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/internal/components/Draggable.test.tsx b/src/internal/components/Draggable.test.tsx index 4a09e99178..fafca32d8f 100644 --- a/src/internal/components/Draggable.test.tsx +++ b/src/internal/components/Draggable.test.tsx @@ -155,6 +155,44 @@ describe('Draggable', () => { expect(draggable).toHaveStyle({ left: '100px', top: '75px' }); }); + it('prevents the click event when drag distance is more than 2 pixels', async () => { + const onClick = vi.fn(); + + render( + +
Drag me
+
, + ); + const draggable = screen.getByTestId('ockDraggable'); + + // Start drag + fireEvent.pointerDown(draggable, { clientX: 0, clientY: 0 }); + + // Move more than 2 pixels + fireEvent.pointerMove(document, { clientX: 10, clientY: 10 }); + + // Mock the click event that would be triggered by pointerup + const preventDefaultSpy = vi.fn(); + const stopPropagationSpy = vi.fn(); + const mockClickEvent = new MouseEvent('click', { + bubbles: true, + cancelable: true, + }); + Object.defineProperties(mockClickEvent, { + preventDefault: { value: preventDefaultSpy }, + stopPropagation: { value: stopPropagationSpy } + }); + + // End drag + fireEvent.pointerUp(document, { clientX: 10, clientY: 10 }); + + // Simulate the click that would happen + document.dispatchEvent(mockClickEvent); + + expect(preventDefaultSpy).toHaveBeenCalled(); + expect(stopPropagationSpy).toHaveBeenCalled(); + }); + it('cleans up event listeners when unmounted during drag', () => { const { unmount } = render( From 648f829e2b27a15e26f6532334dcdead0c86ccac Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 7 Jan 2025 13:13:46 -0800 Subject: [PATCH 085/150] fix: lint --- src/internal/components/Draggable.test.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/internal/components/Draggable.test.tsx b/src/internal/components/Draggable.test.tsx index fafca32d8f..672447525f 100644 --- a/src/internal/components/Draggable.test.tsx +++ b/src/internal/components/Draggable.test.tsx @@ -160,7 +160,9 @@ describe('Draggable', () => { render( -
Drag me
+
+ Drag me +
, ); const draggable = screen.getByTestId('ockDraggable'); @@ -180,7 +182,7 @@ describe('Draggable', () => { }); Object.defineProperties(mockClickEvent, { preventDefault: { value: preventDefaultSpy }, - stopPropagation: { value: stopPropagationSpy } + stopPropagation: { value: stopPropagationSpy }, }); // End drag From 3ba0475763fb4debc9408e8332b5e075fbe0a884 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 7 Jan 2025 14:55:44 -0800 Subject: [PATCH 086/150] use standard tailwind --- src/wallet/components/WalletIslandAddressDetails.tsx | 2 +- src/wallet/components/WalletIslandQrReceive.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wallet/components/WalletIslandAddressDetails.tsx b/src/wallet/components/WalletIslandAddressDetails.tsx index 9b9825989a..759ebb2ba9 100644 --- a/src/wallet/components/WalletIslandAddressDetails.tsx +++ b/src/wallet/components/WalletIslandAddressDetails.tsx @@ -63,7 +63,7 @@ export function AddressDetails() { color.foreground, border.default, border.radius, - 'absolute top-full right-[0%] z-10 mt-0.5 px-1.5 py-0.5 opacity-0 transition-opacity group-hover:opacity-100', + 'absolute top-full right-0 z-10 mt-0.5 px-1.5 py-0.5 opacity-0 transition-opacity group-hover:opacity-100', )} aria-live="polite" data-testid="ockWalletIsland_NameTooltip" diff --git a/src/wallet/components/WalletIslandQrReceive.tsx b/src/wallet/components/WalletIslandQrReceive.tsx index 015edcea20..9601b29c42 100644 --- a/src/wallet/components/WalletIslandQrReceive.tsx +++ b/src/wallet/components/WalletIslandQrReceive.tsx @@ -110,7 +110,7 @@ export function WalletIslandQrReceive() { color.foreground, border.default, border.radius, - 'absolute top-full right-[0%] z-10 mt-0.5 px-1.5 py-0.5 opacity-0 transition-opacity group-hover:opacity-100', + 'absolute top-full right-0 z-10 mt-0.5 px-1.5 py-0.5 opacity-0 transition-opacity group-hover:opacity-100', )} aria-live="polite" aria-label="Copy tooltip" From 2bccd8d56bec78801fe8520dec94ed933a134d67 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 7 Jan 2025 16:18:19 -0800 Subject: [PATCH 087/150] update get portfolios data structure --- src/core-react/wallet/hooks/usePortfolioTokenBalances.ts | 2 +- src/core/api/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts index 50acb5d314..c3af8e6872 100644 --- a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts @@ -23,7 +23,7 @@ export function usePortfolioTokenBalances({ throw new Error(response.message); } - const userPortfolio = response.tokens[0]; + const userPortfolio = response.portfolios[0]; const transformedPortfolio: PortfolioTokenBalances = { address: userPortfolio.address, diff --git a/src/core/api/types.ts b/src/core/api/types.ts index cbb34416ea..eec28d575d 100644 --- a/src/core/api/types.ts +++ b/src/core/api/types.ts @@ -276,7 +276,7 @@ export type PortfolioTokenWithFiatValue = Token & { * Note: exported as public Type */ export type GetPortfoliosAPIResponse = { - tokens: PortfolioAPIResponse[]; + portfolios: PortfolioAPIResponse[]; }; /** From 418326ebd63ce8838a70a7131e6b3b04c107f838 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 7 Jan 2025 16:20:02 -0800 Subject: [PATCH 088/150] remove focus, fix spacing, fix animation --- src/wallet/components/WalletIslandSwap.tsx | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/wallet/components/WalletIslandSwap.tsx b/src/wallet/components/WalletIslandSwap.tsx index 0855828235..22cdad8dbb 100644 --- a/src/wallet/components/WalletIslandSwap.tsx +++ b/src/wallet/components/WalletIslandSwap.tsx @@ -14,7 +14,7 @@ import { SwapToggleButton, } from '@/swap'; import type { SwapDefaultReact } from '@/swap/types'; -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback } from 'react'; import { useWalletIslandContext } from './WalletIslandProvider'; export function WalletIslandSwap({ @@ -30,9 +30,8 @@ export function WalletIslandSwap({ title, to, }: SwapDefaultReact) { - const { showSwap, setShowSwap, isSwapClosing, setIsSwapClosing } = + const { setShowSwap, isSwapClosing, setIsSwapClosing } = useWalletIslandContext(); - const swapDivRef = useRef(null); const handleCloseSwap = useCallback(() => { setIsSwapClosing(true); @@ -46,12 +45,6 @@ export function WalletIslandSwap({ }, 400); }, [setShowSwap, setIsSwapClosing]); - useEffect(() => { - if (showSwap) { - swapDivRef.current?.focus(); - } - }, [showSwap]); - const backButton = ( From 95ea7f271b39e3c4e2a4ff8796dbcf5b5ba55caa Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 7 Jan 2025 16:21:35 -0800 Subject: [PATCH 090/150] fix spacing --- src/wallet/components/WalletIslandContent.tsx | 12 +++++------- src/wallet/components/WalletIslandWalletActions.tsx | 2 +- tailwind.config.js | 1 + 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/wallet/components/WalletIslandContent.tsx b/src/wallet/components/WalletIslandContent.tsx index 3f489c9e68..ec46cd5dca 100644 --- a/src/wallet/components/WalletIslandContent.tsx +++ b/src/wallet/components/WalletIslandContent.tsx @@ -39,8 +39,7 @@ export function WalletIslandContent({
@@ -49,8 +48,7 @@ export function WalletIslandContent({
@@ -76,9 +74,9 @@ export function WalletIslandContent({
diff --git a/src/wallet/components/WalletIslandWalletActions.tsx b/src/wallet/components/WalletIslandWalletActions.tsx index 5c8af8e6d8..1eaf39b7e2 100644 --- a/src/wallet/components/WalletIslandWalletActions.tsx +++ b/src/wallet/components/WalletIslandWalletActions.tsx @@ -64,7 +64,7 @@ export function WalletIslandWalletActions() {
-
+
+ , ); - const draggable = screen.getByTestId('ockDraggable'); + const clickable = screen.getByTestId('clickable'); - // Start drag - fireEvent.pointerDown(draggable, { clientX: 0, clientY: 0 }); - - // Move more than 2 pixels - fireEvent.pointerMove(document, { clientX: 10, clientY: 10 }); - - // Mock the click event that would be triggered by pointerup - const preventDefaultSpy = vi.fn(); - const stopPropagationSpy = vi.fn(); - const mockClickEvent = new MouseEvent('click', { - bubbles: true, - cancelable: true, - }); - Object.defineProperties(mockClickEvent, { - preventDefault: { value: preventDefaultSpy }, - stopPropagation: { value: stopPropagationSpy }, - }); - - // End drag - fireEvent.pointerUp(document, { clientX: 10, clientY: 10 }); - - // Simulate the click that would happen - document.dispatchEvent(mockClickEvent); + await user.pointer([ + { keys: '[MouseLeft>]', target: clickable, coords: { x: 0, y: 0 } }, + { coords: { x: 50, y: 50 } }, + { keys: '[/MouseLeft]' }, + ]); + expect(onClick).not.toHaveBeenCalled(); - expect(preventDefaultSpy).toHaveBeenCalled(); - expect(stopPropagationSpy).toHaveBeenCalled(); + await user.pointer([ + { keys: '[MouseLeft>]', target: clickable, coords: { x: 0, y: 0 } }, + { keys: '[/MouseLeft]' }, + ]); + expect(onClick).toHaveBeenCalled(); }); it('cleans up event listeners when unmounted during drag', () => { From a873f7cdeefe72840a6ffe43ccf5a3607869c0bc Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 7 Jan 2025 19:26:34 -0800 Subject: [PATCH 095/150] remove duplicate theme detection --- src/wallet/components/WalletIslandContent.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/wallet/components/WalletIslandContent.tsx b/src/wallet/components/WalletIslandContent.tsx index ec46cd5dca..dfdded4ba1 100644 --- a/src/wallet/components/WalletIslandContent.tsx +++ b/src/wallet/components/WalletIslandContent.tsx @@ -1,4 +1,3 @@ -import { useTheme } from '@/core-react/internal/hooks/useTheme'; import { background, border, cn, text } from '@/styles/theme'; import { WALLET_ISLAND_DEFAULT_SWAPPABLE_TOKENS } from '../constants'; import type { WalletIslandReact } from '../types'; @@ -13,13 +12,11 @@ export function WalletIslandContent({ }: WalletIslandReact) { const { isClosing, setIsOpen, setIsClosing } = useWalletContext(); const { showQr, showSwap, tokenBalances } = useWalletIslandContext(); - const componentTheme = useTheme(); return (
Date: Tue, 7 Jan 2025 19:49:09 -0800 Subject: [PATCH 096/150] remove default swap tokens --- src/wallet/components/WalletIslandSwap.tsx | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/wallet/components/WalletIslandSwap.tsx b/src/wallet/components/WalletIslandSwap.tsx index 22cdad8dbb..e7adffab5b 100644 --- a/src/wallet/components/WalletIslandSwap.tsx +++ b/src/wallet/components/WalletIslandSwap.tsx @@ -83,19 +83,9 @@ export function WalletIslandSwap({ - + - + From fdc3fe5c3ceec220c11fe72fbc850d898b34f46a Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 7 Jan 2025 20:15:38 -0800 Subject: [PATCH 097/150] rename WalletIslandDefault to WalletIslandDraggable --- .../nextjs-app-router/components/Demo.tsx | 31 +++++------------ .../components/DemoOptions.tsx | 2 -- ...dDefault.tsx => WalletIslandDraggable.tsx} | 0 .../components/form/active-component.tsx | 4 +-- .../components/form/anchor-position.tsx | 33 ------------------- .../nextjs-app-router/types/onchainkit.ts | 2 +- ...est.tsx => WalletIslandDraggable.test.tsx} | 8 ++--- ...dDefault.tsx => WalletIslandDraggable.tsx} | 2 +- src/wallet/index.ts | 2 +- 9 files changed, 17 insertions(+), 67 deletions(-) rename playground/nextjs-app-router/components/demo/{WalletIslandDefault.tsx => WalletIslandDraggable.tsx} (100%) delete mode 100644 playground/nextjs-app-router/components/form/anchor-position.tsx rename src/wallet/components/{WalletIslandDefault.test.tsx => WalletIslandDraggable.test.tsx} (94%) rename src/wallet/components/{WalletIslandDefault.tsx => WalletIslandDraggable.tsx} (95%) diff --git a/playground/nextjs-app-router/components/Demo.tsx b/playground/nextjs-app-router/components/Demo.tsx index ef691793f9..6d5a50e251 100644 --- a/playground/nextjs-app-router/components/Demo.tsx +++ b/playground/nextjs-app-router/components/Demo.tsx @@ -20,7 +20,7 @@ import TransactionDemo from './demo/Transaction'; import TransactionDefaultDemo from './demo/TransactionDefault'; import WalletDemo from './demo/Wallet'; import WalletDefaultDemo from './demo/WalletDefault'; -import WalletIslandDemo from './demo/WalletIslandDefault'; +import WalletIslandDraggableDemo from './demo/WalletIslandDraggable'; const activeComponentMapping: Record = { [OnchainKitComponent.Buy]: BuyDemo, @@ -32,7 +32,7 @@ const activeComponentMapping: Record = { [OnchainKitComponent.SwapDefault]: SwapDefaultDemo, [OnchainKitComponent.Wallet]: WalletDemo, [OnchainKitComponent.WalletDefault]: WalletDefaultDemo, - [OnchainKitComponent.WalletIslandDefault]: WalletIslandDemo, + [OnchainKitComponent.WalletIslandDraggable]: WalletIslandDraggableDemo, [OnchainKitComponent.TransactionDefault]: TransactionDefaultDemo, [OnchainKitComponent.NFTMintCard]: NFTMintCardDemo, [OnchainKitComponent.NFTCard]: NFTCardDemo, @@ -42,7 +42,7 @@ const activeComponentMapping: Record = { }; export default function Demo() { - const { activeComponent, anchorPosition } = useContext(AppContext); + const { activeComponent } = useContext(AppContext); const [isDarkMode, setIsDarkMode] = useState(true); const [sideBarVisible, setSideBarVisible] = useState(true); const [copied, setCopied] = useState(false); @@ -80,11 +80,6 @@ export default function Demo() { ? activeComponentMapping[activeComponent] : null; - const componentPosition = getComponentPosition( - activeComponent, - anchorPosition, - ); - return ( <>
-
+
{ActiveComponent && }
); } - -function getComponentPosition( - activeComponent: OnchainKitComponent | undefined, - anchorPosition: string | undefined, -) { - if (activeComponent === OnchainKitComponent.WalletIslandDefault) { - if (anchorPosition?.includes('top')) { - return 'justify-start'; - } - return 'justify-end'; - } - - return 'items-center justify-center'; -} diff --git a/playground/nextjs-app-router/components/DemoOptions.tsx b/playground/nextjs-app-router/components/DemoOptions.tsx index 4c3a0562be..1c11330b45 100644 --- a/playground/nextjs-app-router/components/DemoOptions.tsx +++ b/playground/nextjs-app-router/components/DemoOptions.tsx @@ -3,7 +3,6 @@ import { ComponentTheme } from '@/components/form/component-theme'; import { PaymasterUrl } from '@/components/form/paymaster'; import { OnchainKitComponent } from '@/types/onchainkit'; import { ActiveComponent } from './form/active-component'; -import { AnchorPosition } from './form/anchor-position'; import { Chain } from './form/chain'; import { CheckoutOptions } from './form/checkout-options'; import { IsSponsored } from './form/is-sponsored'; @@ -62,7 +61,6 @@ const COMPONENT_CONFIG: Partial< IsSponsored, NFTOptions, ], - [OnchainKitComponent.WalletIslandDefault]: [AnchorPosition], }; export default function DemoOptions({ diff --git a/playground/nextjs-app-router/components/demo/WalletIslandDefault.tsx b/playground/nextjs-app-router/components/demo/WalletIslandDraggable.tsx similarity index 100% rename from playground/nextjs-app-router/components/demo/WalletIslandDefault.tsx rename to playground/nextjs-app-router/components/demo/WalletIslandDraggable.tsx diff --git a/playground/nextjs-app-router/components/form/active-component.tsx b/playground/nextjs-app-router/components/form/active-component.tsx index 838911279c..c9d86cc05a 100644 --- a/playground/nextjs-app-router/components/form/active-component.tsx +++ b/playground/nextjs-app-router/components/form/active-component.tsx @@ -47,8 +47,8 @@ export function ActiveComponent() { WalletDefault - - WalletIslandDefault + + WalletIslandDraggable NFT Card diff --git a/playground/nextjs-app-router/components/form/anchor-position.tsx b/playground/nextjs-app-router/components/form/anchor-position.tsx deleted file mode 100644 index 8d1ec253b0..0000000000 --- a/playground/nextjs-app-router/components/form/anchor-position.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Label } from '@/components/ui/label'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { useContext } from 'react'; -import { AppContext } from '../AppProvider'; - -export function AnchorPosition() { - const { anchorPosition, setAnchorPosition } = useContext(AppContext); - - return ( -
- - -
- ); -} diff --git a/playground/nextjs-app-router/types/onchainkit.ts b/playground/nextjs-app-router/types/onchainkit.ts index fb18a7e95f..8033c65bec 100644 --- a/playground/nextjs-app-router/types/onchainkit.ts +++ b/playground/nextjs-app-router/types/onchainkit.ts @@ -10,7 +10,7 @@ export enum OnchainKitComponent { TransactionDefault = 'transaction-default', Wallet = 'wallet', WalletDefault = 'wallet-default', - WalletIslandDefault = 'wallet-island-default', + WalletIslandDraggable = 'wallet-island-draggable', NFTCard = 'nft-card', NFTCardDefault = 'nft-card-default', NFTMintCard = 'nft-mint-card', diff --git a/src/wallet/components/WalletIslandDefault.test.tsx b/src/wallet/components/WalletIslandDraggable.test.tsx similarity index 94% rename from src/wallet/components/WalletIslandDefault.test.tsx rename to src/wallet/components/WalletIslandDraggable.test.tsx index d7b86bc370..ee6fe67605 100644 --- a/src/wallet/components/WalletIslandDefault.test.tsx +++ b/src/wallet/components/WalletIslandDraggable.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; import { useAccount, useConnect } from 'wagmi'; -import { WalletIslandDefault } from './WalletIslandDefault'; +import { WalletIslandDraggable } from './WalletIslandDraggable'; import { useWalletContext } from './WalletProvider'; vi.mock('wagmi', () => ({ @@ -63,7 +63,7 @@ describe('WalletIslandDefault', () => { it('renders ConnectWallet in disconnected state', () => { mockUseWalletContext.mockReturnValue({ isOpen: false }); - render(); + render(); expect(screen.getByTestId('ockConnectWallet_Container')).toBeDefined(); }); @@ -80,7 +80,7 @@ describe('WalletIslandDefault', () => { mockUseWalletContext.mockReturnValue({ isOpen: false }); - render(); + render(); expect(screen.getByTestId('ockAvatar_ImageContainer')).toBeDefined(); expect(screen.getByTestId('ockIdentity_Text')).toBeDefined(); @@ -98,7 +98,7 @@ describe('WalletIslandDefault', () => { mockUseWalletContext.mockReturnValue({ isOpen: true }); - render(); + render(); expect(screen.getByTestId('ockWalletIslandContent')).toBeDefined(); }); diff --git a/src/wallet/components/WalletIslandDefault.tsx b/src/wallet/components/WalletIslandDraggable.tsx similarity index 95% rename from src/wallet/components/WalletIslandDefault.tsx rename to src/wallet/components/WalletIslandDraggable.tsx index 8a908d879b..0c9c454432 100644 --- a/src/wallet/components/WalletIslandDefault.tsx +++ b/src/wallet/components/WalletIslandDraggable.tsx @@ -8,7 +8,7 @@ import { WalletIslandTokenHoldings } from './WalletIslandTokenHoldings'; import { WalletIslandTransactionActions } from './WalletIslandTransactionActions'; import { WalletIslandWalletActions } from './WalletIslandWalletActions'; -export function WalletIslandDefault() { +export function WalletIslandDraggable() { return ( diff --git a/src/wallet/index.ts b/src/wallet/index.ts index 5912ec2c88..f1c00eb9da 100644 --- a/src/wallet/index.ts +++ b/src/wallet/index.ts @@ -9,7 +9,7 @@ export { WalletDropdownDisconnect } from './components/WalletDropdownDisconnect' export { WalletDropdownFundLink } from './components/WalletDropdownFundLink'; export { WalletDropdownLink } from './components/WalletDropdownLink'; export { WalletIsland } from './components/WalletIsland'; -export { WalletIslandDefault } from './components/WalletIslandDefault'; +export { WalletIslandDraggable as WalletIslandDefault } from './components/WalletIslandDraggable'; export { WalletIslandQrReceive } from './components/WalletIslandQrReceive'; export { WalletIslandTransactionActions } from './components/WalletIslandTransactionActions'; export { WalletIslandTokenHoldings } from './components/WalletIslandTokenHoldings'; From 555fdac58067abbe4c3e35e02f1807e3b2a75632 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 7 Jan 2025 20:26:04 -0800 Subject: [PATCH 098/150] added WalletIslandFixed --- src/wallet/components/WalletIslandFixed.tsx | 27 +++++++++++++++++++++ src/wallet/index.ts | 3 ++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 src/wallet/components/WalletIslandFixed.tsx diff --git a/src/wallet/components/WalletIslandFixed.tsx b/src/wallet/components/WalletIslandFixed.tsx new file mode 100644 index 0000000000..b17c4b5023 --- /dev/null +++ b/src/wallet/components/WalletIslandFixed.tsx @@ -0,0 +1,27 @@ +import { Avatar, Name } from '../../ui/react/identity'; +import { ConnectWallet } from './ConnectWallet'; +import { ConnectWalletText } from './ConnectWalletText'; +import { Wallet } from './Wallet'; +import { WalletIsland } from './WalletIsland'; +import { AddressDetails } from './WalletIslandAddressDetails'; +import { WalletIslandTokenHoldings } from './WalletIslandTokenHoldings'; +import { WalletIslandTransactionActions } from './WalletIslandTransactionActions'; +import { WalletIslandWalletActions } from './WalletIslandWalletActions'; + +export function WalletIslandFixed() { + return ( + + + Connect Wallet + + + + + + + + + + + ); +} diff --git a/src/wallet/index.ts b/src/wallet/index.ts index f1c00eb9da..1682ea2388 100644 --- a/src/wallet/index.ts +++ b/src/wallet/index.ts @@ -9,7 +9,8 @@ export { WalletDropdownDisconnect } from './components/WalletDropdownDisconnect' export { WalletDropdownFundLink } from './components/WalletDropdownFundLink'; export { WalletDropdownLink } from './components/WalletDropdownLink'; export { WalletIsland } from './components/WalletIsland'; -export { WalletIslandDraggable as WalletIslandDefault } from './components/WalletIslandDraggable'; +export { WalletIslandDraggable } from './components/WalletIslandDraggable'; +export { WalletIslandFixed } from './components/WalletIslandFixed'; export { WalletIslandQrReceive } from './components/WalletIslandQrReceive'; export { WalletIslandTransactionActions } from './components/WalletIslandTransactionActions'; export { WalletIslandTokenHoldings } from './components/WalletIslandTokenHoldings'; From 69b45ba559899f745f2004a10331dda09ee594eb Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 7 Jan 2025 20:28:11 -0800 Subject: [PATCH 099/150] add WalletIslandFixed to playground --- playground/nextjs-app-router/components/Demo.tsx | 2 ++ .../components/demo/WalletIslandFixed.tsx | 11 +++++++++++ .../components/form/active-component.tsx | 3 +++ playground/nextjs-app-router/types/onchainkit.ts | 1 + 4 files changed, 17 insertions(+) create mode 100644 playground/nextjs-app-router/components/demo/WalletIslandFixed.tsx diff --git a/playground/nextjs-app-router/components/Demo.tsx b/playground/nextjs-app-router/components/Demo.tsx index 6d5a50e251..6929c40421 100644 --- a/playground/nextjs-app-router/components/Demo.tsx +++ b/playground/nextjs-app-router/components/Demo.tsx @@ -21,6 +21,7 @@ import TransactionDefaultDemo from './demo/TransactionDefault'; import WalletDemo from './demo/Wallet'; import WalletDefaultDemo from './demo/WalletDefault'; import WalletIslandDraggableDemo from './demo/WalletIslandDraggable'; +import WalletIslandFixedDemo from './demo/WalletIslandFixed'; const activeComponentMapping: Record = { [OnchainKitComponent.Buy]: BuyDemo, @@ -33,6 +34,7 @@ const activeComponentMapping: Record = { [OnchainKitComponent.Wallet]: WalletDemo, [OnchainKitComponent.WalletDefault]: WalletDefaultDemo, [OnchainKitComponent.WalletIslandDraggable]: WalletIslandDraggableDemo, + [OnchainKitComponent.WalletIslandFixed]: WalletIslandFixedDemo, [OnchainKitComponent.TransactionDefault]: TransactionDefaultDemo, [OnchainKitComponent.NFTMintCard]: NFTMintCardDemo, [OnchainKitComponent.NFTCard]: NFTCardDemo, diff --git a/playground/nextjs-app-router/components/demo/WalletIslandFixed.tsx b/playground/nextjs-app-router/components/demo/WalletIslandFixed.tsx new file mode 100644 index 0000000000..2adc42eb4b --- /dev/null +++ b/playground/nextjs-app-router/components/demo/WalletIslandFixed.tsx @@ -0,0 +1,11 @@ +import { WalletIslandFixed } from '@coinbase/onchainkit/wallet'; + +export default function WalletIslandFixedDemo() { + return ( +
+
+ +
+
+ ); +} diff --git a/playground/nextjs-app-router/components/form/active-component.tsx b/playground/nextjs-app-router/components/form/active-component.tsx index c9d86cc05a..7e1f95d297 100644 --- a/playground/nextjs-app-router/components/form/active-component.tsx +++ b/playground/nextjs-app-router/components/form/active-component.tsx @@ -50,6 +50,9 @@ export function ActiveComponent() { WalletIslandDraggable + + WalletIslandFixed + NFT Card NFT Card Default diff --git a/playground/nextjs-app-router/types/onchainkit.ts b/playground/nextjs-app-router/types/onchainkit.ts index 8033c65bec..6ecb372194 100644 --- a/playground/nextjs-app-router/types/onchainkit.ts +++ b/playground/nextjs-app-router/types/onchainkit.ts @@ -11,6 +11,7 @@ export enum OnchainKitComponent { Wallet = 'wallet', WalletDefault = 'wallet-default', WalletIslandDraggable = 'wallet-island-draggable', + WalletIslandFixed = 'wallet-island-fixed', NFTCard = 'nft-card', NFTCardDefault = 'nft-card-default', NFTMintCard = 'nft-mint-card', From 84e359978f9953b4fa42f70d7eee1c0fbeeaff2f Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 7 Jan 2025 20:28:27 -0800 Subject: [PATCH 100/150] simplify demo --- .../components/demo/WalletIslandDraggable.tsx | 30 ++----------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/playground/nextjs-app-router/components/demo/WalletIslandDraggable.tsx b/playground/nextjs-app-router/components/demo/WalletIslandDraggable.tsx index bb3e0e3493..6a97a66615 100644 --- a/playground/nextjs-app-router/components/demo/WalletIslandDraggable.tsx +++ b/playground/nextjs-app-router/components/demo/WalletIslandDraggable.tsx @@ -1,29 +1,5 @@ -import { AppContext } from '@/components/AppProvider'; -import { cn } from '@/lib/utils'; -import { WalletIslandDefault } from '@coinbase/onchainkit/wallet'; -import { useContext } from 'react'; +import { WalletIslandDraggable } from '@coinbase/onchainkit/wallet'; -const anchorPositionToClassMap = { - 'top-left': 'justify-start self-start', - 'top-center': 'justify-start self-center', - 'top-right': 'justify-start self-end', - 'bottom-left': 'justify-end self-start', - 'bottom-center': 'justify-end self-center', - 'bottom-right': 'justify-end self-end', -}; - -export default function WalletIslandDefaultDemo() { - const { anchorPosition } = useContext(AppContext); - - const anchorPositionClass = anchorPosition - ? anchorPositionToClassMap[ - anchorPosition as keyof typeof anchorPositionToClassMap - ] - : ''; - - return ( -
- -
- ); +export default function WalletIslandDraggableDemo() { + return ; } From 0f1ec45f0572abb0275a115a410cb4d9626d63c3 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 7 Jan 2025 20:28:37 -0800 Subject: [PATCH 101/150] update ock --- playground/nextjs-app-router/onchainkit/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/nextjs-app-router/onchainkit/package.json b/playground/nextjs-app-router/onchainkit/package.json index 67381c60cb..d79c635b7f 100644 --- a/playground/nextjs-app-router/onchainkit/package.json +++ b/playground/nextjs-app-router/onchainkit/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/onchainkit", - "version": "0.36.3", + "version": "0.36.4", "type": "module", "repository": "https://github.com/coinbase/onchainkit.git", "license": "MIT", From 652b5c79c9ccea5196b217ec98b27273cdd22595 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 7 Jan 2025 20:31:16 -0800 Subject: [PATCH 102/150] fix tests --- .../components/WalletIslandDraggable.test.tsx | 2 +- .../components/WalletIslandFixed.test.tsx | 105 ++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 src/wallet/components/WalletIslandFixed.test.tsx diff --git a/src/wallet/components/WalletIslandDraggable.test.tsx b/src/wallet/components/WalletIslandDraggable.test.tsx index ee6fe67605..0a9bb5a3c1 100644 --- a/src/wallet/components/WalletIslandDraggable.test.tsx +++ b/src/wallet/components/WalletIslandDraggable.test.tsx @@ -42,7 +42,7 @@ vi.mock('./WalletProvider', () => ({ ), })); -describe('WalletIslandDefault', () => { +describe('WalletIslandDraggable', () => { const mockUseWalletContext = useWalletContext as ReturnType; beforeEach(() => { diff --git a/src/wallet/components/WalletIslandFixed.test.tsx b/src/wallet/components/WalletIslandFixed.test.tsx new file mode 100644 index 0000000000..e342b0c285 --- /dev/null +++ b/src/wallet/components/WalletIslandFixed.test.tsx @@ -0,0 +1,105 @@ +import { render, screen } from '@testing-library/react'; +import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useAccount, useConnect } from 'wagmi'; +import { WalletIslandFixed } from './WalletIslandFixed'; +import { useWalletContext } from './WalletProvider'; + +vi.mock('wagmi', () => ({ + useAccount: vi.fn(), + useConnect: vi.fn(), + useConfig: vi.fn(), +})); + +vi.mock('../../core-react/internal/hooks/useTheme', () => ({ + useTheme: vi.fn(), +})); + +vi.mock('../../core-react/identity/hooks/useAvatar', () => ({ + useAvatar: () => ({ data: null, isLoading: false }), +})); + +vi.mock('../../core-react/identity/hooks/useName', () => ({ + useName: () => ({ data: null, isLoading: false }), +})); + +vi.mock('./WalletIslandProvider', () => ({ + useWalletIslandContext: vi.fn(), + WalletIslandProvider: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), +})); + +vi.mock('./WalletIslandContent', () => ({ + WalletIslandContent: () => ( +
WalletIslandContent
+ ), +})); + +vi.mock('./WalletProvider', () => ({ + useWalletContext: vi.fn(), + WalletProvider: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), +})); + +describe('WalletIslandFixed', () => { + const mockUseWalletContext = useWalletContext as ReturnType; + + beforeEach(() => { + vi.clearAllMocks(); + (useConnect as ReturnType).mockReturnValue({ + connectors: [], + status: 'disconnected', + }); + (useAccount as ReturnType).mockReturnValue({ + status: 'disconnected', + address: '', + }); + (useWalletContext as Mock).mockReturnValue({ + isOpen: false, + }); + }); + + it('renders ConnectWallet in disconnected state', () => { + mockUseWalletContext.mockReturnValue({ isOpen: false }); + + render(); + + expect(screen.getByTestId('ockConnectWallet_Container')).toBeDefined(); + }); + + it('renders Avatar and Name in connected state and isOpen is false', () => { + (useConnect as ReturnType).mockReturnValue({ + connectors: [], + status: 'connected', + }); + (useAccount as ReturnType).mockReturnValue({ + status: 'connected', + address: '0x123', + }); + + mockUseWalletContext.mockReturnValue({ isOpen: false }); + + render(); + + expect(screen.getByTestId('ockAvatar_ImageContainer')).toBeDefined(); + expect(screen.getByTestId('ockIdentity_Text')).toBeDefined(); + }); + + it('renders WalletIslandContent in connected state and isOpen is true', () => { + (useConnect as ReturnType).mockReturnValue({ + connectors: [], + status: 'connected', + }); + (useAccount as ReturnType).mockReturnValue({ + status: 'connected', + address: '0x123', + }); + + mockUseWalletContext.mockReturnValue({ isOpen: true }); + + render(); + + expect(screen.getByTestId('ockWalletIslandContent')).toBeDefined(); + }); +}); From 292afe09a5348cc222d69d7eeff487f143066135 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 10:46:30 -0800 Subject: [PATCH 103/150] use radiusInner, fix spacing, address comments --- .../nextjs-app-router/components/AppProvider.tsx | 11 ----------- src/core/api/getPortfolioTokenBalances.ts | 4 ++-- src/internal/components/PressableIcon.tsx | 2 +- src/wallet/components/WalletIslandContent.tsx | 4 ++-- src/wallet/components/WalletIslandQrReceive.tsx | 2 +- src/wallet/components/WalletIslandTokenHoldings.tsx | 1 - 6 files changed, 6 insertions(+), 18 deletions(-) diff --git a/playground/nextjs-app-router/components/AppProvider.tsx b/playground/nextjs-app-router/components/AppProvider.tsx index 9f49672807..1fd4ca1108 100644 --- a/playground/nextjs-app-router/components/AppProvider.tsx +++ b/playground/nextjs-app-router/components/AppProvider.tsx @@ -38,8 +38,6 @@ type State = { setNFTToken: (nftToken: string) => void; setIsSponsored: (isSponsored: boolean) => void; isSponsored?: boolean; - anchorPosition?: string; - setAnchorPosition: (anchorPosition: string) => void; }; export const defaultState: State = { @@ -51,8 +49,6 @@ export const defaultState: State = { setComponentMode: () => {}, setNFTToken: () => {}, setIsSponsored: () => {}, - anchorPosition: 'top-center', - setAnchorPosition: () => {}, }; export const AppContext = createContext(defaultState); @@ -120,11 +116,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { parser: (v) => v === 'true', }); - const [anchorPosition, setAnchorPosition] = useStateWithStorage({ - key: 'anchorPosition', - defaultValue: defaultState.anchorPosition, - }); - // Load initial values from localStorage useEffect(() => { const storedPaymasters = localStorage.getItem('paymasters'); @@ -168,8 +159,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { setNFTToken, setIsSponsored, isSponsored, - anchorPosition, - setAnchorPosition, }} >
diff --git a/src/wallet/components/WalletIslandQrReceive.tsx b/src/wallet/components/WalletIslandQrReceive.tsx index d186d6976f..50ac9f7125 100644 --- a/src/wallet/components/WalletIslandQrReceive.tsx +++ b/src/wallet/components/WalletIslandQrReceive.tsx @@ -66,7 +66,7 @@ export function WalletIslandQrReceive() { text.headline, 'flex flex-col items-center justify-between', 'h-full w-full', - 'p-2', + 'px-4 pt-3 pb-4', isQrClosing ? 'fade-out slide-out-to-left-5 animate-out fill-mode-forwards ease-in-out' : 'fade-in slide-in-from-left-5 linear animate-in duration-150', diff --git a/src/wallet/components/WalletIslandTokenHoldings.tsx b/src/wallet/components/WalletIslandTokenHoldings.tsx index 45d518f48f..b50d64bf09 100644 --- a/src/wallet/components/WalletIslandTokenHoldings.tsx +++ b/src/wallet/components/WalletIslandTokenHoldings.tsx @@ -32,7 +32,6 @@ export function WalletIslandTokenHoldings() { 'fade-in slide-in-from-top-2.5 animate-in fill-mode-forwards duration-300 ease-out': !isClosing, }, - 'shadow-[inset_0_-15px_10px_-10px_rgba(0,0,0,0.05)]', )} data-testid="ockWalletIsland_TokenHoldings" > From f1381660811053d79a90d3edbaa213a2ecfabb8a Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 11:38:01 -0800 Subject: [PATCH 104/150] better svg titles --- src/internal/svg/cbwSvg.tsx | 2 +- src/internal/svg/clockSvg.tsx | 2 +- src/internal/svg/copySvg.tsx | 2 +- src/internal/svg/refreshSvg.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/internal/svg/cbwSvg.tsx b/src/internal/svg/cbwSvg.tsx index ad1196dbac..eba2168f0f 100644 --- a/src/internal/svg/cbwSvg.tsx +++ b/src/internal/svg/cbwSvg.tsx @@ -5,7 +5,7 @@ export const cbwSvg = ( viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" > - Wallet Icon + Wallet - Clock Icon + Clock - Copy Icon + Copy - Refresh SVG + Refresh Date: Wed, 8 Jan 2025 11:39:04 -0800 Subject: [PATCH 105/150] add startingPosition prop for WalletIslandDraggable --- src/wallet/components/WalletIslandDraggable.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/wallet/components/WalletIslandDraggable.tsx b/src/wallet/components/WalletIslandDraggable.tsx index 0c9c454432..47b9c17848 100644 --- a/src/wallet/components/WalletIslandDraggable.tsx +++ b/src/wallet/components/WalletIslandDraggable.tsx @@ -8,9 +8,16 @@ import { WalletIslandTokenHoldings } from './WalletIslandTokenHoldings'; import { WalletIslandTransactionActions } from './WalletIslandTransactionActions'; import { WalletIslandWalletActions } from './WalletIslandWalletActions'; -export function WalletIslandDraggable() { +export function WalletIslandDraggable({ + startingPosition = { + x: window.innerWidth - 250, + y: window.innerHeight - 100, + }, +}: { + startingPosition?: { x: number; y: number }; +}) { return ( - + Connect Wallet From ba219f0c3e7d7e3e265cec83c8676bd107619a96 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 11:39:28 -0800 Subject: [PATCH 106/150] better aria-labels --- src/wallet/components/WalletIslandQrReceive.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wallet/components/WalletIslandQrReceive.tsx b/src/wallet/components/WalletIslandQrReceive.tsx index 50ac9f7125..861bba5de3 100644 --- a/src/wallet/components/WalletIslandQrReceive.tsx +++ b/src/wallet/components/WalletIslandQrReceive.tsx @@ -84,7 +84,7 @@ export function WalletIslandQrReceive() { @@ -101,7 +101,7 @@ export function WalletIslandQrReceive() { 'absolute top-full right-0 z-10 mt-0.5 px-1.5 py-0.5 opacity-0 transition-opacity group-hover:opacity-100', )} aria-live="polite" - aria-label="Copy tooltip" + aria-label="Copy your address" > {copyText} @@ -112,7 +112,7 @@ export function WalletIslandQrReceive() { type="button" className={cn(border.radius, pressable.alternate, 'w-full p-3')} onClick={() => handleCopyAddress('button')} - aria-label="Copy button" + aria-label="Copy your address" > {copyButtonText} From 04a236ee11208de0e62672a46199b36bd210476e Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 11:40:05 -0800 Subject: [PATCH 107/150] remove throttle on portfolio refresh --- src/wallet/components/WalletIslandWalletActions.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/wallet/components/WalletIslandWalletActions.tsx b/src/wallet/components/WalletIslandWalletActions.tsx index 1eaf39b7e2..ca16f2e53c 100644 --- a/src/wallet/components/WalletIslandWalletActions.tsx +++ b/src/wallet/components/WalletIslandWalletActions.tsx @@ -11,8 +11,7 @@ import { useWalletContext } from './WalletProvider'; export function WalletIslandWalletActions() { const { isClosing, handleClose } = useWalletContext(); - const { setShowQr, refetchPortfolioData, portfolioDataUpdatedAt } = - useWalletIslandContext(); + const { setShowQr, refetchPortfolioData } = useWalletIslandContext(); const { disconnect, connectors } = useDisconnect(); const handleDisconnect = useCallback(() => { @@ -27,14 +26,8 @@ export function WalletIslandWalletActions() { }, [setShowQr]); const handleRefreshPortfolioData = useCallback(async () => { - if ( - portfolioDataUpdatedAt && - Date.now() - portfolioDataUpdatedAt < 1000 * 15 - ) { - return; - } await refetchPortfolioData(); - }, [refetchPortfolioData, portfolioDataUpdatedAt]); + }, [refetchPortfolioData]); return (
Date: Wed, 8 Jan 2025 11:40:34 -0800 Subject: [PATCH 108/150] extract actions into callbacks --- .../WalletIslandTransactionActions.tsx | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/wallet/components/WalletIslandTransactionActions.tsx b/src/wallet/components/WalletIslandTransactionActions.tsx index e823d97b5f..1042c6f628 100644 --- a/src/wallet/components/WalletIslandTransactionActions.tsx +++ b/src/wallet/components/WalletIslandTransactionActions.tsx @@ -4,6 +4,7 @@ import { toggleSvg } from '@/internal/svg/toggleSvg'; import { border, cn, color, pressable, text } from '@/styles/theme'; import { useWalletIslandContext } from './WalletIslandProvider'; import { useWalletContext } from './WalletProvider'; +import { useCallback } from 'react'; type TransactionActionProps = { icon: React.ReactNode; @@ -15,6 +16,18 @@ export function WalletIslandTransactionActions() { const { isClosing } = useWalletContext(); const { setShowSwap } = useWalletIslandContext(); + const handleBuy = useCallback(() => { + window.open('https://pay.coinbase.com', '_blank'); + }, []); + + const handleSend = useCallback(() => { + window.open('https://wallet.coinbase.com', '_blank'); + }, []); + + const handleSwap = useCallback(() => { + setShowSwap(true); + }, [setShowSwap]); + return (
{ - window.open('https://pay.coinbase.com', '_blank'); - }} + action={handleBuy} /> { - window.open('https://wallet.coinbase.com', '_blank'); - }} + action={handleSend} /> { - setShowSwap(true); - }} + action={handleSwap} />
); From 59c41ce5e8cbfc6379c6a7b4d6d9125e20bbeea3 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 11:41:05 -0800 Subject: [PATCH 109/150] better return value if no portfolios received --- src/core-react/wallet/hooks/usePortfolioTokenBalances.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts index e2cc566839..717c7cf3bb 100644 --- a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts @@ -25,7 +25,7 @@ export function usePortfolioTokenBalances({ if (response.portfolios.length === 0) { return { - address: '', + address: addresses?.[0] ?? '', portfolioBalanceUsd: 0, tokenBalances: [], }; From d9d283b5c2a32beff9839c94559f52f30bbbf204 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 11:41:44 -0800 Subject: [PATCH 110/150] fix: lint --- src/wallet/components/WalletIslandTransactionActions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wallet/components/WalletIslandTransactionActions.tsx b/src/wallet/components/WalletIslandTransactionActions.tsx index 1042c6f628..98589df44e 100644 --- a/src/wallet/components/WalletIslandTransactionActions.tsx +++ b/src/wallet/components/WalletIslandTransactionActions.tsx @@ -2,9 +2,9 @@ import { addSvgForeground } from '@/internal/svg/addForegroundSvg'; import { arrowUpRightSvg } from '@/internal/svg/arrowUpRightSvg'; import { toggleSvg } from '@/internal/svg/toggleSvg'; import { border, cn, color, pressable, text } from '@/styles/theme'; +import { useCallback } from 'react'; import { useWalletIslandContext } from './WalletIslandProvider'; import { useWalletContext } from './WalletProvider'; -import { useCallback } from 'react'; type TransactionActionProps = { icon: React.ReactNode; From f36ea1c906bfab64da6792b83016a6c6feffc3f0 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 11:54:44 -0800 Subject: [PATCH 111/150] fix: tests --- .../hooks/usePortfolioTokenBalances.test.tsx | 2 +- .../components/WalletIslandQrReceive.test.tsx | 16 ++++++++-------- .../components/WalletIslandQrReceive.tsx | 3 +++ .../WalletIslandWalletActions.test.tsx | 19 +------------------ 4 files changed, 13 insertions(+), 27 deletions(-) diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx index d22621e88b..b3efd88791 100644 --- a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx @@ -195,7 +195,7 @@ describe('usePortfolioTokenBalances', () => { await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(result.current.data).toEqual({ - address: '', + address: '0x123', portfolioBalanceUsd: 0, tokenBalances: [], }); diff --git a/src/wallet/components/WalletIslandQrReceive.test.tsx b/src/wallet/components/WalletIslandQrReceive.test.tsx index 6506e4372e..577b05525d 100644 --- a/src/wallet/components/WalletIslandQrReceive.test.tsx +++ b/src/wallet/components/WalletIslandQrReceive.test.tsx @@ -154,7 +154,7 @@ describe('WalletIslandQrReceive', () => { render(); - const copyIcon = screen.getByRole('button', { name: /copy icon/i }); + const copyIcon = screen.getByTestId('ockWalletIslandQrReceive_CopyIcon'); await act(async () => { fireEvent.click(copyIcon); @@ -165,7 +165,7 @@ describe('WalletIslandQrReceive', () => { expect(mockClipboard.writeText).toHaveBeenCalledWith('0x1234567890'); expect(mockSetCopyText).toHaveBeenCalledWith('Copied'); - const tooltip = screen.getByRole('button', { name: /copy tooltip/i }); + const tooltip = screen.getByTestId('ockWalletIslandQrReceive_CopyTooltip'); expect(tooltip).toBeInTheDocument(); vi.advanceTimersByTime(2000); @@ -192,7 +192,7 @@ describe('WalletIslandQrReceive', () => { render(); - const copyTooltip = screen.getByRole('button', { name: /copy tooltip/i }); + const copyTooltip = screen.getByTestId('ockWalletIslandQrReceive_CopyTooltip'); await act(async () => { fireEvent.click(copyTooltip); @@ -203,7 +203,7 @@ describe('WalletIslandQrReceive', () => { expect(mockClipboard.writeText).toHaveBeenCalledWith('0x1234567890'); expect(mockSetCopyText).toHaveBeenCalledWith('Copied'); - const tooltip = screen.getByRole('button', { name: /copy tooltip/i }); + const tooltip = screen.getByTestId('ockWalletIslandQrReceive_CopyTooltip'); expect(tooltip).toBeInTheDocument(); vi.advanceTimersByTime(2000); @@ -230,7 +230,7 @@ describe('WalletIslandQrReceive', () => { render(); - const copyButton = screen.getByRole('button', { name: /copy button/i }); + const copyButton = screen.getByTestId('ockWalletIslandQrReceive_CopyButton'); await act(async () => { fireEvent.click(copyButton); @@ -261,7 +261,7 @@ describe('WalletIslandQrReceive', () => { render(); mockSetCopyText.mockClear(); - const copyIcon = screen.getByRole('button', { name: /copy icon/i }); + const copyIcon = screen.getByTestId('ockWalletIslandQrReceive_CopyIcon'); await act(async () => { fireEvent.click(copyIcon); await Promise.resolve(); @@ -273,7 +273,7 @@ describe('WalletIslandQrReceive', () => { vi.advanceTimersByTime(2000); mockSetCopyButtonText.mockClear(); - const copyButton = screen.getByRole('button', { name: /copy button/i }); + const copyButton = screen.getByTestId('ockWalletIslandQrReceive_CopyButton'); await act(async () => { fireEvent.click(copyButton); await Promise.resolve(); @@ -294,7 +294,7 @@ describe('WalletIslandQrReceive', () => { render(); - const copyIcon = screen.getByRole('button', { name: /copy icon/i }); + const copyIcon = screen.getByTestId('ockWalletIslandQrReceive_CopyIcon'); fireEvent.click(copyIcon); expect(mockClipboard.writeText).toHaveBeenCalledWith(''); diff --git a/src/wallet/components/WalletIslandQrReceive.tsx b/src/wallet/components/WalletIslandQrReceive.tsx index 861bba5de3..58991d2b5b 100644 --- a/src/wallet/components/WalletIslandQrReceive.tsx +++ b/src/wallet/components/WalletIslandQrReceive.tsx @@ -85,6 +85,7 @@ export function WalletIslandQrReceive() { type="button" onClick={() => handleCopyAddress('icon')} aria-label="Copy your address" + data-testid="ockWalletIslandQrReceive_CopyIcon" >
{copySvg}
@@ -102,6 +103,7 @@ export function WalletIslandQrReceive() { )} aria-live="polite" aria-label="Copy your address" + data-testid="ockWalletIslandQrReceive_CopyTooltip" > {copyText} @@ -113,6 +115,7 @@ export function WalletIslandQrReceive() { className={cn(border.radius, pressable.alternate, 'w-full p-3')} onClick={() => handleCopyAddress('button')} aria-label="Copy your address" + data-testid="ockWalletIslandQrReceive_CopyButton" > {copyButtonText} diff --git a/src/wallet/components/WalletIslandWalletActions.test.tsx b/src/wallet/components/WalletIslandWalletActions.test.tsx index f0c9bdb8d0..a751f3cd22 100644 --- a/src/wallet/components/WalletIslandWalletActions.test.tsx +++ b/src/wallet/components/WalletIslandWalletActions.test.tsx @@ -118,12 +118,11 @@ describe('WalletIslandWalletActions', () => { expect(setShowQrMock).toHaveBeenCalled(); }); - it('refreshes portfolio data when refresh button is clicked and data is not stale', () => { + it('refreshes portfolio data when refresh button is clicked', () => { const refetchPortfolioDataMock = vi.fn(); mockUseWalletIslandContext.mockReturnValue({ ...defaultMockUseWalletIslandContext, refetchPortfolioData: refetchPortfolioDataMock, - portfolioDataUpdatedAt: Date.now() - 1000 * 15 - 1, }); render(); @@ -133,20 +132,4 @@ describe('WalletIslandWalletActions', () => { expect(refetchPortfolioDataMock).toHaveBeenCalled(); }); - - it('does not refresh portfolio data when data is not stale', () => { - const refetchPortfolioDataMock = vi.fn(); - mockUseWalletIslandContext.mockReturnValue({ - ...defaultMockUseWalletIslandContext, - refetchPortfolioData: refetchPortfolioDataMock, - portfolioDataUpdatedAt: Date.now() - 1000 * 14, - }); - - render(); - - const refreshButton = screen.getByTestId('ockWalletIsland_RefreshButton'); - fireEvent.click(refreshButton); - - expect(refetchPortfolioDataMock).not.toHaveBeenCalled(); - }); }); From 6bb31e95669f520fbb8be589054ed9063760096b Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 11:55:09 -0800 Subject: [PATCH 112/150] fix: lint --- src/wallet/components/WalletIslandQrReceive.test.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/wallet/components/WalletIslandQrReceive.test.tsx b/src/wallet/components/WalletIslandQrReceive.test.tsx index 577b05525d..726170ca15 100644 --- a/src/wallet/components/WalletIslandQrReceive.test.tsx +++ b/src/wallet/components/WalletIslandQrReceive.test.tsx @@ -192,7 +192,9 @@ describe('WalletIslandQrReceive', () => { render(); - const copyTooltip = screen.getByTestId('ockWalletIslandQrReceive_CopyTooltip'); + const copyTooltip = screen.getByTestId( + 'ockWalletIslandQrReceive_CopyTooltip', + ); await act(async () => { fireEvent.click(copyTooltip); @@ -230,7 +232,9 @@ describe('WalletIslandQrReceive', () => { render(); - const copyButton = screen.getByTestId('ockWalletIslandQrReceive_CopyButton'); + const copyButton = screen.getByTestId( + 'ockWalletIslandQrReceive_CopyButton', + ); await act(async () => { fireEvent.click(copyButton); @@ -273,7 +277,9 @@ describe('WalletIslandQrReceive', () => { vi.advanceTimersByTime(2000); mockSetCopyButtonText.mockClear(); - const copyButton = screen.getByTestId('ockWalletIslandQrReceive_CopyButton'); + const copyButton = screen.getByTestId( + 'ockWalletIslandQrReceive_CopyButton', + ); await act(async () => { fireEvent.click(copyButton); await Promise.resolve(); From 32075e868561dd57eb466420adbe41f598762cee Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 12:17:04 -0800 Subject: [PATCH 113/150] update parameter for usePortfolioTokenBalances --- .../hooks/usePortfolioTokenBalances.test.tsx | 37 ++++++++++--------- .../wallet/hooks/usePortfolioTokenBalances.ts | 19 ++++++---- .../components/WalletIslandProvider.tsx | 2 +- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx index b3efd88791..b893915aeb 100644 --- a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx @@ -12,7 +12,7 @@ import { usePortfolioTokenBalances } from './usePortfolioTokenBalances'; vi.mock('@/core/api/getPortfolioTokenBalances'); -const mockAddresses: `0x${string}`[] = ['0x123']; +const mockAddress: `0x${string}` = '0x123'; const mockTokensAPIResponse: PortfolioTokenBalanceAPIResponse[] = [ { address: '0x123', @@ -27,7 +27,7 @@ const mockTokensAPIResponse: PortfolioTokenBalanceAPIResponse[] = [ ]; const mockPortfolioTokenBalancesAPIResponse: PortfolioAPIResponse[] = [ { - address: mockAddresses[0], + address: mockAddress, portfolio_balance_usd: 100, token_balances: mockTokensAPIResponse, }, @@ -45,7 +45,7 @@ const mockTokens: PortfolioTokenWithFiatValue[] = [ }, ]; const mockPortfolioTokenBalances: PortfolioTokenBalances = { - address: mockAddresses[0], + address: mockAddress, portfolioBalanceUsd: 100, tokenBalances: mockTokens, }; @@ -74,7 +74,7 @@ describe('usePortfolioTokenBalances', () => { }); const { result } = renderHook( - () => usePortfolioTokenBalances({ addresses: mockAddresses }), + () => usePortfolioTokenBalances({ address: mockAddress }), { wrapper: createWrapper() }, ); @@ -83,7 +83,7 @@ describe('usePortfolioTokenBalances', () => { await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(getPortfolioTokenBalances).toHaveBeenCalledWith({ - addresses: mockAddresses, + addresses: [mockAddress], }); expect(result.current.data).toEqual(mockPortfolioTokenBalances); @@ -105,7 +105,7 @@ describe('usePortfolioTokenBalances', () => { const mockPortfolioTokenBalancesAPIResponseWithEth: PortfolioAPIResponse[] = [ { - address: mockAddresses[0], + address: mockAddress, portfolio_balance_usd: 100, token_balances: mockTokensAPIResponseWithEth, }, @@ -116,7 +116,7 @@ describe('usePortfolioTokenBalances', () => { }); const { result } = renderHook( - () => usePortfolioTokenBalances({ addresses: mockAddresses }), + () => usePortfolioTokenBalances({ address: mockAddress }), { wrapper: createWrapper() }, ); @@ -125,7 +125,7 @@ describe('usePortfolioTokenBalances', () => { await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(getPortfolioTokenBalances).toHaveBeenCalledWith({ - addresses: mockAddresses, + addresses: [mockAddress], }); const mockTokensWithEth: PortfolioTokenWithFiatValue[] = [ @@ -141,7 +141,7 @@ describe('usePortfolioTokenBalances', () => { }, ]; const mockPortfolioTokenBalancesWithEth: PortfolioTokenBalances = { - address: mockAddresses[0], + address: mockAddress, portfolioBalanceUsd: 100, tokenBalances: mockTokensWithEth, }; @@ -156,7 +156,7 @@ describe('usePortfolioTokenBalances', () => { }); const { result } = renderHook( - () => usePortfolioTokenBalances({ addresses: mockAddresses }), + () => usePortfolioTokenBalances({ address: mockAddress }), { wrapper: createWrapper() }, ); @@ -166,16 +166,19 @@ describe('usePortfolioTokenBalances', () => { expect(result.current.error?.message).toBe('API Error'); }); - it('should not fetch when addresses is empty', () => { - renderHook(() => usePortfolioTokenBalances({ addresses: [] }), { - wrapper: createWrapper(), - }); + it('should not fetch when address is empty', () => { + renderHook( + () => usePortfolioTokenBalances({ address: '' as `0x${string}` }), + { + wrapper: createWrapper(), + }, + ); expect(getPortfolioTokenBalances).not.toHaveBeenCalled(); }); - it('should not fetch when addresses is undefined', () => { - renderHook(() => usePortfolioTokenBalances({ addresses: undefined }), { + it('should not fetch when address is undefined', () => { + renderHook(() => usePortfolioTokenBalances({ address: undefined }), { wrapper: createWrapper(), }); @@ -188,7 +191,7 @@ describe('usePortfolioTokenBalances', () => { }); const { result } = renderHook( - () => usePortfolioTokenBalances({ addresses: mockAddresses }), + () => usePortfolioTokenBalances({ address: mockAddress }), { wrapper: createWrapper() }, ); diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts index 717c7cf3bb..c74c09b278 100644 --- a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts @@ -1,22 +1,25 @@ import { getPortfolioTokenBalances } from '@/core/api/getPortfolioTokenBalances'; import type { - GetPortfolioTokenBalancesParams, PortfolioTokenBalanceAPIResponse, PortfolioTokenBalances, PortfolioTokenWithFiatValue, } from '@/core/api/types'; import { isApiError } from '@/core/utils/isApiResponseError'; import { type UseQueryResult, useQuery } from '@tanstack/react-query'; +import type { Address } from 'viem'; export function usePortfolioTokenBalances({ - addresses, -}: GetPortfolioTokenBalancesParams): UseQueryResult { - const actionKey = `usePortfolioTokenBalances-${addresses}`; + address, +}: { + address: Address | undefined | null; +}): UseQueryResult { + console.log({ address }); + const actionKey = `usePortfolioTokenBalances-${address}`; return useQuery({ queryKey: ['usePortfolioTokenBalances', actionKey], queryFn: async () => { const response = await getPortfolioTokenBalances({ - addresses, + addresses: [address ?? '0x000'], }); if (isApiError(response)) { @@ -25,7 +28,7 @@ export function usePortfolioTokenBalances({ if (response.portfolios.length === 0) { return { - address: addresses?.[0] ?? '', + address: address ?? '', portfolioBalanceUsd: 0, tokenBalances: [], }; @@ -48,7 +51,7 @@ export function usePortfolioTokenBalances({ symbol: tokenBalance.symbol, cryptoBalance: tokenBalance.crypto_balance, fiatBalance: tokenBalance.fiat_balance, - }) as PortfolioTokenWithFiatValue, + } as PortfolioTokenWithFiatValue), ), }; @@ -63,7 +66,7 @@ export function usePortfolioTokenBalances({ return filteredPortfolio; }, retry: false, - enabled: !!addresses && addresses.length > 0, + enabled: !!address, refetchOnWindowFocus: false, staleTime: 1000 * 60 * 5, // refresh on mount every 5 minutes refetchOnMount: true, diff --git a/src/wallet/components/WalletIslandProvider.tsx b/src/wallet/components/WalletIslandProvider.tsx index 5b847656a5..779228c9f5 100644 --- a/src/wallet/components/WalletIslandProvider.tsx +++ b/src/wallet/components/WalletIslandProvider.tsx @@ -34,7 +34,7 @@ export function WalletIslandProvider({ children }: WalletIslandProviderReact) { refetch: refetchPortfolioData, isFetching: isFetchingPortfolioData, dataUpdatedAt: portfolioDataUpdatedAt, - } = usePortfolioTokenBalances({ addresses: [address ?? '0x000'] }); + } = usePortfolioTokenBalances({ address: address ?? '0x000' }); const portfolioFiatValue = portfolioData?.portfolioBalanceUsd; const tokenBalances = portfolioData?.tokenBalances; From 6b5f91463e3e47f1de06aede5b8d7b01592d2fed Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 12:30:36 -0800 Subject: [PATCH 114/150] fix: tests --- src/core-react/wallet/hooks/usePortfolioTokenBalances.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts index c74c09b278..dac2245c56 100644 --- a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts @@ -13,13 +13,12 @@ export function usePortfolioTokenBalances({ }: { address: Address | undefined | null; }): UseQueryResult { - console.log({ address }); const actionKey = `usePortfolioTokenBalances-${address}`; return useQuery({ queryKey: ['usePortfolioTokenBalances', actionKey], queryFn: async () => { const response = await getPortfolioTokenBalances({ - addresses: [address ?? '0x000'], + addresses: [address as Address], // Safe to coerce to Address because useQuery's enabled flag will prevent the query from running if address is undefined }); if (isApiError(response)) { @@ -28,7 +27,7 @@ export function usePortfolioTokenBalances({ if (response.portfolios.length === 0) { return { - address: address ?? '', + address: address, portfolioBalanceUsd: 0, tokenBalances: [], }; @@ -51,7 +50,7 @@ export function usePortfolioTokenBalances({ symbol: tokenBalance.symbol, cryptoBalance: tokenBalance.crypto_balance, fiatBalance: tokenBalance.fiat_balance, - } as PortfolioTokenWithFiatValue), + }) as PortfolioTokenWithFiatValue, ), }; From c860663f0b98a485d9fcc9f325c621bf0eb50930 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 12:36:40 -0800 Subject: [PATCH 115/150] fix: test --- src/wallet/components/WalletIslandProvider.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wallet/components/WalletIslandProvider.test.tsx b/src/wallet/components/WalletIslandProvider.test.tsx index 4085313596..2d3a8ff866 100644 --- a/src/wallet/components/WalletIslandProvider.test.tsx +++ b/src/wallet/components/WalletIslandProvider.test.tsx @@ -102,7 +102,7 @@ describe('useWalletIslandContext', () => { }); expect(mockUsePortfolioTokenBalances).toHaveBeenCalledWith({ - addresses: ['0x000'], + address: '0x000', }); mockUseWalletContext.mockReturnValue({ @@ -113,7 +113,7 @@ describe('useWalletIslandContext', () => { rerender(); expect(mockUsePortfolioTokenBalances).toHaveBeenCalledWith({ - addresses: ['0x123'], + address: '0x123', }); }); }); From 0d3c3bc9bc23e563e6500f348e2c96d57975504c Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 13:05:49 -0800 Subject: [PATCH 116/150] update QrCodeSvg props and value handling --- src/internal/components/QrCode/QrCodeSvg.tsx | 4 ++-- src/internal/components/QrCode/useMatrix.ts | 6 ++++-- src/wallet/components/WalletIslandQrReceive.tsx | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/internal/components/QrCode/QrCodeSvg.tsx b/src/internal/components/QrCode/QrCodeSvg.tsx index 35f2025b32..df93b37037 100644 --- a/src/internal/components/QrCode/QrCodeSvg.tsx +++ b/src/internal/components/QrCode/QrCodeSvg.tsx @@ -22,7 +22,7 @@ function coordinateAsPercentage(coordinate: number) { } export type QRCodeSVGProps = { - value: string; + value?: string | null; size?: number; backgroundColor?: string; logo?: React.ReactNode; @@ -79,7 +79,7 @@ export function QrCodeSvg({ const presetGradientForColor = presetGradients[radialGradientColor as keyof typeof presetGradients]; - const matrix = useMatrix(value, ecl); + const matrix = useMatrix(ecl, value); const corners = useCorners(size, matrix.length, bgColor, fillColor, uid); const { x: x1, y: y1 } = GRADIENT_START_COORDINATES; const { x: x2, y: y2 } = GRADIENT_END_COORDINATES; diff --git a/src/internal/components/QrCode/useMatrix.ts b/src/internal/components/QrCode/useMatrix.ts index e816662f7c..f01b4941fa 100644 --- a/src/internal/components/QrCode/useMatrix.ts +++ b/src/internal/components/QrCode/useMatrix.ts @@ -2,16 +2,18 @@ import QRCode from 'qrcode'; import { useMemo } from 'react'; export function useMatrix( - value: string, errorCorrectionLevel: 'L' | 'M' | 'Q' | 'H', + value?: string | null, ) { const matrix = useMemo(() => { if (!value) { return []; } + const transformedValue = `ethereum:${value}`; + const arr = Array.from( - QRCode.create(value, { errorCorrectionLevel }).modules.data, + QRCode.create(transformedValue, { errorCorrectionLevel }).modules.data, ); const sqrt = Math.sqrt(arr.length); diff --git a/src/wallet/components/WalletIslandQrReceive.tsx b/src/wallet/components/WalletIslandQrReceive.tsx index 58991d2b5b..88f05d4c00 100644 --- a/src/wallet/components/WalletIslandQrReceive.tsx +++ b/src/wallet/components/WalletIslandQrReceive.tsx @@ -109,7 +109,7 @@ export function WalletIslandQrReceive() {
- +
); } diff --git a/src/wallet/components/WalletIslandQrReceive.tsx b/src/wallet/components/WalletIslandQrReceive.tsx index 88f05d4c00..4c7e2ee11e 100644 --- a/src/wallet/components/WalletIslandQrReceive.tsx +++ b/src/wallet/components/WalletIslandQrReceive.tsx @@ -73,22 +73,16 @@ export function WalletIslandQrReceive() { )} >
- - + +
{backArrowSvg}
Scan to receive
- - + handleCopyAddress('icon')} + > +
{copySvg}
+ +
{backArrowSvg}
); diff --git a/src/wallet/components/WalletIslandWalletActions.tsx b/src/wallet/components/WalletIslandWalletActions.tsx index ca16f2e53c..24265916ce 100644 --- a/src/wallet/components/WalletIslandWalletActions.tsx +++ b/src/wallet/components/WalletIslandWalletActions.tsx @@ -14,6 +14,10 @@ export function WalletIslandWalletActions() { const { setShowQr, refetchPortfolioData } = useWalletIslandContext(); const { disconnect, connectors } = useDisconnect(); + const handleTransactions = useCallback(() => { + window.open('https://wallet.coinbase.com/assets/transactions', '_blank'); + }, []); + const handleDisconnect = useCallback(() => { handleClose(); for (const connector of connectors) { @@ -37,44 +41,25 @@ export function WalletIslandWalletActions() { })} >
- - - {clockSvg} - + + {clockSvg} - - + + {qrIconSvg}
- - + +
{disconnectSvg}
- - + +
{refreshSvg}
From 9d1148352eb178e3bf88f58309e8be78ef4aef16 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 13:51:21 -0800 Subject: [PATCH 119/150] fix tests --- .../components/PressableIcon.test.tsx | 31 ++++++++++++++++++- src/internal/components/PressableIcon.tsx | 7 +++-- .../components/WalletIslandQrReceive.tsx | 7 ++++- .../components/WalletIslandWalletActions.tsx | 22 ++++++++++--- 4 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/internal/components/PressableIcon.test.tsx b/src/internal/components/PressableIcon.test.tsx index 162c32996d..692cd130c9 100644 --- a/src/internal/components/PressableIcon.test.tsx +++ b/src/internal/components/PressableIcon.test.tsx @@ -32,7 +32,7 @@ describe('PressableIcon', () => {
, ); - const container = screen.getByText('Icon').parentElement; + const container = screen.getByTestId('ockPressableIconButton').parentElement; expect(container).toHaveClass( 'flex', 'items-center', @@ -40,4 +40,33 @@ describe('PressableIcon', () => { customClass, ); }); + + it('merges custom button className with default classes', () => { + const customButtonClass = 'custom-class'; + render( + + Icon + , + ); + + const pressableIconButton = screen.getByText('Icon').parentElement; + expect(pressableIconButton).toHaveClass( + 'flex', + 'items-center', + 'justify-center', + customButtonClass, + ); + }); + + it('applies aria-label to button', () => { + const ariaLabel = 'test-aria-label'; + render( + + Icon + , + ); + + const button = screen.getByTestId('ockPressableIconButton'); + expect(button).toHaveAttribute('aria-label', ariaLabel); + }); }); diff --git a/src/internal/components/PressableIcon.tsx b/src/internal/components/PressableIcon.tsx index dacb8e77dc..63170e62c2 100644 --- a/src/internal/components/PressableIcon.tsx +++ b/src/internal/components/PressableIcon.tsx @@ -6,13 +6,15 @@ type PressableIconProps = { className?: string; onClick?: () => void; ariaLabel?: string; -} + buttonClassName?: string; +}; export function PressableIcon({ children, className, onClick, ariaLabel, + buttonClassName, }: PressableIconProps) { return (
{children} diff --git a/src/wallet/components/WalletIslandQrReceive.tsx b/src/wallet/components/WalletIslandQrReceive.tsx index 4c7e2ee11e..217ab9f628 100644 --- a/src/wallet/components/WalletIslandQrReceive.tsx +++ b/src/wallet/components/WalletIslandQrReceive.tsx @@ -82,7 +82,12 @@ export function WalletIslandQrReceive() { ariaLabel="Copy your address" onClick={() => handleCopyAddress('icon')} > -
{copySvg}
+
+ {copySvg} +
-
{disconnectSvg}
+
+ {disconnectSvg} +
-
{refreshSvg}
+
+ {refreshSvg} +
From 27899ae496d9257dfecc7f478582e185f2c29721 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 13:51:57 -0800 Subject: [PATCH 120/150] fix: lint --- src/internal/components/PressableIcon.test.tsx | 4 +++- src/wallet/components/WalletIslandWalletActions.tsx | 8 ++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/internal/components/PressableIcon.test.tsx b/src/internal/components/PressableIcon.test.tsx index 692cd130c9..c9ab944ca9 100644 --- a/src/internal/components/PressableIcon.test.tsx +++ b/src/internal/components/PressableIcon.test.tsx @@ -32,7 +32,9 @@ describe('PressableIcon', () => { , ); - const container = screen.getByTestId('ockPressableIconButton').parentElement; + const container = screen.getByTestId( + 'ockPressableIconButton', + ).parentElement; expect(container).toHaveClass( 'flex', 'items-center', diff --git a/src/wallet/components/WalletIslandWalletActions.tsx b/src/wallet/components/WalletIslandWalletActions.tsx index 39f07b4ff7..efa866a409 100644 --- a/src/wallet/components/WalletIslandWalletActions.tsx +++ b/src/wallet/components/WalletIslandWalletActions.tsx @@ -45,14 +45,10 @@ export function WalletIslandWalletActions() { ariaLabel="Open transaction history" onClick={handleTransactions} > -
- {clockSvg} -
+
{clockSvg}
-
- {qrIconSvg} -
+
{qrIconSvg}
From d45febc5845e3af9ea593248d3ac074e8b9b396a Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 13:59:59 -0800 Subject: [PATCH 121/150] add test --- .../WalletIslandWalletActions.test.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/wallet/components/WalletIslandWalletActions.test.tsx b/src/wallet/components/WalletIslandWalletActions.test.tsx index a751f3cd22..4557d65045 100644 --- a/src/wallet/components/WalletIslandWalletActions.test.tsx +++ b/src/wallet/components/WalletIslandWalletActions.test.tsx @@ -132,4 +132,24 @@ describe('WalletIslandWalletActions', () => { expect(refetchPortfolioDataMock).toHaveBeenCalled(); }); + + it('opens transaction history when transactions button is clicked', () => { + const windowOpenSpy = vi + .spyOn(window, 'open') + .mockImplementation(() => null); + + render(); + + const transactionsButton = screen.getByTestId( + 'ockWalletIsland_TransactionsButton', + ); + fireEvent.click(transactionsButton); + + expect(windowOpenSpy).toHaveBeenCalledWith( + 'https://wallet.coinbase.com/assets/transactions', + '_blank', + ); + + windowOpenSpy.mockRestore(); + }); }); From 13d0b98c25e600031a19a7b4d4863d5959556ebf Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 14:11:57 -0800 Subject: [PATCH 122/150] fix circular deps --- src/wallet/components/WalletIslandSwap.tsx | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/wallet/components/WalletIslandSwap.tsx b/src/wallet/components/WalletIslandSwap.tsx index 6206546dcd..cef9b4e516 100644 --- a/src/wallet/components/WalletIslandSwap.tsx +++ b/src/wallet/components/WalletIslandSwap.tsx @@ -1,18 +1,16 @@ import { PressableIcon } from '@/internal/components/PressableIcon'; import { backArrowSvg } from '@/internal/svg/backArrowSvg'; import { cn } from '@/styles/theme'; -import { - Swap, - SwapAmountInput, - SwapButton, - SwapMessage, - SwapSettings, - SwapSettingsSlippageDescription, - SwapSettingsSlippageInput, - SwapSettingsSlippageTitle, - SwapToast, - SwapToggleButton, -} from '@/swap'; +import { Swap } from '@/swap/components/Swap'; +import { SwapAmountInput } from '@/swap/components/SwapAmountInput'; +import { SwapButton } from '@/swap/components/SwapButton'; +import { SwapMessage } from '@/swap/components/SwapMessage'; +import { SwapSettings } from '@/swap/components/SwapSettings'; +import { SwapSettingsSlippageDescription } from '@/swap/components/SwapSettingsSlippageDescription'; +import { SwapSettingsSlippageInput } from '@/swap/components/SwapSettingsSlippageInput'; +import { SwapSettingsSlippageTitle } from '@/swap/components/SwapSettingsSlippageTitle'; +import { SwapToast } from '@/swap/components/SwapToast'; +import { SwapToggleButton } from '@/swap/components/SwapToggleButton'; import type { SwapDefaultReact } from '@/swap/types'; import { useCallback } from 'react'; import { useWalletIslandContext } from './WalletIslandProvider'; From 39943a5f501de5aeb4d30e6a09b825f061446818 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 14:20:10 -0800 Subject: [PATCH 123/150] fix circular dependencies --- src/swap/components/SwapButton.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/swap/components/SwapButton.tsx b/src/swap/components/SwapButton.tsx index 975c2eb95c..40c26ea28f 100644 --- a/src/swap/components/SwapButton.tsx +++ b/src/swap/components/SwapButton.tsx @@ -1,13 +1,6 @@ -import { Spinner } from '../../internal/components/Spinner'; -import { - background, - border, - cn, - color, - pressable, - text, -} from '../../styles/theme'; -import { ConnectWallet } from '../../wallet'; +import { Spinner } from '@/internal/components/Spinner'; +import { background, border, cn, color, pressable, text } from '@/styles/theme'; +import { ConnectWallet } from '@/wallet/components/ConnectWallet'; import type { SwapButtonReact } from '../types'; import { useSwapContext } from './SwapProvider'; From 38eb00c251a91b83e17d62bef5247435cd262fb9 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 14:38:57 -0800 Subject: [PATCH 124/150] min-height on token holdings --- src/wallet/components/WalletIslandTokenHoldings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wallet/components/WalletIslandTokenHoldings.tsx b/src/wallet/components/WalletIslandTokenHoldings.tsx index b50d64bf09..90f13295dc 100644 --- a/src/wallet/components/WalletIslandTokenHoldings.tsx +++ b/src/wallet/components/WalletIslandTokenHoldings.tsx @@ -26,7 +26,7 @@ export function WalletIslandTokenHoldings() {
Date: Wed, 8 Jan 2025 21:07:45 -0800 Subject: [PATCH 125/150] update types --- src/core/api/types.ts | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/src/core/api/types.ts b/src/core/api/types.ts index eec28d575d..efdbd593ad 100644 --- a/src/core/api/types.ts +++ b/src/core/api/types.ts @@ -276,28 +276,5 @@ export type PortfolioTokenWithFiatValue = Token & { * Note: exported as public Type */ export type GetPortfoliosAPIResponse = { - portfolios: PortfolioAPIResponse[]; -}; - -/** - * Note: exported as public Type - */ -export type PortfolioAPIResponse = { - address: Address; - portfolio_balance_usd: number; - token_balances: PortfolioTokenBalanceAPIResponse[]; -}; - -/** - * Note: exported as public Type - */ -export type PortfolioTokenBalanceAPIResponse = { - address: Address | 'native'; - chain_id: number; - decimals: number; - image: string; - name: string; - symbol: string; - crypto_balance: number; - fiat_balance: number; + portfolios: PortfolioTokenBalances[]; }; From 7eda23df440ee66feefe04bdfb6c46c0cd07254f Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 21:08:37 -0800 Subject: [PATCH 126/150] update tests for new types and filters --- .../hooks/usePortfolioTokenBalances.test.tsx | 82 +------------------ .../api/getPortfolioTokenBalances.test.ts | 40 +++++---- 2 files changed, 20 insertions(+), 102 deletions(-) diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx index b893915aeb..9bc6df3b10 100644 --- a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx @@ -1,7 +1,5 @@ import { getPortfolioTokenBalances } from '@/core/api/getPortfolioTokenBalances'; import type { - PortfolioAPIResponse, - PortfolioTokenBalanceAPIResponse, PortfolioTokenBalances, PortfolioTokenWithFiatValue, } from '@/core/api/types'; @@ -13,25 +11,6 @@ import { usePortfolioTokenBalances } from './usePortfolioTokenBalances'; vi.mock('@/core/api/getPortfolioTokenBalances'); const mockAddress: `0x${string}` = '0x123'; -const mockTokensAPIResponse: PortfolioTokenBalanceAPIResponse[] = [ - { - address: '0x123', - chain_id: 8453, - decimals: 6, - image: '', - name: 'Token', - symbol: 'TOKEN', - crypto_balance: 100, - fiat_balance: 100, - }, -]; -const mockPortfolioTokenBalancesAPIResponse: PortfolioAPIResponse[] = [ - { - address: mockAddress, - portfolio_balance_usd: 100, - token_balances: mockTokensAPIResponse, - }, -]; const mockTokens: PortfolioTokenWithFiatValue[] = [ { address: '0x123', @@ -70,7 +49,7 @@ describe('usePortfolioTokenBalances', () => { it('should fetch token balances successfully', async () => { vi.mocked(getPortfolioTokenBalances).mockResolvedValueOnce({ - portfolios: mockPortfolioTokenBalancesAPIResponse, + portfolios: [mockPortfolioTokenBalances], }); const { result } = renderHook( @@ -89,65 +68,6 @@ describe('usePortfolioTokenBalances', () => { expect(result.current.data).toEqual(mockPortfolioTokenBalances); }); - it('should transform the address for ETH to an empty string', async () => { - const mockTokensAPIResponseWithEth: PortfolioTokenBalanceAPIResponse[] = [ - { - address: 'native', - chain_id: 8453, - decimals: 6, - image: '', - name: 'Ethereum', - symbol: 'ETH', - crypto_balance: 100, - fiat_balance: 100, - }, - ]; - const mockPortfolioTokenBalancesAPIResponseWithEth: PortfolioAPIResponse[] = - [ - { - address: mockAddress, - portfolio_balance_usd: 100, - token_balances: mockTokensAPIResponseWithEth, - }, - ]; - - vi.mocked(getPortfolioTokenBalances).mockResolvedValueOnce({ - portfolios: mockPortfolioTokenBalancesAPIResponseWithEth, - }); - - const { result } = renderHook( - () => usePortfolioTokenBalances({ address: mockAddress }), - { wrapper: createWrapper() }, - ); - - expect(result.current.isLoading).toBe(true); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(getPortfolioTokenBalances).toHaveBeenCalledWith({ - addresses: [mockAddress], - }); - - const mockTokensWithEth: PortfolioTokenWithFiatValue[] = [ - { - address: '', - chainId: 8453, - decimals: 6, - image: '', - name: 'Ethereum', - symbol: 'ETH', - cryptoBalance: 100, - fiatBalance: 100, - }, - ]; - const mockPortfolioTokenBalancesWithEth: PortfolioTokenBalances = { - address: mockAddress, - portfolioBalanceUsd: 100, - tokenBalances: mockTokensWithEth, - }; - expect(result.current.data).toEqual(mockPortfolioTokenBalancesWithEth); - }); - it('should handle API errors', async () => { vi.mocked(getPortfolioTokenBalances).mockResolvedValueOnce({ code: 'API Error', diff --git a/src/core/api/getPortfolioTokenBalances.test.ts b/src/core/api/getPortfolioTokenBalances.test.ts index 4f46753d3b..d486fdacd9 100644 --- a/src/core/api/getPortfolioTokenBalances.test.ts +++ b/src/core/api/getPortfolioTokenBalances.test.ts @@ -1,7 +1,9 @@ import type { - PortfolioAPIResponse, - PortfolioTokenBalanceAPIResponse, + GetPortfolioTokenBalancesParams, + PortfolioTokenBalances, + PortfolioTokenWithFiatValue, } from '@/core/api/types'; +import type { Address } from 'viem'; import { type Mock, describe, expect, it, vi } from 'vitest'; import { CDP_GET_PORTFOLIO_TOKEN_BALANCES } from '../network/definitions/wallet'; import { sendRequest } from '../network/request'; @@ -11,24 +13,26 @@ vi.mock('../network/request', () => ({ sendRequest: vi.fn(), })); -const mockAddresses: `0x${string}`[] = ['0x123']; -const mockTokens: PortfolioTokenBalanceAPIResponse[] = [ +const mockApiParams: GetPortfolioTokenBalancesParams = { + addresses: ['0x0000000000000000000000000000000000000000'], +}; +const mockTokens: PortfolioTokenWithFiatValue[] = [ { address: '0x123', - chain_id: 8453, + chainId: 8453, decimals: 6, image: '', name: 'Token', symbol: 'TOKEN', - crypto_balance: 100, - fiat_balance: 100, + cryptoBalance: 100, + fiatBalance: 100, }, ]; -const mockPortfolioTokenBalances: PortfolioAPIResponse[] = [ +const mockPortfolioTokenBalances: PortfolioTokenBalances[] = [ { - address: mockAddresses[0], - portfolio_balance_usd: 100, - token_balances: mockTokens, + address: mockApiParams.addresses?.[0] as Address, + portfolioBalanceUsd: 100, + tokenBalances: mockTokens, }, ]; @@ -44,14 +48,12 @@ describe('getPortfolioTokenBalances', () => { result: mockSuccessResponse, }); - const result = await getPortfolioTokenBalances({ - addresses: mockAddresses as `0x${string}`[], - }); + const result = await getPortfolioTokenBalances(mockApiParams); expect(result).toEqual(mockSuccessResponse); expect(mockSendRequest).toHaveBeenCalledWith( CDP_GET_PORTFOLIO_TOKEN_BALANCES, - [{ addresses: mockAddresses }], + [{ addresses: mockApiParams }], ); }); @@ -66,9 +68,7 @@ describe('getPortfolioTokenBalances', () => { error: mockError, }); - const result = await getPortfolioTokenBalances({ - addresses: mockAddresses, - }); + const result = await getPortfolioTokenBalances(mockApiParams); expect(result).toEqual({ code: '500', @@ -81,9 +81,7 @@ describe('getPortfolioTokenBalances', () => { const errorMessage = 'Network Error'; mockSendRequest.mockRejectedValueOnce(new Error(errorMessage)); - const result = await getPortfolioTokenBalances({ - addresses: mockAddresses, - }); + const result = await getPortfolioTokenBalances(mockApiParams); expect(result).toEqual({ code: 'uncaught-portfolio', From a9793bd28bc0b33dace8bcc4be2b45f4481d9725 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 21:09:07 -0800 Subject: [PATCH 127/150] remove filters and transform, now performed on backend --- .../wallet/hooks/usePortfolioTokenBalances.ts | 37 +------------------ 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts index dac2245c56..9622afd325 100644 --- a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts @@ -1,9 +1,5 @@ import { getPortfolioTokenBalances } from '@/core/api/getPortfolioTokenBalances'; -import type { - PortfolioTokenBalanceAPIResponse, - PortfolioTokenBalances, - PortfolioTokenWithFiatValue, -} from '@/core/api/types'; +import type { PortfolioTokenBalances } from '@/core/api/types'; import { isApiError } from '@/core/utils/isApiResponseError'; import { type UseQueryResult, useQuery } from '@tanstack/react-query'; import type { Address } from 'viem'; @@ -33,36 +29,7 @@ export function usePortfolioTokenBalances({ }; } - const userPortfolio = response.portfolios[0]; - - const transformedPortfolio: PortfolioTokenBalances = { - address: userPortfolio.address, - portfolioBalanceUsd: userPortfolio.portfolio_balance_usd, - tokenBalances: userPortfolio.token_balances.map( - (tokenBalance: PortfolioTokenBalanceAPIResponse) => - ({ - address: - tokenBalance.symbol === 'ETH' ? '' : tokenBalance.address, - chainId: tokenBalance.chain_id, - decimals: tokenBalance.decimals, - image: tokenBalance.image, - name: tokenBalance.name, - symbol: tokenBalance.symbol, - cryptoBalance: tokenBalance.crypto_balance, - fiatBalance: tokenBalance.fiat_balance, - }) as PortfolioTokenWithFiatValue, - ), - }; - - const filteredPortfolio = { - ...transformedPortfolio, - tokenBalances: transformedPortfolio.tokenBalances.filter( - (tokenBalance: PortfolioTokenWithFiatValue) => - tokenBalance.cryptoBalance > 0, - ), - }; - - return filteredPortfolio; + return response.portfolios[0]; }, retry: false, enabled: !!address, From 22e1e1d1981dcd20b7ac82c85114f480f0bd8deb Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 21:14:24 -0800 Subject: [PATCH 128/150] fix tests --- .../api/getPortfolioTokenBalances.test.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/core/api/getPortfolioTokenBalances.test.ts b/src/core/api/getPortfolioTokenBalances.test.ts index d486fdacd9..ea11d7e468 100644 --- a/src/core/api/getPortfolioTokenBalances.test.ts +++ b/src/core/api/getPortfolioTokenBalances.test.ts @@ -13,9 +13,7 @@ vi.mock('../network/request', () => ({ sendRequest: vi.fn(), })); -const mockApiParams: GetPortfolioTokenBalancesParams = { - addresses: ['0x0000000000000000000000000000000000000000'], -}; +const mockAddresses: Address[] = ['0x0000000000000000000000000000000000000000']; const mockTokens: PortfolioTokenWithFiatValue[] = [ { address: '0x123', @@ -30,7 +28,7 @@ const mockTokens: PortfolioTokenWithFiatValue[] = [ ]; const mockPortfolioTokenBalances: PortfolioTokenBalances[] = [ { - address: mockApiParams.addresses?.[0] as Address, + address: mockAddresses[0], portfolioBalanceUsd: 100, tokenBalances: mockTokens, }, @@ -48,12 +46,14 @@ describe('getPortfolioTokenBalances', () => { result: mockSuccessResponse, }); - const result = await getPortfolioTokenBalances(mockApiParams); + const result = await getPortfolioTokenBalances({ + addresses: mockAddresses, + }); expect(result).toEqual(mockSuccessResponse); expect(mockSendRequest).toHaveBeenCalledWith( CDP_GET_PORTFOLIO_TOKEN_BALANCES, - [{ addresses: mockApiParams }], + [{ addresses: mockAddresses }], ); }); @@ -68,7 +68,9 @@ describe('getPortfolioTokenBalances', () => { error: mockError, }); - const result = await getPortfolioTokenBalances(mockApiParams); + const result = await getPortfolioTokenBalances({ + addresses: mockAddresses, + }); expect(result).toEqual({ code: '500', @@ -81,7 +83,9 @@ describe('getPortfolioTokenBalances', () => { const errorMessage = 'Network Error'; mockSendRequest.mockRejectedValueOnce(new Error(errorMessage)); - const result = await getPortfolioTokenBalances(mockApiParams); + const result = await getPortfolioTokenBalances({ + addresses: mockAddresses, + }); expect(result).toEqual({ code: 'uncaught-portfolio', From 337b5bfe094495e36e340e08a2d6e41c6b71f5c3 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 21:14:40 -0800 Subject: [PATCH 129/150] fix: lint --- src/core/api/getPortfolioTokenBalances.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/api/getPortfolioTokenBalances.test.ts b/src/core/api/getPortfolioTokenBalances.test.ts index ea11d7e468..8dd278a051 100644 --- a/src/core/api/getPortfolioTokenBalances.test.ts +++ b/src/core/api/getPortfolioTokenBalances.test.ts @@ -1,5 +1,4 @@ import type { - GetPortfolioTokenBalancesParams, PortfolioTokenBalances, PortfolioTokenWithFiatValue, } from '@/core/api/types'; From 2b62dd5f9b2d71230143eaea6cb4713ce92110b8 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 21:43:09 -0800 Subject: [PATCH 130/150] update type to match data structure --- src/core/api/types.ts | 2 +- src/wallet/components/WalletIslandProvider.test.tsx | 2 +- src/wallet/components/WalletIslandProvider.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/api/types.ts b/src/core/api/types.ts index efdbd593ad..4d3c019ede 100644 --- a/src/core/api/types.ts +++ b/src/core/api/types.ts @@ -260,7 +260,7 @@ export type GetPortfolioTokenBalancesParams = { */ export type PortfolioTokenBalances = { address: Address; - portfolioBalanceUsd: number; + portfolioBalanceInUsd: number; tokenBalances: PortfolioTokenWithFiatValue[]; }; diff --git a/src/wallet/components/WalletIslandProvider.test.tsx b/src/wallet/components/WalletIslandProvider.test.tsx index 2d3a8ff866..f52cfa2454 100644 --- a/src/wallet/components/WalletIslandProvider.test.tsx +++ b/src/wallet/components/WalletIslandProvider.test.tsx @@ -44,7 +44,7 @@ describe('useWalletIslandContext', () => { data: { address: '0x123', tokenBalances: [], - portfolioBalanceUsd: 0, + portfolioBalanceInUsd: 0, }, refetch: vi.fn(), isFetching: false, diff --git a/src/wallet/components/WalletIslandProvider.tsx b/src/wallet/components/WalletIslandProvider.tsx index 779228c9f5..d1b0fce511 100644 --- a/src/wallet/components/WalletIslandProvider.tsx +++ b/src/wallet/components/WalletIslandProvider.tsx @@ -36,7 +36,7 @@ export function WalletIslandProvider({ children }: WalletIslandProviderReact) { dataUpdatedAt: portfolioDataUpdatedAt, } = usePortfolioTokenBalances({ address: address ?? '0x000' }); - const portfolioFiatValue = portfolioData?.portfolioBalanceUsd; + const portfolioFiatValue = portfolioData?.portfolioBalanceInUsd; const tokenBalances = portfolioData?.tokenBalances; const value = useValue({ From 346aa33ce156e180abf60e0364fe2c624419342e Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 22:16:29 -0800 Subject: [PATCH 131/150] animations based on open position --- src/wallet/components/Wallet.tsx | 15 +++++++---- .../components/WalletIslandAddressDetails.tsx | 6 ++--- src/wallet/components/WalletIslandContent.tsx | 7 +++--- .../components/WalletIslandProvider.tsx | 25 ++++++++++++++++++- .../components/WalletIslandTokenHoldings.tsx | 10 +++----- .../WalletIslandTransactionActions.tsx | 12 ++++----- .../components/WalletIslandWalletActions.tsx | 13 +++++----- src/wallet/components/WalletProvider.tsx | 6 +++++ src/wallet/types.ts | 8 ++++++ 9 files changed, 68 insertions(+), 34 deletions(-) diff --git a/src/wallet/components/Wallet.tsx b/src/wallet/components/Wallet.tsx index 9683f5903c..d26f600646 100644 --- a/src/wallet/components/Wallet.tsx +++ b/src/wallet/components/Wallet.tsx @@ -4,7 +4,7 @@ import { findComponent } from '@/core-react/internal/utils/findComponent'; import { Draggable } from '@/internal/components/Draggable'; import { cn } from '@/styles/theme'; import { useOutsideClick } from '@/ui-react/internal/hooks/useOutsideClick'; -import { Children, useEffect, useMemo, useRef, useState } from 'react'; +import { Children, useEffect, useMemo, useRef } from 'react'; import { WALLET_ISLAND_MAX_HEIGHT, WALLET_ISLAND_MAX_WIDTH, @@ -51,9 +51,14 @@ function WalletContent({ draggable, startingPosition, }: WalletReact) { - const [showSubComponentAbove, setShowSubComponentAbove] = useState(false); - const [alignSubComponentRight, setAlignSubComponentRight] = useState(false); - const { isOpen, handleClose } = useWalletContext(); + const { + isOpen, + handleClose, + showSubComponentAbove, + setShowSubComponentAbove, + alignSubComponentRight, + setAlignSubComponentRight, + } = useWalletContext(); const walletContainerRef = useRef(null); const connectRef = useRef(null); @@ -86,7 +91,7 @@ function WalletContent({ setShowSubComponentAbove(spaceAvailableBelow < WALLET_ISLAND_MAX_HEIGHT); setAlignSubComponentRight(spaceAvailableRight < WALLET_ISLAND_MAX_WIDTH); } - }, [isOpen]); + }, [isOpen, setShowSubComponentAbove, setAlignSubComponentRight]); if (draggable) { return ( diff --git a/src/wallet/components/WalletIslandAddressDetails.tsx b/src/wallet/components/WalletIslandAddressDetails.tsx index 759ebb2ba9..3b2efd2528 100644 --- a/src/wallet/components/WalletIslandAddressDetails.tsx +++ b/src/wallet/components/WalletIslandAddressDetails.tsx @@ -7,6 +7,7 @@ import { useWalletContext } from './WalletProvider'; export function AddressDetails() { const { address, chain, isClosing } = useWalletContext(); + const { animations } = useWalletIslandContext(); const [copyText, setCopyText] = useState('Copy'); const handleCopyAddress = useCallback(async () => { @@ -31,10 +32,7 @@ export function AddressDetails() { 'mt-2 flex flex-col items-center justify-center', color.foreground, text.body, - { - 'fade-in slide-in-from-top-2.5 animate-in fill-mode-forwards duration-300 ease-out': - !isClosing, - }, + animations.content, )} >
diff --git a/src/wallet/components/WalletIslandContent.tsx b/src/wallet/components/WalletIslandContent.tsx index e965d17e03..744b07740f 100644 --- a/src/wallet/components/WalletIslandContent.tsx +++ b/src/wallet/components/WalletIslandContent.tsx @@ -11,7 +11,8 @@ export function WalletIslandContent({ swappableTokens, }: WalletIslandReact) { const { isClosing, setIsOpen, setIsClosing } = useWalletContext(); - const { showQr, showSwap, tokenBalances } = useWalletIslandContext(); + const { showQr, showSwap, tokenBalances, animations } = + useWalletIslandContext(); return (
{ if (isClosing) { diff --git a/src/wallet/components/WalletIslandProvider.tsx b/src/wallet/components/WalletIslandProvider.tsx index d1b0fce511..7d3c4e0e9b 100644 --- a/src/wallet/components/WalletIslandProvider.tsx +++ b/src/wallet/components/WalletIslandProvider.tsx @@ -24,7 +24,7 @@ export function useWalletIslandContext() { } export function WalletIslandProvider({ children }: WalletIslandProviderReact) { - const { address } = useWalletContext(); + const { address, isClosing, showSubComponentAbove } = useWalletContext(); const [showSwap, setShowSwap] = useState(false); const [isSwapClosing, setIsSwapClosing] = useState(false); const [showQr, setShowQr] = useState(false); @@ -39,6 +39,8 @@ export function WalletIslandProvider({ children }: WalletIslandProviderReact) { const portfolioFiatValue = portfolioData?.portfolioBalanceInUsd; const tokenBalances = portfolioData?.tokenBalances; + const animations = getAnimations(isClosing, showSubComponentAbove); + const value = useValue({ showSwap, setShowSwap, @@ -53,6 +55,7 @@ export function WalletIslandProvider({ children }: WalletIslandProviderReact) { isFetchingPortfolioData, portfolioDataUpdatedAt, refetchPortfolioData, + animations, }); return ( @@ -61,3 +64,23 @@ export function WalletIslandProvider({ children }: WalletIslandProviderReact) { ); } + +function getAnimations(isClosing: boolean, showSubComponentAbove: boolean) { + if (isClosing) { + return { + container: showSubComponentAbove + ? 'fade-out slide-out-to-bottom-1.5 animate-out fill-mode-forwards ease-in-out' + : 'fade-out slide-out-to-top-1.5 animate-out fill-mode-forwards ease-in-out', + content: '', + }; + } + + return { + container: showSubComponentAbove + ? 'fade-in slide-in-from-bottom-1.5 animate-in duration-300 ease-out' + : 'fade-in slide-in-from-top-1.5 animate-in duration-300 ease-out', + content: showSubComponentAbove + ? 'fade-in slide-in-from-bottom-2.5 animate-in fill-mode-forwards duration-300 ease-out' + : 'fade-in slide-in-from-top-2.5 animate-in fill-mode-forwards duration-300 ease-out', + }; +} diff --git a/src/wallet/components/WalletIslandTokenHoldings.tsx b/src/wallet/components/WalletIslandTokenHoldings.tsx index 90f13295dc..ee57de050c 100644 --- a/src/wallet/components/WalletIslandTokenHoldings.tsx +++ b/src/wallet/components/WalletIslandTokenHoldings.tsx @@ -2,7 +2,6 @@ import { Spinner } from '@/internal/components/Spinner'; import { cn, color, text } from '@/styles/theme'; import { type Token, TokenImage } from '@/token'; import { useWalletIslandContext } from './WalletIslandProvider'; -import { useWalletContext } from './WalletProvider'; type TokenDetailsProps = { token: Token; @@ -11,8 +10,8 @@ type TokenDetailsProps = { }; export function WalletIslandTokenHoldings() { - const { isClosing } = useWalletContext(); - const { tokenBalances, isFetchingPortfolioData } = useWalletIslandContext(); + const { tokenBalances, isFetchingPortfolioData, animations } = + useWalletIslandContext(); if (isFetchingPortfolioData) { return ; @@ -28,10 +27,7 @@ export function WalletIslandTokenHoldings() { 'max-h-44 overflow-y-auto', 'flex min-h-44 w-full flex-col items-center gap-4', 'mt-2 mb-2', - { - 'fade-in slide-in-from-top-2.5 animate-in fill-mode-forwards duration-300 ease-out': - !isClosing, - }, + animations.content, )} data-testid="ockWalletIsland_TokenHoldings" > diff --git a/src/wallet/components/WalletIslandTransactionActions.tsx b/src/wallet/components/WalletIslandTransactionActions.tsx index 98589df44e..b5a62ce6b3 100644 --- a/src/wallet/components/WalletIslandTransactionActions.tsx +++ b/src/wallet/components/WalletIslandTransactionActions.tsx @@ -4,7 +4,6 @@ import { toggleSvg } from '@/internal/svg/toggleSvg'; import { border, cn, color, pressable, text } from '@/styles/theme'; import { useCallback } from 'react'; import { useWalletIslandContext } from './WalletIslandProvider'; -import { useWalletContext } from './WalletProvider'; type TransactionActionProps = { icon: React.ReactNode; @@ -13,8 +12,7 @@ type TransactionActionProps = { }; export function WalletIslandTransactionActions() { - const { isClosing } = useWalletContext(); - const { setShowSwap } = useWalletIslandContext(); + const { setShowSwap, animations } = useWalletIslandContext(); const handleBuy = useCallback(() => { window.open('https://pay.coinbase.com', '_blank'); @@ -30,10 +28,10 @@ export function WalletIslandTransactionActions() { return (
{ @@ -35,10 +36,10 @@ export function WalletIslandWalletActions() { return (
{ @@ -34,6 +36,10 @@ export function WalletProvider({ children }: WalletProviderReact) { isClosing, setIsClosing, handleClose, + showSubComponentAbove, + setShowSubComponentAbove, + alignSubComponentRight, + setAlignSubComponentRight, }); return ( diff --git a/src/wallet/types.ts b/src/wallet/types.ts index 5e42d62499..0c33423f9e 100644 --- a/src/wallet/types.ts +++ b/src/wallet/types.ts @@ -83,6 +83,10 @@ export type WalletContextType = { isClosing: boolean; setIsClosing: Dispatch>; handleClose: () => void; + showSubComponentAbove: boolean; + setShowSubComponentAbove: Dispatch>; + alignSubComponentRight: boolean; + setAlignSubComponentRight: Dispatch>; }; /** @@ -195,4 +199,8 @@ export type WalletIslandContextType = { refetchPortfolioData: () => Promise< QueryObserverResult >; + animations: { + container: string; + content: string; + }; }; From 20938b251ae67dc8b1ff644f63eef1df7c763f7c Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 23:19:04 -0800 Subject: [PATCH 132/150] move positioning useEffect to provider --- src/wallet/components/Wallet.tsx | 24 ++---------------------- src/wallet/components/WalletProvider.tsx | 24 +++++++++++++++++++++--- src/wallet/types.ts | 3 +-- 3 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/wallet/components/Wallet.tsx b/src/wallet/components/Wallet.tsx index d26f600646..715daada02 100644 --- a/src/wallet/components/Wallet.tsx +++ b/src/wallet/components/Wallet.tsx @@ -4,11 +4,7 @@ import { findComponent } from '@/core-react/internal/utils/findComponent'; import { Draggable } from '@/internal/components/Draggable'; import { cn } from '@/styles/theme'; import { useOutsideClick } from '@/ui-react/internal/hooks/useOutsideClick'; -import { Children, useEffect, useMemo, useRef } from 'react'; -import { - WALLET_ISLAND_MAX_HEIGHT, - WALLET_ISLAND_MAX_WIDTH, -} from '../constants'; +import { Children, useMemo, useRef } from 'react'; import type { WalletReact } from '../types'; import { ConnectWallet } from './ConnectWallet'; import { WalletDropdown } from './WalletDropdown'; @@ -54,13 +50,11 @@ function WalletContent({ const { isOpen, handleClose, + connectRef, showSubComponentAbove, - setShowSubComponentAbove, alignSubComponentRight, - setAlignSubComponentRight, } = useWalletContext(); const walletContainerRef = useRef(null); - const connectRef = useRef(null); useOutsideClick(walletContainerRef, handleClose); @@ -79,20 +73,6 @@ function WalletContent({ ); } - useEffect(() => { - if (isOpen && connectRef?.current) { - const connectRect = connectRef.current.getBoundingClientRect(); - const viewportHeight = window.innerHeight; - const viewportWidth = window.innerWidth; - - const spaceAvailableBelow = viewportHeight - connectRect.bottom; - const spaceAvailableRight = viewportWidth - connectRect.left; - - setShowSubComponentAbove(spaceAvailableBelow < WALLET_ISLAND_MAX_HEIGHT); - setAlignSubComponentRight(spaceAvailableRight < WALLET_ISLAND_MAX_WIDTH); - } - }, [isOpen, setShowSubComponentAbove, setAlignSubComponentRight]); - if (draggable) { return (
(null); const handleClose = useCallback(() => { if (!isOpen) { @@ -28,6 +33,20 @@ export function WalletProvider({ children }: WalletProviderReact) { setIsClosing(true); }, [isOpen]); + useEffect(() => { + if (isOpen && connectRef?.current) { + const connectRect = connectRef.current.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + const viewportWidth = window.innerWidth; + + const spaceAvailableBelow = viewportHeight - connectRect.bottom; + const spaceAvailableRight = viewportWidth - connectRect.left; + + setShowSubComponentAbove(spaceAvailableBelow < WALLET_ISLAND_MAX_HEIGHT); + setAlignSubComponentRight(spaceAvailableRight < WALLET_ISLAND_MAX_WIDTH); + } + }, [isOpen]); + const value = useValue({ address, chain, @@ -36,10 +55,9 @@ export function WalletProvider({ children }: WalletProviderReact) { isClosing, setIsClosing, handleClose, + connectRef, showSubComponentAbove, - setShowSubComponentAbove, alignSubComponentRight, - setAlignSubComponentRight, }); return ( diff --git a/src/wallet/types.ts b/src/wallet/types.ts index 0c33423f9e..88540c1676 100644 --- a/src/wallet/types.ts +++ b/src/wallet/types.ts @@ -83,10 +83,9 @@ export type WalletContextType = { isClosing: boolean; setIsClosing: Dispatch>; handleClose: () => void; + connectRef: React.RefObject; showSubComponentAbove: boolean; - setShowSubComponentAbove: Dispatch>; alignSubComponentRight: boolean; - setAlignSubComponentRight: Dispatch>; }; /** From 0b13fbc83bf0645eb00f0a677cecf7495ea97274 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 23:19:11 -0800 Subject: [PATCH 133/150] fix tests --- src/wallet/components/Wallet.test.tsx | 75 ++++++++------------------- 1 file changed, 21 insertions(+), 54 deletions(-) diff --git a/src/wallet/components/Wallet.test.tsx b/src/wallet/components/Wallet.test.tsx index 452376a9d0..be67180cef 100644 --- a/src/wallet/components/Wallet.test.tsx +++ b/src/wallet/components/Wallet.test.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useOutsideClick } from '../../ui/react/internal/hooks/useOutsideClick'; import { ConnectWallet } from './ConnectWallet'; import { Wallet } from './Wallet'; @@ -34,7 +34,7 @@ vi.mock('./WalletProvider', () => ({ WalletProvider: ({ children }: WalletProviderReact) => <>{children}, })); -const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect; +// const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect; describe('Wallet Component', () => { let mockHandleClose: ReturnType; @@ -45,14 +45,15 @@ describe('Wallet Component', () => { isOpen: true, handleClose: mockHandleClose, containerRef: { current: document.createElement('div') }, + connectRef: { current: document.createElement('div') }, }); vi.clearAllMocks(); }); - afterEach(() => { - Element.prototype.getBoundingClientRect = originalGetBoundingClientRect; - }); + // afterEach(() => { + // Element.prototype.getBoundingClientRect = originalGetBoundingClientRect; + // }); it('should render the Wallet component with ConnectWallet', () => { (useWalletContext as ReturnType).mockReturnValue({ @@ -203,19 +204,20 @@ describe('Wallet Component', () => { (useWalletContext as ReturnType).mockReturnValue({ isOpen: true, + alignSubComponentRight: true, }); - const mockGetBoundingClientRect = vi.fn().mockReturnValue({ - left: 400, - right: 450, - bottom: 100, - top: 0, - width: 50, - height: 100, - }); + // const mockGetBoundingClientRect = vi.fn().mockReturnValue({ + // left: 400, + // right: 450, + // bottom: 100, + // top: 0, + // width: 50, + // height: 100, + // }); - // Mock Element.prototype.getBoundingClientRect called on the ConnectWallet ref - Element.prototype.getBoundingClientRect = mockGetBoundingClientRect; + // // Mock Element.prototype.getBoundingClientRect called on the ConnectWallet ref + // Element.prototype.getBoundingClientRect = mockGetBoundingClientRect; render( @@ -242,18 +244,6 @@ describe('Wallet Component', () => { isOpen: true, }); - const mockGetBoundingClientRect = vi.fn().mockReturnValue({ - left: 400, - right: 450, - bottom: 100, - top: 0, - width: 50, - height: 100, - }); - - // Mock Element.prototype.getBoundingClientRect called on the ConnectWallet ref - Element.prototype.getBoundingClientRect = mockGetBoundingClientRect; - render( @@ -277,20 +267,9 @@ describe('Wallet Component', () => { (useWalletContext as ReturnType).mockReturnValue({ isOpen: true, + showSubComponentAbove: true, }); - const mockGetBoundingClientRect = vi.fn().mockReturnValue({ - left: 400, - right: 450, - bottom: 100, - top: 0, - width: 50, - height: 100, - }); - - // Mock Element.prototype.getBoundingClientRect called on the ConnectWallet ref - Element.prototype.getBoundingClientRect = mockGetBoundingClientRect; - render( @@ -301,11 +280,11 @@ describe('Wallet Component', () => { ); expect(screen.getByTestId('ockWalletIslandContainer')).toHaveClass( - 'top-full', + 'bottom-full', ); }); - it('should render WalletIsland above ConnectWallet when there is not enough space on the bottom', () => { + it('should render WalletIsland below ConnectWallet when there is enough space on the bottom', () => { Object.defineProperty(window, 'innerHeight', { writable: true, configurable: true, @@ -316,18 +295,6 @@ describe('Wallet Component', () => { isOpen: true, }); - const mockGetBoundingClientRect = vi.fn().mockReturnValue({ - left: 400, - right: 450, - bottom: 800, - top: 0, - width: 50, - height: 100, - }); - - // Mock Element.prototype.getBoundingClientRect called on the ConnectWallet ref - Element.prototype.getBoundingClientRect = mockGetBoundingClientRect; - render( @@ -338,7 +305,7 @@ describe('Wallet Component', () => { ); expect(screen.getByTestId('ockWalletIslandContainer')).toHaveClass( - 'bottom-full', + 'top-full', ); }); }); From d514c29ec6c44ea3335d467f246e0353a96e950a Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 8 Jan 2025 23:39:28 -0800 Subject: [PATCH 134/150] fix tests --- .../WalletIslandAddressDetails.test.tsx | 13 ++- .../components/WalletIslandContent.test.tsx | 104 +++++++++++++++++- .../components/WalletIslandProvider.test.tsx | 4 + .../WalletIslandTokenHoldings.test.tsx | 3 + .../WalletIslandTransactionActions.test.tsx | 4 +- .../WalletIslandWalletActions.test.tsx | 4 +- 6 files changed, 122 insertions(+), 10 deletions(-) diff --git a/src/wallet/components/WalletIslandAddressDetails.test.tsx b/src/wallet/components/WalletIslandAddressDetails.test.tsx index 3cd8d03c1c..5f45ba94ab 100644 --- a/src/wallet/components/WalletIslandAddressDetails.test.tsx +++ b/src/wallet/components/WalletIslandAddressDetails.test.tsx @@ -56,8 +56,8 @@ describe('WalletIslandAddressDetails', () => { beforeEach(() => { vi.clearAllMocks(); mockUseWalletIslandContext.mockReturnValue({ - animationClasses: { - addressDetails: 'animate-walletIslandContainerItem2', + animations: { + content: '', }, }); }); @@ -156,6 +156,9 @@ describe('WalletIslandAddressDetails', () => { it('should show spinner when fetching portfolio data', () => { mockUseWalletIslandContext.mockReturnValue({ isFetchingPortfolioData: true, + animations: { + content: '', + }, }); render(); @@ -167,6 +170,9 @@ describe('WalletIslandAddressDetails', () => { mockUseWalletIslandContext.mockReturnValue({ isFetchingPortfolioData: false, portfolioFiatValue: null, + animations: { + content: '', + }, }); const { rerender } = render(); @@ -178,6 +184,9 @@ describe('WalletIslandAddressDetails', () => { mockUseWalletIslandContext.mockReturnValue({ isFetchingPortfolioData: false, portfolioFiatValue: '1234.567', + animations: { + content: '', + }, }); rerender(); diff --git a/src/wallet/components/WalletIslandContent.test.tsx b/src/wallet/components/WalletIslandContent.test.tsx index 1e96dcd604..024292de86 100644 --- a/src/wallet/components/WalletIslandContent.test.tsx +++ b/src/wallet/components/WalletIslandContent.test.tsx @@ -52,6 +52,10 @@ describe('WalletIslandContent', () => { showQr: false, isQrClosing: false, tokenHoldings: [], + animations: { + container: '', + content: '', + }, }; beforeEach(() => { @@ -61,8 +65,21 @@ describe('WalletIslandContent', () => { ); }); - it('renders WalletIslandContent when isClosing is false', () => { - mockUseWalletContext.mockReturnValue({ isClosing: false }); + it('renders WalletIslandContent with correct animations when isClosing is false and showSubComponentAbove is false', () => { + mockUseWalletContext.mockReturnValue({ + isClosing: false, + showSubComponentAbove: false, + }); + + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + animations: { + container: + 'fade-in slide-in-from-top-1.5 animate-in duration-300 ease-out', + content: + 'fade-in slide-in-from-top-2.5 animate-in fill-mode-forwards duration-300 ease-out', + }, + }); render( @@ -82,8 +99,54 @@ describe('WalletIslandContent', () => { ).toHaveClass('hidden'); }); - it('closes WalletIslandContent when isClosing is true', () => { - mockUseWalletContext.mockReturnValue({ isClosing: true }); + it('renders WalletIslandContent with correct animations when isClosing is false and showSubComponentAbove is true', () => { + mockUseWalletContext.mockReturnValue({ + isClosing: false, + showSubComponentAbove: true, + }); + + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + animations: { + container: + 'fade-in slide-in-from-bottom-1.5 animate-in duration-300 ease-out', + content: + 'fade-in slide-in-from-bottom-2.5 animate-in fill-mode-forwards duration-300 ease-out', + }, + }); + + render( + +
WalletIslandContent
+
, + ); + + expect(screen.getByTestId('ockWalletIslandContent')).toBeDefined(); + expect(screen.queryByTestId('ockWalletIslandContent')).toHaveClass( + 'fade-in slide-in-from-bottom-1.5 animate-in duration-300 ease-out', + ); + expect( + screen.queryByTestId('ockWalletIslandQrReceive')?.parentElement, + ).toHaveClass('hidden'); + expect( + screen.queryByTestId('ockWalletIslandSwap')?.parentElement, + ).toHaveClass('hidden'); + }); + + it('closes WalletIslandContent with correct animations when isClosing is true and showSubComponentAbove is false', () => { + mockUseWalletContext.mockReturnValue({ + isClosing: true, + showSubComponentAbove: false, + }); + + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + animations: { + container: + 'fade-out slide-out-to-top-1.5 animate-out fill-mode-forwards ease-in-out', + content: '', + }, + }); render( @@ -103,6 +166,39 @@ describe('WalletIslandContent', () => { ).toHaveClass('hidden'); }); + it('closes WalletIslandContent with correct animations when isClosing is true and showSubComponentAbove is true', () => { + mockUseWalletContext.mockReturnValue({ + isClosing: true, + showSubComponentAbove: true, + }); + + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + animations: { + container: + 'fade-out slide-out-to-bottom-1.5 animate-out fill-mode-forwards ease-in-out', + content: '', + }, + }); + + render( + +
WalletIslandContent
+
, + ); + + expect(screen.getByTestId('ockWalletIslandContent')).toBeDefined(); + expect(screen.queryByTestId('ockWalletIslandContent')).toHaveClass( + 'fade-out slide-out-to-bottom-1.5 animate-out fill-mode-forwards ease-in-out', + ); + expect( + screen.queryByTestId('ockWalletIslandQrReceive')?.parentElement, + ).toHaveClass('hidden'); + expect( + screen.queryByTestId('ockWalletIslandSwap')?.parentElement, + ).toHaveClass('hidden'); + }); + it('handles animation end when closing', () => { const setIsOpen = vi.fn(); const setIsClosing = vi.fn(); diff --git a/src/wallet/components/WalletIslandProvider.test.tsx b/src/wallet/components/WalletIslandProvider.test.tsx index f52cfa2454..58ea74f0fd 100644 --- a/src/wallet/components/WalletIslandProvider.test.tsx +++ b/src/wallet/components/WalletIslandProvider.test.tsx @@ -71,6 +71,10 @@ describe('useWalletIslandContext', () => { refetchPortfolioData: expect.any(Function), isFetchingPortfolioData: false, portfolioDataUpdatedAt: expect.any(Date), + animations: { + container: expect.any(String), + content: expect.any(String), + }, }); }); diff --git a/src/wallet/components/WalletIslandTokenHoldings.test.tsx b/src/wallet/components/WalletIslandTokenHoldings.test.tsx index ed3db8ad94..33707370a2 100644 --- a/src/wallet/components/WalletIslandTokenHoldings.test.tsx +++ b/src/wallet/components/WalletIslandTokenHoldings.test.tsx @@ -21,6 +21,9 @@ describe('WalletIslandTokenHoldings', () => { refetchPortfolioData: vi.fn(), isFetchingPortfolioData: false, portfolioDataUpdatedAt: new Date(), + animations: { + content: '', + }, }; beforeEach(() => { diff --git a/src/wallet/components/WalletIslandTransactionActions.test.tsx b/src/wallet/components/WalletIslandTransactionActions.test.tsx index 63b1ff5455..497e1fe284 100644 --- a/src/wallet/components/WalletIslandTransactionActions.test.tsx +++ b/src/wallet/components/WalletIslandTransactionActions.test.tsx @@ -17,8 +17,8 @@ describe('WalletIslandTransactionActons', () => { const defaultMockUseWalletIslandContext = { setShowSwap: vi.fn(), - animationClasses: { - transactionActions: 'animate-walletIslandContainerItem3', + animations: { + content: '', }, }; diff --git a/src/wallet/components/WalletIslandWalletActions.test.tsx b/src/wallet/components/WalletIslandWalletActions.test.tsx index 4557d65045..3db1364617 100644 --- a/src/wallet/components/WalletIslandWalletActions.test.tsx +++ b/src/wallet/components/WalletIslandWalletActions.test.tsx @@ -34,8 +34,8 @@ describe('WalletIslandWalletActions', () => { >; const defaultMockUseWalletIslandContext = { - animationClasses: { - walletActions: 'animate-walletIslandContainerItem1', + animations: { + content: '', }, }; From 2ce661eb5efa8ba6f0744d767818b1b030974b7f Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 9 Jan 2025 00:13:09 -0800 Subject: [PATCH 135/150] add tests --- src/wallet/components/Wallet.test.tsx | 18 -- .../components/WalletIslandProvider.test.tsx | 70 ++++++++ src/wallet/components/WalletProvider.test.tsx | 169 +++++++++++++++++- 3 files changed, 238 insertions(+), 19 deletions(-) diff --git a/src/wallet/components/Wallet.test.tsx b/src/wallet/components/Wallet.test.tsx index be67180cef..367eaa2afe 100644 --- a/src/wallet/components/Wallet.test.tsx +++ b/src/wallet/components/Wallet.test.tsx @@ -34,8 +34,6 @@ vi.mock('./WalletProvider', () => ({ WalletProvider: ({ children }: WalletProviderReact) => <>{children}, })); -// const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect; - describe('Wallet Component', () => { let mockHandleClose: ReturnType; @@ -51,10 +49,6 @@ describe('Wallet Component', () => { vi.clearAllMocks(); }); - // afterEach(() => { - // Element.prototype.getBoundingClientRect = originalGetBoundingClientRect; - // }); - it('should render the Wallet component with ConnectWallet', () => { (useWalletContext as ReturnType).mockReturnValue({ isOpen: false, @@ -207,18 +201,6 @@ describe('Wallet Component', () => { alignSubComponentRight: true, }); - // const mockGetBoundingClientRect = vi.fn().mockReturnValue({ - // left: 400, - // right: 450, - // bottom: 100, - // top: 0, - // width: 50, - // height: 100, - // }); - - // // Mock Element.prototype.getBoundingClientRect called on the ConnectWallet ref - // Element.prototype.getBoundingClientRect = mockGetBoundingClientRect; - render( diff --git a/src/wallet/components/WalletIslandProvider.test.tsx b/src/wallet/components/WalletIslandProvider.test.tsx index 58ea74f0fd..adc68fbf5a 100644 --- a/src/wallet/components/WalletIslandProvider.test.tsx +++ b/src/wallet/components/WalletIslandProvider.test.tsx @@ -120,4 +120,74 @@ describe('useWalletIslandContext', () => { address: '0x123', }); }); + + describe('getAnimations', () => { + it('should return closing animations with top slide when isClosing is true and showSubComponentAbove is false', () => { + mockUseWalletContext.mockReturnValue({ + address: '0x123', + isClosing: true, + showSubComponentAbove: false, + }); + + const { result } = renderHook(() => useWalletIslandContext(), { + wrapper: WalletIslandProvider, + }); + + expect(result.current.animations).toEqual({ + container: 'fade-out slide-out-to-top-1.5 animate-out fill-mode-forwards ease-in-out', + content: '', + }); + }); + + it('should return closing animations with bottom slide when isClosing is true and showSubComponentAbove is true', () => { + mockUseWalletContext.mockReturnValue({ + address: '0x123', + isClosing: true, + showSubComponentAbove: true, + }); + + const { result } = renderHook(() => useWalletIslandContext(), { + wrapper: WalletIslandProvider, + }); + + expect(result.current.animations).toEqual({ + container: 'fade-out slide-out-to-bottom-1.5 animate-out fill-mode-forwards ease-in-out', + content: '', + }); + }); + + it('should return opening animations with top slide when isClosing is false and showSubComponentAbove is false', () => { + mockUseWalletContext.mockReturnValue({ + address: '0x123', + isClosing: false, + showSubComponentAbove: false, + }); + + const { result } = renderHook(() => useWalletIslandContext(), { + wrapper: WalletIslandProvider, + }); + + expect(result.current.animations).toEqual({ + container: 'fade-in slide-in-from-top-1.5 animate-in duration-300 ease-out', + content: 'fade-in slide-in-from-top-2.5 animate-in fill-mode-forwards duration-300 ease-out', + }); + }); + + it('should return opening animations with bottom slide when isClosing is false and showSubComponentAbove is true', () => { + mockUseWalletContext.mockReturnValue({ + address: '0x123', + isClosing: false, + showSubComponentAbove: true, + }); + + const { result } = renderHook(() => useWalletIslandContext(), { + wrapper: WalletIslandProvider, + }); + + expect(result.current.animations).toEqual({ + container: 'fade-in slide-in-from-bottom-1.5 animate-in duration-300 ease-out', + content: 'fade-in slide-in-from-bottom-2.5 animate-in fill-mode-forwards duration-300 ease-out', + }); + }); + }); }); diff --git a/src/wallet/components/WalletProvider.test.tsx b/src/wallet/components/WalletProvider.test.tsx index f9e9861e04..787520b534 100644 --- a/src/wallet/components/WalletProvider.test.tsx +++ b/src/wallet/components/WalletProvider.test.tsx @@ -1,8 +1,9 @@ import '@testing-library/jest-dom'; import { act, render, renderHook } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { type Config, WagmiProvider } from 'wagmi'; import { WalletProvider, useWalletContext } from './WalletProvider'; +import { useEffect, useState } from 'react'; vi.mock('wagmi', () => ({ useAccount: vi.fn().mockReturnValue({ address: null }), @@ -11,11 +12,17 @@ vi.mock('wagmi', () => ({ ), })); +// const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect; + describe('useWalletContext', () => { beforeEach(() => { vi.clearAllMocks(); }); + // afterEach(() => { + // Element.prototype.getBoundingClientRect = originalGetBoundingClientRect; + // }); + it('should return default context', () => { render( @@ -83,4 +90,164 @@ describe('useWalletContext', () => { expect(result.current.isOpen).toBe(true); expect(result.current.isClosing).toBe(true); }); + + it('should keep alignSubComponentRight false when there is enough space on the right', () => { + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: 1000, + }); + + const mockRef = { + getBoundingClientRect: () => ({ + bottom: 100, + left: 100, + right: 200, + top: 0, + width: 100, + height: 100, + }), + }; + + const TestComponent = () => { + const { connectRef, setIsOpen } = useWalletContext(); + useEffect(() => { + // @ts-ignore - we know this is safe for testing + connectRef.current = mockRef; + setIsOpen(true); + }, [connectRef, setIsOpen]); + return null; + }; + + const { result } = renderHook(() => useWalletContext(), { + wrapper: ({ children }) => ( + + + {children} + + ), + }); + + expect(result.current.alignSubComponentRight).toBe(false); + }); + + it('should set alignSubComponentRight to true when there is not enough space on the right', () => { + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: 500, + }); + + const mockRef = { + getBoundingClientRect: () => ({ + bottom: 100, + left: 400, + right: 500, + top: 0, + width: 100, + height: 100, + }), + }; + + const TestComponent = () => { + const { connectRef, setIsOpen } = useWalletContext(); + useEffect(() => { + // @ts-ignore - we know this is safe for testing + connectRef.current = mockRef; + setIsOpen(true); + }, [connectRef, setIsOpen]); + return null; + }; + + const { result } = renderHook(() => useWalletContext(), { + wrapper: ({ children }) => ( + + + {children} + + ), + }); + + expect(result.current.alignSubComponentRight).toBe(true); + }); + + it('should keep showSubComponentAbove false when there is enough space below', () => { + Object.defineProperty(window, 'innerHeight', { + writable: true, + configurable: true, + value: 1000, + }); + + const mockRef = { + getBoundingClientRect: () => ({ + bottom: 100, + left: 100, + right: 200, + top: 0, + width: 100, + height: 100, + }), + }; + + const TestComponent = () => { + const { connectRef, setIsOpen } = useWalletContext(); + useEffect(() => { + // @ts-ignore - we know this is safe for testing + connectRef.current = mockRef; + setIsOpen(true); + }, [connectRef, setIsOpen]); + return null; + }; + + const { result } = renderHook(() => useWalletContext(), { + wrapper: ({ children }) => ( + + + {children} + + ), + }); + + expect(result.current.showSubComponentAbove).toBe(false); + }); + + it('should set showSubComponentAbove to true when there is not enough space below', () => { + Object.defineProperty(window, 'innerHeight', { + writable: true, + configurable: true, + value: 500, + }); + + const mockRef = { + getBoundingClientRect: () => ({ + bottom: 200, + left: 100, + right: 200, + top: 100, + width: 100, + height: 100, + }), + }; + + const TestComponent = () => { + const { connectRef, setIsOpen } = useWalletContext(); + useEffect(() => { + // @ts-ignore - we know this is safe for testing + connectRef.current = mockRef; + setIsOpen(true); + }, [connectRef, setIsOpen]); + return null; + }; + + const { result } = renderHook(() => useWalletContext(), { + wrapper: ({ children }) => ( + + + {children} + + ), + }); + + expect(result.current.showSubComponentAbove).toBe(true); + }); }); From b2b375969a728f775f18f4d4fac75d58d6a03085 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 9 Jan 2025 00:15:25 -0800 Subject: [PATCH 136/150] fix: lints --- .../components/WalletIslandProvider.test.tsx | 18 ++++++++++++------ src/wallet/components/WalletProvider.test.tsx | 10 ++-------- src/wallet/components/WalletProvider.tsx | 11 +++++++++-- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/wallet/components/WalletIslandProvider.test.tsx b/src/wallet/components/WalletIslandProvider.test.tsx index adc68fbf5a..a1403e10fd 100644 --- a/src/wallet/components/WalletIslandProvider.test.tsx +++ b/src/wallet/components/WalletIslandProvider.test.tsx @@ -134,7 +134,8 @@ describe('useWalletIslandContext', () => { }); expect(result.current.animations).toEqual({ - container: 'fade-out slide-out-to-top-1.5 animate-out fill-mode-forwards ease-in-out', + container: + 'fade-out slide-out-to-top-1.5 animate-out fill-mode-forwards ease-in-out', content: '', }); }); @@ -151,7 +152,8 @@ describe('useWalletIslandContext', () => { }); expect(result.current.animations).toEqual({ - container: 'fade-out slide-out-to-bottom-1.5 animate-out fill-mode-forwards ease-in-out', + container: + 'fade-out slide-out-to-bottom-1.5 animate-out fill-mode-forwards ease-in-out', content: '', }); }); @@ -168,8 +170,10 @@ describe('useWalletIslandContext', () => { }); expect(result.current.animations).toEqual({ - container: 'fade-in slide-in-from-top-1.5 animate-in duration-300 ease-out', - content: 'fade-in slide-in-from-top-2.5 animate-in fill-mode-forwards duration-300 ease-out', + container: + 'fade-in slide-in-from-top-1.5 animate-in duration-300 ease-out', + content: + 'fade-in slide-in-from-top-2.5 animate-in fill-mode-forwards duration-300 ease-out', }); }); @@ -185,8 +189,10 @@ describe('useWalletIslandContext', () => { }); expect(result.current.animations).toEqual({ - container: 'fade-in slide-in-from-bottom-1.5 animate-in duration-300 ease-out', - content: 'fade-in slide-in-from-bottom-2.5 animate-in fill-mode-forwards duration-300 ease-out', + container: + 'fade-in slide-in-from-bottom-1.5 animate-in duration-300 ease-out', + content: + 'fade-in slide-in-from-bottom-2.5 animate-in fill-mode-forwards duration-300 ease-out', }); }); }); diff --git a/src/wallet/components/WalletProvider.test.tsx b/src/wallet/components/WalletProvider.test.tsx index 787520b534..8b457cfeb0 100644 --- a/src/wallet/components/WalletProvider.test.tsx +++ b/src/wallet/components/WalletProvider.test.tsx @@ -1,9 +1,9 @@ import '@testing-library/jest-dom'; import { act, render, renderHook } from '@testing-library/react'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useEffect } from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { type Config, WagmiProvider } from 'wagmi'; import { WalletProvider, useWalletContext } from './WalletProvider'; -import { useEffect, useState } from 'react'; vi.mock('wagmi', () => ({ useAccount: vi.fn().mockReturnValue({ address: null }), @@ -12,17 +12,11 @@ vi.mock('wagmi', () => ({ ), })); -// const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect; - describe('useWalletContext', () => { beforeEach(() => { vi.clearAllMocks(); }); - // afterEach(() => { - // Element.prototype.getBoundingClientRect = originalGetBoundingClientRect; - // }); - it('should return default context', () => { render( diff --git a/src/wallet/components/WalletProvider.tsx b/src/wallet/components/WalletProvider.tsx index cc6249beed..08771741c3 100644 --- a/src/wallet/components/WalletProvider.tsx +++ b/src/wallet/components/WalletProvider.tsx @@ -1,13 +1,20 @@ -import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'; +import { + createContext, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react'; import type { ReactNode } from 'react'; import { useAccount } from 'wagmi'; import { useValue } from '../../core-react/internal/hooks/useValue'; import { useOnchainKit } from '../../core-react/useOnchainKit'; -import type { WalletContextType } from '../types'; import { WALLET_ISLAND_MAX_HEIGHT, WALLET_ISLAND_MAX_WIDTH, } from '../constants'; +import type { WalletContextType } from '../types'; const emptyContext = {} as WalletContextType; From 3b883739c7d5b9256304dc26d51eaa85b85ff049 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 9 Jan 2025 00:30:39 -0800 Subject: [PATCH 137/150] fix test --- src/core/api/getPortfolioTokenBalances.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/api/getPortfolioTokenBalances.test.ts b/src/core/api/getPortfolioTokenBalances.test.ts index 8dd278a051..17786e3f4c 100644 --- a/src/core/api/getPortfolioTokenBalances.test.ts +++ b/src/core/api/getPortfolioTokenBalances.test.ts @@ -28,7 +28,7 @@ const mockTokens: PortfolioTokenWithFiatValue[] = [ const mockPortfolioTokenBalances: PortfolioTokenBalances[] = [ { address: mockAddresses[0], - portfolioBalanceUsd: 100, + portfolioBalanceInUsd: 100, tokenBalances: mockTokens, }, ]; From 582e1a69f1cc4f449ca2a8d4de2c7eb17070c8f9 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 9 Jan 2025 00:32:50 -0800 Subject: [PATCH 138/150] fix test --- src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx index 9bc6df3b10..16977e1770 100644 --- a/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx @@ -25,7 +25,7 @@ const mockTokens: PortfolioTokenWithFiatValue[] = [ ]; const mockPortfolioTokenBalances: PortfolioTokenBalances = { address: mockAddress, - portfolioBalanceUsd: 100, + portfolioBalanceInUsd: 100, tokenBalances: mockTokens, }; From a50947e49bd98c0f39ff48060786818740690810 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 9 Jan 2025 10:29:02 -0800 Subject: [PATCH 139/150] reuse token constants --- src/wallet/constants.ts | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/src/wallet/constants.ts b/src/wallet/constants.ts index cba4bf1386..42669d34bf 100644 --- a/src/wallet/constants.ts +++ b/src/wallet/constants.ts @@ -1,5 +1,5 @@ import type { Token } from '@/token'; -import { base } from 'viem/chains'; +import { usdcToken, ethToken } from '@/token/constants'; // The bytecode for the Coinbase Smart Wallet proxy contract. export const CB_SW_PROXY_BYTECODE = @@ -16,22 +16,6 @@ export const CB_SW_FACTORY_ADDRESS = export const WALLET_ISLAND_MAX_HEIGHT = 400; export const WALLET_ISLAND_MAX_WIDTH = 352; export const WALLET_ISLAND_DEFAULT_SWAPPABLE_TOKENS: Token[] = [ - { - name: 'ETH', - address: '', - symbol: 'ETH', - decimals: 18, - image: - 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', - chainId: base.id, - }, - { - name: 'USDC', - address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', - symbol: 'USDC', - decimals: 6, - image: - 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', - chainId: base.id, - }, + ethToken, + usdcToken, ]; From fea30ed24844419d2c02418b8177c288aa16d4a7 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 9 Jan 2025 10:29:47 -0800 Subject: [PATCH 140/150] remove outer div --- src/internal/components/PressableIcon.tsx | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/internal/components/PressableIcon.tsx b/src/internal/components/PressableIcon.tsx index 63170e62c2..08103c5ec9 100644 --- a/src/internal/components/PressableIcon.tsx +++ b/src/internal/components/PressableIcon.tsx @@ -6,7 +6,6 @@ type PressableIconProps = { className?: string; onClick?: () => void; ariaLabel?: string; - buttonClassName?: string; }; export function PressableIcon({ @@ -14,10 +13,13 @@ export function PressableIcon({ className, onClick, ariaLabel, - buttonClassName, }: PressableIconProps) { return ( -
- -
+ {children} + ); } From ee348b26bcd7d31589ad810217ff7d912fdf9a92 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 9 Jan 2025 10:31:09 -0800 Subject: [PATCH 141/150] address QA comments --- src/core-react/wallet/hooks/usePortfolioTokenBalances.ts | 2 +- src/wallet/components/WalletIslandAddressDetails.tsx | 6 +++++- src/wallet/components/WalletIslandProvider.tsx | 2 +- src/wallet/components/WalletIslandTokenHoldings.tsx | 4 +--- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts index 9622afd325..d12c33bb00 100644 --- a/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts +++ b/src/core-react/wallet/hooks/usePortfolioTokenBalances.ts @@ -23,7 +23,7 @@ export function usePortfolioTokenBalances({ if (response.portfolios.length === 0) { return { - address: address, + address, portfolioBalanceUsd: 0, tokenBalances: [], }; diff --git a/src/wallet/components/WalletIslandAddressDetails.tsx b/src/wallet/components/WalletIslandAddressDetails.tsx index 3b2efd2528..85906ef5a5 100644 --- a/src/wallet/components/WalletIslandAddressDetails.tsx +++ b/src/wallet/components/WalletIslandAddressDetails.tsx @@ -84,9 +84,13 @@ function AddressBalanceInFiat() { return ; } + if (portfolioFiatValue === null) { + return null; + } + return ( - {portfolioFiatValue && `$${Number(portfolioFiatValue)?.toFixed(2)}`} + {`$${Number(portfolioFiatValue)?.toFixed(2)}`} ); } diff --git a/src/wallet/components/WalletIslandProvider.tsx b/src/wallet/components/WalletIslandProvider.tsx index 7d3c4e0e9b..36e62a5ca4 100644 --- a/src/wallet/components/WalletIslandProvider.tsx +++ b/src/wallet/components/WalletIslandProvider.tsx @@ -34,7 +34,7 @@ export function WalletIslandProvider({ children }: WalletIslandProviderReact) { refetch: refetchPortfolioData, isFetching: isFetchingPortfolioData, dataUpdatedAt: portfolioDataUpdatedAt, - } = usePortfolioTokenBalances({ address: address ?? '0x000' }); + } = usePortfolioTokenBalances({ address }); const portfolioFiatValue = portfolioData?.portfolioBalanceInUsd; const tokenBalances = portfolioData?.tokenBalances; diff --git a/src/wallet/components/WalletIslandTokenHoldings.tsx b/src/wallet/components/WalletIslandTokenHoldings.tsx index ee57de050c..117eec4639 100644 --- a/src/wallet/components/WalletIslandTokenHoldings.tsx +++ b/src/wallet/components/WalletIslandTokenHoldings.tsx @@ -54,8 +54,6 @@ export function WalletIslandTokenHoldings() { } function TokenDetails({ token, balance, valueInFiat }: TokenDetailsProps) { - const currencySymbol = '$'; // TODO: get from user settings - return (
@@ -70,7 +68,7 @@ function TokenDetails({ token, balance, valueInFiat }: TokenDetailsProps) {
- {`${currencySymbol}${valueInFiat.toFixed(2)}`} + {`$${valueInFiat.toFixed(2)}`}
); From d33598b96f018b6ebc17040bb1a7e217afbb5b66 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 9 Jan 2025 10:31:38 -0800 Subject: [PATCH 142/150] use onAnimationEnd instead of timeouts for close handling --- src/wallet/components/WalletIslandQrReceive.tsx | 12 ++++++------ src/wallet/components/WalletIslandSwap.tsx | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/wallet/components/WalletIslandQrReceive.tsx b/src/wallet/components/WalletIslandQrReceive.tsx index 217ab9f628..7e6892c98a 100644 --- a/src/wallet/components/WalletIslandQrReceive.tsx +++ b/src/wallet/components/WalletIslandQrReceive.tsx @@ -15,15 +15,14 @@ export function WalletIslandQrReceive() { const handleCloseQr = useCallback(() => { setIsQrClosing(true); + }, [setIsQrClosing]); - setTimeout(() => { + const handleAnimationEnd = useCallback(() => { + if (isQrClosing) { setShowQr(false); - }, 200); - - setTimeout(() => { setIsQrClosing(false); - }, 400); - }, [setShowQr, setIsQrClosing]); + } + }, [isQrClosing, setShowQr, setIsQrClosing]); const handleCopyAddress = useCallback( async (element: 'button' | 'icon') => { @@ -71,6 +70,7 @@ export function WalletIslandQrReceive() { ? 'fade-out slide-out-to-left-5 animate-out fill-mode-forwards ease-in-out' : 'fade-in slide-in-from-left-5 linear animate-in duration-150', )} + onAnimationEnd={handleAnimationEnd} >
diff --git a/src/wallet/components/WalletIslandSwap.tsx b/src/wallet/components/WalletIslandSwap.tsx index cef9b4e516..ef8877a2ef 100644 --- a/src/wallet/components/WalletIslandSwap.tsx +++ b/src/wallet/components/WalletIslandSwap.tsx @@ -33,15 +33,14 @@ export function WalletIslandSwap({ const handleCloseSwap = useCallback(() => { setIsSwapClosing(true); + }, [setIsSwapClosing]); - setTimeout(() => { + const handleAnimationEnd = useCallback(() => { + if (isSwapClosing) { setShowSwap(false); - }, 200); - - setTimeout(() => { setIsSwapClosing(false); - }, 400); - }, [setShowSwap, setIsSwapClosing]); + } + }, [isSwapClosing, setShowSwap, setIsSwapClosing]); const backButton = ( @@ -58,6 +57,7 @@ export function WalletIslandSwap({ : 'fade-in slide-in-from-right-5 linear animate-in duration-150', 'relative', )} + onAnimationEnd={handleAnimationEnd} data-testid="ockWalletIslandSwap" > Date: Thu, 9 Jan 2025 10:45:27 -0800 Subject: [PATCH 143/150] update title --- src/internal/svg/qrIconSvg.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internal/svg/qrIconSvg.tsx b/src/internal/svg/qrIconSvg.tsx index 0a969c8b1e..2e34d09e73 100644 --- a/src/internal/svg/qrIconSvg.tsx +++ b/src/internal/svg/qrIconSvg.tsx @@ -8,7 +8,7 @@ export const qrIconSvg = ( fill="none" xmlns="http://www.w3.org/2000/svg" > - QR Code Icon + QR Code Date: Thu, 9 Jan 2025 10:45:37 -0800 Subject: [PATCH 144/150] update default start position --- src/wallet/components/Wallet.tsx | 2 +- src/wallet/components/WalletIslandDraggable.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wallet/components/Wallet.tsx b/src/wallet/components/Wallet.tsx index 715daada02..a93c2ca726 100644 --- a/src/wallet/components/Wallet.tsx +++ b/src/wallet/components/Wallet.tsx @@ -16,7 +16,7 @@ export const Wallet = ({ className, draggable = false, startingPosition = { - x: window.innerWidth - 250, + x: window.innerWidth - 300, y: window.innerHeight - 100, }, }: WalletReact) => { diff --git a/src/wallet/components/WalletIslandDraggable.tsx b/src/wallet/components/WalletIslandDraggable.tsx index 47b9c17848..700c1fdf2a 100644 --- a/src/wallet/components/WalletIslandDraggable.tsx +++ b/src/wallet/components/WalletIslandDraggable.tsx @@ -10,7 +10,7 @@ import { WalletIslandWalletActions } from './WalletIslandWalletActions'; export function WalletIslandDraggable({ startingPosition = { - x: window.innerWidth - 250, + x: window.innerWidth - 300, y: window.innerHeight - 100, }, }: { From a99517fc3a37388a3cf69ca8c45926da75b8e1f0 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 9 Jan 2025 11:13:37 -0800 Subject: [PATCH 145/150] remove empty div when balance is null --- src/wallet/components/WalletIslandAddressDetails.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/wallet/components/WalletIslandAddressDetails.tsx b/src/wallet/components/WalletIslandAddressDetails.tsx index 85906ef5a5..12191e59a4 100644 --- a/src/wallet/components/WalletIslandAddressDetails.tsx +++ b/src/wallet/components/WalletIslandAddressDetails.tsx @@ -69,9 +69,7 @@ export function AddressDetails() { {copyText}
-
- -
+
); } @@ -89,8 +87,11 @@ function AddressBalanceInFiat() { } return ( - +
{`$${Number(portfolioFiatValue)?.toFixed(2)}`} - +
); } From f6d4793b4eb5e9f51b5b55bdc968de1829589876 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 9 Jan 2025 11:13:44 -0800 Subject: [PATCH 146/150] fix lints and tests --- .../components/PressableIcon.test.tsx | 23 +----------- .../WalletIslandAddressDetails.test.tsx | 4 +- .../components/WalletIslandProvider.test.tsx | 2 +- .../components/WalletIslandQrReceive.test.tsx | 37 +++++++++++-------- .../components/WalletIslandSwap.test.tsx | 33 ++++++++++++----- src/wallet/constants.ts | 2 +- 6 files changed, 50 insertions(+), 51 deletions(-) diff --git a/src/internal/components/PressableIcon.test.tsx b/src/internal/components/PressableIcon.test.tsx index c9ab944ca9..35e2b486cb 100644 --- a/src/internal/components/PressableIcon.test.tsx +++ b/src/internal/components/PressableIcon.test.tsx @@ -32,10 +32,8 @@ describe('PressableIcon', () => { , ); - const container = screen.getByTestId( - 'ockPressableIconButton', - ).parentElement; - expect(container).toHaveClass( + const button = screen.getByTestId('ockPressableIconButton'); + expect(button).toHaveClass( 'flex', 'items-center', 'justify-center', @@ -43,23 +41,6 @@ describe('PressableIcon', () => { ); }); - it('merges custom button className with default classes', () => { - const customButtonClass = 'custom-class'; - render( - - Icon - , - ); - - const pressableIconButton = screen.getByText('Icon').parentElement; - expect(pressableIconButton).toHaveClass( - 'flex', - 'items-center', - 'justify-center', - customButtonClass, - ); - }); - it('applies aria-label to button', () => { const ariaLabel = 'test-aria-label'; render( diff --git a/src/wallet/components/WalletIslandAddressDetails.test.tsx b/src/wallet/components/WalletIslandAddressDetails.test.tsx index 5f45ba94ab..cec5d37efa 100644 --- a/src/wallet/components/WalletIslandAddressDetails.test.tsx +++ b/src/wallet/components/WalletIslandAddressDetails.test.tsx @@ -177,9 +177,7 @@ describe('WalletIslandAddressDetails', () => { const { rerender } = render(); - expect( - screen.getByTestId('ockWalletIsland_AddressBalance'), - ).toHaveTextContent(''); + expect(screen.queryByTestId('ockWalletIsland_AddressBalance')).toBeNull(); mockUseWalletIslandContext.mockReturnValue({ isFetchingPortfolioData: false, diff --git a/src/wallet/components/WalletIslandProvider.test.tsx b/src/wallet/components/WalletIslandProvider.test.tsx index a1403e10fd..b11b9e8f5d 100644 --- a/src/wallet/components/WalletIslandProvider.test.tsx +++ b/src/wallet/components/WalletIslandProvider.test.tsx @@ -106,7 +106,7 @@ describe('useWalletIslandContext', () => { }); expect(mockUsePortfolioTokenBalances).toHaveBeenCalledWith({ - address: '0x000', + address: null, }); mockUseWalletContext.mockReturnValue({ diff --git a/src/wallet/components/WalletIslandQrReceive.test.tsx b/src/wallet/components/WalletIslandQrReceive.test.tsx index 726170ca15..4a93906fb4 100644 --- a/src/wallet/components/WalletIslandQrReceive.test.tsx +++ b/src/wallet/components/WalletIslandQrReceive.test.tsx @@ -112,30 +112,37 @@ describe('WalletIslandQrReceive', () => { ); }); - it('should close when the back button is clicked', () => { - vi.useFakeTimers(); - - const setShowQrMock = vi.fn(); - const setIsQrClosingMock = vi.fn(); + it('should close when back button is clicked', () => { + const mockSetShowQr = vi.fn(); + const mockSetIsQrClosing = vi.fn(); mockUseWalletIslandContext.mockReturnValue({ ...defaultMockUseWalletIslandContext, showQr: true, - setShowQr: setShowQrMock, - setIsQrClosing: setIsQrClosingMock, + setShowQr: mockSetShowQr, + setIsQrClosing: mockSetIsQrClosing, }); - render(); - const backButton = screen.getByRole('button', { name: /back/i }); + const { rerender } = render(); + + const backButton = screen.getByRole('button', { name: /back button/i }); fireEvent.click(backButton); - expect(setIsQrClosingMock).toHaveBeenCalledWith(true); + expect(mockSetIsQrClosing).toHaveBeenCalledWith(true); - vi.advanceTimersByTime(200); - expect(setShowQrMock).toHaveBeenCalledWith(false); + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + showQr: true, + setShowQr: mockSetShowQr, + setIsQrClosing: mockSetIsQrClosing, + isQrClosing: true, + }); - vi.advanceTimersByTime(200); - expect(setIsQrClosingMock).toHaveBeenCalledWith(false); + rerender(); - vi.useRealTimers(); + const qrContainer = screen.getByTestId('ockWalletIslandQrReceive'); + fireEvent.animationEnd(qrContainer); + + expect(mockSetShowQr).toHaveBeenCalledWith(false); + expect(mockSetIsQrClosing).toHaveBeenCalledWith(false); }); it('should copy address when the copy icon is clicked', async () => { diff --git a/src/wallet/components/WalletIslandSwap.test.tsx b/src/wallet/components/WalletIslandSwap.test.tsx index 09b90d54a7..5b87e65630 100644 --- a/src/wallet/components/WalletIslandSwap.test.tsx +++ b/src/wallet/components/WalletIslandSwap.test.tsx @@ -200,8 +200,6 @@ describe('WalletIslandSwap', () => { }); it('should close swap when back button is clicked', () => { - vi.useFakeTimers(); - const mockSetShowSwap = vi.fn(); const mockSetIsSwapClosing = vi.fn(); mockUseWalletIslandContext.mockReturnValue({ @@ -212,7 +210,7 @@ describe('WalletIslandSwap', () => { setIsSwapClosing: mockSetIsSwapClosing, }); - render( + const { rerender } = render( { />, ); - expect(screen.getByTestId('ockWalletIslandSwap')).toBeInTheDocument(); - const backButton = screen.getByRole('button', { name: /back button/i }); fireEvent.click(backButton); expect(mockSetIsSwapClosing).toHaveBeenCalledWith(true); - vi.advanceTimersByTime(200); - expect(mockSetShowSwap).toHaveBeenCalledWith(false); + mockUseWalletIslandContext.mockReturnValue({ + ...defaultMockUseWalletIslandContext, + tokenHoldings: [tokens], + setShowSwap: mockSetShowSwap, + setIsSwapClosing: mockSetIsSwapClosing, + isSwapClosing: true, + }); - vi.advanceTimersByTime(200); - expect(mockSetIsSwapClosing).toHaveBeenCalledWith(false); + rerender( + , + ); - vi.useRealTimers(); + const swapContainer = screen.getByTestId('ockWalletIslandSwap'); + fireEvent.animationEnd(swapContainer); + + expect(mockSetShowSwap).toHaveBeenCalledWith(false); + expect(mockSetIsSwapClosing).toHaveBeenCalledWith(false); }); }); diff --git a/src/wallet/constants.ts b/src/wallet/constants.ts index 42669d34bf..9d29267618 100644 --- a/src/wallet/constants.ts +++ b/src/wallet/constants.ts @@ -1,5 +1,5 @@ import type { Token } from '@/token'; -import { usdcToken, ethToken } from '@/token/constants'; +import { ethToken, usdcToken } from '@/token/constants'; // The bytecode for the Coinbase Smart Wallet proxy contract. export const CB_SW_PROXY_BYTECODE = From 68735f6ce35abf887e5c52e3fd350def45e16bdc Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 9 Jan 2025 12:34:47 -0800 Subject: [PATCH 147/150] fix tests --- .../components/WalletIslandAddressDetails.test.tsx | 9 ++++++++- src/wallet/components/WalletIslandAddressDetails.tsx | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/wallet/components/WalletIslandAddressDetails.test.tsx b/src/wallet/components/WalletIslandAddressDetails.test.tsx index cec5d37efa..b70f87f29b 100644 --- a/src/wallet/components/WalletIslandAddressDetails.test.tsx +++ b/src/wallet/components/WalletIslandAddressDetails.test.tsx @@ -81,6 +81,13 @@ describe('WalletIslandAddressDetails', () => { schemaId: '1', }); + mockUseWalletIslandContext.mockReturnValue({ + portfolioFiatValue: 1000, + animations: { + content: '', + }, + }); + render(); expect(screen.getByTestId('ockAvatar_ImageContainer')).toBeDefined(); @@ -181,7 +188,7 @@ describe('WalletIslandAddressDetails', () => { mockUseWalletIslandContext.mockReturnValue({ isFetchingPortfolioData: false, - portfolioFiatValue: '1234.567', + portfolioFiatValue: 1234.567, animations: { content: '', }, diff --git a/src/wallet/components/WalletIslandAddressDetails.tsx b/src/wallet/components/WalletIslandAddressDetails.tsx index 12191e59a4..f19923d59d 100644 --- a/src/wallet/components/WalletIslandAddressDetails.tsx +++ b/src/wallet/components/WalletIslandAddressDetails.tsx @@ -82,7 +82,7 @@ function AddressBalanceInFiat() { return ; } - if (portfolioFiatValue === null) { + if (portfolioFiatValue === null || portfolioFiatValue === undefined) { return null; } @@ -91,7 +91,7 @@ function AddressBalanceInFiat() { className={cn(text.title1, 'mt-1 font-normal')} data-testid="ockWalletIsland_AddressBalance" > - {`$${Number(portfolioFiatValue)?.toFixed(2)}`} + {`$${portfolioFiatValue.toFixed(2)}`}
); } From 839861b7f608295a49fa6e64989ad4f101ab04de Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 9 Jan 2025 12:56:25 -0800 Subject: [PATCH 148/150] fix default position for WalletIslandFixed demo --- playground/nextjs-app-router/components/Demo.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/playground/nextjs-app-router/components/Demo.tsx b/playground/nextjs-app-router/components/Demo.tsx index 6929c40421..977db599d4 100644 --- a/playground/nextjs-app-router/components/Demo.tsx +++ b/playground/nextjs-app-router/components/Demo.tsx @@ -146,7 +146,10 @@ export default function Demo() {
{ActiveComponent && } From 10e8c6954620fca7887a0a8a47424e506c981e59 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 9 Jan 2025 14:23:48 -0800 Subject: [PATCH 149/150] update name and exports --- .../WalletIslandAddressDetails.test.tsx | 18 +++++++++--------- .../components/WalletIslandAddressDetails.tsx | 2 +- .../components/WalletIslandDraggable.tsx | 4 ++-- src/wallet/components/WalletIslandFixed.tsx | 4 ++-- src/wallet/index.ts | 3 ++- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/wallet/components/WalletIslandAddressDetails.test.tsx b/src/wallet/components/WalletIslandAddressDetails.test.tsx index b70f87f29b..90af0c5b22 100644 --- a/src/wallet/components/WalletIslandAddressDetails.test.tsx +++ b/src/wallet/components/WalletIslandAddressDetails.test.tsx @@ -1,7 +1,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useIdentityContext } from '../../core-react/identity/providers/IdentityProvider'; -import { AddressDetails } from './WalletIslandAddressDetails'; +import { WalletIslandAddressDetails } from './WalletIslandAddressDetails'; import { useWalletIslandContext } from './WalletIslandProvider'; import { useWalletContext } from './WalletProvider'; @@ -65,7 +65,7 @@ describe('WalletIslandAddressDetails', () => { it('renders null when isClosing is true', () => { mockUseWalletContext.mockReturnValue({ isClosing: true }); - render(); + render(); expect(screen.queryByTestId('address-details')).toBeNull(); }); @@ -88,7 +88,7 @@ describe('WalletIslandAddressDetails', () => { }, }); - render(); + render(); expect(screen.getByTestId('ockAvatar_ImageContainer')).toBeDefined(); expect(screen.getByTestId('ockAvatar_BadgeContainer')).toBeDefined(); @@ -103,7 +103,7 @@ describe('WalletIslandAddressDetails', () => { chain: { id: 8453 }, }); - render(); + render(); const nameButton = screen.getByTestId('ockWalletIsland_NameButton'); const tooltip = screen.getByTestId('ockWalletIsland_NameTooltip'); @@ -132,7 +132,7 @@ describe('WalletIslandAddressDetails', () => { }; Object.assign(navigator, { clipboard: mockClipboard }); - render(); + render(); const nameButton = screen.getByTestId('ockWalletIsland_NameButton'); const tooltip = screen.getByTestId('ockWalletIsland_NameTooltip'); @@ -151,7 +151,7 @@ describe('WalletIslandAddressDetails', () => { chain: { id: 8453 }, }); - render(); + render(); const nameButton = screen.getByTestId('ockWalletIsland_NameButton'); @@ -168,7 +168,7 @@ describe('WalletIslandAddressDetails', () => { }, }); - render(); + render(); expect(screen.getByTestId('ockSpinner')).toBeInTheDocument(); }); @@ -182,7 +182,7 @@ describe('WalletIslandAddressDetails', () => { }, }); - const { rerender } = render(); + const { rerender } = render(); expect(screen.queryByTestId('ockWalletIsland_AddressBalance')).toBeNull(); @@ -194,7 +194,7 @@ describe('WalletIslandAddressDetails', () => { }, }); - rerender(); + rerender(); expect( screen.getByTestId('ockWalletIsland_AddressBalance'), diff --git a/src/wallet/components/WalletIslandAddressDetails.tsx b/src/wallet/components/WalletIslandAddressDetails.tsx index f19923d59d..dcc07fd280 100644 --- a/src/wallet/components/WalletIslandAddressDetails.tsx +++ b/src/wallet/components/WalletIslandAddressDetails.tsx @@ -5,7 +5,7 @@ import { useWalletIslandContext } from '@/wallet/components/WalletIslandProvider import { useCallback, useState } from 'react'; import { useWalletContext } from './WalletProvider'; -export function AddressDetails() { +export function WalletIslandAddressDetails() { const { address, chain, isClosing } = useWalletContext(); const { animations } = useWalletIslandContext(); const [copyText, setCopyText] = useState('Copy'); diff --git a/src/wallet/components/WalletIslandDraggable.tsx b/src/wallet/components/WalletIslandDraggable.tsx index 700c1fdf2a..6f27918e9a 100644 --- a/src/wallet/components/WalletIslandDraggable.tsx +++ b/src/wallet/components/WalletIslandDraggable.tsx @@ -3,7 +3,7 @@ import { ConnectWallet } from './ConnectWallet'; import { ConnectWalletText } from './ConnectWalletText'; import { Wallet } from './Wallet'; import { WalletIsland } from './WalletIsland'; -import { AddressDetails } from './WalletIslandAddressDetails'; +import { WalletIslandAddressDetails } from './WalletIslandAddressDetails'; import { WalletIslandTokenHoldings } from './WalletIslandTokenHoldings'; import { WalletIslandTransactionActions } from './WalletIslandTransactionActions'; import { WalletIslandWalletActions } from './WalletIslandWalletActions'; @@ -25,7 +25,7 @@ export function WalletIslandDraggable({ - + diff --git a/src/wallet/components/WalletIslandFixed.tsx b/src/wallet/components/WalletIslandFixed.tsx index b17c4b5023..b1e23a6417 100644 --- a/src/wallet/components/WalletIslandFixed.tsx +++ b/src/wallet/components/WalletIslandFixed.tsx @@ -3,7 +3,7 @@ import { ConnectWallet } from './ConnectWallet'; import { ConnectWalletText } from './ConnectWalletText'; import { Wallet } from './Wallet'; import { WalletIsland } from './WalletIsland'; -import { AddressDetails } from './WalletIslandAddressDetails'; +import { WalletIslandAddressDetails } from './WalletIslandAddressDetails'; import { WalletIslandTokenHoldings } from './WalletIslandTokenHoldings'; import { WalletIslandTransactionActions } from './WalletIslandTransactionActions'; import { WalletIslandWalletActions } from './WalletIslandWalletActions'; @@ -18,7 +18,7 @@ export function WalletIslandFixed() { - + diff --git a/src/wallet/index.ts b/src/wallet/index.ts index 1682ea2388..e54762c634 100644 --- a/src/wallet/index.ts +++ b/src/wallet/index.ts @@ -11,9 +11,10 @@ export { WalletDropdownLink } from './components/WalletDropdownLink'; export { WalletIsland } from './components/WalletIsland'; export { WalletIslandDraggable } from './components/WalletIslandDraggable'; export { WalletIslandFixed } from './components/WalletIslandFixed'; -export { WalletIslandQrReceive } from './components/WalletIslandQrReceive'; +export { WalletIslandAddressDetails } from './components/WalletIslandAddressDetails'; export { WalletIslandTransactionActions } from './components/WalletIslandTransactionActions'; export { WalletIslandTokenHoldings } from './components/WalletIslandTokenHoldings'; +export { WalletIslandQrReceive } from './components/WalletIslandQrReceive'; export { WalletIslandSwap } from './components/WalletIslandSwap'; export { WalletIslandWalletActions } from './components/WalletIslandWalletActions'; export { isValidAAEntrypoint } from './utils/isValidAAEntrypoint'; From 3d63de473545d068b5a557054ac1105c53860244 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 9 Jan 2025 14:27:29 -0800 Subject: [PATCH 150/150] wallet island docs --- .prettierrc | 3 +- site/docs/pages/wallet/types.mdx | 47 ++++ site/docs/pages/wallet/wallet-island.mdx | 278 +++++++++++++++++++++++ 3 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 site/docs/pages/wallet/wallet-island.mdx diff --git a/.prettierrc b/.prettierrc index 8d6ce6a7e4..261af18cc7 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,6 @@ { "tabWidth": 2, "singleQuote": true, - "trailingComma": "all" + "trailingComma": "all", + "embeddedLanguageFormatting": "off" } diff --git a/site/docs/pages/wallet/types.mdx b/site/docs/pages/wallet/types.mdx index 6251b18e74..b479c909ef 100644 --- a/site/docs/pages/wallet/types.mdx +++ b/site/docs/pages/wallet/types.mdx @@ -47,6 +47,15 @@ export type IsWalletACoinbaseSmartWalletResponse = ```ts type WalletContextType = { address?: Address | null; // The Ethereum address to fetch the avatar and name for. + chain?: Chain; // Optional chain for domain resolution + isOpen: boolean; + setIsOpen: Dispatch>; + isClosing: boolean; + setIsClosing: Dispatch>; + handleClose: () => void; + connectRef: React.RefObject; + showSubComponentAbove: boolean; + alignSubComponentRight: boolean; }; ``` @@ -55,6 +64,9 @@ type WalletContextType = { ```ts type WalletReact = { children: React.ReactNode; + className?: string; + draggable?: boolean; + startingPosition?: { x: number; y: number }; }; ``` @@ -110,3 +122,38 @@ export type WalletDropdownLinkReact = { target?: string; }; ``` + +## `WalletIslandReact` + +```ts +export type WalletIslandReact = { + children: React.ReactNode; + swappableTokens?: Token[]; // Optional array of tokens to specify which tokens the user can swap _to_ +}; +``` + +## `WalletIslandContextType` + +```ts +export type WalletIslandContextType = { + showSwap: boolean; + setShowSwap: Dispatch>; + isSwapClosing: boolean; + setIsSwapClosing: Dispatch>; + showQr: boolean; + setShowQr: Dispatch>; + isQrClosing: boolean; + setIsQrClosing: Dispatch>; + tokenBalances: PortfolioTokenWithFiatValue[] | undefined; + portfolioFiatValue: number | undefined; + isFetchingPortfolioData: boolean; + portfolioDataUpdatedAt: number | undefined; + refetchPortfolioData: () => Promise< + QueryObserverResult + >; + animations: { + container: string; + content: string; + }; +}; +``` diff --git a/site/docs/pages/wallet/wallet-island.mdx b/site/docs/pages/wallet/wallet-island.mdx new file mode 100644 index 0000000000..d03c28ad52 --- /dev/null +++ b/site/docs/pages/wallet/wallet-island.mdx @@ -0,0 +1,278 @@ +--- +title: ยท OnchainKit +description: The `` components provide an advanced Wallet interface for users. +--- + +import { + ConnectWallet, + Wallet, + WalletIsland, + WalletIslandDraggable, + WalletIslandFixed, +} from '@coinbase/onchainkit/wallet'; +import { + Address, + Avatar, + Name, + Identity, + EthBalance, +} from '@coinbase/onchainkit/identity'; +import { color } from '@coinbase/onchainkit/theme'; +import WalletComponents from '../../components/WalletComponents'; +import AppWithWalletModal from '../../components/AppWithWalletModal'; + +# `` + +The `` components provide an advanced Wallet interface for users, +including a QR code for receiving funds, a swap interface, and the user's token +portfolio. + +Designed primarily for desktop experiences, the `WalletIsland` component gives +users a seamless way to interact with their wallet and manage their assets. + +Before using them, ensure you've completed all [Getting Started steps](/getting-started). + +## Quick start + +`WalletIsland` has two default implementations: `WalletIslandDraggable` and +`WalletIslandFixed`. As suggested by their names, `WalletIslandDraggable` enables +the user to drag the component around the window, while `WalletIslandFixed` is +fixed to the anchor position. + +If you'd like more customization, follow the implementation guide for our `WalletIsland` component below. + +```tsx twoslash +import { WalletIslandDraggable } from '@coinbase/onchainkit/wallet'; +import { WalletIslandFixed } from '@coinbase/onchainkit/wallet'; + + + +``` + +You will see two Wallet buttons below. Right below us is `WalletIslandFixed`. This +version will stay anchored to its current position. + +`WalletIslandDraggable` is in the bottom-right corner of the window. Feel free to drag +it around. + + + + + + +Whether fixed or draggable, `WalletIsland` will expand from the ConnectWallet +component with the following logic: + +- If there is sufficient space to the right, it will be left-aligned. +- If there is not enough space to the right, it will be right-aligned. +- If there is enough space to the top, it will open above. +- If there is not enough space to the top, it will open below. + +## Walkthrough + +::::steps + +### Set up your wallet connections + +Kick off your wallet connection setup by configuring the `wagmi` provider. + +And make sure to update the `appName` as that will be displayed to +the user when they connect their wallet. + +```tsx twoslash +import { ReactNode } from 'react'; +import { WagmiProvider, createConfig, http } from 'wagmi'; +import { baseSepolia } from 'wagmi/chains'; +import { coinbaseWallet } from 'wagmi/connectors'; + +const wagmiConfig = createConfig({ + chains: [baseSepolia], + connectors: [ + coinbaseWallet({ + appName: 'onchainkit', + }), + ], + ssr: true, + transports: { + [baseSepolia.id]: http(), + }, +}); + +function App({ children }: { children: ReactNode }) { + return {children}; +} +``` + +### Drop in the `` components + +Experience the magic by simply dropping in the `` component +and watch it seamlessly come to life. + +
+As with [`WalletDefault`](/wallet/wallet) , `WalletIsland` leverages several +[``](/identity/identity) components like [``](/identity/avatar), + [``](/identity/name), and [`
`](/identity/address). + +And `WalletIsland` introduces several advanced wallet components, including +``, ``, and +``. + +```tsx twoslash +import { + ConnectWallet, // [!code focus] + Wallet, // [!code focus] + WalletIsland, // [!code focus] + WalletIslandAddressDetails, // [!code focus] + WalletIslandTokenHoldings, // [!code focus] + WalletIslandTransactionActions, // [!code focus] + WalletIslandWalletActions, // [!code focus] +} from '@coinbase/onchainkit/wallet'; // [!code focus] +import { Address, Avatar, Name, Identity } from '@coinbase/onchainkit/identity'; +import { color } from '@coinbase/onchainkit/theme'; + +export function YourFixedWalletIsland() { + return ( +
+ // [!code focus] + + + + + // [!code focus] + // [!code focus] + // [!code focus] + // [!code focus] + // [!code focus] + // [!code focus] + // [!code focus] +
+ ); +} +``` + + + + + Connect Wallet + + + + + + + + + + + + +When customizing your `WalletIsland` implementation, use the `draggable` prop on +`Wallet` to enable the draggable behavior. `draggable` defaults to `false`, but +when `draggable` is set to `true`, you can also set a `startingPosition` prop to +specify the initial position of your `WalletIsland`. + +```tsx twoslash +import { + ConnectWallet, // [!code focus] + Wallet, // [!code focus] + WalletIsland, // [!code focus] + WalletIslandAddressDetails, // [!code focus] + WalletIslandTokenHoldings, // [!code focus] + WalletIslandTransactionActions, // [!code focus] + WalletIslandWalletActions, // [!code focus] +} from '@coinbase/onchainkit/wallet'; // [!code focus] +import { Address, Avatar, Name, Identity } from '@coinbase/onchainkit/identity'; +import { color } from '@coinbase/onchainkit/theme'; + +export function YourDraggableWalletIsland() { + return ( +
+ // [!code focus] + + + + + + + + + + + +
+ ); +} +``` + +:::: + +## Customize Connect button text and style + +Each OnchainKit component offers the flexibility to customize `className` +and adjust the style of the React components it represents. + +Explore the options for customizing the Connect button text and style [here](/wallet/wallet#customize-connect-button-text-and-style). + +## Using Wallet Modal + +Wallet Modal + +Wallet modal offers users multiple wallet connection options. Explore these options +[here](/wallet/wallet#using-wallet-modal). + +## Example usage + +### Usage with Sign In With Ethereum (SIWE) + +To use [Sign In With Ethereum (SIWE)](https://docs.login.xyz/general-information/siwe-overview) with OnchainKit, +check out our [SIWE example](/wallet/wallet#usage-with-sign-in-with-ethereum-siwe). + +## Components + +The components are designed to work together hierarchically. For each component, ensure the following: + +- `` - Serves as the main container for all wallet-related components. +- `` - Contains additional wallet information and options. Place inside the `` component. +- `` - Provides wallet actions like View Transaction History, view QR Code, and Disconnect wallet. Place inside the `` component. +- `` - Displays user address, avatar, and portfolio balance in fiat. Place inside the `` component. +- `` - Buttons for buying crypto with fiat, transfering crypto, and swapping. Place inside the `` component. +- `` - Displays token balances and their value in fiat. Place inside the `` component. +- `` - Handles the wallet connection process. Place child components inside to customize the connect button appearance. +- `` - Enables a wallet aggregation experience. +- `` - Contains additional wallet information and options. Place inside the `` component. +- `` - Displays user identity information. Place inside `` for a complete profile view. +- `` - Displays the user's Basename within the dropdown. +- `` - Creates a custom link within the dropdown. Use the `icon` prop to add an icon, and `href` to specify the destination. +- `` - Provides a disconnect option within the dropdown. + +Additional components for customizing the wallet interface include: + +- `` - Displays the user's avatar image. +- `` - Shows the user's name or ENS. +- `` - Can be used to display additional user status or information. +- `
` - Shows the user's wallet address. +- `` - Displays the user's ETH balance. + +The Wallet component automatically handles the wallet connection state and updates the UI accordingly. +You need to wrap your application or relevant part of it with these components +to provide a complete wallet interaction experience. + +## Component types + +- [`WalletIslandReact`](/wallet/types#walletislandreact) +- [`WalletIslandContextType`](/wallet/types#walletislandcontexttype) +- [`WalletReact`](/wallet/types#walletreact) +- [`ConnectWalletReact`](/wallet/types#connectwalletreact) +- [`WalletDropdownReact`](/wallet/types#walletdropdownreact) +- [`WalletDropdownBasenameReact`](/wallet/types#walletdropdownbasenamereact) +- [`WalletDropdownDisconnectReact`](/wallet/types#walletdropdowndisconnectreact) +- [`WalletDropdownLinkReact`](/wallet/types#walletdropdownlinkreact)