Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
ab5ab51
feat: add CONNECT_RUX feature and related authentication methods
Jameson13B May 19, 2026
e357be3
feat: implement ReturnUserExperience component and integrate with Red…
Jameson13B May 26, 2026
c36a807
feat: enhance ReturnUserExperience component with new RuxInfo, RuxLog…
Jameson13B May 26, 2026
9d8ce73
fix: correct border color format in infoContainer style
Jameson13B May 26, 2026
f616314
refactor: simplify RuxInfo button structure and enhance RuxLogosHeade…
Jameson13B May 26, 2026
211ac30
refactor: remove unnecessary endIcon from RuxInfo button and clean up…
Jameson13B May 26, 2026
d5b9b63
feat: refactor RuxInfo component to use useMemo for information clust…
Jameson13B May 27, 2026
e651d23
feat: enhance ReturnUserExperience component with analytics tracking …
Jameson13B May 28, 2026
c03f523
fix: add missing semicolon in clientLogo style and clean up infoRowCo…
Jameson13B May 28, 2026
097974c
feat: update RuxInfo component to include app name display and additi…
Jameson13B May 28, 2026
4a33fc9
refactor: remove unused RuxTitle component and clean up related styles
Jameson13B May 28, 2026
e521c1f
feat: update RuxInfo component to use titleContainer class for consis…
Jameson13B May 28, 2026
34bfe69
feat: integrate useTheme for consistent styling in RuxInfo component
Jameson13B Jun 1, 2026
7b51809
feat: update ReturnUserExperience to handle RUX info continue action …
Jameson13B Jun 1, 2026
582cb57
fix: add comment to clarify backend skipping in handleRuxInfoContinue…
Jameson13B Jun 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions docs/APIDOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -658,3 +658,45 @@ xee
</details>

---

#### authPersonInitiate(phoneNumber)

<details>
<summary>Initiates authentication for a person</summary>

##### 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#}}` |

</details>

---

#### authPersonVerify(code)

<details>
<summary>Verifies authentication for a person</summary>

##### 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#}}` |

---
1 change: 1 addition & 0 deletions docs/USER_FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | <pre><pre>{<br>&nbsp;feature_name: 'SHOW_CONNECT_GLOBAL_NAVIGATION_HEADER',<br>&nbsp;guid: 'FTR-123', <br>&nbsp;is_enabled: true <br>&nbsp;}</pre> |
| `CONNECT_COMBO_JOBS` | When enabled, the Connect widget will create COMBINATION jobs instead of individual jobs (aggregate, verification, reward, etc). | <pre>{<br>&nbsp;feature_name: 'CONNECT_COMBO_JOBS',<br>&nbsp;guid: 'FTR-123', <br>&nbsp;is_enabled: true <br>&nbsp;}</pre> |
| `CONNECT_RUX` | When enabled, the Connect widget will start by initializing the RUX authentication flow. | <pre>{<br>&nbsp;feature_name: 'CONNECT_RUX',<br>&nbsp;guid: 'FTR-123', <br>&nbsp;is_enabled: true <br>&nbsp;}</pre> |

</details>

Expand Down
56 changes: 56 additions & 0 deletions src/ReturnUserExperience/ReturnUserExperience.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react'
import { useSelector } from 'react-redux'

import styles from './returnUserExperience.module.css'
import RuxInfo from 'src/ReturnUserExperience/RuxInfo'

import { Stack } from '@mui/material'
import { Icon } from '@mxenabled/mxui'
import { MXLogoFilledIcon } from '@mxenabled/mxui'

import useAnalyticsEvent from 'src/hooks/useAnalyticsEvent'
import { __ } from 'src/utilities/Intl'
import { AnalyticEvents } from 'src/const/Analytics'
import { RootState } from 'src/redux/Store'
import { ClientLogo } from 'src/components/ClientLogo'

export const RUXViews = {
INFO: 'info',
PHONE_NUMBER: 'phoneNumber',
OTP: 'otp',
LIST: 'list',
}

