Skip to content

Commit

Permalink
feat: wallet island (#1793)
Browse files Browse the repository at this point in the history
  • Loading branch information
brendan-defi authored Jan 14, 2025
1 parent 6680c4c commit bb80419
Show file tree
Hide file tree
Showing 85 changed files with 7,641 additions and 5,021 deletions.
Binary file modified playground/nextjs-app-router/bun.lockb
Binary file not shown.
17 changes: 13 additions & 4 deletions playground/nextjs-app-router/components/Demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import SwapDefaultDemo from './demo/SwapDefault';
import TransactionDemo from './demo/Transaction';
import TransactionDefaultDemo from './demo/TransactionDefault';
import WalletDemo from './demo/Wallet';
import WalletAdvancedDefaultDemo from './demo/WalletAdvancedDefault';
import WalletDefaultDemo from './demo/WalletDefault';
import WalletIslandDemo from './demo/WalletIsland';

const activeComponentMapping: Record<OnchainKitComponent, React.FC> = {
[OnchainKitComponent.Buy]: BuyDemo,
Expand All @@ -31,6 +33,8 @@ const activeComponentMapping: Record<OnchainKitComponent, React.FC> = {
[OnchainKitComponent.SwapDefault]: SwapDefaultDemo,
[OnchainKitComponent.Wallet]: WalletDemo,
[OnchainKitComponent.WalletDefault]: WalletDefaultDemo,
[OnchainKitComponent.WalletIsland]: WalletIslandDemo,
[OnchainKitComponent.WalletAdvancedDefault]: WalletAdvancedDefaultDemo,
[OnchainKitComponent.TransactionDefault]: TransactionDefaultDemo,
[OnchainKitComponent.NFTMintCard]: NFTMintCardDemo,
[OnchainKitComponent.NFTCard]: NFTCardDemo,
Expand All @@ -39,7 +43,7 @@ const activeComponentMapping: Record<OnchainKitComponent, React.FC> = {
[OnchainKitComponent.IdentityCard]: IdentityCardDemo,
};

function Demo() {
export default function Demo() {
const { activeComponent } = useContext(AppContext);
const [isDarkMode, setIsDarkMode] = useState(true);
const [sideBarVisible, setSideBarVisible] = useState(true);
Expand Down Expand Up @@ -140,12 +144,17 @@ function Demo() {
</div>
</div>
<div className="linear-gradient(to_bottom,#f0f0f0_1px,transparent_1px)] flex flex-1 flex-col bg-[linear-gradient(to_right,#f0f0f0_1px,transparent_1px), bg-[size:6rem_4rem]">
<div className="flex h-full w-full flex-col items-center justify-center">
<div
className={cn(
'flex h-full w-full flex-col items-center',
activeComponent === OnchainKitComponent.WalletAdvancedDefault
? 'mt-12 justify-start'
: 'justify-center',
)}
>
{ActiveComponent && <ActiveComponent />}
</div>
</div>
</>
);
}

export default Demo;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { WalletAdvancedDefault } from '@coinbase/onchainkit/wallet';

export default function WalletAdvancedDefaultDemo() {
return (
<div className="mx-auto flex justify-end">
<WalletAdvancedDefault />
</div>
);
}
5 changes: 5 additions & 0 deletions playground/nextjs-app-router/components/demo/WalletIsland.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { WalletIsland } from '@coinbase/onchainkit/wallet';

export default function WalletIslandDemo() {
return <WalletIsland />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ export function ActiveComponent() {
<SelectItem value={OnchainKitComponent.WalletDefault}>
WalletDefault
</SelectItem>
<SelectItem value={OnchainKitComponent.WalletAdvancedDefault}>
WalletAdvancedDefault
</SelectItem>
<SelectItem value={OnchainKitComponent.WalletIsland}>
WalletIsland
</SelectItem>
<SelectItem value={OnchainKitComponent.NFTCard}>NFT Card</SelectItem>
<SelectItem value={OnchainKitComponent.NFTCardDefault}>
NFT Card Default
Expand Down
24 changes: 16 additions & 8 deletions playground/nextjs-app-router/onchainkit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coinbase/onchainkit",
"version": "0.36.0",
"version": "0.36.4",
"type": "module",
"repository": "https://github.com/coinbase/onchainkit.git",
"license": "MIT",
Expand Down Expand Up @@ -30,15 +30,16 @@
"watch:tailwind": "tailwind -i ./src/styles/index.css -o ./src/tailwind.css --watch"
},
"peerDependencies": {
"@types/react": "^18",
"react": "^18",
"react-dom": "^18"
"@types/react": "^18 || ^19",
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
},
"dependencies": {
"@tanstack/react-query": "^5",
"clsx": "^2.1.1",
"graphql": "^14 || ^15 || ^16",
"graphql-request": "^6.1.0",
"qrcode": "^1.5.4",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"viem": "^2.21.33",
Expand All @@ -59,6 +60,7 @@
"@storybook/test-runner": "^0.19.1",
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/react": "^14.2.0",
"@types/qrcode": "^1",
"@types/react": "^18",
"@types/react-dom": "^18",
"@vitest/coverage-v8": "^2.0.5",
Expand Down Expand Up @@ -119,6 +121,12 @@
"import": "./esm/core/api/index.js",
"default": "./esm/core/api/index.js"
},
"./buy": {
"types": "./esm/buy/index.d.ts",
"module": "./esm/buy/index.js",
"import": "./esm/buy/index.js",
"default": "./esm/buy/index.js"
},
"./checkout": {
"types": "./esm/checkout/index.d.ts",
"module": "./esm/checkout/index.js",
Expand All @@ -138,10 +146,10 @@
"default": "./esm/fund/index.js"
},
"./identity": {
"types": "./esm/identity/index.d.ts",
"module": "./esm/identity/index.js",
"import": "./esm/identity/index.js",
"default": "./esm/identity/index.js"
"types": "./esm/ui/react/identity/index.d.ts",
"module": "./esm/ui/react/identity/index.js",
"import": "./esm/ui/react/identity/index.js",
"default": "./esm/ui/react/identity/index.js"
},
"./nft": {
"types": "./esm/ui/react/nft/index.d.ts",
Expand Down
2 changes: 2 additions & 0 deletions playground/nextjs-app-router/types/onchainkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export enum OnchainKitComponent {
TransactionDefault = 'transaction-default',
Wallet = 'wallet',
WalletDefault = 'wallet-default',
WalletIsland = 'wallet-island',
WalletAdvancedDefault = 'wallet-advanced-default',
NFTCard = 'nft-card',
NFTCardDefault = 'nft-card-default',
NFTMintCard = 'nft-mint-card',
Expand Down
126 changes: 126 additions & 0 deletions src/core-react/wallet/hooks/usePortfolioTokenBalances.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { getPortfolioTokenBalances } from '@/core/api/getPortfolioTokenBalances';
import type {
PortfolioTokenBalances,
PortfolioTokenWithFiatValue,
} 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 mockAddress: `0x${string}` = '0x123';
const mockTokens: PortfolioTokenWithFiatValue[] = [
{
address: '0x123',
chainId: 8453,
decimals: 6,
image: '',
name: 'Token',
symbol: 'TOKEN',
cryptoBalance: 100,
fiatBalance: 100,
},
];
const mockPortfolioTokenBalances: PortfolioTokenBalances = {
address: mockAddress,
portfolioBalanceInUsd: 100,
tokenBalances: mockTokens,
};

const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};

describe('usePortfolioTokenBalances', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('should fetch token balances successfully', async () => {
vi.mocked(getPortfolioTokenBalances).mockResolvedValueOnce({
portfolios: [mockPortfolioTokenBalances],
});

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],
});

expect(result.current.data).toEqual(mockPortfolioTokenBalances);
});

it('should handle API errors', async () => {
vi.mocked(getPortfolioTokenBalances).mockResolvedValueOnce({
code: 'API Error',
error: 'API Error',
message: 'API Error',
});

const { result } = renderHook(
() => usePortfolioTokenBalances({ address: mockAddress }),
{ 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 address is empty', () => {
renderHook(
() => usePortfolioTokenBalances({ address: '' as `0x${string}` }),
{
wrapper: createWrapper(),
},
);

expect(getPortfolioTokenBalances).not.toHaveBeenCalled();
});

it('should not fetch when address is undefined', () => {
renderHook(() => usePortfolioTokenBalances({ address: undefined }), {
wrapper: createWrapper(),
});

expect(getPortfolioTokenBalances).not.toHaveBeenCalled();
});

it('should return empty data when portfolios is empty', async () => {
vi.mocked(getPortfolioTokenBalances).mockResolvedValueOnce({
portfolios: [],
});

const { result } = renderHook(
() => usePortfolioTokenBalances({ address: mockAddress }),
{ wrapper: createWrapper() },
);

await waitFor(() => expect(result.current.isSuccess).toBe(true));

expect(result.current.data).toEqual({
address: '0x123',
portfolioBalanceUsd: 0,
tokenBalances: [],
});
});
});
41 changes: 41 additions & 0 deletions src/core-react/wallet/hooks/usePortfolioTokenBalances.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { getPortfolioTokenBalances } from '@/core/api/getPortfolioTokenBalances';
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';

export function usePortfolioTokenBalances({
address,
}: {
address: Address | undefined | null;
}): UseQueryResult<PortfolioTokenBalances> {
return useQuery({
queryKey: ['usePortfolioTokenBalances', address],
queryFn: async () => {
const response = await getPortfolioTokenBalances({
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)) {
throw new Error(response.message);
}

if (response.portfolios.length === 0) {
return {
address,
portfolioBalanceUsd: 0,
tokenBalances: [],
};
}

return response.portfolios[0];
},
retry: false,
enabled: !!address,
refetchOnWindowFocus: true, // refresh on window focus
staleTime: 1000 * 60 * 5, // refresh on mount every 5 minutes
refetchOnMount: true,
refetchInterval: 1000 * 60 * 15, // refresh in background every 15 minutes
refetchIntervalInBackground: true,
});
}
Loading

0 comments on commit bb80419

Please sign in to comment.