Repository: MetaMask/metamask-mobile
Pull request #14370: feat: add new `completedOnboarding` onboarding state property
Author: login: mathieuartu
Labels: name: No QA Needed
description: Apply this label when your PR does not need any QA effort.
name: team-identity-deprecated
description: DEPRECATED: team no longer exists
name: release-7.45.0
description: Issue or pull request that will be included in release 7.45.0
Created: 2025-04-02T10:26:36Z | Merged: 2025-04-07T20:35:49Z

## **Description**

This PR adds a new `onboarding` state property, `completedOnboarding`.
This is done as to replicate the current behavior on extension, as some features rely on this property.

A migration has been added in order to populate this value for existing users.

## **Related issues**

Fixes: https://consensyssoftware.atlassian.net/browse/IDENTITY-74

## **Manual testing steps**

1. Onboard by importing an SRP
2. Verify that a call to `https://authentication.api.cx.metamask.io/api/v2/srp/login` happened when onboarding is finished (by clicking "Done" on the last onboarding screen)
3. Delete wallet
4. Onboard by creating a new SRP
5. Verify that a call to `https://authentication.api.cx.metamask.io/api/v2/srp/login` happened when onboarding is finished (by clicking "Done" on the last onboarding screen)

## **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.



Diff:
diff --git a/app/actions/onboarding/index.test.ts b/app/actions/onboarding/index.test.ts
new file mode 100644
index 00000000000..635a23d45f1
--- /dev/null
+++ b/app/actions/onboarding/index.test.ts
@@ -0,0 +1,41 @@
+import {
+  saveOnboardingEvent,
+  clearOnboardingEvents,
+  setCompletedOnboarding,
+  SAVE_EVENT,
+  CLEAR_EVENTS,
+  SET_COMPLETED_ONBOARDING,
+} from '.';
+import { ITrackingEvent } from '../../core/Analytics/MetaMetrics.types';
+
+describe('Onboarding actions', () => {
+  describe('saveOnboardingEvent', () => {
+    it('creates an action to save onboarding events', () => {
+      const mockEvent = {
+        name: 'test_event',
+      } as ITrackingEvent;
+      expect(saveOnboardingEvent([mockEvent])).toEqual({
+        type: SAVE_EVENT,
+        event: [mockEvent],
+      });
+    });
+  });
+
+  describe('clearOnboardingEvents', () => {
+    it('creates an action to clear onboarding events', () => {
+      expect(clearOnboardingEvents()).toEqual({
+        type: CLEAR_EVENTS,
+      });
+    });
+  });
+
+  describe('setCompletedOnboarding', () => {
+    it('creates an action to set completedOnboarding', () => {
+      const completedOnboarding = true;
+      expect(setCompletedOnboarding(completedOnboarding)).toEqual({
+        type: SET_COMPLETED_ONBOARDING,
+        completedOnboarding,
+      });
+    });
+  });
+});
diff --git a/app/actions/onboarding/index.ts b/app/actions/onboarding/index.ts
index 8b1e2c3e6e0..ea149b87994 100644
--- a/app/actions/onboarding/index.ts
+++ b/app/actions/onboarding/index.ts
@@ -2,6 +2,7 @@ import { ITrackingEvent } from '../../core/Analytics/MetaMetrics.types';
 
 export const SAVE_EVENT = 'SAVE_EVENT';
 export const CLEAR_EVENTS = 'CLEAR_EVENTS';
+export const SET_COMPLETED_ONBOARDING = 'SET_COMPLETED_ONBOARDING';
 
 interface SaveEventAction {
   type: typeof SAVE_EVENT;
@@ -12,7 +13,15 @@ interface ClearEventsAction {
   type: typeof CLEAR_EVENTS;
 }
 
-export type OnboardingActionTypes = SaveEventAction | ClearEventsAction;
+interface SetCompletedOnboardingAction {
+  type: typeof SET_COMPLETED_ONBOARDING;
+  completedOnboarding: boolean;
+}
+
+export type OnboardingActionTypes =
+  | SaveEventAction
+  | ClearEventsAction
+  | SetCompletedOnboardingAction;
 
 export function saveOnboardingEvent(
   eventArgs: [ITrackingEvent],
@@ -28,3 +37,12 @@ export function clearOnboardingEvents(): ClearEventsAction {
     type: CLEAR_EVENTS,
   };
 }
+
+export function setCompletedOnboarding(
+  completedOnboarding: boolean,
+): SetCompletedOnboardingAction {
+  return {
+    type: SET_COMPLETED_ONBOARDING,
+    completedOnboarding,
+  };
+}
diff --git a/app/components/UI/DeleteWalletModal/index.test.tsx b/app/components/UI/DeleteWalletModal/index.test.tsx
index 145e4da6fd5..58b69189ff1 100644
--- a/app/components/UI/DeleteWalletModal/index.test.tsx
+++ b/app/components/UI/DeleteWalletModal/index.test.tsx
@@ -4,6 +4,7 @@ import renderWithProvider from '../../../util/test/renderWithProvider';
 import { backgroundState } from '../../../util/test/initial-root-state';
 import { DeleteWalletModalSelectorsIDs } from '../../../../e2e/selectors/Settings/SecurityAndPrivacy/DeleteWalletModal.selectors';
 import { fireEvent, waitFor } from '@testing-library/react-native';
+import { SET_COMPLETED_ONBOARDING } from '../../../actions/onboarding';
 
 const mockInitialState = {
   engine: { backgroundState },
@@ -12,9 +13,11 @@ const mockInitialState = {
   },
 };
 
+const mockUseDispatch = jest.fn();
+
 jest.mock('react-redux', () => ({
   ...jest.requireActual('react-redux'),
-  useDispatch: () => jest.fn(),
+  useDispatch: () => mockUseDispatch,
   useSelector: jest.fn(),
 }));
 const mockNavigate = jest.fn();
@@ -74,4 +77,24 @@ describe('DeleteWalletModal', () => {
       expect(mockSignOut).toHaveBeenCalled();
     });
   });
