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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;

Expand All @@ -98,14 +98,29 @@ describe('BackupAndSyncToggle', () => {
<BackupAndSyncToggle />
</Redux.Provider>,
);

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;
Expand All @@ -123,6 +138,172 @@ 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 () => {
const store = initialStore();
store.metamask.isBackupAndSyncEnabled = true;
store.metamask.useExternalServices = false; // Basic functionality disabled

const { setIsBackupAndSyncFeatureEnabledMock } = arrangeMocks();

render(
<Redux.Provider store={mockStore(store)}>
<BackupAndSyncToggle />
</Redux.Provider>,
);

// 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 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(
<Redux.Provider store={mockStore(store)}>
<BackupAndSyncToggle />
</Redux.Provider>,
);

// 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(
<Redux.Provider store={mockStore(store)}>
<BackupAndSyncToggle />
</Redux.Provider>,
);

// 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(
<Redux.Provider store={mockStore(store)}>
<BackupAndSyncToggle />
</Redux.Provider>,
);

// 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;

const { setIsBackupAndSyncFeatureEnabledMock } = arrangeMocks();

const { getByTestId } = render(
<Redux.Provider store={mockStore(store)}>
<BackupAndSyncToggle />
</Redux.Provider>,
);

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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,24 +87,59 @@ export const BackupAndSyncToggle = () => {
[trackEvent, isBackupAndSyncEnabled, isMetamaskNotificationsEnabled],
);

// Cascading side effects
// Cascading side effects - disable backup & sync when basic functionality is disabled
useEffect(() => {
if (!isBasicFunctionalityEnabled && isBackupAndSyncEnabled) {
setIsBackupAndSyncFeatureEnabled(BACKUPANDSYNC_FEATURES.main, false);
// 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
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 (err) {
console.error('Failed to disable backup and sync features:', err);
}
})();
}
}, [
isBasicFunctionalityEnabled,
isOnboardingBasicFunctionalityEnabled,
isBackupAndSyncEnabled,
setIsBackupAndSyncFeatureEnabled,
]);

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);

Expand All @@ -116,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,
);
}
}
};
Expand Down
32 changes: 1 addition & 31 deletions ui/pages/onboarding-flow/privacy-settings/privacy-settings.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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));
Expand All @@ -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));
Expand Down Expand Up @@ -425,18 +407,6 @@ export default function PrivacySettings() {

<BackupAndSyncToggle />

{backupAndSyncError && (
<Box paddingBottom={4}>
<Text
as="p"
color={TextColor.errorDefault}
variant={TextVariant.bodySm}
>
{t('notificationsSettingsBoxError')}
</Text>
</Box>
)}

<Setting
title={t('onboardingAdvancedPrivacyNetworkTitle')}
showToggle={false}
Expand Down
Loading