diff --git a/src/app/(auth)/forgot-password/page.tsx b/src/app/(auth)/forgot-password/page.tsx new file mode 100644 index 0000000..664d7d1 --- /dev/null +++ b/src/app/(auth)/forgot-password/page.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { useState } from "react"; +import { useSignIn } from "@clerk/nextjs"; +import { useRouter } from "next/navigation"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +/** + * Forgot password page — /forgot-password + * + * Initiates Clerk's built-in password reset flow using the Clerk 7 API: + * 1. signIn.create({ identifier: email }) — sets the identifier context + * 2. signIn.resetPasswordEmailCode.sendCode() — sends the email code + * + * On success, redirects to /reset-password where the user enters the code + new password. + * + * UI-SPEC: Auth Screens — Forgot Password + * Heading: "Reset your password" + * CTA: "Send reset link" + * Confirmation: "Check your email — we sent a reset link to {email}" + * + * Deviation note: Clerk 7 removed the old useSignIn().isLoaded / signIn.create({ strategy: + * 'reset_password_email_code' }) approach. The new SignInFutureResource separates identifier + * setting (create) from strategy invocation (resetPasswordEmailCode.sendCode). + */ +export default function ForgotPasswordPage() { + // Clerk 7: useSignIn returns { errors, fetchStatus, signIn: SignInFutureResource } + const { signIn, fetchStatus } = useSignIn(); + const router = useRouter(); + + const [email, setEmail] = useState(""); + const [error, setError] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + + const isPending = fetchStatus === "fetching"; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + setError(null); + setSuccessMessage(null); + + try { + // Step 1: set the identifier context + const createResult = await signIn.create({ identifier: email }); + if (createResult.error) { + setError(createResult.error.message ?? "Something went wrong."); + return; + } + + // Step 2: trigger the password reset email + const sendResult = await signIn.resetPasswordEmailCode.sendCode(); + if (sendResult.error) { + setError(sendResult.error.message ?? "Could not send reset email."); + return; + } + + setSuccessMessage( + `Check your email — we sent a reset link to ${email}` + ); + // Give the user a moment to read the message, then redirect + setTimeout(() => router.push("/reset-password"), 1500); + } catch (err: unknown) { + const clerkError = err as { errors?: Array<{ message: string }> }; + setError( + clerkError.errors?.[0]?.message ?? + "Something went wrong. Please try again." + ); + } + }; + + return ( + + + + Reset your password + + + +
+
+ + setEmail(e.target.value)} + required + autoComplete="email" + placeholder="you@example.com" + /> +
+ + {error && ( +

+ {error} +

+ )} + + {successMessage && ( +

+ {successMessage} +

+ )} + + +
+
+
+ ); +} diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..8db9f34 --- /dev/null +++ b/src/app/(auth)/layout.tsx @@ -0,0 +1,17 @@ +/** + * Auth layout — centers all auth pages in a 448px card. + * + * No sidebar or navigation — auth pages are standalone experiences. + * Applied to all routes under /(auth)/. + */ +export default function AuthLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+
{children}
+
+ ); +} diff --git a/src/app/(auth)/log-in/[[...sign-in]]/page.tsx b/src/app/(auth)/log-in/[[...sign-in]]/page.tsx new file mode 100644 index 0000000..f57f795 --- /dev/null +++ b/src/app/(auth)/log-in/[[...sign-in]]/page.tsx @@ -0,0 +1,35 @@ +import { SignIn } from "@clerk/nextjs"; + +/** + * Log-in page — /log-in + * + * Clerk renders the full sign-in form (email, password, OAuth options). + * Appearance is overridden to match the OnlyPets orange-500 design system. + * After successful login, owner is redirected to /pets. + * + * UI-SPEC: Auth Screens — Log In + * CTA copy: "Log in" (Clerk default) + */ +export default function LogInPage() { + return ( + + ); +} diff --git a/src/app/(auth)/reset-password/page.tsx b/src/app/(auth)/reset-password/page.tsx new file mode 100644 index 0000000..90d139f --- /dev/null +++ b/src/app/(auth)/reset-password/page.tsx @@ -0,0 +1,159 @@ +"use client"; + +import { useState } from "react"; +import { useSignIn } from "@clerk/nextjs"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +/** + * Reset password page — /reset-password + * + * Completes Clerk's password reset flow using the Clerk 7 SignInFutureResource API: + * 1. signIn.resetPasswordEmailCode.verifyCode({ code }) — verify the emailed code + * 2. signIn.resetPasswordEmailCode.submitPassword({ password }) — set new password + * 3. signIn.finalize() — create the session and redirect + * + * The reset code comes from the Clerk-generated email link URL params. + * + * UI-SPEC: Auth Screens — Reset Password + * Heading: "Set a new password" + * CTA: "Reset password" + * Error states: + * - "Password must be at least 8 characters." + * - "Passwords don't match." + * + * Deviation note: Clerk 7 uses the new SignInFutureResource API. The old + * useSignIn().attemptFirstFactor() is replaced by resetPasswordEmailCode.verifyCode() + * + resetPasswordEmailCode.submitPassword() + finalize(). + */ +export default function ResetPasswordPage() { + // Clerk 7: useSignIn returns { errors, fetchStatus, signIn: SignInFutureResource } + const { signIn, fetchStatus } = useSignIn(); + const router = useRouter(); + const searchParams = useSearchParams(); + + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [error, setError] = useState(null); + + const isPending = fetchStatus === "fetching"; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + setError(null); + + // Client-side validation per UI-SPEC Error States + if (password.length < 8) { + setError("Password must be at least 8 characters."); + return; + } + if (password !== confirmPassword) { + setError("Passwords don't match."); + return; + } + + try { + // Clerk appends the code to the URL when the user clicks the email reset link + const code = + searchParams.get("__clerk_ticket") ?? + searchParams.get("code") ?? + ""; + + // Step 1: verify the reset code from the email + const verifyResult = await signIn.resetPasswordEmailCode.verifyCode({ code }); + if (verifyResult.error) { + setError( + verifyResult.error.message ?? + "Invalid or expired reset code. Please request a new reset link." + ); + return; + } + + // Step 2: submit the new password + const submitResult = await signIn.resetPasswordEmailCode.submitPassword({ + password, + }); + if (submitResult.error) { + setError( + submitResult.error.message ?? + "Could not set new password. Please try again." + ); + return; + } + + // Step 3: finalize the session + const finalizeResult = await signIn.finalize(); + if (finalizeResult.error) { + setError( + finalizeResult.error.message ?? + "Password reset failed. Please log in with your new password." + ); + return; + } + + router.push("/pets"); + } catch (err: unknown) { + const clerkError = err as { errors?: Array<{ message: string }> }; + setError( + clerkError.errors?.[0]?.message ?? + "Something went wrong. Please try again." + ); + } + }; + + return ( + + + + Set a new password + + + +
+
+ + setPassword(e.target.value)} + required + autoComplete="new-password" + minLength={8} + /> +
+ +
+ + setConfirmPassword(e.target.value)} + required + autoComplete="new-password" + /> +
+ + {error && ( +

+ {error} +

+ )} + + +
+
+
+ ); +} diff --git a/src/app/(auth)/sign-up/[[...sign-up]]/page.tsx b/src/app/(auth)/sign-up/[[...sign-up]]/page.tsx new file mode 100644 index 0000000..0c241f3 --- /dev/null +++ b/src/app/(auth)/sign-up/[[...sign-up]]/page.tsx @@ -0,0 +1,35 @@ +import { SignUp } from "@clerk/nextjs"; + +/** + * Sign-up page — /sign-up + * + * Clerk renders the full sign-up form (email, password, email verification). + * Appearance is overridden to match the OnlyPets orange-500 design system. + * After successful sign-up + verification, owner is redirected to /onboarding/pet. + * + * UI-SPEC: Auth Screens — Sign Up + * CTA copy: "Sign Up" (controlled by Clerk's built-in copy) + */ +export default function SignUpPage() { + return ( + + ); +} diff --git a/src/app/(auth)/verify-email/ResendEmailButton.tsx b/src/app/(auth)/verify-email/ResendEmailButton.tsx new file mode 100644 index 0000000..b66b10c --- /dev/null +++ b/src/app/(auth)/verify-email/ResendEmailButton.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { useState } from "react"; +import { useSignUp } from "@clerk/nextjs"; +import { Button } from "@/components/ui/button"; + +/** + * Client component island — resend email verification link. + * + * Uses the Clerk 7 SignUpFutureResource API: + * signUp.verifications.sendEmailCode() — resends the verification email + * + * Isolated so the parent verify-email page can remain a server component. + * + * Deviation note: Clerk 7 replaced the old signUp.prepareEmailAddressVerification() + * with signUp.verifications.sendEmailCode() on the SignUpFutureResource. + */ +export function ResendEmailButton() { + // Clerk 7: useSignUp returns { errors, fetchStatus, signUp: SignUpFutureResource } + const { signUp, fetchStatus } = useSignUp(); + const [sent, setSent] = useState(false); + const [error, setError] = useState(false); + + const isPending = fetchStatus === "fetching"; + + const handleResend = async () => { + setError(false); + try { + const result = await signUp.verifications.sendEmailCode(); + if (result.error) { + setError(true); + } else { + setSent(true); + } + } catch { + setError(true); + } + }; + + return ( +
+ + {error && ( +

+ Could not resend. Please try again. +

+ )} +
+ ); +} diff --git a/src/app/(auth)/verify-email/page.tsx b/src/app/(auth)/verify-email/page.tsx new file mode 100644 index 0000000..bdaf7c1 --- /dev/null +++ b/src/app/(auth)/verify-email/page.tsx @@ -0,0 +1,32 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { ResendEmailButton } from "./ResendEmailButton"; + +/** + * Verify email page — /verify-email + * + * Static server component shown after sign-up while email verification is pending. + * The "Resend email" button is a client component island (ResendEmailButton). + * + * UI-SPEC: Auth Screens — Email Verification Pending + * Heading: "Verify your email" + * Body: "We sent a verification link to {email}. Check your inbox to continue." + * Link: "Resend email" + */ +export default function VerifyEmailPage() { + return ( + + + + Verify your email + + + +

+ We sent a verification link to your email address. Check your inbox to + continue. +

+ +
+
+ ); +}