Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions shared/lib/toast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from 'react-hot-toast';
51 changes: 32 additions & 19 deletions ui/components/app/toast-master/selectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from '../../../selectors';
import { getCaip25CaveatValueFromPermissions } from '../../../pages/permissions-connect/connect-page/utils';
import { supportsChainIds } from '../../../hooks/useAccountGroupsForPermissions';
import { selectSelectedAccountGroup } from '../../../selectors/multichain-accounts/account-tree';
import { AccountGroupWithInternalAccounts } from '../../../selectors/multichain-accounts/account-tree.types';
import { createMockInternalAccount } from '../../../../test/jest/mocks';
import { StorageWriteErrorType } from '../../../../shared/constants/app-state';
Expand Down Expand Up @@ -49,6 +50,10 @@ jest.mock('../../../hooks/useAccountGroupsForPermissions', () => ({
supportsChainIds: jest.fn(),
}));

jest.mock('../../../selectors/multichain-accounts/account-tree', () => ({
selectSelectedAccountGroup: jest.fn(),
}));

const mockGetAlertEnabledness = getAlertEnabledness as jest.MockedFunction<
typeof getAlertEnabledness
>;
Expand Down Expand Up @@ -77,8 +82,12 @@ const mockIsInternalAccountInPermittedAccountIds =
isInternalAccountInPermittedAccountIds as jest.MockedFunction<
typeof isInternalAccountInPermittedAccountIds
>;
const mockGetSelectedAccountGroupWithAccounts =
selectSelectedAccountGroup as jest.MockedFunction<
typeof selectSelectedAccountGroup
>;

describe('#selectShowConnectAccountGroupToast', () => {
describe('#selectShowConnectSelectedAccountGroupToast', () => {
const mockAccount1 = createMockInternalAccount({
id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3',
address: '0x742d35Cc6634C0532925a3b8D4F25dE8B8C5C8B4',
Expand Down Expand Up @@ -140,7 +149,8 @@ describe('#selectShowConnectAccountGroupToast', () => {
appState: {},
metamask: {},
activeTab: { origin: 'https://example.com' },
} as const;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;

afterEach(() => {
jest.clearAllMocks();
Expand All @@ -164,8 +174,9 @@ describe('#selectShowConnectAccountGroupToast', () => {
mockAccount2,
mockAccount3,
]);
mockGetSelectedAccountGroupWithAccounts.mockReturnValue(accountGroup);

const result = selectShowConnectAccountGroupToast(baseState, accountGroup);
const result = selectShowConnectAccountGroupToast(baseState);

// Assert
expect(result).toBe(true);
Expand All @@ -189,8 +200,9 @@ describe('#selectShowConnectAccountGroupToast', () => {
mockAccount2,
mockAccount3,
]);
mockGetSelectedAccountGroupWithAccounts.mockReturnValue(accountGroup);

const result = selectShowConnectAccountGroupToast(baseState, accountGroup);
const result = selectShowConnectAccountGroupToast(baseState);

expect(result).toBe(false);
});
Expand All @@ -212,8 +224,9 @@ describe('#selectShowConnectAccountGroupToast', () => {
mockAccount2,
mockAccount3,
]);
mockGetSelectedAccountGroupWithAccounts.mockReturnValue(accountGroup);

const result = selectShowConnectAccountGroupToast(baseState, accountGroup);
const result = selectShowConnectAccountGroupToast(baseState);

expect(result).toBe(false);
});
Expand All @@ -239,12 +252,9 @@ describe('#selectShowConnectAccountGroupToast', () => {
...baseState,
activeTab: { origin: null },
};
mockGetSelectedAccountGroupWithAccounts.mockReturnValue(accountGroup);

const result = selectShowConnectAccountGroupToast(
// @ts-expect-error - Testing null origin case
stateWithoutOrigin,
accountGroup,
);
const result = selectShowConnectAccountGroupToast(stateWithoutOrigin);

expect(result).toBeFalsy();
});
Expand All @@ -257,8 +267,9 @@ describe('#selectShowConnectAccountGroupToast', () => {
mockAccount2,
mockAccount3,
]);
mockGetSelectedAccountGroupWithAccounts.mockReturnValue(accountGroup);

const result = selectShowConnectAccountGroupToast(baseState, accountGroup);
const result = selectShowConnectAccountGroupToast(baseState);

expect(result).toBe(false);
});
Expand Down Expand Up @@ -287,8 +298,9 @@ describe('#selectShowConnectAccountGroupToast', () => {
mockAccount2,
mockAccount3,
]);
mockGetSelectedAccountGroupWithAccounts.mockReturnValue(accountGroup);

const result = selectShowConnectAccountGroupToast(baseState, accountGroup);
const result = selectShowConnectAccountGroupToast(baseState);

expect(result).toBe(false);
});
Expand Down Expand Up @@ -316,8 +328,9 @@ describe('#selectShowConnectAccountGroupToast', () => {
mockAccount2,
mockAccount3,
]);
mockGetSelectedAccountGroupWithAccounts.mockReturnValue(accountGroup);

