Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions src/components/RenderConnectStep.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { Microdeposits } from 'src/views/microdeposits/Microdeposits'
import VerifyExistingMember from 'src/views/verification/VerifyExistingMember'
import { VerifyError } from 'src/views/verification/VerifyError'
import { ManualAccountConnect } from 'src/views/manualAccount/ManualAccountConnect'
import { DemoConnectGuard } from 'src/views/demoConnectGuard/DemoConnectGuard'
import AdditionalProductStep, {
ADDITIONAL_PRODUCT_OPTIONS,
} from 'src/views/additionalProduct/AdditionalProductStep'
Expand Down Expand Up @@ -110,6 +111,8 @@ const RenderConnectStep = (props) => {
throw new Error('invalid product offer')

connectStepView = <AdditionalProductStep ref={props.navigationRef} />
} else if (step === STEPS.DEMO_CONNECT_GUARD) {
connectStepView = <DemoConnectGuard />
} else if (step === STEPS.ADD_MANUAL_ACCOUNT) {
connectStepView = (
<ManualAccountConnect
Expand Down
1 change: 1 addition & 0 deletions src/const/Connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const STEPS = {
CONNECTING: 'connecting',
CONSENT: 'consent',
DELETE_MEMBER_SUCCESS: 'deleteMemberSuccess',
DEMO_CONNECT_GUARD: 'demoConnectGuard',
DISCLOSURE: 'disclosure',
ENTER_CREDENTIALS: 'enterCreds',
EXISTING_MEMBER: 'existingMember',
Expand Down
2 changes: 2 additions & 0 deletions src/hooks/useSelectInstitution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const useSelectInstitution = () => {
const dispatch = useDispatch()
const consentIsEnabled = useSelector((state: RootState) => isConsentEnabled(state))
const connectConfig = useSelector(selectConnectConfig)
const user = useSelector((state: RootState) => state.profiles.user)

const handleSelectInstitution = useCallback(
(institution: InstitutionResponseType) => {
Expand All @@ -42,6 +43,7 @@ const useSelectInstitution = () => {
institutionStatus,
consentIsEnabled: consentIsEnabled || false,
additionalProductOption: connectConfig?.additional_product_option || null,
user,
},
})
}),
Expand Down
1 change: 1 addition & 0 deletions src/redux/actions/Connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const ActionTypes = {
LOGIN_ERROR_START_OVER: 'connect/login_error_start_over',
CONNECT_GO_BACK: 'connect/go_back',
REJECT_ADDITIONAL_PRODUCT: 'connect/reject_additional_product',
DEMO_CONNECT_GUARD_RETURN_TO_SEARCH: 'connect/demo_connect_guard_return_to_search',
}

export const loadConnect = (config = {}) => ({ type: ActionTypes.LOAD_CONNECT, payload: config })
Expand Down
4 changes: 4 additions & 0 deletions src/redux/reducers/Connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ const selectInstitutionSuccess = (state, action) => {
// 2. Additional product - if the client is offering a product AND the institution has support for the product
// 3. Consent - if the Client has enabled consent
// 4. Institution disabled - if the institution is disabled by the client
// 5. Demo connect guard - if the user is a demo user but the institution is not a demo institution
let nextStep = STEPS.ENTER_CREDENTIALS

const canOfferVerification =
Expand All @@ -292,6 +293,8 @@ const selectInstitutionSuccess = (state, action) => {
action.payload.institutionStatus === InstitutionStatus.UNAVAILABLE)
) {
nextStep = STEPS.INSTITUTION_STATUS_DETAILS
} else if (action.payload.user?.is_demo && !action.payload.institution?.is_demo) {
nextStep = STEPS.DEMO_CONNECT_GUARD
} else if (canOfferVerification || canOfferAggregation) {
nextStep = STEPS.ADDITIONAL_PRODUCT
} else if (action.payload.consentIsEnabled) {
Expand Down Expand Up @@ -733,6 +736,7 @@ export const connect = createReducer(defaultState, {
[ActionTypes.ADD_MANUAL_ACCOUNT_SUCCESS]: addManualAccount,
[ActionTypes.LOGIN_ERROR_START_OVER]: loginErrorStartOver,
[ActionTypes.CONNECT_GO_BACK]: connectGoBack,
[ActionTypes.DEMO_CONNECT_GUARD_RETURN_TO_SEARCH]: goBackSearchOrVerify,

// Addtional product offer / step up reducers
// These are here to manage changing the location/step of the widget
Expand Down
69 changes: 69 additions & 0 deletions src/views/demoConnectGuard/DemoConnectGuard-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react'
import { useDispatch } from 'react-redux'
import { render, screen } from 'src/utilities/testingLibrary'
import { DemoConnectGuard } from './DemoConnectGuard'
import { initialState } from 'src/services/mockedData'
import * as connectActions from 'src/redux/actions/Connect'

// Mock useDispatch
vitest.mock('react-redux', async () => {
const actual = await vitest.importActual('react-redux')
return { ...actual, useDispatch: vitest.fn() }
})
const mockDispatch = vitest.fn()
const mockedUseDispatch = vitest.mocked(useDispatch)

describe('DemoConnectGuard', () => {
const mockInstitution = {
guid: 'INS-test-123',
name: 'Test Bank',
logo_url: 'https://example.com/logo.png',
code: 'TEST',
url: 'https://testbank.com',
}

const mockInitialState = {
...initialState,
connect: {
...initialState.connect,
selectedInstitution: mockInstitution,
},
}

beforeEach(() => {
mockDispatch.mockClear()
mockedUseDispatch.mockReturnValue(mockDispatch)
})

it('renders all component elements correctly', () => {
const { container } = render(<DemoConnectGuard />, { preloadedState: mockInitialState })

expect(screen.getByText('Demo mode active')).toBeInTheDocument()
expect(
screen.getByText(
'Live institutions are not available in the demo environment. Please select MX Bank to test the connection process.',
),
).toBeInTheDocument()

const logo = screen.getByAltText('Logo for Test Bank')
expect(logo).toBeInTheDocument()

const errorIcon = container.querySelector('svg.MuiSvgIcon-colorError')
expect(errorIcon).toBeInTheDocument()

const button = screen.getByRole('button', { name: /return to institution selection/i })
expect(button).toBeInTheDocument()
})

it('dispatches the correct action when return button is clicked', async () => {
const { user } = render(<DemoConnectGuard />, { preloadedState: mockInitialState })

const returnButton = screen.getByTestId('return-to-search-button')
await user.click(returnButton)

expect(mockDispatch).toHaveBeenCalledWith({
type: connectActions.ActionTypes.DEMO_CONNECT_GUARD_RETURN_TO_SEARCH,
payload: {},
})
})
})
96 changes: 96 additions & 0 deletions src/views/demoConnectGuard/DemoConnectGuard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Button } from '@mui/material'
import { P, H2 } from '@mxenabled/mxui'
import { Icon } from '@mxenabled/mxui'

import { __ } from 'src/utilities/Intl'
import { InstitutionLogo } from '@mxenabled/mxui'
import { SlideDown } from 'src/components/SlideDown'
import { getSelectedInstitution } from 'src/redux/selectors/Connect'
import * as connectActions from 'src/redux/actions/Connect'
import { selectInitialConfig } from 'src/redux/reducers/configSlice'

export const DemoConnectGuard: React.FC = () => {
const institution = useSelector(getSelectedInstitution)
const initialConfig = useSelector(selectInitialConfig)
const containerRef = useRef(null)
const styles = getStyles()

const dispatch = useDispatch()

return (
<div ref={containerRef} style={styles.container}>
<SlideDown>
<div style={styles.logoContainer}>
<InstitutionLogo
alt={__('Logo for %1', institution.name)}
aria-hidden={true}
institutionGuid={institution.guid}
logoUrl={institution.logo_url}
size={64}
/>
<Icon color="error" fill={true} name="error" size={32} sx={styles.icon} />
</div>
<H2 sx={styles.title} truncate={false}>
{__('Demo mode active')}
</H2>
<P sx={styles.body} truncate={false}>
{__(
'Live institutions are not available in the demo environment. Please select MX Bank to test the connection process.',
)}
</P>
<Button
data-test="return-to-search-button"
fullWidth={true}
onClick={() => {
dispatch({
type: connectActions.ActionTypes.DEMO_CONNECT_GUARD_RETURN_TO_SEARCH,
payload: initialConfig,
})
}}
variant="contained"
>
{__('Return to institution selection')}
</Button>
</SlideDown>
</div>
)
}

const getStyles = () => ({
container: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
paddingTop: 20,
} as React.CSSProperties,
logoContainer: {
position: 'relative',
display: 'inline-block',
} as React.CSSProperties,
icon: {
position: 'absolute',
top: '-16px',
right: '-16px',
background: 'white',
borderRadius: '50%',
},
title: {
marginBottom: '4px',
marginTop: '32px',
fontSize: '23px',
fontWeight: 700,
lineHeight: '32px',
textAlign: 'center',
},
body: {
textAlign: 'center',
marginBottom: '32px',
fontSize: '15px',
fontWeight: 400,
lineHeight: '24px',
} as React.CSSProperties,
})
Loading