Skip to content

Commit af57390

Browse files
authored
fix: New Persistence Improvements based on Abuse testing cp-7.59.0 (#21990)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> When Redux state migrations fail, redux-persist resets the state to defaults and updates the version number to the latest, preventing migrations from re-running. This leaves users stuck on the onboarding screen unable to access their wallets, even though their vault backup exists in secure storage. ## Implementation ### 1. Vault Recovery Detection on Onboarding Screen Added automatic detection when users land on the onboarding screen with an existing vault backup: - Detects migration failure scenario: `!existingUser` (Redux reset) + vault backup exists - Skips detection if user explicitly deleted their wallet (`route.params.delete`) - Automatically navigates to vault recovery screen - Users can restore their wallet using their password - Prevents data loss from failed migrations **Files Changed:** - `app/components/Views/Onboarding/index.js` - Added vault recovery detection - `app/components/Views/RestoreWallet/WalletRestored.tsx` - Sets `existingUser` flag after restore ### 2. Fixed Vault Recovery Persistence Bug Fixed critical bug where vault recovery didn't persist controller state changes: - Added `setupEnginePersistence()` call in `initializeVaultFromBackup()` path - Ensures controller state changes are saved to individual files after vault recovery - Previously caused infinite vault recovery loops after successful restore - Consolidated persistence setup in `initializeControllers()` for consistency **Files Changed:** - `app/core/EngineService/EngineService.ts` - Fixed persistence setup in vault recovery path ### 3. Fixed Race Condition in Wallet Deletion Prevents temporary wallets (created during reset) from being backed up: - Added `disableAutomaticVaultBackup` flag to temporarily disable vault backups during wallet reset - Clears all vault backups before creating temporary wallet - Re-enables automatic backups in `finally` block for robustness - Applies to both manual wallet deletion and OAuth error recovery - Prevents false vault recovery prompts after intentional wallet resets **Files Changed:** - `app/core/Engine/Engine.ts` - Added circuit breaker flag - `app/components/hooks/DeleteWallet/useDeleteWallet.ts` - Manual deletion fix - `app/core/Authentication/Authentication.ts` - Uncommented `setExistingUser(true)` for new wallets - `app/core/BackupVault/backupVault.ts` - Vault backup utilities ### 4. Fixed Migration Inflation/Deflation Logic Corrected conditions for the new file-based persistence system (migrations 104-105): - **Inflation**: `> 106` - Only migrations 106+ need to inflate from individual controller files - **Deflation**: `>= 106` - Migration 106 is the first to deflate into individual controller files - Only future migrations require inflation, that will follow this PR - Ensures smooth transition to new persistence system without breaking app on restart **Files Changed:** - `app/store/migrations/index.ts` - Fixed inflation/deflation conditions and removed debug logs - `app/store/migrations/index.test.ts` - Updated tests to reflect correct logic ### 5. Code Cleanup Removed all debugging logs added during investigation: - `app/components/Views/Onboarding/index.js` - Removed migration detection logs - `app/core/EngineService/EngineService.ts` - Removed persistence setup logs - `app/store/migrations/index.ts` - Removed KeyringController debug logs - `app/core/BackupVault/backupVault.ts` - Simplified error handling - `app/core/Engine/Engine.ts` - Removed vault backup logs ### 6. Test Compliance Fixed unit test naming violations to comply with project guidelines: - Updated 19 tests in `EngineService.test.ts` to remove "should" from test names - Ensured all tests follow action-oriented naming convention ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: ## **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] ``` **Feature: Vault Recovery After Migration Failure** Scenario: user restores wallet after migration failure Given user has MetaMask installed with wallet data And a state migration fails during app startup And redux state is reset to defaults And vault backup exists in secure storage When user opens the app Then user sees the vault recovery screen automatically When user enters their wallet password Then user successfully restores their wallet And user can access their accounts and assets **Feature: Manual Wallet Deletion** Scenario: user deletes wallet without false vault recovery Given user has MetaMask installed with an active wallet And user is on the Settings screen When user navigates to "Security & Privacy" And user taps "Delete Wallet" And user confirms the deletion And user restarts the app Then user sees the onboarding screen And user does NOT see the vault recovery screen **Feature: OAuth Login Error Recovery** Feature: Vault Recovery After Migration Failure Scenario: user restores wallet after migration failure Given user has MetaMask installed with wallet data And a state migration fails during app startup And redux state is reset to defaults And vault backup exists in secure storage When user opens the app Then user sees the vault recovery screen automatically When user enters their wallet password Then user successfully restores their wallet And user can access their accounts and assets **Feature: Manual Wallet Deletion** Scenario: user deletes wallet without false vault recovery Given user has MetaMask installed with an active wallet And user is on the Settings screen When user navigates to "Security & Privacy" And user taps "Delete Wallet" And user confirms the deletion And user restarts the app Then user sees the onboarding screen And user does NOT see the vault recovery screen **Feature: OAuth Login Error Recovery** Scenario: user experiences OAuth backup failure without false vault recovery Given user is creating a new wallet with OAuth (Google/Apple login) And local wallet creation succeeds And cloud backup fails during OAuth process When the error handler resets the wallet state And user restarts the app Then user sees the onboarding screen And user does NOT see the vault recovery screen And user can retry OAuth login or import with seed phrase **Feature: Migration System Upgrade Path** Scenario: user upgrades from version 103 to version 104+ Given user has MetaMask version with migrations up to 103 And controller data is stored in redux-persist And user has an active wallet When user upgrades to version 104 or higher Then migrations 104+ run successfully And controller data is deflated to individual files And user's wallet remains accessible And app functions normally after restart Scenario: user upgrades from version 104 to version 105+ Given user has MetaMask version 104 And controller data is in individual files And user has an active wallet When user upgrades to version 105 or higher Then controller data is inflated for migrations And new migrations run successfully And controller data is deflated back to individual files And user's wallet remains accessible And app functions normally after restart**: user experiences OAuth backup failure without false vault recovery Given user is creating a new wallet with OAuth (Google/Apple login) And local wallet creation succeeds And cloud backup fails during OAuth process When the error handler resets the wallet state And user restarts the app Then user sees the onboarding screen And user does NOT see the vault recovery screen And user can retry OAuth login or import with seed phrase **Feature: Migration System Upgrade Path** Scenario: user upgrades from version 103 to version 104+ Given user has MetaMask version with migrations up to 103 And controller data is stored in redux-persist And user has an active wallet When user upgrades to version 104 or higher Then migrations 104+ run successfully And controller data is deflated to individual files And user's wallet remains accessible And app functions normally after restart Scenario: user upgrades from version 104 to version 105+ Given user has MetaMask version 104 And controller data is in individual files And user has an active wallet When user upgrades to version 105 or higher Then controller data is inflated for migrations And new migrations run successfully And controller data is deflated back to individual files And user's wallet remains accessible And app functions normally after restart ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Detects migration failure and routes to vault recovery, ensures controller persistence after recovery, prevents backing up temporary wallets during resets, and updates login/recovery flows with tests. > > - **Onboarding/Recovery**: > - Detects migration failure on `Onboarding` by checking vault backup and `existingUser`; skips in E2E and explicit delete, then navigates to `Routes.VAULT_RECOVERY.RESTORE_WALLET`. > - `WalletRestored` now routes to `Routes.ONBOARDING.LOGIN` with `isVaultRecovery` instead of auto-auth; adds tests. > - `Login` sets `existingUser` via `setExistingUser(true)` after successful unlock when `isVaultRecovery` is true; adds tests. > - **Engine/Authentication**: > - Adds `Engine.disableAutomaticVaultBackup` and skips `backupVault` when true. > - Clears vault backups and temporarily disables auto-backup during wallet reset and OAuth error recovery; always re-enables. > - `EngineService` moves filesystem persistence setup to `initializeControllers` and ensures it runs after vault recovery; updates batching and tests. > - **Hooks/Tests**: > - `useDeleteWallet` clears backups first, disables auto-backup during reset, re-enables in `finally`, and resets controllers; adds robust tests. > - Renames and tightens numerous test titles/expectations; snapshot names updated. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ed1a8a7. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 038133d commit af57390

File tree

14 files changed

+816
-123
lines changed

14 files changed

+816
-123
lines changed

app/components/Views/Login/index.test.tsx

Lines changed: 136 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
TRUE,
2626
} from '../../../constants/storage';
2727
import { useMetrics } from '../../hooks/useMetrics';
28+
import { setExistingUser } from '../../../actions/user';
2829