const result = selectShowConnectAccountGroupToast(baseState, accountGroup);
const result = selectShowConnectAccountGroupToast(baseState);

expect(result).toBe(true);
});
Expand All @@ -339,8 +352,9 @@ describe('#selectShowConnectAccountGroupToast', () => {
mockAccount2,
mockAccount3,
]);
mockGetSelectedAccountGroupWithAccounts.mockReturnValue(accountGroup);

const result = selectShowConnectAccountGroupToast(baseState, accountGroup);
const result = selectShowConnectAccountGroupToast(baseState);

expect(result).toBe(true);
expect(mockGetCaip25CaveatValueFromPermissions).not.toHaveBeenCalled();
Expand All @@ -367,8 +381,9 @@ describe('#selectShowConnectAccountGroupToast', () => {
mockAccount2,
mockAccount3,
]);
mockGetSelectedAccountGroupWithAccounts.mockReturnValue(accountGroup);

const result = selectShowConnectAccountGroupToast(baseState, accountGroup);
const result = selectShowConnectAccountGroupToast(baseState);

expect(result).toBe(true);
});
Expand All @@ -388,11 +403,9 @@ describe('#selectShowConnectAccountGroupToast', () => {
mockIsInternalAccountInPermittedAccountIds.mockReturnValue(false);

const emptyAccountGroup = createMockAccountGroup('empty', []);
mockGetSelectedAccountGroupWithAccounts.mockReturnValue(emptyAccountGroup);

const result = selectShowConnectAccountGroupToast(
baseState,
emptyAccountGroup,
);
const result = selectShowConnectAccountGroupToast(baseState);

expect(result).toBe(true);
});
Expand Down
12 changes: 8 additions & 4 deletions ui/components/app/toast-master/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ import {
ClaimSubmitToastType,
StorageWriteErrorType,
} from '../../../../shared/constants/app-state';
import { AccountGroupWithInternalAccounts } from '../../../selectors/multichain-accounts/account-tree.types';
import { getCaip25CaveatValueFromPermissions } from '../../../pages/permissions-connect/connect-page/utils';
import { supportsChainIds } from '../../../hooks/useAccountGroupsForPermissions';
import { selectSelectedAccountGroup } from '../../../selectors/multichain-accounts/account-tree';
import type { MultichainAccountsState } from '../../../selectors/multichain-accounts/account-tree.types';
import { getIsPrivacyToastRecent } from './utils';

