Skip to content

Surface classified card activation failures to users - #2519

Merged
MusabShakeel576 merged 2 commits into
masterfrom
claude/cool-ride-g3nzu2
Sep 14, 2026
Merged

MusabShakeel576 merged 2 commits into
masterfrom
claude/cool-ride-g3nzu2

Conversation

@MusabShakeel576

Copy link
Copy Markdown
Contributor

Summary

This PR improves the card activation failure experience by surfacing classified failure reasons from the backend to users, replacing generic error messages with specific, actionable explanations.

Key Changes

  • Enhanced error handling in API client: Modified createCard() and submitCardConsents() to throw ApiError instead of bare Response objects, preserving server error messages for display to users.

  • New CardActivationFailure type: Added a structured type to represent classified card activation failures with:

    • code: Machine-readable failure code for branching logic
    • reason: User-facing headline
    • detail: Explanation and guidance
    • terminal: Boolean indicating if retry is futile
  • Updated CardStatusBanner component:

    • Now accepts optional failure prop to display classified failures
    • Distinguishes between self-service failures (yellow) and support-escalation failures (red)
    • Shows "Contact support" button only for non-self-service failures
    • Displays failure code as reference for support tickets
    • Added accessibility attributes (accessibilityRole="alert")
  • Improved card review state logic: Updated isCardIssuanceUnderReview() to recognize terminal failures and prevent them from being hidden behind "card is on its way" messaging.

  • Updated card steps and activation hooks: Modified to use classified failure information when available, gating the activate button for terminal failures while allowing retries for transient issues.

  • Immediate feedback on activation: Added queryClient.invalidateQueries() after activation attempts to refetch card status, ensuring failure details appear immediately rather than waiting for the next poll interval.

Notable Implementation Details

  • Fallback explanations: Included FALLBACK_DETAIL_BY_CODE map for legacy failures recorded before classification was implemented, preventing duplicate messaging.

  • Self-service vs. escalation: Failures like ACTIVATION_PENDING and TEMPORARY_FAILURE are treated as self-service (user should retry), while others like COUNTRY_NOT_SUPPORTED and ISSUER_DECLINED require support intervention.

  • Backward compatibility: Existing activationBlockedReason is still used as fallback when classified failure detail is unavailable.

https://claude.ai/code/session_015qyutjUxPkoGD5qv8n4ZNP

Two separate reasons a user never learned why their card would not activate.

First, `createCard` and `submitCardConsents` threw the bare `Response` on a
non-2xx. The activation handler renders `error instanceof Error ?
error.message : 'Something went wrong. Please try again.'`, and a Response is
not an Error — so the backend's reason was discarded unread on every single
failure, and everyone saw the fallback. That string is what users quoted back
to support verbatim. Both now throw `toApiError`, which already exists for
exactly this and carries `status`/`statusCode` so token refresh is unaffected.

Second, /card/activate could only render `activationBlockedReason`, which the
backend sends only alongside the sticky `activationBlocked` flag. Real issuance
failures do not set that flag, so the screen showed nothing and the steps list
fell back to "There was an issue activating your card. Please contact support."

The backend now classifies each failure and returns `activationFailure`
{code, reason, detail, terminal}. This renders it as a proper error state:
the reason as a headline, the detail explaining what happened and what to do,
a Contact support action, and the code as a reference support can quote back.

`terminal` drives the difference that matters — an unsupported document
country or an issuer decline stops offering the "Activate card" button, since
pressing it again can only reproduce the same failure, while a provisioning
delay or an upstream blip leaves it alone because retrying is the right move
there. It also stops "your card is on its way" from covering a failure that no
amount of waiting will clear.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qyutjUxPkoGD5qv8n4ZNP
@vercel

vercel Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated
solid-app Ignored Ignored Preview Sep 14, 2026 1:48pm UTC
solid-app-staging Ignored Ignored Preview Sep 14, 2026 1:48pm UTC

Request Review

Comment thread hooks/useCardSteps/useCardSteps.ts Outdated
Comment on lines +443 to +445
cardStatusResponse?.activationBlocked || cardStatusResponse?.activationFailure?.terminal,
cardStatusResponse?.activationFailure?.reason ??
cardStatusResponse?.activationBlockedReason,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: A terminal activation failure causes the failure reason to be displayed twice: once in the top banner and again in the expanded activation step.
Severity: LOW

Suggested Fix

Modify buildCardSteps to avoid setting the description for the activation step when a terminal activation failure has occurred, as the reason is already displayed in the CardStatusBanner. This will prevent the duplicate message from appearing when the step is expanded.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: hooks/useCardSteps/useCardSteps.ts#L443-L445

Potential issue: When a terminal card activation failure occurs, the failure reason from
`activationFailure.reason` is displayed in the `CardStatusBanner`. The new code also
passes this same reason to `buildCardSteps`, which sets it as the description for the
activation step. If a user expands this step, the failure reason is shown again,
resulting in a redundant message being displayed on the screen.

Did we get this right? 👍 / 👎 to inform future reviews.

@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

…list

Seer flagged the reason rendering twice on /card/activate, and it is right —
and it predates this branch for the `activationBlocked` path.

`CardStatusBanner` renders the reason as a headline with the detail and a
support action. `buildCardSteps` then set that same string as the activate
step's description, and `useStepNavigation` auto-expands the first incomplete
step — which for a blocked applicant IS the activate step. So the sentence met
the user twice on first paint, once as a headline and once four rows down.

The banner owns the explanation. The step now carries a short line pointing at
it, and `activationBlockedReason` is dropped from `buildCardSteps` rather than
left as a parameter nothing reads. Its only other consumer, useHomeSetupSteps,
writes its own descriptions and never read it.

Adds coverage for all three behaviours of a blocked activation: no duplicated
reason, no activate button (so it cannot reproduce the failure), and the button
still offered when nothing is blocking.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qyutjUxPkoGD5qv8n4ZNP
@MusabShakeel576
MusabShakeel576 merged commit 95ee201 into master Sep 14, 2026
5 checks passed
@MusabShakeel576
MusabShakeel576 deleted the claude/cool-ride-g3nzu2 branch September 14, 2026 13:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants