Skip to content

Commit

Permalink
feat: onStatus (#1034)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zizzamia authored Aug 14, 2024
1 parent 7d03bf3 commit 932d5b2
Show file tree
Hide file tree
Showing 15 changed files with 202 additions and 108 deletions.
5 changes: 2 additions & 3 deletions .changeset/many-ravens-cry.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@
"@coinbase/onchainkit": patch
---

-**chore**: Add testing to Transaction Toast. By @cpcramer #1023
-**chore**: Add Connect Wallet tests and refactor to use Vitest. By @cpcramer #1036
-**chore**: Increase Wallet dropdown png size to 18x18. By @cpcramer #1041
- **feat**: introduced `onStatus` listener, to help expose the internal `Transaction`'s component lifecycle. By @zizzamia #1034
- **chore**: increased `Wallet` dropdown png size to 18x18. By @cpcramer #1041
37 changes: 31 additions & 6 deletions src/transaction/components/Transaction.test.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { Transaction } from './Transaction';
import { TransactionProvider } from './TransactionProvider';

vi.mock('./TransactionProvider', () => ({
TransactionProvider: ({ children }: { children: React.ReactNode }) => (
TransactionProvider: vi.fn(({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
)),
}));

describe('Transaction', () => {
it('renders the children inside the TransactionProvider', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('should renders the children inside the TransactionProvider', () => {
render(
<Transaction
address="0x123"
Expand All @@ -23,11 +28,10 @@ describe('Transaction', () => {
<div>Test Child</div>
</Transaction>,
);

expect(screen.getByText('Test Child')).toBeInTheDocument();
});

it('applies the correct className', () => {
it('should applies the correct className', () => {
render(
<Transaction
address="0x123"
Expand All @@ -41,9 +45,30 @@ describe('Transaction', () => {
<div>Test Child</div>
</Transaction>,
);

// Checking if the div has the correct classes applied
const container = screen.getByText('Test Child').parentElement;
expect(container).toHaveClass('test-class');
});

it('should call TransactionProvider', () => {
const onError = vi.fn();
const onStatus = vi.fn();
const onSuccess = vi.fn();
render(
<Transaction
address="0x123"
capabilities={{}}
chainId={123}
className="test-class"
contracts={[]}
onError={onError}
onStatus={onStatus}
onSuccess={onSuccess}
>
<div>Test Child</div>
</Transaction>,
);
// Checking if the TransactionProvider is called
expect(TransactionProvider).toHaveBeenCalledTimes(1);
});
});
2 changes: 2 additions & 0 deletions src/transaction/components/Transaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export function Transaction({
children,
contracts,
onError,
onStatus,
onSuccess,
}: TransactionReact) {
return (
Expand All @@ -19,6 +20,7 @@ export function Transaction({
chainId={chainId}
contracts={contracts}
onError={onError}
onStatus={onStatus}
onSuccess={onSuccess}
>
<div className={cn(className, 'flex w-full flex-col gap-2')}>
Expand Down
37 changes: 37 additions & 0 deletions src/transaction/components/TransactionProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ vi.mock('../hooks/useWriteContracts', () => ({

const TestComponent = () => {
const context = useTransactionContext();
const handleSetLifeCycleStatus = async () => {
context.setLifeCycleStatus({
statusName: 'error',
statusData: { code: 'code', error: 'error_long_messages' },
});
};
return (
<div data-testid="test-component">
<button type="button" onClick={context.onSubmit}>
Expand All @@ -47,6 +53,9 @@ const TestComponent = () => {
<span data-testid="context-value-isToastVisible">
{`${context.isToastVisible}`}
</span>
<button type="button" onClick={handleSetLifeCycleStatus}>
setLifeCycleStatus.error
</button>
</div>
);
};
Expand Down Expand Up @@ -76,6 +85,34 @@ describe('TransactionProvider', () => {
});
});

it('should emit onError when setLifeCycleStatus is called with error', async () => {
const onErrorMock = vi.fn();
render(
<TransactionProvider address="0x123" contracts={[]} onError={onErrorMock}>
<TestComponent />
</TransactionProvider>,
);
const button = screen.getByText('setLifeCycleStatus.error');
fireEvent.click(button);
expect(onErrorMock).toHaveBeenCalled();
});

it('should emit onStatus when setLifeCycleStatus is called', async () => {
const onStatusMock = vi.fn();
render(
<TransactionProvider
address="0x123"
contracts={[]}
onStatus={onStatusMock}
>
<TestComponent />
</TransactionProvider>,
);
const button = screen.getByText('setLifeCycleStatus.error');
fireEvent.click(button);
expect(onStatusMock).toHaveBeenCalled();
});

it('should update context on handleSubmit', async () => {
const writeContractsAsyncMock = vi.fn();
(useWriteContracts as ReturnType<typeof vi.fn>).mockReturnValue({
Expand Down
41 changes: 33 additions & 8 deletions src/transaction/components/TransactionProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { useCallsStatus } from '../hooks/useCallsStatus';
import { useWriteContract } from '../hooks/useWriteContract';
import { useWriteContracts } from '../hooks/useWriteContracts';
import type {
LifeCycleStatus,
TransactionContextType,
TransactionProviderReact,
} from '../types';
Expand All @@ -51,43 +52,66 @@ export function TransactionProvider({
children,
contracts,
onError,
onStatus,
onSuccess,
}: TransactionProviderReact) {
// Core Hooks
const account = useAccount();
const config = useConfig();
const [errorMessage, setErrorMessage] = useState('');
const [transactionId, setTransactionId] = useState('');
const [isToastVisible, setIsToastVisible] = useState(false);
const [lifeCycleStatus, setLifeCycleStatus] = useState<LifeCycleStatus>({
statusName: 'init',
statusData: null,
}); // Component lifecycle
const [receiptArray, setReceiptArray] = useState<TransactionReceipt[]>([]);
const [transactionId, setTransactionId] = useState('');
const [transactionHashArray, setTransactionHashArray] = useState<Address[]>(
[],
);
const [receiptArray, setReceiptArray] = useState<TransactionReceipt[]>([]);
const account = useAccount();
const config = useConfig();
const { switchChainAsync } = useSwitchChain();

// Hooks that depend from Core Hooks
const { status: statusWriteContracts, writeContractsAsync } =
useWriteContracts({
onError,
setErrorMessage,
setLifeCycleStatus,
setTransactionId,
});
const {
status: statusWriteContract,
writeContractAsync,
data: writeContractTransactionHash,
} = useWriteContract({
onError,
setErrorMessage,
setLifeCycleStatus,
setTransactionHashArray,
transactionHashArray,
});
const { transactionHash, status: callStatus } = useCallsStatus({
onError,
setLifeCycleStatus,
transactionId,
});

const { data: receipt } = useWaitForTransactionReceipt({
hash: writeContractTransactionHash || transactionHash,
});

// Component lifecycle emitters
useEffect(() => {
// Emit Error
if (lifeCycleStatus.statusName === 'error') {
onError?.(lifeCycleStatus.statusData);
}
// Emit State
onStatus?.(lifeCycleStatus);
}, [
onError,
onStatus,
lifeCycleStatus,
lifeCycleStatus.statusData, // Keep statusData, so that the effect runs when it changes
lifeCycleStatus.statusName, // Keep statusName, so that the effect runs when it changes
]);

const getTransactionReceipts = useCallback(async () => {
const receipts = [];
for (const hash of transactionHashArray) {
Expand Down Expand Up @@ -208,6 +232,7 @@ export function TransactionProvider({
receipt,
setErrorMessage,
setIsToastVisible,
setLifeCycleStatus,
setTransactionId,
statusWriteContracts,
statusWriteContract,
Expand Down
22 changes: 11 additions & 11 deletions src/transaction/hooks/useCallsStatus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,34 @@ describe('useCallsStatus', () => {
status: 'CONFIRMED',
receipts: [{ transactionHash: '0x123' }],
};

(useCallsStatusWagmi as ReturnType<typeof vi.fn>).mockReturnValue({
data: mockData,
});

const { result } = renderHook(() => useCallsStatus({ transactionId }));

expect(result.current.status).toBe('CONFIRMED');
expect(result.current.transactionHash).toBe('0x123');
});

it('should handle errors and call onError callback', () => {
const mockOnError = vi.fn();
const mockSetLifeCycleStatus = vi.fn();
const mockError = new Error('Test error');

(useCallsStatusWagmi as ReturnType<typeof vi.fn>).mockImplementation(() => {
throw mockError;
});

const { result } = renderHook(() =>
useCallsStatus({ transactionId, onError: mockOnError }),
useCallsStatus({
setLifeCycleStatus: mockSetLifeCycleStatus,
transactionId,
}),
);

expect(result.current.status).toBe('error');
expect(result.current.transactionHash).toBeUndefined();
expect(mockOnError).toHaveBeenCalledWith({
code: 'UNCAUGHT_CALL_STATUS_ERROR',
error: JSON.stringify(mockError),
expect(mockSetLifeCycleStatus).toHaveBeenCalledWith({
statusName: 'error',
statusData: {
code: 'UNCAUGHT_CALL_STATUS_ERROR',
error: JSON.stringify(mockError),
},
});
});
});
13 changes: 7 additions & 6 deletions src/transaction/hooks/useCallsStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,26 @@ import type { UseCallsStatusParams } from '../types';
const uncaughtErrorCode = 'UNCAUGHT_CALL_STATUS_ERROR';

export function useCallsStatus({
onError,
setLifeCycleStatus,
transactionId,
}: UseCallsStatusParams) {
try {
const { data } = useCallsStatusWagmi({
id: transactionId,
query: {
refetchInterval: (data) => {
return data.state.data?.status === 'CONFIRMED' ? false : 1000;
refetchInterval: (query) => {
return query.state.data?.status === 'CONFIRMED' ? false : 1000;
},
enabled: !!transactionId,
},
});

const transactionHash = data?.receipts?.[0]?.transactionHash;

return { status: data?.status, transactionHash };
} catch (err) {
onError?.({ code: uncaughtErrorCode, error: JSON.stringify(err) });
setLifeCycleStatus({
statusName: 'error',
statusData: { code: uncaughtErrorCode, error: JSON.stringify(err) },
});
return { status: 'error', transactionHash: undefined };
}
}
10 changes: 0 additions & 10 deletions src/transaction/hooks/useGetTransactionStatus.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,15 @@ describe('useGetTransactionStatus', () => {
(useTransactionContext as vi.Mock).mockReturnValue({
statusWriteContract: 'pending',
});

const { result } = renderHook(() => useGetTransactionStatus());

expect(result.current.label).toBe('Confirm in wallet.');
});

it('should return correct status and actionElement when transaction hash exists', () => {
(useTransactionContext as vi.Mock).mockReturnValue({
transactionHash: 'ab123',
});

const { result } = renderHook(() => useGetTransactionStatus());

expect(result.current.label).toBe('Transaction in progress...');
expect(result.current.actionElement).not.toBeNull();
});
Expand All @@ -42,9 +38,7 @@ describe('useGetTransactionStatus', () => {
receipt: 'receipt',
transactionHash: '123',
});

const { result } = renderHook(() => useGetTransactionStatus());

expect(result.current.label).toBe('Successful!');
expect(result.current.actionElement).not.toBeNull();
});
Expand All @@ -53,9 +47,7 @@ describe('useGetTransactionStatus', () => {
(useTransactionContext as vi.Mock).mockReturnValue({
errorMessage: 'error',
});

const { result } = renderHook(() => useGetTransactionStatus());

expect(result.current.label).toBe('error');
expect(result.current.labelClassName).toBe('text-ock-error');
});
Expand All @@ -64,9 +56,7 @@ describe('useGetTransactionStatus', () => {
(useTransactionContext as vi.Mock).mockReturnValue({
errorMessage: '',
});

const { result } = renderHook(() => useGetTransactionStatus());

expect(result.current.label).toBe('');
expect(result.current.actionElement).toBeNull();
});
Expand Down
Loading

0 comments on commit 932d5b2

Please sign in to comment.