Plugin Directory

Changeset 3306839


Ignore:
Timestamp:
06/05/2025 06:43:09 AM (10 months ago)
Author:
multisafepayplugin
Message:

Update to version 6.9.0 from GitHub

Location:
multisafepay
Files:
2 added
54 edited
1 copied

Legend:

Unmodified
Added
Removed
  • multisafepay/tags/6.9.0/assets/public/js/multisafepay-apple-pay-wallet.js

    r3048898 r3306839  
    6262        this._sessionActive = false;
    6363
     64        /**
     65         * Flag to track if a session has been aborted
     66         *
     67         * @type {boolean}
     68         * @private
     69         */
     70        this._sessionAborted = false;
     71
    6472        this.init()
    6573            .then(
     
    174182     * Event handler for Apple Pay button click
    175183     *
    176      * @returns {Promise<void>}
    177      */
    178     onApplePaymentButtonClicked = async() => {
     184     * @returns {void}
     185     */
     186    onApplePaymentButtonClicked = () => {
     187        // Check if a session is already active
     188        if ( this.isSessionActive() ) {
     189            debugDirect( 'Apple Pay session was already activated', this.debug, 'log' );
     190            // Force reset session status to allow retry
     191            this.setSessionStatus( false );
     192            this._sessionAborted = false;
     193            return;
     194        }
     195
    179196        try {
    180             await this.beginApplePaySession();
    181         } catch ( error ) {
    182             console.error( 'Error starting Apple Pay session when button is clicked:', error );
    183             this.setSessionStatus( false );
    184         }
    185     }
    186 
    187     /**
    188      * Create Apple Pay button
    189      *
    190      * @returns {Promise<void>}
    191      */
    192     async createApplePayButton()
    193     {
    194         // Check if previous buttons already exist and remove them
    195         cleanUpDirectButtons();
    196 
    197         const buttonContainer = document.getElementById( 'place_order' ).parentElement;
    198         if ( ! buttonContainer ) {
    199             debugDirect( 'Button container not found', this.debug );
    200             return;
    201         }
    202 
    203         // Features of the button
    204         const buttonTag        = document.createElement( 'button' );
    205         buttonTag.className    = 'apple-pay-button apple-pay-button-black';
    206         buttonTag.style.cursor = 'pointer';
    207 
    208         buttonContainer.addEventListener(
    209             'click',
    210             (
    211                 event ) => {
    212                     this.onApplePaymentButtonClicked();
    213                     // Avoid that WordPress submits the form
    214                     event.preventDefault();
    215                 }
    216         );
    217 
    218         // Append the button to the div
    219         buttonContainer.appendChild( buttonTag );
    220     }
    221 
    222     /**
    223      * Create the Apple Pay payment request object and session
    224      *
    225      * Some variables from the global scope are launched from
    226      * the internal code of Prestashop
    227      *
    228      * @returns {Promise<void>}
    229      */
    230     async beginApplePaySession()
    231     {
    232         try {
    233             const validatorInstance = new FieldsValidator();
    234             const fieldsAreValid    = validatorInstance.checkFields();
    235             if ( ! fieldsAreValid ) {
    236                 debugDirect( 'Not all mandatory fields were filled out', this.debug, 'warn' );
    237                 return;
    238             }
    239 
    240             if ( this.isSessionActive() ) {
    241                 debugDirect( 'Apple Pay session was already activated', this.debug, 'log' );
    242                 return;
    243             }
    244 
    245             // Create the payment request object
     197            // Create the payment request object first
    246198            const paymentRequest = {
    247199                countryCode: configApplePay.countryCode,
     
    258210            };
    259211
    260             // Create the session and handle the events
     212            // Create the session immediately in the click handler
    261213            const session = new ApplePaySession( this.config.applePayVersion, paymentRequest );
     214
    262215            if ( session ) {
     216                // Reset the aborted flag
     217                this._sessionAborted = false;
     218
     219                // Setup event handlers
    263220                session.onvalidatemerchant  = ( event ) => this.handleValidateMerchant( event, session );
    264221                session.onpaymentauthorized = ( event ) => this.handlePaymentAuthorized( event, session );
    265222                session.oncancel            = ( event ) => this.handleCancel( event, session );
    266                 session.begin();
    267 
     223
     224                // Set session as active
    268225                this.setSessionStatus( true );
     226
     227                // Validate fields before beginning the session
     228                this.validateFieldsBeforeSession( session );
    269229            }
    270230        } catch ( error ) {
    271             console.error( 'Error starting Apple Pay session:', error );
     231            console.error( 'Error starting Apple Pay session when button is clicked:', error );
    272232            this.setSessionStatus( false );
    273         }
    274     }
    275 
    276     /**
    277      * Fetch merchant session data from MultiSafepay
    278      *
    279      * @param {string} validationURL
    280      * @param {string} originDomain
    281      * @returns {Promise<object>}
    282      */
    283     async fetchMerchantSession(validationURL, originDomain)
    284     {
    285         const data = new URLSearchParams();
    286         data.append( 'action', 'applepay_direct_validation' );
    287         data.append( 'validation_url', validationURL );
    288         data.append( 'origin_domain', originDomain );
    289 
    290         const response = await fetch(
    291             this.config.multiSafepayServerScript,
    292             {
    293                 method: 'POST',
    294                 body: data,
    295                 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
    296             }
    297         );
    298 
    299         return JSON.parse( await response.json() );
     233            this._sessionAborted = false;
     234        }
     235    }
     236
     237    /**
     238     * Validate fields before beginning the Apple Pay session
     239     *
     240     * @param {object} session - The Apple Pay session
     241     * @returns {void}
     242     */
     243    validateFieldsBeforeSession( session ) {
     244        // Create a validator instance
     245        const validatorInstance = new FieldsValidator();
     246
     247        // Clear any previous error messages
     248        validatorInstance.clearAllErrors();
     249
     250        // Validate fields
     251        validatorInstance.checkFields()
     252            .then(
     253                fieldsAreValid => {
     254                    if ( fieldsAreValid ) {
     255                        // Fields are valid, now begin the session
     256                        session.begin();
     257                    } else {
     258                        // Fields are not valid, don't begin the session
     259                        debugDirect( 'Not all mandatory fields were filled out', this.debug, 'warn' );
     260                        this._sessionAborted = true;
     261                        this.setSessionStatus( false );
     262                    }
     263                }
     264            )
     265            .catch(
     266                error => {
     267                    // Error during validation
     268                    debugDirect( 'Error during field validation: ' + error, this.debug, 'error' );
     269                    this._sessionAborted = true;
     270                    this.setSessionStatus( false );
     271                }
     272            );
    300273    }
    301274
     
    309282    handleValidateMerchant = async( event, session ) => {
    310283        try {
     284            // Check if the session has been aborted
     285            if ( this._sessionAborted ) {
     286                debugDirect( 'Session was already aborted, skipping merchant validation', this.debug, 'log' );
     287                return;
     288            }
     289
    311290            const validationURL = event.validationURL;
    312291            const originDomain  = window.location.hostname;
     
    314293            const merchantSession = await this.fetchMerchantSession( validationURL, originDomain );
    315294            if ( merchantSession && ( typeof merchantSession === 'object' ) ) {
    316                 session.completeMerchantValidation( merchantSession );
     295                // Only complete merchant validation if the session hasn't been aborted
     296                if ( ! this._sessionAborted ) {
     297                    session.completeMerchantValidation( merchantSession );
     298                }
    317299            } else {
    318300                debugDirect( 'Error validating merchant', this.debug );
     301                this._sessionAborted = true;
    319302                session.abort();
    320303            }
    321304        } catch ( error ) {
    322305            console.error( 'Error validating merchant:', error );
     306            this._sessionAborted = true;
    323307            session.abort();
    324308        }
     
    336320    handlePaymentAuthorized = async( event, session ) => {
    337321        try {
     322            // Check if the session has been aborted
     323            if ( this._sessionAborted ) {
     324                debugDirect( 'Session was already aborted, skipping payment authorization', this.debug, 'log' );
     325                return;
     326            }
     327
    338328            const paymentToken = JSON.stringify( event.payment.token );
    339329            const success      = await this.submitApplePayForm( paymentToken );
     
    361351            try {
    362352                debugDirect( 'Apple Pay Direct session successfully aborted.', this.debug, 'log' );
     353                this._sessionAborted = true;
    363354                session.abort();
    364355            } catch ( error ) {
     
    410401        return true;
    411402    }
     403
     404    /**
     405     * Fetch merchant session data from MultiSafepay
     406     *
     407     * @param {string} validationURL
     408     * @param {string} originDomain
     409     * @returns {Promise<object>}
     410     */
     411    async fetchMerchantSession( validationURL, originDomain )
     412    {
     413        const data = new URLSearchParams();
     414        data.append( 'action', 'applepay_direct_validation' );
     415        data.append( 'validation_url', validationURL );
     416        data.append( 'origin_domain', originDomain );
     417
     418        const response = await fetch(
     419            this.config.multiSafepayServerScript,
     420            {
     421                method: 'POST',
     422                body: data,
     423                headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
     424            }
     425        );
     426
     427        return JSON.parse( await response.json() );
     428    }
     429
     430    /**
     431     * Create Apple Pay button
     432     *
     433     * @returns {Promise<void>}
     434     */
     435    async createApplePayButton()
     436    {
     437        // Check if previous buttons already exist and remove them
     438        cleanUpDirectButtons();
     439
     440        const buttonContainer = document.getElementById( 'place_order' ).parentElement;
     441        if ( ! buttonContainer ) {
     442            debugDirect( 'Button container not found', this.debug );
     443            return;
     444        }
     445
     446        // Features of the button
     447        const buttonTag        = document.createElement( 'button' );
     448        buttonTag.className    = 'apple-pay-button apple-pay-button-black';
     449        buttonTag.style.cursor = 'pointer';
     450
     451        // Add an event listener directly to the Apple Pay button, not the container
     452        buttonTag.addEventListener(
     453            'click',
     454            ( event ) => {
     455                this.onApplePaymentButtonClicked();
     456                // Prevent that WordPress submits the form
     457                event.preventDefault();
     458                event.stopPropagation();
     459            }
     460        );
     461
     462        // Append the button to the div
     463        buttonContainer.appendChild( buttonTag );
     464    }
    412465}
  • multisafepay/tags/6.9.0/assets/public/js/multisafepay-google-pay-wallet.js

    r3048898 r3306839  
    222222    {
    223223        const validatorInstance = new FieldsValidator();
    224         const fieldsAreValid    = validatorInstance.checkFields();
     224        const fieldsAreValid    = await validatorInstance.checkFields();
    225225        if ( fieldsAreValid ) {
    226226            if ( paymentsClient && paymentsClient.loadPaymentData ) {
  • multisafepay/tags/6.9.0/assets/public/js/multisafepay-jquery-wallets.js

    r3048898 r3306839  
    4444                            function() {
    4545                                debugDirect( 'Select2 action initialized for field: ' + this.name, debugStatus, 'log' );
    46                                 const select2Container = $( this ).closest( '.validate-required' ).find( '.select2-selection' );
     46
     47                                const wrapper          = $( this ).closest( '.validate-required' );
     48                                const select2Container = wrapper.find( '.select2-selection' );
    4749                                if ( select2Container.length ) {
    48                                     select2Container.attr( 'style', '' );
     50                                    // Remove the inline red border style
     51                                    select2Container.removeAttr( 'style' );
     52                                    // Alternative approach if removeAttr doesn't work
     53                                    select2Container.css( 'border', '' );
     54                                }
     55
     56                                // Get the field ID container
     57                                const fieldId        = this.name + '_field';
     58                                const fieldContainer = $( '#' + fieldId );
     59                                if ( fieldContainer.length ) {
     60                                    // Remove WooCommerce invalid class and add validated class
     61                                    fieldContainer.removeClass( 'woocommerce-invalid' );
     62                                    fieldContainer.addClass( 'woocommerce-validated' );
    4963                                }
    5064                                validatorInstance.removeErrorMessage( this.name );
     
    167181                                // Remove the orphan error messages from the notice group
    168182                                validatorInstance.removeOrphanErrorMessages();
     183                                // Re-initialize select2 validation after checkout is updated
     184                                select2Validation();
    169185                            }
    170186                        );
  • multisafepay/tags/6.9.0/assets/public/js/multisafepay-validator-wallets.js

    r3048898 r3306839  
    2020 */
    2121
     22// Check if the debugDirect function is available, if not define it
     23if ( typeof debugDirect !== 'function' ) {
     24    window.debugDirect = function( debugMessage, debugEnabled, loggingType = 'error' ) {
     25        const allowedTypeArray = ['log', 'info', 'warn', 'error', 'debug'];
     26
     27        if ( ! allowedTypeArray.includes( loggingType ) ) {
     28            loggingType = 'log';
     29        }
     30
     31        if ( debugMessage && debugEnabled ) {
     32            console[loggingType]( debugMessage );
     33        }
     34    };
     35}
     36
    2237/**
    2338 * Class to validate the fields in the checkout form
     
    3247
    3348    /**
     49     * Cache for field labels to avoid repeated DOM lookups
     50     *
     51     * @type {Object}
     52     */
     53    fieldLabelsCache = {};
     54
     55    /**
     56     * Cache for validated postcodes to avoid redundant AJAX calls
     57     * Format: { 'prefix[billing/shipping]:country:postcode': boolean }
     58     *
     59     * @type {Object}
     60     */
     61    validatedPostcodesCache = {};
     62
     63    /**
     64     * Flag to indicate if validation is currently in progress
     65     *
     66     * @type {boolean}
     67     */
     68    validationInProgress = false;
     69
     70    /**
     71     * Constructor - setup event listeners for form submission
     72     */
     73    constructor() {
     74        // Set up a global event listener to prevent payment when validation is in progress
     75        this.setupPaymentBlockingWhileValidating();
     76    }
     77
     78    /**
     79     * Check if an element is visible
     80     *
     81     * @param {HTMLElement} element - Element to check visibility
     82     * @returns {boolean} - Whether the element is visible
     83     */
     84    isElementVisible( element ) {
     85        if ( ! element ) {
     86            return false;
     87        }
     88
     89        const style = window.getComputedStyle( element );
     90        return ( element.offsetParent !== null ) ||
     91            (
     92                ( style.position === 'fixed' ) &&
     93                ( style.display !== 'none' ) &&
     94                ( style.visibility !== 'hidden' )
     95            );
     96    }
     97
     98    /**
     99     * Setup event listener to prevent payment when validation is in progress
     100     *
     101     * @returns {void}
     102     */
     103    setupPaymentBlockingWhileValidating() {
     104        document.addEventListener(
     105            'click',
     106            ( event ) => {
     107                // If validation is in progress
     108                if ( this.validationInProgress ) {
     109                    // Look for payment buttons or elements that may trigger payment
     110                    const targetElement   = event.target;
     111                    const paymentTriggers = [
     112                        '.payment_method_multisafepay_applepay',
     113                        '.payment_method_multisafepay_googlepay',
     114                        '#place_order',
     115                        '.apple-pay-button',
     116                        '.google-pay-button'
     117                    ];
     118
     119                    // Check if the clicked element matches any payment trigger selectors
     120                    const isPaymentTrigger = paymentTriggers.some(
     121                        selector =>
     122                            targetElement.matches && (
     123                                targetElement.matches( selector ) ||
     124                                targetElement.closest( selector )
     125                            )
     126                    );
     127
     128                    if ( isPaymentTrigger ) {
     129                        debugDirect( 'Payment blocked: Validation in progress', debugStatus, 'log' );
     130                        event.preventDefault();
     131                        event.stopPropagation();
     132                        return false;
     133                    }
     134                }
     135            },
     136            true
     137        );
     138    }
     139
     140    /**
    34141     * Get the label text for a field
    35142     *
     
    38145     */
    39146    getLabelText( fieldName ) {
    40         // Skip if this field has already been processed
    41         if ( this.processedFields.indexOf( fieldName ) !== -1 ) {
    42             return '';
     147        // If we already have the label cached, return it
     148        if ( this.fieldLabelsCache[fieldName] ) {
     149            return this.fieldLabelsCache[fieldName];
    43150        }
    44151
     
    72179        this.processedFields.push( fieldName );
    73180
    74         // Return the label text including the prefix
    75         return prefix + labelElement.firstChild.textContent.trim();
     181        // Create the label text including the prefix
     182        const labelText = prefix + labelElement.firstChild.textContent.trim();
     183
     184        // Cache the label text
     185        this.fieldLabelsCache[fieldName] = labelText;
     186
     187        // Return the label text
     188        return labelText;
     189    }
     190
     191    /**
     192     * Validate an email address format
     193     *
     194     * @param {string} email - The email to validate
     195     * @returns {boolean} - Whether the email is valid
     196     */
     197    validateEmail( email ) {
     198        // Using the same logic as WooCommerce:
     199        // wp-content/plugins/woocommerce/assets/js/frontend/checkout.js
     200        const pattern = new RegExp( /^([a-z\d!#$%&'*+\-\/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+(\.[a-z\d!#$%&'*+\-\/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)*|"((([ \t]*\r\n)?[ \t]+)?([\x01-\x08\x0b\x0c\x0e-\x1f\x7f\x21\x23-\x5b\x5d-\x7e\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|\\[\x01-\x09\x0b\x0c\x0d-\x7f\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))*(([ \t]*\r\n)?[ \t]+)?")@(([a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|[a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF][a-z\d\-._~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]*[a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])\.)+([a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|[a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF][a-z\d\-._~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]*[0-9a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])\.?$/i );
     201        return pattern.test( email );
     202    }
     203
     204    /**
     205     * Validate a postcode format via AJAX using WooCommerce's validation
     206     *
     207     * @param {string} postcode - The postcode to validate
     208     * @param {string} fieldName - The field name (to determine if billing or shipping)
     209     * @returns {Promise<boolean>} - Promise resolving to whether the postcode is valid
     210     */
     211    async validatePostcode( postcode, fieldName ) {
     212        // Signal that validation is in progress
     213        this.validationInProgress = true;
     214
     215        // Extract country value based on whether this is billing or shipping
     216        const prefix       = fieldName.startsWith( 'billing_' ) ? 'billing' : 'shipping';
     217        const countryField = document.getElementById( prefix + '_country' );
     218
     219        if ( ! countryField ) {
     220            debugDirect( 'Country field not found for ' + fieldName, debugStatus );
     221            this.validationInProgress = false;
     222            return true; // Default to valid if the country cannot be found
     223        }
     224
     225        const country = countryField.value;
     226
     227        // Generate a cache key using an address type, country and postcode
     228        const cacheKey = prefix + ':' + country + ':' + postcode;
     229
     230        // Check if this postcode has already been validated for this country and address type
     231        if ( this.validatedPostcodesCache.hasOwnProperty( cacheKey ) ) {
     232            debugDirect( 'Using cached validation result for postcode: ' + postcode + ' in country: ' + country + ' for ' + prefix + ' address', debugStatus, 'log' );
     233            this.validationInProgress = false;
     234            return this.validatedPostcodesCache[cacheKey];
     235        }
     236
     237        try {
     238            // Return a promise that resolves with a validation result
     239            return await new Promise(
     240                ( resolve ) => {
     241                    // Create data for AJAX request similar to WooCommerce's update_order_review
     242                    const data = {
     243                        action: 'multisafepay_validate_postcode',
     244                        security: multisafepayParams.nonce,
     245                        postcode: postcode,
     246                        country: country
     247                    };
     248                    debugDirect( 'Validating postcode via AJAX: ' + postcode + ' for country: ' + country + ' for ' + prefix + ' address', debugStatus, 'log' );
     249                    // Send AJAX request to validate postcode
     250                    jQuery.ajax(
     251                    {
     252                        type: 'POST',
     253                        url: multisafepayParams.location,
     254                        data: data,
     255                        success: function ( response ) {
     256                            let isValid = false;
     257                            if ( response.success ) {
     258                                isValid = true;
     259                            }
     260                            // Cache the validation result
     261                            this.validatedPostcodesCache[cacheKey] = isValid;
     262
     263                            debugDirect( 'Postcode validation result via Ajax for ' + prefix + ' address: ' + postcode + ': ' + ( isValid ? 'valid' : 'invalid' ), debugStatus, 'log' );
     264
     265                            // Signal that validation is complete
     266                            this.validationInProgress = false;
     267
     268                            resolve( isValid );
     269                        }.bind( this ),
     270                        error: function () {
     271                            debugDirect( 'Error validating postcode via AJAX', debugStatus );
     272                            this.validationInProgress = false;
     273                            resolve( true ); // Default to valid on error
     274                        }.bind( this )
     275                    }
     276                    );
     277                }
     278            );
     279        } catch ( error ) {
     280            debugDirect( 'Exception validating postcode: ' + error, debugStatus );
     281            this.validationInProgress = false;
     282            return true; // Default to valid on error
     283        }
     284    }
     285
     286    /**
     287     * Validate special fields like email and postcodes
     288     *
     289     * @param {HTMLElement} element - The element to be validated
     290     * @param labelText - The label text for the field
     291     * @returns {Promise<boolean>} - Whether the field is valid or not
     292     */
     293    async validateSpecialFields( element, labelText ) {
     294        const fieldName  = element.name.trim();
     295        const fieldValue = element.value.trim();
     296
     297        // Get the field container element
     298        const getFieldId = document.getElementById( fieldName + '_field' );
     299
     300        let isValid      = true;
     301        let errorMessage = '';
     302
     303        // Check if this is one of the fields we need to specifically validate
     304        if ( fieldName === 'billing_email' ) {
     305            // Validate email format
     306            if ( ! this.validateEmail( fieldValue ) ) {
     307                isValid      = false;
     308                errorMessage = labelText + ' is not a valid address and ';
     309            }
     310        } else if ( fieldName === 'billing_postcode' || fieldName === 'shipping_postcode' ) {
     311            // Validate postcode via AJAX
     312            isValid = await this.validatePostcode( fieldValue, fieldName );
     313            if ( ! isValid ) {
     314                // If labelText is empty, get it from the cache or fall back to default
     315                if ( ! labelText || labelText.trim() === '' ) {
     316                    labelText = this.getLabelText( fieldName );
     317
     318                    // If still empty, fallback to default
     319                    if ( ! labelText || labelText.trim() === '' ) {
     320                        const type = fieldName.startsWith( 'billing_' ) ? 'Billing' : 'Shipping';
     321                        labelText  = type + ' Postcode / ZIP';
     322                    }
     323                }
     324                errorMessage = labelText + ' is not valid for the selected country';
     325            }
     326        }
     327
     328        // Apply validation results
     329        if ( isValid ) {
     330            this.removeErrorMessage( fieldName, true );
     331            this.removeInlineError( fieldName );
     332            element.style.cssText = '';
     333            ['aria-invalid', 'aria-describedby'].forEach( attr => element.removeAttribute( attr ) );
     334            getFieldId.classList.remove( 'woocommerce-invalid', 'woocommerce-invalid-required-field', 'woocommerce-invalid-email' );
     335            getFieldId.classList.add( 'woocommerce-validated' );
     336        } else {
     337            const errorId = fieldName + '_error';
     338            // For the top error banner - use the same message format
     339            this.appendErrorMessage( [{ field: fieldName, label: errorMessage }], true );
     340
     341            // For inline errors, add a period at the end for postcode errors
     342            let inlineErrorMessage = errorMessage;
     343            if ( fieldName === 'billing_postcode' || fieldName === 'shipping_postcode' ) {
     344                inlineErrorMessage = errorMessage + '.';
     345            }
     346
     347            this.addInlineError( fieldName, inlineErrorMessage, errorId );
     348            element.setAttribute( 'aria-invalid', 'true' );
     349            element.setAttribute( 'aria-describedby', errorId );
     350            element.style.cssText = 'box-shadow: inset 2px 0 0 #e2401c !important;';
     351            getFieldId.classList.remove( 'woocommerce-validated' );
     352            getFieldId.classList.add( 'woocommerce-invalid' );
     353            if ( fieldName.includes( 'email' ) ) {
     354                getFieldId.classList.add( 'woocommerce-invalid-email' );
     355            }
     356        }
     357
     358        return isValid;
     359    }
     360
     361    /**
     362     * Add an inline error message below a field
     363     *
     364     * @param {string} fieldName - The name of the field
     365     * @param {string} errorMessage - The error message
     366     * @param {string} errorId - The ID for the error element (for aria-describedby)
     367     * @returns {void}
     368     */
     369    addInlineError( fieldName, errorMessage, errorId ) {
     370        // Get the field element
     371        const fieldElement = document.querySelector(
     372            'input[name="' + fieldName + '"],' +
     373            'select[name="' + fieldName + '"],' +
     374            'textarea[name="' + fieldName + '"]'
     375        );
     376
     377        if ( ! fieldElement ) {
     378            return;
     379        }
     380
     381        // Remove any existing inline error
     382        this.removeInlineError( fieldName );
     383
     384        // Create an inline error message
     385        const errorElement = document.createElement( 'p' );
     386        errorElement.setAttribute( 'id', errorId );
     387        errorElement.className   = 'checkout-inline-error-message';
     388        errorElement.textContent = errorMessage;
     389
     390        // Add to a parent element
     391        const formRow = fieldElement.closest( '.form-row' );
     392        if ( formRow ) {
     393            formRow.appendChild( errorElement );
     394        }
     395    }
     396
     397    /**
     398     * Remove inline error message
     399     *
     400     * @param {string} fieldName - The name of the field
     401     * @returns {void}
     402     */
     403    removeInlineError( fieldName ) {
     404        // Get the field element
     405        const fieldElement = document.querySelector(
     406            'input[name="' + fieldName + '"],' +
     407            'select[name="' + fieldName + '"],' +
     408            'textarea[name="' + fieldName + '"]'
     409        );
     410
     411        if ( ! fieldElement ) {
     412            return;
     413        }
     414
     415        // Remove any existing inline error
     416        const formRow = fieldElement.closest( '.form-row' );
     417        if ( formRow ) {
     418            const errorElement = formRow.querySelector( '.checkout-inline-error-message' );
     419            if ( errorElement ) {
     420                errorElement.remove();
     421            }
     422        }
     423
     424        // Remove aria attributes
     425        ['aria-invalid', 'aria-describedby'].forEach( attr => fieldElement.removeAttribute( attr ) );
    76426    }
    77427
     
    80430     *
    81431     * @param element - The element to be validated
     432     * @returns {void}
    82433     */
    83434    realtimeValidation( element ) {
    84         const fieldName = element.name.trim();
     435        const fieldName  = element.name.trim();
     436        const fieldValue = element.value.trim();
    85437
    86438        // Remove this field from processedFields if it's there
     
    90442        }
    91443
    92         if ( element.value.trim() !== '' ) {
     444        // Get the field container element
     445        const getFieldId = document.getElementById( fieldName + '_field' );
     446        const labelText  = this.getLabelText( fieldName );
     447
     448        // If empty and required, show the required error
     449        if ( fieldValue === '' ) {
     450            if ( ( labelText !== '' ) && getFieldId && getFieldId.classList.contains( 'validate-required' ) ) {
     451                const errorId      = fieldName + '_error';
     452                const errorMessage = labelText + ' is a required field';
     453                this.appendErrorMessage( [{ field: fieldName, label: labelText }], true );
     454                this.addInlineError( fieldName, errorMessage, errorId );
     455                // Using the same error style as WooCommerce
     456                element.style.cssText = 'box-shadow: inset 2px 0 0 #e2401c !important;';
     457                element.setAttribute( 'aria-invalid', 'true' );
     458                element.setAttribute( 'aria-describedby', errorId );
     459                getFieldId.classList.remove( 'woocommerce-validated' );
     460                getFieldId.classList.add( 'woocommerce-invalid', 'woocommerce-invalid-required-field' );
     461            }
     462            return;
     463        }
     464
     465        // For non-empty fields with specific validation needs, validate them
     466        if ( fieldName === 'billing_email' ) {
     467            this.validateSpecialFields( element, labelText ).catch(
     468                error => {
     469                    debugDirect( 'Error validating specific field: ' + error, debugStatus );
     470                }
     471            );
     472        } else {
     473            // For other fields that were filled in, remove any error messages
    93474            this.removeErrorMessage( fieldName, true );
     475            this.removeInlineError( fieldName );
    94476            element.style.cssText = '';
    95         } else {
    96             const labelText = this.getLabelText( fieldName );
    97             // Any field name has an associated ID with the suffix '_field'
    98             const getFieldId = document.getElementById( fieldName + '_field' );
    99             // If the label text is not empty and field is required
    100             if ( (labelText !== '') && getFieldId.classList.contains( 'validate-required' ) ) {
    101                 this.appendErrorMessage( [{ field: fieldName, label: labelText }], true );
    102             }
    103             // Using the same error style as WooCommerce
    104             element.style.cssText = 'box-shadow: inset 2px 0 0 #e2401c !important;';
    105         }
    106     }
    107 
    108     /**
    109      * Validate all the fields with the class 'validate-required'
    110      * inside the element with the id 'customer_details'
    111      *
    112      * @returns {boolean}
    113      */
    114     checkFields() {
    115         // Clear the processedFields array at the start of validation
    116         this.processedFields.length = 0;
    117 
     477            getFieldId.classList.remove( 'woocommerce-invalid', 'woocommerce-invalid-required-field' );
     478            getFieldId.classList.add( 'woocommerce-validated' );
     479        }
     480    }
     481
     482    /**
     483     * Setup validation listeners for postcode fields
     484     *
     485     * @param element - The element to setup listeners for
     486     * @returns {void}
     487     */
     488    setupPostcodeValidation( element ) {
     489        const fieldName = element.name.trim();
     490
     491        // Get the label text for the field from our cache
     492        const labelText = this.getLabelText( fieldName );
     493
     494        // Setup change event handler for postcodes
     495        element._changeHandler = () => {
     496            debugDirect( 'Validating postcode on change: ' + fieldName, debugStatus, 'log' );
     497            this.validateSpecialFields( element, labelText )
     498                .catch(
     499                    error => {
     500                        debugDirect( 'Error validating postcode: ' + error, debugStatus, 'error' );
     501                    }
     502                );
     503        };
     504        element.addEventListener( 'change', element._changeHandler );
     505
     506        // Remove any previous focusout handler
     507        if ( element._focusoutHandler ) {
     508            element.removeEventListener( 'focusout', element._focusoutHandler );
     509            element._focusoutHandler = null;
     510        }
     511    }
     512
     513    /**
     514     * Add event listeners to all required fields
     515     *
     516     * @returns {void}
     517     */
     518    setupValidationListeners() {
    118519        // Getting the customer details element which includes all the user fields
    119520        const customerDetails = document.getElementById( 'customer_details' );
    120         // Getting all the fields with the class 'validate-required' so we can loop through them
     521        if ( ! customerDetails ) {
     522            return;
     523        }
     524
     525        // Getting all the fields with the class 'validate-required'
    121526        const selectWrappers = customerDetails.querySelectorAll( '.validate-required' );
    122         // Check if the element is visible
    123         const isElementVisible = element => element && element.offsetParent !== null;
    124         // Are all fields valid? For now, yes
    125         let allFieldsValid = true;
    126 
    127         // Loop through all the fields with the class 'validate-required'
     527
    128528        selectWrappers.forEach(
    129529            wrapper => {
    130                 // Getting all the fields inside the wrapper including input, text-areas and selects
    131                 const elements = wrapper.querySelectorAll( 'input, textarea, select' );
    132                 // Loop through all the fields inside the wrapper
     530                // Getting all the fields inside the wrapper
     531                const elements          = wrapper.querySelectorAll( 'input, textarea, select' );
    133532                elements.forEach(
    134533                    element => {
    135                         // Remove existing listener and add a new one
    136                         element.removeEventListener( 'input', event => this.realtimeValidation( event.target ) );
    137                         element.addEventListener( 'input', event => this.realtimeValidation( event.target ) );
    138534                        const fieldName = element.name.trim();
    139                         // Initial validation logic: if the field is empty and visible
    140                         if ( ! element.value.trim() && ( isElementVisible( element ) || element.type === 'hidden' ) ) {
    141                             const labelText = this.getLabelText( fieldName );
    142                             if ( labelText !== '' ) {
    143                                 this.appendErrorMessage( [{ field: fieldName, label: labelText }] );
    144                             }
    145                             // Not all fields are valid
    146                             allFieldsValid = false;
    147                             // Add a different error style to the field if it's a Select2 or input,
    148                             // using the same error style as WooCommerce
    149                             const select2Container = element.tagName.toLowerCase() === 'select' ? wrapper.querySelector( '.select2-selection' ) : null;
    150                             if ( select2Container ) {
    151                                 select2Container.style.cssText = 'border: 2px solid #e2401c !important;';
    152                             } else {
    153                                 element.style.cssText = 'box-shadow: inset 2px 0 0 #e2401c !important;';
    154                             }
     535                        // Remove any existing listeners first
     536                        element.removeEventListener( 'input', element._inputHandler );
     537                        element.removeEventListener( 'change', element._changeHandler );
     538                        element.removeEventListener( 'focusout', element._focusoutHandler );
     539                        // For postcodes, use only change event instead of input
     540                        if ( fieldName === 'billing_postcode' || fieldName === 'shipping_postcode' ) {
     541                            this.setupPostcodeValidation( element );
    155542                        } else {
    156                             this.removeErrorMessage( fieldName );
    157                             element.style.cssText = '';
     543                            // For all other fields, use the input event for real-time validation
     544                            element._inputHandler = event => this.realtimeValidation( event.target );
     545                            element.addEventListener( 'input', element._inputHandler );
     546
     547                            // Add focusout handler for all fields
     548                            element._focusoutHandler = event => this.realtimeValidation( event.target );
     549                            element.addEventListener( 'focusout', element._focusoutHandler );
    158550                        }
    159551                    }
     
    161553            }
    162554        );
     555    }
     556
     557    /**
     558     * Validate a specific postcode field
     559     *
     560     * @param {HTMLElement} postcodeElement - The postcode input element
     561     * @param {string} labelText - The label text for the field
     562     * @returns {Promise<boolean>} - Promise resolving to whether validation pass
     563     */
     564    async sharedPostcodeValidation( postcodeElement, labelText ) {
     565        const fieldName = postcodeElement.name.trim();
     566
     567        // Start validation
     568        this.validationInProgress = true;
     569
     570        // Validate the field
     571        const isValid = await this.validateSpecialFields( postcodeElement, labelText );
     572
     573        // Update field styling if invalid
     574        if ( ! isValid ) {
     575            const postcodeField = document.getElementById( fieldName + '_field' );
     576            if ( postcodeField ) {
     577                postcodeField.classList.remove( 'woocommerce-validated' );
     578                postcodeField.classList.add( 'woocommerce-invalid' );
     579            }
     580        }
     581
     582        return isValid;
     583    }
     584
     585    /**
     586     * Validate all the fields with the class 'validate-required'
     587     * inside the element with the id 'customer_details'
     588     *
     589     * @returns {Promise<boolean>}
     590     */
     591    async checkFields() {
     592        // Early check - if validation is in progress, block payment
     593        if ( this.validationInProgress ) {
     594            debugDirect( 'Blocking payment: Validation in progress', debugStatus, 'log' );
     595            return false;
     596        }
     597
     598        // Clear any existing errors from previous validations
     599        this.clearAllErrors();
     600
     601        // Clear the processedFields array at the start of validation
     602        this.processedFields.length = 0;
     603
     604        // Getting the customer details element which includes all the user fields
     605        const customerDetails = document.getElementById( 'customer_details' );
     606        if ( ! customerDetails ) {
     607            debugDirect( 'Customer details element not found', debugStatus, 'error' );
     608            return false;
     609        }
     610
     611        // Are all fields valid? For now, yes
     612        let allFieldsValid = true;
     613
     614        // STEP 1: Check all empty required fields first
     615        // Getting all the fields with the class 'validate-required'
     616        const requiredFields = customerDetails.querySelectorAll( '.validate-required' );
     617        debugDirect( 'Found ' + requiredFields.length + ' required fields', debugStatus, 'log' );
     618
     619        // First pass: Validate empty required fields
     620        requiredFields.forEach(
     621            wrapper => {
     622                // Getting all the input elements inside the wrapper
     623                const elements      = wrapper.querySelectorAll( 'input, textarea, select' );
     624                elements.forEach(
     625                element => {
     626                    const fieldName = element.name.trim();
     627                    if ( ! fieldName ) {
     628                        return; // Skip elements without a name
     629                    }
     630                    // If the field is visible (or hidden) and is empty
     631                    if ( this.isElementVisible( element ) || element.type === 'hidden' ) {
     632                        if ( ! element.value.trim()) {
     633                            const labelText = this.getLabelText( fieldName );
     634                            if ( labelText ) {
     635                                const errorId      = fieldName + '_error';
     636                                const errorMessage = labelText + ' is a required field';
     637
     638                                // Add an error message to the top of the page
     639                                this.appendErrorMessage( [{ field: fieldName, label: labelText }], true );
     640
     641                                // Add inline error
     642                                this.addInlineError( fieldName, errorMessage, errorId );
     643
     644                                // Invalid field styling
     645                                element.setAttribute( 'aria-invalid', 'true' );
     646                                element.setAttribute( 'aria-describedby', errorId );
     647
     648                                // Add different error style depending on an element type
     649                                const select2Container = element.tagName.toLowerCase() === 'select' ?
     650                                wrapper.querySelector( '.select2-selection' ) : null;
     651
     652                                if ( select2Container ) {
     653                                    select2Container.style.cssText = 'border: 2px solid #e2401c !important;';
     654                                } else {
     655                                    element.style.cssText = 'box-shadow: inset 2px 0 0 #e2401c !important;';
     656                                }
     657
     658                                // Update wrapper classes
     659                                wrapper.classList.remove( 'woocommerce-validated' );
     660                                wrapper.classList.add( 'woocommerce-invalid', 'woocommerce-invalid-required-field' );
     661
     662                                // Not all fields are valid
     663                                allFieldsValid = false;
     664
     665                                debugDirect( 'Field is required but empty: ' + fieldName, debugStatus, 'log' );
     666                            }
     667                        }
     668                    }
     669                }
     670                );
     671            }
     672        );
     673
     674        // STEP 2: Special validation for email and postcode
     675        // Setup validation listeners
     676        this.setupValidationListeners();
     677
     678        // Explicitly check special fields (email and postcodes)
     679        const billingEmail     = document.querySelector( '[name="billing_email"]' );
     680        const billingPostcode  = document.querySelector( '[name="billing_postcode"]' );
     681        const shippingPostcode = document.querySelector( '[name="shipping_postcode"]' );
     682
     683        // Flag to track if we need to wait for validations
     684        let pendingValidation = false;
     685
     686        // Array to track validation promises
     687        const validationPromises = [];
     688
     689        // Validate billing_email
     690        if ( billingEmail && this.isElementVisible( billingEmail ) && billingEmail.value.trim() !== '' ) {
     691            const labelText = this.getLabelText( 'billing_email' );
     692
     693            // Check the email format synchronously
     694            if ( ! this.validateEmail( billingEmail.value.trim() )) {
     695                allFieldsValid          = false;
     696                const billingEmailField = document.getElementById( 'billing_email_field' );
     697                const errorId           = 'billing_email_error';
     698                const errorMessage      = labelText + ' is not a valid email address';
     699
     700                // Add an error message to the top of the page
     701                this.appendErrorMessage( [{ field: 'billing_email', label: labelText + ' is not a valid email address and ' }], true );
     702
     703                // Add inline error
     704                this.addInlineError( 'billing_email', errorMessage, errorId );
     705                billingEmail.setAttribute( 'aria-invalid', 'true' );
     706                billingEmail.setAttribute( 'aria-describedby', errorId );
     707                billingEmail.style.cssText = 'box-shadow: inset 2px 0 0 #e2401c !important;';
     708
     709                if ( billingEmailField ) {
     710                    billingEmailField.classList.remove( 'woocommerce-validated' );
     711                    billingEmailField.classList.add( 'woocommerce-invalid', 'woocommerce-invalid-email' );
     712                }
     713            } else {
     714                // Email is valid, remove any error messages
     715                const billingEmailField = document.getElementById( 'billing_email_field' );
     716                this.removeErrorMessage( 'billing_email', true );
     717                this.removeInlineError( 'billing_email' );
     718                billingEmail.style.cssText = '';
     719                ['aria-invalid', 'aria-describedby'].forEach( attr => billingEmail.removeAttribute( attr ) );
     720
     721                if ( billingEmailField ) {
     722                    billingEmailField.classList.remove( 'woocommerce-invalid', 'woocommerce-invalid-required-field', 'woocommerce-invalid-email' );
     723                    billingEmailField.classList.add( 'woocommerce-validated' );
     724                }
     725            }
     726        }
     727
     728        // Check billing postcode
     729        if ( billingPostcode && this.isElementVisible( billingPostcode ) && billingPostcode.value.trim() !== '' ) {
     730            pendingValidation    = true;
     731            const labelText      = this.getLabelText( 'billing_postcode' );
     732            const billingPromise = this.sharedPostcodeValidation( billingPostcode, labelText )
     733                .then(
     734                    isValid => {
     735                        if ( ! isValid ) {
     736                            allFieldsValid = false;
     737                        }
     738                        return isValid;
     739                    }
     740                );
     741            validationPromises.push( billingPromise );
     742        }
     743
     744        // Check shipping postcode
     745        if ( shippingPostcode && this.isElementVisible( shippingPostcode ) && shippingPostcode.value.trim() !== '' ) {
     746            pendingValidation     = true;
     747            const labelText       = this.getLabelText( 'shipping_postcode' );
     748            const shippingPromise = this.sharedPostcodeValidation( shippingPostcode, labelText )
     749                .then(
     750                    isValid => {
     751                        if ( ! isValid ) {
     752                            allFieldsValid = false;
     753                        }
     754                        return isValid;
     755                    }
     756                );
     757            validationPromises.push( shippingPromise );
     758        }
     759
     760        // If there are pending validations, wait for them to complete
     761        if ( pendingValidation ) {
     762            debugDirect(
     763                'Waiting for ' + validationPromises.length + ' pending validation' +
     764                ( validationPromises.length === 1 ? '' : 's' ) + ' to complete',
     765                debugStatus,
     766                'log'
     767            );
     768
     769            try {
     770                await Promise.all( validationPromises );
     771                debugDirect( 'All validations for special fields were completed', debugStatus, 'log' );
     772            } catch ( error ) {
     773                debugDirect( 'Error during special fields validation: ' + error, debugStatus, 'error' );
     774                allFieldsValid = false;
     775            } finally {
     776                this.validationInProgress = false;
     777            }
     778        }
     779
     780        // Final check - ensure no validation is in progress
     781        if ( this.validationInProgress ) {
     782            debugDirect( 'Blocking payment: Validation still in progress at the end of checks', debugStatus, 'log' );
     783            return false;
     784        }
    163785
    164786        // Scroll to the previously added notice group area if there are any errors
     
    166788            this.scrollToElement( '.entry-content' );
    167789        }
     790
     791        debugDirect( 'Final validation result: ' + ( allFieldsValid ? 'Valid' : 'Invalid' ), debugStatus, 'log' );
    168792        return allFieldsValid;
    169793    }
    170794
    171795    /**
     796     * Clear all error messages and validation states
     797     *
     798     * @returns {void}
     799     */
     800    clearAllErrors() {
     801        // Remove the entire notice group
     802        const noticeGroup = document.querySelector( '.woocommerce-NoticeGroup-checkout' );
     803        if ( noticeGroup ) {
     804            noticeGroup.remove();
     805            debugDirect( 'Cleared all error messages', debugStatus, 'log' );
     806        }
     807
     808        // Remove all inline errors
     809        const inlineErrors = document.querySelectorAll( '.checkout-inline-error-message' );
     810        inlineErrors.forEach( error => error.remove() );
     811    }
     812
     813    /**
    172814     * Scroll to a specific class element in the document.
    173815     *
    174816     * @param {string} className - The class name of the element to scroll to.
     817     * @returns {void}
    175818     */
    176819    scrollToElement( className ) {
     
    204847        }
    205848
     849        // Make sure we have a valid error object
     850        if ( ! error || ! error[0] || ! error[0].field ) {
     851            debugDirect( 'Invalid error object passed to appendErrorMessage', debugStatus );
     852            return;
     853        }
     854
    206855        // Create the notice group using the WooCommerce style, where the errors will be built using JS vanilla,
    207         // so PHPCS doesn't complain if HTML code is used because the opening and closing of tags <>
     856        // so PHPCS doesn't complain if HTML code is used because of the opening and closing of tags <>
    208857        let noticeGroup = document.querySelector( '.woocommerce-NoticeGroup-checkout' );
    209858        if ( ! noticeGroup ) {
     
    215864            noticeBanner.className = 'woocommerce-error';
    216865            noticeBanner.setAttribute( 'role', 'alert' );
    217 
    218             const contentDiv     = document.createElement( 'div' );
    219             contentDiv.className = 'wc-block-components-notice-banner__content';
    220 
    221             const summaryParagraph       = document.createElement( 'p' );
    222             summaryParagraph.className   = 'wc-block-components-notice-banner__summary';
    223             summaryParagraph.textContent = 'The following problems were found:';
    224             contentDiv.appendChild( summaryParagraph );
    225 
    226             const ulElement = document.createElement( 'ul' );
    227             contentDiv.appendChild( ulElement );
    228 
    229             noticeBanner.appendChild( contentDiv );
    230866            noticeGroup.appendChild( noticeBanner );
    231867        }
    232868
    233869        const ulList = noticeGroup.querySelector( 'ul' );
    234 
    235         // Check if an error message for this field already exists, therefore, is not added again
     870        if ( ! ulList ) {
     871            debugDirect( 'Error banner not found', debugStatus );
     872            return;
     873        }
     874
     875        // Check if an error message for this field already exists
    236876        const existingErrorItem = ulList.querySelector( 'li[data-id="' + error[0].field + '"]' );
    237877        if ( existingErrorItem ) {
    238             return;
     878            // If we're in real-time validation, update the existing error message
     879            if ( isRealtimeValidation ) {
     880                debugDirect( 'Updating existing error message for field: ' + error[0].field, debugStatus, 'log' );
     881                // Remove the existing error so we can add an updated one
     882                existingErrorItem.remove();
     883            } else {
     884                // If not in real-time validation, just leave the existing error
     885                return;
     886            }
    239887        }
    240888
     
    242890        const errorItem = document.createElement( 'li' );
    243891        errorItem.setAttribute( 'data-id', error[0].field );
    244 
     892        errorItem.style.display = 'block'; // Ensure it's visible
     893
     894        // Create the complete error message with formatting
     895        const errorLink                = document.createElement( 'a' );
     896        errorLink.href                 = '#' + error[0].field;
     897        errorLink.style.textDecoration = 'none'; // Don't show underline on the text
     898
     899        // Create a strong element for the field name/error
    245900        const strongElement = document.createElement( 'strong' );
    246         // Add the label text to the error message
    247         strongElement.textContent = error[0].label;
    248 
    249         errorItem.appendChild( strongElement );
    250         errorItem.append( ' is a required field.' );
     901
     902        // Set the text for the strong element based on the error
     903        let errorText = error[0].label;
     904        // Make sure we end with "and" if the error is about invalid format
     905        if ( errorText.includes( 'is not valid' ) && ! errorText.endsWith( ' and' ) ) {
     906            errorText += ' and';
     907        }
     908        strongElement.textContent = errorText;
     909
     910        // Determine the complete error message text
     911        let fullErrorText;
     912        if ( errorText.includes( 'is not valid' ) || errorText.includes( 'is not a valid' ) ) {
     913            fullErrorText = ' is a required field.';
     914        } else {
     915            fullErrorText = ' is a required field.';
     916        }
     917
     918        // Add the complete message to the link
     919        errorLink.appendChild( strongElement );
     920        errorLink.appendChild( document.createTextNode( fullErrorText ) );
     921
     922        // Add the link containing the full message to the error item
     923        errorItem.appendChild( errorLink );
     924
    251925        // Add the error message to the notice group
    252926        ulList.appendChild( errorItem );
     927
     928        // Add a click handler to focus the field when the error is clicked
     929        errorLink.addEventListener(
     930            'click',
     931            ( event ) => {
     932                event.preventDefault();
     933                const fieldToFocus = document.querySelector( '[name="' + error[0].field + '"]' );
     934                if ( fieldToFocus ) {
     935                    fieldToFocus.focus();
     936                }
     937            }
     938        );
     939
     940        // Make sure the notice group is visible and properly positioned
     941        noticeGroup.style.display = 'block';
     942        noticeGroup.style.margin  = '0 0 2em';
    253943
    254944        debugDirect( ( isRealtimeValidation ? '"Interactively" a' : 'A' ) + 'dding error message in the notice group for field: ' + error[0].field, debugStatus, 'log' );
     
    287977                // Getting the field name associated with the error message
    288978                const fieldElement = document.querySelector( '[name="' + fieldName + '"]' );
    289                 // Check if the fieldElement exists and is not hidden using CSS
    290                 const isFieldVisible = fieldElement && ( window.getComputedStyle( fieldElement ).display !== 'none' );
     979                // Check if the fieldElement exists and is not hidden
     980                const isFieldVisible = fieldElement && this.isElementVisible( fieldElement );
    291981                if ( ! isFieldVisible ) {
    292982                    errorItem.remove();
     
    307997    removeEntireNoticeGroup() {
    308998        // Check if there are no more error messages left
    309         const errorList = document.querySelector( '.woocommerce-NoticeGroup-checkout ul' );
    310         if ( errorList && ( errorList.children.length === 0 ) ) {
    311             const noticeGroup = document.querySelector( '.woocommerce-NoticeGroup-checkout' );
    312             if ( noticeGroup ) {
     999        const noticeGroup = document.querySelector( '.woocommerce-NoticeGroup-checkout' );
     1000        if ( noticeGroup ) {
     1001            // Count elements with data-id attribute within the group
     1002            const errorItems = noticeGroup.querySelectorAll( 'li[data-id]' );
     1003            if ( errorItems.length === 0 ) {
    3131004                noticeGroup.remove();
    3141005                debugDirect( 'Removing the entire notice group as there are no more error messages left', debugStatus, 'log' );
  • multisafepay/tags/6.9.0/multisafepay.php

    r3269746 r3306839  
    55 * Plugin URI:              https://docs.multisafepay.com/docs/woocommerce
    66 * Description:             MultiSafepay Payment Plugin
    7  * Version:                 6.8.3
     7 * Version:                 6.9.0
    88 * Author:                  MultiSafepay
    99 * Author URI:              https://www.multisafepay.com
     
    1212 * License URI:             http://www.gnu.org/licenses/gpl-3.0.html
    1313 * Requires at least:       6.0
    14  * Tested up to:            6.7.2
     14 * Tested up to:            6.8.1
    1515 * WC requires at least:    6.0.0
    16  * WC tested up to:         9.7.1
     16 * WC tested up to:         9.8.5
    1717 * Requires PHP:            7.3
    1818 * Text Domain:             multisafepay
     
    2727 * Plugin version
    2828 */
    29 define( 'MULTISAFEPAY_PLUGIN_VERSION', '6.8.3' );
     29define( 'MULTISAFEPAY_PLUGIN_VERSION', '6.9.0' );
    3030
    3131/**
  • multisafepay/tags/6.9.0/readme.txt

    r3269746 r3306839  
    33Tags: multisafepay, payment gateway, credit cards, ideal, bnpl
    44Requires at least: 6.0
    5 Tested up to: 6.7.2
     5Tested up to: 6.8.1
    66Requires PHP: 7.3
    7 Stable tag: 6.8.3
     7Stable tag: 6.9.0
    88License: MIT
    99
     
    139139
    140140== Changelog ==
     141= Release Notes - WooCommerce 6.9.0 (Jun 5th, 2025) =
     142
     143### Added
     144+ PLGWOOS-1003: Add zip code and email format validation to QR code implementation
     145+ PLGWOOS-1001: Improvement over QR code implementation validating checkout when"ship to a different address" is checked.
     146
     147### Fixed
     148+ PLGWOOS-1009: Fix Bancontact QR showing up even when it hasn’t been explicitly enabled
     149+ PLGWOOS-1007: Refund request based on amount is including the checkout data, even when it's not needed.
     150+ PLGWOOS-997: Validate zip code checkout fields to prevent payments via Wallets
     151+ PLGWOOS-1002: Payment Component shows "Store my details for future visits" field, when user is not logged in
     152+ PLGWOOS-999: Fix URL Parameter Concatenation in QR Payment Redirect Flow
     153+ PLGWOOS-1000: Remove unneeded get_customer_ip_address() and get_user_agent() methods in QrCustomerService
     154
     155### Changed
     156+ PLGWOOS-1005: Adjusting the minimum discrepancy allowed to filter Billink tax rates
     157+ PLGWOOS-1004: Adding shipping method name in the Order Request, instead of generic label "Shipping"
     158
    141159= Release Notes - WooCommerce 6.8.3 (Apr 9th, 2025) =
    142160
  • multisafepay/tags/6.9.0/src/Main.php

    r3264984 r3306839  
    88use MultiSafepay\WooCommerce\Services\Qr\QrPaymentComponentService;
    99use MultiSafepay\WooCommerce\Services\Qr\QrPaymentWebhook;
     10use MultiSafepay\WooCommerce\Services\ValidationService;
    1011use MultiSafepay\WooCommerce\Settings\SettingsController;
    1112use MultiSafepay\WooCommerce\Settings\ThirdPartyCompatibility;
     
    4445        $this->payment_components_qr_hooks();
    4546        $this->callback_hooks();
     47        $this->validation_hooks();
    4648    }
    4749
     
    210212
    211213    /**
     214     * Register the hooks related to field validation
     215     *
     216     * @return void
     217     */
     218    public function validation_hooks(): void {
     219        $validation_service = new ValidationService();
     220        // Register AJAX action for zip code validation
     221        $this->loader->add_action( 'wp_ajax_multisafepay_validate_postcode', $validation_service, 'validate_postcode' );
     222        $this->loader->add_action( 'wp_ajax_nopriv_multisafepay_validate_postcode', $validation_service, 'validate_postcode' );
     223    }
     224
     225    /**
    212226     * Run the loader to execute the hooks with WordPress.
    213227     *
  • multisafepay/tags/6.9.0/src/PaymentMethods/Base/BasePaymentMethod.php

    r3264984 r3306839  
    400400        }
    401401
    402         $settings = get_option( 'woocommerce_' . $this->id . '_settings', array( 'payment_component' => 'qr' ) );
     402        $settings = get_option( 'woocommerce_' . $this->id . '_settings', array( 'payment_component' => 'yes' ) );
    403403        if ( ! isset( $settings['payment_component'] ) ) {
    404             return true;
     404            return false;
    405405        }
    406406        return 'qr' === $settings['payment_component'];
     
    417417        }
    418418
    419         $settings = get_option( 'woocommerce_' . $this->id . '_settings', array( 'payment_component' => 'qr_only' ) );
     419        $settings = get_option( 'woocommerce_' . $this->id . '_settings', array( 'payment_component' => 'yes' ) );
    420420        if ( ! isset( $settings['payment_component'] ) ) {
    421             return true;
     421            return false;
    422422        }
    423423        return 'qr_only' === $settings['payment_component'];
     
    501501                wp_enqueue_script( 'multisafepay-validator-wallets', MULTISAFEPAY_PLUGIN_URL . '/assets/public/js/multisafepay-validator-wallets.js', array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true );
    502502                wp_enqueue_script( 'multisafepay-common-wallets', MULTISAFEPAY_PLUGIN_URL . '/assets/public/js/multisafepay-common-wallets.js', array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true );
     503                // Add parameters for validator wallets
     504                wp_localize_script(
     505                    'multisafepay-validator-wallets',
     506                    'multisafepayParams',
     507                    array(
     508                        'location' => admin_url( 'admin-ajax.php' ),
     509                        'nonce'    => wp_create_nonce( 'multisafepay_validator_nonce' ),
     510                    )
     511                );
     512
    503513                $admin_url_array = array(
    504514                    'location' => admin_url( 'admin-ajax.php' ),
  • multisafepay/tags/6.9.0/src/PaymentMethods/Base/BaseRefunds.php

    r3264984 r3306839  
    5252        );
    5353
    54         /** @var RefundRequest $refund_request */
    55         $refund_request = $transaction_manager->createRefundRequest( $multisafepay_transaction );
     54        if ( $multisafepay_transaction->requiresShoppingCart() ) {
     55            /** @var RefundRequest $refund_request */
     56            $refund_request = $transaction_manager->createRefundRequest( $multisafepay_transaction );
    5657
    57         $refund_request->addDescriptionText( $reason );
    58 
    59         if ( $multisafepay_transaction->requiresShoppingCart() ) {
    6058            $refunds                 = $order->get_refunds();
    6159            $refund_merchant_item_id = reset( $refunds )->id;
     
    7270
    7371        if ( ! $multisafepay_transaction->requiresShoppingCart() ) {
     72            $refund_request = new RefundRequest();
     73            $refund_request->addDescriptionText( $reason );
    7474            $refund_request->addMoney( MoneyUtil::create_money( (float) $amount, $order->get_currency() ) );
    7575        }
     
    8080        } catch ( Exception | ClientExceptionInterface | ApiException $exception ) {
    8181            $error = __( 'Error:', 'multisafepay' ) . htmlspecialchars( $exception->getMessage() );
    82             $this->logger->log_error( $error );
    83             wc_add_notice( $error, 'error' );
     82            $this->logger->log_error( 'Error during refund: ' . $error . ' Refund request : ' . wp_json_encode( $refund_request->getData() ) );
    8483        }
    8584
  • multisafepay/tags/6.9.0/src/Services/PaymentComponentService.php

    r3264984 r3306839  
    8383
    8484        // Tokenization and recurring model
    85         if ( $woocommerce_payment_gateway->is_tokenization_enabled() ) {
     85        if ( $woocommerce_payment_gateway->is_tokenization_enabled() && is_user_logged_in() ) {
    8686            $payment_component_arguments['recurring'] = array(
    8787                'model'  => 'cardOnFile',
  • multisafepay/tags/6.9.0/src/Services/Qr/QrCustomerService.php

    r3264984 r3306839  
    4848        );
    4949    }
    50 
    51     /**
    52      * Get the customer IP address.
    53      *
    54      * @return string
    55      */
    56     public function get_customer_ip_address(): string {
    57         $possible_ip_sources = array(
    58             'HTTP_CLIENT_IP',
    59             'HTTP_X_FORWARDED_FOR',
    60             'REMOTE_ADDR',
    61         );
    62 
    63         foreach ( $possible_ip_sources as $source ) {
    64             if ( ! empty( $_SERVER[ $source ] ) ) {
    65                 return sanitize_text_field( wp_unslash( $_SERVER[ $source ] ) );
    66             }
    67         }
    68 
    69         return '';
    70     }
    71 
    72     /**
    73      * Get the user agent.
    74      *
    75      * @return string
    76      */
    77     public function get_user_agent(): string {
    78         return sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) );
    79     }
    8050}
  • multisafepay/tags/6.9.0/src/Services/Qr/QrOrderService.php

    r3269746 r3306839  
    122122     */
    123123    public function create_payment_options( string $token ): PaymentOptions {
    124         $redirect_cancel_url = get_rest_url( get_current_blog_id(), 'multisafepay/v1/qr-balancer' ) . '?token=' . rawurlencode( $token );
     124        $redirect_cancel_url = add_query_arg( 'token', $token, get_rest_url( get_current_blog_id(), 'multisafepay/v1/qr-balancer' ) );
    125125        $payment_options     = new PaymentOptions();
    126126        $payment_options->addNotificationUrl( get_rest_url( get_current_blog_id(), 'multisafepay/v1/qr-notification' ) );
  • multisafepay/tags/6.9.0/src/Services/ShoppingCartService.php

    r3264984 r3306839  
    66use MultiSafepay\Api\Transactions\OrderRequest\Arguments\ShoppingCart\Item as CartItem;
    77use MultiSafepay\Api\Transactions\OrderRequest\Arguments\ShoppingCart\ShippingItem;
     8use MultiSafepay\Exception\InvalidArgumentException;
    89use MultiSafepay\WooCommerce\Utils\Hpos;
    910use MultiSafepay\WooCommerce\Utils\Logger;
     
    4142     * @param string|null $gateway_code
    4243     * @return ShoppingCart
     44     * @throws InvalidArgumentException
    4345     */
    4446    public function create_shopping_cart( WC_Order $order, string $currency, ?string $gateway_code = '' ): ShoppingCart {
     
    162164     * @param string                 $gateway_code
    163165     * @return ShippingItem
     166     * @throws InvalidArgumentException
    164167     */
    165168    private function create_shipping_cart_item( WC_Order_Item_Shipping $item, string $currency, string $gateway_code ): ShippingItem {
    166         $cart_item = new ShippingItem();
    167         return $cart_item->addName( __( 'Shipping', 'multisafepay' ) )
     169        $cart_item            = new ShippingItem();
     170        $shipping_method_name = $item->get_method_title();
     171        if ( empty( $shipping_method_name ) ) {
     172            $shipping_method_name = __( 'Shipping', 'multisafepay' );
     173        }
     174        return $cart_item->addName( $shipping_method_name )
    168175            ->addQuantity( 1 )
    169176            ->addUnitPrice( MoneyUtil::create_money( (float) $item->get_total(), $currency ) )
     
    286293
    287294        foreach ( $allowed_rates as $rate ) {
    288             if ( abs( $tax_rate - $rate ) <= 0.15 ) {
     295            if ( abs( $tax_rate - $rate ) <= 0.05 ) {
    289296                return round( $tax_rate );
    290297            }
  • multisafepay/tags/6.9.0/src/Utils/QrCheckoutManager.php

    r3264984 r3306839  
    55use WC_Cart;
    66use WC_Shipping_Rate;
     7use WC_Validation;
    78
    89/**
     
    3536
    3637    /**
    37      * Check if all mandatory fields are filled in the checkout in order to submit a MultiSafepay transaction
     38     * Check if all mandatory fields are filled in the checkout to submit a MultiSafepay transaction
    3839     * using Payment Component with QR code.
    3940     *
     
    5152
    5253        // Get required and extra fields
    53         $required_fields = $this->get_required_fields();
    54         $extra_fields    = $this->get_extra_fields();
     54        $billing_required_fields = $this->get_required_fields();
     55        $billing_extra_fields    = $this->get_extra_fields();
    5556
    5657        // Determine if shipping to a different address
     
    5859
    5960        // Get shipping fields if necessary
    60         $shipping_fields = $ship_to_different_address ?
    61             $this->get_shipping_fields( $required_fields, $extra_fields ) :
    62             array();
     61        $shipping_fields          = array();
     62        $shipping_required_fields = array();
     63
     64        if ( $ship_to_different_address ) {
     65            $shipping_fields          = $this->get_shipping_fields( $billing_required_fields, $billing_extra_fields );
     66            $shipping_required_fields = $this->get_shipping_fields( $billing_required_fields, array() );
     67        }
    6368
    6469        // Combine all fields
    65         $all_fields = array_merge( $required_fields, $extra_fields, $shipping_fields );
     70        $all_fields = array_merge( $billing_required_fields, $billing_extra_fields, $shipping_fields );
     71
     72        // Combine all required fields
     73        $all_required_fields = array_merge( $billing_required_fields, $shipping_required_fields );
    6674
    6775        // Get order fields
     
    6977
    7078        // Process and validate fields
    71         $this->process_checkout_data( $all_fields, $required_fields, $order_fields );
     79        $this->process_checkout_data( $all_fields, $all_required_fields, $order_fields );
    7280
    7381        return $this->is_validated;
     
    353361     * Get the shipping fields based on required and extra fields.
    354362     *
    355      * @param array $required_fields The required fields.
    356      * @param array $extra_fields The extra fields.
    357      * @return array
    358      */
    359     public function get_shipping_fields( array $required_fields, array $extra_fields ): array {
     363     * @param array $billing_required_fields The required fields.
     364     * @param array $billing_extra_fields The extra fields.
     365     * @return array
     366     */
     367    public function get_shipping_fields( array $billing_required_fields, array $billing_extra_fields ): array {
    360368        return array_map(
    361369            static function( $field ) {
     
    363371            },
    364372            array_filter(
    365                 array_merge( $required_fields, $extra_fields ),
     373                array_merge( $billing_required_fields, $billing_extra_fields ),
    366374                static function( $field ) {
    367375                    // Exclude email and phone fields to be created as shipping fields.
     
    376384     *
    377385     * @param array $all_fields All fields to check.
    378      * @param array $required_fields The required fields.
     386     * @param array $all_required_fields The required fields.
    379387     * @param array $order_fields The order fields.
    380388     */
    381     public function process_checkout_data( array $all_fields, array $required_fields, array $order_fields ): void {
     389    public function process_checkout_data( array $all_fields, array $all_required_fields, array $order_fields ): void {
    382390        $this->is_validated = true;
    383391
     
    386394            if ( 'billing_email' === $field ) {
    387395                $field_value = isset( $this->posted_data[ $field ] ) ? sanitize_email( wp_unslash( $this->posted_data[ $field ] ) ) : '';
     396
     397                // Verify the email format using PHP's built-in filter validation
     398                if ( ! empty( $field_value ) && ! $this->validate_email( $field_value ) ) {
     399                    $this->is_validated = false;
     400                }
     401            } elseif ( strpos( $field, '_postcode' ) !== false ) {
     402                $field_value = isset( $this->posted_data[ $field ] ) ? wp_unslash( $this->posted_data[ $field ] ) : '';
     403                $field_value = trim( wp_strip_all_tags( $field_value ) );
     404
     405                // Validate a postcode format if not empty
     406                if ( ! empty( $field_value ) ) {
     407                    $prefix  = strpos( $field, 'billing_' ) === 0 ? 'billing' : 'shipping';
     408                    $country = isset( $this->posted_data[ $prefix . '_country' ] ) ? wp_unslash( $this->posted_data[ $prefix . '_country' ] ) : '';
     409                    $country = trim( wp_strip_all_tags( $country ) );
     410
     411                    if ( ! $this->validate_postcode( $field_value, $country ) ) {
     412                        $this->is_validated = false;
     413                    }
     414                }
    388415            } else {
    389416                $field_value = isset( $this->posted_data[ $field ] ) ? wp_unslash( $this->posted_data[ $field ] ) : '';
     
    391418            }
    392419
    393             // Check if required field is empty
    394             if ( empty( $field_value ) && in_array( $field, $required_fields, true ) ) {
     420            // Check if the required field is empty
     421            if ( empty( $field_value ) && in_array( $field, $all_required_fields, true ) ) {
    395422                $this->is_validated = false;
    396423            }
     
    454481        }
    455482    }
     483
     484    /**
     485     * Validate the email address format
     486     *
     487     * @param string $email The email to validate
     488     * @return bool Whether the email is valid
     489     */
     490    private function validate_email( string $email ): bool {
     491        return (bool) filter_var( $email, FILTER_VALIDATE_EMAIL );
     492    }
     493
     494    /**
     495     * Validate a postcode format using WooCommerce's validation
     496     *
     497     * @param string $postcode The postcode to validate
     498     * @param string $country The country code
     499     * @return bool Whether the postcode is valid
     500     */
     501    private function validate_postcode( string $postcode, string $country ): bool {
     502        if ( ! WC_Validation::is_postcode( $postcode, $country ) ) {
     503            return false;
     504        }
     505
     506        return true;
     507    }
    456508}
  • multisafepay/tags/6.9.0/vendor/autoload.php

    r3269746 r3306839  
    1515        }
    1616    }
    17     trigger_error(
    18         $err,
    19         E_USER_ERROR
    20     );
     17    throw new RuntimeException($err);
    2118}
    2219
    2320require_once __DIR__ . '/composer/autoload_real.php';
    2421
    25 return ComposerAutoloaderInit4c82aacc71005aae20a63510a82d5e07::getLoader();
     22return ComposerAutoloaderIniteb5be672a765e05cf872148d946d1d0b::getLoader();
  • multisafepay/tags/6.9.0/vendor/composer/InstalledVersions.php

    r3230524 r3306839  
    2727class InstalledVersions
    2828{
     29    /**
     30     * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
     31     * @internal
     32     */
     33    private static $selfDir = null;
     34
    2935    /**
    3036     * @var mixed[]|null
     
    324330
    325331    /**
     332     * @return string
     333     */
     334    private static function getSelfDir()
     335    {
     336        if (self::$selfDir === null) {
     337            self::$selfDir = strtr(__DIR__, '\\', '/');
     338        }
     339
     340        return self::$selfDir;
     341    }
     342
     343    /**
    326344     * @return array[]
    327345     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
     
    337355
    338356        if (self::$canGetVendors) {
    339             $selfDir = strtr(__DIR__, '\\', '/');
     357            $selfDir = self::getSelfDir();
    340358            foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
    341359                $vendorDir = strtr($vendorDir, '\\', '/');
  • multisafepay/tags/6.9.0/vendor/composer/autoload_real.php

    r3269746 r3306839  
    33// autoload_real.php @generated by Composer
    44
    5 class ComposerAutoloaderInit4c82aacc71005aae20a63510a82d5e07
     5class ComposerAutoloaderIniteb5be672a765e05cf872148d946d1d0b
    66{
    77    private static $loader;
     
    2525        require __DIR__ . '/platform_check.php';
    2626
    27         spl_autoload_register(array('ComposerAutoloaderInit4c82aacc71005aae20a63510a82d5e07', 'loadClassLoader'), true, true);
     27        spl_autoload_register(array('ComposerAutoloaderIniteb5be672a765e05cf872148d946d1d0b', 'loadClassLoader'), true, true);
    2828        self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
    29         spl_autoload_unregister(array('ComposerAutoloaderInit4c82aacc71005aae20a63510a82d5e07', 'loadClassLoader'));
     29        spl_autoload_unregister(array('ComposerAutoloaderIniteb5be672a765e05cf872148d946d1d0b', 'loadClassLoader'));
    3030
    3131        require __DIR__ . '/autoload_static.php';
    32         call_user_func(\Composer\Autoload\ComposerStaticInit4c82aacc71005aae20a63510a82d5e07::getInitializer($loader));
     32        call_user_func(\Composer\Autoload\ComposerStaticIniteb5be672a765e05cf872148d946d1d0b::getInitializer($loader));
    3333
    3434        $loader->register(true);
  • multisafepay/tags/6.9.0/vendor/composer/autoload_static.php

    r3269746 r3306839  
    55namespace Composer\Autoload;
    66
    7 class ComposerStaticInit4c82aacc71005aae20a63510a82d5e07
     7class ComposerStaticIniteb5be672a765e05cf872148d946d1d0b
    88{
    99    public static $prefixLengthsPsr4 = array (
     
    6363    {
    6464        return \Closure::bind(function () use ($loader) {
    65             $loader->prefixLengthsPsr4 = ComposerStaticInit4c82aacc71005aae20a63510a82d5e07::$prefixLengthsPsr4;
    66             $loader->prefixDirsPsr4 = ComposerStaticInit4c82aacc71005aae20a63510a82d5e07::$prefixDirsPsr4;
    67             $loader->classMap = ComposerStaticInit4c82aacc71005aae20a63510a82d5e07::$classMap;
     65            $loader->prefixLengthsPsr4 = ComposerStaticIniteb5be672a765e05cf872148d946d1d0b::$prefixLengthsPsr4;
     66            $loader->prefixDirsPsr4 = ComposerStaticIniteb5be672a765e05cf872148d946d1d0b::$prefixDirsPsr4;
     67            $loader->classMap = ComposerStaticIniteb5be672a765e05cf872148d946d1d0b::$classMap;
    6868
    6969        }, null, ClassLoader::class);
  • multisafepay/tags/6.9.0/vendor/composer/installed.json

    r3264984 r3306839  
    33        {
    44            "name": "multisafepay/php-sdk",
    5             "version": "5.16.0",
    6             "version_normalized": "5.16.0.0",
     5            "version": "5.17.0",
     6            "version_normalized": "5.17.0.0",
    77            "source": {
    88                "type": "git",
    99                "url": "https://github.com/MultiSafepay/php-sdk.git",
    10                 "reference": "849f1cf0c5ae23819422db4f78db948fd590c4ec"
    11             },
    12             "dist": {
    13                 "type": "zip",
    14                 "url": "https://api.github.com/repos/MultiSafepay/php-sdk/zipball/849f1cf0c5ae23819422db4f78db948fd590c4ec",
    15                 "reference": "849f1cf0c5ae23819422db4f78db948fd590c4ec",
     10                "reference": "4c46227cf3139d76ff08bc4191f06445c867798b"
     11            },
     12            "dist": {
     13                "type": "zip",
     14                "url": "https://api.github.com/repos/MultiSafepay/php-sdk/zipball/4c46227cf3139d76ff08bc4191f06445c867798b",
     15                "reference": "4c46227cf3139d76ff08bc4191f06445c867798b",
    1616                "shasum": ""
    1717            },
     
    3838                "jschaedl/iban-validation": "Adds additional IBAN validation for \\MultiSafepay\\ValueObject\\IbanNumber"
    3939            },
    40             "time": "2025-03-19T15:29:14+00:00",
     40            "time": "2025-06-04T13:12:21+00:00",
    4141            "type": "library",
    4242            "installation-source": "dist",
     
    5353            "support": {
    5454                "issues": "https://github.com/MultiSafepay/php-sdk/issues",
    55                 "source": "https://github.com/MultiSafepay/php-sdk/tree/5.16.0"
     55                "source": "https://github.com/MultiSafepay/php-sdk/tree/5.17.0"
    5656            },
    5757            "install-path": "../multisafepay/php-sdk"
  • multisafepay/tags/6.9.0/vendor/composer/installed.php

    r3269746 r3306839  
    22    'root' => array(
    33        'name' => 'multisafepay/woocommerce',
    4         'pretty_version' => '6.8.3',
    5         'version' => '6.8.3.0',
     4        'pretty_version' => '6.9.0',
     5        'version' => '6.9.0.0',
    66        'reference' => null,
    77        'type' => 'wordpress-plugin',
     
    1212    'versions' => array(
    1313        'multisafepay/php-sdk' => array(
    14             'pretty_version' => '5.16.0',
    15             'version' => '5.16.0.0',
    16             'reference' => '849f1cf0c5ae23819422db4f78db948fd590c4ec',
     14            'pretty_version' => '5.17.0',
     15            'version' => '5.17.0.0',
     16            'reference' => '4c46227cf3139d76ff08bc4191f06445c867798b',
    1717            'type' => 'library',
    1818            'install_path' => __DIR__ . '/../multisafepay/php-sdk',
     
    2121        ),
    2222        'multisafepay/woocommerce' => array(
    23             'pretty_version' => '6.8.3',
    24             'version' => '6.8.3.0',
     23            'pretty_version' => '6.9.0',
     24            'version' => '6.9.0.0',
    2525            'reference' => null,
    2626            'type' => 'wordpress-plugin',
  • multisafepay/tags/6.9.0/vendor/multisafepay/php-sdk/CHANGELOG.md

    r3264984 r3306839  
    66
    77## [Unreleased]
     8
     9## [5.17.0] - 2025-06-04
     10### Added
     11- PHPSDK-172: Add BILLINK to SHOPPING_CART_REQUIRED_GATEWAYS constant
     12
     13### Fixed
     14- PHPSDK-173: Fix typo in \MultiSafepay\Api\Transactions\OrderRequest\Arguments\GatewayInfo\Creditcard object
     15- PHPSDK-174: Fix UnitPrice sometimes having more than 10 decimals
    816
    917## [5.16.0] - 2025-03-19
  • multisafepay/tags/6.9.0/vendor/multisafepay/php-sdk/composer.json

    r3264984 r3306839  
    44  "type": "library",
    55  "license": "MIT",
    6   "version": "5.16.0",
     6  "version": "5.17.0",
    77  "require": {
    88    "php": "^7.2|^8.0",
  • multisafepay/tags/6.9.0/vendor/multisafepay/php-sdk/src/Api/Transactions/Gateways.php

    r3072171 r3306839  
    2727        'BNPL_OB',
    2828        'BNPL_MF',
     29        'BILLINK',
    2930    );
    3031}
  • multisafepay/tags/6.9.0/vendor/multisafepay/php-sdk/src/Api/Transactions/OrderRequest/Arguments/GatewayInfo/Creditcard.php

    r3050467 r3306839  
    148148        return [
    149149            'card_number' => $this->cardNumber ? $this->cardNumber->get() : null,
    150             'cart_holder_name' => $this->cardHolderName,
    151             'cart_expiry_date' => $this->cardExpiryDate ? $this->cardExpiryDate->get('my') : null,
     150            'card_holder_name' => $this->cardHolderName,
     151            'card_expiry_date' => $this->cardExpiryDate ? $this->cardExpiryDate->get('ym') : null,
    152152            'cvc' => $this->cvc ? $this->cvc->get() : null,
    153153            'card_cvc' => $this->cvc ? $this->cvc->get() : null,
  • multisafepay/tags/6.9.0/vendor/multisafepay/php-sdk/src/Util/Version.php

    r3264984 r3306839  
    1818class Version
    1919{
    20     public const SDK_VERSION = '5.16.0';
     20    public const SDK_VERSION = '5.17.0';
    2121
    2222    /**
  • multisafepay/tags/6.9.0/vendor/multisafepay/php-sdk/src/ValueObject/CartItem.php

    r3264984 r3306839  
    244244    {
    245245        if ($this->unitPrice) {
    246             return $this->unitPrice->getAmount() / 100;
    247         }
    248 
    249         return $this->unitPriceValue->get() ?? 0.0;
     246            return round($this->unitPrice->getAmount() / 100, 10);
     247        }
     248
     249        return round(($this->unitPriceValue->get() ?? 0.0), 10);
    250250    }
    251251
  • multisafepay/tags/6.9.0/vendor/multisafepay/php-sdk/src/ValueObject/UnitPrice.php

    r3230524 r3306839  
    1515
    1616    /**
    17      * Should be given in full units excluding tax, preferably including all decimal places, e.g. 3.305785124
     17     * Should be given in full units excluding tax, preferably including 10 decimal at most, e.g. 3.305785124
    1818     *
    1919     * @param float $unitPrice
  • multisafepay/trunk/assets/public/js/multisafepay-apple-pay-wallet.js

    r3048898 r3306839  
    6262        this._sessionActive = false;
    6363
     64        /**
     65         * Flag to track if a session has been aborted
     66         *
     67         * @type {boolean}
     68         * @private
     69         */
     70        this._sessionAborted = false;
     71
    6472        this.init()
    6573            .then(
     
    174182     * Event handler for Apple Pay button click
    175183     *
    176      * @returns {Promise<void>}
    177      */
    178     onApplePaymentButtonClicked = async() => {
     184     * @returns {void}
     185     */
     186    onApplePaymentButtonClicked = () => {
     187        // Check if a session is already active
     188        if ( this.isSessionActive() ) {
     189            debugDirect( 'Apple Pay session was already activated', this.debug, 'log' );
     190            // Force reset session status to allow retry
     191            this.setSessionStatus( false );
     192            this._sessionAborted = false;
     193            return;
     194        }
     195
    179196        try {
    180             await this.beginApplePaySession();
    181         } catch ( error ) {
    182             console.error( 'Error starting Apple Pay session when button is clicked:', error );
    183             this.setSessionStatus( false );
    184         }
    185     }
    186 
    187     /**
    188      * Create Apple Pay button
    189      *
    190      * @returns {Promise<void>}
    191      */
    192     async createApplePayButton()
    193     {
    194         // Check if previous buttons already exist and remove them
    195         cleanUpDirectButtons();
    196 
    197         const buttonContainer = document.getElementById( 'place_order' ).parentElement;
    198         if ( ! buttonContainer ) {
    199             debugDirect( 'Button container not found', this.debug );
    200             return;
    201         }
    202 
    203         // Features of the button
    204         const buttonTag        = document.createElement( 'button' );
    205         buttonTag.className    = 'apple-pay-button apple-pay-button-black';
    206         buttonTag.style.cursor = 'pointer';
    207 
    208         buttonContainer.addEventListener(
    209             'click',
    210             (
    211                 event ) => {
    212                     this.onApplePaymentButtonClicked();
    213                     // Avoid that WordPress submits the form
    214                     event.preventDefault();
    215                 }
    216         );
    217 
    218         // Append the button to the div
    219         buttonContainer.appendChild( buttonTag );
    220     }
    221 
    222     /**
    223      * Create the Apple Pay payment request object and session
    224      *
    225      * Some variables from the global scope are launched from
    226      * the internal code of Prestashop
    227      *
    228      * @returns {Promise<void>}
    229      */
    230     async beginApplePaySession()
    231     {
    232         try {
    233             const validatorInstance = new FieldsValidator();
    234             const fieldsAreValid    = validatorInstance.checkFields();
    235             if ( ! fieldsAreValid ) {
    236                 debugDirect( 'Not all mandatory fields were filled out', this.debug, 'warn' );
    237                 return;
    238             }
    239 
    240             if ( this.isSessionActive() ) {
    241                 debugDirect( 'Apple Pay session was already activated', this.debug, 'log' );
    242                 return;
    243             }
    244 
    245             // Create the payment request object
     197            // Create the payment request object first
    246198            const paymentRequest = {
    247199                countryCode: configApplePay.countryCode,
     
    258210            };
    259211
    260             // Create the session and handle the events
     212            // Create the session immediately in the click handler
    261213            const session = new ApplePaySession( this.config.applePayVersion, paymentRequest );
     214
    262215            if ( session ) {
     216                // Reset the aborted flag
     217                this._sessionAborted = false;
     218
     219                // Setup event handlers
    263220                session.onvalidatemerchant  = ( event ) => this.handleValidateMerchant( event, session );
    264221                session.onpaymentauthorized = ( event ) => this.handlePaymentAuthorized( event, session );
    265222                session.oncancel            = ( event ) => this.handleCancel( event, session );
    266                 session.begin();
    267 
     223
     224                // Set session as active
    268225                this.setSessionStatus( true );
     226
     227                // Validate fields before beginning the session
     228                this.validateFieldsBeforeSession( session );
    269229            }
    270230        } catch ( error ) {
    271             console.error( 'Error starting Apple Pay session:', error );
     231            console.error( 'Error starting Apple Pay session when button is clicked:', error );
    272232            this.setSessionStatus( false );
    273         }
    274     }
    275 
    276     /**
    277      * Fetch merchant session data from MultiSafepay
    278      *
    279      * @param {string} validationURL
    280      * @param {string} originDomain
    281      * @returns {Promise<object>}
    282      */
    283     async fetchMerchantSession(validationURL, originDomain)
    284     {
    285         const data = new URLSearchParams();
    286         data.append( 'action', 'applepay_direct_validation' );
    287         data.append( 'validation_url', validationURL );
    288         data.append( 'origin_domain', originDomain );
    289 
    290         const response = await fetch(
    291             this.config.multiSafepayServerScript,
    292             {
    293                 method: 'POST',
    294                 body: data,
    295                 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
    296             }
    297         );
    298 
    299         return JSON.parse( await response.json() );
     233            this._sessionAborted = false;
     234        }
     235    }
     236
     237    /**
     238     * Validate fields before beginning the Apple Pay session
     239     *
     240     * @param {object} session - The Apple Pay session
     241     * @returns {void}
     242     */
     243    validateFieldsBeforeSession( session ) {
     244        // Create a validator instance
     245        const validatorInstance = new FieldsValidator();
     246
     247        // Clear any previous error messages
     248        validatorInstance.clearAllErrors();
     249
     250        // Validate fields
     251        validatorInstance.checkFields()
     252            .then(
     253                fieldsAreValid => {
     254                    if ( fieldsAreValid ) {
     255                        // Fields are valid, now begin the session
     256                        session.begin();
     257                    } else {
     258                        // Fields are not valid, don't begin the session
     259                        debugDirect( 'Not all mandatory fields were filled out', this.debug, 'warn' );
     260                        this._sessionAborted = true;
     261                        this.setSessionStatus( false );
     262                    }
     263                }
     264            )
     265            .catch(
     266                error => {
     267                    // Error during validation
     268                    debugDirect( 'Error during field validation: ' + error, this.debug, 'error' );
     269                    this._sessionAborted = true;
     270                    this.setSessionStatus( false );
     271                }
     272            );
    300273    }
    301274
     
    309282    handleValidateMerchant = async( event, session ) => {
    310283        try {
     284            // Check if the session has been aborted
     285            if ( this._sessionAborted ) {
     286                debugDirect( 'Session was already aborted, skipping merchant validation', this.debug, 'log' );
     287                return;
     288            }
     289
    311290            const validationURL = event.validationURL;
    312291            const originDomain  = window.location.hostname;
     
    314293            const merchantSession = await this.fetchMerchantSession( validationURL, originDomain );
    315294            if ( merchantSession && ( typeof merchantSession === 'object' ) ) {
    316                 session.completeMerchantValidation( merchantSession );
     295                // Only complete merchant validation if the session hasn't been aborted
     296                if ( ! this._sessionAborted ) {
     297                    session.completeMerchantValidation( merchantSession );
     298                }
    317299            } else {
    318300                debugDirect( 'Error validating merchant', this.debug );
     301                this._sessionAborted = true;
    319302                session.abort();
    320303            }
    321304        } catch ( error ) {
    322305            console.error( 'Error validating merchant:', error );
     306            this._sessionAborted = true;
    323307            session.abort();
    324308        }
     
    336320    handlePaymentAuthorized = async( event, session ) => {
    337321        try {
     322            // Check if the session has been aborted
     323            if ( this._sessionAborted ) {
     324                debugDirect( 'Session was already aborted, skipping payment authorization', this.debug, 'log' );
     325                return;
     326            }
     327
    338328            const paymentToken = JSON.stringify( event.payment.token );
    339329            const success      = await this.submitApplePayForm( paymentToken );
     
    361351            try {
    362352                debugDirect( 'Apple Pay Direct session successfully aborted.', this.debug, 'log' );
     353                this._sessionAborted = true;
    363354                session.abort();
    364355            } catch ( error ) {
     
    410401        return true;
    411402    }
     403
     404    /**
     405     * Fetch merchant session data from MultiSafepay
     406     *
     407     * @param {string} validationURL
     408     * @param {string} originDomain
     409     * @returns {Promise<object>}
     410     */
     411    async fetchMerchantSession( validationURL, originDomain )
     412    {
     413        const data = new URLSearchParams();
     414        data.append( 'action', 'applepay_direct_validation' );
     415        data.append( 'validation_url', validationURL );
     416        data.append( 'origin_domain', originDomain );
     417
     418        const response = await fetch(
     419            this.config.multiSafepayServerScript,
     420            {
     421                method: 'POST',
     422                body: data,
     423                headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
     424            }
     425        );
     426
     427        return JSON.parse( await response.json() );
     428    }
     429
     430    /**
     431     * Create Apple Pay button
     432     *
     433     * @returns {Promise<void>}
     434     */
     435    async createApplePayButton()
     436    {
     437        // Check if previous buttons already exist and remove them
     438        cleanUpDirectButtons();
     439
     440        const buttonContainer = document.getElementById( 'place_order' ).parentElement;
     441        if ( ! buttonContainer ) {
     442            debugDirect( 'Button container not found', this.debug );
     443            return;
     444        }
     445
     446        // Features of the button
     447        const buttonTag        = document.createElement( 'button' );
     448        buttonTag.className    = 'apple-pay-button apple-pay-button-black';
     449        buttonTag.style.cursor = 'pointer';
     450
     451        // Add an event listener directly to the Apple Pay button, not the container
     452        buttonTag.addEventListener(
     453            'click',
     454            ( event ) => {
     455                this.onApplePaymentButtonClicked();
     456                // Prevent that WordPress submits the form
     457                event.preventDefault();
     458                event.stopPropagation();
     459            }
     460        );
     461
     462        // Append the button to the div
     463        buttonContainer.appendChild( buttonTag );
     464    }
    412465}
  • multisafepay/trunk/assets/public/js/multisafepay-google-pay-wallet.js

    r3048898 r3306839  
    222222    {
    223223        const validatorInstance = new FieldsValidator();
    224         const fieldsAreValid    = validatorInstance.checkFields();
     224        const fieldsAreValid    = await validatorInstance.checkFields();
    225225        if ( fieldsAreValid ) {
    226226            if ( paymentsClient && paymentsClient.loadPaymentData ) {
  • multisafepay/trunk/assets/public/js/multisafepay-jquery-wallets.js

    r3048898 r3306839  
    4444                            function() {
    4545                                debugDirect( 'Select2 action initialized for field: ' + this.name, debugStatus, 'log' );
    46                                 const select2Container = $( this ).closest( '.validate-required' ).find( '.select2-selection' );
     46
     47                                const wrapper          = $( this ).closest( '.validate-required' );
     48                                const select2Container = wrapper.find( '.select2-selection' );
    4749                                if ( select2Container.length ) {
    48                                     select2Container.attr( 'style', '' );
     50                                    // Remove the inline red border style
     51                                    select2Container.removeAttr( 'style' );
     52                                    // Alternative approach if removeAttr doesn't work
     53                                    select2Container.css( 'border', '' );
     54                                }
     55
     56                                // Get the field ID container
     57                                const fieldId        = this.name + '_field';
     58                                const fieldContainer = $( '#' + fieldId );
     59                                if ( fieldContainer.length ) {
     60                                    // Remove WooCommerce invalid class and add validated class
     61                                    fieldContainer.removeClass( 'woocommerce-invalid' );
     62                                    fieldContainer.addClass( 'woocommerce-validated' );
    4963                                }
    5064                                validatorInstance.removeErrorMessage( this.name );
     
    167181                                // Remove the orphan error messages from the notice group
    168182                                validatorInstance.removeOrphanErrorMessages();
     183                                // Re-initialize select2 validation after checkout is updated
     184                                select2Validation();
    169185                            }
    170186                        );
  • multisafepay/trunk/assets/public/js/multisafepay-validator-wallets.js

    r3048898 r3306839  
    2020 */
    2121
     22// Check if the debugDirect function is available, if not define it
     23if ( typeof debugDirect !== 'function' ) {
     24    window.debugDirect = function( debugMessage, debugEnabled, loggingType = 'error' ) {
     25        const allowedTypeArray = ['log', 'info', 'warn', 'error', 'debug'];
     26
     27        if ( ! allowedTypeArray.includes( loggingType ) ) {
     28            loggingType = 'log';
     29        }
     30
     31        if ( debugMessage && debugEnabled ) {
     32            console[loggingType]( debugMessage );
     33        }
     34    };
     35}
     36
    2237/**
    2338 * Class to validate the fields in the checkout form
     
    3247
    3348    /**
     49     * Cache for field labels to avoid repeated DOM lookups
     50     *
     51     * @type {Object}
     52     */
     53    fieldLabelsCache = {};
     54
     55    /**
     56     * Cache for validated postcodes to avoid redundant AJAX calls
     57     * Format: { 'prefix[billing/shipping]:country:postcode': boolean }
     58     *
     59     * @type {Object}
     60     */
     61    validatedPostcodesCache = {};
     62
     63    /**
     64     * Flag to indicate if validation is currently in progress
     65     *
     66     * @type {boolean}
     67     */
     68    validationInProgress = false;
     69
     70    /**
     71     * Constructor - setup event listeners for form submission
     72     */
     73    constructor() {
     74        // Set up a global event listener to prevent payment when validation is in progress
     75        this.setupPaymentBlockingWhileValidating();
     76    }
     77
     78    /**
     79     * Check if an element is visible
     80     *
     81     * @param {HTMLElement} element - Element to check visibility
     82     * @returns {boolean} - Whether the element is visible
     83     */
     84    isElementVisible( element ) {
     85        if ( ! element ) {
     86            return false;
     87        }
     88
     89        const style = window.getComputedStyle( element );
     90        return ( element.offsetParent !== null ) ||
     91            (
     92                ( style.position === 'fixed' ) &&
     93                ( style.display !== 'none' ) &&
     94                ( style.visibility !== 'hidden' )
     95            );
     96    }
     97
     98    /**
     99     * Setup event listener to prevent payment when validation is in progress
     100     *
     101     * @returns {void}
     102     */
     103    setupPaymentBlockingWhileValidating() {
     104        document.addEventListener(
     105            'click',
     106            ( event ) => {
     107                // If validation is in progress
     108                if ( this.validationInProgress ) {
     109                    // Look for payment buttons or elements that may trigger payment
     110                    const targetElement   = event.target;
     111                    const paymentTriggers = [
     112                        '.payment_method_multisafepay_applepay',
     113                        '.payment_method_multisafepay_googlepay',
     114                        '#place_order',
     115                        '.apple-pay-button',
     116                        '.google-pay-button'
     117                    ];
     118
     119                    // Check if the clicked element matches any payment trigger selectors
     120                    const isPaymentTrigger = paymentTriggers.some(
     121                        selector =>
     122                            targetElement.matches && (
     123                                targetElement.matches( selector ) ||
     124                                targetElement.closest( selector )
     125                            )
     126                    );
     127
     128                    if ( isPaymentTrigger ) {
     129                        debugDirect( 'Payment blocked: Validation in progress', debugStatus, 'log' );
     130                        event.preventDefault();
     131                        event.stopPropagation();
     132                        return false;
     133                    }
     134                }
     135            },
     136            true
     137        );
     138    }
     139
     140    /**
    34141     * Get the label text for a field
    35142     *
     
    38145     */
    39146    getLabelText( fieldName ) {
    40         // Skip if this field has already been processed
    41         if ( this.processedFields.indexOf( fieldName ) !== -1 ) {
    42             return '';
     147        // If we already have the label cached, return it
     148        if ( this.fieldLabelsCache[fieldName] ) {
     149            return this.fieldLabelsCache[fieldName];
    43150        }
    44151
     
    72179        this.processedFields.push( fieldName );
    73180
    74         // Return the label text including the prefix
    75         return prefix + labelElement.firstChild.textContent.trim();
     181        // Create the label text including the prefix
     182        const labelText = prefix + labelElement.firstChild.textContent.trim();
     183
     184        // Cache the label text
     185        this.fieldLabelsCache[fieldName] = labelText;
     186
     187        // Return the label text
     188        return labelText;
     189    }
     190
     191    /**
     192     * Validate an email address format
     193     *
     194     * @param {string} email - The email to validate
     195     * @returns {boolean} - Whether the email is valid
     196     */
     197    validateEmail( email ) {
     198        // Using the same logic as WooCommerce:
     199        // wp-content/plugins/woocommerce/assets/js/frontend/checkout.js
     200        const pattern = new RegExp( /^([a-z\d!#$%&'*+\-\/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+(\.[a-z\d!#$%&'*+\-\/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)*|"((([ \t]*\r\n)?[ \t]+)?([\x01-\x08\x0b\x0c\x0e-\x1f\x7f\x21\x23-\x5b\x5d-\x7e\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|\\[\x01-\x09\x0b\x0c\x0d-\x7f\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))*(([ \t]*\r\n)?[ \t]+)?")@(([a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|[a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF][a-z\d\-._~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]*[a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])\.)+([a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|[a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF][a-z\d\-._~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]*[0-9a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])\.?$/i );
     201        return pattern.test( email );
     202    }
     203
     204    /**
     205     * Validate a postcode format via AJAX using WooCommerce's validation
     206     *
     207     * @param {string} postcode - The postcode to validate
     208     * @param {string} fieldName - The field name (to determine if billing or shipping)
     209     * @returns {Promise<boolean>} - Promise resolving to whether the postcode is valid
     210     */
     211    async validatePostcode( postcode, fieldName ) {
     212        // Signal that validation is in progress
     213        this.validationInProgress = true;
     214
     215        // Extract country value based on whether this is billing or shipping
     216        const prefix       = fieldName.startsWith( 'billing_' ) ? 'billing' : 'shipping';
     217        const countryField = document.getElementById( prefix + '_country' );
     218
     219        if ( ! countryField ) {
     220            debugDirect( 'Country field not found for ' + fieldName, debugStatus );
     221            this.validationInProgress = false;
     222            return true; // Default to valid if the country cannot be found
     223        }
     224
     225        const country = countryField.value;
     226
     227        // Generate a cache key using an address type, country and postcode
     228        const cacheKey = prefix + ':' + country + ':' + postcode;
     229
     230        // Check if this postcode has already been validated for this country and address type
     231        if ( this.validatedPostcodesCache.hasOwnProperty( cacheKey ) ) {
     232            debugDirect( 'Using cached validation result for postcode: ' + postcode + ' in country: ' + country + ' for ' + prefix + ' address', debugStatus, 'log' );
     233            this.validationInProgress = false;
     234            return this.validatedPostcodesCache[cacheKey];
     235        }
     236
     237        try {
     238            // Return a promise that resolves with a validation result
     239            return await new Promise(
     240                ( resolve ) => {
     241                    // Create data for AJAX request similar to WooCommerce's update_order_review
     242                    const data = {
     243                        action: 'multisafepay_validate_postcode',
     244                        security: multisafepayParams.nonce,
     245                        postcode: postcode,
     246                        country: country
     247                    };
     248                    debugDirect( 'Validating postcode via AJAX: ' + postcode + ' for country: ' + country + ' for ' + prefix + ' address', debugStatus, 'log' );
     249                    // Send AJAX request to validate postcode
     250                    jQuery.ajax(
     251                    {
     252                        type: 'POST',
     253                        url: multisafepayParams.location,
     254                        data: data,
     255                        success: function ( response ) {
     256                            let isValid = false;
     257                            if ( response.success ) {
     258                                isValid = true;
     259                            }
     260                            // Cache the validation result
     261                            this.validatedPostcodesCache[cacheKey] = isValid;
     262
     263                            debugDirect( 'Postcode validation result via Ajax for ' + prefix + ' address: ' + postcode + ': ' + ( isValid ? 'valid' : 'invalid' ), debugStatus, 'log' );
     264
     265                            // Signal that validation is complete
     266                            this.validationInProgress = false;
     267
     268                            resolve( isValid );
     269                        }.bind( this ),
     270                        error: function () {
     271                            debugDirect( 'Error validating postcode via AJAX', debugStatus );
     272                            this.validationInProgress = false;
     273                            resolve( true ); // Default to valid on error
     274                        }.bind( this )
     275                    }
     276                    );
     277                }
     278            );
     279        } catch ( error ) {
     280            debugDirect( 'Exception validating postcode: ' + error, debugStatus );
     281            this.validationInProgress = false;
     282            return true; // Default to valid on error
     283        }
     284    }
     285
     286    /**
     287     * Validate special fields like email and postcodes
     288     *
     289     * @param {HTMLElement} element - The element to be validated
     290     * @param labelText - The label text for the field
     291     * @returns {Promise<boolean>} - Whether the field is valid or not
     292     */
     293    async validateSpecialFields( element, labelText ) {
     294        const fieldName  = element.name.trim();
     295        const fieldValue = element.value.trim();
     296
     297        // Get the field container element
     298        const getFieldId = document.getElementById( fieldName + '_field' );
     299
     300        let isValid      = true;
     301        let errorMessage = '';
     302
     303        // Check if this is one of the fields we need to specifically validate
     304        if ( fieldName === 'billing_email' ) {
     305            // Validate email format
     306            if ( ! this.validateEmail( fieldValue ) ) {
     307                isValid      = false;
     308                errorMessage = labelText + ' is not a valid address and ';
     309            }
     310        } else if ( fieldName === 'billing_postcode' || fieldName === 'shipping_postcode' ) {
     311            // Validate postcode via AJAX
     312            isValid = await this.validatePostcode( fieldValue, fieldName );
     313            if ( ! isValid ) {
     314                // If labelText is empty, get it from the cache or fall back to default
     315                if ( ! labelText || labelText.trim() === '' ) {
     316                    labelText = this.getLabelText( fieldName );
     317
     318                    // If still empty, fallback to default
     319                    if ( ! labelText || labelText.trim() === '' ) {
     320                        const type = fieldName.startsWith( 'billing_' ) ? 'Billing' : 'Shipping';
     321                        labelText  = type + ' Postcode / ZIP';
     322                    }
     323                }
     324                errorMessage = labelText + ' is not valid for the selected country';
     325            }
     326        }
     327
     328        // Apply validation results
     329        if ( isValid ) {
     330            this.removeErrorMessage( fieldName, true );
     331            this.removeInlineError( fieldName );
     332            element.style.cssText = '';
     333            ['aria-invalid', 'aria-describedby'].forEach( attr => element.removeAttribute( attr ) );
     334            getFieldId.classList.remove( 'woocommerce-invalid', 'woocommerce-invalid-required-field', 'woocommerce-invalid-email' );
     335            getFieldId.classList.add( 'woocommerce-validated' );
     336        } else {
     337            const errorId = fieldName + '_error';
     338            // For the top error banner - use the same message format
     339            this.appendErrorMessage( [{ field: fieldName, label: errorMessage }], true );
     340
     341            // For inline errors, add a period at the end for postcode errors
     342            let inlineErrorMessage = errorMessage;
     343            if ( fieldName === 'billing_postcode' || fieldName === 'shipping_postcode' ) {
     344                inlineErrorMessage = errorMessage + '.';
     345            }
     346
     347            this.addInlineError( fieldName, inlineErrorMessage, errorId );
     348            element.setAttribute( 'aria-invalid', 'true' );
     349            element.setAttribute( 'aria-describedby', errorId );
     350            element.style.cssText = 'box-shadow: inset 2px 0 0 #e2401c !important;';
     351            getFieldId.classList.remove( 'woocommerce-validated' );
     352            getFieldId.classList.add( 'woocommerce-invalid' );
     353            if ( fieldName.includes( 'email' ) ) {
     354                getFieldId.classList.add( 'woocommerce-invalid-email' );
     355            }
     356        }
     357
     358        return isValid;
     359    }
     360
     361    /**
     362     * Add an inline error message below a field
     363     *
     364     * @param {string} fieldName - The name of the field
     365     * @param {string} errorMessage - The error message
     366     * @param {string} errorId - The ID for the error element (for aria-describedby)
     367     * @returns {void}
     368     */
     369    addInlineError( fieldName, errorMessage, errorId ) {
     370        // Get the field element
     371        const fieldElement = document.querySelector(
     372            'input[name="' + fieldName + '"],' +
     373            'select[name="' + fieldName + '"],' +
     374            'textarea[name="' + fieldName + '"]'
     375        );
     376
     377        if ( ! fieldElement ) {
     378            return;
     379        }
     380
     381        // Remove any existing inline error
     382        this.removeInlineError( fieldName );
     383
     384        // Create an inline error message
     385        const errorElement = document.createElement( 'p' );
     386        errorElement.setAttribute( 'id', errorId );
     387        errorElement.className   = 'checkout-inline-error-message';
     388        errorElement.textContent = errorMessage;
     389
     390        // Add to a parent element
     391        const formRow = fieldElement.closest( '.form-row' );
     392        if ( formRow ) {
     393            formRow.appendChild( errorElement );
     394        }
     395    }
     396
     397    /**
     398     * Remove inline error message
     399     *
     400     * @param {string} fieldName - The name of the field
     401     * @returns {void}
     402     */
     403    removeInlineError( fieldName ) {
     404        // Get the field element
     405        const fieldElement = document.querySelector(
     406            'input[name="' + fieldName + '"],' +
     407            'select[name="' + fieldName + '"],' +
     408            'textarea[name="' + fieldName + '"]'
     409        );
     410
     411        if ( ! fieldElement ) {
     412            return;
     413        }
     414
     415        // Remove any existing inline error
     416        const formRow = fieldElement.closest( '.form-row' );
     417        if ( formRow ) {
     418            const errorElement = formRow.querySelector( '.checkout-inline-error-message' );
     419            if ( errorElement ) {
     420                errorElement.remove();
     421            }
     422        }
     423
     424        // Remove aria attributes
     425        ['aria-invalid', 'aria-describedby'].forEach( attr => fieldElement.removeAttribute( attr ) );
    76426    }
    77427
     
    80430     *
    81431     * @param element - The element to be validated
     432     * @returns {void}
    82433     */
    83434    realtimeValidation( element ) {
    84         const fieldName = element.name.trim();
     435        const fieldName  = element.name.trim();
     436        const fieldValue = element.value.trim();
    85437
    86438        // Remove this field from processedFields if it's there
     
    90442        }
    91443
    92         if ( element.value.trim() !== '' ) {
     444        // Get the field container element
     445        const getFieldId = document.getElementById( fieldName + '_field' );
     446        const labelText  = this.getLabelText( fieldName );
     447
     448        // If empty and required, show the required error
     449        if ( fieldValue === '' ) {
     450            if ( ( labelText !== '' ) && getFieldId && getFieldId.classList.contains( 'validate-required' ) ) {
     451                const errorId      = fieldName + '_error';
     452                const errorMessage = labelText + ' is a required field';
     453                this.appendErrorMessage( [{ field: fieldName, label: labelText }], true );
     454                this.addInlineError( fieldName, errorMessage, errorId );
     455                // Using the same error style as WooCommerce
     456                element.style.cssText = 'box-shadow: inset 2px 0 0 #e2401c !important;';
     457                element.setAttribute( 'aria-invalid', 'true' );
     458                element.setAttribute( 'aria-describedby', errorId );
     459                getFieldId.classList.remove( 'woocommerce-validated' );
     460                getFieldId.classList.add( 'woocommerce-invalid', 'woocommerce-invalid-required-field' );
     461            }
     462            return;
     463        }
     464
     465        // For non-empty fields with specific validation needs, validate them
     466        if ( fieldName === 'billing_email' ) {
     467            this.validateSpecialFields( element, labelText ).catch(
     468                error => {
     469                    debugDirect( 'Error validating specific field: ' + error, debugStatus );
     470                }
     471            );
     472        } else {
     473            // For other fields that were filled in, remove any error messages
    93474            this.removeErrorMessage( fieldName, true );
     475            this.removeInlineError( fieldName );
    94476            element.style.cssText = '';
    95         } else {
    96             const labelText = this.getLabelText( fieldName );
    97             // Any field name has an associated ID with the suffix '_field'
    98             const getFieldId = document.getElementById( fieldName + '_field' );
    99             // If the label text is not empty and field is required
    100             if ( (labelText !== '') && getFieldId.classList.contains( 'validate-required' ) ) {
    101                 this.appendErrorMessage( [{ field: fieldName, label: labelText }], true );
    102             }
    103             // Using the same error style as WooCommerce
    104             element.style.cssText = 'box-shadow: inset 2px 0 0 #e2401c !important;';
    105         }
    106     }
    107 
    108     /**
    109      * Validate all the fields with the class 'validate-required'
    110      * inside the element with the id 'customer_details'
    111      *
    112      * @returns {boolean}
    113      */
    114     checkFields() {
    115         // Clear the processedFields array at the start of validation
    116         this.processedFields.length = 0;
    117 
     477            getFieldId.classList.remove( 'woocommerce-invalid', 'woocommerce-invalid-required-field' );
     478            getFieldId.classList.add( 'woocommerce-validated' );
     479        }
     480    }
     481
     482    /**
     483     * Setup validation listeners for postcode fields
     484     *
     485     * @param element - The element to setup listeners for
     486     * @returns {void}
     487     */
     488    setupPostcodeValidation( element ) {
     489        const fieldName = element.name.trim();
     490
     491        // Get the label text for the field from our cache
     492        const labelText = this.getLabelText( fieldName );
     493
     494        // Setup change event handler for postcodes
     495        element._changeHandler = () => {
     496            debugDirect( 'Validating postcode on change: ' + fieldName, debugStatus, 'log' );
     497            this.validateSpecialFields( element, labelText )
     498                .catch(
     499                    error => {
     500                        debugDirect( 'Error validating postcode: ' + error, debugStatus, 'error' );
     501                    }
     502                );
     503        };
     504        element.addEventListener( 'change', element._changeHandler );
     505
     506        // Remove any previous focusout handler
     507        if ( element._focusoutHandler ) {
     508            element.removeEventListener( 'focusout', element._focusoutHandler );
     509            element._focusoutHandler = null;
     510        }
     511    }
     512
     513    /**
     514     * Add event listeners to all required fields
     515     *
     516     * @returns {void}
     517     */
     518    setupValidationListeners() {
    118519        // Getting the customer details element which includes all the user fields
    119520        const customerDetails = document.getElementById( 'customer_details' );
    120         // Getting all the fields with the class 'validate-required' so we can loop through them
     521        if ( ! customerDetails ) {
     522            return;
     523        }
     524
     525        // Getting all the fields with the class 'validate-required'
    121526        const selectWrappers = customerDetails.querySelectorAll( '.validate-required' );
    122         // Check if the element is visible
    123         const isElementVisible = element => element && element.offsetParent !== null;
    124         // Are all fields valid? For now, yes
    125         let allFieldsValid = true;
    126 
    127         // Loop through all the fields with the class 'validate-required'
     527
    128528        selectWrappers.forEach(
    129529            wrapper => {
    130                 // Getting all the fields inside the wrapper including input, text-areas and selects
    131                 const elements = wrapper.querySelectorAll( 'input, textarea, select' );
    132                 // Loop through all the fields inside the wrapper
     530                // Getting all the fields inside the wrapper
     531                const elements          = wrapper.querySelectorAll( 'input, textarea, select' );
    133532                elements.forEach(
    134533                    element => {
    135                         // Remove existing listener and add a new one
    136                         element.removeEventListener( 'input', event => this.realtimeValidation( event.target ) );
    137                         element.addEventListener( 'input', event => this.realtimeValidation( event.target ) );
    138534                        const fieldName = element.name.trim();
    139                         // Initial validation logic: if the field is empty and visible
    140                         if ( ! element.value.trim() && ( isElementVisible( element ) || element.type === 'hidden' ) ) {
    141                             const labelText = this.getLabelText( fieldName );
    142                             if ( labelText !== '' ) {
    143                                 this.appendErrorMessage( [{ field: fieldName, label: labelText }] );
    144                             }
    145                             // Not all fields are valid
    146                             allFieldsValid = false;
    147                             // Add a different error style to the field if it's a Select2 or input,
    148                             // using the same error style as WooCommerce
    149                             const select2Container = element.tagName.toLowerCase() === 'select' ? wrapper.querySelector( '.select2-selection' ) : null;
    150                             if ( select2Container ) {
    151                                 select2Container.style.cssText = 'border: 2px solid #e2401c !important;';
    152                             } else {
    153                                 element.style.cssText = 'box-shadow: inset 2px 0 0 #e2401c !important;';
    154                             }
     535                        // Remove any existing listeners first
     536                        element.removeEventListener( 'input', element._inputHandler );
     537                        element.removeEventListener( 'change', element._changeHandler );
     538                        element.removeEventListener( 'focusout', element._focusoutHandler );
     539                        // For postcodes, use only change event instead of input
     540                        if ( fieldName === 'billing_postcode' || fieldName === 'shipping_postcode' ) {
     541                            this.setupPostcodeValidation( element );
    155542                        } else {
    156                             this.removeErrorMessage( fieldName );
    157                             element.style.cssText = '';
     543                            // For all other fields, use the input event for real-time validation
     544                            element._inputHandler = event => this.realtimeValidation( event.target );
     545                            element.addEventListener( 'input', element._inputHandler );
     546
     547                            // Add focusout handler for all fields
     548                            element._focusoutHandler = event => this.realtimeValidation( event.target );
     549                            element.addEventListener( 'focusout', element._focusoutHandler );
    158550                        }
    159551                    }
     
    161553            }
    162554        );
     555    }
     556
     557    /**
     558     * Validate a specific postcode field
     559     *
     560     * @param {HTMLElement} postcodeElement - The postcode input element
     561     * @param {string} labelText - The label text for the field
     562     * @returns {Promise<boolean>} - Promise resolving to whether validation pass
     563     */
     564    async sharedPostcodeValidation( postcodeElement, labelText ) {
     565        const fieldName = postcodeElement.name.trim();
     566
     567        // Start validation
     568        this.validationInProgress = true;
     569
     570        // Validate the field
     571        const isValid = await this.validateSpecialFields( postcodeElement, labelText );
     572
     573        // Update field styling if invalid
     574        if ( ! isValid ) {
     575            const postcodeField = document.getElementById( fieldName + '_field' );
     576            if ( postcodeField ) {
     577                postcodeField.classList.remove( 'woocommerce-validated' );
     578                postcodeField.classList.add( 'woocommerce-invalid' );
     579            }
     580        }
     581
     582        return isValid;
     583    }
     584
     585    /**
     586     * Validate all the fields with the class 'validate-required'
     587     * inside the element with the id 'customer_details'
     588     *
     589     * @returns {Promise<boolean>}
     590     */
     591    async checkFields() {
     592        // Early check - if validation is in progress, block payment
     593        if ( this.validationInProgress ) {
     594            debugDirect( 'Blocking payment: Validation in progress', debugStatus, 'log' );
     595            return false;
     596        }
     597
     598        // Clear any existing errors from previous validations
     599        this.clearAllErrors();
     600
     601        // Clear the processedFields array at the start of validation
     602        this.processedFields.length = 0;
     603
     604        // Getting the customer details element which includes all the user fields
     605        const customerDetails = document.getElementById( 'customer_details' );
     606        if ( ! customerDetails ) {
     607            debugDirect( 'Customer details element not found', debugStatus, 'error' );
     608            return false;
     609        }
     610
     611        // Are all fields valid? For now, yes
     612        let allFieldsValid = true;
     613
     614        // STEP 1: Check all empty required fields first
     615        // Getting all the fields with the class 'validate-required'
     616        const requiredFields = customerDetails.querySelectorAll( '.validate-required' );
     617        debugDirect( 'Found ' + requiredFields.length + ' required fields', debugStatus, 'log' );
     618
     619        // First pass: Validate empty required fields
     620        requiredFields.forEach(
     621            wrapper => {
     622                // Getting all the input elements inside the wrapper
     623                const elements      = wrapper.querySelectorAll( 'input, textarea, select' );
     624                elements.forEach(
     625                element => {
     626                    const fieldName = element.name.trim();
     627                    if ( ! fieldName ) {
     628                        return; // Skip elements without a name
     629                    }
     630                    // If the field is visible (or hidden) and is empty
     631                    if ( this.isElementVisible( element ) || element.type === 'hidden' ) {
     632                        if ( ! element.value.trim()) {
     633                            const labelText = this.getLabelText( fieldName );
     634                            if ( labelText ) {
     635                                const errorId      = fieldName + '_error';
     636                                const errorMessage = labelText + ' is a required field';
     637
     638                                // Add an error message to the top of the page
     639                                this.appendErrorMessage( [{ field: fieldName, label: labelText }], true );
     640
     641                                // Add inline error
     642                                this.addInlineError( fieldName, errorMessage, errorId );
     643
     644                                // Invalid field styling
     645                                element.setAttribute( 'aria-invalid', 'true' );
     646                                element.setAttribute( 'aria-describedby', errorId );
     647
     648                                // Add different error style depending on an element type
     649                                const select2Container = element.tagName.toLowerCase() === 'select' ?
     650                                wrapper.querySelector( '.select2-selection' ) : null;
     651
     652                                if ( select2Container ) {
     653                                    select2Container.style.cssText = 'border: 2px solid #e2401c !important;';
     654                                } else {
     655                                    element.style.cssText = 'box-shadow: inset 2px 0 0 #e2401c !important;';
     656                                }
     657
     658                                // Update wrapper classes
     659                                wrapper.classList.remove( 'woocommerce-validated' );
     660                                wrapper.classList.add( 'woocommerce-invalid', 'woocommerce-invalid-required-field' );
     661
     662                                // Not all fields are valid
     663                                allFieldsValid = false;
     664
     665                                debugDirect( 'Field is required but empty: ' + fieldName, debugStatus, 'log' );
     666                            }
     667                        }
     668                    }
     669                }
     670                );
     671            }
     672        );
     673
     674        // STEP 2: Special validation for email and postcode
     675        // Setup validation listeners
     676        this.setupValidationListeners();
     677
     678        // Explicitly check special fields (email and postcodes)
     679        const billingEmail     = document.querySelector( '[name="billing_email"]' );
     680        const billingPostcode  = document.querySelector( '[name="billing_postcode"]' );
     681        const shippingPostcode = document.querySelector( '[name="shipping_postcode"]' );
     682
     683        // Flag to track if we need to wait for validations
     684        let pendingValidation = false;
     685
     686        // Array to track validation promises
     687        const validationPromises = [];
     688
     689        // Validate billing_email
     690        if ( billingEmail && this.isElementVisible( billingEmail ) && billingEmail.value.trim() !== '' ) {
     691            const labelText = this.getLabelText( 'billing_email' );
     692
     693            // Check the email format synchronously
     694            if ( ! this.validateEmail( billingEmail.value.trim() )) {
     695                allFieldsValid          = false;
     696                const billingEmailField = document.getElementById( 'billing_email_field' );
     697                const errorId           = 'billing_email_error';
     698                const errorMessage      = labelText + ' is not a valid email address';
     699
     700                // Add an error message to the top of the page
     701                this.appendErrorMessage( [{ field: 'billing_email', label: labelText + ' is not a valid email address and ' }], true );
     702
     703                // Add inline error
     704                this.addInlineError( 'billing_email', errorMessage, errorId );
     705                billingEmail.setAttribute( 'aria-invalid', 'true' );
     706                billingEmail.setAttribute( 'aria-describedby', errorId );
     707                billingEmail.style.cssText = 'box-shadow: inset 2px 0 0 #e2401c !important;';
     708
     709                if ( billingEmailField ) {
     710                    billingEmailField.classList.remove( 'woocommerce-validated' );
     711                    billingEmailField.classList.add( 'woocommerce-invalid', 'woocommerce-invalid-email' );
     712                }
     713            } else {
     714                // Email is valid, remove any error messages
     715                const billingEmailField = document.getElementById( 'billing_email_field' );
     716                this.removeErrorMessage( 'billing_email', true );
     717                this.removeInlineError( 'billing_email' );
     718                billingEmail.style.cssText = '';
     719                ['aria-invalid', 'aria-describedby'].forEach( attr => billingEmail.removeAttribute( attr ) );
     720
     721                if ( billingEmailField ) {
     722                    billingEmailField.classList.remove( 'woocommerce-invalid', 'woocommerce-invalid-required-field', 'woocommerce-invalid-email' );
     723                    billingEmailField.classList.add( 'woocommerce-validated' );
     724                }
     725            }
     726        }
     727
     728        // Check billing postcode
     729        if ( billingPostcode && this.isElementVisible( billingPostcode ) && billingPostcode.value.trim() !== '' ) {
     730            pendingValidation    = true;
     731            const labelText      = this.getLabelText( 'billing_postcode' );
     732            const billingPromise = this.sharedPostcodeValidation( billingPostcode, labelText )
     733                .then(
     734                    isValid => {
     735                        if ( ! isValid ) {
     736                            allFieldsValid = false;
     737                        }
     738                        return isValid;
     739                    }
     740                );
     741            validationPromises.push( billingPromise );
     742        }
     743
     744        // Check shipping postcode
     745        if ( shippingPostcode && this.isElementVisible( shippingPostcode ) && shippingPostcode.value.trim() !== '' ) {
     746            pendingValidation     = true;
     747            const labelText       = this.getLabelText( 'shipping_postcode' );
     748            const shippingPromise = this.sharedPostcodeValidation( shippingPostcode, labelText )
     749                .then(
     750                    isValid => {
     751                        if ( ! isValid ) {
     752                            allFieldsValid = false;
     753                        }
     754                        return isValid;
     755                    }
     756                );
     757            validationPromises.push( shippingPromise );
     758        }
     759
     760        // If there are pending validations, wait for them to complete
     761        if ( pendingValidation ) {
     762            debugDirect(
     763                'Waiting for ' + validationPromises.length + ' pending validation' +
     764                ( validationPromises.length === 1 ? '' : 's' ) + ' to complete',
     765                debugStatus,
     766                'log'
     767            );
     768
     769            try {
     770                await Promise.all( validationPromises );
     771                debugDirect( 'All validations for special fields were completed', debugStatus, 'log' );
     772            } catch ( error ) {
     773                debugDirect( 'Error during special fields validation: ' + error, debugStatus, 'error' );
     774                allFieldsValid = false;
     775            } finally {
     776                this.validationInProgress = false;
     777            }
     778        }
     779
     780        // Final check - ensure no validation is in progress
     781        if ( this.validationInProgress ) {
     782            debugDirect( 'Blocking payment: Validation still in progress at the end of checks', debugStatus, 'log' );
     783            return false;
     784        }
    163785
    164786        // Scroll to the previously added notice group area if there are any errors
     
    166788            this.scrollToElement( '.entry-content' );
    167789        }
     790
     791        debugDirect( 'Final validation result: ' + ( allFieldsValid ? 'Valid' : 'Invalid' ), debugStatus, 'log' );
    168792        return allFieldsValid;
    169793    }
    170794
    171795    /**
     796     * Clear all error messages and validation states
     797     *
     798     * @returns {void}
     799     */
     800    clearAllErrors() {
     801        // Remove the entire notice group
     802        const noticeGroup = document.querySelector( '.woocommerce-NoticeGroup-checkout' );
     803        if ( noticeGroup ) {
     804            noticeGroup.remove();
     805            debugDirect( 'Cleared all error messages', debugStatus, 'log' );
     806        }
     807
     808        // Remove all inline errors
     809        const inlineErrors = document.querySelectorAll( '.checkout-inline-error-message' );
     810        inlineErrors.forEach( error => error.remove() );
     811    }
     812
     813    /**
    172814     * Scroll to a specific class element in the document.
    173815     *
    174816     * @param {string} className - The class name of the element to scroll to.
     817     * @returns {void}
    175818     */
    176819    scrollToElement( className ) {
     
    204847        }
    205848
     849        // Make sure we have a valid error object
     850        if ( ! error || ! error[0] || ! error[0].field ) {
     851            debugDirect( 'Invalid error object passed to appendErrorMessage', debugStatus );
     852            return;
     853        }
     854
    206855        // Create the notice group using the WooCommerce style, where the errors will be built using JS vanilla,
    207         // so PHPCS doesn't complain if HTML code is used because the opening and closing of tags <>
     856        // so PHPCS doesn't complain if HTML code is used because of the opening and closing of tags <>
    208857        let noticeGroup = document.querySelector( '.woocommerce-NoticeGroup-checkout' );
    209858        if ( ! noticeGroup ) {
     
    215864            noticeBanner.className = 'woocommerce-error';
    216865            noticeBanner.setAttribute( 'role', 'alert' );
    217 
    218             const contentDiv     = document.createElement( 'div' );
    219             contentDiv.className = 'wc-block-components-notice-banner__content';
    220 
    221             const summaryParagraph       = document.createElement( 'p' );
    222             summaryParagraph.className   = 'wc-block-components-notice-banner__summary';
    223             summaryParagraph.textContent = 'The following problems were found:';
    224             contentDiv.appendChild( summaryParagraph );
    225 
    226             const ulElement = document.createElement( 'ul' );
    227             contentDiv.appendChild( ulElement );
    228 
    229             noticeBanner.appendChild( contentDiv );
    230866            noticeGroup.appendChild( noticeBanner );
    231867        }
    232868
    233869        const ulList = noticeGroup.querySelector( 'ul' );
    234 
    235         // Check if an error message for this field already exists, therefore, is not added again
     870        if ( ! ulList ) {
     871            debugDirect( 'Error banner not found', debugStatus );
     872            return;
     873        }
     874
     875        // Check if an error message for this field already exists
    236876        const existingErrorItem = ulList.querySelector( 'li[data-id="' + error[0].field + '"]' );
    237877        if ( existingErrorItem ) {
    238             return;
     878            // If we're in real-time validation, update the existing error message
     879            if ( isRealtimeValidation ) {
     880                debugDirect( 'Updating existing error message for field: ' + error[0].field, debugStatus, 'log' );
     881                // Remove the existing error so we can add an updated one
     882                existingErrorItem.remove();
     883            } else {
     884                // If not in real-time validation, just leave the existing error
     885                return;
     886            }
    239887        }
    240888
     
    242890        const errorItem = document.createElement( 'li' );
    243891        errorItem.setAttribute( 'data-id', error[0].field );
    244 
     892        errorItem.style.display = 'block'; // Ensure it's visible
     893
     894        // Create the complete error message with formatting
     895        const errorLink                = document.createElement( 'a' );
     896        errorLink.href                 = '#' + error[0].field;
     897        errorLink.style.textDecoration = 'none'; // Don't show underline on the text
     898
     899        // Create a strong element for the field name/error
    245900        const strongElement = document.createElement( 'strong' );
    246         // Add the label text to the error message
    247         strongElement.textContent = error[0].label;
    248 
    249         errorItem.appendChild( strongElement );
    250         errorItem.append( ' is a required field.' );
     901
     902        // Set the text for the strong element based on the error
     903        let errorText = error[0].label;
     904        // Make sure we end with "and" if the error is about invalid format
     905        if ( errorText.includes( 'is not valid' ) && ! errorText.endsWith( ' and' ) ) {
     906            errorText += ' and';
     907        }
     908        strongElement.textContent = errorText;
     909
     910        // Determine the complete error message text
     911        let fullErrorText;
     912        if ( errorText.includes( 'is not valid' ) || errorText.includes( 'is not a valid' ) ) {
     913            fullErrorText = ' is a required field.';
     914        } else {
     915            fullErrorText = ' is a required field.';
     916        }
     917
     918        // Add the complete message to the link
     919        errorLink.appendChild( strongElement );
     920        errorLink.appendChild( document.createTextNode( fullErrorText ) );
     921
     922        // Add the link containing the full message to the error item
     923        errorItem.appendChild( errorLink );
     924
    251925        // Add the error message to the notice group
    252926        ulList.appendChild( errorItem );
     927
     928        // Add a click handler to focus the field when the error is clicked
     929        errorLink.addEventListener(
     930            'click',
     931            ( event ) => {
     932                event.preventDefault();
     933                const fieldToFocus = document.querySelector( '[name="' + error[0].field + '"]' );
     934                if ( fieldToFocus ) {
     935                    fieldToFocus.focus();
     936                }
     937            }
     938        );
     939
     940        // Make sure the notice group is visible and properly positioned
     941        noticeGroup.style.display = 'block';
     942        noticeGroup.style.margin  = '0 0 2em';
    253943
    254944        debugDirect( ( isRealtimeValidation ? '"Interactively" a' : 'A' ) + 'dding error message in the notice group for field: ' + error[0].field, debugStatus, 'log' );
     
    287977                // Getting the field name associated with the error message
    288978                const fieldElement = document.querySelector( '[name="' + fieldName + '"]' );
    289                 // Check if the fieldElement exists and is not hidden using CSS
    290                 const isFieldVisible = fieldElement && ( window.getComputedStyle( fieldElement ).display !== 'none' );
     979                // Check if the fieldElement exists and is not hidden
     980                const isFieldVisible = fieldElement && this.isElementVisible( fieldElement );
    291981                if ( ! isFieldVisible ) {
    292982                    errorItem.remove();
     
    307997    removeEntireNoticeGroup() {
    308998        // Check if there are no more error messages left
    309         const errorList = document.querySelector( '.woocommerce-NoticeGroup-checkout ul' );
    310         if ( errorList && ( errorList.children.length === 0 ) ) {
    311             const noticeGroup = document.querySelector( '.woocommerce-NoticeGroup-checkout' );
    312             if ( noticeGroup ) {
     999        const noticeGroup = document.querySelector( '.woocommerce-NoticeGroup-checkout' );
     1000        if ( noticeGroup ) {
     1001            // Count elements with data-id attribute within the group
     1002            const errorItems = noticeGroup.querySelectorAll( 'li[data-id]' );
     1003            if ( errorItems.length === 0 ) {
    3131004                noticeGroup.remove();
    3141005                debugDirect( 'Removing the entire notice group as there are no more error messages left', debugStatus, 'log' );
  • multisafepay/trunk/multisafepay.php

    r3269746 r3306839  
    55 * Plugin URI:              https://docs.multisafepay.com/docs/woocommerce
    66 * Description:             MultiSafepay Payment Plugin
    7  * Version:                 6.8.3
     7 * Version:                 6.9.0
    88 * Author:                  MultiSafepay
    99 * Author URI:              https://www.multisafepay.com
     
    1212 * License URI:             http://www.gnu.org/licenses/gpl-3.0.html
    1313 * Requires at least:       6.0
    14  * Tested up to:            6.7.2
     14 * Tested up to:            6.8.1
    1515 * WC requires at least:    6.0.0
    16  * WC tested up to:         9.7.1
     16 * WC tested up to:         9.8.5
    1717 * Requires PHP:            7.3
    1818 * Text Domain:             multisafepay
     
    2727 * Plugin version
    2828 */
    29 define( 'MULTISAFEPAY_PLUGIN_VERSION', '6.8.3' );
     29define( 'MULTISAFEPAY_PLUGIN_VERSION', '6.9.0' );
    3030
    3131/**
  • multisafepay/trunk/readme.txt

    r3269746 r3306839  
    33Tags: multisafepay, payment gateway, credit cards, ideal, bnpl
    44Requires at least: 6.0
    5 Tested up to: 6.7.2
     5Tested up to: 6.8.1
    66Requires PHP: 7.3
    7 Stable tag: 6.8.3
     7Stable tag: 6.9.0
    88License: MIT
    99
     
    139139
    140140== Changelog ==
     141= Release Notes - WooCommerce 6.9.0 (Jun 5th, 2025) =
     142
     143### Added
     144+ PLGWOOS-1003: Add zip code and email format validation to QR code implementation
     145+ PLGWOOS-1001: Improvement over QR code implementation validating checkout when"ship to a different address" is checked.
     146
     147### Fixed
     148+ PLGWOOS-1009: Fix Bancontact QR showing up even when it hasn’t been explicitly enabled
     149+ PLGWOOS-1007: Refund request based on amount is including the checkout data, even when it's not needed.
     150+ PLGWOOS-997: Validate zip code checkout fields to prevent payments via Wallets
     151+ PLGWOOS-1002: Payment Component shows "Store my details for future visits" field, when user is not logged in
     152+ PLGWOOS-999: Fix URL Parameter Concatenation in QR Payment Redirect Flow
     153+ PLGWOOS-1000: Remove unneeded get_customer_ip_address() and get_user_agent() methods in QrCustomerService
     154
     155### Changed
     156+ PLGWOOS-1005: Adjusting the minimum discrepancy allowed to filter Billink tax rates
     157+ PLGWOOS-1004: Adding shipping method name in the Order Request, instead of generic label "Shipping"
     158
    141159= Release Notes - WooCommerce 6.8.3 (Apr 9th, 2025) =
    142160
  • multisafepay/trunk/src/Main.php

    r3264984 r3306839  
    88use MultiSafepay\WooCommerce\Services\Qr\QrPaymentComponentService;
    99use MultiSafepay\WooCommerce\Services\Qr\QrPaymentWebhook;
     10use MultiSafepay\WooCommerce\Services\ValidationService;
    1011use MultiSafepay\WooCommerce\Settings\SettingsController;
    1112use MultiSafepay\WooCommerce\Settings\ThirdPartyCompatibility;
     
    4445        $this->payment_components_qr_hooks();
    4546        $this->callback_hooks();
     47        $this->validation_hooks();
    4648    }
    4749
     
    210212
    211213    /**
     214     * Register the hooks related to field validation
     215     *
     216     * @return void
     217     */
     218    public function validation_hooks(): void {
     219        $validation_service = new ValidationService();
     220        // Register AJAX action for zip code validation
     221        $this->loader->add_action( 'wp_ajax_multisafepay_validate_postcode', $validation_service, 'validate_postcode' );
     222        $this->loader->add_action( 'wp_ajax_nopriv_multisafepay_validate_postcode', $validation_service, 'validate_postcode' );
     223    }
     224
     225    /**
    212226     * Run the loader to execute the hooks with WordPress.
    213227     *
  • multisafepay/trunk/src/PaymentMethods/Base/BasePaymentMethod.php

    r3264984 r3306839  
    400400        }
    401401
    402         $settings = get_option( 'woocommerce_' . $this->id . '_settings', array( 'payment_component' => 'qr' ) );
     402        $settings = get_option( 'woocommerce_' . $this->id . '_settings', array( 'payment_component' => 'yes' ) );
    403403        if ( ! isset( $settings['payment_component'] ) ) {
    404             return true;
     404            return false;
    405405        }
    406406        return 'qr' === $settings['payment_component'];
     
    417417        }
    418418
    419         $settings = get_option( 'woocommerce_' . $this->id . '_settings', array( 'payment_component' => 'qr_only' ) );
     419        $settings = get_option( 'woocommerce_' . $this->id . '_settings', array( 'payment_component' => 'yes' ) );
    420420        if ( ! isset( $settings['payment_component'] ) ) {
    421             return true;
     421            return false;
    422422        }
    423423        return 'qr_only' === $settings['payment_component'];
     
    501501                wp_enqueue_script( 'multisafepay-validator-wallets', MULTISAFEPAY_PLUGIN_URL . '/assets/public/js/multisafepay-validator-wallets.js', array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true );
    502502                wp_enqueue_script( 'multisafepay-common-wallets', MULTISAFEPAY_PLUGIN_URL . '/assets/public/js/multisafepay-common-wallets.js', array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true );
     503                // Add parameters for validator wallets
     504                wp_localize_script(
     505                    'multisafepay-validator-wallets',
     506                    'multisafepayParams',
     507                    array(
     508                        'location' => admin_url( 'admin-ajax.php' ),
     509                        'nonce'    => wp_create_nonce( 'multisafepay_validator_nonce' ),
     510                    )
     511                );
     512
    503513                $admin_url_array = array(
    504514                    'location' => admin_url( 'admin-ajax.php' ),
  • multisafepay/trunk/src/PaymentMethods/Base/BaseRefunds.php

    r3264984 r3306839  
    5252        );
    5353
    54         /** @var RefundRequest $refund_request */
    55         $refund_request = $transaction_manager->createRefundRequest( $multisafepay_transaction );
     54        if ( $multisafepay_transaction->requiresShoppingCart() ) {
     55            /** @var RefundRequest $refund_request */
     56            $refund_request = $transaction_manager->createRefundRequest( $multisafepay_transaction );
    5657
    57         $refund_request->addDescriptionText( $reason );
    58 
    59         if ( $multisafepay_transaction->requiresShoppingCart() ) {
    6058            $refunds                 = $order->get_refunds();
    6159            $refund_merchant_item_id = reset( $refunds )->id;
     
    7270
    7371        if ( ! $multisafepay_transaction->requiresShoppingCart() ) {
     72            $refund_request = new RefundRequest();
     73            $refund_request->addDescriptionText( $reason );
    7474            $refund_request->addMoney( MoneyUtil::create_money( (float) $amount, $order->get_currency() ) );
    7575        }
     
    8080        } catch ( Exception | ClientExceptionInterface | ApiException $exception ) {
    8181            $error = __( 'Error:', 'multisafepay' ) . htmlspecialchars( $exception->getMessage() );
    82             $this->logger->log_error( $error );
    83             wc_add_notice( $error, 'error' );
     82            $this->logger->log_error( 'Error during refund: ' . $error . ' Refund request : ' . wp_json_encode( $refund_request->getData() ) );
    8483        }
    8584
  • multisafepay/trunk/src/Services/PaymentComponentService.php

    r3264984 r3306839  
    8383
    8484        // Tokenization and recurring model
    85         if ( $woocommerce_payment_gateway->is_tokenization_enabled() ) {
     85        if ( $woocommerce_payment_gateway->is_tokenization_enabled() && is_user_logged_in() ) {
    8686            $payment_component_arguments['recurring'] = array(
    8787                'model'  => 'cardOnFile',
  • multisafepay/trunk/src/Services/Qr/QrCustomerService.php

    r3264984 r3306839  
    4848        );
    4949    }
    50 
    51     /**
    52      * Get the customer IP address.
    53      *
    54      * @return string
    55      */
    56     public function get_customer_ip_address(): string {
    57         $possible_ip_sources = array(
    58             'HTTP_CLIENT_IP',
    59             'HTTP_X_FORWARDED_FOR',
    60             'REMOTE_ADDR',
    61         );
    62 
    63         foreach ( $possible_ip_sources as $source ) {
    64             if ( ! empty( $_SERVER[ $source ] ) ) {
    65                 return sanitize_text_field( wp_unslash( $_SERVER[ $source ] ) );
    66             }
    67         }
    68 
    69         return '';
    70     }
    71 
    72     /**
    73      * Get the user agent.
    74      *
    75      * @return string
    76      */
    77     public function get_user_agent(): string {
    78         return sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) );
    79     }
    8050}
  • multisafepay/trunk/src/Services/Qr/QrOrderService.php

    r3269746 r3306839  
    122122     */
    123123    public function create_payment_options( string $token ): PaymentOptions {
    124         $redirect_cancel_url = get_rest_url( get_current_blog_id(), 'multisafepay/v1/qr-balancer' ) . '?token=' . rawurlencode( $token );
     124        $redirect_cancel_url = add_query_arg( 'token', $token, get_rest_url( get_current_blog_id(), 'multisafepay/v1/qr-balancer' ) );
    125125        $payment_options     = new PaymentOptions();
    126126        $payment_options->addNotificationUrl( get_rest_url( get_current_blog_id(), 'multisafepay/v1/qr-notification' ) );
  • multisafepay/trunk/src/Services/ShoppingCartService.php

    r3264984 r3306839  
    66use MultiSafepay\Api\Transactions\OrderRequest\Arguments\ShoppingCart\Item as CartItem;
    77use MultiSafepay\Api\Transactions\OrderRequest\Arguments\ShoppingCart\ShippingItem;
     8use MultiSafepay\Exception\InvalidArgumentException;
    89use MultiSafepay\WooCommerce\Utils\Hpos;
    910use MultiSafepay\WooCommerce\Utils\Logger;
     
    4142     * @param string|null $gateway_code
    4243     * @return ShoppingCart
     44     * @throws InvalidArgumentException
    4345     */
    4446    public function create_shopping_cart( WC_Order $order, string $currency, ?string $gateway_code = '' ): ShoppingCart {
     
    162164     * @param string                 $gateway_code
    163165     * @return ShippingItem
     166     * @throws InvalidArgumentException
    164167     */
    165168    private function create_shipping_cart_item( WC_Order_Item_Shipping $item, string $currency, string $gateway_code ): ShippingItem {
    166         $cart_item = new ShippingItem();
    167         return $cart_item->addName( __( 'Shipping', 'multisafepay' ) )
     169        $cart_item            = new ShippingItem();
     170        $shipping_method_name = $item->get_method_title();
     171        if ( empty( $shipping_method_name ) ) {
     172            $shipping_method_name = __( 'Shipping', 'multisafepay' );
     173        }
     174        return $cart_item->addName( $shipping_method_name )
    168175            ->addQuantity( 1 )
    169176            ->addUnitPrice( MoneyUtil::create_money( (float) $item->get_total(), $currency ) )
     
    286293
    287294        foreach ( $allowed_rates as $rate ) {
    288             if ( abs( $tax_rate - $rate ) <= 0.15 ) {
     295            if ( abs( $tax_rate - $rate ) <= 0.05 ) {
    289296                return round( $tax_rate );
    290297            }
  • multisafepay/trunk/src/Utils/QrCheckoutManager.php

    r3264984 r3306839  
    55use WC_Cart;
    66use WC_Shipping_Rate;
     7use WC_Validation;
    78
    89/**
     
    3536
    3637    /**
    37      * Check if all mandatory fields are filled in the checkout in order to submit a MultiSafepay transaction
     38     * Check if all mandatory fields are filled in the checkout to submit a MultiSafepay transaction
    3839     * using Payment Component with QR code.
    3940     *
     
    5152
    5253        // Get required and extra fields
    53         $required_fields = $this->get_required_fields();
    54         $extra_fields    = $this->get_extra_fields();
     54        $billing_required_fields = $this->get_required_fields();
     55        $billing_extra_fields    = $this->get_extra_fields();
    5556
    5657        // Determine if shipping to a different address
     
    5859
    5960        // Get shipping fields if necessary
    60         $shipping_fields = $ship_to_different_address ?
    61             $this->get_shipping_fields( $required_fields, $extra_fields ) :
    62             array();
     61        $shipping_fields          = array();
     62        $shipping_required_fields = array();
     63
     64        if ( $ship_to_different_address ) {
     65            $shipping_fields          = $this->get_shipping_fields( $billing_required_fields, $billing_extra_fields );
     66            $shipping_required_fields = $this->get_shipping_fields( $billing_required_fields, array() );
     67        }
    6368
    6469        // Combine all fields
    65         $all_fields = array_merge( $required_fields, $extra_fields, $shipping_fields );
     70        $all_fields = array_merge( $billing_required_fields, $billing_extra_fields, $shipping_fields );
     71
     72        // Combine all required fields
     73        $all_required_fields = array_merge( $billing_required_fields, $shipping_required_fields );
    6674
    6775        // Get order fields
     
    6977
    7078        // Process and validate fields
    71         $this->process_checkout_data( $all_fields, $required_fields, $order_fields );
     79        $this->process_checkout_data( $all_fields, $all_required_fields, $order_fields );
    7280
    7381        return $this->is_validated;
     
    353361     * Get the shipping fields based on required and extra fields.
    354362     *
    355      * @param array $required_fields The required fields.
    356      * @param array $extra_fields The extra fields.
    357      * @return array
    358      */
    359     public function get_shipping_fields( array $required_fields, array $extra_fields ): array {
     363     * @param array $billing_required_fields The required fields.
     364     * @param array $billing_extra_fields The extra fields.
     365     * @return array
     366     */
     367    public function get_shipping_fields( array $billing_required_fields, array $billing_extra_fields ): array {
    360368        return array_map(
    361369            static function( $field ) {
     
    363371            },
    364372            array_filter(
    365                 array_merge( $required_fields, $extra_fields ),
     373                array_merge( $billing_required_fields, $billing_extra_fields ),
    366374                static function( $field ) {
    367375                    // Exclude email and phone fields to be created as shipping fields.
     
    376384     *
    377385     * @param array $all_fields All fields to check.
    378      * @param array $required_fields The required fields.
     386     * @param array $all_required_fields The required fields.
    379387     * @param array $order_fields The order fields.
    380388     */
    381     public function process_checkout_data( array $all_fields, array $required_fields, array $order_fields ): void {
     389    public function process_checkout_data( array $all_fields, array $all_required_fields, array $order_fields ): void {
    382390        $this->is_validated = true;
    383391
     
    386394            if ( 'billing_email' === $field ) {
    387395                $field_value = isset( $this->posted_data[ $field ] ) ? sanitize_email( wp_unslash( $this->posted_data[ $field ] ) ) : '';
     396
     397                // Verify the email format using PHP's built-in filter validation
     398                if ( ! empty( $field_value ) && ! $this->validate_email( $field_value ) ) {
     399                    $this->is_validated = false;
     400                }
     401            } elseif ( strpos( $field, '_postcode' ) !== false ) {
     402                $field_value = isset( $this->posted_data[ $field ] ) ? wp_unslash( $this->posted_data[ $field ] ) : '';
     403                $field_value = trim( wp_strip_all_tags( $field_value ) );
     404
     405                // Validate a postcode format if not empty
     406                if ( ! empty( $field_value ) ) {
     407                    $prefix  = strpos( $field, 'billing_' ) === 0 ? 'billing' : 'shipping';
     408                    $country = isset( $this->posted_data[ $prefix . '_country' ] ) ? wp_unslash( $this->posted_data[ $prefix . '_country' ] ) : '';
     409                    $country = trim( wp_strip_all_tags( $country ) );
     410
     411                    if ( ! $this->validate_postcode( $field_value, $country ) ) {
     412                        $this->is_validated = false;
     413                    }
     414                }
    388415            } else {
    389416                $field_value = isset( $this->posted_data[ $field ] ) ? wp_unslash( $this->posted_data[ $field ] ) : '';
     
    391418            }
    392419
    393             // Check if required field is empty
    394             if ( empty( $field_value ) && in_array( $field, $required_fields, true ) ) {
     420            // Check if the required field is empty
     421            if ( empty( $field_value ) && in_array( $field, $all_required_fields, true ) ) {
    395422                $this->is_validated = false;
    396423            }
     
    454481        }
    455482    }
     483
     484    /**
     485     * Validate the email address format
     486     *
     487     * @param string $email The email to validate
     488     * @return bool Whether the email is valid
     489     */
     490    private function validate_email( string $email ): bool {
     491        return (bool) filter_var( $email, FILTER_VALIDATE_EMAIL );
     492    }
     493
     494    /**
     495     * Validate a postcode format using WooCommerce's validation
     496     *
     497     * @param string $postcode The postcode to validate
     498     * @param string $country The country code
     499     * @return bool Whether the postcode is valid
     500     */
     501    private function validate_postcode( string $postcode, string $country ): bool {
     502        if ( ! WC_Validation::is_postcode( $postcode, $country ) ) {
     503            return false;
     504        }
     505
     506        return true;
     507    }
    456508}
  • multisafepay/trunk/vendor/autoload.php

    r3269746 r3306839  
    1515        }
    1616    }
    17     trigger_error(
    18         $err,
    19         E_USER_ERROR
    20     );
     17    throw new RuntimeException($err);
    2118}
    2219
    2320require_once __DIR__ . '/composer/autoload_real.php';
    2421
    25 return ComposerAutoloaderInit4c82aacc71005aae20a63510a82d5e07::getLoader();
     22return ComposerAutoloaderIniteb5be672a765e05cf872148d946d1d0b::getLoader();
  • multisafepay/trunk/vendor/composer/InstalledVersions.php

    r3230524 r3306839  
    2727class InstalledVersions
    2828{
     29    /**
     30     * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
     31     * @internal
     32     */
     33    private static $selfDir = null;
     34
    2935    /**
    3036     * @var mixed[]|null
     
    324330
    325331    /**
     332     * @return string
     333     */
     334    private static function getSelfDir()
     335    {
     336        if (self::$selfDir === null) {
     337            self::$selfDir = strtr(__DIR__, '\\', '/');
     338        }
     339
     340        return self::$selfDir;
     341    }
     342
     343    /**
    326344     * @return array[]
    327345     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
     
    337355
    338356        if (self::$canGetVendors) {
    339             $selfDir = strtr(__DIR__, '\\', '/');
     357            $selfDir = self::getSelfDir();
    340358            foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
    341359                $vendorDir = strtr($vendorDir, '\\', '/');
  • multisafepay/trunk/vendor/composer/autoload_real.php

    r3269746 r3306839  
    33// autoload_real.php @generated by Composer
    44
    5 class ComposerAutoloaderInit4c82aacc71005aae20a63510a82d5e07
     5class ComposerAutoloaderIniteb5be672a765e05cf872148d946d1d0b
    66{
    77    private static $loader;
     
    2525        require __DIR__ . '/platform_check.php';
    2626
    27         spl_autoload_register(array('ComposerAutoloaderInit4c82aacc71005aae20a63510a82d5e07', 'loadClassLoader'), true, true);
     27        spl_autoload_register(array('ComposerAutoloaderIniteb5be672a765e05cf872148d946d1d0b', 'loadClassLoader'), true, true);
    2828        self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
    29         spl_autoload_unregister(array('ComposerAutoloaderInit4c82aacc71005aae20a63510a82d5e07', 'loadClassLoader'));
     29        spl_autoload_unregister(array('ComposerAutoloaderIniteb5be672a765e05cf872148d946d1d0b', 'loadClassLoader'));
    3030
    3131        require __DIR__ . '/autoload_static.php';
    32         call_user_func(\Composer\Autoload\ComposerStaticInit4c82aacc71005aae20a63510a82d5e07::getInitializer($loader));
     32        call_user_func(\Composer\Autoload\ComposerStaticIniteb5be672a765e05cf872148d946d1d0b::getInitializer($loader));
    3333
    3434        $loader->register(true);
  • multisafepay/trunk/vendor/composer/autoload_static.php

    r3269746 r3306839  
    55namespace Composer\Autoload;
    66
    7 class ComposerStaticInit4c82aacc71005aae20a63510a82d5e07
     7class ComposerStaticIniteb5be672a765e05cf872148d946d1d0b
    88{
    99    public static $prefixLengthsPsr4 = array (
     
    6363    {
    6464        return \Closure::bind(function () use ($loader) {
    65             $loader->prefixLengthsPsr4 = ComposerStaticInit4c82aacc71005aae20a63510a82d5e07::$prefixLengthsPsr4;
    66             $loader->prefixDirsPsr4 = ComposerStaticInit4c82aacc71005aae20a63510a82d5e07::$prefixDirsPsr4;
    67             $loader->classMap = ComposerStaticInit4c82aacc71005aae20a63510a82d5e07::$classMap;
     65            $loader->prefixLengthsPsr4 = ComposerStaticIniteb5be672a765e05cf872148d946d1d0b::$prefixLengthsPsr4;
     66            $loader->prefixDirsPsr4 = ComposerStaticIniteb5be672a765e05cf872148d946d1d0b::$prefixDirsPsr4;
     67            $loader->classMap = ComposerStaticIniteb5be672a765e05cf872148d946d1d0b::$classMap;
    6868
    6969        }, null, ClassLoader::class);
  • multisafepay/trunk/vendor/composer/installed.json

    r3264984 r3306839  
    33        {
    44            "name": "multisafepay/php-sdk",
    5             "version": "5.16.0",
    6             "version_normalized": "5.16.0.0",
     5            "version": "5.17.0",
     6            "version_normalized": "5.17.0.0",
    77            "source": {
    88                "type": "git",
    99                "url": "https://github.com/MultiSafepay/php-sdk.git",
    10                 "reference": "849f1cf0c5ae23819422db4f78db948fd590c4ec"
    11             },
    12             "dist": {
    13                 "type": "zip",
    14                 "url": "https://api.github.com/repos/MultiSafepay/php-sdk/zipball/849f1cf0c5ae23819422db4f78db948fd590c4ec",
    15                 "reference": "849f1cf0c5ae23819422db4f78db948fd590c4ec",
     10                "reference": "4c46227cf3139d76ff08bc4191f06445c867798b"
     11            },
     12            "dist": {
     13                "type": "zip",
     14                "url": "https://api.github.com/repos/MultiSafepay/php-sdk/zipball/4c46227cf3139d76ff08bc4191f06445c867798b",
     15                "reference": "4c46227cf3139d76ff08bc4191f06445c867798b",
    1616                "shasum": ""
    1717            },
     
    3838                "jschaedl/iban-validation": "Adds additional IBAN validation for \\MultiSafepay\\ValueObject\\IbanNumber"
    3939            },
    40             "time": "2025-03-19T15:29:14+00:00",
     40            "time": "2025-06-04T13:12:21+00:00",
    4141            "type": "library",
    4242            "installation-source": "dist",
     
    5353            "support": {
    5454                "issues": "https://github.com/MultiSafepay/php-sdk/issues",
    55                 "source": "https://github.com/MultiSafepay/php-sdk/tree/5.16.0"
     55                "source": "https://github.com/MultiSafepay/php-sdk/tree/5.17.0"
    5656            },
    5757            "install-path": "../multisafepay/php-sdk"
  • multisafepay/trunk/vendor/composer/installed.php

    r3269746 r3306839  
    22    'root' => array(
    33        'name' => 'multisafepay/woocommerce',
    4         'pretty_version' => '6.8.3',
    5         'version' => '6.8.3.0',
     4        'pretty_version' => '6.9.0',
     5        'version' => '6.9.0.0',
    66        'reference' => null,
    77        'type' => 'wordpress-plugin',
     
    1212    'versions' => array(
    1313        'multisafepay/php-sdk' => array(
    14             'pretty_version' => '5.16.0',
    15             'version' => '5.16.0.0',
    16             'reference' => '849f1cf0c5ae23819422db4f78db948fd590c4ec',
     14            'pretty_version' => '5.17.0',
     15            'version' => '5.17.0.0',
     16            'reference' => '4c46227cf3139d76ff08bc4191f06445c867798b',
    1717            'type' => 'library',
    1818            'install_path' => __DIR__ . '/../multisafepay/php-sdk',
     
    2121        ),
    2222        'multisafepay/woocommerce' => array(
    23             'pretty_version' => '6.8.3',
    24             'version' => '6.8.3.0',
     23            'pretty_version' => '6.9.0',
     24            'version' => '6.9.0.0',
    2525            'reference' => null,
    2626            'type' => 'wordpress-plugin',
  • multisafepay/trunk/vendor/multisafepay/php-sdk/CHANGELOG.md

    r3264984 r3306839  
    66
    77## [Unreleased]
     8
     9## [5.17.0] - 2025-06-04
     10### Added
     11- PHPSDK-172: Add BILLINK to SHOPPING_CART_REQUIRED_GATEWAYS constant
     12
     13### Fixed
     14- PHPSDK-173: Fix typo in \MultiSafepay\Api\Transactions\OrderRequest\Arguments\GatewayInfo\Creditcard object
     15- PHPSDK-174: Fix UnitPrice sometimes having more than 10 decimals
    816
    917## [5.16.0] - 2025-03-19
  • multisafepay/trunk/vendor/multisafepay/php-sdk/composer.json

    r3264984 r3306839  
    44  "type": "library",
    55  "license": "MIT",
    6   "version": "5.16.0",
     6  "version": "5.17.0",
    77  "require": {
    88    "php": "^7.2|^8.0",
  • multisafepay/trunk/vendor/multisafepay/php-sdk/src/Api/Transactions/Gateways.php

    r3072171 r3306839  
    2727        'BNPL_OB',
    2828        'BNPL_MF',
     29        'BILLINK',
    2930    );
    3031}
  • multisafepay/trunk/vendor/multisafepay/php-sdk/src/Api/Transactions/OrderRequest/Arguments/GatewayInfo/Creditcard.php

    r3050467 r3306839  
    148148        return [
    149149            'card_number' => $this->cardNumber ? $this->cardNumber->get() : null,
    150             'cart_holder_name' => $this->cardHolderName,
    151             'cart_expiry_date' => $this->cardExpiryDate ? $this->cardExpiryDate->get('my') : null,
     150            'card_holder_name' => $this->cardHolderName,
     151            'card_expiry_date' => $this->cardExpiryDate ? $this->cardExpiryDate->get('ym') : null,
    152152            'cvc' => $this->cvc ? $this->cvc->get() : null,
    153153            'card_cvc' => $this->cvc ? $this->cvc->get() : null,
  • multisafepay/trunk/vendor/multisafepay/php-sdk/src/Util/Version.php

    r3264984 r3306839  
    1818class Version
    1919{
    20     public const SDK_VERSION = '5.16.0';
     20    public const SDK_VERSION = '5.17.0';
    2121
    2222    /**
  • multisafepay/trunk/vendor/multisafepay/php-sdk/src/ValueObject/CartItem.php

    r3264984 r3306839  
    244244    {
    245245        if ($this->unitPrice) {
    246             return $this->unitPrice->getAmount() / 100;
    247         }
    248 
    249         return $this->unitPriceValue->get() ?? 0.0;
     246            return round($this->unitPrice->getAmount() / 100, 10);
     247        }
     248
     249        return round(($this->unitPriceValue->get() ?? 0.0), 10);
    250250    }
    251251
  • multisafepay/trunk/vendor/multisafepay/php-sdk/src/ValueObject/UnitPrice.php

    r3230524 r3306839  
    1515
    1616    /**
    17      * Should be given in full units excluding tax, preferably including all decimal places, e.g. 3.305785124
     17     * Should be given in full units excluding tax, preferably including 10 decimal at most, e.g. 3.305785124
    1818     *
    1919     * @param float $unitPrice
Note: See TracChangeset for help on using the changeset viewer.