nuxt-community / recaptcha-module

🤖 Simple and easy Google reCAPTCHA integration with Nuxt.js
MIT License
252 stars 63 forks source link

ERROR Cannot start nuxt: Cannot read properties of undefined (reading 'options') 16:21:56 #123

Open prazink opened 1 year ago

prazink commented 1 year ago

ERROR Cannot start nuxt: Cannot read properties of undefined (reading 'options') 16:21:56

devi4nt commented 1 year ago

I'm also getting this error, here is the stack trace in case it helps.

  at module.exports (node_modules/.pnpm/@nuxtjs+recaptcha@1.1.2/node_modules/@nuxtjs/recaptcha/lib/module.js:8:13)
  at installModule (node_modules/.pnpm/@nuxt+kit@3.2.3/node_modules/@nuxt/kit/dist/index.mjs:435:21)
  at async initNuxt (node_modules/.pnpm/nuxt@3.2.3_ycpbpc6yetojsgtrx3mwntkhsu/node_modules/nuxt/dist/index.mjs:2254:7)
  at async load (node_modules/.pnpm/nuxi@3.2.3/node_modules/nuxi/dist/chunks/dev.mjs:6810:9)                                                        18:11:21
  at async Object.invoke (node_modules/.pnpm/nuxi@3.2.3/node_modules/nuxi/dist/chunks/dev.mjs:6871:5)
  at async _main (node_modules/.pnpm/nuxi@3.2.3/node_modules/nuxi/dist/cli.mjs:50:20)
devi4nt commented 1 year ago

Oh, seems like nuxt3 isn't supported right now.

Perhaps this is a good alternative in the meantime: https://github.com/AurityLab/vue-recaptcha-v3/issues/609

fahmifitu commented 1 year ago

You can use the following plugin snippet for Nuxt 3 support, same steps will work. if you need the recaptcha component just copy it from the repository to your nuxt components folder.

Add recaptcha options to the runtime config in nuxt.config

export default defineNuxtPlugin((nuxtApp) => {
  const { grecaptcha } = useRuntimeConfig().public
  nuxtApp.provide('recaptcha', new ReCaptcha(grecaptcha))
})

const API_URL = 'https://www.recaptcha.net/recaptcha/api.js'

// https://github.com/PierfrancescoSoffritti/light-event-bus.js/blob/master/src/EventBus.js
function EventBus() {
  const subscriptions = {}

  this.subscribe = function subscribeCallbackToEvent(eventType, callback) {
    const id = Symbol('id')
    if (!subscriptions[eventType]) subscriptions[eventType] = {}
    subscriptions[eventType][id] = callback
    return {
      unsubscribe: function unsubscribe() {
        delete subscriptions[eventType][id]
        if (
          Object.getOwnPropertySymbols(subscriptions[eventType]).length === 0
        ) {
          delete subscriptions[eventType]
        }
      },
    }
  }

  this.publish = function publishEventWithArgs(eventType, arg) {
    if (!subscriptions[eventType]) return

    Object.getOwnPropertySymbols(subscriptions[eventType]).forEach((key) =>
      subscriptions[eventType][key](arg)
    )
  }
}

class ReCaptcha {
  constructor({ hideBadge, language, mode, siteKey, version, size }) {
    if (!siteKey) {
      throw new Error('ReCaptcha error: No key provided')
    }

    if (!version) {
      throw new Error('ReCaptcha error: No version provided')
    }

    this._elements = {}
    this._grecaptcha = null

    this._eventBus = null
    this._ready = false

    this.hideBadge = hideBadge
    this.language = language

    this.siteKey = siteKey
    this.version = version
    this.size = size

    this.mode = mode
  }

  destroy() {
    if (this._ready) {
      this._ready = false

      const { head } = document
      const { style } = this._elements

      const scripts = [...document.head.querySelectorAll('script')].filter(
        (script) => script.src.includes('recaptcha')
      )

      if (scripts.length) {
        scripts.forEach((script) => head.removeChild(script))
      }

      if (head.contains(style)) {
        head.removeChild(style)
      }

      const badge = document.querySelector('.grecaptcha-badge')
      if (badge) {
        badge.remove()
      }
    }
  }

  async execute(action) {
    try {
      await this.init()

      if ('grecaptcha' in window) {
        return this._grecaptcha.execute(this.siteKey, { action })
      }
    } catch (error) {
      throw new Error(`ReCaptcha error: Failed to execute ${error}`)
    }
  }

  getResponse(widgetId) {
    return new Promise((resolve, reject) => {
      if ('grecaptcha' in window) {
        if (this.size == 'invisible') {
          this._grecaptcha.execute(widgetId)

          window.recaptchaSuccessCallback = (token) => {
            this._eventBus.publish('recaptcha-success', token)
            resolve(token)
          }

          window.recaptchaErrorCallback = (error) => {
            this._eventBus.publish('recaptcha-error', error)
            reject(error)
          }
        } else {
          const response = this._grecaptcha.getResponse(widgetId)

          if (response) {
            this._eventBus.publish('recaptcha-success', response)
            resolve(response)
          } else {
            const errorMessage = 'Failed to execute'

            this._eventBus.publish('recaptcha-error', errorMessage)
            reject(errorMessage)
          }
        }
      }
    })
  }

