From e0a4bdb23b0c9d2f5b1f6f85d34701daffd337e3 Mon Sep 17 00:00:00 2001
From: Ayobami Haastrup <47716486+AyobamiH@users.noreply.github.com>
Date: Fri, 4 Sep 2026 21:19:23 +0100
Subject: [PATCH] fix(auth): show OAuth callback errors
---
src/app/login/login-form.test.tsx | 67 +++++++++++++++++++++++++++++++
src/app/login/login-form.tsx | 33 ++++++++++++++-
2 files changed, 99 insertions(+), 1 deletion(-)
create mode 100644 src/app/login/login-form.test.tsx
diff --git a/src/app/login/login-form.test.tsx b/src/app/login/login-form.test.tsx
new file mode 100644
index 0000000..cbea730
--- /dev/null
+++ b/src/app/login/login-form.test.tsx
@@ -0,0 +1,67 @@
+import { cleanup, fireEvent, render, screen } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+import { LoginForm } from "./login-form";
+
+const { searchParamsGet } = vi.hoisted(() => ({
+ searchParamsGet: vi.fn(),
+}));
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({ push: vi.fn(), refresh: vi.fn() }),
+ useSearchParams: () => ({ get: searchParamsGet }),
+}));
+
+vi.mock("@/lib/supabase/client", () => ({
+ createClient: () => ({ auth: {} }),
+}));
+
+beforeEach(() => {
+ searchParamsGet.mockImplementation((name: string) =>
+ name === "error" ? "auth_error" : null,
+ );
+});
+
+afterEach(() => {
+ cleanup();
+ vi.clearAllMocks();
+});
+
+describe("LoginForm OAuth errors", () => {
+ it("shows the callback error in a dismissible alert", () => {
+ render();
+
+ expect(screen.getByRole("alert").textContent).toContain(
+ "Sign-in failed or was cancelled. Please try again.",
+ );
+
+ fireEvent.click(
+ screen.getByRole("button", { name: "Dismiss sign-in error" }),
+ );
+
+ expect(screen.queryByRole("alert")).toBeNull();
+ });
+
+ it("uses safe fallback copy for an unknown error code", () => {
+ searchParamsGet.mockImplementation((name: string) =>
+ name === "error" ? "__proto__" : null,
+ );
+
+ render();
+
+ expect(screen.getByRole("alert").textContent).toContain(
+ "Sign-in failed. Please try again.",
+ );
+ expect(screen.getByRole("alert").textContent).not.toContain(
+ "__proto__",
+ );
+ });
+
+ it("does not show an alert without an error code", () => {
+ searchParamsGet.mockReturnValue(null);
+
+ render();
+
+ expect(screen.queryByRole("alert")).toBeNull();
+ });
+});
diff --git a/src/app/login/login-form.tsx b/src/app/login/login-form.tsx
index e91df90..df7c842 100644
--- a/src/app/login/login-form.tsx
+++ b/src/app/login/login-form.tsx
@@ -4,7 +4,7 @@ import * as React from "react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { toast } from "sonner";
-import { Loader2 } from "lucide-react";
+import { Loader2, TriangleAlert, X } from "lucide-react";
import { createClient } from "@/lib/supabase/client";
import { safeNext } from "@/lib/safe-next";
@@ -13,10 +13,20 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
+const AUTH_ERROR_MESSAGES = new Map([
+ ["auth_error", "Sign-in failed or was cancelled. Please try again."],
+]);
+
+const DEFAULT_AUTH_ERROR_MESSAGE = "Sign-in failed. Please try again.";
+
export function LoginForm() {
const router = useRouter();
const searchParams = useSearchParams();
const next = safeNext(searchParams.get("next"));
+ const errorCode = searchParams.get("error");
+ const errorMessage = errorCode
+ ? (AUTH_ERROR_MESSAGES.get(errorCode) ?? DEFAULT_AUTH_ERROR_MESSAGE)
+ : null;
const supabase = createClient();
@@ -24,6 +34,9 @@ export function LoginForm() {
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const [loading, setLoading] = React.useState(false);
+ const [dismissedErrorCode, setDismissedErrorCode] = React.useState(
+ null,
+ );
// Self-host / preview mode: Supabase isn't configured, so there's no auth.
if (!supabase) {
@@ -114,6 +127,24 @@ export function LoginForm() {
+ {errorMessage && dismissedErrorCode !== errorCode && (
+
+
+
{errorMessage}
+
+
+ )}
+
{/* Card */}
{/* Google */}