From 281aea63f168d4ff1c07f180b61f2062763cb91c Mon Sep 17 00:00:00 2001 From: EtherWizard33 Date: Tue, 1 Oct 2024 14:12:21 -0600 Subject: [PATCH 01/45] feat: (wip) replace some the in-app browser mocked permissions with real ones --- .../PermissionItem/PermissionItem.tsx | 4 +- .../PermissionsManager.tsx | 74 +++++++++++++++---- 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/app/components/Views/Settings/PermissionsSettings/PermissionItem/PermissionItem.tsx b/app/components/Views/Settings/PermissionsSettings/PermissionItem/PermissionItem.tsx index d0eb1bc2dfc..03622bbb28b 100644 --- a/app/components/Views/Settings/PermissionsSettings/PermissionItem/PermissionItem.tsx +++ b/app/components/Views/Settings/PermissionsSettings/PermissionItem/PermissionItem.tsx @@ -40,14 +40,14 @@ const PermissionItem: React.FC = ({ - {item.numberOfAccountPermissions} + {item.numberOfAccountPermissions}{' '} {item.numberOfAccountPermissions > 1 ? strings('app_settings.accounts') : strings('app_settings.account')} - {item.numberOfNetworkPermissions} + {item.numberOfNetworkPermissions}{' '} {item.numberOfNetworkPermissions > 1 ? strings('app_settings.networks') : strings('app_settings.network')} diff --git a/app/components/Views/Settings/PermissionsSettings/PermissionsManager.tsx b/app/components/Views/Settings/PermissionsSettings/PermissionsManager.tsx index 0e5f26cb4ff..9cfa1e5ed85 100644 --- a/app/components/Views/Settings/PermissionsSettings/PermissionsManager.tsx +++ b/app/components/Views/Settings/PermissionsSettings/PermissionsManager.tsx @@ -1,4 +1,5 @@ -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; import { ScrollView, StyleSheet, View } from 'react-native'; import { EdgeInsets, useSafeAreaInsets } from 'react-native-safe-area-context'; import { strings } from '../../../../../locales/i18n'; @@ -17,8 +18,12 @@ import Text, { import { isMultichainVersion1Enabled } from '../../../../util/networks'; import { getNavigationOptionsTitle } from '../../../UI/Navbar'; import PermissionItem from './PermissionItem'; -import mockPermissionItems from './PermissionItem/PermissionItem.constants'; import { NavigationProp, ParamListBase } from '@react-navigation/native'; +import { RootState } from '../../../../reducers'; +import { + PermissionListItemViewModel, + PermissionSource, +} from './PermissionItem/PermissionItem.types'; interface SDKSessionsManagerProps { navigation: NavigationProp; @@ -53,6 +58,44 @@ const PermissionsManager = (props: SDKSessionsManagerProps) => { const { colors, typography } = useTheme(); const styles = createStyles(colors, typography, safeAreaInsets); const { navigation } = props; + const [inAppBrowserPermissions, setInAppBrowserPermissions] = useState< + PermissionListItemViewModel[] + >([]); + const subjects = useSelector((state: RootState) => { + return state.engine.backgroundState.PermissionController.subjects; + }); + + useEffect(() => { + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + const uuidSubjects: any[] = []; + const otherSubjects: any[] = []; + + Object.entries(subjects || {}).forEach(([key, value]) => { + if (key === 'npm:@metamask/message-signing-snap') return; + + if (uuidRegex.test(key)) { + uuidSubjects.push(value); + } else { + otherSubjects.push(value); + } + }); + + const mappedPermissions: PermissionListItemViewModel[] = otherSubjects.map( + (subject) => ({ + dappLogoUrl: '', + dappHostName: subject.origin, + numberOfAccountPermissions: + subject.permissions?.eth_accounts?.caveats?.[0]?.value?.length ?? 0, + numberOfNetworkPermissions: 0, + permissionSource: PermissionSource.MetaMaskBrowser, + }), + ); + + setInAppBrowserPermissions(mappedPermissions); + // console.log('>>> uuidSubjects: ', JSON.stringify(uuidSubjects)); + // console.log('>>> mappedPermissions: ', JSON.stringify(mappedPermissions)); + }, [subjects]); useEffect(() => { navigation.setOptions( @@ -80,21 +123,22 @@ const PermissionsManager = (props: SDKSessionsManagerProps) => { () => ( <> - { - /* TODO: replace mock data with real data once available */ - isMultichainVersion1Enabled && - mockPermissionItems.map((mockPermissionItem, _index) => ( - - )) - } + {isMultichainVersion1Enabled && + inAppBrowserPermissions.map((permissionItem, index) => ( + + ))} ), - [goToPermissionsDetails], + [ + goToPermissionsDetails, + inAppBrowserPermissions, + isMultichainVersion1Enabled, + ], ); const renderEmptyResult = () => ( @@ -114,7 +158,7 @@ const PermissionsManager = (props: SDKSessionsManagerProps) => { style={styles.perissionsWrapper} testID={SDKSelectorsIDs.SESSION_MANAGER_CONTAINER} > - {isMultichainVersion1Enabled && mockPermissionItems.length + {isMultichainVersion1Enabled && inAppBrowserPermissions.length ? renderPermissions() : renderEmptyResult()} From fc8f462eb97c9585272c047ee28e94412338d622 Mon Sep 17 00:00:00 2001 From: EtherWizard33 Date: Wed, 2 Oct 2024 18:05:06 -0600 Subject: [PATCH 02/45] feat: add the in sdk permissions --- .../PermissionsManager.tsx | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/app/components/Views/Settings/PermissionsSettings/PermissionsManager.tsx b/app/components/Views/Settings/PermissionsSettings/PermissionsManager.tsx index 9cfa1e5ed85..055d25d1e27 100644 --- a/app/components/Views/Settings/PermissionsSettings/PermissionsManager.tsx +++ b/app/components/Views/Settings/PermissionsSettings/PermissionsManager.tsx @@ -81,16 +81,30 @@ const PermissionsManager = (props: SDKSessionsManagerProps) => { } }); - const mappedPermissions: PermissionListItemViewModel[] = otherSubjects.map( - (subject) => ({ + const mappedOtherPermissions: PermissionListItemViewModel[] = + otherSubjects.map((subject) => ({ dappLogoUrl: '', dappHostName: subject.origin, numberOfAccountPermissions: subject.permissions?.eth_accounts?.caveats?.[0]?.value?.length ?? 0, numberOfNetworkPermissions: 0, permissionSource: PermissionSource.MetaMaskBrowser, - }), - ); + })); + + const mappedUuidPermissions: PermissionListItemViewModel[] = + uuidSubjects.map((subject) => ({ + dappLogoUrl: '', + dappHostName: subject.origin, + numberOfAccountPermissions: + subject.permissions?.eth_accounts?.caveats?.[0]?.value?.length ?? 0, + numberOfNetworkPermissions: 0, + permissionSource: PermissionSource.SDK, + })); + + const mappedPermissions: PermissionListItemViewModel[] = [ + ...mappedOtherPermissions, + ...mappedUuidPermissions, + ]; setInAppBrowserPermissions(mappedPermissions); // console.log('>>> uuidSubjects: ', JSON.stringify(uuidSubjects)); From 393f97dddb6a00c60215d38b0207b5c497b26878 Mon Sep 17 00:00:00 2001 From: EtherWizard33 Date: Wed, 2 Oct 2024 19:13:23 -0600 Subject: [PATCH 03/45] feat: get the permission from subject and use regex to attemtp to filter the sdk versus in app browser vs wallet connect, for local testing only, prod will require a real source property --- .../PermissionItem.test.tsx.snap | 2 + .../PermissionsManager.tsx | 42 +++++++++++++------ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/app/components/Views/Settings/PermissionsSettings/PermissionItem/__snapshots__/PermissionItem.test.tsx.snap b/app/components/Views/Settings/PermissionsSettings/PermissionItem/__snapshots__/PermissionItem.test.tsx.snap index 26274e0fa98..0b20f44179f 100644 --- a/app/components/Views/Settings/PermissionsSettings/PermissionItem/__snapshots__/PermissionItem.test.tsx.snap +++ b/app/components/Views/Settings/PermissionsSettings/PermissionItem/__snapshots__/PermissionItem.test.tsx.snap @@ -133,6 +133,7 @@ exports[`PermissionItem renders correctly 1`] = ` } > 5 + accounts 2 + networks diff --git a/app/components/Views/Settings/PermissionsSettings/PermissionsManager.tsx b/app/components/Views/Settings/PermissionsSettings/PermissionsManager.tsx index 055d25d1e27..98aa02f7899 100644 --- a/app/components/Views/Settings/PermissionsSettings/PermissionsManager.tsx +++ b/app/components/Views/Settings/PermissionsSettings/PermissionsManager.tsx @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import React, { useCallback, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { ScrollView, StyleSheet, View } from 'react-native'; @@ -61,28 +63,35 @@ const PermissionsManager = (props: SDKSessionsManagerProps) => { const [inAppBrowserPermissions, setInAppBrowserPermissions] = useState< PermissionListItemViewModel[] >([]); - const subjects = useSelector((state: RootState) => { - return state.engine.backgroundState.PermissionController.subjects; - }); + const subjects = useSelector( + (state: RootState) => + state.engine.backgroundState.PermissionController.subjects, + ); useEffect(() => { const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + const walletConnectRegex = /^https?:\/\//; const uuidSubjects: any[] = []; - const otherSubjects: any[] = []; + const inAppBrowserSubjects: any[] = []; + const walletConnectSubjects: any[] = []; Object.entries(subjects || {}).forEach(([key, value]) => { if (key === 'npm:@metamask/message-signing-snap') return; if (uuidRegex.test(key)) { uuidSubjects.push(value); + } else if ( + walletConnectRegex.test((value as { origin: string }).origin) + ) { + walletConnectSubjects.push(value); } else { - otherSubjects.push(value); + inAppBrowserSubjects.push(value); } }); - const mappedOtherPermissions: PermissionListItemViewModel[] = - otherSubjects.map((subject) => ({ + const mappedInAppBrowserPermissions: PermissionListItemViewModel[] = + inAppBrowserSubjects.map((subject) => ({ dappLogoUrl: '', dappHostName: subject.origin, numberOfAccountPermissions: @@ -101,9 +110,20 @@ const PermissionsManager = (props: SDKSessionsManagerProps) => { permissionSource: PermissionSource.SDK, })); + const mappedWalletConnectPermissions: PermissionListItemViewModel[] = + walletConnectSubjects.map((subject) => ({ + dappLogoUrl: '', + dappHostName: subject.origin, + numberOfAccountPermissions: + subject.permissions?.eth_accounts?.caveats?.[0]?.value?.length ?? 0, + numberOfNetworkPermissions: 0, + permissionSource: PermissionSource.WalletConnect, + })); + const mappedPermissions: PermissionListItemViewModel[] = [ - ...mappedOtherPermissions, + ...mappedInAppBrowserPermissions, ...mappedUuidPermissions, + ...mappedWalletConnectPermissions, ]; setInAppBrowserPermissions(mappedPermissions); @@ -148,11 +168,7 @@ const PermissionsManager = (props: SDKSessionsManagerProps) => { ), - [ - goToPermissionsDetails, - inAppBrowserPermissions, - isMultichainVersion1Enabled, - ], + [goToPermissionsDetails, inAppBrowserPermissions], ); const renderEmptyResult = () => ( From 8f2c5cf05df2e3f75a665b119a092faad1dd4889 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 15 Oct 2024 08:58:59 -0500 Subject: [PATCH 04/45] next steps for integration --- .../NetworkSelectorList.tsx | 9 +- .../NetworkSelectorList.types.ts | 2 +- .../AccountPermissions/AccountPermissions.tsx | 14 +- .../NetworkConnectMultiSelector.tsx | 139 +++++++++++++----- .../RPCMethods/lib/ethereum-chain-utils.js | 2 +- .../RPCMethods/wallet_switchEthereumChain.js | 1 - app/selectors/networkController.ts | 5 +- 7 files changed, 121 insertions(+), 51 deletions(-) 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/Views/AccountPermissions/AccountPermissions.tsx b/app/components/Views/AccountPermissions/AccountPermissions.tsx index 7db1f844369..c85b0eba071 100755 --- a/app/components/Views/AccountPermissions/AccountPermissions.tsx +++ b/app/components/Views/AccountPermissions/AccountPermissions.tsx @@ -116,6 +116,9 @@ const AccountPermissions = (props: AccountPermissionsProps) => { const activeAddress: string = permittedAccountsByHostname[0]; const [userIntent, setUserIntent] = useState(USER_INTENT.None); + const [networkSelectorUserIntent, setNetworkSelectorUserIntent] = useState( + USER_INTENT.None, + ); const hideSheet = useCallback( (callback?: () => void) => @@ -371,8 +374,13 @@ const AccountPermissions = (props: AccountPermissionsProps) => { ]); useEffect(() => { - if (userIntent === USER_INTENT.None) return; + if (networkSelectorUserIntent === USER_INTENT.Confirm) { + hideSheet(); + } + }, [networkSelectorUserIntent, hideSheet]); + useEffect(() => { + if (userIntent === USER_INTENT.None) return; const handleUserActions = (action: USER_INTENT) => { switch (action) { case USER_INTENT.Confirm: { @@ -586,7 +594,7 @@ const AccountPermissions = (props: AccountPermissionsProps) => { @@ -597,7 +605,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..80977152295 100644 --- a/app/components/Views/NetworkConnect/NetworkConnectMultiSelector/NetworkConnectMultiSelector.tsx +++ b/app/components/Views/NetworkConnect/NetworkConnectMultiSelector/NetworkConnectMultiSelector.tsx @@ -1,6 +1,9 @@ // 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'; // External dependencies. import { strings } from '../../../../../locales/i18n'; @@ -10,14 +13,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 +27,10 @@ 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'; const NetworkConnectMultiSelector = ({ isLoading, @@ -38,34 +42,95 @@ const NetworkConnectMultiSelector = ({ }: NetworkConnectMultiSelectorProps) => { const { styles } = useStyles(styleSheet, { isRenderedAsBottomSheet }); const { navigate } = useNavigation(); - const [selectedNetworkIds, setSelectedNetworkIds] = useState([]); + const [selectedChainIds, setSelectedChainIds] = useState([]); + const networkConfigurations = useSelector(selectNetworkConfigurations); + + useEffect(() => { + let currentlyPermittedChains; + try { + currentlyPermittedChains = Engine.context.PermissionController.getCaveat( + hostname, + PermissionKeys.permittedChains, + CaveatTypes.restrictNetworkSwitching, + ); + } catch (e) { + // noop + } - const mockNetworks: Network[] = PopularList.map((network) => ({ - id: network.chainId, - name: network.nickname, - rpcUrl: network.rpcUrl, - isSelected: false, - imageSource: network.rpcPrefs.imageSource, - })); + setSelectedChainIds(currentlyPermittedChains?.value || []); + }, [hostname]); + + const handleUpdateNetworkPermissions = useCallback(async () => { + 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]); + const networks = Object.entries(networkConfigurations).map( + ([key, network]: [string, NetworkConfiguration]) => ({ + id: key, + name: network.name, + rpcUrl: network.rpcEndpoints[network.defaultRpcEndpointIndex].url, + isSelected: false, + imageSource: '', // TODO not sure where to get image sources + }), + ); 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(() => { + // not sure if we want to do this here or on the sub modal + // which provides the extra warning that it will fully disconnect you + Engine.context.PermissionController.revokePermissions({ + [hostname]: [PermissionKeys.permittedChains, PermissionKeys.eth_accounts], + }); + navigate(Routes.MODAL.ROOT_MODAL_FLOW, { screen: Routes.SHEET.REVOKE_ALL_ACCOUNT_PERMISSIONS, params: { @@ -76,14 +141,14 @@ const NetworkConnectMultiSelector = ({ }, }, }); - }, [navigate, urlWithProtocol]); + }, [navigate, urlWithProtocol, hostname]); - 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 +156,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,14 +183,14 @@ const NetworkConnectMultiSelector = ({ }, [ areAllNetworksSelected, areAnyNetworksSelected, - mockNetworks, + networks, isLoading, - setSelectedNetworkIds, + setSelectedChainIds, styles.selectAllContainer, ]); const renderCtaButtons = useCallback(() => { - const isConnectDisabled = Boolean(!selectedNetworkIds.length) || isLoading; + const isConnectDisabled = Boolean(!selectedChainIds.length) || isLoading; return ( @@ -134,7 +199,7 @@ const NetworkConnectMultiSelector = ({