`;
const $forms = Array.from(document.querySelectorAll('form:not([data-cf-state])'));
const entrypoints = [];
for (let $form of $forms) {
if (isIgnored($form)) continue;
const id = getFormId($form);
if (!id) continue;
const formData = forms.find(form => form.id === id);
if (!formData) {
console.error(`[Customer Fields] Unable to find form data with id ${id}`);
setFormState($form, 'failed');
continue;
}
// Do not try to mount the same form element more than once,
// otherwise failures are much harder to handle.
if (isDetected($form)) continue;
markAsDetected($form);
const $originalForm = $form.cloneNode(true);
// Shopify's captcha script can bind to the form that CF mounted to.
// Their submit handler eventually calls the submit method after generating
// the captcha response token, causing native submission behavior to occur.
// We do not want this, so we override it to a no-op. See #2092
$form.submit = () => {};
injectReactTarget($form);
setFormState($form, 'loading');
const entrypoint = {
$form,
registration: isRegistrationForm($form),
formId: formData.id,
updatedAt: formData.updated_at,
originalForm: $originalForm,
version: formData.version,
restore: () => restoreEntrypoint(entrypoint),
};
entrypoints.push(entrypoint);
// Required to be backwards compatible with older versions of the JS Form API, and prevent Shopify captcha
$form.setAttribute('data-cf-form', formData.id);
$form.setAttribute('action', '');
}
if ($preInitStyles && $preInitStyles.parentElement) {
$preInitStyles.parentElement.removeChild($preInitStyles);
}
if (!entrypoints.length) return;
initializeEmbedScript();
function initializeEmbedScript() {
if (!window.CF.requestedEmbedJS) {
const $script = document.createElement('script');
$script.src = getAssetUrl('customer-fields.js');
document.head.appendChild($script);
window.CF.requestedEmbedJS = true;
}
if (!window.CF.requestedEmbedCSS) {
const $link = document.createElement('link');
$link.href = getAssetUrl('customer-fields.css');
$link.rel = 'stylesheet';
$link.type = 'text/css';
document.head.appendChild($link);
window.CF.requestedEmbedCSS = true;
}
}
const uniqueEntrypoints = entrypoints.reduce((acc, entrypoint) => {
if (acc.some(e => e.formId === entrypoint.formId)) return acc;
acc.push(entrypoint);
return acc;
}, []);
const fullForms = await Promise.all(uniqueEntrypoints.map(e => getFormData(e.formId, e.updatedAt)));
fullForms.forEach((fullForm, index) => {
// Could be a failed request.
if (!fullForm) return;
const invalidFormTargets = ['customer-account'];
if (invalidFormTargets.includes(fullForm.form.target)) {
console.error('[Customer Fields] Invalid form target', fullForm);
return;
}
entrypoints
.filter(e => e.formId === fullForm.form.id)
.forEach(entrypoint => {
entrypoint.form = {
...fullForm.form,
currentRevision: fullForm.revision,
};
})
});
entrypoints.forEach(e => {
if (!e.form) {
// Form can be null if the request failed one way or another.
restoreEntrypoint(e);
return;
}
})
if (window.CF.entrypoints) {
window.CF.entrypoints.push(...entrypoints);
if (window.CF.mountForm) {
entrypoints.forEach(entrypoint => {
if (!entrypoint.form) return;
window.CF.mountForm(entrypoint.form);
});
}
} else {
window.CF.entrypoints = entrypoints;
// The Core class has some logic that gets invoked as a result of this event
// that we only want to fire once, so let's not emit this event multiple times.
document.dispatchEvent(new CustomEvent('cf:entrypoints_ready'));
}
function getFormData(formId, updatedAt) {
return new Promise(resolve => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FORM_DATA_TIMEOUT);
const maxAttempts = 3;
let attempts = 0;
const attemptFetch = () => {
if (controller.signal.aborted) {
resolve(null);
return;
}
attempts++;
fetch(`https://app.customerfields.com/embed_api/v4/forms/${formId}.json?v=${updatedAt}`, {
headers: {
'X-Shopify-Shop-Domain': "bydeeau.myshopify.com"
},
signal: controller.signal
}).then(response => {
if (controller.signal.aborted) {
resolve(null);
return;
}
if (response.ok) {
response.json().then(resolve);
return;
}
if (attempts < maxAttempts) {
pause(2000).then(() => attemptFetch());
return;
}
console.error(`[Customer Fields] Received non-OK response from the back-end when fetching form ${formId}`)
resolve(null);
}).catch((err) => {
if (controller.signal.aborted) {
resolve(null);
return;
}
if (attempts < maxAttempts) {
pause(2000).then(() => attemptFetch());
return;
}
console.error(`[Customer Fields] Encountered unknown error while fetching form ${formId}`, err);
resolve(null);
});
};
attemptFetch();
});
}
function restoreEntrypoint(entrypoint) {
// This has a side effect of removing the Form class' submit handlers.
// Previously this only replaced the original children within the form, but the submit event
// was still being handled by our script.
entrypoint.$form.replaceWith(entrypoint.originalForm);
// After a form has been restored, make sure we don't touch it again.
// Otherwise we might treat it as an "async mounted" entrypoint and try to mount it again
entrypoint.originalForm.setAttribute('data-cf-ignore', 'true');
// Opacity was set to 0 with the #cf-pre-init-styles element
entrypoint.$form.style.opacity = 1;
console.error(`[Customer Fields] Encountered an issue while mounting form, reverting to original form contents.`, entrypoint);
}
function getAssetUrl(filename) {
// We changed this to always get the latest embed assets
// 4.15.7 included a crucial hotfix for recaptcha, see #2028
return `https://static.customerfields.com/releases/${latestEmbedVersion}/${filename}`;
}
function injectReactTarget($form) {
const containsReactTarget = !!$form.querySelector('.cf-react-target');
if (containsReactTarget) return;
$form.innerHTML = reactTarget;
}
function isIgnored($form) {
return $form.getAttribute('data-cf-ignore') === 'true';
}
function isDetected($form) {
return $form.__cfDetected === true;
}
function markAsDetected($form) {
$form.__cfDetected = true;
}
function isEditAccountForm($form) {
return $form.getAttribute('data-cf-edit-account') === 'true';
}
function isVintageRegistrationForm($form) {
return (
window.location.pathname.includes('/account/register')
&& $form.id === 'create_customer'
&& !!$form.getAttribute('data-cf-form')
);
}
function isRegistrationForm($form) {
try {
const isWithinAppBlock = !!$form.closest('.cf-form-block');
if (isWithinAppBlock) return false;
const action = $form.getAttribute('action');
if (!action) return false;
const formActionUrl = new URL(action, window.location.origin);
const hasAccountPath = formActionUrl.pathname.endsWith('/account');
const matchesShopDomain = formActionUrl.host === window.location.host;
const hasPostMethod = $form.method.toLowerCase() === 'post';
const $formTypeInput = $form.querySelector('[name="form_type"]')
const hasCreateCustomerFormType = $formTypeInput && $formTypeInput.value === 'create_customer';
return (matchesShopDomain && hasAccountPath && hasPostMethod) || hasCreateCustomerFormType
} catch (err) {
return false;
}
}
function mountTextEntrypoints() {
const tree = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, (node) => {
if (typeof node.data !== 'string' || !node.data) return NodeFilter.FILTER_REJECT;
return node.data.includes('data-cf-form="') ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
});
/**
* Walks through every text node on the document that contains 'data-cf-form="' and attempts to
* splice a form element in place of every shortcode.
*
* @type Node[]
*/
while (tree.nextNode()) {
let node = tree.currentNode;
const parser = new DOMParser();
while (entrypointContent = node.data.match(/.*<\/form>/)) {
const [match] = entrypointContent;
const doc = parser.parseFromString(match, 'text/html');
const $form = doc.body.firstElementChild;
// Substring is better than split here in case the text node contains multiple forms.
const beforeText = node.data.substring(0, node.data.indexOf(match));
const afterText = node.data.substring(node.data.indexOf(match) + match.length);
node.replaceWith($form);
node.data = node.data.replace(match, '');
if (beforeText) $form.insertAdjacentText('beforebegin', beforeText);
if (afterText) {
$form.insertAdjacentText('afterend', afterText);
// Continue scanning the rest of the node text in case there are more forms
node = $form.nextSibling;
}
}
}
}
function getFormId($form) {
const currentFormId = $form.getAttribute('data-cf-form');
let id;
if (isEditAccountForm($form)) {
id = "";
} else if (isVintageRegistrationForm($form) || isRegistrationForm($form)) {
id = "";
}
return id || currentFormId;
}
function setFormState($form, state) {
$form.setAttribute('data-cf-state', state);
}
}
function onFallbackTemplate() {
const params = new URLSearchParams(window.location.search);
return location.pathname.includes('/account/register') && params.get('view') === 'orig';
}
function injectHiddenForms() {
if (!devToolsEnabled && !CF.entrypoints?.length) return;
if (document.querySelector('#cf_hidden_forms')) return;
const container = document.createElement('div');
container.id = "cf_hidden_forms";
container.style.display = 'none';
container.setAttribute('aria-hidden', 'true');
document.body.appendChild(container);
const loginForm = createLoginForm();
const recoverForm = createRecoverPasswordForm();
container.appendChild(loginForm);
container.appendChild(recoverForm);
if (window.Shopify.captcha) {
// Only applicable for grecaptcha shops, but also safe for hcaptcha
triggerShopifyRecaptchaLoad(container);
window.Shopify.captcha.protect(loginForm);
window.Shopify.captcha.protect(recoverForm);
}
}
function triggerShopifyRecaptchaLoad(container) {
if (document.getElementById('cf-hidden-recaptcha-trigger__create_customer')) return;
if (document.getElementById('cf-hidden-recaptcha-trigger__contact')) return;
// Triggering a focus event on a form causes Shopify to load their recaptcha script.
// This allows our Customer class to handle the copying/injecting of `grecaptcha` so we can
// handle multiple `grecaptcha` instances. See methods `injectRecaptchaScript`
// and `captureShopifyGrecaptcha` in `Customer.ts`.
// Note: We have to try both types, in case the merchant has only one of the two recaptcha
// options checked
const $customerRecaptchaForm = createDummyRecaptchaForm('/account', 'create_customer');
container.appendChild($customerRecaptchaForm);
const $contactRecaptchaForm = createDummyRecaptchaForm('/contact', 'contact');
container.appendChild($contactRecaptchaForm);
triggerFocusEvent($customerRecaptchaForm);
triggerFocusEvent($contactRecaptchaForm);
}
function createDummyRecaptchaForm(action, type) {
const dummyRecaptchaForm = document.createElement('form');
dummyRecaptchaForm.action = action;
dummyRecaptchaForm.method = "post";
dummyRecaptchaForm.id = `cf-hidden-recaptcha-trigger__${type}`;
dummyRecaptchaForm.setAttribute('data-cf-ignore', 'true');
dummyRecaptchaForm.setAttribute('aria-hidden', 'true');
dummyRecaptchaForm.style.display = 'none';
const formTypeInput = document.createElement('input');
formTypeInput.name = "form_type"
formTypeInput.setAttribute('value', type);
dummyRecaptchaForm.appendChild(formTypeInput);
return dummyRecaptchaForm;
}
function triggerFocusEvent(element) {
const event = new Event('focusin', { bubbles: true, cancelable: false });
element.dispatchEvent(event);
}
function createLoginForm() {
const form = createDummyRecaptchaForm('/account/login', 'customer_login');
const email = document.createElement('input');
email.name = 'customer[email]';
const password = document.createElement('input');
password.name = 'customer[password]';
const redirect = document.createElement('input');
redirect.name = 'return_to';
form.appendChild(email);
form.appendChild(password);
form.appendChild(redirect);
form.setAttribute('aria-hidden', 'true');
return form;
}
function createRecoverPasswordForm() {
const parser = new DOMParser();
const result = parser.parseFromString(``, 'text/html');
const form = result.querySelector('form');
form.setAttribute('aria-hidden', 'true');
form.id = "cf_recover_password_form";
return form;
}
function pause(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
})();
-->