+
+  it('sets completedOnboarding to false when deleting the wallet', async () => {
+    const { getByTestId } = renderWithProvider(<DeleteWalletModal />, {
+      state: mockInitialState,
+    });
+
+    fireEvent.press(getByTestId(DeleteWalletModalSelectorsIDs.CONTINUE_BUTTON));
+    fireEvent.press(
+      getByTestId(DeleteWalletModalSelectorsIDs.DELETE_PERMANENTLY_BUTTON),
+    );
+
+    waitFor(() => {
+      expect(mockUseDispatch).toHaveBeenCalledWith(
+        expect.objectContaining({
+          type: SET_COMPLETED_ONBOARDING,
+          completedOnboarding: false,
+        }),
+      );
+    });
+  });
 });
diff --git a/app/components/UI/DeleteWalletModal/index.tsx b/app/components/UI/DeleteWalletModal/index.tsx
index 77b6b144ecf..fe4539b7040 100644
--- a/app/components/UI/DeleteWalletModal/index.tsx
+++ b/app/components/UI/DeleteWalletModal/index.tsx
@@ -30,6 +30,7 @@ import { clearHistory } from '../../../actions/browser';
 import CookieManager from '@react-native-cookies/cookies';
 import { RootState } from '../../../reducers';
 import { useSignOut } from '../../../util/identity/hooks/useAuthentication';
+import { setCompletedOnboarding } from '../../../actions/onboarding';
 
 const DELETE_KEYWORD = 'delete';
 
@@ -100,6 +101,7 @@ const DeleteWalletModal = () => {
       clearHistory(isEnabled(), isDataCollectionForMarketingEnabled),
     );
     signOut();
+    await dispatch(setCompletedOnboarding(false));
     await CookieManager.clearAll(true);
     triggerClose();
     await resetWalletState();
diff --git a/app/components/Views/OnboardingSuccess/__snapshots__/index.test.js.snap b/app/components/Views/OnboardingSuccess/__snapshots__/index.test.js.snap
index 35c537046fd..e2a9c7bb8e6 100644
--- a/app/components/Views/OnboardingSuccess/__snapshots__/index.test.js.snap
+++ b/app/components/Views/OnboardingSuccess/__snapshots__/index.test.js.snap
@@ -195,6 +195,7 @@ Change these at any time.
         accessibilityRole="button"
         accessible={true}
         activeOpacity={1}