type State = {
Expand Down Expand Up @@ -113,9 +114,13 @@ export function selectNftDetectionEnablementToast(
// If there is more than one connected account to activeTabOrigin,
// *BUT* the current account is not one of them, show the banner
export function selectShowConnectAccountGroupToast(
state: State & Pick<MetaMaskReduxState, 'activeTab'>,
accountGroup: AccountGroupWithInternalAccounts,
state: MetaMaskReduxState & MultichainAccountsState,
): boolean {
const accountGroup = selectSelectedAccountGroup(state);
if (!accountGroup) {
return false;
}

const allowShowAccountSetting = getAlertEnabledness(state).unconnectedAccount;
const connectedAccounts = getAllPermittedAccountsForCurrentTab(state);
const activeTabOrigin = getOriginOfCurrentTab(state);
Expand All @@ -135,7 +140,6 @@ export function selectShowConnectAccountGroupToast(

const showConnectAccountToast =
allowShowAccountSetting &&
accountGroup &&
isAccountSupported &&
activeTabOrigin &&
connectedAccounts.length > 0 &&
Expand Down
116 changes: 2 additions & 114 deletions ui/components/app/toast-master/toast-master.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate, useLocation } from 'react-router-dom';
import classnames from 'clsx';
import { getAllScopesFromCaip25CaveatValue } from '@metamask/chain-agnostic-permission';
import {
AvatarNetwork,
AvatarNetworkSize,
} from '@metamask/design-system-react';
import { PRODUCT_TYPES } from '@metamask/subscription-controller';
import { MILLISECOND, SECOND } from '../../../../shared/constants/time';
import { SECOND } from '../../../../shared/constants/time';
import {
PRIVACY_POLICY_LINK,
SURVEY_LINK,
Expand All @@ -29,21 +28,15 @@
} from '../../../helpers/constants/routes';
import { getURLHost } from '../../../helpers/utils/util';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { usePrevious } from '../../../hooks/usePrevious';
import {
getCurrentNetwork,
getMetaMaskHdKeyrings,
getOriginOfCurrentTab,
getPermissions,
getUseNftDetection,
} from '../../../selectors';
import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../shared/constants/network';
import {
addPermittedAccount,
hidePermittedNetworkToast,
} from '../../../store/actions';
import { hidePermittedNetworkToast } from '../../../store/actions';
import { Icon, IconName, IconSize } from '../../component-library';
import { PreferredAvatar } from '../preferred-avatar';
import { Toast, ToastContainer } from '../../multichain';
import { SurveyToast } from '../../ui/survey-toast';
import {
Expand All @@ -54,13 +47,6 @@
import { MerklClaimToast, MusdConversionToast } from '../musd';
import { PerpsWithdrawToast } from '../perps/perps-withdraw-toast';
import { getDappActiveNetwork } from '../../../selectors/dapp';
import {
getAccountGroupWithInternalAccounts,
getIconSeedAddressByAccountGroupId,
getSelectedAccountGroup,
} from '../../../selectors/multichain-accounts/account-tree';
import { hasChainIdSupport } from '../../../../shared/lib/multichain/scope-utils';
import { getCaip25CaveatValueFromPermissions } from '../../../pages/permissions-connect/connect-page/utils';
import {
useUserSubscriptionByProduct,
useUserSubscriptions,
Expand Down Expand Up @@ -93,7 +79,6 @@
selectNewSrpAdded,
selectPasswordChangeToast,
selectShowCopyAddressToast,
selectShowConnectAccountGroupToast,
selectClaimSubmitToast,
selectShowShieldPausedToast,
selectShowShieldEndingToast,
Expand Down Expand Up @@ -135,7 +120,6 @@
<ToastContainer>
{storageErrorToast}
<SurveyToast />
<ConnectAccountGroupToast />
<SurveyToastMayDelete />
<PrivacyPolicyToast />
<NftEnablementToast />
Expand Down Expand Up @@ -171,103 +155,7 @@
return null;
}

function ConnectAccountGroupToast() {
const t = useI18nContext();
const dispatch = useDispatch();

const [hideConnectAccountToast, setHideConnectAccountToast] = useState(false);
const selectedAccountGroup = useSelector(getSelectedAccountGroup);
const selectedAccountGroupInternalAccounts = useSelector((state) =>
getAccountGroupWithInternalAccounts(state, selectedAccountGroup),
)?.find((accountGroup) => accountGroup.id === selectedAccountGroup);

// If the account has changed, allow the connect account toast again
const prevAccountGroup = usePrevious(selectedAccountGroup);
if (selectedAccountGroup !== prevAccountGroup && hideConnectAccountToast) {
setHideConnectAccountToast(false);
}

const showConnectAccountToast = useSelector((state) =>
selectedAccountGroupInternalAccounts
? selectShowConnectAccountGroupToast(
state,
selectedAccountGroupInternalAccounts,
)
: false,
);

const activeTabOrigin = useSelector(getOriginOfCurrentTab);
const existingPermissions = useSelector((state) =>
getPermissions(state, activeTabOrigin),
);
const existingCaip25CaveatValue = existingPermissions
? getCaip25CaveatValueFromPermissions(existingPermissions)
: null;
const existingChainIds = useMemo(
() =>
existingCaip25CaveatValue
? getAllScopesFromCaip25CaveatValue(existingCaip25CaveatValue)
: [],
[existingCaip25CaveatValue],
);

const addressesToPermit = useMemo(() => {
if (!selectedAccountGroupInternalAccounts?.accounts) {
return [];
}
return selectedAccountGroupInternalAccounts.accounts
.filter((account) => hasChainIdSupport(account.scopes, existingChainIds))
.map((account) => account.address);
}, [existingChainIds, selectedAccountGroupInternalAccounts?.accounts]);

const seedAddress = useSelector((state) =>
getIconSeedAddressByAccountGroupId(
state,
selectedAccountGroupInternalAccounts?.id,
),
);

// Early return if selectedAccountGroupInternalAccounts is undefined
if (!selectedAccountGroupInternalAccounts || !seedAddress) {
return null;
}

return (
Boolean(!hideConnectAccountToast && showConnectAccountToast) && (
<Toast
dataTestId="connect-account-toast"
key="connect-account-toast"
startAdornment={
<PreferredAvatar address={seedAddress} className="self-center" />
}
text={t('accountIsntConnectedToastText', [
selectedAccountGroupInternalAccounts.metadata?.name,
getURLHost(activeTabOrigin),
])}
actionText={t('connectAccount')}
onActionClick={() => {
// Connect this account
addressesToPermit.forEach((address) => {
dispatch(addPermittedAccount(activeTabOrigin, address));
});
// Use setTimeout to prevent React re-render from
// hiding the tooltip
setTimeout(() => {
// Trigger a mouseenter on the header's connection icon
// to display the informative connection tooltip
document
.querySelector(
'[data-testid="connection-menu"] [data-tooltipped]',
)
?.dispatchEvent(new CustomEvent('mouseenter', {}));
}, 250 * MILLISECOND);
}}
onClose={() => setHideConnectAccountToast(true)}
/>
)
);
}

Check failure on line 158 in ui/components/app/toast-master/toast-master.js

View workflow job for this annotation

GitHub Actions / Test lint

Delete `⏎`
function SurveyToastMayDelete() {
const t = useI18nContext();

Expand Down
Loading
Loading