From 06b33b5f9e9cf0eeaa124881b24e0a87d7a26eab Mon Sep 17 00:00:00 2001 From: Clement Mwimo Date: Thu, 28 May 2026 15:35:55 -0600 Subject: [PATCH 1/8] =?UTF-8?q?Revert=20"fix:=20revert=20show=20demo=20con?= =?UTF-8?q?nect=20guard=20for=20demo=20users=20launching=20widgets=20?= =?UTF-8?q?=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit c158c7613dd8fc969a84d799dceb2f160613e688. --- src/__tests__/Connect-test.tsx | 69 ++++++++++++ src/hooks/useLoadConnect.tsx | 8 +- src/redux/actions/Connect.js | 12 ++ src/redux/reducers/Connect.js | 12 ++ src/redux/reducers/__tests__/Connect-test.js | 109 +++++++++++++++---- 5 files changed, 182 insertions(+), 28 deletions(-) create mode 100644 src/__tests__/Connect-test.tsx diff --git a/src/__tests__/Connect-test.tsx b/src/__tests__/Connect-test.tsx new file mode 100644 index 0000000000..d598887759 --- /dev/null +++ b/src/__tests__/Connect-test.tsx @@ -0,0 +1,69 @@ +import React from 'react' +import { describe, it, expect, vi } from 'vitest' +import { screen } from '@testing-library/react' + +import { Connect } from '../Connect' +import { render } from 'src/utilities/testingLibrary' +import { apiValue as apiValueMock } from 'src/const/apiProviderMock' +import { masterData, institutionData } from 'src/services/mockedData' + +describe('Connect - Demo Connect Guard', () => { + const defaultProps = { + clientConfig: { current_institution_guid: 'INS-123' } as ClientConfigType, + onShowConnectSuccessSurvey: () => undefined, + onSubmitConnectSuccessSurvey: () => {}, + profiles: { ...masterData, loading: false }, + } + + const nonDemoInstitution = { ...institutionData.institution, is_demo: false } + const demoInstitution = { ...institutionData.institution, is_demo: true } + const demoUser = { ...masterData.user, is_demo: true } + const regularUser = { ...masterData.user, is_demo: false } + + it('blocks demo user from accessing non-demo institution', async () => { + const mockApiValue = { + ...apiValueMock, + loadInstitutionByGuid: vi.fn().mockResolvedValue(nonDemoInstitution), + loadMembers: vi.fn().mockResolvedValue([]), + } + + render( + , + { apiValue: mockApiValue }, + ) + + expect(await screen.findByText(/Demo mode active/i)).toBeInTheDocument() + }) + + it('allows demo user to access demo institution', async () => { + const mockApiValue = { + ...apiValueMock, + loadInstitutionByGuid: vi.fn().mockResolvedValue(demoInstitution), + loadMembers: vi.fn().mockResolvedValue([]), + } + + render( + , + { apiValue: mockApiValue }, + ) + + expect(await screen.findByText(/Log in at Test Bank/i)).toBeInTheDocument() + expect(screen.queryByText(/Demo mode active/i)).not.toBeInTheDocument() + }) + + it('allows regular user to access non-demo institution', async () => { + const mockApiValue = { + ...apiValueMock, + loadInstitutionByGuid: vi.fn().mockResolvedValue(nonDemoInstitution), + loadMembers: vi.fn().mockResolvedValue([]), + } + + render( + , + { apiValue: mockApiValue }, + ) + + expect(await screen.findByText(/Log in at Test Bank/i)).toBeInTheDocument() + expect(screen.queryByText(/Demo mode active/i)).not.toBeInTheDocument() + }) +}) diff --git a/src/hooks/useLoadConnect.tsx b/src/hooks/useLoadConnect.tsx index 8bbd416c60..f21cc302ee 100644 --- a/src/hooks/useLoadConnect.tsx +++ b/src/hooks/useLoadConnect.tsx @@ -8,14 +8,14 @@ import _isEmpty from 'lodash/isEmpty' import { loadConnect as loadConnectStart, - loadConnectSuccess, + loadConnectSuccessWithProfile, loadConnectError, } from 'src/redux/actions/Connect' import { COMBO_JOB_DATA_TYPES } from 'src/const/comboJobDataTypes' import { VERIFY_MODE } from 'src/const/Connect' import { useApi, ApiContextTypes } from 'src/context/ApiContext' import { __ } from 'src/utilities/Intl' -import type { RootState } from 'src/redux/Store' +import type { RootState, AppDispatch } from 'src/redux/Store' import { instutionSupportRequestedProducts } from 'src/utilities/Institution' import { getExperimentalFeatures } from 'src/redux/reducers/experimentalFeaturesSlice' @@ -53,7 +53,7 @@ const useLoadConnect = () => { return document.querySelector('html')?.getAttribute('lang') || 'en' }, [document.querySelector('html')?.getAttribute('lang')]) const [config, setConfig] = useState({} as ClientConfigType) - const dispatch = useDispatch() + const dispatch = useDispatch() const loadConnect = useCallback((config: ClientConfigType) => setConfig(config), [config]) @@ -78,7 +78,7 @@ const useLoadConnect = () => { if (clientSupportRequestedProducts(config, profiles.clientProfile)) { return from(api.loadMembers(clientLocale)).pipe( map((members = []) => - loadConnectSuccess({ + loadConnectSuccessWithProfile({ experimentalFeatures, members, widgetProfile: profiles.widgetProfile, diff --git a/src/redux/actions/Connect.js b/src/redux/actions/Connect.js index 4ee4dfbbfe..8b365df66b 100644 --- a/src/redux/actions/Connect.js +++ b/src/redux/actions/Connect.js @@ -58,6 +58,18 @@ export const loadConnectSuccess = (dependencies = {}) => ({ type: ActionTypes.LOAD_CONNECT_SUCCESS, payload: dependencies, }) +export const loadConnectSuccessWithProfile = + (dependencies = {}) => + (dispatch, getState) => { + const { profiles } = getState() + + dispatch( + loadConnectSuccess({ + ...dependencies, + user: profiles.user, + }), + ) + } export const loadConnectError = (err) => ({ type: ActionTypes.LOAD_CONNECT_ERROR, diff --git a/src/redux/reducers/Connect.js b/src/redux/reducers/Connect.js index 5844271c30..1674c91268 100644 --- a/src/redux/reducers/Connect.js +++ b/src/redux/reducers/Connect.js @@ -65,6 +65,7 @@ const loadConnectSuccess = (state, action) => { institution = {}, experimentalFeatures = {}, widgetProfile, + user = {}, } = action.payload return { @@ -83,6 +84,7 @@ const loadConnectSuccess = (state, action) => { institution, widgetProfile, experimentalFeatures, + user, ), ), selectedInstitution: institution, @@ -548,6 +550,7 @@ function getStartingStep( institution, widgetProfile, experimentalFeatures = {}, + user = {}, ) { // Unavailable institutions experimental feature: Make sure we don't load a user // directly to an institution that should be unavailable. @@ -574,9 +577,18 @@ function getStartingStep( (institution && institutionIsBlockedForCostReasons(institution)) || (member && memberIsBlockedForCostReasons(member)) || !institutionIsAvailable + const shouldStepToDemoConnectGuard = + user?.is_demo && + institution && + !institution?.is_demo && + (config.current_institution_guid || + config.current_institution_code || + config.current_member_guid) if (shouldStepToInstitutionStatusDetails) { return STEPS.INSTITUTION_STATUS_DETAILS + } else if (shouldStepToDemoConnectGuard) { + return STEPS.DEMO_CONNECT_GUARD } else if (shouldStepToMFA) // They configured connect to resolve MFA on a member. return STEPS.MFA diff --git a/src/redux/reducers/__tests__/Connect-test.js b/src/redux/reducers/__tests__/Connect-test.js index 3751806e11..e3c41c28de 100644 --- a/src/redux/reducers/__tests__/Connect-test.js +++ b/src/redux/reducers/__tests__/Connect-test.js @@ -389,6 +389,85 @@ describe('Connect redux store', () => { STEPS.ENTER_CREDENTIALS, ) }) + + it('should set the step to DEMO_CONNECT_GUARD when launching with current_institution_guid and user is demo but institution is not', () => { + const institution = { guid: 'INS-1', is_demo: false, credentials } + const user = { guid: 'USR-1', is_demo: true } + const config = { current_institution_guid: 'INS-1' } + const afterState = reducer( + defaultState, + loadConnectSuccess({ institution, config, widgetProfile, user }), + ) + + expect(afterState.location[afterState.location.length - 1].step).toEqual( + STEPS.DEMO_CONNECT_GUARD, + ) + }) + + it('should set the step to DEMO_CONNECT_GUARD when launching with current_institution_code and user is demo but institution is not', () => { + const institution = { guid: 'INS-1', code: 'bank_code', is_demo: false, credentials } + const user = { guid: 'USR-1', is_demo: true } + const config = { current_institution_code: 'bank_code' } + const afterState = reducer( + defaultState, + loadConnectSuccess({ institution, config, widgetProfile, user }), + ) + + expect(afterState.location[afterState.location.length - 1].step).toEqual( + STEPS.DEMO_CONNECT_GUARD, + ) + }) + + it('should set the step to DEMO_CONNECT_GUARD when launching with current_member_guid and user is demo but institution is not', () => { + const institution = { guid: 'INS-1', is_demo: false, credentials } + const user = { guid: 'USR-1', is_demo: true } + const member = genMember({ guid: 'MBR-1', connection_status: ReadableStatuses.CONNECTED }) + const config = { current_member_guid: 'MBR-1' } + const afterState = reducer( + defaultState, + loadConnectSuccess({ member, institution, config, widgetProfile, user }), + ) + + expect(afterState.location[afterState.location.length - 1].step).toEqual( + STEPS.DEMO_CONNECT_GUARD, + ) + }) + + it('should NOT set the step to DEMO_CONNECT_GUARD when launching with current_institution_guid but user is not demo', () => { + const institution = { guid: 'INS-1', is_demo: false, credentials } + const user = { guid: 'USR-1', is_demo: false } + const config = { current_institution_guid: 'INS-1' } + const afterState = reducer( + defaultState, + loadConnectSuccess({ institution, config, widgetProfile, user }), + ) + + expect(afterState.location[afterState.location.length - 1].step).toEqual( + STEPS.ENTER_CREDENTIALS, + ) + }) + + it('should NOT set the step to DEMO_CONNECT_GUARD when launching with current_institution_guid and both user and institution are demo', () => { + const institution = { guid: 'INS-1', is_demo: true, credentials } + const user = { guid: 'USR-1', is_demo: true } + const config = { current_institution_guid: 'INS-1' } + const afterState = reducer( + defaultState, + loadConnectSuccess({ institution, config, widgetProfile, user }), + ) + + expect(afterState.location[afterState.location.length - 1].step).toEqual( + STEPS.ENTER_CREDENTIALS, + ) + }) + + it('should NOT set the step to DEMO_CONNECT_GUARD when user is demo but no institution parameters are provided', () => { + const user = { guid: 'USR-1', is_demo: true } + const config = {} + const afterState = reducer(defaultState, loadConnectSuccess({ config, widgetProfile, user })) + + expect(afterState.location[afterState.location.length - 1].step).toEqual(STEPS.SEARCH) + }) }) describe('loadConnectError', () => { @@ -455,10 +534,7 @@ describe('Connect redux store', () => { const config = { mode: VERIFY_MODE } const afterState = reducer( { ...defaultState, isComponentLoading: true }, - { - type: ActionTypes.LOAD_CONNECT_SUCCESS, - payload: { config, members: [], widgetProfile }, - }, + loadConnectSuccess({ config, members: [], widgetProfile }), ) expect(afterState.location[afterState.location.length - 1].step).toEqual(STEPS.SEARCH) }) @@ -473,10 +549,7 @@ describe('Connect redux store', () => { const members = [member] const afterState = reducer( { ...defaultState, isComponentLoading: true }, - { - type: ActionTypes.LOAD_CONNECT_SUCCESS, - payload: { config, member, members, widgetProfile }, - }, + loadConnectSuccess({ config, member, members, widgetProfile }), ) expect(afterState.location[afterState.location.length - 1].step).toEqual( STEPS.ACTIONABLE_ERROR, @@ -500,10 +573,7 @@ describe('Connect redux store', () => { const members = [member] const afterState = reducer( { ...defaultState, isComponentLoading: true }, - { - type: ActionTypes.LOAD_CONNECT_SUCCESS, - payload: { config, member, members, widgetProfile }, - }, + loadConnectSuccess({ config, member, members, widgetProfile }), ) expect(afterState.location[afterState.location.length - 1].step).toEqual( STEPS.ACTIONABLE_ERROR, @@ -527,10 +597,7 @@ describe('Connect redux store', () => { const members = [member] const afterState = reducer( { ...defaultState, isComponentLoading: true }, - { - type: ActionTypes.LOAD_CONNECT_SUCCESS, - payload: { config, member, members, widgetProfile }, - }, + loadConnectSuccess({ config, member, members, widgetProfile }), ) expect(afterState.location[afterState.location.length - 1].step).toEqual( STEPS.ENTER_CREDENTIALS, @@ -554,10 +621,7 @@ describe('Connect redux store', () => { const members = [member] const afterState = reducer( { ...defaultState, isComponentLoading: true }, - { - type: ActionTypes.LOAD_CONNECT_SUCCESS, - payload: { config, member, members, widgetProfile }, - }, + loadConnectSuccess({ config, member, members, widgetProfile }), ) expect(afterState.location[afterState.location.length - 1].step).toEqual(STEPS.MFA) }) @@ -578,10 +642,7 @@ describe('Connect redux store', () => { const members = [member] const afterState = reducer( { ...defaultState, isComponentLoading: true }, - { - type: ActionTypes.LOAD_CONNECT_SUCCESS, - payload: { config, member, members, accounts: [], widgetProfile }, - }, + loadConnectSuccess({ config, member, members, accounts: [], widgetProfile }), ) expect(afterState.location[afterState.location.length - 1].step).toEqual( STEPS.ACTIONABLE_ERROR, From 81f3da983e7891afca1efd4f79fa031422b01826 Mon Sep 17 00:00:00 2001 From: Clement Mwimo Date: Tue, 25 Aug 2026 16:35:35 -0600 Subject: [PATCH 2/8] rename the test file --- src/{Connect-test.tsx => Connect.test.tsx} | 67 ++++++++++++++++++++- src/__tests__/Connect-test.tsx | 69 ---------------------- 2 files changed, 66 insertions(+), 70 deletions(-) rename src/{Connect-test.tsx => Connect.test.tsx} (74%) delete mode 100644 src/__tests__/Connect-test.tsx diff --git a/src/Connect-test.tsx b/src/Connect.test.tsx similarity index 74% rename from src/Connect-test.tsx rename to src/Connect.test.tsx index bb736254e2..d77b7cce22 100644 --- a/src/Connect-test.tsx +++ b/src/Connect.test.tsx @@ -2,7 +2,8 @@ import React from 'react' import { beforeEach, describe, it, expect, vi, afterEach } from 'vitest' import { render, screen, waitFor } from 'src/utilities/testingLibrary' import { Connect } from './Connect' -import { initialState, masterData } from 'src/services/mockedData' +import { apiValue as apiValueMock } from 'src/const/apiProviderMock' +import { initialState, masterData, institutionData } from 'src/services/mockedData' import { STEPS } from 'src/const/Connect' describe('', () => { @@ -261,4 +262,68 @@ describe('', () => { }) }) }) + + describe('Connect - Demo Connect Guard', () => { + const defaultProps = { + clientConfig: { current_institution_guid: 'INS-123' } as ClientConfigType, + onShowConnectSuccessSurvey: () => undefined, + onSubmitConnectSuccessSurvey: () => {}, + profiles: { ...masterData, loading: false }, + } + + const nonDemoInstitution = { ...institutionData.institution, is_demo: false } + const demoInstitution = { ...institutionData.institution, is_demo: true } + const demoUser = { ...masterData.user, is_demo: true } + const regularUser = { ...masterData.user, is_demo: false } + + it('blocks demo user from accessing non-demo institution', async () => { + const mockApiValue = { + ...apiValueMock, + loadInstitutionByGuid: vi.fn().mockResolvedValue(nonDemoInstitution), + loadMembers: vi.fn().mockResolvedValue([]), + } + + render( + , + { apiValue: mockApiValue }, + ) + + expect(await screen.findByText(/Demo mode active/i)).toBeInTheDocument() + }) + + it('allows demo user to access demo institution', async () => { + const mockApiValue = { + ...apiValueMock, + loadInstitutionByGuid: vi.fn().mockResolvedValue(demoInstitution), + loadMembers: vi.fn().mockResolvedValue([]), + } + + render( + , + { apiValue: mockApiValue }, + ) + + expect(await screen.findByText(/Log in at Test Bank/i)).toBeInTheDocument() + expect(screen.queryByText(/Demo mode active/i)).not.toBeInTheDocument() + }) + + it('allows regular user to access non-demo institution', async () => { + const mockApiValue = { + ...apiValueMock, + loadInstitutionByGuid: vi.fn().mockResolvedValue(nonDemoInstitution), + loadMembers: vi.fn().mockResolvedValue([]), + } + + render( + , + { apiValue: mockApiValue }, + ) + + expect(await screen.findByText(/Log in at Test Bank/i)).toBeInTheDocument() + expect(screen.queryByText(/Demo mode active/i)).not.toBeInTheDocument() + }) + }) }) diff --git a/src/__tests__/Connect-test.tsx b/src/__tests__/Connect-test.tsx deleted file mode 100644 index d598887759..0000000000 --- a/src/__tests__/Connect-test.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react' -import { describe, it, expect, vi } from 'vitest' -import { screen } from '@testing-library/react' - -import { Connect } from '../Connect' -import { render } from 'src/utilities/testingLibrary' -import { apiValue as apiValueMock } from 'src/const/apiProviderMock' -import { masterData, institutionData } from 'src/services/mockedData' - -describe('Connect - Demo Connect Guard', () => { - const defaultProps = { - clientConfig: { current_institution_guid: 'INS-123' } as ClientConfigType, - onShowConnectSuccessSurvey: () => undefined, - onSubmitConnectSuccessSurvey: () => {}, - profiles: { ...masterData, loading: false }, - } - - const nonDemoInstitution = { ...institutionData.institution, is_demo: false } - const demoInstitution = { ...institutionData.institution, is_demo: true } - const demoUser = { ...masterData.user, is_demo: true } - const regularUser = { ...masterData.user, is_demo: false } - - it('blocks demo user from accessing non-demo institution', async () => { - const mockApiValue = { - ...apiValueMock, - loadInstitutionByGuid: vi.fn().mockResolvedValue(nonDemoInstitution), - loadMembers: vi.fn().mockResolvedValue([]), - } - - render( - , - { apiValue: mockApiValue }, - ) - - expect(await screen.findByText(/Demo mode active/i)).toBeInTheDocument() - }) - - it('allows demo user to access demo institution', async () => { - const mockApiValue = { - ...apiValueMock, - loadInstitutionByGuid: vi.fn().mockResolvedValue(demoInstitution), - loadMembers: vi.fn().mockResolvedValue([]), - } - - render( - , - { apiValue: mockApiValue }, - ) - - expect(await screen.findByText(/Log in at Test Bank/i)).toBeInTheDocument() - expect(screen.queryByText(/Demo mode active/i)).not.toBeInTheDocument() - }) - - it('allows regular user to access non-demo institution', async () => { - const mockApiValue = { - ...apiValueMock, - loadInstitutionByGuid: vi.fn().mockResolvedValue(nonDemoInstitution), - loadMembers: vi.fn().mockResolvedValue([]), - } - - render( - , - { apiValue: mockApiValue }, - ) - - expect(await screen.findByText(/Log in at Test Bank/i)).toBeInTheDocument() - expect(screen.queryByText(/Demo mode active/i)).not.toBeInTheDocument() - }) -}) From 0d51178eace3d00443f0451a9109af241777f653 Mon Sep 17 00:00:00 2001 From: Clement Mwimo Date: Tue, 25 Aug 2026 17:01:24 -0600 Subject: [PATCH 3/8] feat: show Connect Guard for demo users launching widgets on non-demo institutions From 766fe66e084891200dae47d8a44985ba71315519 Mon Sep 17 00:00:00 2001 From: Clement Mwimo Date: Thu, 27 Aug 2026 13:12:08 -0600 Subject: [PATCH 4/8] use commpont level gate approach --- src/Connect.test.tsx | 47 ++++++++ src/components/RenderConnectStep-test.jsx | 76 ++++++++++++- src/components/RenderConnectStep.js | 23 +++- src/const/Connect.js | 1 - src/redux/reducers/Connect.js | 15 --- src/redux/reducers/__tests__/Connect-test.js | 92 --------------- .../DemoConnectGuard-test.tsx | 18 ++- .../demoConnectGuard/DemoConnectGuard.tsx | 106 ++++++++++-------- 8 files changed, 215 insertions(+), 163 deletions(-) diff --git a/src/Connect.test.tsx b/src/Connect.test.tsx index d77b7cce22..9060e22868 100644 --- a/src/Connect.test.tsx +++ b/src/Connect.test.tsx @@ -5,6 +5,7 @@ import { Connect } from './Connect' import { apiValue as apiValueMock } from 'src/const/apiProviderMock' import { initialState, masterData, institutionData } from 'src/services/mockedData' import { STEPS } from 'src/const/Connect' +import { createRenderConnectStepInitialState } from 'src/utilities/test/createRenderConnectStepInitialState' describe('', () => { const mockPostMessage = vi.fn() @@ -325,5 +326,51 @@ describe('', () => { expect(await screen.findByText(/Log in at Test Bank/i)).toBeInTheDocument() expect(screen.queryByText(/Demo mode active/i)).not.toBeInTheDocument() }) + + describe('back button', () => { + const props = { + ...defaultProps, + clientConfig: {} as ClientConfigType, + profiles: { ...masterData, user: demoUser, loading: false }, + } + const guardState = (steps: string[]) => { + const base = createRenderConnectStepInitialState( + steps[steps.length - 1], + nonDemoInstitution, + ) + return { + ...base, + connect: { + ...base.connect, + isComponentLoading: false, + location: steps.map((s) => ({ step: s })), + }, + } + } + + it('is hidden when guard fires as the first screen', async () => { + render(, { preloadedState: guardState([STEPS.ENTER_CREDENTIALS]) }) + await waitFor(() => expect(screen.getByText(/Demo mode active/i)).toBeInTheDocument()) + expect(screen.queryByTestId('back-button')).not.toBeInTheDocument() + }) + + it('navigates back to search when the previous step is search', async () => { + const { user } = render(, { + preloadedState: guardState([STEPS.SEARCH, STEPS.ENTER_CREDENTIALS]), + }) + await waitFor(() => expect(screen.getByTestId('back-button')).toBeInTheDocument()) + await user.click(screen.getByTestId('back-button')) + expect(screen.queryByText('Select your institution')).toBeInTheDocument() + }) + + it('pops one step back rather than resetting to search', async () => { + const { user } = render(, { + preloadedState: guardState([STEPS.SEARCH, STEPS.CONSENT, STEPS.ENTER_CREDENTIALS]), + }) + await waitFor(() => expect(screen.getByTestId('back-button')).toBeInTheDocument()) + await user.click(screen.getByTestId('back-button')) + await waitFor(() => expect(screen.getByText(/Demo mode active/i)).toBeInTheDocument()) + }) + }) }) }) diff --git a/src/components/RenderConnectStep-test.jsx b/src/components/RenderConnectStep-test.jsx index 34674b7c27..eef0c2be07 100644 --- a/src/components/RenderConnectStep-test.jsx +++ b/src/components/RenderConnectStep-test.jsx @@ -36,8 +36,15 @@ describe('RenderConnectStep', () => { }) describe('Step Rendering', () => { - it('should render DemoConnectGuard when step is DEMO_CONNECT_GUARD', () => { - const state = createRenderConnectStepInitialState(STEPS.DEMO_CONNECT_GUARD, mockInstitution) + it('should render DemoConnectGuard when user is demo and institution is not demo', () => { + const nonDemoInstitution = { ...mockInstitution, is_demo: false } + const state = { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, nonDemoInstitution), + profiles: { + ...initialState.profiles, + user: { ...initialState.profiles.user, is_demo: true }, + }, + } const { container } = render(, { preloadedState: state, @@ -60,6 +67,69 @@ describe('RenderConnectStep', () => { expect(button).toBeInTheDocument() }) + it('should render DemoConnectGuard on ACTIONABLE_ERROR when user is demo and institution is not demo', () => { + const nonDemoInstitution = { ...mockInstitution, is_demo: false } + const state = { + ...createRenderConnectStepInitialState(STEPS.ACTIONABLE_ERROR, nonDemoInstitution), + profiles: { + ...initialState.profiles, + user: { ...initialState.profiles.user, is_demo: true }, + }, + } + + render(, { preloadedState: state }) + + expect(screen.getByText('Demo mode active')).toBeInTheDocument() + }) + + it('should NOT render DemoConnectGuard on INSTITUTION_STATUS_DETAILS even when user is demo and institution is not demo', () => { + const nonDemoInstitution = { ...mockInstitution, is_demo: false } + const state = { + ...createRenderConnectStepInitialState( + STEPS.INSTITUTION_STATUS_DETAILS, + nonDemoInstitution, + ), + profiles: { + ...initialState.profiles, + user: { ...initialState.profiles.user, is_demo: true }, + }, + } + + render(, { preloadedState: state }) + + expect(screen.queryByText('Demo mode active')).not.toBeInTheDocument() + }) + + it('should NOT render DemoConnectGuard when user is demo but institution is also demo', () => { + const demoInstitution = { ...mockInstitution, is_demo: true } + const state = { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, demoInstitution), + profiles: { + ...initialState.profiles, + user: { ...initialState.profiles.user, is_demo: true }, + }, + } + + render(, { preloadedState: state }) + + expect(screen.queryByText('Demo mode active')).not.toBeInTheDocument() + }) + + it('should NOT render DemoConnectGuard when user is not demo', () => { + const nonDemoInstitution = { ...mockInstitution, is_demo: false } + const state = { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, nonDemoInstitution), + profiles: { + ...initialState.profiles, + user: { ...initialState.profiles.user, is_demo: false }, + }, + } + + render(, { preloadedState: state }) + + expect(screen.queryByText('Demo mode active')).not.toBeInTheDocument() + }) + it('should render Search view for SEARCH step', async () => { const state = createRenderConnectStepInitialState(STEPS.SEARCH) @@ -244,7 +314,7 @@ describe('RenderConnectStep', () => { }) it('should not apply maxHeight for non-SEARCH steps', () => { - const state = createRenderConnectStepInitialState(STEPS.DEMO_CONNECT_GUARD, mockInstitution) + const state = createRenderConnectStepInitialState(STEPS.CONNECTING, mockInstitution) const { container } = render(, { preloadedState: state, diff --git a/src/components/RenderConnectStep.js b/src/components/RenderConnectStep.js index 06569aba79..b67e9042a6 100644 --- a/src/components/RenderConnectStep.js +++ b/src/components/RenderConnectStep.js @@ -87,8 +87,29 @@ const RenderConnectStep = (props) => { const hasAtriumAPI = client.has_atrium_api const showSupport = widgetProfile.enable_support_requests && mode === AGG_MODE + const user = useSelector((state) => state.profiles.user) const isDeleteInstitutionOptionEnabled = widgetProfile?.display_delete_option_in_connect ?? true + const STEPS_TO_DEMO_GUARD = [ + STEPS.ADDITIONAL_PRODUCT, + STEPS.CONSENT, + STEPS.ENTER_CREDENTIALS, + STEPS.CONNECTING, + STEPS.MFA, + STEPS.CONNECTED, + STEPS.ACTIONABLE_ERROR, + STEPS.OAUTH_ERROR, + ] + const shouldShowDemoConnectGuard = + STEPS_TO_DEMO_GUARD.includes(step) && + user?.is_demo && + selectedInstitution?.guid && + !selectedInstitution?.is_demo + + if (shouldShowDemoConnectGuard) { + return + } + let connectStepView = null if (step === STEPS.DISCLOSURE) { @@ -111,8 +132,6 @@ 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 = ( { institution = {}, experimentalFeatures = {}, widgetProfile, - user = {}, } = action.payload return { @@ -84,7 +83,6 @@ const loadConnectSuccess = (state, action) => { institution, widgetProfile, experimentalFeatures, - user, ), ), selectedInstitution: institution, @@ -299,8 +297,6 @@ const selectInstitutionSuccess = (state, action) => { institutionStatusIsUnavailable(action.payload.institutionStatus)) ) { 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) { @@ -550,7 +546,6 @@ function getStartingStep( institution, widgetProfile, experimentalFeatures = {}, - user = {}, ) { // Unavailable institutions experimental feature: Make sure we don't load a user // directly to an institution that should be unavailable. @@ -577,18 +572,8 @@ function getStartingStep( (institution && institutionIsBlockedForCostReasons(institution)) || (member && memberIsBlockedForCostReasons(member)) || !institutionIsAvailable - const shouldStepToDemoConnectGuard = - user?.is_demo && - institution && - !institution?.is_demo && - (config.current_institution_guid || - config.current_institution_code || - config.current_member_guid) - if (shouldStepToInstitutionStatusDetails) { return STEPS.INSTITUTION_STATUS_DETAILS - } else if (shouldStepToDemoConnectGuard) { - return STEPS.DEMO_CONNECT_GUARD } else if (shouldStepToMFA) // They configured connect to resolve MFA on a member. return STEPS.MFA diff --git a/src/redux/reducers/__tests__/Connect-test.js b/src/redux/reducers/__tests__/Connect-test.js index e3c41c28de..c6c467a9ef 100644 --- a/src/redux/reducers/__tests__/Connect-test.js +++ b/src/redux/reducers/__tests__/Connect-test.js @@ -389,85 +389,6 @@ describe('Connect redux store', () => { STEPS.ENTER_CREDENTIALS, ) }) - - it('should set the step to DEMO_CONNECT_GUARD when launching with current_institution_guid and user is demo but institution is not', () => { - const institution = { guid: 'INS-1', is_demo: false, credentials } - const user = { guid: 'USR-1', is_demo: true } - const config = { current_institution_guid: 'INS-1' } - const afterState = reducer( - defaultState, - loadConnectSuccess({ institution, config, widgetProfile, user }), - ) - - expect(afterState.location[afterState.location.length - 1].step).toEqual( - STEPS.DEMO_CONNECT_GUARD, - ) - }) - - it('should set the step to DEMO_CONNECT_GUARD when launching with current_institution_code and user is demo but institution is not', () => { - const institution = { guid: 'INS-1', code: 'bank_code', is_demo: false, credentials } - const user = { guid: 'USR-1', is_demo: true } - const config = { current_institution_code: 'bank_code' } - const afterState = reducer( - defaultState, - loadConnectSuccess({ institution, config, widgetProfile, user }), - ) - - expect(afterState.location[afterState.location.length - 1].step).toEqual( - STEPS.DEMO_CONNECT_GUARD, - ) - }) - - it('should set the step to DEMO_CONNECT_GUARD when launching with current_member_guid and user is demo but institution is not', () => { - const institution = { guid: 'INS-1', is_demo: false, credentials } - const user = { guid: 'USR-1', is_demo: true } - const member = genMember({ guid: 'MBR-1', connection_status: ReadableStatuses.CONNECTED }) - const config = { current_member_guid: 'MBR-1' } - const afterState = reducer( - defaultState, - loadConnectSuccess({ member, institution, config, widgetProfile, user }), - ) - - expect(afterState.location[afterState.location.length - 1].step).toEqual( - STEPS.DEMO_CONNECT_GUARD, - ) - }) - - it('should NOT set the step to DEMO_CONNECT_GUARD when launching with current_institution_guid but user is not demo', () => { - const institution = { guid: 'INS-1', is_demo: false, credentials } - const user = { guid: 'USR-1', is_demo: false } - const config = { current_institution_guid: 'INS-1' } - const afterState = reducer( - defaultState, - loadConnectSuccess({ institution, config, widgetProfile, user }), - ) - - expect(afterState.location[afterState.location.length - 1].step).toEqual( - STEPS.ENTER_CREDENTIALS, - ) - }) - - it('should NOT set the step to DEMO_CONNECT_GUARD when launching with current_institution_guid and both user and institution are demo', () => { - const institution = { guid: 'INS-1', is_demo: true, credentials } - const user = { guid: 'USR-1', is_demo: true } - const config = { current_institution_guid: 'INS-1' } - const afterState = reducer( - defaultState, - loadConnectSuccess({ institution, config, widgetProfile, user }), - ) - - expect(afterState.location[afterState.location.length - 1].step).toEqual( - STEPS.ENTER_CREDENTIALS, - ) - }) - - it('should NOT set the step to DEMO_CONNECT_GUARD when user is demo but no institution parameters are provided', () => { - const user = { guid: 'USR-1', is_demo: true } - const config = {} - const afterState = reducer(defaultState, loadConnectSuccess({ config, widgetProfile, user })) - - expect(afterState.location[afterState.location.length - 1].step).toEqual(STEPS.SEARCH) - }) }) describe('loadConnectError', () => { @@ -702,19 +623,6 @@ 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 index d3f393ad37..38ddbe5070 100644 --- a/src/views/demoConnectGuard/DemoConnectGuard-test.tsx +++ b/src/views/demoConnectGuard/DemoConnectGuard-test.tsx @@ -62,13 +62,21 @@ describe('DemoConnectGuard', () => { url: 'https://testbank.com', } - const initialState = createRenderConnectStepInitialState( - STEPS.DEMO_CONNECT_GUARD, - mockInstitution as unknown as InstitutionResponseType, - ) + const nonDemoInstitution = { + ...mockInstitution, + is_demo: false, + } as unknown as InstitutionResponseType + + const state = { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, nonDemoInstitution), + profiles: { + ...initialState.profiles, + user: { ...initialState.profiles.user, is_demo: true }, + }, + } const { user } = render(, { - preloadedState: initialState, + preloadedState: state, }) const returnButton = screen.getByRole('button', { name: /return to institution selection/i }) diff --git a/src/views/demoConnectGuard/DemoConnectGuard.tsx b/src/views/demoConnectGuard/DemoConnectGuard.tsx index 516bd987b4..2eebb7ecc8 100644 --- a/src/views/demoConnectGuard/DemoConnectGuard.tsx +++ b/src/views/demoConnectGuard/DemoConnectGuard.tsx @@ -1,5 +1,6 @@ -import React from 'react' +import React, { useImperativeHandle } from 'react' import { useDispatch, useSelector } from 'react-redux' +import { RootState } from 'src/redux/Store' import { Button } from '@mui/material' import { P, H2 } from '@mxenabled/mxui' import { Icon } from '@mxenabled/mxui' @@ -12,50 +13,65 @@ import * as connectActions from 'src/redux/actions/Connect' import { selectInitialConfig } from 'src/redux/reducers/configSlice' import styles from 'src/views/demoConnectGuard/DemoConnectGuard.module.css' -export const DemoConnectGuard: React.FC = () => { - const institution = useSelector(getSelectedInstitution) - const initialConfig = useSelector(selectInitialConfig) +export type DemoConnectGuardHandle = { + showBackButton: () => boolean +} + +export const DemoConnectGuard = React.forwardRef( + function DemoConnectGuard(_, ref) { + const institution = useSelector(getSelectedInstitution) + const initialConfig = useSelector(selectInitialConfig) + const location = useSelector((state: RootState) => state.connect.location) - const dispatch = useDispatch() + const dispatch = useDispatch() - return ( -
- -
- -
- + useImperativeHandle( + ref, + () => ({ + showBackButton: () => location.length > 1, + }), + [location.length], + ) + + return ( +
+ +
+ +
+ +
-
-

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

-

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

- - -
- ) -} +

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

