Skip to content
226 changes: 224 additions & 2 deletions square/js/frontend.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@

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 } );
Expand Down Expand Up @@ -60,6 +67,12 @@
if ( squareCardElementIsComplete ) {
enableSubmit();
} else {
if ( squareIsConditionallyDisabled( thisForm ) ) {
running = 0;
enableSubmit();
return;
}

disableSubmit( thisForm );
}
}
Expand Down Expand Up @@ -87,6 +100,84 @@
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 x.x
*
* @param {HTMLElement} cardElement
* @return {Promise<void>}
*/
function waitForVisibleCardElement( cardElement ) {
return new Promise( resolve => {
if ( cardElement.getBoundingClientRect().width > 0 ) {
resolve();
return;
}

const form = cardElement.closest( 'form' );

Check failure on line 120 in square/js/frontend.js

View workflow job for this annotation

GitHub Actions / Run ESLint

Multiple spaces found before '='
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 x.x
*
* @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.
*/
Expand Down Expand Up @@ -222,11 +313,19 @@
if ( cardContainer ) {
thisForm = cardContainer.closest( 'form' );
if ( thisForm ) {
// Initially disable the submit button until card is valid
disableSubmit( thisForm );
listenForFieldMutations( 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();

Expand Down Expand Up @@ -316,6 +415,129 @@
}
}

/**
* Possibly toggle on and off the submit button when a Stripe Link payment field is conditionally shown or hidden.
*
* @since x.x
*
* @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 );
}

const formId = getFormIdForForm( form );

/**
* 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;
}

if ( submitButtonIsConditionallyDisabled( formId ) ) {
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();
}
}

/**
* @param {HTMLElement} element
* @param {function} mutationHandler

Check failure on line 486 in square/js/frontend.js

View workflow job for this annotation

GitHub Actions / Run ESLint

Syntax error in type: function
*
* @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 x.x
*
* @param {string} formId
*
* @return {boolean} True if the submit button is conditionally disabled, false otherwise.
*/
function submitButtonIsConditionallyDisabled( formId ) {
return submitButtonIsConditionallyNotAvailable( formId ) && 'disable' === __FRMRULES[ `submit_${ formId }` ].hideDisable;
}
Comment on lines +512 to +514

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Prevent TypeError if form rules are undefined.

If a form is evaluated but __FRMRULES or the specific submit_${formId} rules object is not defined, accessing .hideDisable will throw a TypeError and crash the script. Use optional chaining to safely check for this property.

🛡️ Proposed fix
 	function submitButtonIsConditionallyDisabled( formId ) {
-		return submitButtonIsConditionallyNotAvailable( formId ) && 'disable' === __FRMRULES[ `submit_${ formId }` ].hideDisable;
+		return submitButtonIsConditionallyNotAvailable( formId ) && 'disable' === window.__FRMRULES?.[ `submit_${ formId }` ]?.hideDisable;
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@square/js/frontend.js` around lines 456 - 458, Update
submitButtonIsConditionallyDisabled to use optional chaining when accessing
__FRMRULES[ `submit_${ formId }` ].hideDisable, so missing global or
form-specific rules safely yield an undefined property result without throwing.


/**
* Check submit button is conditionally "hidden". This is also used for the enabled check and is used in submitButtonIsConditionallyDisabled.
*
* @since x.x
*
* @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 }` );
return hideFields && hideFields.value.includes( `["frm_form_${ formId }_container .frm_final_submit"]` );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer using an optional chain expression instead, as it's more concise and easier to read


The optional chaining operator can be used to perform null checks before accessing a property, or calling a function.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer using an optional chain expression instead, as it's more concise and easier to read


The optional chaining operator can be used to perform null checks before accessing a property, or calling a function.

}

/**
* 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' );
Expand Down
Loading