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
45 changes: 14 additions & 31 deletions src/views/actionableError/ActionableError.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useRef } from 'react'
import React, { useContext, useEffect } from 'react'
import { useSelector } from 'react-redux'
import { InstitutionLogo, Text } from '@mxenabled/mxui'
import { useTokens } from '@kyper/tokenprovider'
Expand All @@ -7,7 +7,6 @@ import { Button, Badge } from '@mui/material'
import { SlideDown } from 'src/components/SlideDown'
import { PostMessageContext } from 'src/ConnectWidget'
import { useActionableErrorMap } from 'src/views/actionableError/useActionableErrorMap'
import { Support as UntypedSupport, VIEWS as SUPPORT_VIEWS } from 'src/components/support/Support'

import { ACTIONABLE_ERROR_CODES_READABLE } from 'src/views/actionableError/consts'
import { PageviewInfo } from 'src/const/Analytics'
Expand All @@ -17,18 +16,7 @@ import { useAnalyticsPath } from 'src/hooks/useAnalyticsPath'
import { RootState } from 'src/redux/Store'
import { getCurrentMember } from 'src/redux/selectors/Connect'

// This is due to trying to forwardRef a component written in JS
const Support = UntypedSupport as React.ForwardRefExoticComponent<
React.PropsWithoutRef<{
loadToView: (typeof SUPPORT_VIEWS)[keyof typeof SUPPORT_VIEWS]
onClose: () => void
}> &
// eslint-disable-next-line @typescript-eslint/no-explicit-any
React.RefAttributes<any>
>

