It looks like you've previously used this email to sign up via
- email login. Please enter your email below to receive a sign in
- link.
+ email login. Please enter your email.
)}
diff --git a/apps/web/app/(org)/verify-otp/form.tsx b/apps/web/app/(org)/verify-otp/form.tsx
index f2effe76cf..4c7ff058aa 100644
--- a/apps/web/app/(org)/verify-otp/form.tsx
+++ b/apps/web/app/(org)/verify-otp/form.tsx
@@ -156,19 +156,19 @@ export function VerifyOTPForm({
+
{code.map((digit, index) => (
))}
@@ -221,7 +221,7 @@ export function VerifyOTPForm({
- By verifying your email, you acknowledge that you have both read and
+ By entering your email, you acknowledge that you have both read and
agree to Cap's{" "}
= ({
const [email, setEmail] = useState("");
const [loading, setLoading] = useState(false);
const [emailSent, setEmailSent] = useState(false);
+ const [step, setStep] = useState(1);
+ const [code, setCode] = useState(["", "", "", "", "", ""]);
- const handleGoogleSignIn = () => {
- signIn("google");
- };
+ const [lastResendTime, setLastResendTime] = useState(null);
+ const emailId = useId();
return (
-
+
+ {emailSent && (
+ {
+ setEmailSent(false);
+ setEmail("");
+ setLoading(false);
+ setStep(1);
+ setCode(["", "", "", "", "", ""]);
+ setLastResendTime(null);
+ }}
+ className="absolute top-5 left-5 cursor-pointer z-20 flex gap-2 items-center py-1.5 px-3 text-gray-12 bg-transparent border border-gray-4 rounded-full hover:bg-gray-1 transition-colors duration-300"
+ >
+
+
Back
+
+ )}
-
+
-
-
Sign in to comment
-
Join the conversation.
+
+
+ {step === 1 ? "Sign in to comment" : "Email sent"}
+
+
+ {step === 1
+ ? "Join the conversation."
+ : "We sent a 6-digit code to your email."}
+
-
- {NODE_ENV === "development" && (
- <>
-
-
- Continue with Google
-
-
- >
+
+
+ {step === 1 ? (
+
+ ) : (
+
)}
-
-
-
-
- {emailSent && (
-
- {
- setEmailSent(false);
- setEmail("");
- setLoading(false);
- }}
+ Terms of Service
+ {" "}
+ and{" "}
+
- Click to restart sign in process.
-
-
- )}
+ Privacy Policy
+
+ .
+
+
);
};
+
+const StepOne = ({
+ email,
+ emailSent,
+ setEmail,
+ loading,
+ setEmailSent,
+ setLoading,
+ setStep,
+ setLastResendTime,
+ emailId,
+}: {
+ email: string;
+ emailSent: boolean;
+ setEmail: (email: string) => void;
+ loading: boolean;
+ setEmailSent: (emailSent: boolean) => void;
+ setLoading: (loading: boolean) => void;
+ setStep: (step: number) => void;
+ setLastResendTime: (time: number | null) => void;
+ emailId: string;
+}) => {
+ return (
+
+ );
+};
diff --git a/apps/web/app/s/[videoId]/_components/OtpForm.tsx b/apps/web/app/s/[videoId]/_components/OtpForm.tsx
new file mode 100644
index 0000000000..56d3855c3e
--- /dev/null
+++ b/apps/web/app/s/[videoId]/_components/OtpForm.tsx
@@ -0,0 +1,203 @@
+import { Button } from "@cap/ui";
+import { useMutation } from "@tanstack/react-query";
+import { useRouter } from "next/navigation";
+import { signIn } from "next-auth/react";
+import { useEffect, useRef } from "react";
+import { toast } from "sonner";
+
+const OtpForm = ({
+ email,
+ step,
+ onClose,
+ code,
+ setCode,
+ lastResendTime,
+ setLastResendTime,
+}: {
+ email: string;
+ step: number;
+ onClose: () => void;
+ code: string[];
+ setCode: (code: string[]) => void;
+ lastResendTime: number | null;
+ setLastResendTime: (time: number | null) => void;
+}) => {
+ const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
+ const router = useRouter();
+
+ useEffect(() => {
+ if (step === 2) {
+ inputRefs.current[0]?.focus();
+ }
+ }, [step]);
+
+ const handleOTPChange = (index: number, value: string) => {
+ if (value.length > 1) {
+ const pastedCode = value.slice(0, 6).split("");
+ const newCode = [...code];
+ pastedCode.forEach((digit, i) => {
+ if (index + i < 6) {
+ newCode[index + i] = digit;
+ }
+ });
+ setCode(newCode);
+
+ const nextEmptyIndex = newCode.findIndex((digit) => digit === "");
+ if (nextEmptyIndex !== -1) {
+ inputRefs.current[nextEmptyIndex]?.focus();
+ } else {
+ inputRefs.current[5]?.focus();
+ }
+
+ if (index + value.length >= 5) handleVerify.mutate(undefined);
+ } else {
+ const newCode = [...code];
+ newCode[index] = value;
+ setCode(newCode);
+
+ if (value && index < 5) {
+ inputRefs.current[index + 1]?.focus();
+ }
+ }
+ };
+
+ const handleOTPKeyDown = (
+ index: number,
+ e: React.KeyboardEvent
,
+ ) => {
+ if (e.key === "Backspace" && !code[index] && index > 0) {
+ inputRefs.current[index - 1]?.focus();
+ }
+ };
+
+ const handleVerify = useMutation({
+ mutationFn: async () => {
+ const otpCode = code.join("");
+ if (otpCode.length !== 6) throw "Please enter a complete 6-digit code";
+
+ const res = await fetch(
+ `/api/auth/callback/email?email=${encodeURIComponent(email)}&token=${encodeURIComponent(otpCode)}&callbackUrl=${encodeURIComponent("/login-success")}`,
+ );
+
+ if (!res.url.includes("/login-success")) {
+ setCode(["", "", "", "", "", ""]);
+ inputRefs.current[0]?.focus();
+ throw "Invalid code. Please try again.";
+ }
+ },
+ onSuccess: () => {
+ router.refresh();
+ toast.success("Sign in successful!");
+ onClose();
+ },
+ onError: (e) => {
+ if (typeof e === "string") {
+ toast.error(e);
+ } else {
+ toast.error("An error occurred. Please try again.");
+ }
+ },
+ });
+
+ const handleResend = useMutation({
+ mutationFn: async () => {
+ // Check client-side rate limiting
+ if (lastResendTime) {
+ const timeSinceLastRequest = Date.now() - lastResendTime;
+ const waitTime = 30000; // 30 seconds
+ if (timeSinceLastRequest < waitTime) {
+ const remainingSeconds = Math.ceil(
+ (waitTime - timeSinceLastRequest) / 1000,
+ );
+ throw `Please wait ${remainingSeconds} seconds before requesting a new code`;
+ }
+ }
+
+ const result = await signIn("email", {
+ email,
+ redirect: false,
+ });
+
+ if (result?.error) {
+ throw "Please wait 30 seconds before requesting a new code";
+ }
+ },
+ onSuccess: () => {
+ toast.success("A new code has been sent to your email!");
+ setCode(["", "", "", "", "", ""]);
+ inputRefs.current[0]?.focus();
+ setLastResendTime(Date.now());
+ },
+ onError: (e) => {
+ if (typeof e === "string") {
+ toast.error(e);
+ } else {
+ toast.error("An error occurred. Please try again.");
+ }
+ },
+ });
+
+ const isVerifying = handleVerify.isPending || handleVerify.isSuccess;
+
+ return (
+
+
+ {code.map((digit, index) => (
+ {
+ inputRefs.current[index] = el;
+ }}
+ type="text"
+ inputMode="numeric"
+ pattern="[0-9]*"
+ maxLength={1}
+ value={digit}
+ onChange={(e) =>
+ handleOTPChange(index, e.target.value.replace(/\D/g, ""))
+ }
+ onKeyDown={(e) => handleOTPKeyDown(index, e)}
+ onPaste={(e) => {
+ e.preventDefault();
+ const pastedData = e.clipboardData
+ .getData("text")
+ .replace(/\D/g, "");
+ handleOTPChange(0, pastedData);
+ }}
+ className="flex-1 h-[52px] text-lg font-semibold text-center rounded-lg border transition-all bg-gray-1 border-gray-5 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+ disabled={isVerifying}
+ />
+ ))}
+
+
+
{
+ handleVerify.mutate(undefined);
+ }}
+ disabled={code.some((digit) => !digit) || isVerifying}
+ >
+ {isVerifying ? "Verifying..." : "Verify Code"}
+
+
+
+ {
+ handleResend.mutate(undefined);
+ }}
+ disabled={handleResend.isPending}
+ className="text-sm underline transition-colors text-gray-10 hover:text-gray-12"
+ >
+ {handleResend.isPending
+ ? "Sending..."
+ : "Didn't receive the code? Resend"}
+
+
+
+ );
+};
+
+export default OtpForm;