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 defaultProps = { + availableAccountTypes: [], + handleConsentGoBack: vi.fn(), + handleCredentialsGoBack: vi.fn(), + navigationRef: React.createRef(), + onManualAccountAdded: vi.fn(), + onUpsertMember: vi.fn(), + setConnectLocalState: vi.fn(), + } + + const mockInstitution = { + guid: 'INS-123', + name: 'Test Bank', + logo_url: 'https://example.com/logo.png', + code: 'TEST', + url: 'https://testbank.com', + } + + const createInitialState = (step) => ({ + browser: { + height: 768, + width: 1024, + size: 'large', + }, + connect: { + location: [{ step }], + selectedInstitution: mockInstitution, + updateCredentials: false, + error: null, + }, + config: { + mode: 'aggregation', + initialConfig: {}, + }, + profiles: { + client: { has_atrium_api: false }, + clientProfile: { + uses_oauth: false, + account_verification_is_enabled: true, + is_microdeposits_enabled: true, + }, + widgetProfile: { + enable_support_requests: true, + show_microdeposits_in_connect: false, + display_delete_option_in_connect: true, + }, + }, + }) + + it('should render DemoConnectGuard when step is DEMO_CONNECT_GUARD', () => { + const initialState = createInitialState(STEPS.DEMO_CONNECT_GUARD) + + const { container } = render(, { + preloadedState: initialState, + }) + + expect(screen.getByText('Demo mode active')).toBeInTheDocument() + expect( + screen.getByText(/Live institutions are not available in the demo environment/i), + ).toBeInTheDocument() + expect(screen.getByText('MX Bank')).toBeInTheDocument() + + const logo = screen.getByAltText('Logo for Test Bank') + expect(logo).toBeInTheDocument() + expect(logo).toHaveAttribute('src', mockInstitution.logo_url) + + const errorIcon = container.querySelector('svg.MuiSvgIcon-colorError') + expect(errorIcon).toBeInTheDocument() + + const button = screen.getByRole('button', { name: /return to institution selection/i }) + expect(button).toBeInTheDocument() + }) +}) diff --git a/src/const/Connect.js b/src/const/Connect.js index 0dd4b07485..dc73c577e9 100644 --- a/src/const/Connect.js +++ b/src/const/Connect.js @@ -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', diff --git a/src/const/language/es.json b/src/const/language/es.json index a3ff3087f4..550d0a5b76 100644 --- a/src/const/language/es.json +++ b/src/const/language/es.json @@ -421,6 +421,8 @@ "Profile information": "Información del perfil", "Account numbers": "Números de cuenta", "To complete your connection, please %1share%2 the following after signing in:": "Para completar su conexión, por favor, %1comparta%2 lo siguiente después de iniciar sesión:", + "Demo mode active": "Modo de demostración activo", + "Live institutions are not available in the demo environment. Please select *MX Bank* to test the connection process.": "Las instituciones en vivo no están disponibles en el entorno de demostración. Seleccione *MX Bank* para probar el proceso de conexión.", "connect/disclosure/button\u0004Continue": "Continuar", "connect/disclosure/policy/text\u0004By clicking Continue, you agree to the ": "Al hacer clic en Continuar, tu aceptas la ", "connect/disclosure/policy/link\u0004MX Privacy Policy.": "Política de privacidad de Money Experience.", diff --git a/src/const/language/es.po b/src/const/language/es.po index 11247d9da6..b713fb6bf8 100644 --- a/src/const/language/es.po +++ b/src/const/language/es.po @@ -1986,6 +1986,7 @@ msgstr "No, solo añadir gestión financiera" msgid "DISABLED" msgstr "DESACTIVADO" +#: src/views/demoConnectGuard/DemoConnectGuard.tsx #: src/views/institutionStatusDetails/InstitutionStatusDetails.tsx msgid "Logo for %1" msgstr "Logotipo de %1" @@ -2010,6 +2011,7 @@ msgstr "¡Éxito!" msgid "No accounts found" msgstr "No se encontraron cuentas" +#: src/views/demoConnectGuard/DemoConnectGuard.tsx #: src/views/actionableError/useActionableErrorMap.tsx msgid "Return to institution selection" msgstr "Volver a la selección de instituciones" @@ -2104,3 +2106,13 @@ msgid "" msgstr "" "Para completar su conexión, por favor, %1comparta%2 lo siguiente después de " "iniciar sesión:" + +#: src/views/demoConnectGuard/DemoConnectGuard.tsx +msgid "Demo mode active" +msgstr "Modo de demostración activo" + +#: src/views/demoConnectGuard/DemoConnectGuard.tsx +msgid "" +"Live institutions are not available in the demo environment. Please select " +"*MX Bank* to test the connection process." +msgstr "Las instituciones en vivo no están disponibles en el entorno de demostración. Seleccione *MX Bank* para probar el proceso de conexión." diff --git a/src/const/language/frCa.json b/src/const/language/frCa.json index 81f997538e..b9ee931572 100644 --- a/src/const/language/frCa.json +++ b/src/const/language/frCa.json @@ -422,6 +422,8 @@ "Profile information": "Informations de profil", "Account numbers": "Numéros de compte", "To complete your connection, please %1share%2 the following after signing in:": "Pour finaliser votre connexion, veuillez %1partager%2 les informations suivantes après vous être connecté :", + "Demo mode active": "Mode démo actif", + "Live institutions are not available in the demo environment. Please select *MX Bank* to test the connection process.": "Les établissements réels ne sont pas disponibles dans l'environnement de démonstration. Veuillez sélectionner *MX Bank* pour tester la procédure de connexion.", "connect/disclosure/policy/text\u0004By clicking Continue, you agree to the ": "En cliquant sur Continuer, vous acceptez la ", "connect/disclosure/policy/link\u0004MX Privacy Policy.": "Politique de confidentialité de MX.", "connect/disclosure/policy/link\u0004MX Privacy Policy": "Politique de confidentialité de MX.", diff --git a/src/const/language/frCa.po b/src/const/language/frCa.po index 02a7af67e1..f2408b2d7e 100644 --- a/src/const/language/frCa.po +++ b/src/const/language/frCa.po @@ -2063,6 +2063,7 @@ msgstr "Non, ajoutez uniquement la gestion financière" msgid "DISABLED" msgstr "DÉSACTIVÉ" +#: src/views/demoConnectGuard/DemoConnectGuard.tsx #: src/views/institutionStatusDetails/InstitutionStatusDetails.tsx msgid "Logo for %1" msgstr "Logo pour %1" @@ -2088,6 +2089,7 @@ msgstr "Succès!" msgid "No accounts found" msgstr "Aucun compte trouvé" +#: src/views/demoConnectGuard/DemoConnectGuard.tsx #: src/views/actionableError/useActionableErrorMap.tsx msgid "Return to institution selection" msgstr "Retour à la sélection des établissements" @@ -2182,3 +2184,13 @@ msgid "" msgstr "" "Pour finaliser votre connexion, veuillez %1partager%2 les informations " "suivantes après vous être connecté :" + +#: src/views/demoConnectGuard/DemoConnectGuard.tsx +msgid "Demo mode active" +msgstr "Mode démo actif" + +#: src/views/demoConnectGuard/DemoConnectGuard.tsx +msgid "" +"Live institutions are not available in the demo environment. Please select " +"*MX Bank* to test the connection process." +msgstr "Les établissements réels ne sont pas disponibles dans l'environnement de démonstration. Veuillez sélectionner *MX Bank* pour tester la procédure de connexion." diff --git a/src/hooks/useSelectInstitution.tsx b/src/hooks/useSelectInstitution.tsx index 65f6726993..5e6aaaa37b 100644 --- a/src/hooks/useSelectInstitution.tsx +++ b/src/hooks/useSelectInstitution.tsx @@ -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) => { @@ -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/redux/reducers/__tests__/Connect-test.js b/src/redux/reducers/__tests__/Connect-test.js index 578f50ec13..84b30b3472 100644 --- a/src/redux/reducers/__tests__/Connect-test.js +++ b/src/redux/reducers/__tests__/Connect-test.js @@ -640,6 +640,19 @@ describe('Connect redux store', () => { STEPS.INSTITUTION_STATUS_DETAILS, ) }) + + it('should set the step to DEMO_CONNECT_GUARD when the user is a demo user but the institution is not a demo institution', () => { + const institution = { guid: 'INST-1', is_demo: false, credentials } + const user = { guid: 'USR-1', is_demo: true } + const afterState = reducer(defaultState, { + type: ActionTypes.SELECT_INSTITUTION_SUCCESS, + payload: { institution, user }, + }) + + expect(afterState.location[afterState.location.length - 1].step).toEqual( + STEPS.DEMO_CONNECT_GUARD, + ) + }) }) describe('LOAD_CONNECT', () => { diff --git a/src/views/demoConnectGuard/DemoConnectGuard-test.tsx b/src/views/demoConnectGuard/DemoConnectGuard-test.tsx new file mode 100644 index 0000000000..5e62dc86f1 --- /dev/null +++ b/src/views/demoConnectGuard/DemoConnectGuard-test.tsx @@ -0,0 +1,68 @@ +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/i), + ).toBeInTheDocument() + expect(screen.getByText('MX Bank')).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.getByRole('button', { name: /return to institution selection/i }) + 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..831e1335e3 --- /dev/null +++ b/src/views/demoConnectGuard/DemoConnectGuard.tsx @@ -0,0 +1,98 @@ +import React 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 { __, B } 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 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', + lineHeight: '24px', + '& strong': { + fontWeight: 700, + }, + }, +})