From 5e5190fa6aa2832c5771e7ff9fbdaaae0fdb448a Mon Sep 17 00:00:00 2001 From: Jonathan Ferreira <44679989+Jonathansoufer@users.noreply.github.com> Date: Mon, 21 Oct 2024 11:25:24 +0100 Subject: [PATCH 1/2] fix: accounts not syncing between devices bug (#11801) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR fixed an issue preventing mobile accounts to sync its states (enable/disable) with other clients (platform and extension). [NOTIFY-1218](https://consensyssoftware.atlassian.net/browse/NOTIFY-1218?atlOrigin=eyJpIjoiZTRlYTlkNjVjOGVjNDAyYTljNmM3YTg2NjBiYzllNGYiLCJwIjoiaiJ9) ## **Related issues** Fixes: ## **Manual testing steps** 1. Import the same SRP on Mobile 2. Make sure all accounts are visible on both devices/platforms. If not, import all accounts 3. Switch Notifications state in account "A" on extension. 4. Go to Notifications Settings on mobile and see if the state is propagated. 5. Switch back on mobile 6. Go to extension and see if state is propagated. ## **Screenshots/Recordings** ### **Before** ### **After** https://github.com/user-attachments/assets/860e2e46-f08a-45fd-b965-a9948ef59630 ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. [NOTIFY-1218]: https://consensyssoftware.atlassian.net/browse/NOTIFY-1218?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .../AccountsList.test.tsx | 88 +++ .../NotificationsSettings/AccountsList.tsx | 53 ++ .../NotificationOptionToggle/index.tsx | 8 +- .../NotificationsSettings.styles.ts | 9 +- .../__snapshots__/AccountsList.test.tsx.snap | 520 ++++++++++++++++++ .../Settings/NotificationsSettings/index.tsx | 69 +-- app/core/redux/slices/notifications/index.ts | 9 +- .../hooks/useSwitchNotifications.test.tsx | 134 +++-- .../hooks/useSwitchNotifications.ts | 104 +--- .../hooks/useUpdateAccountSetting.tsx | 12 +- 10 files changed, 807 insertions(+), 199 deletions(-) create mode 100644 app/components/Views/Settings/NotificationsSettings/AccountsList.test.tsx create mode 100644 app/components/Views/Settings/NotificationsSettings/AccountsList.tsx create mode 100644 app/components/Views/Settings/NotificationsSettings/__snapshots__/AccountsList.test.tsx.snap diff --git a/app/components/Views/Settings/NotificationsSettings/AccountsList.test.tsx b/app/components/Views/Settings/NotificationsSettings/AccountsList.test.tsx new file mode 100644 index 00000000000..aa3d30a0ee7 --- /dev/null +++ b/app/components/Views/Settings/NotificationsSettings/AccountsList.test.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import renderWithProvider, { DeepPartial } from '../../../../util/test/renderWithProvider'; +import { AccountsList } from './AccountsList'; +import { AvatarAccountType } from '../../../../component-library/components/Avatars/Avatar'; +import { Account } from '../../../../components/hooks/useAccounts/useAccounts.types'; +import { MOCK_ACCOUNTS_CONTROLLER_STATE } from '../../../../util/test/accountsControllerTestUtils'; +import { Hex } from '@metamask/utils'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import { toChecksumAddress } from 'ethereumjs-util'; +import { RootState } from '../../../../reducers'; +import { backgroundState } from '../../../../util/test/initial-root-state'; + +const MOCK_ACCOUNT_ADDRESSES = Object.values( + MOCK_ACCOUNTS_CONTROLLER_STATE.internalAccounts.accounts, +).map((account) => account.address); + +const MOCK_ACCOUNT_1: Account = { + name: 'Account 1', + address: toChecksumAddress(MOCK_ACCOUNT_ADDRESSES[0]) as Hex, + type: KeyringTypes.hd, + yOffset: 0, + isSelected: false, + assets: { + fiatBalance: '\n0 ETH', + }, + balanceError: undefined, +}; +const MOCK_ACCOUNT_2: Account = { + name: 'Account 2', + address: toChecksumAddress(MOCK_ACCOUNT_ADDRESSES[1]) as Hex, + type: KeyringTypes.hd, + yOffset: 78, + isSelected: true, + assets: { + fiatBalance: '\n< 0.00001 ETH', + }, + balanceError: undefined, +}; + +const MOCK_ACCOUNTS = [MOCK_ACCOUNT_1, MOCK_ACCOUNT_2]; + +const mockInitialState: DeepPartial = { + engine: { + backgroundState: { + ...backgroundState, + NotificationServicesController: { + metamaskNotificationsList: [], + }, + }, + }, +}; + +describe('AccountsList', () => { + it('matches snapshot', () => { + + const { toJSON } = renderWithProvider( + , + { + state: mockInitialState, + }, + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('triggers updateAndfetchAccountSettings on mount', () => { + const updateAndfetchAccountSettings = jest.fn(); + renderWithProvider( + , + { + state: mockInitialState, + }, + ); + + expect(updateAndfetchAccountSettings).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app/components/Views/Settings/NotificationsSettings/AccountsList.tsx b/app/components/Views/Settings/NotificationsSettings/AccountsList.tsx new file mode 100644 index 00000000000..8dded276cf9 --- /dev/null +++ b/app/components/Views/Settings/NotificationsSettings/AccountsList.tsx @@ -0,0 +1,53 @@ +import React, { useEffect } from 'react'; +import { FlatList, View } from 'react-native'; +import NotificationOptionToggle from './NotificationOptionToggle'; +import { Account } from '../../../../components/hooks/useAccounts/useAccounts.types'; +import { NotificationsToggleTypes } from './NotificationsSettings.constants'; +import { NotificationsAccountsState } from '../../../../core/redux/slices/notifications'; +import { AvatarAccountType } from '../../../../component-library/components/Avatars/Avatar'; + +export const AccountsList = ({ + accounts, + accountAvatarType, + accountSettingsData, + updateAndfetchAccountSettings, + isUpdatingMetamaskNotificationsAccount, +}: { + accounts: Account[]; + accountAvatarType: AvatarAccountType; + accountSettingsData: NotificationsAccountsState; + updateAndfetchAccountSettings: () => Promise | undefined>; + isUpdatingMetamaskNotificationsAccount: string[]; +}) => { + + useEffect(() => { + const fetchInitialData = async () => { + await updateAndfetchAccountSettings(); + }; + fetchInitialData(); + }, [updateAndfetchAccountSettings]); + + return ( + + `address-${item.address}`} + renderItem={({ item }) => ( + 0} + isLoading={isUpdatingMetamaskNotificationsAccount.includes( + item.address.toLowerCase(), + )} + isEnabled={accountSettingsData?.[item.address.toLowerCase()]} + updateAndfetchAccountSettings={updateAndfetchAccountSettings} + /> + )} + /> + + ); +}; diff --git a/app/components/Views/Settings/NotificationsSettings/NotificationOptionToggle/index.tsx b/app/components/Views/Settings/NotificationsSettings/NotificationOptionToggle/index.tsx index d04cad3c248..ad6ae7921a3 100644 --- a/app/components/Views/Settings/NotificationsSettings/NotificationOptionToggle/index.tsx +++ b/app/components/Views/Settings/NotificationsSettings/NotificationOptionToggle/index.tsx @@ -34,7 +34,7 @@ interface NotificationOptionsToggleProps { disabledSwitch?: boolean; isLoading?: boolean; isEnabled: boolean; - refetchAccountSettings: () => Promise; + updateAndfetchAccountSettings: () => Promise | undefined>; } /** @@ -50,7 +50,7 @@ const NotificationOptionToggle = ({ isEnabled, disabledSwitch, isLoading, - refetchAccountSettings, + updateAndfetchAccountSettings, }: NotificationOptionsToggleProps) => { const theme = useTheme(); const { colors } = theme; @@ -59,7 +59,7 @@ const NotificationOptionToggle = ({ const { toggleAccount, loading: isUpdatingAccount } = useUpdateAccountSetting( address, - refetchAccountSettings, + updateAndfetchAccountSettings, ); const loading = isLoading || isUpdatingAccount; @@ -104,7 +104,7 @@ const NotificationOptionToggle = ({ )} - {isLoading || loading ? ( + {loading ? ( ) : ( }, button: { alignSelf: 'stretch', - marginBottom: 16, + marginBottom: 48, + }, + + }); + + export const styles = StyleSheet.create({ + headerLeft: { + marginHorizontal: 16, }, }); diff --git a/app/components/Views/Settings/NotificationsSettings/__snapshots__/AccountsList.test.tsx.snap b/app/components/Views/Settings/NotificationsSettings/__snapshots__/AccountsList.test.tsx.snap new file mode 100644 index 00000000000..621e0158390 --- /dev/null +++ b/app/components/Views/Settings/NotificationsSettings/__snapshots__/AccountsList.test.tsx.snap @@ -0,0 +1,520 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccountsList matches snapshot 1`] = ` + + + + + + + + + + + + + + + + + + + Account 1 + + + 0xc495...d272 + + + + + + + + + + + + + + + + + + + + + + + Account 2 + + + 0xc496...a756 + + + + + + + + + + +`; diff --git a/app/components/Views/Settings/NotificationsSettings/index.tsx b/app/components/Views/Settings/NotificationsSettings/index.tsx index e046b203c2e..20456dc6389 100644 --- a/app/components/Views/Settings/NotificationsSettings/index.tsx +++ b/app/components/Views/Settings/NotificationsSettings/index.tsx @@ -1,6 +1,5 @@ -/* eslint-disable react-native/no-inline-styles */ /* eslint-disable react/display-name */ -import React, { useEffect, useMemo, useCallback } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { ScrollView, Switch, View, Linking } from 'react-native'; import { useSelector } from 'react-redux'; import { NavigationProp, ParamListBase } from '@react-navigation/native'; @@ -13,6 +12,7 @@ import Text, { TextVariant, TextColor, } from '../../../../component-library/components/Texts/Text'; +import { AccountsList} from './AccountsList'; import { useAccounts } from '../../../../components/hooks/useAccounts'; import { useMetrics } from '../../../../components/hooks/useMetrics'; import { AvatarAccountType } from '../../../../component-library/components/Avatars/Avatar'; @@ -21,9 +21,7 @@ import SwitchLoadingModal from '../../../UI/Notification/SwitchLoadingModal'; import { Props } from './NotificationsSettings.types'; import { useStyles } from '../../../../component-library/hooks'; -import NotificationOptionToggle from './NotificationOptionToggle'; import CustomNotificationsRow from './CustomNotificationsRow'; -import { NotificationsToggleTypes } from './NotificationsSettings.constants'; import { selectIsFeatureAnnouncementsEnabled, selectIsMetamaskNotificationsEnabled, @@ -51,7 +49,7 @@ import { useAccountSettingsProps, useSwitchNotifications, } from '../../../../util/notifications/hooks/useSwitchNotifications'; -import styleSheet from './NotificationsSettings.styles'; +import styleSheet, { styles as navigationOptionsStyles } from './NotificationsSettings.styles'; import AppConstants from '../../../../core/AppConstants'; import notificationsRows from './notificationsRows'; import { IconName } from '../../../../component-library/components/Icons/Icon'; @@ -121,19 +119,6 @@ const NotificationsSettings = ({ navigation, route }: Props) => { selectIsUpdatingMetamaskNotificationsAccount, ); - const accountAddresses = useMemo( - () => accounts.map((a) => a.address), - [accounts], - ); - - const { switchFeatureAnnouncements } = useSwitchNotifications(); - - // Account Settings - const accountSettingsProps = useAccountSettingsProps(accountAddresses); - const refetchAccountSettings = useCallback(async () => { - await accountSettingsProps.update(accountAddresses); - }, [accountAddresses, accountSettingsProps]); - const { enableNotifications, loading: enableLoading, @@ -146,6 +131,9 @@ const NotificationsSettings = ({ navigation, route }: Props) => { error: disablingError, } = useDisableNotifications(); + const { switchFeatureAnnouncements } = useSwitchNotifications(); + const { updateAndfetchAccountSettings } = useAccountSettingsProps(accounts); + const accountAvatarType = useSelector((state: RootState) => state.settings.useBlockieIcon ? AvatarAccountType.Blockies @@ -157,7 +145,7 @@ const NotificationsSettings = ({ navigation, route }: Props) => { const [uiNotificationStatus, setUiNotificationStatus] = React.useState(false); const [platformAnnouncementsState, setPlatformAnnouncementsState] = React.useState(isFeatureAnnouncementsEnabled); - + const accountSettingsData = useSelector((state: RootState) => state.notifications); const loading = enableLoading || disableLoading; const errorText = enablingError || disablingError; const loadingText = !uiNotificationStatus @@ -170,7 +158,7 @@ const NotificationsSettings = ({ navigation, route }: Props) => { const isFullScreenModal = route?.params?.isFullScreenModal; // Style const { colors } = theme; - const { styles } = useStyles(styleSheet, {}); + const { styles } = useStyles(styleSheet, { theme }); /** * Initializes the notifications feature. @@ -220,37 +208,6 @@ const NotificationsSettings = ({ navigation, route }: Props) => { ); }, [colors, isFullScreenModal, navigation]); - const renderAccounts = useCallback( - () => - accounts.map((account) => { - const isEnabled = - accountSettingsProps.data?.[account.address.toLowerCase()]; - return ( - 0} - isLoading={accountSettingsProps.accountsBeingUpdated.includes( - account.address.toLowerCase(), - )} - isEnabled={isEnabled ?? false} - refetchAccountSettings={refetchAccountSettings} - /> - ); - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - accountSettingsProps.data, - accountSettingsProps.accountsBeingUpdated, - accountAvatarType, - isUpdatingMetamaskNotificationsAccount.length, - refetchAccountSettings, - ], - ); - const renderResetNotificationsBtn = useCallback(() => (