I am rendering grecaptcha with code like this
let callback;
const p = new Promise((resolve) => callback = (result) => resolve(result));
grecaptcha.render(el, {
sitekey: window.settings.recaptchaKey,
size: "invisible",
type: "image",
callback: result => callback(result),
badge: "inline"
});
const key = await p;
all working fine, but if user clicks on the backdrop of recaptcha modal, recaptcha closes and i can't detect it, so i wait infinite for response
i need some kind of event or callback to detect when it closes
Unfortunately, Google doesn't have an API event to track this, but we can use the Mutation Observer Web API to track DOM changes by Google API on our own.
We have 2 challenges here.
1) Detect when the challenge is shown and get the overlay div of the challenge
function detectWhenReCaptchaChallengeIsShown() {
return new Promise(function(resolve) {
const targetElement = document.body;
const observerConfig = {
childList: true,
attributes: false,
attributeOldValue: false,
characterData: false,
characterDataOldValue: false,
subtree: false
};
function DOMChangeCallbackFunction(mutationRecords) {
mutationRecords.forEach((mutationRecord) => {
if (mutationRecord.addedNodes.length) {
var reCaptchaParentContainer = mutationRecord.addedNodes[0];
var reCaptchaIframe = reCaptchaParentContainer.querySelectorAll('iframe[title*="recaptcha"]');
if (reCaptchaIframe.length) {
var reCaptchaChallengeOverlayDiv = reCaptchaParentContainer.firstChild;
if (reCaptchaChallengeOverlayDiv.length) {
reCaptchaObserver.disconnect();
resolve(reCaptchaChallengeOverlayDiv);
}
}
}
});
}
const reCaptchaObserver = new MutationObserver(DOMChangeCallbackFunction);
reCaptchaObserver.observe(targetElement, observerConfig);
});
}
First, we created a target element that we would observe for Google iframe appearance. We targeted document.body as an iframe will be appended to it:
const targetElement = document.body;
Then we created a config object for MutationObserver. Here we might specify what exactly we track in DOM changes. Please note that all values are 'false' by default so we could only leave 'childList' - which means that we would observe only the child node changes for the target element - document.body in our case:
const observerConfig = {
childList: true,
attributes: false,
attributeOldValue: false,
characterData: false,
characterDataOldValue: false,
subtree: false
};
Then we created a function that would be invoked when an observer detects a specific type of DOM change that we specified in config object. The first argument represents an array of Mutation Observer objects. We grabbed the overlay div and returned in with Promise.
function DOMChangeCallbackFunction(mutationRecords) {
mutationRecords.forEach((mutationRecord) => {
if (mutationRecord.addedNodes.length) { //check only when notes were added to DOM
var reCaptchaParentContainer = mutationRecord.addedNodes[0];
var reCaptchaIframe = reCaptchaParentContainer.querySelectorAll('iframe[title*="recaptcha"]');
if (reCaptchaIframe.length) { // Google reCaptcha iframe was loaded
var reCaptchaChallengeOverlayDiv = reCaptchaParentContainer.firstChild;
if (reCaptchaChallengeOverlayDiv.length) {
reCaptchaObserver.disconnect(); // We don't want to observe more DOM changes for better performance
resolve(reCaptchaChallengeOverlayDiv); // Returning the overlay div to detect close events
}
}
}
});
}
Lastly we instantiated an observer itself and started observing DOM changes:
const reCaptchaObserver = new MutationObserver(DOMChangeCallbackFunction);
reCaptchaObserver.observe(targetElement, observerConfig);
2) Second challenge is the main question of that post - how do we detect that the challenge is closed? Well, we need help of MutationObserver again.
detectReCaptchaChallengeAppearance().then(function (reCaptchaChallengeOverlayDiv) {
var reCaptchaChallengeClosureObserver = new MutationObserver(function () {
if ((reCaptchaChallengeOverlayDiv.style.visibility === 'hidden') && !grecaptcha.getResponse()) {
// TADA!! Do something here as the challenge was either closed by hitting outside of an overlay div OR by pressing ESC key
reCaptchaChallengeClosureObserver.disconnect();
}
});
reCaptchaChallengeClosureObserver.observe(reCaptchaChallengeOverlayDiv, {
attributes: true,
attributeFilter: ['style']
});
});
So what we did is we get the Google reCaptcha challenge overlay div with the Promise we created in Step1 and then we subscribed for "style" changes on overlay div. This is because when the challenge is closed - Google fade it out. It's important to note that the visibility will be also hidden when a person solves the captcha successfully. That is why we added !grecaptcha.getResponse() check. It will return nothing unless the challenge is resolved. This is pretty much it - I hope that helps :)
As a dirty workaround, we can set timeout and wait for recaptcha iframe to show and then wait for it to hide
I made module that makes all manipulations
It depends on jquery and global recaptcha
and i use it like this
try {
key = await captcha(elementToBind, 'yoursitekey');
}
catch (error) {
console.log(error); // when recaptcha canceled it will print captcha canceled
}
the bad part, it may break when google change something in html structure
code of the module
/* global grecaptcha */
import $ from "jquery";
let callback = () => {};
let hideCallback = () => {};
export default function captcha (el, sitekey) {
const $el = $(el);
el = $el[0];
let captchaId = $el.attr("captcha-id");
let wrapper;
if (captchaId == null) {
captchaId = grecaptcha.render(el, {
sitekey,
size: "invisible",
type: "image",
callback: result => callback(result),
badge: "inline",
});
$(el).attr("captcha-id", captchaId);
}
else {
grecaptcha.reset(captchaId);
}
const waitForWrapper = setInterval(() => {
// first we search for recaptcha iframe
const iframe = $("iframe").filter((idx, iframe) => iframe.src.includes("recaptcha/api2/bframe"));
iframe.toArray().some(iframe => {
const w = $(iframe).closest("body > *");
// find the corresponding iframe for current captcha
if (w[0] && !w[0].hasAttribute("captcha-id") || w.attr("captcha-id") == captchaId) {
w.attr("captcha-id", captchaId);
wrapper = w; // save iframe wrapper element
clearInterval(waitForWrapper);
return true;
}
});
}, 100);
const result = new Promise((resolve, reject) => {
callback = (result) => {
clearInterval(waitForHide);
resolve(result);
};
hideCallback = (result) => {
clearInterval(waitForHide);
reject(result);
};
});
grecaptcha.execute(captchaId);
let shown = false;
const waitForHide = setInterval(() => {
if (wrapper) { // if we find iframe wrapper
if (!shown) {
// waiting for captcha to show
if (wrapper.css("visibility") !== "hidden") {
shown = true;
console.log("shown");
}
}
else {
// now waiting for it to hide
if (wrapper.css("visibility") === "hidden") {
console.log("hidden");
hideCallback(new Error("captcha canceled"));
}
}
}
}, 100);
return result;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With