  init() {
    if (this._ready) {
      // make sure caller waits until recaptcha get ready
      return this._ready
    }

    this._eventBus = new EventBus()
    this._elements = {
      script: document.createElement('script'),
      style: document.createElement('style'),
    }

    const { script, style } = this._elements

    script.setAttribute('async', '')
    script.setAttribute('defer', '')

    const params = []
    if (this.version === 3) {
      params.push('render=' + this.siteKey)
    }
    if (this.language) {
      params.push('hl=' + this.language)
    }

    let scriptUrl = API_URL

    if (this.mode === 'enterprise') {
      scriptUrl = scriptUrl.replace('api.js', 'enterprise.js')
      params.push('render=' + this.siteKey)
    }

    script.setAttribute('src', scriptUrl + '?' + params.join('&'))

    window.recaptchaSuccessCallback = (token) =>
      this._eventBus.publish('recaptcha-success', token)
    window.recaptchaExpiredCallback = () =>
      this._eventBus.publish('recaptcha-expired')
    window.recaptchaErrorCallback = () =>
      this._eventBus.publish('recaptcha-error', 'Failed to execute')

    this._ready = new Promise((resolve, reject) => {
      script.addEventListener('load', () => {
        if (this.version === 3 && this.hideBadge) {
          style.innerHTML = '.grecaptcha-badge { display: none }'
          document.head.appendChild(style)
        } else if (this.version === 2 && this.hideBadge) {
          // display: none DISABLES the spam checking!
          // ref: https://stackoverflow.com/questions/44543157/how-to-hide-the-google-invisible-recaptcha-badge
          style.innerHTML = '.grecaptcha-badge { visibility: hidden; }'
          document.head.appendChild(style)
        }

        this._grecaptcha = window.grecaptcha.enterprise || window.grecaptcha
        this._grecaptcha.ready(resolve)
      })

      script.addEventListener('error', () => {
        document.head.removeChild(script)
        reject('ReCaptcha error: Failed to load script')
        this._ready = null
      })

      document.head.appendChild(script)
    })

    return this._ready
  }

  on(event, callback) {
    return this._eventBus.subscribe(event, callback)
  }

  reset(widgetId) {
    if (this.version === 2 || typeof widgetId !== 'undefined') {
      this._grecaptcha.reset(widgetId)
    }
  }

  render(reference, { sitekey, theme }) {
    return this._grecaptcha.render(reference.$el || reference, {
      sitekey,
      theme,
    })
  }
}
akarkaselis commented 1 year ago

You can use the following plugin snippet for Nuxt 3 support, same steps will work. if you need the recaptcha component just copy it from the repository to your nuxt components folder.

Add recaptcha options to the runtime config in nuxt.config

export default defineNuxtPlugin((nuxtApp) => {
  const { grecaptcha } = useRuntimeConfig().public
  nuxtApp.provide('recaptcha', new ReCaptcha(grecaptcha))
})

const API_URL = 'https://www.recaptcha.net/recaptcha/api.js'

// https://github.com/PierfrancescoSoffritti/light-event-bus.js/blob/master/src/EventBus.js
function EventBus() {
  const subscriptions = {}

  this.subscribe = function subscribeCallbackToEvent(eventType, callback) {
    const id = Symbol('id')
    if (!subscriptions[eventType]) subscriptions[eventType] = {}
    subscriptions[eventType][id] = callback
    return {
      unsubscribe: function unsubscribe() {
        delete subscriptions[eventType][id]
        if (
          Object.getOwnPropertySymbols(subscriptions[eventType]).length === 0
        ) {
          delete subscriptions[eventType]
        }
      },
    }
  }

  this.publish = function publishEventWithArgs(eventType, arg) {
    if (!subscriptions[eventType]) return

    Object.getOwnPropertySymbols(subscriptions[eventType]).forEach((key) =>
      subscriptions[eventType][key](arg)
    )
  }
}

class ReCaptcha {
  constructor({ hideBadge, language, mode, siteKey, version, size }) {
    if (!siteKey) {
      throw new Error('ReCaptcha error: No key provided')
    }

    if (!version) {
      throw new Error('ReCaptcha error: No version provided')
    }

    this._elements = {}
    this._grecaptcha = null

    this._eventBus = null
    this._ready = false

    this.hideBadge = hideBadge
    this.language = language

    this.siteKey = siteKey
    this.version = version
    this.size = size

    this.mode = mode
  }

  destroy() {
    if (this._ready) {
      this._ready = false

      const { head } = document
      const { style } = this._elements

      const scripts = [...document.head.querySelectorAll('script')].filter(
        (script) => script.src.includes('recaptcha')
      )

      if (scripts.length) {
        scripts.forEach((script) => head.removeChild(script))
      }

      if (head.contains(style)) {
        head.removeChild(style)
      }

      const badge = document.querySelector('.grecaptcha-badge')
      if (badge) {
        badge.remove()
      }
    }
  }

