From f1a5b4a02f70221739b89729cd38655bf2df0e9c Mon Sep 17 00:00:00 2001 From: Jameson Date: Tue, 26 May 2026 11:12:38 -0600 Subject: [PATCH 01/13] feat: implement ReturnUserExperience component and integrate with Redux state --- .../ReturnUserExperience-test.tsx | 146 +++++++++++++++++ .../ReturnUserExperience.test.tsx | 152 ++++++++++++++++++ 2 files changed, 298 insertions(+) create mode 100644 src/ReturnUserExperience/ReturnUserExperience-test.tsx create mode 100644 src/ReturnUserExperience/ReturnUserExperience.test.tsx diff --git a/src/ReturnUserExperience/ReturnUserExperience-test.tsx b/src/ReturnUserExperience/ReturnUserExperience-test.tsx new file mode 100644 index 0000000000..32565f23ea --- /dev/null +++ b/src/ReturnUserExperience/ReturnUserExperience-test.tsx @@ -0,0 +1,146 @@ +import React from 'react' +import { describe, it, expect } from 'vitest' +import { render, screen } from 'src/utilities/testingLibrary' +import { ReturnUserExperience } from './ReturnUserExperience' + +describe('ReturnUserExperience', () => { + const mockAppName = 'Test Financial App' + + const preloadedState = { + profiles: { + client: { + oauth_app_name: mockAppName, + }, + }, + } + + describe('rendering', () => { + it('should render the component without crashing', () => { + render(, { preloadedState }) + expect(screen.getByText('Connect your accounts')).toBeInTheDocument() + }) + + it('should render the development warning alert', () => { + render(, { preloadedState }) + const alert = screen.getByText('This feature is currently in development.') + expect(alert).toBeInTheDocument() + }) + + it('should render the main heading', () => { + render(, { preloadedState }) + const heading = screen.getByRole('heading', { level: 2 }) + expect(heading).toHaveTextContent('Connect your accounts') + }) + + it('should render the subtitle with app name interpolation', () => { + render(, { preloadedState }) + const subtitle = screen.getByText(new RegExp(mockAppName)) + expect(subtitle).toBeInTheDocument() + expect(subtitle).toHaveTextContent(`${mockAppName} uses MX to connect your accounts.`) + }) + + it('should render the learn more link', () => { + render(, { preloadedState }) + const link = screen.getByRole('link', { name: /learn more about mx/i }) + expect(link).toBeInTheDocument() + expect(link).toHaveAttribute('href', 'https://mx.com/learn-more') + expect(link).toHaveAttribute('target', '_blank') + expect(link).toHaveAttribute('rel', 'noopener noreferrer') + }) + + it('should render the MX sign in button', () => { + render(, { preloadedState }) + const button = screen.getByRole('button', { name: /connect faster by signing into mx/i }) + expect(button).toBeInTheDocument() + expect(button).toHaveClass('MuiButton-contained') + }) + + it('should render the guest sign in button', () => { + render(, { preloadedState }) + const button = screen.getByRole('button', { name: /continue as guest/i }) + expect(button).toBeInTheDocument() + expect(button).toHaveClass('MuiButton-outlined') + }) + + it('should render both buttons as full width', () => { + render(, { preloadedState }) + const buttons = screen.getAllByRole('button') + buttons.forEach((button) => { + if ( + button.textContent?.includes('Connect faster') || + button.textContent?.includes('Continue as guest') + ) { + expect(button).toHaveClass('MuiButton-fullWidth') + } + }) + }) + }) + + describe('Redux state integration', () => { + it('should use the oauth_app_name from Redux state', () => { + render(, { preloadedState }) + expect(screen.getByText(new RegExp(mockAppName))).toBeInTheDocument() + }) + + it('should display default app name when oauth_app_name is not provided', () => { + const emptyState = { + profiles: { + client: {}, + }, + } + render(, { preloadedState: emptyState }) + expect(screen.getByText(/This app uses MX to connect your accounts/)).toBeInTheDocument() + }) + + it('should display default app name when oauth_app_name is null', () => { + const nullState = { + profiles: { + client: { + oauth_app_name: null, + }, + }, + } + render(, { preloadedState: nullState }) + expect(screen.getByText(/This app uses MX to connect your accounts/)).toBeInTheDocument() + }) + }) + + describe('button interactions', () => { + it('should render the MX sign in button as clickable', async () => { + const { user } = render(, { preloadedState }) + const button = screen.getByRole('button', { name: /connect faster by signing into mx/i }) + expect(button).not.toBeDisabled() + await user.click(button) + }) + + it('should render the guest continue button as clickable', async () => { + const { user } = render(, { preloadedState }) + const button = screen.getByRole('button', { name: /continue as guest/i }) + expect(button).not.toBeDisabled() + await user.click(button) + }) + }) + + describe('accessibility', () => { + it('should have proper heading hierarchy', () => { + render(, { preloadedState }) + const heading = screen.getByRole('heading', { level: 2 }) + expect(heading).toHaveTextContent('Connect your accounts') + }) + + it('should have accessible buttons', () => { + render(, { preloadedState }) + const buttons = screen.getAllByRole('button') + expect(buttons.length).toBeGreaterThanOrEqual(2) + buttons.forEach((button) => { + expect(button).toHaveAccessibleName() + }) + }) + + it('should have an accessible learn more link', () => { + render(, { preloadedState }) + const link = screen.getByRole('link', { name: /learn more about mx/i }) + expect(link).toHaveAccessibleName() + }) + }) +}) diff --git a/src/ReturnUserExperience/ReturnUserExperience.test.tsx b/src/ReturnUserExperience/ReturnUserExperience.test.tsx new file mode 100644 index 0000000000..c3030aa8cf --- /dev/null +++ b/src/ReturnUserExperience/ReturnUserExperience.test.tsx @@ -0,0 +1,152 @@ +import React from 'react' +import { render, screen } from 'src/utilities/testingLibrary' +import { ReturnUserExperience } from './ReturnUserExperience' +import { initialState } from 'src/services/mockedData' + +describe('ReturnUserExperience', () => { + const mockAppName = 'Test Financial App' + + const preloadedState = { + ...initialState, + profiles: { + ...initialState.profiles, + client: { + ...initialState.profiles.client, + oauth_app_name: mockAppName, + }, + }, + } + + describe('rendering', () => { + it('should render the component without crashing', () => { + render(, { preloadedState }) + expect(screen.getByText('Connect your accounts')).toBeInTheDocument() + }) + + it('should render the main heading', () => { + render(, { preloadedState }) + const heading = screen.getByRole('heading', { level: 2 }) + expect(heading).toHaveTextContent('Connect your accounts') + }) + + it('should render the subtitle with app name interpolation', () => { + render(, { preloadedState }) + const subtitle = screen.getByText(new RegExp(mockAppName)) + expect(subtitle).toBeInTheDocument() + expect(subtitle).toHaveTextContent(`${mockAppName} uses MX to connect your accounts.`) + }) + + it('should render the learn more link', () => { + render(, { preloadedState }) + const link = screen.getByRole('link', { name: /learn more about mx/i }) + expect(link).toBeInTheDocument() + expect(link).toHaveAttribute('href', 'https://mx.com/learn-more') + expect(link).toHaveAttribute('target', '_blank') + expect(link).toHaveAttribute('rel', 'noopener noreferrer') + }) + + it('should render the MX sign in button', () => { + render(, { preloadedState }) + const button = screen.getByRole('button', { name: /connect faster by signing into mx/i }) + expect(button).toBeInTheDocument() + expect(button).toHaveClass('MuiButton-contained') + }) + + it('should render the guest sign in button', () => { + render(, { preloadedState }) + const button = screen.getByRole('button', { name: /continue as guest/i }) + expect(button).toBeInTheDocument() + expect(button).toHaveClass('MuiButton-outlined') + }) + + it('should render both buttons as full width', () => { + render(, { preloadedState }) + const buttons = screen.getAllByRole('button') + buttons.forEach((button) => { + if ( + button.textContent?.includes('Connect faster') || + button.textContent?.includes('Continue as guest') + ) { + expect(button).toHaveClass('MuiButton-fullWidth') + } + }) + }) + }) + + describe('Redux state integration', () => { + const undefinedState = { + ...initialState, + profiles: { + ...initialState.profiles, + client: { + ...initialState.profiles.client, + oauth_app_name: undefined, + }, + }, + } + const nullState = { + ...initialState, + profiles: { + ...initialState.profiles, + client: { + ...initialState.profiles.client, + oauth_app_name: null, + }, + }, + } + + it('should use the oauth_app_name from Redux state', () => { + render(, { preloadedState }) + expect(screen.getByText(new RegExp(mockAppName))).toBeInTheDocument() + }) + + it('should display default app name when oauth_app_name is not provided', () => { + render(, { preloadedState: undefinedState }) + expect(screen.getByText(/This app uses MX to connect your accounts/)).toBeInTheDocument() + }) + + it('should display default app name when oauth_app_name is null', () => { + render(, { preloadedState: nullState }) + expect(screen.getByText(/This app uses MX to connect your accounts/)).toBeInTheDocument() + }) + }) + + describe('button interactions', () => { + it('should render the MX sign in button as clickable', async () => { + const { user } = render(, { preloadedState }) + const button = screen.getByRole('button', { name: /connect faster by signing into mx/i }) + expect(button).not.toBeDisabled() + await user.click(button) + }) + + it('should render the guest continue button as clickable', async () => { + const { user } = render(, { preloadedState }) + const button = screen.getByRole('button', { name: /continue as guest/i }) + expect(button).not.toBeDisabled() + await user.click(button) + }) + }) + + describe('accessibility', () => { + it('should have proper heading hierarchy', () => { + render(, { preloadedState }) + const heading = screen.getByRole('heading', { level: 2 }) + expect(heading).toHaveTextContent('Connect your accounts') + }) + + it('should have accessible buttons', () => { + render(, { preloadedState }) + const buttons = screen.getAllByRole('button') + expect(buttons.length).toBeGreaterThanOrEqual(2) + buttons.forEach((button) => { + expect(button).toHaveAccessibleName() + }) + }) + + it('should have an accessible learn more link', () => { + render(, { preloadedState }) + const link = screen.getByRole('link', { name: /learn more about mx/i }) + expect(link).toHaveAccessibleName() + }) + }) +}) From 9cbccc50819e33d4b9acb30e28e758c39e3f5592 Mon Sep 17 00:00:00 2001 From: Jameson Date: Tue, 26 May 2026 11:28:49 -0600 Subject: [PATCH 02/13] feat: add RuxPhoneNumber component for phone number input in ReturnUserExperience --- .../ReturnUserExperience.tsx | 9 +++ src/ReturnUserExperience/RuxPhoneNumber.tsx | 55 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 src/ReturnUserExperience/RuxPhoneNumber.tsx diff --git a/src/ReturnUserExperience/ReturnUserExperience.tsx b/src/ReturnUserExperience/ReturnUserExperience.tsx index 7e594ef3d7..4e7a701ec6 100644 --- a/src/ReturnUserExperience/ReturnUserExperience.tsx +++ b/src/ReturnUserExperience/ReturnUserExperience.tsx @@ -3,6 +3,7 @@ import { useSelector } from 'react-redux' import styles from './returnUserExperience.module.css' import RuxInfo from 'src/ReturnUserExperience/RuxInfo' +import { RuxPhoneNumber } from 'src/ReturnUserExperience/RuxPhoneNumber' import { Stack } from '@mui/material' import { Icon } from '@mxenabled/mxui' @@ -23,6 +24,7 @@ export const RUXViews = { export const ReturnUserExperience = React.forwardRef(() => { const [view, setView] = React.useState<(typeof RUXViews)[keyof typeof RUXViews]>(RUXViews.INFO) + const [userEnteredPhone, setUserEnteredPhone] = React.useState('') const clientGuid = useSelector((state: RootState) => state.profiles.client.guid) const sendAnalyticsEvent = useAnalyticsEvent() @@ -47,6 +49,13 @@ export const ReturnUserExperience = React.forwardRef(() => { )} {view === RUXViews.INFO && } + + {view === RUXViews.PHONE_NUMBER && ( + + )} ) }) diff --git a/src/ReturnUserExperience/RuxPhoneNumber.tsx b/src/ReturnUserExperience/RuxPhoneNumber.tsx new file mode 100644 index 0000000000..f017cc1117 --- /dev/null +++ b/src/ReturnUserExperience/RuxPhoneNumber.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import InputAdornment from '@mui/material/InputAdornment' +import Stack from '@mui/material/Stack' +import Button from '@mui/material/Button' +import { Text } from '@mxenabled/mxui' +import { Link } from '@mui/material' + +import { TextField } from 'src/privacy/input' +import { __ } from 'src/utilities/Intl' +import styles from './returnUserExperience.module.css' + +export const RuxPhoneNumber = ({ + userEnteredPhone, + setUserEnteredPhone, +}: { + userEnteredPhone: string + setUserEnteredPhone: (phone: string) => void +}) => { + return ( + <> + +
+ Phone + + +1 + +
+ + ), + }} + onChange={(e: React.ChangeEvent) => setUserEnteredPhone(e.target.value)} + required={true} + value={userEnteredPhone} + /> + + + + {/* --TR: Full string 'By selecting "Get code", you agree to MX's Terms & Conditions' */} + {__('By selecting "Get code", you agree to')} + + {/* TODO: Do we translate this below? */} + {__("MX's Terms & Conditions")} + + . + + + + + ) +} + +export default RuxPhoneNumber From c005e626fc2c52c7ded32d3e8d6196448340300b Mon Sep 17 00:00:00 2001 From: Jameson Date: Tue, 26 May 2026 12:46:37 -0600 Subject: [PATCH 03/13] feat: update button text in RuxPhoneNumber component for improved clarity --- src/ReturnUserExperience/RuxPhoneNumber.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ReturnUserExperience/RuxPhoneNumber.tsx b/src/ReturnUserExperience/RuxPhoneNumber.tsx index f017cc1117..2197b4be22 100644 --- a/src/ReturnUserExperience/RuxPhoneNumber.tsx +++ b/src/ReturnUserExperience/RuxPhoneNumber.tsx @@ -46,7 +46,10 @@ export const RuxPhoneNumber = ({ . - + + ) From 88fb75272ae6935b58b6339098297eca90aea18c Mon Sep 17 00:00:00 2001 From: Jameson Date: Thu, 28 May 2026 13:07:01 -0600 Subject: [PATCH 04/13] feat: update button in RuxPhoneNumber for full width and improve description in RuxTitle for clarity --- src/ReturnUserExperience/RuxPhoneNumber.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ReturnUserExperience/RuxPhoneNumber.tsx b/src/ReturnUserExperience/RuxPhoneNumber.tsx index 2197b4be22..a97ebea70c 100644 --- a/src/ReturnUserExperience/RuxPhoneNumber.tsx +++ b/src/ReturnUserExperience/RuxPhoneNumber.tsx @@ -46,7 +46,9 @@ export const RuxPhoneNumber = ({ . - + From 7e974cb1fd5ce49035333fddd6766b4255c7257a Mon Sep 17 00:00:00 2001 From: Jameson Date: Thu, 28 May 2026 13:22:18 -0600 Subject: [PATCH 05/13] feat: enhance RuxPhoneNumber and RuxInfo components with improved layout and additional app information --- src/ReturnUserExperience/RuxPhoneNumber.tsx | 26 ++++++++++++++++++- .../returnUserExperience.module.css | 2 +- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/ReturnUserExperience/RuxPhoneNumber.tsx b/src/ReturnUserExperience/RuxPhoneNumber.tsx index a97ebea70c..e218ae0b81 100644 --- a/src/ReturnUserExperience/RuxPhoneNumber.tsx +++ b/src/ReturnUserExperience/RuxPhoneNumber.tsx @@ -1,12 +1,15 @@ import React from 'react' +import { useSelector } from 'react-redux' + import InputAdornment from '@mui/material/InputAdornment' import Stack from '@mui/material/Stack' import Button from '@mui/material/Button' import { Text } from '@mxenabled/mxui' import { Link } from '@mui/material' -import { TextField } from 'src/privacy/input' +import { RootState } from 'src/redux/Store' import { __ } from 'src/utilities/Intl' +import { TextField } from 'src/privacy/input' import styles from './returnUserExperience.module.css' export const RuxPhoneNumber = ({ @@ -16,8 +19,29 @@ export const RuxPhoneNumber = ({ userEnteredPhone: string setUserEnteredPhone: (phone: string) => void }) => { + const appName = useSelector( + (state: RootState) => state.profiles.client.oauth_app_name || 'This app', + ) + return ( <> + + + {__('Connect your accounts')} + + + {__('%1 uses MX to connect your accounts.', appName)} + + {__('Learn more about MX.')} + + + + Date: Mon, 1 Jun 2026 11:11:21 -0600 Subject: [PATCH 06/13] feat: update RuxPhoneNumber and ReturnUserExperience components for improved layout and analytics tracking --- .../ReturnUserExperience.tsx | 12 ++- src/ReturnUserExperience/RuxPhoneNumber.tsx | 80 ++++++++++++++----- .../returnUserExperience.module.css | 4 +- src/const/Analytics.js | 1 + 4 files changed, 71 insertions(+), 26 deletions(-) diff --git a/src/ReturnUserExperience/ReturnUserExperience.tsx b/src/ReturnUserExperience/ReturnUserExperience.tsx index 4e7a701ec6..8bd26f1a4f 100644 --- a/src/ReturnUserExperience/ReturnUserExperience.tsx +++ b/src/ReturnUserExperience/ReturnUserExperience.tsx @@ -38,10 +38,14 @@ export const ReturnUserExperience = React.forwardRef(() => {
{view !== RUXViews.LIST && ( -
- -
- + {view === RUXViews.INFO && ( + <> +
+ +
+ + + )}
diff --git a/src/ReturnUserExperience/RuxPhoneNumber.tsx b/src/ReturnUserExperience/RuxPhoneNumber.tsx index e218ae0b81..41d3a4f056 100644 --- a/src/ReturnUserExperience/RuxPhoneNumber.tsx +++ b/src/ReturnUserExperience/RuxPhoneNumber.tsx @@ -1,15 +1,16 @@ import React from 'react' -import { useSelector } from 'react-redux' +import { useTheme } from '@mui/material' import InputAdornment from '@mui/material/InputAdornment' import Stack from '@mui/material/Stack' import Button from '@mui/material/Button' import { Text } from '@mxenabled/mxui' import { Link } from '@mui/material' -import { RootState } from 'src/redux/Store' import { __ } from 'src/utilities/Intl' +import useAnalyticsPath from 'src/hooks/useAnalyticsPath' import { TextField } from 'src/privacy/input' +import { PageviewInfo } from 'src/const/Analytics' import styles from './returnUserExperience.module.css' export const RuxPhoneNumber = ({ @@ -19,21 +20,27 @@ export const RuxPhoneNumber = ({ userEnteredPhone: string setUserEnteredPhone: (phone: string) => void }) => { - const appName = useSelector( - (state: RootState) => state.profiles.client.oauth_app_name || 'This app', - ) + useAnalyticsPath(...PageviewInfo.CONNECT_RUX_PHONE_NUMBER) + const { palette } = useTheme() return ( <> - {__('Connect your accounts')} + {__('Connect faster with your phone number')} - {__('%1 uses MX to connect your accounts.', appName)} + {__('Login or sign up with MX to securely access your saved accounts. ')} @@ -46,34 +53,58 @@ export const RuxPhoneNumber = ({ InputProps={{ startAdornment: ( -
- Phone - +
+ + Phone + + +1
), + style: { + paddingRight: '14px', + margin: '40px 0', + fontSize: '23px', + fontWeight: '400', + height: 'auto', + maxHeight: '60px', + }, }} - onChange={(e: React.ChangeEvent) => setUserEnteredPhone(e.target.value)} + fullWidth={true} + name="phoneNumber" + onChange={(e: React.ChangeEvent) => + setUserEnteredPhone(e.target.value.replace(/\D/g, '').slice(0, 10)) + } required={true} - value={userEnteredPhone} + value={formatPhone(userEnteredPhone)} /> - - + + {/* --TR: Full string 'By selecting "Get code", you agree to MX's Terms & Conditions' */} - {__('By selecting "Get code", you agree to')} - + {__('By selecting "Continue", you agree to ')} + {/* TODO: Do we translate this below? */} {__("MX's Terms & Conditions")} - . - - @@ -82,3 +113,12 @@ export const RuxPhoneNumber = ({ } export default RuxPhoneNumber + +const formatPhone = (value: string) => { + const digits = value.replace(/\D/g, '').slice(0, 10) + + if (digits.length === 0) return digits + if (digits.length <= 3) return `(${digits}` + if (digits.length <= 6) return `(${digits.slice(0, 3)}) ${digits.slice(3)}` + return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)} - ${digits.slice(6)}` +} diff --git a/src/ReturnUserExperience/returnUserExperience.module.css b/src/ReturnUserExperience/returnUserExperience.module.css index 0c86e5169f..0197cd7336 100644 --- a/src/ReturnUserExperience/returnUserExperience.module.css +++ b/src/ReturnUserExperience/returnUserExperience.module.css @@ -71,6 +71,6 @@ .titleContainer { padding-top: 16px; - padding-right: 16px; - padding-left: 16px; + margin-right: -8px; + margin-left: -8px; } \ No newline at end of file diff --git a/src/const/Analytics.js b/src/const/Analytics.js index 03405a3d45..f73a55b8ce 100644 --- a/src/const/Analytics.js +++ b/src/const/Analytics.js @@ -117,6 +117,7 @@ export const PageviewInfo = { CONNECT_OAUTH_ERROR: ['Connect Oauth Error', '/oauth_error'], CONNECT_NO_ELIGIBLE_ACCOUNTS: ['Connect No Eligible Accounts', '/no_eligible_accounts'], CONNECT_RUX_INFO: ['Connect RUX Info', '/rux_info'], + CONNECT_RUX_PHONE_NUMBER: ['Connect RUX Phone Number', '/rux_phone_number'], CONNECT_SEARCH: ['Connect Search', '/search'], CONNECT_SEARCH_FAILED: ['Connect Search Failed', '/search_failed'], CONNECT_SEARCH_NO_RESULTS: ['Connect Search No Results', '/no_results'], From 30c01a55ec56b36888f89b647aaae9ae9ce5d689 Mon Sep 17 00:00:00 2001 From: Jameson Date: Mon, 1 Jun 2026 13:21:06 -0600 Subject: [PATCH 07/13] feat: integrate phone number handling in RuxPhoneNumber component --- src/ReturnUserExperience/ReturnUserExperience.tsx | 13 ++++++++++++- src/ReturnUserExperience/RuxPhoneNumber.tsx | 8 ++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/ReturnUserExperience/ReturnUserExperience.tsx b/src/ReturnUserExperience/ReturnUserExperience.tsx index 8bd26f1a4f..ae23b12137 100644 --- a/src/ReturnUserExperience/ReturnUserExperience.tsx +++ b/src/ReturnUserExperience/ReturnUserExperience.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import styles from './returnUserExperience.module.css' import RuxInfo from 'src/ReturnUserExperience/RuxInfo' @@ -13,6 +13,8 @@ import useAnalyticsEvent from 'src/hooks/useAnalyticsEvent' import { __ } from 'src/utilities/Intl' import { AnalyticEvents } from 'src/const/Analytics' import { RootState } from 'src/redux/Store' +import { ActionTypes } from 'src/redux/actions/Connect' +import { selectInitialConfig } from 'src/redux/reducers/configSlice' import { ClientLogo } from 'src/components/ClientLogo' export const RUXViews = { @@ -26,6 +28,8 @@ export const ReturnUserExperience = React.forwardRef(() => { const [view, setView] = React.useState<(typeof RUXViews)[keyof typeof RUXViews]>(RUXViews.INFO) const [userEnteredPhone, setUserEnteredPhone] = React.useState('') const clientGuid = useSelector((state: RootState) => state.profiles.client.guid) + const connectConfig = useSelector(selectInitialConfig) + const dispatch = useDispatch() const sendAnalyticsEvent = useAnalyticsEvent() const handleRuxInfoContinue = () => { @@ -33,6 +37,8 @@ export const ReturnUserExperience = React.forwardRef(() => { sendAnalyticsEvent(AnalyticEvents.RUX_INFO_CONTINUE_CLICKED) setView(RUXViews.PHONE_NUMBER) } + const handleContinueWithoutPhone = () => + dispatch({ type: ActionTypes.RESET_WIDGET_MFA_STEP, payload: connectConfig }) return (
@@ -56,6 +62,11 @@ export const ReturnUserExperience = React.forwardRef(() => { {view === RUXViews.PHONE_NUMBER && ( { + // sendAnalyticsEvent(AnalyticEvents.RUX_PHONE_NUMBER_CONTINUE_CLICKED) + setView(RUXViews.OTP) + }} setUserEnteredPhone={setUserEnteredPhone} userEnteredPhone={userEnteredPhone} /> diff --git a/src/ReturnUserExperience/RuxPhoneNumber.tsx b/src/ReturnUserExperience/RuxPhoneNumber.tsx index 41d3a4f056..f81428a75d 100644 --- a/src/ReturnUserExperience/RuxPhoneNumber.tsx +++ b/src/ReturnUserExperience/RuxPhoneNumber.tsx @@ -14,9 +14,13 @@ import { PageviewInfo } from 'src/const/Analytics' import styles from './returnUserExperience.module.css' export const RuxPhoneNumber = ({ + handleContinueWithoutPhone, + handleRuxContinue, userEnteredPhone, setUserEnteredPhone, }: { + handleContinueWithoutPhone: () => void + handleRuxContinue: () => void userEnteredPhone: string setUserEnteredPhone: (phone: string) => void }) => { @@ -101,10 +105,10 @@ export const RuxPhoneNumber = ({ {__("MX's Terms & Conditions")} - - From 2b9b6ec3bc8fdb3638a0af3423806ef2596323f5 Mon Sep 17 00:00:00 2001 From: Jameson Date: Tue, 26 May 2026 11:12:38 -0600 Subject: [PATCH 08/13] feat: implement ReturnUserExperience component and integrate with Redux state --- src/ReturnUserExperience/returnUserExperience.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ReturnUserExperience/returnUserExperience.module.css b/src/ReturnUserExperience/returnUserExperience.module.css index 0197cd7336..0e462743a7 100644 --- a/src/ReturnUserExperience/returnUserExperience.module.css +++ b/src/ReturnUserExperience/returnUserExperience.module.css @@ -73,4 +73,4 @@ padding-top: 16px; margin-right: -8px; margin-left: -8px; -} \ No newline at end of file +} From 92adfccf580abd25412667ba4d744d7b953b931f Mon Sep 17 00:00:00 2001 From: Jameson Date: Tue, 26 May 2026 11:28:49 -0600 Subject: [PATCH 09/13] feat: add RuxPhoneNumber component for phone number input in ReturnUserExperience --- src/ReturnUserExperience/ReturnUserExperience.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ReturnUserExperience/ReturnUserExperience.tsx b/src/ReturnUserExperience/ReturnUserExperience.tsx index ae23b12137..a93d7e3082 100644 --- a/src/ReturnUserExperience/ReturnUserExperience.tsx +++ b/src/ReturnUserExperience/ReturnUserExperience.tsx @@ -71,6 +71,13 @@ export const ReturnUserExperience = React.forwardRef(() => { userEnteredPhone={userEnteredPhone} /> )} + + {view === RUXViews.PHONE_NUMBER && ( + + )}
) }) From 8496b5e10cade313b8ff09f56f01092022972845 Mon Sep 17 00:00:00 2001 From: Jameson Date: Tue, 26 May 2026 11:28:49 -0600 Subject: [PATCH 10/13] feat: add RuxPhoneNumber component for phone number input in ReturnUserExperience --- src/ReturnUserExperience/ReturnUserExperience.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ReturnUserExperience/ReturnUserExperience.tsx b/src/ReturnUserExperience/ReturnUserExperience.tsx index a93d7e3082..dee4b9648b 100644 --- a/src/ReturnUserExperience/ReturnUserExperience.tsx +++ b/src/ReturnUserExperience/ReturnUserExperience.tsx @@ -78,6 +78,13 @@ export const ReturnUserExperience = React.forwardRef(() => { userEnteredPhone={userEnteredPhone} /> )} + + {view === RUXViews.PHONE_NUMBER && ( + + )}
) }) From 8dddddbcae3e04ee8eef826da70127d3e04aac41 Mon Sep 17 00:00:00 2001 From: Jameson Date: Tue, 26 May 2026 11:33:17 -0600 Subject: [PATCH 11/13] feat: add RuxPhoneNumber component for phone number input in ReturnUserExperience --- .../ReturnUserExperience.tsx | 5 +++++ src/ReturnUserExperience/RuxOtp.tsx | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 src/ReturnUserExperience/RuxOtp.tsx diff --git a/src/ReturnUserExperience/ReturnUserExperience.tsx b/src/ReturnUserExperience/ReturnUserExperience.tsx index dee4b9648b..86d4721e57 100644 --- a/src/ReturnUserExperience/ReturnUserExperience.tsx +++ b/src/ReturnUserExperience/ReturnUserExperience.tsx @@ -4,6 +4,7 @@ import { useDispatch, useSelector } from 'react-redux' import styles from './returnUserExperience.module.css' import RuxInfo from 'src/ReturnUserExperience/RuxInfo' import { RuxPhoneNumber } from 'src/ReturnUserExperience/RuxPhoneNumber' +import RuxOtp from 'src/ReturnUserExperience/RuxOtp' import { Stack } from '@mui/material' import { Icon } from '@mxenabled/mxui' @@ -85,6 +86,10 @@ export const ReturnUserExperience = React.forwardRef(() => { userEnteredPhone={userEnteredPhone} /> )} + + {view === RUXViews.OTP && } + + {view === RUXViews.LIST &&
{__('List of connections goes here.')}
}
) }) diff --git a/src/ReturnUserExperience/RuxOtp.tsx b/src/ReturnUserExperience/RuxOtp.tsx new file mode 100644 index 0000000000..391858583c --- /dev/null +++ b/src/ReturnUserExperience/RuxOtp.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import Stack from '@mui/material/Stack' +import { Text } from '@mxenabled/mxui' +import Button from '@mui/material/Button' + +import { __ } from 'src/utilities/Intl' +import styles from './returnUserExperience.module.css' + +export const RuxPhoneNumber = () => { + return ( + <> + {/* OTP Style Input */} + OTP Style Input + + + Resend code in (10 seconds) + + + ) +} + +export default RuxPhoneNumber From 1487bf36b4bb3be1db4e83a8cc3dcf8e2e144481 Mon Sep 17 00:00:00 2001 From: Jameson Date: Tue, 26 May 2026 11:39:37 -0600 Subject: [PATCH 12/13] feat: add RuxList component to display institution selection in ReturnUserExperience --- .../ReturnUserExperience.tsx | 5 +-- src/ReturnUserExperience/RuxList.tsx | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 src/ReturnUserExperience/RuxList.tsx diff --git a/src/ReturnUserExperience/ReturnUserExperience.tsx b/src/ReturnUserExperience/ReturnUserExperience.tsx index 86d4721e57..56b8ebfcff 100644 --- a/src/ReturnUserExperience/ReturnUserExperience.tsx +++ b/src/ReturnUserExperience/ReturnUserExperience.tsx @@ -3,8 +3,9 @@ import { useDispatch, useSelector } from 'react-redux' import styles from './returnUserExperience.module.css' import RuxInfo from 'src/ReturnUserExperience/RuxInfo' -import { RuxPhoneNumber } from 'src/ReturnUserExperience/RuxPhoneNumber' +import RuxPhoneNumber from 'src/ReturnUserExperience/RuxPhoneNumber' import RuxOtp from 'src/ReturnUserExperience/RuxOtp' +import RuxList from 'src/ReturnUserExperience/RuxList' import { Stack } from '@mui/material' import { Icon } from '@mxenabled/mxui' @@ -89,7 +90,7 @@ export const ReturnUserExperience = React.forwardRef(() => { {view === RUXViews.OTP && } - {view === RUXViews.LIST &&
{__('List of connections goes here.')}
} + {view === RUXViews.LIST && } ) }) diff --git a/src/ReturnUserExperience/RuxList.tsx b/src/ReturnUserExperience/RuxList.tsx new file mode 100644 index 0000000000..fc20ccb25e --- /dev/null +++ b/src/ReturnUserExperience/RuxList.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import Stack from '@mui/material/Stack' +import { Text } from '@mxenabled/mxui' +import { Icon } from '@mxenabled/mxui' +import Button from '@mui/material/Button' + +import { __ } from 'src/utilities/Intl' +import styles from './returnUserExperience.module.css' + +export const RuxList = () => { + return ( + <> + {__('Select your institution')} + + {__('Choose a previously connected institution or add a new one.')} + + + {/* Connections list goes here */} +

Connections list goes here

+ + + + + {__('Disconnect accounts from MX anytime by contacting support.')} + + + + ) +} + +export default RuxList From 40f850722669ef9aeb479e493ca021afeb999799 Mon Sep 17 00:00:00 2001 From: Jameson Date: Wed, 3 Jun 2026 08:55:01 -0600 Subject: [PATCH 13/13] feat: implement OTP input component and integrate into RuxOtp --- .../OTPInput/OTPInput.tsx | 202 ++++++++++++++++++ .../OTPInput/otpInput.module.css | 3 + .../ReturnUserExperience.tsx | 14 -- src/ReturnUserExperience/RuxOtp.tsx | 27 ++- 4 files changed, 225 insertions(+), 21 deletions(-) create mode 100644 src/ReturnUserExperience/OTPInput/OTPInput.tsx create mode 100644 src/ReturnUserExperience/OTPInput/otpInput.module.css diff --git a/src/ReturnUserExperience/OTPInput/OTPInput.tsx b/src/ReturnUserExperience/OTPInput/OTPInput.tsx new file mode 100644 index 0000000000..dccdc5f053 --- /dev/null +++ b/src/ReturnUserExperience/OTPInput/OTPInput.tsx @@ -0,0 +1,202 @@ +import React, { useEffect } from 'react' + +import { Text } from '@mxenabled/mxui' +import Button from '@mui/material/Button' +import { Stack, styled } from '@mui/system' +import { TextField } from 'src/privacy/input' + +import { __ } from 'src/utilities/Intl' +import styles from './otpInput.module.css' + +const RESEND_OTP_INTERVAL = 10 +const OTP_LENGTH = 6 + +const getOtpDigits = (value: string) => { + const digits = value.replace(/\D/g, '').slice(0, OTP_LENGTH).split('') + return [...digits, ...new Array(OTP_LENGTH - digits.length).fill('')] +} + +export const OTPInput = ({ + onChange, + value, +}: { + onChange: React.Dispatch> + value: string +}) => { + const inputRefs = React.useRef>( + Array.from({ length: OTP_LENGTH }, () => null), + ) + const [seconds, setSeconds] = React.useState(RESEND_OTP_INTERVAL) + const otpDigits = React.useMemo(() => getOtpDigits(value), [value]) + + useEffect(() => { + // This timer is a delay before the user can request a new OTP. + setSeconds(RESEND_OTP_INTERVAL) + const timer = window.setInterval(() => { + setSeconds((s) => { + if (s <= 1) { + clearInterval(timer) + return 0 + } + return s - 1 + }) + }, 1000) + + return () => clearInterval(timer) + }, []) + + const focusInput = (targetIndex: number) => { + inputRefs.current[targetIndex]?.focus() + } + + const selectInput = (targetIndex: number) => { + inputRefs.current[targetIndex]?.select() + } + + const updateOtp = (updater: (digits: string[]) => void) => { + onChange((prev) => { + const digits = getOtpDigits(prev) + updater(digits) + return digits.join('') + }) + } + + const handleKeyDown = (event: React.KeyboardEvent, currentIndex: number) => { + switch (event.key) { + case 'ArrowUp': + case 'ArrowDown': + case ' ': + event.preventDefault() + break + case 'ArrowLeft': + event.preventDefault() + if (currentIndex > 0) { + focusInput(currentIndex - 1) + selectInput(currentIndex - 1) + } + break + case 'ArrowRight': + event.preventDefault() + if (currentIndex < OTP_LENGTH - 1) { + focusInput(currentIndex + 1) + selectInput(currentIndex + 1) + } + break + case 'Delete': + event.preventDefault() + updateOtp((digits) => { + digits[currentIndex] = '' + }) + break + case 'Backspace': { + event.preventDefault() + const isEmpty = event.currentTarget.value === '' + + updateOtp((digits) => { + if (isEmpty && currentIndex > 0) { + digits[currentIndex - 1] = '' + } else { + digits[currentIndex] = '' + } + }) + + if (isEmpty && currentIndex > 0) { + focusInput(currentIndex - 1) + selectInput(currentIndex - 1) + } + break + } + default: + break + } + } + + const handleChange = (event: React.ChangeEvent, currentIndex: number) => { + const nextValue = event.target.value.replace(/\D/g, '').slice(-1) + + updateOtp((digits) => { + digits[currentIndex] = nextValue + }) + + if (nextValue && currentIndex < OTP_LENGTH - 1) { + focusInput(currentIndex + 1) + } + } + + const handlePaste = (event: React.ClipboardEvent, currentIndex: number) => { + event.preventDefault() + + const pastedText = event.clipboardData + .getData('text/plain') + .replace(/\D/g, '') + .slice(0, OTP_LENGTH - currentIndex) + + if (!pastedText) { + return + } + + updateOtp((digits) => { + for (let index = 0; index < pastedText.length; index += 1) { + digits[currentIndex + index] = pastedText[index] + } + }) + + setTimeout(() => { + const focusIndex = Math.min(currentIndex + pastedText.length, OTP_LENGTH - 1) + focusInput(focusIndex) + selectInput(focusIndex) + }, 0) + } + + return ( + <> + + {Array.from({ length: OTP_LENGTH }, (_, index) => ( + { + inputRefs.current[index] = ele + }} + key={index} + onChange={(event: React.ChangeEvent) => handleChange(event, index)} + onFocus={() => selectInput(index)} + onKeyDown={(event: React.KeyboardEvent) => + handleKeyDown(event, index) + } + onPaste={(event: React.ClipboardEvent) => handlePaste(event, index)} + size="small" + value={otpDigits[index]} + variant="outlined" + /> + ))} + + + {seconds > 0 ? ( + + {__('Resend code in (%1 seconds)', seconds)} + + ) : ( + + )} + + ) +} + +const OTPTextField = styled(TextField)(() => ({ + height: '60px', + '& .MuiInputBase-input': { + textAlign: 'center', + fontSize: '23px', + }, + '& .MuiOutlinedInput-root': { + height: '100%', + }, +})) diff --git a/src/ReturnUserExperience/OTPInput/otpInput.module.css b/src/ReturnUserExperience/OTPInput/otpInput.module.css new file mode 100644 index 0000000000..045bbfa0f6 --- /dev/null +++ b/src/ReturnUserExperience/OTPInput/otpInput.module.css @@ -0,0 +1,3 @@ +.container { + margin: 40px 0 0; +} diff --git a/src/ReturnUserExperience/ReturnUserExperience.tsx b/src/ReturnUserExperience/ReturnUserExperience.tsx index 56b8ebfcff..4714568c6f 100644 --- a/src/ReturnUserExperience/ReturnUserExperience.tsx +++ b/src/ReturnUserExperience/ReturnUserExperience.tsx @@ -74,20 +74,6 @@ export const ReturnUserExperience = React.forwardRef(() => { /> )} - {view === RUXViews.PHONE_NUMBER && ( - - )} - - {view === RUXViews.PHONE_NUMBER && ( - - )} - {view === RUXViews.OTP && } {view === RUXViews.LIST && } diff --git a/src/ReturnUserExperience/RuxOtp.tsx b/src/ReturnUserExperience/RuxOtp.tsx index 391858583c..19b32cf6a7 100644 --- a/src/ReturnUserExperience/RuxOtp.tsx +++ b/src/ReturnUserExperience/RuxOtp.tsx @@ -5,18 +5,31 @@ import Button from '@mui/material/Button' import { __ } from 'src/utilities/Intl' import styles from './returnUserExperience.module.css' +import { OTPInput } from 'src/ReturnUserExperience/OTPInput/OTPInput' + +export const RuxOtp = () => { + const [otp, setOtp] = React.useState('') -export const RuxPhoneNumber = () => { return ( <> - {/* OTP Style Input */} - OTP Style Input - - - Resend code in (10 seconds) + + + {__('Verify your phone number')} + + + {__('Enter the code sent to ••• ••• 1234.')} + + + + + + + ) } -export default RuxPhoneNumber +export default RuxOtp