import MiniCartBootstap from './modules/ContextBootstrap/MiniCartBootstap'; import SingleProductBootstap from './modules/ContextBootstrap/SingleProductBootstap'; import CartBootstrap from './modules/ContextBootstrap/CartBootstap'; import CheckoutBootstap from './modules/ContextBootstrap/CheckoutBootstap'; import PayNowBootstrap from './modules/ContextBootstrap/PayNowBootstrap'; import Renderer from './modules/Renderer/Renderer'; import ErrorHandler from './modules/ErrorHandler'; import HostedFieldsRenderer from './modules/Renderer/HostedFieldsRenderer'; import CardFieldsRenderer from './modules/Renderer/CardFieldsRenderer'; import CardFieldsFreeTrialRenderer from './modules/Renderer/CardFieldsFreeTrialRenderer'; import MessageRenderer from './modules/Renderer/MessageRenderer'; import Spinner from './modules/Helper/Spinner'; import { getCurrentPaymentMethod, ORDER_BUTTON_SELECTOR, PaymentMethods, } from './modules/Helper/CheckoutMethodState'; import { setVisibleByClass } from './modules/Helper/Hiding'; import { isChangePaymentPage } from './modules/Helper/Subscriptions'; import FreeTrialHandler from './modules/ActionHandler/FreeTrialHandler'; import MultistepCheckoutHelper from './modules/Helper/MultistepCheckoutHelper'; import FormSaver from './modules/Helper/FormSaver'; import FormValidator from './modules/Helper/FormValidator'; import { loadPaypalScript } from './modules/Helper/ScriptLoading'; import buttonModuleWatcher from './modules/ButtonModuleWatcher'; import MessagesBootstrap from './modules/ContextBootstrap/MessagesBootstap'; import { apmButtonsInit } from './modules/Helper/ApmButtons'; // TODO: could be a good idea to have a separate spinner for each gateway, // but I think we care mainly about the script loading, so one spinner should be enough. const buttonsSpinner = new Spinner( document.querySelector( '.ppc-button-wrapper' ) ); const cardsSpinner = new Spinner( '#ppcp-hosted-fields' ); const bootstrap = () => { const checkoutFormSelector = 'form.woocommerce-checkout'; const context = PayPalCommerceGateway.context; const errorHandler = new ErrorHandler( PayPalCommerceGateway.labels.error.generic, document.querySelector( checkoutFormSelector ) ?? document.querySelector( '.woocommerce-notices-wrapper' ) ); const spinner = new Spinner(); const formSaver = new FormSaver( PayPalCommerceGateway.ajax.save_checkout_form.endpoint, PayPalCommerceGateway.ajax.save_checkout_form.nonce ); const formValidator = PayPalCommerceGateway.early_checkout_validation_enabled ? new FormValidator( PayPalCommerceGateway.ajax.validate_checkout.endpoint, PayPalCommerceGateway.ajax.validate_checkout.nonce ) : null; const freeTrialHandler = new FreeTrialHandler( PayPalCommerceGateway, checkoutFormSelector, formSaver, formValidator, spinner, errorHandler ); new MultistepCheckoutHelper( checkoutFormSelector ); jQuery( 'form.woocommerce-checkout input' ).on( 'keydown', ( e ) => { if ( e.key === 'Enter' && [ PaymentMethods.PAYPAL, PaymentMethods.CARDS, PaymentMethods.CARD_BUTTON, ].includes( getCurrentPaymentMethod() ) ) { e.preventDefault(); } } ); const hasMessages = () => { return ( PayPalCommerceGateway.messages.is_hidden === false && document.querySelector( PayPalCommerceGateway.messages.wrapper ) ); }; const doBasicCheckoutValidation = () => { if ( PayPalCommerceGateway.basic_checkout_validation_enabled ) { // A quick fix to get the errors about empty form fields before attempting PayPal order, // it should solve #513 for most of the users, but it is not a proper solution. // Currently it is disabled by default because a better solution is now implemented // (see woocommerce_paypal_payments_basic_checkout_validation_enabled, // woocommerce_paypal_payments_early_wc_checkout_validation_enabled filters). const invalidFields = Array.from( jQuery( 'form.woocommerce-checkout .validate-required.woocommerce-invalid:visible' ) ); if ( invalidFields.length ) { const billingFieldsContainer = document.querySelector( '.woocommerce-billing-fields' ); const shippingFieldsContainer = document.querySelector( '.woocommerce-shipping-fields' ); const nameMessageMap = PayPalCommerceGateway.labels.error.required.elements; const messages = invalidFields .map( ( el ) => { const name = el .querySelector( '[name]' ) ?.getAttribute( 'name' ); if ( name && name in nameMessageMap ) { return nameMessageMap[ name ]; } let label = el .querySelector( 'label' ) .textContent.replaceAll( '*', '' ) .trim(); if ( billingFieldsContainer?.contains( el ) ) { label = PayPalCommerceGateway.labels.billing_field.replace( '%s', label ); } if ( shippingFieldsContainer?.contains( el ) ) { label = PayPalCommerceGateway.labels.shipping_field.replace( '%s', label ); } return PayPalCommerceGateway.labels.error.required.field.replace( '%s', `${ label }` ); } ) .filter( ( s ) => s.length > 2 ); errorHandler.clear(); if ( messages.length ) { errorHandler.messages( messages ); } else { errorHandler.message( PayPalCommerceGateway.labels.error.required.generic ); } return false; } } return true; }; const onCardFieldsBeforeSubmit = () => { return doBasicCheckoutValidation(); }; const onSmartButtonClick = async ( data, actions ) => { window.ppcpFundingSource = data.fundingSource; const requiredFields = jQuery( 'form.woocommerce-checkout .validate-required:visible :input' ); requiredFields.each( ( i, input ) => { jQuery( input ).trigger( 'validate' ); } ); if ( ! doBasicCheckoutValidation() ) { return actions.reject(); } const form = document.querySelector( checkoutFormSelector ); if ( form ) { jQuery( '#ppcp-funding-source-form-input' ).remove(); form.insertAdjacentHTML( 'beforeend', `` ); } const isFreeTrial = PayPalCommerceGateway.is_free_trial_cart; if ( isFreeTrial && data.fundingSource !== 'card' && ! PayPalCommerceGateway.subscription_plan_id && ! PayPalCommerceGateway.vault_v3_enabled ) { freeTrialHandler.handle(); return actions.reject(); } if ( context === 'checkout' ) { try { await formSaver.save( form ); } catch ( error ) { console.error( error ); } } }; const onSmartButtonsInit = () => { jQuery( document ).trigger( 'ppcp-smart-buttons-init', this ); buttonsSpinner.unblock(); }; let creditCardRenderer = new HostedFieldsRenderer( PayPalCommerceGateway, errorHandler, spinner ); if ( typeof paypal.CardFields !== 'undefined' ) { if ( PayPalCommerceGateway.is_free_trial_cart ) { creditCardRenderer = new CardFieldsFreeTrialRenderer( PayPalCommerceGateway, errorHandler, spinner ); } else { creditCardRenderer = new CardFieldsRenderer( PayPalCommerceGateway, errorHandler, spinner, onCardFieldsBeforeSubmit ); } } const renderer = new Renderer( creditCardRenderer, PayPalCommerceGateway, onSmartButtonClick, onSmartButtonsInit ); const messageRenderer = new MessageRenderer( PayPalCommerceGateway.messages ); if ( PayPalCommerceGateway.mini_cart_buttons_enabled === '1' ) { const miniCartBootstrap = new MiniCartBootstap( PayPalCommerceGateway, renderer, errorHandler ); miniCartBootstrap.init(); buttonModuleWatcher.registerContextBootstrap( 'mini-cart', miniCartBootstrap ); } if ( context === 'product' && ( PayPalCommerceGateway.single_product_buttons_enabled === '1' || hasMessages() ) ) { const singleProductBootstrap = new SingleProductBootstap( PayPalCommerceGateway, renderer, errorHandler ); singleProductBootstrap.init(); buttonModuleWatcher.registerContextBootstrap( 'product', singleProductBootstrap ); } if ( context === 'cart' ) { const cartBootstrap = new CartBootstrap( PayPalCommerceGateway, renderer, errorHandler ); cartBootstrap.init(); buttonModuleWatcher.registerContextBootstrap( 'cart', cartBootstrap ); } if ( context === 'checkout' ) { const checkoutBootstap = new CheckoutBootstap( PayPalCommerceGateway, renderer, spinner, errorHandler ); checkoutBootstap.init(); buttonModuleWatcher.registerContextBootstrap( 'checkout', checkoutBootstap ); } if ( context === 'pay-now' ) { const payNowBootstrap = new PayNowBootstrap( PayPalCommerceGateway, renderer, spinner, errorHandler ); payNowBootstrap.init(); buttonModuleWatcher.registerContextBootstrap( 'pay-now', payNowBootstrap ); } const messagesBootstrap = new MessagesBootstrap( PayPalCommerceGateway, messageRenderer ); messagesBootstrap.init(); apmButtonsInit( PayPalCommerceGateway ); }; document.addEventListener( 'DOMContentLoaded', () => { if ( ! typeof PayPalCommerceGateway ) { console.error( 'PayPal button could not be configured.' ); return; } if ( PayPalCommerceGateway.context !== 'checkout' && PayPalCommerceGateway.data_client_id.user === 0 && PayPalCommerceGateway.data_client_id.has_subscriptions ) { return; } const paypalButtonGatewayIds = [ PaymentMethods.PAYPAL, ...Object.entries( PayPalCommerceGateway.separate_buttons ).map( ( [ k, data ] ) => data.id ), ]; // Sometimes PayPal script takes long time to load, // so we additionally hide the standard order button here to avoid failed orders. // Normally it is hidden later after the script load. const hideOrderButtonIfPpcpGateway = () => { // only in checkout and pay now page, otherwise it may break things (e.g. payment via product page), // and also the loading spinner may look weird on other pages if ( ! [ 'checkout', 'pay-now' ].includes( PayPalCommerceGateway.context ) || isChangePaymentPage() || ( PayPalCommerceGateway.is_free_trial_cart && PayPalCommerceGateway.vaulted_paypal_email !== '' ) ) { return; } const currentPaymentMethod = getCurrentPaymentMethod(); const isPaypalButton = paypalButtonGatewayIds.includes( currentPaymentMethod ); const isCards = currentPaymentMethod === PaymentMethods.CARDS; setVisibleByClass( ORDER_BUTTON_SELECTOR, ! isPaypalButton && ! isCards, 'ppcp-hidden' ); if ( isPaypalButton ) { // stopped after the first rendering of the buttons, in onInit buttonsSpinner.block(); } else { buttonsSpinner.unblock(); } if ( isCards ) { cardsSpinner.block(); } else { cardsSpinner.unblock(); } }; jQuery( document ).on( 'hosted_fields_loaded', () => { cardsSpinner.unblock(); } ); let bootstrapped = false; let failed = false; hideOrderButtonIfPpcpGateway(); jQuery( document.body ).on( 'updated_checkout payment_method_selected', () => { if ( bootstrapped || failed ) { return; } hideOrderButtonIfPpcpGateway(); } ); loadPaypalScript( PayPalCommerceGateway, () => { bootstrapped = true; bootstrap(); }, () => { failed = true; setVisibleByClass( ORDER_BUTTON_SELECTOR, true, 'ppcp-hidden' ); buttonsSpinner.unblock(); cardsSpinner.unblock(); } ); } );