Snapshot: MLS sync fixes, image refresh, plugin/theme updates

MLS plugin fixes from this session:
- Fix silent insert failures: location column NOT NULL was rejecting wpdb->insert calls,
  causing ~18k new properties since Dec 2025 to be lost. Inserts now build raw SQL
  with ST_PointFromText so the spatial column is populated atomically.
- Auto-refresh expired media URLs in MLS_Media_Handler::fetch_and_cache(), guarded by
  a property-level GET_LOCK so concurrent fetches share one API refresh.
- Normalize WP_Error to null in mls_get_property_image() so callers can rely on the
  documented string|null contract.
- Support comma-separated property_type filters in MLS_Query and MLS_Cluster so the
  homepage "View All Commercial" link (?property_type=Commercial+Sale,Land,Farm)
  actually filters correctly.
- Incremental sync now looks back 10 minutes past the latest modification timestamp
  as a safety margin against missed records.
- Smart sync exits silently (info-level, not warning) when a full sync is in progress.

Operational:
- New cron: weekly full sync Sundays at 3 AM (/usr/local/bin/mls-full-sync).
- New cron: hourly 2GB cap on mls-thumbnails/ and cache/transformed-images/
  (/usr/local/bin/mls-image-cache-cap).
- Logrotate config for wp-content/debug.log (2-day retention, daily rotation,
  delaycompress).

Repo policy:
- CLAUDE.md updated with explicit "commit everything except build artifacts" policy.
- .gitignore: untrack runtime image caches and debug.log rotations.

