Skip to content

FIx errors when turnstile token expiring near checkout time#304

Merged
devksingh4 merged 1 commit intomainfrom
fix-turnstile-expiry
Feb 17, 2026
Merged

FIx errors when turnstile token expiring near checkout time#304
devksingh4 merged 1 commit intomainfrom
fix-turnstile-expiry

Conversation

@devksingh4
Copy link
Member

@devksingh4 devksingh4 commented Feb 17, 2026

Summary by CodeRabbit

  • Chores
    • Enhanced internal verification system with improved token management and error handling during checkout processes.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 17, 2026

Walkthrough

This pull request introduces a new Turnstile widget component in src/components/TurnstileWidget.tsx that encapsulates token lifecycle management through a custom hook (useTurnstile). The hook maintains token state, tracks token age, validates freshness against configurable thresholds, and automatically refreshes stale tokens via getResponsePromise. The component wraps the Turnstile element with handlers for success, expiration, and error events. Concurrently, src/components/store/StoreItem.tsx is refactored to replace the external Turnstile library dependency with the new TurnstileWidget abstraction, removing inline token management and delegating to the hook's ensureFreshToken method for obtaining current tokens during API calls.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: introducing a Turnstile token management system (useTurnstile hook and TurnstileWidget component) to handle token expiration issues near checkout time.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-turnstile-expiry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Feb 17, 2026

Deploying with Cloudflare Pages

Name Result
Last commit: f5865de
Preview URL: https://da5790a4.acmuiuc.pages.dev
Branch Preview URL: https://fix-turnstile-expiry.acmuiuc.pages.dev

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/components/TurnstileWidget.tsx (1)

7-55: token state is set but never consumed by the only current caller