  async execute(action) {
    try {
      await this.init()

      if ('grecaptcha' in window) {
        return this._grecaptcha.execute(this.siteKey, { action })
      }
    } catch (error) {
      throw new Error(`ReCaptcha error: Failed to execute ${error}`)
    }
  }

  getResponse(widgetId) {
    return new Promise((resolve, reject) => {
      if ('grecaptcha' in window) {
        if (this.size == 'invisible') {
          this._grecaptcha.execute(widgetId)

          window.recaptchaSuccessCallback = (token) => {
            this._eventBus.publish('recaptcha-success', token)
            resolve(token)
          }

          window.recaptchaErrorCallback = (error) => {
            this._eventBus.publish('recaptcha-error', error)
            reject(error)
          }
        } else {
          const response = this._grecaptcha.getResponse(widgetId)

          if (response) {
            this._eventBus.publish('recaptcha-success', response)
            resolve(response)
          } else {
            const errorMessage = 'Failed to execute'

            this._eventBus.publish('recaptcha-error', errorMessage)
            reject(errorMessage)
          }
        }
      }
    })
  }

  init() {
    if (this._ready) {
      // make sure caller waits until recaptcha get ready
      return this._ready
    }

    this._eventBus = new EventBus()
    this._elements = {
      script: document.createElement('script'),
      style: document.createElement('style'),
    }

    const { script, style } = this._elements

    script.setAttribute('async', '')
    script.setAttribute('defer', '')

    const params = []
    if (this.version === 3) {
      params.push('render=' + this.siteKey)
    }
    if (this.language) {
      params.push('hl=' + this.language)
    }

    let scriptUrl = API_URL

    if (this.mode === 'enterprise') {
      scriptUrl = scriptUrl.replace('api.js', 'enterprise.js')
      params.push('render=' + this.siteKey)
    }

    script.setAttribute('src', scriptUrl + '?' + params.join('&'))

    window.recaptchaSuccessCallback = (token) =>
      this._eventBus.publish('recaptcha-success', token)
    window.recaptchaExpiredCallback = () =>
      this._eventBus.publish('recaptcha-expired')
    window.recaptchaErrorCallback = () =>
      this._eventBus.publish('recaptcha-error', 'Failed to execute')

    this._ready = new Promise((resolve, reject) => {
      script.addEventListener('load', () => {
        if (this.version === 3 && this.hideBadge) {
          style.innerHTML = '.grecaptcha-badge { display: none }'
          document.head.appendChild(style)
        } else if (this.version === 2 && this.hideBadge) {
          // display: none DISABLES the spam checking!
          // ref: https://stackoverflow.com/questions/44543157/how-to-hide-the-google-invisible-recaptcha-badge
          style.innerHTML = '.grecaptcha-badge { visibility: hidden; }'
          document.head.appendChild(style)
        }

        this._grecaptcha = window.grecaptcha.enterprise || window.grecaptcha
        this._grecaptcha.ready(resolve)
      })

      script.addEventListener('error', () => {
        document.head.removeChild(script)
        reject('ReCaptcha error: Failed to load script')
        this._ready = null
      })

      document.head.appendChild(script)
    })

    return this._ready
  }

  on(event, callback) {
    return this._eventBus.subscribe(event, callback)
  }

  reset(widgetId) {
    if (this.version === 2 || typeof widgetId !== 'undefined') {
      this._grecaptcha.reset(widgetId)
    }
  }

  render(reference, { sitekey, theme }) {
    return this._grecaptcha.render(reference.$el || reference, {
      sitekey,
      theme,
    })
  }
}

This sounds like an amazing solution, unfortunately it's missing some clarity and I haven't managed to make it work.

fahmifitu commented 1 year ago

This sounds like an amazing solution, unfortunately it's missing some clarity and I haven't managed to make it work.

1- add the snippet in new plugin file in the plugins folder 2- the same usage steps from this module should work because the snippet implements the same api

marcdix commented 1 year ago

For all the ones that need a little more help:

  1. Put the content of the snippet into plugins/recaptcha.js - this will make nuxt autoload the plugin
  2. Add configuration in your nuxt.config.ts in runtimeConfig.public.grecaptcha:
    {
      runtimeConfig: {
        public: {
          grecaptcha: {
            hideBadge: true,
            mode: "base",
            siteKey: "",
            version: 3,
          },
        },
      },
    }
  3. (optional - is also done within execute which you call in step 4, but when doing it here already your form submit is a bit faster) Initialize the plugin onMounted of your application, e.g. in app.vue:
    onMounted(() => {
      const nuxtApp = useNuxtApp();
      nuxtApp.$recaptcha.init();
    });
  4. Wherever you send your form, get a token and send it as a header:
    async function send() {
      return fetch(`https://example.com/v2/do-something`, {
        headers: {
          "X-Recaptcha-Token": await useNuxtApp().$recaptcha.execute('whatever-name-you-want-to-give-it'),
        },
      });
    }
