diff --git a/src/internal/svg/errorSvg.tsx b/src/internal/svg/errorSvg.tsx index 0eb31288e1..98ffd47dff 100644 --- a/src/internal/svg/errorSvg.tsx +++ b/src/internal/svg/errorSvg.tsx @@ -5,6 +5,7 @@ export const errorSvg = ( viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" + data-testid="ockErrorSvg" > Error SVG Success SVG ({ useTransactionContext: vi.fn(), })); +vi.mock('wagmi', () => ({ + useChainId: vi.fn(), +})); + vi.mock('wagmi/experimental', () => ({ useShowCallsStatus: vi.fn(), })); +vi.mock('../../network/getChainExplorer', () => ({ + getChainExplorer: vi.fn(), +})); + describe('TransactionButton', () => { + beforeEach(() => { + (useChainId as vi.Mock).mockReturnValue(123); + (useShowCallsStatus as vi.Mock).mockReturnValue({ + showCallsStatus: vi.fn(), + }); + }); it('renders correctly', () => { (useTransactionContext as vi.Mock).mockReturnValue({ isLoading: false, @@ -34,7 +51,7 @@ describe('TransactionButton', () => { expect(spinner).toBeInTheDocument(); }); - it('renders checkmark svg correctly when receipt exists', () => { + it('renders view txn text when receipt exists', () => { (useTransactionContext as vi.Mock).mockReturnValue({ isLoading: true, receipt: '123', @@ -42,8 +59,8 @@ describe('TransactionButton', () => { render(); - const checkmark = screen.getByTestId('ockCheckmarkSvg'); - expect(checkmark).toBeInTheDocument(); + const text = screen.getByText('View transaction'); + expect(text).toBeInTheDocument(); }); it('renders try again when error exists', () => { @@ -86,6 +103,21 @@ describe('TransactionButton', () => { expect(button).toBeDisabled(); }); + it('should call showCallsStatus when receipt and transactionId exist', () => { + const showCallsStatus = vi.fn(); + (useShowCallsStatus as vi.Mock).mockReturnValue({ showCallsStatus }); + (useTransactionContext as vi.Mock).mockReturnValue({ + receipt: '123', + transactionId: '456', + }); + + render(); + const button = screen.getByText('View transaction'); + fireEvent.click(button); + + expect(showCallsStatus).toHaveBeenCalledWith({ id: '456' }); + }); + it('should enable button when not in progress, not missing props, and not waiting for receipt', () => { (useTransactionContext as vi.Mock).mockReturnValue({ isLoading: false, @@ -100,4 +132,45 @@ describe('TransactionButton', () => { const button = getByRole('button'); expect(button).not.toBeDisabled(); }); + + it('should open transaction link when only receipt exists', () => { + const onSubmit = vi.fn(); + const chainExplorerUrl = 'https://explorer.com'; + (useTransactionContext as vi.Mock).mockReturnValue({ + receipt: 'receipt-123', + transactionId: undefined, + transactionHash: 'hash-789', + onSubmit, + }); + (getChainExplorer as vi.Mock).mockReturnValue(chainExplorerUrl); + window.open = vi.fn(); + + render(); + const button = screen.getByText('View transaction'); + fireEvent.click(button); + + expect(window.open).toHaveBeenCalledWith( + `${chainExplorerUrl}/tx/hash-789`, + '_blank', + 'noopener,noreferrer', + ); + expect(onSubmit).not.toHaveBeenCalled(); + }); + + it('should call onSubmit when neither receipt nor transactionId exists', () => { + const onSubmit = vi.fn(); + (useTransactionContext as vi.Mock).mockReturnValue({ + receipt: undefined, + transactionId: undefined, + onSubmit, + address: '123', + contracts: [{}], + }); + + render(); + const button = screen.getByText('Transact'); + fireEvent.click(button); + + expect(onSubmit).toHaveBeenCalled(); + }); }); diff --git a/src/transaction/components/TransactionButton.tsx b/src/transaction/components/TransactionButton.tsx index 557e7154d0..49f5edf959 100644 --- a/src/transaction/components/TransactionButton.tsx +++ b/src/transaction/components/TransactionButton.tsx @@ -1,6 +1,8 @@ -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; +import { useChainId } from 'wagmi'; +import { useShowCallsStatus } from 'wagmi/experimental'; import { Spinner } from '../../internal/components/Spinner'; -import { checkmarkSvg } from '../../internal/svg/checkmarkSvg'; +import { getChainExplorer } from '../../network/getChainExplorer'; import { background, cn, color, pressable, text } from '../../styles/theme'; import type { TransactionButtonReact } from '../types'; import { isSpinnerDisplayed } from '../utils/isSpinnerDisplayed'; @@ -14,6 +16,7 @@ export function TransactionButton({ const { address, contracts, + chainId, errorMessage, isLoading, onSubmit, @@ -24,6 +27,9 @@ export function TransactionButton({ transactionId, } = useTransactionContext(); + const accountChainId = chainId ?? useChainId(); + const { showCallsStatus } = useShowCallsStatus(); + const isInProgress = statusWriteContract === 'pending' || statusWriteContracts === 'pending' || @@ -46,8 +52,9 @@ export function TransactionButton({ }); const buttonContent = useMemo(() => { + // txn successful if (receipt) { - return checkmarkSvg; + return 'View transaction'; } if (errorMessage) { return 'Try again'; @@ -55,6 +62,31 @@ export function TransactionButton({ return buttonText; }, [buttonText, errorMessage, receipt]); + const handleSubmit = useCallback(() => { + // SW will have txn id so open in wallet + if (receipt && transactionId) { + showCallsStatus({ id: transactionId }); + // EOA will not have txn id so open in explorer + } else if (receipt) { + const chainExplorer = getChainExplorer(accountChainId); + window.open( + `${chainExplorer}/tx/${transactionHash}`, + '_blank', + 'noopener,noreferrer', + ); + } else { + // if no receipt, submit txn + onSubmit(); + } + }, [ + accountChainId, + onSubmit, + receipt, + showCallsStatus, + transactionHash, + transactionId, + ]); + return ( , }); diff --git a/src/transaction/components/TransactionStatusAction.tsx b/src/transaction/components/TransactionStatusAction.tsx index 27997a737c..c83ab6eed0 100644 --- a/src/transaction/components/TransactionStatusAction.tsx +++ b/src/transaction/components/TransactionStatusAction.tsx @@ -1,11 +1,11 @@ import { cn, text } from '../../styles/theme'; -import { useGetTransactionStatus } from '../hooks/useGetTransactionStatus'; +import { useGetTransactionStatusAction } from '../hooks/useGetTransactionStatusAction'; import type { TransactionStatusActionReact } from '../types'; export function TransactionStatusAction({ className, }: TransactionStatusActionReact) { - const { actionElement } = useGetTransactionStatus(); + const { actionElement } = useGetTransactionStatusAction(); return (
diff --git a/src/transaction/components/TransactionStatusLabel.test.tsx b/src/transaction/components/TransactionStatusLabel.test.tsx index 3e42d3f641..6c2e18af6e 100644 --- a/src/transaction/components/TransactionStatusLabel.test.tsx +++ b/src/transaction/components/TransactionStatusLabel.test.tsx @@ -1,15 +1,15 @@ import { render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; -import { useGetTransactionStatus } from '../hooks/useGetTransactionStatus'; +import { useGetTransactionStatusLabel } from '../hooks/useGetTransactionStatusLabel'; import { TransactionStatusLabel } from './TransactionStatusLabel'; -vi.mock('../hooks/useGetTransactionStatus', () => ({ - useGetTransactionStatus: vi.fn(), +vi.mock('../hooks/useGetTransactionStatusLabel', () => ({ + useGetTransactionStatusLabel: vi.fn(), })); describe('TransactionStatusLabel', () => { it('renders transaction status label', () => { - (useGetTransactionStatus as vi.Mock).mockReturnValue({ + (useGetTransactionStatusLabel as vi.Mock).mockReturnValue({ label: 'Successful!', labelClassName: 'text-ock-foreground-muted', }); diff --git a/src/transaction/components/TransactionStatusLabel.tsx b/src/transaction/components/TransactionStatusLabel.tsx index 3ba0584ebe..4edb5729f8 100644 --- a/src/transaction/components/TransactionStatusLabel.tsx +++ b/src/transaction/components/TransactionStatusLabel.tsx @@ -1,11 +1,11 @@ import { cn, text } from '../../styles/theme'; -import { useGetTransactionStatus } from '../hooks/useGetTransactionStatus'; +import { useGetTransactionStatusLabel } from '../hooks/useGetTransactionStatusLabel'; import type { TransactionStatusLabelReact } from '../types'; export function TransactionStatusLabel({ className, }: TransactionStatusLabelReact) { - const { label, labelClassName } = useGetTransactionStatus(); + const { label, labelClassName } = useGetTransactionStatusLabel(); return (
diff --git a/src/transaction/components/TransactionToastAction.test.tsx b/src/transaction/components/TransactionToastAction.test.tsx index 61924ab518..17c125327e 100644 --- a/src/transaction/components/TransactionToastAction.test.tsx +++ b/src/transaction/components/TransactionToastAction.test.tsx @@ -1,15 +1,15 @@ import { render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; -import { useGetTransactionToast } from '../hooks/useGetTransactionToast'; +import { useGetTransactionToastAction } from '../hooks/useGetTransactionToastAction'; import { TransactionToastAction } from './TransactionToastAction'; -vi.mock('../hooks/useGetTransactionToast', () => ({ - useGetTransactionToast: vi.fn(), +vi.mock('../hooks/useGetTransactionToastAction', () => ({ + useGetTransactionToastAction: vi.fn(), })); describe('TransactionToastAction', () => { it('renders transaction status action', () => { - (useGetTransactionToast as vi.Mock).mockReturnValue({ + (useGetTransactionToastAction as vi.Mock).mockReturnValue({ actionElement: , }); diff --git a/src/transaction/components/TransactionToastAction.tsx b/src/transaction/components/TransactionToastAction.tsx index d51ae55e13..0a4b64c1b4 100644 --- a/src/transaction/components/TransactionToastAction.tsx +++ b/src/transaction/components/TransactionToastAction.tsx @@ -1,11 +1,11 @@ import { cn, text } from '../../styles/theme'; -import { useGetTransactionToast } from '../hooks/useGetTransactionToast'; +import { useGetTransactionToastAction } from '../hooks/useGetTransactionToastAction'; import type { TransactionToastActionReact } from '../types'; export function TransactionToastAction({ className, }: TransactionToastActionReact) { - const { actionElement } = useGetTransactionToast(); + const { actionElement } = useGetTransactionToastAction(); return (
diff --git a/src/transaction/components/TransactionToastIcon.test.tsx b/src/transaction/components/TransactionToastIcon.test.tsx index 257776fd41..d8de66c253 100644 --- a/src/transaction/components/TransactionToastIcon.test.tsx +++ b/src/transaction/components/TransactionToastIcon.test.tsx @@ -1,21 +1,53 @@ import { render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; -import { useGetTransactionToast } from '../hooks/useGetTransactionToast'; +import { useTransactionContext } from '../components/TransactionProvider'; import { TransactionToastIcon } from './TransactionToastIcon'; -vi.mock('../hooks/useGetTransactionToast', () => ({ - useGetTransactionToast: vi.fn(), +vi.mock('../components/TransactionProvider', () => ({ + useTransactionContext: vi.fn(), })); describe('TransactionToastIcon', () => { - it('renders transaction toast icon', () => { - (useGetTransactionToast as vi.Mock).mockReturnValue({ - icon:
icon
, + it('renders success icon when receipt exists', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + receipt: '123', }); render(); - const iconElement = screen.getByText('icon'); + const iconElement = screen.getByTestId('ockSuccessSvg'); expect(iconElement).toBeInTheDocument(); }); + it('renders error icon when error exists', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + errorMessage: 'error', + }); + + render(); + + const iconElement = screen.getByTestId('ockErrorSvg'); + expect(iconElement).toBeInTheDocument(); + }); + it('renders loading icon when txn is in progress', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + isLoading: true, + }); + + render(); + + const iconElement = screen.getByTestId('ockSpinner'); + expect(iconElement).toBeInTheDocument(); + }); + it('renders null when if no status exists', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + isLoading: false, + }); + + const { container } = render( + , + ); + + // Assert that nothing is rendered (container should be empty) + expect(container.firstChild).toBeNull(); + }); }); diff --git a/src/transaction/components/TransactionToastIcon.tsx b/src/transaction/components/TransactionToastIcon.tsx index 5401adc107..19915e01e8 100644 --- a/src/transaction/components/TransactionToastIcon.tsx +++ b/src/transaction/components/TransactionToastIcon.tsx @@ -1,8 +1,33 @@ +import { useMemo } from 'react'; +import { Spinner } from '../../internal/components/Spinner'; +import { errorSvg } from '../../internal/svg/errorSvg'; +import { successSvg } from '../../internal/svg/successSvg'; import { cn, text } from '../../styles/theme'; -import { useGetTransactionToast } from '../hooks/useGetTransactionToast'; import type { TransactionToastIconReact } from '../types'; +import { useTransactionContext } from './TransactionProvider'; export function TransactionToastIcon({ className }: TransactionToastIconReact) { - const { icon } = useGetTransactionToast(); + const { errorMessage, isLoading, receipt, transactionHash, transactionId } = + useTransactionContext(); + const isInProgress = isLoading || !!transactionId || !!transactionHash; + + const icon = useMemo(() => { + // txn successful + if (receipt) { + return successSvg; + } + if (errorMessage) { + return errorSvg; + } + if (isInProgress) { + return ; + } + return null; + }, [isInProgress, errorMessage, receipt]); + + if (!icon) { + return null; + } + return
{icon}
; } diff --git a/src/transaction/components/TransactionToastLabel.test.tsx b/src/transaction/components/TransactionToastLabel.test.tsx index 068327cc84..1ecea3873a 100644 --- a/src/transaction/components/TransactionToastLabel.test.tsx +++ b/src/transaction/components/TransactionToastLabel.test.tsx @@ -1,15 +1,15 @@ import { render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; -import { useGetTransactionToast } from '../hooks/useGetTransactionToast'; +import { useGetTransactionToastLabel } from '../hooks/useGetTransactionToastLabel'; import { TransactionToastLabel } from './TransactionToastLabel'; -vi.mock('../hooks/useGetTransactionToast', () => ({ - useGetTransactionToast: vi.fn(), +vi.mock('../hooks/useGetTransactionToastLabel', () => ({ + useGetTransactionToastLabel: vi.fn(), })); describe('TransactionToastLabel', () => { it('renders transaction status action', () => { - (useGetTransactionToast as vi.Mock).mockReturnValue({ + (useGetTransactionToastLabel as vi.Mock).mockReturnValue({ label: 'Successful', }); diff --git a/src/transaction/components/TransactionToastLabel.tsx b/src/transaction/components/TransactionToastLabel.tsx index d1b2c69699..dfba18e5e9 100644 --- a/src/transaction/components/TransactionToastLabel.tsx +++ b/src/transaction/components/TransactionToastLabel.tsx @@ -1,11 +1,11 @@ import { cn, color, text } from '../../styles/theme'; -import { useGetTransactionToast } from '../hooks/useGetTransactionToast'; +import { useGetTransactionToastLabel } from '../hooks/useGetTransactionToastLabel'; import type { TransactionToastLabelReact } from '../types'; export function TransactionToastLabel({ className, }: TransactionToastLabelReact) { - const { label } = useGetTransactionToast(); + const { label } = useGetTransactionToastLabel(); return (

{label}

diff --git a/src/transaction/hooks/useGetTransactionStatus.test.tsx b/src/transaction/hooks/useGetTransactionStatus.test.tsx deleted file mode 100644 index ea5abf9b82..0000000000 --- a/src/transaction/hooks/useGetTransactionStatus.test.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { useChainId } from 'wagmi'; -import { useTransactionContext } from '../components/TransactionProvider'; -import { useGetTransactionStatus } from './useGetTransactionStatus'; - -vi.mock('../components/TransactionProvider', () => ({ - useTransactionContext: vi.fn(), -})); - -vi.mock('wagmi', () => ({ - useChainId: vi.fn(), -})); - -describe('useGetTransactionStatus', () => { - beforeEach(() => { - (useChainId as vi.Mock).mockReturnValue(123); - }); - it('should return correct status and actionElement when transaction is pending', () => { - (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(); - }); - - it('should return correct status and actionElement when receipt exists', () => { - (useTransactionContext as vi.Mock).mockReturnValue({ - receipt: 'receipt', - transactionHash: '123', - }); - const { result } = renderHook(() => useGetTransactionStatus()); - expect(result.current.label).toBe('Successful!'); - expect(result.current.actionElement).not.toBeNull(); - }); - - it('should return correct status and actionElement when error occurs', () => { - (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'); - }); - - it('should return correct status and actionElement when no status available', () => { - (useTransactionContext as vi.Mock).mockReturnValue({ - errorMessage: '', - }); - const { result } = renderHook(() => useGetTransactionStatus()); - expect(result.current.label).toBe(''); - expect(result.current.actionElement).toBeNull(); - }); -}); diff --git a/src/transaction/hooks/useGetTransactionStatusAction.test.tsx b/src/transaction/hooks/useGetTransactionStatusAction.test.tsx new file mode 100644 index 0000000000..17ce2a069b --- /dev/null +++ b/src/transaction/hooks/useGetTransactionStatusAction.test.tsx @@ -0,0 +1,119 @@ +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useChainId } from 'wagmi'; +import { useShowCallsStatus } from 'wagmi/experimental'; +import { getChainExplorer } from '../../network/getChainExplorer'; +import { useTransactionContext } from '../components/TransactionProvider'; +import { useGetTransactionStatusAction } from './useGetTransactionStatusAction'; + +vi.mock('../components/TransactionProvider', () => ({ + useTransactionContext: vi.fn(), +})); + +vi.mock('wagmi', () => ({ + useChainId: vi.fn(), +})); + +vi.mock('wagmi/experimental', () => ({ + useShowCallsStatus: vi.fn(), +})); + +vi.mock('../../network/getChainExplorer', () => ({ + getChainExplorer: vi.fn(), +})); + +const mockGetChainExplorer = 'https://etherscan.io'; + +describe('useGetTransactionStatusAction', () => { + beforeEach(() => { + (useChainId as vi.Mock).mockReturnValue(123); + (useShowCallsStatus as vi.Mock).mockReturnValue({ + showCallsStatus: vi.fn(), + }); + (getChainExplorer as vi.Mock).mockReturnValue(mockGetChainExplorer); + }); + + it('should return actionElement when transaction hash exists', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + transactionHash: '0x123', + }); + + const { result } = renderHook(() => useGetTransactionStatusAction()); + + expect(result.current.actionElement).toMatchInlineSnapshot(` + + + View transaction + + + `); + }); + + it('should return actionElement when transaction id exists', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + transactionId: 'ab123', + onSubmit: vi.fn(), + }); + + const showCallsStatus = vi.fn(); + (useShowCallsStatus as vi.Mock).mockReturnValue({ showCallsStatus }); + + const { result } = renderHook(() => useGetTransactionStatusAction()); + + const button = result.current.actionElement as JSX.Element; + expect(button.props.onClick).toBeDefined(); + expect(button).not.toBeNull(); + }); + + it('should return null when receipt exists', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + receipt: 'receipt', + transactionHash: '123', + }); + + const { result } = renderHook(() => useGetTransactionStatusAction()); + + expect(result.current.actionElement).toBeNull(); + }); + + it('should return actionElement when error occurs', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + errorMessage: 'error', + }); + + const { result } = renderHook(() => useGetTransactionStatusAction()); + + expect(result.current.actionElement).toBeNull(); + }); + + it('should return actionElement when no status available', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + errorMessage: '', + }); + + const { result } = renderHook(() => useGetTransactionStatusAction()); + + expect(result.current.actionElement).toBeNull(); + }); + + it('should call showCallsStatus when button is clicked', () => { + const showCallsStatus = vi.fn(); + (useShowCallsStatus as vi.Mock).mockReturnValue({ showCallsStatus }); + (useTransactionContext as vi.Mock).mockReturnValue({ + transactionId: 'ab123', + }); + + const { result } = renderHook(() => useGetTransactionStatusAction()); + + const button = result.current.actionElement as JSX.Element; + button.props.onClick(); + + expect(showCallsStatus).toHaveBeenCalledWith({ id: 'ab123' }); + }); +}); diff --git a/src/transaction/hooks/useGetTransactionStatus.tsx b/src/transaction/hooks/useGetTransactionStatusAction.tsx similarity index 53% rename from src/transaction/hooks/useGetTransactionStatus.tsx rename to src/transaction/hooks/useGetTransactionStatusAction.tsx index b101dcca17..0bb6d72dae 100644 --- a/src/transaction/hooks/useGetTransactionStatus.tsx +++ b/src/transaction/hooks/useGetTransactionStatusAction.tsx @@ -1,41 +1,24 @@ import { useMemo } from 'react'; import type { ReactNode } from 'react'; import { useChainId } from 'wagmi'; +import { useShowCallsStatus } from 'wagmi/experimental'; import { getChainExplorer } from '../../network/getChainExplorer'; import { cn, color, text } from '../../styles/theme'; import { useTransactionContext } from '../components/TransactionProvider'; -export function useGetTransactionStatus() { - const { - chainId, - errorMessage, - isLoading, - receipt, - statusWriteContract, - statusWriteContracts, - transactionHash, - transactionId, - } = useTransactionContext(); +export function useGetTransactionStatusAction() { + const { chainId, receipt, transactionHash, transactionId } = + useTransactionContext(); const accountChainId = chainId ?? useChainId(); - const isPending = - statusWriteContract === 'pending' || statusWriteContracts === 'pending'; - const isInProgress = isLoading || !!transactionId || !!transactionHash; + + const { showCallsStatus } = useShowCallsStatus(); return useMemo(() => { const chainExplorer = getChainExplorer(accountChainId); let actionElement: ReactNode = null; - let label = ''; - let labelClassName: string = color.foregroundMuted; - - if (isPending) { - label = 'Confirm in wallet.'; - } - - if (isInProgress) { - label = 'Transaction in progress...'; - } + // EOA will have txn hash if (transactionHash) { actionElement = ( showCallsStatus({ id: transactionId })} + type="button" + > + + View transaction + + + ); } - if (errorMessage) { - label = errorMessage; - labelClassName = color.error; + if (receipt) { + actionElement = null; } - return { actionElement, label, labelClassName }; + return { actionElement }; }, [ accountChainId, - errorMessage, - isInProgress, - isPending, receipt, + showCallsStatus, transactionHash, + transactionId, ]); } diff --git a/src/transaction/hooks/useGetTransactionStatusLabel.test.tsx b/src/transaction/hooks/useGetTransactionStatusLabel.test.tsx new file mode 100644 index 0000000000..b4ee914e4a --- /dev/null +++ b/src/transaction/hooks/useGetTransactionStatusLabel.test.tsx @@ -0,0 +1,98 @@ +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useChainId } from 'wagmi'; +import { useShowCallsStatus } from 'wagmi/experimental'; +import { getChainExplorer } from '../../network/getChainExplorer'; +import { useTransactionContext } from '../components/TransactionProvider'; +import { useGetTransactionStatusLabel } from './useGetTransactionStatusLabel'; + +vi.mock('../components/TransactionProvider', () => ({ + useTransactionContext: vi.fn(), +})); + +vi.mock('wagmi', () => ({ + useChainId: vi.fn(), +})); + +vi.mock('wagmi/experimental', () => ({ + useShowCallsStatus: vi.fn(), +})); + +vi.mock('../../network/getChainExplorer', () => ({ + getChainExplorer: vi.fn(), +})); + +const mockGetChainExplorer = 'https://etherscan.io'; +describe('useGetTransactionStatusLabel', () => { + beforeEach(() => { + (useChainId as vi.Mock).mockReturnValue(123); + (useShowCallsStatus as vi.Mock).mockReturnValue({ + showCallsStatus: vi.fn(), + }); + (getChainExplorer as vi.Mock).mockReturnValue(mockGetChainExplorer); + }); + + it('should return correct status when transaction is pending', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + statusWriteContract: 'pending', + }); + + const { result } = renderHook(() => useGetTransactionStatusLabel()); + + expect(result.current.label).toBe('Confirm in wallet.'); + }); + it('should return status when transaction hash exists', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + transactionHash: '0x123', + }); + + const { result } = renderHook(() => useGetTransactionStatusLabel()); + + expect(result.current.label).toBe('Transaction in progress...'); + }); + + it('should return status when transaction id exists', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + transactionId: 'ab123', + onSubmit: vi.fn(), + }); + + const showCallsStatus = vi.fn(); + (useShowCallsStatus as vi.Mock).mockReturnValue({ showCallsStatus }); + + const { result } = renderHook(() => useGetTransactionStatusLabel()); + + expect(result.current.label).toBe('Transaction in progress...'); + }); + + it('should return status when receipt exists', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + receipt: 'receipt', + transactionHash: '123', + }); + + const { result } = renderHook(() => useGetTransactionStatusLabel()); + + expect(result.current.label).toBe('Successful'); + }); + + it('should return status when error occurs', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + errorMessage: 'error', + }); + + const { result } = renderHook(() => useGetTransactionStatusLabel()); + + expect(result.current.label).toBe('error'); + }); + + it('should return status when no status available', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + errorMessage: '', + }); + + const { result } = renderHook(() => useGetTransactionStatusLabel()); + + expect(result.current.label).toBe(''); + }); +}); diff --git a/src/transaction/hooks/useGetTransactionStatusLabel.tsx b/src/transaction/hooks/useGetTransactionStatusLabel.tsx new file mode 100644 index 0000000000..f3b31536ea --- /dev/null +++ b/src/transaction/hooks/useGetTransactionStatusLabel.tsx @@ -0,0 +1,45 @@ +import { useMemo } from 'react'; +import { color } from '../../styles/theme'; +import { useTransactionContext } from '../components/TransactionProvider'; + +export function useGetTransactionStatusLabel() { + const { + errorMessage, + isLoading, + receipt, + statusWriteContract, + statusWriteContracts, + transactionHash, + transactionId, + } = useTransactionContext(); + // user confirmed in wallet, txn in progress + const isInProgress = isLoading || !!transactionId || !!transactionHash; + + // user started txn and needs to confirm in wallet + const isPending = + statusWriteContract === 'pending' || statusWriteContracts === 'pending'; + + return useMemo(() => { + let label = ''; + let labelClassName: string = color.foregroundMuted; + + if (isPending) { + label = 'Confirm in wallet.'; + } + + if (isInProgress) { + label = 'Transaction in progress...'; + } + + if (receipt) { + label = 'Successful'; + } + + if (errorMessage) { + label = errorMessage; + labelClassName = color.error; + } + + return { label, labelClassName }; + }, [errorMessage, isInProgress, isPending, receipt]); +} diff --git a/src/transaction/hooks/useGetTransactionToast.test.tsx b/src/transaction/hooks/useGetTransactionToast.test.tsx deleted file mode 100644 index c0c683566e..0000000000 --- a/src/transaction/hooks/useGetTransactionToast.test.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { useChainId } from 'wagmi'; -import { useTransactionContext } from '../components/TransactionProvider'; -import { useGetTransactionToast } from './useGetTransactionToast'; - -vi.mock('../components/TransactionProvider', () => ({ - useTransactionContext: vi.fn(), -})); - -vi.mock('wagmi', () => ({ - useChainId: vi.fn(), -})); - -describe('useGetTransactionToast', () => { - beforeEach(() => { - (useChainId as vi.Mock).mockReturnValue(123); - }); - it('should return correct toast and actionElement when transaction is loading', () => { - (useTransactionContext as vi.Mock).mockReturnValue({ - isLoading: true, - }); - - const { result } = renderHook(() => useGetTransactionToast()); - - expect(result.current.label).toBe('Transaction in progress'); - }); - - it('should return correct toast and actionElement when receipt exists', () => { - (useTransactionContext as vi.Mock).mockReturnValue({ - receipt: 'receipt', - transactionHash: '123', - }); - - const { result } = renderHook(() => useGetTransactionToast()); - - expect(result.current.label).toBe('Successful'); - expect(result.current.actionElement).not.toBeNull(); - }); - - it('should return correct toast and actionElement when error occurs', () => { - (useTransactionContext as vi.Mock).mockReturnValue({ - errorMessage: 'error', - }); - - const { result } = renderHook(() => useGetTransactionToast()); - - expect(result.current.label).toBe('Something went wrong'); - expect(result.current.actionElement).not.toBeNull(); - }); - - it('should return correct toast and actionElement when no status available', () => { - (useTransactionContext as vi.Mock).mockReturnValue({ - errorMessage: '', - }); - - const { result } = renderHook(() => useGetTransactionToast()); - - expect(result.current.label).toBe(''); - expect(result.current.actionElement).toBeNull(); - }); -}); diff --git a/src/transaction/hooks/useGetTransactionToastAction.test.tsx b/src/transaction/hooks/useGetTransactionToastAction.test.tsx new file mode 100644 index 0000000000..2923a43e31 --- /dev/null +++ b/src/transaction/hooks/useGetTransactionToastAction.test.tsx @@ -0,0 +1,172 @@ +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useChainId } from 'wagmi'; +import { useShowCallsStatus } from 'wagmi/experimental'; +import { getChainExplorer } from '../../network/getChainExplorer'; +import { useTransactionContext } from '../components/TransactionProvider'; +import { useGetTransactionToastAction } from './useGetTransactionToastAction'; + +vi.mock('../components/TransactionProvider', () => ({ + useTransactionContext: vi.fn(), +})); + +vi.mock('wagmi', () => ({ + useChainId: vi.fn(), +})); + +vi.mock('wagmi/experimental', () => ({ + useShowCallsStatus: vi.fn(), +})); + +vi.mock('../../network/getChainExplorer', () => ({ + getChainExplorer: vi.fn(), +})); + +const mockGetChainExplorer = 'https://etherscan.io'; + +describe('useGetTransactionToastAction', () => { + beforeEach(() => { + (useChainId as vi.Mock).mockReturnValue(123); + (useShowCallsStatus as vi.Mock).mockReturnValue({ + showCallsStatus: vi.fn(), + }); + (getChainExplorer as vi.Mock).mockReturnValue(mockGetChainExplorer); + }); + + it('should return actionElement when transaction hash exists', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + transactionHash: '0x123', + }); + + const { result } = renderHook(() => useGetTransactionToastAction()); + + expect(result.current.actionElement).toMatchInlineSnapshot(` + + + View transaction + + + `); + }); + + it('should return actionElement when transaction id exists', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + transactionId: 'ab123', + onSubmit: vi.fn(), + }); + + const showCallsStatus = vi.fn(); + (useShowCallsStatus as vi.Mock).mockReturnValue({ showCallsStatus }); + + const { result } = renderHook(() => useGetTransactionToastAction()); + + const button = result.current.actionElement as JSX.Element; + expect(button.props.onClick).toBeDefined(); + expect(button).not.toBeNull(); + }); + + it('should return actionElement when receipt exists', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + receipt: 'receipt', + transactionHash: '0x123', + }); + + const { result } = renderHook(() => useGetTransactionToastAction()); + + expect(result.current.actionElement).toMatchInlineSnapshot(` + + + View transaction + + + `); + }); + + it('should return actionElement when error occurs', () => { + const onSubmitMock = vi.fn(); + (useTransactionContext as vi.Mock).mockReturnValue({ + errorMessage: 'error', + onSubmit: onSubmitMock, + }); + + const { result } = renderHook(() => useGetTransactionToastAction()); + + const button = result.current.actionElement as JSX.Element; + expect(button.props.onClick).toBe(onSubmitMock); + }); + + it('should return actionElement when no status available', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + errorMessage: '', + }); + + const { result } = renderHook(() => useGetTransactionToastAction()); + + expect(result.current.actionElement).toBeNull(); + }); + + it('should prioritize transactionId over transactionHash when both are provided', () => { + const showCallsStatus = vi.fn(); + (useTransactionContext as vi.Mock).mockReturnValue({ + transactionHash: '0x123', + transactionId: 'ab123', + }); + (useShowCallsStatus as vi.Mock).mockReturnValue({ showCallsStatus }); + + const { result } = renderHook(() => useGetTransactionToastAction()); + + const button = result.current.actionElement as JSX.Element; + expect(button.props.onClick).toBeDefined(); + expect(button).not.toBeNull(); + }); + + it('should use accountChainId from useChainId when chainId is not available in context', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + chainId: undefined, + transactionHash: '0x123', + }); + + const { result } = renderHook(() => useGetTransactionToastAction()); + + expect(result.current.actionElement).toMatchInlineSnapshot(` + + + View transaction + + + `); + }); + + it('should call showCallsStatus when button is clicked', () => { + const showCallsStatus = vi.fn(); + (useShowCallsStatus as vi.Mock).mockReturnValue({ showCallsStatus }); + (useTransactionContext as vi.Mock).mockReturnValue({ + transactionId: 'ab123', + }); + + const { result } = renderHook(() => useGetTransactionToastAction()); + + const button = result.current.actionElement as JSX.Element; + button.props.onClick(); + + expect(showCallsStatus).toHaveBeenCalledWith({ id: 'ab123' }); + }); +}); diff --git a/src/transaction/hooks/useGetTransactionToast.tsx b/src/transaction/hooks/useGetTransactionToastAction.tsx similarity index 58% rename from src/transaction/hooks/useGetTransactionToast.tsx rename to src/transaction/hooks/useGetTransactionToastAction.tsx index 6006e0173c..846209fab5 100644 --- a/src/transaction/hooks/useGetTransactionToast.tsx +++ b/src/transaction/hooks/useGetTransactionToastAction.tsx @@ -1,38 +1,24 @@ import { useMemo } from 'react'; import type { ReactNode } from 'react'; import { useChainId } from 'wagmi'; -import { Spinner } from '../../internal/components/Spinner'; -import { errorSvg } from '../../internal/svg/errorSvg'; -import { successSvg } from '../../internal/svg/successSvg'; +import { useShowCallsStatus } from 'wagmi/experimental'; import { getChainExplorer } from '../../network/getChainExplorer'; import { cn, color, text } from '../../styles/theme'; import { useTransactionContext } from '../components/TransactionProvider'; -export function useGetTransactionToast() { - const { - chainId, - errorMessage, - isLoading, - onSubmit, - receipt, - transactionHash, - transactionId, - } = useTransactionContext(); +export function useGetTransactionToastAction() { + const { chainId, errorMessage, onSubmit, transactionHash, transactionId } = + useTransactionContext(); const accountChainId = chainId ?? useChainId(); - const isInProgress = isLoading || !!transactionId || !!transactionHash; + const { showCallsStatus } = useShowCallsStatus(); return useMemo(() => { const chainExplorer = getChainExplorer(accountChainId); let actionElement: ReactNode = null; - let label = ''; - let icon: ReactNode = null; - if (isInProgress) { - icon = ; - label = 'Transaction in progress'; - } + // EOA will have txn hash if (transactionHash) { actionElement = ( ); } - if (receipt) { - icon = successSvg; - label = 'Successful'; + + // SW will have txn id + if (transactionId) { + actionElement = ( + + ); } + if (errorMessage) { actionElement = ( ); - icon = errorSvg; - label = 'Something went wrong'; } - return { actionElement, icon, label }; + return { actionElement }; }, [ accountChainId, errorMessage, - isInProgress, onSubmit, - receipt, + showCallsStatus, transactionHash, + transactionId, ]); } diff --git a/src/transaction/hooks/useGetTransactionToastLabel.test.tsx b/src/transaction/hooks/useGetTransactionToastLabel.test.tsx new file mode 100644 index 0000000000..49b761b7fd --- /dev/null +++ b/src/transaction/hooks/useGetTransactionToastLabel.test.tsx @@ -0,0 +1,101 @@ +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useChainId } from 'wagmi'; +import { useShowCallsStatus } from 'wagmi/experimental'; +import { getChainExplorer } from '../../network/getChainExplorer'; +import { useTransactionContext } from '../components/TransactionProvider'; +import { useGetTransactionToastLabel } from './useGetTransactionToastLabel'; + +vi.mock('../components/TransactionProvider', () => ({ + useTransactionContext: vi.fn(), +})); + +vi.mock('wagmi', () => ({ + useChainId: vi.fn(), +})); + +vi.mock('wagmi/experimental', () => ({ + useShowCallsStatus: vi.fn(), +})); + +vi.mock('../../network/getChainExplorer', () => ({ + getChainExplorer: vi.fn(), +})); + +const mockGetChainExplorer = 'https://etherscan.io'; + +describe('useGetTransactionToastLabel', () => { + beforeEach(() => { + (useChainId as vi.Mock).mockReturnValue(123); + (useShowCallsStatus as vi.Mock).mockReturnValue({ + showCallsStatus: vi.fn(), + }); + (getChainExplorer as vi.Mock).mockReturnValue(mockGetChainExplorer); + }); + it('should return correct toast and actionElement when transaction is loading', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + isLoading: true, + }); + + const { result } = renderHook(() => useGetTransactionToastLabel()); + + expect(result.current.label).toBe('Transaction in progress'); + }); + + it('should return status when transaction hash exists', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + transactionHash: '0x123', + }); + + const { result } = renderHook(() => useGetTransactionToastLabel()); + + expect(result.current.label).toBe('Transaction in progress'); + }); + + it('should return status when transaction id exists', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + transactionId: 'ab123', + onSubmit: vi.fn(), + }); + + const showCallsStatus = vi.fn(); + (useShowCallsStatus as vi.Mock).mockReturnValue({ showCallsStatus }); + + const { result } = renderHook(() => useGetTransactionToastLabel()); + + expect(result.current.label).toBe('Transaction in progress'); + }); + + it('should return status when receipt exists', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + receipt: 'receipt', + transactionHash: '0x123', + }); + + const { result } = renderHook(() => useGetTransactionToastLabel()); + + expect(result.current.label).toBe('Successful'); + }); + + it('should return status when error occurs', () => { + const onSubmitMock = vi.fn(); + (useTransactionContext as vi.Mock).mockReturnValue({ + errorMessage: 'error', + onSubmit: onSubmitMock, + }); + + const { result } = renderHook(() => useGetTransactionToastLabel()); + + expect(result.current.label).toBe('Something went wrong'); + }); + + it('should return status when no status available', () => { + (useTransactionContext as vi.Mock).mockReturnValue({ + errorMessage: '', + }); + + const { result } = renderHook(() => useGetTransactionToastLabel()); + + expect(result.current.label).toBe(''); + }); +}); diff --git a/src/transaction/hooks/useGetTransactionToastLabel.tsx b/src/transaction/hooks/useGetTransactionToastLabel.tsx new file mode 100644 index 0000000000..1d16c00326 --- /dev/null +++ b/src/transaction/hooks/useGetTransactionToastLabel.tsx @@ -0,0 +1,31 @@ +import { useMemo } from 'react'; +import { color } from '../../styles/theme'; +import { useTransactionContext } from '../components/TransactionProvider'; + +export function useGetTransactionToastLabel() { + const { errorMessage, isLoading, receipt, transactionHash, transactionId } = + useTransactionContext(); + + // user confirmed in wallet, txn in progress + const isInProgress = isLoading || !!transactionId || !!transactionHash; + + return useMemo(() => { + let label = ''; + let labelClassName: string = color.foregroundMuted; + + if (isInProgress) { + label = 'Transaction in progress'; + } + + if (receipt) { + label = 'Successful'; + } + + if (errorMessage) { + label = 'Something went wrong'; + labelClassName = color.error; + } + + return { label, labelClassName }; + }, [errorMessage, isInProgress, receipt]); +}