-
Notifications
You must be signed in to change notification settings - Fork 41
Improve support for conditional Square payments #2594
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
8df9923
abd1b99
ffe463f
292d64a
880260d
47f7574
f94f0ef
5808b8c
eb724db
39e6e04
d611ed3
13351f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 } ); | ||
|
|
@@ -60,6 +67,12 @@ | |
| if ( squareCardElementIsComplete ) { | ||
| enableSubmit(); | ||
| } else { | ||
| if ( squareIsConditionallyDisabled( thisForm ) ) { | ||
| running = 0; | ||
| enableSubmit(); | ||
| return; | ||
| } | ||
|
|
||
| disableSubmit( thisForm ); | ||
| } | ||
| } | ||
|
|
@@ -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' ); | ||
| 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. | ||
| */ | ||
|
|
@@ -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(); | ||
|
|
||
|
|
@@ -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 | ||
| * | ||
| * @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; | ||
| } | ||
|
|
||
| /** | ||
| * 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"]` ); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| /** | ||
| * 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' ); | ||
|
|
||
There was a problem hiding this comment.
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
__FRMRULESor the specificsubmit_${formId}rules object is not defined, accessing.hideDisablewill throw aTypeErrorand 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