Skip to content

Commit 2035532

Browse files
nullcoderclaude
andauthored
refactor: simplify Turnstile component and align with official API (#143)
- Remove unnecessary complexity (memoization, state management, Next.js Script) - Use direct DOM script injection with deduplication - Update prop names: onVerify → onSuccess for consistency - Add correct Cloudflare Turnstile attributes per official docs: - Remove invalid "invisible" size option - Add appearance attribute ("always", "execute", "interaction-only") - Add execution and language attributes - Use appearance="interaction-only" for invisible-like behavior - Follow consistent export pattern with other UI components 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9c26443 commit 2035532

File tree

3 files changed

+120
-172
lines changed

3 files changed

+120
-172
lines changed

app/create/page.tsx

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -322,32 +322,35 @@ export default function CreateGistPage() {
322322
</Card>
323323

324324
{/* Invisible Turnstile Verification */}
325-
{turnstileSiteKey && (
326-
<div className="hidden">
327-
<Turnstile
328-
sitekey={turnstileSiteKey}
329-
onVerify={(token) => {
330-
setTurnstileToken(token);
331-
setIsTurnstileReady(true);
332-
}}
333-
onError={() => {
334-
setError(
335-
"🛡️ Security check failed. Please refresh the page and try again."
336-
);
337-
setIsTurnstileReady(false);
338-
}}
339-
onExpire={() => {
340-
setTurnstileToken(null);
341-
setIsTurnstileReady(false);
342-
setError(
343-
"⏰ Security verification expired. Please refresh the page to continue."
344-
);
345-
}}
346-
theme="auto"
347-
size="invisible"
348-
/>
349-
</div>
350-
)}
325+
{turnstileSiteKey &&
326+
typeof turnstileSiteKey === "string" &&
327+
turnstileSiteKey.length > 0 && (
328+
<div className="hidden">
329+
<Turnstile
330+
sitekey={turnstileSiteKey}
331+
action="create_gist"
332+
onSuccess={(token) => {
333+
setTurnstileToken(token);
334+
setIsTurnstileReady(true);
335+
}}
336+
onError={() => {
337+
setError(
338+
"🛡️ Security check failed. Please refresh the page and try again."
339+
);
340+
setIsTurnstileReady(false);
341+
}}
342+
onExpire={() => {
343+
setTurnstileToken(null);
344+
setIsTurnstileReady(false);
345+
setError(
346+
"⏰ Security verification expired. Please refresh the page to continue."
347+
);
348+
}}
349+
theme="auto"
350+
appearance="interaction-only"
351+
/>
352+
</div>
353+
)}
351354

352355
{/* Error Display */}
353356
{(error || validationMessage) && (

components/ui/turnstile.test.tsx

Lines changed: 19 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,11 @@ import { render, waitFor } from "@testing-library/react";
22
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
33
import { Turnstile } from "./turnstile";
44

5-
// Mock Next.js Script component
6-
vi.mock("next/script", () => ({
7-
default: ({ onLoad }: { onLoad: () => void }) => {
8-
// Simulate script loading
9-
setTimeout(() => onLoad(), 0);
10-
return null;
11-
},
12-
}));
13-
145
describe("Turnstile", () => {
156
const mockRender = vi.fn().mockReturnValue("widget-123");
167
const mockReset = vi.fn();
178
const mockRemove = vi.fn();
18-
const mockOnVerify = vi.fn();
9+
const mockOnSuccess = vi.fn();
1910
const mockOnError = vi.fn();
2011
const mockOnExpire = vi.fn();
2112

@@ -39,7 +30,7 @@ describe("Turnstile", () => {
3930
const { container } = render(
4031
<Turnstile
4132
sitekey="test-site-key"
42-
onVerify={mockOnVerify}
33+
onSuccess={mockOnSuccess}
4334
onError={mockOnError}
4435
onExpire={mockOnExpire}
4536
/>
@@ -56,24 +47,27 @@ describe("Turnstile", () => {
5647
"expired-callback": expect.any(Function),
5748
theme: "auto",
5849
size: "normal",
50+
appearance: "interaction-only",
51+
execution: "render",
52+
language: "auto",
5953
})
6054
);
6155
});
6256

6357
// Check container exists
64-
const turnstileContainer = container.querySelector(".cf-turnstile");
58+
const turnstileContainer = container.querySelector("div");
6559
expect(turnstileContainer).toBeInTheDocument();
6660

6761
// Test that callbacks are properly forwarded
6862
const renderCall = mockRender.mock.calls[0][1];
6963

70-
// Test onVerify callback
64+
// Test onSuccess callback
7165
renderCall.callback("test-token");
72-
expect(mockOnVerify).toHaveBeenCalledWith("test-token");
66+
expect(mockOnSuccess).toHaveBeenCalledWith("test-token");
7367

7468
// Test onError callback
75-
renderCall["error-callback"]("test-error");
76-
expect(mockOnError).toHaveBeenCalledWith("test-error");
69+
renderCall["error-callback"]();
70+
expect(mockOnError).toHaveBeenCalled();
7771

7872
// Test onExpire callback
7973
renderCall["expired-callback"]();
@@ -84,7 +78,7 @@ describe("Turnstile", () => {
8478
render(
8579
<Turnstile
8680
sitekey="test-site-key"
87-
onVerify={mockOnVerify}
81+
onSuccess={mockOnSuccess}
8882
theme="dark"
8983
size="compact"
9084
/>
@@ -103,7 +97,7 @@ describe("Turnstile", () => {
10397

10498
it("cleans up widget on unmount", async () => {
10599
const { unmount } = render(
106-
<Turnstile sitekey="test-site-key" onVerify={mockOnVerify} />
100+
<Turnstile sitekey="test-site-key" onSuccess={mockOnSuccess} />
107101
);
108102

109103
await waitFor(() => {
@@ -115,76 +109,14 @@ describe("Turnstile", () => {
115109
expect(mockRemove).toHaveBeenCalledWith("widget-123");
116110
});
117111

118-
it("handles render errors gracefully", async () => {
119-
mockRender.mockImplementationOnce(() => {
120-
throw new Error("Render failed");
121-
});
122-
123-
render(
124-
<Turnstile
125-
sitekey="test-site-key"
126-
onVerify={mockOnVerify}
127-
onError={mockOnError}
128-
/>
129-
);
130-
131-
await waitFor(() => {
132-
expect(mockOnError).toHaveBeenCalledWith(
133-
"Failed to load verification widget"
134-
);
135-
});
136-
});
137-
138-
it("applies custom className", () => {
139-
const { container } = render(
140-
<Turnstile
141-
sitekey="test-site-key"
142-
onVerify={mockOnVerify}
143-
className="custom-class"
144-
/>
145-
);
146-
147-
const turnstileContainer = container.querySelector(".cf-turnstile");
148-
expect(turnstileContainer).toHaveClass("custom-class");
149-
});
150-
151-
it("applies correct height classes based on size", () => {
152-
const { container: container1 } = render(
153-
<Turnstile
154-
sitekey="test-site-key"
155-
onVerify={mockOnVerify}
156-
size="compact"
157-
/>
158-
);
159-
expect(container1.querySelector(".cf-turnstile")).toHaveClass("h-[65px]");
112+
it("checks script is loaded", async () => {
113+
render(<Turnstile sitekey="test-site-key" onSuccess={mockOnSuccess} />);
160114

161-
const { container: container2 } = render(
162-
<Turnstile
163-
sitekey="test-site-key"
164-
onVerify={mockOnVerify}
165-
size="normal"
166-
/>
167-
);
168-
expect(container2.querySelector(".cf-turnstile")).toHaveClass("h-[65px]");
169-
170-
const { container: container3 } = render(
171-
<Turnstile
172-
sitekey="test-site-key"
173-
onVerify={mockOnVerify}
174-
size="flexible"
175-
/>
176-
);
177-
expect(container3.querySelector(".cf-turnstile")).toHaveClass(
178-
"min-h-[65px]"
179-
);
180-
181-
const { container: container4 } = render(
182-
<Turnstile
183-
sitekey="test-site-key"
184-
onVerify={mockOnVerify}
185-
size="invisible"
186-
/>
115+
// Check that script was added
116+
const script = document.getElementById("cf-turnstile-script");
117+
expect(script).toBeTruthy();
118+
expect(script?.getAttribute("src")).toBe(
119+
"https://challenges.cloudflare.com/turnstile/v0/api.js"
187120
);
188-
expect(container4.querySelector(".cf-turnstile")).toHaveClass("h-0");
189121
});
190122
});

0 commit comments

Comments
 (0)