-
-
Save callaginn/7e64626dc6d648936ff0e4f3d83a5304 to your computer and use it in GitHub Desktop.
// Before implementing this, you'll need to contact Shopify support and ask them to turn off Google's ReCaptcha | |
// for your Shopify store's contact forms. Otherwise, it will redirect to the captcha's verification page. | |
// Retrieves input data from a form and returns it as a JSON object: | |
function formToJSON(elements) { | |
return [].reduce.call(elements, function (data, element) { | |
data[element.name] = element.value; | |
return data; | |
}, {}); | |
} | |
// Get Shopify Friendly URL String | |
function getUrlString(data) { | |
var urlParameters = Object.entries(data).map(function (e) { | |
return e.join('='); | |
}).join('&'); | |
return urlParameters; | |
} | |
function getUrlParameter(sParam) { | |
var sPageURL = decodeURIComponent(window.location.search.substring(1)), | |
sURLVariables = sPageURL.split('&'), | |
sParameterName, | |
i; | |
for (i = 0; i < sURLVariables.length; i++) { | |
sParameterName = sURLVariables[i].split('='); | |
if (sParameterName[0] === sParam) { | |
return sParameterName[1] === undefined ? true : sParameterName[1]; | |
} | |
} | |
} | |
function ajaxFormInit(form) { | |
var form_type = form.querySelector("[name=form_type]").value, | |
inputs = form.querySelectorAll("[name]"), | |
alert = form.querySelector('[data-alert="status"]'), | |
alert_msgs = form.querySelector('.form-alerts'); | |
form.addEventListener('submit', function(e){ | |
e.preventDefault(); | |
var action = form.getAttribute("action"); | |
if (alert_msgs) { | |
var alert_msg = JSON.parse(alert_msgs.innerHTML) | |
} | |
console.log("Form Action: " + action); | |
console.log("Submitting " + form_type + " form..."); | |
fetch(action, { | |
method: 'POST', | |
body: getUrlString(formToJSON(inputs)), | |
headers: { | |
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', | |
'Accept': 'text/html, */*; q=0.01', | |
'X-Requested-With': 'XMLHttpRequest' | |
} | |
}).then(function(response) { | |
console.log(response); | |
console.log(response.status); | |
if (alert) { | |
alert.className = "alert alert-success"; | |
alert.innerHTML = alert_msg.success; | |
} | |
var checkoutUrl = getUrlParameter("checkout_url"); | |
if (checkoutUrl) { | |
window.location = getUrlParameter("checkout_url"); | |
} else if (response.status === 200 && form_type !== "contact") { | |
window.location.pathname = "/account" | |
} | |
}).catch(function(err) { | |
console.error(err); | |
if (alert) { | |
alert.className = "alert alert-error"; | |
alert.innerHTML = alert_msg.error; | |
} | |
}); | |
}); | |
} | |
// Init Shopify Forms | |
document.querySelectorAll("[name=form_type]").forEach(function(el) { | |
ajaxFormInit(el.closest("form")); | |
}); |
{% comment %} | |
Contact Form Wide | |
{% endcomment %} | |
{%- form 'contact' -%} | |
<div class="row"> | |
<div class="col-md-4 mb-3"> | |
<label for="name">{{ 'contact.form.name' | t }} <i class="fa fa-asterisk"></i></label> | |
<input type="text" class="form-control" name="contact[name]" autocomplete="name" required> | |
</div> | |
<div class="col-md-4 mb-3"> | |
<label for="email">{{ 'contact.form.email' | t }} <i class="fa fa-asterisk"></i></label> | |
<input type="email" class="form-control" name="contact[email]" autocomplete="email" required> | |
</div> | |
<div class="col-md-4 mb-3"> | |
<label for="phone">{{ 'contact.form.phone' | t }} <i class="fa fa-asterisk"></i></label> | |
<input type="tel" class="form-control" name="contact[phone]" autocomplete="tel" required> | |
</div> | |
</div> | |
<div class="mb-3"> | |
<label for="message">{{ 'contact.form.message' | t }}</label> | |
<textarea id="message" class="form-control" name="contact[body]" rows="7"></textarea> | |
</div> | |
<!-- 2019 Honeypot / Checkbox Placeholder --> | |
<div class="checkbox captcha"><input type="text" class="honeypot" autocomplete="off" style="display:none;"></div> | |
<script type="application/json" class="form-alerts"> | |
{ | |
"error": "{{ 'general.forms.post_error' | t }}", | |
"success": "{{ 'contact.form.post_success' | t }}" | |
} | |
</script> | |
<div class="d-none" data-alert="status"></div> | |
<button type="button" class="btn btn-lg btn-outline-primary btn-submit disabled" disabled>{{ 'contact.form.send' | t }}</button> | |
{%- endform -%} |
Hey @callaginn, this is incredibly generous of you to put in the time to update this for me! I really appreciate it. :) I'm always amazed at the developer community and how much it gives back, you're definitely part of that. Thanks again!
hi @callaginn, could you help create the Shopify Ajax newsletter form too? Thank you!
Hey @callaginn
I am using your awesome script, but I end up receiving this error 429 after a couple of tries, then I tried again the next day and it doesn't seem to work. I turned off the Captcha in Shopify Preferences. Any help is appreciated.
@know-nothing-john-snow Hey friend, the Captcha settings don't help you fully. I just talked with support and they can't help me. They told me I need to talk with Google' support. What you can do to avoid the 429 error is ensure your form only fires once on submission.
The lines below:
console.log(response);
console.log(response.status);
Will output a response and if it is successful you will see it is either doing it correctly and submitting or it is sending you to your-url.exmaple/challenge
which is where the captcha gets involved.
Using a custom-contact.liquid
file and having alpine.js running I was able to create a custom pop up contact form. It still triggers the challenge if you try too many times. But using a VPN I was able to confirm that it works the first few times. I don't anticipate users to spam it unless they are bots. Additionally I have a backup hidden contact page with the default form that is linked to from the pop up form.
I used a wrapping div with the Alpine directive x-data
, with the form inside like so:
<div x-data="{
submitting: false,
showAlert: false,
alertMessage: '',
formSubmitted: false,
submitForm() {
if (this.submitting) {
// Form is already being submitted, prevent multiple submissions
return;
}
this.submitting = true;
let formData = new FormData($refs.submit_contact_custom_form);
console.log('submitting form', formData);
// Introduce a delay before form submission (e.g., 1000 milliseconds = 1 second)
setTimeout(() => {
fetch('/contact', {
method: 'POST',
body: formData
})
.then(response => {
console.log(response);
console.log(response.status);
if (response.status === 200) {
// Successful response
this.formSubmitted = true;
this.showAlert = true;
this.alertMessage = 'Your message was successfully sent.';
this.$refs.submit_contact_custom_form.reset() // Reset the form inputs
} else {
// Non-successful response
throw new Error('Form submission failed.');
}
})
.catch(error => {
// Handle errors here
console.error('Form submission error:', error);
this.showAlert = true;
this.alertMessage = 'An error occurred while submitting the form.';
this.formSubmitted = false; // Reset formSubmitted if there was an error
})
.finally(() => {
// Reset the submitting status after form submission
this.submitting = false;
});
}, 1000); // 1000 milliseconds = 1 second
}
}">
{%- form 'contact', id: 'ContactFormCustom', class: 'bg-white', x-ref: 'submit_contact_custom_form' -%}
<div class="field">
<input
class="field__input"
autocomplete="given-name"
type="text"
id="ContactForm-first_name"
name="contact[{{ 'templates.contact_custom.form.firstname' | t }}]"
value="{% if form.first_name %}{{ form.first_name }}{% elsif customer %}{{ customer.first_name }}{% endif %}"
placeholder="{{ 'templates.contact_custom.form.firstname' | t }}"
>
<label class="field__label" for="ContactForm-first_name">{{ 'templates.contact_custom.form.firstname' | t }}</label>
</div>
<div class="field">
<input
class="field__input"
autocomplete="family-name"
type="text"
id="ContactForm-last_name"
name="contact[{{ 'templates.contact_custom.form.lastname' | t }}]"
value="{% if form.last_name %}{{ form.last_name }}{% elsif customer %}{{ customer.last_name }}{% endif %}"
placeholder="{{ 'templates.contact_custom.form.lastname' | t }}"
>
<label class="field__label" for="ContactForm-last_name">{{ 'templates.contact_custom.form.lastname' | t }}</label>
</div>
<div class="field field--with-error">
<input
autocomplete="email"
type="email"
id="ContactForm-email"
class="field__input"
name="contact[email]"
spellcheck="false"
autocapitalize="off"
value="{% if form.email %}{{ form.email }}{% elsif customer %}{{ customer.email }}{% endif %}"
aria-required="true"
{% if form.errors contains 'email' %}
aria-invalid="true"
aria-describedby="ContactForm-email-error"
{% endif %}
placeholder="{{ 'templates.contact_custom.form.email' | t }}"
>
<label class="field__label" for="ContactForm-email">
{{- 'templates.contact_custom.form.email' | t }}
<span aria-hidden="true">*</span></label
>
{%- if form.errors contains 'email' -%}
<small class="contact__field-error" id="ContactForm-email-error">
<span class="visually-hidden">{{ 'accessibility.error' | t }}</span>
<span class="form__message">
{%- render 'icon-error' -%}
{{- form.errors.translated_fields.email | capitalize }}
{{ form.errors.messages.email -}}
</span>
</small>
{%- endif -%}
</div>
<div class="field">
<input
type="tel"
id="ContactForm-phone"
class="field__input"
autocomplete="tel"
name="contact[{{ 'templates.contact_custom.form.phone' | t }}]"
pattern="[0-9\-]*"
value="{% if form.phone %}{{ form.phone }}{% elsif customer %}{{ customer.phone }}{% endif %}"
placeholder="{{ 'templates.contact_custom.form.phone' | t }}"
>
<label class="field__label" for="ContactForm-phone">{{ 'templates.contact_custom.form.phone' | t }}</label>
</div>
<div class="field">
<textarea
rows="10"
id="ContactForm-body"
class="text-area field__input"
name="contact[{{ 'templates.contact_custom.form.comment' | t }}]"
placeholder="{{ 'templates.contact_custom.form.comment' | t }}"
>
{{- form.body -}}
</textarea>
<label class="form__label field__label" for="ContactForm-body">
{{- 'templates.contact_custom.form.comment' | t -}}
</label>
</div>
<div x-show="!formSubmitted">
<button type="button" class="primary_button alt" @click="submitForm()" x-bind:disabled="submitting">{{ 'templates.contact_custom.form.send' | t }}</button>
</div>
{%- if form.posted_successfully? -%}
<h2 class="form-status form-status-list form__message" tabindex="-1" autofocus>
{% render 'icon-success' %}
{{ 'templates.contact_custom.form.post_success' | t }}
</h2>
{%- elsif form.errors -%}
<div class="form__message">
<h2 class="form-status caption-large text-body" role="alert" tabindex="-1" autofocus>
{% render 'icon-error' %}
{{ 'templates.contact_custom.form.error_heading' | t }}
</h2>
</div>
<ul class="form-status-list caption-large" role="list">
<li>
<a href="#ContactForm-email" class="link">
{{ form.errors.translated_fields.email | capitalize }}
{{ form.errors.messages.email }}
</a>
</li>
</ul>
{%- endif -%}
{%- endform -%}
<div x-show="showAlert" x-text="alertMessage" x-bind:class="{'success-message text-primary-main_dark': formSubmitted, 'error-message': !formSubmitted}"></div>
</div>
{% schema %}
{
"name": "Custom Contact Form",
"tag": "section",
"class": "section",
"settings": [],
"presets": [
{
"name": "Custom Contact Form"
}
]
}
{% endschema %}
Hope you find some success.
@callaginn Do you have any idea, if we can overwrite the reCaptcha with reCAPTCHA v3 to skip the redirection/challenge?
Thanks for the script. Has anybody have an luck with this recently?
I have a sandbox store that I've been playing around with and after some slight modification to the code to get "mostly" working in my environment (the stock functions still bind to the form, even with event.preventDefault(), so the custom Ajax code essentially doesn't run for me anyway. I circumvented this by cloning the form at page load and then binding this Ajax library instead.
This is all great, and prevents any sort of default page redirection, etc, with the hopes of just staying on the page from where the form is submitted (for me this is a modal / dialog). However, no matter what I do, I'm getting a HTTP error '429' on the attempted post once turning the captcha off in the store preferences.
If I turn captcha back on, it forces the redirect even when posting via AJAX thus defeating the point.
What's even more crazy is with captcha turned off, the default contact form (separate template) still issues the captcha. What's the point?
This would makes sense though that while I have captcha turned off in the store I am still getting the HTTP 429 error when attempting to submit via the ajax since it seems clear captcha is still being enforced.
I have not reached out to Shopify yet, but in reading the posts here, it seems like they have no interest in helping fully disable despite the 'settings' being turned off.
EDIT: I can confirm as well that going in over VPN and switching between various locations the 429 status code does not initially occur. I guess it is just a matter of what the "rate limit" is, but I feel like there is too much risk in "hoping" this won't be encountered in a production environment.
Ohhh, I see what could've been confusing on your end. I was capturing the entire form (form-contact.liquid) to a variable and changing the "action" to a "data-action" to try to trick bots. We have a custom captcha that flips that back after it's validated.
I've switched form-contact.liquid to use more standard form properties. Also swapped out some of the inside of it with our latest code.