Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const POPULATED_KEYS = new Set([
'stepUpFailure:body',
'common:linkContactUs',
'common:back',
'common:continue',
'dashboard:alertApplicationsTitle',
'dashboard:alertApplicationsBody',
'dashboard:alertApplicationsAction'
Expand Down Expand Up @@ -75,6 +76,10 @@ vi.mock('@/features/auth', () => ({
data-apply-label={String(props.applyLabel)}
data-back-href={String(props.backHref)}
data-back-label={String(props.backLabel)}
data-body-list={String(props.bodyList)}
data-body-note={String(props.bodyNote)}
data-continue-href={String(props.continueHref)}
data-continue-label={String(props.continueLabel)}
/>
)
}))
Expand Down Expand Up @@ -195,8 +200,18 @@ describe('OffBoardingPage', () => {
const content = screen.getByTestId('off-boarding-content')
expect(content).toHaveAttribute('data-title', 'offBoarding:title')
expect(content).toHaveAttribute('data-body', 'offBoarding:body1')
expect(content).toHaveAttribute('data-apply-body', 'offBoarding:body2')
expect(content).toHaveAttribute('data-apply-label', 'offBoarding:action2')
// body2/body3 are now core body content (list + note), not the apply section
expect(content).toHaveAttribute('data-body-list', 'offBoarding:body2')
expect(content).toHaveAttribute('data-body-note', 'offBoarding:body3')
expect(content).toHaveAttribute('data-apply-body', 'undefined')
})

it('passes a Continue primary action to /login/id-proofing on the generic screen', () => {
renderPage({ isCoLoaded: false })

const content = screen.getByTestId('off-boarding-content')
expect(content).toHaveAttribute('data-continue-href', '/login/id-proofing')
expect(content).toHaveAttribute('data-continue-label', 'common:continue')
})

it('uses the co-loaded off-boarding copy when the session is co-loaded', () => {
Expand Down Expand Up @@ -279,15 +294,14 @@ describe('OffBoardingPage', () => {
expect(content).toHaveAttribute('data-title', 'offBoarding:title')
})

it('renders docVerificationFailed-specific title and body when reason is docVerificationFailed', async () => {
it('routes docVerificationFailed to the generic "keep your account safe" screen with Continue', async () => {
await renderPage({ reason: 'docVerificationFailed' })

const content = screen.getByTestId('off-boarding-content')
expect(content).toHaveAttribute('data-title', "We couldn't verify your identity")
expect(content).toHaveAttribute(
'data-body',
"Your document couldn't be verified. You can try again with a different ID, or contact us if you need help."
)
expect(content).toHaveAttribute('data-title', 'offBoarding:title')
expect(content).toHaveAttribute('data-body', 'offBoarding:body1')
expect(content).toHaveAttribute('data-continue-href', '/login/id-proofing')
expect(content).toHaveAttribute('data-continue-label', 'common:continue')
})

it('uses step-up failure copy for OIDC callback errors (CO MyCO step-up)', async () => {
Expand All @@ -308,13 +322,6 @@ describe('OffBoardingPage', () => {
expect(content).toHaveAttribute('data-title', 'stepUpFailure:title')
})

it('forces canApply to false for docVerificationFailed regardless of query param', async () => {
await renderPage({ reason: 'docVerificationFailed', canApply: 'true' })

const content = screen.getByTestId('off-boarding-content')
expect(content).toHaveAttribute('data-can-apply', 'false')
})

it('uses dashboard application-alert copy when reason is noQualifyingHousehold', async () => {
await renderPage({ reason: 'noQualifyingHousehold' })

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export default function OffBoardingPage() {
let applyBody: string | undefined
let applySkipBody: string | undefined
let applyLabel: string | undefined
let bodyList: string[] | undefined
let bodyNote: string | undefined
let continueHref: string | undefined
let continueLabel: string | undefined

if (reason === 'oidcCallbackError') {
title =
Expand Down Expand Up @@ -78,23 +82,19 @@ export default function OffBoardingPage() {
applyBody = undefined
applySkipBody = undefined
applyLabel = undefined
} else if (reason === 'docVerificationFailed') {
title = "We couldn't verify your identity"
body =
"Your document couldn't be verified. You can try again with a different ID, or contact us if you need help."
canApply = false
contactLabel = tCommon('linkContactUs')
applyBody = undefined
applySkipBody = undefined
applyLabel = undefined
} else {
// Generic "We want to keep your account safe" screen. Both inline Socure
// rejects (reason=idProofingFailed) and webhook rejects/resubmits land here.
// The accepted-ID list and skip note are core body content; the primary
// action is "Continue" (forward), with "Enter an ID number" as the back
// affordance. Both route to the form; contact lives in the global help band.
title = t('title')
body = t('body1')
// TODO: Use t('action1') once key is available in dc.csv
contactLabel = tCommon('linkContactUs')
applyBody = t('body2', '') || undefined
applySkipBody = t('body3', '') || undefined
applyLabel = t('action2', '') || undefined
bodyList = (t('body2', '') || '').split('\n').filter(Boolean)
bodyNote = t('body3', '') || undefined
continueHref = '/login/id-proofing'
continueLabel = tCommon('continue')
}

return (
Expand All @@ -113,6 +113,10 @@ export default function OffBoardingPage() {
applySkipBody={applySkipBody}
applyLabel={applyLabel}
applyHref={getApplyHref(i18n.language)}
bodyList={bodyList}
bodyNote={bodyNote}
continueHref={continueHref}
continueLabel={continueLabel}
/>
</section>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ export function DocVerifyPage({ contactLink }: DocVerifyPageProps) {
trackEvent(AnalyticsEvents.IDV_FINAL_RESULT)
clearChallengeContext()
// Pass the reason via URL so the off-boarding route can render distinct
// copy (docVerificationFailed, challengeNotFound, etc). Mirrors the
// pattern IdProofingForm uses for noIdProvided.
// copy where it has a dedicated branch (e.g. noIdProvided). Socure
// rejects route to the generic "keep your account safe" screen.
const params = new URLSearchParams()
if (offboardingReason) {
params.set('reason', offboardingReason)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
render(<OffBoardingContent {...DEFAULT_PROPS} />)

const contactLink = screen.getByRole('link', {
name: new RegExp(`${DEFAULT_PROPS.contactLabel}`)

Check warning on line 56 in src/SEBT.Portal.Web/src/features/auth/components/id-proofing/OffBoardingContent.test.tsx

View workflow job for this annotation

GitHub Actions / co - Build & Test

Found non-literal argument to RegExp Constructor

Check warning on line 56 in src/SEBT.Portal.Web/src/features/auth/components/id-proofing/OffBoardingContent.test.tsx

View workflow job for this annotation

GitHub Actions / dc - Build & Test

Found non-literal argument to RegExp Constructor
})
expect(contactLink).toHaveAttribute('href', DEFAULT_PROPS.contactHref)
})
Expand All @@ -78,7 +78,7 @@
)

const contactLink = screen.getByRole('link', {
name: new RegExp(`${DEFAULT_PROPS.contactLabel}`)

Check warning on line 81 in src/SEBT.Portal.Web/src/features/auth/components/id-proofing/OffBoardingContent.test.tsx

View workflow job for this annotation

GitHub Actions / co - Build & Test

Found non-literal argument to RegExp Constructor

Check warning on line 81 in src/SEBT.Portal.Web/src/features/auth/components/id-proofing/OffBoardingContent.test.tsx

View workflow job for this annotation

GitHub Actions / dc - Build & Test

Found non-literal argument to RegExp Constructor
})
expect(contactLink).not.toHaveAttribute('target')
expect(contactLink).not.toHaveTextContent(/opens in a new tab/i)
Expand Down Expand Up @@ -186,4 +186,90 @@
expect(paragraphs).toHaveLength(1)
})
})

describe('Continue action', () => {
const CONTINUE_PROPS = {
...DEFAULT_PROPS,
continueHref: '/login/id-proofing',
continueLabel: 'Continue'
}

it('renders a Continue button as the primary action when continue props are provided', () => {
render(<OffBoardingContent {...CONTINUE_PROPS} />)

const continueLink = screen.getByRole('link', { name: 'Continue' })
expect(continueLink).toHaveAttribute('href', '/login/id-proofing')
expect(continueLink).toHaveClass('usa-button')
})

it('omits the Contact button when Continue is provided', () => {
render(<OffBoardingContent {...CONTINUE_PROPS} />)

expect(screen.queryByRole('link', { name: /contact us/i })).not.toBeInTheDocument()
})

it('keeps the "Enter an ID number" outline button alongside Continue', () => {
render(<OffBoardingContent {...CONTINUE_PROPS} />)

const backLink = screen.getByRole('link', { name: CONTINUE_PROPS.backLabel })
expect(backLink).toHaveClass('usa-button--outline')
})

it('renders the Contact button (not Continue) when continue props are absent', () => {
render(<OffBoardingContent {...DEFAULT_PROPS} />)

expect(screen.queryByRole('link', { name: 'Continue' })).not.toBeInTheDocument()
expect(screen.getByRole('link', { name: /contact us/i })).toBeInTheDocument()
})
})

describe('Body list and note (Figma core body content)', () => {
const ID_TYPES = ['driver’s license', 'foreign passport', 'or another photo ID']

it('renders bodyList items as list items above the buttons', () => {
render(
<OffBoardingContent
{...DEFAULT_PROPS}
bodyList={ID_TYPES}
/>
)

const items = screen.getAllByRole('listitem')
expect(items.map((li) => li.textContent)).toEqual(ID_TYPES)
})

it('renders the bodyNote paragraph when provided', () => {
const note =
'You may be able to skip this step by going back and typing in an ID number instead.'
render(
<OffBoardingContent
{...DEFAULT_PROPS}
bodyNote={note}
/>
)

expect(screen.getByText(note)).toBeInTheDocument()
})

it('shows bodyList and bodyNote even when canApply is false', () => {
const note = 'Skip note text.'
render(
<OffBoardingContent
{...DEFAULT_PROPS}
canApply={false}
bodyList={ID_TYPES}
bodyNote={note}
/>
)

expect(screen.getAllByRole('listitem')).toHaveLength(ID_TYPES.length)
expect(screen.getByText(note)).toBeInTheDocument()
})

it('renders no list when bodyList is empty or omitted', () => {
render(<OffBoardingContent {...DEFAULT_PROPS} />)

expect(screen.queryByRole('listitem')).not.toBeInTheDocument()
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ interface OffBoardingContentProps {
applySkipBody?: string | undefined
applyLabel?: string | undefined
applyHref?: string | undefined
/**
* Items shown as a bulleted list directly under the body (e.g. accepted ID
* types). Always visible, independent of canApply.
*/
bodyList?: string[] | undefined
/** Note shown under the body list (e.g. the "you may be able to skip" line). */
bodyNote?: string | undefined
/**
* When both are provided, a "Continue" primary button replaces the Contact
* button. Contact stays reachable via the global help section below.
*/
continueHref?: string | undefined
continueLabel?: string | undefined
}

export function OffBoardingContent({
Expand All @@ -25,9 +38,14 @@ export function OffBoardingContent({
applyBody,
applySkipBody,
applyLabel,
applyHref
applyHref,
bodyList,
bodyNote,
continueHref,
continueLabel
}: OffBoardingContentProps) {
const isExternalLink = contactHref.startsWith('http')
const showContinue = Boolean(continueHref && continueLabel)

return (
<>
Expand All @@ -40,21 +58,40 @@ export function OffBoardingContent({

<p className="font-sans-sm">{body}</p>

{bodyList && bodyList.length > 0 && (
<ul className="usa-list font-sans-sm">
{bodyList.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)}

{bodyNote && <p className="font-sans-sm">{bodyNote}</p>}

<div className="display-flex flex-row gap-2 margin-y-4">
<Link
href={backHref}
className="usa-button usa-button--outline margin-right-2"
>
{backLabel}
</Link>
<a
href={contactHref}
{...(isExternalLink ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
className="usa-button"
>
{contactLabel}
{isExternalLink && <span className="usa-sr-only"> (opens in a new tab)</span>}
</a>
{showContinue ? (
<Link
href={continueHref!}
className="usa-button"
>
{continueLabel}
</Link>
) : (
<a
href={contactHref}
{...(isExternalLink ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
className="usa-button"
>
{contactLabel}
{isExternalLink && <span className="usa-sr-only"> (opens in a new tab)</span>}
</a>
)}
</div>

{canApply && (applyBody || applySkipBody || (applyLabel && applyHref)) && (
Expand Down
Loading