From fec9f0d81571ff1ac4700ce3ebe0b7dbc70538b1 Mon Sep 17 00:00:00 2001 From: Alissa Crane Date: Mon, 10 Mar 2025 14:35:26 -0700 Subject: [PATCH 1/4] add event handlers to earn component --- .../components/demo/Earn.tsx | 30 +++++++++++++++++-- src/earn/components/Earn.tsx | 11 ++++++- src/earn/components/EarnProvider.tsx | 14 +++++++++ src/earn/types.ts | 9 +++++- 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/playground/nextjs-app-router/components/demo/Earn.tsx b/playground/nextjs-app-router/components/demo/Earn.tsx index d30287de4f..c11ec7f249 100644 --- a/playground/nextjs-app-router/components/demo/Earn.tsx +++ b/playground/nextjs-app-router/components/demo/Earn.tsx @@ -1,13 +1,37 @@ import { AppContext } from '@/components/AppProvider'; -import { Earn } from '@coinbase/onchainkit/earn'; -import { useContext } from 'react'; +import { Earn, LifecycleStatus } from '@coinbase/onchainkit/earn'; +import { TransactionError } from '@coinbase/onchainkit/transaction'; +import { useCallback, useContext } from 'react'; +import { TransactionReceipt } from 'viem'; export function EarnDemo() { const { vaultAddress } = useContext(AppContext); + const handleOnSuccess = useCallback( + (transactionReceipt?: TransactionReceipt) => { + console.log('Success: ', transactionReceipt); + }, + [], + ); + + const handleOnError = useCallback((earnError: TransactionError) => { + console.log('Error:', earnError); + }, []); + + const handleOnStatus = useCallback((lifecycleStatus: LifecycleStatus) => { + console.log('Status:', lifecycleStatus); + }, []); + if (!vaultAddress) { return
Please set a vault address
; } - return ; + return ( + + ); } diff --git a/src/earn/components/Earn.tsx b/src/earn/components/Earn.tsx index 34922bde40..81e2c2eb13 100644 --- a/src/earn/components/Earn.tsx +++ b/src/earn/components/Earn.tsx @@ -54,10 +54,19 @@ export function Earn({ className, vaultAddress, isSponsored, + onError, + onStatus, + onSuccess, }: EarnReact) { const componentTheme = useTheme(); return ( - +
{ + if (lifecycleStatus.statusName === 'error') { + onError?.(lifecycleStatus.statusData); + } + if (lifecycleStatus?.statusName === 'success') { + onSuccess?.(lifecycleStatus?.statusData?.transactionReceipts?.[0]); + } + onStatus?.(lifecycleStatus); + }, [lifecycleStatus]); + const { asset, balance: depositedBalance, diff --git a/src/earn/types.ts b/src/earn/types.ts index a7d224840d..a9b11b0847 100644 --- a/src/earn/types.ts +++ b/src/earn/types.ts @@ -1,10 +1,11 @@ +import type { TransactionError } from '@/api/types'; import type { UseMorphoVaultReturnType } from '@/earn/hooks/useMorphoVault'; import type { LifecycleStatusUpdate } from '@/internal/types'; import type { Token } from '@/token'; import type { Call } from '@/transaction/types'; import type { LifecycleStatus as TransactionLifecycleStatus } from '@/transaction/types'; import type React from 'react'; -import type { Address } from 'viem'; +import type { Address, TransactionReceipt } from 'viem'; /** * Note: exported as public Type @@ -14,6 +15,9 @@ export type EarnReact = { className?: string; vaultAddress: Address; isSponsored?: boolean; + onError?: (error: TransactionError) => void; // An optional callback function that handles errors within the provider. + onStatus?: (lifecycleStatus: LifecycleStatus) => void; // An optional callback function that exposes the component lifecycle state + onSuccess?: (transactionReceipt?: TransactionReceipt) => void; // An optional callback function that exposes the transaction receipt }; /** @@ -23,6 +27,9 @@ export type EarnProviderReact = { children: React.ReactNode; vaultAddress: Address; isSponsored?: boolean; + onError?: (error: TransactionError) => void; // An optional callback function that handles errors within the provider. + onStatus?: (lifecycleStatus: LifecycleStatus) => void; // An optional callback function that exposes the component lifecycle state + onSuccess?: (transactionReceipt?: TransactionReceipt) => void; // An optional callback function that exposes the transaction receipt }; /** From 5e79bb08115822fbca3367bc04b172f99ab5a7b2 Mon Sep 17 00:00:00 2001 From: Alissa Crane Date: Mon, 10 Mar 2025 15:20:15 -0700 Subject: [PATCH 2/4] add test coverage --- src/earn/components/EarnProvider.test.tsx | 95 ++++++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/src/earn/components/EarnProvider.test.tsx b/src/earn/components/EarnProvider.test.tsx index 39573b8402..1f035902a3 100644 --- a/src/earn/components/EarnProvider.test.tsx +++ b/src/earn/components/EarnProvider.test.tsx @@ -1,12 +1,13 @@ import { useMorphoVault } from '@/earn/hooks/useMorphoVault'; import { useGetTokenBalance } from '@/wallet/hooks/useGetTokenBalance'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { render, renderHook } from '@testing-library/react'; +import { fireEvent, render, renderHook, screen } from '@testing-library/react'; import { act } from 'react'; import { baseSepolia } from 'viem/chains'; import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; import { http, WagmiProvider, createConfig, mock, useAccount } from 'wagmi'; import { EarnProvider, useEarnContext } from './EarnProvider'; +import { TransactionReceipt } from 'viem'; const DUMMY_ADDRESS = '0x9E95f497a7663B70404496dB6481c890C4825fe1' as const; const queryClient = new QueryClient(); @@ -23,7 +24,49 @@ const mockConfig = createConfig({ }, }); -const wrapper = ({ children }: { children: React.ReactNode }) => ( +const TestComponent = () => { + const context = useEarnContext(); + const handleStatusError = async () => { + context.updateLifecycleStatus({ + statusName: 'error', + statusData: { + code: 'code', + error: 'error_long_messages', + message: 'error_long_messages', + }, + }); + }; + const handleStatusSuccess = async () => { + context.updateLifecycleStatus({ + statusName: 'success', + statusData: { + transactionReceipts: [ + { hash: '0x1235' } as unknown as TransactionReceipt, + ], + }, + }); + }; + + return ( +
+ + {context.lifecycleStatus.statusName} + + + +
+ ); +}; + +const wrapper = ({ + children, +}: { + children?: React.ReactNode; +}) => ( {children} @@ -58,6 +101,54 @@ describe('EarnProvider', () => { }); }); + it('should emit onError when setLifecycleStatus is called with error', async () => { + const onErrorMock = vi.fn(); + const onStatusMock = vi.fn(); + (useMorphoVault as Mock).mockReturnValue({ + asset: DUMMY_ADDRESS, + assetDecimals: 18, + assetSymbol: 'TEST', + balance: '100', + totalApy: '0.05', + }); + + render( + + + , + ); + + const button = screen.getByText('setLifecycleStatus.error'); + fireEvent.click(button); + expect(onErrorMock).toHaveBeenCalled(); + expect(onStatusMock).toHaveBeenCalled(); + }); + + it('should emit onSuccess when setLifecycleStatus is called with success', async () => { + const onSuccessMock = vi.fn(); + (useMorphoVault as Mock).mockReturnValue({ + asset: DUMMY_ADDRESS, + assetDecimals: 18, + assetSymbol: 'TEST', + balance: '100', + totalApy: '0.05', + }); + + render( + + + , + ); + + const button = screen.getByText('setLifecycleStatus.success'); + fireEvent.click(button); + expect(onSuccessMock).toHaveBeenCalledWith({ hash: '0x1235' }); + }); + it('throws an error when vaultAddress is not provided', () => { expect(() => renderHook(() => useEarnContext(), { From 410e535cee98ca3118279a9f116a91f3bab03b69 Mon Sep 17 00:00:00 2001 From: Alissa Crane Date: Mon, 10 Mar 2025 15:25:58 -0700 Subject: [PATCH 3/4] fix lint --- playground/nextjs-app-router/components/demo/Earn.tsx | 6 +++--- src/earn/components/EarnProvider.test.tsx | 2 +- src/earn/components/EarnProvider.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/playground/nextjs-app-router/components/demo/Earn.tsx b/playground/nextjs-app-router/components/demo/Earn.tsx index c11ec7f249..32f80f9120 100644 --- a/playground/nextjs-app-router/components/demo/Earn.tsx +++ b/playground/nextjs-app-router/components/demo/Earn.tsx @@ -1,8 +1,8 @@ import { AppContext } from '@/components/AppProvider'; -import { Earn, LifecycleStatus } from '@coinbase/onchainkit/earn'; -import { TransactionError } from '@coinbase/onchainkit/transaction'; +import { Earn, type LifecycleStatus } from '@coinbase/onchainkit/earn'; +import type { TransactionError } from '@coinbase/onchainkit/transaction'; import { useCallback, useContext } from 'react'; -import { TransactionReceipt } from 'viem'; +import type { TransactionReceipt } from 'viem'; export function EarnDemo() { const { vaultAddress } = useContext(AppContext); diff --git a/src/earn/components/EarnProvider.test.tsx b/src/earn/components/EarnProvider.test.tsx index 1f035902a3..8624b0098d 100644 --- a/src/earn/components/EarnProvider.test.tsx +++ b/src/earn/components/EarnProvider.test.tsx @@ -3,11 +3,11 @@ import { useGetTokenBalance } from '@/wallet/hooks/useGetTokenBalance'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { fireEvent, render, renderHook, screen } from '@testing-library/react'; import { act } from 'react'; +import type { TransactionReceipt } from 'viem'; import { baseSepolia } from 'viem/chains'; import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; import { http, WagmiProvider, createConfig, mock, useAccount } from 'wagmi'; import { EarnProvider, useEarnContext } from './EarnProvider'; -import { TransactionReceipt } from 'viem'; const DUMMY_ADDRESS = '0x9E95f497a7663B70404496dB6481c890C4825fe1' as const; const queryClient = new QueryClient(); diff --git a/src/earn/components/EarnProvider.tsx b/src/earn/components/EarnProvider.tsx index 11eb36c9e1..c144345450 100644 --- a/src/earn/components/EarnProvider.tsx +++ b/src/earn/components/EarnProvider.tsx @@ -55,7 +55,7 @@ export function EarnProvider({ onSuccess?.(lifecycleStatus?.statusData?.transactionReceipts?.[0]); } onStatus?.(lifecycleStatus); - }, [lifecycleStatus]); + }, [lifecycleStatus, onStatus, onError, onSuccess]); const { asset, From db5624a112d3502cc66d86c4461a9f578382154a Mon Sep 17 00:00:00 2001 From: Alissa Crane Date: Tue, 11 Mar 2025 10:22:24 -0700 Subject: [PATCH 4/4] update comments and add props to docs --- site/docs/pages/earn/types.mdx | 10 ++++++++++ src/earn/types.ts | 18 ++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/site/docs/pages/earn/types.mdx b/site/docs/pages/earn/types.mdx index 36d3ee274c..4d437c4d35 100644 --- a/site/docs/pages/earn/types.mdx +++ b/site/docs/pages/earn/types.mdx @@ -40,6 +40,11 @@ export type EarnReact = { className?: string; vaultAddress: Address; isSponsored?: boolean; + onError?: (error: TransactionError) => void; + /** An optional callback function that exposes the component lifecycle state */ + onStatus?: (lifecycleStatus: LifecycleStatus) => void; + /** An optional callback function that exposes the transaction receipt */ + onSuccess?: (transactionReceipt?: TransactionReceipt) => void; }; ``` @@ -50,6 +55,11 @@ export type EarnProviderReact = { children: React.ReactNode; vaultAddress: Address; isSponsored?: boolean; + onError?: (error: TransactionError) => void; + /** An optional callback function that exposes the component lifecycle state */ + onStatus?: (lifecycleStatus: LifecycleStatus) => void; + /** An optional callback function that exposes the transaction receipt */ + onSuccess?: (transactionReceipt?: TransactionReceipt) => void; }; ``` diff --git a/src/earn/types.ts b/src/earn/types.ts index a9b11b0847..02a4f37f84 100644 --- a/src/earn/types.ts +++ b/src/earn/types.ts @@ -15,9 +15,12 @@ export type EarnReact = { className?: string; vaultAddress: Address; isSponsored?: boolean; - onError?: (error: TransactionError) => void; // An optional callback function that handles errors within the provider. - onStatus?: (lifecycleStatus: LifecycleStatus) => void; // An optional callback function that exposes the component lifecycle state - onSuccess?: (transactionReceipt?: TransactionReceipt) => void; // An optional callback function that exposes the transaction receipt + /** An optional callback function that handles errors within the provider. */ + onError?: (error: TransactionError) => void; + /** An optional callback function that exposes the component lifecycle state */ + onStatus?: (lifecycleStatus: LifecycleStatus) => void; + /** An optional callback function that exposes the transaction receipt */ + onSuccess?: (transactionReceipt?: TransactionReceipt) => void; }; /** @@ -27,9 +30,12 @@ export type EarnProviderReact = { children: React.ReactNode; vaultAddress: Address; isSponsored?: boolean; - onError?: (error: TransactionError) => void; // An optional callback function that handles errors within the provider. - onStatus?: (lifecycleStatus: LifecycleStatus) => void; // An optional callback function that exposes the component lifecycle state - onSuccess?: (transactionReceipt?: TransactionReceipt) => void; // An optional callback function that exposes the transaction receipt + /** An optional callback function that handles errors within the provider. */ + onError?: (error: TransactionError) => void; + /** An optional callback function that exposes the component lifecycle state */ + onStatus?: (lifecycleStatus: LifecycleStatus) => void; + /** An optional callback function that exposes the transaction receipt */ + onSuccess?: (transactionReceipt?: TransactionReceipt) => void; }; /**