efesezer commented 1 year ago

You can use the following plugin snippet for Nuxt 3 support, same steps will work. if you need the recaptcha component just copy it from the repository to your nuxt components folder.

Add recaptcha options to the runtime config in nuxt.config

export default defineNuxtPlugin((nuxtApp) => {
  const { grecaptcha } = useRuntimeConfig().public
  nuxtApp.provide('recaptcha', new ReCaptcha(grecaptcha))
})

const API_URL = 'https://www.recaptcha.net/recaptcha/api.js'

// https://github.com/PierfrancescoSoffritti/light-event-bus.js/blob/master/src/EventBus.js
function EventBus() {
  const subscriptions = {}

  this.subscribe = function subscribeCallbackToEvent(eventType, callback) {
    const id = Symbol('id')
    if (!subscriptions[eventType]) subscriptions[eventType] = {}
    subscriptions[eventType][id] = callback
    return {
      unsubscribe: function unsubscribe() {
        delete subscriptions[eventType][id]
        if (
          Object.getOwnPropertySymbols(subscriptions[eventType]).length === 0
        ) {
          delete subscriptions[eventType]
        }
      },
    }
  }

  this.publish = function publishEventWithArgs(eventType, arg) {
    if (!subscriptions[eventType]) return

    Object.getOwnPropertySymbols(subscriptions[eventType]).forEach((key) =>
      subscriptions[eventType][key](arg)
    )
  }
}

class ReCaptcha {
  constructor({ hideBadge, language, mode, siteKey, version, size }) {
    if (!siteKey) {
      throw new Error('ReCaptcha error: No key provided')
    }

    if (!version) {
      throw new Error('ReCaptcha error: No version provided')
    }

    this._elements = {}
    this._grecaptcha = null

    this._eventBus = null
    this._ready = false

    this.hideBadge = hideBadge
    this.language = language

    this.siteKey = siteKey
    this.version = version
    this.size = size

    this.mode = mode
  }

  destroy() {
    if (this._ready) {
      this._ready = false

      const { head } = document
      const { style } = this._elements

      const scripts = [...document.head.querySelectorAll('script')].filter(
        (script) => script.src.includes('recaptcha')
      )

      if (scripts.length) {
        scripts.forEach((script) => head.removeChild(script))
      }

      if (head.contains(style)) {
        head.removeChild(style)
      }

      const badge = document.querySelector('.grecaptcha-badge')
      if (badge) {
        badge.remove()
      }
    }
  }

  async execute(action) {
    try {
      await this.init()

      if ('grecaptcha' in window) {
        return this._grecaptcha.execute(this.siteKey, { action })
      }
    } catch (error) {
      throw new Error(`ReCaptcha error: Failed to execute ${error}`)
    }
  }

  getResponse(widgetId) {
    return new Promise((resolve, reject) => {
      if ('grecaptcha' in window) {
        if (this.size == 'invisible') {
          this._grecaptcha.execute(widgetId)

          window.recaptchaSuccessCallback = (token) => {
            this._eventBus.publish('recaptcha-success', token)
            resolve(token)
          }

          window.recaptchaErrorCallback = (error) => {
            this._eventBus.publish('recaptcha-error', error)
            reject(error)
          }
        } else {
          const response = this._grecaptcha.getResponse(widgetId)

          if (response) {
            this._eventBus.publish('recaptcha-success', response)
            resolve(response)
          } else {
            const errorMessage = 'Failed to execute'

            this._eventBus.publish('recaptcha-error', errorMessage)
            reject(errorMessage)
          }
        }
      }
    })
  }

  init() {
    if (this._ready) {
      // make sure caller waits until recaptcha get ready
      return this._ready
    }

    this._eventBus = new EventBus()
    this._elements = {
      script: document.createElement('script'),
      style: document.createElement('style'),
    }

    const { script, style } = this._elements

    script.setAttribute('async', '')
    script.setAttribute('defer', '')

    const params = []
    if (this.version === 3) {
      params.push('render=' + this.siteKey)
    }
    if (this.language) {
      params.push('hl=' + this.language)
    }

    let scriptUrl = API_URL

    if (this.mode === 'enterprise') {
      scriptUrl = scriptUrl.replace('api.js', 'enterprise.js')
      params.push('render=' + this.siteKey)
    }

    script.setAttribute('src', scriptUrl + '?' + params.join('&'))

    window.recaptchaSuccessCallback = (token) =>
      this._eventBus.publish('recaptcha-success', token)
    window.recaptchaExpiredCallback = () =>
      this._eventBus.publish('recaptcha-expired')
    window.recaptchaErrorCallback = () =>
      this._eventBus.publish('recaptcha-error', 'Failed to execute')

    this._ready = new Promise((resolve, reject) => {
      script.addEventListener('load', () => {
        if (this.version === 3 && this.hideBadge) {
          style.innerHTML = '.grecaptcha-badge { display: none }'
          document.head.appendChild(style)
        } else if (this.version === 2 && this.hideBadge) {
          // display: none DISABLES the spam checking!
          // ref: https://stackoverflow.com/questions/44543157/how-to-hide-the-google-invisible-recaptcha-badge
          style.innerHTML = '.grecaptcha-badge { visibility: hidden; }'
          document.head.appendChild(style)
        }

        this._grecaptcha = window.grecaptcha.enterprise || window.grecaptcha
        this._grecaptcha.ready(resolve)
      })

      script.addEventListener('error', () => {
        document.head.removeChild(script)
        reject('ReCaptcha error: Failed to load script')
        this._ready = null
      })

      document.head.appendChild(script)
    })

    return this._ready
  }

