New email/password accounts must verify before signing in. Social login users (Google, Discord) are unaffected. Existing users are grandfathered via an idempotent migration. Adds a /check-email interstitial with a resend button, and a --success CSS token for consistent styling. Closes #343 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
89 lines
3 KiB
TypeScript
89 lines
3 KiB
TypeScript
import { useState } from "react";
|
|
import { Link, redirect, useLoaderData } from "react-router";
|
|
import { authClient } from "~/lib/auth-client";
|
|
import { Button } from "~/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "~/components/ui/card";
|
|
import type { Route } from "./+types/check-email";
|
|
|
|
export function meta(): Route.MetaDescriptors {
|
|
return [{ title: "Check Your Email - Brackt" }];
|
|
}
|
|
|
|
export function safeRedirectTo(value: string | null): string {
|
|
if (!value) return "/";
|
|
return value.startsWith("/") && !value.startsWith("//") ? value : "/";
|
|
}
|
|
|
|
export async function loader(args: Route.LoaderArgs) {
|
|
const url = new URL(args.request.url);
|
|
const email = url.searchParams.get("email");
|
|
if (!email) return redirect("/register");
|
|
return {
|
|
email,
|
|
redirectTo: safeRedirectTo(url.searchParams.get("redirectTo")),
|
|
};
|
|
}
|
|
|
|
type ResendState = "idle" | "sending" | "sent" | "error";
|
|
|
|
export default function CheckEmailPage() {
|
|
const { email, redirectTo } = useLoaderData<typeof loader>();
|
|
const [resendState, setResendState] = useState<ResendState>("idle");
|
|
const [resendError, setResendError] = useState<string | null>(null);
|
|
|
|
async function handleResend() {
|
|
setResendState("sending");
|
|
setResendError(null);
|
|
const { error } = await authClient.sendVerificationEmail({
|
|
email,
|
|
callbackURL: redirectTo,
|
|
});
|
|
if (error) {
|
|
setResendState("error");
|
|
setResendError(error.message ?? "Could not resend. Please try again.");
|
|
} else {
|
|
setResendState("sent");
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center p-4">
|
|
<Card className="w-full max-w-md">
|
|
<CardHeader className="space-y-1">
|
|
<CardTitle className="text-2xl">Check your email</CardTitle>
|
|
<CardDescription>We sent a verification link to {email}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<p className="text-sm text-muted-foreground">
|
|
Click the link in the email to verify your account and get started. If you
|
|
don't see it, check your spam folder.
|
|
</p>
|
|
|
|
<div className="space-y-2">
|
|
<Button
|
|
variant="outline"
|
|
className="w-full"
|
|
onClick={handleResend}
|
|
disabled={resendState === "sending"}
|
|
>
|
|
{resendState === "sending" ? "Sending…" : "Resend verification email"}
|
|
</Button>
|
|
{resendState === "sent" && (
|
|
<p className="text-sm text-success">Email sent! Check your inbox.</p>
|
|
)}
|
|
{resendState === "error" && resendError && (
|
|
<p className="text-sm text-destructive">{resendError}</p>
|
|
)}
|
|
</div>
|
|
|
|
<p className="text-center text-sm text-muted-foreground">
|
|
Wrong account?{" "}
|
|
<Link to="/login" className="underline">
|
|
Sign in
|
|
</Link>
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|