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} + +
+
+ ))} +
+ + + + ) +} + +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