diff --git a/app/components/UI/NetworkSelectorList/NetworkSelectorList.styles.ts b/app/components/UI/NetworkSelectorList/NetworkSelectorList.styles.ts index 133a87f969d..e279b943357 100644 --- a/app/components/UI/NetworkSelectorList/NetworkSelectorList.styles.ts +++ b/app/components/UI/NetworkSelectorList/NetworkSelectorList.styles.ts @@ -3,9 +3,8 @@ import { StyleSheet } from 'react-native'; const styleSheet = () => StyleSheet.create({ networkItemContainer: { - flexDirection: 'row', - alignItems: 'center', - padding: 10, + paddingHorizontal: 10, + paddingVertical: 14, }, networkAvatar: { marginHorizontal: 10, diff --git a/app/components/UI/NetworkSelectorList/NetworkSelectorList.tsx b/app/components/UI/NetworkSelectorList/NetworkSelectorList.tsx index 59fd85ddfef..152348b8573 100644 --- a/app/components/UI/NetworkSelectorList/NetworkSelectorList.tsx +++ b/app/components/UI/NetworkSelectorList/NetworkSelectorList.tsx @@ -24,7 +24,7 @@ const NetworkSelectorList = ({ onSelectNetwork, networks = [], isLoading = false, - selectedNetworkIds, + selectedChainIds, isMultiSelect = true, renderRightAccessory, isSelectionDisabled, @@ -33,7 +33,6 @@ const NetworkSelectorList = ({ }: NetworkConnectMultiSelectorProps) => { const networksLengthRef = useRef(0); const { styles } = useStyles(styleSheet, {}); - /** * Ref for the FlatList component. * The type of the ref is not explicitly defined. @@ -51,8 +50,8 @@ const NetworkSelectorList = ({ ? CellVariant.MultiSelect : CellVariant.Select; let isSelectedNetwork = isSelected; - if (selectedNetworkIds) { - isSelectedNetwork = selectedNetworkIds.includes(id); + if (selectedChainIds) { + isSelectedNetwork = selectedChainIds.includes(id); } return ( @@ -76,7 +75,7 @@ const NetworkSelectorList = ({ }, [ isLoading, - selectedNetworkIds, + selectedChainIds, renderRightAccessory, isSelectionDisabled, onSelectNetwork, diff --git a/app/components/UI/NetworkSelectorList/NetworkSelectorList.types.ts b/app/components/UI/NetworkSelectorList/NetworkSelectorList.types.ts index ebc0e3b4d6c..48b3bd6ab07 100644 --- a/app/components/UI/NetworkSelectorList/NetworkSelectorList.types.ts +++ b/app/components/UI/NetworkSelectorList/NetworkSelectorList.types.ts @@ -12,7 +12,7 @@ export interface NetworkConnectMultiSelectorProps { onSelectNetwork?: (id: string, isSelected: boolean) => void; networks?: Network[]; isLoading?: boolean; - selectedNetworkIds?: string[]; + selectedChainIds?: string[]; isMultiSelect?: boolean; renderRightAccessory?: (id: string, name: string) => React.ReactNode; isSelectionDisabled?: boolean; diff --git a/app/components/UI/NetworkVerificationInfo/NetworkVerificationInfo.tsx b/app/components/UI/NetworkVerificationInfo/NetworkVerificationInfo.tsx index e9a4df6a70e..96761b37689 100644 --- a/app/components/UI/NetworkVerificationInfo/NetworkVerificationInfo.tsx +++ b/app/components/UI/NetworkVerificationInfo/NetworkVerificationInfo.tsx @@ -100,6 +100,20 @@ const NetworkVerificationInfo = ({ [customNetworkInformation], ); + const dappOrigin = useMemo(() => { + // @ts-expect-error - The CustomNetworkInformation type is missing the pageMeta property + const customNetworkUrl = customNetworkInformation.pageMeta?.url; + const url = customNetworkUrl ? new URL(customNetworkUrl) : null; + if (url) { + try { + return url.hostname; + } catch (error) { + console.error('Invalid URL:', error); + } + } + return 'Undefined dapp origin'; + }, [customNetworkInformation]); + const renderCurrencySymbol = () => ( <> diff --git a/app/components/UI/PermissionsSummary/PermissionsSummary.tsx b/app/components/UI/PermissionsSummary/PermissionsSummary.tsx index 9fea3eecabf..9c9015d261f 100644 --- a/app/components/UI/PermissionsSummary/PermissionsSummary.tsx +++ b/app/components/UI/PermissionsSummary/PermissionsSummary.tsx @@ -24,7 +24,6 @@ import TextComponent, { TextVariant, } from '../../../component-library/components/Texts/Text'; import AvatarGroup from '../../../component-library/components/Avatars/AvatarGroup'; -import { SAMPLE_AVATARGROUP_PROPS } from '../../../component-library/components/Avatars/AvatarGroup/AvatarGroup.constants'; import Button, { ButtonSize, ButtonVariants, @@ -57,6 +56,8 @@ const PermissionsSummary = ({ isDisconnectAllShown = true, isNetworkSwitch = false, accountAddresses = [], + accounts = [], + networkAvatars = [], }: PermissionsSummaryProps) => { const { colors } = useTheme(); const { styles } = useStyles(styleSheet, { isRenderedAsBottomSheet }); @@ -169,27 +170,56 @@ const PermissionsSummary = ({ if (accountAddresses.length === 0 && selectedAccount) { return `${strings('permissions.connected_to')} ${selectedAccount.name}`; } - return accountAddresses.length === 1 - ? `1 ${strings('accounts.account_connected')}` - : `${accountAddresses.length} ${strings( - 'accounts.accounts_connected', - )}`; + if (accountAddresses.length === 1) { + const matchedConnectedAccount = accounts.find( + (account) => account.address === accountAddresses[0], + ); + return matchedConnectedAccount?.name; + } + + return `${accountAddresses.length} ${strings( + 'accounts.accounts_connected', + )}`; } - if ( - accountAddresses.length === 1 || - (accountAddresses.length === 0 && selectedAccount) - ) { - return ( - selectedAccount?.name && - `${strings('permissions.requesting_for')}${selectedAccount?.name}` + if (accountAddresses.length === 1 && accounts?.length >= 1) { + const matchedAccount = accounts.find( + (account) => account.address === accountAddresses[0], ); + return `${strings('permissions.requesting_for')}${ + matchedAccount?.name ? matchedAccount.name : accountAddresses[0] + }`; + } + + if (accountAddresses.length === 0 && selectedAccount) { + return `${strings('permissions.requesting_for')}${selectedAccount?.name}`; } return strings('permissions.requesting_for_accounts', { numberOfAccounts: accountAddresses.length, }); - }, [accountAddresses, isAlreadyConnected, selectedAccount]); + }, [accountAddresses, isAlreadyConnected, selectedAccount, accounts]); + + const getNetworkLabel = useCallback(() => { + if (isAlreadyConnected) { + return networkAvatars.length === 1 + ? networkAvatars[0]?.name + : `${strings('permissions.n_networks_connect', { + numberOfNetworks: networkAvatars.length, + })}`; + } + + if (networkAvatars.length === 1) { + return ( + networkAvatars[0]?.name && + `${strings('permissions.requesting_for')}${networkAvatars[0]?.name}` + ); + } + + return strings('permissions.requesting_for_networks', { + numberOfNetworks: networkAvatars.length, + }); + }, [networkAvatars, isAlreadyConnected]); function renderAccountPermissionsRequestInfoCard() { return ( @@ -280,11 +310,24 @@ const PermissionsSummary = ({ )} {!isNetworkSwitch && ( - - - + <> + + + + {getNetworkLabel()} + + + + + ({ + ...avatar, + variant: AvatarVariant.Network, + }))} + /> + + )} diff --git a/app/components/UI/PermissionsSummary/PermissionsSummary.types.ts b/app/components/UI/PermissionsSummary/PermissionsSummary.types.ts index 470f893f93d..3bf71acd277 100644 --- a/app/components/UI/PermissionsSummary/PermissionsSummary.types.ts +++ b/app/components/UI/PermissionsSummary/PermissionsSummary.types.ts @@ -1,4 +1,5 @@ import { USER_INTENT } from '../../../constants/permissions'; +import { Account } from '../../hooks/useAccounts'; export interface PermissionsSummaryProps { currentPageInformation: { @@ -21,5 +22,7 @@ export interface PermissionsSummaryProps { chainName: string; chainId: string; }; + accounts?: Account[]; accountAddresses?: string[]; + networkAvatars?: ({ name: string; imageSource: string } | null)[]; } diff --git a/app/components/UI/PermissionsSummary/__snapshots__/PermissionsSummary.test.tsx.snap b/app/components/UI/PermissionsSummary/__snapshots__/PermissionsSummary.test.tsx.snap index 2543543d2a1..5fb871e2b3d 100644 --- a/app/components/UI/PermissionsSummary/__snapshots__/PermissionsSummary.test.tsx.snap +++ b/app/components/UI/PermissionsSummary/__snapshots__/PermissionsSummary.test.tsx.snap @@ -488,310 +488,59 @@ exports[`PermissionsSummary should render correctly 1`] = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +1 + 0 networks connected - + + + + diff --git a/app/components/Views/AccountConnect/AccountConnect.tsx b/app/components/Views/AccountConnect/AccountConnect.tsx index 2258e29f9a0..a9f976fe494 100644 --- a/app/components/Views/AccountConnect/AccountConnect.tsx +++ b/app/components/Views/AccountConnect/AccountConnect.tsx @@ -48,7 +48,7 @@ import { Account, useAccounts } from '../../hooks/useAccounts'; // Internal dependencies. import { PermissionsRequest } from '@metamask/permission-controller'; -import { ImageURISource, StyleSheet } from 'react-native'; +import { ImageURISource, ImageSourcePropType, StyleSheet } from 'react-native'; import URLParse from 'url-parse'; import PhishingModal from '../../../components/UI/PhishingModal'; import { useMetrics } from '../../../components/hooks/useMetrics'; @@ -75,8 +75,16 @@ import AccountConnectSingle from './AccountConnectSingle'; import AccountConnectSingleSelector from './AccountConnectSingleSelector'; import { PermissionsSummaryProps } from '../../../components/UI/PermissionsSummary/PermissionsSummary.types'; import PermissionsSummary from '../../../components/UI/PermissionsSummary'; -import { isMultichainVersion1Enabled } from '../../../util/networks'; +import { + isMultichainVersion1Enabled, + getNetworkImageSource, +} from '../../../util/networks'; import NetworkConnectMultiSelector from '../NetworkConnect/NetworkConnectMultiSelector'; +import { PermissionKeys } from '../../../core/Permissions/specifications'; +import { CaveatTypes } from '../../../core/Permissions/constants'; +import { useNetworkInfo } from '../../../selectors/selectedNetworkController'; +import { AvatarSize } from '../../../component-library/components/Avatars/Avatar'; +import { selectNetworkConfigurations } from '../../../selectors/networkController'; const createStyles = () => StyleSheet.create({ @@ -116,6 +124,14 @@ const AccountConnect = (props: AccountConnectProps) => { const [showPhishingModal, setShowPhishingModal] = useState(false); const [userIntent, setUserIntent] = useState(USER_INTENT.None); + const [selectedNetworkAvatars, setSelectedNetworkAvatars] = useState< + { + size: AvatarSize; + name: string; + imageSource: ImageSourcePropType; + }[] + >([]); + const { toastRef } = useContext(ToastContext); const accountAvatarType = useSelector((state: RootState) => state.settings.useBlockieIcon @@ -128,6 +144,8 @@ const AccountConnect = (props: AccountConnectProps) => { const accountsLength = useSelector(selectAccountsLength); const { wc2Metadata } = useSelector((state: RootState) => state.sdk); + const networkConfigurations = useSelector(selectNetworkConfigurations); + const { origin: channelIdOrHostname } = hostInfo.metadata as { id: string; origin: string; @@ -189,6 +207,72 @@ const AccountConnect = (props: AccountConnectProps) => { ? prefixUrlWithProtocol(getHost(hostname)) : domainTitle; + const { chainId } = useNetworkInfo(hostname); + + const [selectedChainIds, setSelectedChainIds] = useState(() => + chainId ? [chainId] : [], + ); + const [selectedNetworkIds, setSelectedNetworkIds] = useState(() => + chainId ? [chainId] : [], + ); + + useEffect(() => { + if (chainId) { + const initialNetworkAvatar = { + size: AvatarSize.Xs, + name: networkConfigurations[chainId]?.name || '', + // @ts-expect-error getNetworkImageSourcenot yet typed + imageSource: getNetworkImageSource({ chainId }), + }; + setSelectedNetworkAvatars([initialNetworkAvatar]); + + setSelectedChainIds([chainId]); + } + }, [chainId, networkConfigurations]); + + const handleUpdateNetworkPermissions = useCallback(async () => { + let hasPermittedChains = false; + let chainsToPermit = selectedChainIds.length > 0 ? selectedChainIds : []; + if (chainId && chainsToPermit.length === 0) { + chainsToPermit = [chainId]; + } + + try { + hasPermittedChains = Engine.context.PermissionController.hasCaveat( + new URL(hostname).hostname, + PermissionKeys.permittedChains, + CaveatTypes.restrictNetworkSwitching, + ); + } catch { + // noop + } + + if (hasPermittedChains) { + Engine.context.PermissionController.updateCaveat( + new URL(hostname).hostname, + PermissionKeys.permittedChains, + CaveatTypes.restrictNetworkSwitching, + chainsToPermit, + ); + } else { + Engine.context.PermissionController.grantPermissionsIncremental({ + subject: { + origin: new URL(hostname).hostname, + }, + approvedPermissions: { + [PermissionKeys.permittedChains]: { + caveats: [ + { + type: CaveatTypes.restrictNetworkSwitching, + value: chainsToPermit, + }, + ], + }, + }, + }); + } + }, [selectedChainIds, chainId, hostname]); + const isAllowedOrigin = useCallback((origin: string) => { const { PhishingController } = Engine.context; @@ -444,6 +528,27 @@ const AccountConnect = (props: AccountConnectProps) => { [trackEvent], ); + const handleNetworksSelected = useCallback( + (newSelectedChainIds: string[]) => { + setSelectedChainIds(newSelectedChainIds); + setSelectedNetworkIds(newSelectedChainIds); + + const newNetworkAvatars = newSelectedChainIds.map( + (newSelectedChainId) => ({ + size: AvatarSize.Xs, + // @ts-expect-error - networkConfigurations is not typed + name: networkConfigurations[newSelectedChainId]?.name || '', + // @ts-expect-error - getNetworkImageSource is not typed + imageSource: getNetworkImageSource({ chainId: newSelectedChainId }), + }), + ); + setSelectedNetworkAvatars(newNetworkAvatars); + + setScreen(AccountConnectScreens.SingleConnect); + }, + [networkConfigurations, setScreen], + ); + const hideSheet = (callback?: () => void) => sheetRef?.current?.onCloseBottomSheet?.(callback); @@ -463,6 +568,7 @@ const AccountConnect = (props: AccountConnectProps) => { switch (action) { case USER_INTENT.Confirm: { handleConnect(); + handleUpdateNetworkPermissions(); hideSheet(); break; } @@ -506,6 +612,7 @@ const AccountConnect = (props: AccountConnectProps) => { handleCreateAccount, handleConnect, trackEvent, + handleUpdateNetworkPermissions, ]); const handleSheetDismiss = () => { @@ -573,9 +680,18 @@ const AccountConnect = (props: AccountConnectProps) => { onUserAction: setUserIntent, isAlreadyConnected: false, accountAddresses: confirmedAddresses, + accounts, + // @ts-expect-error imageSource not yet typed + networkAvatars: selectedNetworkAvatars, }; return ; - }, [faviconSource, urlWithProtocol, confirmedAddresses]); + }, [ + faviconSource, + urlWithProtocol, + confirmedAddresses, + selectedNetworkAvatars, + accounts, + ]); const renderSingleConnectSelectorScreen = useCallback( () => ( @@ -647,11 +763,21 @@ const AccountConnect = (props: AccountConnectProps) => { isLoading={isLoading} onUserAction={setUserIntent} urlWithProtocol={urlWithProtocol} - hostname={hostname} + hostname={new URL(urlWithProtocol).hostname} onBack={() => setScreen(AccountConnectScreens.SingleConnect)} + onNetworksSelected={handleNetworksSelected} + initialChainId={chainId} + selectedChainIds={selectedNetworkIds} + isInitializedWithPermittedChains={false} /> ), - [isLoading, urlWithProtocol, hostname], + [ + isLoading, + urlWithProtocol, + chainId, + handleNetworksSelected, + selectedNetworkIds, + ], ); const renderPhishingModal = useCallback( diff --git a/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.tsx b/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.tsx index 1820b9e33c2..157534a5c53 100644 --- a/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.tsx +++ b/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.tsx @@ -1,6 +1,7 @@ // Third party dependencies. import React, { useCallback, useState } from 'react'; import { View, SafeAreaView } from 'react-native'; +import { isEqual } from 'lodash'; // External dependencies. import { strings } from '../../../../../locales/i18n'; @@ -59,6 +60,12 @@ const AccountConnectMultiSelector = ({ const [screen, setScreen] = useState( AccountConnectMultiSelectorScreens.AccountMultiSelector, ); + const sortedSelectedAddresses = [...selectedAddresses].sort((a, b) => + a.localeCompare(b), + ); + const [originalSelectedAddresses] = useState( + sortedSelectedAddresses, + ); const onSelectAccount = useCallback( (accAddress) => { @@ -184,6 +191,10 @@ const AccountConnectMultiSelector = ({ const renderCtaButtons = useCallback(() => { const isConnectDisabled = Boolean(!selectedAddresses.length) || isLoading; + const areUpdateDisabled = isEqual( + [...selectedAddresses].sort((a, b) => a.localeCompare(b)), + originalSelectedAddresses, + ); return ( @@ -225,9 +236,10 @@ const AccountConnectMultiSelector = ({ size={ButtonSize.Lg} style={{ ...styles.button, - ...(isConnectDisabled && styles.disabled), + ...((isConnectDisabled || areUpdateDisabled) && + styles.disabled), }} - disabled={isConnectDisabled} + disabled={isConnectDisabled || areUpdateDisabled} testID={ConnectAccountBottomSheetSelectorsIDs.SELECT_MULTI_BUTTON} /> )} @@ -270,6 +282,7 @@ const AccountConnectMultiSelector = ({ toggleRevokeAllAccountPermissionsModal, showDisconnectAllButton, onPrimaryActionButtonPress, + originalSelectedAddresses, ]); const renderAccountConnectMultiSelector = useCallback( diff --git a/app/components/Views/AccountPermissions/AccountPermissions.tsx b/app/components/Views/AccountPermissions/AccountPermissions.tsx index b571decaa67..ed6589becd9 100755 --- a/app/components/Views/AccountPermissions/AccountPermissions.tsx +++ b/app/components/Views/AccountPermissions/AccountPermissions.tsx @@ -54,10 +54,17 @@ import { useMetrics } from '../../../components/hooks/useMetrics'; import { selectInternalAccounts } from '../../../selectors/accountsController'; import { selectPermissionControllerState } from '../../../selectors/snaps/permissionController'; import { RootState } from '../../../reducers'; -import { isMultichainVersion1Enabled } from '../../../util/networks'; +import { + isMultichainVersion1Enabled, + getNetworkImageSource, +} from '../../../util/networks'; import PermissionsSummary from '../../../components/UI/PermissionsSummary'; import { PermissionsSummaryProps } from '../../../components/UI/PermissionsSummary/PermissionsSummary.types'; import { toChecksumHexAddress } from '@metamask/controller-utils'; +import { PermissionKeys } from '../../../core/Permissions/specifications'; +import { CaveatTypes } from '../../../core/Permissions/constants'; +import { NetworkConfiguration } from '@metamask/network-controller'; +import { AvatarVariant } from '../../../component-library/components/Avatars/Avatar'; const AccountPermissions = (props: AccountPermissionsProps) => { const navigation = useNavigation(); @@ -104,6 +111,11 @@ const AccountPermissions = (props: AccountPermissionsProps) => { hostname, ); const [selectedAddresses, setSelectedAddresses] = useState([]); + const [networkAvatars, setNetworkAvatars] = useState< + ({ name: string; imageSource: string } | null)[] + >([]); + const networkConfigurations = useSelector(selectNetworkConfigurations); + const sheetRef = useRef(null); const [permissionsScreen, setPermissionsScreen] = useState(initialScreen); @@ -116,6 +128,62 @@ const AccountPermissions = (props: AccountPermissionsProps) => { const activeAddress: string = permittedAccountsByHostname[0]; const [userIntent, setUserIntent] = useState(USER_INTENT.None); + const [networkSelectorUserIntent, setNetworkSelectorUserIntent] = useState( + USER_INTENT.None, + ); + + useEffect(() => { + let currentlyPermittedChains: string[] = []; + try { + const caveat = Engine.context.PermissionController.getCaveat( + hostname, + PermissionKeys.permittedChains, + CaveatTypes.restrictNetworkSwitching, + ); + if (Array.isArray(caveat?.value)) { + currentlyPermittedChains = caveat.value.filter( + (item): item is string => typeof item === 'string', + ); + } + } catch (e) { + // noop + } + + const networks = Object.entries(networkConfigurations).map( + ([key, network]: [string, NetworkConfiguration]) => ({ + id: key, + name: network.name, + rpcUrl: network.rpcEndpoints[network.defaultRpcEndpointIndex].url, + isSelected: false, + chainId: network?.chainId, + //@ts-expect-error - The utils/network file is still JS and this function expects a networkType, and should be optional + imageSource: getNetworkImageSource({ + chainId: network?.chainId, + }), + }), + ); + + const theNetworkAvatars: ({ name: string; imageSource: string } | null)[] = + currentlyPermittedChains.map((selectedId) => { + const network = networks.find(({ id }) => id === selectedId); + if (network) { + return { + name: network.name, + imageSource: network.imageSource as string, + variant: AvatarVariant.Network, + }; + } + return null; + }); + + if ( + [USER_INTENT.None, USER_INTENT.Confirm].includes( + networkSelectorUserIntent, + ) + ) { + setNetworkAvatars(theNetworkAvatars); + } + }, [hostname, networkConfigurations, networkSelectorUserIntent]); const hideSheet = useCallback( (callback?: () => void) => @@ -232,13 +300,11 @@ const AccountPermissions = (props: AccountPermissionsProps) => { setIsLoading(true); let newActiveAddress; let connectedAccountLength = 0; - let addedAccountCount = 0; let removedAccountCount = 0; if (!isMultichainVersion1Enabled) { newActiveAddress = addPermittedAccounts(hostname, selectedAddresses); connectedAccountLength = selectedAddresses.length; - addedAccountCount = selectedAddresses.length; } else { // Function to normalize Ethereum addresses using checksum const normalizeAddresses = (addresses: string[]) => @@ -263,7 +329,6 @@ const AccountPermissions = (props: AccountPermissionsProps) => { accountsToAdd = normalizedSelectedAddresses.filter( (account) => !normalizedPermittedAccounts.includes(account), ); - addedAccountCount = accountsToAdd.length; // Add newly selected accounts if (accountsToAdd.length > 0) { @@ -273,7 +338,6 @@ const AccountPermissions = (props: AccountPermissionsProps) => { newActiveAddress = normalizedSelectedAddresses[0]; } - // if (!isFirstRenderOfEditingAllAccountPermissions.current) { // Identify accounts to be removed accountsToRemove = normalizedPermittedAccounts.filter( (account) => !normalizedSelectedAddresses.includes(account), @@ -284,7 +348,6 @@ const AccountPermissions = (props: AccountPermissionsProps) => { if (accountsToRemove.length > 0) { removePermittedAccounts(hostname, accountsToRemove); } - // } // Calculate the number of connected accounts after changes connectedAccountLength = @@ -302,34 +365,9 @@ const AccountPermissions = (props: AccountPermissionsProps) => { let labelOptions: ToastOptions['labelOptions'] = []; // Start of Selection if (connectedAccountLength >= 1) { - if (addedAccountCount > 0) { - labelOptions = [ - { label: `${addedAccountCount} `, isBold: true }, - { - label: `${strings( - addedAccountCount > 1 - ? 'toast.accounts_connected' - : 'toast.account_connected', - )}\n`, - }, - ]; - } - if (removedAccountCount > 0) { - labelOptions.push( - { label: `${removedAccountCount} `, isBold: true }, - { - label: `${strings( - removedAccountCount > 1 - ? 'toast.accounts_disconnected' - : 'toast.account_disconnected', - )}\n`, - }, - ); - } - labelOptions.push( - { label: `${activeAccountName} `, isBold: true }, - { label: strings('toast.now_active') }, - ); + labelOptions = [ + { label: `${strings('toast.accounts_permissions_updated')}` }, + ]; } if (connectedAccountLength === 1 && removedAccountCount === 0) { @@ -371,8 +409,29 @@ const AccountPermissions = (props: AccountPermissionsProps) => { ]); useEffect(() => { - if (userIntent === USER_INTENT.None) return; + if (!isMultichainVersion1Enabled) { + return; + } + + if (networkSelectorUserIntent === USER_INTENT.Confirm) { + setPermissionsScreen(AccountPermissionsScreens.PermissionsSummary); + setNetworkSelectorUserIntent(USER_INTENT.None); + const networkToastProps: ToastOptions = { + variant: ToastVariants.Network, + labelOptions: [ + { + label: strings('toast.network_permissions_updated'), + }, + ], + hasNoTimeout: false, + networkImageSource: faviconSource, + }; + toastRef?.current?.showToast(networkToastProps); + } + }, [networkSelectorUserIntent, hideSheet, faviconSource, toastRef]); + useEffect(() => { + if (userIntent === USER_INTENT.None) return; const handleUserActions = (action: USER_INTENT) => { switch (action) { case USER_INTENT.Confirm: { @@ -488,6 +547,8 @@ const AccountPermissions = (props: AccountPermissionsProps) => { : navigation.navigate('PermissionsManager'), isRenderedAsBottomSheet, accountAddresses: checksummedPermittedAddresses, + accounts, + networkAvatars, }; return ; @@ -498,6 +559,8 @@ const AccountPermissions = (props: AccountPermissionsProps) => { navigation, permittedAccountsByHostname, setSelectedAddresses, + networkAvatars, + accounts, ]); const renderEditAccountsPermissionsScreen = useCallback( @@ -587,7 +650,7 @@ const AccountPermissions = (props: AccountPermissionsProps) => { @@ -598,7 +661,7 @@ const AccountPermissions = (props: AccountPermissionsProps) => { ), [ isLoading, - setUserIntent, + setNetworkSelectorUserIntent, urlWithProtocol, hostname, isRenderedAsBottomSheet, diff --git a/app/components/Views/NetworkConnect/NetworkConnectMultiSelector/NetworkConnectMultiSelector.tsx b/app/components/Views/NetworkConnect/NetworkConnectMultiSelector/NetworkConnectMultiSelector.tsx index 181b86d44d7..d44f7b3bea5 100644 --- a/app/components/Views/NetworkConnect/NetworkConnectMultiSelector/NetworkConnectMultiSelector.tsx +++ b/app/components/Views/NetworkConnect/NetworkConnectMultiSelector/NetworkConnectMultiSelector.tsx @@ -1,6 +1,10 @@ // Third party dependencies. -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useState, useEffect } from 'react'; import { Platform, SafeAreaView, View } from 'react-native'; +import { useSelector } from 'react-redux'; +import { useNavigation } from '@react-navigation/native'; +import { NetworkConfiguration } from '@metamask/network-controller'; +import { isEqual } from 'lodash'; // External dependencies. import { strings } from '../../../../../locales/i18n'; @@ -10,14 +14,12 @@ import Button, { ButtonVariants, } from '../../../../component-library/components/Buttons/Button'; import SheetHeader from '../../../../component-library/components/Sheet/SheetHeader'; -import { useNavigation } from '@react-navigation/native'; import { useStyles } from '../../../../component-library/hooks'; import { USER_INTENT } from '../../../../constants/permissions'; import HelpText, { HelpTextSeverity, } from '../../../../component-library/components/Form/HelpText'; -import { Network } from '../../../../components/UI/NetworkSelectorList/NetworkSelectorList.types'; // Internal dependencies. import ConnectNetworkModalSelectorsIDs from '../../../../../e2e/selectors/Modals/ConnectNetworkModal.selectors'; @@ -26,7 +28,11 @@ import { NetworkConnectMultiSelectorProps } from './NetworkConnectMultiSelector. import Routes from '../../../../constants/navigation/Routes'; import Checkbox from '../../../../component-library/components/Checkbox'; import NetworkSelectorList from '../../../UI/NetworkSelectorList/NetworkSelectorList'; -import { PopularList } from '../../../../util/networks/customNetworks'; +import { selectNetworkConfigurations } from '../../../../selectors/networkController'; +import Engine from '../../../../core/Engine'; +import { PermissionKeys } from '../../../../core/Permissions/specifications'; +import { CaveatTypes } from '../../../../core/Permissions/constants'; +import { getNetworkImageSource } from '../../../../util/networks'; const NetworkConnectMultiSelector = ({ isLoading, @@ -35,34 +41,122 @@ const NetworkConnectMultiSelector = ({ hostname, onBack, isRenderedAsBottomSheet = true, + onNetworksSelected, + initialChainId, + selectedChainIds: propSelectedChainIds, + isInitializedWithPermittedChains = true, }: NetworkConnectMultiSelectorProps) => { const { styles } = useStyles(styleSheet, { isRenderedAsBottomSheet }); const { navigate } = useNavigation(); - const [selectedNetworkIds, setSelectedNetworkIds] = useState([]); + const [selectedChainIds, setSelectedChainIds] = useState([]); + const [originalChainIds, setOriginalChainIds] = useState([]); + const networkConfigurations = useSelector(selectNetworkConfigurations); + + useEffect(() => { + if (propSelectedChainIds && !isInitializedWithPermittedChains) { + setSelectedChainIds(propSelectedChainIds); + setOriginalChainIds(propSelectedChainIds); + } + }, [propSelectedChainIds, isInitializedWithPermittedChains]); + + useEffect(() => { + if (!isInitializedWithPermittedChains) return; + + let currentlyPermittedChains: string[] = []; + try { + const caveat = Engine.context.PermissionController.getCaveat( + hostname, + PermissionKeys.permittedChains, + CaveatTypes.restrictNetworkSwitching, + ); + if (Array.isArray(caveat?.value)) { + currentlyPermittedChains = caveat.value.filter( + (item): item is string => typeof item === 'string', + ); + } + } catch (e) { + // noop + } + + if (currentlyPermittedChains.length === 0 && initialChainId) { + currentlyPermittedChains = [initialChainId]; + } - const mockNetworks: Network[] = PopularList.map((network) => ({ - id: network.chainId, - name: network.nickname, - rpcUrl: network.rpcUrl, - isSelected: false, - imageSource: network.rpcPrefs.imageSource, - })); + setSelectedChainIds(currentlyPermittedChains); + setOriginalChainIds(currentlyPermittedChains); + }, [hostname, isInitializedWithPermittedChains, initialChainId]); + + const handleUpdateNetworkPermissions = useCallback(async () => { + if (onNetworksSelected) { + onNetworksSelected(selectedChainIds); + } else { + let hasPermittedChains = false; + try { + hasPermittedChains = Engine.context.PermissionController.hasCaveat( + hostname, + PermissionKeys.permittedChains, + CaveatTypes.restrictNetworkSwitching, + ); + } catch { + // noop + } + + if (hasPermittedChains) { + Engine.context.PermissionController.updateCaveat( + hostname, + PermissionKeys.permittedChains, + CaveatTypes.restrictNetworkSwitching, + selectedChainIds, + ); + } else { + Engine.context.PermissionController.grantPermissionsIncremental({ + subject: { + origin: hostname, + }, + approvedPermissions: { + [PermissionKeys.permittedChains]: { + caveats: [ + { + type: CaveatTypes.restrictNetworkSwitching, + value: selectedChainIds, + }, + ], + }, + }, + }); + } + onUserAction(USER_INTENT.Confirm); + } + }, [selectedChainIds, hostname, onUserAction, onNetworksSelected]); + + const networks = Object.entries(networkConfigurations).map( + ([key, network]: [string, NetworkConfiguration]) => ({ + id: key, + name: network.name, + rpcUrl: network.rpcEndpoints[network.defaultRpcEndpointIndex].url, + isSelected: false, + //@ts-expect-error - The utils/network file is still JS and this function expects a networkType, and should be optional + imageSource: getNetworkImageSource({ + chainId: network?.chainId, + }), + }), + ); const onSelectNetwork = useCallback( - (clickedNetworkId) => { - const selectedAddressIndex = selectedNetworkIds.indexOf(clickedNetworkId); + (clickedChainId) => { + const selectedAddressIndex = selectedChainIds.indexOf(clickedChainId); // Reconstruct selected network ids. - const newNetworkList = mockNetworks.reduce((acc, { id }) => { - if (clickedNetworkId === id) { + const newNetworkList = networks.reduce((acc, { id }) => { + if (clickedChainId === id) { selectedAddressIndex === -1 && acc.push(id); - } else if (selectedNetworkIds.includes(id)) { + } else if (selectedChainIds.includes(id)) { acc.push(id); } return acc; }, [] as string[]); - setSelectedNetworkIds(newNetworkList); + setSelectedChainIds(newNetworkList); }, - [mockNetworks, selectedNetworkIds], + [networks, selectedChainIds], ); const toggleRevokeAllNetworkPermissionsModal = useCallback(() => { @@ -77,13 +171,12 @@ const NetworkConnectMultiSelector = ({ }, }); }, [navigate, urlWithProtocol]); - - const areAllNetworksSelected = mockNetworks + const areAllNetworksSelected = networks .map(({ id }) => id) - .every((id) => selectedNetworkIds.includes(id)); + .every((id) => selectedChainIds?.includes(id)); - const areAnyNetworksSelected = selectedNetworkIds?.length !== 0; - const areNoNetworksSelected = selectedNetworkIds?.length === 0; + const areAnyNetworksSelected = selectedChainIds?.length !== 0; + const areNoNetworksSelected = selectedChainIds?.length === 0; const renderSelectAllCheckbox = useCallback((): React.JSX.Element | null => { const areSomeNetworksSelectedButNotAll = @@ -91,13 +184,13 @@ const NetworkConnectMultiSelector = ({ const selectAll = () => { if (isLoading) return; - const allSelectedNetworkIds = mockNetworks.map(({ id }) => id); - setSelectedNetworkIds(allSelectedNetworkIds); + const allSelectedChainIds = networks.map(({ id }) => id); + setSelectedChainIds(allSelectedChainIds); }; const unselectAll = () => { if (isLoading) return; - setSelectedNetworkIds([]); + setSelectedChainIds([]); }; const onPress = () => { @@ -118,29 +211,32 @@ const NetworkConnectMultiSelector = ({ }, [ areAllNetworksSelected, areAnyNetworksSelected, - mockNetworks, + networks, isLoading, - setSelectedNetworkIds, + setSelectedChainIds, styles.selectAllContainer, ]); - const renderCtaButtons = useCallback(() => { - const isConnectDisabled = Boolean(!selectedNetworkIds.length) || isLoading; + const isUpdateDisabled = + selectedChainIds.length === 0 || + isLoading || + isEqual(selectedChainIds, originalChainIds); - return ( + const renderCtaButtons = useCallback( + () => ( {areAnyNetworksSelected && (