Skip to content

feat: MUSD-776 create money account onboarding flow with rive animation#30137

Merged
Matt561 merged 16 commits into
mainfrom
feat/musd-776-create-money-account-onboarding-flow-with-rive-animation
May 14, 2026
Merged

feat: MUSD-776 create money account onboarding flow with rive animation#30137
Matt561 merged 16 commits into
mainfrom
feat/musd-776-create-money-account-onboarding-flow-with-rive-animation

Conversation

@Matt561

@Matt561 Matt561 commented May 13, 2026

Copy link
Copy Markdown
Contributor

Description

This PR creates the Money account onboarding flow and wires it up for first time users.

Changes

  • Created reusable highly-configurable <RiveOnboardingStepper/> to support future onboarding flows.
  • Created MoneyOnboardingView which uses <RiveOnboardingStepper/>
  • Wired up redirects to the MoneyOnboardingView for first time users when:
    • Pressing the "Money" navbar button
    • Pressing the home screen Money balance card
    • Pressing the home screen Money balance card's "Get started" button
  • Added Money onboarding reset button to developer options menu to reset state

Changelog

CHANGELOG entry: added Money account onboarding flow

Related issues

Fixes: MUSD-776: Create Money Account Onboarding with Rive Animation

Manual testing steps

Feature: Money onboarding redirect

  Scenario: user navigates to Money via the Money navbar button
    Given user has not previously seen the Money onboarding
    And user is on the home screen

    When user taps the Money navbar item
    Then user is navigated to the Money onboarding screen
    And the animated onboarding stepper plays through its steps
    And when completed, the Money onboarding seen flag is set to true
    And user is navigated to the Money home screen

  Scenario: user navigates to Money via the MoneyBalanceCard
    Given user has not previously seen the Money onboarding
    And user is on the wallet home screen with the MoneyBalanceCard visible

    When user taps the "Get Started" button on the MoneyBalanceCard
    Then user is navigated to the Money onboarding screen
    And the animated onboarding stepper plays through its steps
    And when completed, the Money onboarding seen flag is set to true
    And user is navigated to the Money home screen

Screenshots/Recordings

Before

N/A - Onboarding flow didn't exist

After

money-onboarding-v0.1.mov

Pre-merge author checklist

Performance checks (if applicable)

  • I've tested on Android
    • Ideally on a mid-range device; emulator is acceptable
  • I've tested with a power user scenario
    • Use these power-user SRPs to import wallets with many accounts and tokens
  • I've instrumented key operations with Sentry traces for production performance metrics

For performance guidelines and tooling, see the Performance Guide.

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

Medium Risk
Adds a new multi-step onboarding screen and changes Money entry navigation to conditionally redirect based on a new persisted moneyOnboardingSeen flag, which could affect user routing if mis-set. Risk is mitigated by reducer/selector coverage and component-level tests but still touches core navigation paths (TabBar, Home, Money card).

Overview
Introduces a new Money account onboarding flow (Routes.MONEY.ONBOARDING) backed by a reusable RiveOnboardingStepper, including progress UI, step timing, close handling, and optional auto-complete on the final step.

Adds a new persisted user flag (moneyOnboardingSeen) with action/reducer/selector plumbing, and updates Money entry points (TabBar Money tab and MoneyBalanceCard) to use a shared useMoneyNavigation.navigateToMoneyHome() redirect that sends first-time users to onboarding before allowing navigation to Money home.

Extends Developer Options (behind the Money feature flag) with a Money UI section to reset onboarding state and copy the primary money account address, and adds i18n strings plus targeted tests for the new components and navigation behavior.

Reviewed by Cursor Bugbot for commit 381fd23. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

Copy link
Copy Markdown
Contributor

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.