2930
const mockNavigate = jest.fn();
3031
const mockReplace = jest.fn();
@@ -113,6 +114,13 @@ jest.mock('../../../actions/security', () => ({
113114
},
114115
}));
115116

117+
jest.mock('../../../actions/user', () => ({
118+
setExistingUser: jest.fn((value) => ({
119+
type: 'SET_EXISTING_USER',
120+
existingUser: value,
121+
})),
122+
}));
123+
116124
jest.mock('../../../store/storage-wrapper', () => ({
117125
getItem: jest.fn().mockResolvedValue(null),
118126
setItem: jest.fn(),
@@ -263,7 +271,7 @@ describe('Login', () => {
263271
expect(toJSON()).toMatchSnapshot();
264272
});
265273

266-
it('should call trace function for AuthenticateUser during non-OAuth login', async () => {
274+
it('calls trace function for AuthenticateUser during non-OAuth login', async () => {
267275
(StorageWrapper.getItem as jest.Mock).mockImplementation((key) => {
268276
if (key === OPTIN_META_METRICS_UI_SEEN) return true;
269277
return null;
@@ -296,11 +304,14 @@ describe('Login', () => {
296304
});
297305

298306
describe('Forgot Password', () => {
299-
it('show the forgot password modal', () => {
307+
it('shows forgot password modal when reset wallet pressed', () => {
308+
// Arrange
300309
const { getByTestId } = renderWithProvider(<Login />);
301-
expect(getByTestId(LoginViewSelectors.RESET_WALLET)).toBeTruthy();
310+
311+
// Act
302312
fireEvent.press(getByTestId(LoginViewSelectors.RESET_WALLET));
303313

314+
// Assert
304315
expect(mockNavigate).toHaveBeenCalledWith(Routes.MODAL.ROOT_MODAL_FLOW, {
305316
screen: Routes.MODAL.DELETE_WALLET,
306317
params: {
@@ -320,7 +331,7 @@ describe('Login', () => {
320331
});
321332
});
322333

323-
it('should navigate to opt-in metrics when UI not seen and metrics disabled', async () => {
334+
it('navigates to opt-in metrics when UI not seen and metrics disabled', async () => {
324335
// Arrange
325336
(StorageWrapper.getItem as jest.Mock).mockImplementation((key) => {
326337
if (key === OPTIN_META_METRICS_UI_SEEN) return Promise.resolve(null);
@@ -354,7 +365,6 @@ describe('Login', () => {
354365
fireEvent.press(loginButton);
355366
});
356367

357-
// Wait for async operations to complete
358368
await act(async () => {
359369
await new Promise((resolve) => setTimeout(resolve, 0));
360370
});
@@ -375,7 +385,7 @@ describe('Login', () => {
375385
});
376386
});
377387

378-
it('should navigate to home when UI not seen but metrics enabled', async () => {
388+
it('navigates to home when UI not seen but metrics enabled', async () => {
379389
// Arrange
380390
(StorageWrapper.getItem as jest.Mock).mockImplementation((key) => {
381391
if (key === OPTIN_META_METRICS_UI_SEEN) return Promise.resolve(null);
@@ -418,7 +428,7 @@ describe('Login', () => {
418428
expect(mockReplace).toHaveBeenCalledWith(Routes.ONBOARDING.HOME_NAV);
419429
});
420430

421-
it('should navigate to home when UI seen and metrics disabled', async () => {
431+
it('navigates to home when UI seen and metrics disabled', async () => {
422432
// Arrange
423433
(StorageWrapper.getItem as jest.Mock).mockImplementation((key) => {
424434
if (key === OPTIN_META_METRICS_UI_SEEN)
@@ -462,7 +472,7 @@ describe('Login', () => {
462472
expect(mockReplace).toHaveBeenCalledWith(Routes.ONBOARDING.HOME_NAV);
463473
});
464474

465-
it('should navigate to home when UI seen and metrics enabled', async () => {
475+
it('navigates to home when UI seen and metrics enabled', async () => {
466476
// Arrange
467477
(StorageWrapper.getItem as jest.Mock).mockImplementation((key) => {
468478
if (key === OPTIN_META_METRICS_UI_SEEN)
@@ -726,7 +736,103 @@ describe('Login', () => {
726736
fireEvent.changeText(passwordInput, 'testpassword123');
727737

728738
// Assert
729-
expect(passwordInput).toBeTruthy();
739+
expect(passwordInput).toBeOnTheScreen();
740+
});
741+
});
742+
743+
describe('Vault Recovery', () => {
744+
beforeEach(() => {
745+
jest.clearAllMocks();
746+
});
747+
748+
it('dispatches setExistingUser after successful login from vault recovery', async () => {
749+
// Arrange
750+
mockRoute.mockReturnValue({
751+
params: {
752+
locked: false,
753+
oauthLoginSuccess: false,
754+
isVaultRecovery: true,
755+
},
756+
});
757+
758+
(StorageWrapper.getItem as jest.Mock).mockImplementation((key) => {
759+
if (key === OPTIN_META_METRICS_UI_SEEN) return Promise.resolve('true');
760+
return Promise.resolve(null);
761+
});
762+
763+
(Authentication.userEntryAuth as jest.Mock).mockResolvedValueOnce(
764+
undefined,
765+
);
766+
(
767+
Authentication.componentAuthenticationType as jest.Mock
768+
).mockResolvedValueOnce({
769+
currentAuthType: 'password',
770+
});
771+
772+
const { getByTestId } = renderWithProvider(<Login />);
773+
const passwordInput = getByTestId(LoginViewSelectors.PASSWORD_INPUT);
774+
const loginButton = getByTestId(LoginViewSelectors.LOGIN_BUTTON_ID);
775+
776+
// Act
777+
await act(async () => {
778+
fireEvent.changeText(passwordInput, 'validPassword123');
779+
});
780+
781+
await act(async () => {
782+
fireEvent.press(loginButton);
783+
});
784+
785+
await act(async () => {
786+
await new Promise((resolve) => setTimeout(resolve, 0));
787+
});
788+
789+
// Assert
790+
expect(setExistingUser).toHaveBeenCalledWith(true);
791+
});
792+
793+
it('does not dispatch setExistingUser when not from vault recovery', async () => {
794+
// Arrange
795+
mockRoute.mockReturnValue({
796+
params: {
797+
locked: false,
798+
oauthLoginSuccess: false,
799+
isVaultRecovery: false,
800+
},
801+
});
802+
803+
(StorageWrapper.getItem as jest.Mock).mockImplementation((key) => {
804+
if (key === OPTIN_META_METRICS_UI_SEEN) return Promise.resolve('true');
805+
return Promise.resolve(null);
806+
});
807+
808+
(Authentication.userEntryAuth as jest.Mock).mockResolvedValueOnce(
809+
undefined,
810+
);
811+
(
812+
Authentication.componentAuthenticationType as jest.Mock
813+
).mockResolvedValueOnce({
814+
currentAuthType: 'password',
815+
});
816+
817+
const { getByTestId } = renderWithProvider(<Login />);
818+
const passwordInput = getByTestId(LoginViewSelectors.PASSWORD_INPUT);
819+
const loginButton = getByTestId(LoginViewSelectors.LOGIN_BUTTON_ID);
820+
821+
// Act
822+
await act(async () => {
823+
fireEvent.changeText(passwordInput, 'validPassword123');
824+
});
825+
826+
await act(async () => {
827+
fireEvent.press(loginButton);
828+
});
829+
830+
await act(async () => {
831+
await new Promise((resolve) => setTimeout(resolve, 0));
832+
});
833+
834+
// Assert
835+
expect(setExistingUser).not.toHaveBeenCalled();
730836
});
731837
});
732838

@@ -795,7 +901,7 @@ describe('Login', () => {
795901
});
796902

797903
// Should render biometric button when biometric is available
798-
expect(getByTestId(LoginViewSelectors.BIOMETRY_BUTTON)).toBeTruthy();
904+
expect(getByTestId(LoginViewSelectors.BIOMETRY_BUTTON)).toBeOnTheScreen();
799905
});
800906

801907
it('biometric button is not shown when device is locked', async () => {
@@ -860,7 +966,8 @@ describe('Login', () => {
860966
jest.clearAllMocks();
861967
});
862968

863-
it('should handle WRONG_PASSWORD_ERROR', async () => {
969+
it('displays invalid password error when decryption fails', async () => {
970+
// Arrange
864971
mockRoute.mockReturnValue({
865972
params: {
866973
locked: false,
@@ -880,15 +987,17 @@ describe('Login', () => {
880987
const { getByTestId } = renderWithProvider(<Login />);
881988
const passwordInput = getByTestId(LoginViewSelectors.PASSWORD_INPUT);
882989

990+
// Act
883991
await act(async () => {
884992
fireEvent.changeText(passwordInput, 'valid-password123');
885993
});
886994
await act(async () => {
887995
fireEvent(passwordInput, 'submitEditing');
888996
});
889997

998+
// Assert
890999
const errorElement = getByTestId(LoginViewSelectors.PASSWORD_ERROR);
891-
expect(errorElement).toBeTruthy();
1000+
expect(errorElement).toBeOnTheScreen();
8921001
expect(errorElement.props.children).toEqual(
8931002
strings('login.invalid_password'),
8941003
);
@@ -910,7 +1019,7 @@ describe('Login', () => {
9101019
expect(rehydrationCall).toBeDefined();
9111020
});
9121021

913-
it('should handle WRONG_PASSWORD_ERROR_ANDROID', async () => {
1022+
it('displays invalid password error for Android BAD_DECRYPT error', async () => {
9141023
mockRoute.mockReturnValue({
9151024
params: {
9161025
locked: false,
@@ -940,7 +1049,7 @@ describe('Login', () => {
9401049
});
9411050

9421051
const errorElement = getByTestId(LoginViewSelectors.PASSWORD_ERROR);
943-
expect(errorElement).toBeTruthy();
1052+
expect(errorElement).toBeOnTheScreen();
9441053
expect(errorElement.props.children).toEqual(
9451054
strings('login.invalid_password'),
9461055
);
@@ -962,7 +1071,7 @@ describe('Login', () => {
9621071
expect(rehydrationCall).toBeDefined();
9631072
});
9641073

965-
it('should handle WRONG_PASSWORD_ERROR_ANDROID_2', async () => {
1074+
it('displays invalid password error for Android DoCipher error', async () => {
9661075
mockRoute.mockReturnValue({
9671076
params: {
9681077
locked: false,
@@ -990,7 +1099,7 @@ describe('Login', () => {
9901099
});
9911100

9921101
const errorElement = getByTestId(LoginViewSelectors.PASSWORD_ERROR);
993-
expect(errorElement).toBeTruthy();
1102+
expect(errorElement).toBeOnTheScreen();
9941103
expect(errorElement.props.children).toEqual(
9951104
strings('login.invalid_password'),
9961105
);
@@ -1012,7 +1121,7 @@ describe('Login', () => {
10121121
expect(rehydrationCall).toBeDefined();
10131122
});
10141123

1015-
it('should handle PASSWORD_REQUIREMENTS_NOT_MET error', async () => {
1124+
it('displays invalid password error when password requirements not met', async () => {
10161125
mockRoute.mockReturnValue({
10171126
params: {
10181127
locked: false,
@@ -1040,7 +1149,7 @@ describe('Login', () => {
10401149
});
10411150

10421151
const errorElement = getByTestId(LoginViewSelectors.PASSWORD_ERROR);
1043-
expect(errorElement).toBeTruthy();
1152+
expect(errorElement).toBeOnTheScreen();
10441153
expect(errorElement.props.children).toEqual(
10451154
strings('login.invalid_password'),
10461155
);
@@ -1054,7 +1163,7 @@ describe('Login', () => {
10541163
expect(rehydrationCall).toBeUndefined();
10551164
});
10561165

1057-
it('should handle generic error (else case)', async () => {
1166+
it('displays generic error message for unexpected errors', async () => {
10581167
(Authentication.userEntryAuth as jest.Mock).mockRejectedValue(
10591168
new Error('Some unexpected error'),
10601169
);
@@ -1070,13 +1179,13 @@ describe('Login', () => {
10701179
});
10711180

10721181
const errorElement = getByTestId(LoginViewSelectors.PASSWORD_ERROR);
1073-
expect(errorElement).toBeTruthy();
1182+
expect(errorElement).toBeOnTheScreen();
10741183
expect(errorElement.props.children).toEqual(
10751184
'Error: Some unexpected error',
10761185
);
10771186
});
10781187

1079-
it('should handle OnboardingPasswordLoginError trace during onboarding flow', async () => {
1188+
it('traces OnboardingPasswordLoginError during onboarding flow', async () => {
10801189
mockRoute.mockReturnValue({
10811190
params: {
10821191
locked: false,
@@ -1100,7 +1209,7 @@ describe('Login', () => {
11001209
});
11011210

11021211
const errorElement = getByTestId(LoginViewSelectors.PASSWORD_ERROR);
1103-
expect(errorElement).toBeTruthy();
1212+
expect(errorElement).toBeOnTheScreen();
11041213
expect(errorElement.props.children).toEqual(
11051214
'Error: Some unexpected error',
11061215
);
@@ -1131,7 +1240,7 @@ describe('Login', () => {
11311240
jest.clearAllMocks();
11321241
});
11331242

1134-
it('should handle PASSCODE_NOT_SET_ERROR with alert', async () => {
1243+
it('displays alert when passcode not set', async () => {
11351244
const mockAlert = jest
11361245
.spyOn(Alert, 'alert')
11371246
.mockImplementation(() => undefined);
@@ -1220,7 +1329,8 @@ describe('Login', () => {
12201329
});
12211330
});
12221331

1223-
it('handle biometric authentication failure', async () => {
1332+
it('does not navigate when biometric authentication fails', async () => {
1333+
// Arrange
12241334
(passcodeType as jest.Mock).mockReturnValueOnce('device_passcode');
12251335
(Authentication.getType as jest.Mock).mockResolvedValueOnce({
12261336
currentAuthType: AUTHENTICATION_TYPE.PASSCODE,
@@ -1239,13 +1349,14 @@ describe('Login', () => {
12391349

12401350
const biometryButton = getByTestId(LoginViewSelectors.BIOMETRY_BUTTON);
12411351

1352+
// Act
12421353
await act(async () => {
12431354
fireEvent.press(biometryButton);
12441355
});
12451356

1357+
// Assert
12461358
expect(Authentication.appTriggeredAuth).toHaveBeenCalled();
12471359
expect(mockReplace).not.toHaveBeenCalled();
1248-
expect(biometryButton).toBeTruthy();
12491360
});
12501361
});
12511362

0 commit comments

Comments
 (0)