Changeset 3306839
- Timestamp:
- 06/05/2025 06:43:09 AM (10 months ago)
- Location:
- multisafepay
- Files:
-
- 2 added
- 54 edited
- 1 copied
-
tags/6.9.0 (copied) (copied from multisafepay/trunk)
-
tags/6.9.0/assets/public/js/multisafepay-apple-pay-wallet.js (modified) (8 diffs)
-
tags/6.9.0/assets/public/js/multisafepay-google-pay-wallet.js (modified) (1 diff)
-
tags/6.9.0/assets/public/js/multisafepay-jquery-wallets.js (modified) (2 diffs)
-
tags/6.9.0/assets/public/js/multisafepay-validator-wallets.js (modified) (13 diffs)
-
tags/6.9.0/multisafepay.php (modified) (3 diffs)
-
tags/6.9.0/readme.txt (modified) (2 diffs)
-
tags/6.9.0/src/Main.php (modified) (3 diffs)
-
tags/6.9.0/src/PaymentMethods/Base/BasePaymentMethod.php (modified) (3 diffs)
-
tags/6.9.0/src/PaymentMethods/Base/BaseRefunds.php (modified) (3 diffs)
-
tags/6.9.0/src/Services/PaymentComponentService.php (modified) (1 diff)
-
tags/6.9.0/src/Services/Qr/QrCustomerService.php (modified) (1 diff)
-
tags/6.9.0/src/Services/Qr/QrOrderService.php (modified) (1 diff)
-
tags/6.9.0/src/Services/ShoppingCartService.php (modified) (4 diffs)
-
tags/6.9.0/src/Services/ValidationService.php (added)
-
tags/6.9.0/src/Utils/QrCheckoutManager.php (modified) (11 diffs)
-
tags/6.9.0/vendor/autoload.php (modified) (1 diff)
-
tags/6.9.0/vendor/composer/InstalledVersions.php (modified) (3 diffs)
-
tags/6.9.0/vendor/composer/autoload_real.php (modified) (2 diffs)
-
tags/6.9.0/vendor/composer/autoload_static.php (modified) (2 diffs)
-
tags/6.9.0/vendor/composer/installed.json (modified) (3 diffs)
-
tags/6.9.0/vendor/composer/installed.php (modified) (3 diffs)
-
tags/6.9.0/vendor/multisafepay/php-sdk/CHANGELOG.md (modified) (1 diff)
-
tags/6.9.0/vendor/multisafepay/php-sdk/composer.json (modified) (1 diff)
-
tags/6.9.0/vendor/multisafepay/php-sdk/src/Api/Transactions/Gateways.php (modified) (1 diff)
-
tags/6.9.0/vendor/multisafepay/php-sdk/src/Api/Transactions/OrderRequest/Arguments/GatewayInfo/Creditcard.php (modified) (1 diff)
-
tags/6.9.0/vendor/multisafepay/php-sdk/src/Util/Version.php (modified) (1 diff)
-
tags/6.9.0/vendor/multisafepay/php-sdk/src/ValueObject/CartItem.php (modified) (1 diff)
-
tags/6.9.0/vendor/multisafepay/php-sdk/src/ValueObject/UnitPrice.php (modified) (1 diff)
-
trunk/assets/public/js/multisafepay-apple-pay-wallet.js (modified) (8 diffs)
-
trunk/assets/public/js/multisafepay-google-pay-wallet.js (modified) (1 diff)
-
trunk/assets/public/js/multisafepay-jquery-wallets.js (modified) (2 diffs)
-
trunk/assets/public/js/multisafepay-validator-wallets.js (modified) (13 diffs)
-
trunk/multisafepay.php (modified) (3 diffs)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/src/Main.php (modified) (3 diffs)
-
trunk/src/PaymentMethods/Base/BasePaymentMethod.php (modified) (3 diffs)
-
trunk/src/PaymentMethods/Base/BaseRefunds.php (modified) (3 diffs)
-
trunk/src/Services/PaymentComponentService.php (modified) (1 diff)
-
trunk/src/Services/Qr/QrCustomerService.php (modified) (1 diff)
-
trunk/src/Services/Qr/QrOrderService.php (modified) (1 diff)
-
trunk/src/Services/ShoppingCartService.php (modified) (4 diffs)
-
trunk/src/Services/ValidationService.php (added)
-
trunk/src/Utils/QrCheckoutManager.php (modified) (11 diffs)
-
trunk/vendor/autoload.php (modified) (1 diff)
-
trunk/vendor/composer/InstalledVersions.php (modified) (3 diffs)
-
trunk/vendor/composer/autoload_real.php (modified) (2 diffs)
-
trunk/vendor/composer/autoload_static.php (modified) (2 diffs)
-
trunk/vendor/composer/installed.json (modified) (3 diffs)
-
trunk/vendor/composer/installed.php (modified) (3 diffs)
-
trunk/vendor/multisafepay/php-sdk/CHANGELOG.md (modified) (1 diff)
-
trunk/vendor/multisafepay/php-sdk/composer.json (modified) (1 diff)
-
trunk/vendor/multisafepay/php-sdk/src/Api/Transactions/Gateways.php (modified) (1 diff)
-
trunk/vendor/multisafepay/php-sdk/src/Api/Transactions/OrderRequest/Arguments/GatewayInfo/Creditcard.php (modified) (1 diff)
-
trunk/vendor/multisafepay/php-sdk/src/Util/Version.php (modified) (1 diff)
-
trunk/vendor/multisafepay/php-sdk/src/ValueObject/CartItem.php (modified) (1 diff)
-
trunk/vendor/multisafepay/php-sdk/src/ValueObject/UnitPrice.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
multisafepay/tags/6.9.0/assets/public/js/multisafepay-apple-pay-wallet.js
r3048898 r3306839 62 62 this._sessionActive = false; 63 63 64 /** 65 * Flag to track if a session has been aborted 66 * 67 * @type {boolean} 68 * @private 69 */ 70 this._sessionAborted = false; 71 64 72 this.init() 65 73 .then( … … 174 182 * Event handler for Apple Pay button click 175 183 * 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 179 196 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 246 198 const paymentRequest = { 247 199 countryCode: configApplePay.countryCode, … … 258 210 }; 259 211 260 // Create the session and handle the events212 // Create the session immediately in the click handler 261 213 const session = new ApplePaySession( this.config.applePayVersion, paymentRequest ); 214 262 215 if ( session ) { 216 // Reset the aborted flag 217 this._sessionAborted = false; 218 219 // Setup event handlers 263 220 session.onvalidatemerchant = ( event ) => this.handleValidateMerchant( event, session ); 264 221 session.onpaymentauthorized = ( event ) => this.handlePaymentAuthorized( event, session ); 265 222 session.oncancel = ( event ) => this.handleCancel( event, session ); 266 session.begin(); 267 223 224 // Set session as active 268 225 this.setSessionStatus( true ); 226 227 // Validate fields before beginning the session 228 this.validateFieldsBeforeSession( session ); 269 229 } 270 230 } catch ( error ) { 271 console.error( 'Error starting Apple Pay session :', error );231 console.error( 'Error starting Apple Pay session when button is clicked:', error ); 272 232 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 ); 300 273 } 301 274 … … 309 282 handleValidateMerchant = async( event, session ) => { 310 283 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 311 290 const validationURL = event.validationURL; 312 291 const originDomain = window.location.hostname; … … 314 293 const merchantSession = await this.fetchMerchantSession( validationURL, originDomain ); 315 294 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 } 317 299 } else { 318 300 debugDirect( 'Error validating merchant', this.debug ); 301 this._sessionAborted = true; 319 302 session.abort(); 320 303 } 321 304 } catch ( error ) { 322 305 console.error( 'Error validating merchant:', error ); 306 this._sessionAborted = true; 323 307 session.abort(); 324 308 } … … 336 320 handlePaymentAuthorized = async( event, session ) => { 337 321 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 338 328 const paymentToken = JSON.stringify( event.payment.token ); 339 329 const success = await this.submitApplePayForm( paymentToken ); … … 361 351 try { 362 352 debugDirect( 'Apple Pay Direct session successfully aborted.', this.debug, 'log' ); 353 this._sessionAborted = true; 363 354 session.abort(); 364 355 } catch ( error ) { … … 410 401 return true; 411 402 } 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 } 412 465 } -
multisafepay/tags/6.9.0/assets/public/js/multisafepay-google-pay-wallet.js
r3048898 r3306839 222 222 { 223 223 const validatorInstance = new FieldsValidator(); 224 const fieldsAreValid = validatorInstance.checkFields();224 const fieldsAreValid = await validatorInstance.checkFields(); 225 225 if ( fieldsAreValid ) { 226 226 if ( paymentsClient && paymentsClient.loadPaymentData ) { -
multisafepay/tags/6.9.0/assets/public/js/multisafepay-jquery-wallets.js
r3048898 r3306839 44 44 function() { 45 45 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' ); 47 49 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' ); 49 63 } 50 64 validatorInstance.removeErrorMessage( this.name ); … … 167 181 // Remove the orphan error messages from the notice group 168 182 validatorInstance.removeOrphanErrorMessages(); 183 // Re-initialize select2 validation after checkout is updated 184 select2Validation(); 169 185 } 170 186 ); -
multisafepay/tags/6.9.0/assets/public/js/multisafepay-validator-wallets.js
r3048898 r3306839 20 20 */ 21 21 22 // Check if the debugDirect function is available, if not define it 23 if ( 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 22 37 /** 23 38 * Class to validate the fields in the checkout form … … 32 47 33 48 /** 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 /** 34 141 * Get the label text for a field 35 142 * … … 38 145 */ 39 146 getLabelText( fieldName ) { 40 // Skip if this field has already been processed41 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]; 43 150 } 44 151 … … 72 179 this.processedFields.push( fieldName ); 73 180 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 ) ); 76 426 } 77 427 … … 80 430 * 81 431 * @param element - The element to be validated 432 * @returns {void} 82 433 */ 83 434 realtimeValidation( element ) { 84 const fieldName = element.name.trim(); 435 const fieldName = element.name.trim(); 436 const fieldValue = element.value.trim(); 85 437 86 438 // Remove this field from processedFields if it's there … … 90 442 } 91 443 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 93 474 this.removeErrorMessage( fieldName, true ); 475 this.removeInlineError( fieldName ); 94 476 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() { 118 519 // Getting the customer details element which includes all the user fields 119 520 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' 121 526 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 128 528 selectWrappers.forEach( 129 529 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' ); 133 532 elements.forEach( 134 533 element => { 135 // Remove existing listener and add a new one136 element.removeEventListener( 'input', event => this.realtimeValidation( event.target ) );137 element.addEventListener( 'input', event => this.realtimeValidation( event.target ) );138 534 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 ); 155 542 } 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 ); 158 550 } 159 551 } … … 161 553 } 162 554 ); 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 } 163 785 164 786 // Scroll to the previously added notice group area if there are any errors … … 166 788 this.scrollToElement( '.entry-content' ); 167 789 } 790 791 debugDirect( 'Final validation result: ' + ( allFieldsValid ? 'Valid' : 'Invalid' ), debugStatus, 'log' ); 168 792 return allFieldsValid; 169 793 } 170 794 171 795 /** 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 /** 172 814 * Scroll to a specific class element in the document. 173 815 * 174 816 * @param {string} className - The class name of the element to scroll to. 817 * @returns {void} 175 818 */ 176 819 scrollToElement( className ) { … … 204 847 } 205 848 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 206 855 // 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 <> 208 857 let noticeGroup = document.querySelector( '.woocommerce-NoticeGroup-checkout' ); 209 858 if ( ! noticeGroup ) { … … 215 864 noticeBanner.className = 'woocommerce-error'; 216 865 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 );230 866 noticeGroup.appendChild( noticeBanner ); 231 867 } 232 868 233 869 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 236 876 const existingErrorItem = ulList.querySelector( 'li[data-id="' + error[0].field + '"]' ); 237 877 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 } 239 887 } 240 888 … … 242 890 const errorItem = document.createElement( 'li' ); 243 891 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 245 900 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 251 925 // Add the error message to the notice group 252 926 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'; 253 943 254 944 debugDirect( ( isRealtimeValidation ? '"Interactively" a' : 'A' ) + 'dding error message in the notice group for field: ' + error[0].field, debugStatus, 'log' ); … … 287 977 // Getting the field name associated with the error message 288 978 const fieldElement = document.querySelector( '[name="' + fieldName + '"]' ); 289 // Check if the fieldElement exists and is not hidden using CSS290 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 ); 291 981 if ( ! isFieldVisible ) { 292 982 errorItem.remove(); … … 307 997 removeEntireNoticeGroup() { 308 998 // 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 ) { 313 1004 noticeGroup.remove(); 314 1005 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 5 5 * Plugin URI: https://docs.multisafepay.com/docs/woocommerce 6 6 * Description: MultiSafepay Payment Plugin 7 * Version: 6. 8.37 * Version: 6.9.0 8 8 * Author: MultiSafepay 9 9 * Author URI: https://www.multisafepay.com … … 12 12 * License URI: http://www.gnu.org/licenses/gpl-3.0.html 13 13 * Requires at least: 6.0 14 * Tested up to: 6. 7.214 * Tested up to: 6.8.1 15 15 * WC requires at least: 6.0.0 16 * WC tested up to: 9. 7.116 * WC tested up to: 9.8.5 17 17 * Requires PHP: 7.3 18 18 * Text Domain: multisafepay … … 27 27 * Plugin version 28 28 */ 29 define( 'MULTISAFEPAY_PLUGIN_VERSION', '6. 8.3' );29 define( 'MULTISAFEPAY_PLUGIN_VERSION', '6.9.0' ); 30 30 31 31 /** -
multisafepay/tags/6.9.0/readme.txt
r3269746 r3306839 3 3 Tags: multisafepay, payment gateway, credit cards, ideal, bnpl 4 4 Requires at least: 6.0 5 Tested up to: 6. 7.25 Tested up to: 6.8.1 6 6 Requires PHP: 7.3 7 Stable tag: 6. 8.37 Stable tag: 6.9.0 8 8 License: MIT 9 9 … … 139 139 140 140 == 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 141 159 = Release Notes - WooCommerce 6.8.3 (Apr 9th, 2025) = 142 160 -
multisafepay/tags/6.9.0/src/Main.php
r3264984 r3306839 8 8 use MultiSafepay\WooCommerce\Services\Qr\QrPaymentComponentService; 9 9 use MultiSafepay\WooCommerce\Services\Qr\QrPaymentWebhook; 10 use MultiSafepay\WooCommerce\Services\ValidationService; 10 11 use MultiSafepay\WooCommerce\Settings\SettingsController; 11 12 use MultiSafepay\WooCommerce\Settings\ThirdPartyCompatibility; … … 44 45 $this->payment_components_qr_hooks(); 45 46 $this->callback_hooks(); 47 $this->validation_hooks(); 46 48 } 47 49 … … 210 212 211 213 /** 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 /** 212 226 * Run the loader to execute the hooks with WordPress. 213 227 * -
multisafepay/tags/6.9.0/src/PaymentMethods/Base/BasePaymentMethod.php
r3264984 r3306839 400 400 } 401 401 402 $settings = get_option( 'woocommerce_' . $this->id . '_settings', array( 'payment_component' => ' qr' ) );402 $settings = get_option( 'woocommerce_' . $this->id . '_settings', array( 'payment_component' => 'yes' ) ); 403 403 if ( ! isset( $settings['payment_component'] ) ) { 404 return true;404 return false; 405 405 } 406 406 return 'qr' === $settings['payment_component']; … … 417 417 } 418 418 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' ) ); 420 420 if ( ! isset( $settings['payment_component'] ) ) { 421 return true;421 return false; 422 422 } 423 423 return 'qr_only' === $settings['payment_component']; … … 501 501 wp_enqueue_script( 'multisafepay-validator-wallets', MULTISAFEPAY_PLUGIN_URL . '/assets/public/js/multisafepay-validator-wallets.js', array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true ); 502 502 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 503 513 $admin_url_array = array( 504 514 'location' => admin_url( 'admin-ajax.php' ), -
multisafepay/tags/6.9.0/src/PaymentMethods/Base/BaseRefunds.php
r3264984 r3306839 52 52 ); 53 53 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 ); 56 57 57 $refund_request->addDescriptionText( $reason );58 59 if ( $multisafepay_transaction->requiresShoppingCart() ) {60 58 $refunds = $order->get_refunds(); 61 59 $refund_merchant_item_id = reset( $refunds )->id; … … 72 70 73 71 if ( ! $multisafepay_transaction->requiresShoppingCart() ) { 72 $refund_request = new RefundRequest(); 73 $refund_request->addDescriptionText( $reason ); 74 74 $refund_request->addMoney( MoneyUtil::create_money( (float) $amount, $order->get_currency() ) ); 75 75 } … … 80 80 } catch ( Exception | ClientExceptionInterface | ApiException $exception ) { 81 81 $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() ) ); 84 83 } 85 84 -
multisafepay/tags/6.9.0/src/Services/PaymentComponentService.php
r3264984 r3306839 83 83 84 84 // Tokenization and recurring model 85 if ( $woocommerce_payment_gateway->is_tokenization_enabled() ) {85 if ( $woocommerce_payment_gateway->is_tokenization_enabled() && is_user_logged_in() ) { 86 86 $payment_component_arguments['recurring'] = array( 87 87 'model' => 'cardOnFile', -
multisafepay/tags/6.9.0/src/Services/Qr/QrCustomerService.php
r3264984 r3306839 48 48 ); 49 49 } 50 51 /**52 * Get the customer IP address.53 *54 * @return string55 */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 string76 */77 public function get_user_agent(): string {78 return sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) );79 }80 50 } -
multisafepay/tags/6.9.0/src/Services/Qr/QrOrderService.php
r3269746 r3306839 122 122 */ 123 123 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' ) ); 125 125 $payment_options = new PaymentOptions(); 126 126 $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 6 6 use MultiSafepay\Api\Transactions\OrderRequest\Arguments\ShoppingCart\Item as CartItem; 7 7 use MultiSafepay\Api\Transactions\OrderRequest\Arguments\ShoppingCart\ShippingItem; 8 use MultiSafepay\Exception\InvalidArgumentException; 8 9 use MultiSafepay\WooCommerce\Utils\Hpos; 9 10 use MultiSafepay\WooCommerce\Utils\Logger; … … 41 42 * @param string|null $gateway_code 42 43 * @return ShoppingCart 44 * @throws InvalidArgumentException 43 45 */ 44 46 public function create_shopping_cart( WC_Order $order, string $currency, ?string $gateway_code = '' ): ShoppingCart { … … 162 164 * @param string $gateway_code 163 165 * @return ShippingItem 166 * @throws InvalidArgumentException 164 167 */ 165 168 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 ) 168 175 ->addQuantity( 1 ) 169 176 ->addUnitPrice( MoneyUtil::create_money( (float) $item->get_total(), $currency ) ) … … 286 293 287 294 foreach ( $allowed_rates as $rate ) { 288 if ( abs( $tax_rate - $rate ) <= 0. 15 ) {295 if ( abs( $tax_rate - $rate ) <= 0.05 ) { 289 296 return round( $tax_rate ); 290 297 } -
multisafepay/tags/6.9.0/src/Utils/QrCheckoutManager.php
r3264984 r3306839 5 5 use WC_Cart; 6 6 use WC_Shipping_Rate; 7 use WC_Validation; 7 8 8 9 /** … … 35 36 36 37 /** 37 * Check if all mandatory fields are filled in the checkout in orderto submit a MultiSafepay transaction38 * Check if all mandatory fields are filled in the checkout to submit a MultiSafepay transaction 38 39 * using Payment Component with QR code. 39 40 * … … 51 52 52 53 // 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(); 55 56 56 57 // Determine if shipping to a different address … … 58 59 59 60 // 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 } 63 68 64 69 // 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 ); 66 74 67 75 // Get order fields … … 69 77 70 78 // 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 ); 72 80 73 81 return $this->is_validated; … … 353 361 * Get the shipping fields based on required and extra fields. 354 362 * 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 { 360 368 return array_map( 361 369 static function( $field ) { … … 363 371 }, 364 372 array_filter( 365 array_merge( $ required_fields, $extra_fields ),373 array_merge( $billing_required_fields, $billing_extra_fields ), 366 374 static function( $field ) { 367 375 // Exclude email and phone fields to be created as shipping fields. … … 376 384 * 377 385 * @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. 379 387 * @param array $order_fields The order fields. 380 388 */ 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 { 382 390 $this->is_validated = true; 383 391 … … 386 394 if ( 'billing_email' === $field ) { 387 395 $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 } 388 415 } else { 389 416 $field_value = isset( $this->posted_data[ $field ] ) ? wp_unslash( $this->posted_data[ $field ] ) : ''; … … 391 418 } 392 419 393 // Check if required field is empty394 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 ) ) { 395 422 $this->is_validated = false; 396 423 } … … 454 481 } 455 482 } 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 } 456 508 } -
multisafepay/tags/6.9.0/vendor/autoload.php
r3269746 r3306839 15 15 } 16 16 } 17 trigger_error( 18 $err, 19 E_USER_ERROR 20 ); 17 throw new RuntimeException($err); 21 18 } 22 19 23 20 require_once __DIR__ . '/composer/autoload_real.php'; 24 21 25 return ComposerAutoloaderInit 4c82aacc71005aae20a63510a82d5e07::getLoader();22 return ComposerAutoloaderIniteb5be672a765e05cf872148d946d1d0b::getLoader(); -
multisafepay/tags/6.9.0/vendor/composer/InstalledVersions.php
r3230524 r3306839 27 27 class InstalledVersions 28 28 { 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 29 35 /** 30 36 * @var mixed[]|null … … 324 330 325 331 /** 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 /** 326 344 * @return array[] 327 345 * @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[]}>}> … … 337 355 338 356 if (self::$canGetVendors) { 339 $selfDir = s trtr(__DIR__, '\\', '/');357 $selfDir = self::getSelfDir(); 340 358 foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { 341 359 $vendorDir = strtr($vendorDir, '\\', '/'); -
multisafepay/tags/6.9.0/vendor/composer/autoload_real.php
r3269746 r3306839 3 3 // autoload_real.php @generated by Composer 4 4 5 class ComposerAutoloaderInit 4c82aacc71005aae20a63510a82d5e075 class ComposerAutoloaderIniteb5be672a765e05cf872148d946d1d0b 6 6 { 7 7 private static $loader; … … 25 25 require __DIR__ . '/platform_check.php'; 26 26 27 spl_autoload_register(array('ComposerAutoloaderInit 4c82aacc71005aae20a63510a82d5e07', 'loadClassLoader'), true, true);27 spl_autoload_register(array('ComposerAutoloaderIniteb5be672a765e05cf872148d946d1d0b', 'loadClassLoader'), true, true); 28 28 self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); 29 spl_autoload_unregister(array('ComposerAutoloaderInit 4c82aacc71005aae20a63510a82d5e07', 'loadClassLoader'));29 spl_autoload_unregister(array('ComposerAutoloaderIniteb5be672a765e05cf872148d946d1d0b', 'loadClassLoader')); 30 30 31 31 require __DIR__ . '/autoload_static.php'; 32 call_user_func(\Composer\Autoload\ComposerStaticInit 4c82aacc71005aae20a63510a82d5e07::getInitializer($loader));32 call_user_func(\Composer\Autoload\ComposerStaticIniteb5be672a765e05cf872148d946d1d0b::getInitializer($loader)); 33 33 34 34 $loader->register(true); -
multisafepay/tags/6.9.0/vendor/composer/autoload_static.php
r3269746 r3306839 5 5 namespace Composer\Autoload; 6 6 7 class ComposerStaticInit 4c82aacc71005aae20a63510a82d5e077 class ComposerStaticIniteb5be672a765e05cf872148d946d1d0b 8 8 { 9 9 public static $prefixLengthsPsr4 = array ( … … 63 63 { 64 64 return \Closure::bind(function () use ($loader) { 65 $loader->prefixLengthsPsr4 = ComposerStaticInit 4c82aacc71005aae20a63510a82d5e07::$prefixLengthsPsr4;66 $loader->prefixDirsPsr4 = ComposerStaticInit 4c82aacc71005aae20a63510a82d5e07::$prefixDirsPsr4;67 $loader->classMap = ComposerStaticInit 4c82aacc71005aae20a63510a82d5e07::$classMap;65 $loader->prefixLengthsPsr4 = ComposerStaticIniteb5be672a765e05cf872148d946d1d0b::$prefixLengthsPsr4; 66 $loader->prefixDirsPsr4 = ComposerStaticIniteb5be672a765e05cf872148d946d1d0b::$prefixDirsPsr4; 67 $loader->classMap = ComposerStaticIniteb5be672a765e05cf872148d946d1d0b::$classMap; 68 68 69 69 }, null, ClassLoader::class); -
multisafepay/tags/6.9.0/vendor/composer/installed.json
r3264984 r3306839 3 3 { 4 4 "name": "multisafepay/php-sdk", 5 "version": "5.1 6.0",6 "version_normalized": "5.1 6.0.0",5 "version": "5.17.0", 6 "version_normalized": "5.17.0.0", 7 7 "source": { 8 8 "type": "git", 9 9 "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", 16 16 "shasum": "" 17 17 }, … … 38 38 "jschaedl/iban-validation": "Adds additional IBAN validation for \\MultiSafepay\\ValueObject\\IbanNumber" 39 39 }, 40 "time": "2025-0 3-19T15:29:14+00:00",40 "time": "2025-06-04T13:12:21+00:00", 41 41 "type": "library", 42 42 "installation-source": "dist", … … 53 53 "support": { 54 54 "issues": "https://github.com/MultiSafepay/php-sdk/issues", 55 "source": "https://github.com/MultiSafepay/php-sdk/tree/5.1 6.0"55 "source": "https://github.com/MultiSafepay/php-sdk/tree/5.17.0" 56 56 }, 57 57 "install-path": "../multisafepay/php-sdk" -
multisafepay/tags/6.9.0/vendor/composer/installed.php
r3269746 r3306839 2 2 'root' => array( 3 3 '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', 6 6 'reference' => null, 7 7 'type' => 'wordpress-plugin', … … 12 12 'versions' => array( 13 13 'multisafepay/php-sdk' => array( 14 'pretty_version' => '5.1 6.0',15 'version' => '5.1 6.0.0',16 'reference' => ' 849f1cf0c5ae23819422db4f78db948fd590c4ec',14 'pretty_version' => '5.17.0', 15 'version' => '5.17.0.0', 16 'reference' => '4c46227cf3139d76ff08bc4191f06445c867798b', 17 17 'type' => 'library', 18 18 'install_path' => __DIR__ . '/../multisafepay/php-sdk', … … 21 21 ), 22 22 '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', 25 25 'reference' => null, 26 26 'type' => 'wordpress-plugin', -
multisafepay/tags/6.9.0/vendor/multisafepay/php-sdk/CHANGELOG.md
r3264984 r3306839 6 6 7 7 ## [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 8 16 9 17 ## [5.16.0] - 2025-03-19 -
multisafepay/tags/6.9.0/vendor/multisafepay/php-sdk/composer.json
r3264984 r3306839 4 4 "type": "library", 5 5 "license": "MIT", 6 "version": "5.1 6.0",6 "version": "5.17.0", 7 7 "require": { 8 8 "php": "^7.2|^8.0", -
multisafepay/tags/6.9.0/vendor/multisafepay/php-sdk/src/Api/Transactions/Gateways.php
r3072171 r3306839 27 27 'BNPL_OB', 28 28 'BNPL_MF', 29 'BILLINK', 29 30 ); 30 31 } -
multisafepay/tags/6.9.0/vendor/multisafepay/php-sdk/src/Api/Transactions/OrderRequest/Arguments/GatewayInfo/Creditcard.php
r3050467 r3306839 148 148 return [ 149 149 'card_number' => $this->cardNumber ? $this->cardNumber->get() : null, 150 'car t_holder_name' => $this->cardHolderName,151 'car t_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, 152 152 'cvc' => $this->cvc ? $this->cvc->get() : null, 153 153 'card_cvc' => $this->cvc ? $this->cvc->get() : null, -
multisafepay/tags/6.9.0/vendor/multisafepay/php-sdk/src/Util/Version.php
r3264984 r3306839 18 18 class Version 19 19 { 20 public const SDK_VERSION = '5.1 6.0';20 public const SDK_VERSION = '5.17.0'; 21 21 22 22 /** -
multisafepay/tags/6.9.0/vendor/multisafepay/php-sdk/src/ValueObject/CartItem.php
r3264984 r3306839 244 244 { 245 245 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); 250 250 } 251 251 -
multisafepay/tags/6.9.0/vendor/multisafepay/php-sdk/src/ValueObject/UnitPrice.php
r3230524 r3306839 15 15 16 16 /** 17 * Should be given in full units excluding tax, preferably including all decimal places, e.g. 3.30578512417 * Should be given in full units excluding tax, preferably including 10 decimal at most, e.g. 3.305785124 18 18 * 19 19 * @param float $unitPrice -
multisafepay/trunk/assets/public/js/multisafepay-apple-pay-wallet.js
r3048898 r3306839 62 62 this._sessionActive = false; 63 63 64 /** 65 * Flag to track if a session has been aborted 66 * 67 * @type {boolean} 68 * @private 69 */ 70 this._sessionAborted = false; 71 64 72 this.init() 65 73 .then( … … 174 182 * Event handler for Apple Pay button click 175 183 * 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 179 196 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 246 198 const paymentRequest = { 247 199 countryCode: configApplePay.countryCode, … … 258 210 }; 259 211 260 // Create the session and handle the events212 // Create the session immediately in the click handler 261 213 const session = new ApplePaySession( this.config.applePayVersion, paymentRequest ); 214 262 215 if ( session ) { 216 // Reset the aborted flag 217 this._sessionAborted = false; 218 219 // Setup event handlers 263 220 session.onvalidatemerchant = ( event ) => this.handleValidateMerchant( event, session ); 264 221 session.onpaymentauthorized = ( event ) => this.handlePaymentAuthorized( event, session ); 265 222 session.oncancel = ( event ) => this.handleCancel( event, session ); 266 session.begin(); 267 223 224 // Set session as active 268 225 this.setSessionStatus( true ); 226 227 // Validate fields before beginning the session 228 this.validateFieldsBeforeSession( session ); 269 229 } 270 230 } catch ( error ) { 271 console.error( 'Error starting Apple Pay session :', error );231 console.error( 'Error starting Apple Pay session when button is clicked:', error ); 272 232 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 ); 300 273 } 301 274 … … 309 282 handleValidateMerchant = async( event, session ) => { 310 283 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 311 290 const validationURL = event.validationURL; 312 291 const originDomain = window.location.hostname; … … 314 293 const merchantSession = await this.fetchMerchantSession( validationURL, originDomain ); 315 294 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 } 317 299 } else { 318 300 debugDirect( 'Error validating merchant', this.debug ); 301 this._sessionAborted = true; 319 302 session.abort(); 320 303 } 321 304 } catch ( error ) { 322 305 console.error( 'Error validating merchant:', error ); 306 this._sessionAborted = true; 323 307 session.abort(); 324 308 } … … 336 320 handlePaymentAuthorized = async( event, session ) => { 337 321 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 338 328 const paymentToken = JSON.stringify( event.payment.token ); 339 329 const success = await this.submitApplePayForm( paymentToken ); … … 361 351 try { 362 352 debugDirect( 'Apple Pay Direct session successfully aborted.', this.debug, 'log' ); 353 this._sessionAborted = true; 363 354 session.abort(); 364 355 } catch ( error ) { … … 410 401 return true; 411 402 } 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 } 412 465 } -
multisafepay/trunk/assets/public/js/multisafepay-google-pay-wallet.js
r3048898 r3306839 222 222 { 223 223 const validatorInstance = new FieldsValidator(); 224 const fieldsAreValid = validatorInstance.checkFields();224 const fieldsAreValid = await validatorInstance.checkFields(); 225 225 if ( fieldsAreValid ) { 226 226 if ( paymentsClient && paymentsClient.loadPaymentData ) { -
multisafepay/trunk/assets/public/js/multisafepay-jquery-wallets.js
r3048898 r3306839 44 44 function() { 45 45 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' ); 47 49 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' ); 49 63 } 50 64 validatorInstance.removeErrorMessage( this.name ); … … 167 181 // Remove the orphan error messages from the notice group 168 182 validatorInstance.removeOrphanErrorMessages(); 183 // Re-initialize select2 validation after checkout is updated 184 select2Validation(); 169 185 } 170 186 ); -
multisafepay/trunk/assets/public/js/multisafepay-validator-wallets.js
r3048898 r3306839 20 20 */ 21 21 22 // Check if the debugDirect function is available, if not define it 23 if ( 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 22 37 /** 23 38 * Class to validate the fields in the checkout form … … 32 47 33 48 /** 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 /** 34 141 * Get the label text for a field 35 142 * … … 38 145 */ 39 146 getLabelText( fieldName ) { 40 // Skip if this field has already been processed41 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]; 43 150 } 44 151 … … 72 179 this.processedFields.push( fieldName ); 73 180 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 ) ); 76 426 } 77 427 … … 80 430 * 81 431 * @param element - The element to be validated 432 * @returns {void} 82 433 */ 83 434 realtimeValidation( element ) { 84 const fieldName = element.name.trim(); 435 const fieldName = element.name.trim(); 436 const fieldValue = element.value.trim(); 85 437 86 438 // Remove this field from processedFields if it's there … … 90 442 } 91 443 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 93 474 this.removeErrorMessage( fieldName, true ); 475 this.removeInlineError( fieldName ); 94 476 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() { 118 519 // Getting the customer details element which includes all the user fields 119 520 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' 121 526 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 128 528 selectWrappers.forEach( 129 529 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' ); 133 532 elements.forEach( 134 533 element => { 135 // Remove existing listener and add a new one136 element.removeEventListener( 'input', event => this.realtimeValidation( event.target ) );137 element.addEventListener( 'input', event => this.realtimeValidation( event.target ) );138 534 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 ); 155 542 } 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 ); 158 550 } 159 551 } … … 161 553 } 162 554 ); 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 } 163 785 164 786 // Scroll to the previously added notice group area if there are any errors … … 166 788 this.scrollToElement( '.entry-content' ); 167 789 } 790 791 debugDirect( 'Final validation result: ' + ( allFieldsValid ? 'Valid' : 'Invalid' ), debugStatus, 'log' ); 168 792 return allFieldsValid; 169 793 } 170 794 171 795 /** 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 /** 172 814 * Scroll to a specific class element in the document. 173 815 * 174 816 * @param {string} className - The class name of the element to scroll to. 817 * @returns {void} 175 818 */ 176 819 scrollToElement( className ) { … … 204 847 } 205 848 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 206 855 // 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 <> 208 857 let noticeGroup = document.querySelector( '.woocommerce-NoticeGroup-checkout' ); 209 858 if ( ! noticeGroup ) { … … 215 864 noticeBanner.className = 'woocommerce-error'; 216 865 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 );230 866 noticeGroup.appendChild( noticeBanner ); 231 867 } 232 868 233 869 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 236 876 const existingErrorItem = ulList.querySelector( 'li[data-id="' + error[0].field + '"]' ); 237 877 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 } 239 887 } 240 888 … … 242 890 const errorItem = document.createElement( 'li' ); 243 891 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 245 900 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 251 925 // Add the error message to the notice group 252 926 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'; 253 943 254 944 debugDirect( ( isRealtimeValidation ? '"Interactively" a' : 'A' ) + 'dding error message in the notice group for field: ' + error[0].field, debugStatus, 'log' ); … … 287 977 // Getting the field name associated with the error message 288 978 const fieldElement = document.querySelector( '[name="' + fieldName + '"]' ); 289 // Check if the fieldElement exists and is not hidden using CSS290 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 ); 291 981 if ( ! isFieldVisible ) { 292 982 errorItem.remove(); … … 307 997 removeEntireNoticeGroup() { 308 998 // 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 ) { 313 1004 noticeGroup.remove(); 314 1005 debugDirect( 'Removing the entire notice group as there are no more error messages left', debugStatus, 'log' ); -
multisafepay/trunk/multisafepay.php
r3269746 r3306839 5 5 * Plugin URI: https://docs.multisafepay.com/docs/woocommerce 6 6 * Description: MultiSafepay Payment Plugin 7 * Version: 6. 8.37 * Version: 6.9.0 8 8 * Author: MultiSafepay 9 9 * Author URI: https://www.multisafepay.com … … 12 12 * License URI: http://www.gnu.org/licenses/gpl-3.0.html 13 13 * Requires at least: 6.0 14 * Tested up to: 6. 7.214 * Tested up to: 6.8.1 15 15 * WC requires at least: 6.0.0 16 * WC tested up to: 9. 7.116 * WC tested up to: 9.8.5 17 17 * Requires PHP: 7.3 18 18 * Text Domain: multisafepay … … 27 27 * Plugin version 28 28 */ 29 define( 'MULTISAFEPAY_PLUGIN_VERSION', '6. 8.3' );29 define( 'MULTISAFEPAY_PLUGIN_VERSION', '6.9.0' ); 30 30 31 31 /** -
multisafepay/trunk/readme.txt
r3269746 r3306839 3 3 Tags: multisafepay, payment gateway, credit cards, ideal, bnpl 4 4 Requires at least: 6.0 5 Tested up to: 6. 7.25 Tested up to: 6.8.1 6 6 Requires PHP: 7.3 7 Stable tag: 6. 8.37 Stable tag: 6.9.0 8 8 License: MIT 9 9 … … 139 139 140 140 == 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 141 159 = Release Notes - WooCommerce 6.8.3 (Apr 9th, 2025) = 142 160 -
multisafepay/trunk/src/Main.php
r3264984 r3306839 8 8 use MultiSafepay\WooCommerce\Services\Qr\QrPaymentComponentService; 9 9 use MultiSafepay\WooCommerce\Services\Qr\QrPaymentWebhook; 10 use MultiSafepay\WooCommerce\Services\ValidationService; 10 11 use MultiSafepay\WooCommerce\Settings\SettingsController; 11 12 use MultiSafepay\WooCommerce\Settings\ThirdPartyCompatibility; … … 44 45 $this->payment_components_qr_hooks(); 45 46 $this->callback_hooks(); 47 $this->validation_hooks(); 46 48 } 47 49 … … 210 212 211 213 /** 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 /** 212 226 * Run the loader to execute the hooks with WordPress. 213 227 * -
multisafepay/trunk/src/PaymentMethods/Base/BasePaymentMethod.php
r3264984 r3306839 400 400 } 401 401 402 $settings = get_option( 'woocommerce_' . $this->id . '_settings', array( 'payment_component' => ' qr' ) );402 $settings = get_option( 'woocommerce_' . $this->id . '_settings', array( 'payment_component' => 'yes' ) ); 403 403 if ( ! isset( $settings['payment_component'] ) ) { 404 return true;404 return false; 405 405 } 406 406 return 'qr' === $settings['payment_component']; … … 417 417 } 418 418 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' ) ); 420 420 if ( ! isset( $settings['payment_component'] ) ) { 421 return true;421 return false; 422 422 } 423 423 return 'qr_only' === $settings['payment_component']; … … 501 501 wp_enqueue_script( 'multisafepay-validator-wallets', MULTISAFEPAY_PLUGIN_URL . '/assets/public/js/multisafepay-validator-wallets.js', array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true ); 502 502 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 503 513 $admin_url_array = array( 504 514 'location' => admin_url( 'admin-ajax.php' ), -
multisafepay/trunk/src/PaymentMethods/Base/BaseRefunds.php
r3264984 r3306839 52 52 ); 53 53 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 ); 56 57 57 $refund_request->addDescriptionText( $reason );58 59 if ( $multisafepay_transaction->requiresShoppingCart() ) {60 58 $refunds = $order->get_refunds(); 61 59 $refund_merchant_item_id = reset( $refunds )->id; … … 72 70 73 71 if ( ! $multisafepay_transaction->requiresShoppingCart() ) { 72 $refund_request = new RefundRequest(); 73 $refund_request->addDescriptionText( $reason ); 74 74 $refund_request->addMoney( MoneyUtil::create_money( (float) $amount, $order->get_currency() ) ); 75 75 } … … 80 80 } catch ( Exception | ClientExceptionInterface | ApiException $exception ) { 81 81 $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() ) ); 84 83 } 85 84 -
multisafepay/trunk/src/Services/PaymentComponentService.php
r3264984 r3306839 83 83 84 84 // Tokenization and recurring model 85 if ( $woocommerce_payment_gateway->is_tokenization_enabled() ) {85 if ( $woocommerce_payment_gateway->is_tokenization_enabled() && is_user_logged_in() ) { 86 86 $payment_component_arguments['recurring'] = array( 87 87 'model' => 'cardOnFile', -
multisafepay/trunk/src/Services/Qr/QrCustomerService.php
r3264984 r3306839 48 48 ); 49 49 } 50 51 /**52 * Get the customer IP address.53 *54 * @return string55 */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 string76 */77 public function get_user_agent(): string {78 return sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) );79 }80 50 } -
multisafepay/trunk/src/Services/Qr/QrOrderService.php
r3269746 r3306839 122 122 */ 123 123 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' ) ); 125 125 $payment_options = new PaymentOptions(); 126 126 $payment_options->addNotificationUrl( get_rest_url( get_current_blog_id(), 'multisafepay/v1/qr-notification' ) ); -
multisafepay/trunk/src/Services/ShoppingCartService.php
r3264984 r3306839 6 6 use MultiSafepay\Api\Transactions\OrderRequest\Arguments\ShoppingCart\Item as CartItem; 7 7 use MultiSafepay\Api\Transactions\OrderRequest\Arguments\ShoppingCart\ShippingItem; 8 use MultiSafepay\Exception\InvalidArgumentException; 8 9 use MultiSafepay\WooCommerce\Utils\Hpos; 9 10 use MultiSafepay\WooCommerce\Utils\Logger; … … 41 42 * @param string|null $gateway_code 42 43 * @return ShoppingCart 44 * @throws InvalidArgumentException 43 45 */ 44 46 public function create_shopping_cart( WC_Order $order, string $currency, ?string $gateway_code = '' ): ShoppingCart { … … 162 164 * @param string $gateway_code 163 165 * @return ShippingItem 166 * @throws InvalidArgumentException 164 167 */ 165 168 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 ) 168 175 ->addQuantity( 1 ) 169 176 ->addUnitPrice( MoneyUtil::create_money( (float) $item->get_total(), $currency ) ) … … 286 293 287 294 foreach ( $allowed_rates as $rate ) { 288 if ( abs( $tax_rate - $rate ) <= 0. 15 ) {295 if ( abs( $tax_rate - $rate ) <= 0.05 ) { 289 296 return round( $tax_rate ); 290 297 } -
multisafepay/trunk/src/Utils/QrCheckoutManager.php
r3264984 r3306839 5 5 use WC_Cart; 6 6 use WC_Shipping_Rate; 7 use WC_Validation; 7 8 8 9 /** … … 35 36 36 37 /** 37 * Check if all mandatory fields are filled in the checkout in orderto submit a MultiSafepay transaction38 * Check if all mandatory fields are filled in the checkout to submit a MultiSafepay transaction 38 39 * using Payment Component with QR code. 39 40 * … … 51 52 52 53 // 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(); 55 56 56 57 // Determine if shipping to a different address … … 58 59 59 60 // 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 } 63 68 64 69 // 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 ); 66 74 67 75 // Get order fields … … 69 77 70 78 // 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 ); 72 80 73 81 return $this->is_validated; … … 353 361 * Get the shipping fields based on required and extra fields. 354 362 * 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 { 360 368 return array_map( 361 369 static function( $field ) { … … 363 371 }, 364 372 array_filter( 365 array_merge( $ required_fields, $extra_fields ),373 array_merge( $billing_required_fields, $billing_extra_fields ), 366 374 static function( $field ) { 367 375 // Exclude email and phone fields to be created as shipping fields. … … 376 384 * 377 385 * @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. 379 387 * @param array $order_fields The order fields. 380 388 */ 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 { 382 390 $this->is_validated = true; 383 391 … … 386 394 if ( 'billing_email' === $field ) { 387 395 $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 } 388 415 } else { 389 416 $field_value = isset( $this->posted_data[ $field ] ) ? wp_unslash( $this->posted_data[ $field ] ) : ''; … … 391 418 } 392 419 393 // Check if required field is empty394 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 ) ) { 395 422 $this->is_validated = false; 396 423 } … … 454 481 } 455 482 } 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 } 456 508 } -
multisafepay/trunk/vendor/autoload.php
r3269746 r3306839 15 15 } 16 16 } 17 trigger_error( 18 $err, 19 E_USER_ERROR 20 ); 17 throw new RuntimeException($err); 21 18 } 22 19 23 20 require_once __DIR__ . '/composer/autoload_real.php'; 24 21 25 return ComposerAutoloaderInit 4c82aacc71005aae20a63510a82d5e07::getLoader();22 return ComposerAutoloaderIniteb5be672a765e05cf872148d946d1d0b::getLoader(); -
multisafepay/trunk/vendor/composer/InstalledVersions.php
r3230524 r3306839 27 27 class InstalledVersions 28 28 { 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 29 35 /** 30 36 * @var mixed[]|null … … 324 330 325 331 /** 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 /** 326 344 * @return array[] 327 345 * @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[]}>}> … … 337 355 338 356 if (self::$canGetVendors) { 339 $selfDir = s trtr(__DIR__, '\\', '/');357 $selfDir = self::getSelfDir(); 340 358 foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { 341 359 $vendorDir = strtr($vendorDir, '\\', '/'); -
multisafepay/trunk/vendor/composer/autoload_real.php
r3269746 r3306839 3 3 // autoload_real.php @generated by Composer 4 4 5 class ComposerAutoloaderInit 4c82aacc71005aae20a63510a82d5e075 class ComposerAutoloaderIniteb5be672a765e05cf872148d946d1d0b 6 6 { 7 7 private static $loader; … … 25 25 require __DIR__ . '/platform_check.php'; 26 26 27 spl_autoload_register(array('ComposerAutoloaderInit 4c82aacc71005aae20a63510a82d5e07', 'loadClassLoader'), true, true);27 spl_autoload_register(array('ComposerAutoloaderIniteb5be672a765e05cf872148d946d1d0b', 'loadClassLoader'), true, true); 28 28 self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); 29 spl_autoload_unregister(array('ComposerAutoloaderInit 4c82aacc71005aae20a63510a82d5e07', 'loadClassLoader'));29 spl_autoload_unregister(array('ComposerAutoloaderIniteb5be672a765e05cf872148d946d1d0b', 'loadClassLoader')); 30 30 31 31 require __DIR__ . '/autoload_static.php'; 32 call_user_func(\Composer\Autoload\ComposerStaticInit 4c82aacc71005aae20a63510a82d5e07::getInitializer($loader));32 call_user_func(\Composer\Autoload\ComposerStaticIniteb5be672a765e05cf872148d946d1d0b::getInitializer($loader)); 33 33 34 34 $loader->register(true); -
multisafepay/trunk/vendor/composer/autoload_static.php
r3269746 r3306839 5 5 namespace Composer\Autoload; 6 6 7 class ComposerStaticInit 4c82aacc71005aae20a63510a82d5e077 class ComposerStaticIniteb5be672a765e05cf872148d946d1d0b 8 8 { 9 9 public static $prefixLengthsPsr4 = array ( … … 63 63 { 64 64 return \Closure::bind(function () use ($loader) { 65 $loader->prefixLengthsPsr4 = ComposerStaticInit 4c82aacc71005aae20a63510a82d5e07::$prefixLengthsPsr4;66 $loader->prefixDirsPsr4 = ComposerStaticInit 4c82aacc71005aae20a63510a82d5e07::$prefixDirsPsr4;67 $loader->classMap = ComposerStaticInit 4c82aacc71005aae20a63510a82d5e07::$classMap;65 $loader->prefixLengthsPsr4 = ComposerStaticIniteb5be672a765e05cf872148d946d1d0b::$prefixLengthsPsr4; 66 $loader->prefixDirsPsr4 = ComposerStaticIniteb5be672a765e05cf872148d946d1d0b::$prefixDirsPsr4; 67 $loader->classMap = ComposerStaticIniteb5be672a765e05cf872148d946d1d0b::$classMap; 68 68 69 69 }, null, ClassLoader::class); -
multisafepay/trunk/vendor/composer/installed.json
r3264984 r3306839 3 3 { 4 4 "name": "multisafepay/php-sdk", 5 "version": "5.1 6.0",6 "version_normalized": "5.1 6.0.0",5 "version": "5.17.0", 6 "version_normalized": "5.17.0.0", 7 7 "source": { 8 8 "type": "git", 9 9 "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", 16 16 "shasum": "" 17 17 }, … … 38 38 "jschaedl/iban-validation": "Adds additional IBAN validation for \\MultiSafepay\\ValueObject\\IbanNumber" 39 39 }, 40 "time": "2025-0 3-19T15:29:14+00:00",40 "time": "2025-06-04T13:12:21+00:00", 41 41 "type": "library", 42 42 "installation-source": "dist", … … 53 53 "support": { 54 54 "issues": "https://github.com/MultiSafepay/php-sdk/issues", 55 "source": "https://github.com/MultiSafepay/php-sdk/tree/5.1 6.0"55 "source": "https://github.com/MultiSafepay/php-sdk/tree/5.17.0" 56 56 }, 57 57 "install-path": "../multisafepay/php-sdk" -
multisafepay/trunk/vendor/composer/installed.php
r3269746 r3306839 2 2 'root' => array( 3 3 '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', 6 6 'reference' => null, 7 7 'type' => 'wordpress-plugin', … … 12 12 'versions' => array( 13 13 'multisafepay/php-sdk' => array( 14 'pretty_version' => '5.1 6.0',15 'version' => '5.1 6.0.0',16 'reference' => ' 849f1cf0c5ae23819422db4f78db948fd590c4ec',14 'pretty_version' => '5.17.0', 15 'version' => '5.17.0.0', 16 'reference' => '4c46227cf3139d76ff08bc4191f06445c867798b', 17 17 'type' => 'library', 18 18 'install_path' => __DIR__ . '/../multisafepay/php-sdk', … … 21 21 ), 22 22 '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', 25 25 'reference' => null, 26 26 'type' => 'wordpress-plugin', -
multisafepay/trunk/vendor/multisafepay/php-sdk/CHANGELOG.md
r3264984 r3306839 6 6 7 7 ## [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 8 16 9 17 ## [5.16.0] - 2025-03-19 -
multisafepay/trunk/vendor/multisafepay/php-sdk/composer.json
r3264984 r3306839 4 4 "type": "library", 5 5 "license": "MIT", 6 "version": "5.1 6.0",6 "version": "5.17.0", 7 7 "require": { 8 8 "php": "^7.2|^8.0", -
multisafepay/trunk/vendor/multisafepay/php-sdk/src/Api/Transactions/Gateways.php
r3072171 r3306839 27 27 'BNPL_OB', 28 28 'BNPL_MF', 29 'BILLINK', 29 30 ); 30 31 } -
multisafepay/trunk/vendor/multisafepay/php-sdk/src/Api/Transactions/OrderRequest/Arguments/GatewayInfo/Creditcard.php
r3050467 r3306839 148 148 return [ 149 149 'card_number' => $this->cardNumber ? $this->cardNumber->get() : null, 150 'car t_holder_name' => $this->cardHolderName,151 'car t_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, 152 152 'cvc' => $this->cvc ? $this->cvc->get() : null, 153 153 'card_cvc' => $this->cvc ? $this->cvc->get() : null, -
multisafepay/trunk/vendor/multisafepay/php-sdk/src/Util/Version.php
r3264984 r3306839 18 18 class Version 19 19 { 20 public const SDK_VERSION = '5.1 6.0';20 public const SDK_VERSION = '5.17.0'; 21 21 22 22 /** -
multisafepay/trunk/vendor/multisafepay/php-sdk/src/ValueObject/CartItem.php
r3264984 r3306839 244 244 { 245 245 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); 250 250 } 251 251 -
multisafepay/trunk/vendor/multisafepay/php-sdk/src/ValueObject/UnitPrice.php
r3230524 r3306839 15 15 16 16 /** 17 * Should be given in full units excluding tax, preferably including all decimal places, e.g. 3.30578512417 * Should be given in full units excluding tax, preferably including 10 decimal at most, e.g. 3.305785124 18 18 * 19 19 * @param float $unitPrice
Note: See TracChangeset
for help on using the changeset viewer.