export const ActionableError = () => {
const supportNavRef = useRef(null)
const postMessageFunctions = useContext(PostMessageContext)
const institution = useSelector((state: RootState) => state.connect.selectedInstitution)
const currentMember = useSelector(getCurrentMember)
Expand All @@ -41,8 +29,7 @@ export const ActionableError = () => {
const tokens = useTokens()
const styles = getStyles(tokens)
const getNextDelay = getDelay()
const [showSupport, setShowSupport] = React.useState(false)
const errorDetails = useActionableErrorMap(jobDetailCode, setShowSupport)
const errorDetails = useActionableErrorMap(jobDetailCode)

useEffect(() => {
// Legacy postMessage for backwards compatibility
Expand All @@ -54,13 +41,7 @@ export const ActionableError = () => {
})
}, [jobDetailCode])

return showSupport ? (
<Support
loadToView={SUPPORT_VIEWS.GENERAL_SUPPORT}
onClose={() => setShowSupport(false)}
ref={supportNavRef}
/>
) : (
return (
<>
<SlideDown delay={getNextDelay()}>
<div style={styles.logoWrapper}>
Expand Down Expand Up @@ -105,15 +86,17 @@ export const ActionableError = () => {
>
{errorDetails?.primaryAction.label}
</Button>
<Button
data-test="actionable-error-secondary-button"
fullWidth={true}
onClick={errorDetails?.secondaryActions.action}
style={{ marginBottom: 8 }}
variant="text"
>
{errorDetails?.secondaryActions.label}
</Button>
{errorDetails?.secondaryActions && (
<Button
data-test="actionable-error-secondary-button"
fullWidth={true}
onClick={errorDetails?.secondaryActions.action}
style={{ marginBottom: 8 }}
variant="text"
>
{errorDetails?.secondaryActions.label}
</Button>
)}
</SlideDown>
</>
)
Expand Down
32 changes: 31 additions & 1 deletion src/views/actionableError/__tests__/ActionableError-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,42 @@ describe('ActionableError', () => {
expect(primaryButton).toHaveClass('MuiButton-contained')
})

it('should render secondary action buttons', () => {
it('should render secondary action buttons if they exist', () => {
render(<ActionableError />, {
preloadedState: initialState,
})
const secondaryButton = screen.getByRole('button', { name: 'Connect a different institution' })
expect(secondaryButton).toBeInTheDocument()
expect(secondaryButton).toHaveClass('MuiButton-text')
})

it('should not render secondary action if it does not exist in the mapping', () => {
const modifiedInitialState = {
...initialState,
connect: {
...initialState.connect,
selectedInstitution: institutionMock,
currentMemberGuid: membersMock[0].guid,
members: [
{
guid: 'MEM-123',
error: {
error_code: ACTIONABLE_ERROR_CODES.NO_ACCOUNTS,
error_message: 'No accounts found.',
error_type: 'MEMBER',
locale: 'en',
user_message:
'This may be due to closed accounts, revoked access, or a connection issue. Please try again later or connect a different institution.',
},
name: 'Member',
},
],
},
}
render(<ActionableError />, {
preloadedState: modifiedInitialState,
})
const secondaryButton = screen.queryByTestId('actionable-error-secondary-button')
expect(secondaryButton).not.toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { AGG_MODE, VERIFY_MODE } from 'src/const/Connect'
import { initialState as configInitialState } from 'src/redux/reducers/configSlice'

// Setup Mocks
const setShowSupport = vitest.fn()
export const dispatch = vitest.fn()
vitest.mock('react-redux', async (importActual) => {
const actual = (await importActual()) as object
Expand Down Expand Up @@ -36,17 +35,19 @@ const aggregationPreloadedState = {

// Test Component to utilize the hook
const TestComponent = ({ errorCode }: { errorCode: number }) => {
const errorDetails = useActionableErrorMap(errorCode, setShowSupport)
const errorDetails = useActionableErrorMap(errorCode)

return (
<div>
<h1>{errorDetails.title}</h1>
<button onClick={errorDetails.primaryAction.action}>
{errorDetails.primaryAction.label}
</button>
<button onClick={errorDetails.secondaryActions.action}>
{errorDetails.secondaryActions.label}
</button>
{errorDetails.secondaryActions && (
<button onClick={errorDetails.secondaryActions.action}>
{errorDetails.secondaryActions.label}
</button>
)}
</div>
)
}
Expand Down Expand Up @@ -83,19 +84,15 @@ describe('useActionableErrorMap', () => {
})
expect(screen.getByText('No accounts found')).toBeInTheDocument()
expect(screen.getByText('Return to institution selection')).toBeInTheDocument()
expect(screen.getByText('Get help')).toBeInTheDocument()
expect(screen.queryByText('Get help')).not.toBeInTheDocument()
Comment thread
wesrisenmay-mx marked this conversation as resolved.

const primaryButton = screen.getByText('Return to institution selection')
const secondaryButton = screen.getByText('Get help')

primaryButton.click()
expect(dispatch).toHaveBeenCalledWith({
type: ActionTypes.ACTIONABLE_ERROR_CONNECT_DIFFERENT_INSTITUTION,
payload: AGG_MODE,
})

secondaryButton.click()
expect(setShowSupport).toHaveBeenCalledTimes(1)
})

it('should return correct mapping and actions for ACCESS_DENIED', () => {
Expand All @@ -104,18 +101,14 @@ describe('useActionableErrorMap', () => {
})
expect(screen.getByText('Additional permissions needed')).toBeInTheDocument()
expect(screen.getByText('Review instructions')).toBeInTheDocument()
expect(screen.getByText('Get help')).toBeInTheDocument()
expect(screen.queryByText('Get help')).not.toBeInTheDocument()

const primaryButton = screen.getByText('Review instructions')
const secondaryButton = screen.getByText('Get help')

primaryButton.click()
expect(dispatch).toHaveBeenCalledWith({
type: ActionTypes.ACTIONABLE_ERROR_LOG_IN_AGAIN,
})

secondaryButton.click()
expect(setShowSupport).toHaveBeenCalledTimes(1)
})

it('should return correct mapping and actions for INSTITUTION_DOWN', () => {
Expand All @@ -124,19 +117,15 @@ describe('useActionableErrorMap', () => {
})
expect(screen.getByText('Unable to connect')).toBeInTheDocument()
expect(screen.getByText('Return to institution selection')).toBeInTheDocument()
expect(screen.getByText('Get help')).toBeInTheDocument()
expect(screen.queryByText('Get help')).not.toBeInTheDocument()

const primaryButton = screen.getByText('Return to institution selection')
const secondaryButton = screen.getByText('Get help')

primaryButton.click()
expect(dispatch).toHaveBeenCalledWith({
type: ActionTypes.ACTIONABLE_ERROR_CONNECT_DIFFERENT_INSTITUTION,
payload: AGG_MODE,
})

secondaryButton.click()
expect(setShowSupport).toHaveBeenCalledTimes(1)
})

it('should return correct mapping and actions for INSTITUTION_MAINTENANCE', () => {
Expand All @@ -145,19 +134,15 @@ describe('useActionableErrorMap', () => {
})
expect(screen.getByText('Maintenance in progress')).toBeInTheDocument()
expect(screen.getByText('Return to institution selection')).toBeInTheDocument()
expect(screen.getByText('Get help')).toBeInTheDocument()
expect(screen.queryByText('Get help')).not.toBeInTheDocument()

const primaryButton = screen.getByText('Return to institution selection')
const secondaryButton = screen.getByText('Get help')

primaryButton.click()
expect(dispatch).toHaveBeenCalledWith({
type: ActionTypes.ACTIONABLE_ERROR_CONNECT_DIFFERENT_INSTITUTION,
payload: AGG_MODE,
})

secondaryButton.click()
expect(setShowSupport).toHaveBeenCalledTimes(1)
})

it('should return correct mapping and actions for INSTITUTION_UNAVAILABLE', () => {
Expand All @@ -166,18 +151,14 @@ describe('useActionableErrorMap', () => {
})
expect(screen.getByText('Unable to connect')).toBeInTheDocument()
expect(screen.getByText('Return to institution selection')).toBeInTheDocument()
expect(screen.getByText('Get help')).toBeInTheDocument()
expect(screen.queryByText('Get help')).not.toBeInTheDocument()

const primaryButton = screen.getByText('Return to institution selection')
const secondaryButton = screen.getByText('Get help')

primaryButton.click()
expect(dispatch).toHaveBeenCalledWith({
type: ActionTypes.ACTIONABLE_ERROR_CONNECT_DIFFERENT_INSTITUTION,
payload: AGG_MODE,
})

secondaryButton.click()
expect(setShowSupport).toHaveBeenCalledTimes(1)
})
})
13 changes: 2 additions & 11 deletions src/views/actionableError/useActionableErrorMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,10 @@ type ActionableErrorAction = {
type ActionableErrorMapEntry = {
title: string
primaryAction: ActionableErrorAction
secondaryActions: ActionableErrorAction
secondaryActions?: ActionableErrorAction
}

export const useActionableErrorMap = (
jobDetailCode: number,
setShowSupport: React.Dispatch<React.SetStateAction<boolean>>,
) => {
export const useActionableErrorMap = (jobDetailCode: number) => {
const postMessageFunctions = useContext(PostMessageContext)
const initialConfig = useSelector(selectInitialConfig)
const dispatch = useDispatch()
Expand All @@ -33,7 +30,6 @@ export const useActionableErrorMap = (
payload: initialConfig.mode || AGG_MODE,
})
}
const goToSupport = () => setShowSupport(true)
const goToCredentials = () => dispatch({ type: ActionTypes.ACTIONABLE_ERROR_LOG_IN_AGAIN })

// AED Step 3: Add code mapping for new codes here
Expand All @@ -47,27 +43,22 @@ export const useActionableErrorMap = (
[ACTIONABLE_ERROR_CODES.NO_ACCOUNTS]: {
title: __('No accounts found'),
primaryAction: { label: __('Return to institution selection'), action: goToSearch },
secondaryActions: { label: __('Get help'), action: goToSupport },
},
[ACTIONABLE_ERROR_CODES.ACCESS_DENIED]: {
title: __('Additional permissions needed'),
primaryAction: { label: __('Review instructions'), action: goToCredentials },
secondaryActions: { label: __('Get help'), action: goToSupport },
},
[ACTIONABLE_ERROR_CODES.INSTITUTION_DOWN]: {
title: __('Unable to connect'),
primaryAction: { label: __('Return to institution selection'), action: goToSearch },
secondaryActions: { label: __('Get help'), action: goToSupport },
},
[ACTIONABLE_ERROR_CODES.INSTITUTION_MAINTENANCE]: {
title: __('Maintenance in progress'),
primaryAction: { label: __('Return to institution selection'), action: goToSearch },
secondaryActions: { label: __('Get help'), action: goToSupport },
},
[ACTIONABLE_ERROR_CODES.INSTITUTION_UNAVAILABLE]: {
title: __('Unable to connect'),
primaryAction: { label: __('Return to institution selection'), action: goToSearch },
secondaryActions: { label: __('Get help'), action: goToSupport },
},
}),
[dispatch],
Expand Down
Loading