diff --git a/.eslintrc b/.eslintrc index 7a97d815d..947e3acbe 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,11 @@ { "extends": [ "plugin:@wordpress/eslint-plugin/recommended" ], + "env": { + "browser": true + }, "globals": { - "wc": true + "wc": true, + "jQuery": "readonly" }, "rules": { "no-console": ["error", { "allow": ["warn", "error"] }] diff --git a/.megaignore b/.megaignore new file mode 100644 index 000000000..6ad02d892 --- /dev/null +++ b/.megaignore @@ -0,0 +1 @@ +-s:* \ No newline at end of file diff --git a/.psalm/wc-bookings.php b/.psalm/wc-bookings.php new file mode 100644 index 000000000..2ef838f51 --- /dev/null +++ b/.psalm/wc-bookings.php @@ -0,0 +1,20 @@ + array( 'mastercard' => array(), 'visa' => array(), - 'amex' => array(), ), 'CY' => array( 'mastercard' => array(), diff --git a/modules/ppcp-applepay/extensions.php b/modules/ppcp-applepay/extensions.php index daf5cf2ea..8f9d84b5a 100644 --- a/modules/ppcp-applepay/extensions.php +++ b/modules/ppcp-applepay/extensions.php @@ -269,7 +269,7 @@ return array( 'classes' => array( 'ppcp-field-indent' ), 'class' => array(), 'input_class' => array( 'wc-enhanced-select' ), - 'default' => 'pay', + 'default' => 'plain', 'options' => PropertiesDictionary::button_types(), 'screens' => array( State::STATE_ONBOARDED ), 'gateway' => 'dcc', diff --git a/modules/ppcp-applepay/resources/css/styles.scss b/modules/ppcp-applepay/resources/css/styles.scss index 3818b8db5..3402b1a20 100644 --- a/modules/ppcp-applepay/resources/css/styles.scss +++ b/modules/ppcp-applepay/resources/css/styles.scss @@ -52,3 +52,7 @@ } } } + +#ppc-button-ppcp-applepay { + display: none; +} diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index 281a08ae1..83b0a95b5 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -1,3 +1,6 @@ +/* global ApplePaySession */ +/* global PayPalCommerceGateway */ + import ContextHandlerFactory from './Context/ContextHandlerFactory'; import { createAppleErrors } from './Helper/applePayError'; import { setVisible } from '../../../ppcp-button/resources/js/modules/Helper/Hiding'; @@ -7,18 +10,95 @@ import ErrorHandler from '../../../ppcp-button/resources/js/modules/ErrorHandler import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder'; import { apmButtonsInit } from '../../../ppcp-button/resources/js/modules/Helper/ApmButtons'; -class ApplepayButton { - constructor( context, externalHandler, buttonConfig, ppcpConfig ) { - apmButtonsInit( ppcpConfig ); +/** + * Plugin-specific styling. + * + * Note that most properties of this object do not apply to the Apple Pay button. + * + * @typedef {Object} PPCPStyle + * @property {string} shape - Outline shape. + * @property {?number} height - Button height in pixel. + */ - this.isInitialized = false; +/** + * Style options that are defined by the Apple Pay SDK and are required to render the button. + * + * @typedef {Object} ApplePayStyle + * @property {string} type - Defines the button label. + * @property {string} color - Button color + * @property {string} lang - The locale; an empty string will apply the user-agent's language. + */ + +/** + * List of valid context values that the button can have. + * + * @type {Object} + */ +const CONTEXT = { + Product: 'product', + Cart: 'cart', + Checkout: 'checkout', + PayNow: 'pay-now', + MiniCart: 'mini-cart', + BlockCart: 'cart-block', + BlockCheckout: 'checkout-block', + Preview: 'preview', + + // Block editor contexts. + Blocks: [ 'cart-block', 'checkout-block' ], + + // Custom gateway contexts. + Gateways: [ 'checkout', 'pay-now' ], +}; + +/** + * A payment button for Apple Pay. + * + * On a single page, multiple Apple Pay buttons can be displayed, which also means multiple + * ApplePayButton instances exist. A typical case is on the product page, where one Apple Pay button + * is located inside the minicart-popup, and another pay-now button is in the product context. + */ +class ApplePayButton { + /** + * Whether the payment button is initialized. + * + * @type {boolean} + */ + #isInitialized = false; + + #wrapperId = ''; + #ppcpButtonWrapperId = ''; + + /** + * Context describes the button's location on the website and what details it submits. + * + * @type {''|'product'|'cart'|'checkout'|'pay-now'|'mini-cart'|'cart-block'|'checkout-block'|'preview'} + */ + context = ''; + + externalHandler = null; + buttonConfig = null; + ppcpConfig = null; + paymentsClient = null; + formData = null; + contextHandler = null; + updatedContactInfo = []; + selectedShippingMethod = []; + + /** + * Stores initialization data sent to the button. + */ + initialPaymentRequest = null; + + constructor( context, externalHandler, buttonConfig, ppcpConfig ) { + this._initDebug( !! buttonConfig?.is_debug ); + + apmButtonsInit( ppcpConfig ); this.context = context; this.externalHandler = externalHandler; this.buttonConfig = buttonConfig; this.ppcpConfig = ppcpConfig; - this.paymentsClient = null; - this.formData = null; this.contextHandler = ContextHandlerFactory.create( this.context, @@ -26,36 +106,226 @@ class ApplepayButton { this.ppcpConfig ); - this.updatedContactInfo = []; - this.selectedShippingMethod = []; - this.nonce = - document.getElementById( 'woocommerce-process-checkout-nonce' ) - ?.value || buttonConfig.nonce; - - // Stores initialization data sent to the button. - this.initialPaymentRequest = null; - - // Default eligibility status. - this.isEligible = true; - - this.log = function () { - if ( this.buttonConfig.is_debug ) { - //console.log('[ApplePayButton]', ...arguments); - } - }; - this.refreshContextData(); + } + + /** + * NOOP log function to avoid errors when debugging is disabled. + */ + log() {} + + /** + * Enables debugging tools, when the button's is_debug flag is set. + * + * @param {boolean} enableDebugging If debugging features should be enabled for this instance. + * @private + */ + _initDebug( enableDebugging ) { + if ( ! enableDebugging || this.#isInitialized ) { + return; + } - // Debug helpers - jQuery( document ).on( 'ppcp-applepay-debug', () => { - console.log( 'ApplePayButton', this.context, this ); - } ); document.ppcpApplepayButtons = document.ppcpApplepayButtons || {}; document.ppcpApplepayButtons[ this.context ] = this; + + this.log = ( ...args ) => { + console.log( `[ApplePayButton | ${ this.context }]`, ...args ); + }; + + jQuery( document ).on( 'ppcp-applepay-debug', () => { + this.log( this ); + } ); + } + + /** + * The nonce for ajax requests. + * + * @return {string} The nonce value + */ + get nonce() { + const input = document.getElementById( + 'woocommerce-process-checkout-nonce' + ); + + return input?.value || this.buttonConfig.nonce; + } + + /** + * Whether the current page qualifies to use the Apple Pay button. + * + * In admin, the button is always eligible, to display an accurate preview. + * On front-end, PayPal's response decides if customers can use Apple Pay. + * + * @return {boolean} True, if the button can be displayed. + */ + get isEligible() { + if ( ! this.#isInitialized ) { + return true; + } + + if ( CONTEXT.Preview === this.context ) { + return true; + } + + /** + * Ensure the ApplePaySession is available and accepts payments + * This check is required when using Apple Pay SDK v1; canMakePayments() returns false + * if the current device is not liked to iCloud or the Apple Wallet is not available + * for a different reason. + */ + try { + if ( ! window.ApplePaySession?.canMakePayments() ) { + return false; + } + } catch ( error ) { + console.warn( error ); + return false; + } + + return !! this.applePayConfig.isEligible; + } + + /** + * Determines if the current payment button should be rendered as a stand-alone gateway. + * The return value `false` usually means, that the payment button is bundled with all available + * payment buttons. + * + * The decision depends on the button context (placement) and the plugin settings. + * + * @return {boolean} True, if the current button represents a stand-alone gateway. + */ + get isSeparateGateway() { + return ( + this.buttonConfig.is_wc_gateway_enabled && + CONTEXT.Gateways.includes( this.context ) + ); + } + + /** + * Returns the wrapper ID for the current button context. + * The ID varies for the MiniCart context. + * + * @return {string} The wrapper-element's ID (without the `#` prefix). + */ + get wrapperId() { + if ( ! this.#wrapperId ) { + let id; + + if ( CONTEXT.MiniCart === this.context ) { + id = this.buttonConfig.button.mini_cart_wrapper; + } else if ( this.isSeparateGateway ) { + id = 'ppc-button-ppcp-applepay'; + } else { + id = this.buttonConfig.button.wrapper; + } + + this.#wrapperId = id.replace( /^#/, '' ); + } + + return this.#wrapperId; + } + + /** + * Returns the wrapper ID for the ppcpButton + * + * @return {string} The wrapper-element's ID (without the `#` prefix). + */ + get ppcpButtonWrapperId() { + if ( ! this.#ppcpButtonWrapperId ) { + let id; + + if ( CONTEXT.MiniCart === this.context ) { + id = this.ppcpConfig.button.mini_cart_wrapper; + } else if ( CONTEXT.Blocks.includes( this.context ) ) { + id = '#express-payment-method-ppcp-gateway-paypal'; + } else { + id = this.ppcpConfig.button.wrapper; + } + + this.#ppcpButtonWrapperId = id.replace( /^#/, '' ); + } + + return this.#ppcpButtonWrapperId; + } + + /** + * Returns the context-relevant PPCP style object. + * The style for the MiniCart context can be different. + * + * The PPCP style are custom style options, that are provided by this plugin. + * + * @return {PPCPStyle} The style object. + */ + get ppcpStyle() { + if ( CONTEXT.MiniCart === this.context ) { + return this.ppcpConfig.button.mini_cart_style; + } + + return this.ppcpConfig.button.style; + } + + /** + * Returns default style options that are propagated to and rendered by the Apple Pay button. + * + * These styles are the official style options provided by the Apple Pay SDK. + * + * @return {ApplePayStyle} The style object. + */ + get buttonStyle() { + return { + type: this.buttonConfig.button.type, + lang: this.buttonConfig.button.lang, + color: this.buttonConfig.button.color, + }; + } + + /** + * Returns the HTML element that wraps the current button + * + * @return {HTMLElement|null} The wrapper element, or null. + */ + get wrapperElement() { + return document.getElementById( this.wrapperId ); + } + + /** + * Returns an array of HTMLElements that belong to the payment button. + * + * @return {HTMLElement[]} List of payment button wrapper elements. + */ + get allElements() { + const selectors = []; + + // Payment button (Pay now, smart button block) + selectors.push( `#${ this.wrapperId }` ); + + // Block Checkout: Express checkout button. + if ( CONTEXT.Blocks.includes( this.context ) ) { + selectors.push( '#express-payment-method-ppcp-applepay' ); + } + + // Classic Checkout: Apple Pay gateway. + if ( CONTEXT.Gateways.includes( this.context ) ) { + selectors.push( '.wc_payment_method.payment_method_ppcp-applepay' ); + } + + this.log( 'Wrapper Elements:', selectors ); + return /** @type {HTMLElement[]} */ selectors.flatMap( ( selector ) => + Array.from( document.querySelectorAll( selector ) ) + ); + } + + /** + * Checks whether the main button-wrapper is present in the current DOM. + * + * @return {boolean} True, if the button context (wrapper element) is found. + */ + get isPresent() { + return this.wrapperElement instanceof HTMLElement; } init( config ) { - if ( this.isInitialized ) { + if ( this.#isInitialized ) { return; } @@ -63,41 +333,35 @@ class ApplepayButton { return; } - this.log( 'Init', this.context ); + this.log( 'Init' ); this.initEventHandlers(); - this.isInitialized = true; + + this.#isInitialized = true; this.applePayConfig = config; - this.isEligible = - ( this.applePayConfig.isEligible && window.ApplePaySession ) || - this.buttonConfig.is_admin; - if ( this.isEligible ) { - this.fetchTransactionInfo().then( () => { - this.addButton(); - const id_minicart = - '#apple-' + this.buttonConfig.button.mini_cart_wrapper; - const id = '#apple-' + this.buttonConfig.button.wrapper; - - if ( this.context === 'mini-cart' ) { - document - .querySelector( id_minicart ) - ?.addEventListener( 'click', ( evt ) => { - evt.preventDefault(); - this.onButtonClick(); - } ); - } else { - document - .querySelector( id ) - ?.addEventListener( 'click', ( evt ) => { - evt.preventDefault(); - this.onButtonClick(); - } ); - } - } ); + if ( ! this.isEligible ) { + this.hide(); } else { - jQuery( '#' + this.buttonConfig.button.wrapper ).hide(); - jQuery( '#' + this.buttonConfig.button.mini_cart_wrapper ).hide(); - jQuery( '#express-payment-method-ppcp-applepay' ).hide(); + // Bail if the button wrapper is not present; handles mini-cart logic on checkout page. + if ( ! this.isPresent ) { + this.log( 'Abort init (no wrapper found)' ); + return; + } + + this.show(); + + this.fetchTransactionInfo().then( () => { + const button = this.addButton(); + + if ( ! button ) { + return; + } + + button.addEventListener( 'click', ( evt ) => { + evt.preventDefault(); + this.onButtonClick(); + } ); + } ); } } @@ -106,49 +370,51 @@ class ApplepayButton { return; } - this.isInitialized = false; + this.#isInitialized = false; this.init( this.applePayConfig ); } + /** + * Hides all wrappers that belong to this ApplePayButton instance. + */ + hide() { + this.log( 'Hide button' ); + this.allElements.forEach( ( element ) => { + element.style.display = 'none'; + } ); + } + + /** + * Ensures all wrapper elements of this ApplePayButton instance are visible. + */ + show() { + this.log( 'Show button' ); + if ( ! this.isPresent ) { + this.log( '!! Cannot show button, wrapper is not present' ); + return; + } + + // Classic Checkout/PayNow: Make the Apple Pay gateway visible after page load. + document + .querySelectorAll( 'style#ppcp-hide-apple-pay' ) + .forEach( ( el ) => el.remove() ); + + this.allElements.forEach( ( element ) => { + element.style.display = ''; + } ); + } + async fetchTransactionInfo() { this.transactionInfo = await this.contextHandler.transactionInfo(); } - /** - * Returns configurations relative to this button context. - */ - contextConfig() { - const config = { - wrapper: this.buttonConfig.button.wrapper, - ppcpStyle: this.ppcpConfig.button.style, - buttonStyle: this.buttonConfig.button.style, - ppcpButtonWrapper: this.ppcpConfig.button.wrapper, - }; - - if ( this.context === 'mini-cart' ) { - config.wrapper = this.buttonConfig.button.mini_cart_wrapper; - config.ppcpStyle = this.ppcpConfig.button.mini_cart_style; - config.buttonStyle = this.buttonConfig.button.mini_cart_style; - config.ppcpButtonWrapper = this.ppcpConfig.button.mini_cart_wrapper; - } - - if ( - [ 'cart-block', 'checkout-block' ].indexOf( this.context ) !== -1 - ) { - config.ppcpButtonWrapper = - '#express-payment-method-ppcp-gateway-paypal'; - } - - return config; - } - initEventHandlers() { - const { wrapper, ppcpButtonWrapper } = this.contextConfig(); - const wrapper_id = '#' + wrapper; + const ppcpButtonWrapper = `#${ this.ppcpButtonWrapperId }`; + const wrapperId = `#${ this.wrapperId }`; - if ( wrapper_id === ppcpButtonWrapper ) { + if ( wrapperId === ppcpButtonWrapper ) { throw new Error( - `[ApplePayButton] "wrapper" and "ppcpButtonWrapper" values must differ to avoid infinite loop. Current value: "${ wrapper_id }"` + `[ApplePayButton] "wrapper" and "ppcpButtonWrapper" values must differ to avoid infinite loop. Current value: "${ wrapperId }"` ); } @@ -158,9 +424,9 @@ class ApplepayButton { } const $ppcpButtonWrapper = jQuery( ppcpButtonWrapper ); - setVisible( wrapper_id, $ppcpButtonWrapper.is( ':visible' ) ); + setVisible( wrapperId, $ppcpButtonWrapper.is( ':visible' ) ); setEnabled( - wrapper_id, + wrapperId, ! $ppcpButtonWrapper.hasClass( 'ppcp-disabled' ) ); }; @@ -178,8 +444,9 @@ class ApplepayButton { } /** - * Starts an ApplePay session. - * @param paymentRequest + * Starts an Apple Pay session. + * + * @param {Object} paymentRequest The payment request object. */ applePaySession( paymentRequest ) { this.log( 'applePaySession', paymentRequest ); @@ -192,6 +459,7 @@ class ApplepayButton { session.onshippingcontactselected = this.onShippingContactSelected( session ); } + session.onvalidatemerchant = this.onValidateMerchant( session ); session.onpaymentauthorized = this.onPaymentAuthorized( session ); return session; @@ -199,32 +467,38 @@ class ApplepayButton { /** * Adds an Apple Pay purchase button. + * + * @return {HTMLElement|null} The newly created `` element. Null on failure. */ addButton() { - this.log( 'addButton', this.context ); + this.log( 'addButton' ); - const { wrapper, ppcpStyle } = this.contextConfig(); + const wrapper = this.wrapperElement; + const style = this.buttonStyle; + const id = 'apple-' + this.wrapperId; - const appleContainer = document.getElementById( wrapper ); - const type = this.buttonConfig.button.type; - const language = this.buttonConfig.button.lang; - const color = this.buttonConfig.button.color; - const id = 'apple-' + wrapper; - - if ( appleContainer ) { - appleContainer.innerHTML = ``; + if ( ! wrapper ) { + return null; } - const $wrapper = jQuery( '#' + wrapper ); - $wrapper.addClass( 'ppcp-button-' + ppcpStyle.shape ); + const ppcpStyle = this.ppcpStyle; + + wrapper.innerHTML = ``; + wrapper.classList.add( + `ppcp-button-${ ppcpStyle.shape }`, + 'ppcp-button-apm', + 'ppcp-button-applepay' + ); if ( ppcpStyle.height ) { - $wrapper.css( + wrapper.style.setProperty( '--apple-pay-button-height', `${ ppcpStyle.height }px` ); - $wrapper.css( 'height', `${ ppcpStyle.height }px` ); + wrapper.style.height = `${ ppcpStyle.height }px`; } + + return wrapper.querySelector( 'apple-pay-button' ); } //------------------------ @@ -235,19 +509,21 @@ class ApplepayButton { * Show Apple Pay payment sheet when Apple Pay payment button is clicked */ async onButtonClick() { - this.log( 'onButtonClick', this.context ); + this.log( 'onButtonClick' ); const paymentRequest = this.paymentRequest(); - window.ppcpFundingSource = 'apple_pay'; // Do this on another place like on create order endpoint handler. + // Do this on another place like on create order endpoint handler. + window.ppcpFundingSource = 'apple_pay'; // Trigger woocommerce validation if we are in the checkout page. - if ( this.context === 'checkout' ) { + if ( CONTEXT.Checkout === this.context ) { const checkoutFormSelector = 'form.woocommerce-checkout'; const errorHandler = new ErrorHandler( PayPalCommerceGateway.labels.error.generic, document.querySelector( '.woocommerce-notices-wrapper' ) ); + try { const formData = new FormData( document.querySelector( checkoutFormSelector ) @@ -269,6 +545,7 @@ class ApplepayButton { PayPalCommerceGateway.ajax.validate_checkout.nonce ) : null; + if ( formValidator ) { try { const errors = await formValidator.validate( @@ -296,13 +573,13 @@ class ApplepayButton { /** * If the button should show the shipping fields. * - * @return {false|*} + * @return {boolean} True, if shipping fields should be captured by ApplePay. */ shouldRequireShippingInButton() { return ( this.contextHandler.shippingAllowed() && this.buttonConfig.product.needShipping && - ( this.context !== 'checkout' || + ( CONTEXT.Checkout !== this.context || this.shouldUpdateButtonWithFormData() ) ); } @@ -310,10 +587,10 @@ class ApplepayButton { /** * If the button should be updated with the form addresses. * - * @return {boolean} + * @return {boolean} True, when Apple Pay data should be submitted to WooCommerce. */ shouldUpdateButtonWithFormData() { - if ( this.context !== 'checkout' ) { + if ( CONTEXT.Checkout !== this.context ) { return false; } return ( @@ -323,29 +600,28 @@ class ApplepayButton { } /** - * Indicates how payment completion should be handled if with the context handler default actions. - * Or with ApplePay module specific completion. + * Indicates how payment completion should be handled if with the context handler default + * actions. Or with Apple Pay module specific completion. * - * @return {boolean} + * @return {boolean} True, when the Apple Pay data should be submitted to WooCommerce. */ shouldCompletePaymentWithContextHandler() { // Data already handled, ex: PayNow if ( ! this.contextHandler.shippingAllowed() ) { return true; } + // Use WC form data mode in Checkout. - if ( - this.context === 'checkout' && + return ( + CONTEXT.Checkout === this.context && ! this.shouldUpdateButtonWithFormData() - ) { - return true; - } - return false; + ); } /** - * Updates ApplePay paymentRequest with form data. - * @param paymentRequest + * Updates Apple Pay paymentRequest with form data. + * + * @param {Object} paymentRequest Object to extend with form data. */ updateRequestDataWithForm( paymentRequest ) { if ( ! this.shouldUpdateButtonWithFormData() ) { @@ -358,8 +634,9 @@ class ApplepayButton { ); // Add custom data. - // "applicationData" is originating a "PayPalApplePayError: An internal server error has occurred" on paypal.Applepay().confirmOrder(). - // paymentRequest.applicationData = this.fillApplicationData(this.formData); + // "applicationData" is originating a "PayPalApplePayError: An internal server error has + // occurred" on paypal.Applepay().confirmOrder(). paymentRequest.applicationData = + // this.fillApplicationData(this.formData); if ( ! this.shouldRequireShippingInButton() ) { return; @@ -425,7 +702,8 @@ class ApplepayButton { 'email', 'phone', ], - requiredBillingContactFields: [ 'postalAddress' ], // ApplePay does not implement billing email and phone fields. + requiredBillingContactFields: [ 'postalAddress' ], // ApplePay does not implement billing + // email and phone fields. }; if ( ! this.shouldRequireShippingInButton() ) { @@ -453,14 +731,11 @@ class ApplepayButton { } refreshContextData() { - switch ( this.context ) { - case 'product': - // Refresh product data that makes the price change. - this.productQuantity = - document.querySelector( 'input.qty' )?.value; - this.products = this.contextHandler.products(); - this.log( 'Products updated', this.products ); - break; + if ( CONTEXT.Product === this.context ) { + // Refresh product data that makes the price change. + this.productQuantity = document.querySelector( 'input.qty' )?.value; + this.products = this.contextHandler.products(); + this.log( 'Products updated', this.products ); } } @@ -468,8 +743,36 @@ class ApplepayButton { // Payment process //------------------------ + /** + * Make ajax call to change the verification-status of the current domain. + * + * @param {boolean} isValid + */ + adminValidation( isValid ) { + // eslint-disable-next-line no-unused-vars + const ignored = fetch( this.buttonConfig.ajax_url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams( { + action: 'ppcp_validate', + 'woocommerce-process-checkout-nonce': this.nonce, + validation: isValid, + } ).toString(), + } ); + } + + /** + * Returns an event handler that Apple Pay calls when displaying the payment sheet. + * + * @see https://developer.apple.com/documentation/apple_pay_on_the_web/applepaysession/1778021-onvalidatemerchant + * + * @param {Object} session The ApplePaySession object. + * + * @return {(function(*): void)|*} Callback that runs after the merchant validation + */ onValidateMerchant( session ) { - this.log( 'onvalidatemerchant', this.buttonConfig.ajax_url ); return ( applePayValidateMerchantEvent ) => { this.log( 'onvalidatemerchant call' ); @@ -479,34 +782,15 @@ class ApplepayButton { validationUrl: applePayValidateMerchantEvent.validationURL, } ) .then( ( validateResult ) => { - this.log( 'onvalidatemerchant ok' ); session.completeMerchantValidation( validateResult.merchantSession ); - //call backend to update validation to true - jQuery.ajax( { - url: this.buttonConfig.ajax_url, - type: 'POST', - data: { - action: 'ppcp_validate', - validation: true, - 'woocommerce-process-checkout-nonce': this.nonce, - }, - } ); + + this.adminValidation( true ); } ) .catch( ( validateError ) => { - this.log( 'onvalidatemerchant error', validateError ); console.error( validateError ); - //call backend to update validation to false - jQuery.ajax( { - url: this.buttonConfig.ajax_url, - type: 'POST', - data: { - action: 'ppcp_validate', - validation: false, - 'woocommerce-process-checkout-nonce': this.nonce, - }, - } ); + this.adminValidation( false ); this.log( 'onvalidatemerchant session abort' ); session.abort(); } ); @@ -515,14 +799,14 @@ class ApplepayButton { onShippingMethodSelected( session ) { this.log( 'onshippingmethodselected', this.buttonConfig.ajax_url ); - const ajax_url = this.buttonConfig.ajax_url; + const ajaxUrl = this.buttonConfig.ajax_url; return ( event ) => { this.log( 'onshippingmethodselected call' ); const data = this.getShippingMethodData( event ); jQuery.ajax( { - url: ajax_url, + url: ajaxUrl, method: 'POST', data, success: ( @@ -537,7 +821,8 @@ class ApplepayButton { } this.selectedShippingMethod = event.shippingMethod; - // Sort the response shipping methods, so that the selected shipping method is the first one. + // Sort the response shipping methods, so that the selected shipping method is + // the first one. response.newShippingMethods = response.newShippingMethods.sort( ( a, b ) => { if ( @@ -565,7 +850,7 @@ class ApplepayButton { onShippingContactSelected( session ) { this.log( 'onshippingcontactselected', this.buttonConfig.ajax_url ); - const ajax_url = this.buttonConfig.ajax_url; + const ajaxUrl = this.buttonConfig.ajax_url; return ( event ) => { this.log( 'onshippingcontactselected call' ); @@ -573,7 +858,7 @@ class ApplepayButton { const data = this.getShippingContactData( event ); jQuery.ajax( { - url: ajax_url, + url: ajaxUrl, method: 'POST', data, success: ( @@ -603,15 +888,15 @@ class ApplepayButton { } getShippingContactData( event ) { - const product_id = this.buttonConfig.product.id; + const productId = this.buttonConfig.product.id; this.refreshContextData(); switch ( this.context ) { - case 'product': + case CONTEXT.Product: return { action: 'ppcp_update_shipping_contact', - product_id, + product_id: productId, products: JSON.stringify( this.products ), caller_page: 'productDetail', product_quantity: this.productQuantity, @@ -619,11 +904,12 @@ class ApplepayButton { need_shipping: this.shouldRequireShippingInButton(), 'woocommerce-process-checkout-nonce': this.nonce, }; - case 'cart': - case 'checkout': - case 'cart-block': - case 'checkout-block': - case 'mini-cart': + + case CONTEXT.Cart: + case CONTEXT.Checkout: + case CONTEXT.BlockCart: + case CONTEXT.BlockCheckout: + case CONTEXT.MiniCart: return { action: 'ppcp_update_shipping_contact', simplified_contact: event.shippingContact, @@ -635,12 +921,12 @@ class ApplepayButton { } getShippingMethodData( event ) { - const product_id = this.buttonConfig.product.id; + const productId = this.buttonConfig.product.id; this.refreshContextData(); switch ( this.context ) { - case 'product': + case CONTEXT.Product: return { action: 'ppcp_update_shipping_method', shipping_method: event.shippingMethod, @@ -650,17 +936,18 @@ class ApplepayButton { ? this.updatedContactInfo : this.initialPaymentRequest?.shippingContact ?? this.initialPaymentRequest?.billingContact, - product_id, + product_id: productId, products: JSON.stringify( this.products ), caller_page: 'productDetail', product_quantity: this.productQuantity, 'woocommerce-process-checkout-nonce': this.nonce, }; - case 'cart': - case 'checkout': - case 'cart-block': - case 'checkout-block': - case 'mini-cart': + + case CONTEXT.Cart: + case CONTEXT.Checkout: + case CONTEXT.BlockCart: + case CONTEXT.BlockCheckout: + case CONTEXT.MiniCart: return { action: 'ppcp_update_shipping_method', shipping_method: event.shippingMethod, @@ -681,9 +968,6 @@ class ApplepayButton { return async ( event ) => { this.log( 'onpaymentauthorized call' ); - function form() { - return document.querySelector( 'form.cart' ); - } const processInWooAndCapture = async ( data ) => { return new Promise( ( resolve, reject ) => { try { @@ -698,7 +982,7 @@ class ApplepayButton { ( this.initialPaymentRequest.shippingMethods || [] )[ 0 ]; - const request_data = { + const requestData = { action: 'ppcp_create_order', caller_page: this.context, product_id: this.buttonConfig.product.id ?? null, @@ -723,7 +1007,7 @@ class ApplepayButton { jQuery.ajax( { url: this.buttonConfig.ajax_url, method: 'POST', - data: request_data, + data: requestData, complete: ( jqXHR, textStatus ) => { this.log( 'onpaymentauthorized complete' ); }, @@ -785,7 +1069,8 @@ class ApplepayButton { if ( this.shouldCompletePaymentWithContextHandler() ) { - // No shipping, expect immediate capture, ex: PayNow, Checkout with form data. + // No shipping, expect immediate capture, ex: PayNow, Checkout with + // form data. let approveFailed = false; await this.contextHandler.approveOrder( @@ -960,4 +1245,4 @@ class ApplepayButton { } } -export default ApplepayButton; +export default ApplePayButton; diff --git a/modules/ppcp-applepay/resources/js/ApplepayManager.js b/modules/ppcp-applepay/resources/js/ApplepayManager.js index 0992d9e7d..264610f28 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayManager.js +++ b/modules/ppcp-applepay/resources/js/ApplepayManager.js @@ -1,7 +1,9 @@ -import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher'; -import ApplepayButton from './ApplepayButton'; +/* global paypal */ -class ApplepayManager { +import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher'; +import ApplePayButton from './ApplepayButton'; + +class ApplePayManager { constructor( buttonConfig, ppcpConfig ) { this.buttonConfig = buttonConfig; this.ppcpConfig = ppcpConfig; @@ -9,7 +11,7 @@ class ApplepayManager { this.buttons = []; buttonModuleWatcher.watchContextBootstrap( ( bootstrap ) => { - const button = new ApplepayButton( + const button = new ApplePayButton( bootstrap.context, bootstrap.handler, buttonConfig, @@ -40,8 +42,7 @@ class ApplepayManager { } /** - * Gets ApplePay configuration of the PayPal merchant. - * @return {Promise} + * Gets Apple Pay configuration of the PayPal merchant. */ async config() { this.ApplePayConfig = await paypal.Applepay().config(); @@ -49,4 +50,4 @@ class ApplepayManager { } } -export default ApplepayManager; +export default ApplePayManager; diff --git a/modules/ppcp-applepay/resources/js/ApplepayManagerBlockEditor.js b/modules/ppcp-applepay/resources/js/ApplepayManagerBlockEditor.js index 2f4db9d41..1c10bb997 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayManagerBlockEditor.js +++ b/modules/ppcp-applepay/resources/js/ApplepayManagerBlockEditor.js @@ -1,6 +1,8 @@ -import ApplepayButton from './ApplepayButton'; +/* global paypal */ -class ApplepayManagerBlockEditor { +import ApplePayButton from './ApplepayButton'; + +class ApplePayManagerBlockEditor { constructor( buttonConfig, ppcpConfig ) { this.buttonConfig = buttonConfig; this.ppcpConfig = ppcpConfig; @@ -17,7 +19,7 @@ class ApplepayManagerBlockEditor { try { this.applePayConfig = await paypal.Applepay().config(); - const button = new ApplepayButton( + const button = new ApplePayButton( this.ppcpConfig.context, null, this.buttonConfig, @@ -31,4 +33,4 @@ class ApplepayManagerBlockEditor { } } -export default ApplepayManagerBlockEditor; +export default ApplePayManagerBlockEditor; diff --git a/modules/ppcp-applepay/resources/js/Context/PreviewHandler.js b/modules/ppcp-applepay/resources/js/Context/PreviewHandler.js index 8740705e9..5febfe0c3 100644 --- a/modules/ppcp-applepay/resources/js/Context/PreviewHandler.js +++ b/modules/ppcp-applepay/resources/js/Context/PreviewHandler.js @@ -1,10 +1,6 @@ import BaseHandler from './BaseHandler'; class PreviewHandler extends BaseHandler { - constructor( buttonConfig, ppcpConfig, externalHandler ) { - super( buttonConfig, ppcpConfig, externalHandler ); - } - transactionInfo() { // We need to return something as ApplePay button initialization expects valid data. return { @@ -19,7 +15,7 @@ class PreviewHandler extends BaseHandler { throw new Error( 'Create order fail. This is just a preview.' ); } - approveOrder( data, actions ) { + approveOrder() { throw new Error( 'Approve order fail. This is just a preview.' ); } diff --git a/modules/ppcp-applepay/resources/js/Helper/utils.js b/modules/ppcp-applepay/resources/js/Helper/utils.js index 3b8edc2c8..793fe7f42 100644 --- a/modules/ppcp-applepay/resources/js/Helper/utils.js +++ b/modules/ppcp-applepay/resources/js/Helper/utils.js @@ -1,4 +1,4 @@ -export const buttonID = 'applepay-container'; +export const buttonID = 'ppc-button-applepay-container'; export const endpoints = { validation: '_apple_pay_validation', createOrderCart: '_apple_pay_create_order_cart', diff --git a/modules/ppcp-applepay/resources/js/boot-admin.js b/modules/ppcp-applepay/resources/js/boot-admin.js index 080d7c8aa..81d8c0b7b 100644 --- a/modules/ppcp-applepay/resources/js/boot-admin.js +++ b/modules/ppcp-applepay/resources/js/boot-admin.js @@ -1,4 +1,4 @@ -import ApplepayButton from './ApplepayButton'; +import ApplePayButton from './ApplepayButton'; import PreviewButton from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButton'; import PreviewButtonManager from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager'; @@ -86,7 +86,7 @@ class ApplePayPreviewButton extends PreviewButton { } createButton( buttonConfig ) { - const button = new ApplepayButton( + const button = new ApplePayButton( 'preview', null, buttonConfig, diff --git a/modules/ppcp-applepay/resources/js/boot-block.js b/modules/ppcp-applepay/resources/js/boot-block.js index 8445466eb..c447cb064 100644 --- a/modules/ppcp-applepay/resources/js/boot-block.js +++ b/modules/ppcp-applepay/resources/js/boot-block.js @@ -4,8 +4,8 @@ import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Help import { cartHasSubscriptionProducts } from '../../../ppcp-blocks/resources/js/Helper/Subscription'; import { loadCustomScript } from '@paypal/paypal-js'; import CheckoutHandler from './Context/CheckoutHandler'; -import ApplepayManager from './ApplepayManager'; -import ApplepayManagerBlockEditor from './ApplepayManagerBlockEditor'; +import ApplePayManager from './ApplepayManager'; +import ApplePayManagerBlockEditor from './ApplepayManagerBlockEditor'; const ppcpData = wc.wcSettings.getSetting( 'ppcp-gateway_data' ); const ppcpConfig = ppcpData.scriptData; @@ -24,8 +24,8 @@ const ApplePayComponent = ( props ) => { const bootstrap = function () { const ManagerClass = props.isEditing - ? ApplepayManagerBlockEditor - : ApplepayManager; + ? ApplePayManagerBlockEditor + : ApplePayManager; const manager = new ManagerClass( buttonConfig, ppcpConfig ); manager.init(); }; diff --git a/modules/ppcp-applepay/resources/js/boot.js b/modules/ppcp-applepay/resources/js/boot.js index aee735231..8eddafbcb 100644 --- a/modules/ppcp-applepay/resources/js/boot.js +++ b/modules/ppcp-applepay/resources/js/boot.js @@ -1,13 +1,13 @@ import { loadCustomScript } from '@paypal/paypal-js'; import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'; -import ApplepayManager from './ApplepayManager'; +import ApplePayManager from './ApplepayManager'; import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper'; ( function ( { buttonConfig, ppcpConfig, jQuery } ) { let manager; const bootstrap = function () { - manager = new ApplepayManager( buttonConfig, ppcpConfig ); + manager = new ApplePayManager( buttonConfig, ppcpConfig ); manager.init(); }; diff --git a/modules/ppcp-applepay/services.php b/modules/ppcp-applepay/services.php index e4a3297dc..76de2150d 100644 --- a/modules/ppcp-applepay/services.php +++ b/modules/ppcp-applepay/services.php @@ -299,5 +299,15 @@ return array( esc_html( $button_text ) ); }, + 'applepay.wc-gateway' => static function ( ContainerInterface $container ): ApplePayGateway { + return new ApplePayGateway( + $container->get( 'wcgateway.order-processor' ), + $container->get( 'api.factory.paypal-checkout-url' ), + $container->get( 'wcgateway.processor.refunds' ), + $container->get( 'wcgateway.transaction-url-provider' ), + $container->get( 'session.handler' ), + $container->get( 'applepay.url' ) + ); + }, ); diff --git a/modules/ppcp-applepay/src/ApplePayGateway.php b/modules/ppcp-applepay/src/ApplePayGateway.php new file mode 100644 index 000000000..1cccda0f2 --- /dev/null +++ b/modules/ppcp-applepay/src/ApplePayGateway.php @@ -0,0 +1,231 @@ +id = self::ID; + + $this->method_title = __( 'Apple Pay (via PayPal) ', 'woocommerce-paypal-payments' ); + $this->method_description = __( 'Display Apple Pay as a standalone payment option instead of bundling it with PayPal.', 'woocommerce-paypal-payments' ); + + $this->title = $this->get_option( 'title', __( 'Apple Pay', 'woocommerce-paypal-payments' ) ); + $this->description = $this->get_option( 'description', '' ); + + $this->module_url = $module_url; + $this->icon = esc_url( $this->module_url ) . 'assets/images/applepay.png'; + + $this->init_form_fields(); + $this->init_settings(); + $this->order_processor = $order_processor; + $this->paypal_checkout_url_factory = $paypal_checkout_url_factory; + $this->refund_processor = $refund_processor; + $this->transaction_url_provider = $transaction_url_provider; + $this->session_handler = $session_handler; + + add_action( + 'woocommerce_update_options_payment_gateways_' . $this->id, + array( $this, 'process_admin_options' ) + ); + } + + /** + * Initialize the form fields. + */ + public function init_form_fields() { + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ), + 'type' => 'checkbox', + 'label' => __( 'Enable Apple Pay', 'woocommerce-paypal-payments' ), + 'default' => 'no', + 'desc_tip' => true, + 'description' => __( 'Enable/Disable Apple Pay payment gateway.', 'woocommerce-paypal-payments' ), + ), + 'title' => array( + 'title' => __( 'Title', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'default' => $this->title, + 'desc_tip' => true, + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-paypal-payments' ), + ), + 'description' => array( + 'title' => __( 'Description', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'default' => $this->description, + 'desc_tip' => true, + 'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-paypal-payments' ), + ), + ); + } + + /** + * Process payment for a WooCommerce order. + * + * @param int $order_id The WooCommerce order id. + * + * @return array + */ + public function process_payment( $order_id ) : array { + $wc_order = wc_get_order( $order_id ); + if ( ! is_a( $wc_order, WC_Order::class ) ) { + return $this->handle_payment_failure( + null, + new GatewayGenericException( new Exception( 'WC order was not found.' ) ) + ); + } + + do_action( 'woocommerce_paypal_payments_before_process_order', $wc_order ); + + try { + try { + $this->order_processor->process( $wc_order ); + + do_action( 'woocommerce_paypal_payments_before_handle_payment_success', $wc_order ); + + return $this->handle_payment_success( $wc_order ); + } catch ( PayPalOrderMissingException $exc ) { + $order = $this->order_processor->create_order( $wc_order ); + + return array( + 'result' => 'success', + 'redirect' => ( $this->paypal_checkout_url_factory )( $order->id() ), + ); + } + } catch ( PayPalApiException $error ) { + return $this->handle_payment_failure( + $wc_order, + new Exception( + Messages::generic_payment_error_message() . ' ' . $error->getMessage(), + $error->getCode(), + $error + ) + ); + } catch ( Exception $error ) { + return $this->handle_payment_failure( $wc_order, $error ); + } + } + + /** + * Process refund. + * + * If the gateway declares 'refunds' support, this will allow it to refund. + * a passed in amount. + * + * @param int $order_id Order ID. + * @param float $amount Refund amount. + * @param string $reason Refund reason. + * + * @return boolean True or false based on success, or a WP_Error object. + */ + public function process_refund( $order_id, $amount = null, $reason = '' ) : bool { + $order = wc_get_order( $order_id ); + if ( ! is_a( $order, WC_Order::class ) ) { + return false; + } + + return $this->refund_processor->process( $order, (float) $amount, (string) $reason ); + } + + /** + * Return transaction url for this gateway and given order. + * + * @param WC_Order $order WC order to get transaction url by. + * + * @return string + */ + public function get_transaction_url( $order ) : string { + $this->view_transaction_url = $this->transaction_url_provider->get_transaction_url_base( $order ); + + return parent::get_transaction_url( $order ); + } +} diff --git a/modules/ppcp-applepay/src/ApplepayModule.php b/modules/ppcp-applepay/src/ApplepayModule.php index 409a9c81e..878d4b322 100644 --- a/modules/ppcp-applepay/src/ApplepayModule.php +++ b/modules/ppcp-applepay/src/ApplepayModule.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Applepay; +use WC_Payment_Gateway; use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry; use WooCommerce\PayPalCommerce\Applepay\Assets\ApplePayButton; use WooCommerce\PayPalCommerce\Applepay\Assets\AppleProductStatus; @@ -117,6 +118,48 @@ class ApplepayModule implements ModuleInterface { 100, 2 ); + + add_filter( + 'woocommerce_payment_gateways', + /** + * Param types removed to avoid third-party issues. + * + * @psalm-suppress MissingClosureParamType + */ + static function ( $methods ) use ( $c ): array { + if ( ! is_array( $methods ) ) { + return $methods; + } + + $settings = $c->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + + if ( $settings->has( 'applepay_button_enabled' ) && $settings->get( 'applepay_button_enabled' ) ) { + $applepay_gateway = $c->get( 'applepay.wc-gateway' ); + assert( $applepay_gateway instanceof WC_Payment_Gateway ); + + $methods[] = $applepay_gateway; + } + + return $methods; + } + ); + + add_action( + 'woocommerce_review_order_after_submit', + function () { + // Wrapper ID: #ppc-button-ppcp-applepay. + echo '
'; + } + ); + + add_action( + 'woocommerce_pay_order_after_submit', + function () { + // Wrapper ID: #ppc-button-ppcp-applepay. + echo '
'; + } + ); } /** @@ -306,7 +349,7 @@ class ApplepayModule implements ModuleInterface { * @param bool $is_sandbox The environment for this merchant. * @return string */ - public function validation_string( bool $is_sandbox ) { + public function validation_string( bool $is_sandbox ) : string { $sandbox_string = $this->sandbox_validation_string(); $live_string = $this->live_validation_string(); return $is_sandbox ? $sandbox_string : $live_string; diff --git a/modules/ppcp-applepay/src/Assets/ApplePayButton.php b/modules/ppcp-applepay/src/Assets/ApplePayButton.php index 926fc5ce2..bfcc88133 100644 --- a/modules/ppcp-applepay/src/Assets/ApplePayButton.php +++ b/modules/ppcp-applepay/src/Assets/ApplePayButton.php @@ -20,12 +20,13 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\Webhooks\Handler\RequestHandlerTrait; +use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; /** * Class ApplePayButton */ class ApplePayButton implements ButtonInterface { - use RequestHandlerTrait; + use RequestHandlerTrait, ContextTrait; /** * The settings. @@ -340,7 +341,7 @@ class ApplePayButton implements ButtonInterface { } $response = $this->response_templates->apple_formatted_response( $payment_details ); $this->response_templates->response_success( $response ); - } catch ( \Exception $e ) { + } catch ( Exception $e ) { $this->response_templates->response_with_data_errors( array( array( @@ -382,7 +383,7 @@ class ApplePayButton implements ButtonInterface { } $response = $this->response_templates->apple_formatted_response( $payment_details ); $this->response_templates->response_success( $response ); - } catch ( \Exception $e ) { + } catch ( Exception $e ) { $this->response_templates->response_with_data_errors( array( array( @@ -399,7 +400,7 @@ class ApplePayButton implements ButtonInterface { * On error returns an array of errors to be handled by the script * On success returns the new order data * - * @throws \Exception When validation fails. + * @throws Exception When validation fails. */ public function create_wc_order(): void { $applepay_request_data_object = $this->applepay_data_object_http(); @@ -420,15 +421,18 @@ class ApplePayButton implements ButtonInterface { $applepay_request_data_object->order_data( $context ); $this->update_posted_data( $applepay_request_data_object ); + if ( $context === 'product' ) { $cart_item_key = $this->prepare_cart( $applepay_request_data_object ); $cart = WC()->cart; $address = $applepay_request_data_object->shipping_address(); + $this->calculate_totals_single_product( $cart, $address, $applepay_request_data_object->shipping_method() ); + if ( ! $cart_item_key ) { $this->response_templates->response_with_data_errors( array( @@ -438,19 +442,16 @@ class ApplePayButton implements ButtonInterface { ), ) ); - return; - } - add_filter( - 'woocommerce_payment_successful_result', - function ( array $result ) use ( $cart, $cart_item_key ) : array { - if ( ! is_string( $cart_item_key ) ) { + } else { + add_filter( + 'woocommerce_payment_successful_result', + function ( array $result ) use ( $cart, $cart_item_key ) : array { + $this->clear_current_cart( $cart, $cart_item_key ); + $this->reload_cart( $cart ); return $result; } - $this->clear_current_cart( $cart, $cart_item_key ); - $this->reload_cart( $cart ); - return $result; - } - ); + ); + } } WC()->checkout()->process_checkout(); @@ -460,17 +461,20 @@ class ApplePayButton implements ButtonInterface { /** * Checks if the nonce in the data object is valid * - * @return bool|int + * @return bool */ protected function is_nonce_valid(): bool { $nonce = filter_input( INPUT_POST, 'woocommerce-process-checkout-nonce', FILTER_SANITIZE_SPECIAL_CHARS ); if ( ! $nonce ) { return false; } - return wp_verify_nonce( + + // Return value 1 indicates "valid nonce, generated in past 12 hours". + // Return value 2 also indicated valid nonce, but older than 12 hours. + return 1 === wp_verify_nonce( $nonce, 'woocommerce-process_checkout' - ) === 1; + ); } /** @@ -511,7 +515,7 @@ class ApplePayButton implements ButtonInterface { $address, $applepay_request_data_object->shipping_method() ); - if ( is_string( $cart_item_key ) ) { + if ( $cart_item_key ) { $this->clear_current_cart( $cart, $cart_item_key ); $this->reload_cart( $cart ); } @@ -819,9 +823,9 @@ class ApplePayButton implements ButtonInterface { /** * Removes the old cart, saves it, and creates a new one * + * @throws Exception If it cannot be added to cart. * @param ApplePayDataObjectHttp $applepay_request_data_object The request data object. - * @return bool | string The cart item key after adding to the new cart. - * @throws \Exception If it cannot be added to cart. + * @return string The cart item key after adding to the new cart. */ public function prepare_cart( ApplePayDataObjectHttp $applepay_request_data_object ): string { $this->save_old_cart(); @@ -838,7 +842,7 @@ class ApplePayButton implements ButtonInterface { ); $this->cart_products->add_products( array( $product ) ); - return $this->cart_products->cart_item_keys()[0]; + return $this->cart_products->cart_item_keys()[0] ?? ''; } /** @@ -949,6 +953,7 @@ class ApplePayButton implements ButtonInterface { $render_placeholder, function () { $this->applepay_button(); + $this->hide_gateway_until_eligible(); }, 21 ); @@ -961,6 +966,7 @@ class ApplePayButton implements ButtonInterface { $render_placeholder, function () { $this->applepay_button(); + $this->hide_gateway_until_eligible(); }, 21 ); @@ -973,7 +979,7 @@ class ApplePayButton implements ButtonInterface { add_action( $render_placeholder, function () { - echo ''; + echo ''; }, 21 ); @@ -981,24 +987,29 @@ class ApplePayButton implements ButtonInterface { return true; } + /** * ApplePay button markup */ protected function applepay_button(): void { ?> -
+
+ + data_for_product_page( - $shop_country_code, - $currency_code, - $total_label - ); + return $this->data_for_product_page(); } - return $this->data_for_cart_page( - $shop_country_code, - $currency_code, - $total_label - ); + return $this->data_for_cart_page(); } /** * Returns the appropriate admin data to send to ApplePay script * * @return array - * @throws NotFoundException When the setting is not found. */ public function apple_pay_script_data_for_admin() : array { + return $this->data_for_admin_page(); + } + + /** + * Returns the full config array for the Apple Pay integration with default values. + * + * @param array $product - Optional. Product details for the payment button. + * + * @return array + */ + private function get_apple_pay_data( array $product = array() ) : array { + // true: Use Apple Pay as distinct gateway. + // false: integrate it with the smart buttons. + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + $is_wc_gateway_enabled = isset( $available_gateways[ ApplePayGateway::ID ] ); + + // use_wc: Use WC checkout data + // use_applepay: Use data provided by Apple Pay. + $checkout_data_mode = $this->settings->has( 'applepay_checkout_data_mode' ) + ? $this->settings->get( 'applepay_checkout_data_mode' ) + : PropertiesDictionary::BILLING_DATA_MODE_DEFAULT; + + // Store country, currency and name. $base_location = wc_get_base_location(); $shop_country_code = $base_location['country']; $currency_code = get_woocommerce_currency(); $total_label = get_bloginfo( 'name' ); - return $this->data_for_admin_page( - $shop_country_code, - $currency_code, - $total_label + // Button layout (label, color, language). + $type = $this->settings->has( 'applepay_button_type' ) ? $this->settings->get( 'applepay_button_type' ) : ''; + $color = $this->settings->has( 'applepay_button_color' ) ? $this->settings->get( 'applepay_button_color' ) : ''; + $lang = $this->settings->has( 'applepay_button_language' ) ? $this->settings->get( 'applepay_button_language' ) : ''; + $lang = apply_filters( 'woocommerce_paypal_payments_applepay_button_language', $lang ); + $is_enabled = $this->settings->has( 'applepay_button_enabled' ) && $this->settings->get( 'applepay_button_enabled' ); + + return array( + 'sdk_url' => $this->sdk_url, + 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG, + 'is_admin' => false, + 'is_enabled' => $is_enabled, + 'is_wc_gateway_enabled' => $is_wc_gateway_enabled, + 'preferences' => array( + 'checkout_data_mode' => $checkout_data_mode, + ), + 'button' => array( + 'wrapper' => 'ppc-button-applepay-container', + 'mini_cart_wrapper' => 'ppc-button-applepay-container-minicart', + 'type' => $type, + 'color' => $color, + 'lang' => $lang, + ), + 'product' => $product, + 'shop' => array( + 'countryCode' => $shop_country_code, + 'currencyCode' => $currency_code, + 'totalLabel' => $total_label, + ), + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'woocommerce-process_checkout' ), ); } /** * Check if the product needs shipping * - * @param \WC_Product $product The product. + * @param WC_Product $product Product to check. * * @return bool */ - protected function check_if_need_shipping( $product ) { + protected function check_if_need_shipping( WC_Product $product ) : bool { if ( ! wc_shipping_enabled() || 0 === wc_get_shipping_method_count( @@ -104,30 +141,20 @@ class DataToAppleButtonScripts { ) { return false; } - $needs_shipping = false; if ( $product->needs_shipping() ) { - $needs_shipping = true; + return true; } - return $needs_shipping; + return false; } /** * Prepares the data for the product page. * - * @param string $shop_country_code The shop country code. - * @param string $currency_code The currency code. - * @param string $total_label The label for the total amount. - * * @return array - * @throws NotFoundException When the setting is not found. */ - protected function data_for_product_page( - $shop_country_code, - $currency_code, - $total_label - ) { + protected function data_for_product_page() : array { $product = wc_get_product( get_the_id() ); if ( ! $product ) { return array(); @@ -136,146 +163,59 @@ class DataToAppleButtonScripts { if ( $product->get_type() === 'variable' || $product->get_type() === 'variable-subscription' ) { $is_variation = true; } + $product_need_shipping = $this->check_if_need_shipping( $product ); $product_id = get_the_id(); $product_price = $product->get_price(); $product_stock = $product->get_stock_status(); - $type = $this->settings->has( 'applepay_button_type' ) ? $this->settings->get( 'applepay_button_type' ) : ''; - $color = $this->settings->has( 'applepay_button_color' ) ? $this->settings->get( 'applepay_button_color' ) : ''; - $lang = $this->settings->has( 'applepay_button_language' ) ? $this->settings->get( 'applepay_button_language' ) : ''; - $checkout_data_mode = $this->settings->has( 'applepay_checkout_data_mode' ) ? $this->settings->get( 'applepay_checkout_data_mode' ) : PropertiesDictionary::BILLING_DATA_MODE_DEFAULT; - return array( - 'sdk_url' => $this->sdk_url, - 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false, - 'is_admin' => false, - 'preferences' => array( - 'checkout_data_mode' => $checkout_data_mode, - ), - 'button' => array( - 'wrapper' => 'applepay-container', - 'mini_cart_wrapper' => 'applepay-container-minicart', - 'type' => $type, - 'color' => $color, - 'lang' => $lang, - ), - 'product' => array( + return $this->get_apple_pay_data( + array( 'needShipping' => $product_need_shipping, 'id' => $product_id, 'price' => $product_price, 'isVariation' => $is_variation, 'stock' => $product_stock, - ), - 'shop' => array( - 'countryCode' => $shop_country_code, - 'currencyCode' => $currency_code, - 'totalLabel' => $total_label, - ), - 'ajax_url' => admin_url( 'admin-ajax.php' ), - 'nonce' => wp_create_nonce( 'woocommerce-process_checkout' ), + ) ); } /** * Prepares the data for the cart page. * - * @param string $shop_country_code The shop country code. - * @param string $currency_code The currency code. - * @param string $total_label The label for the total amount. - * * @return array */ - protected function data_for_cart_page( - $shop_country_code, - $currency_code, - $total_label - ) { + protected function data_for_cart_page() : array { $cart = WC()->cart; if ( ! $cart ) { return array(); } - $type = $this->settings->has( 'applepay_button_type' ) ? $this->settings->get( 'applepay_button_type' ) : ''; - $color = $this->settings->has( 'applepay_button_color' ) ? $this->settings->get( 'applepay_button_color' ) : ''; - $lang = $this->settings->has( 'applepay_button_language' ) ? $this->settings->get( 'applepay_button_language' ) : ''; - $lang = apply_filters( 'woocommerce_paypal_payments_applepay_button_language', $lang ); - $checkout_data_mode = $this->settings->has( 'applepay_checkout_data_mode' ) ? $this->settings->get( 'applepay_checkout_data_mode' ) : PropertiesDictionary::BILLING_DATA_MODE_DEFAULT; - - return array( - 'sdk_url' => $this->sdk_url, - 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false, - 'is_admin' => false, - 'preferences' => array( - 'checkout_data_mode' => $checkout_data_mode, - ), - 'button' => array( - 'wrapper' => 'applepay-container', - 'mini_cart_wrapper' => 'applepay-container-minicart', - 'type' => $type, - 'color' => $color, - 'lang' => $lang, - ), - 'product' => array( + return $this->get_apple_pay_data( + array( 'needShipping' => $cart->needs_shipping(), 'subtotal' => $cart->get_subtotal(), - ), - 'shop' => array( - 'countryCode' => $shop_country_code, - 'currencyCode' => $currency_code, - 'totalLabel' => $total_label, - ), - 'ajax_url' => admin_url( 'admin-ajax.php' ), - 'nonce' => wp_create_nonce( 'woocommerce-process_checkout' ), + ) ); } /** * Prepares the data for the cart page. - * Consider refactoring this method along with data_for_cart_page() and data_for_product_page() methods. - * - * @param string $shop_country_code The shop country code. - * @param string $currency_code The currency code. - * @param string $total_label The label for the total amount. + * Consider refactoring this method along with data_for_cart_page() and data_for_product_page() + * methods. * * @return array */ - protected function data_for_admin_page( - $shop_country_code, - $currency_code, - $total_label - ) { - $type = $this->settings->has( 'applepay_button_type' ) ? $this->settings->get( 'applepay_button_type' ) : ''; - $color = $this->settings->has( 'applepay_button_color' ) ? $this->settings->get( 'applepay_button_color' ) : ''; - $lang = $this->settings->has( 'applepay_button_language' ) ? $this->settings->get( 'applepay_button_language' ) : ''; - $lang = apply_filters( 'woocommerce_paypal_payments_applepay_button_language', $lang ); - $checkout_data_mode = $this->settings->has( 'applepay_checkout_data_mode' ) ? $this->settings->get( 'applepay_checkout_data_mode' ) : PropertiesDictionary::BILLING_DATA_MODE_DEFAULT; - $is_enabled = $this->settings->has( 'applepay_button_enabled' ) && $this->settings->get( 'applepay_button_enabled' ); - - return array( - 'sdk_url' => $this->sdk_url, - 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG, - 'is_admin' => true, - 'is_enabled' => $is_enabled, - 'preferences' => array( - 'checkout_data_mode' => $checkout_data_mode, - ), - 'button' => array( - 'wrapper' => 'applepay-container', - 'mini_cart_wrapper' => 'applepay-container-minicart', - 'type' => $type, - 'color' => $color, - 'lang' => $lang, - ), - 'product' => array( + protected function data_for_admin_page() : array { + $data = $this->get_apple_pay_data( + array( 'needShipping' => false, 'subtotal' => 0, - ), - 'shop' => array( - 'countryCode' => $shop_country_code, - 'currencyCode' => $currency_code, - 'totalLabel' => $total_label, - ), - 'ajax_url' => admin_url( 'admin-ajax.php' ), + ) ); + + $data['is_admin'] = true; + + return $data; } } diff --git a/modules/ppcp-axo/extensions.php b/modules/ppcp-axo/extensions.php index 8d8bf4378..636aee38e 100644 --- a/modules/ppcp-axo/extensions.php +++ b/modules/ppcp-axo/extensions.php @@ -83,7 +83,7 @@ return array( ->rule() ->condition_element( 'axo_enabled', '1' ) ->action_visible( 'axo_gateway_title' ) - ->action_visible( 'axo_checkout_config_notice' ) + ->action_visible( 'axo_main_notice' ) ->action_visible( 'axo_privacy' ) ->action_visible( 'axo_name_on_card' ) ->action_visible( 'axo_style_heading' ) @@ -114,9 +114,17 @@ return array( ), 'classes' => array( 'ppcp-valign-label-middle', 'ppcp-align-label-center' ), ), - 'axo_checkout_config_notice' => array( + 'axo_main_notice' => array( 'heading' => '', - 'html' => $container->get( 'axo.checkout-config-notice' ), + 'html' => implode( + '', + array( + $container->get( 'axo.settings-conflict-notice' ), + $container->get( 'axo.shipping-config-notice' ), + $container->get( 'axo.checkout-config-notice' ), + $container->get( 'axo.incompatible-plugins-notice' ), + ) + ), 'type' => 'ppcp-html', 'classes' => array( 'ppcp-field-indent' ), 'class' => array(), diff --git a/modules/ppcp-axo/resources/js/AxoManager.js b/modules/ppcp-axo/resources/js/AxoManager.js index b0837bcd6..34b8d24c5 100644 --- a/modules/ppcp-axo/resources/js/AxoManager.js +++ b/modules/ppcp-axo/resources/js/AxoManager.js @@ -709,6 +709,8 @@ class AxoManager { }` ); + this.emailInput.value = this.stripSpaces( this.emailInput.value ); + this.$( this.el.paymentContainer.selector + '-detail' ).html( '' ); this.$( this.el.paymentContainer.selector + '-form' ).html( '' ); @@ -1134,6 +1136,10 @@ class AxoManager { return emailPattern.test( value ); } + stripSpaces( str ) { + return str.replace( /\s+/g, '' ); + } + validateEmail( billingEmail ) { const billingEmailSelector = document.querySelector( billingEmail ); const value = document.querySelector( billingEmail + ' input' ).value; diff --git a/modules/ppcp-axo/resources/js/Helper/Debug.js b/modules/ppcp-axo/resources/js/Helper/Debug.js index 84cda012c..6345ee935 100644 --- a/modules/ppcp-axo/resources/js/Helper/Debug.js +++ b/modules/ppcp-axo/resources/js/Helper/Debug.js @@ -1,7 +1,19 @@ export function log( message, level = 'info' ) { const wpDebug = window.wc_ppcp_axo?.wp_debug; const endpoint = window.wc_ppcp_axo?.ajax?.frontend_logger?.endpoint; - if ( ! endpoint ) { + const loggingEnabled = window.wc_ppcp_axo?.logging_enabled; + + if ( wpDebug ) { + switch ( level ) { + case 'error': + console.error( `[AXO] ${ message }` ); + break; + default: + console.log( `[AXO] ${ message }` ); + } + } + + if ( ! endpoint || ! loggingEnabled ) { return; } @@ -15,15 +27,5 @@ export function log( message, level = 'info' ) { level, }, } ), - } ).then( () => { - if ( wpDebug ) { - switch ( level ) { - case 'error': - console.error( `[AXO] ${ message }` ); - break; - default: - console.log( `[AXO] ${ message }` ); - } - } } ); } diff --git a/modules/ppcp-axo/services.php b/modules/ppcp-axo/services.php index 9d4e6d5fe..5b468835f 100644 --- a/modules/ppcp-axo/services.php +++ b/modules/ppcp-axo/services.php @@ -12,10 +12,10 @@ namespace WooCommerce\PayPalCommerce\Axo; use WooCommerce\PayPalCommerce\Axo\Assets\AxoManager; use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway; use WooCommerce\PayPalCommerce\Axo\Helper\ApmApplies; +use WooCommerce\PayPalCommerce\Axo\Helper\SettingsNoticeGenerator; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; -use WooCommerce\PayPalCommerce\WcGateway\Helper\CartCheckoutDetector; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; return array( @@ -25,7 +25,7 @@ return array( $apm_applies = $container->get( 'axo.helpers.apm-applies' ); assert( $apm_applies instanceof ApmApplies ); - return $apm_applies->for_country_currency() && $apm_applies->for_settings(); + return $apm_applies->for_country_currency(); }, 'axo.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies { @@ -36,6 +36,10 @@ return array( ); }, + 'axo.helpers.settings-notice-generator' => static function ( ContainerInterface $container ) : SettingsNoticeGenerator { + return new SettingsNoticeGenerator(); + }, + // If AXO is configured and onboarded. 'axo.available' => static function ( ContainerInterface $container ): bool { return true; @@ -159,48 +163,35 @@ return array( ); }, + 'axo.settings-conflict-notice' => static function ( ContainerInterface $container ) : string { + $settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' ); + assert( $settings_notice_generator instanceof SettingsNoticeGenerator ); + + $settings = $container->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + + return $settings_notice_generator->generate_settings_conflict_notice( $settings ); + }, + 'axo.checkout-config-notice' => static function ( ContainerInterface $container ) : string { - $checkout_page_link = esc_url( get_edit_post_link( wc_get_page_id( 'checkout' ) ) ?? '' ); - $block_checkout_docs_link = __( - 'https://woocommerce.com/document/cart-checkout-blocks-status/#reverting-to-the-cart-and-checkout-shortcodes', - 'woocommerce-paypal-payments' - ); + $settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' ); + assert( $settings_notice_generator instanceof SettingsNoticeGenerator ); - if ( CartCheckoutDetector::has_elementor_checkout() ) { - $notice_content = sprintf( - /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */ - __( - 'Warning: The Checkout page of your store currently uses the Elementor Checkout widget. To enable Fastlane and accelerate payments, the page must include either the Classic Checkout or the [woocommerce_checkout] shortcode. See this page for instructions on how to switch to the classic layout.', - 'woocommerce-paypal-payments' - ), - esc_url( $checkout_page_link ), - esc_url( $block_checkout_docs_link ) - ); - } elseif ( CartCheckoutDetector::has_block_checkout() ) { - $notice_content = sprintf( - /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */ - __( - 'Warning: The Checkout page of your store currently uses the WooCommerce Checkout block. To enable Fastlane and accelerate payments, the page must include either the Classic Checkout or the [woocommerce_checkout] shortcode. See this page for instructions on how to switch to the classic layout.', - 'woocommerce-paypal-payments' - ), - esc_url( $checkout_page_link ), - esc_url( $block_checkout_docs_link ) - ); - } elseif ( ! CartCheckoutDetector::has_classic_checkout() ) { - $notice_content = sprintf( - /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */ - __( - 'Warning: The Checkout page of your store does not seem to be properly configured or uses an incompatible third-party Checkout solution. To enable Fastlane and accelerate payments, the page must include either the Classic Checkout or the [woocommerce_checkout] shortcode. See this page for instructions on how to switch to the classic layout.', - 'woocommerce-paypal-payments' - ), - esc_url( $checkout_page_link ), - esc_url( $block_checkout_docs_link ) - ); - } else { - return ''; - } + return $settings_notice_generator->generate_checkout_notice(); + }, - return '

' . $notice_content . '

'; + 'axo.shipping-config-notice' => static function ( ContainerInterface $container ) : string { + $settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' ); + assert( $settings_notice_generator instanceof SettingsNoticeGenerator ); + + return $settings_notice_generator->generate_shipping_notice(); + }, + + 'axo.incompatible-plugins-notice' => static function ( ContainerInterface $container ) : string { + $settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' ); + assert( $settings_notice_generator instanceof SettingsNoticeGenerator ); + + return $settings_notice_generator->generate_incompatible_plugins_notice(); }, 'axo.smart-button-location-notice' => static function ( ContainerInterface $container ) : string { @@ -230,6 +221,7 @@ return array( return '

' . $notice_content . '

'; }, + 'axo.endpoint.frontend-logger' => static function ( ContainerInterface $container ): FrontendLoggerEndpoint { return new FrontendLoggerEndpoint( $container->get( 'button.request-data' ), diff --git a/modules/ppcp-axo/src/Assets/AxoManager.php b/modules/ppcp-axo/src/Assets/AxoManager.php index 363f0d092..2e7e60804 100644 --- a/modules/ppcp-axo/src/Assets/AxoManager.php +++ b/modules/ppcp-axo/src/Assets/AxoManager.php @@ -216,6 +216,7 @@ class AxoManager { 'nonce' => wp_create_nonce( FrontendLoggerEndpoint::nonce() ), ), ), + 'logging_enabled' => $this->settings->has( 'logging_enabled' ) ? $this->settings->get( 'logging_enabled' ) : '', 'wp_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG, 'billing_email_button_text' => __( 'Continue', 'woocommerce-paypal-payments' ), ); diff --git a/modules/ppcp-axo/src/AxoModule.php b/modules/ppcp-axo/src/AxoModule.php index c0fc34b24..9b5a6985d 100644 --- a/modules/ppcp-axo/src/AxoModule.php +++ b/modules/ppcp-axo/src/AxoModule.php @@ -66,7 +66,7 @@ class AxoModule implements ModuleInterface { // Add the gateway in admin area. if ( is_admin() ) { - $methods[] = $gateway; + // $methods[] = $gateway; - Temporarily remove Fastlane from the payment gateway list in admin area. return $methods; } @@ -77,9 +77,10 @@ class AxoModule implements ModuleInterface { $settings = $c->get( 'wcgateway.settings' ); assert( $settings instanceof Settings ); + $is_paypal_enabled = $settings->has( 'enabled' ) && $settings->get( 'enabled' ) ?? false; $is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ) ?? false; - if ( ! $is_dcc_enabled ) { + if ( ! $is_paypal_enabled || ! $is_dcc_enabled ) { return $methods; } @@ -87,6 +88,10 @@ class AxoModule implements ModuleInterface { return $methods; } + if ( ! $this->is_compatible_shipping_config() ) { + return $methods; + } + $methods[] = $gateway; return $methods; }, @@ -144,13 +149,20 @@ class AxoModule implements ModuleInterface { function () use ( $c ) { $module = $this; + $settings = $c->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + + $is_paypal_enabled = $settings->has( 'enabled' ) && $settings->get( 'enabled' ) ?? false; + $subscription_helper = $c->get( 'wc-subscriptions.helper' ); assert( $subscription_helper instanceof SubscriptionHelper ); // Check if the module is applicable, correct country, currency, ... etc. - if ( ! $c->get( 'axo.eligible' ) + if ( ! $is_paypal_enabled + || ! $c->get( 'axo.eligible' ) || 'continuation' === $c->get( 'button.context' ) - || $subscription_helper->cart_contains_subscription() ) { + || $subscription_helper->cart_contains_subscription() + || ! $this->is_compatible_shipping_config() ) { return; } @@ -194,9 +206,17 @@ class AxoModule implements ModuleInterface { add_action( 'wp_head', - function () { + function () use ( $c ) { // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript echo ''; + + // Add meta tag to allow feature-detection of the site's AXO payment state. + $settings = $c->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + + $this->add_feature_detection_tag( + $settings->has( 'axo_enabled' ) && $settings->get( 'axo_enabled' ) + ); } ); @@ -333,6 +353,7 @@ class AxoModule implements ModuleInterface { return ! is_user_logged_in() && CartCheckoutDetector::has_classic_checkout() + && $this->is_compatible_shipping_config() && $is_axo_enabled && $is_dcc_enabled && ! $this->is_excluded_endpoint(); @@ -388,4 +409,32 @@ class AxoModule implements ModuleInterface { // Exclude the Order Pay endpoint. return is_wc_endpoint_url( 'order-pay' ); } + + /** + * Condition to evaluate if the shipping configuration is compatible. + * + * @return bool + */ + private function is_compatible_shipping_config(): bool { + return ! wc_shipping_enabled() || ( wc_shipping_enabled() && ! wc_ship_to_billing_address_only() ); + } + + /** + * Outputs a meta tag to allow feature detection on certain pages. + * + * @param bool $axo_enabled Whether the gateway is enabled. + * @return void + */ + private function add_feature_detection_tag( bool $axo_enabled ) { + $show_tag = is_checkout() || is_cart() || is_shop(); + + if ( ! $show_tag ) { + return; + } + + printf( + '', + $axo_enabled ? 'enabled' : 'disabled' + ); + } } diff --git a/modules/ppcp-axo/src/Gateway/AxoGateway.php b/modules/ppcp-axo/src/Gateway/AxoGateway.php index 8d50b6253..4fa80cd4a 100644 --- a/modules/ppcp-axo/src/Gateway/AxoGateway.php +++ b/modules/ppcp-axo/src/Gateway/AxoGateway.php @@ -168,7 +168,7 @@ class AxoGateway extends WC_Payment_Gateway { ? $this->ppcp_settings->get( 'axo_gateway_title' ) : $this->get_option( 'title', $this->method_title ); - $this->description = __( 'Enter your email address to continue.', 'woocommerce-paypal-payments' ); + $this->description = __( 'Enter your email address above to continue.', 'woocommerce-paypal-payments' ); $this->init_form_fields(); $this->init_settings(); diff --git a/modules/ppcp-axo/src/Helper/ApmApplies.php b/modules/ppcp-axo/src/Helper/ApmApplies.php index c5fe645a9..c8a709f55 100644 --- a/modules/ppcp-axo/src/Helper/ApmApplies.php +++ b/modules/ppcp-axo/src/Helper/ApmApplies.php @@ -64,19 +64,4 @@ class ApmApplies { } return in_array( $this->currency, $this->allowed_country_currency_matrix[ $this->country ], true ); } - - /** - * Returns whether the settings are compatible with AXO. - * - * @return bool - */ - public function for_settings(): bool { - if ( get_option( 'woocommerce_ship_to_destination' ) === 'billing_only' ) { // Force shipping to the customer billing address. - return false; - } - return true; - } - - - } diff --git a/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php b/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php new file mode 100644 index 000000000..503e135cb --- /dev/null +++ b/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php @@ -0,0 +1,179 @@ +

%2$s

', + $is_error ? 'ppcp-notice-error' : '', + $message + ); + } + + /** + * Generates the checkout notice. + * + * @return string + */ + public function generate_checkout_notice(): string { + $checkout_page_link = esc_url( get_edit_post_link( wc_get_page_id( 'checkout' ) ) ?? '' ); + $block_checkout_docs_link = __( + 'https://woocommerce.com/document/cart-checkout-blocks-status/#reverting-to-the-cart-and-checkout-shortcodes', + 'woocommerce-paypal-payments' + ); + + $notice_content = ''; + + if ( CartCheckoutDetector::has_elementor_checkout() ) { + $notice_content = sprintf( + /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */ + __( + 'Warning: The Checkout page of your store currently uses the Elementor Checkout widget. To enable Fastlane and accelerate payments, the page must include either the Classic Checkout or the [woocommerce_checkout] shortcode. See this page for instructions on how to switch to the classic layout.', + 'woocommerce-paypal-payments' + ), + esc_url( $checkout_page_link ), + esc_url( $block_checkout_docs_link ) + ); + } elseif ( CartCheckoutDetector::has_block_checkout() ) { + $notice_content = sprintf( + /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */ + __( + 'Warning: The Checkout page of your store currently uses the WooCommerce Checkout block. To enable Fastlane and accelerate payments, the page must include either the Classic Checkout or the [woocommerce_checkout] shortcode. See this page for instructions on how to switch to the classic layout.', + 'woocommerce-paypal-payments' + ), + esc_url( $checkout_page_link ), + esc_url( $block_checkout_docs_link ) + ); + } elseif ( ! CartCheckoutDetector::has_classic_checkout() ) { + $notice_content = sprintf( + /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */ + __( + 'Warning: The Checkout page of your store does not seem to be properly configured or uses an incompatible third-party Checkout solution. To enable Fastlane and accelerate payments, the page must include either the Classic Checkout or the [woocommerce_checkout] shortcode. See this page for instructions on how to switch to the classic layout.', + 'woocommerce-paypal-payments' + ), + esc_url( $checkout_page_link ), + esc_url( $block_checkout_docs_link ) + ); + } + + return $notice_content ? '

' . $notice_content . '

' : ''; + } + + /** + * Generates the shipping notice. + * + * @return string + */ + public function generate_shipping_notice(): string { + $shipping_settings_link = admin_url( 'admin.php?page=wc-settings&tab=shipping§ion=options' ); + + $notice_content = ''; + + if ( wc_shipping_enabled() && wc_ship_to_billing_address_only() ) { + $notice_content = sprintf( + /* translators: %1$s: URL to the Shipping destination settings page. */ + __( + 'Warning: The Shipping destination of your store is currently configured to Force shipping to the customer billing address. To enable Fastlane and accelerate payments, the shipping destination must be configured either to Default to customer shipping address or Default to customer billing address so buyers can set separate billing and shipping details.', + 'woocommerce-paypal-payments' + ), + esc_url( $shipping_settings_link ) + ); + } + + return $notice_content ? '

' . $notice_content . '

' : ''; + } + + /** + * Generates the incompatible plugins notice. + * + * @return string + */ + public function generate_incompatible_plugins_notice(): string { + $incompatible_plugins = array( + 'Elementor' => did_action( 'elementor/loaded' ), + 'CheckoutWC' => defined( 'CFW_NAME' ), + ); + + $active_plugins_list = array_filter( $incompatible_plugins ); + + if ( empty( $active_plugins_list ) ) { + return ''; + } + + $incompatible_plugin_items = array_map( + function ( $plugin ) { + return "
  • {$plugin}
  • "; + }, + array_keys( $active_plugins_list ) + ); + + $plugins_settings_link = esc_url( admin_url( 'plugins.php' ) ); + $notice_content = sprintf( + /* translators: %1$s: URL to the plugins settings page. %2$s: List of incompatible plugins. */ + __( + 'Note: The accelerated guest buyer experience provided by Fastlane may not be fully compatible with some of the following active plugins:
      %2$s
    ', + 'woocommerce-paypal-payments' + ), + $plugins_settings_link, + implode( '', $incompatible_plugin_items ) + ); + + return '

    ' . $notice_content . '

    '; + } + + /** + * Generates a warning notice with instructions on conflicting plugin-internal settings. + * + * @param Settings $settings The plugin settings container, which is checked for conflicting + * values. + * @return string + */ + public function generate_settings_conflict_notice( Settings $settings ) : string { + $notice_content = ''; + $is_dcc_enabled = false; + + try { + $is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ); + // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch + } catch ( NotFoundException $ignored ) { + // Never happens. + } + + if ( ! $is_dcc_enabled ) { + $notice_content = __( + 'Warning: To enable Fastlane and accelerate payments, the Advanced Card Processing payment method must also be enabled.', + 'woocommerce-paypal-payments' + ); + } + + return $this->render_notice( $notice_content, true ); + } +} diff --git a/modules/ppcp-blocks/package.json b/modules/ppcp-blocks/package.json index 993b115e3..3398edd5e 100644 --- a/modules/ppcp-blocks/package.json +++ b/modules/ppcp-blocks/package.json @@ -10,7 +10,7 @@ "Edge >= 14" ], "dependencies": { - "@paypal/react-paypal-js": "^8.3.0", + "@paypal/react-paypal-js": "^8.5.0", "core-js": "^3.25.0", "react": "^17.0.0", "react-dom": "^17.0.0" diff --git a/modules/ppcp-blocks/resources/js/Components/card-fields.js b/modules/ppcp-blocks/resources/js/Components/card-fields.js index 4a1f25785..2726bc12e 100644 --- a/modules/ppcp-blocks/resources/js/Components/card-fields.js +++ b/modules/ppcp-blocks/resources/js/Components/card-fields.js @@ -7,7 +7,12 @@ import { } from '@paypal/react-paypal-js'; import { CheckoutHandler } from './checkout-handler'; -import { createOrder, onApprove } from '../card-fields-config'; +import { + createOrder, + onApprove, + createVaultSetupToken, + onApproveSavePayment, +} from '../card-fields-config'; import { cartHasSubscriptionProducts } from '../Helper/Subscription'; export function CardFields( { @@ -70,8 +75,21 @@ export function CardFields( { } } > { console.error( err ); } } diff --git a/modules/ppcp-blocks/resources/js/card-fields-config.js b/modules/ppcp-blocks/resources/js/card-fields-config.js index 5383729dc..8932ef916 100644 --- a/modules/ppcp-blocks/resources/js/card-fields-config.js +++ b/modules/ppcp-blocks/resources/js/card-fields-config.js @@ -44,3 +44,61 @@ export async function onApprove( data ) { console.error( err ); } ); } + +export async function createVaultSetupToken() { + const config = wc.wcSettings.getSetting( 'ppcp-credit-card-gateway_data' ); + + return fetch( config.scriptData.ajax.create_setup_token.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify( { + nonce: config.scriptData.ajax.create_setup_token.nonce, + payment_method: 'ppcp-credit-card-gateway', + } ), + } ) + .then( ( response ) => response.json() ) + .then( ( result ) => { + console.log( result ); + return result.data.id; + } ) + .catch( ( err ) => { + console.error( err ); + } ); +} + +export async function onApproveSavePayment( { vaultSetupToken } ) { + const config = wc.wcSettings.getSetting( 'ppcp-credit-card-gateway_data' ); + + let endpoint = + config.scriptData.ajax.create_payment_token_for_guest.endpoint; + let bodyContent = { + nonce: config.scriptData.ajax.create_payment_token_for_guest.nonce, + vault_setup_token: vaultSetupToken, + }; + + if ( config.scriptData.user.is_logged_in ) { + endpoint = config.scriptData.ajax.create_payment_token.endpoint; + + bodyContent = { + nonce: config.scriptData.ajax.create_payment_token.nonce, + vault_setup_token: vaultSetupToken, + is_free_trial_cart: config.scriptData.is_free_trial_cart, + }; + } + + const response = await fetch( endpoint, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify( bodyContent ), + } ); + + const result = await response.json(); + if ( result.success !== true ) { + console.error( result ); + } +} diff --git a/modules/ppcp-blocks/resources/js/checkout-block.js b/modules/ppcp-blocks/resources/js/checkout-block.js index bf36dcb65..d1b6ad990 100644 --- a/modules/ppcp-blocks/resources/js/checkout-block.js +++ b/modules/ppcp-blocks/resources/js/checkout-block.js @@ -227,7 +227,7 @@ const PayPalComponent = ( { throw new Error( config.scriptData.labels.error.generic ); } - if ( ! shouldHandleShippingInPayPal() ) { + if ( ! shouldskipFinalConfirmation() ) { location.href = getCheckoutRedirectUrl(); } else { setGotoContinuationOnError( true ); @@ -318,7 +318,7 @@ const PayPalComponent = ( { throw new Error( config.scriptData.labels.error.generic ); } - if ( ! shouldHandleShippingInPayPal() ) { + if ( ! shouldskipFinalConfirmation() ) { location.href = getCheckoutRedirectUrl(); } else { setGotoContinuationOnError( true ); @@ -364,16 +364,20 @@ const PayPalComponent = ( { }; const shouldHandleShippingInPayPal = () => { - if ( config.finalReviewEnabled ) { - return false; - } - - return ( - window.ppcpFundingSource !== 'venmo' || - ! config.scriptData.vaultingEnabled - ); + return shouldskipFinalConfirmation() && config.needShipping }; + const shouldskipFinalConfirmation = () => { + if ( config.finalReviewEnabled ) { + return false; + } + + return ( + window.ppcpFundingSource !== 'venmo' || + ! config.scriptData.vaultingEnabled + ); + }; + let handleShippingOptionsChange = null; let handleShippingAddressChange = null; let handleSubscriptionShippingOptionsChange = null; @@ -544,7 +548,7 @@ const PayPalComponent = ( { if ( config.scriptData.continuation ) { return true; } - if ( shouldHandleShippingInPayPal() ) { + if ( shouldskipFinalConfirmation() ) { location.href = getCheckoutRedirectUrl(); } return true; diff --git a/modules/ppcp-blocks/src/Endpoint/UpdateShippingEndpoint.php b/modules/ppcp-blocks/src/Endpoint/UpdateShippingEndpoint.php index be196fa03..c8e0417ab 100644 --- a/modules/ppcp-blocks/src/Endpoint/UpdateShippingEndpoint.php +++ b/modules/ppcp-blocks/src/Endpoint/UpdateShippingEndpoint.php @@ -97,15 +97,6 @@ class UpdateShippingEndpoint implements EndpointInterface { $pu = $this->purchase_unit_factory->from_wc_cart( null, true ); $pu_data = $pu->to_array(); - if ( ! isset( $pu_data['shipping']['options'] ) ) { - wp_send_json_error( - array( - 'message' => 'No shipping methods.', - ) - ); - return false; - } - // TODO: maybe should patch only if methods changed. // But it seems a bit difficult to detect, // e.g. ->order($id) may not have Shipping because we drop it when address or name are missing. diff --git a/modules/ppcp-blocks/src/PayPalPaymentMethod.php b/modules/ppcp-blocks/src/PayPalPaymentMethod.php index d94eec53d..1d0654bac 100644 --- a/modules/ppcp-blocks/src/PayPalPaymentMethod.php +++ b/modules/ppcp-blocks/src/PayPalPaymentMethod.php @@ -254,6 +254,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType { ), ), 'scriptData' => $script_data, + 'needShipping' => WC()->cart->needs_shipping(), ); } diff --git a/modules/ppcp-blocks/yarn.lock b/modules/ppcp-blocks/yarn.lock index 1e812a4ae..1debd6e24 100644 --- a/modules/ppcp-blocks/yarn.lock +++ b/modules/ppcp-blocks/yarn.lock @@ -1005,19 +1005,19 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@paypal/paypal-js@^8.0.5": - version "8.0.5" - resolved "https://registry.yarnpkg.com/@paypal/paypal-js/-/paypal-js-8.0.5.tgz#77bc461b4d1e5a2c6f081269e3ef0b2e3331a68c" - integrity sha512-yQNV7rOILeaVCNU4aVDRPqEnbIlzfxgQfFsxzsBuZW1ouqRD/4kYBWJDzczCiscSr2xOeA/Pkm7e3a9fRfnuMQ== +"@paypal/paypal-js@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@paypal/paypal-js/-/paypal-js-8.1.0.tgz#4e7d10e0a0b4164985029cfdac748e5694d117e9" + integrity sha512-f64bom5xYwmxyeKPJUFS/XpM0tXojQEgjRIADPqe1R9WmK+PFqL4SEkT85cGU0ZXLVx4EGbjwREHhqEOR+OstA== dependencies: promise-polyfill "^8.3.0" -"@paypal/react-paypal-js@^8.3.0": - version "8.3.0" - resolved "https://registry.yarnpkg.com/@paypal/react-paypal-js/-/react-paypal-js-8.3.0.tgz#a103080b752766b8ff59b8620887abf802e1a01b" - integrity sha512-SX17d2h1CMNFGI+wtjb329AEDaBR8Ziy2LCV076eDcY1Q0MFKRkfQ/v0HOAvZtk3sJoydRmYez2pq47BRblwqQ== +"@paypal/react-paypal-js@^8.5.0": + version "8.5.0" + resolved "https://registry.yarnpkg.com/@paypal/react-paypal-js/-/react-paypal-js-8.5.0.tgz#cf17483202c8fa7a33dae86798d50a102705f182" + integrity sha512-YIAyLw4OiUoHHoUgXvibrBDdluzqnqVMGsJXyBcoOzlWHQIe5zhh8dgYezNNRXjXwy6t22YmPljjw7lr+eD9cw== dependencies: - "@paypal/paypal-js" "^8.0.5" + "@paypal/paypal-js" "^8.1.0" "@paypal/sdk-constants" "^1.0.122" "@paypal/sdk-constants@^1.0.122": diff --git a/modules/ppcp-button/resources/js/button.js b/modules/ppcp-button/resources/js/button.js index 9f5485b38..505fa60af 100644 --- a/modules/ppcp-button/resources/js/button.js +++ b/modules/ppcp-button/resources/js/button.js @@ -7,6 +7,7 @@ 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 { @@ -215,12 +216,23 @@ const bootstrap = () => { spinner ); if ( typeof paypal.CardFields !== 'undefined' ) { - creditCardRenderer = new CardFieldsRenderer( - PayPalCommerceGateway, - errorHandler, - spinner, - onCardFieldsBeforeSubmit - ); + if ( + PayPalCommerceGateway.is_free_trial_cart && + PayPalCommerceGateway.user?.has_wc_card_payment_tokens !== true + ) { + creditCardRenderer = new CardFieldsFreeTrialRenderer( + PayPalCommerceGateway, + errorHandler, + spinner + ); + } else { + creditCardRenderer = new CardFieldsRenderer( + PayPalCommerceGateway, + errorHandler, + spinner, + onCardFieldsBeforeSubmit + ); + } } const renderer = new Renderer( diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js index a93a24212..4c2fa9b2d 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js @@ -173,61 +173,6 @@ class CheckoutActionHandler { }, }; } - - addPaymentMethodConfiguration() { - return { - createVaultSetupToken: async () => { - const response = await fetch( - this.config.ajax.create_setup_token.endpoint, - { - method: 'POST', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify( { - nonce: this.config.ajax.create_setup_token.nonce, - } ), - } - ); - - const result = await response.json(); - if ( result.data.id ) { - return result.data.id; - } - - console.error( result ); - }, - onApprove: async ( { vaultSetupToken } ) => { - const response = await fetch( - this.config.ajax.create_payment_token_for_guest.endpoint, - { - method: 'POST', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify( { - nonce: this.config.ajax - .create_payment_token_for_guest.nonce, - vault_setup_token: vaultSetupToken, - } ), - } - ); - - const result = await response.json(); - if ( result.success === true ) { - document.querySelector( '#place_order' ).click(); - return; - } - - console.error( result ); - }, - onError: ( error ) => { - console.error( error ); - }, - }; - } } export default CheckoutActionHandler; diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js index 33d1ecfd3..a85ad68ee 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js @@ -1,3 +1,5 @@ +/* global PayPalCommerceGateway */ + import CheckoutActionHandler from '../ActionHandler/CheckoutActionHandler'; import { setVisible, setVisibleByClass } from '../Helper/Hiding'; import { @@ -7,6 +9,11 @@ import { PaymentMethods, } from '../Helper/CheckoutMethodState'; import BootstrapHelper from '../Helper/BootstrapHelper'; +import { addPaymentMethodConfiguration } from '../../../../../ppcp-save-payment-methods/resources/js/Configuration'; +import { + ButtonEvents, + dispatchButtonEvent, +} from '../Helper/PaymentButtonHelpers'; class CheckoutBootstap { constructor( gateway, renderer, spinner, errorHandler ) { @@ -68,6 +75,7 @@ class CheckoutBootstap { jQuery( document.body ).on( 'updated_checkout payment_method_selected', () => { + this.invalidatePaymentMethods(); this.updateUi(); } ); @@ -160,7 +168,7 @@ class CheckoutBootstap { PayPalCommerceGateway.vault_v3_enabled ) { this.renderer.render( - actionHandler.addPaymentMethodConfiguration(), + addPaymentMethodConfiguration( PayPalCommerceGateway ), {}, actionHandler.configuration() ); @@ -174,6 +182,14 @@ class CheckoutBootstap { ); } + invalidatePaymentMethods() { + /** + * Custom JS event to notify other modules that the payment button on the checkout page + * has become irrelevant or invalid. + */ + dispatchButtonEvent( { event: ButtonEvents.INVALIDATE } ); + } + updateUi() { const currentPaymentMethod = getCurrentPaymentMethod(); const isPaypal = currentPaymentMethod === PaymentMethods.PAYPAL; @@ -183,12 +199,15 @@ class CheckoutBootstap { ); const isGooglePayMethod = currentPaymentMethod === PaymentMethods.GOOGLEPAY; + const isApplePayMethod = + currentPaymentMethod === PaymentMethods.APPLEPAY; const isSavedCard = isCard && isSavedCardSelected(); const isNotOurGateway = ! isPaypal && ! isCard && ! isSeparateButtonGateway && - ! isGooglePayMethod; + ! isGooglePayMethod && + ! isApplePayMethod; const isFreeTrial = PayPalCommerceGateway.is_free_trial_cart; const hasVaultedPaypal = PayPalCommerceGateway.vaulted_paypal_email !== ''; @@ -232,9 +251,20 @@ class CheckoutBootstap { } } - setVisible( '#ppc-button-ppcp-googlepay', isGooglePayMethod ); + /** + * Custom JS event that is observed by the relevant payment gateway. + * + * Dynamic part of the event name is the payment method ID, for example + * "ppcp-credit-card-gateway" or "ppcp-googlepay" + */ + dispatchButtonEvent( { + event: ButtonEvents.RENDER, + paymentMethod: currentPaymentMethod, + } ); - jQuery( document.body ).trigger( 'ppcp_checkout_rendered' ); + setVisible( '#ppc-button-ppcp-applepay', isApplePayMethod ); + + document.body.dispatchEvent( new Event( 'ppcp_checkout_rendered' ) ); } shouldShowMessages() { diff --git a/modules/ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper.js b/modules/ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper.js index 3492462e7..7620547c0 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper.js +++ b/modules/ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper.js @@ -26,8 +26,11 @@ export function setupButtonEvents( refresh ) { document.addEventListener( REFRESH_BUTTON_EVENT, debouncedRefresh ); // Listen for cart and checkout update events. - document.body.addEventListener( 'updated_cart_totals', debouncedRefresh ); - document.body.addEventListener( 'updated_checkout', debouncedRefresh ); + // Note: we need jQuery here, because WooCommerce uses jQuery.trigger() to dispatch the events. + window + .jQuery( 'body' ) + .on( 'updated_cart_totals', debouncedRefresh ) + .on( 'updated_checkout', debouncedRefresh ); // Use setTimeout for fragment events to avoid unnecessary refresh on initial render. setTimeout( () => { diff --git a/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js b/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js index 3e284c8ef..4275e48d4 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js +++ b/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js @@ -4,6 +4,31 @@ export const PaymentMethods = { OXXO: 'ppcp-oxxo-gateway', CARD_BUTTON: 'ppcp-card-button-gateway', GOOGLEPAY: 'ppcp-googlepay', + APPLEPAY: 'ppcp-applepay', +}; + +/** + * List of valid context values that the button can have. + * + * The "context" describes the placement or page where a payment button might be displayed. + * + * @type {Object} + */ +export const PaymentContext = { + Cart: 'cart', // Classic cart. + Checkout: 'checkout', // Classic checkout. + BlockCart: 'cart-block', // Block cart. + BlockCheckout: 'checkout-block', // Block checkout. + Product: 'product', // Single product page. + MiniCart: 'mini-cart', // Mini cart available on all pages except checkout & cart. + PayNow: 'pay-now', // Pay for order, via admin generated link. + Preview: 'preview', // Layout preview on settings page. + + // Contexts that use blocks to render payment methods. + Blocks: [ 'cart-block', 'checkout-block' ], + + // Contexts that display "classic" payment gateways. + Gateways: [ 'checkout', 'pay-now' ], }; export const ORDER_BUTTON_SELECTOR = '#place_order'; diff --git a/modules/ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers.js b/modules/ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers.js new file mode 100644 index 000000000..f9a066a23 --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers.js @@ -0,0 +1,117 @@ +/** + * Helper function used by PaymentButton instances. + * + * @file + */ + +/** + * Collection of recognized event names for payment button events. + * + * @type {Object} + */ +export const ButtonEvents = Object.freeze( { + INVALIDATE: 'ppcp_invalidate_methods', + RENDER: 'ppcp_render_method', + REDRAW: 'ppcp_redraw_method', +} ); + +/** + * + * @param {string} defaultId - Default wrapper ID. + * @param {string} miniCartId - Wrapper inside the mini-cart. + * @param {string} smartButtonId - ID of the smart button wrapper. + * @param {string} blockId - Block wrapper ID (express checkout, block cart). + * @param {string} gatewayId - Gateway wrapper ID (classic checkout). + * @return {{MiniCart, Gateway, Block, SmartButton, Default}} List of all wrapper IDs, by context. + */ +export function combineWrapperIds( + defaultId = '', + miniCartId = '', + smartButtonId = '', + blockId = '', + gatewayId = '' +) { + const sanitize = ( id ) => id.replace( /^#/, '' ); + + return { + Default: sanitize( defaultId ), + SmartButton: sanitize( smartButtonId ), + Block: sanitize( blockId ), + Gateway: sanitize( gatewayId ), + MiniCart: sanitize( miniCartId ), + }; +} + +/** + * Returns full payment button styles by combining the global ppcpConfig with + * payment-method-specific styling provided via buttonConfig. + * + * @param {Object} ppcpConfig - Global plugin configuration. + * @param {Object} buttonConfig - Payment method specific configuration. + * @return {{MiniCart: (*), Default: (*)}} Combined styles, separated by context. + */ +export function combineStyles( ppcpConfig, buttonConfig ) { + return { + Default: { + ...ppcpConfig.style, + ...buttonConfig.style, + }, + MiniCart: { + ...ppcpConfig.mini_cart_style, + ...buttonConfig.mini_cart_style, + }, + }; +} + +/** + * Verifies if the given event name is a valid Payment Button event. + * + * @param {string} event - The event name to verify. + * @return {boolean} True, if the event name is valid. + */ +export function isValidButtonEvent( event ) { + const buttonEventValues = Object.values( ButtonEvents ); + + return buttonEventValues.includes( event ); +} + +/** + * Dispatches a payment button event. + * + * @param {Object} options - The options for dispatching the event. + * @param {string} options.event - Event to dispatch. + * @param {string} [options.paymentMethod] - Optional. Name of payment method, to target a specific button only. + * @throws {Error} Throws an error if the event is invalid. + */ +export function dispatchButtonEvent( { event, paymentMethod = '' } ) { + if ( ! isValidButtonEvent( event ) ) { + throw new Error( `Invalid event: ${ event }` ); + } + + const fullEventName = paymentMethod + ? `${ event }-${ paymentMethod }` + : event; + + document.body.dispatchEvent( new Event( fullEventName ) ); +} + +/** + * Adds an event listener for the provided button event. + * + * @param {Object} options - The options for the event listener. + * @param {string} options.event - Event to observe. + * @param {string} [options.paymentMethod] - The payment method name (optional). + * @param {Function} options.callback - The callback function to execute when the event is triggered. + * @throws {Error} Throws an error if the event is invalid. + */ +export function observeButtonEvent( { event, paymentMethod = '', callback } ) { + if ( ! isValidButtonEvent( event ) ) { + throw new Error( `Invalid event: ${ event }` ); + } + + const fullEventName = paymentMethod + ? `${ event }-${ paymentMethod }` + : event; + + document.body.addEventListener( fullEventName, callback ); +} diff --git a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js index c60c163fd..54f4e123a 100644 --- a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js +++ b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js @@ -20,10 +20,7 @@ const onApprove = ( context, errorHandler ) => { } ) .then( ( data ) => { if ( ! data.success ) { - errorHandler.genericError(); - return actions.restart().catch( ( err ) => { - errorHandler.genericError(); - } ); + location.href = context.config.redirect; } const orderReceivedUrl = data.data?.order_received_url; diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CardFieldsFreeTrialRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CardFieldsFreeTrialRenderer.js new file mode 100644 index 000000000..b97569762 --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Renderer/CardFieldsFreeTrialRenderer.js @@ -0,0 +1,88 @@ +import { show } from '../Helper/Hiding'; +import { renderFields } from '../../../../../ppcp-card-fields/resources/js/Render'; +import { + addPaymentMethodConfiguration, + cardFieldsConfiguration, +} from '../../../../../ppcp-save-payment-methods/resources/js/Configuration'; + +class CardFieldsFreeTrialRenderer { + constructor( defaultConfig, errorHandler, spinner ) { + this.defaultConfig = defaultConfig; + this.errorHandler = errorHandler; + this.spinner = spinner; + } + + render( wrapper, contextConfig ) { + if ( + ( this.defaultConfig.context !== 'checkout' && + this.defaultConfig.context !== 'pay-now' ) || + wrapper === null || + document.querySelector( wrapper ) === null + ) { + return; + } + + const buttonSelector = wrapper + ' button'; + + const gateWayBox = document.querySelector( + '.payment_box.payment_method_ppcp-credit-card-gateway' + ); + if ( ! gateWayBox ) { + return; + } + + const oldDisplayStyle = gateWayBox.style.display; + gateWayBox.style.display = 'block'; + + const hideDccGateway = document.querySelector( '#ppcp-hide-dcc' ); + if ( hideDccGateway ) { + hideDccGateway.parentNode.removeChild( hideDccGateway ); + } + + this.errorHandler.clear(); + + let cardFields = paypal.CardFields( + addPaymentMethodConfiguration( this.defaultConfig ) + ); + if ( this.defaultConfig.user.is_logged ) { + cardFields = paypal.CardFields( + cardFieldsConfiguration( this.defaultConfig, this.errorHandler ) + ); + } + + if ( cardFields.isEligible() ) { + renderFields( cardFields ); + } + + gateWayBox.style.display = oldDisplayStyle; + + show( buttonSelector ); + + if ( this.defaultConfig.cart_contains_subscription ) { + const saveToAccount = document.querySelector( + '#wc-ppcp-credit-card-gateway-new-payment-method' + ); + if ( saveToAccount ) { + saveToAccount.checked = true; + saveToAccount.disabled = true; + } + } + + document + .querySelector( buttonSelector ) + ?.addEventListener( 'click', ( event ) => { + event.preventDefault(); + this.spinner.block(); + this.errorHandler.clear(); + + cardFields.submit().catch( ( error ) => { + console.error( error ); + } ); + } ); + } + + disableFields() {} + enableFields() {} +} + +export default CardFieldsFreeTrialRenderer; diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CardFieldsRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CardFieldsRenderer.js index b3e70011f..4a0ec09f2 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/CardFieldsRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/CardFieldsRenderer.js @@ -1,5 +1,5 @@ import { show } from '../Helper/Hiding'; -import { cardFieldStyles } from '../Helper/CardFieldsHelper'; +import { renderFields } from '../../../../../ppcp-card-fields/resources/js/Render'; class CardFieldsRenderer { constructor( @@ -45,7 +45,7 @@ class CardFieldsRenderer { hideDccGateway.parentNode.removeChild( hideDccGateway ); } - const cardField = paypal.CardFields( { + const cardFields = paypal.CardFields( { createOrder: contextConfig.createOrder, onApprove( data ) { return contextConfig.onApprove( data ); @@ -56,79 +56,8 @@ class CardFieldsRenderer { }, } ); - if ( cardField.isEligible() ) { - const nameField = document.getElementById( - 'ppcp-credit-card-gateway-card-name' - ); - if ( nameField ) { - const styles = cardFieldStyles( nameField ); - const fieldOptions = { - style: { input: styles }, - }; - if ( nameField.getAttribute( 'placeholder' ) ) { - fieldOptions.placeholder = - nameField.getAttribute( 'placeholder' ); - } - cardField - .NameField( fieldOptions ) - .render( nameField.parentNode ); - nameField.remove(); - } - - const numberField = document.getElementById( - 'ppcp-credit-card-gateway-card-number' - ); - if ( numberField ) { - const styles = cardFieldStyles( numberField ); - const fieldOptions = { - style: { input: styles }, - }; - if ( numberField.getAttribute( 'placeholder' ) ) { - fieldOptions.placeholder = - numberField.getAttribute( 'placeholder' ); - } - cardField - .NumberField( fieldOptions ) - .render( numberField.parentNode ); - numberField.remove(); - } - - const expiryField = document.getElementById( - 'ppcp-credit-card-gateway-card-expiry' - ); - if ( expiryField ) { - const styles = cardFieldStyles( expiryField ); - const fieldOptions = { - style: { input: styles }, - }; - if ( expiryField.getAttribute( 'placeholder' ) ) { - fieldOptions.placeholder = - expiryField.getAttribute( 'placeholder' ); - } - cardField - .ExpiryField( fieldOptions ) - .render( expiryField.parentNode ); - expiryField.remove(); - } - - const cvvField = document.getElementById( - 'ppcp-credit-card-gateway-card-cvc' - ); - if ( cvvField ) { - const styles = cardFieldStyles( cvvField ); - const fieldOptions = { - style: { input: styles }, - }; - if ( cvvField.getAttribute( 'placeholder' ) ) { - fieldOptions.placeholder = - cvvField.getAttribute( 'placeholder' ); - } - cardField - .CVVField( fieldOptions ) - .render( cvvField.parentNode ); - cvvField.remove(); - } - + if ( cardFields.isEligible() ) { + renderFields( cardFields ); document.dispatchEvent( new CustomEvent( 'hosted_fields_loaded' ) ); } @@ -169,7 +98,7 @@ class CardFieldsRenderer { return; } - cardField.submit().catch( ( error ) => { + cardFields.submit().catch( ( error ) => { this.spinner.unblock(); console.error( error ); this.errorHandler.message( diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js new file mode 100644 index 000000000..11fe31f79 --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js @@ -0,0 +1,822 @@ +import ConsoleLogger from '../../../../../ppcp-wc-gateway/resources/js/helper/ConsoleLogger'; +import { apmButtonsInit } from '../Helper/ApmButtons'; +import { + getCurrentPaymentMethod, + PaymentContext, + PaymentMethods, +} from '../Helper/CheckoutMethodState'; +import { + ButtonEvents, + dispatchButtonEvent, + observeButtonEvent, +} from '../Helper/PaymentButtonHelpers'; + +/** + * Collection of all available styling options for this button. + * + * @typedef {Object} StylesCollection + * @property {string} Default - Default button styling. + * @property {string} MiniCart - Styles for mini-cart button. + */ + +/** + * Collection of all available wrapper IDs that are possible for the button. + * + * @typedef {Object} WrapperCollection + * @property {string} Default - Default button wrapper. + * @property {string} Gateway - Wrapper for separate gateway. + * @property {string} Block - Wrapper for block checkout button. + * @property {string} MiniCart - Wrapper for mini-cart button. + * @property {string} SmartButton - Wrapper for smart button container. + */ + +/** + * Adds the provided PaymentButton instance to a global payment-button collection. + * + * This is debugging logic that should not be used on a production site. + * + * @param {string} methodName - Used to group the buttons. + * @param {PaymentButton} button - Appended to the button collection. + */ +const addToDebuggingCollection = ( methodName, button ) => { + window.ppcpPaymentButtonList = window.ppcpPaymentButtonList || {}; + + const collection = window.ppcpPaymentButtonList; + + collection[ methodName ] = collection[ methodName ] || []; + collection[ methodName ].push( button ); +}; + +/** + * Provides a context-independent instance Map for `PaymentButton` components. + * + * This function addresses a potential issue in multi-context environments, such as pages using + * Block-components. In these scenarios, multiple React execution contexts can lead to duplicate + * `PaymentButton` instances. To prevent this, we store instances in a `Map` that is bound to the + * document's `body` (the rendering context) rather than to individual React components + * (execution contexts). + * + * The `Map` is created as a non-enumerable, non-writable, and non-configurable property of + * `document.body` to ensure its integrity and prevent accidental modifications. + * + * @return {Map} A Map containing all `PaymentButton` instances for the current page. + */ +const getInstances = () => { + const collectionKey = '__ppcpPBInstances'; + + if ( ! document.body[ collectionKey ] ) { + Object.defineProperty( document.body, collectionKey, { + value: new Map(), + enumerable: false, + writable: false, + configurable: false, + } ); + } + + return document.body[ collectionKey ]; +}; + +/** + * Base class for APM payment buttons, like GooglePay and ApplePay. + * + * This class is not intended for the PayPal button. + */ +export default class PaymentButton { + /** + * Defines the implemented payment method. + * + * Used to identify and address the button internally. + * Overwrite this in the derived class. + * + * @type {string} + */ + static methodId = 'generic'; + + /** + * CSS class that is added to the payment button wrapper. + * + * Overwrite this in the derived class. + * + * @type {string} + */ + static cssClass = ''; + + /** + * @type {ConsoleLogger} + */ + #logger; + + /** + * Whether the payment button is initialized. + * + * @type {boolean} + */ + #isInitialized = false; + + /** + * The button's context. + * + * @type {string} + */ + #context; + + /** + * Object containing the IDs of all possible wrapper elements that might contain this + * button; only one wrapper is relevant, depending on the value of the context. + * + * @type {Object} + */ + #wrappers; + + /** + * @type {StylesCollection} + */ + #styles; + + /** + * Keeps track of CSS classes that were added to the wrapper element. + * We use this list to remove CSS classes that we've added, e.g. to change shape from + * pill to rect in the preview. + * + * @type {string[]} + */ + #appliedClasses = []; + + /** + * APM relevant configuration; e.g., configuration of the GooglePay button. + */ + #buttonConfig; + + /** + * Plugin-wide configuration; i.e., PayPal client ID, shop currency, etc. + */ + #ppcpConfig; + + /** + * A variation of a context bootstrap handler. + */ + #externalHandler; + + /** + * A variation of a context handler object, like CheckoutHandler. + * This handler provides a standardized interface for certain standardized checks and actions. + */ + #contextHandler; + + /** + * Whether the current browser/website support the payment method. + * + * @type {boolean} + */ + #isEligible = false; + + /** + * Whether this button is visible. Modified by `show()` and `hide()` + * + * @type {boolean} + */ + #isVisible = true; + + /** + * The currently visible payment button. + * + * @see {PaymentButton.insertButton} + * @type {HTMLElement|null} + */ + #button = null; + + /** + * Factory method to create a new PaymentButton while limiting a single instance per context. + * + * @param {string} context - Button context name. + * @param {unknown} externalHandler - Handler object. + * @param {Object} buttonConfig - Payment button specific configuration. + * @param {Object} ppcpConfig - Plugin wide configuration object. + * @param {unknown} contextHandler - Handler object. + * @return {PaymentButton} The button instance. + */ + static createButton( + context, + externalHandler, + buttonConfig, + ppcpConfig, + contextHandler + ) { + const buttonInstances = getInstances(); + const instanceKey = `${ this.methodId }.${ context }`; + + if ( ! buttonInstances.has( instanceKey ) ) { + const button = new this( + context, + externalHandler, + buttonConfig, + ppcpConfig, + contextHandler + ); + + buttonInstances.set( instanceKey, button ); + } + + return buttonInstances.get( instanceKey ); + } + + /** + * Returns a list with all wrapper IDs for the implemented payment method, categorized by + * context. + * + * @abstract + * @param {Object} buttonConfig - Payment method specific configuration. + * @param {Object} ppcpConfig - Global plugin configuration. + * @return {{MiniCart, Gateway, Block, SmartButton, Default}} The wrapper ID collection. + */ + // eslint-disable-next-line no-unused-vars + static getWrappers( buttonConfig, ppcpConfig ) { + throw new Error( 'Must be implemented in the child class' ); + } + + /** + * Returns a list of all button styles for the implemented payment method, categorized by + * context. + * + * @abstract + * @param {Object} buttonConfig - Payment method specific configuration. + * @param {Object} ppcpConfig - Global plugin configuration. + * @return {{MiniCart: (*), Default: (*)}} Combined styles, separated by context. + */ + // eslint-disable-next-line no-unused-vars + static getStyles( buttonConfig, ppcpConfig ) { + throw new Error( 'Must be implemented in the child class' ); + } + + /** + * Initialize the payment button instance. + * + * Do not create new button instances directly; use the `createButton` method instead + * to avoid multiple button instances handling the same context. + * + * @private + * @param {string} context - Button context name. + * @param {Object} externalHandler - Handler object. + * @param {Object} buttonConfig - Payment button specific configuration. + * @param {Object} ppcpConfig - Plugin wide configuration object. + * @param {Object} contextHandler - Handler object. + */ + constructor( + context, + externalHandler = null, + buttonConfig = {}, + ppcpConfig = {}, + contextHandler = null + ) { + if ( this.methodId === PaymentButton.methodId ) { + throw new Error( 'Cannot initialize the PaymentButton base class' ); + } + + if ( ! buttonConfig ) { + buttonConfig = {}; + } + + const isDebugging = !! buttonConfig?.is_debug; + const methodName = this.methodId.replace( /^ppcp?-/, '' ); + + this.#context = context; + this.#buttonConfig = buttonConfig; + this.#ppcpConfig = ppcpConfig; + this.#externalHandler = externalHandler; + this.#contextHandler = contextHandler; + + this.#logger = new ConsoleLogger( methodName, context ); + + if ( isDebugging ) { + this.#logger.enabled = true; + addToDebuggingCollection( methodName, this ); + } + + this.#wrappers = this.constructor.getWrappers( + this.#buttonConfig, + this.#ppcpConfig + ); + this.applyButtonStyles( this.#buttonConfig ); + + apmButtonsInit( this.#ppcpConfig ); + this.initEventListeners(); + } + + /** + * Internal ID of the payment gateway. + * + * @readonly + * @return {string} The internal gateway ID, defined in the derived class. + */ + get methodId() { + return this.constructor.methodId; + } + + /** + * CSS class that is added to the button wrapper. + * + * @readonly + * @return {string} CSS class, defined in the derived class. + */ + get cssClass() { + return this.constructor.cssClass; + } + + /** + * Whether the payment button was fully initialized. + * + * @readonly + * @return {boolean} True indicates, that the button was fully initialized. + */ + get isInitialized() { + return this.#isInitialized; + } + + /** + * The button's context. + * + * TODO: Convert the string to a context-object (primitive obsession smell) + * + * @readonly + * @return {string} The button context. + */ + get context() { + return this.#context; + } + + /** + * Configuration, specific for the implemented payment button. + * + * @return {Object} Configuration object. + */ + get buttonConfig() { + return this.#buttonConfig; + } + + /** + * Plugin-wide configuration; i.e., PayPal client ID, shop currency, etc. + * + * @return {Object} Configuration object. + */ + get ppcpConfig() { + return this.#ppcpConfig; + } + + /** + * @return {Object} The bootstrap handler instance, or an empty object. + */ + get externalHandler() { + return this.#externalHandler || {}; + } + + /** + * Access the button's context handler. + * When no context handler was provided (like for a preview button), an empty object is + * returned. + * + * @return {Object} The context handler instance, or an empty object. + */ + get contextHandler() { + return this.#contextHandler || {}; + } + + /** + * Whether customers need to provide shipping details during payment. + * + * Can be extended by child classes to take method specific configuration into account. + * + * @return {boolean} True means, shipping fields are displayed and must be filled. + */ + get requiresShipping() { + // Default check: Is shipping enabled in WooCommerce? + return ( + 'function' === typeof this.contextHandler.shippingAllowed && + this.contextHandler.shippingAllowed() + ); + } + + /** + * Button wrapper details. + * + * @readonly + * @return {WrapperCollection} Wrapper IDs. + */ + get wrappers() { + return this.#wrappers; + } + + /** + * Returns the context-relevant button style object. + * + * @readonly + * @return {string} Styling options. + */ + get style() { + if ( PaymentContext.MiniCart === this.context ) { + return this.#styles.MiniCart; + } + + return this.#styles.Default; + } + + /** + * Returns the context-relevant wrapper ID. + * + * @readonly + * @return {string} The wrapper-element's ID (without the `#` prefix). + */ + get wrapperId() { + if ( PaymentContext.MiniCart === this.context ) { + return this.wrappers.MiniCart; + } else if ( this.isSeparateGateway ) { + return this.wrappers.Gateway; + } else if ( PaymentContext.Blocks.includes( this.context ) ) { + return this.wrappers.Block; + } + + return this.wrappers.Default; + } + + /** + * Determines if the current payment button should be rendered as a stand-alone gateway. + * The return value `false` usually means, that the payment button is bundled with all available + * payment buttons. + * + * The decision depends on the button context (placement) and the plugin settings. + * + * @return {boolean} True, if the current button represents a stand-alone gateway. + */ + get isSeparateGateway() { + return ( + this.#buttonConfig.is_wc_gateway_enabled && + PaymentContext.Gateways.includes( this.context ) + ); + } + + /** + * Whether the currently selected payment gateway is set to the payment method. + * + * Only relevant on checkout pages, when `this.isSeparateGateway` is true. + * + * @return {boolean} True means that this payment method is selected as current gateway. + */ + get isCurrentGateway() { + if ( ! this.isSeparateGateway ) { + return false; + } + + /* + * We need to rely on `getCurrentPaymentMethod()` here, as the `CheckoutBootstrap.js` + * module fires the "ButtonEvents.RENDER" event before any PaymentButton instances are + * created. I.e. we cannot observe the initial gateway selection event. + */ + return this.methodId === getCurrentPaymentMethod(); + } + + /** + * Flags a preview button without actual payment logic. + * + * @return {boolean} True indicates a preview instance that has no payment logic. + */ + get isPreview() { + return PaymentContext.Preview === this.context; + } + + /** + * Whether the browser can accept this payment method. + * + * @return {boolean} True, if payments are technically possible. + */ + get isEligible() { + return this.#isEligible; + } + + /** + * Changes the eligibility state of this button component. + * + * @param {boolean} newState Whether the browser can accept payments. + */ + set isEligible( newState ) { + if ( newState === this.#isEligible ) { + return; + } + + this.#isEligible = newState; + this.triggerRedraw(); + } + + /** + * The visibility state of the button. + * This flag does not reflect actual visibility on the page, but rather, if the button + * is intended/allowed to be displayed, in case all other checks pass. + * + * @return {boolean} True indicates, that the button can be displayed. + */ + get isVisible() { + return this.#isVisible; + } + + /** + * Change the visibility of the button. + * + * A visible button does not always force the button to render on the page. It only means, that + * the button is allowed or not allowed to render, if certain other conditions are met. + * + * @param {boolean} newState Whether rendering the button is allowed. + */ + set isVisible( newState ) { + if ( this.#isVisible === newState ) { + return; + } + + this.#isVisible = newState; + this.triggerRedraw(); + } + + /** + * Returns the HTML element that wraps the current button + * + * @readonly + * @return {HTMLElement|null} The wrapper element, or null. + */ + get wrapperElement() { + return document.getElementById( this.wrapperId ); + } + + /** + * Checks whether the main button-wrapper is present in the current DOM. + * + * @readonly + * @return {boolean} True, if the button context (wrapper element) is found. + */ + get isPresent() { + return this.wrapperElement instanceof HTMLElement; + } + + /** + * Checks, if the payment button is still attached to the DOM. + * + * WooCommerce performs some partial reloads in many cases, which can lead to our payment + * button + * to move into the browser's memory. In that case, we need to recreate the button in the + * updated DOM. + * + * @return {boolean} True means, the button is still present (and typically visible) on the + * page. + */ + get isButtonAttached() { + if ( ! this.#button ) { + return false; + } + + let parent = this.#button.parentElement; + while ( parent?.parentElement ) { + if ( 'BODY' === parent.tagName ) { + return true; + } + + parent = parent.parentElement; + } + + return false; + } + + /** + * Log a debug detail to the browser console. + * + * @param {any} args + */ + log( ...args ) { + this.#logger.log( ...args ); + } + + /** + * Log an error message to the browser console. + * + * @param {any} args + */ + error( ...args ) { + this.#logger.error( ...args ); + } + + /** + * Determines if the current button instance has valid and complete configuration details. + * Used during initialization to decide if the button can be initialized or should be skipped. + * + * Can be implemented by the derived class. + * + * @param {boolean} [silent=false] - Set to true to suppress console errors. + * @return {boolean} True indicates the config is valid and initialization can continue. + */ + validateConfiguration( silent = false ) { + return true; + } + + applyButtonStyles( buttonConfig, ppcpConfig = null ) { + if ( ! ppcpConfig ) { + ppcpConfig = this.ppcpConfig; + } + + this.#styles = this.constructor.getStyles( buttonConfig, ppcpConfig ); + + if ( this.isInitialized ) { + this.triggerRedraw(); + } + } + + /** + * Configures the button instance. Must be called before the initial `init()`. + * + * Parameters are defined by the derived class. + * + * @abstract + */ + configure() {} + + /** + * Must be named `init()` to simulate "protected" visibility: + * Since the derived class also implements a method with the same name, this method can only + * be called by the derived class, but not from any other code. + */ + init() { + this.#isInitialized = true; + } + + /** + * Must be named `reinit()` to simulate "protected" visibility: + * Since the derived class also implements a method with the same name, this method can only + * be called by the derived class, but not from any other code. + */ + reinit() { + this.#isInitialized = false; + this.#isEligible = false; + } + + triggerRedraw() { + this.showPaymentGateway(); + + dispatchButtonEvent( { + event: ButtonEvents.REDRAW, + paymentMethod: this.methodId, + } ); + } + + /** + * Attaches event listeners to show or hide the payment button when needed. + */ + initEventListeners() { + // Refresh the button - this might show, hide or re-create the payment button. + observeButtonEvent( { + event: ButtonEvents.REDRAW, + paymentMethod: this.methodId, + callback: () => this.refresh(), + } ); + + // Events relevant for buttons inside a payment gateway. + if ( PaymentContext.Gateways.includes( this.context ) ) { + const parentMethod = this.isSeparateGateway + ? this.methodId + : PaymentMethods.PAYPAL; + + // Hide the button right after the user selected _any_ gateway. + observeButtonEvent( { + event: ButtonEvents.INVALIDATE, + callback: () => ( this.isVisible = false ), + } ); + + // Show the button (again) when the user selected the current gateway. + observeButtonEvent( { + event: ButtonEvents.RENDER, + paymentMethod: parentMethod, + callback: () => ( this.isVisible = true ), + } ); + } + } + + /** + * Refreshes the payment button on the page. + */ + refresh() { + if ( ! this.isPresent ) { + return; + } + + this.applyWrapperStyles(); + + if ( this.isEligible && this.isPresent && this.isVisible ) { + if ( ! this.isButtonAttached ) { + this.log( 'refresh.addButton' ); + this.addButton(); + } + } + } + + /** + * Makes the custom payment gateway visible by removing initial inline styles from the DOM. + * + * Only relevant on the checkout page, i.e., when `this.isSeparateGateway` is `true` + */ + showPaymentGateway() { + if ( ! this.isSeparateGateway || ! this.isEligible ) { + return; + } + + const styleSelectors = `style[data-hide-gateway="${ this.methodId }"]`; + + const styles = document.querySelectorAll( styleSelectors ); + + if ( ! styles.length ) { + return; + } + this.log( 'Show gateway' ); + + styles.forEach( ( el ) => el.remove() ); + + // This code runs only once, during button initialization, and fixes the initial visibility. + this.isVisible = this.isCurrentGateway; + } + + /** + * Applies CSS classes and inline styling to the payment button wrapper. + */ + applyWrapperStyles() { + const wrapper = this.wrapperElement; + const { shape, height } = this.style; + + for ( const classItem of this.#appliedClasses ) { + wrapper.classList.remove( classItem ); + } + + this.#appliedClasses = []; + + const newClasses = [ + `ppcp-button-${ shape }`, + 'ppcp-button-apm', + this.cssClass, + ]; + + wrapper.classList.add( ...newClasses ); + this.#appliedClasses.push( ...newClasses ); + + if ( height ) { + wrapper.style.height = `${ height }px`; + } + + // Apply the wrapper visibility. + wrapper.style.display = this.isVisible ? 'block' : 'none'; + } + + /** + * Creates a new payment button (HTMLElement) and must call `this.insertButton()` to display + * that button in the correct wrapper. + * + * @abstract + */ + addButton() { + throw new Error( 'Must be implemented by the child class' ); + } + + /** + * Prepares the button wrapper element and inserts the provided payment button into the DOM. + * + * If a payment button was previously inserted to the wrapper, calling this method again will + * first remove the previous button. + * + * @param {HTMLElement} button - The button element to inject. + */ + insertButton( button ) { + if ( ! this.isPresent ) { + return; + } + + const wrapper = this.wrapperElement; + + if ( this.#button ) { + this.removeButton(); + } + + this.log( 'addButton', button ); + + this.#button = button; + wrapper.appendChild( this.#button ); + } + + /** + * Removes the payment button from the DOM. + */ + removeButton() { + if ( ! this.isPresent || ! this.#button ) { + return; + } + + this.log( 'removeButton' ); + + try { + this.wrapperElement.removeChild( this.#button ); + } catch ( Exception ) { + // Ignore this. + } + + this.#button = null; + } +} diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js b/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js index 42e715904..1ea2f04bc 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js @@ -1,11 +1,17 @@ import { loadCustomScript } from '@paypal/paypal-js'; import widgetBuilder from './WidgetBuilder'; import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounce'; +import ConsoleLogger from '../../../../../ppcp-wc-gateway/resources/js/helper/ConsoleLogger'; /** * Manages all PreviewButton instances of a certain payment method on the page. */ class PreviewButtonManager { + /** + * @type {ConsoleLogger} + */ + #logger; + /** * Resolves the promise. * Used by `this.boostrap()` to process enqueued initialization logic. @@ -32,6 +38,9 @@ class PreviewButtonManager { this.apiConfig = null; this.apiError = ''; + this.#logger = new ConsoleLogger( this.methodName, 'preview-manager' ); + this.#logger.enabled = true; // Manually set this to true for development. + this.#onInit = new Promise( ( resolve ) => { this.#onInitResolver = resolve; } ); @@ -61,9 +70,11 @@ class PreviewButtonManager { * Responsible for fetching and returning the PayPal configuration object for this payment * method. * + * @abstract * @param {{}} payPal - The PayPal SDK object provided by WidgetBuilder. * @return {Promise<{}>} */ + // eslint-disable-next-line no-unused-vars async fetchConfig( payPal ) { throw new Error( 'The "fetchConfig" method must be implemented by the derived class' @@ -74,9 +85,11 @@ class PreviewButtonManager { * Protected method that needs to be implemented by the derived class. * This method is responsible for creating a new PreviewButton instance and returning it. * + * @abstract * @param {string} wrapperId - CSS ID of the wrapper element. * @return {PreviewButton} */ + // eslint-disable-next-line no-unused-vars createButtonInstance( wrapperId ) { throw new Error( 'The "createButtonInstance" method must be implemented by the derived class' @@ -90,7 +103,7 @@ class PreviewButtonManager { * This dummy is only visible on the admin side, and not rendered on the front-end. * * @todo Consider refactoring this into a new class that extends the PreviewButton class. - * @param wrapperId + * @param {string} wrapperId * @return {any} */ createDummy( wrapperId ) { @@ -128,13 +141,24 @@ class PreviewButtonManager { ); } + /** + * Output a debug message to the console, with a module-specific prefix. + * + * @param {string} message - Log message. + * @param {...any} args - Optional. Additional args to output. + */ + log( message, ...args ) { + this.#logger.log( message, ...args ); + } + /** * Output an error message to the console, with a module-specific prefix. - * @param message - * @param {...any} args + * + * @param {string} message - Log message. + * @param {...any} args - Optional. Additional args to output. */ error( message, ...args ) { - console.error( `${ this.methodName } ${ message }`, ...args ); + this.#logger.error( message, ...args ); } /** @@ -242,21 +266,21 @@ class PreviewButtonManager { } if ( ! this.shouldInsertPreviewButton( id ) ) { + this.log( 'Skip preview rendering for this preview-box', id ); return; } if ( ! this.buttons[ id ] ) { this._addButton( id, ppcpConfig ); } else { - // This is a debounced method, that fires after 100ms. - this._configureAllButtons( ppcpConfig ); + this._configureButton( id, ppcpConfig ); } } /** * Determines if the preview box supports the current button. * - * When this function returns false, this manager instance does not create a new preview button. + * E.g. "Should the current preview-box display Google Pay buttons?" * * @param {string} previewId - ID of the inner preview box container. * @return {boolean} True if the box is eligible for the preview button, false otherwise. @@ -271,10 +295,14 @@ class PreviewButtonManager { /** * Applies a new configuration to an existing preview button. + * + * @private * @param id * @param ppcpConfig */ _configureButton( id, ppcpConfig ) { + this.log( 'configureButton', id, ppcpConfig ); + this.buttons[ id ] .setDynamic( this.isDynamic() ) .setPpcpConfig( ppcpConfig ) @@ -283,9 +311,13 @@ class PreviewButtonManager { /** * Apples the provided configuration to all existing preview buttons. - * @param ppcpConfig + * + * @private + * @param ppcpConfig - The new styling to use for the preview buttons. */ _configureAllButtons( ppcpConfig ) { + this.log( 'configureAllButtons', ppcpConfig ); + Object.entries( this.buttons ).forEach( ( [ id, button ] ) => { this._configureButton( id, { ...ppcpConfig, @@ -302,13 +334,20 @@ class PreviewButtonManager { /** * Creates a new preview button, that is rendered once the bootstrapping Promise resolves. - * @param id - * @param ppcpConfig + * + * @private + * @param id - The button to add. + * @param ppcpConfig - The styling to apply to the preview button. */ _addButton( id, ppcpConfig ) { + this.log( 'addButton', id, ppcpConfig ); + const createButton = () => { if ( ! this.buttons[ id ] ) { + this.log( 'createButton.new', id ); + let newInst; + if ( this.apiConfig && 'object' === typeof this.apiConfig ) { newInst = this.createButtonInstance( id ).setButtonConfig( this.buttonConfig diff --git a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js index 448432e3c..63bd5e2ed 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js @@ -139,7 +139,7 @@ class Renderer { }; // Check the condition and add the handler if needed - if ( this.defaultSettings.should_handle_shipping_in_paypal ) { + if ( this.shouldEnableShippingCallback() ) { options.onShippingOptionsChange = ( data, actions ) => { let shippingOptionsChange = ! this.isVenmoButtonClickedWhenVaultingIsEnabled( @@ -227,6 +227,12 @@ class Renderer { return venmoButtonClicked && this.defaultSettings.vaultingEnabled; }; + shouldEnableShippingCallback = () => { + console.log(this.defaultSettings.context, this.defaultSettings) + let needShipping = this.defaultSettings.needShipping || this.defaultSettings.context === 'product' + return this.defaultSettings.should_handle_shipping_in_paypal && needShipping + }; + isAlreadyRendered( wrapper, fundingSource ) { return this.renderedSources.has( wrapper + ( fundingSource ?? '' ) ); } diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index 4b309818e..e5fc8cfc6 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Button\Assets; use Exception; use Psr\Log\LoggerInterface; use WC_Order; +use WC_Payment_Tokens; use WC_Product; use WC_Product_Variation; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint; @@ -1292,9 +1293,11 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages 'early_checkout_validation_enabled' => $this->early_validation_enabled, 'funding_sources_without_redirect' => $this->funding_sources_without_redirect, 'user' => array( - 'is_logged' => is_user_logged_in(), + 'is_logged' => is_user_logged_in(), + 'has_wc_card_payment_tokens' => $this->user_has_wc_card_payment_tokens( get_current_user_id() ), ), 'should_handle_shipping_in_paypal' => $this->should_handle_shipping_in_paypal && ! $this->is_checkout(), + 'needShipping' => WC()->cart->needs_shipping(), 'vaultingEnabled' => $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' ), ); @@ -2132,4 +2135,19 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages return $location; } } + + /** + * Whether the given user has WC card payment tokens. + * + * @param int $user_id The user ID. + * @return bool + */ + private function user_has_wc_card_payment_tokens( int $user_id ): bool { + $tokens = WC_Payment_Tokens::get_customer_tokens( $user_id, CreditCardGateway::ID ); + if ( $tokens ) { + return true; + } + + return false; + } } diff --git a/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php b/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php index 888a567c5..9d3326010 100644 --- a/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php +++ b/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php @@ -9,8 +9,10 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Button\Helper; +use Exception; use RuntimeException; use WC_Cart; +use WC_Data_Exception; use WC_Order; use WC_Order_Item_Product; use WC_Order_Item_Shipping; @@ -85,17 +87,24 @@ class WooCommerceOrderCreator { throw new RuntimeException( 'Problem creating WC order.' ); } - $payer = $order->payer(); - $shipping = $order->purchase_units()[0]->shipping(); + try { + $payer = $order->payer(); + $shipping = $order->purchase_units()[0]->shipping(); - $this->configure_payment_source( $wc_order ); - $this->configure_customer( $wc_order ); - $this->configure_line_items( $wc_order, $wc_cart, $payer, $shipping ); - $this->configure_shipping( $wc_order, $payer, $shipping ); - $this->configure_coupons( $wc_order, $wc_cart->get_applied_coupons() ); + $this->configure_payment_source( $wc_order ); + $this->configure_customer( $wc_order ); + $this->configure_line_items( $wc_order, $wc_cart, $payer, $shipping ); + $this->configure_shipping( $wc_order, $payer, $shipping, $wc_cart ); + $this->configure_coupons( $wc_order, $wc_cart->get_applied_coupons() ); - $wc_order->calculate_totals(); - $wc_order->save(); + $wc_order->calculate_totals(); + $wc_order->save(); + } catch ( Exception $exception ) { + $wc_order->delete( true ); + throw new RuntimeException( 'Failed to create WooCommerce order: ' . $exception->getMessage() ); + } + + do_action( 'woocommerce_paypal_payments_shipping_callback_woocommerce_order_created', $wc_order, $wc_cart ); return $wc_order; } @@ -153,7 +162,7 @@ class WooCommerceOrderCreator { $item->set_total( $subscription_total ); $subscription->add_product( $product ); - $this->configure_shipping( $subscription, $payer, $shipping ); + $this->configure_shipping( $subscription, $payer, $shipping, $wc_cart ); $this->configure_payment_source( $subscription ); $this->configure_coupons( $subscription, $wc_cart->get_applied_coupons() ); @@ -178,9 +187,11 @@ class WooCommerceOrderCreator { * @param WC_Order $wc_order The WC order. * @param Payer|null $payer The payer. * @param Shipping|null $shipping The shipping. + * @param WC_Cart $wc_cart The Cart. * @return void + * @throws WC_Data_Exception|RuntimeException When failing to configure shipping. */ - protected function configure_shipping( WC_Order $wc_order, ?Payer $payer, ?Shipping $shipping ): void { + protected function configure_shipping( WC_Order $wc_order, ?Payer $payer, ?Shipping $shipping, WC_Cart $wc_cart ): void { $shipping_address = null; $billing_address = null; $shipping_options = null; @@ -218,6 +229,10 @@ class WooCommerceOrderCreator { $shipping_options = $shipping->options()[0] ?? ''; } + if ( $wc_cart->needs_shipping() && empty( $shipping_options ) ) { + throw new RuntimeException( 'No shipping method has been selected.' ); + } + if ( $shipping_address ) { $wc_order->set_shipping_address( $shipping_address ); } diff --git a/modules/ppcp-card-fields/package.json b/modules/ppcp-card-fields/package.json new file mode 100644 index 000000000..20ea98a53 --- /dev/null +++ b/modules/ppcp-card-fields/package.json @@ -0,0 +1,31 @@ +{ + "name": "ppcp-card-fields", + "version": "1.0.0", + "license": "GPL-3.0-or-later", + "browserslist": [ + "> 0.5%", + "Safari >= 8", + "Chrome >= 41", + "Firefox >= 43", + "Edge >= 14" + ], + "dependencies": { + "core-js": "^3.25.0" + }, + "devDependencies": { + "@babel/core": "^7.19", + "@babel/preset-env": "^7.19", + "babel-loader": "^8.2", + "cross-env": "^7.0.3", + "file-loader": "^6.2.0", + "sass": "^1.42.1", + "sass-loader": "^12.1.0", + "webpack": "^5.76", + "webpack-cli": "^4.10" + }, + "scripts": { + "build": "cross-env BABEL_ENV=default NODE_ENV=production webpack", + "watch": "cross-env BABEL_ENV=default NODE_ENV=production webpack --watch", + "dev": "cross-env BABEL_ENV=default webpack --watch" + } +} diff --git a/modules/ppcp-button/resources/js/modules/Helper/CardFieldsHelper.js b/modules/ppcp-card-fields/resources/js/CardFieldsHelper.js similarity index 100% rename from modules/ppcp-button/resources/js/modules/Helper/CardFieldsHelper.js rename to modules/ppcp-card-fields/resources/js/CardFieldsHelper.js diff --git a/modules/ppcp-card-fields/resources/js/Render.js b/modules/ppcp-card-fields/resources/js/Render.js new file mode 100644 index 000000000..9a35ff449 --- /dev/null +++ b/modules/ppcp-card-fields/resources/js/Render.js @@ -0,0 +1,47 @@ +import { cardFieldStyles } from './CardFieldsHelper'; + +export function renderFields( cardFields ) { + const nameField = document.getElementById( + 'ppcp-credit-card-gateway-card-name' + ); + if ( nameField && nameField.hidden !== true ) { + const styles = cardFieldStyles( nameField ); + cardFields + .NameField( { style: { input: styles } } ) + .render( nameField.parentNode ); + nameField.hidden = true; + } + + const numberField = document.getElementById( + 'ppcp-credit-card-gateway-card-number' + ); + if ( numberField && numberField.hidden !== true ) { + const styles = cardFieldStyles( numberField ); + cardFields + .NumberField( { style: { input: styles } } ) + .render( numberField.parentNode ); + numberField.hidden = true; + } + + const expiryField = document.getElementById( + 'ppcp-credit-card-gateway-card-expiry' + ); + if ( expiryField && expiryField.hidden !== true ) { + const styles = cardFieldStyles( expiryField ); + cardFields + .ExpiryField( { style: { input: styles } } ) + .render( expiryField.parentNode ); + expiryField.hidden = true; + } + + const cvvField = document.getElementById( + 'ppcp-credit-card-gateway-card-cvc' + ); + if ( cvvField && cvvField.hidden !== true ) { + const styles = cardFieldStyles( cvvField ); + cardFields + .CVVField( { style: { input: styles } } ) + .render( cvvField.parentNode ); + cvvField.hidden = true; + } +} diff --git a/modules/ppcp-card-fields/services.php b/modules/ppcp-card-fields/services.php index 85f8004b6..1e32cc4f6 100644 --- a/modules/ppcp-card-fields/services.php +++ b/modules/ppcp-card-fields/services.php @@ -17,795 +17,51 @@ return array( $save_payment_methods_applies = $container->get( 'card-fields.helpers.save-payment-methods-applies' ); assert( $save_payment_methods_applies instanceof CardFieldsApplies ); - return $save_payment_methods_applies->for_country_currency(); + return $save_payment_methods_applies->for_country(); }, 'card-fields.helpers.save-payment-methods-applies' => static function ( ContainerInterface $container ) : CardFieldsApplies { return new CardFieldsApplies( - $container->get( 'card-fields.supported-country-currency-matrix' ), - $container->get( 'api.shop.currency' ), + $container->get( 'card-fields.supported-country-matrix' ), $container->get( 'api.shop.country' ) ); }, - 'card-fields.supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array { + 'card-fields.supported-country-matrix' => static function ( ContainerInterface $container ) : array { return apply_filters( - 'woocommerce_paypal_payments_card_fields_supported_country_currency_matrix', + 'woocommerce_paypal_payments_card_fields_supported_country_matrix', array( - 'AU' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'AT' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'BE' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'BG' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'CA' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'CN' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'CY' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'CZ' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'DK' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'EE' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'FI' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'FR' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'DE' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'GR' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'HU' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'IE' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'IT' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'LV' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'LI' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'LT' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'LU' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'MT' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'NL' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'PL' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'PT' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'RO' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'SK' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'SI' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'ES' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'SE' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'GB' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), - 'US' => array( - 'AUD', - 'CAD', - 'EUR', - 'GBP', - 'JPY', - 'USD', - ), - 'NO' => array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ), + 'AU', + 'AT', + 'BE', + 'BG', + 'CA', + 'CN', + 'CY', + 'CZ', + 'DK', + 'EE', + 'FI', + 'FR', + 'DE', + 'GR', + 'HU', + 'IE', + 'IT', + 'LV', + 'LI', + 'LT', + 'LU', + 'MT', + 'NL', + 'PL', + 'PT', + 'RO', + 'SK', + 'SI', + 'ES', + 'SE', + 'GB', + 'US', + 'NO', ) ); }, diff --git a/modules/ppcp-card-fields/src/Helper/CardFieldsApplies.php b/modules/ppcp-card-fields/src/Helper/CardFieldsApplies.php index c5111b9bb..fad1b5d43 100644 --- a/modules/ppcp-card-fields/src/Helper/CardFieldsApplies.php +++ b/modules/ppcp-card-fields/src/Helper/CardFieldsApplies.php @@ -1,6 +1,6 @@ allowed_country_currency_matrix = $allowed_country_currency_matrix; - $this->currency = $currency; - $this->country = $country; + $this->allowed_country_matrix = $allowed_country_matrix; + $this->country = $country; } /** - * Returns whether Card Fields can be used in the current country and the current currency. + * Returns whether Card Fields can be used in the current country. * * @return bool */ - public function for_country_currency(): bool { - if ( ! in_array( $this->country, array_keys( $this->allowed_country_currency_matrix ), true ) ) { - return false; - } - return in_array( $this->currency, $this->allowed_country_currency_matrix[ $this->country ], true ); + public function for_country(): bool { + return in_array( $this->country, $this->allowed_country_matrix, true ); } } diff --git a/modules/ppcp-card-fields/webpack.config.js b/modules/ppcp-card-fields/webpack.config.js new file mode 100644 index 000000000..a952f7de0 --- /dev/null +++ b/modules/ppcp-card-fields/webpack.config.js @@ -0,0 +1,38 @@ +const path = require( 'path' ); +const isProduction = process.env.NODE_ENV === 'production'; + +module.exports = { + devtool: isProduction ? 'source-map' : 'eval-source-map', + mode: isProduction ? 'production' : 'development', + target: 'web', + entry: { + render: path.resolve( './resources/js/Render.js' ), + helper: path.resolve( './resources/js/CardFieldsHelper.js' ), + }, + output: { + path: path.resolve( __dirname, 'assets/' ), + filename: 'js/[name].js', + }, + module: { + rules: [ + { + test: /\.js?$/, + exclude: /node_modules/, + loader: 'babel-loader', + }, + { + test: /\.scss$/, + exclude: /node_modules/, + use: [ + { + loader: 'file-loader', + options: { + name: 'css/[name].css', + }, + }, + { loader: 'sass-loader' }, + ], + }, + ], + }, +}; diff --git a/modules/ppcp-card-fields/yarn.lock b/modules/ppcp-card-fields/yarn.lock new file mode 100644 index 000000000..42b77f28b --- /dev/null +++ b/modules/ppcp-card-fields/yarn.lock @@ -0,0 +1,2194 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@babel/code-frame@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" + integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== + dependencies: + "@babel/highlight" "^7.24.7" + picocolors "^1.0.0" + +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.24.8": + version "7.24.9" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.9.tgz#53eee4e68f1c1d0282aa0eb05ddb02d033fc43a0" + integrity sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng== + +"@babel/core@^7.19": + version "7.24.9" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.9.tgz#dc07c9d307162c97fa9484ea997ade65841c7c82" + integrity sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.24.9" + "@babel/helper-compilation-targets" "^7.24.8" + "@babel/helper-module-transforms" "^7.24.9" + "@babel/helpers" "^7.24.8" + "@babel/parser" "^7.24.8" + "@babel/template" "^7.24.7" + "@babel/traverse" "^7.24.8" + "@babel/types" "^7.24.9" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.24.8", "@babel/generator@^7.24.9": + version "7.24.10" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.10.tgz#a4ab681ec2a78bbb9ba22a3941195e28a81d8e76" + integrity sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg== + dependencies: + "@babel/types" "^7.24.9" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^2.5.1" + +"@babel/helper-annotate-as-pure@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz#5373c7bc8366b12a033b4be1ac13a206c6656aab" + integrity sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg== + dependencies: + "@babel/types" "^7.24.7" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz#37d66feb012024f2422b762b9b2a7cfe27c7fba3" + integrity sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.24.7", "@babel/helper-compilation-targets@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz#b607c3161cd9d1744977d4f97139572fe778c271" + integrity sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw== + dependencies: + "@babel/compat-data" "^7.24.8" + "@babel/helper-validator-option" "^7.24.8" + browserslist "^4.23.1" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.24.7": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.8.tgz#47f546408d13c200c0867f9d935184eaa0851b09" + integrity sha512-4f6Oqnmyp2PP3olgUMmOwC3akxSm5aBYraQ6YDdKy7NcAMkDECHWG0DEnV6M2UAkERgIBhYt8S27rURPg7SxWA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-function-name" "^7.24.7" + "@babel/helper-member-expression-to-functions" "^7.24.8" + "@babel/helper-optimise-call-expression" "^7.24.7" + "@babel/helper-replace-supers" "^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + "@babel/helper-split-export-declaration" "^7.24.7" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz#be4f435a80dc2b053c76eeb4b7d16dd22cfc89da" + integrity sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + regexpu-core "^5.3.1" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.6.1", "@babel/helper-define-polyfill-provider@^0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz#18594f789c3594acb24cfdb4a7f7b7d2e8bd912d" + integrity sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ== + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + +"@babel/helper-environment-visitor@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz#4b31ba9551d1f90781ba83491dd59cf9b269f7d9" + integrity sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ== + dependencies: + "@babel/types" "^7.24.7" + +"@babel/helper-function-name@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz#75f1e1725742f39ac6584ee0b16d94513da38dd2" + integrity sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA== + dependencies: + "@babel/template" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-hoist-variables@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz#b4ede1cde2fd89436397f30dc9376ee06b0f25ee" + integrity sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ== + dependencies: + "@babel/types" "^7.24.7" + +"@babel/helper-member-expression-to-functions@^7.24.7", "@babel/helper-member-expression-to-functions@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz#6155e079c913357d24a4c20480db7c712a5c3fb6" + integrity sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA== + dependencies: + "@babel/traverse" "^7.24.8" + "@babel/types" "^7.24.8" + +"@babel/helper-module-imports@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b" + integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-module-transforms@^7.24.7", "@babel/helper-module-transforms@^7.24.8", "@babel/helper-module-transforms@^7.24.9": + version "7.24.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz#e13d26306b89eea569180868e652e7f514de9d29" + integrity sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw== + dependencies: + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-module-imports" "^7.24.7" + "@babel/helper-simple-access" "^7.24.7" + "@babel/helper-split-export-declaration" "^7.24.7" + "@babel/helper-validator-identifier" "^7.24.7" + +"@babel/helper-optimise-call-expression@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz#8b0a0456c92f6b323d27cfd00d1d664e76692a0f" + integrity sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A== + dependencies: + "@babel/types" "^7.24.7" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.24.8", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz#94ee67e8ec0e5d44ea7baeb51e571bd26af07878" + integrity sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg== + +"@babel/helper-remap-async-to-generator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz#b3f0f203628522713849d49403f1a414468be4c7" + integrity sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-wrap-function" "^7.24.7" + +"@babel/helper-replace-supers@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz#f933b7eed81a1c0265740edc91491ce51250f765" + integrity sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg== + dependencies: + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-member-expression-to-functions" "^7.24.7" + "@babel/helper-optimise-call-expression" "^7.24.7" + +"@babel/helper-simple-access@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3" + integrity sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-skip-transparent-expression-wrappers@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz#5f8fa83b69ed5c27adc56044f8be2b3ea96669d9" + integrity sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-split-export-declaration@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz#83949436890e07fa3d6873c61a96e3bbf692d856" + integrity sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA== + dependencies: + "@babel/types" "^7.24.7" + +"@babel/helper-string-parser@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d" + integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== + +"@babel/helper-validator-identifier@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" + integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== + +"@babel/helper-validator-option@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz#3725cdeea8b480e86d34df15304806a06975e33d" + integrity sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q== + +"@babel/helper-wrap-function@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz#52d893af7e42edca7c6d2c6764549826336aae1f" + integrity sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw== + dependencies: + "@babel/helper-function-name" "^7.24.7" + "@babel/template" "^7.24.7" + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helpers@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.8.tgz#2820d64d5d6686cca8789dd15b074cd862795873" + integrity sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ== + dependencies: + "@babel/template" "^7.24.7" + "@babel/types" "^7.24.8" + +"@babel/highlight@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" + integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== + dependencies: + "@babel/helper-validator-identifier" "^7.24.7" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/parser@^7.24.7", "@babel/parser@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.8.tgz#58a4dbbcad7eb1d48930524a3fd93d93e9084c6f" + integrity sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w== + +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz#fd059fd27b184ea2b4c7e646868a9a381bbc3055" + integrity sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ== + dependencies: + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz#468096ca44bbcbe8fcc570574e12eb1950e18107" + integrity sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz#e4eabdd5109acc399b38d7999b2ef66fc2022f89" + integrity sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + "@babel/plugin-transform-optional-chaining" "^7.24.7" + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.7.tgz#71b21bb0286d5810e63a1538aa901c58e87375ec" + integrity sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg== + dependencies: + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-import-assertions@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz#2a0b406b5871a20a841240586b1300ce2088a778" + integrity sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-syntax-import-attributes@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz#b4f9ea95a79e6912480c4b626739f86a076624ca" + integrity sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-syntax-import-meta@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-arrow-functions@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz#4f6886c11e423bd69f3ce51dbf42424a5f275514" + integrity sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-async-generator-functions@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.7.tgz#7330a5c50e05181ca52351b8fd01642000c96cfd" + integrity sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g== + dependencies: + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-remap-async-to-generator" "^7.24.7" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-transform-async-to-generator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz#72a3af6c451d575842a7e9b5a02863414355bdcc" + integrity sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA== + dependencies: + "@babel/helper-module-imports" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-remap-async-to-generator" "^7.24.7" + +"@babel/plugin-transform-block-scoped-functions@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz#a4251d98ea0c0f399dafe1a35801eaba455bbf1f" + integrity sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-block-scoping@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz#42063e4deb850c7bd7c55e626bf4e7ab48e6ce02" + integrity sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-class-properties@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz#256879467b57b0b68c7ddfc5b76584f398cd6834" + integrity sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-class-static-block@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz#c82027ebb7010bc33c116d4b5044fbbf8c05484d" + integrity sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + +"@babel/plugin-transform-classes@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.8.tgz#ad23301fe5bc153ca4cf7fb572a9bc8b0b711cf7" + integrity sha512-VXy91c47uujj758ud9wx+OMgheXm4qJfyhj1P18YvlrQkNOSrwsteHk+EFS3OMGfhMhpZa0A+81eE7G4QC+3CA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-compilation-targets" "^7.24.8" + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-function-name" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-replace-supers" "^7.24.7" + "@babel/helper-split-export-declaration" "^7.24.7" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz#4cab3214e80bc71fae3853238d13d097b004c707" + integrity sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/template" "^7.24.7" + +"@babel/plugin-transform-destructuring@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz#c828e814dbe42a2718a838c2a2e16a408e055550" + integrity sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.8" + +"@babel/plugin-transform-dotall-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz#5f8bf8a680f2116a7207e16288a5f974ad47a7a0" + integrity sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-duplicate-keys@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz#dd20102897c9a2324e5adfffb67ff3610359a8ee" + integrity sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-dynamic-import@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz#4d8b95e3bae2b037673091aa09cd33fecd6419f4" + integrity sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-transform-exponentiation-operator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz#b629ee22645f412024297d5245bce425c31f9b0d" + integrity sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-export-namespace-from@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz#176d52d8d8ed516aeae7013ee9556d540c53f197" + integrity sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-transform-for-of@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz#f25b33f72df1d8be76399e1b8f3f9d366eb5bc70" + integrity sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + +"@babel/plugin-transform-function-name@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz#6d8601fbffe665c894440ab4470bc721dd9131d6" + integrity sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w== + dependencies: + "@babel/helper-compilation-targets" "^7.24.7" + "@babel/helper-function-name" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-json-strings@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz#f3e9c37c0a373fee86e36880d45b3664cedaf73a" + integrity sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-json-strings" "^7.8.3" + +"@babel/plugin-transform-literals@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz#36b505c1e655151a9d7607799a9988fc5467d06c" + integrity sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-logical-assignment-operators@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz#a58fb6eda16c9dc8f9ff1c7b1ba6deb7f4694cb0" + integrity sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-transform-member-expression-literals@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz#3b4454fb0e302e18ba4945ba3246acb1248315df" + integrity sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-modules-amd@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz#65090ed493c4a834976a3ca1cde776e6ccff32d7" + integrity sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg== + dependencies: + "@babel/helper-module-transforms" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-modules-commonjs@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz#ab6421e564b717cb475d6fff70ae7f103536ea3c" + integrity sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA== + dependencies: + "@babel/helper-module-transforms" "^7.24.8" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-simple-access" "^7.24.7" + +"@babel/plugin-transform-modules-systemjs@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz#f8012316c5098f6e8dee6ecd58e2bc6f003d0ce7" + integrity sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw== + dependencies: + "@babel/helper-hoist-variables" "^7.24.7" + "@babel/helper-module-transforms" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-validator-identifier" "^7.24.7" + +"@babel/plugin-transform-modules-umd@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz#edd9f43ec549099620df7df24e7ba13b5c76efc8" + integrity sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A== + dependencies: + "@babel/helper-module-transforms" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz#9042e9b856bc6b3688c0c2e4060e9e10b1460923" + integrity sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-new-target@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz#31ff54c4e0555cc549d5816e4ab39241dfb6ab00" + integrity sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz#1de4534c590af9596f53d67f52a92f12db984120" + integrity sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-transform-numeric-separator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz#bea62b538c80605d8a0fac9b40f48e97efa7de63" + integrity sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-transform-object-rest-spread@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz#d13a2b93435aeb8a197e115221cab266ba6e55d6" + integrity sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q== + dependencies: + "@babel/helper-compilation-targets" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.24.7" + +"@babel/plugin-transform-object-super@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz#66eeaff7830bba945dd8989b632a40c04ed625be" + integrity sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-replace-supers" "^7.24.7" + +"@babel/plugin-transform-optional-catch-binding@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz#00eabd883d0dd6a60c1c557548785919b6e717b4" + integrity sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-transform-optional-chaining@^7.24.7", "@babel/plugin-transform-optional-chaining@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz#bb02a67b60ff0406085c13d104c99a835cdf365d" + integrity sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-transform-parameters@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz#5881f0ae21018400e320fc7eb817e529d1254b68" + integrity sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-private-methods@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz#e6318746b2ae70a59d023d5cc1344a2ba7a75f5e" + integrity sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-private-property-in-object@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz#4eec6bc701288c1fab5f72e6a4bbc9d67faca061" + integrity sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-create-class-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-transform-property-literals@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz#f0d2ed8380dfbed949c42d4d790266525d63bbdc" + integrity sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-regenerator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz#021562de4534d8b4b1851759fd7af4e05d2c47f8" + integrity sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + regenerator-transform "^0.15.2" + +"@babel/plugin-transform-reserved-words@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz#80037fe4fbf031fc1125022178ff3938bb3743a4" + integrity sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-shorthand-properties@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz#85448c6b996e122fa9e289746140aaa99da64e73" + integrity sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-spread@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz#e8a38c0fde7882e0fb8f160378f74bd885cc7bb3" + integrity sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + +"@babel/plugin-transform-sticky-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz#96ae80d7a7e5251f657b5cf18f1ea6bf926f5feb" + integrity sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-template-literals@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz#a05debb4a9072ae8f985bcf77f3f215434c8f8c8" + integrity sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-typeof-symbol@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz#383dab37fb073f5bfe6e60c654caac309f92ba1c" + integrity sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.8" + +"@babel/plugin-transform-unicode-escapes@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz#2023a82ced1fb4971630a2e079764502c4148e0e" + integrity sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-unicode-property-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz#9073a4cd13b86ea71c3264659590ac086605bbcd" + integrity sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-unicode-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz#dfc3d4a51127108099b19817c0963be6a2adf19f" + integrity sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-unicode-sets-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz#d40705d67523803a576e29c63cef6e516b858ed9" + integrity sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/preset-env@^7.19": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.8.tgz#e0db94d7f17d6f0e2564e8d29190bc8cdacec2d1" + integrity sha512-vObvMZB6hNWuDxhSaEPTKCwcqkAIuDtE+bQGn4XMXne1DSLzFVY8Vmj1bm+mUQXYNN8NmaQEO+r8MMbzPr1jBQ== + dependencies: + "@babel/compat-data" "^7.24.8" + "@babel/helper-compilation-targets" "^7.24.8" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-validator-option" "^7.24.8" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.24.7" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.24.7" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.24.7" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.24.7" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-import-assertions" "^7.24.7" + "@babel/plugin-syntax-import-attributes" "^7.24.7" + "@babel/plugin-syntax-import-meta" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.24.7" + "@babel/plugin-transform-async-generator-functions" "^7.24.7" + "@babel/plugin-transform-async-to-generator" "^7.24.7" + "@babel/plugin-transform-block-scoped-functions" "^7.24.7" + "@babel/plugin-transform-block-scoping" "^7.24.7" + "@babel/plugin-transform-class-properties" "^7.24.7" + "@babel/plugin-transform-class-static-block" "^7.24.7" + "@babel/plugin-transform-classes" "^7.24.8" + "@babel/plugin-transform-computed-properties" "^7.24.7" + "@babel/plugin-transform-destructuring" "^7.24.8" + "@babel/plugin-transform-dotall-regex" "^7.24.7" + "@babel/plugin-transform-duplicate-keys" "^7.24.7" + "@babel/plugin-transform-dynamic-import" "^7.24.7" + "@babel/plugin-transform-exponentiation-operator" "^7.24.7" + "@babel/plugin-transform-export-namespace-from" "^7.24.7" + "@babel/plugin-transform-for-of" "^7.24.7" + "@babel/plugin-transform-function-name" "^7.24.7" + "@babel/plugin-transform-json-strings" "^7.24.7" + "@babel/plugin-transform-literals" "^7.24.7" + "@babel/plugin-transform-logical-assignment-operators" "^7.24.7" + "@babel/plugin-transform-member-expression-literals" "^7.24.7" + "@babel/plugin-transform-modules-amd" "^7.24.7" + "@babel/plugin-transform-modules-commonjs" "^7.24.8" + "@babel/plugin-transform-modules-systemjs" "^7.24.7" + "@babel/plugin-transform-modules-umd" "^7.24.7" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.24.7" + "@babel/plugin-transform-new-target" "^7.24.7" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.24.7" + "@babel/plugin-transform-numeric-separator" "^7.24.7" + "@babel/plugin-transform-object-rest-spread" "^7.24.7" + "@babel/plugin-transform-object-super" "^7.24.7" + "@babel/plugin-transform-optional-catch-binding" "^7.24.7" + "@babel/plugin-transform-optional-chaining" "^7.24.8" + "@babel/plugin-transform-parameters" "^7.24.7" + "@babel/plugin-transform-private-methods" "^7.24.7" + "@babel/plugin-transform-private-property-in-object" "^7.24.7" + "@babel/plugin-transform-property-literals" "^7.24.7" + "@babel/plugin-transform-regenerator" "^7.24.7" + "@babel/plugin-transform-reserved-words" "^7.24.7" + "@babel/plugin-transform-shorthand-properties" "^7.24.7" + "@babel/plugin-transform-spread" "^7.24.7" + "@babel/plugin-transform-sticky-regex" "^7.24.7" + "@babel/plugin-transform-template-literals" "^7.24.7" + "@babel/plugin-transform-typeof-symbol" "^7.24.8" + "@babel/plugin-transform-unicode-escapes" "^7.24.7" + "@babel/plugin-transform-unicode-property-regex" "^7.24.7" + "@babel/plugin-transform-unicode-regex" "^7.24.7" + "@babel/plugin-transform-unicode-sets-regex" "^7.24.7" + "@babel/preset-modules" "0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2 "^0.4.10" + babel-plugin-polyfill-corejs3 "^0.10.4" + babel-plugin-polyfill-regenerator "^0.6.1" + core-js-compat "^3.37.1" + semver "^6.3.1" + +"@babel/preset-modules@0.1.6-no-external-plugins": + version "0.1.6-no-external-plugins" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" + integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/regjsgen@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" + integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== + +"@babel/runtime@^7.8.4": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.8.tgz#5d958c3827b13cc6d05e038c07fb2e5e3420d82e" + integrity sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/template@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.7.tgz#02efcee317d0609d2c07117cb70ef8fb17ab7315" + integrity sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/parser" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/traverse@^7.24.7", "@babel/traverse@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.8.tgz#6c14ed5232b7549df3371d820fbd9abfcd7dfab7" + integrity sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.24.8" + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-function-name" "^7.24.7" + "@babel/helper-hoist-variables" "^7.24.7" + "@babel/helper-split-export-declaration" "^7.24.7" + "@babel/parser" "^7.24.8" + "@babel/types" "^7.24.8" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.24.7", "@babel/types@^7.24.8", "@babel/types@^7.24.9", "@babel/types@^7.4.4": + version "7.24.9" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.9.tgz#228ce953d7b0d16646e755acf204f4cf3d08cc73" + integrity sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ== + dependencies: + "@babel/helper-string-parser" "^7.24.8" + "@babel/helper-validator-identifier" "^7.24.7" + to-fast-properties "^2.0.0" + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@types/eslint-scope@^3.7.3": + version "3.7.7" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" + integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "9.6.0" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.0.tgz#51d4fe4d0316da9e9f2c80884f2c20ed5fb022ff" + integrity sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + +"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/node@*": + version "20.14.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.12.tgz#129d7c3a822cb49fc7ff661235f19cfefd422b49" + integrity sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ== + dependencies: + undici-types "~5.26.4" + +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== + +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== + +"@webassemblyjs/helper-buffer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== + +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== + +"@webassemblyjs/helper-wasm-section@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" + +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" + +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.2.0.tgz#7b20ce1c12533912c3b217ea68262365fa29a6f5" + integrity sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg== + +"@webpack-cli/info@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.5.0.tgz#6c78c13c5874852d6e2dd17f08a41f3fe4c261b1" + integrity sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ== + dependencies: + envinfo "^7.7.3" + +"@webpack-cli/serve@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.7.0.tgz#e1993689ac42d2b16e9194376cfb6753f6254db1" + integrity sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== + +acorn@^8.7.1, acorn@^8.8.2: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@^6.12.4, ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +babel-loader@^8.2: + version "8.3.0" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.3.0.tgz#124936e841ba4fe8176786d6ff28add1f134d6a8" + integrity sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q== + dependencies: + find-cache-dir "^3.3.1" + loader-utils "^2.0.0" + make-dir "^3.1.0" + schema-utils "^2.6.5" + +babel-plugin-polyfill-corejs2@^0.4.10: + version "0.4.11" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz#30320dfe3ffe1a336c15afdcdafd6fd615b25e33" + integrity sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q== + dependencies: + "@babel/compat-data" "^7.22.6" + "@babel/helper-define-polyfill-provider" "^0.6.2" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.10.4: + version "0.10.4" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz#789ac82405ad664c20476d0233b485281deb9c77" + integrity sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.1" + core-js-compat "^3.36.1" + +babel-plugin-polyfill-regenerator@^0.6.1: + version "0.6.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz#addc47e240edd1da1058ebda03021f382bba785e" + integrity sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.2" + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.21.10, browserslist@^4.23.0, browserslist@^4.23.1: + version "4.23.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.2.tgz#244fe803641f1c19c28c48c4b6ec9736eb3d32ed" + integrity sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA== + dependencies: + caniuse-lite "^1.0.30001640" + electron-to-chromium "^1.4.820" + node-releases "^2.0.14" + update-browserslist-db "^1.1.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +caniuse-lite@^1.0.30001640: + version "1.0.30001643" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz#9c004caef315de9452ab970c3da71085f8241dbd" + integrity sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg== + +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +"chokidar@>=3.0.0 <4.0.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" + integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +colorette@^2.0.14: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +core-js-compat@^3.36.1, core-js-compat@^3.37.1: + version "3.37.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.37.1.tgz#c844310c7852f4bdf49b8d339730b97e17ff09ee" + integrity sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg== + dependencies: + browserslist "^4.23.0" + +core-js@^3.25.0: + version "3.37.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.37.1.tgz#d21751ddb756518ac5a00e4d66499df981a62db9" + integrity sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw== + +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + +cross-spawn@^7.0.1, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + +electron-to-chromium@^1.4.820: + version "1.5.1" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.1.tgz#24640bd4dcfaccb6d82bb4c3f4c7311503241581" + integrity sha512-FKbOCOQ5QRB3VlIbl1LZQefWIYwszlBloaXcY2rbfpu9ioJnNh3TK03YtIDKDo3WKBi8u+YV4+Fn2CkEozgf4w== + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +enhanced-resolve@^5.17.0: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +envinfo@^7.7.3: + version "7.13.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.13.0.tgz#81fbb81e5da35d74e814941aeab7c325a606fb31" + integrity sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q== + +es-module-lexer@^1.2.1: + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== + +escalade@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +file-loader@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" + integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-cache-dir@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" + integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +immutable@^4.0.0: + version "4.3.7" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381" + integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw== + +import-local@^3.0.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +interpret@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" + integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.13.0: + version "2.15.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea" + integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA== + dependencies: + hasown "^2.0.2" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json5@^2.1.2, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +klona@^2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" + integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +loader-utils@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" + integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +make-dir@^3.0.2, make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.27: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-releases@^2.0.14: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^1.0.0, picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkg-dir@^4.1.0, pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.7.0: + version "0.7.1" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686" + integrity sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg== + dependencies: + resolve "^1.9.0" + +regenerate-unicode-properties@^10.1.0: + version "10.1.1" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480" + integrity sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +regenerator-transform@^0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4" + integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== + dependencies: + "@babel/runtime" "^7.8.4" + +regexpu-core@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" + integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== + dependencies: + "@babel/regjsgen" "^0.8.0" + regenerate "^1.4.2" + regenerate-unicode-properties "^10.1.0" + regjsparser "^0.9.1" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + +regjsparser@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" + integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== + dependencies: + jsesc "~0.5.0" + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.14.2, resolve@^1.9.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +sass-loader@^12.1.0: + version "12.6.0" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-12.6.0.tgz#5148362c8e2cdd4b950f3c63ac5d16dbfed37bcb" + integrity sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA== + dependencies: + klona "^2.0.4" + neo-async "^2.6.2" + +sass@^1.42.1: + version "1.77.8" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.8.tgz#9f18b449ea401759ef7ec1752a16373e296b52bd" + integrity sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + +schema-utils@^2.6.5: + version "2.7.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" + integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== + dependencies: + "@types/json-schema" "^7.0.5" + ajv "^6.12.4" + ajv-keywords "^3.5.2" + +schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +semver@^6.0.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +serialize-javascript@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +"source-map-js@>=0.6.2 <2.0.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.3.10: + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.20" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.26.0" + +terser@^5.26.0: + version "5.31.3" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.3.tgz#b24b7beb46062f4653f049eea4f0cd165d0f0c38" + integrity sha512-pAfYn3NIZLyZpa83ZKigvj6Rn9c/vd5KfYGX7cN1mnzqgDcxWvrU5ZtAfIKhEXz9nRecw4z3LXkjaq96/qZqAA== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" + integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + +update-browserslist-db@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== + dependencies: + escalade "^3.1.2" + picocolors "^1.0.1" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +watchpack@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.1.tgz#29308f2cac150fa8e4c92f90e0ec954a9fed7fff" + integrity sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +webpack-cli@^4.10: + version "4.10.0" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.10.0.tgz#37c1d69c8d85214c5a65e589378f53aec64dab31" + integrity sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^1.2.0" + "@webpack-cli/info" "^1.5.0" + "@webpack-cli/serve" "^1.7.0" + colorette "^2.0.14" + commander "^7.0.0" + cross-spawn "^7.0.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^2.2.0" + rechoir "^0.7.0" + webpack-merge "^5.7.3" + +webpack-merge@^5.7.3: + version "5.10.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.0" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@^5.76: + version "5.93.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.93.0.tgz#2e89ec7035579bdfba9760d26c63ac5c3462a5e5" + integrity sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" + acorn "^8.7.1" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.17.0" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.11" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.2.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" + webpack-sources "^3.2.3" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== diff --git a/modules/ppcp-compat/resources/js/tracking-compat.js b/modules/ppcp-compat/resources/js/tracking-compat.js index 589e747b5..3849249ba 100644 --- a/modules/ppcp-compat/resources/js/tracking-compat.js +++ b/modules/ppcp-compat/resources/js/tracking-compat.js @@ -15,6 +15,8 @@ document.addEventListener( 'DOMContentLoaded', () => { ); const wcShipmentTaxBuyLabelButtonSelector = '.components-modal__screen-overlay .label-purchase-modal__sidebar .purchase-section button.components-button'; + const dhlGenerateLabelButton = + document.getElementById( 'dhl-label-button' ); const toggleLoaderVisibility = function () { const loader = document.querySelector( '.ppcp-tracking-loader' ); @@ -44,6 +46,20 @@ document.addEventListener( 'DOMContentLoaded', () => { } }; + const waitForButtonRemoval = function ( button ) { + if ( document.body.contains( button ) ) { + setTimeout( () => waitForButtonRemoval( button ), 100 ); + } else { + jQuery( orderTrackingContainerSelector ).load( + loadLocation, + '', + function () { + toggleLoaderVisibility(); + } + ); + } + }; + if ( gzdSyncEnabled && typeof gzdSaveButton !== 'undefined' && @@ -66,10 +82,30 @@ document.addEventListener( 'DOMContentLoaded', () => { } ); } + if ( + typeof dhlGenerateLabelButton !== 'undefined' && + dhlGenerateLabelButton != null + ) { + dhlGenerateLabelButton.addEventListener( 'click', function ( event ) { + toggleLoaderVisibility(); + waitForButtonRemoval( dhlGenerateLabelButton ); + } ); + } + + jQuery( document ).on( + 'mouseover mouseout', + '#dhl_delete_label', + function ( event ) { + jQuery( '#ppcp-shipment-status' ) + .val( 'CANCELLED' ) + .trigger( 'change' ); + document.querySelector( '.update_shipment' ).click(); + } + ); + if ( wcShippingTaxSyncEnabled && - typeof wcShippingTaxSyncEnabled !== 'undefined' && - wcShippingTaxSyncEnabled != null + typeof wcShippingTaxSyncEnabled !== 'undefined' ) { document.addEventListener( 'click', function ( event ) { const wcShipmentTaxBuyLabelButton = event.target.closest( diff --git a/modules/ppcp-compat/services.php b/modules/ppcp-compat/services.php index d27091162..b6abcea56 100644 --- a/modules/ppcp-compat/services.php +++ b/modules/ppcp-compat/services.php @@ -77,6 +77,9 @@ return array( 'compat.ywot.is_supported_plugin_version_active' => function (): bool { return function_exists( 'yith_ywot_init' ); }, + 'compat.dhl.is_supported_plugin_version_active' => function (): bool { + return function_exists( 'PR_DHL' ); + }, 'compat.shipstation.is_supported_plugin_version_active' => function (): bool { return function_exists( 'woocommerce_shipstation_init' ); }, @@ -86,6 +89,9 @@ return array( 'compat.nyp.is_supported_plugin_version_active' => function (): bool { return function_exists( 'wc_nyp_init' ); }, + 'compat.wc_bookings.is_supported_plugin_version_active' => function (): bool { + return class_exists( 'WC_Bookings' ); + }, 'compat.module.url' => static function ( ContainerInterface $container ): string { /** diff --git a/modules/ppcp-compat/src/CompatModule.php b/modules/ppcp-compat/src/CompatModule.php index 1749dcc0d..5c53aa7dc 100644 --- a/modules/ppcp-compat/src/CompatModule.php +++ b/modules/ppcp-compat/src/CompatModule.php @@ -9,6 +9,11 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Compat; +use Exception; +use Psr\Log\LoggerInterface; +use WC_Cart; +use WC_Order; +use WC_Order_Item_Product; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface; @@ -61,6 +66,13 @@ class CompatModule implements ModuleInterface { if ( $is_nyp_active ) { $this->initialize_nyp_compat_layer(); } + + $logger = $c->get( 'woocommerce.logger.woocommerce' ); + + $is_wc_bookings_active = $c->get( 'compat.wc_bookings.is_supported_plugin_version_active' ); + if ( $is_wc_bookings_active ) { + $this->initialize_wc_bookings_compat_layer( $logger ); + } } /** @@ -412,4 +424,62 @@ class CompatModule implements ModuleInterface { 2 ); } + + /** + * Sets up the compatibility layer for WooCommerce Bookings plugin. + * + * @param LoggerInterface $logger The logger. + * @return void + */ + protected function initialize_wc_bookings_compat_layer( LoggerInterface $logger ): void { + add_action( + 'woocommerce_paypal_payments_shipping_callback_woocommerce_order_created', + static function ( WC_Order $wc_order, WC_Cart $wc_cart ) use ( $logger ): void { + try { + $cart_contents = $wc_cart->get_cart(); + foreach ( $cart_contents as $cart_item ) { + if ( empty( $cart_item['booking'] ) ) { + continue; + } + + foreach ( $wc_order->get_items() as $wc_order_item ) { + if ( ! is_a( $wc_order_item, WC_Order_Item_Product::class ) ) { + continue; + } + + $product_id = $wc_order_item->get_variation_id() ?: $wc_order_item->get_product_id(); + $product = wc_get_product( $product_id ); + + if ( ! is_wc_booking_product( $product ) ) { + continue; + } + + $booking_data = array( + 'cost' => $cart_item['booking']['_cost'] ?? 0, + 'start_date' => $cart_item['booking']['_start_date'] ?? 0, + 'end_date' => $cart_item['booking']['_end_date'] ?? 0, + 'all_day' => $cart_item['booking']['_all_day'] ?? 0, + 'local_timezone' => $cart_item['booking']['_local_timezone'] ?? 0, + 'order_item_id' => $wc_order_item->get_id(), + ); + + if ( isset( $cart_item['booking']['_resource_id'] ) ) { + $booking_data['resource_id'] = $cart_item['booking']['_resource_id']; + } + + if ( isset( $cart_item['booking']['_persons'] ) ) { + $booking_data['persons'] = $cart_item['booking']['_persons']; + } + + create_wc_booking( $cart_item['product_id'], $booking_data, 'unpaid' ); + } + } + } catch ( Exception $exception ) { + $logger->warning( 'Failed to create booking for WooCommerce Bookings plugin: ' . $exception->getMessage() ); + } + }, + 10, + 2 + ); + } } diff --git a/modules/ppcp-googlepay/extensions.php b/modules/ppcp-googlepay/extensions.php index e0a01901f..df012fb8a 100644 --- a/modules/ppcp-googlepay/extensions.php +++ b/modules/ppcp-googlepay/extensions.php @@ -166,7 +166,7 @@ return array( 'classes' => array( 'ppcp-field-indent' ), 'class' => array(), 'input_class' => array( 'wc-enhanced-select' ), - 'default' => 'pay', + 'default' => 'plain', 'options' => PropertiesDictionary::button_types(), 'screens' => array( State::STATE_ONBOARDED ), 'gateway' => 'dcc', diff --git a/modules/ppcp-googlepay/resources/css/styles.scss b/modules/ppcp-googlepay/resources/css/styles.scss index 6cf2119a0..93cb05f47 100644 --- a/modules/ppcp-googlepay/resources/css/styles.scss +++ b/modules/ppcp-googlepay/resources/css/styles.scss @@ -1,3 +1,10 @@ +/* Front end display */ +.ppcp-button-apm .gpay-card-info-container-fill .gpay-card-info-container { + outline-offset: -1px; + border-radius: var(--apm-button-border-radius); +} + +/* Admin preview */ .ppcp-button-googlepay { min-height: 40px; diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index d626a172b..f6ceafc61 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -1,15 +1,120 @@ -import { setVisible } from '../../../ppcp-button/resources/js/modules/Helper/Hiding'; -import { setEnabled } from '../../../ppcp-button/resources/js/modules/Helper/ButtonDisabler'; +import { + combineStyles, + combineWrapperIds, +} from '../../../ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers'; +import PaymentButton from '../../../ppcp-button/resources/js/modules/Renderer/PaymentButton'; import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder'; import UpdatePaymentData from './Helper/UpdatePaymentData'; -import { apmButtonsInit } from '../../../ppcp-button/resources/js/modules/Helper/ApmButtons'; import TransactionInfo from './Helper/TransactionInfo'; +import { PaymentMethods } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState'; -class GooglepayButton { +/** + * Plugin-specific styling. + * + * Note that most properties of this object do not apply to the Google Pay button. + * + * @typedef {Object} PPCPStyle + * @property {string} shape - Outline shape. + * @property {?number} height - Button height in pixel. + */ + +/** + * Style options that are defined by the Google Pay SDK and are required to render the button. + * + * @typedef {Object} GooglePayStyle + * @property {string} type - Defines the button label. + * @property {string} color - Button color + * @property {string} language - The locale; an empty string will apply the user-agent's language. + */ + +/** + * Google Pay JS SDK + * + * @see https://developers.google.com/pay/api/web/reference/request-objects + * @typedef {Object} GooglePaySDK + * @property {typeof PaymentsClient} PaymentsClient - Main API client for payment actions. + */ + +/** + * The Payments Client class, generated by the Google Pay SDK. + * + * @see https://developers.google.com/pay/api/web/reference/client + * @typedef {Object} PaymentsClient + * @property {Function} createButton - The convenience method is used to generate a Google Pay payment button styled with the latest Google Pay branding for insertion into a webpage. + * @property {Function} isReadyToPay - Use the isReadyToPay(isReadyToPayRequest) method to determine a user's ability to return a form of payment from the Google Pay API. + * @property {Function} loadPaymentData - This method presents a Google Pay payment sheet that allows selection of a payment method and optionally configured parameters + * @property {Function} onPaymentAuthorized - This method is called when a payment is authorized in the payment sheet. + * @property {Function} onPaymentDataChanged - This method handles payment data changes in the payment sheet such as shipping address and shipping options. + */ + +/** + * This object describes the transaction details. + * + * @see https://developers.google.com/pay/api/web/reference/request-objects#TransactionInfo + * @typedef {Object} TransactionInfo + * @property {string} currencyCode - Required. The ISO 4217 alphabetic currency code. + * @property {string} countryCode - Optional. required for EEA countries, + * @property {string} transactionId - Optional. A unique ID that identifies a facilitation attempt. Highly encouraged for troubleshooting. + * @property {string} totalPriceStatus - Required. [ESTIMATED|FINAL] The status of the total price used: + * @property {string} totalPrice - Required. Total monetary value of the transaction with an optional decimal precision of two decimal places. + * @property {Array} displayItems - Optional. A list of cart items shown in the payment sheet (e.g. subtotals, sales taxes, shipping charges, discounts etc.). + * @property {string} totalPriceLabel - Optional. Custom label for the total price within the display items. + * @property {string} checkoutOption - Optional. Affects the submit button text displayed in the Google Pay payment sheet. + */ + +class GooglepayButton extends PaymentButton { /** - * @type {TransactionInfo} + * @inheritDoc */ - transactionInfo; + static methodId = PaymentMethods.GOOGLEPAY; + + /** + * @inheritDoc + */ + static cssClass = 'google-pay'; + + /** + * Client reference, provided by the Google Pay JS SDK. + */ + #paymentsClient = null; + + /** + * Details about the processed transaction. + * + * @type {?TransactionInfo} + */ + #transactionInfo = null; + + googlePayConfig = null; + + /** + * @inheritDoc + */ + static getWrappers( buttonConfig, ppcpConfig ) { + return combineWrapperIds( + buttonConfig?.button?.wrapper || '', + buttonConfig?.button?.mini_cart_wrapper || '', + ppcpConfig?.button?.wrapper || '', + 'ppc-button-googlepay-container', + 'ppc-button-ppcp-googlepay' + ); + } + + /** + * @inheritDoc + */ + static getStyles( buttonConfig, ppcpConfig ) { + const styles = combineStyles( + ppcpConfig?.button || {}, + buttonConfig?.button || {} + ); + + if ( 'buy' === styles.MiniCart.type ) { + styles.MiniCart.type = 'pay'; + } + + return styles; + } constructor( context, @@ -18,274 +123,257 @@ class GooglepayButton { ppcpConfig, contextHandler ) { - apmButtonsInit( ppcpConfig ); + // Disable debug output in the browser console: + // buttonConfig.is_debug = false; - this.isInitialized = false; + super( + context, + externalHandler, + buttonConfig, + ppcpConfig, + contextHandler + ); - this.context = context; - this.externalHandler = externalHandler; - this.buttonConfig = buttonConfig; - this.ppcpConfig = ppcpConfig; - this.contextHandler = contextHandler; + this.init = this.init.bind( this ); + this.onPaymentAuthorized = this.onPaymentAuthorized.bind( this ); + this.onPaymentDataChanged = this.onPaymentDataChanged.bind( this ); + this.onButtonClick = this.onButtonClick.bind( this ); - this.paymentsClient = null; + this.log( 'Create instance' ); + } - this.log = function () { - if ( this.buttonConfig.is_debug ) { - //console.log('[GooglePayButton]', ...arguments); + /** + * @inheritDoc + */ + get requiresShipping() { + return super.requiresShipping && this.buttonConfig.shipping?.enabled; + } + + /** + * The Google Pay API. + * + * @return {?GooglePaySDK} API for the Google Pay JS SDK, or null when SDK is not ready yet. + */ + get googlePayApi() { + return window.google?.payments?.api; + } + + /** + * The Google Pay PaymentsClient instance created by this button. + * @see https://developers.google.com/pay/api/web/reference/client + * + * @return {?PaymentsClient} The SDK object, or null when SDK is not ready yet. + */ + get paymentsClient() { + return this.#paymentsClient; + } + + /** + * Details about the processed transaction. + * + * This object defines the price that is charged, and text that is displayed inside the + * payment sheet. + * + * @return {?TransactionInfo} The TransactionInfo object. + */ + get transactionInfo() { + return this.#transactionInfo; + } + + /** + * Assign the new transaction details to the payment button. + * + * @param {TransactionInfo} newTransactionInfo - Transaction details. + */ + set transactionInfo( newTransactionInfo ) { + this.#transactionInfo = newTransactionInfo; + + this.refresh(); + } + + /** + * @inheritDoc + */ + validateConfiguration( silent = false ) { + const validEnvs = [ 'PRODUCTION', 'TEST' ]; + + const isInvalid = ( ...args ) => { + if ( ! silent ) { + this.error( ...args ); } + return false; }; - } - init( config, transactionInfo ) { - if ( this.isInitialized ) { - return; - } - this.isInitialized = true; - - if ( ! this.validateConfig() ) { - return; + if ( ! validEnvs.includes( this.buttonConfig.environment ) ) { + return isInvalid( + 'Invalid environment:', + this.buttonConfig.environment + ); } - if ( ! this.contextHandler.validateContext() ) { - return; + // Preview buttons only need a valid environment. + if ( this.isPreview ) { + return true; } - this.googlePayConfig = config; - this.transactionInfo = transactionInfo; - this.allowedPaymentMethods = config.allowedPaymentMethods; - this.baseCardPaymentMethod = this.allowedPaymentMethods[ 0 ]; - - this.initClient(); - this.initEventHandlers(); - - this.paymentsClient - .isReadyToPay( - this.buildReadyToPayRequest( - this.allowedPaymentMethods, - config - ) - ) - .then( ( response ) => { - if ( response.result ) { - if ( - ( this.context === 'checkout' || - this.context === 'pay-now' ) && - this.buttonConfig.is_wc_gateway_enabled === '1' - ) { - const wrapper = document.getElementById( - 'ppc-button-ppcp-googlepay' - ); - - if ( wrapper ) { - const { ppcpStyle, buttonStyle } = - this.contextConfig(); - - wrapper.classList.add( - `ppcp-button-${ ppcpStyle.shape }`, - 'ppcp-button-apm', - 'ppcp-button-googlepay' - ); - - if ( ppcpStyle.height ) { - wrapper.style.height = `${ ppcpStyle.height }px`; - } - - this.addButtonCheckout( - this.baseCardPaymentMethod, - wrapper, - buttonStyle - ); - - return; - } - } - - this.addButton( this.baseCardPaymentMethod ); - } - } ) - .catch( function ( err ) { - console.error( err ); - } ); - } - - reinit() { if ( ! this.googlePayConfig ) { - return; + return isInvalid( + 'No API configuration - missing configure() call?' + ); } - this.isInitialized = false; - this.init( this.googlePayConfig, this.transactionInfo ); - } - - validateConfig() { - if ( - [ 'PRODUCTION', 'TEST' ].indexOf( - this.buttonConfig.environment - ) === -1 - ) { - console.error( - '[GooglePayButton] Invalid environment.', - this.buttonConfig.environment + if ( ! this.transactionInfo ) { + return isInvalid( + 'No transactionInfo - missing configure() call?' ); - return false; } - if ( ! this.contextHandler ) { - console.error( - '[GooglePayButton] Invalid context handler.', - this.contextHandler - ); - return false; + if ( ! typeof this.contextHandler?.validateContext() ) { + return isInvalid( 'Invalid context handler.', this.contextHandler ); } return true; } /** - * Returns configurations relative to this button context. + * Configures the button instance. Must be called before the initial `init()`. + * + * @param {Object} apiConfig - API configuration. + * @param {Object} transactionInfo - Transaction details; required before "init" call. */ - contextConfig() { - const config = { - wrapper: this.buttonConfig.button.wrapper, - ppcpStyle: this.ppcpConfig.button.style, - buttonStyle: this.buttonConfig.button.style, - ppcpButtonWrapper: this.ppcpConfig.button.wrapper, - }; + configure( apiConfig, transactionInfo ) { + this.googlePayConfig = apiConfig; + this.#transactionInfo = transactionInfo; - if ( this.context === 'mini-cart' ) { - config.wrapper = this.buttonConfig.button.mini_cart_wrapper; - config.ppcpStyle = this.ppcpConfig.button.mini_cart_style; - config.buttonStyle = this.buttonConfig.button.mini_cart_style; - config.ppcpButtonWrapper = this.ppcpConfig.button.mini_cart_wrapper; - - // Handle incompatible types. - if ( config.buttonStyle.type === 'buy' ) { - config.buttonStyle.type = 'pay'; - } - } - - if ( - [ 'cart-block', 'checkout-block' ].indexOf( this.context ) !== -1 - ) { - config.ppcpButtonWrapper = - '#express-payment-method-ppcp-gateway-paypal'; - } - - return config; + this.allowedPaymentMethods = this.googlePayConfig.allowedPaymentMethods; + this.baseCardPaymentMethod = this.allowedPaymentMethods[ 0 ]; } - initClient() { - const callbacks = { - onPaymentAuthorized: this.onPaymentAuthorized.bind( this ), - }; - - if ( - this.buttonConfig.shipping.enabled && - this.contextHandler.shippingAllowed() - ) { - callbacks.onPaymentDataChanged = - this.onPaymentDataChanged.bind( this ); + init() { + // Use `reinit()` to force a full refresh of an initialized button. + if ( this.isInitialized ) { + return; } - this.paymentsClient = new google.payments.api.PaymentsClient( { + // Stop, if configuration is invalid. + if ( ! this.validateConfiguration() ) { + return; + } + + super.init(); + this.#paymentsClient = this.createPaymentsClient(); + + if ( ! this.isPresent ) { + this.log( 'Payment wrapper not found', this.wrapperId ); + return; + } + + if ( ! this.paymentsClient ) { + this.log( 'Could not initialize the payments client' ); + return; + } + + this.paymentsClient + .isReadyToPay( + this.buildReadyToPayRequest( + this.allowedPaymentMethods, + this.googlePayConfig + ) + ) + .then( ( response ) => { + this.log( 'PaymentsClient.isReadyToPay response:', response ); + this.isEligible = !! response.result; + } ) + .catch( ( err ) => { + this.error( err ); + this.isEligible = false; + } ); + } + + reinit() { + // Missing (invalid) configuration indicates, that the first `init()` call did not happen yet. + if ( ! this.validateConfiguration( true ) ) { + return; + } + + super.reinit(); + + this.init(); + } + + /** + * Provides an object with relevant paymentDataCallbacks for the current button instance. + * + * @return {Object} An object containing callbacks for the current scope & configuration. + */ + preparePaymentDataCallbacks() { + const callbacks = {}; + + // We do not attach any callbacks to preview buttons. + if ( this.isPreview ) { + return callbacks; + } + + callbacks.onPaymentAuthorized = this.onPaymentAuthorized; + + if ( this.requiresShipping ) { + callbacks.onPaymentDataChanged = this.onPaymentDataChanged; + } + + return callbacks; + } + + createPaymentsClient() { + if ( ! this.googlePayApi ) { + return null; + } + + const callbacks = this.preparePaymentDataCallbacks(); + + /** + * Consider providing merchant info here: + * + * @see https://developers.google.com/pay/api/web/reference/request-objects#PaymentOptions + */ + return new this.googlePayApi.PaymentsClient( { environment: this.buttonConfig.environment, - // add merchant info maybe paymentDataCallbacks: callbacks, } ); } - initEventHandlers() { - const { wrapper, ppcpButtonWrapper } = this.contextConfig(); - - if ( wrapper === ppcpButtonWrapper ) { - throw new Error( - `[GooglePayButton] "wrapper" and "ppcpButtonWrapper" values must differ to avoid infinite loop. Current value: "${ wrapper }"` - ); - } - - const syncButtonVisibility = () => { - const $ppcpButtonWrapper = jQuery( ppcpButtonWrapper ); - setVisible( wrapper, $ppcpButtonWrapper.is( ':visible' ) ); - setEnabled( - wrapper, - ! $ppcpButtonWrapper.hasClass( 'ppcp-disabled' ) - ); - }; - - jQuery( document ).on( - 'ppcp-shown ppcp-hidden ppcp-enabled ppcp-disabled', - ( ev, data ) => { - if ( jQuery( data.selector ).is( ppcpButtonWrapper ) ) { - syncButtonVisibility(); - } - } - ); - - syncButtonVisibility(); - } - buildReadyToPayRequest( allowedPaymentMethods, baseRequest ) { + this.log( 'Ready To Pay request', baseRequest, allowedPaymentMethods ); + return Object.assign( {}, baseRequest, { allowedPaymentMethods, } ); } /** - * Add a Google Pay purchase button - * @param baseCardPaymentMethod + * Creates the payment button and calls `this.insertButton()` to make the button visible in the + * correct wrapper. */ - addButton( baseCardPaymentMethod ) { - this.log( 'addButton', this.context ); + addButton() { + if ( ! this.paymentsClient ) { + return; + } - const { wrapper, ppcpStyle, buttonStyle } = this.contextConfig(); + const baseCardPaymentMethod = this.baseCardPaymentMethod; + const { color, type, language } = this.style; - this.waitForWrapper( wrapper, () => { - jQuery( wrapper ).addClass( 'ppcp-button-' + ppcpStyle.shape ); - - if ( ppcpStyle.height ) { - jQuery( wrapper ).css( 'height', `${ ppcpStyle.height }px` ); - } - - const button = this.paymentsClient.createButton( { - onClick: this.onButtonClick.bind( this ), - allowedPaymentMethods: [ baseCardPaymentMethod ], - buttonColor: buttonStyle.color || 'black', - buttonType: buttonStyle.type || 'pay', - buttonLocale: buttonStyle.language || 'en', - buttonSizeMode: 'fill', - } ); - - jQuery( wrapper ).append( button ); - } ); - } - - addButtonCheckout( baseCardPaymentMethod, wrapper, buttonStyle ) { + /** + * @see https://developers.google.com/pay/api/web/reference/client#createButton + */ const button = this.paymentsClient.createButton( { - onClick: this.onButtonClick.bind( this ), + onClick: this.onButtonClick, allowedPaymentMethods: [ baseCardPaymentMethod ], - buttonColor: buttonStyle.color || 'black', - buttonType: buttonStyle.type || 'pay', - buttonLocale: buttonStyle.language || 'en', + buttonColor: color || 'black', + buttonType: type || 'pay', + buttonLocale: language || 'en', buttonSizeMode: 'fill', } ); - wrapper.appendChild( button ); - } - - waitForWrapper( selector, callback, delay = 100, timeout = 2000 ) { - const startTime = Date.now(); - const interval = setInterval( () => { - const el = document.querySelector( selector ); - const timeElapsed = Date.now() - startTime; - - if ( el ) { - clearInterval( interval ); - callback( el ); - } else if ( timeElapsed > timeout ) { - clearInterval( interval ); - } - }, delay ); + this.insertButton( button ); } //------------------------ @@ -296,35 +384,49 @@ class GooglepayButton { * Show Google Pay payment sheet when Google Pay payment button is clicked */ onButtonClick() { - this.log( 'onButtonClick', this.context ); + this.log( 'onButtonClick' ); const initiatePaymentRequest = () => { window.ppcpFundingSource = 'googlepay'; - const paymentDataRequest = this.paymentDataRequest(); - this.log( 'onButtonClick: paymentDataRequest', paymentDataRequest, this.context ); - this.paymentsClient.loadPaymentData( paymentDataRequest ); }; - if ( 'function' === typeof this.contextHandler.validateForm ) { - // During regular checkout, validate the checkout form before initiating the payment. - this.contextHandler - .validateForm() - .then( initiatePaymentRequest, () => { - console.error( - '[GooglePayButton] Form validation failed.' - ); + const validateForm = () => { + if ( 'function' !== typeof this.contextHandler.validateForm ) { + return Promise.resolve(); + } + + return this.contextHandler.validateForm().catch( ( error ) => { + this.error( 'Form validation failed:', error ); + throw error; + } ); + }; + + const getTransactionInfo = () => { + if ( 'function' !== typeof this.contextHandler.transactionInfo ) { + return Promise.resolve(); + } + + return this.contextHandler + .transactionInfo() + .then( ( transactionInfo ) => { + this.transactionInfo = transactionInfo; + } ) + .catch( ( error ) => { + this.error( 'Failed to get transaction info:', error ); + throw error; } ); - } else { - // This is the flow on product page, cart, and other non-checkout pages. - initiatePaymentRequest(); - } + }; + + validateForm() + .then( getTransactionInfo ) + .then( initiatePaymentRequest ); } paymentDataRequest() { @@ -340,10 +442,7 @@ class GooglepayButton { paymentDataRequest.transactionInfo = this.transactionInfo.finalObject; paymentDataRequest.merchantInfo = googlePayConfig.merchantInfo; - if ( - this.buttonConfig.shipping.enabled && - this.contextHandler.shippingAllowed() - ) { + if ( this.requiresShipping ) { paymentDataRequest.callbackIntents = [ 'SHIPPING_ADDRESS', 'SHIPPING_OPTION', @@ -372,8 +471,7 @@ class GooglepayButton { } onPaymentDataChanged( paymentData ) { - this.log( 'onPaymentDataChanged', this.context ); - this.log( 'paymentData', paymentData ); + this.log( 'onPaymentDataChanged', paymentData ); return new Promise( async ( resolve, reject ) => { try { @@ -437,7 +535,7 @@ class GooglepayButton { resolve( paymentDataRequestUpdate ); } catch ( error ) { - console.error( 'Error during onPaymentDataChanged:', error ); + this.error( 'Error during onPaymentDataChanged:', error ); reject( error ); } } ); @@ -508,18 +606,18 @@ class GooglepayButton { //------------------------ onPaymentAuthorized( paymentData ) { - this.log( 'onPaymentAuthorized', this.context ); + this.log( 'onPaymentAuthorized' ); return this.processPayment( paymentData ); } async processPayment( paymentData ) { - this.log( 'processPayment', this.context ); + this.log( 'processPayment' ); return new Promise( async ( resolve, reject ) => { try { const id = await this.contextHandler.createOrder(); - this.log( 'processPayment: createOrder', id, this.context ); + this.log( 'processPayment: createOrder', id ); const confirmOrderResponse = await widgetBuilder.paypal .Googlepay() @@ -530,8 +628,7 @@ class GooglepayButton { this.log( 'processPayment: confirmOrder', - confirmOrderResponse, - this.context + confirmOrderResponse ); /** Capture the Order on the Server */ @@ -601,7 +698,7 @@ class GooglepayButton { }; } - this.log( 'processPaymentResponse', response, this.context ); + this.log( 'processPaymentResponse', response ); return response; } diff --git a/modules/ppcp-googlepay/resources/js/GooglepayManager.js b/modules/ppcp-googlepay/resources/js/GooglepayManager.js index e267f1b8a..aaf85a6b0 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayManager.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayManager.js @@ -20,7 +20,7 @@ class GooglepayManager { bootstrap.handler ); - const button = new GooglepayButton( + const button = GooglepayButton.createButton( bootstrap.context, bootstrap.handler, buttonConfig, @@ -30,13 +30,19 @@ class GooglepayManager { this.buttons.push( button ); + const initButton = () => { + button.configure( this.googlePayConfig, this.transactionInfo ); + button.init(); + }; + // Initialize button only if googlePayConfig and transactionInfo are already fetched. if ( this.googlePayConfig && this.transactionInfo ) { - button.init( this.googlePayConfig, this.transactionInfo ); + initButton(); } else { await this.init(); + if ( this.googlePayConfig && this.transactionInfo ) { - button.init( this.googlePayConfig, this.transactionInfo ); + initButton(); } } } ); @@ -53,8 +59,18 @@ class GooglepayManager { this.transactionInfo = await this.fetchTransactionInfo(); } - for ( const button of this.buttons ) { - button.init( this.googlePayConfig, this.transactionInfo ); + if ( ! this.googlePayConfig ) { + console.error( 'No GooglePayConfig received during init' ); + } else if ( ! this.transactionInfo ) { + console.error( 'No transactionInfo found during init' ); + } else { + for ( const button of this.buttons ) { + button.configure( + this.googlePayConfig, + this.transactionInfo + ); + button.init(); + } } } catch ( error ) { console.error( 'Error during initialization:', error ); diff --git a/modules/ppcp-googlepay/resources/js/GooglepayPreviewButton.js b/modules/ppcp-googlepay/resources/js/GooglepayPreviewButton.js new file mode 100644 index 000000000..6c3629a8a --- /dev/null +++ b/modules/ppcp-googlepay/resources/js/GooglepayPreviewButton.js @@ -0,0 +1,78 @@ +import PreviewButton from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButton'; +import ContextHandlerFactory from './Context/ContextHandlerFactory'; +import GooglepayButton from './GooglepayButton'; + +/** + * A single GooglePay preview button instance. + */ +export default class GooglePayPreviewButton extends PreviewButton { + /** + * Instance of the preview button. + * + * @type {?PaymentButton} + */ + #button = null; + + constructor( args ) { + super( args ); + + this.selector = `${ args.selector }GooglePay`; + this.defaultAttributes = { + button: { + style: { + type: 'pay', + color: 'black', + language: 'en', + }, + }, + }; + } + + createNewWrapper() { + const element = super.createNewWrapper(); + element.addClass( 'ppcp-button-googlepay' ); + + return element; + } + + createButton( buttonConfig ) { + const contextHandler = ContextHandlerFactory.create( + 'preview', + buttonConfig, + this.ppcpConfig, + null + ); + + if ( ! this.#button ) { + /* Intentionally using `new` keyword, instead of the `.createButton()` factory, + * as the factory is designed to only create a single button per context, while a single + * page can contain multiple instances of a preview button. + */ + this.#button = new GooglepayButton( + 'preview', + null, + buttonConfig, + this.ppcpConfig, + contextHandler + ); + } + + this.#button.configure( this.apiConfig, null ); + this.#button.applyButtonStyles( buttonConfig, this.ppcpConfig ); + this.#button.reinit(); + } + + /** + * Merge form details into the config object for preview. + * Mutates the previewConfig object; no return value. + * + * @param {Object} buttonConfig + * @param {Object} ppcpConfig + */ + dynamicPreviewConfig( buttonConfig, ppcpConfig ) { + // Merge the current form-values into the preview-button configuration. + if ( ppcpConfig.button && buttonConfig.button ) { + Object.assign( buttonConfig.button.style, ppcpConfig.button.style ); + } + } +} diff --git a/modules/ppcp-googlepay/resources/js/boot-admin.js b/modules/ppcp-googlepay/resources/js/boot-admin.js index b9f7b3c87..7b5342078 100644 --- a/modules/ppcp-googlepay/resources/js/boot-admin.js +++ b/modules/ppcp-googlepay/resources/js/boot-admin.js @@ -1,7 +1,5 @@ -import GooglepayButton from './GooglepayButton'; -import PreviewButton from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButton'; import PreviewButtonManager from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager'; -import ContextHandlerFactory from './Context/ContextHandlerFactory'; +import GooglePayPreviewButton from './GooglepayPreviewButton'; /** * Accessor that creates and returns a single PreviewButtonManager instance. @@ -33,7 +31,7 @@ class GooglePayPreviewButtonManager extends PreviewButtonManager { * method. * * @param {{}} payPal - The PayPal SDK object provided by WidgetBuilder. - * @return {Promise<{}>} + * @return {Promise<{}>} Promise that resolves when API configuration is available. */ async fetchConfig( payPal ) { const apiMethod = payPal?.Googlepay()?.config; @@ -59,7 +57,7 @@ class GooglePayPreviewButtonManager extends PreviewButtonManager { * This method is responsible for creating a new PreviewButton instance and returning it. * * @param {string} wrapperId - CSS ID of the wrapper element. - * @return {GooglePayPreviewButton} + * @return {GooglePayPreviewButton} The new preview button instance. */ createButtonInstance( wrapperId ) { return new GooglePayPreviewButton( { @@ -69,64 +67,5 @@ class GooglePayPreviewButtonManager extends PreviewButtonManager { } } -/** - * A single GooglePay preview button instance. - */ -class GooglePayPreviewButton extends PreviewButton { - constructor( args ) { - super( args ); - - this.selector = `${ args.selector }GooglePay`; - this.defaultAttributes = { - button: { - style: { - type: 'pay', - color: 'black', - language: 'en', - }, - }, - }; - } - - createNewWrapper() { - const element = super.createNewWrapper(); - element.addClass( 'ppcp-button-googlepay' ); - - return element; - } - - createButton( buttonConfig ) { - const contextHandler = ContextHandlerFactory.create( - 'preview', - buttonConfig, - this.ppcpConfig, - null - ); - - const button = new GooglepayButton( - 'preview', - null, - buttonConfig, - this.ppcpConfig, - contextHandler - ); - - button.init( this.apiConfig, null ); - } - - /** - * Merge form details into the config object for preview. - * Mutates the previewConfig object; no return value. - * @param buttonConfig - * @param ppcpConfig - */ - dynamicPreviewConfig( buttonConfig, ppcpConfig ) { - // Merge the current form-values into the preview-button configuration. - if ( ppcpConfig.button && buttonConfig.button ) { - Object.assign( buttonConfig.button.style, ppcpConfig.button.style ); - } - } -} - // Initialize the preview button manager. buttonManager(); diff --git a/modules/ppcp-googlepay/src/Assets/Button.php b/modules/ppcp-googlepay/src/Assets/Button.php index bd6faea79..575def21f 100644 --- a/modules/ppcp-googlepay/src/Assets/Button.php +++ b/modules/ppcp-googlepay/src/Assets/Button.php @@ -290,6 +290,7 @@ class Button implements ButtonInterface { $render_placeholder, function () { $this->googlepay_button(); + $this->hide_gateway_until_eligible(); }, 21 ); @@ -303,6 +304,7 @@ class Button implements ButtonInterface { $render_placeholder, function () { $this->googlepay_button(); + $this->hide_gateway_until_eligible(); }, 21 ); @@ -335,6 +337,23 @@ class Button implements ButtonInterface { + + get( 'compat.gzd.is_supported_plugin_version_active' ); $is_wc_shipment_active = $container->get( 'compat.wc_shipment_tracking.is_supported_plugin_version_active' ); $is_yith_ywot_active = $container->get( 'compat.ywot.is_supported_plugin_version_active' ); + $is_dhl_de_active = $container->get( 'compat.dhl.is_supported_plugin_version_active' ); $is_ship_station_active = $container->get( 'compat.shipstation.is_supported_plugin_version_active' ); $is_wc_shipping_tax_active = $container->get( 'compat.wc_shipping_tax.is_supported_plugin_version_active' ); @@ -135,6 +137,10 @@ return array( $integrations[] = new YithShipmentIntegration( $shipment_factory, $logger, $endpoint ); } + if ( $is_dhl_de_active ) { + $integrations[] = new DhlShipmentIntegration( $shipment_factory, $logger, $endpoint ); + } + if ( $is_ship_station_active ) { $integrations[] = new ShipStationIntegration( $shipment_factory, $logger, $endpoint ); } diff --git a/modules/ppcp-order-tracking/src/Integration/DhlShipmentIntegration.php b/modules/ppcp-order-tracking/src/Integration/DhlShipmentIntegration.php new file mode 100644 index 000000000..f51d880ef --- /dev/null +++ b/modules/ppcp-order-tracking/src/Integration/DhlShipmentIntegration.php @@ -0,0 +1,112 @@ +shipment_factory = $shipment_factory; + $this->logger = $logger; + $this->endpoint = $endpoint; + } + + /** + * {@inheritDoc} + */ + public function integrate(): void { + add_action( + 'pr_save_dhl_label_tracking', + function( int $order_id, array $tracking_details ) { + try { + $wc_order = wc_get_order( $order_id ); + if ( ! is_a( $wc_order, WC_Order::class ) ) { + return; + } + + $paypal_order = ppcp_get_paypal_order( $wc_order ); + $capture_id = $this->get_paypal_order_transaction_id( $paypal_order ); + $tracking_number = $tracking_details['tracking_number']; + $carrier = $tracking_details['carrier']; + + if ( ! $tracking_number || ! is_string( $tracking_number ) || ! $carrier || ! is_string( $carrier ) || ! $capture_id ) { + return; + } + + $ppcp_shipment = $this->shipment_factory->create_shipment( + $order_id, + $capture_id, + $tracking_number, + 'SHIPPED', + 'DE_DHL', + $carrier, + array() + ); + + $tracking_information = $this->endpoint->get_tracking_information( $order_id, $tracking_number ); + + $tracking_information + ? $this->endpoint->update_tracking_information( $ppcp_shipment, $order_id ) + : $this->endpoint->add_tracking_information( $ppcp_shipment, $order_id ); + + } catch ( Exception $exception ) { + return; + } + }, + 600, + 2 + ); + } +} diff --git a/modules/ppcp-save-payment-methods/resources/js/Configuration.js b/modules/ppcp-save-payment-methods/resources/js/Configuration.js new file mode 100644 index 000000000..a602d4d32 --- /dev/null +++ b/modules/ppcp-save-payment-methods/resources/js/Configuration.js @@ -0,0 +1,230 @@ +import { + getCurrentPaymentMethod, + PaymentMethods, +} from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState'; + +export function buttonConfiguration( ppcp_add_payment_method, errorHandler ) { + return { + createVaultSetupToken: async () => { + const response = await fetch( + ppcp_add_payment_method.ajax.create_setup_token.endpoint, + { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify( { + nonce: ppcp_add_payment_method.ajax.create_setup_token + .nonce, + } ), + } + ); + + const result = await response.json(); + if ( result.data.id ) { + return result.data.id; + } + + errorHandler.message( ppcp_add_payment_method.error_message ); + }, + onApprove: async ( { vaultSetupToken } ) => { + const response = await fetch( + ppcp_add_payment_method.ajax.create_payment_token.endpoint, + { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify( { + nonce: ppcp_add_payment_method.ajax.create_payment_token + .nonce, + vault_setup_token: vaultSetupToken, + } ), + } + ); + + const result = await response.json(); + if ( result.success === true ) { + window.location.href = + ppcp_add_payment_method.payment_methods_page; + return; + } + + errorHandler.message( ppcp_add_payment_method.error_message ); + }, + onError: ( error ) => { + console.error( error ); + errorHandler.message( ppcp_add_payment_method.error_message ); + }, + }; +} + +export function cardFieldsConfiguration( + ppcp_add_payment_method, + errorHandler +) { + return { + createVaultSetupToken: async () => { + const response = await fetch( + ppcp_add_payment_method.ajax.create_setup_token.endpoint, + { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify( { + nonce: ppcp_add_payment_method.ajax.create_setup_token + .nonce, + payment_method: PaymentMethods.CARDS, + verification_method: + ppcp_add_payment_method.verification_method, + } ), + } + ); + + const result = await response.json(); + if ( result.data.id ) { + return result.data.id; + } + + errorHandler.message( ppcp_add_payment_method.error_message ); + }, + onApprove: async ( { vaultSetupToken } ) => { + const isFreeTrialCart = + ppcp_add_payment_method?.is_free_trial_cart ?? false; + const response = await fetch( + ppcp_add_payment_method.ajax.create_payment_token.endpoint, + { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify( { + nonce: ppcp_add_payment_method.ajax.create_payment_token + .nonce, + vault_setup_token: vaultSetupToken, + payment_method: PaymentMethods.CARDS, + is_free_trial_cart: isFreeTrialCart, + } ), + } + ); + + const result = await response.json(); + if ( result.success === true ) { + const context = ppcp_add_payment_method?.context ?? ''; + if ( context === 'checkout' ) { + document.querySelector( '#place_order' ).click(); + return; + } + + if ( + ppcp_add_payment_method.is_subscription_change_payment_page + ) { + const subscriptionId = + ppcp_add_payment_method.subscription_id_to_change_payment; + if ( subscriptionId && result.data ) { + const req = await fetch( + ppcp_add_payment_method.ajax + .subscription_change_payment_method.endpoint, + { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify( { + nonce: ppcp_add_payment_method.ajax + .subscription_change_payment_method + .nonce, + subscription_id: subscriptionId, + payment_method: getCurrentPaymentMethod(), + wc_payment_token_id: result.data, + } ), + } + ); + + const res = await req.json(); + if ( res.success === true ) { + window.location.href = `${ ppcp_add_payment_method.view_subscriptions_page }/${ subscriptionId }`; + return; + } + } + + return; + } + + window.location.href = + ppcp_add_payment_method.payment_methods_page; + return; + } + + this.errorHandler.message( ppcp_add_payment_method.error_message ); + }, + onError: ( error ) => { + console.error( error ); + errorHandler.message( ppcp_add_payment_method.error_message ); + }, + }; +} + +export function addPaymentMethodConfiguration( ppcp_add_payment_method ) { + return { + createVaultSetupToken: async () => { + const response = await fetch( + ppcp_add_payment_method.ajax.create_setup_token.endpoint, + { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify( { + nonce: ppcp_add_payment_method.ajax.create_setup_token + .nonce, + payment_method: getCurrentPaymentMethod(), + } ), + } + ); + + const result = await response.json(); + if ( result.data.id ) { + return result.data.id; + } + + console.error( result ); + }, + onApprove: async ( { vaultSetupToken } ) => { + const response = await fetch( + ppcp_add_payment_method.ajax.create_payment_token_for_guest + .endpoint, + { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify( { + nonce: ppcp_add_payment_method.ajax + .create_payment_token_for_guest.nonce, + vault_setup_token: vaultSetupToken, + } ), + } + ); + + const result = await response.json(); + if ( result.success === true ) { + document.querySelector( '#place_order' ).click(); + return; + } + + console.error( result ); + }, + onError: ( error ) => { + console.error( error ); + }, + }; +} diff --git a/modules/ppcp-save-payment-methods/resources/js/add-payment-method.js b/modules/ppcp-save-payment-methods/resources/js/add-payment-method.js index 659d515f4..8e75e3ad7 100644 --- a/modules/ppcp-save-payment-methods/resources/js/add-payment-method.js +++ b/modules/ppcp-save-payment-methods/resources/js/add-payment-method.js @@ -4,303 +4,108 @@ import { PaymentMethods, } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState'; import { loadScript } from '@paypal/paypal-js'; +import ErrorHandler from '../../../ppcp-button/resources/js/modules/ErrorHandler'; +import { buttonConfiguration, cardFieldsConfiguration } from './Configuration'; +import { renderFields } from '../../../ppcp-card-fields/resources/js/Render'; import { setVisible, setVisibleByClass, } from '../../../ppcp-button/resources/js/modules/Helper/Hiding'; -import ErrorHandler from '../../../ppcp-button/resources/js/modules/ErrorHandler'; -import { cardFieldStyles } from '../../../ppcp-button/resources/js/modules/Helper/CardFieldsHelper'; -const errorHandler = new ErrorHandler( - ppcp_add_payment_method.labels.error.generic, - document.querySelector( '.woocommerce-notices-wrapper' ) -); - -const init = () => { - setVisibleByClass( - ORDER_BUTTON_SELECTOR, - getCurrentPaymentMethod() !== PaymentMethods.PAYPAL, - 'ppcp-hidden' - ); - setVisible( - `#ppc-button-${ PaymentMethods.PAYPAL }-save-payment-method`, - getCurrentPaymentMethod() === PaymentMethods.PAYPAL - ); -}; - -document.addEventListener( 'DOMContentLoaded', () => { - jQuery( document.body ).on( - 'click init_add_payment_method', - '.payment_methods input.input-radio', - function () { - init(); - } - ); - - if ( ppcp_add_payment_method.is_subscription_change_payment_page ) { - const saveToAccount = document.querySelector( - '#wc-ppcp-credit-card-gateway-new-payment-method' - ); - if ( saveToAccount ) { - saveToAccount.checked = true; - saveToAccount.disabled = true; - } - } - - setTimeout( () => { - loadScript( { - clientId: ppcp_add_payment_method.client_id, - merchantId: ppcp_add_payment_method.merchant_id, - dataUserIdToken: ppcp_add_payment_method.id_token, - components: 'buttons,card-fields', - } ).then( ( paypal ) => { - errorHandler.clear(); - - const paypalButtonContainer = document.querySelector( - `#ppc-button-${ PaymentMethods.PAYPAL }-save-payment-method` - ); - if ( paypalButtonContainer ) { - paypal - .Buttons( { - createVaultSetupToken: async () => { - const response = await fetch( - ppcp_add_payment_method.ajax.create_setup_token - .endpoint, - { - method: 'POST', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify( { - nonce: ppcp_add_payment_method.ajax - .create_setup_token.nonce, - } ), - } - ); - - const result = await response.json(); - if ( result.data.id ) { - return result.data.id; - } - - errorHandler.message( - ppcp_add_payment_method.error_message - ); - }, - onApprove: async ( { vaultSetupToken } ) => { - const response = await fetch( - ppcp_add_payment_method.ajax - .create_payment_token.endpoint, - { - method: 'POST', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify( { - nonce: ppcp_add_payment_method.ajax - .create_payment_token.nonce, - vault_setup_token: vaultSetupToken, - } ), - } - ); - - const result = await response.json(); - if ( result.success === true ) { - window.location.href = - ppcp_add_payment_method.payment_methods_page; - return; - } - - errorHandler.message( - ppcp_add_payment_method.error_message - ); - }, - onError: ( error ) => { - console.error( error ); - errorHandler.message( - ppcp_add_payment_method.error_message - ); - }, - } ) - .render( - `#ppc-button-${ PaymentMethods.PAYPAL }-save-payment-method` - ); +( function ( { ppcp_add_payment_method, jQuery } ) { + document.addEventListener( 'DOMContentLoaded', () => { + jQuery( document.body ).on( + 'click init_add_payment_method', + '.payment_methods input.input-radio', + function () { + setVisibleByClass( + ORDER_BUTTON_SELECTOR, + getCurrentPaymentMethod() !== PaymentMethods.PAYPAL, + 'ppcp-hidden' + ); + setVisible( + `#ppc-button-${ PaymentMethods.PAYPAL }-save-payment-method`, + getCurrentPaymentMethod() === PaymentMethods.PAYPAL + ); } + ); - const cardField = paypal.CardFields( { - createVaultSetupToken: async () => { - const response = await fetch( - ppcp_add_payment_method.ajax.create_setup_token - .endpoint, - { - method: 'POST', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify( { - nonce: ppcp_add_payment_method.ajax - .create_setup_token.nonce, - payment_method: PaymentMethods.CARDS, - verification_method: - ppcp_add_payment_method.verification_method, - } ), - } - ); + // TODO move to wc subscriptions module + if ( ppcp_add_payment_method.is_subscription_change_payment_page ) { + const saveToAccount = document.querySelector( + '#wc-ppcp-credit-card-gateway-new-payment-method' + ); + if ( saveToAccount ) { + saveToAccount.checked = true; + saveToAccount.disabled = true; + } + } - const result = await response.json(); - if ( result.data.id ) { - return result.data.id; - } + setTimeout( () => { + loadScript( { + clientId: ppcp_add_payment_method.client_id, + merchantId: ppcp_add_payment_method.merchant_id, + dataUserIdToken: ppcp_add_payment_method.id_token, + components: 'buttons,card-fields', + } ).then( ( paypal ) => { + const errorHandler = new ErrorHandler( + ppcp_add_payment_method.labels.error.generic, + document.querySelector( '.woocommerce-notices-wrapper' ) + ); + errorHandler.clear(); - errorHandler.message( - ppcp_add_payment_method.error_message - ); - }, - onApprove: async ( { vaultSetupToken } ) => { - const response = await fetch( - ppcp_add_payment_method.ajax.create_payment_token - .endpoint, - { - method: 'POST', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify( { - nonce: ppcp_add_payment_method.ajax - .create_payment_token.nonce, - vault_setup_token: vaultSetupToken, - payment_method: PaymentMethods.CARDS, - } ), - } - ); + const paypalButtonContainer = document.querySelector( + `#ppc-button-${ PaymentMethods.PAYPAL }-save-payment-method` + ); - const result = await response.json(); - if ( result.success === true ) { + if ( paypalButtonContainer ) { + paypal + .Buttons( + buttonConfiguration( + ppcp_add_payment_method, + errorHandler + ) + ) + .render( + `#ppc-button-${ PaymentMethods.PAYPAL }-save-payment-method` + ); + } + + const cardFields = paypal.CardFields( + cardFieldsConfiguration( + ppcp_add_payment_method, + errorHandler + ) + ); + + if ( cardFields.isEligible() ) { + renderFields( cardFields ); + } + + document + .querySelector( '#place_order' ) + ?.addEventListener( 'click', ( event ) => { + const cardPaymentToken = document.querySelector( + 'input[name="wc-ppcp-credit-card-gateway-payment-token"]:checked' + )?.value; if ( - ppcp_add_payment_method.is_subscription_change_payment_page + getCurrentPaymentMethod() !== + 'ppcp-credit-card-gateway' || + ( cardPaymentToken && cardPaymentToken !== 'new' ) ) { - const subscriptionId = - ppcp_add_payment_method.subscription_id_to_change_payment; - if ( subscriptionId && result.data ) { - const req = await fetch( - ppcp_add_payment_method.ajax - .subscription_change_payment_method - .endpoint, - { - method: 'POST', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify( { - nonce: ppcp_add_payment_method.ajax - .subscription_change_payment_method - .nonce, - subscription_id: subscriptionId, - payment_method: - getCurrentPaymentMethod(), - wc_payment_token_id: result.data, - } ), - } - ); - - const res = await req.json(); - if ( res.success === true ) { - window.location.href = `${ ppcp_add_payment_method.view_subscriptions_page }/${ subscriptionId }`; - return; - } - } - return; } - window.location.href = - ppcp_add_payment_method.payment_methods_page; - return; - } + event.preventDefault(); - errorHandler.message( - ppcp_add_payment_method.error_message - ); - }, - onError: ( error ) => { - console.error( error ); - errorHandler.message( - ppcp_add_payment_method.error_message - ); - }, - } ); - - if ( cardField.isEligible() ) { - const nameField = document.getElementById( - 'ppcp-credit-card-gateway-card-name' - ); - if ( nameField ) { - const styles = cardFieldStyles( nameField ); - cardField - .NameField( { style: { input: styles } } ) - .render( nameField.parentNode ); - nameField.hidden = true; - } - - const numberField = document.getElementById( - 'ppcp-credit-card-gateway-card-number' - ); - if ( numberField ) { - const styles = cardFieldStyles( numberField ); - cardField - .NumberField( { style: { input: styles } } ) - .render( numberField.parentNode ); - numberField.hidden = true; - } - - const expiryField = document.getElementById( - 'ppcp-credit-card-gateway-card-expiry' - ); - if ( expiryField ) { - const styles = cardFieldStyles( expiryField ); - cardField - .ExpiryField( { style: { input: styles } } ) - .render( expiryField.parentNode ); - expiryField.hidden = true; - } - - const cvvField = document.getElementById( - 'ppcp-credit-card-gateway-card-cvc' - ); - if ( cvvField ) { - const styles = cardFieldStyles( cvvField ); - cardField - .CVVField( { style: { input: styles } } ) - .render( cvvField.parentNode ); - cvvField.hidden = true; - } - } - - document - .querySelector( '#place_order' ) - ?.addEventListener( 'click', ( event ) => { - const cardPaymentToken = document.querySelector( - 'input[name="wc-ppcp-credit-card-gateway-payment-token"]:checked' - )?.value; - if ( - getCurrentPaymentMethod() !== - 'ppcp-credit-card-gateway' || - ( cardPaymentToken && cardPaymentToken !== 'new' ) - ) { - return; - } - - event.preventDefault(); - - cardField.submit().catch( ( error ) => { - console.error( error ); + cardFields.submit().catch( ( error ) => { + console.error( error ); + } ); } ); - } ); - } ); - }, 1000 ); + } ); + }, 1000 ); + } ); +} )( { + ppcp_add_payment_method: window.ppcp_add_payment_method, + jQuery: window.jQuery, } ); diff --git a/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php b/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php index 4fea1f188..ae3d7dfa0 100644 --- a/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php +++ b/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php @@ -115,6 +115,11 @@ class CreatePaymentToken implements EndpointInterface { if ( isset( $result->payment_source->card ) ) { $wc_token_id = $this->wc_payment_tokens->create_payment_token_card( $current_user_id, $result ); + + $is_free_trial_cart = $data['is_free_trial_cart'] ?? ''; + if ( $is_free_trial_cart === '1' ) { + WC()->session->set( 'ppcp_card_payment_token_for_free_trial', $wc_token_id ); + } } } diff --git a/modules/ppcp-wc-gateway/resources/css/common.scss b/modules/ppcp-wc-gateway/resources/css/common.scss index b694547c8..ac4bcab32 100644 --- a/modules/ppcp-wc-gateway/resources/css/common.scss +++ b/modules/ppcp-wc-gateway/resources/css/common.scss @@ -133,6 +133,11 @@ $background-ident-color: #fbfbfb; } } + .ppcp-notice-list { + list-style-type: disc; + padding-left: 20px; + } + th, td { border-top: 1px solid $border-color; } diff --git a/modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js b/modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js new file mode 100644 index 000000000..c76aa8960 --- /dev/null +++ b/modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js @@ -0,0 +1,58 @@ +/** + * Helper component to log debug details to the browser console. + * + * A utility class that is used by payment buttons on the front-end, like the GooglePayButton. + */ +export default class ConsoleLogger { + /** + * The prefix to display before every log output. + * + * @type {string} + */ + #prefix = ''; + + /** + * Whether logging is enabled, disabled by default. + * + * @type {boolean} + */ + #enabled = false; + + constructor( ...prefixes ) { + if ( prefixes.length ) { + this.#prefix = `[${ prefixes.join( ' | ' ) }]`; + } + } + + /** + * Enable or disable logging. Only impacts `log()` output. + * + * @param {boolean} state True to enable log output. + */ + set enabled( state ) { + this.#enabled = state; + } + + /** + * Output log-level details to the browser console, if logging is enabled. + * + * @param {...any} args - All provided values are output to the browser console. + */ + log( ...args ) { + if ( this.#enabled ) { + // eslint-disable-next-line + console.log( this.#prefix, ...args ); + } + } + + /** + * Generate an error message in the browser's console. + * + * Error messages are always output, even when logging is disabled. + * + * @param {...any} args - All provided values are output to the browser console. + */ + error( ...args ) { + console.error( this.#prefix, ...args ); + } +} diff --git a/modules/ppcp-wc-gateway/resources/js/helper/preview-button.js b/modules/ppcp-wc-gateway/resources/js/helper/preview-button.js index 30b71f511..d9c4f8264 100644 --- a/modules/ppcp-wc-gateway/resources/js/helper/preview-button.js +++ b/modules/ppcp-wc-gateway/resources/js/helper/preview-button.js @@ -1,9 +1,11 @@ +/* global jQuery */ + /** * Returns a Map with all input fields that are relevant to render the preview of the * given payment button. * * @param {string} apmName - Value of the custom attribute `data-ppcp-apm-name`. - * @return {Map} + * @return {Map} List of input elements found on the current admin page. */ export function getButtonFormFields( apmName ) { const inputFields = document.querySelectorAll( @@ -28,9 +30,9 @@ export function getButtonFormFields( apmName ) { /** * Returns a function that triggers an update of the specified preview button, when invoked. - + * * @param {string} apmName - * @return {((object) => void)} + * @return {((object) => void)} Trigger-function; updates preview buttons when invoked. */ export function buttonRefreshTriggerFactory( apmName ) { const eventName = `ppcp_paypal_render_preview_${ apmName }`; @@ -44,7 +46,7 @@ export function buttonRefreshTriggerFactory( apmName ) { * Returns a function that gets the current form values of the specified preview button. * * @param {string} apmName - * @return {() => {button: {wrapper:string, is_enabled:boolean, style:{}}}} + * @return {() => {button: {wrapper:string, is_enabled:boolean, style:{}}}} Getter-function; returns preview config details when invoked. */ export function buttonSettingsGetterFactory( apmName ) { const fields = getButtonFormFields( apmName ); diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 52a41576e..e92b7ee6a 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -71,6 +71,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsListener; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; +use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway; return array( 'wcgateway.paypal-gateway' => static function ( ContainerInterface $container ): PayPalGateway { @@ -198,6 +199,7 @@ return array( Settings::PAY_LATER_TAB_ID, AxoGateway::ID, GooglePayGateway::ID, + ApplePayGateway::ID, ), true ); @@ -220,6 +222,7 @@ return array( Settings::PAY_LATER_TAB_ID, Settings::CONNECTION_TAB_ID, GooglePayGateway::ID, + ApplePayGateway::ID, ), true ); @@ -1411,10 +1414,10 @@ return array( return $label; }, 'wcgateway.enable-dcc-url-sandbox' => static function ( ContainerInterface $container ): string { - return 'https://www.sandbox.paypal.com/bizsignup/entry/product/ppcp'; + return 'https://www.sandbox.paypal.com/bizsignup/entry?product=ppcp'; }, 'wcgateway.enable-dcc-url-live' => static function ( ContainerInterface $container ): string { - return 'https://www.paypal.com/bizsignup/entry/product/ppcp'; + return 'https://www.paypal.com/bizsignup/entry?product=ppcp'; }, 'wcgateway.enable-pui-url-sandbox' => static function ( ContainerInterface $container ): string { return 'https://www.sandbox.paypal.com/bizsignup/entry?country.x=DE&product=payment_methods&capabilities=PAY_UPON_INVOICE'; diff --git a/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php b/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php index b2ce4cbc5..e60b9e1bd 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php @@ -426,12 +426,42 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { public function process_payment( $order_id ) { $wc_order = wc_get_order( $order_id ); if ( ! is_a( $wc_order, WC_Order::class ) ) { + WC()->session->set( 'ppcp_card_payment_token_for_free_trial', null ); + return $this->handle_payment_failure( null, new GatewayGenericException( new Exception( 'WC order was not found.' ) ) ); } + $guest_card_payment_for_free_trial = WC()->session->get( 'ppcp_guest_payment_for_free_trial' ) ?? null; + WC()->session->get( 'ppcp_guest_payment_for_free_trial', null ); + if ( $guest_card_payment_for_free_trial ) { + $customer_id = $guest_card_payment_for_free_trial->customer->id ?? ''; + if ( $customer_id ) { + update_user_meta( $wc_order->get_customer_id(), '_ppcp_target_customer_id', $customer_id ); + } + + if ( isset( $guest_card_payment_for_free_trial->payment_source->card ) ) { + $this->wc_payment_tokens->create_payment_token_card( $wc_order->get_customer_id(), $guest_card_payment_for_free_trial ); + + $wc_order->payment_complete(); + return $this->handle_payment_success( $wc_order ); + } + } + + $card_payment_token_for_free_trial = WC()->session->get( 'ppcp_card_payment_token_for_free_trial' ) ?? null; + WC()->session->set( 'ppcp_card_payment_token_for_free_trial', null ); + if ( $card_payment_token_for_free_trial ) { + $tokens = WC_Payment_Tokens::get_customer_tokens( get_current_user_id() ); + foreach ( $tokens as $token ) { + if ( $token->get_id() === (int) $card_payment_token_for_free_trial ) { + $wc_order->payment_complete(); + return $this->handle_payment_success( $wc_order ); + } + } + } + // phpcs:ignore WordPress.Security.NonceVerification.Missing $card_payment_token_id = wc_clean( wp_unslash( $_POST['wc-ppcp-credit-card-gateway-payment-token'] ?? '' ) ); diff --git a/modules/ppcp-wc-gateway/src/Settings/Settings.php b/modules/ppcp-wc-gateway/src/Settings/Settings.php index 25fcb808d..e083a91d1 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Settings.php +++ b/modules/ppcp-wc-gateway/src/Settings/Settings.php @@ -118,18 +118,15 @@ class Settings implements ContainerInterface { * Stores the settings to the database. */ public function persist() { - return update_option( self::KEY, $this->settings ); } - /** * Loads the settings. * * @return bool */ private function load(): bool { - if ( $this->settings ) { return false; } diff --git a/package.json b/package.json index 72aad99db..6518c2e76 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "install:modules:ppcp-save-payment-methods": "cd modules/ppcp-save-payment-methods && yarn install", "install:modules:ppcp-axo": "cd modules/ppcp-axo && yarn install", "install:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn install", + "install:modules:ppcp-card-fields": "cd modules/ppcp-card-fields && yarn install", "install:modules:ppcp-compat": "cd modules/ppcp-compat && yarn install", "install:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn install", "build:modules:ppcp-applepay": "cd modules/ppcp-applepay && yarn run build", @@ -37,6 +38,7 @@ "build:modules:ppcp-axo": "cd modules/ppcp-axo && yarn run build", "build:modules:ppcp-paypal-subscriptions": "cd modules/ppcp-paypal-subscriptions && yarn run build", "build:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn run build", + "build:modules:ppcp-card-fields": "cd modules/ppcp-card-fields && yarn run build", "build:modules:ppcp-compat": "cd modules/ppcp-compat && yarn run build", "build:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn run build", "build:modules": "run-p build:modules:*", @@ -54,6 +56,7 @@ "watch:modules:ppcp-save-payment-methods": "cd modules/ppcp-save-payment-methods && yarn run watch", "watch:modules:ppcp-axo": "cd modules/ppcp-axo && yarn run watch", "watch:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn run watch", + "watch:modules:ppcp-card-fields": "cd modules/ppcp-card-fields && yarn run watch", "watch:modules:ppcp-compat": "cd modules/ppcp-compat && yarn run watch", "watch:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn run watch", "watch:modules": "run-p watch:modules:*", diff --git a/psalm.xml.dist b/psalm.xml.dist index 092361998..33986e444 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -35,6 +35,7 @@ + diff --git a/tests/Playwright/playwright.config.js b/tests/Playwright/playwright.config.js index dbe3173da..402a04bb4 100644 --- a/tests/Playwright/playwright.config.js +++ b/tests/Playwright/playwright.config.js @@ -1,78 +1,78 @@ // @ts-check -const { defineConfig, devices } = require('@playwright/test'); +const { defineConfig, devices } = require( '@playwright/test' ); -require('dotenv').config({ path: '.env' }); +require( 'dotenv' ).config( { path: '.env' } ); /** * @see https://playwright.dev/docs/test-configuration */ -module.exports = defineConfig({ - timeout: 60000, - testDir: './tests', - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: [ - [process.env.CI ? 'github' : 'list'], - ['html', {open: 'never'}], - ], - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: process.env.BASEURL, - ignoreHTTPSErrors: true, - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - }, +module.exports = defineConfig( { + timeout: 30000, + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: false, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !! process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [ + [ process.env.CI ? 'github' : 'list' ], + [ 'html', { open: 'never' } ], + ], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: process.env.BASEURL, + ignoreHTTPSErrors: true, + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, - /* Configure projects for major browsers */ - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices[ 'Desktop Chrome' ] }, + }, - // { - // name: 'firefox', - // use: { ...devices['Desktop Firefox'] }, - // }, - // - // { - // name: 'webkit', - // use: { ...devices['Desktop Safari'] }, - // }, + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + // + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ..devices['Desktop Chrome'], channel: 'chrome' }, - // }, - ], + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ..devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], - /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://127.0.0.1:3000', - // reuseExistingServer: !process.env.CI, - // }, -}); + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +} ); diff --git a/tests/Playwright/tests/apms-place-order.spec.js b/tests/Playwright/tests/apms-place-order.spec.js new file mode 100644 index 000000000..f12337840 --- /dev/null +++ b/tests/Playwright/tests/apms-place-order.spec.js @@ -0,0 +1,76 @@ +const { test, expect } = require( '@playwright/test' ); +const { + fillCheckoutForm, + expectOrderReceivedPage, + acceptTerms, +} = require( './utils/checkout' ); +const { + openPaypalPopup, + completePaypalPayment, +} = require( './utils/paypal-popup' ); + +const { PRODUCT_ID, CHECKOUT_URL, CART_URL, APM_ID } = process.env; + +async function expectContinuation( page ) { + await expect( + page.locator( '#payment_method_ppcp-gateway' ) + ).toBeChecked(); + + await expect( page.locator( '.component-frame' ) ).toHaveCount( 0 ); +} + +async function completeContinuation( page ) { + await expectContinuation( page ); + + await Promise.all( [ + page.waitForNavigation(), + page.locator( '#place_order' ).click(), + ] ); +} + +test( 'PayPal APM button place order', async ( { page } ) => { + await page.goto( CART_URL + '?add-to-cart=' + PRODUCT_ID ); + + await page.goto( CHECKOUT_URL ); + + await fillCheckoutForm( page ); + + const popup = await openPaypalPopup( page, { fundingSource: APM_ID } ); + + await popup.getByText( 'Continue', { exact: true } ).click(); + await completePaypalPayment( popup, { + selector: '[name="Successful"]', + } ); + + await expectOrderReceivedPage( page ); +} ); + +test( 'PayPal APM button place order when redirect fails', async ( { + page, +} ) => { + await page.goto( CART_URL + '?add-to-cart=' + PRODUCT_ID ); + + await page.goto( CHECKOUT_URL ); + + await fillCheckoutForm( page ); + + await page.evaluate( 'PayPalCommerceGateway.ajax.approve_order = null' ); + + const popup = await openPaypalPopup( page, { fundingSource: APM_ID } ); + + await popup.getByText( 'Continue', { exact: true } ).click(); + await completePaypalPayment( popup, { + selector: '[name="Successful"]', + } ); + + await expect( page.locator( '.woocommerce-error' ) ).toBeVisible(); + + await page.reload(); + await expectContinuation( page ); + + await acceptTerms( page ); + + await completeContinuation( page ); + + await expectOrderReceivedPage( page ); +} ); diff --git a/tests/Playwright/tests/blocks-place-order.spec.js b/tests/Playwright/tests/blocks-place-order.spec.js new file mode 100644 index 000000000..efb0cf38b --- /dev/null +++ b/tests/Playwright/tests/blocks-place-order.spec.js @@ -0,0 +1,109 @@ +const { expect, test } = require( '@playwright/test' ); +const { serverExec } = require( './utils/server' ); +const { + openPaypalPopup, + loginIntoPaypal, + completePaypalPayment, + waitForPaypalShippingList, +} = require( './utils/paypal-popup' ); +const { expectOrderReceivedPage } = require( './utils/checkout' ); + +const { + PRODUCT_ID, + BLOCK_CHECKOUT_URL, + BLOCK_CHECKOUT_PAGE_ID, + BLOCK_CART_URL, +} = process.env; + +async function completeBlockContinuation( page ) { + await expect( + page.locator( '#radio-control-wc-payment-method-options-ppcp-gateway' ) + ).toBeChecked(); + + await expect( page.locator( '.component-frame' ) ).toHaveCount( 0 ); + + await Promise.all( + page.waitForNavigation(), + page + .locator( '.wc-block-components-checkout-place-order-button' ) + .click() + ); +} + +test.beforeAll( async ( { browser } ) => { + await serverExec( + 'wp option update woocommerce_checkout_page_id ' + + BLOCK_CHECKOUT_PAGE_ID + ); + await serverExec( + 'wp pcp settings update blocks_final_review_enabled true' + ); +} ); + +test( 'PayPal express block checkout', async ( { page } ) => { + await page.goto( '?add-to-cart=' + PRODUCT_ID ); + + await page.goto( BLOCK_CHECKOUT_URL ); + + const popup = await openPaypalPopup( page ); + + await loginIntoPaypal( popup ); + + await completePaypalPayment( popup ); + + await completeBlockContinuation( page ); + + await expectOrderReceivedPage( page ); +} ); + +test( 'PayPal express block cart', async ( { page } ) => { + await page.goto( BLOCK_CART_URL + '?add-to-cart=' + PRODUCT_ID ); + + const popup = await openPaypalPopup( page ); + + await loginIntoPaypal( popup ); + + await completePaypalPayment( popup ); + + await completeBlockContinuation( page ); + + await expectOrderReceivedPage( page ); +} ); + +test.describe( 'Without review', () => { + test.beforeAll( async ( { browser } ) => { + await serverExec( + 'wp pcp settings update blocks_final_review_enabled false' + ); + } ); + + test( 'PayPal express block checkout', async ( { page } ) => { + await page.goto( '?add-to-cart=' + PRODUCT_ID ); + + await page.goto( BLOCK_CHECKOUT_URL ); + + const popup = await openPaypalPopup( page ); + + await loginIntoPaypal( popup ); + + await waitForPaypalShippingList( popup ); + + await completePaypalPayment( popup ); + + await expectOrderReceivedPage( page ); + } ); + + test( 'PayPal express block cart', async ( { page } ) => { + await page.goto( BLOCK_CART_URL + '?add-to-cart=' + PRODUCT_ID ); + + const popup = await openPaypalPopup( page ); + + await loginIntoPaypal( popup ); + + await waitForPaypalShippingList( popup ); + + await completePaypalPayment( popup ); + + await expectOrderReceivedPage( page ); + } ); +} ); diff --git a/tests/Playwright/tests/classic-place-order.spec.js b/tests/Playwright/tests/classic-place-order.spec.js new file mode 100644 index 000000000..22cc5055d --- /dev/null +++ b/tests/Playwright/tests/classic-place-order.spec.js @@ -0,0 +1,97 @@ +const { test, expect } = require( '@playwright/test' ); +const { serverExec } = require( './utils/server' ); +const { + fillCheckoutForm, + expectOrderReceivedPage, +} = require( './utils/checkout' ); +const { + openPaypalPopup, + loginIntoPaypal, + completePaypalPayment, +} = require( './utils/paypal-popup' ); + +const { + CREDIT_CARD_NUMBER, + CREDIT_CARD_CVV, + PRODUCT_URL, + CHECKOUT_URL, + CHECKOUT_PAGE_ID, +} = process.env; + +async function expectContinuation( page ) { + await expect( + page.locator( '#payment_method_ppcp-gateway' ) + ).toBeChecked(); + + await expect( page.locator( '.component-frame' ) ).toHaveCount( 0 ); +} + +async function completeContinuation( page ) { + await expectContinuation( page ); + + await Promise.all( [ + page.waitForNavigation(), + page.locator( '#place_order' ).click(), + ] ); +} + +test.beforeAll( async ( { browser } ) => { + await serverExec( + 'wp option update woocommerce_checkout_page_id ' + CHECKOUT_PAGE_ID + ); +} ); + +test( 'PayPal button place order from Product page', async ( { page } ) => { + await serverExec( + 'wp pcp settings update blocks_final_review_enabled true' + ); + + await page.goto( PRODUCT_URL ); + + const popup = await openPaypalPopup( page ); + + await loginIntoPaypal( popup ); + + await completePaypalPayment( popup ); + + await fillCheckoutForm( page ); + + await completeContinuation( page ); + + await expectOrderReceivedPage( page ); +} ); + +test( 'Advanced Credit and Debit Card place order from Checkout page', async ( { + page, +} ) => { + await page.goto( PRODUCT_URL ); + await page.locator( '.single_add_to_cart_button' ).click(); + + await page.goto( CHECKOUT_URL ); + await fillCheckoutForm( page ); + + await page.click( 'text=Credit Cards' ); + + const expirationDate = await page + .frameLocator( 'iframe[title="paypal_card_expiry_field"]' ) + .locator( 'input.card-field-expiry' ); + await expirationDate.click(); + await page.keyboard.type( '01/42' ); + + const creditCardNumber = await page + .frameLocator( '[title="paypal_card_number_field"]' ) + .locator( '.card-field-number' ); + await creditCardNumber.fill( CREDIT_CARD_NUMBER ); + + const cvv = await page + .frameLocator( '[title="paypal_card_cvv_field"]' ) + .locator( '.card-field-cvv' ); + await cvv.fill( CREDIT_CARD_CVV ); + + await Promise.all( [ + page.waitForNavigation(), + page.locator( '.ppcp-dcc-order-button' ).click(), + ] ); + + await expectOrderReceivedPage( page ); +} ); diff --git a/tests/Playwright/tests/free-trial-subscriptions.spec.js b/tests/Playwright/tests/free-trial-subscriptions.spec.js new file mode 100644 index 000000000..8451884fd --- /dev/null +++ b/tests/Playwright/tests/free-trial-subscriptions.spec.js @@ -0,0 +1,97 @@ +const { test, expect } = require( '@playwright/test' ); +const { loginAsCustomer } = require( './utils/user' ); +const { openPaypalPopup, loginIntoPaypal } = require( './utils/paypal-popup' ); +const { serverExec } = require( './utils/server' ); +const { expectOrderReceivedPage } = require( './utils/checkout' ); + +const { CREDIT_CARD_NUMBER, CREDIT_CARD_CVV } = process.env; + +test( 'PayPal logged-in user free trial subscription without payment token with shipping callback enabled', async ( { + page, +} ) => { + await serverExec( + 'wp pcp settings update blocks_final_review_enabled false' + ); + + await loginAsCustomer( page ); + await page.goto( '/product/free-trial' ); + await page.click( 'text=Sign up now' ); + await page.goto( '/classic-checkout' ); + + const popup = await openPaypalPopup( page ); + await loginIntoPaypal( popup ); + popup.locator( '#consentButton' ).click(); + + await page.waitForURL( '**/order-received/**' ); +} ); + +test( 'ACDC logged-in user free trial subscription without payment token', async ( { + page, +} ) => { + await loginAsCustomer( page ); + await page.goto( '/product/free-trial' ); + await page.click( 'text=Sign up now' ); + await page.goto( '/classic-checkout' ); + + await page.click( 'text=Credit Cards' ); + + const creditCardNumber = await page + .frameLocator( '[title="paypal_card_number_field"]' ) + .locator( '.card-field-number' ); + await creditCardNumber.fill( CREDIT_CARD_NUMBER ); + + const expirationDate = await page + .frameLocator( 'iframe[title="paypal_card_expiry_field"]' ) + .locator( 'input.card-field-expiry' ); + await expirationDate.click(); + await page.keyboard.type( '01/42' ); + + const cvv = await page + .frameLocator( '[title="paypal_card_cvv_field"]' ) + .locator( '.card-field-cvv' ); + await cvv.fill( CREDIT_CARD_CVV ); + + await Promise.all( [ + page.waitForNavigation(), + page.locator( '.ppcp-dcc-order-button' ).click(), + ] ); + + await expectOrderReceivedPage( page ); +} ); + +test( 'ACDC purchase free trial in Block checkout page as logged-in without saved card payments', async ( { + page, +} ) => { + await loginAsCustomer( page ); + await page.goto( '/product/free-trial' ); + await page.click( 'text=Sign up now' ); + await page.goto( '/checkout' ); + + await page + .locator( + '#radio-control-wc-payment-method-options-ppcp-credit-card-gateway' + ) + .click(); + + const expirationDate = await page + .frameLocator( 'iframe[title="paypal_card_expiry_field"]' ) + .locator( 'input.card-field-expiry' ); + await expirationDate.click(); + await page.keyboard.type( '01/42' ); + + const creditCardNumber = await page + .frameLocator( '[title="paypal_card_number_field"]' ) + .locator( '.card-field-number' ); + await creditCardNumber.fill( CREDIT_CARD_NUMBER ); + + const cvv = await page + .frameLocator( '[title="paypal_card_cvv_field"]' ) + .locator( '.card-field-cvv' ); + await cvv.fill( CREDIT_CARD_CVV ); + + await page + .locator( '.wc-block-components-checkout-place-order-button' ) + .click(); + + await page.waitForURL( '**/order-received/**' ); +} ); diff --git a/tests/Playwright/tests/place-order.spec.js b/tests/Playwright/tests/place-order.spec.js deleted file mode 100644 index 4a9f7523f..000000000 --- a/tests/Playwright/tests/place-order.spec.js +++ /dev/null @@ -1,207 +0,0 @@ -const {test, expect} = require('@playwright/test'); -const {serverExec} = require("./utils/server"); -const {fillCheckoutForm, expectOrderReceivedPage, acceptTerms} = require("./utils/checkout"); -const {openPaypalPopup, loginIntoPaypal, waitForPaypalShippingList, completePaypalPayment} = require("./utils/paypal-popup"); - -const { - CREDIT_CARD_NUMBER, - CREDIT_CARD_EXPIRATION, - CREDIT_CARD_CVV, - PRODUCT_URL, - PRODUCT_ID, - CHECKOUT_URL, - CHECKOUT_PAGE_ID, - CART_URL, - BLOCK_CHECKOUT_URL, - BLOCK_CHECKOUT_PAGE_ID, - BLOCK_CART_URL, - APM_ID, -} = process.env; - -async function completeBlockContinuation(page) { - await expect(page.locator('#radio-control-wc-payment-method-options-ppcp-gateway')).toBeChecked(); - - await expect(page.locator('.component-frame')).toHaveCount(0); - - await Promise.all( - page.waitForNavigation(), - page.locator('.wc-block-components-checkout-place-order-button').click(), - ); -} - -async function expectContinuation(page) { - await expect(page.locator('#payment_method_ppcp-gateway')).toBeChecked(); - - await expect(page.locator('.component-frame')).toHaveCount(0); -} - -async function completeContinuation(page) { - await expectContinuation(page); - - await Promise.all([ - page.waitForNavigation(), - page.locator('#place_order').click(), - ]); -} - -test.describe('Classic checkout', () => { - test.beforeAll(async ({ browser }) => { - await serverExec('wp option update woocommerce_checkout_page_id ' + CHECKOUT_PAGE_ID); - }); - - test('PayPal button place order from Product page', async ({page}) => { - await page.goto(PRODUCT_URL); - - const popup = await openPaypalPopup(page); - - await loginIntoPaypal(popup); - - await completePaypalPayment(popup); - - await fillCheckoutForm(page); - - await completeContinuation(page); - - await expectOrderReceivedPage(page); - }); - - test('Advanced Credit and Debit Card place order from Checkout page', async ({page}) => { - await page.goto(PRODUCT_URL); - await page.locator('.single_add_to_cart_button').click(); - - await page.goto(CHECKOUT_URL); - await fillCheckoutForm(page); - - await page.click("text=Credit Cards"); - - const creditCardNumber = page.frameLocator('#braintree-hosted-field-number').locator('#credit-card-number'); - await creditCardNumber.fill(CREDIT_CARD_NUMBER); - - const expirationDate = page.frameLocator('#braintree-hosted-field-expirationDate').locator('#expiration'); - await expirationDate.fill(CREDIT_CARD_EXPIRATION); - - const cvv = page.frameLocator('#braintree-hosted-field-cvv').locator('#cvv'); - await cvv.fill(CREDIT_CARD_CVV); - - await Promise.all([ - page.waitForNavigation(), - page.locator('.ppcp-dcc-order-button').click(), - ]); - - await expectOrderReceivedPage(page); - }); - - test('PayPal APM button place order', async ({page}) => { - await page.goto(CART_URL + '?add-to-cart=' + PRODUCT_ID); - - await page.goto(CHECKOUT_URL); - - await fillCheckoutForm(page); - - const popup = await openPaypalPopup(page, {fundingSource: APM_ID}); - - await popup.getByText('Continue', { exact: true }).click(); - await completePaypalPayment(popup, {selector: '[name="Successful"]'}); - - await expectOrderReceivedPage(page); - }); - - test('PayPal APM button place order when redirect fails', async ({page}) => { - await page.goto(CART_URL + '?add-to-cart=' + PRODUCT_ID); - - await page.goto(CHECKOUT_URL); - - await fillCheckoutForm(page); - - await page.evaluate('PayPalCommerceGateway.ajax.approve_order = null'); - - const popup = await openPaypalPopup(page, {fundingSource: APM_ID}); - - await popup.getByText('Continue', { exact: true }).click(); - await completePaypalPayment(popup, {selector: '[name="Successful"]'}); - - await expect(page.locator('.woocommerce-error')).toBeVisible(); - - await page.reload(); - await expectContinuation(page); - - await acceptTerms(page); - - await completeContinuation(page); - - await expectOrderReceivedPage(page); - }); -}); - -test.describe('Block checkout', () => { - test.beforeAll(async ({browser}) => { - await serverExec('wp option update woocommerce_checkout_page_id ' + BLOCK_CHECKOUT_PAGE_ID); - await serverExec('wp pcp settings update blocks_final_review_enabled true'); - }); - - test('PayPal express block checkout', async ({page}) => { - await page.goto('?add-to-cart=' + PRODUCT_ID); - - await page.goto(BLOCK_CHECKOUT_URL) - - const popup = await openPaypalPopup(page); - - await loginIntoPaypal(popup); - - await completePaypalPayment(popup); - - await completeBlockContinuation(page); - - await expectOrderReceivedPage(page); - }); - - test('PayPal express block cart', async ({page}) => { - await page.goto(BLOCK_CART_URL + '?add-to-cart=' + PRODUCT_ID) - - const popup = await openPaypalPopup(page); - - await loginIntoPaypal(popup); - - await completePaypalPayment(popup); - - await completeBlockContinuation(page); - - await expectOrderReceivedPage(page); - }); - - test.describe('Without review', () => { - test.beforeAll(async ({browser}) => { - await serverExec('wp pcp settings update blocks_final_review_enabled false'); - }); - - test('PayPal express block checkout', async ({page}) => { - await page.goto('?add-to-cart=' + PRODUCT_ID); - - await page.goto(BLOCK_CHECKOUT_URL) - - const popup = await openPaypalPopup(page); - - await loginIntoPaypal(popup); - - await waitForPaypalShippingList(popup); - - await completePaypalPayment(popup); - - await expectOrderReceivedPage(page); - }); - - test('PayPal express block cart', async ({page}) => { - await page.goto(BLOCK_CART_URL + '?add-to-cart=' + PRODUCT_ID) - - const popup = await openPaypalPopup(page); - - await loginIntoPaypal(popup); - - await waitForPaypalShippingList(popup); - - await completePaypalPayment(popup); - - await expectOrderReceivedPage(page); - }); - }); -}); diff --git a/tests/Playwright/tests/save-payment-methods.spec.js b/tests/Playwright/tests/save-payment-methods.spec.js index f6fb46315..077207141 100644 --- a/tests/Playwright/tests/save-payment-methods.spec.js +++ b/tests/Playwright/tests/save-payment-methods.spec.js @@ -1,89 +1,84 @@ -const {test, expect} = require('@playwright/test'); -const {loginAsCustomer} = require("./utils/user"); -const {openPaypalPopup, loginIntoPaypal, completePaypalPayment} = require("./utils/paypal-popup"); -const {fillCheckoutForm, expectOrderReceivedPage} = require("./utils/checkout"); - +const { test, expect } = require( '@playwright/test' ); +const { loginAsCustomer } = require( './utils/user' ); const { - PRODUCT_URL, -} = process.env; + openPaypalPopup, + loginIntoPaypal, + completePaypalPayment, +} = require( './utils/paypal-popup' ); +const { + fillCheckoutForm, + expectOrderReceivedPage, +} = require( './utils/checkout' ); -async function expectContinuation(page) { - await expect(page.locator('#payment_method_ppcp-gateway')).toBeChecked(); +const { PRODUCT_URL } = process.env; - await expect(page.locator('.component-frame')).toHaveCount(0); +async function expectContinuation( page ) { + await expect( + page.locator( '#payment_method_ppcp-gateway' ) + ).toBeChecked(); + + await expect( page.locator( '.component-frame' ) ).toHaveCount( 0 ); } -async function completeContinuation(page) { - await expectContinuation(page); +async function completeContinuation( page ) { + await expectContinuation( page ); - await Promise.all([ - page.waitForNavigation(), - page.locator('#place_order').click(), - ]); + await Promise.all( [ + page.waitForNavigation(), + page.locator( '#place_order' ).click(), + ] ); } -test('Save during purchase', async ({page}) => { - await loginAsCustomer(page) +// preconditions: shipping callback disabled and no saved payments +test( 'Save during purchase', async ( { page } ) => { + await loginAsCustomer( page ); - await page.goto(PRODUCT_URL); - const popup = await openPaypalPopup(page); + await page.goto( PRODUCT_URL ); + const popup = await openPaypalPopup( page ); - await loginIntoPaypal(popup); - await completePaypalPayment(popup); - await fillCheckoutForm(page); + await loginIntoPaypal( popup ); + await completePaypalPayment( popup ); + await fillCheckoutForm( page ); - await completeContinuation(page); + await completeContinuation( page ); - await expectOrderReceivedPage(page); -}); + await expectOrderReceivedPage( page ); +} ); -test('PayPal add payment method', async ({page}) => { - await loginAsCustomer(page); - await page.goto('/my-account/add-payment-method'); +test( 'PayPal add payment method', async ( { page } ) => { + await loginAsCustomer( page ); + await page.goto( '/my-account/add-payment-method' ); - const popup = await openPaypalPopup(page); - await loginIntoPaypal(popup); - popup.locator('#consentButton').click(); + const popup = await openPaypalPopup( page ); + await loginIntoPaypal( popup ); + popup.locator( '#consentButton' ).click(); - await page.waitForURL('/my-account/payment-methods'); -}); + await page.waitForURL( '/my-account/payment-methods' ); +} ); -test('ACDC add payment method', async ({page}) => { - await loginAsCustomer(page); - await page.goto('/my-account/add-payment-method'); +test( 'ACDC add payment method', async ( { page } ) => { + await loginAsCustomer( page ); + await page.goto( '/my-account/add-payment-method' ); - await page.click("text=Debit & Credit Cards"); - - const creditCardNumber = await page.frameLocator('[title="paypal_card_number_field"]').locator('.card-field-number'); - await creditCardNumber.fill('4005519200000004'); - - const expirationDate = await page.frameLocator('[title="paypal_card_expiry_field"]').locator('.card-field-expiry'); - await expirationDate.fill('01/25'); - - const cvv = await page.frameLocator('[title="paypal_card_cvv_field"]').locator('.card-field-cvv'); - await cvv.fill('123'); - - await page.waitForURL('/my-account/payment-methods'); -}); - -test('PayPal logged-in user free trial subscription without payment token', async ({page}) => { - await loginAsCustomer(page); - - await page.goto('/shop'); - await page.click("text=Sign up now"); - await page.goto('/classic-checkout'); - - const popup = await openPaypalPopup(page); - await loginIntoPaypal(popup); - popup.locator('#consentButton').click(); - - await page.click("text=Proceed to PayPal"); - - const title = await page.locator('.entry-title'); - await expect(title).toHaveText('Order received'); -}) + await page.click( 'text=Debit & Credit Cards' ); + const creditCardNumber = await page + .frameLocator( '[title="paypal_card_number_field"]' ) + .locator( '.card-field-number' ); + await creditCardNumber.fill( '4005519200000004' ); + const expirationDate = await page + .frameLocator( 'iframe[title="paypal_card_expiry_field"]' ) + .locator( 'input.card-field-expiry' ); + await expirationDate.click(); + await page.keyboard.type( '12/25' ); + const cvv = await page + .frameLocator( '[title="paypal_card_cvv_field"]' ) + .locator( '.card-field-cvv' ); + await cvv.fill( '123' ); + await page.getByRole( 'button', { name: 'Add payment method' } ).click(); + await page.waitForURL( '/my-account/payment-methods' ); +} );