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
5 changes: 5 additions & 0 deletions .changeset/calm-codes-render.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@onkernel/managed-auth-react": patch
---

Render typed reauthentication blockers and rejected authenticator codes with specific recovery guidance.
29 changes: 29 additions & 0 deletions packages/managed-auth-react/src/components/StepError.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, test } from "bun:test";
import { errorDisplayForCode } from "./StepError";

describe("errorDisplayForCode", () => {
test("renders typed challenge outcomes with context-neutral copy", () => {
const cases = [
["totp_required", "Authenticator code required"],
["sms_code_required", "SMS code required"],
["email_code_required", "Email code required"],
["account_choice_required", "Account selection required"],
["customer_input_required", "Additional input required"],
["external_action_required", "External action required"],
];

for (const [code, title] of cases) {
const display = errorDisplayForCode(code);
expect(display?.title).toBe(title);
expect(display?.description.toLowerCase()).not.toContain("reauth");
}
});

test("distinguishes a rejected authenticator code from invalid credentials", () => {
const display = errorDisplayForCode("totp_code_rejected");

expect(display?.title).toBe("Authenticator code rejected");
expect(display?.description).toContain("Try a fresh code");
expect(display).not.toEqual(errorDisplayForCode("credentials_invalid"));
});
});
39 changes: 38 additions & 1 deletion packages/managed-auth-react/src/components/StepError.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,39 @@ const ERROR_DISPLAY: Record<string, { title: string; description: string }> = {
title: "Invalid credentials",
description: "The username or password was not accepted by the website.",
},
totp_required: {
title: "Authenticator code required",
description:
"An authenticator code is required, but no saved TOTP secret is available.",
},
sms_code_required: {
title: "SMS code required",
description: "A code sent by SMS is required.",
},
email_code_required: {
title: "Email code required",
description: "A code sent by email is required.",
},
account_choice_required: {
title: "Account selection required",
description:
"An account or identity must be selected before login can continue.",
},
customer_input_required: {
title: "Additional input required",
description:
"Additional customer input is required before login can continue.",
},
external_action_required: {
title: "External action required",
description:
"An external action is required to continue. Check your authenticator app, email, or phone for a verification request.",
},
totp_code_rejected: {
title: "Authenticator code rejected",
description:
"The website rejected the authenticator code. Try a fresh code, or reconnect the account if generated codes keep failing across new code windows.",
},
bot_detected: {
title: "Verification required",
description:
Expand Down Expand Up @@ -66,6 +99,10 @@ const ERROR_DISPLAY: Record<string, { title: string; description: string }> = {
},
};

export function errorDisplayForCode(errorCode?: string) {
return errorCode ? ERROR_DISPLAY[errorCode] : undefined;
}

function extractErrorText(raw: string): string {
try {
const parsed = JSON.parse(raw);
Expand All @@ -92,7 +129,7 @@ export function StepError({
const siteName = extractDomainName(targetDomain);
const [showDetails, setShowDetails] = useState(false);

const display = errorCode ? ERROR_DISPLAY[errorCode] : undefined;
const display = errorDisplayForCode(errorCode);
const title = display?.title ?? l.errorTitle;
const description = display?.description ?? l.errorGenericMessage;

Expand Down
Loading