From ab5ab51b20d69071cb629195ea4ca1e313d5fe89 Mon Sep 17 00:00:00 2001 From: Jameson Date: Tue, 19 May 2026 08:31:45 -0600 Subject: [PATCH 01/20] feat: add CONNECT_RUX feature and related authentication methods --- docs/APIDOCUMENTATION.md | 42 +++++++++++++++++++ docs/USER_FEATURES.md | 1 + src/const/Connect.js | 1 + src/const/UserFeatures.js | 1 + src/hooks/useLoadConnect.tsx | 3 ++ .../__tests__/userFeaturesSlice-test.js | 25 +++++++++++ src/redux/reducers/userFeaturesSlice.ts | 6 ++- 7 files changed, 78 insertions(+), 1 deletion(-) diff --git a/docs/APIDOCUMENTATION.md b/docs/APIDOCUMENTATION.md index e51f98fab9..ace09a490d 100644 --- a/docs/APIDOCUMENTATION.md +++ b/docs/APIDOCUMENTATION.md @@ -658,3 +658,45 @@ xee --- + +#### authPersonInitiate(phoneNumber) + +
+ Initiates authentication for a person + +##### Parameters + +> | name | type | data type | description | +> | ------------- | -------- | --------- | ---------------------------------------------- | +> | `phoneNumber` | required | string | The phone number of the person to authenticate | + +##### Responses + +> | http code | content-type | response | +> | --------- | ------------------ | ------------------------------- | +> | `200` | `application/json` | `TBD` | +> | `40#` | `application/json` | `{"response": {"status": 40#}}` | + +
+ +--- + +#### authPersonVerify(code) + +
+ Verifies authentication for a person + +##### Parameters + +> | name | type | data type | description | +> | ------ | -------- | --------- | ----------------------------------------------------- | +> | `code` | required | string | The verification code sent to the user's phone number | + +##### Responses + +> | http code | content-type | response | +> | --------- | ------------------ | ------------------------------- | +> | `200` | `application/json` | `TBD | +> | `40#` | `application/json` | `{"response": {"status": 40#}}` | + +--- diff --git a/docs/USER_FEATURES.md b/docs/USER_FEATURES.md index 9ebed82507..2ab1083e07 100644 --- a/docs/USER_FEATURES.md +++ b/docs/USER_FEATURES.md @@ -25,6 +25,7 @@ const userFeatures = [ | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `SHOW_CONNECT_GLOBAL_NAVIGATION_HEADER` | When enabled, adds a back button to the top of the widget and gets rid of any explicit back buttons |
{
 feature_name: 'SHOW_CONNECT_GLOBAL_NAVIGATION_HEADER',
 guid: 'FTR-123',
 is_enabled: true
 }
| | `CONNECT_COMBO_JOBS` | When enabled, the Connect widget will create COMBINATION jobs instead of individual jobs (aggregate, verification, reward, etc). |
{
 feature_name: 'CONNECT_COMBO_JOBS',
 guid: 'FTR-123',
 is_enabled: true
 }
| +| `CONNECT_RUX` | When enabled, the Connect widget will start by initializing the RUX authentication flow. |
{
 feature_name: 'CONNECT_RUX',
 guid: 'FTR-123',
 is_enabled: true
 }
