Skip to content
Merged
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
43 changes: 39 additions & 4 deletions src/hooks/__tests__/useLoadConnect-test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useEffect } from 'react'
import { useSelector } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import type { RootState } from 'src/redux/Store'
import { screen, render } from 'src/utilities/testingLibrary'
import useLoadConnect from 'src/hooks/useLoadConnect'
Expand All @@ -9,10 +9,13 @@ import { ApiProvider } from 'src/context/ApiContext'
import { apiValue } from 'src/const/apiProviderMock'
import { ConfigError } from 'src/components/ConfigError'
import { COMBO_JOB_DATA_TYPES } from 'src/const/comboJobDataTypes'
import { loadExperimentalFeatures } from 'src/redux/reducers/experimentalFeaturesSlice'

const TestLoadConnectComponent: React.FC<{ clientConfig: ClientConfigType }> = ({
clientConfig,
}) => {
const TestLoadConnectComponent: React.FC<{
clientConfig: ClientConfigType
experimentalFeatures?: { unavailableInstitutions: { guid: string; name: string }[] }
}> = ({ clientConfig, experimentalFeatures }) => {
const dispatch = useDispatch()
const step = useSelector(
(state: RootState) =>
state.connect.location[state.connect.location.length - 1]?.step ?? STEPS.SEARCH,
Expand All @@ -21,6 +24,7 @@ const TestLoadConnectComponent: React.FC<{ clientConfig: ClientConfigType }> = (
const { loadConnect } = useLoadConnect()

useEffect(() => {
dispatch(loadExperimentalFeatures(experimentalFeatures || {}))
loadConnect(clientConfig)
}, [])

Expand All @@ -35,6 +39,8 @@ const TestLoadConnectComponent: React.FC<{ clientConfig: ClientConfigType }> = (
return <p>Search</p>
} else if (step === STEPS.ENTER_CREDENTIALS) {
return <p>Enter credentials</p>
} else if (step === STEPS.INSTITUTION_STATUS_DETAILS) {
return <p>Institution status details</p>
} else {
return <p>Search</p>
}
Expand Down Expand Up @@ -302,4 +308,33 @@ describe('useLoadConnect', () => {
),
).toBeInTheDocument()
})

it('will return the INSTITUTION_STATUS_DETAILS step if the state contains a configured unavailable institution', async () => {
const mockApi = {
...apiValue,
loadInstitutionByGuid: vi.fn().mockResolvedValue(
Promise.resolve({
...institutionData.institution,
guid: 'INS-unavailable',
name: 'Unavailable Bank',
}),
),
}
render(
<ApiProvider apiValue={mockApi}>
<TestLoadConnectComponent
clientConfig={{
...initialState.config,
current_institution_guid: 'INS-unavailable',
}}
experimentalFeatures={{
// Because the current_institution_guid in clientConfig matches this unavailableInstitution,
// the step should end up at INSTITUTION_STATUS_DETAILS
unavailableInstitutions: [{ guid: 'INS-unavailable', name: 'Unavailable Bank' }],
}}
/>
</ApiProvider>,
)
expect(await screen.findByText(/Institution status details/i)).toBeInTheDocument()
})
})
3 changes: 3 additions & 0 deletions src/hooks/useLoadConnect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { useApi, ApiContextTypes } from 'src/context/ApiContext'
import { __ } from 'src/utilities/Intl'
import type { RootState } from 'src/redux/Store'
import { instutionSupportRequestedProducts } from 'src/utilities/Institution'
import { getExperimentalFeatures } from 'src/redux/reducers/experimentalFeaturesSlice'