export const ReturnUserExperience = React.forwardRef(() => {
const [view, setView] = React.useState<(typeof RUXViews)[keyof typeof RUXViews]>(RUXViews.INFO)
const clientGuid = useSelector((state: RootState) => state.profiles.client.guid)
const sendAnalyticsEvent = useAnalyticsEvent()

const handleRuxInfoContinue = () => {
// This is currently skipping the backend. See epic/ticket for more details.
sendAnalyticsEvent(AnalyticEvents.RUX_INFO_CONTINUE_CLICKED)
setView(RUXViews.PHONE_NUMBER)
}

return (
<div className={styles.pageContainer}>
{view !== RUXViews.LIST && (
<Stack className={styles.logoHeaders} direction="row" spacing="8px">
<div className={styles.clientLogo}>
<ClientLogo alt="Client logo" clientGuid={clientGuid} size={64} />
</div>
<Icon name="add" size={20} />
<div className={styles.mxLogo}>
<MXLogoFilledIcon size={64} />
</div>
</Stack>
)}

{view === RUXViews.INFO && <RuxInfo handleRuxContinue={handleRuxInfoContinue} />}
</div>
)
})

ReturnUserExperience.displayName = 'ReturnUserExperience'

export default ReturnUserExperience
91 changes: 91 additions & 0 deletions src/ReturnUserExperience/RuxInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { useMemo } from 'react'
import { useSelector } from 'react-redux'

import { useTheme } from '@mui/material'
import Button from '@mui/material/Button'
import Link from '@mui/material/Link'
import Stack from '@mui/material/Stack'
import { Text, Icon } from '@mxenabled/mxui'

import { RootState } from 'src/redux/Store'
import { __ } from 'src/utilities/Intl'
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 { palette } = useTheme()
const appName = useSelector(
(state: RootState) => state.profiles.client.oauth_app_name || 'This app',
)
const informationClusters = useMemo(
() => [
{
icon: 'verified_user',
title: __('Trusted'),
description: __('Used by over 13,000 banks & credit unions.'),
},
{
icon: 'lock',
title: __('Secure'),
description: __('Protected with multi-factor authentication and encryption.'),
},
{
icon: 'notifications_off',
title: __('Private'),
description: __('We never sell your phone number or use it for marketing.'),
},
],
[],
)

return (
<>
<Stack className={styles.titleContainer} spacing="6px">
<Text bold={true} className={styles.centerText} truncate={false} variant="h2">
{__('Connect your accounts')}
</Text>
<Text className={styles.centerText} truncate={false} variant="subtitle1">
{__('%1 uses MX to connect your accounts. ', appName)}
<Link
href="https://mx.com/learn-more"
sx={{
color: palette.primary.main,
fontWeight: 'normal',
marginLeft: 0,
textDecoration: 'underline',
}}
underline="always"
variant="subtitle1"
>
{__('Learn more about MX.')}
</Link>
</Text>
</Stack>

<div className={styles.infoContainer}>
{informationClusters.map((info, index) => (
<div className={styles.infoRow} key={index}>
<div className={styles.avatar}>
<Icon color="primary" fill={true} name={info.icon} size={24} />
</div>

<div className={styles.infoRowContent}>
<Text bold={true}>{info.title}</Text>
<Text truncate={false} variant="caption">
{info.description}
</Text>
</div>
</div>
))}
</div>

<Button fullWidth={true} onClick={handleRuxContinue} variant="contained">
{__('Continue')}
</Button>
</>
)
}

export default RuxInfo
146 changes: 146 additions & 0 deletions src/ReturnUserExperience/__tests__/ReturnUserExperience-test.tsx
Original file line number Diff line number Diff line change
@@ -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(<ReturnUserExperience />, { preloadedState })
expect(screen.getByText('Connect your accounts')).toBeInTheDocument()
})

it('should render the development warning alert', () => {
render(<ReturnUserExperience />, { preloadedState })
const alert = screen.getByText('This feature is currently in development.')
expect(alert).toBeInTheDocument()
})

it('should render the main heading', () => {
render(<ReturnUserExperience />, { preloadedState })
const heading = screen.getByRole('heading', { level: 2 })
expect(heading).toHaveTextContent('Connect your accounts')
})

it('should render the subtitle with app name interpolation', () => {
render(<ReturnUserExperience />, { 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(<ReturnUserExperience />, { 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(<ReturnUserExperience />, { 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(<ReturnUserExperience />, { 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(<ReturnUserExperience />, { 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(<ReturnUserExperience />, { 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(<ReturnUserExperience />, { 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(<ReturnUserExperience />, { 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(<ReturnUserExperience />, { 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(<ReturnUserExperience />, { 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(<ReturnUserExperience />, { preloadedState })
const heading = screen.getByRole('heading', { level: 2 })
expect(heading).toHaveTextContent('Connect your accounts')
})

it('should have accessible buttons', () => {
render(<ReturnUserExperience />, { 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(<ReturnUserExperience />, { preloadedState })
const link = screen.getByRole('link', { name: /learn more about mx/i })
expect(link).toHaveAccessibleName()
})
})
})
Loading