+

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

+ + +
+ ) + }, +) From 69c6586b0abbefccdf6452f3feaeac3663e301c9 Mon Sep 17 00:00:00 2001 From: Clement Mwimo Date: Thu, 27 Aug 2026 13:25:49 -0600 Subject: [PATCH 5/8] remove unused code --- src/hooks/useLoadConnect.tsx | 4 ++-- src/redux/actions/Connect.js | 12 ------------ src/views/demoConnectGuard/DemoConnectGuard.tsx | 4 ++-- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/hooks/useLoadConnect.tsx b/src/hooks/useLoadConnect.tsx index f21cc302ee..98c71f6090 100644 --- a/src/hooks/useLoadConnect.tsx +++ b/src/hooks/useLoadConnect.tsx @@ -8,7 +8,7 @@ import _isEmpty from 'lodash/isEmpty' import { loadConnect as loadConnectStart, - loadConnectSuccessWithProfile, + loadConnectSuccess, loadConnectError, } from 'src/redux/actions/Connect' import { COMBO_JOB_DATA_TYPES } from 'src/const/comboJobDataTypes' @@ -78,7 +78,7 @@ const useLoadConnect = () => { if (clientSupportRequestedProducts(config, profiles.clientProfile)) { return from(api.loadMembers(clientLocale)).pipe( map((members = []) => - loadConnectSuccessWithProfile({ + loadConnectSuccess({ experimentalFeatures, members, widgetProfile: profiles.widgetProfile, diff --git a/src/redux/actions/Connect.js b/src/redux/actions/Connect.js index 8b365df66b..4ee4dfbbfe 100644 --- a/src/redux/actions/Connect.js +++ b/src/redux/actions/Connect.js @@ -58,18 +58,6 @@ export const loadConnectSuccess = (dependencies = {}) => ({ type: ActionTypes.LOAD_CONNECT_SUCCESS, payload: dependencies, }) -export const loadConnectSuccessWithProfile = - (dependencies = {}) => - (dispatch, getState) => { - const { profiles } = getState() - - dispatch( - loadConnectSuccess({ - ...dependencies, - user: profiles.user, - }), - ) - } export const loadConnectError = (err) => ({ type: ActionTypes.LOAD_CONNECT_ERROR, diff --git a/src/views/demoConnectGuard/DemoConnectGuard.tsx b/src/views/demoConnectGuard/DemoConnectGuard.tsx index 2eebb7ecc8..758c573f68 100644 --- a/src/views/demoConnectGuard/DemoConnectGuard.tsx +++ b/src/views/demoConnectGuard/DemoConnectGuard.tsx @@ -13,11 +13,11 @@ import * as connectActions from 'src/redux/actions/Connect' import { selectInitialConfig } from 'src/redux/reducers/configSlice' import styles from 'src/views/demoConnectGuard/DemoConnectGuard.module.css' -export type DemoConnectGuardHandle = { +export type DemoConnectGuardProps = { showBackButton: () => boolean } -export const DemoConnectGuard = React.forwardRef( +export const DemoConnectGuard = React.forwardRef( function DemoConnectGuard(_, ref) { const institution = useSelector(getSelectedInstitution) const initialConfig = useSelector(selectInitialConfig) From 6b002a5374303f5ecda37f0277ac884a82b23a12 Mon Sep 17 00:00:00 2001 From: Clement Mwimo Date: Thu, 27 Aug 2026 13:34:37 -0600 Subject: [PATCH 6/8] remove more unused code --- src/hooks/useLoadConnect.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useLoadConnect.tsx b/src/hooks/useLoadConnect.tsx index 98c71f6090..8bbd416c60 100644 --- a/src/hooks/useLoadConnect.tsx +++ b/src/hooks/useLoadConnect.tsx @@ -15,7 +15,7 @@ import { COMBO_JOB_DATA_TYPES } from 'src/const/comboJobDataTypes' import { VERIFY_MODE } from 'src/const/Connect' import { useApi, ApiContextTypes } from 'src/context/ApiContext' import { __ } from 'src/utilities/Intl' -import type { RootState, AppDispatch } from 'src/redux/Store' +import type { RootState } from 'src/redux/Store' import { instutionSupportRequestedProducts } from 'src/utilities/Institution' import { getExperimentalFeatures } from 'src/redux/reducers/experimentalFeaturesSlice' @@ -53,7 +53,7 @@ const useLoadConnect = () => { return document.querySelector('html')?.getAttribute('lang') || 'en' }, [document.querySelector('html')?.getAttribute('lang')]) const [config, setConfig] = useState({} as ClientConfigType) - const dispatch = useDispatch() + const dispatch = useDispatch() const loadConnect = useCallback((config: ClientConfigType) => setConfig(config), [config]) From cadc4300fc1b2008fff2a9e98c1cfadd94039028 Mon Sep 17 00:00:00 2001 From: Clement Mwimo Date: Fri, 28 Aug 2026 10:32:36 -0600 Subject: [PATCH 7/8] fix button label --- .../DemoConnectGuard-test.tsx | 62 ++++++++++++------- .../demoConnectGuard/DemoConnectGuard.tsx | 8 ++- 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/src/views/demoConnectGuard/DemoConnectGuard-test.tsx b/src/views/demoConnectGuard/DemoConnectGuard-test.tsx index 38ddbe5070..3d8018096a 100644 --- a/src/views/demoConnectGuard/DemoConnectGuard-test.tsx +++ b/src/views/demoConnectGuard/DemoConnectGuard-test.tsx @@ -3,6 +3,7 @@ import { render, screen } from 'src/utilities/testingLibrary' import { DemoConnectGuard } from './DemoConnectGuard' import { initialState } from 'src/services/mockedData' import RenderConnectStep from 'src/components/RenderConnectStep' +import { PostMessageContext } from 'src/ConnectWidget' import { STEPS } from 'src/const/Connect' import { createRenderConnectStepInitialState } from 'src/utilities/test/createRenderConnectStepInitialState' @@ -15,6 +16,17 @@ describe('DemoConnectGuard', () => { url: 'https://testbank.com', } + const defaultProps = { + availableAccountTypes: [], + handleConsentGoBack: vi.fn(), + handleOAuthGoBack: vi.fn(), + handleCredentialsGoBack: vi.fn(), + navigationRef: vi.fn(), + onManualAccountAdded: vi.fn(), + onUpsertMember: vi.fn(), + setConnectLocalState: vi.fn(), + } + const mockInitialState = { ...initialState, connect: { @@ -38,30 +50,11 @@ describe('DemoConnectGuard', () => { const errorIcon = container.querySelector('svg.MuiSvgIcon-colorError') expect(errorIcon).toBeInTheDocument() - const button = screen.getByRole('button', { name: /return to institution selection/i }) + const button = screen.getByRole('button', { name: /go back/i }) expect(button).toBeInTheDocument() }) it('should navigate back to search when return button is clicked', async () => { - const defaultProps = { - availableAccountTypes: [], - handleConsentGoBack: vi.fn(), - handleOAuthGoBack: vi.fn(), - handleCredentialsGoBack: vi.fn(), - navigationRef: vi.fn(), - 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 nonDemoInstitution = { ...mockInstitution, is_demo: false, @@ -79,9 +72,36 @@ describe('DemoConnectGuard', () => { preloadedState: state, }) - const returnButton = screen.getByRole('button', { name: /return to institution selection/i }) + const returnButton = screen.getByRole('button', { name: /go back/i }) await user.click(returnButton) expect(await screen.findByText(/Select your institution/i)).toBeInTheDocument() }) + + it('sends BACK_TO_SEARCH post message when Go back is clicked', async () => { + const onPostMessage = vi.fn() + + const nonDemoInstitution = { + ...mockInstitution, + is_demo: false, + } as unknown as InstitutionResponseType + const state = { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, nonDemoInstitution), + profiles: { + ...initialState.profiles, + user: { ...initialState.profiles.user, is_demo: true }, + }, + } + + const { user } = render( + + + , + { preloadedState: state }, + ) + + await user.click(screen.getByRole('button', { name: /go back/i })) + + expect(onPostMessage).toHaveBeenCalledWith('connect/backToSearch') + }) }) diff --git a/src/views/demoConnectGuard/DemoConnectGuard.tsx b/src/views/demoConnectGuard/DemoConnectGuard.tsx index 758c573f68..aa4ed1b1ad 100644 --- a/src/views/demoConnectGuard/DemoConnectGuard.tsx +++ b/src/views/demoConnectGuard/DemoConnectGuard.tsx @@ -1,4 +1,4 @@ -import React, { useImperativeHandle } from 'react' +import React, { useImperativeHandle, useContext } from 'react' import { useDispatch, useSelector } from 'react-redux' import { RootState } from 'src/redux/Store' import { Button } from '@mui/material' @@ -12,6 +12,8 @@ import { getSelectedInstitution } from 'src/redux/selectors/Connect' import * as connectActions from 'src/redux/actions/Connect' import { selectInitialConfig } from 'src/redux/reducers/configSlice' import styles from 'src/views/demoConnectGuard/DemoConnectGuard.module.css' +import { PostMessageContext } from 'src/ConnectWidget' +import { POST_MESSAGES } from 'src/const/postMessages' export type DemoConnectGuardProps = { showBackButton: () => boolean @@ -19,6 +21,7 @@ export type DemoConnectGuardProps = { export const DemoConnectGuard = React.forwardRef( function DemoConnectGuard(_, ref) { + const postMessageFunctions = useContext(PostMessageContext) const institution = useSelector(getSelectedInstitution) const initialConfig = useSelector(selectInitialConfig) const location = useSelector((state: RootState) => state.connect.location) @@ -61,6 +64,7 @@ export const DemoConnectGuard = React.forwardRef(
From b3b4e2ce3bf28765a804efa3d550259c7251ce08 Mon Sep 17 00:00:00 2001 From: Clement Mwimo Date: Fri, 28 Aug 2026 10:40:16 -0600 Subject: [PATCH 8/8] fix a failing test --- src/components/RenderConnectStep-test.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RenderConnectStep-test.jsx b/src/components/RenderConnectStep-test.jsx index eef0c2be07..4aa9c65bc4 100644 --- a/src/components/RenderConnectStep-test.jsx +++ b/src/components/RenderConnectStep-test.jsx @@ -63,7 +63,7 @@ describe('RenderConnectStep', () => { const errorIcon = container.querySelector('svg.MuiSvgIcon-colorError') expect(errorIcon).toBeInTheDocument() - const button = screen.getByRole('button', { name: /return to institution selection/i }) + const button = screen.getByRole('button', { name: /go back/i }) expect(button).toBeInTheDocument() })