@Matt561 Matt561 marked this pull request as ready for review May 13, 2026 17:47
@Matt561 Matt561 requested review from a team as code owners May 13, 2026 17:47
Comment thread app/components/UI/Money/hooks/useMoneyNavigation.ts
Comment thread app/components/UI/Money/Views/MoneyOnboardingView/MoneyOnboardingView.tsx Outdated
Comment thread app/components/UI/Money/hooks/useMoneyNavigation.ts
Comment thread app/components/UI/Money/Views/MoneyOnboardingView/MoneyOnboardingView.tsx Outdated
Comment thread app/components/UI/RiveOnboardingStepper/RiveOnboardingStepper.tsx
return (
<>
<Text variant={TextVariant.HeadingLg} style={styles.heading}>
{'Money UI'}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: We don't need locale strings here since this is for internal use only.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6864a98. Configure here.

@Matt561 Matt561 force-pushed the feat/musd-776-create-money-account-onboarding-flow-with-rive-animation branch from 6864a98 to f9fda76 Compare May 13, 2026 19:59
vinnyhoward
vinnyhoward previously approved these changes May 13, 2026

@vinnyhoward vinnyhoward left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very cool! I love the reusable component for future stepper animations. Well done

@vinnyhoward vinnyhoward left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was it intentional to show Tether in the designs @Matt561 ?

@Matt561

Matt561 commented May 13, 2026

Copy link
Copy Markdown
Contributor Author

Was it intentional to show Tether in the designs @Matt561 ?

Do you mean in the Rive animation? If yes, it's intentional.

} from '@metamask/design-system-react-native';
import styleSheet from '../../../Views/Settings/DeveloperOptions/DeveloperOptions.styles';

export const MoneyUiDeveloperOptionsSection = () => {

@shane-t shane-t May 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you feel about tagging this on @Matt561 ?

diff --git a/app/components/UI/Money/components/MoneyUiDeveloperOptionsSection.test.tsx b/app/components/UI/Money/components/MoneyUiDeveloperOptionsSection.test.tsx
index 9c16eb18a9..f02c2114e5 100644
--- a/app/components/UI/Money/components/MoneyUiDeveloperOptionsSection.test.tsx
+++ b/app/components/UI/Money/components/MoneyUiDeveloperOptionsSection.test.tsx
@@ -1,7 +1,9 @@
 import React from 'react';
-import { render, fireEvent } from '@testing-library/react-native';
+import { render, fireEvent, act } from '@testing-library/react-native';
 import { MoneyUiDeveloperOptionsSection } from './MoneyUiDeveloperOptionsSection';
 import { UserActionType } from '../../../../actions/user/types';
+import { selectMoneyOnboardingSeen } from '../../../../reducers/user/selectors';
+import { selectPrimaryMoneyAccount } from '../../../../selectors/moneyAccountController';
 
 const mockDispatch = jest.fn();
 const mockUseSelector = jest.fn();
@@ -19,10 +21,49 @@ jest.mock('../../../../util/theme', () => {
   };
 });
 