  on(event, callback) {
    return this._eventBus.subscribe(event, callback)
  }

  reset(widgetId) {
    if (this.version === 2 || typeof widgetId !== 'undefined') {
      this._grecaptcha.reset(widgetId)
    }
  }

  render(reference, { sitekey, theme }) {
    return this._grecaptcha.render(reference.$el || reference, {
      sitekey,
      theme,
    })
  }
}

What is the exact options of the plugin? I added the below code in nuxt.config.ts file but I got the same error { runtimeConfig: { public: { grecaptcha: { hideBadge: true, mode: "base", siteKey: "", version: 3, }, }, }, } What I should do? I need to add recaptcha functionality to my project.

marcdix commented 1 year ago

@efesezer Did you make sure to remove the package (@nuxtjs/recaptcha) you might've installed earlier? When you say you get the exact same error it's at least likely that it's caused by the exact same code (i.e. the above mentioned package). Make sure to remove it from the package.json and run npm prune to uninstall it (and make sure you're not importing it in your code). The configuration you posted looks good to me.

efesezer commented 1 year ago

I loaded recaptcha component correctly. However when I call the getResponse method in order to get captcha response, I get error below

image

Do I suppose the send a parameter to getResponse() method? What should be the value?

marcdix commented 1 year ago

@efesezer I don't have enough info to debug this issue. I googled the error (No reCAPTCHA clients exist.) and found some clues. Can you try to do the same and see if you can solve the problem that way? Edit: You have a usable siteKey defined in the config, right?

efesezer commented 1 year ago

Thanks for your help. I managed to run the captcha module and saw the picture selection panel. However, I did not get the captha token.

First I tried with execute method and it returned null

var ReCaptcha = await useNuxtApp().$recaptcha.execute(siteKey)

After that, I tried with getResponse method, nothing happened. I couldnt get the captcha token.

var ReCaptcha = await useNuxtApp().$recaptcha.getResponse(siteKey)

What should I do in order to get captcha token?

fahmifitu commented 1 year ago

@efesezer The site key should go in nuxt.config file, not the way you're passing it

const { $recaptcha } =  useNuxtApp()
onMounted(() => {
  $recaptcha.init()
})

// example action that retrieves the token
const submitForm = async () => {
  const token =   await $recaptcha.execute('login')
}
marcdix commented 1 year ago

@efesezer You need to define the siteKey in yor configuration. What you pass to await useNuxtApp().$recaptcha.execute('whatever-name-you-want-to-give-it') (which returns the token that you then pass to the backend) is the action (I think you can track with this or use it for other purposes) - so call it "registration-form-submit" or so. You know how async / await works? It's an asynchronous process, so you can not just get the value but need to wait for the underlying Promise to resolve or reject.

efesezer commented 1 year ago

What does the parameter suppose to be in execute method? You gave "login" in the example, but What is this value? Is it the button id or form id?

marcdix commented 1 year ago

@efesezer It does not matter. You can decide. They call it 'action name'. I think you can read it in the backend so you can identify what this token is meant to be used for. It does totally not affect the functionality, so providing 'foobarbaz' is also fine.

efesezer commented 1 year ago

@marcdix Thank you for your help. I have one issue left. I am using below code in order to get captcha token. However it returns null. All configurations are done correctly. Where am I doing wrong? Could you please help me on this issue?

var reCaptcha = await useNuxtApp().$recaptcha.execute('login') console.log(reCaptcha) // Prints null

marcdix commented 1 year ago

@efesezer That's why I asked if you know how async / await works. The code you shared should give a syntax error. Please check the example @fahmifitu shared. You need to have an async function and inside you use await.

When you can't or don't want to use async / await, then you can chain with a then method:

function runStuff() {
  useNuxtApp().$recaptcha.execute('login').then((token) => {
    // the token is ONLY available in here, you can not 
    // log/use it "outside" of this function
    console.log(token);
  });
}
efesezer commented 1 year ago

I know the concept of async/await functions. I am calling the execute method with await option in an async function. However I still could not get the captcha token.

        async submit (e) {
          e.preventDefault()
          var reCaptcha = await useNuxtApp().$recaptcha.execute('checkCustomer')
          console.log(reCaptcha) 
         // prints the value null
        }

