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