Skip to content

Commit 169959e

Browse files
committed
fix: add retry logic for Turnstile CAPTCHA widget rendering
- initTurnstileWithRetry() polls every 500ms (up to 10x) for async script - Added onload=onTurnstileLoad callback to Turnstile script URL - turnstile-ready event auto-renders widget when script loads - Fixes widget not showing when share modal opens before script loads
1 parent 5a30086 commit 169959e

3 files changed

Lines changed: 74 additions & 3 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Turnstile Widget Retry Fix — Ensure CAPTCHA renders reliably
2+
3+
- Fixed: Turnstile widget never rendering when the async/defer script loads after the share modal opens
4+
- Added `initTurnstileWithRetry()` polling mechanism (retries every 500ms, up to 10 times)
5+
- Added `onload=onTurnstileLoad` callback to Turnstile script URL for immediate notification
6+
- Added `turnstile-ready` custom event listener to auto-render widget when script loads
7+
- Added `turnstileModalOpen` state tracking to coordinate script loading with modal visibility
8+
9+
---
10+
11+
## Summary
12+
The Turnstile CAPTCHA widget was not rendering because the async/defer script hadn't loaded when `initTurnstile()` was called. The function silently returned with no retry mechanism. This fix ensures reliable widget rendering through polling retries and an onload callback.
13+
14+
---
15+
16+
## 1. Retry Logic
17+
**Files:** `js/cloud-share.js`
18+
**What:** Added `initTurnstileWithRetry()` that polls every 500ms (up to 10 attempts) until the `turnstile` global is available, then renders the widget.
19+
**Impact:** Widget reliably renders regardless of script load timing.
20+
21+
## 2. Onload Callback
22+
**Files:** `index.html`, `js/cloud-share.js`
23+
**What:** Added `onload=onTurnstileLoad` parameter to script URL; callback dispatches `turnstile-ready` event; cloud-share.js listens and auto-inits if modal is open.
24+
**Impact:** Belt-and-suspenders approach — widget renders as soon as script loads.
25+
26+
---
27+
28+
## Files Changed (2 total)
29+
30+
| File | Lines Changed | Type |
31+
|------|:---:|------|
32+
| `js/cloud-share.js` | +30 −8 | Retry logic + event listener |
33+
| `index.html` | +6 −1 | Onload callback + event dispatch |

index.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1014,7 +1014,13 @@ <h4><i class="bi bi-keyboard me-2"></i>Keyboard Shortcuts</h4>
10141014

10151015

10161016
<!-- Cloudflare Turnstile CAPTCHA (explicit render — used in email-to-self flow) -->
1017-
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" async defer></script>
1017+
<script>
1018+
// Cloudflare Turnstile onload callback — dispatches event for cloud-share.js
1019+
function onTurnstileLoad() {
1020+
window.dispatchEvent(new Event('turnstile-ready'));
1021+
}
1022+
</script>
1023+
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=onTurnstileLoad" async defer></script>
10181024

10191025
<script type="module" src="/src/main.js"></script>
10201026
</body>

js/cloud-share.js

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,8 @@
859859
var savedEmail = localStorage.getItem(M.KEYS.EMAIL_SELF);
860860
if (savedEmail && emailInput) emailInput.value = savedEmail;
861861

862+
var turnstileModalOpen = false; // Track if the share result modal is open
863+
862864
/** Render Turnstile widget when the share result modal opens */
863865
function initTurnstile() {
864866
if (turnstileWidgetId !== null) return; // Already rendered
@@ -879,6 +881,23 @@
879881
}
880882
}
881883

884+
/**
885+
* Try to init Turnstile with retry — the script loads async/defer so it
886+
* may not be available when the share result modal first opens.
887+
*/
888+
function initTurnstileWithRetry(attempt) {
889+
attempt = attempt || 0;
890+
if (turnstileWidgetId !== null) return; // Already rendered
891+
if (typeof turnstile !== 'undefined') {
892+
initTurnstile();
893+
return;
894+
}
895+
// Retry up to 10 times (5 seconds total)
896+
if (attempt < 10 && turnstileModalOpen) {
897+
setTimeout(function () { initTurnstileWithRetry(attempt + 1); }, 500);
898+
}
899+
}
900+
882901
/** Reset the Turnstile widget for retry */
883902
function resetTurnstile() {
884903
if (typeof turnstile !== 'undefined') {
@@ -890,8 +909,21 @@
890909
var _origShowShareResult = showShareResult;
891910
showShareResult = function (url, isSecure) {
892911
_origShowShareResult(url, isSecure);
893-
// Small delay so the modal DOM is visible before Turnstile measures it
894-
setTimeout(initTurnstile, 200);
912+
turnstileModalOpen = true;
913+
// Start retry loop — handles case where script hasn't loaded yet
914+
setTimeout(function () { initTurnstileWithRetry(0); }, 200);
915+
};
916+
917+
// Auto-init when Turnstile script finishes loading (if modal is already open)
918+
window.addEventListener('turnstile-ready', function () {
919+
if (turnstileModalOpen) initTurnstile();
920+
});
921+
922+
// When the share result modal closes, mark it
923+
var _origCloseShareResult = M.closeShareResultModal;
924+
M.closeShareResultModal = function () {
925+
turnstileModalOpen = false;
926+
_origCloseShareResult();
895927
};
896928

897929
if (emailSendBtn) emailSendBtn.addEventListener('click', async function () {

0 commit comments

Comments
 (0)