export const getErrorResource = (err: { config: { url: string | string[] } }) => {
if (err.config?.url.includes('/institutions')) {
Expand Down Expand Up @@ -47,6 +48,7 @@ export const getErrorResource = (err: { config: { url: string | string[] } }) =>
const useLoadConnect = () => {
const { api } = useApi()
const profiles = useSelector((state: RootState) => state.profiles)
const experimentalFeatures = useSelector(getExperimentalFeatures)
const clientLocale = useMemo(() => {
return document.querySelector('html')?.getAttribute('lang') || 'en'
}, [document.querySelector('html')?.getAttribute('lang')])
Expand Down Expand Up @@ -77,6 +79,7 @@ const useLoadConnect = () => {
return from(api.loadMembers(clientLocale)).pipe(
map((members = []) =>
loadConnectSuccess({
experimentalFeatures,
members,
widgetProfile: profiles.widgetProfile,
...dependencies,
Expand Down
37 changes: 33 additions & 4 deletions src/redux/reducers/Connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const loadConnectSuccess = (state, action) => {
microdeposit,
config = {},
institution = {},
experimentalFeatures = {},
widgetProfile,
} = action.payload

Expand All @@ -70,7 +71,15 @@ const loadConnectSuccess = (state, action) => {
isComponentLoading: false,
location: pushLocation(
state.location,
getStartingStep(members, member, microdeposit, config, institution, widgetProfile),
getStartingStep(
members,
member,
microdeposit,
config,
institution,
widgetProfile,
experimentalFeatures,
),
),
selectedInstitution: institution,
updateCredentials:
Expand Down Expand Up @@ -520,7 +529,24 @@ const upsertMember = (state, action) => {

return [...state.members, loadedMember]
}
function getStartingStep(members, member, microdeposit, config, institution, widgetProfile) {
function getStartingStep(
members,
member,
microdeposit,
config,
institution,
widgetProfile,
experimentalFeatures = {},
) {
// Unavailable institutions experimental feature: Make sure we don't load a user
// directly to an institution that should be unavailable.
const unavailableInstitutions = experimentalFeatures?.unavailableInstitutions || []
const institutionIsAvailable =
institution &&
unavailableInstitutions.find(
(ins) => ins.guid === institution?.guid || ins.name === institution?.name,
) === undefined

const shouldStepToMFA =
member && config.update_credentials && member.connection_status === ReadableStatuses.CHALLENGED
const shouldUpdateCredentials =
Expand All @@ -530,13 +556,16 @@ function getStartingStep(members, member, microdeposit, config, institution, wid
config.mode === VERIFY_MODE &&
microdeposit.status !== MicrodepositsStatuses.PREINITIATED
const shouldLoadWithInstitution =
institution && (config.current_institution_guid || config.current_institution_code)
institution &&
(config.current_institution_guid || config.current_institution_code) &&
institutionIsAvailable
const shouldStepToConnecting =
member?.connection_status === ReadableStatuses.REJECTED ||
member?.connection_status === ReadableStatuses.EXPIRED
const shouldStepToInstitutionStatusDetails =
(institution && institutionIsBlockedForCostReasons(institution)) ||
(member && memberIsBlockedForCostReasons(member))
(member && memberIsBlockedForCostReasons(member)) ||
!institutionIsAvailable

if (shouldStepToInstitutionStatusDetails) {
return STEPS.INSTITUTION_STATUS_DETAILS
Expand Down
76 changes: 76 additions & 0 deletions src/redux/reducers/__tests__/Connect-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,82 @@ describe('Connect redux store', () => {
expect(afterState.members).toHaveLength(2)
expect(afterState.members[0]).toEqual({ guid: 'MBR-1', institution_guid: 'INST-1' })
})

it('should show the institutionStatusDetails step if the configured institution is blocked for fees/costs', () => {
const afterState = reducer(
defaultState,
loadConnectSuccess({
config: { current_institution_guid: 'INS-1' },
institution: { guid: 'INS-1', name: 'Chase Bank', is_disabled_by_client: true },
widgetProfile: {},
}),
)

expect(afterState.location[afterState.location.length - 1].step).toEqual(
STEPS.INSTITUTION_STATUS_DETAILS,
)
})

it('should show the credentials step if the configured institution is not blocked by the client for fees/costs', () => {
const afterState = reducer(
defaultState,
loadConnectSuccess({
config: { current_institution_guid: 'INS-1' },
institution: { guid: 'INS-1', name: 'Chase Bank', is_disabled_by_client: false },
widgetProfile: {},
}),
)

expect(afterState.location[afterState.location.length - 1].step).toEqual(
STEPS.ENTER_CREDENTIALS,
)
})

it('should show the institutionStatusDetails step if the configured institution is unavailable', () => {
const afterState = reducer(
defaultState,
loadConnectSuccess({
institution: { guid: 'INS-1', name: 'Unavailable Bank' },
experimentalFeatures: {
unavailableInstitutions: [{ guid: 'INS-1', name: 'Unavailable Bank' }],
},
}),
)

expect(afterState.location[afterState.location.length - 1].step).toEqual(
STEPS.INSTITUTION_STATUS_DETAILS,
)
})

it('should show the credentials step if the configured institution_guid is available', () => {
const afterState = reducer(
defaultState,
loadConnectSuccess({
config: { current_institution_guid: 'INS-1' },
institution: { guid: 'INS-1', name: 'Unavailable Bank' },
widgetProfile: {},
}),
)

expect(afterState.location[afterState.location.length - 1].step).toEqual(
STEPS.ENTER_CREDENTIALS,
)
})

it('should show the credentials step if the configured institution code is available', () => {
const afterState = reducer(
defaultState,
loadConnectSuccess({
config: { current_institution_code: 'unavailable_bank' },
institution: { guid: 'INS-1', name: 'Unavailable Bank' },
widgetProfile: {},
}),
)

expect(afterState.location[afterState.location.length - 1].step).toEqual(
STEPS.ENTER_CREDENTIALS,
)
})
})

describe('loadConnectError', () => {
Expand Down
Loading