assets/js/llms-form-checkout.js
/**
* LifterLMS Checkout Screen related events and interactions
*
* @package LifterLMS/Scripts
*
* @since 3.0.0
* @version 7.0.0
*/
( function( $ ) {
var llms_checkout = function() {
/**
* Array of validation functions to call on form submission
*
* @type array
* @since 3.0.0
* @version 3.0.0
*/
var before_submit = [];
/**
* Array of gateways to be automatically bound when needed
*
* @type array
* @since 3.0.0
* @version 3.0.0
*/
var gateways = [];
this.$checkout_form = $( '#llms-product-purchase-form' );
this.$confirm_form = $( '#llms-product-purchase-confirm-form' );
this.$form_sections = false;
this.form_action = false;
/**
* Initialize checkout JS & bind if on the checkout screen
*
* @since 3.0.0
* @since 3.34.5 Make sure we bind click events for the Show / Hide login area at the top of the checkout screen
* even if there's no llms product purchase form.
* @since 7.0.0 Disable smooth scroll-behavior on checkout.
*
* @return void
*/
this.init = function() {
var self = this;
if ( $( '.llms-checkout-wrapper' ).length ) {
this.bind_login();
}
if ( this.$checkout_form.length ) {
this.form_action = 'checkout';
this.$form_sections = this.$checkout_form.find( '.llms-checkout-section' );
this.$checkout_form.on( 'submit', this, this.submit );
/**
* Fix `HTMLFormElement.reportValidity()` when `scroll-behavior: smooth`.
*
* @see {@link https://github.com/gocodebox/lifterlms/issues/2206}
*/
document.querySelector( 'html' ).style.scrollBehavior = 'auto';
// add before submit event for password strength meter if one's found
if ( $( '.llms-password-strength-meter' ).length ) {
this.add_before_submit_event( {
data: LLMS.PasswordStrength,
handler: LLMS.PasswordStrength.checkout,
} );
}
this.bind_coupon();
this.bind_gateways();
} else if ( this.$confirm_form.length ) {
this.form_action = 'confirm';
this.$form_sections = this.$confirm_form.find( '.llms-checkout-section' );
this.$confirm_form.on( 'submit', function() {
self.processing( 'start' );
} );
}
};
/**
* Public function which allows other classes or extensions to add
* before submit events to llms checkout private "before_submit" array
*
* @param object obj object of data to push to the array
* requires at least a "handler" key which should pass a callable function
* "data" can be anything, will be passed as the first parameter to the handler function
* @since 3.0.0
* @version 3.0.0
*/
this.add_before_submit_event = function( obj ) {
if ( ! obj.handler || 'function' !== typeof obj.handler ) {
return;
}
if ( ! obj.data ) {
obj.data = null;
}
before_submit.push( obj );
};
/**
* Add an error message
*
* @param string message error message string
* @param mixed data optional error data to output on the console
* @return void
* @since 3.27.0
* @version 3.27.0
*/
this.add_error = function( message, data ) {
var id = 'llms-checkout-errors';
$err = $( '#' + id );
if ( ! $err.length ) {
$err = $( '<ul class="llms-notice llms-error" id="' + id + '" />' );
$( '.llms-checkout-wrapper' ).prepend( $err );
}
$err.append( '<li>' + message + '</li>' );
if ( data ) {
console.error( data );
}
};
/**
* Public function which allows other classes or extensions to add
* gateways classes that should be bound by this class
*
* @param obj gateway_class callable class object
* @since 3.0.0
* @version 3.0.0
*/
this.add_gateway = function( gateway_class ) {
gateways.push( gateway_class );
};
/**
* Bind coupon add & remove button events
*
* @return void
* @since 3.0.0
* @version 3.0.0
*/
this.bind_coupon = function() {
var self = this;
// show & hide the coupon field & button
$( 'a[href="#llms-coupon-toggle"]' ).on( 'click', function( e ) {
e.preventDefault();
$( '.llms-coupon-entry' ).slideToggle( 400 );
} );
// apply coupon click
$( '#llms-apply-coupon' ).on( 'click', function( e ) {
e.preventDefault();
self.coupon_apply( $( this ) );
} );
// remove coupon click
$( '#llms-remove-coupon' ).on( 'click', function( e ) {
e.preventDefault();
self.coupon_remove( $( this ) );
} );
};
/**
* Bind gateway section events
*
* @return void
* @since 3.0.0
* @version 3.0.0
*/
this.bind_gateways = function() {
this.load_gateways();
if ( ! $( 'input[name="llms_payment_gateway"]' ).length ) {
$( '#llms_create_pending_order' ).removeAttr( 'disabled' );
}
// add class and trigger watchable event when gateway selection changes
$( 'input[name="llms_payment_gateway"]' ).on( 'change', function() {
$( 'input[name="llms_payment_gateway"]' ).each( function() {
var $el = $( this ),
$parent = $el.closest( '.llms-payment-gateway' ),
$fields = $parent.find( '.llms-gateway-fields' ).find( 'input, textarea, select' ),
checked = $el.is( ':checked' ),
display_func = ( checked ) ? 'addClass' : 'removeClass';
$parent[ display_func ]( 'is-selected' );
if ( checked ) {
// enable fields
$fields.removeAttr( 'disabled' );
// emit a watchable event for extensions to hook onto
$( '.llms-payment-gateways' ).trigger( 'llms-gateway-selected', {
id: $el.val(),
$selector: $parent,
} );
} else {
// disable fields
$fields.attr( 'disabled', 'disabled' );
}
} );
} );
// enable / disable buttons depending on field validation status
$( '.llms-payment-gateways' ).on( 'llms-gateway-selected', function( e, data ) {
var $submit = $( '#llms_create_pending_order' );
if ( data.$selector && data.$selector.find( '.llms-gateway-fields .invalid' ).length ) {
$submit.attr( 'disabled', 'disabled' );
} else {
$submit.removeAttr( 'disabled' );
}
} );
};
/**
* Bind click events for the Show / Hide login area at the top of the checkout screen
*
* @since 3.0.0
* @since 3.34.5 When showing the login form area make sure we slide up the `.llms-notice` link's parent too.
*
* @return void
*/
this.bind_login = function() {
$( 'a[href="#llms-show-login"]' ).on( 'click', function( e ) {
e.preventDefault();
$( this ).closest( '.llms-info,.llms-notice' ).slideUp( 400 );
$( 'form.llms-login' ).slideDown( 400 );
} );
};
/**
* Clear error messages
*
* @return void
* @since 3.27.0
* @version 3.27.0
*/
this.clear_errors = function() {
$( '#llms-checkout-errors' ).remove();
};
/**
* Triggered by clicking the "Apply Coupon" Button
* Validates the coupon via JS and adds error / success messages
* On success it will replace partials on the checkout screen with updated
* prices and a "remove coupon" button
*
* @param obj $btn jQuery selector of the Apply button
* @return void
* @since 3.0.0
* @version 3.0.0
*/
this.coupon_apply = function ( $btn ) {
var self = this,
$code = $( '#llms_coupon_code' ),
code = $code.val(),
$messages = $( '.llms-coupon-messages' ),
$errors = $messages.find( '.llms-error' ),
$container = $( 'form.llms-checkout' );
LLMS.Spinner.start( $container );
window.LLMS.Ajax.call( {
data: {
action: 'validate_coupon_code',
code: code,
plan_id: $( '#llms-plan-id' ).val(),
},
beforeSend: function() {
$errors.hide();
},
success: function( r ) {
LLMS.Spinner.stop( $container );
if ( 'error' === r.code ) {
var $message = $( '<li>' + r.message + '</li>' );
if ( ! $errors.length ) {
$errors = $( '<ul class="llms-notice llms-error" />' );
$messages.append( $errors );
} else {
$errors.empty();
}
$message.appendTo( $errors );
$errors.show();
} else if ( r.success ) {
$( '.llms-coupon-wrapper' ).replaceWith( r.data.coupon_html );
self.bind_coupon();
$( '.llms-payment-gateways' ).replaceWith( r.data.gateways_html );
self.bind_gateways();
$( '.llms-order-summary' ).replaceWith( r.data.summary_html );
}
}
} );
};
/**
* Called by clicking the "Remove Coupon" button
* Removes the coupon via AJAX and unsets related session data
*
* @param obj $btn jQuery selector of the Remove button
* @return void
* @since 3.0.0
* @version 3.0.0
*/
this.coupon_remove = function( $btn ) {
var self = this,
$container = $( 'form.llms-checkout' );
LLMS.Spinner.start( $container );
window.LLMS.Ajax.call( {
data: {
action: 'remove_coupon_code',
plan_id: $( '#llms-plan-id' ).val(),
},
success: function( r ) {
LLMS.Spinner.stop( $container );
if ( r.success ) {
$( '.llms-coupon-wrapper' ).replaceWith( r.data.coupon_html );
self.bind_coupon();
$( '.llms-order-summary' ).replaceWith( r.data.summary_html );
$( '.llms-payment-gateways' ).replaceWith( r.data.gateways_html );
self.bind_gateways();
}
}
} );
};
/**
* Scroll error messages into view
*
* @return void
* @since 3.27.0
* @version 3.27.0
*/
this.focus_errors = function() {
$( 'html, body' ).animate( {
scrollTop: $( '#llms-checkout-errors' ).offset().top - 50,
}, 200 );
};
/**
* Bind external gateway JS
*
* @return void
* @since 3.0.0
* @version 3.0.0
*/
this.load_gateways = function() {
for ( var i = 0; i <= gateways.length; i++ ) {
var g = gateways[i];
if ( typeof g === 'object' && g !== null ) {
if ( g.bind !== undefined && 'function' === typeof g.bind ) {
g.bind();
}
}
}
};
/**
* Start or stop processing events on the checkout form
*
* @param string action whether to start or stop processing [start|stop]
* @return void
* @since 3.0.0
* @version 3.24.1
*/
this.processing = function( action ) {
var func, $form;
switch ( action ) {
case 'stop':
func = 'removeClass';
break;
case 'start':
default:
func = 'addClass';
break;
}
if ( 'checkout' === this.form_action ) {
$form = this.$checkout_form;
} else if ( 'confirm' === this.form_action ) {
$form = this.$confirm_form;
}
$form[ func ]( 'llms-is-processing' );
LLMS.Spinner[ action ]( this.$form_sections );
};
/**
* Handles form submission
* Calls all validation events in `before_submit[]`
* waits for call backs and either displays returned errors
* or submits the form when all are successful
*
* @param obj e JS event object
* @return void
* @since 3.0.0
* @version 3.27.0
*/
this.submit = function( e ) {
var self = e.data,
num = before_submit.length,
checks = 0,
max_checks = 60000,
errors = [],
finishes = 0,
successes = 0,
interval;
e.preventDefault();
// add spinners
self.processing( 'start' );
// remove errors to prevent duplicates
self.clear_errors();
// start running all the events
for ( var i = 0; i < before_submit.length; i++ ) {
var obj = before_submit[ i ];
obj.handler( obj.data, function( r ) {
finishes++;
if ( true === r ) {
successes++;
} else if ( 'string' === typeof r ) {
errors.push( r );
}
} );
}
// run an interval to wait for finishes
interval = setInterval( function() {
var clear = false,
stop = false;
// timeout...
if ( checks >= max_checks ) {
clear = true;
stop = true;
} else if ( num === finishes ) {
// everything has finished
// all were successful, submit the form
if ( num === successes ) {
clear = true;
self.$checkout_form.off( 'submit', self.submit );
self.$checkout_form.trigger( 'submit' );
} else if ( errors.length ) {
clear = true;
stop = true;
for ( var i = 0; i < errors.length; i++ ) {
self.add_error( errors[ i ] );
}
self.focus_errors();
}
}
if ( clear ) {
clearInterval( interval );
}
if ( stop ) {
self.processing( 'stop' );
}
checks++;
}, 100 );
};
// initialize
this.init();
return this;
};
window.llms = window.llms || {};
window.llms.checkout = new llms_checkout();
} )( jQuery );