I should get the captcha token immediately because I am using await keyword in async function. But it returns null

marcdix commented 1 year ago

🤔 Only thing I can now imagine is that your siteKey could be wrong(ly configured). Else it looks good.

efesezer commented 1 year ago

You can use the following plugin snippet for Nuxt 3 support, same steps will work. if you need the recaptcha component just copy it from the repository to your nuxt components folder. Add recaptcha options to the runtime config in nuxt.config

export default defineNuxtPlugin((nuxtApp) => {
  const { grecaptcha } = useRuntimeConfig().public
  nuxtApp.provide('recaptcha', new ReCaptcha(grecaptcha))
})

const API_URL = 'https://www.recaptcha.net/recaptcha/api.js'

// https://github.com/PierfrancescoSoffritti/light-event-bus.js/blob/master/src/EventBus.js
function EventBus() {
  const subscriptions = {}

  this.subscribe = function subscribeCallbackToEvent(eventType, callback) {
    const id = Symbol('id')
    if (!subscriptions[eventType]) subscriptions[eventType] = {}
    subscriptions[eventType][id] = callback
    return {
      unsubscribe: function unsubscribe() {
        delete subscriptions[eventType][id]
        if (
          Object.getOwnPropertySymbols(subscriptions[eventType]).length === 0
        ) {
          delete subscriptions[eventType]
        }
      },
    }
  }

  this.publish = function publishEventWithArgs(eventType, arg) {
    if (!subscriptions[eventType]) return

    Object.getOwnPropertySymbols(subscriptions[eventType]).forEach((key) =>
      subscriptions[eventType][key](arg)
    )
  }
}

class ReCaptcha {
  constructor({ hideBadge, language, mode, siteKey, version, size }) {
    if (!siteKey) {
      throw new Error('ReCaptcha error: No key provided')
    }

    if (!version) {
      throw new Error('ReCaptcha error: No version provided')
    }

    this._elements = {}
    this._grecaptcha = null

    this._eventBus = null
    this._ready = false

    this.hideBadge = hideBadge
    this.language = language

    this.siteKey = siteKey
    this.version = version
    this.size = size

    this.mode = mode
  }

  destroy() {
    if (this._ready) {
      this._ready = false

      const { head } = document
      const { style } = this._elements

      const scripts = [...document.head.querySelectorAll('script')].filter(
        (script) => script.src.includes('recaptcha')
      )

      if (scripts.length) {
        scripts.forEach((script) => head.removeChild(script))
      }

      if (head.contains(style)) {
        head.removeChild(style)
      }

      const badge = document.querySelector('.grecaptcha-badge')
      if (badge) {
        badge.remove()
      }
    }
  }

  async execute(action) {
    try {
      await this.init()

      if ('grecaptcha' in window) {
        return this._grecaptcha.execute(this.siteKey, { action })
      }
    } catch (error) {
      throw new Error(`ReCaptcha error: Failed to execute ${error}`)
    }
  }

  getResponse(widgetId) {
    return new Promise((resolve, reject) => {
      if ('grecaptcha' in window) {
        if (this.size == 'invisible') {
          this._grecaptcha.execute(widgetId)

          window.recaptchaSuccessCallback = (token) => {
            this._eventBus.publish('recaptcha-success', token)
            resolve(token)
          }

          window.recaptchaErrorCallback = (error) => {
            this._eventBus.publish('recaptcha-error', error)
            reject(error)
          }
        } else {
          const response = this._grecaptcha.getResponse(widgetId)

          if (response) {
            this._eventBus.publish('recaptcha-success', response)
            resolve(response)
          } else {
            const errorMessage = 'Failed to execute'

            this._eventBus.publish('recaptcha-error', errorMessage)
            reject(errorMessage)
          }
        }
      }
    })
  }

  init() {
    if (this._ready) {
      // make sure caller waits until recaptcha get ready
      return this._ready
    }

    this._eventBus = new EventBus()
    this._elements = {
      script: document.createElement('script'),
      style: document.createElement('style'),
    }

    const { script, style } = this._elements

    script.setAttribute('async', '')
    script.setAttribute('defer', '')

    const params = []
    if (this.version === 3) {
      params.push('render=' + this.siteKey)
    }
    if (this.language) {
      params.push('hl=' + this.language)
    }

    let scriptUrl = API_URL

    if (this.mode === 'enterprise') {
      scriptUrl = scriptUrl.replace('api.js', 'enterprise.js')
      params.push('render=' + this.siteKey)
    }

    script.setAttribute('src', scriptUrl + '?' + params.join('&'))

    window.recaptchaSuccessCallback = (token) =>
      this._eventBus.publish('recaptcha-success', token)
    window.recaptchaExpiredCallback = () =>
      this._eventBus.publish('recaptcha-expired')
    window.recaptchaErrorCallback = () =>
      this._eventBus.publish('recaptcha-error', 'Failed to execute')

    this._ready = new Promise((resolve, reject) => {
      script.addEventListener('load', () => {
        if (this.version === 3 && this.hideBadge) {
          style.innerHTML = '.grecaptcha-badge { display: none }'
          document.head.appendChild(style)
        } else if (this.version === 2 && this.hideBadge) {
          // display: none DISABLES the spam checking!
          // ref: https://stackoverflow.com/questions/44543157/how-to-hide-the-google-invisible-recaptcha-badge
          style.innerHTML = '.grecaptcha-badge { visibility: hidden; }'
          document.head.appendChild(style)
        }

        this._grecaptcha = window.grecaptcha.enterprise || window.grecaptcha
        this._grecaptcha.ready(resolve)
      })

      script.addEventListener('error', () => {
        document.head.removeChild(script)
        reject('ReCaptcha error: Failed to load script')
        this._ready = null
      })

      document.head.appendChild(script)
    })

    return this._ready
  }

