From 427c5f56090638298ded7be4d5a7b69bb26db1cd Mon Sep 17 00:00:00 2001 From: Logan Rasmussen Date: Thu, 23 Apr 2026 11:59:54 -0600 Subject: [PATCH 01/12] test(red): add tests for when institution.status is unavailable --- .../__tests__/InstitutionTile-test.jsx | 32 ++++++++++++ .../__tests__/institutionStatus-test.tsx | 49 +++++++++++++++++++ src/utilities/institutionStatus.ts | 10 ++++ .../InstitutionStatusDetails-test.tsx | 39 ++++++++++++++- src/views/search/__tests__/Search-test.js | 20 ++++++++ 5 files changed, 148 insertions(+), 2 deletions(-) diff --git a/src/components/__tests__/InstitutionTile-test.jsx b/src/components/__tests__/InstitutionTile-test.jsx index a7d7718f9f..b734a95118 100644 --- a/src/components/__tests__/InstitutionTile-test.jsx +++ b/src/components/__tests__/InstitutionTile-test.jsx @@ -2,6 +2,7 @@ import React from 'react' import { render, screen } from 'src/utilities/testingLibrary' import { act } from 'react' import { InstitutionTile } from '../InstitutionTile' +import { InstitutionStatusField } from 'src/utilities/institutionStatus' describe('', () => { it('renders the logoUrl in the src if there is one', async () => { @@ -62,4 +63,35 @@ describe('', () => { expect(screen.queryByText('DISABLED')).not.toBeInTheDocument() }) + + it('renders an UNAVAILABLE Chip if the institution is unavailable by experiment values', async () => { + const institution = { guid: 'testGuid', name: 'testName' } + const preloadedState = { + experimentalFeatures: { + unavailableInstitutions: [institution], + }, + } + + await act(async () => { + render( {}} />, { + preloadedState, + }) + }) + + expect(screen.getByText('UNAVAILABLE')).toBeInTheDocument() + }) + + it('renders an UNAVAILABLE Chip if the institution is unavailable by API', async () => { + const institution = { + guid: 'testGuid', + name: 'testName', + status: InstitutionStatusField.UNAVAILABLE, + } + + await act(async () => { + render( {}} />) + }) + + expect(screen.getByText('UNAVAILABLE')).toBeInTheDocument() + }) }) diff --git a/src/utilities/__tests__/institutionStatus-test.tsx b/src/utilities/__tests__/institutionStatus-test.tsx index 69650c9254..6245bfa32f 100644 --- a/src/utilities/__tests__/institutionStatus-test.tsx +++ b/src/utilities/__tests__/institutionStatus-test.tsx @@ -8,6 +8,7 @@ import { useInstitutionStatusMessage, useInstitutionStatus, getInstitutionStatus, + InstitutionStatusField, } from '../institutionStatus' import * as institutionBlocks from '../institutionBlocks' import { Provider } from 'react-redux' @@ -93,6 +94,20 @@ describe('institutionStatus', () => { const result = getInstitutionStatus(institution, unavailableInstitutions) expect(result).toBe(InstitutionStatus.OPERATIONAL) }) + + // API response for institution.status + it('returns UNAVAILABLE_PER_MX when institution.status is set to UNAVAILABLE', () => { + const institution = { + guid: 'test-guid', + name: 'Test Bank', + status: InstitutionStatusField.UNAVAILABLE, + } + const unavailableInstitutions: { guid: string; name: string }[] = [] + vi.mocked(institutionBlocks.institutionIsBlockedForCostReasons).mockReturnValue(false) + + const result = getInstitutionStatus(institution, unavailableInstitutions) + expect(result).toBe(InstitutionStatus.UNAVAILABLE_PER_MX) + }) }) describe('useInstitutionStatus', () => { @@ -108,6 +123,21 @@ describe('institutionStatus', () => { expect(result.current).toBe(InstitutionStatus.UNAVAILABLE) }) + it('returns UNAVAILABLE_PER_MX when institution.status is set to UNAVAILABLE in API response', () => { + const institution = { + guid: 'test-guid', + name: 'Test Bank', + status: InstitutionStatusField.UNAVAILABLE, + } + const store = createMockStore([]) + + const { result } = renderHook(() => useInstitutionStatus(institution), { + wrapper: ({ children }) => wrapper({ children, store }), + }) + + expect(result.current).toBe(InstitutionStatus.UNAVAILABLE_PER_MX) + }) + it('handles null institution', () => { const store = createMockStore([]) @@ -173,6 +203,25 @@ describe('institutionStatus', () => { }) }) + it('returns a unique unavailable message when institution.status is set to UNAVAILABLE in API response', () => { + const institution = { + guid: 'test-guid', + name: 'Test Bank', + status: InstitutionStatusField.UNAVAILABLE, + } + const store = createMockStore([]) + vi.mocked(institutionBlocks.institutionIsBlockedForCostReasons).mockReturnValue(false) + + const { result } = renderHook(() => useInstitutionStatusMessage(institution), { + wrapper: ({ children }) => wrapper({ children, store }), + }) + + expect(result.current).toEqual({ + title: 'Connection unavailable', + body: "This institution is experiencing issues that prevent successful connections. It's unclear when this will be resolved.", + }) + }) + it('returns empty message for OPERATIONAL status', () => { const institution = { guid: 'test-guid', name: 'Test Bank' } const store = createMockStore([]) diff --git a/src/utilities/institutionStatus.ts b/src/utilities/institutionStatus.ts index 6e3c69f6fa..29b39a5da0 100644 --- a/src/utilities/institutionStatus.ts +++ b/src/utilities/institutionStatus.ts @@ -3,12 +3,22 @@ import { institutionIsBlockedForCostReasons } from './institutionBlocks' import { __ } from 'src/utilities/Intl' import { useSelector } from 'react-redux' +// InstitutionStatus is a manually defined value, it's not something the API will give us export const InstitutionStatus = { CLIENT_BLOCKED_FOR_FEES: 'CLIENT_BLOCKED_FOR_FEES', OPERATIONAL: 'OPERATIONAL', UNAVAILABLE: 'UNAVAILABLE', } +// The InstitutionStatusType and InstitutionStatusField below are API defined values, this is our mapping for them +type _InstitutionStatusType = 0 | 1 | 2 | 3 +export const InstitutionStatusField = { + OPERATIONAL: 0, + MAINTENANCE: 1, + DEGRADED: 2, + UNAVAILABLE: 3, +} + export function useInstitutionStatusMessage(institution: { guid: string name: string diff --git a/src/views/institutionStatusDetails/__tests__/InstitutionStatusDetails-test.tsx b/src/views/institutionStatusDetails/__tests__/InstitutionStatusDetails-test.tsx index d855748edd..24c3f75872 100644 --- a/src/views/institutionStatusDetails/__tests__/InstitutionStatusDetails-test.tsx +++ b/src/views/institutionStatusDetails/__tests__/InstitutionStatusDetails-test.tsx @@ -2,6 +2,7 @@ import React from 'react' import { useDispatch } from 'react-redux' import { ActionTypes } from 'src/redux/actions/Connect' import { initialState } from 'src/redux/reducers/configSlice' +import { InstitutionStatusField } from 'src/utilities/institutionStatus' import { render, screen, waitFor } from 'src/utilities/testingLibrary' import { InstitutionStatusDetails } from 'src/views/institutionStatusDetails/InstitutionStatusDetails' @@ -18,6 +19,11 @@ const blockedInstitution = { guid: 'INS-78c7b591-6512-9c17-b092-1cddbd3c85ba', // PROD INS guid } const unavailableInstitution = { guid: 'INST-unavailable', name: 'Unavailable Bank' } +const apiUnavailableInstitution = { + guid: 'INST-api-unavailable', + name: 'API Unavailable Bank', + status: InstitutionStatusField.UNAVAILABLE, +} describe('InstitutionStatusDetails', () => { const preloadedState = { @@ -55,7 +61,7 @@ describe('InstitutionStatusDetails', () => { expect(disabledIcon).toBeInTheDocument() }) - it('CLIENT_BLOCKED_FOR_FEES status - renders the header title and paragraph explaination', () => { + it('CLIENT_BLOCKED_FOR_FEES status - renders the header title and paragraph explanation', () => { const result = render(, { preloadedState: { connect: { @@ -79,7 +85,7 @@ describe('InstitutionStatusDetails', () => { ).toBeInTheDocument() }) - it('UNAVAILABLE status - renders the header title and paragraph explaination', () => { + it('UNAVAILABLE status - renders the header title and paragraph explanation', () => { const result = render(, { preloadedState: { connect: { @@ -106,6 +112,35 @@ describe('InstitutionStatusDetails', () => { ).toBeInTheDocument() }) + it('UNAVAILABLE institution.status from API - renders the header title and paragraph explanation', () => { + const result = render(, { + preloadedState: { + connect: { + selectedInstitution: apiUnavailableInstitution, + }, + config: { + ...initialState, + _initialValues: JSON.stringify(initialState), + }, + experimentalFeatures: { + unavailableInstitutions: [], + }, + }, + }) + container = result.container + + expect(screen.getByText(`Connection unavailable`)).toBeInTheDocument() + expect( + screen.getByText( + (content, element) => + element?.tagName.toLowerCase() === 'p' && + content.includes( + 'This institution is experiencing issues that prevent successful connections', + ), + ), + ).toBeInTheDocument() + }) + it('renders a primary button that dispatches the correct action', () => { const button = screen.getByRole('button', { name: 'Connect a different institution' }) diff --git a/src/views/search/__tests__/Search-test.js b/src/views/search/__tests__/Search-test.js index 42a7881d6f..b3f0edce6c 100644 --- a/src/views/search/__tests__/Search-test.js +++ b/src/views/search/__tests__/Search-test.js @@ -7,6 +7,7 @@ import { SEARCH_PER_PAGE_DEFAULT, SEARCH_PAGE_DEFAULT } from 'src/views/search/c import { __ } from 'src/utilities/Intl' import { ApiProvider } from 'src/context/ApiContext' import { apiValue } from 'src/const/apiProviderMock' +import { InstitutionStatusField } from 'src/utilities/institutionStatus' describe('Search View', () => { describe('Search component', () => { @@ -265,6 +266,25 @@ describe('Search View', () => { expect(searchResult).toEqual(undefined) }) + it('Does not suggest institutions that have the status of unavailable via the API', () => { + const unavailableInstitution = { + guid: 'unavailable-guid', + popularity: 51, + status: InstitutionStatusField.UNAVAILABLE, + } + + const result = getSuggestedInstitutions( + [...popular, unavailableInstitution], + discovered, + [], + EXPECTED_MAX_SIZE, + ) + const searchResult = result.find( + (institution) => institution.guid === unavailableInstitution.guid, + ) + expect(searchResult).toEqual(undefined) + }) + it('Dedupes popular & discovered lists, only includes an institution once', () => { const institution1 = { guid: '1', popularity: 1 } const institution2 = { guid: '2', popularity: 2 } From 20a11a49d2ad7527cf40288e73958a2ca76f8c8c Mon Sep 17 00:00:00 2001 From: Logan Rasmussen Date: Thu, 23 Apr 2026 14:16:09 -0600 Subject: [PATCH 02/12] test(green): Implement all code to satisfy the tests that were written --- src/components/InstitutionTile.js | 17 ++++++++----- src/redux/reducers/Connect.js | 3 ++- src/utilities/institutionStatus.ts | 22 ++++++++++++++--- src/views/search/Search.js | 39 +++++++++++++++++++++--------- 4 files changed, 60 insertions(+), 21 deletions(-) diff --git a/src/components/InstitutionTile.js b/src/components/InstitutionTile.js index 1f932267ec..f09c0762db 100644 --- a/src/components/InstitutionTile.js +++ b/src/components/InstitutionTile.js @@ -19,6 +19,16 @@ export const InstitutionTile = (props) => { const tokens = useTokens() const styles = getStyles(tokens) + let statusChip = null + if (institution.is_disabled_by_client) { + statusChip = + } else if ( + status === InstitutionStatus.UNAVAILABLE || + status === InstitutionStatus.UNAVAILABLE_PER_MX + ) { + statusChip = + } + return ( ) } diff --git a/src/redux/reducers/Connect.js b/src/redux/reducers/Connect.js index e3e1b1b823..0b0b5e3b96 100644 --- a/src/redux/reducers/Connect.js +++ b/src/redux/reducers/Connect.js @@ -290,7 +290,8 @@ const selectInstitutionSuccess = (state, action) => { if ( action.payload.institution && (institutionIsBlockedForCostReasons(action.payload.institution) || - action.payload.institutionStatus === InstitutionStatus.UNAVAILABLE) + action.payload.institutionStatus === InstitutionStatus.UNAVAILABLE || + action.payload.institutionStatus === InstitutionStatus.UNAVAILABLE_PER_MX) ) { nextStep = STEPS.INSTITUTION_STATUS_DETAILS } else if (action.payload.user?.is_demo && !action.payload.institution?.is_demo) { diff --git a/src/utilities/institutionStatus.ts b/src/utilities/institutionStatus.ts index 29b39a5da0..d3e835c69c 100644 --- a/src/utilities/institutionStatus.ts +++ b/src/utilities/institutionStatus.ts @@ -4,25 +4,28 @@ import { __ } from 'src/utilities/Intl' import { useSelector } from 'react-redux' // InstitutionStatus is a manually defined value, it's not something the API will give us +// These are the values we'll return to our application to determine what messaging to show for an institution, if any export const InstitutionStatus = { CLIENT_BLOCKED_FOR_FEES: 'CLIENT_BLOCKED_FOR_FEES', OPERATIONAL: 'OPERATIONAL', - UNAVAILABLE: 'UNAVAILABLE', + UNAVAILABLE_PER_MX: 'UNAVAILABLE_PER_MX', + UNAVAILABLE: 'UNAVAILABLE', // Experimental feature status, will be remove eventually } // The InstitutionStatusType and InstitutionStatusField below are API defined values, this is our mapping for them -type _InstitutionStatusType = 0 | 1 | 2 | 3 export const InstitutionStatusField = { OPERATIONAL: 0, MAINTENANCE: 1, DEGRADED: 2, UNAVAILABLE: 3, -} +} as const +type InstitutionStatusType = (typeof InstitutionStatusField)[keyof typeof InstitutionStatusField] export function useInstitutionStatusMessage(institution: { guid: string name: string is_disabled_by_client?: boolean + status?: InstitutionStatusType }) { const { unavailableInstitutions } = useSelector(getExperimentalFeatures) const status = useInstitutionStatus(institution) @@ -55,6 +58,13 @@ export function useInstitutionStatusMessage(institution: { institution.name, ), } + case InstitutionStatus.UNAVAILABLE_PER_MX: + return { + title: __('Connection unavailable'), + body: __( + `This institution is experiencing issues that prevent successful connections. It's unclear when this will be resolved.`, + ), + } default: return { title: '', @@ -68,6 +78,7 @@ export function useInstitutionStatus( guid: string name: string is_disabled_by_client?: boolean + status?: InstitutionStatusType } | null, ) { // Right now the statuses are driven by experimental features. @@ -83,6 +94,7 @@ export function getInstitutionStatus( guid: string name: string is_disabled_by_client?: boolean + status?: InstitutionStatusType } | null, unavailableInstitutions: { guid: string; name: string }[], ) { @@ -98,6 +110,10 @@ export function getInstitutionStatus( return InstitutionStatus.CLIENT_BLOCKED_FOR_FEES } + if (institution?.status === InstitutionStatusField.UNAVAILABLE) { + return InstitutionStatus.UNAVAILABLE_PER_MX + } + // Return UNAVAILABLE if the institution is currently marked as unavailable. // This is driven by the experimental feature "unavailableInstitutions". // Each institution must be included manually into the npm package's props diff --git a/src/views/search/Search.js b/src/views/search/Search.js index 23391f35d4..104c8ebe9f 100644 --- a/src/views/search/Search.js +++ b/src/views/search/Search.js @@ -216,15 +216,22 @@ export const Search = React.forwardRef((_, navigationRef) => { }) // Remove any Unavailable institutions from the popular/discovered lists - const filteredPopularInstitutions = updatedPopularInstitutions.filter( - (popular) => - getInstitutionStatus(popular, unavailableInstitutions) !== - InstitutionStatus.UNAVAILABLE, - ) + const filteredPopularInstitutions = updatedPopularInstitutions.filter((popular) => { + const status = getInstitutionStatus(popular, unavailableInstitutions) + return ( + status !== InstitutionStatus.UNAVAILABLE && + status !== InstitutionStatus.UNAVAILABLE_PER_MX + ) + }) + const filteredDiscoveredInstitutions = updatedDiscoveredInstitutions.filter( - (discovered) => - getInstitutionStatus(discovered, unavailableInstitutions) !== - InstitutionStatus.UNAVAILABLE, + (discovered) => { + const status = getInstitutionStatus(discovered, unavailableInstitutions) + return ( + status !== InstitutionStatus.UNAVAILABLE && + status !== InstitutionStatus.UNAVAILABLE_PER_MX + ) + }, ) return dispatch({ @@ -411,6 +418,8 @@ export const Search = React.forwardRef((_, navigationRef) => { state.popularInstitutions, state.discoveredInstitutions, connectedMembers, + MAX_SUGGESTED_LIST_SIZE, + unavailableInstitutions, ) } onSearchInstitutionClick={() => searchInput.current.focus()} @@ -507,14 +516,22 @@ export const getSuggestedInstitutions = ( discoveredInstitutions, connectedMembers, limit = MAX_SUGGESTED_LIST_SIZE, + unavailableInstitutions = [], ) => { // Combine and dedupe both our institution lists const dedupedList = _unionBy(popularInstitutions, discoveredInstitutions, 'guid') // Remove connected institutions from the list - const filteredConnectedList = dedupedList.filter( - (popular) => !_find(connectedMembers, ['institution_guid', popular.guid]), - ) + // Remove UNAVAILABLE institutions from the list + // Remove UNAVAILABLE_PER_MX institutions from the list + const filteredConnectedList = dedupedList.filter((popular) => { + const status = getInstitutionStatus(popular, unavailableInstitutions) + return ( + !_find(connectedMembers, ['institution_guid', popular.guid]) && + status !== InstitutionStatus.UNAVAILABLE && + status !== InstitutionStatus.UNAVAILABLE_PER_MX + ) + }) // Sort list by popularity (highest to lowest) const sortedList = filteredConnectedList.sort((a, b) => b.popularity - a.popularity) From c63765f20ad39882d90013a9f47aa4fdb7207a27 Mon Sep 17 00:00:00 2001 From: Logan Rasmussen Date: Thu, 23 Apr 2026 15:56:50 -0600 Subject: [PATCH 03/12] fix(load): go to the ins status details page when mx has set the status to unavailable Co-authored-by: Copilot --- src/hooks/__tests__/useLoadConnect-test.tsx | 28 ++++++++++++++++++++- src/redux/reducers/Connect.js | 10 +++----- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/hooks/__tests__/useLoadConnect-test.tsx b/src/hooks/__tests__/useLoadConnect-test.tsx index 676cc00b25..4e42244269 100644 --- a/src/hooks/__tests__/useLoadConnect-test.tsx +++ b/src/hooks/__tests__/useLoadConnect-test.tsx @@ -10,6 +10,7 @@ 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' +import { InstitutionStatusField } from 'src/utilities/institutionStatus' const TestLoadConnectComponent: React.FC<{ clientConfig: ClientConfigType @@ -309,7 +310,7 @@ describe('useLoadConnect', () => { ).toBeInTheDocument() }) - it('will return the INSTITUTION_STATUS_DETAILS step if the state contains a configured unavailable institution', async () => { + it('will return the INSTITUTION_STATUS_DETAILS step if the state contains a configured unavailable institution, via the experimental props', async () => { const mockApi = { ...apiValue, loadInstitutionByGuid: vi.fn().mockResolvedValue( @@ -337,4 +338,29 @@ describe('useLoadConnect', () => { ) expect(await screen.findByText(/Institution status details/i)).toBeInTheDocument() }) + + it('will return the INSTITUTION_STATUS_DETAILS step if the state contains a configured unavailable institution, via the api', async () => { + const mockApi = { + ...apiValue, + loadInstitutionByGuid: vi.fn().mockResolvedValue( + Promise.resolve({ + ...institutionData.institution, + guid: 'INS-unavailable', + name: 'Unavailable Bank', + status: InstitutionStatusField.UNAVAILABLE, // This status triggers the UNAVAILABLE_PER_MX status + }), + ), + } + render( + + + , + ) + expect(await screen.findByText(/Institution status details/i)).toBeInTheDocument() + }) }) diff --git a/src/redux/reducers/Connect.js b/src/redux/reducers/Connect.js index 0b0b5e3b96..d220032f80 100644 --- a/src/redux/reducers/Connect.js +++ b/src/redux/reducers/Connect.js @@ -20,7 +20,7 @@ import { institutionIsBlockedForCostReasons, memberIsBlockedForCostReasons, } from 'src/utilities/institutionBlocks' -import { InstitutionStatus } from 'src/utilities/institutionStatus' +import { getInstitutionStatus, InstitutionStatus } from 'src/utilities/institutionStatus' export const defaultState = { error: null, // The most recent job request error, if any @@ -545,11 +545,9 @@ function getStartingStep( // 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 institutionStatus = getInstitutionStatus(institution, unavailableInstitutions) + const unavailableStatuses = [InstitutionStatus.UNAVAILABLE, InstitutionStatus.UNAVAILABLE_PER_MX] + const institutionIsAvailable = institution && !unavailableStatuses.includes(institutionStatus) const shouldStepToMFA = member && config.update_credentials && member.connection_status === ReadableStatuses.CHALLENGED From e5897c02beab56b2eab942a474468f5898bf5bf3 Mon Sep 17 00:00:00 2001 From: Logan Rasmussen Date: Fri, 24 Apr 2026 12:43:15 -0600 Subject: [PATCH 04/12] fix(institution): add fields to the types Co-authored-by: Copilot --- typings/apiTypes.d.ts | 2 ++ typings/mxTypes.d.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/typings/apiTypes.d.ts b/typings/apiTypes.d.ts index 6919ce3f75..c0d664e6a6 100644 --- a/typings/apiTypes.d.ts +++ b/typings/apiTypes.d.ts @@ -125,6 +125,7 @@ type InstitutionResponseType = { account_verification_is_enabled: boolean account_identification_is_enabled: boolean brand_color_hex_code?: string | null + client_status?: number code: string forgot_password_credential_recovery_url?: string | null forgot_username_credential_recovery_url?: string | null @@ -140,6 +141,7 @@ type InstitutionResponseType = { name: string oauth_predirect_instructions?: number[] popularity?: number + status?: number supports_oauth: boolean tax_statement_is_enabled: boolean trouble_signing_credential_recovery_url?: string | null diff --git a/typings/mxTypes.d.ts b/typings/mxTypes.d.ts index eb30575362..6f37fbad0f 100644 --- a/typings/mxTypes.d.ts +++ b/typings/mxTypes.d.ts @@ -66,6 +66,7 @@ type InstitutionResponseType = { account_verification_is_enabled: boolean account_identification_is_enabled: boolean brand_color_hex_code?: string | null + client_status?: number code: string forgot_password_credential_recovery_url?: string | null forgot_username_credential_recovery_url?: string | null @@ -81,6 +82,7 @@ type InstitutionResponseType = { name: string oauth_predirect_instructions?: number[] popularity?: number + status?: number supports_oauth: boolean tax_statement_is_enabled: boolean trouble_signing_credential_recovery_url?: string | null From 4eadac8dae077fea1f7debfe6931f9f9171702d6 Mon Sep 17 00:00:00 2001 From: Logan Rasmussen Date: Fri, 24 Apr 2026 12:44:42 -0600 Subject: [PATCH 05/12] fix(institution): make the unavailable tag red by using the colors that mxui/mui provide --- src/components/InstitutionTile.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/InstitutionTile.js b/src/components/InstitutionTile.js index f09c0762db..4d4967ac70 100644 --- a/src/components/InstitutionTile.js +++ b/src/components/InstitutionTile.js @@ -26,7 +26,7 @@ export const InstitutionTile = (props) => { status === InstitutionStatus.UNAVAILABLE || status === InstitutionStatus.UNAVAILABLE_PER_MX ) { - statusChip = + statusChip = } return ( @@ -143,8 +143,6 @@ const getStyles = (tokens) => { }, chip: { padding: `${tokens.Spacing.XTiny}px 0`, - background: '#ECECEC', - color: '#494949', height: tokens.Spacing.Medium, fontSize: '9px', }, From 60c556ae81e662821a16f227f3ea109962632c14 Mon Sep 17 00:00:00 2001 From: Logan Rasmussen Date: Fri, 24 Apr 2026 13:45:06 -0600 Subject: [PATCH 06/12] fix(i18n): add translations --- src/const/language/es.po | 23 ++++++++++++++++--- src/const/language/frCa.po | 23 ++++++++++++++++--- src/utilities/institutionStatus.ts | 2 +- .../InstitutionStatusDetails.tsx | 2 +- .../InstitutionStatusDetails-test.tsx | 2 +- 5 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/const/language/es.po b/src/const/language/es.po index 44e884c2cc..70082c4ff0 100644 --- a/src/const/language/es.po +++ b/src/const/language/es.po @@ -1881,7 +1881,6 @@ msgstr "" msgid "Log in again" msgstr "Inicie sesión nuevamente" -#: src/views/institutionStatusDetails/InstitutionStatusDetails.tsx #: src/views/actionableError/useActionableErrorMap.tsx msgid "Connect a different institution" msgstr "Conecte una institución diferente" @@ -2071,7 +2070,25 @@ 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 " +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 " +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." + +#: src/utilities/institutionStatus.ts +msgid "Connection unavailable" +msgstr "Conexión no disponible" + +#: src/utilities/institutionStatus.ts +msgid "" +"This institution is experiencing issues that prevent successful " +"connections. It's unclear when this will be resolved." +msgstr "" +"Esta institución está experimentando problemas que impiden establecer " +"conexiones exitosas. No está claro cuándo se resolverá esta situación." + +#: src/views/institutionStatusDetails/InstitutionStatusDetails.tsx +msgid "Back" +msgstr "Retroceder" diff --git a/src/const/language/frCa.po b/src/const/language/frCa.po index 21fb3e08a4..edd8c3537c 100644 --- a/src/const/language/frCa.po +++ b/src/const/language/frCa.po @@ -1957,7 +1957,6 @@ msgstr "" msgid "Log in again" msgstr "Connectez-vous à nouveau" -#: src/views/institutionStatusDetails/InstitutionStatusDetails.tsx #: src/views/actionableError/useActionableErrorMap.tsx msgid "Connect a different institution" msgstr "Mettre en relation un autre établissement" @@ -2149,8 +2148,26 @@ 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 " +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 " +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." + +#: src/utilities/institutionStatus.ts +msgid "Connection unavailable" +msgstr "Connexion indisponible" + +#: src/utilities/institutionStatus.ts +msgid "" +"This institution is experiencing issues that prevent successful " +"connections. It's unclear when this will be resolved." +msgstr "" +"Cet établissement rencontre des problèmes qui empêchent d'établir des " +"connexions. Il est difficile de déterminer quand la situation sera résolue." + +#: src/views/institutionStatusDetails/InstitutionStatusDetails.tsx +msgid "Back" +msgstr "Reculer" diff --git a/src/utilities/institutionStatus.ts b/src/utilities/institutionStatus.ts index d3e835c69c..aecf20a1a5 100644 --- a/src/utilities/institutionStatus.ts +++ b/src/utilities/institutionStatus.ts @@ -62,7 +62,7 @@ export function useInstitutionStatusMessage(institution: { return { title: __('Connection unavailable'), body: __( - `This institution is experiencing issues that prevent successful connections. It's unclear when this will be resolved.`, + "This institution is experiencing issues that prevent successful connections. It's unclear when this will be resolved.", ), } default: diff --git a/src/views/institutionStatusDetails/InstitutionStatusDetails.tsx b/src/views/institutionStatusDetails/InstitutionStatusDetails.tsx index df6e90eed0..8df3a1e506 100644 --- a/src/views/institutionStatusDetails/InstitutionStatusDetails.tsx +++ b/src/views/institutionStatusDetails/InstitutionStatusDetails.tsx @@ -82,7 +82,7 @@ export const InstitutionStatusDetails = React.forwardRef - {__('Connect a different institution')} + {__('Back')} diff --git a/src/views/institutionStatusDetails/__tests__/InstitutionStatusDetails-test.tsx b/src/views/institutionStatusDetails/__tests__/InstitutionStatusDetails-test.tsx index 24c3f75872..9bff25f9a9 100644 --- a/src/views/institutionStatusDetails/__tests__/InstitutionStatusDetails-test.tsx +++ b/src/views/institutionStatusDetails/__tests__/InstitutionStatusDetails-test.tsx @@ -142,7 +142,7 @@ describe('InstitutionStatusDetails', () => { }) it('renders a primary button that dispatches the correct action', () => { - const button = screen.getByRole('button', { name: 'Connect a different institution' }) + const button = screen.getByRole('button', { name: 'Back' }) expect(button).toBeInTheDocument() button.click() From 2bb3299639769d6cb698c8bd1da587fbd17c4f7b Mon Sep 17 00:00:00 2001 From: Logan Rasmussen Date: Fri, 24 Apr 2026 13:52:32 -0600 Subject: [PATCH 07/12] test(tile): remove the usage of act in tests Co-authored-by: Copilot --- .../__tests__/InstitutionTile-test.jsx | 39 +++++++------------ 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/src/components/__tests__/InstitutionTile-test.jsx b/src/components/__tests__/InstitutionTile-test.jsx index b734a95118..a2e4c42ec5 100644 --- a/src/components/__tests__/InstitutionTile-test.jsx +++ b/src/components/__tests__/InstitutionTile-test.jsx @@ -1,19 +1,16 @@ import React from 'react' import { render, screen } from 'src/utilities/testingLibrary' -import { act } from 'react' import { InstitutionTile } from '../InstitutionTile' import { InstitutionStatusField } from 'src/utilities/institutionStatus' describe('', () => { - it('renders the logoUrl in the src if there is one', async () => { + it('renders the logoUrl in the src if there is one', () => { const institution = { name: 'testName', logo_url: 'testLogoUrl', } - await act(async () => { - render( {}} />) - }) + render( {}} />) expect(screen.getByAltText(`${institution.name} logo`)).toHaveAttribute( 'src', @@ -21,50 +18,44 @@ describe('', () => { ) }) - it('renders a generated url with the guid if there is no logoUrl', async () => { + it('renders a generated url with the guid if there is no logoUrl', () => { const institution = { guid: 'testGuid', name: 'testName', } - await act(async () => { - render( {}} />) - }) + render( {}} />) expect(screen.getByAltText(`${institution.name} logo`).src.includes(institution.guid)).toBe( true, ) }) - it('renders a disabled Chip if the institution is disabled', async () => { + it('renders a disabled Chip if the institution is disabled', () => { const institution = { guid: 'testGuid', name: 'testName', is_disabled_by_client: true, } - await act(async () => { - render( {}} />) - }) + render( {}} />) expect(screen.getByText('DISABLED')).toBeInTheDocument() }) - it('does not render a disabled Chip if the institution is not disabled', async () => { + it('does not render a disabled Chip if the institution is not disabled', () => { const institution = { guid: 'testGuid', name: 'testName', is_disabled_by_client: false, } - await act(async () => { - render( {}} />) - }) + render( {}} />) expect(screen.queryByText('DISABLED')).not.toBeInTheDocument() }) - it('renders an UNAVAILABLE Chip if the institution is unavailable by experiment values', async () => { + it('renders an UNAVAILABLE Chip if the institution is unavailable by experiment values', () => { const institution = { guid: 'testGuid', name: 'testName' } const preloadedState = { experimentalFeatures: { @@ -72,25 +63,21 @@ describe('', () => { }, } - await act(async () => { - render( {}} />, { - preloadedState, - }) + render( {}} />, { + preloadedState, }) expect(screen.getByText('UNAVAILABLE')).toBeInTheDocument() }) - it('renders an UNAVAILABLE Chip if the institution is unavailable by API', async () => { + it('renders an UNAVAILABLE Chip if the institution is unavailable by API', () => { const institution = { guid: 'testGuid', name: 'testName', status: InstitutionStatusField.UNAVAILABLE, } - await act(async () => { - render( {}} />) - }) + render( {}} />) expect(screen.getByText('UNAVAILABLE')).toBeInTheDocument() }) From 294633fd9eecfe2c63af683f920d478031f0b05d Mon Sep 17 00:00:00 2001 From: Logan Rasmussen Date: Mon, 27 Apr 2026 08:41:40 -0600 Subject: [PATCH 08/12] refactor(unavailable): use the includes strategy for consistency Co-authored-by: Copilot --- src/redux/reducers/Connect.js | 8 +++----- src/utilities/institutionStatus.ts | 6 ++++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/redux/reducers/Connect.js b/src/redux/reducers/Connect.js index d220032f80..a22cd06802 100644 --- a/src/redux/reducers/Connect.js +++ b/src/redux/reducers/Connect.js @@ -20,7 +20,7 @@ import { institutionIsBlockedForCostReasons, memberIsBlockedForCostReasons, } from 'src/utilities/institutionBlocks' -import { getInstitutionStatus, InstitutionStatus } from 'src/utilities/institutionStatus' +import { getInstitutionStatus, UNAVAILABLE_STATUSES } from 'src/utilities/institutionStatus' export const defaultState = { error: null, // The most recent job request error, if any @@ -290,8 +290,7 @@ const selectInstitutionSuccess = (state, action) => { if ( action.payload.institution && (institutionIsBlockedForCostReasons(action.payload.institution) || - action.payload.institutionStatus === InstitutionStatus.UNAVAILABLE || - action.payload.institutionStatus === InstitutionStatus.UNAVAILABLE_PER_MX) + UNAVAILABLE_STATUSES.includes(action.payload.institutionStatus)) ) { nextStep = STEPS.INSTITUTION_STATUS_DETAILS } else if (action.payload.user?.is_demo && !action.payload.institution?.is_demo) { @@ -546,8 +545,7 @@ function getStartingStep( // directly to an institution that should be unavailable. const unavailableInstitutions = experimentalFeatures?.unavailableInstitutions || [] const institutionStatus = getInstitutionStatus(institution, unavailableInstitutions) - const unavailableStatuses = [InstitutionStatus.UNAVAILABLE, InstitutionStatus.UNAVAILABLE_PER_MX] - const institutionIsAvailable = institution && !unavailableStatuses.includes(institutionStatus) + const institutionIsAvailable = institution && !UNAVAILABLE_STATUSES.includes(institutionStatus) const shouldStepToMFA = member && config.update_credentials && member.connection_status === ReadableStatuses.CHALLENGED diff --git a/src/utilities/institutionStatus.ts b/src/utilities/institutionStatus.ts index aecf20a1a5..7f3f26b83f 100644 --- a/src/utilities/institutionStatus.ts +++ b/src/utilities/institutionStatus.ts @@ -12,6 +12,12 @@ export const InstitutionStatus = { UNAVAILABLE: 'UNAVAILABLE', // Experimental feature status, will be remove eventually } +// These are the status values that should show the "Unavailable Tag" +export const UNAVAILABLE_STATUSES = [ + InstitutionStatus.UNAVAILABLE, + InstitutionStatus.UNAVAILABLE_PER_MX, +] as const + // The InstitutionStatusType and InstitutionStatusField below are API defined values, this is our mapping for them export const InstitutionStatusField = { OPERATIONAL: 0, From 5bd64477fec2e4adcf9c4d29b67499c45f1b06e3 Mon Sep 17 00:00:00 2001 From: Logan Rasmussen Date: Mon, 27 Apr 2026 08:51:47 -0600 Subject: [PATCH 09/12] refactor(test): remove mocks where possible from the institutionStatus tests --- .../__tests__/institutionStatus-test.tsx | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/utilities/__tests__/institutionStatus-test.tsx b/src/utilities/__tests__/institutionStatus-test.tsx index 6245bfa32f..e22220077b 100644 --- a/src/utilities/__tests__/institutionStatus-test.tsx +++ b/src/utilities/__tests__/institutionStatus-test.tsx @@ -10,14 +10,9 @@ import { getInstitutionStatus, InstitutionStatusField, } from '../institutionStatus' -import * as institutionBlocks from '../institutionBlocks' import { Provider } from 'react-redux' // Mock dependencies -vi.mock('../institutionBlocks', () => ({ - institutionIsBlockedForCostReasons: vi.fn(), -})) - vi.mock('src/utilities/Intl', () => ({ __: vi.fn((key: string, ...args: any[]) => { if (args.length > 0) { @@ -61,8 +56,11 @@ describe('institutionStatus', () => { }) it('returns CLIENT_BLOCKED_FOR_FEES when institution is blocked for cost reasons', () => { - const institution = { guid: 'test-guid', name: 'Test Bank' } - vi.mocked(institutionBlocks.institutionIsBlockedForCostReasons).mockReturnValue(true) + const institution = { + guid: 'INS-78c7b591-6512-9c17-b092-1cddbd3c85ba', + name: 'Chase Bank', + is_disabled_by_client: true, + } const result = getInstitutionStatus(institution, []) expect(result).toBe(InstitutionStatus.CLIENT_BLOCKED_FOR_FEES) @@ -71,7 +69,6 @@ describe('institutionStatus', () => { it('returns UNAVAILABLE when institution is in unavailableInstitutions by guid', () => { const institution = { guid: 'test-guid', name: 'Test Bank' } const unavailableInstitutions = [{ guid: 'test-guid', name: 'Other Bank' }] - vi.mocked(institutionBlocks.institutionIsBlockedForCostReasons).mockReturnValue(false) const result = getInstitutionStatus(institution, unavailableInstitutions) expect(result).toBe(InstitutionStatus.UNAVAILABLE) @@ -80,7 +77,6 @@ describe('institutionStatus', () => { it('returns UNAVAILABLE when institution is in unavailableInstitutions by name', () => { const institution = { guid: 'test-guid', name: 'Test Bank' } const unavailableInstitutions = [{ guid: 'other-guid', name: 'Test Bank' }] - vi.mocked(institutionBlocks.institutionIsBlockedForCostReasons).mockReturnValue(false) const result = getInstitutionStatus(institution, unavailableInstitutions) expect(result).toBe(InstitutionStatus.UNAVAILABLE) @@ -89,7 +85,6 @@ describe('institutionStatus', () => { it('returns OPERATIONAL when institution is not blocked or unavailable', () => { const institution = { guid: 'test-guid', name: 'Test Bank' } const unavailableInstitutions = [{ guid: 'other-guid', name: 'Other Bank' }] - vi.mocked(institutionBlocks.institutionIsBlockedForCostReasons).mockReturnValue(false) const result = getInstitutionStatus(institution, unavailableInstitutions) expect(result).toBe(InstitutionStatus.OPERATIONAL) @@ -103,7 +98,6 @@ describe('institutionStatus', () => { status: InstitutionStatusField.UNAVAILABLE, } const unavailableInstitutions: { guid: string; name: string }[] = [] - vi.mocked(institutionBlocks.institutionIsBlockedForCostReasons).mockReturnValue(false) const result = getInstitutionStatus(institution, unavailableInstitutions) expect(result).toBe(InstitutionStatus.UNAVAILABLE_PER_MX) @@ -173,17 +167,20 @@ describe('institutionStatus', () => { }) it('returns fee-related message for CLIENT_BLOCKED_FOR_FEES status', () => { - const institution = { guid: 'test-guid', name: 'Test Bank' } + const institution = { + guid: 'INS-78c7b591-6512-9c17-b092-1cddbd3c85ba', + name: 'Chase Bank', + is_disabled_by_client: true, + } const store = createMockStore([]) - vi.mocked(institutionBlocks.institutionIsBlockedForCostReasons).mockReturnValue(true) const { result } = renderHook(() => useInstitutionStatusMessage(institution), { wrapper: ({ children }) => wrapper({ children, store }), }) expect(result.current).toEqual({ - title: 'Free Test Bank Connections Are No Longer Available', - body: 'Test Bank now charges a fee for us to access your account data. To avoid passing that cost on to you, we no longer support Test Bank connections.', + title: 'Free Chase Bank Connections Are No Longer Available', + body: 'Chase Bank now charges a fee for us to access your account data. To avoid passing that cost on to you, we no longer support Chase Bank connections.', }) }) @@ -191,7 +188,6 @@ describe('institutionStatus', () => { const institution = { guid: 'test-guid', name: 'Test Bank' } const unavailableInstitutions = [{ guid: 'test-guid', name: 'Test Bank' }] const store = createMockStore(unavailableInstitutions) - vi.mocked(institutionBlocks.institutionIsBlockedForCostReasons).mockReturnValue(false) const { result } = renderHook(() => useInstitutionStatusMessage(institution), { wrapper: ({ children }) => wrapper({ children, store }), @@ -210,7 +206,6 @@ describe('institutionStatus', () => { status: InstitutionStatusField.UNAVAILABLE, } const store = createMockStore([]) - vi.mocked(institutionBlocks.institutionIsBlockedForCostReasons).mockReturnValue(false) const { result } = renderHook(() => useInstitutionStatusMessage(institution), { wrapper: ({ children }) => wrapper({ children, store }), @@ -225,7 +220,6 @@ describe('institutionStatus', () => { it('returns empty message for OPERATIONAL status', () => { const institution = { guid: 'test-guid', name: 'Test Bank' } const store = createMockStore([]) - vi.mocked(institutionBlocks.institutionIsBlockedForCostReasons).mockReturnValue(false) const { result } = renderHook(() => useInstitutionStatusMessage(institution), { wrapper: ({ children }) => wrapper({ children, store }), From 13876e0543496269955ec27157b8758f3068ac08 Mon Sep 17 00:00:00 2001 From: Logan Rasmussen Date: Mon, 27 Apr 2026 09:12:55 -0600 Subject: [PATCH 10/12] refactor: use the unavailable status list pattern Co-authored-by: Copilot --- src/components/InstitutionTile.js | 7 ++----- src/views/search/Search.js | 15 ++++----------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/components/InstitutionTile.js b/src/components/InstitutionTile.js index 4d4967ac70..30216f3699 100644 --- a/src/components/InstitutionTile.js +++ b/src/components/InstitutionTile.js @@ -10,7 +10,7 @@ import { ChevronRight } from '@kyper/icon/ChevronRight' import { InstitutionLogo } from '@mxenabled/mxui' import { formatUrl } from 'src/utilities/FormatUrl' -import { InstitutionStatus, useInstitutionStatus } from 'src/utilities/institutionStatus' +import { UNAVAILABLE_STATUSES, useInstitutionStatus } from 'src/utilities/institutionStatus' export const InstitutionTile = (props) => { const { institution, selectInstitution, size } = props @@ -22,10 +22,7 @@ export const InstitutionTile = (props) => { let statusChip = null if (institution.is_disabled_by_client) { statusChip = - } else if ( - status === InstitutionStatus.UNAVAILABLE || - status === InstitutionStatus.UNAVAILABLE_PER_MX - ) { + } else if (UNAVAILABLE_STATUSES.includes(status)) { statusChip = } diff --git a/src/views/search/Search.js b/src/views/search/Search.js index 104c8ebe9f..07778e07bf 100644 --- a/src/views/search/Search.js +++ b/src/views/search/Search.js @@ -43,7 +43,7 @@ import { SEARCH_PAGE_DEFAULT, SEARCH_PER_PAGE_DEFAULT } from 'src/views/search/c import { COMBO_JOB_DATA_TYPES } from 'src/const/comboJobDataTypes' import { PostMessageContext } from 'src/ConnectWidget' import styles from './search.module.css' -import { getInstitutionStatus, InstitutionStatus } from 'src/utilities/institutionStatus' +import { getInstitutionStatus, UNAVAILABLE_STATUSES } from 'src/utilities/institutionStatus' import { getExperimentalFeatures } from 'src/redux/reducers/experimentalFeaturesSlice' export const initialState = { @@ -218,19 +218,13 @@ export const Search = React.forwardRef((_, navigationRef) => { // Remove any Unavailable institutions from the popular/discovered lists const filteredPopularInstitutions = updatedPopularInstitutions.filter((popular) => { const status = getInstitutionStatus(popular, unavailableInstitutions) - return ( - status !== InstitutionStatus.UNAVAILABLE && - status !== InstitutionStatus.UNAVAILABLE_PER_MX - ) + return !UNAVAILABLE_STATUSES.includes(status) }) const filteredDiscoveredInstitutions = updatedDiscoveredInstitutions.filter( (discovered) => { const status = getInstitutionStatus(discovered, unavailableInstitutions) - return ( - status !== InstitutionStatus.UNAVAILABLE && - status !== InstitutionStatus.UNAVAILABLE_PER_MX - ) + return !UNAVAILABLE_STATUSES.includes(status) }, ) @@ -528,8 +522,7 @@ export const getSuggestedInstitutions = ( const status = getInstitutionStatus(popular, unavailableInstitutions) return ( !_find(connectedMembers, ['institution_guid', popular.guid]) && - status !== InstitutionStatus.UNAVAILABLE && - status !== InstitutionStatus.UNAVAILABLE_PER_MX + !UNAVAILABLE_STATUSES.includes(status) ) }) From d8978101845d463c07e1735a26d6d8029f646482 Mon Sep 17 00:00:00 2001 From: Logan Rasmussen Date: Mon, 27 Apr 2026 15:58:43 -0600 Subject: [PATCH 11/12] refactor(institution): create a reusable function for checking if an institution is unavailable Co-authored-by: Copilot --- src/components/InstitutionTile.js | 4 ++-- src/redux/reducers/Connect.js | 6 +++--- src/utilities/institutionStatus.ts | 19 +++++++++++++++++-- src/views/search/Search.js | 8 ++++---- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/components/InstitutionTile.js b/src/components/InstitutionTile.js index 30216f3699..e7548fef3a 100644 --- a/src/components/InstitutionTile.js +++ b/src/components/InstitutionTile.js @@ -10,7 +10,7 @@ import { ChevronRight } from '@kyper/icon/ChevronRight' import { InstitutionLogo } from '@mxenabled/mxui' import { formatUrl } from 'src/utilities/FormatUrl' -import { UNAVAILABLE_STATUSES, useInstitutionStatus } from 'src/utilities/institutionStatus' +import { institutionIsUnavailable, useInstitutionStatus } from 'src/utilities/institutionStatus' export const InstitutionTile = (props) => { const { institution, selectInstitution, size } = props @@ -22,7 +22,7 @@ export const InstitutionTile = (props) => { let statusChip = null if (institution.is_disabled_by_client) { statusChip = - } else if (UNAVAILABLE_STATUSES.includes(status)) { + } else if (institutionIsUnavailable(status)) { statusChip = } diff --git a/src/redux/reducers/Connect.js b/src/redux/reducers/Connect.js index a22cd06802..a382f13843 100644 --- a/src/redux/reducers/Connect.js +++ b/src/redux/reducers/Connect.js @@ -20,7 +20,7 @@ import { institutionIsBlockedForCostReasons, memberIsBlockedForCostReasons, } from 'src/utilities/institutionBlocks' -import { getInstitutionStatus, UNAVAILABLE_STATUSES } from 'src/utilities/institutionStatus' +import { getInstitutionStatus, institutionIsUnavailable } from 'src/utilities/institutionStatus' export const defaultState = { error: null, // The most recent job request error, if any @@ -290,7 +290,7 @@ const selectInstitutionSuccess = (state, action) => { if ( action.payload.institution && (institutionIsBlockedForCostReasons(action.payload.institution) || - UNAVAILABLE_STATUSES.includes(action.payload.institutionStatus)) + institutionIsUnavailable(action.payload.institutionStatus)) ) { nextStep = STEPS.INSTITUTION_STATUS_DETAILS } else if (action.payload.user?.is_demo && !action.payload.institution?.is_demo) { @@ -545,7 +545,7 @@ function getStartingStep( // directly to an institution that should be unavailable. const unavailableInstitutions = experimentalFeatures?.unavailableInstitutions || [] const institutionStatus = getInstitutionStatus(institution, unavailableInstitutions) - const institutionIsAvailable = institution && !UNAVAILABLE_STATUSES.includes(institutionStatus) + const institutionIsAvailable = institution && !institutionIsUnavailable(institutionStatus) const shouldStepToMFA = member && config.update_credentials && member.connection_status === ReadableStatuses.CHALLENGED diff --git a/src/utilities/institutionStatus.ts b/src/utilities/institutionStatus.ts index 7f3f26b83f..4ff85f83bb 100644 --- a/src/utilities/institutionStatus.ts +++ b/src/utilities/institutionStatus.ts @@ -10,13 +10,15 @@ export const InstitutionStatus = { OPERATIONAL: 'OPERATIONAL', UNAVAILABLE_PER_MX: 'UNAVAILABLE_PER_MX', UNAVAILABLE: 'UNAVAILABLE', // Experimental feature status, will be remove eventually -} +} as const +type InstitutionStatusValue = (typeof InstitutionStatus)[keyof typeof InstitutionStatus] // These are the status values that should show the "Unavailable Tag" -export const UNAVAILABLE_STATUSES = [ +const UNAVAILABLE_STATUSES = [ InstitutionStatus.UNAVAILABLE, InstitutionStatus.UNAVAILABLE_PER_MX, ] as const +type UnavailableStatusType = (typeof UNAVAILABLE_STATUSES)[number] // The InstitutionStatusType and InstitutionStatusField below are API defined values, this is our mapping for them export const InstitutionStatusField = { @@ -95,6 +97,10 @@ export function useInstitutionStatus( return getInstitutionStatus(institution, unavailableInstitutions || []) } +// ----------------------------------------------------- +// Non-hook functions that operate on institution status +// ----------------------------------------------------- + export function getInstitutionStatus( institution: { guid: string @@ -135,3 +141,12 @@ export function getInstitutionStatus( return InstitutionStatus.OPERATIONAL } + +/** + * @description This function is meant to be used after getInstitutionStatus(...) + */ +export function institutionIsUnavailable( + status: InstitutionStatusValue, +): status is UnavailableStatusType { + return (UNAVAILABLE_STATUSES as readonly InstitutionStatusValue[]).includes(status) +} diff --git a/src/views/search/Search.js b/src/views/search/Search.js index 07778e07bf..f8ebe6eb27 100644 --- a/src/views/search/Search.js +++ b/src/views/search/Search.js @@ -43,7 +43,7 @@ import { SEARCH_PAGE_DEFAULT, SEARCH_PER_PAGE_DEFAULT } from 'src/views/search/c import { COMBO_JOB_DATA_TYPES } from 'src/const/comboJobDataTypes' import { PostMessageContext } from 'src/ConnectWidget' import styles from './search.module.css' -import { getInstitutionStatus, UNAVAILABLE_STATUSES } from 'src/utilities/institutionStatus' +import { getInstitutionStatus, institutionIsUnavailable } from 'src/utilities/institutionStatus' import { getExperimentalFeatures } from 'src/redux/reducers/experimentalFeaturesSlice' export const initialState = { @@ -218,13 +218,13 @@ export const Search = React.forwardRef((_, navigationRef) => { // Remove any Unavailable institutions from the popular/discovered lists const filteredPopularInstitutions = updatedPopularInstitutions.filter((popular) => { const status = getInstitutionStatus(popular, unavailableInstitutions) - return !UNAVAILABLE_STATUSES.includes(status) + return !institutionIsUnavailable(status) }) const filteredDiscoveredInstitutions = updatedDiscoveredInstitutions.filter( (discovered) => { const status = getInstitutionStatus(discovered, unavailableInstitutions) - return !UNAVAILABLE_STATUSES.includes(status) + return !institutionIsUnavailable(status) }, ) @@ -522,7 +522,7 @@ export const getSuggestedInstitutions = ( const status = getInstitutionStatus(popular, unavailableInstitutions) return ( !_find(connectedMembers, ['institution_guid', popular.guid]) && - !UNAVAILABLE_STATUSES.includes(status) + !institutionIsUnavailable(status) ) }) From 6643d8b5355f7e7f7c5b93ae0114b7f688c1a457 Mon Sep 17 00:00:00 2001 From: Logan Rasmussen Date: Mon, 27 Apr 2026 16:04:48 -0600 Subject: [PATCH 12/12] refactor(institution): add concise naming to the status function --- src/components/InstitutionTile.js | 7 +++++-- src/redux/reducers/Connect.js | 9 ++++++--- src/utilities/institutionStatus.ts | 2 +- src/views/search/Search.js | 11 +++++++---- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/components/InstitutionTile.js b/src/components/InstitutionTile.js index e7548fef3a..63aaef1d01 100644 --- a/src/components/InstitutionTile.js +++ b/src/components/InstitutionTile.js @@ -10,7 +10,10 @@ import { ChevronRight } from '@kyper/icon/ChevronRight' import { InstitutionLogo } from '@mxenabled/mxui' import { formatUrl } from 'src/utilities/FormatUrl' -import { institutionIsUnavailable, useInstitutionStatus } from 'src/utilities/institutionStatus' +import { + institutionStatusIsUnavailable, + useInstitutionStatus, +} from 'src/utilities/institutionStatus' export const InstitutionTile = (props) => { const { institution, selectInstitution, size } = props @@ -22,7 +25,7 @@ export const InstitutionTile = (props) => { let statusChip = null if (institution.is_disabled_by_client) { statusChip = - } else if (institutionIsUnavailable(status)) { + } else if (institutionStatusIsUnavailable(status)) { statusChip = } diff --git a/src/redux/reducers/Connect.js b/src/redux/reducers/Connect.js index a382f13843..4a4ebdcff3 100644 --- a/src/redux/reducers/Connect.js +++ b/src/redux/reducers/Connect.js @@ -20,7 +20,10 @@ import { institutionIsBlockedForCostReasons, memberIsBlockedForCostReasons, } from 'src/utilities/institutionBlocks' -import { getInstitutionStatus, institutionIsUnavailable } from 'src/utilities/institutionStatus' +import { + getInstitutionStatus, + institutionStatusIsUnavailable, +} from 'src/utilities/institutionStatus' export const defaultState = { error: null, // The most recent job request error, if any @@ -290,7 +293,7 @@ const selectInstitutionSuccess = (state, action) => { if ( action.payload.institution && (institutionIsBlockedForCostReasons(action.payload.institution) || - institutionIsUnavailable(action.payload.institutionStatus)) + institutionStatusIsUnavailable(action.payload.institutionStatus)) ) { nextStep = STEPS.INSTITUTION_STATUS_DETAILS } else if (action.payload.user?.is_demo && !action.payload.institution?.is_demo) { @@ -545,7 +548,7 @@ function getStartingStep( // directly to an institution that should be unavailable. const unavailableInstitutions = experimentalFeatures?.unavailableInstitutions || [] const institutionStatus = getInstitutionStatus(institution, unavailableInstitutions) - const institutionIsAvailable = institution && !institutionIsUnavailable(institutionStatus) + const institutionIsAvailable = institution && !institutionStatusIsUnavailable(institutionStatus) const shouldStepToMFA = member && config.update_credentials && member.connection_status === ReadableStatuses.CHALLENGED diff --git a/src/utilities/institutionStatus.ts b/src/utilities/institutionStatus.ts index 4ff85f83bb..ada6765e29 100644 --- a/src/utilities/institutionStatus.ts +++ b/src/utilities/institutionStatus.ts @@ -145,7 +145,7 @@ export function getInstitutionStatus( /** * @description This function is meant to be used after getInstitutionStatus(...) */ -export function institutionIsUnavailable( +export function institutionStatusIsUnavailable( status: InstitutionStatusValue, ): status is UnavailableStatusType { return (UNAVAILABLE_STATUSES as readonly InstitutionStatusValue[]).includes(status) diff --git a/src/views/search/Search.js b/src/views/search/Search.js index f8ebe6eb27..eead808678 100644 --- a/src/views/search/Search.js +++ b/src/views/search/Search.js @@ -43,7 +43,10 @@ import { SEARCH_PAGE_DEFAULT, SEARCH_PER_PAGE_DEFAULT } from 'src/views/search/c import { COMBO_JOB_DATA_TYPES } from 'src/const/comboJobDataTypes' import { PostMessageContext } from 'src/ConnectWidget' import styles from './search.module.css' -import { getInstitutionStatus, institutionIsUnavailable } from 'src/utilities/institutionStatus' +import { + getInstitutionStatus, + institutionStatusIsUnavailable, +} from 'src/utilities/institutionStatus' import { getExperimentalFeatures } from 'src/redux/reducers/experimentalFeaturesSlice' export const initialState = { @@ -218,13 +221,13 @@ export const Search = React.forwardRef((_, navigationRef) => { // Remove any Unavailable institutions from the popular/discovered lists const filteredPopularInstitutions = updatedPopularInstitutions.filter((popular) => { const status = getInstitutionStatus(popular, unavailableInstitutions) - return !institutionIsUnavailable(status) + return !institutionStatusIsUnavailable(status) }) const filteredDiscoveredInstitutions = updatedDiscoveredInstitutions.filter( (discovered) => { const status = getInstitutionStatus(discovered, unavailableInstitutions) - return !institutionIsUnavailable(status) + return !institutionStatusIsUnavailable(status) }, ) @@ -522,7 +525,7 @@ export const getSuggestedInstitutions = ( const status = getInstitutionStatus(popular, unavailableInstitutions) return ( !_find(connectedMembers, ['institution_guid', popular.guid]) && - !institutionIsUnavailable(status) + !institutionStatusIsUnavailable(status) ) })