diff --git a/app/(auth)/actions.ts b/app/(auth)/actions.ts index 778175be2..5a0e0c2df 100644 --- a/app/(auth)/actions.ts +++ b/app/(auth)/actions.ts @@ -119,8 +119,10 @@ export async function updateUser(formData: UpdatePasswordFormData) { redirect("/dashboard"); } -// resend confirmation email -export async function resendConfirmationEmail(email: string) { +export async function resendConfirmationEmail( + email: string, + captchaToken: string, +) { const supabase = createClient(); const { error } = await supabase.auth.resend({ @@ -128,6 +130,7 @@ export async function resendConfirmationEmail(email: string) { email: email, options: { emailRedirectTo: `${getURL()}/dashboard`, + captchaToken: captchaToken, }, }); diff --git a/app/(auth)/auth/verified/page.tsx b/app/(auth)/auth/verified/page.tsx new file mode 100644 index 000000000..c18deb629 --- /dev/null +++ b/app/(auth)/auth/verified/page.tsx @@ -0,0 +1,90 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; +import { createClient } from "@/utils/supabase/client"; +import { useRouter } from "next/navigation"; +import { toast } from "sonner"; + +export default function VerifiedPage() { + const [email, setEmail] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); + + useEffect(() => { + const storedEmail = localStorage.getItem("verificationEmail"); + if (storedEmail) { + setEmail(storedEmail); + } + }, []); + + const signInWithToken = async () => { + if (!email) { + router.push("/signin"); + return; + } + + setIsLoading(true); + + try { + const supabase = createClient(); + const { error } = await supabase.auth.signInWithOtp({ + email: email, + options: { + shouldCreateUser: false, + emailRedirectTo: `${window.location.origin}/dashboard`, + }, + }); + + if (error) { + toast.error( + "Failed to sign in automatically. Please sign in manually.", + ); + router.push(`/signin?email=${encodeURIComponent(email)}`); + return; + } + + toast.success("Magic link sent! Check your email to complete sign in."); + } catch (error) { + toast.error("An error occurred. Please sign in manually."); + router.push(`/signin?email=${encodeURIComponent(email)}`); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+

+ Email Verified Successfully! +

+

+ Your email has been verified and your account is now active. +

