From fa1b19fc725910a8d1c2480435dc41e52917d13d Mon Sep 17 00:00:00 2001 From: Matthew Grainger <46547583+Matt561@users.noreply.github.com> Date: Wed, 10 Dec 2025 18:43:49 -0500 Subject: [PATCH 1/4] fix: cp-7.61.0 Perps eligibility refresh race condition causing users to be incorrectly geo-blocked (#23895) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fix race condition causing users to be geo-blocked incorrectly. The Bug: `refreshEligibility()` is called with fallback regions but isn't awaited — so the constructor continues, updates to remote config, and starts new eligibility checks. The fallback check finishes last and overwrites the correct remote result with false. Workaround: a version counter ensures stale checks (started before the config changed) are discarded when they complete. ## **Changelog** CHANGELOG entry: fix perps refreshEligibility race condition causing users to be geo-blocked ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Guards async eligibility updates with a versioned blocked-region list to avoid stale fallback results overwriting remote-config checks. > > - **PerpsController (`app/components/UI/Perps/controllers/PerpsController.ts`)** > - Add `blockedRegionListVersion` to track changes to `blockedRegionList`. > - Increment version when setting blocked regions in `setBlockedRegionList` and in `refreshEligibilityOnFeatureFlagChange`. > - Update `refreshEligibility()` to: > - Capture version at start and skip state updates if version changed while awaiting. > - Only set default eligible on error if version is unchanged. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 1fa308b498e659e5550e6dedb89fe71d2a11566f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Nicholas Gambino --- .../UI/Perps/controllers/PerpsController.ts | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/app/components/UI/Perps/controllers/PerpsController.ts b/app/components/UI/Perps/controllers/PerpsController.ts index fa53a83434b..e8b39dfae5b 100644 --- a/app/components/UI/Perps/controllers/PerpsController.ts +++ b/app/components/UI/Perps/controllers/PerpsController.ts @@ -679,6 +679,14 @@ export class PerpsController extends BaseController< source: 'fallback', }; + /** + * Version counter for blocked region list. + * Used to prevent race conditions where stale eligibility checks + * (started with fallback config) overwrite results from newer checks + * (started with remote config). + */ + private blockedRegionListVersion = 0; + // Store HIP-3 configuration (mutable for runtime updates from remote flags) private hip3Enabled: boolean; private hip3AllowlistMarkets: string[]; @@ -781,6 +789,7 @@ export class PerpsController extends BaseController< newSource: 'remote' | 'fallback', ) => { 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; + }); + } } } From fa777c2fd125512b422ba693bd42886ce2c2dd1d Mon Sep 17 00:00:00 2001 From: Vince Howard Date: Wed, 10 Dec 2025 16:44:09 -0700 Subject: [PATCH 2/4] fix: remove background from more button in multichain account selector (#23904) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** remove background from more button in multichain account selector ## **Changelog** CHANGELOG entry:null ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MDP-277 ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** | before | after | | -------- | ------- | | ![before](https://github.com/user-attachments/assets/ac3d8983-b0e4-4310-90c9-7ac210a563b1) | ![after](https://github.com/user-attachments/assets/55113c0c-92c8-4432-9405-3f4d3161e832) | ### **Before** `~` ### **After** `~` ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Removes background, border radius, and fixed dimensions from the multichain account cell "more" menu button, updating snapshots accordingly. > > - **UI (Multichain Accounts)** > - `AccountCell.styles.ts`: Remove `menuButton` `backgroundColor`, `borderRadius`, `height`, and `width` to make the "more" button icon-only. > - **Tests** > - Update snapshots in `MultichainAccountsConnectedList.test.tsx.snap` to reflect the menu button style change. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5b2e2cc42f92b658a61535947a73e9f329be4eb5. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../MultichainAccounts/AccountCell/AccountCell.styles.ts | 4 ---- .../MultichainAccountsConnectedList.test.tsx.snap | 8 -------- 2 files changed, 12 deletions(-) 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/components/Views/MultichainAccounts/MultichainAccountsConnectedList/__snapshots__/MultichainAccountsConnectedList.test.tsx.snap b/app/components/Views/MultichainAccounts/MultichainAccountsConnectedList/__snapshots__/MultichainAccountsConnectedList.test.tsx.snap index 765ddb5e7b1..aff55a1d11e 100644 --- a/app/components/Views/MultichainAccounts/MultichainAccountsConnectedList/__snapshots__/MultichainAccountsConnectedList.test.tsx.snap +++ b/app/components/Views/MultichainAccounts/MultichainAccountsConnectedList/__snapshots__/MultichainAccountsConnectedList.test.tsx.snap @@ -250,13 +250,9 @@ exports[`MultichainAccountsConnectedList renders component with different accoun style={ { "alignItems": "center", - "backgroundColor": "#3c4d9d0f", - "borderRadius": 8, "display": "flex", "flexDirection": "row", - "height": 28, "justifyContent": "center", - "width": 28, } } testID="multichain-account-cell-menu" @@ -459,13 +455,9 @@ exports[`MultichainAccountsConnectedList renders component with different accoun style={ { "alignItems": "center", - "backgroundColor": "#3c4d9d0f", - "borderRadius": 8, "display": "flex", "flexDirection": "row", - "height": 28, "justifyContent": "center", - "width": 28, } } testID="multichain-account-cell-menu" From e630595a7bc469b40033491ed8d8402bd8a7b612 Mon Sep 17 00:00:00 2001 From: Vince Howard Date: Wed, 10 Dec 2025 16:54:10 -0700 Subject: [PATCH 3/4] chore: update selected item state background color (#23894) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Updating most instances of selected item background color to `colors.background.muted` ## **Changelog** CHANGELOG entry:null ## **Related issues** Fixes: Region Picker: https://consensyssoftware.atlassian.net/browse/MDP-280 Network Picker: https://consensyssoftware.atlassian.net/browse/MDP-239 Sort: https://consensyssoftware.atlassian.net/browse/MDP-243 ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** ### Network Manager | before | after | | -------- | ------- | | ![before](https://github.com/user-attachments/assets/f1b42b64-fd93-4637-8cc7-221a4c06f701) | ![after](https://github.com/user-attachments/assets/17da788d-b98b-490a-b6eb-b83daa7a9f77) | ### RPC Selection | before | after | | -------- | ------- | | ![before](https://github.com/user-attachments/assets/a2f61ed3-7824-4d50-947e-56009092105f) | ![after](https://github.com/user-attachments/assets/11ac9bf1-944c-4515-ba0e-300b8c8267a2) | ### Sort | before | after | | -------- | ------- | | ![before](https://github.com/user-attachments/assets/526f51e0-66bb-4a06-a624-d7a96d1defbe) | ![after](https://github.com/user-attachments/assets/33ac49a5-5556-4723-bcad-b8e08f79acd5) | ### Payment Selector | before | after | | -------- | ------- | | ![before](https://github.com/user-attachments/assets/d82a614b-9e2e-4360-915e-afd23267120a) | ![after](https://github.com/user-attachments/assets/596288fd-2aaa-43ee-aba6-416dcd6396a0) | ### Region Picker | before | after | | -------- | ------- | | ![before](https://github.com/user-attachments/assets/18557999-e62a-4b46-b329-9a187cd6020a) | ![after](https://github.com/user-attachments/assets/c669f5d9-1f58-473e-a736-13c2bbbcf986) | ### Select Network | before | after | | -------- | ------- | | ![before](https://github.com/user-attachments/assets/e6b0ca02-ce25-4ed1-b9e6-653c3b991ee1) | ![after](https://github.com/user-attachments/assets/9d9f47a6-2b85-4fb1-89e1-d2617c453f26) | ### **Before** `~` ### **After** `~` ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Updates selected item visuals across lists/selectors to use `colors.background.muted` and drops the side underlay bar indicator, with snapshots adjusted. > > - **Styling/UX**: > - Use `colors.background.muted` for selected states in `ListItemMultiSelect`, `ListItemSelect`, `ListItemMultiSelectButton`, and downstream UI (account/network/region/token selectors, permissions screens). > - Remove underlay side bar visuals (`underlayBar`, width strip) and related props/tests; retain simple full-row underlay where applicable. > - **Components**: > - `components-temp/ListItemMultiSelectButton`: selected container background updated; removed underlay view rendering; tests simplified (icon/press only). > - `components/List/ListItemSelect`: underlay color changed; removed `underlayBar`; kept minimal `underlay` view. > - `components/List/ListItemMultiSelect`: underlay color changed. > - Remove unused constant `SELECTABLE_ITEM_UNDERLAY_ID`. > - **Tests/Snapshots**: > - Snapshot updates to reflect color change (`#4459ff1a` → `#3c4d9d0f`) and removal of side bar indicator across numerous selector/modal views. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9dbd116f8536cb922fa47ea32b4134a04b80a470. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../ListItemMultiSelectButton.styles.ts | 15 +-- .../ListItemMultiSelectButton.test.tsx | 18 --- .../ListItemMultiSelectButton.tsx | 5 - .../ListItemMultiSelect.styles.ts | 2 +- .../ListItemSelect.constants.ts | 3 - .../ListItemSelect/ListItemSelect.styles.ts | 9 +- .../List/ListItemSelect/ListItemSelect.tsx | 4 +- .../CaipAccountSelectorList.test.tsx.snap | 120 +----------------- .../EvmAccountSelectorList.test.tsx.snap | 120 +----------------- .../NetworkSelectorList.test.tsx.snap | 2 +- .../RegionSelectorModal.test.tsx.snap | 32 +---- .../PaymentMethodSelectorModal.test.tsx.snap | 16 +-- .../RegionSelectorModal.test.tsx.snap | 64 ++-------- .../TokenSelectorModal.test.tsx.snap | 16 +-- .../AccountConnectMultiSelector.test.tsx.snap | 2 +- .../AccountPermissions.test.tsx.snap | 30 +---- .../AddressSelector.test.tsx.snap | 16 +-- .../NetworkConnectMultiSelector.test.tsx.snap | 2 +- .../RpcSelectionModal.test.tsx.snap | 16 +-- .../NetworkSelector.test.tsx.snap | 46 +------ 20 files changed, 39 insertions(+), 499 deletions(-) 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/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`] = ` - - - - - - - - - - - - - - - - - - - - - - - - - - + /> - - + /> - - + /> - - + /> - - + /> - - + /> - - + /> - - + /> - - - - - + /> - - + /> 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, } } - > - - + /> - - - Date: Thu, 11 Dec 2025 00:09:55 +0000 Subject: [PATCH 4/4] chore: refactor delete wallet into Authentication service (#23882) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR refactors wallet deletion logic by consolidating the `useDeleteWallet` hook into the `Authentication` service class. The refactoring improves code organization, ensures consistent wallet deletion behavior, and simplifies the codebase by removing redundant hook logic. **Reason for the change:** 1. The `useDeleteWallet` hook duplicated logic that should be centralized in the `Authentication` service 2. `resetWalletState` and `deleteUser` were always used together, but there was no enforcement of this pattern 3. Having wallet deletion logic in a hook created unnecessary coupling between React components and core authentication logic 4. The hook pattern was not appropriate for core service operations that don't depend on React lifecycle **Improvement/Solution:** 1. **Moved Logic to Authentication Service**: Consolidated `resetWalletState` and `deleteUser` methods into `app/core/Authentication/Authentication.ts` as protected methods 2. **Created Public API**: Added `deleteWallet()` as the main public method that ensures `resetWalletState()` is always called before `deleteUser()` 3. **Protected Methods**: Made `resetWalletState` and `deleteUser` protected to prevent direct external access and enforce the correct usage pattern through `deleteWallet()` 4. **Updated All Call Sites**: Refactored `usePromptSeedlessRelogin` and `DeleteWalletModal` to use the new `Authentication.deleteWallet()` method 5. **Comprehensive Test Coverage**: Added tests for the new `deleteWallet()` method and maintained test coverage for the protected methods **Key Technical Improvements:** - **Removed Hook**: Deleted `app/components/hooks/DeleteWallet/useDeleteWallet.ts` and related test files - **Centralized Logic**: All wallet deletion logic is now in the `Authentication` service, following the single responsibility principle - **Enforced Usage Pattern**: The protected methods ensure that wallet reset and user deletion always happen in the correct sequence - **Better Encapsulation**: Protected methods prevent accidental misuse and make the API surface clearer - **Consistent Behavior**: All wallet deletion flows now use the same underlying implementation ## **Changelog** CHANGELOG entry: Refactored wallet deletion logic by consolidating useDeleteWallet hook into Authentication service ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: Wallet Deletion Scenario: Delete wallet from seedless relogin prompt Given the user encounters a seedless controller error And the error prompt is displayed When the user taps the primary button to delete wallet Then the wallet state should be reset And user data should be deleted And the user should be navigated to onboarding And no errors should occur during the deletion process Scenario: Delete wallet from settings Given the user is viewing the Security Settings screen And the user navigates to the Delete Wallet modal When the user confirms wallet deletion Then the wallet state should be reset And user data should be deleted And cookies should be cleared And the user should be navigated to onboarding And analytics should track the deletion event Scenario: Wallet deletion error handling Given the user attempts to delete the wallet When an error occurs during wallet reset Then the error should be logged And the loading state should be reset And the user should see appropriate error feedback And the app should remain in a stable state ``` ## **Screenshots/Recordings** ### **Before** ### **After** https://github.com/user-attachments/assets/7dcb9cc3-cb52-4c50-8f1c-009fa402d1de ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Centralizes wallet deletion into `Authentication.deleteWallet()` and refactors consumers/tests to use it, removing the old `useDeleteWallet` hook. > > - **Core (Authentication)**: > - Add `Authentication.deleteWallet()` as the public API; implements sequential `resetWalletState` → `deleteUser`. > - `resetWalletState` (protected): clears vault backups, disables/re-enables `EngineClass.disableAutomaticVaultBackup`, creates temp wallet, clears seedless state, resets rewards via `Engine.controllerMessenger`, clears provider token, locks app (no navigation). > - `deleteUser` (protected): dispatches `setExistingUser(false)` and triggers `MetaMetrics.createDataDeletionTask`. > - Also removes `OPTIN_META_METRICS_UI_SEEN` and dispatches `setCompletedOnboarding(false)`. > - **UI**: > - `DeleteWalletModal`: replace hook flow with `Authentication.deleteWallet()`, keep sign-out, clear cookies, dispatch `clearHistory`, track event, then navigate to onboarding. > - `usePromptSeedlessRelogin`: invoke `Authentication.deleteWallet()` and simplify flow; maintain loading/error state and `clearHistory` + sign-out + navigation. > - **Hooks**: > - Remove `app/components/hooks/DeleteWallet/*` (hook and tests). > - **Tests**: > - Add comprehensive tests for `deleteWallet`, `resetWalletState`, and `deleteUser` in `Authentication.test.ts`. > - Update component/hook tests to assert calls to `Authentication.deleteWallet()` and revised action sequences; remove assertions tied to deleted hook/storage calls. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b71e98da626ab2fd6ba1b3de431999442d49fd2d. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../UI/DeleteWalletModal/index.test.tsx | 35 +- app/components/UI/DeleteWalletModal/index.tsx | 11 +- app/components/hooks/DeleteWallet/index.ts | 1 - .../DeleteWallet/useDeleteWallet.test.tsx | 199 ---------- .../hooks/DeleteWallet/useDeleteWallet.ts | 62 --- .../usePromptSeedlessRelogin.test.tsx | 72 ++-- .../SeedlessHooks/usePromptSeedlessRelogin.ts | 13 +- .../Authentication/Authentication.test.ts | 357 +++++++++++++++++- app/core/Authentication/Authentication.ts | 76 ++++ 9 files changed, 468 insertions(+), 358 deletions(-) delete mode 100644 app/components/hooks/DeleteWallet/index.ts delete mode 100644 app/components/hooks/DeleteWallet/useDeleteWallet.test.tsx delete mode 100644 app/components/hooks/DeleteWallet/useDeleteWallet.ts diff --git a/app/components/UI/DeleteWalletModal/index.test.tsx b/app/components/UI/DeleteWalletModal/index.test.tsx index f489cb8abe3..b337b70082e 100644 --- a/app/components/UI/DeleteWalletModal/index.test.tsx +++ b/app/components/UI/DeleteWalletModal/index.test.tsx @@ -9,11 +9,9 @@ import { createStackNavigator } from '@react-navigation/stack'; import { RootState } from '../../../reducers'; import { strings } from '../../../../locales/i18n'; import { ForgotPasswordModalSelectorsIDs } from '../../../../e2e/selectors/Common/ForgotPasswordModal.selectors'; -import { SET_COMPLETED_ONBOARDING } from '../../../actions/onboarding'; import { InteractionManager } from 'react-native'; -import StorageWrapper from '../../../store/storage-wrapper'; -import { OPTIN_META_METRICS_UI_SEEN } from '../../../constants/storage'; import { clearHistory } from '../../../actions/browser'; +import { Authentication } from '../../../core/Authentication/Authentication'; const mockInitialState = { engine: { backgroundState }, @@ -22,12 +20,6 @@ const mockInitialState = { }, }; -jest.mock('../../../store/storage-wrapper', () => ({ - 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/hooks/DeleteWallet/index.ts b/app/components/hooks/DeleteWallet/index.ts deleted file mode 100644 index c333876015d..00000000000 --- a/app/components/hooks/DeleteWallet/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as useDeleteWallet } from './useDeleteWallet'; diff --git a/app/components/hooks/DeleteWallet/useDeleteWallet.test.tsx b/app/components/hooks/DeleteWallet/useDeleteWallet.test.tsx deleted file mode 100644 index 925e6a8f5e7..00000000000 --- a/app/components/hooks/DeleteWallet/useDeleteWallet.test.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import { renderHookWithProvider } from '../../../util/test/renderWithProvider'; -import useDeleteWallet from './useDeleteWallet'; -import AUTHENTICATION_TYPE from '../../../constants/userProperties'; -import Engine from '../../../core/Engine'; -import { Engine as EngineClass } from '../../../core/Engine/Engine'; -import Logger from '../../../util/Logger'; -import { Authentication } from '../../../core'; -import { clearAllVaultBackups } from '../../../core/BackupVault'; -import { resetProviderToken as depositResetProviderToken } from '../../UI/Ramp/Deposit/utils/ProviderTokenVault'; - -jest.mock('../../../core/Engine', () => ({ - 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();