- I am using Nuxt 2 with the @nuxtjs/recaptcha module on my signup form
- Very specifically I am using an invisible v2 recaptcha on the page as mentioned here
When I click outside the modal currently, my page goes into loading state infinitely as shown in the image below
This is the code for AppSignup.vue component if it helps
<!-- https://uistore.dev/framework/buefy/form/login-form-with-image-on-left -->
<template lang="pug">
.signup__container.is-flex.justify-content-center.is-align-items-center
.section.is-flex-grow-1
.columns.is-centered
.column.is-two-thirds-tablet.is-half-desktop.is-one-third-widescreen.is-one-quarter-fullhd
form(
@submit.prevent='onSubmit',
method='POST'
)
p.subtitle.has-text-centered Sign Up
b-notification(
aria-close-label='Close Notification',
id='error',
role='alert',
type='is-danger',
v-if='errorMessage'
) {{ errorMessage }}
b-field.has-text-left(
:label='formData.email.label',
:message='emailErrorMessage',
:type='emailErrorType',
id='emailField'
)
b-input(
:disabled='formData.email.disabled',
:has-counter='false',
:maxlength='formData.email.maxLength',
:minlength='formData.email.minLength',
:placeholder='formData.email.placeholder',
:use-html5-validation='false',
id='email'
key='email',
ref='email',
required,
type='email',
v-model='formData.email.value'
)
b-field.has-text-left(
:label='formData.password.label',
:message='passwordErrorMessage',
:type='passwordErrorType',
id='passwordField'
)
b-input(
:disabled='formData.password.disabled',
:has-counter='false',
:maxlength='formData.password.maxLength',
:minlength='formData.password.minLength',
:placeholder='formData.password.placeholder',
:use-html5-validation='false',
id='password',
key='password',
password-reveal,
ref='password',
required,
type='password',
v-model='formData.password.value'
)
.field.is-horizontal.is-justify-content-space-between
.control
label.checkbox
input(
required,
type='checkbox'
)
p.help.is-inline-block I agree to the
n-link.help.is-inline-block(to='/terms') terms
p.help.is-inline-block and
n-link.help.is-inline-block(to='/terms') conditions
.field
.control
recaptcha(
@recaptcha-error='onRecaptchaError',
@recaptcha-expired='onRecaptchaExpired'
)
.field
.control
b-button(
:class='{ "is-loading": state === "loading" }',
:disabled='isSignupButtonDisabled',
:type='signupButtonType',
expanded,
id='signup',
native-type='submit'
) {{ signupButtonText }}
.field
.control.has-text-centered or
hr
.field
.control.has-text-centered
n-link.button.is-fullwidth.is-text.signup(to='/login') Login
.field
.control.has-text-centered
n-link(to='/privacy') Privacy Policy
| and
n-link(to='/terms') Terms of Use
</template>
<script>
const SIGNUP_STATES = {
ERROR: 'error',
IDLE: 'idle',
LOADING: 'loading',
SUCCESS: 'success',
}
const RECAPTCHA_SERVER_ERROR_CODES = {
'bad-request': 'Server had an issue, please try again',
'invalid-input-response': 'Server had an issue, please try again',
'invalid-input-secret':
'Server had an issue, please try again after some time',
'missing-input-response': 'Server had an issue, please try again',
'missing-input-secret':
'Server had an issue, please try again after some time',
'timeout-or-duplicate': 'Captcha timed out, please try again',
}
export default {
name: 'AppSignup',
// https://github.com/vuejs/vue-router/issues/1103
beforeRouteEnter(to, from, next) {
next((vm) => {
vm.$store.commit('setRedirectTo', from)
})
},
data() {
return {
formData: this.getDefaultFormData(),
state: SIGNUP_STATES.IDLE,
errorMessage: null,
}
},
computed: {
emailErrorMessage() {
return this.formData.email.errorMessages.join('\n')
},
emailErrorType() {
const hasError =
(typeof this.emailErrorMessage === 'string' &&
this.emailErrorMessage.length > 0) ||
(typeof this.errorMessage === 'string' && this.errorMessage.length > 0)
return hasError ? 'is-danger' : 'is-light'
},
isSignupButtonDisabled() {
const hasEmailAndPassword =
this.formData.email.value.length && this.formData.password.value.length
return !hasEmailAndPassword
},
signupButtonText() {
if (this.state === SIGNUP_STATES.ERROR) {
return 'Create Account'
} else if (this.state === SIGNUP_STATES.LOADING) {
return ''
} else if (this.state === SIGNUP_STATES.SUCCESS) {
return 'Signed In'
} else {
return 'Create Account'
}
},
signupButtonType() {
if (this.state === SIGNUP_STATES.ERROR) {
return 'is-warning'
} else if (this.state === SIGNUP_STATES.LOADING) {
return 'is-warning is-light'
} else if (this.state === SIGNUP_STATES.SUCCESS) {
return 'is-success'
} else {
return 'is-warning'
}
},
passwordErrorMessage() {
return this.formData.password.errorMessages.join('\n')
},
passwordErrorType() {
const hasError =
(typeof this.passwordErrorMessage === 'string' &&
this.passwordErrorMessage.length > 0) ||
(typeof this.errorMessage === 'string' && this.errorMessage.length > 0)
return hasError ? 'is-danger' : 'is-light'
},
},
// https://stackoverflow.com/a/72105354/5371505
async mounted() {
try {
await this.$recaptcha.init()
} catch (error) {
throw new Error(`index# Problem initializing ReCaptcha: ${error}.`)
}
},
// https://stackoverflow.com/a/72105354/5371505
beforeDestroy() {
this.$recaptcha.destroy()
},
methods: {
// {"status_code":400,"type":"validation_error","param":"Bad Request","message":"Validation Failed","details":[{"recaptchaToken":"\"recaptchaToken\" is required"}]}
// {"status_code":400,"type":"validation_error","param":"Bad Request","message":"Validation Failed","details":[{"email":"\"email\" must be a valid email"}]}
// {"status_code":400,"type":"validation_error","param":"Bad Request","message":"Validation Failed","details":[{"password":"\"password\" length must be at least 8 characters long"}]}
// 400 bad request is generated for backend recaptcha errors like {"success":false,"error-codes":["invalid-input-secret"]}
// {"status_code":401,"type":"not_authenticated","param":null,"message":{"message":"Incorrect email or password"},"details":null}
// {"status_code":422,"type":"validation_error","param":"authentication_type_id,email","message":"authentication_type_id must be unique,email must be unique","details":null}
async doSignup() {
try {
const email = this.formData.email.value
const password = this.formData.password.value
const recaptchaToken = await this.$recaptcha.getResponse()
await this.$store.dispatch('auth/signup', {
email,
password,
recaptchaToken,
})
this.resetFormData()
this.state = SIGNUP_STATES.SUCCESS
this.formData.email.disabled = true
this.formData.password.disabled = true
this.$buefy.toast.open({
duration: 5000,
message:
'Account created successfully and logged in as ' +
this.$store.state.auth.user.username ||
this.$store.state.auth.user.email,
position: 'is-top',
type: 'is-success',
})
await this.$utils.timeout(100)
this.$router.replace(this.$store.state.redirectTo)
} catch (error) {
this.state = SIGNUP_STATES.ERROR
// Clear existing error messages before adding new ones
this.errorMessage = null
this.formData.email.errorMessages = []
this.formData.password.errorMessages = []
this.handleAxiosError(error)
this.state = SIGNUP_STATES.IDLE
} finally {
this.$recaptcha.reset()
}
},
getDefaultFormData() {
return {
email: {
disabled: false,
errorMessages: [],
label: '',
maxLength: 320,
minLength: 3,
placeholder: '[email protected]',
value: '',
},
password: {
disabled: false,
errorMessages: [],
label: '',
maxLength: 255,
minLength: 8,
placeholder: '8+ characters',
value: '',
},
}
},
// https://axios-http.com/docs/handling_errors
// https://stackoverflow.com/a/58386844/5371505
// https://developers.google.com/recaptcha/docs/verify#error_code_reference
handleAxiosError(error) {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
if (Array.isArray(error.response.data.details)) {
const errorMessages = error.response.data.details
for (let i = 0; i < errorMessages.length; i++) {
const errorMessage = errorMessages[i]
if (!(typeof errorMessage === 'object')) {
continue
}
if (errorMessage.email) {
this.formData.email.errorMessages.push(errorMessage.email)
} else if (errorMessage.password) {
this.formData.password.errorMessages.push(errorMessage.password)
} else {
const messages = []
for (const key in errorMessage) {
if (errorMessage[key]) {
messages.push(errorMessage[key])
}
}
this.errorMessage = messages.join('\n')
}
}
} else if (Array.isArray(error.response.data['error-codes'])) {
const errorCode = error.response.data['error-codes'][0]
this.errorMessage = RECAPTCHA_SERVER_ERROR_CODES[errorCode]
} else if (
error.response.data.param === 'authentication_type_id,email'
) {
this.errorMessage = 'Account with this email already exists'
} else {
this.errorMessage =
typeof error.response.data.message === 'object' &&
error.response.data.message !== null &&
typeof error.response.data.message.message === 'string'
? error.response.data.message.message
: error.response.data.message ||
'Please try again, something went wrong'
}
} else if (error.request) {
// Sample error when backend is unavailable {"message":"Network Error","name":"Error","fileName":"http://localhost:3000/_nuxt/commons/app.js line 424 > eval","lineNumber":16,"columnNumber":15,"stack":"createError@webpack-internal:///./node_modules/axios/lib/core/createError.js:16:15\nhandleError@webpack-internal:///./node_modules/axios/lib/adapters/xhr.js:99:14\nEventHandlerNonNull*dispatchXhrRequest@webpack-internal:///./node_modules/axios/lib/adapters/xhr.js:96:5\nxhrAdapter@webpack-internal:///./node_modules/axios/lib/adapters/xhr.js:13:10\ndispatchRequest@webpack-internal:///./node_modules/axios/lib/core/dispatchRequest.js:53:10\npromise callback*request@webpack-internal:///./node_modules/axios/lib/core/Axios.js:88:25\nforEachMethodWithData/Axios.prototype[method]@webpack-internal:///./node_modules/axios/lib/core/Axios.js:140:17\nwrap@webpack-internal:///./node_modules/axios/lib/helpers/bind.js:9:15\n_callee2$@webpack-internal:///./node_modules/babel-loader/lib/index.js?!./node_modules/vue-loader/lib/index.js?!./components/auth/AppSignup.vue?vue&type=script&lang=js:95:36\ntryCatch@webpack-internal:///./node_modules/regenerator-runtime/runtime.js:64:40\ninvoke@webpack-internal:///./node_modules/regenerator-runtime/runtime.js:299:30\ndefineIteratorMethods/</<@webpack-internal:///./node_modules/regenerator-runtime/runtime.js:124:21\nasyncGeneratorStep@webpack-internal:///./node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js:5:24\n_next@webpack-internal:///./node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js:24:27\npromise callback*asyncGeneratorStep@webpack-internal:///./node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js:14:28\n_next@webpack-internal:///./node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js:24:27\n_asyncToGenerator/</<@webpack-internal:///./node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js:29:12\n_asyncToGenerator/<@webpack-internal:///./node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js:21:12\ndoSignup@webpack-internal:///./node_modules/babel-loader/lib/index.js?!./node_modules/vue-loader/lib/index.js?!./components/auth/AppSignup.vue?vue&type=script&lang=js:125:10\nonSubmit@webpack-internal:///./node_modules/babel-loader/lib/index.js?!./node_modules/vue-loader/lib/index.js?!./components/auth/AppSignup.vue?vue&type=script&lang=js:195:19\nsubmit@webpack-internal:///./node_modules/babel-loader/lib/index.js?!./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/pug-plain-loader/index.js?!./node_modules/vue-loader/lib/index.js?!./components/auth/AppSignup.vue?vue&type=template&id=1b11a5c4&lang=pug:22:29\ninvokeWithErrorHandling@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:3056:30\ninvoker@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:1857:20\nadd/original_1._wrapper@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:7513:35\nEventListener.handleEvent*add@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:7517:12\nupdateListeners@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:1880:16\nupdateDOMListeners@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:7534:20\ninvokeCreateHooks@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:6692:28\nhydrate@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:7071:42\nhydrate@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:7044:34\nhydrate@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:7044:34\nhydrate@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:7044:34\nhydrate@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:7044:34\npatch@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:7126:36\nlifecycleMixin/Vue.prototype._update@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:3804:25\nupdateComponent@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:3907:16\nWatcher.prototype.get@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:3485:33\nWatcher@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:3475:51\nmountComponent@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:3931:5\nVue.prototype.$mount@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:8812:12\ninit@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:4448:19\nmerged@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:4602:11\nhydrate@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:7007:18\npatch@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:7126:36\nlifecycleMixin/Vue.prototype._update@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:3804:25\nupdateComponent@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:3907:16\nWatcher.prototype.get@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:3485:33\nWatcher@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:3475:51\nmountComponent@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:3931:5\nVue.prototype.$mount@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:8812:12\ninit@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:4448:19\nhydrate@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:7007:18\npatch@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:7126:36\nlifecycleMixin/Vue.prototype._update@webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:3804:25\n","config":{"url":"/api/v1/auth/login","method":"post","data":"{\"email\":\"abc@example\",\"password\":\"4564849\",\"recaptchaToken\":\"03AFcWeA5fuPzu8jMiTWj_9JbB_Rdx2ewMy7hoEvGusuTT0CLHXONAxObXxP7cPYeTE8RxqrM_Kf3dkVVwLB0wpK_gYUGjmHsmF9Y5uE-Sc_zleXLrpuGkVnOAt0R99mA5cDVlXN-tia5yBdBdC42JyhIvQchzL6tCDbm2Yg7oIm_jpozydQb94tXx2wxPWj4LxumvXPRN9Ws2CCoAv9cC6E85U7G9vG8faEYlL2za_JpenPMOT4sdvUQDwHdE1RNNkJe1iotBpjkucyD-_AsndfX2h09xwi8aNlCdwnjIu_SEJuXgmZnh9Kz4X-Eu1hvi1XMlYSKeSOmAQDmYSYlnzb4HTjP8yeTOudbhwwiy5rgC2gZ9kf9knFyvigdFMPfFq86rJpM_4hoRm6FZQ6q8cwEikq1aR0pCHKXps87vFG2g3m7YNdowqGLDjuajzikSQX2g9a9AAMH_g98hmOf5imU-X6dlxoEtL0P9Ni3jiNBGG3rgN7ObE6cDiWDoPm33JFv3cF8fi5T_S04klD7Uypn1REm3_uVC6fDtilyw5sSORYiDWo95cnvcldC-j6wQD8qk4HxvQlpY\"}","headers":{"Accept":"application/json, text/plain, */*","Content-Type":"application/json"},"baseURL":"http://localhost:8000","transformRequest":[null],"transformResponse":[null],"timeout":0,"xsrfCookieName":"XSRF-TOKEN","xsrfHeaderName":"X-XSRF-TOKEN","maxContentLength":-1,"maxBodyLength":-1,"transitional":{"silentJSONParsing":true,"forcedJSONParsing":true,"clarifyTimeoutError":false}}}
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
this.errorMessage =
error.request.message ||
error.request.statusText ||
error.message ||
'Please try again, something went wrong'
} else {
// Something happened in setting up the request that triggered an Error
this.errorMessage =
error.message || 'Please try again, something went wrong'
}
},
onRecaptchaError(error) {
this.state = SIGNUP_STATES.ERROR
this.errorMessage = error.message
},
onRecaptchaExpired() {
this.state = SIGNUP_STATES.ERROR
this.errorMessage = 'Captcha has expired, please try again'
},
onSubmit() {
this.state = SIGNUP_STATES.LOADING
return this.doSignup()
},
resetFormData() {
this.formData = this.getDefaultFormData()
this.errorMessage = null
},
},
}
</script>
<style lang="scss">
.signup__container {
height: 100%;
}
button[type='submit'] {
transition-property: background-color;
transition-duration: 0.5s;
}
</style>
Does anyone know how I can fix this issue?