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/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,