+const mockSetString = jest.fn(() => Promise.resolve());
+
+jest.mock('../../../../core/ClipboardManager', () => ({
+  __esModule: true,
+  default: {
+    setString: (...args: unknown[]) => mockSetString(...args),
+  },
+}));
+
+const MOCK_ADDRESS = '0xABCDEF1234567890ABCDEF1234567890ABCDEF12';
+
+interface SelectorMockOptions {
+  hasSeenMoneyOnboarding?: boolean;
+  /** Pass `null` to simulate no money account being available. */
+  moneyAccount?: { address: string } | null;
+}
+
+/**
+ * Configures the useSelector mock to return appropriate values for each selector
+ * used by MoneyUiDeveloperOptionsSection.
+ *
+ * Default: onboarding not seen, primary money account present with MOCK_ADDRESS.
+ * Pass `moneyAccount: null` to simulate the account being unavailable.
+ */
+function setupSelectorMocks(options: SelectorMockOptions = {}) {
+  const hasSeenMoneyOnboarding = options.hasSeenMoneyOnboarding ?? false;
+  // `null` means "no account", `undefined` (omitted) means use the default.
+  const moneyAccount =
+    options.moneyAccount === null
+      ? undefined
+      : (options.moneyAccount ?? { address: MOCK_ADDRESS });
+
+  mockUseSelector.mockImplementation((selector: unknown) => {
+    if (selector === selectMoneyOnboardingSeen) return hasSeenMoneyOnboarding;
+    if (selector === selectPrimaryMoneyAccount) return moneyAccount;
+    return undefined;
+  });
+}
+
 describe('MoneyUiDeveloperOptionsSection', () => {
   beforeEach(() => {
     jest.clearAllMocks();
-    mockUseSelector.mockReturnValue(false);
+    setupSelectorMocks();
   });
 
   it('renders the "Money UI" heading', () => {
@@ -31,40 +72,89 @@ describe('MoneyUiDeveloperOptionsSection', () => {
     expect(getByText('Money UI')).toBeOnTheScreen();
   });
 
-  it('displays onboarding seen as false when not seen', () => {
-    mockUseSelector.mockReturnValue(false);
+  describe('onboarding seen state', () => {
+    it('displays onboarding seen as false when not seen', () => {
+      setupSelectorMocks({ hasSeenMoneyOnboarding: false });
 
-    const { getByText } = render(<MoneyUiDeveloperOptionsSection />);
+      const { getByText } = render(<MoneyUiDeveloperOptionsSection />);
 
-    expect(getByText('Onboarding seen: false')).toBeOnTheScreen();
-  });
+      expect(getByText('Onboarding seen: false')).toBeOnTheScreen();
+    });
 
-  it('displays onboarding seen as true when seen', () => {
-    mockUseSelector.mockReturnValue(true);
+    it('displays onboarding seen as true when seen', () => {
+      setupSelectorMocks({ hasSeenMoneyOnboarding: true });
 
-    const { getByText } = render(<MoneyUiDeveloperOptionsSection />);
+      const { getByText } = render(<MoneyUiDeveloperOptionsSection />);
 
-    expect(getByText('Onboarding seen: true')).toBeOnTheScreen();
-  });
+      expect(getByText('Onboarding seen: true')).toBeOnTheScreen();
+    });
 
-  it('renders the reset button', () => {
-    const { getByText } = render(<MoneyUiDeveloperOptionsSection />);
+    it('renders the reset button', () => {
+      const { getByText } = render(<MoneyUiDeveloperOptionsSection />);
+
+      expect(getByText('Reset onboarding screen')).toBeOnTheScreen();
+    });
+
+    it('dispatches setMoneyOnboardingSeen(false) when reset button is pressed', () => {
+      setupSelectorMocks({ hasSeenMoneyOnboarding: true });
+
+      const { getByText } = render(<MoneyUiDeveloperOptionsSection />);
 
-    expect(getByText('Reset onboarding screen')).toBeOnTheScreen();
+      fireEvent.press(getByText('Reset onboarding screen'));
+
+      expect(mockDispatch).toHaveBeenCalledWith(
+        expect.objectContaining({
+          type: UserActionType.SET_MONEY_ONBOARDING_SEEN,
+          payload: { seen: false },
+        }),
+      );
+    });
   });
 
-  it('dispatches setMoneyOnboardingSeen(false) when reset button is pressed', () => {
-    mockUseSelector.mockReturnValue(true);
+  describe('Money Account Address', () => {
+    it('displays the money account address when available', () => {
+      const { getByText } = render(<MoneyUiDeveloperOptionsSection />);
 
-    const { getByText } = render(<MoneyUiDeveloperOptionsSection />);
+      expect(
+        getByText(`Money Account Address: ${MOCK_ADDRESS}`),
+      ).toBeOnTheScreen();
+    });
+
+    it('displays N/A when the money account address is unavailable', () => {
+      setupSelectorMocks({ moneyAccount: null });
+
+      const { getByText } = render(<MoneyUiDeveloperOptionsSection />);
+
+      expect(getByText('Money Account Address: N/A')).toBeOnTheScreen();
+    });
+
+    it('renders the copy address button', () => {
+      const { getByText } = render(<MoneyUiDeveloperOptionsSection />);
+
+      expect(getByText('Copy Money Account Address')).toBeOnTheScreen();
+    });
+
+    it('copies the address to clipboard when the copy button is pressed', async () => {
+      const { getByText } = render(<MoneyUiDeveloperOptionsSection />);
+
+      await act(async () => {
+        fireEvent.press(getByText('Copy Money Account Address'));
+      });
+
+      expect(mockSetString).toHaveBeenCalledTimes(1);
+      expect(mockSetString).toHaveBeenCalledWith(MOCK_ADDRESS);
+    });
+
+    it('does not copy to clipboard when the address is unavailable', async () => {
+      setupSelectorMocks({ moneyAccount: null });
+
+      const { getByText } = render(<MoneyUiDeveloperOptionsSection />);
 
-    fireEvent.press(getByText('Reset onboarding screen'));
+      await act(async () => {
+        fireEvent.press(getByText('Copy Money Account Address'));
+      });
 
-    expect(mockDispatch).toHaveBeenCalledWith(
-      expect.objectContaining({
-        type: UserActionType.SET_MONEY_ONBOARDING_SEEN,
-        payload: { seen: false },
-      }),
-    );
+      expect(mockSetString).not.toHaveBeenCalled();
+    });
   });
 });
diff --git a/app/components/UI/Money/components/MoneyUiDeveloperOptionsSection.tsx b/app/components/UI/Money/components/MoneyUiDeveloperOptionsSection.tsx
index a087e8e65e..b4b15d000e 100644
--- a/app/components/UI/Money/components/MoneyUiDeveloperOptionsSection.tsx
+++ b/app/components/UI/Money/components/MoneyUiDeveloperOptionsSection.tsx
@@ -4,6 +4,7 @@ import { useDispatch, useSelector } from 'react-redux';
 import { useTheme } from '../../../../util/theme';
 import { setMoneyOnboardingSeen } from '../../../../actions/user';
 import { selectMoneyOnboardingSeen } from '../../../../reducers/user/selectors';
+import { selectPrimaryMoneyAccount } from '../../../../selectors/moneyAccountController';
 import { useStyles } from '../../../../component-library/hooks';
 import {
   Text,
@@ -14,6 +15,7 @@ import {
   ButtonSize,
 } from '@metamask/design-system-react-native';
 import styleSheet from '../../../Views/Settings/DeveloperOptions/DeveloperOptions.styles';
+import ClipboardManager from '../../../../core/ClipboardManager';
 
 export const MoneyUiDeveloperOptionsSection = () => {
   const dispatch = useDispatch();
@@ -21,11 +23,19 @@ export const MoneyUiDeveloperOptionsSection = () => {
   const { styles } = useStyles(styleSheet, { theme });
 
   const hasSeenMoneyOnboarding = useSelector(selectMoneyOnboardingSeen);
+  const primaryMoneyAccount = useSelector(selectPrimaryMoneyAccount);
+  const moneyAccountAddress = primaryMoneyAccount?.address;
 
   const handleResetOnboardingSeenState = useCallback(() => {
     dispatch(setMoneyOnboardingSeen(false));
   }, [dispatch]);
 
+  const handleCopyAddress = useCallback(async () => {
+    if (moneyAccountAddress) {
+      await ClipboardManager.setString(moneyAccountAddress);
+    }
+  }, [moneyAccountAddress]);
+
   return (
     <>
       <Text variant={TextVariant.HeadingLg} style={styles.heading}>
@@ -47,6 +57,23 @@ export const MoneyUiDeveloperOptionsSection = () => {
       >
         {'Reset onboarding screen'}
       </Button>
+      <Text
+        color={TextColor.TextAlternative}
+        variant={TextVariant.BodyMd}
+        style={styles.desc}
+      >
+        {`Money Account Address: ${moneyAccountAddress ?? 'N/A'}`}
+      </Text>
+      <Button
+        variant={ButtonVariant.Secondary}
+        style={styles.accessory}
+        size={ButtonSize.Lg}
+        onPress={handleCopyAddress}
+        isDisabled={!moneyAccountAddress}
+        isFullWidth
+      >
+        {'Copy Money Account Address'}
+      </Button>
     </>
   );
 };
Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea. I'll add this 👍

shane-t
shane-t previously approved these changes May 14, 2026

const handleContinue = useCallback(() => {
if (isLastStep) {
onComplete();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we set hasCompletedRef.current to true here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch

});
}, [steps.length, progress]);

const restartProgress = useCallback(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this isn't used

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll remove this.

__getLastMockedMethods,
__clearLastMockedMethods,
__mockRiveFireState,
} from '../.././../__mocks__/rive-react-native';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} from '../.././../__mocks__/rive-react-native';
} from '../../../__mocks__/rive-react-native';

</Box>
{/* Footer button */}
<Box
twClassName={`px-4${!isRiveReady || !currentStep?.buttonLabel ? ' opacity-0' : ''}`}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We make the button invisible if there's no label - but I guess it still accepts touches? Could we have also add isDisabled if there's no label

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely.

@Matt561 Matt561 dismissed stale reviews from shane-t and vinnyhoward via 44794e4 May 14, 2026 15:36
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokeMoney, SmokeWalletPlatform
  • Selected Performance tags: None (no tests recommended)
  • Risk Level: medium
  • AI Confidence: 82%
click to see 🤖 AI reasoning details

E2E Test Selection:
The PR introduces a new Money onboarding flow with the following key changes:

  1. TabBar.tsx (shared navigation component): The Money tab navigation now uses useMoneyNavigation hook which checks moneyOnboardingSeen state. If false (default), users are redirected to the new MoneyOnboardingView instead of Money Home. This directly impacts SmokeMoney tests (card-button.spec.ts, card-home-add-funds.spec.ts) which tap the Money/Card tab - if fixtures don't set moneyOnboardingSeen: true, tests could land on the onboarding screen instead of Card Home.

  2. MoneyBalanceCard.tsx: The 'Get Started' button behavior changed - previously navigated to EARN/MUSD conversion flow, now navigates to Money Home (with onboarding check). This affects Money feature tests.

  3. New MoneyOnboardingView + RiveOnboardingStepper: New screens registered in MainNavigator, new route Routes.MONEY.ONBOARDING added.

  4. Redux state changes: New moneyOnboardingSeen field in user reducer - this is a new persisted state that could affect fixture-based tests if not properly initialized.

  5. DeveloperOptions: New MoneyUiDeveloperOptionsSection added (low risk, developer-only).

SmokeMoney is selected because: The TabBar Money tab navigation is directly modified, MoneyBalanceCard behavior changed, and existing card/ramps tests navigate through the Money tab which could be intercepted by the new onboarding flow.

SmokeWalletPlatform is selected because: Per SmokeMoney tag description, 'When changes touch wallet home or actions entry to buy/sell, also select SmokeWalletPlatform.' The TabBar is a shared navigation component and WalletPlatform tests may interact with Money-related navigation.

Other tags (SmokeConfirmations, SmokeSwap, SmokeAccounts, etc.) are not selected because the TabBar change is scoped only to the Routes.MONEY.HOME case and doesn't affect other tab navigation paths.

Performance Test Selection:
The changes introduce a new Money onboarding screen with Rive animation and modify TabBar navigation for the Money tab. While Rive animations could have performance implications, the changes are scoped to a new onboarding flow (first-time user experience) and don't affect core performance-sensitive paths like account list rendering, app launch, login, or asset loading. No performance test tags are warranted for this PR.

View GitHub Actions results

@sonarqubecloud

Copy link
Copy Markdown

@Matt561 Matt561 added this pull request to the merge queue May 14, 2026
Merged via the queue into main with commit 2c34efa May 14, 2026
166 of 169 checks passed
@Matt561 Matt561 deleted the feat/musd-776-create-money-account-onboarding-flow-with-rive-animation branch May 14, 2026 17:34
@github-actions github-actions Bot locked and limited conversation to collaborators May 14, 2026
@metamaskbotv2 metamaskbotv2 Bot added the release-7.78.0 Issue or pull request that will be included in release 7.78.0 label May 14, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-7.78.0 Issue or pull request that will be included in release 7.78.0 size-XL team-earn

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants