diff --git a/src/components/dashboard/RecoveryHeader/index.tsx b/src/components/dashboard/RecoveryHeader/index.tsx index 6775d910c3..b2005b8e39 100644 --- a/src/components/dashboard/RecoveryHeader/index.tsx +++ b/src/components/dashboard/RecoveryHeader/index.tsx @@ -6,8 +6,8 @@ import { useIsGuardian } from '@/hooks/useIsGuardian' import madProps from '@/utils/mad-props' import { FEATURES } from '@/utils/chains' import { useHasFeature } from '@/hooks/useChains' -import { RecoveryProposal } from '@/components/recovery/RecoveryModal/RecoveryProposal' -import { RecoveryInProgress } from '@/components/recovery/RecoveryModal/RecoveryInProgress' +import { RecoveryProposalCard } from '@/components/recovery/RecoveryCards/RecoveryProposalCard' +import { RecoveryInProgressCard } from '@/components/recovery/RecoveryCards/RecoveryInProgressCard' import { WidgetContainer, WidgetBody } from '../styled' import type { RecoveryQueueItem } from '@/store/recoverySlice' @@ -28,9 +28,9 @@ export function _RecoveryHeader({ } const modal = next ? ( - + ) : isGuardian ? ( - + ) : null if (modal) { diff --git a/src/components/recovery/RecoveryModal/RecoveryInProgress.tsx b/src/components/recovery/RecoveryCards/RecoveryInProgressCard.tsx similarity index 92% rename from src/components/recovery/RecoveryModal/RecoveryInProgress.tsx rename to src/components/recovery/RecoveryCards/RecoveryInProgressCard.tsx index 95fcf6fdc3..2598a8b222 100644 --- a/src/components/recovery/RecoveryModal/RecoveryInProgress.tsx +++ b/src/components/recovery/RecoveryCards/RecoveryInProgressCard.tsx @@ -13,17 +13,17 @@ import css from './styles.module.css' type Props = | { - variant?: 'modal' + orientation?: 'vertical' onClose: () => void recovery: RecoveryQueueItem } | { - variant: 'widget' + orientation: 'horizontal' onClose?: never recovery: RecoveryQueueItem } -export function RecoveryInProgress({ variant = 'modal', onClose, recovery }: Props): ReactElement { +export function RecoveryInProgressCard({ orientation = 'vertical', onClose, recovery }: Props): ReactElement { const { isExecutable, remainingSeconds } = useRecoveryTxState(recovery) const router = useRouter() @@ -50,7 +50,7 @@ export function RecoveryInProgress({ variant = 'modal', onClose, recovery }: Pro ) - if (variant === 'widget') { + if (orientation === 'horizontal') { return ( diff --git a/src/components/recovery/RecoveryModal/RecoveryProposal.tsx b/src/components/recovery/RecoveryCards/RecoveryProposalCard.tsx similarity index 85% rename from src/components/recovery/RecoveryModal/RecoveryProposal.tsx rename to src/components/recovery/RecoveryCards/RecoveryProposalCard.tsx index 08ebb3713e..3ace2208aa 100644 --- a/src/components/recovery/RecoveryModal/RecoveryProposal.tsx +++ b/src/components/recovery/RecoveryCards/RecoveryProposalCard.tsx @@ -15,19 +15,19 @@ import css from './styles.module.css' type Props = | { - variant?: 'modal' + orientation?: 'vertical' onClose: () => void safe: SafeInfo setTxFlow: TxModalContextType['setTxFlow'] } | { - variant: 'widget' + orientation: 'horizontal' onClose?: never safe: SafeInfo setTxFlow: TxModalContextType['setTxFlow'] } -export function _RecoveryProposal({ variant = 'modal', onClose, safe, setTxFlow }: Props): ReactElement { +export function _RecoveryProposalCard({ orientation = 'vertical', onClose, safe, setTxFlow }: Props): ReactElement { const onRecover = async () => { onClose?.() setTxFlow() @@ -49,19 +49,19 @@ export function _RecoveryProposal({ variant = 'modal', onClose, safe, setTxFlow ) const recoveryButton = ( - ) - if (variant === 'widget') { + if (orientation === 'horizontal') { return ( {icon} - + {title} @@ -88,7 +88,7 @@ export function _RecoveryProposal({ variant = 'modal', onClose, safe, setTxFlow - + {title} @@ -112,7 +112,7 @@ export function _RecoveryProposal({ variant = 'modal', onClose, safe, setTxFlow const _useSafe = () => useSafeInfo().safe const _useSetTxFlow = () => useContext(TxModalContext).setTxFlow -export const RecoveryProposal = madProps(_RecoveryProposal, { +export const RecoveryProposalCard = madProps(_RecoveryProposalCard, { safe: _useSafe, setTxFlow: _useSetTxFlow, }) diff --git a/src/components/recovery/RecoveryModal/__tests__/RecoveryInProgress.test.tsx b/src/components/recovery/RecoveryCards/__tests__/RecoveryInProgressCard.test.tsx similarity index 85% rename from src/components/recovery/RecoveryModal/__tests__/RecoveryInProgress.test.tsx rename to src/components/recovery/RecoveryCards/__tests__/RecoveryInProgressCard.test.tsx index 8b45428347..91b0d95cbe 100644 --- a/src/components/recovery/RecoveryModal/__tests__/RecoveryInProgress.test.tsx +++ b/src/components/recovery/RecoveryCards/__tests__/RecoveryInProgressCard.test.tsx @@ -2,7 +2,7 @@ import { BigNumber } from 'ethers' import { fireEvent, waitFor } from '@testing-library/react' import { render } from '@/tests/test-utils' -import { RecoveryInProgress } from '../RecoveryInProgress' +import { RecoveryInProgressCard } from '../RecoveryInProgressCard' import { useRecoveryTxState } from '@/hooks/useRecoveryTxState' import type { RecoveryQueueItem } from '@/store/recoverySlice' @@ -10,12 +10,12 @@ jest.mock('@/hooks/useRecoveryTxState') const mockUseRecoveryTxState = useRecoveryTxState as jest.MockedFunction -describe('RecoveryInProgress', () => { +describe('RecoveryInProgressCard', () => { beforeEach(() => { jest.clearAllMocks() }) - describe('modal', () => { + describe('vertical', () => { it('should render executable recovery state correctly', async () => { mockUseRecoveryTxState.mockReturnValue({ isExecutable: true, @@ -25,8 +25,8 @@ describe('RecoveryInProgress', () => { const mockClose = jest.fn() const { queryByText } = render( - , @@ -58,8 +58,8 @@ describe('RecoveryInProgress', () => { const mockClose = jest.fn() const { queryByText } = render( - , @@ -82,7 +82,7 @@ describe('RecoveryInProgress', () => { }) }) }) - describe('widget', () => { + describe('horizontal', () => { it('should render executable recovery state correctly', () => { mockUseRecoveryTxState.mockReturnValue({ isExecutable: true, @@ -90,7 +90,10 @@ describe('RecoveryInProgress', () => { } as any) const { queryByText } = render( - , + , ) ;['days', 'hrs', 'mins'].forEach((unit) => { @@ -109,7 +112,10 @@ describe('RecoveryInProgress', () => { } as any) const { queryByText } = render( - , + , ) expect(queryByText('Go to dashboard')).toBeFalsy() diff --git a/src/components/recovery/RecoveryModal/__tests__/RecoveryProposal.test.tsx b/src/components/recovery/RecoveryCards/__tests__/RecoveryProposalCard.test.tsx similarity index 85% rename from src/components/recovery/RecoveryModal/__tests__/RecoveryProposal.test.tsx rename to src/components/recovery/RecoveryCards/__tests__/RecoveryProposalCard.test.tsx index f5184c0507..8ca5df4bed 100644 --- a/src/components/recovery/RecoveryModal/__tests__/RecoveryProposal.test.tsx +++ b/src/components/recovery/RecoveryCards/__tests__/RecoveryProposalCard.test.tsx @@ -2,17 +2,17 @@ import { faker } from '@faker-js/faker' import type { SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' import { fireEvent, render } from '@/tests/test-utils' -import { _RecoveryProposal } from '../RecoveryProposal' +import { _RecoveryProposalCard } from '../RecoveryProposalCard' -describe('RecoveryProposal', () => { - describe('modal', () => { +describe('RecoveryProposalCard', () => { + describe('vertical', () => { it('should render correctly', () => { const mockClose = jest.fn() const mockSetTxFlow = jest.fn() const { queryByText } = render( - <_RecoveryProposal - variant="modal" + <_RecoveryProposalCard + orientation="vertical" onClose={mockClose} safe={{ owners: [{ value: faker.finance.ethereumAddress() }] } as SafeInfo} setTxFlow={mockSetTxFlow} @@ -36,13 +36,13 @@ describe('RecoveryProposal', () => { expect(mockSetTxFlow).toHaveBeenCalled() }) }) - describe('widget', () => {}) + describe('horizontal', () => {}) it('should render correctly', () => { const mockSetTxFlow = jest.fn() const { queryByText } = render( - <_RecoveryProposal - variant="widget" + <_RecoveryProposalCard + orientation="horizontal" safe={{ owners: [{ value: faker.finance.ethereumAddress() }] } as SafeInfo} setTxFlow={mockSetTxFlow} />, diff --git a/src/components/recovery/RecoveryModal/styles.module.css b/src/components/recovery/RecoveryCards/styles.module.css similarity index 63% rename from src/components/recovery/RecoveryModal/styles.module.css rename to src/components/recovery/RecoveryCards/styles.module.css index ce22c8fe25..6aa366375a 100644 --- a/src/components/recovery/RecoveryModal/styles.module.css +++ b/src/components/recovery/RecoveryCards/styles.module.css @@ -1,8 +1,3 @@ -.backdrop { - z-index: 3; - background-color: var(--color-background-main); -} - .card { max-width: 576px; padding: var(--space-4); diff --git a/src/components/recovery/RecoveryModal/__tests__/index.test.tsx b/src/components/recovery/RecoveryModal/__tests__/index.test.tsx deleted file mode 100644 index 2d1fff928f..0000000000 --- a/src/components/recovery/RecoveryModal/__tests__/index.test.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { BigNumber } from 'ethers' -import * as router from 'next/router' - -import { render, waitFor } from '@/tests/test-utils' -import { _RecoveryModal } from '..' -import type { RecoveryQueueItem } from '@/store/recoverySlice' - -describe('RecoveryModal', () => { - it('should not render the modal if there is a queue but the user is not an owner or guardian', () => { - const { queryByText } = render( - <_RecoveryModal - isOwner={false} - isGuardian={false} - queue={[{ validFrom: BigNumber.from(0) } as RecoveryQueueItem]} - > - Test - , - ) - - expect(queryByText('Test')).toBeTruthy() - expect(queryByText('recovery')).toBeFalsy() - }) - - it('should not render the modal if there is no queue user and the user is a guardian', () => { - const { queryByText } = render( - <_RecoveryModal isOwner={false} isGuardian queue={[]}> - Test - , - ) - - expect(queryByText('Test')).toBeTruthy() - expect(queryByText('recovery')).toBeFalsy() - }) - - it('should not render the modal if there is no queue user and the user is an owner', () => { - const { queryByText } = render( - <_RecoveryModal isOwner isGuardian={false} queue={[]}> - Test - , - ) - - expect(queryByText('Test')).toBeTruthy() - expect(queryByText('recovery')).toBeFalsy() - }) - - it('should render the in-progress modal when there is a queue for guardians', () => { - const { queryByText } = render( - <_RecoveryModal isOwner={false} isGuardian queue={[{ validFrom: BigNumber.from(0) } as RecoveryQueueItem]}> - Test - , - ) - - expect(queryByText('Test')).toBeTruthy() - expect(queryByText('Account recovery in progress')).toBeTruthy() - }) - - it('should render the in-progress modal when there is a queue for owners', () => { - const { queryByText } = render( - <_RecoveryModal isOwner isGuardian={false} queue={[{ validFrom: BigNumber.from(0) } as RecoveryQueueItem]}> - Test - , - ) - - expect(queryByText('Test')).toBeTruthy() - expect(queryByText('Account recovery in progress')).toBeTruthy() - }) - - it('should render the proposal modal when there is no queue for guardians', () => { - const { queryByText } = render( - <_RecoveryModal isOwner={false} isGuardian queue={[]}> - Test - , - ) - - expect(queryByText('Test')).toBeTruthy() - expect(queryByText('Recover this Account')).toBeTruthy() - }) - - it('should close the modal when the user navigates away', async () => { - let onClose = () => {} - - jest.spyOn(router, 'useRouter').mockImplementation( - () => - ({ - events: { - on: jest.fn((_, callback) => { - onClose = callback - }), - }, - } as any), - ) - - const { queryByText } = render( - <_RecoveryModal isOwner isGuardian={false} queue={[{ validFrom: BigNumber.from(0) } as RecoveryQueueItem]}> - Test - , - ) - - expect(queryByText('Test')).toBeTruthy() - expect(queryByText('Account recovery in progress')).toBeTruthy() - - onClose() - - await waitFor(() => { - expect(queryByText('Account recovery in progress')).toBeFalsy() - }) - }) -}) diff --git a/src/components/recovery/RecoveryModal/index.test.tsx b/src/components/recovery/RecoveryModal/index.test.tsx new file mode 100644 index 0000000000..5e8c2a0e95 --- /dev/null +++ b/src/components/recovery/RecoveryModal/index.test.tsx @@ -0,0 +1,252 @@ +import { BigNumber } from 'ethers' +import { faker } from '@faker-js/faker' +import { renderHook } from '@testing-library/react' +import * as router from 'next/router' + +import { render, waitFor } from '@/tests/test-utils' +import { safeInfoBuilder } from '@/tests/builders/safe' +import { connectedWalletBuilder } from '@/tests/builders/wallet' +import * as safeInfo from '@/hooks/useSafeInfo' +import { _useDidDismissProposal } from './index' +import type { RecoveryQueueItem } from '@/store/recoverySlice' + +describe('RecoveryModal', () => { + describe('component', () => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + let _RecoveryModal: typeof import('./index')._RecoveryModal + + beforeEach(() => { + localStorage.clear() + + // Clear cache in between tests + _RecoveryModal = require('./index')._RecoveryModal + }) + + it('should not render the modal if there is a queue but the user is not an owner or guardian', () => { + const wallet = connectedWalletBuilder().build() + const queue = [{ validFrom: BigNumber.from(0) } as RecoveryQueueItem] + + const { queryByText } = render( + <_RecoveryModal wallet={wallet} isOwner={false} isGuardian={false} queue={queue}> + Test + , + ) + + expect(queryByText('Test')).toBeTruthy() + expect(queryByText('recovery')).toBeFalsy() + }) + + it('should not render the modal if there is no queue and the user is a guardian', () => { + const wallet = connectedWalletBuilder().build() + const queue = [] as Array + + const { queryByText } = render( + <_RecoveryModal wallet={wallet} isOwner={false} isGuardian queue={queue}> + Test + , + ) + + expect(queryByText('Test')).toBeTruthy() + expect(queryByText('recovery')).toBeFalsy() + }) + + it('should not render the modal if there is no queue and the user is an owner', () => { + const wallet = connectedWalletBuilder().build() + const queue = [] as Array + + const { queryByText } = render( + <_RecoveryModal wallet={wallet} isOwner isGuardian={false} queue={queue}> + Test + , + ) + + expect(queryByText('Test')).toBeTruthy() + expect(queryByText('recovery')).toBeFalsy() + }) + + it('should render the in-progress modal when there is a queue for guardians', () => { + const wallet = connectedWalletBuilder().build() + const queue = [{ validFrom: BigNumber.from(0) } as RecoveryQueueItem] + + const { queryByText } = render( + <_RecoveryModal wallet={wallet} isOwner={false} isGuardian queue={queue}> + Test + , + ) + + expect(queryByText('Test')).toBeTruthy() + expect(queryByText('Account recovery in progress')).toBeTruthy() + }) + + it('should render the in-progress modal when there is a queue for owners', () => { + const wallet = connectedWalletBuilder().build() + const queue = [{ validFrom: BigNumber.from(0) } as RecoveryQueueItem] + + const { queryByText } = render( + <_RecoveryModal wallet={wallet} isOwner isGuardian={false} queue={queue}> + Test + , + ) + + expect(queryByText('Test')).toBeTruthy() + expect(queryByText('Account recovery in progress')).toBeTruthy() + }) + + it('should render the proposal modal when there is no queue for guardians', () => { + const wallet = connectedWalletBuilder().build() + const queue = [] as Array + + const { queryByText } = render( + <_RecoveryModal wallet={wallet} isOwner={false} isGuardian queue={queue}> + Test + , + ) + + expect(queryByText('Test')).toBeTruthy() + expect(queryByText('Recover this Account')).toBeTruthy() + }) + + it('should not render the proposal modal when there is no queue for owners', () => { + const wallet = connectedWalletBuilder().build() + const queue = [] as Array + + const { queryByText } = render( + <_RecoveryModal wallet={wallet} isOwner isGuardian={false} queue={queue}> + Test + , + ) + + expect(queryByText('Test')).toBeTruthy() + expect(queryByText('Recover this Account')).toBeFalsy() + }) + + it('should close the modal when the user navigates away', async () => { + const mockUseRouter = { + push: jest.fn(), + query: {}, + events: { + on: jest.fn(), + off: jest.fn(), + }, + } + + jest.spyOn(router, 'useRouter').mockReturnValue(mockUseRouter as any) + + const wallet = connectedWalletBuilder().build() + const queue = [{ validFrom: BigNumber.from(0), transactionHash: faker.string.hexadecimal() } as RecoveryQueueItem] + + const { queryByText } = render( + <_RecoveryModal wallet={wallet} isOwner isGuardian={false} queue={queue}> + Test + , + ) + + expect(queryByText('Test')).toBeTruthy() + expect(queryByText('Account recovery in progress')).toBeTruthy() + + // Trigger the route change + mockUseRouter.events.on.mock.calls[0][1]() + + await waitFor(() => { + expect(queryByText('Account recovery in progress')).toBeFalsy() + }) + }) + }) + + describe('hooks', () => { + beforeEach(() => { + localStorage.clear() + + const safe = safeInfoBuilder().build() + jest + .spyOn(safeInfo, 'default') + .mockReturnValue({ safe, safeAddress: safe.address.value } as ReturnType) + }) + + describe('useDidDismissProposal', () => { + it('should return false if the proposal was not dismissed before', () => { + const guardianAddress = faker.finance.ethereumAddress() + + const { result } = renderHook(() => _useDidDismissProposal()) + + expect(result.current.wasProposalDismissed(guardianAddress)).toBeFalsy() + }) + + it('should return true if the proposal was dismissed before', () => { + const guardianAddress = faker.finance.ethereumAddress() + + const { result, rerender } = renderHook(() => _useDidDismissProposal()) + + expect(result.current.wasProposalDismissed(guardianAddress)).toBeFalsy() + result.current.dismissProposal(guardianAddress) + + rerender() + + expect(result.current.wasProposalDismissed(guardianAddress)).toBeTruthy() + }) + + it('should persist dismissals between sessions', () => { + const guardianAddress = faker.finance.ethereumAddress() + + const firstRender = renderHook(() => _useDidDismissProposal()) + + expect(firstRender.result.current.wasProposalDismissed(guardianAddress)).toBeFalsy() + firstRender.result.current.dismissProposal(guardianAddress) + + firstRender.rerender() + + expect(firstRender.result.current.wasProposalDismissed(guardianAddress)).toBeTruthy() + + firstRender.unmount() + + const secondRender = renderHook(() => _useDidDismissProposal()) + expect(secondRender.result.current.wasProposalDismissed(guardianAddress)).toBeTruthy() + }) + }) + + describe('useDidDismissInProgress', () => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + let _useDidDismissInProgress: typeof import('./index')._useDidDismissInProgress + + beforeEach(() => { + localStorage.clear() + + // Clear cache in between tests + _useDidDismissInProgress = require('./index')._useDidDismissInProgress + }) + + it('should return false if in-progress was not dismissed before', () => { + const guardianAddress = faker.finance.ethereumAddress() + + const { result } = renderHook(() => _useDidDismissInProgress()) + + expect(result.current.wasInProgressDismissed(guardianAddress)).toBeFalsy() + }) + + it('should return true if in-progress was not dismissed before', () => { + const guardianAddress = faker.finance.ethereumAddress() + + const { result } = renderHook(() => _useDidDismissInProgress()) + + expect(result.current.wasInProgressDismissed(guardianAddress)).toBeFalsy() + result.current.dismissInProgress(guardianAddress) + expect(result.current.wasInProgressDismissed(guardianAddress)).toBeTruthy() + }) + + it('should not persist dismissals between sessions', () => { + const guardianAddress = faker.finance.ethereumAddress() + + const firstRender = renderHook(() => _useDidDismissInProgress()) + + expect(firstRender.result.current.wasInProgressDismissed(guardianAddress)).toBeFalsy() + firstRender.result.current.dismissInProgress(guardianAddress) + expect(firstRender.result.current.wasInProgressDismissed(guardianAddress)).toBeTruthy() + + firstRender.unmount() + + const secondRender = renderHook(() => _useDidDismissInProgress()) + expect(secondRender.result.current.wasInProgressDismissed(guardianAddress)).toBeFalsy() + }) + }) + }) +}) diff --git a/src/components/recovery/RecoveryModal/index.tsx b/src/components/recovery/RecoveryModal/index.tsx index 78b76f5271..703865d947 100644 --- a/src/components/recovery/RecoveryModal/index.tsx +++ b/src/components/recovery/RecoveryModal/index.tsx @@ -1,34 +1,42 @@ import { Backdrop, Fade } from '@mui/material' -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { useRouter } from 'next/router' import type { ReactElement, ReactNode } from 'react' import { useRecoveryQueue } from '@/hooks/useRecoveryQueue' -import { RecoveryInProgress } from './RecoveryInProgress' -import { RecoveryProposal } from './RecoveryProposal' +import { RecoveryInProgressCard } from '../RecoveryCards/RecoveryInProgressCard' +import { RecoveryProposalCard } from '../RecoveryCards/RecoveryProposalCard' import useIsSafeOwner from '@/hooks/useIsSafeOwner' import { useIsGuardian } from '@/hooks/useIsGuardian' import madProps from '@/utils/mad-props' +import useLocalStorage from '@/services/local-storage/useLocalStorage' +import useWallet from '@/hooks/wallets/useWallet' +import useSafeInfo from '@/hooks/useSafeInfo' +import { sameAddress } from '@/utils/addresses' import type { RecoveryQueueItem } from '@/store/recoverySlice' -import css from './styles.module.css' - export function _RecoveryModal({ children, isOwner, isGuardian, queue, + wallet, }: { children: ReactNode isOwner: boolean isGuardian: boolean queue: Array + wallet: ReturnType }): ReactElement { + const { wasProposalDismissed, dismissProposal } = _useDidDismissProposal() + const { wasInProgressDismissed, dismissInProgress } = _useDidDismissInProgress() + const [modal, setModal] = useState(null) const router = useRouter() const next = queue[0] + // Close modal const onClose = () => { setModal(null) } @@ -36,15 +44,37 @@ export function _RecoveryModal({ // Trigger modal useEffect(() => { setModal(() => { - if (next) { - return + if (next && !wasInProgressDismissed(next.transactionHash)) { + const onCloseWithDismiss = () => { + dismissInProgress(next.transactionHash) + onClose() + } + + return } - if (isGuardian && queue.length === 0) { - return + + if (wallet?.address && !isOwner && !wasProposalDismissed(wallet.address)) { + const onCloseWithDismiss = () => { + dismissProposal(wallet.address) + onClose() + } + + return } + return null }) - }, [queue.length, isOwner, isGuardian, next]) + }, [ + dismissInProgress, + dismissProposal, + isGuardian, + isOwner, + next, + queue.length, + wallet, + wasInProgressDismissed, + wasProposalDismissed, + ]) // Close modal on navigation useEffect(() => { @@ -57,7 +87,7 @@ export function _RecoveryModal({ return ( <> - + palette.background.main }}> {modal} @@ -70,4 +100,79 @@ export const RecoveryModal = madProps(_RecoveryModal, { isOwner: useIsSafeOwner, isGuardian: useIsGuardian, queue: useRecoveryQueue, + wallet: useWallet, }) + +export function _useDidDismissProposal() { + const LS_KEY = 'dismissedRecoveryProposals' + + type Guardian = string + type DismissedProposalCache = { [chainId: string]: { [safeAddress: string]: Guardian } } + + const { safe, safeAddress } = useSafeInfo() + const chainId = safe.chainId + + const [dismissedProposals, setDismissedProposals] = useLocalStorage(LS_KEY) + + // Cache dismissal of proposal modal + const dismissProposal = useCallback( + (guardianAddress: string) => { + const dismissed = dismissedProposals?.[chainId] ?? {} + + setDismissedProposals({ + ...(dismissedProposals ?? {}), + [chainId]: { + ...dismissed, + [safeAddress]: guardianAddress, + }, + }) + }, + [dismissedProposals, chainId, safeAddress, setDismissedProposals], + ) + + const wasProposalDismissed = useCallback( + (guardianAddress: string) => { + // If no proposals, is guardian and didn't ever dismiss + return sameAddress(dismissedProposals?.[chainId]?.[safeAddress], guardianAddress) + }, + [chainId, dismissedProposals, safeAddress], + ) + + return { wasProposalDismissed, dismissProposal } +} + +export function _useDidDismissInProgress() { + type TxHash = string + type DismissedInProgressCache = { [chainId: string]: { [safeAddress: string]: TxHash } } + + const { safe, safeAddress } = useSafeInfo() + const chainId = safe.chainId + + const dismissedInProgress = useRef({}) + + // Cache dismissal of in-progress modal + const dismissInProgress = useCallback( + (txHash: string) => { + const dismissed = dismissedInProgress.current?.[chainId] ?? {} + + dismissedInProgress.current = { + ...dismissedInProgress.current, + [chainId]: { + ...dismissed, + [safeAddress]: txHash, + }, + } + }, + [chainId, safeAddress], + ) + + const wasInProgressDismissed = useCallback( + (txHash: string) => { + // If proposal and did not notify during current session of Safe + return sameAddress(txHash, dismissedInProgress.current?.[chainId]?.[safeAddress]) + }, + [chainId, safeAddress], + ) + + return { wasInProgressDismissed, dismissInProgress } +}