  on(event, callback) {
    return this._eventBus.subscribe(event, callback)
  }

  reset(widgetId) {
    if (this.version === 2 || typeof widgetId !== 'undefined') {
      this._grecaptcha.reset(widgetId)
    }
  }

  render(reference, { sitekey, theme }) {
    return this._grecaptcha.render(reference.$el || reference, {
      sitekey,
      theme,
    })
  }
}

This sounds like an amazing solution, unfortunately it's missing some clarity and I haven't managed to make it work.

I used the snippet above For the configuration, I used the code below

{ 
    runtimeConfig: { 
        public: { 
            grecaptcha: { 
                hideBadge: true, 
                mode: "base", 
                siteKey: "MY VALID SITE KEY HERE",
                version: 3, 
                language: "tr-TR"
            },
        },
    },
}

Are they wrong? What should I do add more apart from them?

marcdix commented 1 year ago

Yes, looks all fine - I'm using a very similar configuration. The only thing I can not check if all is right with your siteKey. So if you say you are 100% sure that the siteKey is right and must work, then I don't know what else you could try. You have the setup I have as well.

hendisantika commented 12 months ago

Maybe this article could help us --> https://dev.to/elquimeras/integrate-recaptcha-v3-on-nuxt3-app-1gma

kakariko-village commented 9 months ago

You can use the following plugin snippet for Nuxt 3 support, same steps will work. if you need the recaptcha component just copy it from the repository to your nuxt components folder. Add recaptcha options to the runtime config in nuxt.config

export default defineNuxtPlugin((nuxtApp) => {
  const { grecaptcha } = useRuntimeConfig().public
  nuxtApp.provide('recaptcha', new ReCaptcha(grecaptcha))
})

const API_URL = 'https://www.recaptcha.net/recaptcha/api.js'

// https://github.com/PierfrancescoSoffritti/light-event-bus.js/blob/master/src/EventBus.js
function EventBus() {
  const subscriptions = {}

  this.subscribe = function subscribeCallbackToEvent(eventType, callback) {
    const id = Symbol('id')
    if (!subscriptions[eventType]) subscriptions[eventType] = {}
    subscriptions[eventType][id] = callback
    return {
      unsubscribe: function unsubscribe() {
        delete subscriptions[eventType][id]
        if (
          Object.getOwnPropertySymbols(subscriptions[eventType]).length === 0
        ) {
          delete subscriptions[eventType]
        }
      },
    }
  }

  this.publish = function publishEventWithArgs(eventType, arg) {
    if (!subscriptions[eventType]) return

    Object.getOwnPropertySymbols(subscriptions[eventType]).forEach((key) =>
      subscriptions[eventType][key](arg)
    )
  }
}

class ReCaptcha {
  constructor({ hideBadge, language, mode, siteKey, version, size }) {
    if (!siteKey) {
      throw new Error('ReCaptcha error: No key provided')
    }

    if (!version) {
      throw new Error('ReCaptcha error: No version provided')
    }

    this._elements = {}
    this._grecaptcha = null

    this._eventBus = null
    this._ready = false

    this.hideBadge = hideBadge
    this.language = language

    this.siteKey = siteKey
    this.version = version
    this.size = size

    this.mode = mode
  }

  destroy() {
    if (this._ready) {
      this._ready = false

      const { head } = document
      const { style } = this._elements

      const scripts = [...document.head.querySelectorAll('script')].filter(
        (script) => script.src.includes('recaptcha')
      )

      if (scripts.length) {
        scripts.forEach((script) => head.removeChild(script))
      }

      if (head.contains(style)) {
        head.removeChild(style)
      }

      const badge = document.querySelector('.grecaptcha-badge')
      if (badge) {
        badge.remove()
      }
    }
  }

  async execute(action) {
    try {
      await this.init()

      if ('grecaptcha' in window) {
        return this._grecaptcha.execute(this.siteKey, { action })
      }
    } catch (error) {
      throw new Error(`ReCaptcha error: Failed to execute ${error}`)
    }
  }

