( function() { if ( ! window.frmSquareVars ) { return; } const { appId } = frmSquareVars; const { locationId } = frmSquareVars; // Track the state of the Square card element let squareCardElementIsComplete = false; let thisForm = null; let running = 0; let cardGlobal; const buyerTokens = {}; // Track the state of each field in the card form. // postalCode is not included by default because Square hides it // for cards issued in countries that don't require it (e.g., Australia). const cardFields = { cardNumber: false, expirationDate: false, cvv: false }; async function initializeCard( payments ) { const cardElement = document.querySelector( '.frm-card-element' ); if ( ! cardElement ) { return; } const card = await payments.card(); const cardStyle = frmSquareVars.style; // Never attach while the card element is hidden (e.g. by conditional // logic). Square measures the container on attach, and a hidden // container measures as zero-size, so the card form renders with the // wrong height. Wait until the element is visible before attaching. await waitForVisibleCardElement( cardElement ); await card.attach( '.frm-card-element' ); card.configure( { style: cardStyle } ); // Track when the postal code field is rendered by Square. // Square hides the postal code for cards issued in certain countries. card.addEventListener( 'focusClassAdded', event => { const { field } = event.detail; if ( field === 'postalCode' ) { cardFields.postalCode = event.detail.currentState.isCompletelyValid; } } ); // Add event listener to track when the card form is valid card.addEventListener( 'focusClassRemoved', event => { const { field } = event.detail; const value = event.detail.currentState.isCompletelyValid; cardFields[ field ] = value; // Check if all fields are valid squareCardElementIsComplete = Object.values( cardFields ).every( item => item === true ); // Update form submit button based on form validity if ( thisForm ) { if ( squareCardElementIsComplete ) { enableSubmit(); } else { if ( squareIsConditionallyDisabled( thisForm ) ) { running = 0; enableSubmit(); return; } disableSubmit( thisForm ); } } } ); /** * Enable the submit button when postal code is completed. * This is the best we can do for now as Square does not provide * any way of knowing that the credit card was auto-filled. */ card.addEventListener( 'postalCodeChanged', function( event ) { if ( event.detail.currentState.isCompletelyValid ) { cardFields.cardNumber = true; cardFields.expirationDate = true; cardFields.cvv = true; cardFields.postalCode = true; enableSubmit(); } else { cardFields.postalCode = false; squareCardElementIsComplete = false; disableSubmit( thisForm ); } } ); return card; } /** * Resolve once the card element is visible (has a layout box). * A width of zero means the element or one of its ancestors is hidden, * usually by conditional logic setting display: none. * * @since 6.34 * * @param {HTMLElement} cardElement * @return {Promise} */ function waitForVisibleCardElement( cardElement ) { return new Promise( resolve => { if ( cardElement.getBoundingClientRect().width > 0 ) { resolve(); return; } const form = cardElement.closest( 'form' ); const observer = new MutationObserver( () => { if ( cardElement.getBoundingClientRect().width > 0 ) { observer.disconnect(); resolve(); } } ); // Conditional logic toggles inline styles on field and section // containers, so watch the whole form for attribute changes and // re-check the card element's visibility on each change. observer.observe( form || document.body, { attributes: true, attributeFilter: [ 'style', 'class' ], subtree: true } ); } ); } /** * Check if a Square card element is conditionally hidden. * If it is, we should not be disabling the submit button. * * @since 6.34 * * @param {HTMLElement} form * * @return {boolean} True if the field is conditionally hidden, false otherwise. */ function squareIsConditionallyDisabled( form ) { const fieldContainer = getPaymentElementFieldContainer( form ); if ( ! fieldContainer ) { return false; } // Field is conditionally hidden. if ( 'none' === fieldContainer.style.display ) { return true; } // Section parent is conditionally hidden. const parentSection = fieldContainer.closest( '.frm_section_heading' ); return parentSection && 'none' === parentSection.style.display; } /** * Try to get the field container for a Square card payment element. * The field container is checked to determine if the field is conditionally hidden or not. * * @param {HTMLElement} form * * @return {HTMLElement|null} The field container element or null if not found. */ function getPaymentElementFieldContainer( form ) { const paymentElement = form.querySelector( '.frm-card-element' ); if ( ! paymentElement ) { return null; } return paymentElement.parentElement.closest( '.frm_form_field' ); } /** * Enable the submit button for the form. * * Square being ready is only half of what enables the button. When the submit * button has conditional logic of its own that is not satisfied, it stays * disabled no matter what state the card is in, so every caller is checked * here rather than at the individual call sites. */ function enableSubmit() { if ( running > 0 ) { return; } if ( submitButtonIsConditionallyDisabled( getFormIdForForm( thisForm ) ) ) { return; } thisForm.classList.add( 'frm_loading_form' ); frmFrontForm.removeSubmitLoading( jQuery( thisForm ), 'enable', 0 ); // Trigger custom event for other scripts to hook into const event = new CustomEvent( 'frmSquareLiteEnableSubmit', { detail: { form: thisForm } } ); document.dispatchEvent( event ); } /** * Disable submit button for a target form. * * @param {Element} form * @return {void} */ function disableSubmit( form ) { jQuery( form ).find( 'input[type="submit"],input[type="button"],button[type="submit"]' ).not( '.frm_prev_page' ).attr( 'disabled', 'disabled' ); // Trigger custom event for other scripts to hook into const event = new CustomEvent( 'frmSquareLiteDisableSubmit', { detail: { form } } ); document.dispatchEvent( event ); } async function createPayment( event, token, verificationToken ) { const tokenInput = document.createElement( 'input' ); tokenInput.type = 'hidden'; tokenInput.value = token; tokenInput.setAttribute( 'name', 'square-token' ); const verificationInput = document.createElement( 'input' ); verificationInput.type = 'hidden'; verificationInput.value = verificationToken; verificationInput.setAttribute( 'name', 'square-verification-token' ); // Use the thisForm variable that we set earlier if ( thisForm ) { thisForm.append( tokenInput ); thisForm.append( verificationInput ); if ( typeof frmFrontForm.submitFormManual === 'function' ) { frmFrontForm.submitFormManual( event, thisForm ); } else { // Fallback if submitFormManual is not available thisForm.submit(); } } } async function tokenize( paymentMethod ) { const tokenResult = await paymentMethod.tokenize(); if ( tokenResult.status === 'OK' ) { return tokenResult.token; } let errorMessage = `Tokenization failed with status: ${ tokenResult.status }`; if ( tokenResult.errors ) { errorMessage += ` and errors: ${ JSON.stringify( tokenResult.errors ) }`; } throw new Error( errorMessage ); } // Required in SCA Mandated Regions: Learn more at https://developer.squareup.com/docs/sca-overview async function verifyBuyer( payments, token ) { const formData = new FormData( thisForm ); formData.append( 'action', 'frm_verify_buyer' ); formData.append( 'nonce', frmSquareVars.nonce ); // Remove a few fields so form validation does not incorrectly trigger. formData.delete( 'frm_action' ); formData.delete( 'form_key' ); formData.delete( 'item_key' ); const response = await fetch( frmSquareVars.ajax, { method: 'POST', body: formData } ); if ( ! response.ok ) { throw new Error( 'Failed to verify buyer' ); } const verificationData = await response.json(); if ( ! verificationData.success ) { throw new Error( verificationData.data ); } if ( buyerTokens[ verificationData.data.hash ] ) { // Avoid a second verify buyer request if the verification data has not changed. return buyerTokens[ verificationData.data.hash ]; } const { verificationDetails } = verificationData.data; const verificationResults = await payments.verifyBuyer( token, verificationDetails ); buyerTokens[ verificationData.data.hash ] = verificationResults.token; return verificationResults.token; } /** * Display an error message in the payment form. * * @param {string} errorMessage * @return {void} */ function displayPaymentFailure( errorMessage ) { if ( ! thisForm ) { return; } const statusContainer = thisForm.querySelector( '.frm-card-errors' ); if ( statusContainer ) { statusContainer.textContent = errorMessage; } } async function squareInit() { // Find the form containing the Square payment element const cardContainer = document.querySelector( '.frm-card-element' ); if ( cardContainer ) { thisForm = cardContainer.closest( 'form' ); if ( thisForm ) { listenForFieldMutations( thisForm ); listenForSubmitButtonMutations( thisForm ); if ( ! squareIsConditionallyDisabled( thisForm ) ) { // Initially disable the submit button until card is valid disableSubmit( thisForm ); } // Add event listener for form submission thisForm.addEventListener( 'submit', function( event ) { if ( squareIsConditionallyDisabled( thisForm ) ) { return; } event.preventDefault(); event.stopPropagation(); if ( ! squareCardElementIsComplete ) { const statusContainer = thisForm.querySelector( '.frm-card-errors' ); if ( statusContainer ) { statusContainer.textContent = 'Please complete all card details before submitting.'; } } else { handlePaymentMethodSubmission( event, cardGlobal ); } return false; } ); } } let payments; try { // Square requires HTTPS to work. payments = window.Square.payments( appId, locationId ); } catch ( e ) { const statusContainer = document.querySelector( '.frm-card-errors' ); statusContainer.classList.add( 'missing-credentials', 'frm_error' ); statusContainer.style.visibility = 'visible'; statusContainer.textContent = e.message; return; } let card; try { card = await initializeCard( payments ); } catch ( e ) { console.error( 'Initializing Card failed', e ); return; } cardGlobal = card; /** * @param {Object} $form * @return {boolean} false if there are errors. */ function validateFormSubmit( $form ) { const errors = frmFrontForm.validateFormSubmit( $form ); const keys = Object.keys( errors ); if ( 1 === keys.length && errors[ keys[ 0 ] ] === '' ) { // Pop the empty error that gets added by invisible recaptcha. keys.pop(); } return 0 === keys.length; } async function handlePaymentMethodSubmission( event, card ) { try { thisForm.classList.add( 'frm_js_validate' ); if ( ! validateFormSubmit( thisForm ) ) { return; } // Increment running counter and disable the submit button running++; if ( thisForm ) { disableSubmit( thisForm ); } const token = await tokenize( card ); const verificationToken = await verifyBuyer( payments, token ); await createPayment( event, token, verificationToken ); // Decrement running counter after successful payment running--; if ( running === 0 && thisForm ) { enableSubmit(); } } catch ( e ) { // Decrement running counter and re-enable submit if appropriate running--; if ( running === 0 && thisForm && squareCardElementIsComplete ) { enableSubmit(); } displayPaymentFailure( e.message ); } } } /** * Possibly toggle on and off the submit button when a Stripe Link payment field is conditionally shown or hidden. * * @since 6.34 * * @param {HTMLElement} form * @return {void} */ function listenForFieldMutations( form ) { const fieldContainer = getPaymentElementFieldContainer( form ); if ( ! fieldContainer ) { return; } observeAttributeMutations( fieldContainer, handleMutation ); const section = fieldContainer.closest( '.frm_section_heading' ); if ( section ) { observeAttributeMutations( section, handleMutation ); } /** * Handle a style attribute change for either a payment field container * or the field container of its parent section. * * @param {MutationRecord} mutation * @return {void} */ function handleMutation( mutation ) { if ( mutation.attributeName !== 'style' ) { return; } const isFieldVisible = 'none' !== mutation.target.style.display; // If field is hidden, enable submit (field is conditionally not required) if ( ! isFieldVisible ) { thisForm = form; running = 0; enableSubmit(); return; } // Field is now visible, recalculate size and check validation if ( cardGlobal ) { cardGlobal.recalculateSize(); } const shouldEnable = squareCardElementIsComplete || squareIsConditionallyDisabled( form ); if ( ! shouldEnable ) { disableSubmit( form ); return; } thisForm = form; running = 0; enableSubmit(); } } /** * Keep the submit button disabled while the Square card is not ready. * * Conditional logic on the submit button enables it as soon as its own * conditions are met, with no knowledge of the payment field. Watch for that * and disable it again until the card details are complete. * * @since 6.34 * * @param {HTMLElement} form * @return {void} */ function listenForSubmitButtonMutations( form ) { const submitButton = form.querySelector( '.frm_final_submit' ); if ( ! submitButton ) { return; } observeAttributeMutations( submitButton, mutation => { if ( mutation.attributeName !== 'disabled' || submitButton.disabled ) { return; } if ( squareCardElementIsComplete || squareIsConditionallyDisabled( form ) ) { // Either the card is filled in or it is conditionally hidden, so // there is nothing left for Square to wait on. return; } disableSubmit( form ); } ); } /** * @param {HTMLElement} element * @param {Function} mutationHandler * * @return {void} */ function observeAttributeMutations( element, mutationHandler ) { const observer = new MutationObserver( mutations => { mutations.forEach( mutationHandler ); } ); observer.observe( element, { attributes: true } ); } /** * Check if the submit button is conditionally disabled. * This is required for Stripe link so the button does not get enabled at the wrong time after completing the Stripe elements. * * @since 6.34 * * @param {string} formId * * @return {boolean} True if the submit button is conditionally disabled, false otherwise. */ function submitButtonIsConditionallyDisabled( formId ) { if ( ! submitButtonIsConditionallyNotAvailable( formId ) ) { return false; } // __FRMRULES is only defined when conditional logic is on the page. const submitRules = typeof __FRMRULES === 'undefined' ? undefined : __FRMRULES[ `submit_${ formId }` ]; return Boolean( submitRules ) && 'disable' === submitRules.hideDisable; } /** * Check submit button is conditionally "hidden". This is also used for the enabled check and is used in submitButtonIsConditionallyDisabled. * * @since 6.34 * * @param {string} formId * * @return {boolean} True if the submit button is conditionally not available, false otherwise. */ function submitButtonIsConditionallyNotAvailable( formId ) { const hideFields = document.getElementById( `frm_hide_fields_${ formId }` ); if ( ! hideFields ) { return false; } // The value is a JSON array of every container conditional logic has // hidden, for example ["frm_field_25_container","frm_form_16_container // .frm_final_submit"], so match the quoted entry anywhere in it. Matching // the array brackets too would only find the submit button when it is the // single hidden entry, and it never is once the payment field has // conditional logic of its own. return hideFields.value.includes( `"frm_form_${ formId }_container .frm_final_submit"` ); } /** * Check a form's form_id input for a form ID value. * * @param {HTMLElement} form * * @return {number} The form ID. */ function getFormIdForForm( form ) { return parseInt( form.querySelector( '[name="form_id"]' ).value ); } document.addEventListener( 'DOMContentLoaded', async function() { if ( ! window.Square ) { console.error( 'Square.js failed to load properly' ); return; } squareInit(); jQuery( document ).on( 'frmPageChanged', function() { squareInit(); } ); } ); }() );