diff --git a/src/components/RenderConnectStep.js b/src/components/RenderConnectStep.js index 6e12918c19..b615703f09 100644 --- a/src/components/RenderConnectStep.js +++ b/src/components/RenderConnectStep.js @@ -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' @@ -110,6 +111,8 @@ const RenderConnectStep = (props) => { throw new Error('invalid product offer') connectStepView = + } else if (step === STEPS.DEMO_CONNECT_GUARD) { + connectStepView = } else if (step === STEPS.ADD_MANUAL_ACCOUNT) { connectStepView = ( { 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) => { @@ -42,6 +43,7 @@ const useSelectInstitution = () => { institutionStatus, consentIsEnabled: consentIsEnabled || false, additionalProductOption: connectConfig?.additional_product_option || null, + user, }, }) }), diff --git a/src/redux/actions/Connect.js b/src/redux/actions/Connect.js index e9841104ab..e772d1aae8 100644 --- a/src/redux/actions/Connect.js +++ b/src/redux/actions/Connect.js @@ -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 }) diff --git a/src/redux/reducers/Connect.js b/src/redux/reducers/Connect.js index 0bc93e513a..e3e1b1b823 100644 --- a/src/redux/reducers/Connect.js +++ b/src/redux/reducers/Connect.js @@ -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 = @@ -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) { @@ -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 diff --git a/src/views/demoConnectGuard/DemoConnectGuard-test.tsx b/src/views/demoConnectGuard/DemoConnectGuard-test.tsx new file mode 100644 index 0000000000..471d807e13 --- /dev/null +++ b/src/views/demoConnectGuard/DemoConnectGuard-test.tsx @@ -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(, { 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(, { 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: {}, + }) + }) +}) diff --git a/src/views/demoConnectGuard/DemoConnectGuard.tsx b/src/views/demoConnectGuard/DemoConnectGuard.tsx new file mode 100644 index 0000000000..4c79d2e658 --- /dev/null +++ b/src/views/demoConnectGuard/DemoConnectGuard.tsx @@ -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 ( +
+ +
+ + +
+

+ {__('Demo mode active')} +

+

+ {__( + 'Live institutions are not available in the demo environment. Please select MX Bank to test the connection process.', + )} +

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