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/ReturnUserExperience/README.md b/src/ReturnUserExperience/README.md
new file mode 100644
index 0000000000..d329335206
--- /dev/null
+++ b/src/ReturnUserExperience/README.md
@@ -0,0 +1,8 @@
+# Return User Experience
+
+This directory contains components related to the Return User Experience, such as Phone number input, and OTP input. These components are designed to provide a seamless and secure experience for users to authenticate for an enhanced connecting experience across all clients and apps.
+
+## Related Docs
+
+- [RUX Program Charter](https://mxcom.atlassian.net/wiki/spaces/PAEP/pages/2338291713/Charter+Returning+User+Experience+RUX)
+- [Figma Designs](https://www.figma.com/design/RU4RCcWv3R6cGhgeWBHcZ4/RUX?node-id=6909-103884&t=LTm5Zjepuittb5xK-1)
diff --git a/src/ReturnUserExperience/ReturnUserExperience.tsx b/src/ReturnUserExperience/ReturnUserExperience.tsx
new file mode 100644
index 0000000000..7e594ef3d7
--- /dev/null
+++ b/src/ReturnUserExperience/ReturnUserExperience.tsx
@@ -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 (
+
+ {view !== RUXViews.LIST && (
+
+
+
+
+
+
+
+
+
+ )}
+
+ {view === RUXViews.INFO &&
}
+
+ )
+})
+
+ReturnUserExperience.displayName = 'ReturnUserExperience'
+
+export default ReturnUserExperience
diff --git a/src/ReturnUserExperience/RuxInfo.tsx b/src/ReturnUserExperience/RuxInfo.tsx
new file mode 100644
index 0000000000..938e43fe80
--- /dev/null
+++ b/src/ReturnUserExperience/RuxInfo.tsx
@@ -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 (
+ <>
+
+
+ {__('Connect your accounts')}
+
+
+ {__('%1 uses MX to connect your accounts. ', appName)}
+
+ {__('Learn more about MX.')}
+
+
+
+
+
+ {informationClusters.map((info, index) => (
+
+
+
+
+
+
+ {info.title}
+
+ {info.description}
+
+
+
+ ))}
+
+
+
+ {__('Continue')}
+
+ >
+ )
+}
+
+export default RuxInfo
diff --git a/src/ReturnUserExperience/__tests__/ReturnUserExperience-test.tsx b/src/ReturnUserExperience/__tests__/ReturnUserExperience-test.tsx
new file mode 100644
index 0000000000..f29483e8c0
--- /dev/null
+++ b/src/ReturnUserExperience/__tests__/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/__tests__/RuxInfo-test.tsx b/src/ReturnUserExperience/__tests__/RuxInfo-test.tsx
new file mode 100644
index 0000000000..578ac904c4
--- /dev/null
+++ b/src/ReturnUserExperience/__tests__/RuxInfo-test.tsx
@@ -0,0 +1,48 @@
+import React from 'react'
+import { RuxInfo } from 'src/ReturnUserExperience/RuxInfo'
+import { render } from 'src/utilities/testingLibrary'
+
+describe('RuxInfo', () => {
+ it('renders the main heading', () => {
+ const { getByRole } = render( {}} />)
+ const heading = getByRole('heading', { level: 2 })
+ expect(heading).toHaveTextContent('Connect your accounts')
+ })
+
+ it('renders the subtitle', () => {
+ const { getByText } = render( {}} />)
+ const subtitle = getByText(/uses MX to connect your accounts./i)
+ expect(subtitle).toBeInTheDocument()
+ })
+
+ it('renders the learn more link with correct attributes', () => {
+ const { getByRole } = render( {}} />)
+ const link = 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('renders the information clusters with correct content', () => {
+ const { getByText } = render( {}} />)
+ expect(getByText('Trusted')).toBeInTheDocument()
+ expect(getByText('Used by over 13,000 banks & credit unions.')).toBeInTheDocument()
+ expect(getByText('Secure')).toBeInTheDocument()
+ expect(
+ getByText('Protected with multi-factor authentication and encryption.'),
+ ).toBeInTheDocument()
+ expect(getByText('Private')).toBeInTheDocument()
+ expect(
+ getByText('We never sell your phone number or use it for marketing.'),
+ ).toBeInTheDocument()
+ })
+
+ it('calls handleRuxContinue when the continue button is clicked', () => {
+ const handleRuxContinueMock = vi.fn()
+ const { getByRole } = render( )
+ const continueButton = getByRole('button', { name: /continue/i })
+ continueButton.click()
+ expect(handleRuxContinueMock).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/src/ReturnUserExperience/returnUserExperience.module.css b/src/ReturnUserExperience/returnUserExperience.module.css
new file mode 100644
index 0000000000..fc76276baf
--- /dev/null
+++ b/src/ReturnUserExperience/returnUserExperience.module.css
@@ -0,0 +1,76 @@
+.pageContainer {
+ display: flex;
+ flex-direction: column;
+ min-height: 100%;
+}
+
+.centerText {
+ text-align: center;
+}
+
+.logoHeaders {
+ align-items: center;
+ justify-content: center;
+}
+
+.mxCopyrightLogo {
+ margin-left: 6px;
+}
+
+.clientLogo {
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.mxLogo {
+ border-radius: 8px;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.infoContainer {
+ border: 1px solid #0000001f;
+ border-radius: 8px;
+ padding: 8px;
+ margin: 40px 0;
+}
+
+.infoRow {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 0;
+}
+
+.infoRow:last-of-type {
+ margin-bottom: 0;
+}
+
+.infoRowContent {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ flex: 1 0 0;
+ text-align: left;
+}
+
+.buttonContainer {
+ padding: 0 24px;
+}
+
+.avatar {
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 48px;
+ min-width: 48px;
+}
+
+.titleContainer {
+ padding-top: 16px;
+ padding-right: 16px;
+ padding-left: 16px;
+}
diff --git a/src/components/RenderConnectStep.js b/src/components/RenderConnectStep.js
index 06569aba79..4be5007615 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 && step === STEPS.RETURNING_USER_EXPERIENCE) {
+ connectStepView =
+ } else if (step === STEPS.DISCLOSURE) {
connectStepView =
} else if (step === STEPS.SEARCH) {
connectStepView =
diff --git a/src/const/Analytics.js b/src/const/Analytics.js
index 3089952e51..03405a3d45 100644
--- a/src/const/Analytics.js
+++ b/src/const/Analytics.js
@@ -30,6 +30,7 @@ export const AnalyticEvents = {
OAUTH_PENDING_MEMBER_CREATED: 'oauth_pending_member_created',
OAUTH_DEFAULT_CANCEL: 'oauth_default_cancel',
OAUTH_DEFAULT_GO_TO_INSTITUTION: 'oauth_default_go_to_institution',
+ RUX_INFO_CONTINUE_CLICKED: 'rux_info_continue_clicked',
SEARCH_QUERY: 'search_query',
SELECT_POPULAR_INSTITUTION: 'select_popular_institution',
SELECT_SEARCHED_INSTITUTION: 'select_searched_institution',
@@ -115,6 +116,7 @@ export const PageviewInfo = {
CONNECT_OAUTH_WAITING: ['Connect Oauth Step Waiting', '/credentials/oauth_step/waiting'],
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_SEARCH: ['Connect Search', '/search'],
CONNECT_SEARCH_FAILED: ['Connect Search Failed', '/search_failed'],
CONNECT_SEARCH_NO_RESULTS: ['Connect Search No Results', '/no_results'],
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/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:
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