Other modifications in this snapshot are pre-existing in-flight theme/plugin/db_content_updates work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-04-29 15:32:23 +00:00
parent 57b752f54e
commit b6df4dbb92
5385 changed files with 838580 additions and 2416 deletions
@@ -0,0 +1,416 @@
/* global wpforms_builder, WPFormsBuilderPaymentsUtils */
/**
* WPForms Square builder function.
*
* @since 1.9.5
*/
const WPFormsBuilderSquare = window.WPFormsBuilderSquare || ( function( document, window, $ ) {
/**
* Elements holder.
*
* @since 1.9.5
*
* @type {Object}
*/
const el = {};
/**
* Public functions and properties.
*
* @since 1.9.5
*
* @type {Object}
*/
const app = {
/**
* Start the engine.
*
* @since 1.9.5
*/
init() {
$( app.ready );
},
/**
* Initialized once the DOM and Providers are fully loaded.
*
* @since 1.9.5
*/
ready() {
// Cache DOM elements.
el.$singlePaymentControl = $( '#wpforms-panel-field-square-enable_one_time' );
el.$recurringPaymentControl = $( '#wpforms-panel-field-square-enable_recurring' );
el.$panelContent = $( '#wpforms-panel-content-section-payment-square' );
el.$AJAXSubmitOption = $( '#wpforms-panel-field-settings-ajax_submit' );
el.$cardButton = $( '#wpforms-add-fields-square' );
el.$alert = $( '#wpforms-square-credit-card-alert' );
el.$feeNotice = $( '.wpforms-square-notice-info' );
app.bindUIActions();
app.bindPlanUIActions();
if ( ! wpforms_builder.square_is_pro ) {
const baseSelector = '.wpforms-panel-content-section-square',
toggleInput = `${ baseSelector } .wpforms-panel-content-section-payment-toggle input`,
planNameInput = `${ baseSelector } .wpforms-panel-content-section-payment-plan-name input`;
$( toggleInput ).each( WPFormsBuilderPaymentsUtils.toggleContent );
$( planNameInput ).each( WPFormsBuilderPaymentsUtils.checkPlanName );
$( '#wpforms-panel-payments' )
.on( 'click', toggleInput, WPFormsBuilderPaymentsUtils.toggleContent )
.on( 'click', `${ baseSelector } .wpforms-panel-content-section-payment-plan-head-buttons-toggle`, WPFormsBuilderPaymentsUtils.togglePlan )
.on( 'click', `${ baseSelector } .wpforms-panel-content-section-payment-plan-head-buttons-delete`, WPFormsBuilderPaymentsUtils.deletePlan )
.on( 'input', planNameInput, WPFormsBuilderPaymentsUtils.renamePlan )
.on( 'focusout', planNameInput, WPFormsBuilderPaymentsUtils.checkPlanName );
}
},
/**
* Process various events.
*
* @since 1.9.5
*/
bindUIActions() {
$( document ).on( 'wpformsSaved', app.ajaxRequiredCheck )
.on( 'wpformsSaved', app.paymentsEnabledCheck )
.on( 'wpformsSaved', app.requiredFieldsCheck )
.on( 'wpformsFieldAdd', app.fieldAdded )
.on( 'wpformsFieldDelete', app.fieldDeleted )
.on( 'wpformsPaymentsPlanCreated', app.toggleMultiplePlansWarning )
.on( 'wpformsPaymentsPlanCreated', app.bindPlanUIActions )
.on( 'wpformsPaymentsPlanDeleted', app.toggleMultiplePlansWarning );
el.$cardButton.on( 'click', app.connectionCheck );
},
/**
* Bind plan UI actions.
*
* @since 1.9.5
*/
bindPlanUIActions() {
el.$panelContent.find( '.wpforms-panel-content-section-payment-plan-body .wpforms-panel-field-select select' ).on( 'change', app.resetRequiredPlanFieldError );
},
/**
* Notify user if AJAX submission is not required.
*
* @since 1.9.5
*/
ajaxRequiredCheck() {
if ( ! $( '#wpforms-panel-fields .wpforms-field.wpforms-field-square' ).length ) {
return;
}
if ( app.isAJAXSubmitEnabled() ) {
return;
}
$.alert( {
title: wpforms_builder.heads_up,
content: wpforms_builder.square_ajax_required,
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* Notify user if Square Payments are not enabled.
*
* @since 1.9.5
*/
paymentsEnabledCheck() {
if ( ! $( '#wpforms-panel-fields .wpforms-field.wpforms-field-square' ).length ) {
return;
}
if ( app.isPaymentsEnabled() ) {
return;
}
$.alert( {
title: wpforms_builder.heads_up,
content: wpforms_builder.square_payments_enabled_required,
icon: 'fa fa-exclamation-circle',
type: 'red',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* On form save notify users about required fields.
*
* @since 1.9.5
*/
requiredFieldsCheck() {
if ( ! el.$recurringPaymentControl.is( ':checked' ) || el.$panelContent.hasClass( 'wpforms-hidden' ) ) {
return;
}
let showAlert = false;
el.$panelContent.find( '.wpforms-panel-content-section-payment-plan' ).each( function() {
const $plan = $( this ),
planId = $plan.data( 'plan-id' ),
$emailField = $( `#wpforms-panel-field-square-recurring-${ planId }-customer_email` ),
$nameField = $( `#wpforms-panel-field-square-recurring-${ planId }-customer_name` );
if (
! $emailField.val()
) {
$emailField.addClass( 'wpforms-required-field-error' );
showAlert = true;
}
if (
! $nameField.val()
) {
$nameField.addClass( 'wpforms-required-field-error' );
showAlert = true;
}
} );
if ( ! showAlert ) {
return;
}
let alertMessage = wpforms_builder.square_recurring_payments_fields_required;
if ( ! $( '.wpforms-panel-content-section-square' ).is( ':visible' ) ) {
alertMessage += ' ' + wpforms_builder.square_recurring_payments_fields_settings;
}
$.alert( {
title: wpforms_builder.square_recurring_payments_fields_heading,
content: alertMessage,
icon: 'fa fa-exclamation-circle',
type: 'red',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
onOpen() {
$( '.wpforms-square-settings-redirect' ).on( 'click', app.settingsRedirect );
},
} );
},
/**
* Redirect to the settings tab.
*
* @since 1.9.5
*/
settingsRedirect() {
// Open the Square settings tab.
$( '.wpforms-panel-payments-button' ).trigger( 'click' );
$( '.wpforms-panel-sidebar-section-square' ).trigger( 'click' );
// Scroll to the Stripe settings.
window.location.href = window.location.pathname + window.location.search + '#wpforms-panel-field-square-enable_recurring-wrap';
// Close the alert.
$( this ).closest( '.jconfirm-box' ).find( '.btn-confirm' ).trigger( 'click' );
},
/**
* Maybe reset required recurring field error class.
*
* @since 1.9.5
*/
resetRequiredPlanFieldError() {
const $nameAttr = $( this ).attr( 'name' );
if ( ! $nameAttr.includes( 'customer_email' ) && ! $nameAttr.includes( 'customer_name' ) ) {
return;
}
$( this ).toggleClass( 'wpforms-required-field-error', ! $( this ).val() );
},
// eslint-disable-next-line jsdoc/require-returns-check
/**
* Notify user if Square connection are missing.
*
* @since 1.9.5
*
* @return {boolean} False if button clicks should be prevented.
*/
connectionCheck() {
if ( $( this ).hasClass( 'wpforms-add-fields-button-disabled' ) ) {
return false;
}
if ( ! $( this ).hasClass( 'square-connection-required' ) ) {
return true;
}
$.alert( {
title: wpforms_builder.heads_up,
content: wpforms_builder.square_connection_required,
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* We have to do several actions when the "Square" field is added.
*
* @since 1.9.5
*
* @param {Object} e Event object.
* @param {number} id Field ID.
* @param {string} type Field type.
*/
fieldAdded( e, id, type ) {
if ( type === 'square' ) {
app.cardButtonToggle( true );
app.settingsToggle( true );
app.paymentsEnabledCheck();
el.$feeNotice.toggleClass( 'wpforms-hidden' );
}
},
/**
* We have to do several actions for UI when the "Square" credit card field is deleted.
*
* @since 1.9.5
*
* @param {Object} e Event object.
* @param {number} id Field ID.
* @param {string} type Field type.
*/
fieldDeleted( e, id, type ) {
if ( type === 'square' ) {
app.cardButtonToggle( false );
app.settingsToggle( false );
app.disablePayments();
app.disableNotifications();
el.$feeNotice.toggleClass( 'wpforms-hidden' );
}
},
/**
* Toggles visibility of multiple plans warning.
*
* @since 1.9.5
*/
toggleMultiplePlansWarning() {
el.$panelContent.find( '.wpforms-square-multiple-plans-warning' ).toggleClass( 'wpforms-hidden', el.$panelContent.find( '.wpforms-panel-content-section-payment-plan' ).length === 1 );
},
/**
* Enable or disable the "Square" field in the fields list.
*
* @since 1.9.5
*
* @param {boolean} isDisabled If true then a card button will be disabled.
*/
cardButtonToggle( isDisabled ) {
el.$cardButton
.prop( 'disabled', isDisabled )
.toggleClass( 'wpforms-add-fields-button-disabled', isDisabled );
},
/**
* Toggle visibility of the Square payment settings.
*
* If the "Square" field has been added then reveal the settings,
* otherwise hide them.
*
* @since 1.9.5
*
* @param {boolean} display Show or hide settings.
*/
settingsToggle( display ) {
if ( ! el.$alert.length ) {
return;
}
el.$alert.toggleClass( 'wpforms-hidden', display );
$( '#wpforms-panel-content-section-payment-square' ).toggleClass( 'wpforms-hidden', ! display );
// Uncheck the Payments > Square > Enable Square Payments setting.
if ( ! display ) {
el.$singlePaymentControl.prop( 'checked', false ).trigger( 'change' );
el.$recurringPaymentControl.prop( 'checked', false ).trigger( 'change' );
}
},
/**
* Make sure that "One-Time Payments" and "Recurring Payments" toggles are turned off.
*
* @since 1.9.5
*/
disablePayments() {
const toggleInput = $( '#wpforms-panel-field-square-enable_one_time, #wpforms-panel-field-square-enable_recurring' );
toggleInput.prop( 'checked', false ).trigger( 'change' ).each( WPFormsBuilderPaymentsUtils.toggleContent );
},
/**
* Disable notifications.
*
* @since 1.9.5
*/
disableNotifications() {
const $notificationWrap = $( '.wpforms-panel-content-section-notifications [id*="-square-wrap"]' );
$notificationWrap.find( 'input[id*="-square"]' ).prop( 'checked', false );
$notificationWrap.addClass( 'wpforms-hidden' );
},
/**
* Determine whether payments are enabled in the Payments > Square panel.
*
* @since 1.9.5
*
* @return {boolean} Payments are enabled.
*/
isPaymentsEnabled() {
return el.$singlePaymentControl.is( ':checked' ) || el.$recurringPaymentControl.is( ':checked' );
},
/**
* Determine whether AJAX form submission is enabled in the Settings > General.
*
* @since 1.9.5
*
* @return {boolean} AJAX form submission is enabled.
*/
isAJAXSubmitEnabled() {
return el.$AJAXSubmitOption.is( ':checked' );
},
};
// Provide access to public functions/properties.
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsBuilderSquare.init();
File diff suppressed because one or more lines are too long
@@ -0,0 +1,375 @@
/* global wpforms_admin, WPFormsAdmin, wpf */
/**
* WPForms Square settings function.
*
* @since 1.9.5
*/
const WPFormsSettingsSquare = window.WPFormsSettingsSquare || ( function( document, window, $ ) {
/**
* Elements.
*
* @since 1.9.5
*
* @type {Object}
*/
const $el = {
sandboxModeCheckbox: $( '#wpforms-setting-square-sandbox-mode' ),
sandboxConnectionStatusBlock: $( '#wpforms-setting-row-square-connection-status-sandbox' ),
productionConnectionStatusBlock: $( '#wpforms-setting-row-square-connection-status-production' ),
sandboxLocationBlock: $( '#wpforms-setting-row-square-location-id-sandbox' ),
sandboxLocationStatusBlock: $( '#wpforms-setting-row-square-location-status-sandbox' ),
productionLocationBlock: $( '#wpforms-setting-row-square-location-id-production' ),
productionLocationStatusBlock: $( '#wpforms-setting-row-square-location-status-production' ),
refreshBtn: $( '.wpforms-square-refresh-btn' ),
copyButton: $( '#wpforms-setting-row-square-webhooks-endpoint-set .wpforms-copy-to-clipboard' ),
webhooksEnableCheckbox: $( '#wpforms-setting-square-webhooks-enabled' ),
webhookEndpointUrl: $( 'input#wpforms-square-webhook-endpoint-url' ),
webhookMethod: $( 'input[name="square-webhooks-communication"]' ),
webhookCommunicationStatusNotice: $( '#wpforms-setting-row-square-webhooks-communication-status' ),
webhookConnectBtn: $( '#wpforms-setting-square-webhooks-connect' ),
webhookConnectRow: $( '#wpforms-setting-row-square-webhooks-connect' ),
webhookConnectStatusRow: $( '#wpforms-setting-row-square-webhooks-connect-status-production, #wpforms-setting-row-square-webhooks-connect-status-sandbox' ),
};
/**
* Public functions and properties.
*
* @since 1.9.5
*
* @type {Object}
*/
const app = {
/**
* Start the engine.
*
* @since 1.9.5
*/
init() {
$( app.ready );
},
/**
* Document ready.
*
* @since 1.9.5
*/
ready() {
app.events();
},
/**
* Register JS events.
*
* @since 1.9.5
*/
events() {
$el.sandboxModeCheckbox.on( 'change', app.credentialsFieldsDisplay );
$el.refreshBtn.on( 'click', app.refreshTokensCallback );
$el.webhooksEnableCheckbox.on( 'change', app.webhooksEnableCallback );
$el.webhookConnectBtn.on( 'click', app.modals.displayWebhookConfigPopup );
$el.webhookMethod.on( 'change', app.updateWebhookEndpointUrl );
$el.copyButton.on( 'click', function( e ) {
wpf.copyValueToClipboard( e, $( this ), $el.webhookEndpointUrl );
} );
},
/**
* Update the endpoint URL.
*
* @since 1.9.5
*/
updateWebhookEndpointUrl() {
const checked = $el.webhookMethod.filter( ':checked' ).val(),
newUrl = wpforms_admin.square.webhook_urls[ checked ];
$el.webhookEndpointUrl.val( newUrl );
$el.webhookCommunicationStatusNotice.removeClass( 'wpforms-hide' );
},
/**
* Enable webhooks.
*
* @since 1.9.5
*/
webhooksEnableCallback() {
$el.webhookConnectRow.toggleClass( 'wpforms-hide', ! $( this ).is( ':checked' ) );
$el.webhookConnectStatusRow.toggleClass( 'wpforms-hide', ! $( this ).is( ':checked' ) );
},
/**
* Create a webhook.
*
* @since 1.9.5
*
* @param {string} token Personal access token.
*
* @return {Promise} Promise an object.
*/
createWebhook( token ) {
return new Promise( ( resolve, reject ) => {
$.ajax( {
url: wpforms_admin.ajax_url,
type: 'post',
dataType: 'json',
data: {
action: 'wpforms_square_create_webhook',
nonce: wpforms_admin.nonce,
token,
},
success( response ) {
if ( response.success ) {
resolve( response );
return;
}
reject( response );
},
error() {
reject( { success: false, message: 'An error occurred.' } );
},
} );
} );
},
/**
* Refresh tokens.
*
* @since 1.9.5
*/
refreshTokensCallback() {
const $btn = $( this );
const buttonWidth = $btn.outerWidth();
const buttonLabel = $btn.text();
const settings = {
url: wpforms_admin.ajax_url,
type: 'post',
dataType: 'json',
data: {
action: 'wpforms_square_refresh_connection',
nonce: wpforms_admin.nonce,
mode: $btn.data( 'mode' ),
},
beforeSend() {
$btn.css( 'width', buttonWidth ).html( WPFormsAdmin.settings.iconSpinner ).prop( 'disabled', true );
},
};
let errorMessage = wpforms_admin.square.refresh_error;
// Perform an Ajax request.
$.ajax( settings )
.done( function( response ) {
if ( response.success ) {
$btn
.css( 'pointerEvents', 'none' )
.removeClass( 'wpforms-btn-light-grey' )
.addClass( 'wpforms-btn-grey' )
.html( 'Refreshed!' );
$btn.closest( 'form' ).css( 'cursor', 'wait' );
window.location = $btn.data( 'url' );
return;
}
if (
Object.prototype.hasOwnProperty.call( response, 'data' ) &&
response.data !== ''
) {
errorMessage = response.data;
}
$btn
.css( 'width', 'auto' )
.html( buttonLabel )
.prop( 'disabled', false );
app.modals.refreshTokensError( errorMessage );
} )
.fail( function() {
$btn
.css( 'width', 'auto' )
.html( buttonLabel )
.prop( 'disabled', false );
app.modals.refreshTokensError( errorMessage );
} );
},
/**
* Conditionally show Square mode switch warning.
*
* @since 1.9.5
*/
credentialsFieldsDisplay() {
const sandboxModeEnabled = $el.sandboxModeCheckbox.is( ':checked' );
if ( sandboxModeEnabled ) {
$el.sandboxConnectionStatusBlock.show();
$el.sandboxLocationBlock.show();
$el.sandboxLocationStatusBlock.show();
$el.productionConnectionStatusBlock.hide();
$el.productionLocationBlock.hide();
$el.productionLocationStatusBlock.hide();
} else {
$el.sandboxConnectionStatusBlock.hide();
$el.sandboxLocationBlock.hide();
$el.sandboxLocationStatusBlock.hide();
$el.productionConnectionStatusBlock.show();
$el.productionLocationBlock.show();
$el.productionLocationStatusBlock.show();
}
if ( sandboxModeEnabled && $el.sandboxConnectionStatusBlock.find( '.wpforms-square-connected' ).length ) {
return;
}
if ( ! sandboxModeEnabled && $el.productionConnectionStatusBlock.find( '.wpforms-square-connected' ).length ) {
return;
}
app.modals.modeChangedWarning();
},
/**
* Modals.
*
* @since 1.9.5
*/
modals: {
/**
* Show the warning modal when Square mode is changed.
*
* @since 1.9.5
*/
modeChangedWarning() {
$.alert( {
title: wpforms_admin.heads_up,
content: wpforms_admin.square.mode_update,
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_admin.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* Refresh tokens error handling.
*
* @since 1.9.5
*
* @param {string} error Error message.
*/
refreshTokensError( error ) {
$.alert( {
title: false,
content: error,
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_admin.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* Show popup with the ability to register a new webhook route or retrieve existing one.
*
* @since 1.9.5
*/
// eslint-disable-next-line max-lines-per-function
displayWebhookConfigPopup() {
$.confirm( {
title: wpforms_admin.square.webhook_create_title,
content: wpforms_admin.square.webhook_create_description +
'<input type="text" id="wpforms-square-personal-access-token" placeholder="' + wpforms_admin.square.webhook_token_placeholder + '" value="">' +
'<p class="wpforms-square-webhooks-connect-error error" style="display:none;">' + wpforms_admin.square.token_is_required + '</p>',
icon: 'fa fa-info-circle',
type: 'blue',
buttons: {
confirm: {
text: wpforms_admin.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action() {
const modal = this;
const tokenField = modal.$content.find( '#wpforms-square-personal-access-token' );
const errorMsg = modal.$content.find( '.error' );
const token = tokenField.val().trim();
const title = modal.$title;
// Disable the button to prevent multiple clicks.
$el.webhookConnectBtn.addClass( 'inactive' );
// Reset error message before validation
errorMsg.hide().text( '' );
if ( token === '' ) {
errorMsg.text( wpforms_admin.square.token_is_required ).show();
return false; // Prevent modal from closing.
}
// Show loading indicator.
modal.buttons.confirm.setText( wpforms_admin.loading );
modal.buttons.confirm.disable();
// Call API.
app.createWebhook( token )
.then( ( response ) => {
modal.setContent( '<p>' + response.data.message + '</p>' );
// Hide OK button and rename Cancel to Close.
modal.buttons.confirm.hide();
title.text( '' ).hide();
modal.buttons.cancel.setText( wpforms_admin.close );
// Ensure user can manually close the modal.
modal.buttons.cancel.action = function() {
window.location.reload();
};
} )
.catch( ( responseError ) => {
errorMsg.text( responseError.data.message ).show();
// Re-enable confirm button for retrying.
modal.buttons.confirm.setText( wpforms_admin.ok );
modal.buttons.confirm.enable();
} );
return false; // Prevent modal from closing immediately.
},
},
cancel: {
text: wpforms_admin.cancel,
action() {
// Re-enable the button.
$el.webhookConnectBtn.removeClass( 'inactive' );
this.close();
},
},
},
} );
},
},
};
// Provide access to public functions/properties.
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsSettingsSquare.init();
File diff suppressed because one or more lines are too long
@@ -0,0 +1,807 @@
/* global Square, wpforms, wpforms_settings, wpforms_square, WPForms, WPFormsUtils */
/**
* WPForms Square function.
*
* @since 1.9.5
*/
const WPFormsSquare = window.WPFormsSquare || ( function( document, window, $ ) {
/**
* Holder for original form submit handler.
*
* @since 1.9.5
*
* @type {Function}
*/
let originalSubmitHandler;
/**
* Credit card data.
*
* @since 1.9.5
*
* @type {Object}
*/
const cardData = {
cardNumber: {
empty: true,
valid: false,
},
expirationDate: {
empty: true,
valid: false,
},
cvv: {
empty: true,
valid: false,
},
postalCode: {
empty: true,
valid: false,
},
};
/**
* Public functions and properties.
*
* @since 1.9.5
*
* @type {Object}
*/
const app = {
/**
* Square payments object.
*
* @since 1.9.5
*
* @type {Object}
*/
payments: null,
/**
* Number of page locked to switch.
*
* @since 1.9.5
*
* @type {number}
*/
lockedPageToSwitch: 0,
/**
* Start the engine.
*
* @since 1.9.5
*/
init() {
app.payments = app.getPaymentsInstance();
// Bail if a Square payments object isn't initialized.
if ( app.payments === null ) {
return;
}
$( document )
.on( 'wpformsReady', app.setupForms )
.on( 'wpformsBeforePageChange', app.pageChange )
.on( 'wpformsPageChange', app.afterPageChange )
.on( 'wpformsProcessConditionalsField', app.conditionalLogicHandler );
},
/**
* Setup and configure Square forms.
*
* @since 1.9.5
*/
setupForms() {
if ( typeof $.fn.validate === 'undefined' ) {
return;
}
$( '.wpforms-square form' )
.filter( ( _, form ) => typeof $( form ).data( 'formid' ) === 'number' ) // filter out forms which are locked (formid changed to 'locked-...').
.each( app.updateSubmitHandler );
},
/**
* Update submitHandler for the forms containing Square.
*
* @since 1.9.5
*/
async updateSubmitHandler() {
const $form = $( this );
const validator = $form.data( 'validator' );
if ( ! validator || $form.hasClass( 'wpforms-square-initialization' ) || $form.hasClass( 'wpforms-square-initialized' ) ) {
return;
}
// if the form is inside the "raw" elementor popup, we should not initialize the Square and wait for the popup to be opened.
if ( $form.closest( '.elementor-location-popup' ).length && ! $form.closest( '.elementor-popup-modal' ).length ) {
return;
}
$form.addClass( 'wpforms-square-initialization' );
// Store the original submitHandler.
originalSubmitHandler = validator.settings.submitHandler;
// Replace the default submit handler.
validator.settings.submitHandler = app.submitHandler;
// Get a new Card object.
await app.getCardInstance( $form );
},
/**
* Trigger resize event if Square field has been conditionally unhidden.
*
* Allows Square Card field to resize itself (fixes the issue with doubled field height on some screen resolutions).
*
* @since 1.9.5
*
* @param {Event} e Event.
* @param {number} formID Form ID.
* @param {number} fieldID Field ID.
* @param {boolean} pass Pass condition logic.
* @param {string} action Action name.
*/
conditionalLogicHandler( e, formID, fieldID, pass, action ) {
if ( ! app.isVisibleField( pass, action ) ) {
return;
}
const el = document.getElementById( 'wpforms-' + formID + '-field_' + fieldID );
if ( ! el || ! el.classList.contains( 'wpforms-field-square-cardnumber' ) ) {
return;
}
window.dispatchEvent( new Event( 'resize' ) );
},
/**
* Determine if the field is visible after being triggered by Conditional Logic.
*
* @since 1.9.5
*
* @param {boolean} pass Pass condition logic.
* @param {string} action Action name.
*
* @return {boolean} The field is visible.
*/
isVisibleField( pass, action ) {
return ( action === 'show' && pass ) || ( action === 'hide' && ! pass );
},
/**
* Update submitHandler for forms containing Square.
*
* @since 1.9.5
*
* @param {Object} form JS form element.
*/
submitHandler( form ) {
const $form = $( form );
const validator = $form.data( 'validator' );
const validForm = validator.form();
const card = $form.find( '.wpforms-square-credit-card-hidden-input' ).data( 'square-card' );
if ( ! validForm || typeof card === 'undefined' || ! app.isProcessedCard( $form ) ) {
originalSubmitHandler( $form );
return;
}
app.tokenize( $form, card );
},
/**
* Tokenize a card payment.
*
* @param {jQuery} $form Form element.
* @param {Object} card Square Card object.
*/
async tokenize( $form, card ) {
app.disableSubmitBtn( $form );
const sourceId = await app.getSourceId( $form, card );
if ( sourceId === null ) {
app.enableSubmitBtn( $form );
return;
}
app.submitForm( $form );
},
/**
* Initialize a Square payments object and retrieve it.
*
* @since 1.9.5
*
* @return {Object|null} Square payments object or null.
*/
getPaymentsInstance() {
if ( ! window.Square ) {
app.displaySdkError( $( '.wpforms-square form' ), wpforms_square.i18n.missing_sdk_script );
return null;
}
try {
return Square.payments( wpforms_square.client_id, wpforms_square.location_id );
} catch ( e ) {
const message = ( typeof e === 'object' && Object.prototype.hasOwnProperty.call( e, 'message' ) ) ? e.message : wpforms_square.i18n.missing_creds;
app.displaySdkError( $( '.wpforms-square form' ), message );
return null;
}
},
/**
* Try to retrieve a Square Card object.
*
* @since 1.9.5
*
* @param {jQuery} $form Form element.
*
* @return {Object|null} Square Card object or null.
*/
async getCardInstance( $form ) {
// Applying the modern styles to the card config if needed.
// eslint-disable-next-line prefer-const
let cardConfig = {};
cardConfig.style = wpforms_square.card_config.style ? wpforms_square.card_config.style : app.getModernMarkupCardStyles( $form );
try {
const card = await app.payments.card( cardConfig );
// Attach the Card form to the page.
await card.attach( $form.find( '.wpforms-field-square-cardnumber' ).get( 0 ) );
const eventsList = [ 'focusClassAdded', 'focusClassRemoved' ];
const eventsLength = eventsList.length;
let counter = 0;
// Bind the Card events.
for ( ; counter < eventsLength; counter++ ) {
card.addEventListener( eventsList[ counter ], function( e ) {
// Card field is filled.
cardData[ e.detail.field ].empty = e.detail.currentState.isEmpty;
cardData[ e.detail.field ].valid = e.detail.currentState.isCompletelyValid;
if ( cardData[ e.detail.field ].valid ) {
app.removeFieldError( $form );
}
} );
}
$form.find( '.wpforms-square-credit-card-hidden-input' ).data( 'square-card', card );
$form.removeClass( 'wpforms-square-initialization' );
$form.addClass( 'wpforms-square-initialized' );
return card;
} catch ( e ) {
app.displaySdkError( $form, wpforms_square.i18n.card_init_error );
$form.removeClass( 'wpforms-square-initialization' );
console.log( 'Error:', e ); // eslint-disable-line no-console
console.log( 'Config', cardConfig ); // eslint-disable-line no-console
return null;
}
},
/**
* Retrieve a source ID (card nonce).
*
* @param {jQuery} $form Form element.
* @param {Object} card Square Card object.
*
* @return {string|null} The source ID or null.
*/
async getSourceId( $form, card ) {
try {
const response = await card.tokenize( app.getChargeVerifyBuyerDetails( $form ) );
$form.find( '.wpforms-square-payment-source-id' ).remove();
if ( response.status !== 'OK' || ! response.token ) {
app.displayFormError( app.getCreditCardInput( $form ), app.getResponseError( response ) );
return null;
}
$form.append( '<input type="hidden" name="wpforms[square][source_id]" class="wpforms-square-payment-source-id" value="' + app.escapeTextString( response.token ) + '">' );
return response.token;
} catch ( e ) {
app.displayFormError( app.getCreditCardInput( $form ), wpforms_square.i18n.token_process_fail );
}
return null;
},
/**
* Retrieve a response error message.
*
* @param {Object} response The response received from a tokenization call.
*
* @return {string} The response error message.
*/
getResponseError( response ) {
const hasErrors = response.errors && Array.isArray( response.errors ) && response.errors.length;
return hasErrors ? response.errors[ 0 ].message : wpforms_square.i18n.token_status_error + ' ' + response.status;
},
/**
* Retrieve details about the buyer for a charge.
*
* @since 1.9.5
*
* @param {jQuery} $form Form element.
*
* @return {Object} Buyer details.
*/
getChargeVerifyBuyerDetails( $form ) {
return {
amount: app.getTotalInMinorUnits( wpforms.amountTotalCalc( $form ) ),
billingContact: app.getBillingContactDetails( $form ),
currencyCode: wpforms_settings.currency_code,
intent: 'CHARGE',
customerInitiated: true,
sellerKeyedIn: false,
};
},
/**
* Retrieve the total amount in minor units.
*
* @since 1.9.5
*
* @param {number} total Total amount.
*
* @return {string} Total amount in minor units.
*/
getTotalInMinorUnits( total ) {
return parseInt( wpforms.numberFormat( total, wpforms_settings.currency_decimal, '', '' ), 10 ).toString();
},
/**
* Retrieve billing contact details.
*
* @since 1.9.5
*
* @param {jQuery} $form Form element.
*
* @return {Object} Billing contact details.
*/
getBillingContactDetails( $form ) { // eslint-disable-line complexity
// Get the form ID and billing mapping for this form, if available.
const formId = $form.data( 'formid' );
const mapping = ( wpforms_square.billing_details && wpforms_square.billing_details[ formId ] ) || {};
const result = {};
// Use mapped selectors if provided.
const $emailField = mapping.buyer_email ? $( `.wpforms-field-email[data-field-id="${ mapping.buyer_email }"]` ) : '';
const $nameField = mapping.billing_name ? $( `.wpforms-field-name[data-field-id="${ mapping.billing_name }"]` ) : '';
const $addressField = mapping.billing_address ? $( `.wpforms-field-address[data-field-id="${ mapping.billing_address }"]` ) : '';
if ( $emailField.length ) {
const emailValue = $emailField.find( 'input' ).first().val(); // Use the first input field knowing there could be confirmation email input as well.
if ( emailValue && emailValue.trim() !== '' ) {
result.email = emailValue;
}
}
if ( $nameField.length ) {
jQuery.extend( result, app.getBillingNameDetails( $nameField ) );
}
if ( $addressField.length ) {
jQuery.extend( result, app.getBillingAddressDetails( $addressField ) );
}
return result;
},
/**
* Retrieve billing name details.
*
* @since 1.9.5
*
* @param {jQuery} $field Field element.
*
* @return {Object} Billing name details.
*/
getBillingNameDetails( $field ) { // eslint-disable-line complexity
const result = {};
let givenName = '';
let familyName = '';
// Try to find separate first and last name fields.
const $firstNameField = $field.find( '.wpforms-field-name-first' );
const $lastNameField = $field.find( '.wpforms-field-name-last' );
if ( $firstNameField.length && $lastNameField.length ) {
// Use separate fields if both are present.
givenName = $firstNameField.val() || '';
familyName = $lastNameField.val() || '';
if ( givenName && givenName.trim() !== '' ) {
result.givenName = givenName;
}
if ( familyName && familyName.trim() !== '' ) {
result.familyName = familyName;
}
return result;
}
// Otherwise, fall back to a single name input field.
const $nameField = $field.find( 'input' );
if ( ! $nameField.length ) {
return result;
}
const fullName = $nameField.val().trim();
if ( ! fullName.length ) {
return result;
}
// Split full name by space; the first part is givenName,
// the rest (if any) are combined as familyName.
const nameParts = fullName.split( ' ' );
givenName = nameParts.shift() || '';
familyName = nameParts.join( ' ' ) || '';
if ( givenName && givenName.trim() !== '' ) {
result.givenName = givenName;
}
if ( familyName && familyName.trim() !== '' ) {
result.familyName = familyName;
}
return result;
},
/**
* Retrieve billing address details.
*
* @since 1.9.5
*
* @param {jQuery} $addressField Field element.
*
* @return {Object} Billing address details.
*/
getBillingAddressDetails( $addressField ) { // eslint-disable-line complexity
const result = {};
// For address fields, use the closest wrapper.
const $addressWrapper = $addressField.closest( '.wpforms-field' );
// Retrieve address components, defaulting to empty strings if not found.
const addressLine1 = $addressWrapper.find( '.wpforms-field-address-address1' ).val() || '';
const addressLine2 = $addressWrapper.find( '.wpforms-field-address-address2' ).val() || '';
const city = $addressWrapper.find( '.wpforms-field-address-city' ).val() || '';
const state = $addressWrapper.find( '.wpforms-field-address-state' ).val() || '';
const country = $addressWrapper.find( '.wpforms-field-address-country' ).val() || 'US';
const addressLines = [ addressLine1, addressLine2 ].filter( ( line ) => line && line.trim() !== '' );
if ( addressLines.length ) {
result.addressLines = addressLines;
}
if ( city && city.trim() !== '' ) {
result.city = city;
}
if ( state && state.trim() !== '' ) {
result.state = state;
}
if ( country && country.trim() !== '' ) {
result.countryCode = country;
}
return result;
},
/**
* Retrieve a jQuery selector for Credit Card hidden input.
*
* @since 1.9.5
*
* @param {jQuery} $form Form element.
*
* @return {jQuery} Credit Card hidden input.
*/
getCreditCardInput( $form ) {
return $form.find( '.wpforms-square-credit-card-hidden-input' );
},
/**
* Submit the form using the original submitHandler.
*
* @since 1.9.5
*
* @param {jQuery} $form Form element.
*/
submitForm( $form ) {
const validator = $form.data( 'validator' );
if ( validator ) {
originalSubmitHandler( $form );
}
},
/**
* Determine if a credit card should be processed.
*
* @since 1.9.5
*
* @param {jQuery} $form Form element.
*
* @return {boolean} True if a credit card should be processed.
*/
isProcessedCard( $form ) {
const $squareDiv = $form.find( '.wpforms-field-square-cardnumber' );
const condHidden = $squareDiv.closest( '.wpforms-field-square' ).hasClass( 'wpforms-conditional-hide' );
const ccRequired = !! $squareDiv.data( 'required' );
if ( condHidden ) {
return false;
}
return ccRequired || app.isCardDataNotEmpty();
},
/**
* Determine if card data not empty.
*
* @since 1.9.5
*
* @return {boolean} True if at least one credit card sub-field is filled.
*/
isCardDataNotEmpty() {
return ! cardData.cardNumber.empty || ! cardData.expirationDate.empty || ! cardData.cvv.empty || ! cardData.postalCode.empty;
},
/**
* Determine if card data is completely valid.
*
* @since 1.9.5
*
* @return {boolean} True if at least one credit card sub-field is filled.
*/
isCardDataValid() {
return cardData.cardNumber.valid && cardData.expirationDate.valid && cardData.cvv.valid && cardData.postalCode.valid;
},
/**
* Display a SDK error.
*
* @param {jQuery} $form Form element.
* @param {string} message Error messages.
*
* @since 1.9.5
*/
displaySdkError( $form, message ) {
$form
.find( '.wpforms-square-credit-card-hidden-input' )
.closest( '.wpforms-field-square-number' )
.append( $( '<label></label>', {
text: message,
class: 'wpforms-error',
} ) );
},
/**
* Remove field error.
*
* @param {jQuery} $form Form element.
*
* @since 1.9.7
*/
removeFieldError( $form ) {
$form.find( '.wpforms-field-square-number .wpforms-error' ).remove();
},
/**
* Display a field error using jQuery Validate library.
*
* @param {jQuery} $field Form element.
* @param {string} message Error messages.
*
* @since 1.9.5
*/
displayFormError( $field, message ) {
const fieldName = $field.attr( 'name' );
const $form = $field.closest( 'form' );
const error = {};
error[ fieldName ] = message;
wpforms.displayFormAjaxFieldErrors( $form, error );
wpforms.scrollToError( $field );
},
/**
* Disable submit button for the form.
*
* @since 1.9.5
*
* @param {jQuery} $form Form element.
*/
disableSubmitBtn( $form ) {
$form.find( '.wpforms-submit' ).prop( 'disabled', true );
},
/**
* Enable submit button for the form.
*
* @since 1.9.5
*
* @param {jQuery} $form Form element.
*/
enableSubmitBtn( $form ) {
$form.find( '.wpforms-submit' ).prop( 'disabled', false );
},
/**
* Replaces &, <, >, ", `, and ' with their escaped counterparts.
*
* @since 1.9.5
*
* @param {string} string String to escape.
*
* @return {string} Escaped string.
*/
escapeTextString( string ) {
return $( '<span></span>' ).text( string ).html();
},
/**
* Callback for a page changing.
*
* @since 1.9.5
*
* @param {Event} event Event.
* @param {number} currentPage Current page.
* @param {jQuery} $form Current form.
* @param {string} action The navigation action.
*/
pageChange( event, currentPage, $form, action ) { // eslint-disable-line complexity
const $squareDiv = $form.find( '.wpforms-field-square-cardnumber' );
// Stop navigation through page break pages.
if (
! $squareDiv.is( ':visible' ) ||
( ! $squareDiv.data( 'required' ) && ! app.isCardDataNotEmpty() ) ||
( app.lockedPageToSwitch && app.lockedPageToSwitch !== currentPage ) ||
action === 'prev'
) {
return;
}
if ( app.isCardDataValid() ) {
app.removeFieldError( $form );
return;
}
app.lockedPageToSwitch = currentPage;
app.displayFormError( app.getCreditCardInput( $form ), wpforms_square.i18n.empty_details );
event.preventDefault();
},
/**
* Callback for a after page changing.
*
* @since 1.9.5
*/
afterPageChange() {
window.dispatchEvent( new Event( 'resize' ) );
},
/**
* Get CSS property value.
* In case of exception return empty string.
*
* @since 1.9.5
*
* @param {jQuery} $element Element.
* @param {string} property Property.
*
* @return {string} Property value.
*/
getCssPropertyValue( $element, property ) {
try {
return $element.css( property );
} catch ( e ) {
return '';
}
},
/**
* Determine whether modern style are needed.
*
* Force run on the classic markup if it is conversational or lead form.
*
* @since 1.9.5
*
* @return {boolean} True if the form needs styles.
*/
needsStyles() {
// Styles are not needed if the classic markup is used
// and it's neither conversational nor lead form.
if (
( ! window.WPForms || ! WPForms.FrontendModern ) &&
! $( '#wpforms-conversational-form-page' ).length &&
! $( '.wpforms-lead-forms-container' ).length
) {
return false;
}
return true;
},
/**
* Get modern card styles.
*
* @since 1.9.5
*
* @param {jQuery} $form Current form.
*
* @return {Object} Card styles object.
*/
getModernMarkupCardStyles( $form ) {
if ( ! app.needsStyles() ) {
return {};
}
const $hiddenInput = app.getCreditCardInput( $form ),
hiddenInputColor = app.getCssPropertyValue( $hiddenInput, 'color' ),
inputStyle = {
fontSize: app.getCssPropertyValue( $hiddenInput, 'font-size' ),
colorText: hiddenInputColor,
colorTextPlaceholder: hiddenInputColor,
};
// Check if WPFormsUtils.cssColorsUtils object is available.
if ( WPFormsUtils.hasOwnProperty( 'cssColorsUtils' ) &&
typeof WPFormsUtils.cssColorsUtils.getColorWithOpacity === 'function' ) {
inputStyle.colorText = WPFormsUtils.cssColorsUtils.getColorWithOpacity( hiddenInputColor );
inputStyle.colorTextPlaceholder = WPFormsUtils.cssColorsUtils.getColorWithOpacity( hiddenInputColor, '0.5' );
}
return {
input: {
color: inputStyle.colorText,
fontSize: inputStyle.fontSize,
},
'input::placeholder': {
color: inputStyle.colorTextPlaceholder,
},
'input.is-error': {
color: inputStyle.colorText,
},
};
},
};
// Provide access to public functions/properties.
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsSquare.init();
File diff suppressed because one or more lines are too long