Enabling reCAPTCHA v2 for multiple Marketo forms on the same page

Many of you use my Forms 2.0 JS snippets to integrate reCAPTCHA v2 or v3 with Marketo. (While Marketo will have native support for reCAPTCHA in the future, as of now you need to set it up yourself.)

To my shame, the existing reCAPTCHA v2 code doesn’t work with multiple forms on the same page, since it only places the “I’m not a robot” widget inside one (effectively) randomly chosen form.  Note this doesn’t matter for the v3 version, which is invisible.

Note: this post will not describe the back end, i.e. Marketo webhook configuration necessary to deploy reCAPTCHA. It assumes you already have reCAPTCHA integrated with a single form per page, likely using my earlier code, and have the webhook set up as well.

I’ve spruced up the code to inject a separate widget into every form, and manage that widget’s events (“fingerprint generated” and “fingerprint expired”) separately:

Here’s the code:

/**
 * Bind reCAPTCHA v2 widgets to all Marketo forms on a page
 * @author Sanford Whiteman
 * @version v1.0 2022-07-17
 * @copyright © 2022 Sanford Whiteman
 * @license Hippocratic 3.0: This license must appear with all reproductions of this software.
 *
 */
function bindReCAPTCHAToMktoForms(userConfig) {
    const defaultUserConfig = {
        productName: "reCAPTCHA Binder",
        log: true,
        alwaysLogWarn: true,
        errorTarget: "button[type='submit']"
    };
   
    const systemConfig = {};
   
    const config = Object.assign({}, defaultUserConfig, userConfig, systemConfig);
   
    const log = {
        always: console.log.bind(console, config.productName + " [SYSTEM]"),
        error: console.log.bind(console, config.productName + " [ERROR]"),
        info: config.log ? console.log.bind(console, config.productName + " [INFO]") : function () {},
        warn: config.alwaysLogWarn ? console.log.bind(console, config.productName + " [WARN]") : function () {},
        fatal: function () { log.error.apply(console, arguments); throw new Error(arguments[0]); }
    }
   
    
    const recaptchaListeners = {
       chosen: function (lastRecaptchaUserInput) {
          let mktoForm = this,
              mktoFields = {};
          
          log.info("reCAPTCHA response chosen", lastRecaptchaUserInput);
          mktoFields[userConfig.lastRecaptchaUserInputField] = lastRecaptchaUserInput;
          mktoForm.addHiddenFields(mktoFields);
       },
       expired: function () {
          let mktoForm = this,
              mktoFields = {};

          log.info("reCAPTCHA response expired");          
          mktoFields[userConfig.lastRecaptchaUserInputField] = "";
          mktoForm.addHiddenFields(mktoFields);
          grecaptcha.reset();
       }
    };
   
   MktoForms2.whenReady(function(mktoForm){
      let formEl = mktoForm.getFormElem()[0],
          submitButtonEl = formEl.querySelector("button[type='submit']");
      
      log.info("Disabling submit button, awaiting reCAPTCHA ready")
      submitButtonEl.disabled = true;      
   });

   window.onRecaptchaLibReady = function onRecaptchaLibReady() {
      MktoForms2.whenReady(function (mktoForm) {
         let formEl = mktoForm.getFormElem()[0],
             submitButtonRow = formEl.querySelector(".mktoButtonRow"),
             submitButtonEl = formEl.querySelector("button[type='submit']");
         
         log.info("Reenabling submit button")
         submitButtonEl.disabled = false;
         
         let recaptchaContainer = document.createElement("div");
         recaptchaContainer.className = "recaptcha-container";
         formEl.insertBefore(recaptchaContainer, submitButtonRow);
         
         grecaptcha.render(
            recaptchaContainer, 
            { 
               "sitekey" : config.siteKey,
               "callback" : recaptchaListeners.chosen.bind(mktoForm),
               "expired-callback" : recaptchaListeners.expired.bind(mktoForm)
            }
         );
                         
         {
            /* apply some intelligence to see if we last set submittability */
            let lastOwnedSubmittable;
            
            mktoForm.onValidate(function (native) {
               log.info("Running validate listener", "native", native, "submittable", mktoForm.submittable(), "owned", lastOwnedSubmittable);
               
               if (!native) {
                  log.info("Native validation pending");
                  return;               
               }
               
               if (mktoForm.submittable() === false && lastOwnedSubmittable !== false) {
                  log.info("Submittability not owned by this widget");
                  return;
               }
               
               mktoForm.submittable(lastOwnedSubmittable = false);               

               let currentValues = mktoForm.getValues();
               if (currentValues[userConfig.lastRecaptchaUserInputField]) {
                  /* continue to submit stage */
                  log.info("reCAPTCHA response present, proceeding to submit");
                  mktoForm.submittable(lastOwnedSubmittable = true);
               } else {
                  log.info("reCAPTCHA response not present, raising error");
                  mktoForm.showErrorMessage( config.errorMessage, MktoForms2.$(formEl.querySelector(config.errorTarget)) )
               }
            });
         }
         
      });
   }

   /* inject the reCAPTCHA library */
   let recaptchaLib = document.createElement("script");
   recaptchaLib.src = "https://www.google.com/recaptcha/api.js?onload=onRecaptchaLibReady&render=explicit";
   document.head.appendChild(recaptchaLib);

}

At minimum, pass these 2 config properties:

bindReCAPTCHAToMktoForms({
   siteKey : "6Le66aIUAAAAAKbTokIE3Rz1X-J_mcjik65gle21",
   lastRecaptchaUserInputField: "lastRecaptchaUserInput"
})

These should be self-explanatory, I hope!

Optionally, you can customize the error message, turn off console logging (leave it on during testing), and set the error message to highlight a particular element (defaults to the submit button):

bindReCAPTCHAToMktoForms({
   siteKey : "6Le66aIUAAAAAKbTokIE3Rz1X-J_mcjik65gle21",
   lastRecaptchaUserInputField: "lastRecaptchaUserInput",
   errorMessage: "Please click the reCAPTCHA checkbox.",
   errorTarget: "button[type='submit']",
   log: false
})

Also note you must not have any reCAPTCHA-related elements in the page (i.e. no <div class="g-recaptcha">). The JS handles everything.