+        onPress={[Function]}
         onPressIn={[Function]}
         onPressOut={[Function]}
         style={
diff --git a/app/components/Views/OnboardingSuccess/index.test.js b/app/components/Views/OnboardingSuccess/index.test.js
index 010edac55dd..1b0634eb4d4 100644
--- a/app/components/Views/OnboardingSuccess/index.test.js
+++ b/app/components/Views/OnboardingSuccess/index.test.js
@@ -5,8 +5,11 @@ import React from 'react';
 import OnboardingSuccess from './';
 import renderWithProvider from '../../../util/test/renderWithProvider';
 import { useNavigation } from '@react-navigation/native';
-import { useSelector } from 'react-redux';
+import { useSelector, useDispatch } from 'react-redux';
 import { selectProviderConfig } from '../../../selectors/networkController';
+import { OnboardingSuccessSelectorIDs } from '../../../../e2e/selectors/Onboarding/OnboardingSuccess.selectors';
+import { SET_COMPLETED_ONBOARDING } from '../../../actions/onboarding';
+import { waitFor } from '@testing-library/react-native';
 
 jest.mock('@react-navigation/native', () => {
   const actualReactNavigation = jest.requireActual('@react-navigation/native');
@@ -27,6 +30,7 @@ jest.mock('@react-navigation/native', () => {
 jest.mock('react-redux', () => ({
   ...jest.requireActual('react-redux'),
   useSelector: jest.fn(),
+  useDispatch: jest.fn(),
 }));
 
 const mockProviderConfig = {
@@ -44,4 +48,25 @@ describe('OnboardingSuccess', () => {
     );
     expect(toJSON()).toMatchSnapshot();
   });
+
+  it('sets completedOnboarding to true when onDone is called', () => {
+    useSelector.mockImplementation((selector) => {
+      if (selector === selectProviderConfig) return mockProviderConfig;
+    });
+    const mockDispatch = jest.fn();
+    useDispatch.mockImplementation(() => mockDispatch);
+
+    const { getByTestId } = renderWithProvider(
+      <OnboardingSuccess navigation={useNavigation()} onDone={jest.fn()} />,
+    );
+    const button = getByTestId(OnboardingSuccessSelectorIDs.DONE_BUTTON);
+    button.props.onPress();
+
+    waitFor(() => {
+      expect(mockDispatch).toHaveBeenCalledWith({
+        type: SET_COMPLETED_ONBOARDING,
+        completedOnboarding: true,
+      });
+    });
+  });
 });
diff --git a/app/components/Views/OnboardingSuccess/index.tsx b/app/components/Views/OnboardingSuccess/index.tsx
index a4b384ffdd5..816a54ad53a 100644
--- a/app/components/Views/OnboardingSuccess/index.tsx
+++ b/app/components/Views/OnboardingSuccess/index.tsx
@@ -1,4 +1,4 @@
-import React, { useLayoutEffect, useState } from 'react';
+import React, { useCallback, useLayoutEffect, useState } from 'react';
 import {
   ScrollView,
   View,
@@ -23,6 +23,7 @@ import HintModal from '../../UI/HintModal';
 import { useTheme } from '../../../util/theme';
 import StorageWrapper from '../../../store/storage-wrapper';
 import { SEED_PHRASE_HINTS } from '../../../constants/storage';
+import { useDispatch } from 'react-redux';
 import Icon, {
   IconName,
   IconColor,
@@ -32,6 +33,7 @@ import AppConstants from '../../../core/AppConstants';
 import Emoji from 'react-native-emoji';
 import { OnboardingSuccessSelectorIDs } from '../../../../e2e/selectors/Onboarding/OnboardingSuccess.selectors';
 import styles from './index.styles';
+import { setCompletedOnboarding } from '../../../actions/onboarding';
 
 interface OnboardingSuccessProps {
   onDone: () => void;
@@ -46,6 +48,7 @@ const OnboardingSuccess = ({
 }: OnboardingSuccessProps) => {
   const navigation = useNavigation();
   const { colors } = useTheme();
+  const dispatch = useDispatch();
   const [showHint, setShowHint] = useState(false);
   const [hintText, setHintText] = useState('');
 
@@ -63,6 +66,14 @@ const OnboardingSuccess = ({
     Linking.openURL(AppConstants.URLS.WHAT_IS_SRP);
   };
 
+  const handleOnDone = useCallback(() => {
+    const onOnboardingSuccess = async () => {
+      await dispatch(setCompletedOnboarding(true));
+    };
+    onOnboardingSuccess();
+    onDone();
+  }, [onDone, dispatch]);
+
   const saveHint = async () => {
     if (!hintText) return;
     setShowHint(false);
@@ -208,7 +219,7 @@ const OnboardingSuccess = ({
           testID={OnboardingSuccessSelectorIDs.DONE_BUTTON}
           label={strings('onboarding_success.done')}
           variant={ButtonVariants.Primary}
-          onPress={onDone}
+          onPress={handleOnDone}
           size={ButtonSize.Lg}
           width={ButtonWidthTypes.Full}
         />
diff --git a/app/reducers/index.ts b/app/reducers/index.ts
index 472f87c43c9..64ee90520aa 100644
--- a/app/reducers/index.ts
+++ b/app/reducers/index.ts
@@ -9,7 +9,7 @@ import transactionReducer from './transaction';
 import legalNoticesReducer from './legalNotices';
 import userReducer, { UserState } from './user';
 import wizardReducer from './wizard';
-import onboardingReducer from './onboarding';
+import onboardingReducer, { OnboardingState } from './onboarding';
 import fiatOrders from './fiatOrders';
 import swapsReducer from './swaps';
 import signatureRequestReducer from './signatureRequest';
@@ -82,9 +82,7 @@ export interface RootState {
   // TODO: Replace "any" with type
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   wizard: any;
-  // TODO: Replace "any" with type
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  onboarding: any;
+  onboarding: OnboardingState;
   // TODO: Replace "any" with type
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   notification: any;
diff --git a/app/reducers/onboarding/index.js b/app/reducers/onboarding/index.js
deleted file mode 100644
index a9f4092211f..00000000000
--- a/app/reducers/onboarding/index.js
+++ /dev/null
@@ -1,26 +0,0 @@
-const initialState = {
-  events: [],
-};
-
-/**
- * Reducer to keep track of user oboarding actions to send it to analytics if the user
- * decides to optin after finishing onboarding flow
- */
-const onboardingReducer = (state = initialState, action) => {
-  switch (action.type) {
-    case 'SAVE_EVENT':
-      return {
-        ...state,
-        events: [...state.events, action.event],
-      };
-    case 'CLEAR_EVENTS':
-      return {
-        ...state,
-        events: [],
-      };
-    default:
-      return state;
-  }
-};
-
-export default onboardingReducer;
diff --git a/app/reducers/onboarding/index.test.ts b/app/reducers/onboarding/index.test.ts
new file mode 100644
index 00000000000..9cdc58e8abf
--- /dev/null
+++ b/app/reducers/onboarding/index.test.ts
@@ -0,0 +1,45 @@
+import onboardingReducer from '.';
+import {
+  CLEAR_EVENTS,
+  SAVE_EVENT,
+  SET_COMPLETED_ONBOARDING,
+} from '../../actions/onboarding';
+import { ITrackingEvent } from '../../core/Analytics/MetaMetrics.types';
+
+describe('onboardingReducer', () => {
+  const initialState = {
+    events: [],
+    completedOnboarding: false,
+  };
+
+  it('returns the initial state when no action is provided', () => {
+    const state = onboardingReducer(undefined, { type: null } as never);
+    expect(state).toEqual(initialState);
+  });
+
+  it('handles the SAVE_EVENT action', () => {
+    const mockEvent = [{ name: 'test_event' }] as [ITrackingEvent];
+    const action = { type: SAVE_EVENT, event: mockEvent } as const;
+    const state = onboardingReducer(initialState, action);
+    expect(state.events).toEqual([mockEvent]);
+  });
+
+  it('handles the CLEAR_EVENTS action', () => {
+    const stateWithEvents = {
+      ...initialState,
+      events: [[{ name: 'test_event' }] as [ITrackingEvent]],
+    };
+    const action = { type: CLEAR_EVENTS } as const;
+    const state = onboardingReducer(stateWithEvents, action);
+    expect(state.events).toEqual([]);
+  });
+
+  it('handle the SET_COMPLETED_ONBOARDING action', () => {
+    const action = {
+      type: SET_COMPLETED_ONBOARDING,
+      completedOnboarding: true,
+    } as const;
+    const state = onboardingReducer(initialState, action);
+    expect(state.completedOnboarding).toBe(true);
+  });
+});
diff --git a/app/reducers/onboarding/index.ts b/app/reducers/onboarding/index.ts
new file mode 100644
index 00000000000..b6fff5c66ec
--- /dev/null
+++ b/app/reducers/onboarding/index.ts
@@ -0,0 +1,51 @@
+/* eslint-disable @typescript-eslint/default-param-last */
+
+import {
+  CLEAR_EVENTS,
+  OnboardingActionTypes,
+  SAVE_EVENT,
+  SET_COMPLETED_ONBOARDING,
+} from '../../actions/onboarding';
+import { ITrackingEvent } from '../../core/Analytics/MetaMetrics.types';
+
+export interface OnboardingState {
+  events: [ITrackingEvent][];
+  completedOnboarding: boolean;
+}
+
+export const initialOnboardingState: OnboardingState = {
+  events: [],
+  completedOnboarding: false,
+};
+
+/**
+ * Reducer to keep track of user oboarding actions:
+ * - send it to analytics if the user decides to optin after finishing onboarding flow
+ * - update the state of the onboarding flow
+ */
+const onboardingReducer = (
+  state = initialOnboardingState,
+  action: OnboardingActionTypes,
+) => {
+  switch (action.type) {
+    case SAVE_EVENT:
+      return {
+        ...state,
+        events: [...state.events, action.event],
+      };
+    case CLEAR_EVENTS:
+      return {
+        ...state,
+        events: [],
+      };
+    case SET_COMPLETED_ONBOARDING:
+      return {
+        ...state,
+        completedOnboarding: action.completedOnboarding,
+      };
+    default:
+      return state;
+  }
+};
+
+export default onboardingReducer;
diff --git a/app/selectors/onboarding/index.test.ts b/app/selectors/onboarding/index.test.ts
new file mode 100644
index 00000000000..7b1ec1be648
--- /dev/null
+++ b/app/selectors/onboarding/index.test.ts
@@ -0,0 +1,16 @@
+import { selectCompletedOnboarding } from '.';
+import { RootState } from '../../reducers';
+
+describe('Onboarding selectors', () => {
+  const mockState = {
+    onboarding: {
+      completedOnboarding: true,
+    },
+  } as RootState;
+
+  it('returns the correct value for selectCompletedOnboarding ', () => {
+    expect(selectCompletedOnboarding(mockState)).toEqual(
+      mockState.onboarding.completedOnboarding,
+    );
+  });
+});
diff --git a/app/selectors/onboarding/index.ts b/app/selectors/onboarding/index.ts
new file mode 100644
index 00000000000..85ed8d1bd1d
--- /dev/null
+++ b/app/selectors/onboarding/index.ts
@@ -0,0 +1,9 @@
+import { RootState } from '../../reducers';
+import { createSelector } from 'reselect';
+
+const selectOnboarding = (state: RootState) => state.onboarding;
+
+export const selectCompletedOnboarding = createSelector(
+  selectOnboarding,
+  (onboardingState) => onboardingState.completedOnboarding,
+);
diff --git a/app/selectors/wizard/index.test.ts b/app/selectors/wizard/index.test.ts
deleted file mode 100644
index ef2f30a4a78..00000000000
--- a/app/selectors/wizard/index.test.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { selectCurrentOnboardingStep } from '.';
-import { RootState } from '../../reducers';
-
-describe('Wizard Selectors', () => {
-  const mockState = {
-    wizard: {
-      step: 3,
-    },
-  } as unknown as RootState;
-
-  it('selectCurrentOnboardingStep returns correct value', () => {
-    expect(selectCurrentOnboardingStep(mockState)).toEqual(
-      mockState.wizard.step,
-    );
-  });
-});
diff --git a/app/selectors/wizard/index.ts b/app/selectors/wizard/index.ts
deleted file mode 100644
index 4a3c99ec883..00000000000
--- a/app/selectors/wizard/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { RootState } from '../../reducers';
-import { createSelector } from 'reselect';
-
-const selectWizard = (state: RootState) => state.wizard;
-
-export const selectCurrentOnboardingStep = createSelector(
-  selectWizard,
-  (wizardState: Record<string, unknown>) => wizardState.step as number,
-);
diff --git a/app/store/migrations/071.test.ts b/app/store/migrations/071.test.ts
new file mode 100644
index 00000000000..d81388f3ab3
--- /dev/null
+++ b/app/store/migrations/071.test.ts
@@ -0,0 +1,102 @@
+import migrate from './071';
+import { captureException } from '@sentry/react-native';
+import { merge } from 'lodash';
+import initialRootState from '../../util/test/initial-root-state';
+
+jest.mock('@sentry/react-native', () => ({
+  captureException: jest.fn(),
+}));
+const mockedCaptureException = jest.mocked(captureException);
+
+describe('Migration 071: Set completedOnboarding based on the KeyringController state', () => {
+  beforeEach(() => {
+    jest.restoreAllMocks();
+    jest.resetAllMocks();
+  });
+
+  const invalidStates = [
+    {
+      state: null,
+      scenario: 'state is invalid',
+    },
+    {
+      state: merge({}, initialRootState, {
+        engine: null,
+      }),
+      scenario: 'engine state is invalid',
+    },
+    {
+      state: merge({}, initialRootState, {
+        engine: {
+          backgroundState: null,
+        },
+      }),
+      scenario: 'backgroundState is invalid',
+    },
+  ];
+
+  it.each(invalidStates)('captures exception if %s', async ({ state }) => {
+    const newState = await migrate(state);
+
+    expect(newState).toStrictEqual(state);
+    expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error));
+  });
+
+  it('sets completedOnboarding to true if vault exists', () => {
+    const state = merge({}, initialRootState, {
+      onboarding: {
+        completedOnboarding: false,
+      },
+      engine: {
+        backgroundState: {
+          KeyringController: {
+            vault: true,
+          },
+        },
+      },
+    });
+
+    migrate(state);
+
+    expect(state.onboarding.completedOnboarding).toBe(true);
+    expect(mockedCaptureException).not.toHaveBeenCalled();
+  });
+
+  it('sets completedOnboarding to false if vault does not exist', () => {
+    const state = merge({}, initialRootState, {
+      onboarding: {
+        completedOnboarding: false,
+      },
+      engine: {
+        backgroundState: {
+          KeyringController: {
+            vault: false,
+          },
+        },
+      },
+    });
+
+    migrate(state);
+
+    expect(state.onboarding.completedOnboarding).toBe(false);
+    expect(mockedCaptureException).not.toHaveBeenCalled();
+  });
+
+  it('adds the completedOnboarding property if it does not exist', () => {
+    const state = merge({}, initialRootState, {
+      onboarding: {},
+      engine: {
+        backgroundState: {
+          KeyringController: {
+            vault: true,
+          },
+        },
+      },
+    });
+
+    migrate(state);
+
+    expect(state.onboarding.completedOnboarding).toBe(true);
+    expect(mockedCaptureException).not.toHaveBeenCalled();
+  });
+});
diff --git a/app/store/migrations/071.ts b/app/store/migrations/071.ts
new file mode 100644
index 00000000000..0b8c999a3e5
--- /dev/null
+++ b/app/store/migrations/071.ts
@@ -0,0 +1,40 @@
+import { hasProperty, isObject } from '@metamask/utils';
+import { ensureValidState } from './util';
+import { captureException } from '@sentry/react-native';
+
+/**
+ * Migration 71: set completedOnboarding based on the state of the KeyringController.
+ */
+const migration = (state: unknown): unknown => {
+  const migrationVersion = 71;
+
+  // Ensure the state is valid for migration
+  if (!ensureValidState(state, migrationVersion)) {
+    return state;
+  }
+
+  try {
+    const KeyringController = state.engine.backgroundState.KeyringController;
+
+    if (
+      KeyringController &&
+      hasProperty(KeyringController, 'vault') &&
+      hasProperty(state, 'onboarding') &&
+      isObject(state.onboarding)
+    ) {
+      const { vault } = KeyringController;
+      state.onboarding.completedOnboarding = Boolean(vault);
+    }
+
+    return state;
+  } catch (error) {
+    captureException(
+      new Error(
+        `Migration 071: setting completedOnboarding failed with error: ${error}`,
+      ),
+    );
+    return state;
+  }
+};
+
+export default migration;
diff --git a/app/store/migrations/index.ts b/app/store/migrations/index.ts
index d1f0c08be5f..41d572e3037 100644
--- a/app/store/migrations/index.ts
+++ b/app/store/migrations/index.ts
@@ -71,6 +71,7 @@ import migration67 from './067';
 import migration68 from './068';
 import migration69 from './069';
 import migration70 from './070';
+import migration71 from './071';
 
 // Add migrations above this line
 import { validatePostMigrationState } from '../validateMigration/validateMigration';
@@ -158,6 +159,7 @@ export const migrationList: MigrationsList = {
   68: migration68,
   69: migration69,
   70: migration70,
+  71: migration71,
 };
 
 // Enable both synchronous and asynchronous migrations
diff --git a/app/util/identity/hooks/useAuthentication/useAutoSignIn.test.ts b/app/util/identity/hooks/useAuthentication/useAutoSignIn.test.ts
index 35da05306e0..7f4a51e0462 100644
--- a/app/util/identity/hooks/useAuthentication/useAutoSignIn.test.ts
+++ b/app/util/identity/hooks/useAuthentication/useAutoSignIn.test.ts
@@ -42,8 +42,8 @@ const arrangeMockState = (
       },
     },
   },
-  wizard: {
-    step: stateOverrides.completedOnboarding ? 0 : 1,
+  onboarding: {
+    completedOnboarding: stateOverrides.completedOnboarding,
   },
   settings: {
     basicFunctionalityEnabled: stateOverrides.useExternalServices,
diff --git a/app/util/identity/hooks/useAuthentication/useAutoSignIn.ts b/app/util/identity/hooks/useAuthentication/useAutoSignIn.ts
index 710a86890dc..631c7f448dd 100644
--- a/app/util/identity/hooks/useAuthentication/useAutoSignIn.ts
+++ b/app/util/identity/hooks/useAuthentication/useAutoSignIn.ts
@@ -9,7 +9,7 @@ import {
   selectIsSignedIn,
 } from '../../../../selectors/identity';
 import { selectIsMetamaskNotificationsEnabled } from '../../../../selectors/notifications';
-import { selectCurrentOnboardingStep } from '../../../../selectors/wizard';
+import { selectCompletedOnboarding } from '../../../../selectors/onboarding';
 import { useMetrics } from '../../../../components/hooks/useMetrics';
 
 /**
@@ -32,7 +32,7 @@ export function useAutoSignIn(): {
     useSelector(selectBasicFunctionalityEnabled),
   );
 
-  const completedOnboarding = useSelector(selectCurrentOnboardingStep) === 0;
+  const completedOnboarding = useSelector(selectCompletedOnboarding);
   const isSignedIn = useSelector(selectIsSignedIn);
 
   const areBasePrerequisitesMet = useMemo(
diff --git a/app/util/test/initial-root-state.ts b/app/util/test/initial-root-state.ts
index 9daccfb2476..b177b6c20b5 100644
--- a/app/util/test/initial-root-state.ts
+++ b/app/util/test/initial-root-state.ts
@@ -9,6 +9,7 @@ import { initialState as initialBridgeState } from '../../core/redux/slices/brid
 import initialBackgroundState from './initial-background-state.json';
 import { userInitialState } from '../../reducers/user';
 import { initialNavigationState } from '../../reducers/navigation';
+import { initialOnboardingState } from '../../reducers/onboarding';
 
 // A cast is needed here because we use enums in some controllers, and TypeScript doesn't consider
 // the string value of an enum as satisfying an enum type.
@@ -28,7 +29,7 @@ const initialRootState: RootState = {
   transaction: undefined,
   user: userInitialState,
   wizard: undefined,
-  onboarding: undefined,
+  onboarding: initialOnboardingState,
   notification: undefined,
   swaps: undefined,
   fiatOrders: initialFiatOrdersState,


Reviews:
user: login: mathieuartu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T10:46:02Z
user: login: mathieuartu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T10:49:18Z
user: login: mathieuartu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T10:49:58Z
user: login: mathieuartu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T10:51:08Z
user: login: mirceanis
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T05:34:38Z
user: login: tommasini
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T10:20:30Z
user: login: tommasini
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T10:32:34Z
user: login: shane-t
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-07T12:40:43Z
user: login: mirceanis
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T13:56:03Z
user: login: darkwing
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T14:40:44Z

Review comments:
diff_hunk: @@ -0,0 +1,51 @@
+/* eslint-disable @typescript-eslint/default-param-last */
+
+import {
+  CLEAR_EVENTS,
+  OnboardingActionTypes,
+  SAVE_EVENT,
+  SET_COMPLETED_ONBOARDING,
+} from '../../actions/onboarding';
+import { ITrackingEvent } from '../../core/Analytics/MetaMetrics.types';
+
+export interface OnboardingState {
+  events: [ITrackingEvent][];
+  completedOnboarding: boolean;
+}
+
+export const initialOnboardingState: OnboardingState = {
+  events: [],
+  completedOnboarding: false,
+};
+
+/**
+ * Reducer to keep track of user oboarding actions:
+ * - send it to analytics if the user decides to optin after finishing onboarding flow
+ * - update the state of the onboarding flow
+ */
+const onboardingReducer = (
+  state = initialOnboardingState,
+  action: OnboardingActionTypes,
+) => {
+  switch (action.type) {
+    case SAVE_EVENT:
+      return {
+        ...state,
+        events: [...state.events, action.event],
+      };
+    case CLEAR_EVENTS:
+      return {
+        ...state,
+        events: [],
+      };
+    case SET_COMPLETED_ONBOARDING:
+      return {
+        ...state,
+        completedOnboarding: action.completedOnboarding,
+      };
+    default:
+      return state;
+  }
+};
+
+export default onboardingReducer;
path: app/reducers/onboarding/index.ts
user: login: mathieuartu
body: This file is marked as new because of migrating it to .ts
created_at: 2025-04-02T10:46:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,40 @@
+import { hasProperty, isObject } from '@metamask/utils';
+import { ensureValidState } from './util';
+import { captureException } from '@sentry/react-native';
+
+/**
+ * Migration 71: set completedOnboarding based on the state of the KeyringController.
+ */
+const migration = (state: unknown): unknown => {
+  const migrationVersion = 71;
+
+  // Ensure the state is valid for migration
+  if (!ensureValidState(state, migrationVersion)) {
+    return state;
+  }
+
+  try {
+    const KeyringController = state.engine.backgroundState.KeyringController;
+
+    if (
+      KeyringController &&
+      hasProperty(KeyringController, 'vault') &&
+      hasProperty(state, 'onboarding') &&
+      isObject(state.onboarding)
+    ) {
+      const { vault } = KeyringController;
+      state.onboarding.completedOnboarding = Boolean(vault);
path: app/store/migrations/071.ts
user: login: mathieuartu
body: This relies on the same logic currently used on extension ([1](https://github.com/MetaMask/metamask-extension/blob/76a2a9bb8b6ea04025328d36404ac3b59121dfc8/app/scripts/migrations/031.js#L26-L29), [2](https://github.com/MetaMask/metamask-extension/blob/c72199a1a6e4151c40c22f79d0f3b6ed7a2d59a7/app/scripts/migrations/065.js#L21-L34))
The only difference being that we save the result directly in `state.onboarding` because we don't have an `OnboardingController` on mobile
created_at: 2025-04-02T10:49:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,8 +42,8 @@ const arrangeMockState = (
       },
     },
   },
-  wizard: {
-    step: stateOverrides.completedOnboarding ? 0 : 1,
path: app/util/identity/hooks/useAuthentication/useAutoSignIn.test.ts
user: login: mathieuartu
body: This was the wrong property to watch. This is fixed by using `completedOnboarding` instead.
created_at: 2025-04-02T10:49:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,51 @@
+/* eslint-disable @typescript-eslint/default-param-last */
+
+import {
+  CLEAR_EVENTS,
+  OnboardingActionTypes,
+  SAVE_EVENT,
+  SET_COMPLETED_ONBOARDING,
+} from '../../actions/onboarding';
+import { ITrackingEvent } from '../../core/Analytics/MetaMetrics.types';
+
+export interface OnboardingState {
+  events: [ITrackingEvent][];
+  completedOnboarding: boolean;
path: app/reducers/onboarding/index.ts
user: login: mathieuartu
body: I know that `onboarding.completedOnboarding` is a bit redundant, but I chose that name so that we have the same names as extension, facilitating global search within projects for logic relying on this property.
created_at: 2025-04-02T10:51:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,51 @@
+/* eslint-disable @typescript-eslint/default-param-last */
+
+import {
+  CLEAR_EVENTS,
+  OnboardingActionTypes,
+  SAVE_EVENT,
+  SET_COMPLETED_ONBOARDING,
+} from '../../actions/onboarding';
+import { ITrackingEvent } from '../../core/Analytics/MetaMetrics.types';
+
+export interface OnboardingState {
+  events: [ITrackingEvent][];
+  completedOnboarding: boolean;
path: app/reducers/onboarding/index.ts
user: login: tommasini
body: It would be awesome if we have a TSDoce for the interface or even only for `completedOnboarding`, since it's a little bit redundant, a future dev could be less confused if it have a TSDocumentation
created_at: 2025-04-07T10:20:30Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-02T10:26:55Z
body: **CLA Signature Action:** All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-02T10:39:58Z
body: ## [<img alt="https://bitrise.io/" src="https://assets-global.website-files.com/5db35de024bb983af1b4e151/5e6f9ccc3e129dfd8a205e4e_Bitrise%20Logo%20-%20Eggplant%20Bg.png" height="20">](https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/bf526729-40fd-45ac-9b2e-a159833107c5) **Bitrise**

❌❌❌ `pr_smoke_e2e_pipeline` failed on Bitrise! ❌❌❌

Commit hash: 1351d35a467411b1e40221c0e30fa56752e6ed24
Build link: https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/bf526729-40fd-45ac-9b2e-a159833107c5

>[!NOTE]
>- You can kick off another `pr_smoke_e2e_pipeline` on Bitrise by removing and re-applying the `Run Smoke E2E` label on the pull request

> [!TIP]
>- Check the [documentation](https://www.notion.so/metamask-consensys/Bitrise-Pipeline-Overview-43159500c43748a389556f0593e8834b#26052f2ea6e24f8c9cfdb57a7522dc1f) if you have any doubts on how to understand the failure on bitrise

<!-- BITRISE_TAG -->
<!-- BITRISE_FAIL_TAG -->

<!-- 1351d35a467411b1e40221c0e30fa56752e6ed24 -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-02T12:59:57Z
body: ## [<img alt="https://bitrise.io/" src="https://assets-global.website-files.com/5db35de024bb983af1b4e151/5e6f9ccc3e129dfd8a205e4e_Bitrise%20Logo%20-%20Eggplant%20Bg.png" height="20">](https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/307ce7fc-3e9d-4100-8596-46801d26c90a) **Bitrise**

❌❌❌ `pr_smoke_e2e_pipeline` failed on Bitrise! ❌❌❌

Commit hash: ad4bf5232aca6fa0509c5852f824447fc296d295
Build link: https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/307ce7fc-3e9d-4100-8596-46801d26c90a

>[!NOTE]
>- You can kick off another `pr_smoke_e2e_pipeline` on Bitrise by removing and re-applying the `Run Smoke E2E` label on the pull request

> [!TIP]
>- Check the [documentation](https://www.notion.so/metamask-consensys/Bitrise-Pipeline-Overview-43159500c43748a389556f0593e8834b#26052f2ea6e24f8c9cfdb57a7522dc1f) if you have any doubts on how to understand the failure on bitrise

<!-- BITRISE_TAG -->
<!-- BITRISE_FAIL_TAG -->

<!-- ad4bf5232aca6fa0509c5852f824447fc296d295 -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-02T14:14:45Z
body: ## [<img alt="https://bitrise.io/" src="https://assets-global.website-files.com/5db35de024bb983af1b4e151/5e6f9ccc3e129dfd8a205e4e_Bitrise%20Logo%20-%20Eggplant%20Bg.png" height="20">](https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/1fcfa76d-0461-4863-8da4-a360afe5f246) **Bitrise**

❌❌❌ `pr_smoke_e2e_pipeline` failed on Bitrise! ❌❌❌

Commit hash: f4676d525a2ac4fa3b08ba3f8920fece58f22040
Build link: https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/1fcfa76d-0461-4863-8da4-a360afe5f246

>[!NOTE]
>- You can kick off another `pr_smoke_e2e_pipeline` on Bitrise by removing and re-applying the `Run Smoke E2E` label on the pull request

> [!TIP]
>- Check the [documentation](https://www.notion.so/metamask-consensys/Bitrise-Pipeline-Overview-43159500c43748a389556f0593e8834b#26052f2ea6e24f8c9cfdb57a7522dc1f) if you have any doubts on how to understand the failure on bitrise

<!-- BITRISE_TAG -->
<!-- BITRISE_FAIL_TAG -->

<!-- f4676d525a2ac4fa3b08ba3f8920fece58f22040 -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-03T08:07:44Z
body: ## [<img alt="https://bitrise.io/" src="https://assets-global.website-files.com/5db35de024bb983af1b4e151/5e6f9ccc3e129dfd8a205e4e_Bitrise%20Logo%20-%20Eggplant%20Bg.png" height="20">](https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/cbb35f97-3a1b-4d4c-a9ce-ef8a948260fb) **Bitrise**

❌❌❌ `pr_smoke_e2e_pipeline` failed on Bitrise! ❌❌❌

Commit hash: bddf9e24c0f48d01446301e13d4594a65fbfb518
Build link: https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/cbb35f97-3a1b-4d4c-a9ce-ef8a948260fb

>[!NOTE]
>- You can kick off another `pr_smoke_e2e_pipeline` on Bitrise by removing and re-applying the `Run Smoke E2E` label on the pull request

> [!TIP]
>- Check the [documentation](https://www.notion.so/metamask-consensys/Bitrise-Pipeline-Overview-43159500c43748a389556f0593e8834b#26052f2ea6e24f8c9cfdb57a7522dc1f) if you have any doubts on how to understand the failure on bitrise

<!-- BITRISE_TAG -->
<!-- BITRISE_FAIL_TAG -->

<!-- bddf9e24c0f48d01446301e13d4594a65fbfb518 -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-03T17:50:17Z
body: ## [<img alt="https://bitrise.io/" src="https://assets-global.website-files.com/5db35de024bb983af1b4e151/5e6f9ccc3e129dfd8a205e4e_Bitrise%20Logo%20-%20Eggplant%20Bg.png" height="20">](https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/a1fe6353-c640-4b06-b16f-60d6d93eebaf) **Bitrise**

❌❌❌ `pr_smoke_e2e_pipeline` failed on Bitrise! ❌❌❌

Commit hash: e0eb23f0d0db96ab897c195400c4559d912be558
Build link: https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/a1fe6353-c640-4b06-b16f-60d6d93eebaf

>[!NOTE]
>- You can kick off another `pr_smoke_e2e_pipeline` on Bitrise by removing and re-applying the `Run Smoke E2E` label on the pull request

> [!TIP]
>- Check the [documentation](https://www.notion.so/metamask-consensys/Bitrise-Pipeline-Overview-43159500c43748a389556f0593e8834b#26052f2ea6e24f8c9cfdb57a7522dc1f) if you have any doubts on how to understand the failure on bitrise

<!-- BITRISE_TAG -->
<!-- BITRISE_FAIL_TAG -->

<!-- e0eb23f0d0db96ab897c195400c4559d912be558 -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-03T19:28:17Z
body: ## [<img alt="https://bitrise.io/" src="https://assets-global.website-files.com/5db35de024bb983af1b4e151/5e6f9ccc3e129dfd8a205e4e_Bitrise%20Logo%20-%20Eggplant%20Bg.png" height="20">](https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/bc5fdc53-46ae-4ccf-a86a-f571ab083b9d) **Bitrise**

❌❌❌ `pr_smoke_e2e_pipeline` failed on Bitrise! ❌❌❌

Commit hash: 1da5ee4b7531686392c1d3248e3c61b158366447
Build link: https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/bc5fdc53-46ae-4ccf-a86a-f571ab083b9d

>[!NOTE]
>- You can kick off another `pr_smoke_e2e_pipeline` on Bitrise by removing and re-applying the `Run Smoke E2E` label on the pull request

> [!TIP]
>- Check the [documentation](https://www.notion.so/metamask-consensys/Bitrise-Pipeline-Overview-43159500c43748a389556f0593e8834b#26052f2ea6e24f8c9cfdb57a7522dc1f) if you have any doubts on how to understand the failure on bitrise

<!-- BITRISE_TAG -->
<!-- BITRISE_FAIL_TAG -->

<!-- 1da5ee4b7531686392c1d3248e3c61b158366447 -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-04T11:42:07Z
body: ## [<img alt="https://bitrise.io/" src="https://assets-global.website-files.com/5db35de024bb983af1b4e151/5e6f9ccc3e129dfd8a205e4e_Bitrise%20Logo%20-%20Eggplant%20Bg.png" height="20">](https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/1ff392be-90d4-4967-a6e3-21752b666f07) **Bitrise**

❌❌❌ `pr_smoke_e2e_pipeline` failed on Bitrise! ❌❌❌

Commit hash: 9f35a2d16363c03d307c26e909899246ee38c938
Build link: https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/1ff392be-90d4-4967-a6e3-21752b666f07

>[!NOTE]
>- You can kick off another `pr_smoke_e2e_pipeline` on Bitrise by removing and re-applying the `Run Smoke E2E` label on the pull request

> [!TIP]
>- Check the [documentation](https://www.notion.so/metamask-consensys/Bitrise-Pipeline-Overview-43159500c43748a389556f0593e8834b#26052f2ea6e24f8c9cfdb57a7522dc1f) if you have any doubts on how to understand the failure on bitrise

<!-- BITRISE_TAG -->
<!-- BITRISE_FAIL_TAG -->

<!-- 9f35a2d16363c03d307c26e909899246ee38c938 -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-04T17:00:39Z
body: ## [<img alt="https://bitrise.io/" src="https://assets-global.website-files.com/5db35de024bb983af1b4e151/5e6f9ccc3e129dfd8a205e4e_Bitrise%20Logo%20-%20Eggplant%20Bg.png" height="20">](https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/9e200772-8e27-471d-bcd5-8fa40909980f) **Bitrise**

✅✅✅ `pr_smoke_e2e_pipeline` passed on Bitrise! ✅✅✅

Commit hash: 139b820c960efee434715d7689f1fc1b9eace342
Build link: https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/9e200772-8e27-471d-bcd5-8fa40909980f

>[!NOTE]
>- You can kick off another `pr_smoke_e2e_pipeline` on Bitrise by removing and re-applying the `Run Smoke E2E` label on the pull request

<!-- BITRISE_TAG -->
<!-- BITRISE_SUCCESS_TAG -->

<!-- 139b820c960efee434715d7689f1fc1b9eace342 -->
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-04-07T20:16:37Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=metamask-mobile&pullRequest=14370) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=metamask-mobile&pullRequest=14370&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=metamask-mobile&pullRequest=14370&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=metamask-mobile&pullRequest=14370&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [95.7% Coverage on New Code](https://sonarcloud.io/component_measures?id=metamask-mobile&pullRequest=14370&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=metamask-mobile&pullRequest=14370&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=metamask-mobile&pullRequest=14370)


author_association: NONE