StoreItem.tsx exclusively uses ensureFreshToken() and never reads turnstile.token. The setToken calls inside ensureFreshToken and onSuccess update React state (triggering re-renders) without any consumer reacting to those changes. If no component needs the reactive token value, you could drop setToken from ensureFreshToken to avoid unnecessary re-renders during the refresh flow. If you plan to use token elsewhere (e.g., to gate the checkout button), this is fine — just noting the current re-render cost.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TurnstileWidget.tsx` around lines 7 - 55, The token state is
updated in useTurnstile but never consumed by callers like StoreItem.tsx,
causing unnecessary re-renders; remove the redundant setToken and
tokenObtainedAt mutations from ensureFreshToken (leave onSuccess/resetToken
behavior intact so explicit success/expire flows still update state if you later
consume token) and have ensureFreshToken simply await and return
ref.current?.getResponsePromise(...) or ref.current?.getResponse() without
calling setToken or updating tokenObtainedAt; update references to
ensureFreshToken, setToken, onSuccess, resetToken, token, and ref in that
function accordingly.
src/components/store/StoreItem.tsx (1)

946-953: Minor: id prop is the product ID — consider a dedicated widget ID

TurnstileWidget receives id={id} where id is the store product ID. This becomes the HTML id attribute on the Turnstile iframe container. If a product ID ever contains characters invalid for HTML IDs (spaces, special chars) or collides with another DOM element, it could cause subtle issues. A namespaced ID would be safer:

-                    <TurnstileWidget
-                      id={id}
+                    <TurnstileWidget
+                      id={`turnstile-${id}`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/store/StoreItem.tsx` around lines 946 - 953, TurnstileWidget
is being passed the product `id` prop (variable `id`) which may contain
characters invalid for an HTML id or collide with other elements; update the
prop passed to TurnstileWidget (component TurnstileWidget in this JSX) to use a
dedicated, namespaced/sanitized id such as prefixing with a constant (e.g.
`turnstile-${id}`) or applying a simple sanitizer that replaces non-alphanumeric
characters, so the rendered HTML id is safe and unique while still referencing
the product id via the namespaced value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/TurnstileWidget.tsx`:
- Around line 23-44: The logic in ensureFreshToken duplicates reset behavior;
replace the inline reset steps with a call to the existing resetToken() helper:
when isStale is true, invoke resetToken() (which should perform
setToken(undefined), tokenObtainedAt.current = 0, and ref.current?.reset()),
then await and return ref.current?.getResponsePromise(60_000) inside the
try/catch as before; keep the final return of ref.current?.getResponse()
unchanged and retain ensureFreshToken's signature and error handling.

---

Nitpick comments:
In `@src/components/store/StoreItem.tsx`:
- Around line 946-953: TurnstileWidget is being passed the product `id` prop
(variable `id`) which may contain characters invalid for an HTML id or collide
with other elements; update the prop passed to TurnstileWidget (component
TurnstileWidget in this JSX) to use a dedicated, namespaced/sanitized id such as
prefixing with a constant (e.g. `turnstile-${id}`) or applying a simple
sanitizer that replaces non-alphanumeric characters, so the rendered HTML id is
safe and unique while still referencing the product id via the namespaced value.

In `@src/components/TurnstileWidget.tsx`:
- Around line 7-55: The token state is updated in useTurnstile but never
consumed by callers like StoreItem.tsx, causing unnecessary re-renders; remove
the redundant setToken and tokenObtainedAt mutations from ensureFreshToken
(leave onSuccess/resetToken behavior intact so explicit success/expire flows
still update state if you later consume token) and have ensureFreshToken simply
await and return ref.current?.getResponsePromise(...) or
ref.current?.getResponse() without calling setToken or updating tokenObtainedAt;
update references to ensureFreshToken, setToken, onSuccess, resetToken, token,
and ref in that function accordingly.

Comment on lines +23 to +44
const ensureFreshToken = useCallback(async (): Promise<
string | undefined
> => {
const age = Date.now() - tokenObtainedAt.current;
const isStale =
!ref.current?.getResponse() ||
ref.current?.isExpired() ||
age >= TOKEN_LIFETIME_MS - REFRESH_BUFFER_MS;

if (isStale) {
setToken(undefined);
tokenObtainedAt.current = 0;
ref.current?.reset();
try {
return await ref.current?.getResponsePromise(60_000);
} catch {
return undefined;
}
}

return ref.current?.getResponse();
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find and read the TurnstileWidget.tsx file
find . -name "TurnstileWidget.tsx" -type f

Repository: acm-uiuc/website

Length of output: 96


🏁 Script executed:

# Check package.json for `@marsidev/react-turnstile` version
cat package.json | grep -A 2 -B 2 "react-turnstile"

Repository: acm-uiuc/website

Length of output: 263


🏁 Script executed:

# Read the TurnstileWidget.tsx file to see the actual code
cat -n src/components/TurnstileWidget.tsx | head -100

Repository: acm-uiuc/website

Length of output: 2669


🌐 Web query:

@marsidev/react-turnstile getResponsePromise reset method API documentation

💡 Result:

@marsidev/react-turnstile documents both getResponsePromise() and reset() under “useRef methods” (the ref API exposed by the <Turnstile /> component). (docs.page)

  • getResponsePromise(): “Returns the widget’s response as a promise, it waits until the widget is rendered and solved. It has a timeout of 30 seconds.”
    Return type: Promise<string>. (docs.page)
  • reset(): “Resets the widget. Useful if a given widget has timed out, expired or needs to be reloaded.”
    Return type: void. (docs.page)

Citations:


Refactor to reuse resetToken() instead of duplicating reset logic

Lines 33–35 duplicate the resetToken function. Extract to avoid maintenance drift:

Suggested refactor
     if (isStale) {
-      setToken(undefined);
-      tokenObtainedAt.current = 0;
-      ref.current?.reset();
+      resetToken();
       try {
         return await ref.current?.getResponsePromise(60_000);

The library's reset()getResponsePromise() sequence is designed for this use case per the docs, so no race condition is expected.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ensureFreshToken = useCallback(async (): Promise<
string | undefined
> => {
const age = Date.now() - tokenObtainedAt.current;
const isStale =
!ref.current?.getResponse() ||
ref.current?.isExpired() ||
age >= TOKEN_LIFETIME_MS - REFRESH_BUFFER_MS;
if (isStale) {
setToken(undefined);
tokenObtainedAt.current = 0;
ref.current?.reset();
try {
return await ref.current?.getResponsePromise(60_000);
} catch {
return undefined;
}
}
return ref.current?.getResponse();
}, []);
const ensureFreshToken = useCallback(async (): Promise<
string | undefined
> => {
const age = Date.now() - tokenObtainedAt.current;
const isStale =
!ref.current?.getResponse() ||
ref.current?.isExpired() ||
age >= TOKEN_LIFETIME_MS - REFRESH_BUFFER_MS;
if (isStale) {
resetToken();
try {
return await ref.current?.getResponsePromise(60_000);
} catch {
return undefined;
}
}
return ref.current?.getResponse();
}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TurnstileWidget.tsx` around lines 23 - 44, The logic in
ensureFreshToken duplicates reset behavior; replace the inline reset steps with
a call to the existing resetToken() helper: when isStale is true, invoke
resetToken() (which should perform setToken(undefined), tokenObtainedAt.current
= 0, and ref.current?.reset()), then await and return
ref.current?.getResponsePromise(60_000) inside the try/catch as before; keep the
final return of ref.current?.getResponse() unchanged and retain
ensureFreshToken's signature and error handling.

@devksingh4 devksingh4 merged commit 4524c70 into main Feb 17, 2026
13 of 15 checks passed
@devksingh4 devksingh4 deleted the fix-turnstile-expiry branch February 17, 2026 21:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant