The problem is quite simple: some calls to refresh() result in window.grecaptcha to be undefined. Not always as I said, I think because network slowdown. It's quite "hard" to debug this, and I'm also quite new to this concept, so I'm asking for some help, maybe a code fault.
class RecaptchaController {
loadPromise;
async refresh() {
await this.load();
// window.grecaptcha is (sometimes) undefined in next line
this.element.value = window.grecaptcha.execute(this.options.siteKey, { action: 'submit' });
}
load() {
if (!this.loadPromise) {
this.context.logDebugActivity('load');
this.loadPromise = new Promise((resolve, reject) => {
const url = new URL(this.options.apiUrl);
url.searchParams.append('render', this.options.siteKey);
url.searchParams.append('hl', this.options.locale);
const script = document.createElement('script');
script.setAttribute('id', this.identifier);
script.setAttribute('src', url.toString());
// The relevant part where I'm resolving the promise
script.addEventListener('load', resolve);
script.addEventListener('error', reject);
document.body.appendChild(script);
})
}
return this.loadPromise;
}
}
Error thrown is:
recaptcha_controller.js:27 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'execute')
Basically, refresh is going to be called in a submit event handler, like this:
form.addEventListener('submit', async (e) => {
// This is NOT how the initialization works!
// Just and example about how I'm calling refresh()
cost recaptchaController = new RecaptchaController();
// This is actually what is happening
await recaptchaController.refresh();
// Submit handling via fetch()...
});
I'm not pasting the entire code to make it simple. In addition, there is a framework behind (but it's not relevant).
I made a test with reCAPTCHA v3, and looking at requests in DevTools network panel, it seems reCAPTCHA itself loads another locale-specific script asynchronously. To dynamically load reCAPTCHA the way you're trying to, you need to setup a globally visible callback and pass its name to the onload parameter of api.js.
load() {
if (!this.loadPromise) {
this.context.logDebugActivity('load');
this.loadPromise = new Promise((resolve, reject) => {
// recaptcha onload will resolve this promise
window._recaptchaOnLoad = () => resolve();
const url = new URL(this.options.apiUrl);
url.searchParams.append('onload', '_recaptchaOnLoad');
url.searchParams.append('render', this.options.siteKey);
url.searchParams.append('hl', this.options.locale);
const script = document.createElement('script');
script.setAttribute('id', this.identifier);
script.setAttribute('src', url.toString());
// load event no longer needed
script.addEventListener('error', reject);
document.body.appendChild(script);
}).finally(() => {
// optionally delete window property
delete window._recaptchaOnLoad;
})
}
return this.loadPromise;
}