+ +
+ {email ? ( + <> + +

+ We'll send a magic link to your email for a seamless login. +

+ + ) : ( + + + + )} +
+
+
+ ); +} diff --git a/app/(auth)/auth/verify/route.ts b/app/(auth)/auth/verify/route.ts new file mode 100644 index 000000000..fe50bd0b1 --- /dev/null +++ b/app/(auth)/auth/verify/route.ts @@ -0,0 +1,35 @@ +import { NextRequest, NextResponse } from "next/server"; +import { createClient } from "@/utils/supabase/server"; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const token_hash = searchParams.get("token_hash"); + const type = searchParams.get("type"); + + if (!token_hash || !type) { + return NextResponse.redirect( + new URL("/signin?error=missing-token", request.url), + ); + } + + const supabase = createClient(); + + try { + const { error } = await supabase.auth.verifyOtp({ + token_hash, + type: type as any, + }); + + if (error) { + return NextResponse.redirect( + new URL(`/signin?error=${error.message}`, request.url), + ); + } + + return NextResponse.redirect(new URL("/auth/verified", request.url)); + } catch (error) { + return NextResponse.redirect( + new URL("/signin?error=verification-failed", request.url), + ); + } +} diff --git a/components/auth/signin.tsx b/components/auth/signin.tsx index a08492626..b02d7b4da 100644 --- a/components/auth/signin.tsx +++ b/components/auth/signin.tsx @@ -1,6 +1,6 @@ "use client"; import Link from "next/link"; -import { useState, useRef } from "react"; +import { useState, useRef, useEffect } from "react"; import HCaptcha from "@hcaptcha/react-hcaptcha"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; @@ -39,7 +39,20 @@ export default function SignIn() { password: "", }, }); + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const emailParam = params.get("email"); + if (emailParam) { + form.setValue("email", emailParam); + } + if (!emailParam) { + const storedEmail = localStorage.getItem("verificationEmail"); + if (storedEmail) { + form.setValue("email", storedEmail); + } + } + }, [form]); const handleSignIn = async (data: SignInFormData) => { if (isSubmitting) return; setIsSubmitting(true); diff --git a/components/auth/signup.tsx b/components/auth/signup.tsx index 753539dbe..a2430f561 100644 --- a/components/auth/signup.tsx +++ b/components/auth/signup.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { useState, useRef } from "react"; import { signinWithOAuth } from "@/app/(auth)/actions"; import { HCAPTCHA_SITE_KEY_PUBLIC } from "@/utils/constants"; -import { AdminUserAttributes, Provider } from "@supabase/supabase-js"; +import { Provider } from "@supabase/supabase-js"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { Button } from "@/components/ui/button"; diff --git a/components/auth/verification.tsx b/components/auth/verification.tsx index dfe134ab4..b40ad5690 100644 --- a/components/auth/verification.tsx +++ b/components/auth/verification.tsx @@ -1,89 +1,162 @@ "use client"; -import { Button } from "@/components/ui/button"; -import { useRouter } from "next/navigation"; +import React, { useEffect, useState, useRef } from "react"; +import Link from "next/link"; import { resendConfirmationEmail } from "@/app/(auth)/actions"; -import { useSearchParams } from "next/navigation"; import { toast } from "sonner"; -import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import HCaptcha from "@hcaptcha/react-hcaptcha"; +import { HCAPTCHA_SITE_KEY_PUBLIC } from "@/utils/constants"; -export default function Verification() { - const router = useRouter(); - const [isSubmitting, setIsSubmitting] = useState(false); - const searchParams = useSearchParams(); - const email = searchParams?.get("email"); +export default function VerificationComponent() { + const [email, setEmail] = useState(""); + const [isResending, setIsResending] = useState(false); + const [captchaToken, setCaptchaToken] = useState(null); + const captcha = useRef(null); - const handleClick = () => { - router.push("/signin"); - }; + const [cooldownActive, setCooldownActive] = useState(false); + const [cooldownTime, setCooldownTime] = useState(0); + const COOLDOWN_DURATION = 60; // 60 sec + + useEffect(() => { + const storedEmail = localStorage.getItem("verificationEmail"); + if (storedEmail) { + setEmail(storedEmail); + } + + const savedCooldownEnd = localStorage.getItem("resendCooldownEnd"); + if (savedCooldownEnd) { + const cooldownEndTime = parseInt(savedCooldownEnd, 10); + const currentTime = Date.now(); + + if (cooldownEndTime > currentTime) { + setCooldownActive(true); + setCooldownTime(Math.ceil((cooldownEndTime - currentTime) / 1000)); + + const timer = setInterval(() => { + setCooldownTime((prevTime) => { + if (prevTime <= 1) { + clearInterval(timer); + setCooldownActive(false); + localStorage.removeItem("resendCooldownEnd"); + return 0; + } + return prevTime - 1; + }); + }, 1000); + + return () => clearInterval(timer); + } else { + localStorage.removeItem("resendCooldownEnd"); + } + } + }, []); - const handleResendEmail = async () => { - if (isSubmitting) return; - setIsSubmitting(true); - const response = await resendConfirmationEmail(email as string); - if (response?.error) { - toast.error(response.error); - } else { - toast.success("Email sent successfully"); + const handleResendVerification = async () => { + if (!email) { + toast.error("Email not found. Please try signing up again."); + return; + } + + if (!captchaToken) { + toast.error("Please complete the captcha challenge"); + return; + } + + setIsResending(true); + try { + const result = await resendConfirmationEmail(email, captchaToken); + if (result?.error) { + toast.error(result.error); + } else { + toast.success("Verification email resent successfully!"); + + setCooldownActive(true); + setCooldownTime(COOLDOWN_DURATION); + + const cooldownEndTime = Date.now() + COOLDOWN_DURATION * 1000; + localStorage.setItem("resendCooldownEnd", cooldownEndTime.toString()); + + const timer = setInterval(() => { + setCooldownTime((prevTime) => { + if (prevTime <= 1) { + clearInterval(timer); + setCooldownActive(false); + localStorage.removeItem("resendCooldownEnd"); + return 0; + } + return prevTime - 1; + }); + }, 1000); + } + } catch (error) { + toast.error("Failed to resend verification email"); + } finally { + setIsResending(false); + captcha.current?.resetCaptcha(); + setCaptchaToken(null); } - setIsSubmitting(false); }; return ( -
-
-
- {/* Page header */} -
-

- We've sent you an email for Confirmation -

-
-
-
- -
- If you've already confirmed your email, please sign in - below -
- -
-
-
- +
+

+ Already verified?{" "} + Sign in - -

- Make sure to check your spam folder if you don't see it! -
- {email && ( -
- Didn't receive an email?{" "} - -
- )} -
-
+ +

+
-
-
-
+ + + ); }