diff --git a/shared/lib/selectors/smart-transactions.ts b/shared/lib/selectors/smart-transactions.ts index 2e13d0a7d580..631d0f9eec80 100644 --- a/shared/lib/selectors/smart-transactions.ts +++ b/shared/lib/selectors/smart-transactions.ts @@ -23,7 +23,7 @@ import { } from '../../../ui/selectors/remote-feature-flags'; import { isProduction } from '../environment'; import { getCurrentChainId, type NetworkState } from './networks'; -import { createDeepEqualSelector } from './util'; +import { createDeepEqualSelector } from './selector-creators'; export type SmartTransactionsMetaMaskState = { metamask: { diff --git a/shared/lib/selectors/util.ts b/shared/lib/selectors/util.ts deleted file mode 100644 index 81d7e94df48e..000000000000 --- a/shared/lib/selectors/util.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { createDeepEqualSelector } from './selector-creators'; - -// re-export for backward compatibility -export { createDeepEqualSelector }; diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index a5004c0820c9..e0a2d961bf75 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -49,7 +49,7 @@ import { ALL_ALLOWED_BRIDGE_CHAIN_IDS, ALLOWED_BRIDGE_CHAIN_IDS, } from '../../../shared/constants/bridge'; -import { createDeepEqualSelector } from '../../../shared/lib/selectors/util'; +import { createDeepEqualSelector } from '../../../shared/lib/selectors/selector-creators'; import { CHAIN_IDS, FEATURED_RPCS } from '../../../shared/constants/network'; import { getCurrencyRateControllerCurrencyRates, diff --git a/ui/ducks/locale/locale.ts b/ui/ducks/locale/locale.ts index 32fe6538b382..691928fac741 100644 --- a/ui/ducks/locale/locale.ts +++ b/ui/ducks/locale/locale.ts @@ -1,7 +1,7 @@ import { Action } from 'redux'; // Import types for actions +import { createSelector } from 'reselect'; import * as actionConstants from '../../store/actionConstants'; import { FALLBACK_LOCALE } from '../../../shared/lib/i18n'; -import { createDeepEqualSelector } from '../../../shared/lib/selectors/util'; /** * Type for the locale messages part of the state @@ -75,11 +75,12 @@ export const getCurrentLocale = (state: AppState): string | undefined => state.localeMessages?.currentLocale; /** - * This selector returns a BCP 47 Language Tag for use with the Intl API. + * Selector to get the locale formatted for Intl API usage. + * Converts locale codes from underscore format (en_US) to hyphen format (en-US). * - * @returns The user's selected locale in BCP 47 format + * @returns The canonicalized locale string for Intl API. */ -export const getIntlLocale = createDeepEqualSelector( +export const getIntlLocale = createSelector( getCurrentLocale, (locale): string => Intl.getCanonicalLocales( diff --git a/ui/pages/confirmations/selectors/accounts.ts b/ui/pages/confirmations/selectors/accounts.ts index ea8a49f4743a..03396fc48bd4 100644 --- a/ui/pages/confirmations/selectors/accounts.ts +++ b/ui/pages/confirmations/selectors/accounts.ts @@ -1,5 +1,5 @@ import { InternalAccount } from '@metamask/keyring-internal-api'; -import { createDeepEqualSelector } from '../../../../shared/lib/selectors/util'; +import { createDeepEqualSelector } from '../../../../shared/lib/selectors/selector-creators'; import { getAccountGroupsByAddress } from '../../../selectors/multichain-accounts/account-tree'; import { AccountGroupWithInternalAccounts, diff --git a/ui/pages/confirmations/selectors/confirm.ts b/ui/pages/confirmations/selectors/confirm.ts index b3e0e3bda74f..3c412d67b745 100644 --- a/ui/pages/confirmations/selectors/confirm.ts +++ b/ui/pages/confirmations/selectors/confirm.ts @@ -4,7 +4,7 @@ import { QuoteResponse } from '@metamask/bridge-controller'; import { createSelector } from 'reselect'; import { getPendingApprovals } from '../../../selectors/approvals'; -import { createDeepEqualSelector } from '../../../../shared/lib/selectors/util'; +import { createDeepEqualSelector } from '../../../../shared/lib/selectors/selector-creators'; import { ConfirmMetamaskState } from '../types/confirm'; const ConfirmationApprovalTypes = [ diff --git a/ui/pages/notification-details/notification-details.test.tsx b/ui/pages/notification-details/notification-details.test.tsx new file mode 100644 index 000000000000..788b78c4f807 --- /dev/null +++ b/ui/pages/notification-details/notification-details.test.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { useAppSelector } from '../../store/store'; +import { renderWithProvider } from '../../../test/lib/render-helpers-navigate'; +import { NOTIFICATIONS_ROUTE } from '../../helpers/constants/routes'; +import NotificationDetails from './notification-details'; + +const configureStore = jest.requireActual('../../store/store').default; + +const mockUseNavigate = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockUseNavigate, +})); + +jest.mock('../../store/store', () => { + const actual = jest.requireActual('../../store/store'); + return { + ...actual, + useAppSelector: jest.fn(), + }; +}); + +jest.mock('../../hooks/metamask-notifications/useNotifications', () => ({ + useMarkNotificationAsRead: () => ({ + markNotificationAsRead: jest.fn(), + }), +})); + +jest.mock('../../hooks/useNotificationTimeouts', () => ({ + useSnapNotificationTimeouts: () => ({ + setNotificationTimeout: jest.fn(), + }), +})); + +describe('NotificationDetails', () => { + beforeEach(() => { + mockUseNavigate.mockClear(); + jest.mocked(useAppSelector).mockReturnValue(undefined); + }); + + it('navigates to the notifications list when the notification is not found', () => { + const store = configureStore({}); + + renderWithProvider( + , + store, + `${NOTIFICATIONS_ROUTE}/missing-id`, + ); + + expect(mockUseNavigate).toHaveBeenCalledWith(NOTIFICATIONS_ROUTE); + expect(useAppSelector).toHaveBeenCalled(); + }); +}); diff --git a/ui/pages/notification-details/notification-details.tsx b/ui/pages/notification-details/notification-details.tsx index 3ba9d90ceb29..f1b45cbb9366 100644 --- a/ui/pages/notification-details/notification-details.tsx +++ b/ui/pages/notification-details/notification-details.tsx @@ -1,5 +1,4 @@ import React, { useEffect } from 'react'; -import { useSelector } from 'react-redux'; import { useNavigate, useLocation } from 'react-router-dom'; import { TRIGGER_TYPES, @@ -21,6 +20,7 @@ import { hasNotificationComponents, } from '../notifications/notification-components'; import { useSnapNotificationTimeouts } from '../../hooks/useNotificationTimeouts'; +import { useAppSelector } from '../../store/store'; import { getExtractIdentifier } from './utils/utils'; import { NotificationDetailsHeader } from './notification-details-header/notification-details-header'; import { NotificationDetailsBody } from './notification-details-body/notification-details-body'; @@ -29,7 +29,9 @@ import { NotificationDetailsFooter } from './notification-details-footer/notific function useNotificationByPath() { const { pathname } = useLocation(); const id = getExtractIdentifier(pathname); - const notification = useSelector(getMetamaskNotificationById(id)); + const notification = useAppSelector((state) => + getMetamaskNotificationById(state, id), + ); return { notification, diff --git a/ui/selectors/approvals.ts b/ui/selectors/approvals.ts index 53a303429369..916a3a557e6e 100644 --- a/ui/selectors/approvals.ts +++ b/ui/selectors/approvals.ts @@ -5,7 +5,7 @@ import { import { ApprovalType } from '@metamask/controller-utils'; import { createSelector } from 'reselect'; import { Json } from '@metamask/utils'; -import { createDeepEqualSelector } from '../../shared/lib/selectors/util'; +import { createDeepEqualSelector } from '../../shared/lib/selectors/selector-creators'; import { EMPTY_OBJECT } from './shared'; export type ApprovalsMetaMaskState = { diff --git a/ui/selectors/assets.ts b/ui/selectors/assets.ts index 3e9b0870856c..5b414965a835 100644 --- a/ui/selectors/assets.ts +++ b/ui/selectors/assets.ts @@ -42,7 +42,7 @@ import type { } from '@metamask/assets-controllers'; import { NetworkEnablementControllerState } from '@metamask/network-enablement-controller'; import { TEST_CHAINS } from '../../shared/constants/network'; -import { createDeepEqualSelector } from '../../shared/lib/selectors/util'; +import { createDeepEqualSelector } from '../../shared/lib/selectors/selector-creators'; import { Token, TokenWithFiatAmount } from '../components/app/assets/types'; import { calculateTokenBalance } from '../components/app/assets/util/calculateTokenBalance'; import { calculateTokenFiatAmount } from '../components/app/assets/util/calculateTokenFiatAmount'; diff --git a/ui/selectors/dapp.ts b/ui/selectors/dapp.ts index 514e6493aac5..4f967a77ea18 100644 --- a/ui/selectors/dapp.ts +++ b/ui/selectors/dapp.ts @@ -1,6 +1,6 @@ import { isEvmAccountType } from '@metamask/keyring-api'; import { getNetworkConfigurationsByChainId } from '../../shared/lib/selectors/networks'; -import { createDeepEqualSelector } from '../../shared/lib/selectors/util'; +import { createDeepEqualSelector } from '../../shared/lib/selectors/selector-creators'; import { getOrderedConnectedAccountsForActiveTab, getOriginOfCurrentTab, diff --git a/ui/selectors/metamask-notifications/metamask-notifications.test.ts b/ui/selectors/metamask-notifications/metamask-notifications.test.ts index 5fc06fa4449f..98ebebb6cab1 100644 --- a/ui/selectors/metamask-notifications/metamask-notifications.test.ts +++ b/ui/selectors/metamask-notifications/metamask-notifications.test.ts @@ -6,6 +6,7 @@ import { createMockNotificationEthReceived } from '@metamask/notification-servic import { selectIsMetamaskNotificationsEnabled, getMetamaskNotifications, + getMetamaskNotificationById, getMetamaskNotificationsReadList, getMetamaskNotificationsUnreadCount, selectIsFeatureAnnouncementsEnabled, @@ -80,4 +81,25 @@ describe('Metamask Notifications Selectors', () => { state.metamask.subscriptionAccountsSeen = ['0x1111']; expect(getValidNotificationAccounts(state)).toStrictEqual(['0x1111']); }); + + describe('getMetamaskNotificationById', () => { + it('returns the notification when the id matches', () => { + const state = mockState(); + const [first] = mockNotifications; + expect(getMetamaskNotificationById(state, first.id)).toStrictEqual(first); + }); + + it('returns undefined when the id is not in the list', () => { + const state = mockState(); + expect( + getMetamaskNotificationById(state, 'non-existent-id'), + ).toBeUndefined(); + }); + + it('returns undefined when the notifications list is empty', () => { + const state = mockState(); + state.metamask.metamaskNotificationsList = []; + expect(getMetamaskNotificationById(state, 'any-id')).toBeUndefined(); + }); + }); }); diff --git a/ui/selectors/metamask-notifications/metamask-notifications.ts b/ui/selectors/metamask-notifications/metamask-notifications.ts index 70078f02212f..ac8a76489344 100644 --- a/ui/selectors/metamask-notifications/metamask-notifications.ts +++ b/ui/selectors/metamask-notifications/metamask-notifications.ts @@ -6,7 +6,7 @@ import { TRIGGER_TYPES, defaultState, } from '@metamask/notification-services-controller/notification-services'; -import { createDeepEqualSelector } from '../../../shared/lib/selectors/util'; +import { createParameterizedSelector } from '../../../shared/lib/selectors/selector-creators'; import { getRemoteFeatureFlags, type RemoteFeatureFlagsState, @@ -54,21 +54,18 @@ export const getMetamaskNotifications = createSelector( ); /** - * Factory function to create a selector that retrieves a specific MetaMask notification by ID. + * Selector to retrieve a specific MetaMask notification by its ID. * - * This function returns a selector that is tailored to fetch a notification by its ID. - * - * @param id - The ID of the notification to retrieve. - * @returns A selector function that takes the AppState and returns the notification. + * @param _state - The current state of the Redux store. + * @param id - The unique identifier of the notification to retrieve. + * @returns The notification matching the given ID, or undefined if not found. */ -export const getMetamaskNotificationById = (id: string) => { - return createDeepEqualSelector( - [getMetamaskNotifications], - (notifications: Notification[]): Notification | undefined => { - return notifications.find((notification) => notification.id === id); - }, - ); -}; +export const getMetamaskNotificationById = createParameterizedSelector(20)( + [getMetamaskNotifications, (_state: NotificationAppState, id: string) => id], + (notifications: Notification[], id: string): Notification | undefined => { + return notifications.find((notification) => notification.id === id); + }, +); /** * Selector to get the list of read MetaMask notifications. diff --git a/ui/selectors/multichain-accounts/account-tree.ts b/ui/selectors/multichain-accounts/account-tree.ts index fdb9f534e43a..251dcfb374a0 100644 --- a/ui/selectors/multichain-accounts/account-tree.ts +++ b/ui/selectors/multichain-accounts/account-tree.ts @@ -19,8 +19,8 @@ import { import { type MultichainNetworkConfiguration } from '@metamask/multichain-network-controller'; import { type NetworkConfiguration } from '@metamask/network-controller'; -import { createDeepEqualSelector } from '../../../shared/lib/selectors/util'; import { + createDeepEqualSelector, createParameterizedSelector, createParameterizedShallowEqualSelector, } from '../../../shared/lib/selectors/selector-creators'; diff --git a/ui/selectors/multichain.ts b/ui/selectors/multichain.ts index 8a78567aec8b..aeb1707de3fa 100644 --- a/ui/selectors/multichain.ts +++ b/ui/selectors/multichain.ts @@ -49,7 +49,7 @@ import { } from '../../shared/lib/selectors/networks'; // eslint-disable-next-line import-x/no-restricted-paths import { getConversionRatesForNativeAsset } from '../../app/scripts/lib/util'; -import { createDeepEqualSelector } from '../../shared/lib/selectors/util'; +import { createDeepEqualSelector } from '../../shared/lib/selectors/selector-creators'; import { AccountsState, getInternalAccounts, diff --git a/ui/selectors/multichain/networks.ts b/ui/selectors/multichain/networks.ts index 94f224ef558a..e5f8f868a7c4 100644 --- a/ui/selectors/multichain/networks.ts +++ b/ui/selectors/multichain/networks.ts @@ -42,7 +42,7 @@ import { selectDefaultNetworkClientIdsByChainId, getNetworksMetadata, } from '../../../shared/lib/selectors/networks'; -import { createDeepEqualSelector } from '../../../shared/lib/selectors/util'; +import { createDeepEqualSelector } from '../../../shared/lib/selectors/selector-creators'; import { getEnabledNetworks } from '../../../shared/lib/selectors/multichain'; import { getIsMetaMaskInfuraEndpointUrl } from '../../../shared/lib/network-utils'; import { type RemoteFeatureFlagsState } from '../remote-feature-flags'; diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 7187bb85dd19..2ab7509fcbc4 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -159,8 +159,8 @@ import { MULTICHAIN_NETWORK_TO_ASSET_TYPES } from '../../shared/constants/multic import { MULTICHAIN_PROVIDER_CONFIGS } from '../../shared/constants/multichain/networks'; import { hasTransactionData } from '../../shared/lib/transaction.utils'; import { toChecksumHexAddress } from '../../shared/lib/hexstring-utils'; -import { createDeepEqualSelector } from '../../shared/lib/selectors/util'; import { + createDeepEqualSelector, createParameterizedSelector, createParameterizedShallowEqualSelector, } from '../../shared/lib/selectors/selector-creators'; @@ -1291,7 +1291,8 @@ export function getTargetAccountWithSendEtherInfo(state, targetAddress) { export function getCurrentEthBalance(state) { return getCurrentAccountWithSendEtherInfo(state)?.balance; } -export const getNetworkConfigurationIdByChainId = createDeepEqualSelector( + +export const getNetworkConfigurationIdByChainId = createSelector( (state) => state.metamask.networkConfigurationsByChainId, (networkConfigurationsByChainId) => Object.entries(networkConfigurationsByChainId).reduce( @@ -2251,13 +2252,14 @@ export const getConnectedSubjectsForAllAddresses = createDeepEqualSelector( }, ); -const getAllConnectedAccounts = createDeepEqualSelector( +const getAllConnectedAccounts = createSelector( getConnectedSubjectsForAllAddresses, (connectedSubjects) => { return Object.keys(connectedSubjects); }, ); -export const getConnectedSitesList = createDeepEqualSelector( + +export const getConnectedSitesList = createSelector( getConnectedSubjectsForAllAddresses, getInternalAccounts, getAllConnectedAccounts, @@ -2586,7 +2588,7 @@ export const getSelectedNetwork = createDeepEqualSelector( }, ); -export const getConnectedSitesListWithNetworkInfo = createDeepEqualSelector( +export const getConnectedSitesListWithNetworkInfo = createSelector( getConnectedSitesList, getAllDomains, getNetworkConfigurationsByChainId, diff --git a/ui/selectors/signatures.ts b/ui/selectors/signatures.ts index 6be005b1a3d7..a1c2ba513c1d 100644 --- a/ui/selectors/signatures.ts +++ b/ui/selectors/signatures.ts @@ -1,6 +1,6 @@ import { createSelector } from 'reselect'; import { DefaultRootState } from 'react-redux'; -import { createDeepEqualSelector } from '../../shared/lib/selectors/util'; +import { createDeepEqualSelector } from '../../shared/lib/selectors/selector-creators'; import { unapprovedPersonalMsgsSelector, unapprovedTypedMessagesSelector, diff --git a/ui/selectors/snaps/address-book.ts b/ui/selectors/snaps/address-book.ts index 836bd0d82b98..52ce35de41f7 100644 --- a/ui/selectors/snaps/address-book.ts +++ b/ui/selectors/snaps/address-book.ts @@ -1,5 +1,5 @@ import { AddressBookController } from '@metamask/address-book-controller'; -import { createDeepEqualSelector } from '../../../shared/lib/selectors/util'; +import { createDeepEqualSelector } from '../../../shared/lib/selectors/selector-creators'; import { isEqualCaseInsensitive } from '../../../shared/lib/string-utils'; /** diff --git a/ui/selectors/snaps/currency.ts b/ui/selectors/snaps/currency.ts index 7275bd2d324a..840b37c2d498 100644 --- a/ui/selectors/snaps/currency.ts +++ b/ui/selectors/snaps/currency.ts @@ -1,4 +1,4 @@ -import { createDeepEqualSelector } from '../../../shared/lib/selectors/util'; +import { createDeepEqualSelector } from '../../../shared/lib/selectors/selector-creators'; import { getCurrentCurrency } from '../../ducks/metamask/metamask'; export const getMemoizedCurrentCurrency = createDeepEqualSelector( diff --git a/ui/selectors/toast.ts b/ui/selectors/toast.ts index fc92cee1cb39..5d1fbf2e0eb4 100644 --- a/ui/selectors/toast.ts +++ b/ui/selectors/toast.ts @@ -1,5 +1,5 @@ import { createSelector } from 'reselect'; -import { createDeepEqualSelector } from '../../shared/lib/selectors/util'; +import { createDeepEqualSelector } from '../../shared/lib/selectors/selector-creators'; import type { MetaMaskReduxState } from '../store/store'; import { TOAST_EXCLUDED_TRANSACTION_TYPES, diff --git a/ui/selectors/transactions.js b/ui/selectors/transactions.js index 092aa2522856..453eb375c875 100644 --- a/ui/selectors/transactions.js +++ b/ui/selectors/transactions.js @@ -21,8 +21,9 @@ import { getProviderConfig, getCurrentChainId, } from '../../shared/lib/selectors/networks'; -import { createDeepEqualSelector } from '../../shared/lib/selectors/util'; + import { + createDeepEqualSelector, createShallowEqualInputAndResultSelector, createParameterizedShallowEqualSelector, } from '../../shared/lib/selectors/selector-creators';