diff --git a/.github/workflows/yarn/action.yml b/.github/workflows/yarn/action.yml index 4d3112fa22..bff04d3a30 100644 --- a/.github/workflows/yarn/action.yml +++ b/.github/workflows/yarn/action.yml @@ -13,3 +13,6 @@ runs: - name: Yarn install shell: bash run: yarn install --frozen-lockfile + - name: Yarn after install + shell: bash + run: yarn after-install diff --git a/package.json b/package.json index 6dad58ec26..714c27f151 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "routes": "node scripts/generate-routes.js > src/config/routes.ts && prettier -w src/config/routes.ts && cat src/config/routes.ts", "css-vars": "ts-node-esm ./scripts/css-vars.ts > ./src/styles/vars.css && prettier -w src/styles/vars.css", "generate-types": "typechain --target ethers-v5 --out-dir src/types/contracts ./node_modules/@safe-global/safe-deployments/dist/assets/**/*.json ./node_modules/@safe-global/safe-modules-deployments/dist/assets/**/*.json ./node_modules/@openzeppelin/contracts/build/contracts/ERC20.json ./node_modules/@openzeppelin/contracts/build/contracts/ERC721.json", - "postinstall": "yarn patch-package && yarn generate-types && yarn css-vars", + "after-install": "yarn patch-package && yarn generate-types && yarn css-vars", + "postinstall": "yarn after-install", "analyze": "cross-env ANALYZE=true yarn build", "cypress:open": "cross-env TZ=UTC cypress open --e2e", "cypress:canary": "cross-env TZ=UTC cypress open --e2e -b chrome:canary", diff --git a/src/components/safe-messages/MsgSummary/index.tsx b/src/components/safe-messages/MsgSummary/index.tsx index 21651cd01b..87fe2b9752 100644 --- a/src/components/safe-messages/MsgSummary/index.tsx +++ b/src/components/safe-messages/MsgSummary/index.tsx @@ -34,7 +34,7 @@ const MsgSummary = ({ msg }: { msg: SafeMessage }): ReactElement => { const isConfirmed = msg.status === SafeMessageStatus.CONFIRMED return ( - + diff --git a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx index 7ef79e8f6f..55feb8c9c7 100644 --- a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -11,6 +11,7 @@ import { ListItemButton, ListItemIcon, ListItemText, + CircularProgress, } from '@mui/material' import { Fragment, useEffect, useMemo, useState } from 'react' import type { ReactElement } from 'react' @@ -44,7 +45,9 @@ export const transformAddedSafes = (addedSafes: AddedSafesState): NotifiableSafe } // Convert data structure of currently notified Safes -const transformCurrentSubscribedSafes = (allPreferences?: PushNotificationPreferences): NotifiableSafes | undefined => { +export const _transformCurrentSubscribedSafes = ( + allPreferences?: PushNotificationPreferences, +): NotifiableSafes | undefined => { if (!allPreferences) { return } @@ -60,7 +63,10 @@ const transformCurrentSubscribedSafes = (allPreferences?: PushNotificationPrefer } // Merges added Safes and currently notified Safes into a single data structure without duplicates -const mergeNotifiableSafes = (addedSafes: AddedSafesState, currentSubscriptions?: NotifiableSafes): NotifiableSafes => { +export const _mergeNotifiableSafes = ( + addedSafes: AddedSafesState, + currentSubscriptions?: NotifiableSafes, +): NotifiableSafes => { const notifiableSafes = transformAddedSafes(addedSafes) if (!currentSubscriptions) { @@ -78,13 +84,19 @@ const mergeNotifiableSafes = (addedSafes: AddedSafesState, currentSubscriptions? return notifiableSafes } -const getTotalNotifiableSafes = (notifiableSafes: NotifiableSafes): number => { +export const _getTotalNotifiableSafes = (notifiableSafes: NotifiableSafes): number => { return Object.values(notifiableSafes).reduce((acc, safeAddresses) => { return (acc += safeAddresses.length) }, 0) } -const areAllSafesSelected = (notifiableSafes: NotifiableSafes, selectedSafes: NotifiableSafes): boolean => { +export const _areAllSafesSelected = (notifiableSafes: NotifiableSafes, selectedSafes: NotifiableSafes): boolean => { + const entries = Object.entries(notifiableSafes) + + if (entries.length === 0) { + return false + } + return Object.entries(notifiableSafes).every(([chainId, safeAddresses]) => { const hasChain = Object.keys(selectedSafes).includes(chainId) const hasEverySafe = safeAddresses?.every((safeAddress) => selectedSafes[chainId]?.includes(safeAddress)) @@ -93,13 +105,24 @@ const areAllSafesSelected = (notifiableSafes: NotifiableSafes, selectedSafes: No } // Total number of signatures required to register selected Safes -const getTotalSignaturesRequired = (selectedSafes: NotifiableSafes, currentNotifiedSafes?: NotifiableSafes): number => { - return Object.keys(selectedSafes).filter((chainId) => { - return !Object.keys(currentNotifiedSafes || {}).includes(chainId) - }).length +export const _getTotalSignaturesRequired = ( + selectedSafes: NotifiableSafes, + currentNotifiedSafes?: NotifiableSafes, +): number => { + return Object.entries(selectedSafes) + .filter(([, safeAddresses]) => safeAddresses.length > 0) + .reduce((acc, [chainId, safeAddresses]) => { + const isNewChain = !currentNotifiedSafes?.[chainId] + const isNewSafe = safeAddresses.some((safeAddress) => !currentNotifiedSafes?.[chainId]?.includes(safeAddress)) + + if (isNewChain || isNewSafe) { + acc += 1 + } + return acc + }, 0) } -const shouldRegisterSelectedSafes = ( +export const _shouldRegisterSelectedSafes = ( selectedSafes: NotifiableSafes, currentNotifiedSafes?: NotifiableSafes, ): boolean => { @@ -108,7 +131,10 @@ const shouldRegisterSelectedSafes = ( }) } -const shouldUnregsiterSelectedSafes = (selectedSafes: NotifiableSafes, currentNotifiedSafes?: NotifiableSafes) => { +export const _shouldUnregsiterSelectedSafes = ( + selectedSafes: NotifiableSafes, + currentNotifiedSafes?: NotifiableSafes, +) => { return Object.entries(currentNotifiedSafes || {}).some(([chainId, safeAddresses]) => { return safeAddresses.some((safeAddress) => !selectedSafes[chainId]?.includes(safeAddress)) }) @@ -117,7 +143,7 @@ const shouldUnregsiterSelectedSafes = (selectedSafes: NotifiableSafes, currentNo // onSave logic // Safes that need to be registered with the service -const getSafesToRegister = ( +export const _getSafesToRegister = ( selectedSafes: NotifiableSafes, currentNotifiedSafes?: NotifiableSafes, ): NotifiableSafes | undefined => { @@ -141,7 +167,7 @@ const getSafesToRegister = ( } // Safes that need to be unregistered with the service -const getSafesToUnregister = ( +export const _getSafesToUnregister = ( selectedSafes: NotifiableSafes, currentNotifiedSafes?: NotifiableSafes, ): NotifiableSafes | undefined => { @@ -171,7 +197,7 @@ const getSafesToUnregister = ( } // Whether the device needs to be unregistered from the service -const shouldUnregisterDevice = ( +export const _shouldUnregisterDevice = ( chainId: string, safeAddresses: Array, currentNotifiedSafes?: NotifiableSafes, @@ -192,6 +218,7 @@ const shouldUnregisterDevice = ( export const GlobalPushNotifications = (): ReactElement | null => { const chains = useChains() const addedSafes = useAppSelector(selectAllAddedSafes) + const [isLoading, setIsLoading] = useState(false) const { dismissPushNotificationBanner } = useDismissPushNotificationsBanner() const { getAllPreferences } = useNotificationPreferences() @@ -204,7 +231,7 @@ export const GlobalPushNotifications = (): ReactElement | null => { // Current Safes registered for notifications in indexedDB const currentNotifiedSafes = useMemo(() => { const allPreferences = getAllPreferences() - return transformCurrentSubscribedSafes(allPreferences) + return _transformCurrentSubscribedSafes(allPreferences) }, [getAllPreferences]) // `currentNotifiedSafes` is initially undefined until indexedDB resolves @@ -222,15 +249,15 @@ export const GlobalPushNotifications = (): ReactElement | null => { // Merged added Safes and `currentNotifiedSafes` (in case subscriptions aren't added) const notifiableSafes = useMemo(() => { - return mergeNotifiableSafes(addedSafes, currentNotifiedSafes) + return _mergeNotifiableSafes(addedSafes, currentNotifiedSafes) }, [addedSafes, currentNotifiedSafes]) const totalNotifiableSafes = useMemo(() => { - return getTotalNotifiableSafes(notifiableSafes) + return _getTotalNotifiableSafes(notifiableSafes) }, [notifiableSafes]) const isAllSelected = useMemo(() => { - return areAllSafesSelected(notifiableSafes, selectedSafes) + return _areAllSafesSelected(notifiableSafes, selectedSafes) }, [notifiableSafes, selectedSafes]) const onSelectAll = () => { @@ -249,13 +276,13 @@ export const GlobalPushNotifications = (): ReactElement | null => { } const totalSignaturesRequired = useMemo(() => { - return getTotalSignaturesRequired(selectedSafes, currentNotifiedSafes) + return _getTotalSignaturesRequired(selectedSafes, currentNotifiedSafes) }, [currentNotifiedSafes, selectedSafes]) const canSave = useMemo(() => { return ( - shouldRegisterSelectedSafes(selectedSafes, currentNotifiedSafes) || - shouldUnregsiterSelectedSafes(selectedSafes, currentNotifiedSafes) + _shouldRegisterSelectedSafes(selectedSafes, currentNotifiedSafes) || + _shouldUnregsiterSelectedSafes(selectedSafes, currentNotifiedSafes) ) }, [selectedSafes, currentNotifiedSafes]) @@ -264,17 +291,20 @@ export const GlobalPushNotifications = (): ReactElement | null => { return } + setIsLoading(true) + // Although the (un-)registration functions will request permission in getToken we manually // check beforehand to prevent multiple promises in registrationPromises from throwing const isGranted = await requestNotificationPermission() if (!isGranted) { + setIsLoading(false) return } const registrationPromises: Array> = [] - const safesToRegister = getSafesToRegister(selectedSafes, currentNotifiedSafes) + const safesToRegister = _getSafesToRegister(selectedSafes, currentNotifiedSafes) if (safesToRegister) { registrationPromises.push(registerNotifications(safesToRegister)) @@ -284,10 +314,10 @@ export const GlobalPushNotifications = (): ReactElement | null => { }) } - const safesToUnregister = getSafesToUnregister(selectedSafes, currentNotifiedSafes) + const safesToUnregister = _getSafesToUnregister(selectedSafes, currentNotifiedSafes) if (safesToUnregister) { const unregistrationPromises = Object.entries(safesToUnregister).flatMap(([chainId, safeAddresses]) => { - if (shouldUnregisterDevice(chainId, safeAddresses, currentNotifiedSafes)) { + if (_shouldUnregisterDevice(chainId, safeAddresses, currentNotifiedSafes)) { return unregisterDeviceNotifications(chainId) } return safeAddresses.map((safeAddress) => unregisterSafeNotifications(chainId, safeAddress)) @@ -299,6 +329,8 @@ export const GlobalPushNotifications = (): ReactElement | null => { await Promise.all(registrationPromises) trackEvent(PUSH_NOTIFICATION_EVENTS.SAVE_SETTINGS) + + setIsLoading(false) } if (totalNotifiableSafes === 0) { @@ -322,8 +354,8 @@ export const GlobalPushNotifications = (): ReactElement | null => { {(isOk) => ( - )} diff --git a/src/components/settings/PushNotifications/__tests__/GlobalPushNotifications.test.ts b/src/components/settings/PushNotifications/__tests__/GlobalPushNotifications.test.ts new file mode 100644 index 0000000000..b6f590b5ee --- /dev/null +++ b/src/components/settings/PushNotifications/__tests__/GlobalPushNotifications.test.ts @@ -0,0 +1,505 @@ +import { + transformAddedSafes, + _mergeNotifiableSafes, + _transformCurrentSubscribedSafes, + _getTotalNotifiableSafes, + _areAllSafesSelected, + _getTotalSignaturesRequired, + _shouldRegisterSelectedSafes, + _shouldUnregsiterSelectedSafes, + _getSafesToRegister, + _getSafesToUnregister, + _shouldUnregisterDevice, +} from '../GlobalPushNotifications' +import type { AddedSafesState } from '@/store/addedSafesSlice' + +describe('GlobalPushNotifications', () => { + describe('transformAddedSafes', () => { + it('should transform added safes into notifiable safes', () => { + const addedSafes = { + '1': { + '0x123': {}, + '0x456': {}, + }, + '4': { + '0x789': {}, + }, + } as unknown as AddedSafesState + + const expectedNotifiableSafes = { + '1': ['0x123', '0x456'], + '4': ['0x789'], + } + + expect(transformAddedSafes(addedSafes)).toEqual(expectedNotifiableSafes) + }) + }) + + describe('mergeNotifiableSafes', () => { + it('should merge added safes and current subscriptions', () => { + const addedSafes = { + '1': { + '0x123': {}, + '0x456': {}, + }, + '4': { + '0x789': {}, + }, + } as unknown as AddedSafesState + + const currentSubscriptions = { + '1': ['0x123', '0x789'], + '4': ['0x789'], + } + + const expectedNotifiableSafes = { + '1': ['0x123', '0x456', '0x789'], + '4': ['0x789'], + } + + expect(_mergeNotifiableSafes(addedSafes, currentSubscriptions)).toEqual(expectedNotifiableSafes) + }) + + it('should return added safes if there are no current subscriptions', () => { + const addedSafes = { + '1': { + '0x123': {}, + '0x456': {}, + }, + '4': { + '0x789': {}, + }, + } as unknown as AddedSafesState + + expect(_mergeNotifiableSafes(addedSafes)).toEqual(transformAddedSafes(addedSafes)) + }) + }) + + describe('transformCurrentSubscribedSafes', () => { + it('should transform current subscriptions into notifiable safes', () => { + const currentSubscriptions = { + '0x123': { + chainId: '1', + safeAddress: '0x123', + }, + '0x456': { + chainId: '1', + safeAddress: '0x456', + }, + '0x789': { + chainId: '4', + safeAddress: '0x789', + }, + } + + const expectedNotifiableSafes = { + '1': ['0x123', '0x456'], + '4': ['0x789'], + } + + expect(_transformCurrentSubscribedSafes(currentSubscriptions)).toEqual(expectedNotifiableSafes) + }) + + it('should return undefined if there are no current subscriptions', () => { + expect(_transformCurrentSubscribedSafes()).toBeUndefined() + }) + }) + + describe('getTotalNotifiableSafes', () => { + it('should return the total number of notifiable safes', () => { + const notifiableSafes = { + '1': ['0x123', '0x456'], + '4': ['0x789'], + } + + expect(_getTotalNotifiableSafes(notifiableSafes)).toEqual(3) + }) + + it('should return 0 if there are no notifiable safes', () => { + expect(_getTotalNotifiableSafes({})).toEqual(0) + }) + }) + + describe('areAllSafesSelected', () => { + it('should return true if all notifiable safes are selected', () => { + const notifiableSafes = { + '1': ['0x123', '0x456'], + '4': ['0x789'], + } + + const selectedSafes = { + '1': ['0x123', '0x456'], + '4': ['0x789'], + } + + expect(_areAllSafesSelected(notifiableSafes, selectedSafes)).toEqual(true) + }) + + it('should return false if not all notifiable safes are selected', () => { + const notifiableSafes = { + '1': ['0x123', '0x456'], + '4': ['0x789'], + } + + const selectedSafes = { + '1': ['0x123', '0x456'], + '4': ['0x123'], + } + + expect(_areAllSafesSelected(notifiableSafes, selectedSafes)).toEqual(false) + }) + + it('should return false if there are no notifiable safes', () => { + const notifiableSafes = {} + + const selectedSafes = { + '1': ['0x123', '0x456'], + '4': ['0x789'], + } + + expect(_areAllSafesSelected(notifiableSafes, selectedSafes)).toEqual(false) + }) + }) + + describe('getTotalSignaturesRequired', () => { + it('should return the total number of signatures required to register a new chain', () => { + const currentNotifiedSafes = { + '1': ['0x123', '0x456'], + '4': ['0x789'], + } + + const selectedSafes = { + ...currentNotifiedSafes, + '5': ['0xabc'], + } + + expect(_getTotalSignaturesRequired(selectedSafes, currentNotifiedSafes)).toEqual(1) + }) + + it('should return the total number of signatures required to register a new Safe', () => { + const currentNotifiedSafes = { + '1': ['0x123'], + '4': ['0x789'], + } + + const selectedSafes = { + '1': ['0x123'], + '4': [...currentNotifiedSafes['4'], '0xabc'], + } + + expect(_getTotalSignaturesRequired(selectedSafes, currentNotifiedSafes)).toEqual(1) + }) + + it('should return the total number of signatures required to register new chains/Safes', () => { + const currentNotifiedSafes = {} + + const selectedSafes = { + '1': ['0x123'], + '4': ['0x789', '0xabc'], + } + + expect(_getTotalSignaturesRequired(selectedSafes, currentNotifiedSafes)).toEqual(2) + }) + + it('should not increase the count if a new chain is empty', () => { + const currentNotifiedSafes = { + '1': ['0x123', '0x456'], + } + + const selectedSafes = { + '1': currentNotifiedSafes['1'], + '5': [], + } + + expect(_getTotalSignaturesRequired(selectedSafes, currentNotifiedSafes)).toEqual(0) + }) + + it('should not increase the count if a chain was removed', () => { + const currentNotifiedSafes = { + '1': ['0x123', '0x456'], + '4': ['0x789'], + } + + const selectedSafes = { + '1': currentNotifiedSafes['1'], + } + + expect(_getTotalSignaturesRequired(selectedSafes, currentNotifiedSafes)).toEqual(0) + }) + + it('should not increase the count if a Safe was removed', () => { + const currentNotifiedSafes = { + '1': ['0x123', '0x456'], + '4': ['0x789'], + } + + const selectedSafes = { + '1': currentNotifiedSafes['1'].slice(0, 1), + '4': ['0x789'], + } + + expect(_getTotalSignaturesRequired(selectedSafes, currentNotifiedSafes)).toEqual(0) + }) + + it('should not increase the count if a chain/Safe was removed', () => { + const currentNotifiedSafes = { + '1': ['0x123'], + '4': ['0x789', '0xabc'], + } + + const selectedSafes = {} + + expect(_getTotalSignaturesRequired(selectedSafes, currentNotifiedSafes)).toEqual(0) + }) + + it('should return 0 if there are no selected safes', () => { + const currentNotifiedSafes = { + '1': ['0x123'], + '4': ['0x789'], + } + + const selectedSafes = {} + + expect(_getTotalSignaturesRequired(selectedSafes, currentNotifiedSafes)).toEqual(0) + }) + }) + + describe('shouldRegisterSelectedSafes', () => { + it('should return true if there are safes to register', () => { + const currentNotifiedSafes = { + '1': ['0x123'], + '4': ['0x789'], + } + + const selectedSafes = { + '1': ['0x123', '0x456'], + '4': ['0x789'], + } + + const result = _shouldRegisterSelectedSafes(selectedSafes, currentNotifiedSafes) + expect(result).toBe(true) + }) + + it('should return true if there are chains to register', () => { + const currentNotifiedSafes = { + '1': ['0x123', '0x456'], + } + + const selectedSafes = { + '1': ['0x123', '0x456'], + '4': ['0x789'], + } + + const result = _shouldRegisterSelectedSafes(selectedSafes, currentNotifiedSafes) + expect(result).toBe(true) + }) + + it('should return true if there are safes/chains to register', () => { + const currentNotifiedSafes = { + '1': ['0x123', '0x456'], + '4': ['0x789'], + } + + const selectedSafes = { + '1': ['0x123', '0x456', '0x789'], + '4': ['0x789'], + } + + const result = _shouldRegisterSelectedSafes(selectedSafes, currentNotifiedSafes) + expect(result).toBe(true) + }) + + it('should return false if there are no safes to register', () => { + const selectedSafes = { + '1': ['0x123'], + '4': ['0x789'], + } + + const currentNotifiedSafes = { + '1': ['0x123'], + '4': ['0x789'], + } + + const result = _shouldRegisterSelectedSafes(selectedSafes, currentNotifiedSafes) + expect(result).toBe(false) + }) + }) + + describe('shouldUnregisterSelectedSafes', () => { + it('should return true if there are safes to unregister', () => { + const currentNotifiedSafes = { + '1': ['0x123', '0x456'], + '4': ['0x789'], + } + + const selectedSafes = { + '1': ['0x123'], + '4': ['0x789'], + } + + const result = _shouldUnregsiterSelectedSafes(selectedSafes, currentNotifiedSafes) + expect(result).toBe(true) + }) + + it('should return true if there are chains to unregister', () => { + const currentNotifiedSafes = { + '1': ['0x123', '0x456'], + '4': ['0x789', '0xabc'], + } + + const selectedSafes = { + '1': ['0x123', '0x456'], + } + + const result = _shouldUnregsiterSelectedSafes(selectedSafes, currentNotifiedSafes) + expect(result).toBe(true) + }) + + it('should return true if there are safes/chains to unregister', () => { + const currentNotifiedSafes = { + '1': ['0x123', '0x456'], + '4': ['0x789', '0xabc'], + } + + const selectedSafes = { + '1': ['0x123'], + } + + const result = _shouldUnregsiterSelectedSafes(selectedSafes, currentNotifiedSafes) + expect(result).toBe(true) + }) + + it('should return false if there are no safes to unregister', () => { + const currentNotifiedSafes = { + '1': ['0x123'], + '4': ['0x789'], + } + + const selectedSafes = { + '1': ['0x123'], + '4': ['0x789'], + } + + const result = _shouldUnregsiterSelectedSafes(selectedSafes, currentNotifiedSafes) + expect(result).toBe(false) + }) + }) + + describe('getSafesToRegister', () => { + it('returns the safes to register', () => { + const currentNotifiedSafes = { + 1: ['0x123'], + 2: ['0xabc'], + 4: ['0x789', '0xdef'], + } + const selectedSafes = { + 1: ['0x123', '0x456'], + 4: ['0x789'], + } + + const result = _getSafesToRegister(selectedSafes, currentNotifiedSafes) + + expect(result).toEqual({ + 1: ['0x456'], + }) + }) + + it('returns undefined if there are no safes to register', () => { + const currentNotifiedSafes = { + 1: ['0x123'], + 2: ['0xabc'], + 4: ['0x789', '0xdef'], + } + const selectedSafes = { + 1: ['0x123'], + 2: ['0xabc'], + 4: ['0x789', '0xdef'], + } + + const result = _getSafesToRegister(selectedSafes, currentNotifiedSafes) + + expect(result).toBeUndefined() + }) + }) + + describe('getSafesToUnregister', () => { + it('returns undefined if there are no current notified safes', () => { + const currentNotifiedSafes = undefined + const selectedSafes = { + 1: ['0x123', '0x456'], + 4: ['0x789'], + } + + const result = _getSafesToUnregister(selectedSafes, currentNotifiedSafes) + + expect(result).toBeUndefined() + }) + + it('returns the safes to unregister', () => { + const currentNotifiedSafes = { + 1: ['0x123'], + 2: ['0xabc'], + 4: ['0x789', '0xdef'], + } + const selectedSafes = { + 1: ['0x123', '0x456'], + 4: ['0x789'], + } + + const result = _getSafesToUnregister(selectedSafes, currentNotifiedSafes) + + expect(result).toEqual({ + 2: ['0xabc'], + 4: ['0xdef'], + }) + }) + + it('returns undefined if there are no safes to unregister', () => { + const currentNotifiedSafes = { + 1: ['0x123'], + 2: ['0xabc'], + 4: ['0x789', '0xdef'], + } + const selectedSafes = { + 1: ['0x123'], + 2: ['0xabc'], + 4: ['0x789', '0xdef'], + } + + const result = _getSafesToUnregister(selectedSafes, currentNotifiedSafes) + + expect(result).toBeUndefined() + }) + }) + + describe('shouldUnregisterDevice', () => { + const chainId = '1' + const safeAddresses = ['0x123', '0x456'] + const currentNotifiedSafes = { + '1': ['0x123', '0x456'], + '4': ['0x789'], + } + + it('returns true if all safe addresses are included in currentNotifiedSafes', () => { + const result = _shouldUnregisterDevice(chainId, safeAddresses, currentNotifiedSafes) + expect(result).toBe(true) + }) + + it('returns false if not all safe addresses are included in currentNotifiedSafes', () => { + const invalidSafeAddresses = ['0x123', '0x789'] + const result = _shouldUnregisterDevice(chainId, invalidSafeAddresses, currentNotifiedSafes) + expect(result).toBe(false) + }) + + it('returns false if currentNotifiedSafes is undefined', () => { + const result = _shouldUnregisterDevice(chainId, safeAddresses) + expect(result).toBe(false) + }) + + it('returns false if the length of safeAddresses is different from the length of currentNotifiedSafes', () => { + const invalidSafeAddresses = ['0x123'] + const result = _shouldUnregisterDevice(chainId, invalidSafeAddresses, currentNotifiedSafes) + expect(result).toBe(false) + }) + }) +}) diff --git a/src/components/settings/PushNotifications/logic.test.ts b/src/components/settings/PushNotifications/__tests__/logic.test.ts similarity index 98% rename from src/components/settings/PushNotifications/logic.test.ts rename to src/components/settings/PushNotifications/__tests__/logic.test.ts index 16353d8969..f709addb8d 100644 --- a/src/components/settings/PushNotifications/logic.test.ts +++ b/src/components/settings/PushNotifications/__tests__/logic.test.ts @@ -4,9 +4,9 @@ import { hexZeroPad } from 'ethers/lib/utils' import { Web3Provider } from '@ethersproject/providers' import type { JsonRpcSigner } from '@ethersproject/providers' -import * as logic from './logic' +import * as logic from '../logic' import * as web3 from '@/hooks/wallets/web3' -import packageJson from '../../../../package.json' +import packageJson from '../../../../../package.json' import type { ConnectedWallet } from '@/services/onboard' jest.mock('firebase/messaging') diff --git a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts index 043e610c51..d4f5cba746 100644 --- a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts +++ b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts @@ -58,66 +58,52 @@ describe('useNotificationPreferences', () => { _setPreferences(undefined) }) - it('should return all existing preferences', async () => { - const chainId = '1' - const safeAddress = hexZeroPad('0x1', 20) + describe('_getAllPreferenceEntries', () => { + it('should get all preference entries', async () => { + const chainId1 = '1' + const safeAddress1 = hexZeroPad('0x1', 20) + const safeAddress2 = hexZeroPad('0x1', 20) - const preferences = { - [`${chainId}:${safeAddress}`]: { - chainId, - safeAddress, - preferences: DEFAULT_NOTIFICATION_PREFERENCES, - }, - } + const chainId2 = '2' - await setMany(Object.entries(preferences), createPushNotificationPrefsIndexedDb()) - - const { result } = renderHook(() => useNotificationPreferences()) - - await waitFor(() => { - expect(result.current.getAllPreferences()).toEqual(preferences) - }) - }) - - it('should return existing Safe preferences', async () => { - const chainId = '1' - const safeAddress = hexZeroPad('0x1', 20) - - const preferences = { - [`${chainId}:${safeAddress}`]: { - chainId, - safeAddress, - preferences: DEFAULT_NOTIFICATION_PREFERENCES, - }, - } + const preferences = { + [`${chainId1}:${safeAddress1}`]: { + chainId: chainId1, + safeAddress: safeAddress1, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + [`${chainId1}:${safeAddress2}`]: { + chainId: chainId1, + safeAddress: safeAddress2, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + [`${chainId2}:${safeAddress1}`]: { + chainId: chainId2, + safeAddress: safeAddress1, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + } - await setMany(Object.entries(preferences), createPushNotificationPrefsIndexedDb()) + await setMany(Object.entries(preferences), createPushNotificationPrefsIndexedDb()) - const { result } = renderHook(() => useNotificationPreferences()) + const { result } = renderHook(() => useNotificationPreferences()) - await waitFor(() => { - expect(result.current.getPreferences(chainId, safeAddress)).toEqual( - preferences[`${chainId}:${safeAddress}`].preferences, - ) + await waitFor(async () => { + const _preferences = await result.current._getAllPreferenceEntries() + expect(_preferences).toEqual(Object.entries(preferences)) + }) }) }) - it('should create preferences, then hydrate the preferences state', async () => { - const { result } = renderHook(() => useNotificationPreferences()) - - const chainId1 = '1' - const safeAddress1 = hexZeroPad('0x1', 20) - const safeAddress2 = hexZeroPad('0x1', 20) + describe('_deleteManyPreferenceKeys', () => { + it('should delete many preference keys', async () => { + const chainId1 = '1' + const safeAddress1 = hexZeroPad('0x1', 20) + const safeAddress2 = hexZeroPad('0x1', 20) - const chainId2 = '2' + const chainId2 = '2' - result.current._createPreferences({ - [chainId1]: [safeAddress1, safeAddress2], - [chainId2]: [safeAddress1], - }) - - await waitFor(() => { - expect(result.current.getAllPreferences()).toEqual({ + const preferences = { [`${chainId1}:${safeAddress1}`]: { chainId: chainId1, safeAddress: safeAddress1, @@ -133,182 +119,337 @@ describe('useNotificationPreferences', () => { safeAddress: safeAddress1, preferences: DEFAULT_NOTIFICATION_PREFERENCES, }, + } + + await setMany(Object.entries(preferences), createPushNotificationPrefsIndexedDb()) + + const { result } = renderHook(() => useNotificationPreferences()) + + await waitFor(() => { + expect(result.current.getAllPreferences()).toEqual(preferences) + }) + + const keysToDelete = Object.entries(preferences).map(([key]) => key) + + result.current._deleteManyPreferenceKeys(keysToDelete as `${string}:${string}`[]) + + await waitFor(() => { + expect(result.current.getAllPreferences()).toEqual({}) }) }) }) - it('should not create preferences when passed an empty object', async () => { - const { result } = renderHook(() => useNotificationPreferences()) + describe('getAllPreferences', () => { + it('should return all existing preferences', async () => { + const chainId = '1' + const safeAddress = hexZeroPad('0x1', 20) - result.current._createPreferences({}) + const preferences = { + [`${chainId}:${safeAddress}`]: { + chainId, + safeAddress, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + } - await waitFor(() => { - expect(result.current.getAllPreferences()).toEqual({}) + await setMany(Object.entries(preferences), createPushNotificationPrefsIndexedDb()) + + const { result } = renderHook(() => useNotificationPreferences()) + + await waitFor(() => { + expect(result.current.getAllPreferences()).toEqual(preferences) + }) }) }) - it('should not create preferences when passed an empty array of Safes', async () => { - const { result } = renderHook(() => useNotificationPreferences()) + describe('getPreferences', () => { + it('should return existing Safe preferences', async () => { + const chainId = '1' + const safeAddress = hexZeroPad('0x1', 20) - result.current._createPreferences({ ['1']: [] }) + const preferences = { + [`${chainId}:${safeAddress}`]: { + chainId, + safeAddress, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + } - await waitFor(() => { - expect(result.current.getAllPreferences()).toEqual({}) + await setMany(Object.entries(preferences), createPushNotificationPrefsIndexedDb()) + + const { result } = renderHook(() => useNotificationPreferences()) + + await waitFor(() => { + expect(result.current.getPreferences(chainId, safeAddress)).toEqual( + preferences[`${chainId}:${safeAddress}`].preferences, + ) + }) }) }) - it('should update preferences, then hydrate the preferences state', async () => { - const chainId = '1' - const safeAddress = hexZeroPad('0x1', 20) + describe('createPreferences', () => { + it('should create preferences, then hydrate the preferences state', async () => { + const { result } = renderHook(() => useNotificationPreferences()) - const preferences = { - [`${chainId}:${safeAddress}`]: { - chainId: chainId, - safeAddress: safeAddress, - preferences: DEFAULT_NOTIFICATION_PREFERENCES, - }, - } + const chainId1 = '1' + const safeAddress1 = hexZeroPad('0x1', 20) + const safeAddress2 = hexZeroPad('0x1', 20) - await setMany(Object.entries(preferences), createPushNotificationPrefsIndexedDb()) + const chainId2 = '2' - const { result } = renderHook(() => useNotificationPreferences()) + result.current.createPreferences({ + [chainId1]: [safeAddress1, safeAddress2], + [chainId2]: [safeAddress1], + }) - result.current.updatePreferences(chainId, safeAddress, { - ...DEFAULT_NOTIFICATION_PREFERENCES, - [WebhookType.CONFIRMATION_REQUEST]: false, + await waitFor(() => { + expect(result.current.getAllPreferences()).toEqual({ + [`${chainId1}:${safeAddress1}`]: { + chainId: chainId1, + safeAddress: safeAddress1, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + [`${chainId1}:${safeAddress2}`]: { + chainId: chainId1, + safeAddress: safeAddress2, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + [`${chainId2}:${safeAddress1}`]: { + chainId: chainId2, + safeAddress: safeAddress1, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + }) + }) }) - await waitFor(() => { - expect(result.current.getAllPreferences()).toEqual({ + it('should not create preferences when passed an empty object', async () => { + const { result } = renderHook(() => useNotificationPreferences()) + + result.current.createPreferences({}) + + await waitFor(() => { + expect(result.current.getAllPreferences()).toEqual({}) + }) + }) + + it('should not create preferences when passed an empty array of Safes', async () => { + const { result } = renderHook(() => useNotificationPreferences()) + + result.current.createPreferences({ ['1']: [] }) + + await waitFor(() => { + expect(result.current.getAllPreferences()).toEqual({}) + }) + }) + + it('should hydrate accross instances', async () => { + const chainId1 = '1' + const safeAddress1 = hexZeroPad('0x1', 20) + const safeAddress2 = hexZeroPad('0x1', 20) + + const chainId2 = '2' + const { result: instance1 } = renderHook(() => useNotificationPreferences()) + const { result: instance2 } = renderHook(() => useNotificationPreferences()) + + instance1.current.createPreferences({ + [chainId1]: [safeAddress1, safeAddress2], + [chainId2]: [safeAddress1], + }) + + const expectedPreferences = { + [`${chainId1}:${safeAddress1}`]: { + chainId: chainId1, + safeAddress: safeAddress1, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + [`${chainId1}:${safeAddress2}`]: { + chainId: chainId1, + safeAddress: safeAddress2, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + [`${chainId2}:${safeAddress1}`]: { + chainId: chainId2, + safeAddress: safeAddress1, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + } + + await waitFor(() => { + expect(instance1.current.getAllPreferences()).toEqual(expectedPreferences) + expect(instance2.current.getAllPreferences()).toEqual(expectedPreferences) + }) + }) + }) + + describe('updatePreferences', () => { + it('should update preferences, then hydrate the preferences state', async () => { + const chainId = '1' + const safeAddress = hexZeroPad('0x1', 20) + + const preferences = { [`${chainId}:${safeAddress}`]: { chainId: chainId, safeAddress: safeAddress, - preferences: { - ...DEFAULT_NOTIFICATION_PREFERENCES, - [WebhookType.CONFIRMATION_REQUEST]: false, - }, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, }, + } + + await setMany(Object.entries(preferences), createPushNotificationPrefsIndexedDb()) + + const { result } = renderHook(() => useNotificationPreferences()) + + result.current.updatePreferences(chainId, safeAddress, { + ...DEFAULT_NOTIFICATION_PREFERENCES, + [WebhookType.CONFIRMATION_REQUEST]: false, + }) + + await waitFor(() => { + expect(result.current.getAllPreferences()).toEqual({ + [`${chainId}:${safeAddress}`]: { + chainId: chainId, + safeAddress: safeAddress, + preferences: { + ...DEFAULT_NOTIFICATION_PREFERENCES, + [WebhookType.CONFIRMATION_REQUEST]: false, + }, + }, + }) }) }) }) - it('should delete preferences, then hydrate the preferences state', async () => { - const chainId1 = '1' - const safeAddress1 = hexZeroPad('0x1', 20) - const safeAddress2 = hexZeroPad('0x1', 20) - - const chainId2 = '2' - - const preferences = { - [`${chainId1}:${safeAddress1}`]: { - chainId: chainId1, - safeAddress: safeAddress1, - preferences: DEFAULT_NOTIFICATION_PREFERENCES, - }, - [`${chainId1}:${safeAddress2}`]: { - chainId: chainId1, - safeAddress: safeAddress2, - preferences: DEFAULT_NOTIFICATION_PREFERENCES, - }, - [`${chainId2}:${safeAddress1}`]: { - chainId: chainId2, - safeAddress: safeAddress1, - preferences: DEFAULT_NOTIFICATION_PREFERENCES, - }, - } - - await setMany(Object.entries(preferences), createPushNotificationPrefsIndexedDb()) + describe('deletePreferences', () => { + it('should delete preferences, then hydrate the preferences state', async () => { + const chainId1 = '1' + const safeAddress1 = hexZeroPad('0x1', 20) + const safeAddress2 = hexZeroPad('0x1', 20) - const { result } = renderHook(() => useNotificationPreferences()) - - result.current._deletePreferences({ - [chainId1]: [safeAddress1, safeAddress2], - }) + const chainId2 = '2' - await waitFor(() => { - expect(result.current.getAllPreferences()).toEqual({ + const preferences = { + [`${chainId1}:${safeAddress1}`]: { + chainId: chainId1, + safeAddress: safeAddress1, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + [`${chainId1}:${safeAddress2}`]: { + chainId: chainId1, + safeAddress: safeAddress2, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, [`${chainId2}:${safeAddress1}`]: { chainId: chainId2, safeAddress: safeAddress1, preferences: DEFAULT_NOTIFICATION_PREFERENCES, }, + } + + await setMany(Object.entries(preferences), createPushNotificationPrefsIndexedDb()) + + const { result } = renderHook(() => useNotificationPreferences()) + + result.current.deletePreferences({ + [chainId1]: [safeAddress1, safeAddress2], + }) + + await waitFor(() => { + expect(result.current.getAllPreferences()).toEqual({ + [`${chainId2}:${safeAddress1}`]: { + chainId: chainId2, + safeAddress: safeAddress1, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + }) }) }) - }) - it('should delete all preferences, then hydrate the preferences state', async () => { - const chainId1 = '1' - const safeAddress1 = hexZeroPad('0x1', 20) - const safeAddress2 = hexZeroPad('0x1', 20) - - const chainId2 = '2' - - const preferences = { - [`${chainId1}:${safeAddress1}`]: { - chainId: chainId1, - safeAddress: safeAddress1, - preferences: DEFAULT_NOTIFICATION_PREFERENCES, - }, - [`${chainId1}:${safeAddress2}`]: { - chainId: chainId1, - safeAddress: safeAddress2, - preferences: DEFAULT_NOTIFICATION_PREFERENCES, - }, - [`${chainId2}:${safeAddress1}`]: { - chainId: chainId2, - safeAddress: safeAddress1, - preferences: DEFAULT_NOTIFICATION_PREFERENCES, - }, - } - - await setMany(Object.entries(preferences), createPushNotificationPrefsIndexedDb()) + it('should delete preferences, then hydrate the preferences state', async () => { + const chainId1 = '1' + const safeAddress1 = hexZeroPad('0x1', 20) + const safeAddress2 = hexZeroPad('0x1', 20) - const { result } = renderHook(() => useNotificationPreferences()) + const chainId2 = '2' - result.current._deletePreferences({ - [chainId1]: [safeAddress1, safeAddress2], - }) + const preferences = { + [`${chainId1}:${safeAddress1}`]: { + chainId: chainId1, + safeAddress: safeAddress1, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + [`${chainId1}:${safeAddress2}`]: { + chainId: chainId1, + safeAddress: safeAddress2, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + [`${chainId2}:${safeAddress1}`]: { + chainId: chainId2, + safeAddress: safeAddress1, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + } - await waitFor(() => { - expect(result.current.getAllPreferences()).toEqual(undefined) + await setMany(Object.entries(preferences), createPushNotificationPrefsIndexedDb()) + + const { result } = renderHook(() => useNotificationPreferences()) + + result.current.deletePreferences({ + [chainId1]: [safeAddress1, safeAddress2], + }) + + await waitFor(() => { + expect(result.current.getAllPreferences()).toEqual({ + [`${chainId2}:${safeAddress1}`]: { + chainId: chainId2, + safeAddress: safeAddress1, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + }) + }) }) }) - it('should hydrate accross instances', async () => { - const chainId1 = '1' - const safeAddress1 = hexZeroPad('0x1', 20) - const safeAddress2 = hexZeroPad('0x1', 20) + describe('deleteAllChainPreferences', () => { + it('should delete per chain, then hydrate the preferences state', async () => { + const chainId1 = '1' + const safeAddress1 = hexZeroPad('0x1', 20) + const safeAddress2 = hexZeroPad('0x1', 20) - const chainId2 = '2' - const { result: instance1 } = renderHook(() => useNotificationPreferences()) - const { result: instance2 } = renderHook(() => useNotificationPreferences()) + const chainId2 = '2' - instance1.current._createPreferences({ - [chainId1]: [safeAddress1, safeAddress2], - [chainId2]: [safeAddress1], - }) + const preferences = { + [`${chainId1}:${safeAddress1}`]: { + chainId: chainId1, + safeAddress: safeAddress1, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + [`${chainId1}:${safeAddress2}`]: { + chainId: chainId1, + safeAddress: safeAddress2, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + [`${chainId2}:${safeAddress1}`]: { + chainId: chainId2, + safeAddress: safeAddress1, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + } - const expectedPreferences = { - [`${chainId1}:${safeAddress1}`]: { - chainId: chainId1, - safeAddress: safeAddress1, - preferences: DEFAULT_NOTIFICATION_PREFERENCES, - }, - [`${chainId1}:${safeAddress2}`]: { - chainId: chainId1, - safeAddress: safeAddress2, - preferences: DEFAULT_NOTIFICATION_PREFERENCES, - }, - [`${chainId2}:${safeAddress1}`]: { - chainId: chainId2, - safeAddress: safeAddress1, - preferences: DEFAULT_NOTIFICATION_PREFERENCES, - }, - } + await setMany(Object.entries(preferences), createPushNotificationPrefsIndexedDb()) - await waitFor(() => { - expect(instance1.current.getAllPreferences()).toEqual(expectedPreferences) - expect(instance2.current.getAllPreferences()).toEqual(expectedPreferences) + const { result } = renderHook(() => useNotificationPreferences()) + + result.current.deleteAllChainPreferences(chainId1) + + await waitFor(() => { + expect(result.current.getAllPreferences()).toEqual({ + [`${chainId2}:${safeAddress1}`]: { + chainId: chainId2, + safeAddress: safeAddress1, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + }) + }) }) }) }) diff --git a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts index 58cd032e87..7ac47cdedf 100644 --- a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts +++ b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts @@ -104,7 +104,7 @@ describe('useNotificationRegistrations', () => { () => ({ uuid: self.crypto.randomUUID(), - _createPreferences: createPreferencesMock, + createPreferences: createPreferencesMock, } as unknown as ReturnType), ) @@ -136,7 +136,7 @@ describe('useNotificationRegistrations', () => { () => ({ uuid: self.crypto.randomUUID(), - _createPreferences: createPreferencesMock, + createPreferences: createPreferencesMock, } as unknown as ReturnType), ) @@ -167,7 +167,7 @@ describe('useNotificationRegistrations', () => { () => ({ uuid: self.crypto.randomUUID(), - _createPreferences: createPreferencesMock, + createPreferences: createPreferencesMock, } as unknown as ReturnType), ) @@ -218,7 +218,7 @@ describe('useNotificationRegistrations', () => { () => ({ uuid, - _deletePreferences: deletePreferencesMock, + deletePreferences: deletePreferencesMock, } as unknown as ReturnType), ) @@ -244,7 +244,7 @@ describe('useNotificationRegistrations', () => { () => ({ uuid, - _deletePreferences: deletePreferencesMock, + deletePreferences: deletePreferencesMock, } as unknown as ReturnType), ) @@ -270,7 +270,7 @@ describe('useNotificationRegistrations', () => { () => ({ uuid, - _deletePreferences: deletePreferencesMock, + deletePreferences: deletePreferencesMock, } as unknown as ReturnType), ) @@ -310,13 +310,13 @@ describe('useNotificationRegistrations', () => { unregisterDeviceSpy.mockImplementation(() => Promise.resolve('Unregistration could not be completed.')) const uuid = self.crypto.randomUUID() - const deleteAllPreferencesMock = jest.fn() + const deleteAllChainPreferencesMock = jest.fn() ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( () => ({ uuid, - _deleteAllPreferences: deleteAllPreferencesMock, + deleteAllChainPreferences: deleteAllChainPreferencesMock, } as unknown as ReturnType), ) @@ -326,20 +326,20 @@ describe('useNotificationRegistrations', () => { expect(unregisterDeviceSpy).toHaveBeenCalledWith('1', uuid) - expect(deleteAllPreferencesMock).not.toHaveBeenCalled() + expect(deleteAllChainPreferencesMock).not.toHaveBeenCalled() }) it('does not clear preferences if unregistration throws', async () => { unregisterDeviceSpy.mockImplementation(() => Promise.reject()) const uuid = self.crypto.randomUUID() - const deleteAllPreferencesMock = jest.fn() + const deleteAllChainPreferencesMock = jest.fn() ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( () => ({ uuid, - _deleteAllPreferences: deleteAllPreferencesMock, + deleteAllChainPreferences: deleteAllChainPreferencesMock, } as unknown as ReturnType), ) @@ -349,20 +349,20 @@ describe('useNotificationRegistrations', () => { expect(unregisterDeviceSpy).toHaveBeenCalledWith('1', uuid) - expect(deleteAllPreferencesMock).not.toHaveBeenCalledWith() + expect(deleteAllChainPreferencesMock).not.toHaveBeenCalledWith() }) - it('clears preferences if unregistration succeeds', async () => { + it('clears chain preferences if unregistration succeeds', async () => { unregisterDeviceSpy.mockImplementation(() => Promise.resolve()) const uuid = self.crypto.randomUUID() - const deleteAllPreferencesMock = jest.fn() + const deleteAllChainPreferencesMock = jest.fn() ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( () => ({ uuid, - _deleteAllPreferences: deleteAllPreferencesMock, + deleteAllChainPreferences: deleteAllChainPreferencesMock, } as unknown as ReturnType), ) @@ -372,7 +372,7 @@ describe('useNotificationRegistrations', () => { expect(unregisterDeviceSpy).toHaveBeenCalledWith('1', uuid) - expect(deleteAllPreferencesMock).toHaveBeenCalled() + expect(deleteAllChainPreferencesMock).toHaveBeenCalledWith('1') }) }) }) diff --git a/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts b/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts index 315604ca3e..4cc26e7cc7 100644 --- a/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts +++ b/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts @@ -3,7 +3,6 @@ import { entries as getEntriesFromIndexedDb, delMany as deleteManyFromIndexedDb, setMany as setManyIndexedDb, - clear as clearIndexedDb, update as updateIndexedDb, } from 'idb-keyval' import { useCallback, useEffect, useMemo } from 'react' @@ -51,9 +50,13 @@ export const useNotificationPreferences = (): { safeAddress: string, preferences: PushNotificationPreferences[PushNotificationPrefsKey]['preferences'], ) => void - _createPreferences: (safesToRegister: NotifiableSafes) => void - _deletePreferences: (safesToUnregister: NotifiableSafes) => void - _deleteAllPreferences: () => void + createPreferences: (safesToRegister: NotifiableSafes) => void + deletePreferences: (safesToUnregister: NotifiableSafes) => void + deleteAllChainPreferences: (chainId: string) => void + _getAllPreferenceEntries: () => Promise< + [PushNotificationPrefsKey, PushNotificationPreferences[PushNotificationPrefsKey]][] + > + _deleteManyPreferenceKeys: (keysToDelete: PushNotificationPrefsKey[]) => void } => { // State const uuid = useUuid() @@ -114,22 +117,38 @@ export const useNotificationPreferences = (): { hydrateUuidStore() }, [hydrateUuidStore, uuidStore]) + const _getAllPreferenceEntries = useCallback(() => { + return getEntriesFromIndexedDb( + preferencesStore, + ) + }, [preferencesStore]) + // Preferences state hydrator const hydratePreferences = useCallback(() => { if (!preferencesStore) { return } - getEntriesFromIndexedDb( - preferencesStore, - ) + _getAllPreferenceEntries() .then((preferencesEntries) => { setPreferences(Object.fromEntries(preferencesEntries)) }) .catch((e) => { logError(ErrorCodes._705, e) }) - }, [preferencesStore]) + }, [_getAllPreferenceEntries, preferencesStore]) + + // Delete array of preferences store keys + const _deleteManyPreferenceKeys = useCallback( + (keysToDelete: PushNotificationPrefsKey[]) => { + deleteManyFromIndexedDb(keysToDelete, preferencesStore) + .then(hydratePreferences) + .catch((e) => { + logError(ErrorCodes._706, e) + }) + }, + [hydratePreferences, preferencesStore], + ) // Hydrate preferences state useEffect(() => { @@ -200,23 +219,27 @@ export const useNotificationPreferences = (): { return safeAddresses.map((safeAddress) => getPushNotificationPrefsKey(chainId, safeAddress)) }) - deleteManyFromIndexedDb(keysToDelete, preferencesStore) - .then(hydratePreferences) - .catch((e) => { - logError(ErrorCodes._706, e) - }) + _deleteManyPreferenceKeys(keysToDelete) } // Delete all preferences store entries - const deleteAllPreferences = () => { + const deleteAllChainPreferences = (chainId: string) => { if (!preferencesStore) { return } - clearIndexedDb(preferencesStore) - .then(hydratePreferences) + _getAllPreferenceEntries() + .then((preferencesEntries) => { + const keysToDelete = preferencesEntries + .filter(([, prefs]) => { + return prefs.chainId === chainId + }) + .map(([key]) => key) + + _deleteManyPreferenceKeys(keysToDelete) + }) .catch((e) => { - logError(ErrorCodes._706, e) + logError(ErrorCodes._705, e) }) } @@ -225,8 +248,10 @@ export const useNotificationPreferences = (): { getAllPreferences, getPreferences, updatePreferences, - _createPreferences: createPreferences, - _deletePreferences: deletePreferences, - _deleteAllPreferences: deleteAllPreferences, + createPreferences, + deletePreferences, + deleteAllChainPreferences, + _getAllPreferenceEntries, + _deleteManyPreferenceKeys, } } diff --git a/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts b/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts index d9d67e3f15..7bde23e3dc 100644 --- a/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts +++ b/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts @@ -39,7 +39,7 @@ export const useNotificationRegistrations = (): { const dispatch = useAppDispatch() const wallet = useWallet() - const { uuid, _createPreferences, _deletePreferences, _deleteAllPreferences } = useNotificationPreferences() + const { uuid, createPreferences, deletePreferences, deleteAllChainPreferences } = useNotificationPreferences() const registerNotifications = async (safesToRegister: NotifiableSafes) => { if (!uuid || !wallet) { @@ -57,7 +57,7 @@ export const useNotificationRegistrations = (): { } return registrationFlow(register(), () => { - _createPreferences(safesToRegister) + createPreferences(safesToRegister) const totalRegistered = Object.values(safesToRegister).reduce( (acc, safeAddresses) => acc + safeAddresses.length, @@ -84,7 +84,7 @@ export const useNotificationRegistrations = (): { const unregisterSafeNotifications = async (chainId: string, safeAddress: string) => { if (uuid) { return registrationFlow(unregisterSafe(chainId, safeAddress, uuid), () => { - _deletePreferences({ [chainId]: [safeAddress] }) + deletePreferences({ [chainId]: [safeAddress] }) trackEvent(PUSH_NOTIFICATION_EVENTS.UNREGISTER_SAFE) }) } @@ -93,7 +93,7 @@ export const useNotificationRegistrations = (): { const unregisterDeviceNotifications = async (chainId: string) => { if (uuid) { return registrationFlow(unregisterDevice(chainId, uuid), () => { - _deleteAllPreferences() + deleteAllChainPreferences(chainId) trackEvent(PUSH_NOTIFICATION_EVENTS.UNREGISTER_DEVICE) }) } diff --git a/src/components/tx/SignOrExecuteForm/hooks.ts b/src/components/tx/SignOrExecuteForm/hooks.ts index 2cf77b95e0..a0a4a712c0 100644 --- a/src/components/tx/SignOrExecuteForm/hooks.ts +++ b/src/components/tx/SignOrExecuteForm/hooks.ts @@ -170,7 +170,7 @@ export const useRecommendedNonce = (): number | undefined => { const recommendedNonce = await getRecommendedNonce(safe.chainId, safeAddress) - return recommendedNonce ? Math.max(safe.nonce, recommendedNonce) : undefined + return recommendedNonce !== undefined ? Math.max(safe.nonce, recommendedNonce) : undefined }, // eslint-disable-next-line react-hooks/exhaustive-deps [safeAddress, safe.chainId, safe.txQueuedTag], // update when tx queue changes diff --git a/src/services/exceptions/ErrorCodes.ts b/src/services/exceptions/ErrorCodes.ts index 3e28789c8e..cf24e6b433 100644 --- a/src/services/exceptions/ErrorCodes.ts +++ b/src/services/exceptions/ErrorCodes.ts @@ -45,8 +45,8 @@ enum ErrorCodes { _702 = '702: Failed to remove from local/session storage', _703 = '703: Error importing an address book', _704 = '704: Error importing global data', - _705 = '704: Failed to read from IndexedDB', - _706 = '704: Failed to write to IndexedDB', + _705 = '705: Failed to read from IndexedDB', + _706 = '706: Failed to write to IndexedDB', _800 = '800: Safe creation tx failed', _801 = '801: Failed to send a tx with a spending limit',