  getResponse(widgetId) {
    return new Promise((resolve, reject) => {
      if ('grecaptcha' in window) {
        if (this.size == 'invisible') {
          this._grecaptcha.execute(widgetId)

          window.recaptchaSuccessCallback = (token) => {
            this._eventBus.publish('recaptcha-success', token)
            resolve(token)
          }

          window.recaptchaErrorCallback = (error) => {
            this._eventBus.publish('recaptcha-error', error)
            reject(error)
          }
        } else {
          const response = this._grecaptcha.getResponse(widgetId)

          if (response) {
            this._eventBus.publish('recaptcha-success', response)
            resolve(response)
          } else {
            const errorMessage = 'Failed to execute'

            this._eventBus.publish('recaptcha-error', errorMessage)
            reject(errorMessage)
          }
        }
      }
    })
  }

  init() {
    if (this._ready) {
      // make sure caller waits until recaptcha get ready
      return this._ready
    }

    this._eventBus = new EventBus()
    this._elements = {
      script: document.createElement('script'),
      style: document.createElement('style'),
    }

    const { script, style } = this._elements

    script.setAttribute('async', '')
    script.setAttribute('defer', '')

    const params = []
    if (this.version === 3) {
      params.push('render=' + this.siteKey)
    }
    if (this.language) {
      params.push('hl=' + this.language)
    }

    let scriptUrl = API_URL

    if (this.mode === 'enterprise') {
      scriptUrl = scriptUrl.replace('api.js', 'enterprise.js')
      params.push('render=' + this.siteKey)
    }

    script.setAttribute('src', scriptUrl + '?' + params.join('&'))

    window.recaptchaSuccessCallback = (token) =>
      this._eventBus.publish('recaptcha-success', token)
    window.recaptchaExpiredCallback = () =>
      this._eventBus.publish('recaptcha-expired')
    window.recaptchaErrorCallback = () =>
      this._eventBus.publish('recaptcha-error', 'Failed to execute')

    this._ready = new Promise((resolve, reject) => {
      script.addEventListener('load', () => {
        if (this.version === 3 && this.hideBadge) {
          style.innerHTML = '.grecaptcha-badge { display: none }'
          document.head.appendChild(style)
        } else if (this.version === 2 && this.hideBadge) {
          // display: none DISABLES the spam checking!
          // ref: https://stackoverflow.com/questions/44543157/how-to-hide-the-google-invisible-recaptcha-badge
          style.innerHTML = '.grecaptcha-badge { visibility: hidden; }'
          document.head.appendChild(style)
        }

        this._grecaptcha = window.grecaptcha.enterprise || window.grecaptcha
        this._grecaptcha.ready(resolve)
      })

      script.addEventListener('error', () => {
        document.head.removeChild(script)
        reject('ReCaptcha error: Failed to load script')
        this._ready = null
      })

      document.head.appendChild(script)
    })

    return this._ready
  }

  on(event, callback) {
    return this._eventBus.subscribe(event, callback)
  }

  reset(widgetId) {
    if (this.version === 2 || typeof widgetId !== 'undefined') {
      this._grecaptcha.reset(widgetId)
    }
  }

  render(reference, { sitekey, theme }) {
    return this._grecaptcha.render(reference.$el || reference, {
      sitekey,
      theme,
    })
  }
}

This sounds like an amazing solution, unfortunately it's missing some clarity and I haven't managed to make it work.

Hi I follow your guide but when I try to get the token by running the execute method, I have this error: "ReCaptcha error: Failed to execute Error: Invalid site key or not loaded in api.js: XXX at ReCaptcha.execute "

XXX is my site key and I am pretty sure it's valid since my current WordPress page is using it

Here's my page:

<script setup lang="ts">
import { Button, message } from "ant-design-vue";

const { $recaptcha } = useNuxtApp();
onMounted(() => {
  ($recaptcha as any).init();
});

const handleMessage = async () => {
  message.info("This is a normal message");
  console.log($recaptcha);
  try {
    const token = await (useNuxtApp().$recaptcha as any).execute("login");
    if (token) {
      console.log(token);
    }
  } catch (err) {
    console.log(err);
  }
};
</script>

<template>
  <div>
    <Button type="primary" @click="handleMessage"> Button </Button>
  </div>
</template>

My nuxt.config.ts:

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: true },
  modules: [
    "@ant-design-vue/nuxt",
    "@nuxt/image",
    "@nuxtjs/tailwindcss",
    "@hebilicious/vue-query-nuxt",
    "@nuxtjs/eslint-module",
    "@nuxtjs/robots",
    "@ant-design-vue/nuxt",
    "@nuxtjs/seo",
  ],
  app: {
    baseURL: "/",
  },
  css: ["~/assets/css/main.css", "ant-design-vue/dist/reset.css"],
  postcss: {
    plugins: {
      tailwindcss: {},
      "tailwindcss/nesting": {},
      autoprefixer: {},
    },
  },
  eslint: {
    lintOnStart: false,
  },
  runtimeConfig: {
    public: {
      grecaptcha: {
        siteKey: process.env.RECAPTCHA_SITE_KEY,
        version: 2,
      },
    },
  },
});

My plugins/recaptcha.ts is exactly like yours