|
diff --git a/src/const/Connect.js b/src/const/Connect.js index dc73c577e9..50dafe8a45 100644 --- a/src/const/Connect.js +++ b/src/const/Connect.js @@ -19,6 +19,7 @@ export const STEPS = { MFA: 'mfa', MICRODEPOSITS: 'microdeposits', OAUTH_ERROR: 'oauthError', + RETURNING_USER_EXPERIENCE: 'returningUserExperience', SEARCH: 'search', VERIFY_ERROR: 'verifyError', VERIFY_EXISTING_MEMBER: 'verifyExistingMember', diff --git a/src/const/UserFeatures.js b/src/const/UserFeatures.js index 0ab70725f3..e311fd6c6c 100644 --- a/src/const/UserFeatures.js +++ b/src/const/UserFeatures.js @@ -2,3 +2,4 @@ export const CONNECT_COMBO_JOBS = 'CONNECT_COMBO_JOBS' export const CONNECT_CONSENT = 'CONNECT_CONSENT' +export const CONNECT_RUX = 'CONNECT_RUX' diff --git a/src/hooks/useLoadConnect.tsx b/src/hooks/useLoadConnect.tsx index f21cc302ee..a165b8dae2 100644 --- a/src/hooks/useLoadConnect.tsx +++ b/src/hooks/useLoadConnect.tsx @@ -18,6 +18,7 @@ import { __ } from 'src/utilities/Intl' import type { RootState, AppDispatch } from 'src/redux/Store' import { instutionSupportRequestedProducts } from 'src/utilities/Institution' import { getExperimentalFeatures } from 'src/redux/reducers/experimentalFeaturesSlice' +import { isConnectRuxEnabled } from 'src/redux/reducers/userFeaturesSlice' export const getErrorResource = (err: { config: { url: string | string[] } }) => { if (err.config?.url.includes('/institutions')) { @@ -49,6 +50,7 @@ const useLoadConnect = () => { const { api } = useApi() const profiles = useSelector((state: RootState) => state.profiles) const experimentalFeatures = useSelector(getExperimentalFeatures) + const isRuxEnabled = useSelector(isConnectRuxEnabled) const clientLocale = useMemo(() => { return document.querySelector('html')?.getAttribute('lang') || 'en' }, [document.querySelector('html')?.getAttribute('lang')]) @@ -82,6 +84,7 @@ const useLoadConnect = () => { experimentalFeatures, members, widgetProfile: profiles.widgetProfile, + isRuxEnabled, ...dependencies, }), ), diff --git a/src/redux/reducers/__tests__/userFeaturesSlice-test.js b/src/redux/reducers/__tests__/userFeaturesSlice-test.js index 13856a5347..c176e4409e 100644 --- a/src/redux/reducers/__tests__/userFeaturesSlice-test.js +++ b/src/redux/reducers/__tests__/userFeaturesSlice-test.js @@ -2,6 +2,7 @@ import reducer, { loadUserFeatures, initialState, getUserFeatures, + isConnectRuxEnabled, } from 'src/redux/reducers/userFeaturesSlice' import Store from 'src/redux/Store' @@ -28,5 +29,29 @@ describe('UserFeatures slice', () => { expect(getUserFeatures(state)).toEqual(state.userFeatures.items) }) }) + + describe('isConnectRuxEnabled selector', () => { + it('should return true if the CONNECT_RUX feature is enabled', () => { + const userFeatures = [{ feature_name: 'CONNECT_RUX', is_enabled: true }] + const mockState = { + ...state, + userFeatures: { + items: userFeatures, + }, + } + expect(isConnectRuxEnabled(mockState)).toBe(true) + }) + + it('should return false if the CONNECT_RUX feature is not enabled', () => { + const userFeatures = [{ feature_name: 'CONNECT_RUX', is_enabled: false }] + const mockState = { + ...state, + userFeatures: { + items: userFeatures, + }, + } + expect(isConnectRuxEnabled(mockState)).toBe(false) + }) + }) }) }) diff --git a/src/redux/reducers/userFeaturesSlice.ts b/src/redux/reducers/userFeaturesSlice.ts index d7258bdbc8..133aa8ab16 100644 --- a/src/redux/reducers/userFeaturesSlice.ts +++ b/src/redux/reducers/userFeaturesSlice.ts @@ -1,6 +1,6 @@ import { createSlice, createSelector } from '@reduxjs/toolkit' import * as UserFeatures from 'src/utilities/UserFeatures' -import { CONNECT_COMBO_JOBS, CONNECT_CONSENT } from 'src/const/UserFeatures' +import { CONNECT_COMBO_JOBS, CONNECT_CONSENT, CONNECT_RUX } from 'src/const/UserFeatures' import { RootState } from 'src/redux/Store' type UserFeaturesSlice = { @@ -33,6 +33,10 @@ export const isConsentEnabled = createSelector(getUserFeatures, (userFeatures) = return UserFeatures.isFeatureEnabled(userFeatures, CONNECT_CONSENT) }) +export const isConnectRuxEnabled = createSelector(getUserFeatures, (userFeatures) => + UserFeatures.isFeatureEnabled(userFeatures, CONNECT_RUX), +) + export const { loadUserFeatures } = userFeaturesSlice.actions export default userFeaturesSlice.reducer From e357be30b38029b248e823534cd6b356b601f033 Mon Sep 17 00:00:00 2001 From: Jameson Date: Tue, 26 May 2026 11:12:38 -0600 Subject: [PATCH 02/20] feat: implement ReturnUserExperience component and integrate with Redux state --- .../ReturnUserExperience-test.tsx | 146 +++++++++++++++++ .../ReturnUserExperience.test.tsx | 152 ++++++++++++++++++ .../ReturnUserExperience.tsx | 60 +++++++ .../returnUserExperience.module.css | 10 ++ src/components/RenderConnectStep.js | 7 +- src/redux/reducers/Connect.js | 23 +-- 6 files changed, 387 insertions(+), 11 deletions(-) create mode 100644 src/ReturnUserExperience/ReturnUserExperience-test.tsx create mode 100644 src/ReturnUserExperience/ReturnUserExperience.test.tsx create mode 100644 src/ReturnUserExperience/ReturnUserExperience.tsx create mode 100644 src/ReturnUserExperience/returnUserExperience.module.css 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() + }) + }) +}) diff --git a/src/ReturnUserExperience/ReturnUserExperience.tsx b/src/ReturnUserExperience/ReturnUserExperience.tsx new file mode 100644 index 0000000000..416dc39cbc --- /dev/null +++ b/src/ReturnUserExperience/ReturnUserExperience.tsx @@ -0,0 +1,60 @@ +import React from 'react' +import styles from './returnUserExperience.module.css' + +import { RootState } from 'src/redux/Store' +import { useSelector } from 'react-redux' +import { __ } from 'src/utilities/Intl' + +import Alert from '@mui/material/Alert' +import Button from '@mui/material/Button' +import Link from '@mui/material/Link' +import Stack from '@mui/material/Stack' +import { Text } from '@mxenabled/mxui' +import { MXLogoCopyrightIcon } from '@mxenabled/mxui' + +export const ReturnUserExperience = () => { + const appName = useSelector( + (state: RootState) => state.profiles.client.oauth_app_name || 'This app', + ) + + return ( +
+ {/* Temporary alert while in development */} + + This feature is currently in development. Contact MX to enable/disable this feature. + + + {/* Missing the logos graphic */} + + + + {__('Connect your accounts')} + + + {__('%1 uses MX to connect your accounts.', appName)}{' '} + + Learn more about MX. + + + + + {/* Missing info section */} + + + + + +
+ ) +} + +export default ReturnUserExperience diff --git a/src/ReturnUserExperience/returnUserExperience.module.css b/src/ReturnUserExperience/returnUserExperience.module.css new file mode 100644 index 0000000000..b5b57ff492 --- /dev/null +++ b/src/ReturnUserExperience/returnUserExperience.module.css @@ -0,0 +1,10 @@ +.pageContainer { + display: flex; + flex-direction: column; + min-height: 100%; + text-align: center; +} + +.mxCopyrightLogo { + margin-left: 6px; +} diff --git a/src/components/RenderConnectStep.js b/src/components/RenderConnectStep.js index 06569aba79..cb8186b593 100644 --- a/src/components/RenderConnectStep.js +++ b/src/components/RenderConnectStep.js @@ -16,6 +16,7 @@ import { selectUIMessageVersion, selectInitialConfig, } from 'src/redux/reducers/configSlice' +import { isConnectRuxEnabled } from 'src/redux/reducers/userFeaturesSlice' import Disclosure from 'src/views/disclosure/Disclosure' import { Search } from 'src/views/search/Search' @@ -45,6 +46,7 @@ import { PostMessageContext } from 'src/ConnectWidget' import useSelectInstitution from 'src/hooks/useSelectInstitution' import { DynamicDisclosure } from 'src/views/consent/DynamicDisclosure' import { canHandleActionableError } from 'src/views/actionableError/consts' +import ReturnUserExperience from 'src/ReturnUserExperience/ReturnUserExperience' const RenderConnectStep = (props) => { const postMessageFunctions = useContext(PostMessageContext) @@ -69,6 +71,7 @@ const RenderConnectStep = (props) => { const selectedInstitution = useSelector(getSelectedInstitution) const updateCredentials = useSelector((state) => state.connect.updateCredentials) const verifyMemberError = useSelector((state) => state.connect.error) + const showRuxStep = useSelector(isConnectRuxEnabled) const { handleSelectInstitution } = useSelectInstitution() @@ -91,7 +94,9 @@ const RenderConnectStep = (props) => { let connectStepView = null - if (step === STEPS.DISCLOSURE) { + if (showRuxStep) { + connectStepView = + } else if (step === STEPS.DISCLOSURE) { connectStepView = } else if (step === STEPS.SEARCH) { connectStepView = diff --git a/src/redux/reducers/Connect.js b/src/redux/reducers/Connect.js index 1674c91268..ac6631004e 100644 --- a/src/redux/reducers/Connect.js +++ b/src/redux/reducers/Connect.js @@ -66,6 +66,7 @@ const loadConnectSuccess = (state, action) => { experimentalFeatures = {}, widgetProfile, user = {}, + isRuxEnabled, } = action.payload return { @@ -76,16 +77,18 @@ const loadConnectSuccess = (state, action) => { isComponentLoading: false, location: pushLocation( state.location, - getStartingStep( - members, - member, - microdeposit, - config, - institution, - widgetProfile, - experimentalFeatures, - user, - ), + isRuxEnabled + ? STEPS.RETURNING_USER_EXPERIENCE + : getStartingStep( + members, + member, + microdeposit, + config, + institution, + widgetProfile, + experimentalFeatures, + user, + ), ), selectedInstitution: institution, updateCredentials: From c36a807be12e8db80dd356bc045de0734d5c3051 Mon Sep 17 00:00:00 2001 From: Jameson Date: Tue, 26 May 2026 11:18:16 -0600 Subject: [PATCH 03/20] feat: enhance ReturnUserExperience component with new RuxInfo, RuxLogosHeader, and RuxTitle components; update styles and tests --- .../ReturnUserExperience.tsx | 61 ++++---------- src/ReturnUserExperience/RuxInfo.tsx | 75 +++++++++++++++++ src/ReturnUserExperience/RuxLogosHeader.tsx | 31 +++++++ src/ReturnUserExperience/RuxTitle.tsx | 84 +++++++++++++++++++ .../ReturnUserExperience-test.tsx | 0 .../ReturnUserExperience.test.tsx | 0 .../__tests__/RuxLogosHeader-test.tsx | 0 .../returnUserExperience.module.css | 27 ++++++ 8 files changed, 234 insertions(+), 44 deletions(-) create mode 100644 src/ReturnUserExperience/RuxInfo.tsx create mode 100644 src/ReturnUserExperience/RuxLogosHeader.tsx create mode 100644 src/ReturnUserExperience/RuxTitle.tsx rename src/ReturnUserExperience/{ => __tests__}/ReturnUserExperience-test.tsx (100%) rename src/ReturnUserExperience/{ => __tests__}/ReturnUserExperience.test.tsx (100%) create mode 100644 src/ReturnUserExperience/__tests__/RuxLogosHeader-test.tsx diff --git a/src/ReturnUserExperience/ReturnUserExperience.tsx b/src/ReturnUserExperience/ReturnUserExperience.tsx index 416dc39cbc..1c56c373ef 100644 --- a/src/ReturnUserExperience/ReturnUserExperience.tsx +++ b/src/ReturnUserExperience/ReturnUserExperience.tsx @@ -1,58 +1,31 @@ import React from 'react' + import styles from './returnUserExperience.module.css' +import RuxLogosHeader from 'src/ReturnUserExperience/RuxLogosHeader' +import RuxTitle from 'src/ReturnUserExperience/RuxTitle' +import RuxInfo from 'src/ReturnUserExperience/RuxInfo' -import { RootState } from 'src/redux/Store' -import { useSelector } from 'react-redux' import { __ } from 'src/utilities/Intl' -import Alert from '@mui/material/Alert' -import Button from '@mui/material/Button' -import Link from '@mui/material/Link' -import Stack from '@mui/material/Stack' -import { Text } from '@mxenabled/mxui' -import { MXLogoCopyrightIcon } from '@mxenabled/mxui' +export const RUXViews = { + INFO: 'info', + PHONE_NUMBER: 'phoneNumber', + OTP: 'otp', + LIST: 'list', +} export const ReturnUserExperience = () => { - const appName = useSelector( - (state: RootState) => state.profiles.client.oauth_app_name || 'This app', - ) + const [view, setView] = React.useState<(typeof RUXViews)[keyof typeof RUXViews]>(RUXViews.INFO) + const [userEnteredPhone, _setUserEnteredPhone] = React.useState('') return (
- {/* Temporary alert while in development */} - - This feature is currently in development. Contact MX to enable/disable this feature. - - - {/* Missing the logos graphic */} - - - - {__('Connect your accounts')} - - - {__('%1 uses MX to connect your accounts.', appName)}{' '} - - Learn more about MX. - - - - - {/* Missing info section */} + + - - - - + {view === RUXViews.INFO && ( + setView(RUXViews.PHONE_NUMBER)} /> + )}
) } diff --git a/src/ReturnUserExperience/RuxInfo.tsx b/src/ReturnUserExperience/RuxInfo.tsx new file mode 100644 index 0000000000..4de8e6a554 --- /dev/null +++ b/src/ReturnUserExperience/RuxInfo.tsx @@ -0,0 +1,75 @@ +import React from 'react' +import { Avatar } from '@mui/material' +import Button from '@mui/material/Button' +import Stack from '@mui/material/Stack' +import { Text, Icon } from '@mxenabled/mxui' +import { MXLogoCopyrightIcon } from '@mxenabled/mxui' + +import { __ } from 'src/utilities/Intl' +import styles from './returnUserExperience.module.css' + +export const RuxInfo = ({ handleRuxContinue }: { handleRuxContinue: () => void }) => { + return ( + <> +
+
+ + + +
+ + {__('Trusted')} + + + {__('Used by over 13,000 banks & credit unions.')} + +
+
+ +
+ + + +
+ + {__('Secure')} + + + {__('Protected with multi-factor authentication and encryption.')} + +
+
+ +
+ + + +
+ + {__('Private')} + + + {__('We never sell your phone number or use it for marketing.')} + +
+
+
+ + + + + + + ) +} + +export default RuxInfo diff --git a/src/ReturnUserExperience/RuxLogosHeader.tsx b/src/ReturnUserExperience/RuxLogosHeader.tsx new file mode 100644 index 0000000000..65b54ad081 --- /dev/null +++ b/src/ReturnUserExperience/RuxLogosHeader.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import { useSelector } from 'react-redux' +import type { RootState } from 'src/redux/Store' + +import { Stack } from '@mui/material' +import { Icon } from '@mxenabled/mxui' +import { MXLogoFilledIcon } from '@mxenabled/mxui' + +import { ClientLogo } from 'src/components/ClientLogo' + +export const RuxLogosHeader = ({ show }: { show?: boolean }) => { + const clientGuid = useSelector((state: RootState) => state.profiles.client.guid) + + return show ? ( + + {/* Client Logo or Default */} +
+ +
+ + + + {/* MX Logo */} +
+ +
+
+ ) : null +} + +export default RuxLogosHeader diff --git a/src/ReturnUserExperience/RuxTitle.tsx b/src/ReturnUserExperience/RuxTitle.tsx new file mode 100644 index 0000000000..32884a7c6c --- /dev/null +++ b/src/ReturnUserExperience/RuxTitle.tsx @@ -0,0 +1,84 @@ +import React from 'react' +import { useSelector } from 'react-redux' +import Link from '@mui/material/Link' +import Stack from '@mui/material/Stack' +import { Text } from '@mxenabled/mxui' + +import { __ } from 'src/utilities/Intl' +import styles from './returnUserExperience.module.css' // TODO: Update this +import { RootState } from 'src/redux/Store' +import { RUXViews } from 'src/ReturnUserExperience/ReturnUserExperience' + +export const RuxTitle = ({ + userEnteredPhone, + view, +}: { + userEnteredPhone: string + view: (typeof RUXViews)[keyof typeof RUXViews] +}) => { + const appName = useSelector( + (state: RootState) => state.profiles.client.oauth_app_name || 'This app', + ) + const [{ title, description }, setTitleAndDescription] = React.useState({ + title: '', + description: '', + }) + + React.useEffect(() => { + switch (view) { + case RUXViews.INFO: + setTitleAndDescription({ + title: __('Connect your accounts'), + description: __('%1 uses MX to connect your accounts.', appName), + }) + break + case RUXViews.PHONE_NUMBER: + setTitleAndDescription({ + title: __('Sign in with your phone'), + description: __('Sign into MX to securely access your accounts.'), + }) + break + case RUXViews.OTP: + setTitleAndDescription({ + title: __('Verify your phone number'), + description: __('Enter the code sent to %1', userEnteredPhone), + }) + break + case RUXViews.LIST: + setTitleAndDescription({ + title: __('Select your institution'), + description: __('Choose a previously connected institution or add a new one.'), + }) + break + default: + setTitleAndDescription({ + title: '', + description: '', + }) + } + }, [view, appName]) + + return ( + + + {title} + + + {description} + + + + ) +} + +export default RuxTitle + +const LearnMoreMXLink = ({ show = false }: { show?: boolean }) => + show ? ( + <> + {' '} + + {__('Learn more about MX.')} + + + ) : null diff --git a/src/ReturnUserExperience/ReturnUserExperience-test.tsx b/src/ReturnUserExperience/__tests__/ReturnUserExperience-test.tsx similarity index 100% rename from src/ReturnUserExperience/ReturnUserExperience-test.tsx rename to src/ReturnUserExperience/__tests__/ReturnUserExperience-test.tsx diff --git a/src/ReturnUserExperience/ReturnUserExperience.test.tsx b/src/ReturnUserExperience/__tests__/ReturnUserExperience.test.tsx similarity index 100% rename from src/ReturnUserExperience/ReturnUserExperience.test.tsx rename to src/ReturnUserExperience/__tests__/ReturnUserExperience.test.tsx diff --git a/src/ReturnUserExperience/__tests__/RuxLogosHeader-test.tsx b/src/ReturnUserExperience/__tests__/RuxLogosHeader-test.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/ReturnUserExperience/returnUserExperience.module.css b/src/ReturnUserExperience/returnUserExperience.module.css index b5b57ff492..078a808c53 100644 --- a/src/ReturnUserExperience/returnUserExperience.module.css +++ b/src/ReturnUserExperience/returnUserExperience.module.css @@ -2,9 +2,36 @@ display: flex; flex-direction: column; min-height: 100%; +} + +.centerText { text-align: center; } .mxCopyrightLogo { margin-left: 6px; } + +.infoContainer { + border: 1px solid #0000001F; + border-radius: 8px; + padding: 8px; + margin: 40px 0; +} + +.infoRow { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 8px; + padding: 8px 0; +} + +.infoRowContent { + text-align: left; + margin-right: 6px; +} + +.buttonContainer { + padding: 0 24px; +} \ No newline at end of file From 9d8ce730aba9822384a25eb1376b8538cadda721 Mon Sep 17 00:00:00 2001 From: Jameson Date: Tue, 26 May 2026 11:19:11 -0600 Subject: [PATCH 04/20] fix: correct border color format in infoContainer style --- src/ReturnUserExperience/returnUserExperience.module.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ReturnUserExperience/returnUserExperience.module.css b/src/ReturnUserExperience/returnUserExperience.module.css index 078a808c53..055ebc2a27 100644 --- a/src/ReturnUserExperience/returnUserExperience.module.css +++ b/src/ReturnUserExperience/returnUserExperience.module.css @@ -13,7 +13,7 @@ } .infoContainer { - border: 1px solid #0000001F; + border: 1px solid #0000001f; border-radius: 8px; padding: 8px; margin: 40px 0; @@ -34,4 +34,4 @@ .buttonContainer { padding: 0 24px; -} \ No newline at end of file +} From f616314fded4792ebcf8a6bd90b714af26c2a927 Mon Sep 17 00:00:00 2001 From: Jameson Date: Tue, 26 May 2026 12:45:41 -0600 Subject: [PATCH 05/20] refactor: simplify RuxInfo button structure and enhance RuxLogosHeader styling --- src/ReturnUserExperience/RuxInfo.tsx | 24 ++++++++------------- src/ReturnUserExperience/RuxLogosHeader.tsx | 10 ++++++++- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/ReturnUserExperience/RuxInfo.tsx b/src/ReturnUserExperience/RuxInfo.tsx index 4de8e6a554..03283003e6 100644 --- a/src/ReturnUserExperience/RuxInfo.tsx +++ b/src/ReturnUserExperience/RuxInfo.tsx @@ -1,7 +1,6 @@ import React from 'react' -import { Avatar } from '@mui/material' +import Avatar from '@mui/material/Avatar' import Button from '@mui/material/Button' -import Stack from '@mui/material/Stack' import { Text, Icon } from '@mxenabled/mxui' import { MXLogoCopyrightIcon } from '@mxenabled/mxui' @@ -55,19 +54,14 @@ export const RuxInfo = ({ handleRuxContinue }: { handleRuxContinue: () => void } - - - - + ) } diff --git a/src/ReturnUserExperience/RuxLogosHeader.tsx b/src/ReturnUserExperience/RuxLogosHeader.tsx index 65b54ad081..62729bb451 100644 --- a/src/ReturnUserExperience/RuxLogosHeader.tsx +++ b/src/ReturnUserExperience/RuxLogosHeader.tsx @@ -21,7 +21,15 @@ export const RuxLogosHeader = ({ show }: { show?: boolean }) => { {/* MX Logo */} -
+
From 211ac302c1b985cc5fb2b11555805c7b097adefe Mon Sep 17 00:00:00 2001 From: Jameson Date: Tue, 26 May 2026 13:00:14 -0600 Subject: [PATCH 06/20] refactor: remove unnecessary endIcon from RuxInfo button and clean up styles --- src/ReturnUserExperience/RuxInfo.tsx | 8 +------- src/ReturnUserExperience/returnUserExperience.module.css | 6 +++++- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/ReturnUserExperience/RuxInfo.tsx b/src/ReturnUserExperience/RuxInfo.tsx index 03283003e6..9a5b59d166 100644 --- a/src/ReturnUserExperience/RuxInfo.tsx +++ b/src/ReturnUserExperience/RuxInfo.tsx @@ -2,7 +2,6 @@ import React from 'react' import Avatar from '@mui/material/Avatar' import Button from '@mui/material/Button' import { Text, Icon } from '@mxenabled/mxui' -import { MXLogoCopyrightIcon } from '@mxenabled/mxui' import { __ } from 'src/utilities/Intl' import styles from './returnUserExperience.module.css' @@ -54,12 +53,7 @@ export const RuxInfo = ({ handleRuxContinue }: { handleRuxContinue: () => void }
- diff --git a/src/ReturnUserExperience/returnUserExperience.module.css b/src/ReturnUserExperience/returnUserExperience.module.css index 055ebc2a27..eaa2f109cf 100644 --- a/src/ReturnUserExperience/returnUserExperience.module.css +++ b/src/ReturnUserExperience/returnUserExperience.module.css @@ -27,6 +27,10 @@ padding: 8px 0; } +.infoRow:last-of-type { + margin-bottom: 0; +} + .infoRowContent { text-align: left; margin-right: 6px; @@ -34,4 +38,4 @@ .buttonContainer { padding: 0 24px; -} +} \ No newline at end of file From d5b9b636dc6a56ec66f630497a0a51b3f48c6a9c Mon Sep 17 00:00:00 2001 From: Jameson Date: Wed, 27 May 2026 12:00:10 -0600 Subject: [PATCH 07/20] feat: refactor RuxInfo component to use useMemo for information clusters and improve rendering; update styles for avatar --- src/ReturnUserExperience/RuxInfo.tsx | 80 +++++++++---------- src/ReturnUserExperience/RuxTitle.tsx | 4 +- .../returnUserExperience.module.css | 5 ++ 3 files changed, 45 insertions(+), 44 deletions(-) diff --git a/src/ReturnUserExperience/RuxInfo.tsx b/src/ReturnUserExperience/RuxInfo.tsx index 9a5b59d166..2cb07d9eef 100644 --- a/src/ReturnUserExperience/RuxInfo.tsx +++ b/src/ReturnUserExperience/RuxInfo.tsx @@ -1,56 +1,52 @@ -import React from 'react' +import React, { useMemo } from 'react' import Avatar from '@mui/material/Avatar' import Button from '@mui/material/Button' import { Text, Icon } from '@mxenabled/mxui' import { __ } from 'src/utilities/Intl' -import styles from './returnUserExperience.module.css' +import useAnalyticsPath from 'src/hooks/useAnalyticsPath' +import { PageviewInfo } from 'src/const/Analytics' +import styles from 'src/ReturnUserExperience/returnUserExperience.module.css' export const RuxInfo = ({ handleRuxContinue }: { handleRuxContinue: () => void }) => { + useAnalyticsPath(...PageviewInfo.CONNECT_RUX_INFO) + const informationClusters = useMemo( + () => [ + { + icon: , + title: __('Trusted'), + description: __('Used by over 13,000 banks & credit unions.'), + }, + { + icon: , + title: __('Secure'), + description: __('Protected with multi-factor authentication and encryption.'), + }, + { + icon: , + title: __('Private'), + description: __('We never sell your phone number or use it for marketing.'), + }, + ], + [], + ) + return ( <>
-
- - - -
- - {__('Trusted')} - - - {__('Used by over 13,000 banks & credit unions.')} - -
-
- -
- - - -
- - {__('Secure')} - - - {__('Protected with multi-factor authentication and encryption.')} - -
-
- -
- - - -
- - {__('Private')} - - - {__('We never sell your phone number or use it for marketing.')} - + {informationClusters.map((info, index) => ( +
+ + {info.icon} + +
+ {info.title} + + {info.description} + +
-
+ ))}