From 1cea92a5923542f07a39ab8cbd7279182e96c4bd Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Thu, 6 Nov 2025 13:53:08 +0100 Subject: [PATCH 1/7] fix: tie backup and sync in onboarding to settings This commit improves the backup and sync feature by ensuring that when basic functionality is disabled, the main backup and sync feature, along with its sub-features (account syncing and contact syncing), are also turned off. Additionally, it removes unused imports and related code from the privacy settings page, streamlining the component. --- .../backup-and-sync-toggle.tsx | 14 ++++++++ .../privacy-settings/privacy-settings.js | 32 +------------------ 2 files changed, 15 insertions(+), 31 deletions(-) diff --git a/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.tsx b/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.tsx index 0ff87e9cf429..2bf2f1cd53cc 100644 --- a/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.tsx +++ b/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.tsx @@ -90,7 +90,11 @@ export const BackupAndSyncToggle = () => { // Cascading side effects useEffect(() => { if (!isBasicFunctionalityEnabled && isBackupAndSyncEnabled) { + // Turn off main backup and sync setIsBackupAndSyncFeatureEnabled(BACKUPANDSYNC_FEATURES.main, false); + // Also turn off all sub-features when basic functionality is disabled + setIsBackupAndSyncFeatureEnabled(BACKUPANDSYNC_FEATURES.accountSyncing, false); + setIsBackupAndSyncFeatureEnabled(BACKUPANDSYNC_FEATURES.contactSyncing, false); } }, [ isBasicFunctionalityEnabled, @@ -101,10 +105,20 @@ export const BackupAndSyncToggle = () => { const handleBackupAndSyncToggleSetValue = async () => { if (isBackupAndSyncEnabled) { trackBackupAndSyncToggleEvent(false); + // Turn off main backup and sync await setIsBackupAndSyncFeatureEnabled( BACKUPANDSYNC_FEATURES.main, false, ); + // Also turn off all sub-features when main toggle is disabled + await setIsBackupAndSyncFeatureEnabled( + BACKUPANDSYNC_FEATURES.accountSyncing, + false, + ); + await setIsBackupAndSyncFeatureEnabled( + BACKUPANDSYNC_FEATURES.contactSyncing, + false, + ); } else { trackBackupAndSyncToggleEvent(true); diff --git a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js index d5209d99b622..36d29664e54d 100644 --- a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js +++ b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js @@ -1,14 +1,12 @@ -import React, { useContext, useState, useEffect } from 'react'; +import React, { useContext, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom-v5-compat'; import classnames from 'classnames'; import { ButtonVariant } from '@metamask/snaps-sdk'; -import { BACKUPANDSYNC_FEATURES } from '@metamask/profile-sync-controller/user-storage'; import log from 'loglevel'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { addUrlProtocolPrefix } from '../../../../app/scripts/lib/util'; -import { useBackupAndSync } from '../../../hooks/identity/useBackupAndSync'; import { MetaMetricsEventCategory, MetaMetricsEventName, @@ -138,17 +136,6 @@ export default function PrivacySettings() { const isBackupAndSyncEnabled = useSelector(selectIsBackupAndSyncEnabled); - const { setIsBackupAndSyncFeatureEnabled, error: backupAndSyncError } = - useBackupAndSync(); - - useEffect(() => { - if (externalServicesOnboardingToggleState) { - setIsBackupAndSyncFeatureEnabled(BACKUPANDSYNC_FEATURES.main, true); - } else { - setIsBackupAndSyncFeatureEnabled(BACKUPANDSYNC_FEATURES.main, false); - } - }, [externalServicesOnboardingToggleState, setIsBackupAndSyncFeatureEnabled]); - const handleSubmit = () => { dispatch(setUse4ByteResolution(turnOn4ByteResolution)); dispatch(setUseTokenDetection(turnOnTokenDetection)); @@ -160,11 +147,6 @@ export default function PrivacySettings() { setUseTransactionSimulations(isTransactionSimulationsEnabled); setUseExternalNameSources(turnOnExternalNameSources); - // Backup and sync Setup - if (!externalServicesOnboardingToggleState) { - setIsBackupAndSyncFeatureEnabled(BACKUPANDSYNC_FEATURES.main, false); - } - if (ipfsURL && !ipfsError) { const { host } = new URL(addUrlProtocolPrefix(ipfsURL)); dispatch(setIpfsGateway(host)); @@ -425,18 +407,6 @@ export default function PrivacySettings() { - {backupAndSyncError && ( - - - {t('notificationsSettingsBoxError')} - - - )} - Date: Thu, 6 Nov 2025 14:07:06 +0100 Subject: [PATCH 2/7] test: add tests for backup and sync toggle functionality This commit introduces new tests for the BackupAndSyncToggle component, ensuring that all backup and sync features are disabled when basic functionality is turned off. It also verifies that all sub-features are disabled when the backup and sync toggle is manually turned off. Additionally, the component's useEffect is updated to handle asynchronous feature disabling correctly. --- .../backup-and-sync-toggle.test.tsx | 65 ++++++++++++++++++- .../backup-and-sync-toggle.tsx | 16 +++-- 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.test.tsx b/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.test.tsx index 2d82ec69e044..c0d88ac6458e 100644 --- a/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.test.tsx +++ b/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import * as Redux from 'react-redux'; import configureMockStore from 'redux-mock-store'; -import { render, fireEvent } from '@testing-library/react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; import { BACKUPANDSYNC_FEATURES } from '@metamask/profile-sync-controller/user-storage'; import { MetamaskIdentityProvider } from '../../../../contexts/identity'; import * as useBackupAndSyncHook from '../../../../hooks/identity/useBackupAndSync/useBackupAndSync'; @@ -125,6 +125,69 @@ describe('BackupAndSyncToggle', () => { ); }); + it('disables all backup and sync features when basic functionality is disabled', async () => { + const store = initialStore(); + store.metamask.isBackupAndSyncEnabled = true; + store.metamask.useExternalServices = false; // Basic functionality disabled + + const { setIsBackupAndSyncFeatureEnabledMock } = arrangeMocks(); + + render( + + + , + ); + + // Wait for the async useEffect to complete + await waitFor(() => { + expect(setIsBackupAndSyncFeatureEnabledMock).toHaveBeenCalledWith( + BACKUPANDSYNC_FEATURES.main, + false, + ); + }); + + expect(setIsBackupAndSyncFeatureEnabledMock).toHaveBeenCalledWith( + BACKUPANDSYNC_FEATURES.accountSyncing, + false, + ); + expect(setIsBackupAndSyncFeatureEnabledMock).toHaveBeenCalledWith( + BACKUPANDSYNC_FEATURES.contactSyncing, + false, + ); + }); + + it('disables all sub-features when manually turning off backup and sync', async () => { + const store = initialStore(); + store.metamask.isBackupAndSyncEnabled = true; + + const { setIsBackupAndSyncFeatureEnabledMock } = arrangeMocks(); + + const { getByTestId } = render( + + + , + ); + + fireEvent.click(getByTestId(backupAndSyncToggleTestIds.toggleButton)); + + // Wait for the async toggle handler to complete + await waitFor(() => { + expect(setIsBackupAndSyncFeatureEnabledMock).toHaveBeenCalledWith( + BACKUPANDSYNC_FEATURES.main, + false, + ); + }); + + expect(setIsBackupAndSyncFeatureEnabledMock).toHaveBeenCalledWith( + BACKUPANDSYNC_FEATURES.accountSyncing, + false, + ); + expect(setIsBackupAndSyncFeatureEnabledMock).toHaveBeenCalledWith( + BACKUPANDSYNC_FEATURES.contactSyncing, + false, + ); + }); + function arrangeMocks() { const setIsBackupAndSyncFeatureEnabledMock = jest.fn(() => Promise.resolve(), diff --git a/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.tsx b/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.tsx index 2bf2f1cd53cc..b61abe5001b1 100644 --- a/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.tsx +++ b/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.tsx @@ -90,11 +90,17 @@ export const BackupAndSyncToggle = () => { // Cascading side effects useEffect(() => { if (!isBasicFunctionalityEnabled && isBackupAndSyncEnabled) { - // Turn off main backup and sync - setIsBackupAndSyncFeatureEnabled(BACKUPANDSYNC_FEATURES.main, false); - // Also turn off all sub-features when basic functionality is disabled - setIsBackupAndSyncFeatureEnabled(BACKUPANDSYNC_FEATURES.accountSyncing, false); - setIsBackupAndSyncFeatureEnabled(BACKUPANDSYNC_FEATURES.contactSyncing, false); + (async () => { + try { + // Turn off main backup and sync + await setIsBackupAndSyncFeatureEnabled(BACKUPANDSYNC_FEATURES.main, false); + // Also turn off all sub-features when basic functionality is disabled + await setIsBackupAndSyncFeatureEnabled(BACKUPANDSYNC_FEATURES.accountSyncing, false); + await setIsBackupAndSyncFeatureEnabled(BACKUPANDSYNC_FEATURES.contactSyncing, false); + } catch (error) { + console.error('Failed to disable backup and sync features:', error); + } + })(); } }, [ isBasicFunctionalityEnabled, From f120d584b268f54ec9c4415da6a972a6e5bd1940 Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Thu, 6 Nov 2025 14:15:40 +0100 Subject: [PATCH 3/7] fix: disables backup & sync if EITHER basic functionality state is false This commit adds new tests to the BackupAndSyncToggle component, verifying that all backup and sync features are disabled when basic functionality is turned off, both for production and onboarding states. It also checks that features remain enabled when both states are active, ensuring comprehensive coverage of the component's behavior in various scenarios. --- .../backup-and-sync-toggle.test.tsx | 85 +++++++++++++++++++ .../backup-and-sync-toggle.tsx | 10 ++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.test.tsx b/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.test.tsx index c0d88ac6458e..552f4d18741d 100644 --- a/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.test.tsx +++ b/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.test.tsx @@ -156,6 +156,91 @@ describe('BackupAndSyncToggle', () => { ); }); + it('disables all backup and sync features when onboarding basic functionality is disabled', async () => { + const store = initialStore(); + store.metamask.isBackupAndSyncEnabled = true; + store.metamask.useExternalServices = true; // Production basic functionality enabled + store.appState.externalServicesOnboardingToggleState = false; // Onboarding basic functionality disabled + + const { setIsBackupAndSyncFeatureEnabledMock } = arrangeMocks(); + + render( + + + , + ); + + // Wait for the async useEffect to complete + await waitFor(() => { + expect(setIsBackupAndSyncFeatureEnabledMock).toHaveBeenCalledWith( + BACKUPANDSYNC_FEATURES.main, + false, + ); + }); + + expect(setIsBackupAndSyncFeatureEnabledMock).toHaveBeenCalledWith( + BACKUPANDSYNC_FEATURES.accountSyncing, + false, + ); + expect(setIsBackupAndSyncFeatureEnabledMock).toHaveBeenCalledWith( + BACKUPANDSYNC_FEATURES.contactSyncing, + false, + ); + }); + + it('disables all backup and sync features when both basic functionality states are disabled', async () => { + const store = initialStore(); + store.metamask.isBackupAndSyncEnabled = true; + store.metamask.useExternalServices = false; // Production basic functionality disabled + store.appState.externalServicesOnboardingToggleState = false; // Onboarding basic functionality disabled + + const { setIsBackupAndSyncFeatureEnabledMock } = arrangeMocks(); + + render( + + + , + ); + + // Wait for the async useEffect to complete + await waitFor(() => { + expect(setIsBackupAndSyncFeatureEnabledMock).toHaveBeenCalledWith( + BACKUPANDSYNC_FEATURES.main, + false, + ); + }); + + expect(setIsBackupAndSyncFeatureEnabledMock).toHaveBeenCalledWith( + BACKUPANDSYNC_FEATURES.accountSyncing, + false, + ); + expect(setIsBackupAndSyncFeatureEnabledMock).toHaveBeenCalledWith( + BACKUPANDSYNC_FEATURES.contactSyncing, + false, + ); + }); + + it('does not disable backup and sync when both basic functionality states are enabled', async () => { + const store = initialStore(); + store.metamask.isBackupAndSyncEnabled = true; + store.metamask.useExternalServices = true; // Production basic functionality enabled + store.appState.externalServicesOnboardingToggleState = true; // Onboarding basic functionality enabled + + const { setIsBackupAndSyncFeatureEnabledMock } = arrangeMocks(); + + render( + + + , + ); + + // Wait a bit to ensure useEffect doesn't fire + await new Promise(resolve => setTimeout(resolve, 100)); + + // Should not have called the disable function at all + expect(setIsBackupAndSyncFeatureEnabledMock).not.toHaveBeenCalled(); + }); + it('disables all sub-features when manually turning off backup and sync', async () => { const store = initialStore(); store.metamask.isBackupAndSyncEnabled = true; diff --git a/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.tsx b/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.tsx index b61abe5001b1..229b7a3d4769 100644 --- a/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.tsx +++ b/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.tsx @@ -87,9 +87,14 @@ export const BackupAndSyncToggle = () => { [trackEvent, isBackupAndSyncEnabled, isMetamaskNotificationsEnabled], ); - // Cascading side effects + // Cascading side effects - disable backup & sync when basic functionality is disabled useEffect(() => { - if (!isBasicFunctionalityEnabled && isBackupAndSyncEnabled) { + // Check both basic functionality states: production and onboarding + const isBasicFunctionalityDisabled = + isBasicFunctionalityEnabled === false || + isOnboardingBasicFunctionalityEnabled === false; + + if (isBasicFunctionalityDisabled && isBackupAndSyncEnabled) { (async () => { try { // Turn off main backup and sync @@ -104,6 +109,7 @@ export const BackupAndSyncToggle = () => { } }, [ isBasicFunctionalityEnabled, + isOnboardingBasicFunctionalityEnabled, isBackupAndSyncEnabled, setIsBackupAndSyncFeatureEnabled, ]); From 981d2bac62778dfcdfe064405c17bbd6a165ac91 Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Thu, 6 Nov 2025 14:28:52 +0100 Subject: [PATCH 4/7] lint --- .../backup-and-sync-toggle.test.tsx | 2 +- .../backup-and-sync-toggle.tsx | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.test.tsx b/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.test.tsx index 552f4d18741d..b866d67baade 100644 --- a/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.test.tsx +++ b/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.test.tsx @@ -235,7 +235,7 @@ describe('BackupAndSyncToggle', () => { ); // Wait a bit to ensure useEffect doesn't fire - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); // Should not have called the disable function at all expect(setIsBackupAndSyncFeatureEnabledMock).not.toHaveBeenCalled(); diff --git a/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.tsx b/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.tsx index 229b7a3d4769..3139c008f700 100644 --- a/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.tsx +++ b/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.tsx @@ -98,12 +98,21 @@ export const BackupAndSyncToggle = () => { (async () => { try { // Turn off main backup and sync - await setIsBackupAndSyncFeatureEnabled(BACKUPANDSYNC_FEATURES.main, false); + await setIsBackupAndSyncFeatureEnabled( + BACKUPANDSYNC_FEATURES.main, + false, + ); // Also turn off all sub-features when basic functionality is disabled - await setIsBackupAndSyncFeatureEnabled(BACKUPANDSYNC_FEATURES.accountSyncing, false); - await setIsBackupAndSyncFeatureEnabled(BACKUPANDSYNC_FEATURES.contactSyncing, false); - } catch (error) { - console.error('Failed to disable backup and sync features:', error); + await setIsBackupAndSyncFeatureEnabled( + BACKUPANDSYNC_FEATURES.accountSyncing, + false, + ); + await setIsBackupAndSyncFeatureEnabled( + BACKUPANDSYNC_FEATURES.contactSyncing, + false, + ); + } catch (err) { + console.error('Failed to disable backup and sync features:', err); } })(); } From bef18ac2c27cee118ab5c184267db2fca29c0a57 Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Fri, 7 Nov 2025 19:26:27 +0100 Subject: [PATCH 5/7] sync sub-features toggles to global backup-and-sync setting This commit updates the tests for the BackupAndSyncToggle component to ensure that when the main toggle is activated, all associated sub-features (account syncing and contact syncing) are also enabled. The tests now include checks for the asynchronous behavior of the toggle handler and confirm that the modal's callback correctly enables all features. Additionally, the component's implementation is adjusted to ensure all sub-features are activated consistently. --- .../backup-and-sync-toggle.test.tsx | 39 +++++++++++++++++-- .../backup-and-sync-toggle.tsx | 20 ++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.test.tsx b/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.test.tsx index b866d67baade..4f8d8f12002e 100644 --- a/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.test.tsx +++ b/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.test.tsx @@ -87,7 +87,7 @@ describe('BackupAndSyncToggle', () => { }); }); - it('enables backup and sync when the toggle is turned on and basic functionality is already on', () => { + it('enables backup and sync and all sub-features when the toggle is turned on and basic functionality is already on', async () => { const store = initialStore(); store.metamask.isBackupAndSyncEnabled = false; @@ -98,14 +98,29 @@ describe('BackupAndSyncToggle', () => { , ); + fireEvent.click(getByTestId(backupAndSyncToggleTestIds.toggleButton)); + + // Wait for the async toggle handler to complete + await waitFor(() => { + expect(setIsBackupAndSyncFeatureEnabledMock).toHaveBeenCalledWith( + BACKUPANDSYNC_FEATURES.main, + true, + ); + }); + + // Should also enable all sub-features for 1-click restore convenience expect(setIsBackupAndSyncFeatureEnabledMock).toHaveBeenCalledWith( - BACKUPANDSYNC_FEATURES.main, + BACKUPANDSYNC_FEATURES.accountSyncing, + true, + ); + expect(setIsBackupAndSyncFeatureEnabledMock).toHaveBeenCalledWith( + BACKUPANDSYNC_FEATURES.contactSyncing, true, ); }); - it('opens the confirm modal when the toggle is turned on and basic functionality is off', () => { + it('opens the confirm modal when the toggle is turned on and basic functionality is off', async () => { const store = initialStore(); store.metamask.isBackupAndSyncEnabled = false; store.metamask.useExternalServices = false; @@ -123,6 +138,24 @@ describe('BackupAndSyncToggle', () => { enableBackupAndSync: expect.any(Function), }), ); + + // Test that the modal's enableBackupAndSync callback enables all features + const modalAction = mockDispatch.mock.calls[0][0]; + const enableCallback = modalAction.payload.enableBackupAndSync; + await enableCallback(); + + expect(setIsBackupAndSyncFeatureEnabledMock).toHaveBeenCalledWith( + BACKUPANDSYNC_FEATURES.main, + true, + ); + expect(setIsBackupAndSyncFeatureEnabledMock).toHaveBeenCalledWith( + BACKUPANDSYNC_FEATURES.accountSyncing, + true, + ); + expect(setIsBackupAndSyncFeatureEnabledMock).toHaveBeenCalledWith( + BACKUPANDSYNC_FEATURES.contactSyncing, + true, + ); }); it('disables all backup and sync features when basic functionality is disabled', async () => { diff --git a/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.tsx b/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.tsx index 3139c008f700..217c9a6afa41 100644 --- a/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.tsx +++ b/ui/components/app/identity/backup-and-sync-toggle/backup-and-sync-toggle.tsx @@ -151,18 +151,38 @@ export const BackupAndSyncToggle = () => { showModal({ name: CONFIRM_TURN_ON_BACKUP_AND_SYNC_MODAL_NAME, enableBackupAndSync: async () => { + // Turn on main backup and sync await setIsBackupAndSyncFeatureEnabled( BACKUPANDSYNC_FEATURES.main, true, ); + // Also turn on all sub-features for convenient 1-click restore + await setIsBackupAndSyncFeatureEnabled( + BACKUPANDSYNC_FEATURES.accountSyncing, + true, + ); + await setIsBackupAndSyncFeatureEnabled( + BACKUPANDSYNC_FEATURES.contactSyncing, + true, + ); }, }), ); } else { + // Turn on main backup and sync await setIsBackupAndSyncFeatureEnabled( BACKUPANDSYNC_FEATURES.main, true, ); + // Also turn on all sub-features for convenient 1-click restore + await setIsBackupAndSyncFeatureEnabled( + BACKUPANDSYNC_FEATURES.accountSyncing, + true, + ); + await setIsBackupAndSyncFeatureEnabled( + BACKUPANDSYNC_FEATURES.contactSyncing, + true, + ); } } }; From 2e69ef1ff58364e3b386c4248a27e360d1301fae Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Mon, 10 Nov 2025 14:59:43 +0100 Subject: [PATCH 6/7] implement reverse cascading logic for backup and sync features This commit enhances the BackupAndSyncFeaturesToggles component by adding a useEffect hook that disables the main backup and sync toggle when all associated sub-features (account syncing and contact syncing) are turned off. Corresponding tests are added to verify this behavior, ensuring that the main toggle remains enabled if at least one sub-feature is active. Additionally, the test suite is updated to include checks for the asynchronous nature of the feature toggling. --- .../backup-and-sync-features-toggles.test.tsx | 49 ++++++++++++++++++- .../backup-and-sync-features-toggles.tsx | 30 +++++++++++- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/ui/components/app/identity/backup-and-sync-features-toggles/backup-and-sync-features-toggles.test.tsx b/ui/components/app/identity/backup-and-sync-features-toggles/backup-and-sync-features-toggles.test.tsx index f01e1a99d9bc..11158bbad6e5 100644 --- a/ui/components/app/identity/backup-and-sync-features-toggles/backup-and-sync-features-toggles.test.tsx +++ b/ui/components/app/identity/backup-and-sync-features-toggles/backup-and-sync-features-toggles.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import * as Redux from 'react-redux'; import configureMockStore from 'redux-mock-store'; -import { render, fireEvent } from '@testing-library/react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; import { BACKUPANDSYNC_FEATURES } from '@metamask/profile-sync-controller/user-storage'; import * as useBackupAndSyncHook from '../../../../hooks/identity/useBackupAndSync/useBackupAndSync'; import { MetamaskIdentityProvider } from '../../../../contexts/identity'; @@ -169,6 +169,53 @@ describe('BackupAndSyncFeaturesToggles', () => { ); }); + it('disables main backup and sync when all sub-features are manually turned off', async () => { + const store = initialStore(); + store.metamask.isBackupAndSyncEnabled = true; + store.metamask.isAccountSyncingEnabled = false; // Already off + store.metamask.isContactSyncingEnabled = false; // Already off + + const { setIsBackupAndSyncFeatureEnabledMock } = arrangeMocks(); + + render( + + + , + ); + + // Wait for the reverse cascade effect to fire + await waitFor(() => { + expect(setIsBackupAndSyncFeatureEnabledMock).toHaveBeenCalledWith( + BACKUPANDSYNC_FEATURES.main, + false, + ); + }); + }); + + it('does not disable main backup and sync when at least one sub-feature is enabled', async () => { + const store = initialStore(); + store.metamask.isBackupAndSyncEnabled = true; + store.metamask.isAccountSyncingEnabled = true; // One is ON + store.metamask.isContactSyncingEnabled = false; // One is OFF + + const { setIsBackupAndSyncFeatureEnabledMock } = arrangeMocks(); + + render( + + + , + ); + + // Wait a bit to ensure useEffect doesn't fire + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Should not have disabled main toggle + expect(setIsBackupAndSyncFeatureEnabledMock).not.toHaveBeenCalledWith( + BACKUPANDSYNC_FEATURES.main, + false, + ); + }); + function arrangeMocks() { const setIsBackupAndSyncFeatureEnabledMock = jest.fn(() => Promise.resolve(), diff --git a/ui/components/app/identity/backup-and-sync-features-toggles/backup-and-sync-features-toggles.tsx b/ui/components/app/identity/backup-and-sync-features-toggles/backup-and-sync-features-toggles.tsx index f1054875c8c7..17f949fdabe1 100644 --- a/ui/components/app/identity/backup-and-sync-features-toggles/backup-and-sync-features-toggles.tsx +++ b/ui/components/app/identity/backup-and-sync-features-toggles/backup-and-sync-features-toggles.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useContext } from 'react'; +import React, { useCallback, useContext, useEffect } from 'react'; import { useSelector } from 'react-redux'; import { BACKUPANDSYNC_FEATURES } from '@metamask/profile-sync-controller/user-storage'; import { useI18nContext } from '../../../../hooks/useI18nContext'; @@ -149,6 +149,34 @@ export const BackupAndSyncFeaturesToggles = () => { const isBackupAndSyncUpdateLoading = useSelector( selectIsBackupAndSyncUpdateLoading, ); + const isAccountSyncingEnabled = useSelector(selectIsAccountSyncingEnabled); + const isContactSyncingEnabled = useSelector(selectIsContactSyncingEnabled); + + const { setIsBackupAndSyncFeatureEnabled } = useBackupAndSync(); + + // Reverse cascading: if all sub-features are manually turned off, turn off main toggle + useEffect(() => { + const allSubFeaturesDisabled = + !isAccountSyncingEnabled && !isContactSyncingEnabled; + + if (isBackupAndSyncEnabled && allSubFeaturesDisabled) { + (async () => { + try { + await setIsBackupAndSyncFeatureEnabled( + BACKUPANDSYNC_FEATURES.main, + false, + ); + } catch (err) { + console.error('Failed to disable main backup and sync toggle:', err); + } + })(); + } + }, [ + isBackupAndSyncEnabled, + isAccountSyncingEnabled, + isContactSyncingEnabled, + setIsBackupAndSyncFeatureEnabled, + ]); return ( Date: Mon, 10 Nov 2025 15:10:57 +0100 Subject: [PATCH 7/7] added loading state check This commit updates the BackupAndSyncFeaturesToggles component to include a guard against race conditions by ensuring that the main toggle is not updated while backup and sync updates are in progress. The useEffect hook now checks the loading state before disabling the main toggle, improving the reliability of the feature toggling logic. --- .../backup-and-sync-features-toggles.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ui/components/app/identity/backup-and-sync-features-toggles/backup-and-sync-features-toggles.tsx b/ui/components/app/identity/backup-and-sync-features-toggles/backup-and-sync-features-toggles.tsx index 17f949fdabe1..67d98054c2ae 100644 --- a/ui/components/app/identity/backup-and-sync-features-toggles/backup-and-sync-features-toggles.tsx +++ b/ui/components/app/identity/backup-and-sync-features-toggles/backup-and-sync-features-toggles.tsx @@ -155,11 +155,16 @@ export const BackupAndSyncFeaturesToggles = () => { const { setIsBackupAndSyncFeatureEnabled } = useBackupAndSync(); // Reverse cascading: if all sub-features are manually turned off, turn off main toggle + // Guard against race conditions by not running while updates are in progress useEffect(() => { const allSubFeaturesDisabled = !isAccountSyncingEnabled && !isContactSyncingEnabled; - if (isBackupAndSyncEnabled && allSubFeaturesDisabled) { + if ( + isBackupAndSyncEnabled && + allSubFeaturesDisabled && + !isBackupAndSyncUpdateLoading + ) { (async () => { try { await setIsBackupAndSyncFeatureEnabled( @@ -175,6 +180,7 @@ export const BackupAndSyncFeaturesToggles = () => { isBackupAndSyncEnabled, isAccountSyncingEnabled, isContactSyncingEnabled, + isBackupAndSyncUpdateLoading, setIsBackupAndSyncFeatureEnabled, ]);