Surface classified card activation failures to users - #2519
Merged
Merged
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Comment on lines
+443
to
+445
| cardStatusResponse?.activationBlocked || cardStatusResponse?.activationFailure?.terminal, | ||
| cardStatusResponse?.activationFailure?.reason ?? | ||
| cardStatusResponse?.activationBlockedReason, |
Contributor
There was a problem hiding this comment.
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.
Code reviewNo 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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()andsubmitCardConsents()to throwApiErrorinstead of bareResponseobjects, preserving server error messages for display to users.New
CardActivationFailuretype: Added a structured type to represent classified card activation failures with:code: Machine-readable failure code for branching logicreason: User-facing headlinedetail: Explanation and guidanceterminal: Boolean indicating if retry is futileUpdated
CardStatusBannercomponent:failureprop to display classified failuresaccessibilityRole="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_CODEmap for legacy failures recorded before classification was implemented, preventing duplicate messaging.Self-service vs. escalation: Failures like
ACTIVATION_PENDINGandTEMPORARY_FAILUREare treated as self-service (user should retry), while others likeCOUNTRY_NOT_SUPPORTEDandISSUER_DECLINEDrequire support intervention.Backward compatibility: Existing
activationBlockedReasonis still used as fallback when classified failure detail is unavailable.https://claude.ai/code/session_015qyutjUxPkoGD5qv8n4ZNP