fix(frontend): more turnstile experiments

This commit is contained in:
Lluis Agusti
2025-10-07 00:22:20 +09:00
parent aa27365e7f
commit 4244979a45
4 changed files with 49 additions and 72 deletions

View File

@@ -11,7 +11,7 @@ import { Form, FormField } from "@/components/__legacy__/ui/form";
import { getBehaveAs } from "@/lib/utils";
import { LoadingLogin } from "./components/LoadingLogin";
import { useLoginPage } from "./useLoginPage";
import { Turnstile2 } from "@/components/auth/Turnstile2";
import Turnstile from "@/components/auth/Turnstile";
export default function LoginPage() {
const {
@@ -29,7 +29,8 @@ export default function LoginPage() {
handleProviderLogin,
handleCloseNotAllowedModal,
handleCaptchaVerify,
handleCaptchaReady,
setCaptchaWidgetId,
captchaResetNonce,
} = useLoginPage();
if (isUserLoading || isLoggedIn) {
@@ -87,10 +88,18 @@ export default function LoginPage() {
/>
<div className="flex items-center justify-center">
<Turnstile2
onVerified={handleCaptchaVerify}
onReady={handleCaptchaReady}
visible={!captchaToken}
<Turnstile
siteKey={
process.env.NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY || ""
}
onVerify={handleCaptchaVerify}
shouldRender={Boolean(
isCloudEnv &&
!captchaToken &&
process.env.NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY,
)}
setWidgetId={setCaptchaWidgetId}
resetSignal={captchaResetNonce}
/>
</div>

View File

@@ -8,7 +8,7 @@ import { useForm } from "react-hook-form";
import z from "zod";
import { login, providerLogin } from "./actions";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { TurnstileInstance } from "@marsidev/react-turnstile";
// Captcha integration handled via widget ID reset in page
export function useLoginPage() {
const { supabase, user, isUserLoading } = useSupabase();
@@ -17,7 +17,8 @@ export function useLoginPage() {
const { toast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const [captchaRef, setCaptchaRef] = useState<TurnstileInstance | null>(null);
const [captchaWidgetId, setCaptchaWidgetId] = useState<string | null>(null);
const [captchaResetNonce, setCaptchaResetNonce] = useState(0);
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const [showNotAllowedModal, setShowNotAllowedModal] = useState(false);
const isCloudEnv = getBehaveAs() === BehaveAs.CLOUD;
@@ -54,7 +55,9 @@ export function useLoginPage() {
setFeedback(null);
} catch (error) {
setCaptchaToken(null);
captchaRef?.reset();
if (captchaWidgetId && window?.turnstile)
window.turnstile.reset(captchaWidgetId);
setCaptchaResetNonce((n) => n + 1);
setIsGoogleLoading(false);
const errorString = JSON.stringify(error);
if (errorString.includes("not_allowed")) {
@@ -97,7 +100,9 @@ export function useLoginPage() {
});
setCaptchaToken(null);
captchaRef?.reset();
if (captchaWidgetId && window?.turnstile)
window.turnstile.reset(captchaWidgetId);
setCaptchaResetNonce((n) => n + 1);
return;
}
setFeedback(null);
@@ -107,10 +112,6 @@ export function useLoginPage() {
setCaptchaToken(token);
}
function handleCaptchaReady(ref: TurnstileInstance) {
if (!captchaRef) setCaptchaRef(ref);
}
return {
form,
feedback,
@@ -126,6 +127,7 @@ export function useLoginPage() {
handleProviderLogin,
handleCloseNotAllowedModal: () => setShowNotAllowedModal(false),
handleCaptchaVerify,
handleCaptchaReady,
setCaptchaWidgetId,
captchaResetNonce,
};
}

View File

@@ -13,6 +13,8 @@ export interface TurnstileProps {
id?: string;
shouldRender?: boolean;
setWidgetId?: (id: string | null) => void;
// Changing this value will trigger a widget reset safely inside the component
resetSignal?: number | string | boolean;
}
export function Turnstile({
@@ -25,6 +27,7 @@ export function Turnstile({
id = "cf-turnstile",
shouldRender = true,
setWidgetId,
resetSignal,
}: TurnstileProps) {
const containerRef = useRef<HTMLDivElement>(null);
const widgetIdRef = useRef<string | null>(null);
@@ -57,16 +60,20 @@ export function Turnstile({
document.head.appendChild(script);
return () => {
if (document.head.contains(script)) {
document.head.removeChild(script);
}
};
// Do not remove the script on unmount to avoid race conditions with
// other potential widgets or future mounts.
return () => {};
}, [onError, shouldRender]);
// Initialize and render the widget when script is loaded
useEffect(() => {
if (!loaded || !containerRef.current || !window.turnstile || !shouldRender)
if (
!loaded ||
!containerRef.current ||
!window.turnstile ||
!shouldRender ||
!siteKey
)
return;
// Reset any existing widget
@@ -123,13 +130,20 @@ export function Turnstile({
// Method to reset the widget manually
useEffect(() => {
if (loaded && widgetIdRef.current && window.turnstile && shouldRender) {
if (
loaded &&
widgetIdRef.current &&
window.turnstile &&
shouldRender &&
siteKey
) {
window.turnstile.reset(widgetIdRef.current);
}
}, [loaded, shouldRender]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loaded, shouldRender, resetSignal, siteKey]);
// If shouldRender is false, don't render anything
if (!shouldRender) {
if (!shouldRender || !siteKey) {
return null;
}
@@ -145,7 +159,6 @@ export function Turnstile({
// Add TypeScript interface to Window to include turnstile property
declare global {
interface Window {
// @ts-expect-error - turnstile is not defined in the window object
turnstile?: {
render: (
container: HTMLElement,

View File

@@ -1,47 +0,0 @@
"use client";
import { BehaveAs, getBehaveAs } from "@/lib/utils";
import { Turnstile, TurnstileInstance } from "@marsidev/react-turnstile";
import { useEffect, useRef } from "react";
const TURNSTILE_SITE_KEY =
process.env.NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY || "";
type Props = {
onVerified: (token: string) => void;
onReady: (ref: TurnstileInstance) => void;
visible: boolean;
};
export function Turnstile2(props: Props) {
const captchaRef = useRef<TurnstileInstance>(null);
const behaveAs = getBehaveAs();
useEffect(() => {
if (captchaRef.current) {
props.onReady(captchaRef.current);
}
}, [captchaRef]);
function handleCaptchaVerify(token: string) {
props.onVerified(token);
}
if (behaveAs !== BehaveAs.CLOUD) {
return null;
}
if (!TURNSTILE_SITE_KEY) {
return null;
}
return (
<div className={props.visible ? "" : "hidden"}>
<Turnstile
ref={captchaRef}
siteKey={TURNSTILE_SITE_KEY}
onSuccess={handleCaptchaVerify}
/>
</div>
);
}