Skip to content

fix: remove redundant checks and add explicit render mode #144

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 27 additions & 29 deletions app/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,35 +322,33 @@ export default function CreateGistPage() {
</Card>

{/* Invisible Turnstile Verification */}
{turnstileSiteKey &&
typeof turnstileSiteKey === "string" &&
turnstileSiteKey.length > 0 && (
<div className="hidden">
<Turnstile
sitekey={turnstileSiteKey}
action="create_gist"
onSuccess={(token) => {
setTurnstileToken(token);
setIsTurnstileReady(true);
}}
onError={() => {
setError(
"🛡️ Security check failed. Please refresh the page and try again."
);
setIsTurnstileReady(false);
}}
onExpire={() => {
setTurnstileToken(null);
setIsTurnstileReady(false);
setError(
"⏰ Security verification expired. Please refresh the page to continue."
);
}}
theme="auto"
appearance="interaction-only"
/>
</div>
)}
{turnstileSiteKey && (
<div className="hidden">
<Turnstile
sitekey={turnstileSiteKey}
action="create_gist"
onSuccess={(token) => {
setTurnstileToken(token);
setIsTurnstileReady(true);
}}
onError={() => {
setError(
"🛡️ Security check failed. Please refresh the page and try again."
);
setIsTurnstileReady(false);
}}
onExpire={() => {
setTurnstileToken(null);
setIsTurnstileReady(false);
setError(
"⏰ Security verification expired. Please refresh the page to continue."
);
}}
theme="auto"
appearance="interaction-only"
/>
</div>
)}

{/* Error Display */}
{(error || validationMessage) && (
Expand Down
43 changes: 36 additions & 7 deletions components/ui/turnstile.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ describe("Turnstile", () => {
});

it("renders turnstile widget when script loads", async () => {
// Temporarily remove window.turnstile to simulate script not loaded yet
const originalTurnstile = window.turnstile;
delete (window as any).turnstile;

const { container } = render(
<Turnstile
sitekey="test-site-key"
Expand All @@ -36,7 +40,11 @@ describe("Turnstile", () => {
/>
);

// Wait for script to load and widget to render
// Restore window.turnstile and call the callback
window.turnstile = originalTurnstile;
window.onloadTurnstileCallback?.();

// Wait for widget to render
await waitFor(() => {
expect(mockRender).toHaveBeenCalledWith(
expect.any(HTMLElement),
Expand Down Expand Up @@ -95,28 +103,49 @@ describe("Turnstile", () => {
});
});

it("cleans up widget on unmount", async () => {
it("cleans up widget on unmount", () => {
const { unmount } = render(
<Turnstile sitekey="test-site-key" onSuccess={mockOnSuccess} />
);

await waitFor(() => {
expect(mockRender).toHaveBeenCalled();
});
// Widget should render immediately since window.turnstile exists
expect(mockRender).toHaveBeenCalled();
expect(mockRender).toHaveReturnedWith("widget-123");

// Clear all mocks to ensure clean state for testing cleanup
mockRender.mockClear();

unmount();

expect(mockRemove).toHaveBeenCalledWith("widget-123");
});

it("checks script is loaded", async () => {
it("renders immediately when turnstile is already loaded", () => {
// Turnstile is already set up in beforeEach
render(<Turnstile sitekey="test-site-key" onSuccess={mockOnSuccess} />);

// Should render immediately without needing the callback
expect(mockRender).toHaveBeenCalled();
});

it("checks script is loaded with correct parameters", async () => {
// Remove existing script if any
const existingScript = document.getElementById("cf-turnstile-script");
existingScript?.remove();

// Temporarily remove window.turnstile
delete (window as any).turnstile;

render(<Turnstile sitekey="test-site-key" onSuccess={mockOnSuccess} />);

// Check that script was added
const script = document.getElementById("cf-turnstile-script");
expect(script).toBeTruthy();
expect(script?.getAttribute("src")).toBe(
"https://challenges.cloudflare.com/turnstile/v0/api.js"
"https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=onloadTurnstileCallback"
);

// Check that callback was set
expect(window.onloadTurnstileCallback).toBeDefined();
});
});
38 changes: 18 additions & 20 deletions components/ui/turnstile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ declare global {
remove: (widgetId: string) => void;
execute: (widgetId: string) => void;
};
onloadTurnstileCallback?: () => void;
}
}

Expand All @@ -56,17 +57,6 @@ const Turnstile: React.FC<TurnstileProps> = ({
const widgetIdRef = useRef<string | null>(null);

useEffect(() => {
// Load Turnstile script if not already present
const scriptId = "cf-turnstile-script";
if (!document.getElementById(scriptId)) {
const script = document.createElement("script");
script.id = scriptId;
script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js";
script.async = true;
script.defer = true;
document.head.appendChild(script);
}

const renderWidget = () => {
if (window.turnstile && containerRef.current && !widgetIdRef.current) {
widgetIdRef.current = window.turnstile.render(containerRef.current, {
Expand All @@ -84,27 +74,35 @@ const Turnstile: React.FC<TurnstileProps> = ({
}
};

// Render after script loads
// Check if Turnstile is already loaded
if (window.turnstile) {
renderWidget();
} else {
const interval = setInterval(() => {
if (window.turnstile) {
clearInterval(interval);
renderWidget();
}
}, 100);
return () => clearInterval(interval);
// Set up the callback for when Turnstile loads
window.onloadTurnstileCallback = renderWidget;

// Load Turnstile script if not already present
const scriptId = "cf-turnstile-script";
if (!document.getElementById(scriptId)) {
const script = document.createElement("script");
script.id = scriptId;
script.src =
"https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=onloadTurnstileCallback";
script.async = true;
script.defer = true;
document.head.appendChild(script);
}
}

// Cleanup
// Cleanup - always register cleanup function
return () => {
if (widgetIdRef.current && window.turnstile) {
try {
window.turnstile.remove(widgetIdRef.current);
} catch {
// Widget might already be removed
}
widgetIdRef.current = null;
}
};
}, [
Expand Down