diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts
index 9f00528c58d..98a13c30943 100644
--- a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts
+++ b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts
@@ -33,19 +33,6 @@ const styleSheet = (params: {
} as ViewStyle,
style,
) as ViewStyle,
- underlay: {
- ...StyleSheet.absoluteFillObject,
- flexDirection: 'row',
- backgroundColor: colors.primary.muted,
- width: 4,
- },
- underlayBar: {
- marginVertical: 4,
- marginLeft: 4,
- width: 4,
- borderRadius: 2,
- backgroundColor: colors.primary.default,
- },
listItem: {
paddingRight: 0,
paddingTop: 0,
@@ -69,7 +56,7 @@ const styleSheet = (params: {
},
container: {
backgroundColor: isSelected
- ? colors.primary.muted
+ ? colors.background.muted
: colors.background.default,
flexDirection: 'row',
alignItems: 'center',
diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.test.tsx b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.test.tsx
index 8ad0d0016d8..cae833657c7 100644
--- a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.test.tsx
+++ b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.test.tsx
@@ -18,24 +18,6 @@ describe('ListItemMultiSelectButton', () => {
expect(wrapper).toMatchSnapshot();
});
- it('should not render the underlay view if isSelected is false', () => {
- const { queryByRole } = render(
-
-
- ,
- );
- expect(queryByRole('checkbox')).toBeNull();
- });
-
- it('should render the underlay view if isSelected is true', () => {
- const { queryByRole } = render(
-
-
- ,
- );
- expect(queryByRole('checkbox')).not.toBeNull();
- });
-
it('should call onPress when the button is pressed', () => {
const mockOnPress = jest.fn();
const { getByRole } = render(
diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.tsx b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.tsx
index 07cb4e858db..48ea2f5e34d 100644
--- a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.tsx
+++ b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.tsx
@@ -57,11 +57,6 @@ const ListItemMultiSelectButton: React.FC = ({
{children}
- {isSelected && (
-
-
-
- )}
{showButtonIcon ? (
diff --git a/app/component-library/components-temp/MultichainAccounts/AccountCell/AccountCell.styles.ts b/app/component-library/components-temp/MultichainAccounts/AccountCell/AccountCell.styles.ts
index fd73ba15155..763ad628b71 100644
--- a/app/component-library/components-temp/MultichainAccounts/AccountCell/AccountCell.styles.ts
+++ b/app/component-library/components-temp/MultichainAccounts/AccountCell/AccountCell.styles.ts
@@ -68,10 +68,6 @@ const styleSheet = (params: { theme: Theme; vars: unknown }) => {
verticalAlign: 'middle',
},
menuButton: {
- backgroundColor: colors.background.muted,
- borderRadius: 8,
- height: 28,
- width: 28,
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
diff --git a/app/component-library/components/List/ListItemMultiSelect/ListItemMultiSelect.styles.ts b/app/component-library/components/List/ListItemMultiSelect/ListItemMultiSelect.styles.ts
index 8bd81b3bb46..9c584ad4163 100644
--- a/app/component-library/components/List/ListItemMultiSelect/ListItemMultiSelect.styles.ts
+++ b/app/component-library/components/List/ListItemMultiSelect/ListItemMultiSelect.styles.ts
@@ -38,7 +38,7 @@ const styleSheet = (params: {
underlay: {
...StyleSheet.absoluteFillObject,
flexDirection: 'row',
- backgroundColor: colors.primary.muted,
+ backgroundColor: colors.background.muted,
},
checkbox: {
marginRight: 8 - Number(gap),
diff --git a/app/component-library/components/List/ListItemSelect/ListItemSelect.constants.ts b/app/component-library/components/List/ListItemSelect/ListItemSelect.constants.ts
index 89c8a366416..6ffca867f4d 100644
--- a/app/component-library/components/List/ListItemSelect/ListItemSelect.constants.ts
+++ b/app/component-library/components/List/ListItemSelect/ListItemSelect.constants.ts
@@ -8,9 +8,6 @@ import { ListItemSelectProps } from './ListItemSelect.types';
// Defaults
export const DEFAULT_SELECTITEM_GAP = 16;
-// Test IDs
-export const SELECTABLE_ITEM_UNDERLAY_ID = 'selectable-item-underlay';
-
// Sample consts
export const SAMPLE_SELECTITEM_PROPS: ListItemSelectProps = {
isSelected: true,
diff --git a/app/component-library/components/List/ListItemSelect/ListItemSelect.styles.ts b/app/component-library/components/List/ListItemSelect/ListItemSelect.styles.ts
index 5e590a70708..cf5bef216bc 100644
--- a/app/component-library/components/List/ListItemSelect/ListItemSelect.styles.ts
+++ b/app/component-library/components/List/ListItemSelect/ListItemSelect.styles.ts
@@ -35,18 +35,11 @@ const styleSheet = (params: {
underlay: {
...StyleSheet.absoluteFillObject,
flexDirection: 'row',
- backgroundColor: colors.primary.muted,
+ backgroundColor: colors.background.muted,
},
listItem: {
padding: 16,
},
- underlayBar: {
- marginVertical: 4,
- marginLeft: 4,
- width: 4,
- borderRadius: 2,
- backgroundColor: colors.primary.default,
- },
});
};
diff --git a/app/component-library/components/List/ListItemSelect/ListItemSelect.tsx b/app/component-library/components/List/ListItemSelect/ListItemSelect.tsx
index c8dd8de20e9..88a15673442 100644
--- a/app/component-library/components/List/ListItemSelect/ListItemSelect.tsx
+++ b/app/component-library/components/List/ListItemSelect/ListItemSelect.tsx
@@ -39,9 +39,7 @@ const ListItemSelect: React.FC = ({
{children}
{isSelected && (
-
-
-
+
)}
);
diff --git a/app/components/UI/CaipAccountSelectorList/__snapshots__/CaipAccountSelectorList.test.tsx.snap b/app/components/UI/CaipAccountSelectorList/__snapshots__/CaipAccountSelectorList.test.tsx.snap
index dba570311fd..55b0be735a9 100644
--- a/app/components/UI/CaipAccountSelectorList/__snapshots__/CaipAccountSelectorList.test.tsx.snap
+++ b/app/components/UI/CaipAccountSelectorList/__snapshots__/CaipAccountSelectorList.test.tsx.snap
@@ -68,7 +68,7 @@ exports[`CaipAccountSelectorList renders all accounts with balances 1`] = `
style={
{
"alignItems": "center",
- "backgroundColor": "#4459ff1a",
+ "backgroundColor": "#3c4d9d0f",
"flexDirection": "row",
}
}
@@ -248,34 +248,6 @@ exports[`CaipAccountSelectorList renders all accounts with balances 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
({
- getItem: jest.fn(),
- setItem: jest.fn(),
- removeItem: jest.fn(),
-}));
-
const mockUseDispatch = jest.fn();
jest.mock('react-redux', () => ({
@@ -74,11 +66,10 @@ jest.mock('../../../util/identity/hooks/useAuthentication', () => ({
}),
}));
-jest.mock('../../hooks/DeleteWallet', () => ({
- useDeleteWallet: () => [
- jest.fn(() => Promise.resolve()),
- jest.fn(() => Promise.resolve()),
- ],
+jest.mock('../../../core/Authentication/Authentication', () => ({
+ Authentication: {
+ deleteWallet: jest.fn(() => Promise.resolve()),
+ },
}));
const Stack = createStackNavigator();
@@ -153,7 +144,6 @@ describe('DeleteWalletModal', () => {
});
it('signs the user out when deleting the wallet', async () => {
- const removeItemSpy = jest.spyOn(StorageWrapper, 'removeItem');
const { getByTestId } = renderComponent(mockInitialState);
fireEvent.press(
@@ -164,11 +154,9 @@ describe('DeleteWalletModal', () => {
);
expect(mockSignOut).toHaveBeenCalled();
- expect(removeItemSpy).toHaveBeenCalledWith(OPTIN_META_METRICS_UI_SEEN);
});
- it('sets completedOnboarding to false when deleting the wallet', async () => {
- const removeItemSpy = jest.spyOn(StorageWrapper, 'removeItem');
+ it('calls deleteWallet when deleting the wallet', async () => {
const { getByTestId } = renderComponent(mockInitialState);
fireEvent.press(
@@ -178,13 +166,10 @@ describe('DeleteWalletModal', () => {
getByTestId(ForgotPasswordModalSelectorsIDs.YES_RESET_WALLET_BUTTON),
);
- expect(mockUseDispatch).toHaveBeenCalledWith(
- expect.objectContaining({
- type: SET_COMPLETED_ONBOARDING,
- completedOnboarding: false,
- }),
- );
- expect(removeItemSpy).toHaveBeenCalledWith(OPTIN_META_METRICS_UI_SEEN);
+ // Wait for async operations
+ await Promise.resolve();
+
+ expect(Authentication.deleteWallet).toHaveBeenCalled();
});
});
diff --git a/app/components/UI/DeleteWalletModal/index.tsx b/app/components/UI/DeleteWalletModal/index.tsx
index 719ddba3d59..7e1a6ca2ca6 100644
--- a/app/components/UI/DeleteWalletModal/index.tsx
+++ b/app/components/UI/DeleteWalletModal/index.tsx
@@ -7,14 +7,13 @@ import Icon, {
IconColor,
} from '../../../component-library/components/Icons/Icon';
import { createStyles } from './styles';
-import { useDeleteWallet } from '../../hooks/DeleteWallet';
+import { Authentication } from '../../../core';
import { strings } from '../../../../locales/i18n';
import { useTheme } from '../../../util/theme';
import Device from '../../../util/device';
import Routes from '../../../constants/navigation/Routes';
import { ForgotPasswordModalSelectorsIDs } from '../../../../e2e/selectors/Common/ForgotPasswordModal.selectors';
import { IMetaMetricsEvent, MetaMetricsEvents } from '../../../core/Analytics';
-import { setCompletedOnboarding } from '../../../actions/onboarding';
import { useDispatch, useSelector } from 'react-redux';
import { clearHistory } from '../../../actions/browser';
import CookieManager from '@react-native-cookies/cookies';
@@ -38,8 +37,6 @@ import { useMetrics } from '../../hooks/useMetrics';
import ButtonIcon, {
ButtonIconSizes,
} from '../../../component-library/components/Buttons/ButtonIcon';
-import StorageWrapper from '../../../store/storage-wrapper';
-import { OPTIN_META_METRICS_UI_SEEN } from '../../../constants/storage';
if (Device.isAndroid() && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
@@ -62,7 +59,6 @@ const DeleteWalletModal: React.FC = () => {
const [isResetWallet, setIsResetWallet] = useState(false);
- const [resetWalletState, deleteUser] = useDeleteWallet();
const dispatch = useDispatch();
const isDataCollectionForMarketingEnabled = useSelector(
(state: RootState) => state.security.dataCollectionForMarketing,
@@ -115,10 +111,7 @@ const DeleteWalletModal: React.FC = () => {
dispatch(clearHistory(isEnabled(), isDataCollectionForMarketingEnabled));
signOut();
await CookieManager.clearAll(true);
- await resetWalletState();
- await deleteUser();
- await StorageWrapper.removeItem(OPTIN_META_METRICS_UI_SEEN);
- dispatch(setCompletedOnboarding(false));
+ await Authentication.deleteWallet();
// Track analytics for successful deletion
track(MetaMetricsEvents.RESET_WALLET_CONFIRMED, {});
InteractionManager.runAfterInteractions(() => {
diff --git a/app/components/UI/EvmAccountSelectorList/__snapshots__/EvmAccountSelectorList.test.tsx.snap b/app/components/UI/EvmAccountSelectorList/__snapshots__/EvmAccountSelectorList.test.tsx.snap
index 389f7aeb78f..1eefc3feca6 100644
--- a/app/components/UI/EvmAccountSelectorList/__snapshots__/EvmAccountSelectorList.test.tsx.snap
+++ b/app/components/UI/EvmAccountSelectorList/__snapshots__/EvmAccountSelectorList.test.tsx.snap
@@ -89,7 +89,7 @@ exports[`EvmAccountSelectorList renders all accounts with balances 1`] = `
style={
{
"alignItems": "center",
- "backgroundColor": "#4459ff1a",
+ "backgroundColor": "#3c4d9d0f",
"flexDirection": "row",
}
}
@@ -271,34 +271,6 @@ exports[`EvmAccountSelectorList renders all accounts with balances 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
{
this.blockedRegionList = { list: newList, source: newSource };
+ this.blockedRegionListVersion += 1;
},
refreshEligibility: () => this.refreshEligibility(),
}),
@@ -807,6 +816,7 @@ export class PerpsController extends BaseController<
source: 'remote' | 'fallback',
) => {
this.blockedRegionList = { list, source };
+ this.blockedRegionListVersion += 1;
},
refreshEligibility: () => this.refreshEligibility(),
getHip3Config: () => ({
@@ -2172,13 +2182,25 @@ export class PerpsController extends BaseController<
* Refresh eligibility status
*/
async refreshEligibility(): Promise {
- try {
- DevLogger.log('PerpsController: Refreshing eligibility');
+ // Capture the current version before starting the async operation.
+ // This prevents race conditions where stale eligibility checks
+ // (started with fallback config) overwrite results from newer checks
+ // (started with remote config after it was fetched).
+ const versionAtStart = this.blockedRegionListVersion;
+ try {
+ // TODO: It would be good to have this location before we call this async function to avoid the race condition
const isEligible = await EligibilityService.checkEligibility(
this.blockedRegionList.list,
);
+ // Only update state if the blocked region list hasn't changed while we were awaiting.
+ // This prevents stale fallback-based eligibility checks from overwriting
+ // results from remote-based checks.
+ if (this.blockedRegionListVersion !== versionAtStart) {
+ return;
+ }
+
this.update((state) => {
state.isEligible = isEligible;
});
@@ -2187,10 +2209,14 @@ export class PerpsController extends BaseController<
ensureError(error),
this.getErrorContext('refreshEligibility'),
);
- // Default to eligible on error
- this.update((state) => {
- state.isEligible = true;
- });
+
+ // Only update on error if version is still current
+ if (this.blockedRegionListVersion === versionAtStart) {
+ // Default to eligible on error
+ this.update((state) => {
+ state.isEligible = true;
+ });
+ }
}
}
diff --git a/app/components/UI/Ramp/Aggregator/components/RegionSelectorModal/__snapshots__/RegionSelectorModal.test.tsx.snap b/app/components/UI/Ramp/Aggregator/components/RegionSelectorModal/__snapshots__/RegionSelectorModal.test.tsx.snap
index fd669def8c0..f2abd40a09f 100644
--- a/app/components/UI/Ramp/Aggregator/components/RegionSelectorModal/__snapshots__/RegionSelectorModal.test.tsx.snap
+++ b/app/components/UI/Ramp/Aggregator/components/RegionSelectorModal/__snapshots__/RegionSelectorModal.test.tsx.snap
@@ -8504,7 +8504,7 @@ exports[`RegionSelectorModal renders the modal with selected region in list 1`]
accessible={true}
style={
{
- "backgroundColor": "#4459ff1a",
+ "backgroundColor": "#3c4d9d0f",
"bottom": 0,
"flexDirection": "row",
"left": 0,
@@ -8513,19 +8513,7 @@ exports[`RegionSelectorModal renders the modal with selected region in list 1`]
"top": 0,
}
}
- >
-
-
+ />
-
-
+ />
-
-
+ />
-
-
+ />
-
-
+ />
-
-
+ />
-
-
+ />
-
-
+ />
-
-
-
-
-
+ />
-
-
+ />
diff --git a/app/components/Views/NetworkSelector/__snapshots__/NetworkSelector.test.tsx.snap b/app/components/Views/NetworkSelector/__snapshots__/NetworkSelector.test.tsx.snap
index 60fab8b4fa4..79533eb22b2 100644
--- a/app/components/Views/NetworkSelector/__snapshots__/NetworkSelector.test.tsx.snap
+++ b/app/components/Views/NetworkSelector/__snapshots__/NetworkSelector.test.tsx.snap
@@ -614,7 +614,7 @@ exports[`Network Selector renders correctly 1`] = `
accessible={true}
style={
{
- "backgroundColor": "#4459ff1a",
+ "backgroundColor": "#3c4d9d0f",
"bottom": 0,
"flexDirection": "row",
"left": 0,
@@ -623,19 +623,7 @@ exports[`Network Selector renders correctly 1`] = `
"top": 0,
}
}
- >
-
-
+ />
-
-
-
({
- context: {
- SeedlessOnboardingController: {
- clearState: jest.fn(),
- },
- },
- controllerMessenger: {
- call: jest.fn(),
- },
-}));
-
-jest.mock('../../../core/Engine/Engine', () => ({
- Engine: {
- disableAutomaticVaultBackup: false,
- },
-}));
-
-jest.mock('../../../store/storage-wrapper', () => ({
- getItem: jest.fn(),
- setItem: jest.fn(),
- removeItem: jest.fn(),
- clearAll: jest.fn(),
-}));
-
-jest.mock('../../../core/BackupVault', () => ({
- clearAllVaultBackups: jest.fn(),
-}));
-
-jest.mock('../../../core', () => ({
- Authentication: {
- newWalletAndKeychain: jest.fn(),
- lockApp: jest.fn(),
- },
-}));
-
-jest.mock('../useMetrics', () => ({
- useMetrics: () => ({
- createDataDeletionTask: jest.fn(),
- }),
-}));
-
-jest.mock('../../../util/Logger', () => ({
- log: jest.fn(),
-}));
-
-jest.mock('../../UI/Ramp/Deposit/utils/ProviderTokenVault', () => ({
- resetProviderToken: jest.fn(),
-}));
-
-describe('useDeleteWallet', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- EngineClass.disableAutomaticVaultBackup = false;
- });
-
- test('provides two functions for wallet operations', () => {
- // Arrange & Act
- const { result } = renderHookWithProvider(() => useDeleteWallet());
- const [resetWalletState, deleteUser] = result.current;
-
- // Assert
- expect(typeof resetWalletState).toBe('function');
- expect(typeof deleteUser).toBe('function');
- });
-
- test('calls vault backup clear before creating temporary wallet', async () => {
- // Arrange
- const { result } = renderHookWithProvider(() => useDeleteWallet());
- const [resetWalletState] = result.current;
- const clearVaultSpy = jest.mocked(clearAllVaultBackups);
- const newWalletSpy = jest.spyOn(Authentication, 'newWalletAndKeychain');
-
- // Act
- await resetWalletState();
-
- // Assert
- expect(clearVaultSpy).toHaveBeenCalledTimes(1);
- const clearCallOrder = clearVaultSpy.mock.invocationCallOrder[0];
- const newWalletCallOrder = newWalletSpy.mock.invocationCallOrder[0];
- expect(clearCallOrder).toBeLessThan(newWalletCallOrder);
- });
-
- test('disables automatic vault backup during wallet reset', async () => {
- // Arrange
- const { result } = renderHookWithProvider(() => useDeleteWallet());
- const [resetWalletState] = result.current;
-
- // Act
- await resetWalletState();
-
- // Assert - flag is re-enabled after reset completes
- expect(EngineClass.disableAutomaticVaultBackup).toBe(false);
- });
-
- test('re-enables automatic vault backup even when error occurs', async () => {
- // Arrange
- const { result } = renderHookWithProvider(() => useDeleteWallet());
- const [resetWalletState] = result.current;
- jest
- .spyOn(Authentication, 'newWalletAndKeychain')
- .mockRejectedValueOnce(new Error('Authentication failed'));
-
- // Act
- await resetWalletState();
-
- // Assert - flag is still re-enabled despite error
- expect(EngineClass.disableAutomaticVaultBackup).toBe(false);
- });
-
- test('calls all required methods to reset wallet state', async () => {
- // Arrange
- const { result } = renderHookWithProvider(() => useDeleteWallet());
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const [resetWalletState, _] = result.current;
- const newWalletAndKeychain = jest.spyOn(
- Authentication,
- 'newWalletAndKeychain',
- );
- const clearStateSpy = jest.spyOn(
- Engine.context.SeedlessOnboardingController,
- 'clearState',
- );
- const resetRewardsSpy = jest.spyOn(Engine.controllerMessenger, 'call');
- const loggerSpy = jest.spyOn(Logger, 'log');
- const resetProviderTokenSpy = jest.mocked(depositResetProviderToken);
-
- // Act
- await resetWalletState();
-
- // Assert
- expect(newWalletAndKeychain).toHaveBeenCalledWith(expect.any(String), {
- currentAuthType: AUTHENTICATION_TYPE.UNKNOWN,
- });
- expect(clearStateSpy).toHaveBeenCalledTimes(1);
- expect(resetRewardsSpy).toHaveBeenCalledTimes(1);
- expect(resetRewardsSpy).toHaveBeenCalledWith('RewardsController:resetAll');
- expect(loggerSpy).not.toHaveBeenCalled();
- expect(resetProviderTokenSpy).toHaveBeenCalledTimes(1);
- });
-
- test('logs error when resetWalletState fails', async () => {
- // Arrange
- const { result } = renderHookWithProvider(() => useDeleteWallet());
- const [resetWalletState] = result.current;
- const newWalletAndKeychain = jest.spyOn(
- Authentication,
- 'newWalletAndKeychain',
- );
- const loggerSpy = jest.spyOn(Logger, 'log');
- newWalletAndKeychain.mockRejectedValueOnce(
- new Error('Authentication failed'),
- );
-
- // Act
- await resetWalletState();
-
- // Assert
- expect(newWalletAndKeychain).toHaveBeenCalled();
- expect(loggerSpy).toHaveBeenCalledWith(
- expect.any(Error),
- expect.stringContaining('Failed to createNewVaultAndKeychain'),
- );
- });
-
- test('dispatches Redux action to delete user', async () => {
- // Arrange
- const { result } = renderHookWithProvider(() => useDeleteWallet());
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const [_, deleteUser] = result.current;
- const loggerSpy = jest.spyOn(Logger, 'log');
-
- // Act
- await deleteUser();
-
- // Assert - Redux action was dispatched (handled by store)
- expect(loggerSpy).not.toHaveBeenCalled();
- });
-
- test('completes without throwing when deleteUser succeeds', async () => {
- // Arrange
- const { result } = renderHookWithProvider(() => useDeleteWallet());
- const [, deleteUser] = result.current;
- const loggerSpy = jest.spyOn(Logger, 'log');
-
- // Act & Assert
- await expect(deleteUser()).resolves.not.toThrow();
- expect(loggerSpy).not.toHaveBeenCalled();
- });
-});
diff --git a/app/components/hooks/DeleteWallet/useDeleteWallet.ts b/app/components/hooks/DeleteWallet/useDeleteWallet.ts
deleted file mode 100644
index ff478e003f9..00000000000
--- a/app/components/hooks/DeleteWallet/useDeleteWallet.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { useCallback } from 'react';
-import { useDispatch } from 'react-redux';
-import Logger from '../../../util/Logger';
-import { setExistingUser } from '../../../actions/user';
-import { Authentication } from '../../../core';
-import AUTHENTICATION_TYPE from '../../../constants/userProperties';
-import { clearAllVaultBackups } from '../../../core/BackupVault';
-import { useMetrics } from '../useMetrics';
-import Engine from '../../../core/Engine';
-import { Engine as EngineClass } from '../../../core/Engine/Engine';
-import { resetProviderToken as depositResetProviderToken } from '../../UI/Ramp/Deposit/utils/ProviderTokenVault';
-
-const useDeleteWallet = () => {
- const metrics = useMetrics();
- const dispatch = useDispatch();
-
- const resetWalletState = useCallback(async () => {
- try {
- // Clear vault backups BEFORE creating temporary wallet
- await clearAllVaultBackups();
-
- // CRITICAL: Disable automatic vault backups during wallet RESET
- // This prevents the temporary wallet (created during reset) from being backed up
- EngineClass.disableAutomaticVaultBackup = true;
-
- try {
- await Authentication.newWalletAndKeychain(`${Date.now()}`, {
- currentAuthType: AUTHENTICATION_TYPE.UNKNOWN,
- });
-
- Engine.context.SeedlessOnboardingController.clearState();
-
- await depositResetProviderToken();
-
- await Engine.controllerMessenger.call('RewardsController:resetAll');
-
- // Lock the app and navigate to onboarding
- await Authentication.lockApp({ navigateToLogin: false });
- } finally {
- // ALWAYS re-enable automatic vault backups, even if error occurs
- EngineClass.disableAutomaticVaultBackup = false;
- }
- } catch (error) {
- const errorMsg = `Failed to createNewVaultAndKeychain: ${error}`;
- Logger.log(error, errorMsg);
- }
- }, []);
-
- const deleteUser = async () => {
- try {
- dispatch(setExistingUser(false));
- await metrics.createDataDeletionTask();
- } catch (error) {
- const errorMsg = `Failed to reset existingUser state in Redux`;
- Logger.log(error, errorMsg);
- }
- };
-
- return [resetWalletState, deleteUser];
-};
-
-export default useDeleteWallet;
diff --git a/app/components/hooks/SeedlessHooks/usePromptSeedlessRelogin.test.tsx b/app/components/hooks/SeedlessHooks/usePromptSeedlessRelogin.test.tsx
index 16b0a945b26..c32e78a43f5 100644
--- a/app/components/hooks/SeedlessHooks/usePromptSeedlessRelogin.test.tsx
+++ b/app/components/hooks/SeedlessHooks/usePromptSeedlessRelogin.test.tsx
@@ -6,22 +6,17 @@ import thunk from 'redux-thunk';
import usePromptSeedlessRelogin from './usePromptSeedlessRelogin';
import Routes from '../../../constants/navigation/Routes';
import { strings } from '../../../../locales/i18n';
-import storageWrapper from '../../../store/storage-wrapper';
-import { OPTIN_META_METRICS_UI_SEEN } from '../../../constants/storage';
import { clearHistory } from '../../../actions/browser';
-import { setCompletedOnboarding } from '../../../actions/onboarding';
// Mock dependencies
jest.mock('../useMetrics');
jest.mock('../../../util/identity/hooks/useAuthentication');
-jest.mock('../DeleteWallet');
-jest.mock('../../../store/storage-wrapper', () => ({
- getItem: jest.fn(),
- setItem: jest.fn(),
- removeItem: jest.fn(),
+jest.mock('../../../core/Authentication/Authentication', () => ({
+ Authentication: {
+ deleteWallet: jest.fn(),
+ },
}));
jest.mock('../../../actions/browser');
-jest.mock('../../../actions/onboarding');
// Mock navigation
const mockNavigate = jest.fn();
@@ -38,25 +33,20 @@ jest.mock('@react-navigation/native', () => ({
// Mock imports
import { useMetrics } from '../useMetrics';
import { useSignOut } from '../../../util/identity/hooks/useAuthentication';
-import { useDeleteWallet } from '../DeleteWallet';
+import { Authentication } from '../../../core/Authentication/Authentication';
const mockUseMetrics = useMetrics as jest.MockedFunction;
const mockUseSignOut = useSignOut as jest.MockedFunction;
-const mockUseDeleteWallet = useDeleteWallet as jest.MockedFunction<
- typeof useDeleteWallet
->;
-const mockStorageWrapper = storageWrapper as jest.Mocked;
const mockClearHistory = clearHistory as jest.MockedFunction<
typeof clearHistory
>;
-const mockSetCompletedOnboarding =
- setCompletedOnboarding as jest.MockedFunction;
+const mockDeleteWallet = Authentication.deleteWallet as jest.MockedFunction<
+ typeof Authentication.deleteWallet
+>;
describe('usePromptSeedlessRelogin', () => {
const mockStore = configureMockStore([thunk]);
const mockSignOut = jest.fn();
- const mockResetWalletState = jest.fn();
- const mockDeleteUser = jest.fn();
const mockMetrics = {
isEnabled: jest.fn().mockReturnValue(true),
trackEvent: jest.fn(),
@@ -94,18 +84,13 @@ describe('usePromptSeedlessRelogin', () => {
// Setup mocks
mockUseMetrics.mockReturnValue(mockMetrics);
mockUseSignOut.mockReturnValue({ signOut: mockSignOut });
- mockUseDeleteWallet.mockReturnValue([mockResetWalletState, mockDeleteUser]);
- (mockStorageWrapper.removeItem as jest.Mock).mockResolvedValue(undefined);
+ mockDeleteWallet.mockResolvedValue(undefined);
mockClearHistory.mockReturnValue({
type: 'CLEAR_BROWSER_HISTORY',
id: expect.any(Number),
metricsEnabled: expect.any(Boolean),
marketingEnabled: expect.any(Boolean),
});
- mockSetCompletedOnboarding.mockReturnValue({
- type: 'SET_COMPLETED_ONBOARDING',
- completedOnboarding: expect.any(Boolean),
- });
});
describe('hook initialization', () => {
@@ -213,12 +198,7 @@ describe('usePromptSeedlessRelogin', () => {
// Assert
expect(mockClearHistory).toHaveBeenCalledWith(true, true);
expect(mockSignOut).toHaveBeenCalledTimes(1);
- expect(mockResetWalletState).toHaveBeenCalledTimes(1);
- expect(mockDeleteUser).toHaveBeenCalledTimes(1);
- expect(mockStorageWrapper.removeItem).toHaveBeenCalledWith(
- OPTIN_META_METRICS_UI_SEEN,
- );
- expect(mockSetCompletedOnboarding).toHaveBeenCalledWith(false);
+ expect(mockDeleteWallet).toHaveBeenCalledTimes(1);
});
it('navigates to onboarding root after deletion flow', async () => {
@@ -260,7 +240,7 @@ describe('usePromptSeedlessRelogin', () => {
});
});
- it('dispatches Redux actions in correct order', async () => {
+ it('dispatches clearHistory action when deleting wallet', async () => {
// Arrange
const { result } = renderHookWithProvider(() =>
usePromptSeedlessRelogin(),
@@ -287,10 +267,6 @@ describe('usePromptSeedlessRelogin', () => {
metricsEnabled: expect.any(Boolean),
marketingEnabled: expect.any(Boolean),
},
- {
- type: 'SET_COMPLETED_ONBOARDING',
- completedOnboarding: expect.any(Boolean),
- },
]);
});
@@ -327,12 +303,12 @@ describe('usePromptSeedlessRelogin', () => {
usePromptSeedlessRelogin(),
);
- // Make resetWalletState async to test loading state
- let resolveResetWallet: () => void;
- const resetWalletPromise = new Promise((resolve) => {
- resolveResetWallet = resolve;
+ // Make deleteWallet async to test loading state
+ let resolveDeleteWallet: () => void;
+ const deleteWalletPromise = new Promise((resolve) => {
+ resolveDeleteWallet = resolve;
});
- mockResetWalletState.mockReturnValue(resetWalletPromise);
+ mockDeleteWallet.mockReturnValueOnce(deleteWalletPromise);
act(() => {
result.current.promptSeedlessRelogin();
@@ -350,9 +326,9 @@ describe('usePromptSeedlessRelogin', () => {
// Assert - check loading state is true during deletion
expect(result.current.isDeletingInProgress).toBe(true);
- // Complete the reset wallet operation
+ // Complete the delete wallet operation
act(() => {
- resolveResetWallet();
+ resolveDeleteWallet();
});
// Wait for completion
@@ -393,7 +369,7 @@ describe('usePromptSeedlessRelogin', () => {
const { result } = renderHookWithProvider(() =>
usePromptSeedlessRelogin(),
);
- mockResetWalletState.mockRejectedValueOnce(new Error('Reset failed'));
+ mockDeleteWallet.mockRejectedValueOnce(new Error('Reset failed'));
act(() => {
result.current.promptSeedlessRelogin();
@@ -417,14 +393,12 @@ describe('usePromptSeedlessRelogin', () => {
);
});
- it('handles storage removal failure gracefully', async () => {
+ it('handles wallet deletion failure gracefully', async () => {
// Arrange
const { result } = renderHookWithProvider(() =>
usePromptSeedlessRelogin(),
);
- (mockStorageWrapper.removeItem as jest.Mock).mockRejectedValueOnce(
- new Error('Storage error'),
- );
+ mockDeleteWallet.mockRejectedValueOnce(new Error('Deletion error'));
act(() => {
result.current.promptSeedlessRelogin();
@@ -442,11 +416,11 @@ describe('usePromptSeedlessRelogin', () => {
// Assert - error state is set
expect(result.current.deleteWalletError).toEqual(
- new Error('Storage error'),
+ new Error('Deletion error'),
);
// Assert other operations were still attempted
expect(mockSignOut).toHaveBeenCalledTimes(1);
- expect(mockResetWalletState).toHaveBeenCalledTimes(1);
+ expect(mockDeleteWallet).toHaveBeenCalledTimes(1);
});
});
diff --git a/app/components/hooks/SeedlessHooks/usePromptSeedlessRelogin.ts b/app/components/hooks/SeedlessHooks/usePromptSeedlessRelogin.ts
index a71e953b57b..f07bd51ce31 100644
--- a/app/components/hooks/SeedlessHooks/usePromptSeedlessRelogin.ts
+++ b/app/components/hooks/SeedlessHooks/usePromptSeedlessRelogin.ts
@@ -6,18 +6,14 @@ import { useSignOut } from '../../../util/identity/hooks/useAuthentication';
import Routes from '../../../constants/navigation/Routes';
import { useNavigation } from '@react-navigation/native';
import { SuccessErrorSheetParams } from '../../Views/SuccessErrorSheet/interface';
-import storageWrapper from '../../../store/storage-wrapper';
-import { OPTIN_META_METRICS_UI_SEEN } from '../../../constants/storage';
import { clearHistory } from '../../../actions/browser';
import { strings } from '../../../../locales/i18n';
-import { setCompletedOnboarding } from '../../../actions/onboarding';
-import { useDeleteWallet } from '../DeleteWallet';
+import { Authentication } from '../../../core';
import Logger from '../../../util/Logger';
const usePromptSeedlessRelogin = () => {
const metrics = useMetrics();
const dispatch = useDispatch();
- const [resetWalletState, deleteUser] = useDeleteWallet();
const [isDeletingInProgress, setIsDeletingInProgress] = useState(false);
const [deleteWalletError, setDeleteWalletError] = useState(
@@ -58,10 +54,7 @@ const usePromptSeedlessRelogin = () => {
clearHistory(metrics.isEnabled(), isDataCollectionForMarketingEnabled),
);
signOut();
- await resetWalletState();
- await deleteUser();
- await storageWrapper.removeItem(OPTIN_META_METRICS_UI_SEEN);
- dispatch(setCompletedOnboarding(false));
+ await Authentication.deleteWallet();
navigateOnboardingRoot();
setIsDeletingInProgress(false);
}, [
@@ -70,8 +63,6 @@ const usePromptSeedlessRelogin = () => {
isDataCollectionForMarketingEnabled,
navigateOnboardingRoot,
signOut,
- resetWalletState,
- deleteUser,
]);
const promptSeedlessRelogin = useCallback(() => {
diff --git a/app/core/Authentication/Authentication.test.ts b/app/core/Authentication/Authentication.test.ts
index d07644aeb35..5b661797e0e 100644
--- a/app/core/Authentication/Authentication.test.ts
+++ b/app/core/Authentication/Authentication.test.ts
@@ -4,6 +4,7 @@ import {
TRUE,
PASSCODE_DISABLED,
SOLANA_DISCOVERY_PENDING,
+ OPTIN_META_METRICS_UI_SEEN,
} from '../../constants/storage';
import { Authentication } from './Authentication';
import AUTHENTICATION_TYPE from '../../constants/userProperties';
@@ -36,12 +37,18 @@ import { EncryptionKey } from '@metamask/browser-passworder';
import { uint8ArrayToMnemonic } from '../../util/mnemonic';
import { SolScope } from '@metamask/keyring-api';
import { logOut, setExistingUser, logIn } from '../../actions/user';
+import { setCompletedOnboarding } from '../../actions/onboarding';
import { RootState } from '../../reducers';
import {
SeedlessOnboardingControllerError,
SeedlessOnboardingControllerErrorType,
} from '../Engine/controllers/seedless-onboarding-controller/error';
import { TraceName, TraceOperation } from '../../util/trace';
+import MetaMetrics from '../Analytics/MetaMetrics';
+import { resetProviderToken as depositResetProviderToken } from '../../components/UI/Ramp/Deposit/utils/ProviderTokenVault';
+import { clearAllVaultBackups } from '../BackupVault/backupVault';
+import { Engine as EngineClass } from '../Engine/Engine';
+import Logger from '../../util/Logger';
export type RecursivePartial = {
[P in keyof T]?: RecursivePartial;
@@ -100,6 +107,9 @@ jest.mock('../SnapKeyring/MultichainWalletSnapClient', () => ({
jest.mock('../Engine', () => ({
resetState: jest.fn(),
+ controllerMessenger: {
+ call: jest.fn(),
+ },
context: {
KeyringController: {
createNewVaultAndKeychain: jest.fn(),
@@ -159,6 +169,22 @@ jest.mock('../BackupVault/backupVault', () => ({
clearAllVaultBackups: jest.fn(),
}));
+jest.mock('../Analytics/MetaMetrics', () => {
+ const mockInstance = {
+ createDataDeletionTask: jest.fn(),
+ };
+ return {
+ __esModule: true,
+ default: {
+ getInstance: jest.fn(() => mockInstance),
+ },
+ };
+});
+
+jest.mock('../../components/UI/Ramp/Deposit/utils/ProviderTokenVault', () => ({
+ resetProviderToken: jest.fn(),
+}));
+
jest.mock('../../multichain-accounts/AccountTreeInitService', () => ({
initializeAccountTree: jest.fn().mockResolvedValue(undefined),
clearState: jest.fn().mockResolvedValue(undefined),
@@ -1349,12 +1375,10 @@ describe('Authentication', () => {
let Engine: typeof import('../Engine').default;
let OAuthService: typeof import('../OAuthService/OAuthService').default;
- let Logger: jest.Mocked;
beforeEach(() => {
Engine = jest.requireMock('../Engine');
OAuthService = jest.requireMock('../OAuthService/OAuthService');
- Logger = jest.requireMock('../../util/Logger');
jest.spyOn(ReduxService, 'store', 'get').mockReturnValue({
dispatch: jest.fn(),
@@ -3166,4 +3190,333 @@ describe('Authentication', () => {
);
});
});
+
+ describe('deleteWallet', () => {
+ let Engine: typeof import('../Engine').default;
+ let mockDispatch: jest.Mock;
+ let mockMetaMetricsInstance: {
+ createDataDeletionTask: jest.MockedFunction<() => Promise>;
+ };
+
+ beforeEach(() => {
+ Engine = jest.requireMock('../Engine');
+ jest.clearAllMocks();
+ EngineClass.disableAutomaticVaultBackup = false;
+ mockDispatch = jest.fn();
+ mockMetaMetricsInstance = {
+ createDataDeletionTask: jest.fn().mockResolvedValue(undefined),
+ };
+
+ jest.spyOn(ReduxService, 'store', 'get').mockReturnValue({
+ dispatch: mockDispatch,
+ getState: () => ({ security: { allowLoginWithRememberMe: true } }),
+ } as unknown as ReduxStore);
+
+ Engine.context.SeedlessOnboardingController = {
+ clearState: jest.fn(),
+ setLocked: jest.fn().mockResolvedValue(undefined),
+ } as unknown as SeedlessOnboardingController;
+
+ Engine.context.KeyringController = {
+ setLocked: jest.fn().mockResolvedValue(undefined),
+ isUnlocked: jest.fn(() => true),
+ } as unknown as KeyringController;
+
+ jest
+ .spyOn(Authentication, 'newWalletAndKeychain')
+ .mockResolvedValue(undefined);
+ jest.spyOn(Authentication, 'lockApp').mockResolvedValue(undefined);
+
+ jest
+ .spyOn(MetaMetrics, 'getInstance')
+ .mockReturnValue(mockMetaMetricsInstance as unknown as MetaMetrics);
+ });
+
+ afterEach(() => {
+ EngineClass.disableAutomaticVaultBackup = false;
+ });
+
+ it('calls resetWalletState followed by deleteUser', async () => {
+ // Arrange
+ const resetWalletStateSpy = jest.spyOn(
+ Authentication as unknown as { resetWalletState: () => Promise },
+ 'resetWalletState',
+ );
+ const deleteUserSpy = jest.spyOn(
+ Authentication as unknown as { deleteUser: () => Promise },
+ 'deleteUser',
+ );
+
+ // Act
+ await Authentication.deleteWallet();
+
+ // Assert
+ expect(resetWalletStateSpy).toHaveBeenCalledTimes(1);
+ expect(deleteUserSpy).toHaveBeenCalledTimes(1);
+ const resetCallOrder = resetWalletStateSpy.mock.invocationCallOrder[0];
+ const deleteCallOrder = deleteUserSpy.mock.invocationCallOrder[0];
+ expect(resetCallOrder).toBeLessThan(deleteCallOrder);
+ });
+
+ it('completes wallet deletion successfully', async () => {
+ // Arrange
+ const clearVaultSpy = jest.mocked(clearAllVaultBackups);
+ const clearStateSpy = jest.spyOn(
+ Engine.context.SeedlessOnboardingController,
+ 'clearState',
+ );
+ const removeItemSpy = jest.spyOn(StorageWrapper, 'removeItem');
+
+ // Act
+ await Authentication.deleteWallet();
+
+ // Assert
+ expect(clearVaultSpy).toHaveBeenCalledTimes(1);
+ expect(clearStateSpy).toHaveBeenCalledTimes(1);
+ expect(mockDispatch).toHaveBeenCalledWith(setExistingUser(false));
+ expect(
+ mockMetaMetricsInstance.createDataDeletionTask,
+ ).toHaveBeenCalledTimes(1);
+ expect(removeItemSpy).toHaveBeenCalledWith(OPTIN_META_METRICS_UI_SEEN);
+ expect(mockDispatch).toHaveBeenCalledWith(setCompletedOnboarding(false));
+ expect(EngineClass.disableAutomaticVaultBackup).toBe(false);
+ });
+ });
+
+ describe('resetWalletState', () => {
+ let Engine: typeof import('../Engine').default;
+
+ beforeEach(() => {
+ Engine = jest.requireMock('../Engine');
+ jest.clearAllMocks();
+ EngineClass.disableAutomaticVaultBackup = false;
+
+ jest.spyOn(ReduxService, 'store', 'get').mockReturnValue({
+ dispatch: jest.fn(),
+ getState: () => ({ security: { allowLoginWithRememberMe: true } }),
+ } as unknown as ReduxStore);
+
+ Engine.context.SeedlessOnboardingController = {
+ clearState: jest.fn(),
+ setLocked: jest.fn().mockResolvedValue(undefined),
+ } as unknown as SeedlessOnboardingController;
+
+ Engine.context.KeyringController = {
+ setLocked: jest.fn().mockResolvedValue(undefined),
+ isUnlocked: jest.fn(() => true),
+ } as unknown as KeyringController;
+
+ jest
+ .spyOn(Authentication, 'newWalletAndKeychain')
+ .mockResolvedValue(undefined);
+ jest.spyOn(Authentication, 'lockApp').mockResolvedValue(undefined);
+ });
+
+ afterEach(() => {
+ EngineClass.disableAutomaticVaultBackup = false;
+ });
+
+ it('calls vault backup clear before creating temporary wallet', async () => {
+ // Arrange
+ const clearVaultSpy = jest.mocked(clearAllVaultBackups);
+ const newWalletSpy = jest.spyOn(Authentication, 'newWalletAndKeychain');
+
+ // Act
+ await (
+ Authentication as unknown as { resetWalletState: () => Promise }
+ ).resetWalletState();
+
+ // Assert
+ expect(clearVaultSpy).toHaveBeenCalledTimes(1);
+ const clearCallOrder = clearVaultSpy.mock.invocationCallOrder[0];
+ const newWalletCallOrder = newWalletSpy.mock.invocationCallOrder[0];
+ expect(clearCallOrder).toBeLessThan(newWalletCallOrder);
+ });
+
+ it('disables automatic vault backup during wallet reset', async () => {
+ // Act
+ await (
+ Authentication as unknown as { resetWalletState: () => Promise }
+ ).resetWalletState();
+
+ // Assert - flag is re-enabled after reset completes
+ expect(EngineClass.disableAutomaticVaultBackup).toBe(false);
+ });
+
+ it('re-enables automatic vault backup even when error occurs', async () => {
+ // Arrange
+ jest
+ .spyOn(Authentication, 'newWalletAndKeychain')
+ .mockRejectedValueOnce(new Error('Authentication failed'));
+
+ // Act
+ await (
+ Authentication as unknown as { resetWalletState: () => Promise }
+ ).resetWalletState();
+
+ // Assert - flag is still re-enabled despite error
+ expect(EngineClass.disableAutomaticVaultBackup).toBe(false);
+ });
+
+ it('calls all required methods to reset wallet state', async () => {
+ // Arrange
+ const newWalletAndKeychain = jest.spyOn(
+ Authentication,
+ 'newWalletAndKeychain',
+ );
+ const clearStateSpy = jest.spyOn(
+ Engine.context.SeedlessOnboardingController,
+ 'clearState',
+ );
+ const resetRewardsSpy = jest.spyOn(Engine.controllerMessenger, 'call');
+ const loggerSpy = jest.spyOn(Logger, 'log');
+ const resetProviderTokenSpy = jest.mocked(depositResetProviderToken);
+
+ // Act
+ await (
+ Authentication as unknown as { resetWalletState: () => Promise }
+ ).resetWalletState();
+
+ // Assert
+ expect(newWalletAndKeychain).toHaveBeenCalledWith(expect.any(String), {
+ currentAuthType: AUTHENTICATION_TYPE.UNKNOWN,
+ });
+ expect(clearStateSpy).toHaveBeenCalledTimes(1);
+ expect(resetRewardsSpy).toHaveBeenCalledTimes(1);
+ expect(resetRewardsSpy).toHaveBeenCalledWith(
+ 'RewardsController:resetAll',
+ );
+ expect(loggerSpy).not.toHaveBeenCalled();
+ expect(resetProviderTokenSpy).toHaveBeenCalledTimes(1);
+ expect(Authentication.lockApp).toHaveBeenCalledWith({
+ navigateToLogin: false,
+ });
+ });
+
+ it('logs error when resetWalletState fails', async () => {
+ // Arrange
+ const newWalletAndKeychain = jest.spyOn(
+ Authentication,
+ 'newWalletAndKeychain',
+ );
+ const loggerSpy = jest.spyOn(Logger, 'log');
+ newWalletAndKeychain.mockRejectedValueOnce(
+ new Error('Authentication failed'),
+ );
+
+ // Act
+ await (
+ Authentication as unknown as { resetWalletState: () => Promise }
+ ).resetWalletState();
+
+ // Assert
+ expect(newWalletAndKeychain).toHaveBeenCalled();
+ expect(loggerSpy).toHaveBeenCalledWith(
+ expect.any(Error),
+ expect.stringContaining('Failed to createNewVaultAndKeychain'),
+ );
+ });
+ });
+
+ describe('deleteUser', () => {
+ let mockDispatch: jest.Mock;
+ let mockMetaMetricsInstance: {
+ createDataDeletionTask: jest.MockedFunction<() => Promise>;
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockDispatch = jest.fn();
+ mockMetaMetricsInstance = {
+ createDataDeletionTask: jest.fn().mockResolvedValue(undefined),
+ };
+
+ jest.spyOn(ReduxService, 'store', 'get').mockReturnValue({
+ dispatch: mockDispatch,
+ getState: () => ({ security: { allowLoginWithRememberMe: true } }),
+ } as unknown as ReduxStore);
+
+ jest
+ .spyOn(MetaMetrics, 'getInstance')
+ .mockReturnValue(mockMetaMetricsInstance as unknown as MetaMetrics);
+ });
+
+ it('dispatches Redux action to set existing user to false', async () => {
+ // Act
+ await (
+ Authentication as unknown as { deleteUser: () => Promise }
+ ).deleteUser();
+
+ // Assert
+ expect(mockDispatch).toHaveBeenCalledWith(setExistingUser(false));
+ expect(
+ mockMetaMetricsInstance.createDataDeletionTask,
+ ).toHaveBeenCalledTimes(1);
+ });
+
+ it('creates data deletion task', async () => {
+ // Act
+ await (
+ Authentication as unknown as { deleteUser: () => Promise }
+ ).deleteUser();
+
+ // Assert
+ expect(
+ mockMetaMetricsInstance.createDataDeletionTask,
+ ).toHaveBeenCalledTimes(1);
+ });
+
+ it('completes without throwing when deleteUser succeeds', async () => {
+ // Arrange
+ const loggerSpy = jest.spyOn(Logger, 'log');
+
+ // Act & Assert
+ await expect(
+ (
+ Authentication as unknown as { deleteUser: () => Promise }
+ ).deleteUser(),
+ ).resolves.not.toThrow();
+ expect(loggerSpy).not.toHaveBeenCalled();
+ });
+
+ it('logs error when deleteUser fails', async () => {
+ // Arrange
+ const error = new Error('Data deletion failed');
+ mockMetaMetricsInstance.createDataDeletionTask.mockRejectedValueOnce(
+ error,
+ );
+ const loggerSpy = jest.spyOn(Logger, 'log');
+
+ // Act
+ await (
+ Authentication as unknown as { deleteUser: () => Promise }
+ ).deleteUser();
+
+ // Assert
+ expect(loggerSpy).toHaveBeenCalledWith(
+ error,
+ 'Failed to reset existingUser state in Redux',
+ );
+ });
+
+ it('logs error when Redux dispatch fails', async () => {
+ // Arrange
+ const error = new Error('Dispatch failed');
+ mockDispatch.mockImplementation(() => {
+ throw error;
+ });
+ const loggerSpy = jest.spyOn(Logger, 'log');
+
+ // Act
+ await (
+ Authentication as unknown as { deleteUser: () => Promise }
+ ).deleteUser();
+
+ // Assert
+ expect(loggerSpy).toHaveBeenCalledWith(
+ error,
+ 'Failed to reset existingUser state in Redux',
+ );
+ });
+ });
});
diff --git a/app/core/Authentication/Authentication.ts b/app/core/Authentication/Authentication.ts
index ac8c93bc1fb..ab842dae6ec 100644
--- a/app/core/Authentication/Authentication.ts
+++ b/app/core/Authentication/Authentication.ts
@@ -6,6 +6,7 @@ import {
TRUE,
PASSCODE_DISABLED,
SEED_PHRASE_HINTS,
+ OPTIN_META_METRICS_UI_SEEN,
} from '../../constants/storage';
import {
authSuccess,
@@ -16,6 +17,7 @@ import {
setExistingUser,
setIsConnectionRemoved,
} from '../../actions/user';
+import { setCompletedOnboarding } from '../../actions/onboarding';
import AUTHENTICATION_TYPE from '../../constants/userProperties';
import AuthenticationError from './AuthenticationError';
import { UserCredentials, BIOMETRY_TYPE } from 'react-native-keychain';
@@ -73,6 +75,8 @@ import AccountTreeInitService from '../../multichain-accounts/AccountTreeInitSer
import { renewSeedlessControllerRefreshTokens } from '../OAuthService/SeedlessControllerHelper';
import { EntropySourceId } from '@metamask/keyring-api';
import { trackVaultCorruption } from '../../util/analytics/vaultCorruptionTracking';
+import MetaMetrics from '../Analytics/MetaMetrics';
+import { resetProviderToken as depositResetProviderToken } from '../../components/UI/Ramp/Deposit/utils/ProviderTokenVault';
/**
* Holds auth data used to determine auth configuration
@@ -1294,6 +1298,78 @@ class AuthenticationService {
keyringEncryptionKey,
);
};
+
+ /**
+ * Deletes the wallet by resetting wallet state and deleting user data.
+ * This is the main public method for wallet deletion/reset flows.
+ * It calls resetWalletState() followed by deleteUser(), and also clears
+ * metrics opt-in UI state and resets onboarding completion status.
+ *
+ * @returns {Promise}
+ */
+ deleteWallet = async (): Promise => {
+ await this.resetWalletState();
+ await this.deleteUser();
+ // Clear metrics opt-in UI state and reset onboarding completion
+ await StorageWrapper.removeItem(OPTIN_META_METRICS_UI_SEEN);
+ ReduxService.store.dispatch(setCompletedOnboarding(false));
+ };
+
+ /**
+ * Resets the wallet state by creating a new wallet and clearing all related state.
+ * This is used during wallet deletion/reset flows.
+ * Protected method - use deleteWallet() instead for complete wallet deletion.
+ *
+ * @returns {Promise}
+ */
+ protected async resetWalletState(): Promise {
+ try {
+ // Clear vault backups BEFORE creating temporary wallet
+ await clearAllVaultBackups();
+
+ // CRITICAL: Disable automatic vault backups during wallet RESET
+ // This prevents the temporary wallet (created during reset) from being backed up
+ EngineClass.disableAutomaticVaultBackup = true;
+
+ try {
+ await this.newWalletAndKeychain(`${Date.now()}`, {
+ currentAuthType: AUTHENTICATION_TYPE.UNKNOWN,
+ });
+
+ Engine.context.SeedlessOnboardingController.clearState();
+
+ await depositResetProviderToken();
+
+ await Engine.controllerMessenger.call('RewardsController:resetAll');
+
+ // Lock the app and navigate to onboarding
+ await this.lockApp({ navigateToLogin: false });
+ } finally {
+ // ALWAYS re-enable automatic vault backups, even if error occurs
+ EngineClass.disableAutomaticVaultBackup = false;
+ }
+ } catch (error) {
+ const errorMsg = `Failed to createNewVaultAndKeychain: ${error}`;
+ Logger.log(error, errorMsg);
+ }
+ }
+
+ /**
+ * Deletes user data by setting existing user state to false and creating a data deletion task.
+ * This is used during wallet deletion flows.
+ * Protected method - use deleteWallet() instead for complete wallet deletion.
+ *
+ * @returns {Promise}
+ */
+ protected async deleteUser(): Promise {
+ try {
+ ReduxService.store.dispatch(setExistingUser(false));
+ await MetaMetrics.getInstance().createDataDeletionTask();
+ } catch (error) {
+ const errorMsg = `Failed to reset existingUser state in Redux`;
+ Logger.log(error, errorMsg);
+ }
+ }
}
export const Authentication = new AuthenticationService();