diff --git a/.ddev/config.yaml b/.ddev/config.yaml index b56e10cf2..d65593b7c 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -27,7 +27,6 @@ web_environment: - ADMIN_PASS=admin - ADMIN_EMAIL=admin@example.com - WC_VERSION=7.7.2 - - PCP_BLOCKS_ENABLED=1 # Key features of ddev's config.yaml: diff --git a/.env.e2e.example b/.env.e2e.example index f080f8faf..98ad27c23 100644 --- a/.env.e2e.example +++ b/.env.e2e.example @@ -15,6 +15,8 @@ PRODUCT_ID=123 SUBSCRIPTION_URL="/product/sub" +APM_ID="sofort" + WP_MERCHANT_USER="admin" WP_MERCHANT_PASSWORD="admin" diff --git a/.psalm/stubs.php b/.psalm/stubs.php index 8b4990022..b57753212 100644 --- a/.psalm/stubs.php +++ b/.psalm/stubs.php @@ -1,4 +1,7 @@ item_factory->from_wc_order( $order ), function ( Item $item ): bool { - return $item->unit_amount()->value() > 0; + return $item->unit_amount()->value() >= 0; } ); $shipping = $this->shipping_factory->from_wc_order( $order ); @@ -166,7 +168,7 @@ class PurchaseUnitFactory { $items = array_filter( $this->item_factory->from_wc_cart( $cart ), function ( Item $item ): bool { - return $item->unit_amount()->value() > 0; + return $item->unit_amount()->value() >= 0; } ); @@ -187,7 +189,14 @@ class PurchaseUnitFactory { $payee = $this->payee_repository->payee(); - $custom_id = ''; + $custom_id = ''; + $session = WC()->session; + if ( $session instanceof WC_Session_Handler ) { + $session_id = $session->get_customer_unique_id(); + if ( $session_id ) { + $custom_id = CustomIds::CUSTOMER_ID_PREFIX . $session_id; + } + } $invoice_id = ''; $soft_descriptor = ''; $purchase_unit = new PurchaseUnit( diff --git a/modules/ppcp-button/resources/js/button.js b/modules/ppcp-button/resources/js/button.js index 718378875..790030920 100644 --- a/modules/ppcp-button/resources/js/button.js +++ b/modules/ppcp-button/resources/js/button.js @@ -28,6 +28,8 @@ const cardsSpinner = new Spinner('#ppcp-hosted-fields'); const bootstrap = () => { const checkoutFormSelector = 'form.woocommerce-checkout'; + const context = PayPalCommerceGateway.context; + const errorHandler = new ErrorHandler( PayPalCommerceGateway.labels.error.generic, document.querySelector(checkoutFormSelector) ?? document.querySelector('.woocommerce-notices-wrapper') @@ -58,7 +60,7 @@ const bootstrap = () => { } }); - const onSmartButtonClick = (data, actions) => { + const onSmartButtonClick = async (data, actions) => { window.ppcpFundingSource = data.fundingSource; const requiredFields = jQuery('form.woocommerce-checkout .validate-required:visible :input'); requiredFields.each((i, input) => { @@ -120,13 +122,21 @@ const bootstrap = () => { freeTrialHandler.handle(); return actions.reject(); } + + if (context === 'checkout' && !PayPalCommerceGateway.funding_sources_without_redirect.includes(data.fundingSource)) { + try { + await formSaver.save(form); + } catch (error) { + console.error(error); + } + } }; + const onSmartButtonsInit = () => { buttonsSpinner.unblock(); }; const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway, onSmartButtonClick, onSmartButtonsInit); const messageRenderer = new MessageRenderer(PayPalCommerceGateway.messages); - const context = PayPalCommerceGateway.context; if (context === 'mini-cart' || context === 'product') { if (PayPalCommerceGateway.mini_cart_buttons_enabled === '1') { const miniCartBootstrap = new MiniCartBootstap( diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js index f0654c4f0..f528f289e 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js @@ -102,6 +102,9 @@ class CheckoutActionHandler { } else { errorHandler.message(data.data.message); } + + // fire WC event for other plugins + jQuery( document.body ).trigger( 'checkout_error' , [ errorHandler.currentHtml() ] ); } throw {type: 'create-order-error', data: data.data}; diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/FreeTrialHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/FreeTrialHandler.js index 878e20207..5486576ff 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/FreeTrialHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/FreeTrialHandler.js @@ -40,6 +40,10 @@ class FreeTrialHandler { if (errors.length > 0) { this.spinner.unblock(); this.errorHandler.messages(errors); + + // fire WC event for other plugins + jQuery( document.body ).trigger( 'checkout_error' , [ this.errorHandler.currentHtml() ] ); + return; } } catch (error) { diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js index 3c3371542..146fd7e94 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js @@ -1,4 +1,3 @@ -import ButtonsToggleListener from '../Helper/ButtonsToggleListener'; import Product from '../Entity/Product'; import onApprove from '../OnApproveHandler/onApproveForContinue'; import {payerData} from "../Helper/PayerData"; diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js index 6cc9a13af..5aadb7f61 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js @@ -1,4 +1,5 @@ import CartActionHandler from '../ActionHandler/CartActionHandler'; +import BootstrapHelper from "../Helper/BootstrapHelper"; import {setVisible} from "../Helper/Hiding"; class CartBootstrap { @@ -8,6 +9,10 @@ class CartBootstrap { this.messages = messages; this.errorHandler = errorHandler; this.lastAmount = this.gateway.messages.amount; + + this.renderer.onButtonsInit(this.gateway.button.wrapper, () => { + this.handleButtonStatus(); + }, true); } init() { @@ -16,9 +21,11 @@ class CartBootstrap { } this.render(); + this.handleButtonStatus(); jQuery(document.body).on('updated_cart_totals updated_checkout', () => { this.render(); + this.handleButtonStatus(); fetch( this.gateway.ajax.cart_script_params.endpoint, @@ -47,10 +54,18 @@ class CartBootstrap { }); } + handleButtonStatus() { + BootstrapHelper.handleButtonStatus(this); + } + shouldRender() { return document.querySelector(this.gateway.button.wrapper) !== null; } + shouldEnable() { + return BootstrapHelper.shouldEnable(this); + } + render() { const actionHandler = new CartActionHandler( PayPalCommerceGateway, diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js index 59c6197a3..b426ac3ee 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js @@ -5,6 +5,8 @@ import { isSavedCardSelected, ORDER_BUTTON_SELECTOR, PaymentMethods } from "../Helper/CheckoutMethodState"; +import BootstrapHelper from "../Helper/BootstrapHelper"; +import {disable, enable} from "../Helper/ButtonDisabler"; class CheckoutBootstap { constructor(gateway, renderer, messages, spinner, errorHandler) { @@ -15,10 +17,15 @@ class CheckoutBootstap { this.errorHandler = errorHandler; this.standardOrderButtonSelector = ORDER_BUTTON_SELECTOR; + + this.renderer.onButtonsInit(this.gateway.button.wrapper, () => { + this.handleButtonStatus(); + }, true); } init() { this.render(); + this.handleButtonStatus(); // Unselect saved card. // WC saves form values, so with our current UI it would be a bit weird @@ -28,6 +35,7 @@ class CheckoutBootstap { jQuery(document.body).on('updated_checkout', () => { this.render() + this.handleButtonStatus(); }); jQuery(document.body).on('updated_checkout payment_method_selected', () => { @@ -43,6 +51,10 @@ class CheckoutBootstap { this.updateUi(); } + handleButtonStatus() { + BootstrapHelper.handleButtonStatus(this); + } + shouldRender() { if (document.querySelector(this.gateway.button.cancel_wrapper)) { return false; @@ -51,6 +63,10 @@ class CheckoutBootstap { return document.querySelector(this.gateway.button.wrapper) !== null || document.querySelector(this.gateway.hosted_fields.wrapper) !== null; } + shouldEnable() { + return BootstrapHelper.shouldEnable(this); + } + render() { if (!this.shouldRender()) { return; diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/MiniCartBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/MiniCartBootstap.js index 443c9afe4..4b4e3efd6 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/MiniCartBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/MiniCartBootstap.js @@ -1,4 +1,5 @@ import CartActionHandler from '../ActionHandler/CartActionHandler'; +import BootstrapHelper from "../Helper/BootstrapHelper"; class MiniCartBootstap { constructor(gateway, renderer, errorHandler) { @@ -15,9 +16,22 @@ class MiniCartBootstap { this.errorHandler, ); this.render(); + this.handleButtonStatus(); jQuery(document.body).on('wc_fragments_loaded wc_fragments_refreshed', () => { this.render(); + this.handleButtonStatus(); + }); + + this.renderer.onButtonsInit(this.gateway.button.mini_cart_wrapper, () => { + this.handleButtonStatus(); + }, true); + } + + handleButtonStatus() { + BootstrapHelper.handleButtonStatus(this, { + wrapper: this.gateway.button.mini_cart_wrapper, + skipMessages: true }); } @@ -26,6 +40,12 @@ class MiniCartBootstap { || document.querySelector(this.gateway.hosted_fields.mini_cart_wrapper) !== null; } + shouldEnable() { + return BootstrapHelper.shouldEnable(this, { + isDisabled: !!this.gateway.button.is_mini_cart_disabled + }); + } + render() { if (!this.shouldRender()) { return; diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js index 376962772..231460642 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js @@ -1,7 +1,7 @@ import UpdateCart from "../Helper/UpdateCart"; import SingleProductActionHandler from "../ActionHandler/SingleProductActionHandler"; -import {hide, show, setVisible} from "../Helper/Hiding"; -import ButtonsToggleListener from "../Helper/ButtonsToggleListener"; +import {hide, show} from "../Helper/Hiding"; +import BootstrapHelper from "../Helper/BootstrapHelper"; class SingleProductBootstap { constructor(gateway, renderer, messages, errorHandler) { @@ -10,56 +10,85 @@ class SingleProductBootstap { this.messages = messages; this.errorHandler = errorHandler; this.mutationObserver = new MutationObserver(this.handleChange.bind(this)); + this.formSelector = 'form.cart'; + + this.renderer.onButtonsInit(this.gateway.button.wrapper, () => { + this.handleChange(); + }, true); } + form() { + return document.querySelector(this.formSelector); + } handleChange() { - const shouldRender = this.shouldRender(); - setVisible(this.gateway.button.wrapper, shouldRender); - setVisible(this.gateway.messages.wrapper, shouldRender); - if (!shouldRender) { - return; - } - - this.render(); - } - - init() { - const form = document.querySelector('form.cart'); - if (!form) { - return; - } - - form.addEventListener('change', this.handleChange.bind(this)); - this.mutationObserver.observe(form, {childList: true, subtree: true}); - - const buttonObserver = new ButtonsToggleListener( - form.querySelector('.single_add_to_cart_button'), - () => { - show(this.gateway.button.wrapper); - show(this.gateway.messages.wrapper); - this.messages.renderWithAmount(this.priceAmount()) - }, - () => { - hide(this.gateway.button.wrapper); - hide(this.gateway.messages.wrapper); - }, - ); - buttonObserver.init(); - if (!this.shouldRender()) { - hide(this.gateway.button.wrapper); + this.renderer.disableSmartButtons(this.gateway.button.wrapper); + hide(this.gateway.button.wrapper, this.formSelector); hide(this.gateway.messages.wrapper); return; } this.render(); + + this.renderer.enableSmartButtons(this.gateway.button.wrapper); + show(this.gateway.button.wrapper); + show(this.gateway.messages.wrapper); + + this.handleButtonStatus(); + } + + handleButtonStatus() { + BootstrapHelper.handleButtonStatus(this, { + formSelector: this.formSelector + }); + } + + init() { + const form = this.form(); + + if (!form) { + return; + } + + form.addEventListener('change', () => { + this.handleChange(); + + setTimeout(() => { // Wait for the DOM to be fully updated + // For the moment renderWithAmount should only be done here to prevent undesired side effects due to priceAmount() + // not being correctly formatted in some cases, can be moved to handleButtonStatus() once this issue is fixed + this.messages.renderWithAmount(this.priceAmount()); + }, 100); + }); + this.mutationObserver.observe(form, { childList: true, subtree: true }); + + const addToCartButton = form.querySelector('.single_add_to_cart_button'); + + if (addToCartButton) { + (new MutationObserver(this.handleButtonStatus.bind(this))) + .observe(addToCartButton, { attributes : true }); + } + + if (!this.shouldRender()) { + return; + } + + this.render(); + this.handleChange(); } shouldRender() { - return document.querySelector('form.cart') !== null + return this.form() !== null + && !this.isWcsattSubscriptionMode(); + } + + shouldEnable() { + const form = this.form(); + const addToCartButton = form ? form.querySelector('.single_add_to_cart_button') : null; + + return BootstrapHelper.shouldEnable(this) && !this.priceAmountIsZero() - && !this.isSubscriptionMode(); + && ((null === addToCartButton) || !addToCartButton.classList.contains('disabled')); } priceAmount() { @@ -93,7 +122,7 @@ class SingleProductBootstap { return !price || price === 0; } - isSubscriptionMode() { + isWcsattSubscriptionMode() { // Check "All products for subscriptions" plugin. return document.querySelector('.wcsatt-options-product:not(.wcsatt-options-product--hidden) .subscription-option input[type="radio"]:checked') !== null || document.querySelector('.wcsatt-options-prompt-label-subscription input[type="radio"]:checked') !== null; // grouped @@ -106,7 +135,7 @@ class SingleProductBootstap { this.gateway.ajax.change_cart.endpoint, this.gateway.ajax.change_cart.nonce, ), - document.querySelector('form.cart'), + this.form(), this.errorHandler, ); diff --git a/modules/ppcp-button/resources/js/modules/ErrorHandler.js b/modules/ppcp-button/resources/js/modules/ErrorHandler.js index 726ef2074..6048360b2 100644 --- a/modules/ppcp-button/resources/js/modules/ErrorHandler.js +++ b/modules/ppcp-button/resources/js/modules/ErrorHandler.js @@ -40,6 +40,15 @@ class ErrorHandler { this._scrollToMessages(); } + /** + * @returns {String} + */ + currentHtml() + { + const messageContainer = this._getMessageContainer(); + return messageContainer.outerHTML; + } + /** * @private * @param {String} text diff --git a/modules/ppcp-button/resources/js/modules/Helper/BootstrapHelper.js b/modules/ppcp-button/resources/js/modules/Helper/BootstrapHelper.js new file mode 100644 index 000000000..ba00e2c76 --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Helper/BootstrapHelper.js @@ -0,0 +1,40 @@ +import {disable, enable} from "./ButtonDisabler"; + +/** + * Common Bootstrap methods to avoid code repetition. + */ +export default class BootstrapHelper { + + static handleButtonStatus(bs, options) { + options = options || {}; + options.wrapper = options.wrapper || bs.gateway.button.wrapper; + options.messagesWrapper = options.messagesWrapper || bs.gateway.messages.wrapper; + options.skipMessages = options.skipMessages || false; + + if (!bs.shouldEnable()) { + bs.renderer.disableSmartButtons(options.wrapper); + disable(options.wrapper, options.formSelector || null); + + if (!options.skipMessages) { + disable(options.messagesWrapper); + } + return; + } + bs.renderer.enableSmartButtons(options.wrapper); + enable(options.wrapper); + + if (!options.skipMessages) { + enable(options.messagesWrapper); + } + } + + static shouldEnable(bs, options) { + options = options || {}; + if (typeof options.isDisabled === 'undefined') { + options.isDisabled = bs.gateway.button.is_disabled; + } + + return bs.shouldRender() + && options.isDisabled !== true; + } +} diff --git a/modules/ppcp-button/resources/js/modules/Helper/ButtonDisabler.js b/modules/ppcp-button/resources/js/modules/Helper/ButtonDisabler.js new file mode 100644 index 000000000..ae7f3665c --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Helper/ButtonDisabler.js @@ -0,0 +1,56 @@ +/** + * @param selectorOrElement + * @returns {Element} + */ +const getElement = (selectorOrElement) => { + if (typeof selectorOrElement === 'string') { + return document.querySelector(selectorOrElement); + } + return selectorOrElement; +} + +export const setEnabled = (selectorOrElement, enable, form = null) => { + const element = getElement(selectorOrElement); + + if (!element) { + return; + } + + if (enable) { + jQuery(element).css({ + 'cursor': '', + '-webkit-filter': '', + 'filter': '', + } ) + .off('mouseup') + .find('> *') + .css('pointer-events', ''); + } else { + jQuery(element).css({ + 'cursor': 'not-allowed', + '-webkit-filter': 'grayscale(100%)', + 'filter': 'grayscale(100%)', + }) + .on('mouseup', function(event) { + event.stopImmediatePropagation(); + + if (form) { + // Trigger form submit to show the error message + let $form = jQuery(form); + if ($form.find('.single_add_to_cart_button').hasClass('disabled')) { + $form.find(':submit').trigger('click'); + } + } + }) + .find('> *') + .css('pointer-events', 'none'); + } +}; + +export const disable = (selectorOrElement, form = null) => { + setEnabled(selectorOrElement, false, form); +}; + +export const enable = (selectorOrElement) => { + setEnabled(selectorOrElement, true); +}; diff --git a/modules/ppcp-button/resources/js/modules/Helper/ButtonsToggleListener.js b/modules/ppcp-button/resources/js/modules/Helper/ButtonsToggleListener.js deleted file mode 100644 index add1ee28a..000000000 --- a/modules/ppcp-button/resources/js/modules/Helper/ButtonsToggleListener.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * When you can't add something to the cart, the PayPal buttons should not show. - * Therefore we listen for changes on the add to cart button and show/hide the buttons accordingly. - */ - -class ButtonsToggleListener { - constructor(element, showCallback, hideCallback) - { - this.element = element; - this.showCallback = showCallback; - this.hideCallback = hideCallback; - this.observer = null; - } - - init() - { - if (!this.element) { - return; - } - const config = { attributes : true }; - const callback = () => { - if (this.element.classList.contains('disabled')) { - this.hideCallback(); - return; - } - this.showCallback(); - } - this.observer = new MutationObserver(callback); - this.observer.observe(this.element, config); - callback(); - } - - disconnect() - { - this.observer.disconnect(); - } -} - -export default ButtonsToggleListener; diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js index 0ddafdd15..fcd20a609 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js @@ -10,6 +10,7 @@ class CreditCardRenderer { this.spinner = spinner; this.cardValid = false; this.formValid = false; + this.emptyFields = new Set(['number', 'cvv', 'expirationDate']); this.currentHostedFieldsInstance = null; } @@ -130,7 +131,7 @@ class CreditCardRenderer { return event.fields[key].isValid; }); - const className = this._cardNumberFiledCLassNameByCardType(event.cards[0].type); + const className = event.cards.length ? this._cardNumberFiledCLassNameByCardType(event.cards[0].type) : ''; event.fields.number.isValid ? cardNumber.classList.add(className) : this._recreateElementClassAttribute(cardNumber, cardNumberField.className); @@ -138,6 +139,12 @@ class CreditCardRenderer { this.formValid = formValid; }); + hostedFields.on('empty', (event) => { + this.emptyFields.add(event.emittedBy); + }); + hostedFields.on('notEmpty', (event) => { + this.emptyFields.delete(event.emittedBy); + }); show(buttonSelector); @@ -249,7 +256,16 @@ class CreditCardRenderer { }); } else { this.spinner.unblock(); - const message = ! this.cardValid ? this.defaultConfig.hosted_fields.labels.card_not_supported : this.defaultConfig.hosted_fields.labels.fields_not_valid; + + let message = this.defaultConfig.labels.error.generic; + if (this.emptyFields.size > 0) { + message = this.defaultConfig.hosted_fields.labels.fields_empty; + } else if (!this.cardValid) { + message = this.defaultConfig.hosted_fields.labels.card_not_supported; + } else if (!this.formValid) { + message = this.defaultConfig.hosted_fields.labels.fields_not_valid; + } + this.errorHandler.message(message); } } diff --git a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js index a07e81d21..80c90c0ef 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js @@ -2,6 +2,7 @@ class MessageRenderer { constructor(config) { this.config = config; + this.optionsFingerprint = null; } render() { @@ -9,38 +10,58 @@ class MessageRenderer { return; } - paypal.Messages({ + const options = { amount: this.config.amount, placement: this.config.placement, style: this.config.style - }).render(this.config.wrapper); + }; + + if (this.optionsEqual(options)) { + return; + } + + paypal.Messages(options).render(this.config.wrapper); jQuery(document.body).on('updated_cart_totals', () => { - paypal.Messages({ - amount: this.config.amount, - placement: this.config.placement, - style: this.config.style - }).render(this.config.wrapper); + paypal.Messages(options).render(this.config.wrapper); }); } renderWithAmount(amount) { - if (! this.shouldRender()) { return; } + const options = { + amount, + placement: this.config.placement, + style: this.config.style + }; + + if (this.optionsEqual(options)) { + return; + } + const newWrapper = document.createElement('div'); newWrapper.setAttribute('id', this.config.wrapper.replace('#', '')); - const sibling = document.querySelector(this.config.wrapper).nextSibling; - document.querySelector(this.config.wrapper).parentElement.removeChild(document.querySelector(this.config.wrapper)); + const oldWrapper = document.querySelector(this.config.wrapper); + const sibling = oldWrapper.nextSibling; + oldWrapper.parentElement.removeChild(oldWrapper); sibling.parentElement.insertBefore(newWrapper, sibling); - paypal.Messages({ - amount, - placement: this.config.placement, - style: this.config.style - }).render(this.config.wrapper); + + paypal.Messages(options).render(this.config.wrapper); + } + + optionsEqual(options) { + const fingerprint = JSON.stringify(options); + + if (this.optionsFingerprint === fingerprint) { + return true; + } + + this.optionsFingerprint = fingerprint; + return false; } shouldRender() { diff --git a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js index 17f58aff3..1f0e3d31a 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js @@ -7,6 +7,9 @@ class Renderer { this.onSmartButtonClick = onSmartButtonClick; this.onSmartButtonsInit = onSmartButtonsInit; + this.buttonsOptions = {}; + this.onButtonsInitListeners = {}; + this.renderedSources = new Set(); } @@ -77,7 +80,10 @@ class Renderer { style, ...contextConfig, onClick: this.onSmartButtonClick, - onInit: this.onSmartButtonsInit, + onInit: (data, actions) => { + this.onSmartButtonsInit(data, actions); + this.handleOnButtonsInit(wrapper, data, actions); + }, }); if (!btn.isEligible()) { return; @@ -106,6 +112,44 @@ class Renderer { enableCreditCardFields() { this.creditCardRenderer.enableFields(); } + + onButtonsInit(wrapper, handler, reset) { + this.onButtonsInitListeners[wrapper] = reset ? [] : (this.onButtonsInitListeners[wrapper] || []); + this.onButtonsInitListeners[wrapper].push(handler); + } + + handleOnButtonsInit(wrapper, data, actions) { + + this.buttonsOptions[wrapper] = { + data: data, + actions: actions + } + + if (this.onButtonsInitListeners[wrapper]) { + for (let handler of this.onButtonsInitListeners[wrapper]) { + if (typeof handler === 'function') { + handler({ + wrapper: wrapper, + ...this.buttonsOptions[wrapper] + }); + } + } + } + } + + disableSmartButtons(wrapper) { + if (!this.buttonsOptions[wrapper]) { + return; + } + this.buttonsOptions[wrapper].actions.disable(); + } + + enableSmartButtons(wrapper) { + if (!this.buttonsOptions[wrapper]) { + return; + } + this.buttonsOptions[wrapper].actions.enable(); + } } export default Renderer; diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index 7f8c50b14..56381fcc2 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -67,23 +67,6 @@ return array( return $dummy_ids[ $shop_country ] ?? $container->get( 'button.client_id' ); }, - 'button.is_paypal_continuation' => static function( ContainerInterface $container ): bool { - $session_handler = $container->get( 'session.handler' ); - - $order = $session_handler->order(); - if ( ! $order ) { - return false; - } - $source = $order->payment_source(); - if ( $source && $source->card() ) { - return false; // Ignore for DCC. - } - if ( 'card' === $session_handler->funding_source() ) { - return false; // Ignore for card buttons. - } - - return true; - }, 'button.smart-button' => static function ( ContainerInterface $container ): SmartButtonInterface { $state = $container->get( 'onboarding.state' ); if ( $state->current_state() !== State::STATE_ONBOARDED ) { @@ -125,6 +108,7 @@ return array( $container->get( 'button.basic-checkout-validation-enabled' ), $container->get( 'button.early-wc-checkout-validation-enabled' ), $container->get( 'button.pay-now-contexts' ), + $container->get( 'wcgateway.funding-sources-without-redirect' ), $container->get( 'woocommerce.logger.woocommerce' ) ); }, @@ -176,6 +160,7 @@ return array( $container->get( 'button.early-wc-checkout-validation-enabled' ), $container->get( 'button.pay-now-contexts' ), $container->get( 'button.handle-shipping-in-paypal' ), + $container->get( 'wcgateway.funding-sources-without-redirect' ), $logger ); }, @@ -184,8 +169,7 @@ return array( $state = $container->get( 'onboarding.state' ); $order_processor = $container->get( 'wcgateway.order-processor' ); $session_handler = $container->get( 'session.handler' ); - $prefix = $container->get( 'api.prefix' ); - return new EarlyOrderHandler( $state, $order_processor, $session_handler, $prefix ); + return new EarlyOrderHandler( $state, $order_processor, $session_handler ); }, 'button.endpoint.approve-order' => static function ( ContainerInterface $container ): ApproveOrderEndpoint { $request_data = $container->get( 'button.request-data' ); @@ -215,7 +199,9 @@ return array( ); }, 'button.checkout-form-saver' => static function ( ContainerInterface $container ): CheckoutFormSaver { - return new CheckoutFormSaver(); + return new CheckoutFormSaver( + $container->get( 'session.handler' ) + ); }, 'button.endpoint.save-checkout-form' => static function ( ContainerInterface $container ): SaveCheckoutFormEndpoint { return new SaveCheckoutFormEndpoint( diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index 7f07c332a..fded20a67 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Button\Assets; use Exception; use Psr\Log\LoggerInterface; +use WC_Order; use WC_Product; use WC_Product_Variation; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; @@ -173,6 +174,13 @@ class SmartButton implements SmartButtonInterface { */ private $pay_now_contexts; + /** + * The sources that do not cause issues about redirecting (on mobile, ...) and sometimes not returning back. + * + * @var string[] + */ + private $funding_sources_without_redirect; + /** * The logger. * @@ -208,6 +216,7 @@ class SmartButton implements SmartButtonInterface { * @param bool $basic_checkout_validation_enabled Whether the basic JS validation of the form iss enabled. * @param bool $early_validation_enabled Whether to execute WC validation of the checkout form. * @param array $pay_now_contexts The contexts that should have the Pay Now button. + * @param string[] $funding_sources_without_redirect The sources that do not cause issues about redirecting (on mobile, ...) and sometimes not returning back. * @param LoggerInterface $logger The logger. */ public function __construct( @@ -229,6 +238,7 @@ class SmartButton implements SmartButtonInterface { bool $basic_checkout_validation_enabled, bool $early_validation_enabled, array $pay_now_contexts, + array $funding_sources_without_redirect, LoggerInterface $logger ) { @@ -250,6 +260,7 @@ class SmartButton implements SmartButtonInterface { $this->basic_checkout_validation_enabled = $basic_checkout_validation_enabled; $this->early_validation_enabled = $early_validation_enabled; $this->pay_now_contexts = $pay_now_contexts; + $this->funding_sources_without_redirect = $funding_sources_without_redirect; $this->logger = $logger; } @@ -796,8 +807,6 @@ class SmartButton implements SmartButtonInterface { * @return array */ public function script_data(): array { - global $wp; - $is_free_trial_cart = $this->is_free_trial_cart(); $url_params = $this->url_params(); @@ -858,10 +867,12 @@ class SmartButton implements SmartButtonInterface { 'bn_codes' => $this->bn_codes(), 'payer' => $this->payerData(), 'button' => array( - 'wrapper' => '#ppc-button-' . PayPalGateway::ID, - 'mini_cart_wrapper' => '#ppc-button-minicart', - 'cancel_wrapper' => '#ppcp-cancel', - 'mini_cart_style' => array( + 'wrapper' => '#ppc-button-' . PayPalGateway::ID, + 'is_disabled' => $this->is_button_disabled(), + 'mini_cart_wrapper' => '#ppc-button-minicart', + 'is_mini_cart_disabled' => $this->is_button_disabled( 'mini-cart' ), + 'cancel_wrapper' => '#ppcp-cancel', + 'mini_cart_style' => array( 'layout' => $this->style_for_context( 'layout', 'mini-cart' ), 'color' => $this->style_for_context( 'color', 'mini-cart' ), 'shape' => $this->style_for_context( 'shape', 'mini-cart' ), @@ -869,7 +880,7 @@ class SmartButton implements SmartButtonInterface { 'tagline' => $this->style_for_context( 'tagline', 'mini-cart' ), 'height' => $this->settings->has( 'button_mini-cart_height' ) && $this->settings->get( 'button_mini-cart_height' ) ? $this->normalize_height( (int) $this->settings->get( 'button_mini-cart_height' ) ) : 35, ), - 'style' => array( + 'style' => array( 'layout' => $this->style_for_context( 'layout', $this->context() ), 'color' => $this->style_for_context( 'color', $this->context() ), 'shape' => $this->style_for_context( 'shape', $this->context() ), @@ -893,6 +904,10 @@ class SmartButton implements SmartButtonInterface { 'credit_card_number' => '', 'cvv' => '', 'mm_yy' => __( 'MM/YY', 'woocommerce-paypal-payments' ), + 'fields_empty' => __( + 'Card payment details are missing. Please fill in all required fields.', + 'woocommerce-paypal-payments' + ), 'fields_not_valid' => __( 'Unfortunately, your credit card details are not valid.', 'woocommerce-paypal-payments' @@ -934,11 +949,12 @@ class SmartButton implements SmartButtonInterface { // phpcs:ignore WordPress.WP.I18n 'shipping_field' => _x( 'Shipping %s', 'checkout-validation', 'woocommerce' ), ), - 'order_id' => 'pay-now' === $this->context() ? absint( $wp->query_vars['order-pay'] ) : 0, + 'order_id' => 'pay-now' === $this->context() ? $this->get_order_pay_id() : 0, 'single_product_buttons_enabled' => $this->settings_status->is_smart_button_enabled_for_location( 'product' ), 'mini_cart_buttons_enabled' => $this->settings_status->is_smart_button_enabled_for_location( 'mini-cart' ), 'basic_checkout_validation_enabled' => $this->basic_checkout_validation_enabled, 'early_checkout_validation_enabled' => $this->early_validation_enabled, + 'funding_sources_without_redirect' => $this->funding_sources_without_redirect, ); if ( $this->style_for_context( 'layout', 'mini-cart' ) !== 'horizontal' ) { @@ -1000,13 +1016,26 @@ class SmartButton implements SmartButtonInterface { ); if ( $this->environment->current_environment_is( Environment::SANDBOX ) - && defined( 'WP_DEBUG' ) && \WP_DEBUG && is_user_logged_in() + && defined( 'WP_DEBUG' ) && \WP_DEBUG && WC()->customer instanceof \WC_Customer && WC()->customer->get_billing_country() && 2 === strlen( WC()->customer->get_billing_country() ) ) { $params['buyer-country'] = WC()->customer->get_billing_country(); } + if ( 'pay-now' === $this->context() ) { + $wc_order_id = $this->get_order_pay_id(); + if ( $wc_order_id ) { + $wc_order = wc_get_order( $wc_order_id ); + if ( $wc_order instanceof WC_Order ) { + $currency = $wc_order->get_currency(); + if ( $currency ) { + $params['currency'] = $currency; + } + } + } + } + $disable_funding = $this->settings->has( 'disable_funding' ) ? $this->settings->get( 'disable_funding' ) : array(); @@ -1334,6 +1363,51 @@ class SmartButton implements SmartButtonInterface { ); } + /** + * Checks if PayPal buttons/messages should be rendered for the current page. + * + * @param string|null $context The context that should be checked, use default otherwise. + * + * @return bool + */ + protected function is_button_disabled( string $context = null ): bool { + if ( null === $context ) { + $context = $this->context(); + } + + if ( 'product' === $context ) { + $product = wc_get_product(); + + /** + * Allows to decide if the button should be disabled for a given product + */ + $is_disabled = apply_filters( + 'woocommerce_paypal_payments_product_buttons_disabled', + null, + $product + ); + + if ( $is_disabled !== null ) { + return $is_disabled; + } + } + + /** + * Allows to decide if the button should be disabled globally or on a given context + */ + $is_disabled = apply_filters( + 'woocommerce_paypal_payments_buttons_disabled', + null, + $context + ); + + if ( $is_disabled !== null ) { + return $is_disabled; + } + + return false; + } + /** * Retrieves all payment tokens for the user, via API or cached if already queried. * @@ -1433,4 +1507,19 @@ class SmartButton implements SmartButtonInterface { return $this->context() === 'product' ? $product_intent : $other_context_intent; } + + /** + * Returns the ID of WC order on the order-pay page, or 0. + * + * @return int + */ + protected function get_order_pay_id(): int { + global $wp; + + if ( ! isset( $wp->query_vars['order-pay'] ) ) { + return 0; + } + + return absint( $wp->query_vars['order-pay'] ); + } } diff --git a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php index 83d5f89b1..686b87dd8 100644 --- a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php @@ -152,6 +152,13 @@ class CreateOrderEndpoint implements EndpointInterface { */ private $handle_shipping_in_paypal; + /** + * The sources that do not cause issues about redirecting (on mobile, ...) and sometimes not returning back. + * + * @var string[] + */ + private $funding_sources_without_redirect; + /** * The logger. * @@ -175,6 +182,7 @@ class CreateOrderEndpoint implements EndpointInterface { * @param bool $early_validation_enabled Whether to execute WC validation of the checkout form. * @param string[] $pay_now_contexts The contexts that should have the Pay Now button. * @param bool $handle_shipping_in_paypal If true, the shipping methods are sent to PayPal allowing the customer to select it inside the popup. + * @param string[] $funding_sources_without_redirect The sources that do not cause issues about redirecting (on mobile, ...) and sometimes not returning back. * @param LoggerInterface $logger The logger. */ public function __construct( @@ -191,23 +199,25 @@ class CreateOrderEndpoint implements EndpointInterface { bool $early_validation_enabled, array $pay_now_contexts, bool $handle_shipping_in_paypal, + array $funding_sources_without_redirect, LoggerInterface $logger ) { - $this->request_data = $request_data; - $this->purchase_unit_factory = $purchase_unit_factory; - $this->shipping_preference_factory = $shipping_preference_factory; - $this->api_endpoint = $order_endpoint; - $this->payer_factory = $payer_factory; - $this->session_handler = $session_handler; - $this->settings = $settings; - $this->early_order_handler = $early_order_handler; - $this->registration_needed = $registration_needed; - $this->card_billing_data_mode = $card_billing_data_mode; - $this->early_validation_enabled = $early_validation_enabled; - $this->pay_now_contexts = $pay_now_contexts; - $this->handle_shipping_in_paypal = $handle_shipping_in_paypal; - $this->logger = $logger; + $this->request_data = $request_data; + $this->purchase_unit_factory = $purchase_unit_factory; + $this->shipping_preference_factory = $shipping_preference_factory; + $this->api_endpoint = $order_endpoint; + $this->payer_factory = $payer_factory; + $this->session_handler = $session_handler; + $this->settings = $settings; + $this->early_order_handler = $early_order_handler; + $this->registration_needed = $registration_needed; + $this->card_billing_data_mode = $card_billing_data_mode; + $this->early_validation_enabled = $early_validation_enabled; + $this->pay_now_contexts = $pay_now_contexts; + $this->handle_shipping_in_paypal = $handle_shipping_in_paypal; + $this->funding_sources_without_redirect = $funding_sources_without_redirect; + $this->logger = $logger; } /** @@ -288,6 +298,11 @@ class CreateOrderEndpoint implements EndpointInterface { } if ( 'checkout' === $data['context'] ) { + if ( $payment_method === PayPalGateway::ID && ! in_array( $funding_source, $this->funding_sources_without_redirect, true ) ) { + $this->session_handler->replace_order( $order ); + $this->session_handler->replace_funding_source( $funding_source ); + } + if ( ! $this->early_order_handler->should_create_early_order() || $this->registration_needed diff --git a/modules/ppcp-button/src/Helper/CheckoutFormSaver.php b/modules/ppcp-button/src/Helper/CheckoutFormSaver.php index 73f8eeefc..c2f10ca28 100644 --- a/modules/ppcp-button/src/Helper/CheckoutFormSaver.php +++ b/modules/ppcp-button/src/Helper/CheckoutFormSaver.php @@ -10,11 +10,30 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Button\Helper; use WC_Checkout; +use WooCommerce\PayPalCommerce\Session\SessionHandler; /** * Class CheckoutFormSaver */ class CheckoutFormSaver extends WC_Checkout { + /** + * The Session handler. + * + * @var SessionHandler + */ + private $session_handler; + + /** + * CheckoutFormSaver constructor. + * + * @param SessionHandler $session_handler The session handler. + */ + public function __construct( + SessionHandler $session_handler + ) { + $this->session_handler = $session_handler; + } + /** * Saves the form data to the WC customer and session. * @@ -28,5 +47,7 @@ class CheckoutFormSaver extends WC_Checkout { $data = $this->get_posted_data(); $this->update_session( $data ); + + $this->session_handler->replace_checkout_form( $data ); } } diff --git a/modules/ppcp-button/src/Helper/ContextTrait.php b/modules/ppcp-button/src/Helper/ContextTrait.php index daddbb0a9..77de2a078 100644 --- a/modules/ppcp-button/src/Helper/ContextTrait.php +++ b/modules/ppcp-button/src/Helper/ContextTrait.php @@ -9,6 +9,8 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Button\Helper; +use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; + trait ContextTrait { /** @@ -18,6 +20,13 @@ trait ContextTrait { */ protected function context(): string { if ( is_product() || wc_post_content_has_shortcode( 'product_page' ) ) { + + // Do this check here instead of reordering outside conditions. + // In order to have more control over the context. + if ( ( is_checkout() ) && ! $this->is_paypal_continuation() ) { + return 'checkout'; + } + return 'product'; } @@ -56,6 +65,12 @@ trait ContextTrait { return false; } + if ( ! $order->status()->is( OrderStatus::APPROVED ) + && ! $order->status()->is( OrderStatus::COMPLETED ) + ) { + return false; + } + $source = $order->payment_source(); if ( $source && $source->card() ) { return false; // Ignore for DCC. diff --git a/modules/ppcp-button/src/Helper/EarlyOrderHandler.php b/modules/ppcp-button/src/Helper/EarlyOrderHandler.php index 3776a6698..4ac8583d0 100644 --- a/modules/ppcp-button/src/Helper/EarlyOrderHandler.php +++ b/modules/ppcp-button/src/Helper/EarlyOrderHandler.php @@ -15,15 +15,12 @@ use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; -use WooCommerce\PayPalCommerce\Webhooks\Handler\PrefixTrait; /** * Class EarlyOrderHandler */ class EarlyOrderHandler { - use PrefixTrait; - /** * The State. * @@ -51,19 +48,16 @@ class EarlyOrderHandler { * @param State $state The State. * @param OrderProcessor $order_processor The Order Processor. * @param SessionHandler $session_handler The Session Handler. - * @param string $prefix The Prefix. */ public function __construct( State $state, OrderProcessor $order_processor, - SessionHandler $session_handler, - string $prefix + SessionHandler $session_handler ) { $this->state = $state; $this->order_processor = $order_processor; $this->session_handler = $session_handler; - $this->prefix = $prefix; } /** @@ -101,7 +95,7 @@ class EarlyOrderHandler { $order_id = false; foreach ( $order->purchase_units() as $purchase_unit ) { if ( $purchase_unit->custom_id() === sanitize_text_field( wp_unslash( $_REQUEST['ppcp-resume-order'] ) ) ) { - $order_id = (int) $this->sanitize_custom_id( $purchase_unit->custom_id() ); + $order_id = (int) $purchase_unit->custom_id(); } } if ( $order_id === $resume_order_id ) { diff --git a/modules/ppcp-compat/src/CompatModule.php b/modules/ppcp-compat/src/CompatModule.php index 059425be7..979056835 100644 --- a/modules/ppcp-compat/src/CompatModule.php +++ b/modules/ppcp-compat/src/CompatModule.php @@ -339,14 +339,6 @@ class CompatModule implements ModuleInterface { }, 5 ); - - add_filter( - 'woocommerce_paypal_payments_checkout_button_renderer_hook', - function(): string { - return 'woocommerce_review_order_after_submit'; - }, - 5 - ); } } diff --git a/modules/ppcp-session/src/Cancellation/CancelController.php b/modules/ppcp-session/src/Cancellation/CancelController.php index 79dbd4dcf..a4723cd96 100644 --- a/modules/ppcp-session/src/Cancellation/CancelController.php +++ b/modules/ppcp-session/src/Cancellation/CancelController.php @@ -9,12 +9,14 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Session\Cancellation; +use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; use WooCommerce\PayPalCommerce\Session\SessionHandler; /** * Class CancelController */ class CancelController { + use ContextTrait; public const NONCE = 'ppcp-cancel'; @@ -50,7 +52,7 @@ class CancelController { /** * Runs the controller. */ - public function run() { + public function run(): void { $param_name = self::NONCE; if ( isset( $_GET[ $param_name ] ) && // Input var ok. wp_verify_nonce( @@ -61,20 +63,10 @@ class CancelController { $this->session_handler->destroy_session_data(); } - $order = $this->session_handler->order(); - if ( ! $order ) { + if ( ! $this->is_paypal_continuation() ) { return; } - $source = $order->payment_source(); - if ( $source && $source->card() ) { - return; // Ignore for DCC. - } - - if ( 'card' === $this->session_handler->funding_source() ) { - return; // Ignore for card buttons. - } - $url = add_query_arg( array( $param_name => wp_create_nonce( self::NONCE ) ), wc_get_checkout_url() ); add_action( 'woocommerce_review_order_after_submit', diff --git a/modules/ppcp-session/src/MemoryWcSession.php b/modules/ppcp-session/src/MemoryWcSession.php new file mode 100644 index 000000000..583aa5a9d --- /dev/null +++ b/modules/ppcp-session/src/MemoryWcSession.php @@ -0,0 +1,75 @@ +session->get_session). + * + * @var array + */ + private static $data; + + /** + * The customer ID. + * + * @var string|int + */ + private static $customer_id; + + /** + * Enqueues this session handler with the given data to be used by WC. + * + * @param array $session_data The session data (from WC()->session->get_session). + * @param int|string $customer_id The customer ID. + */ + public static function replace_session_handler( array $session_data, $customer_id ): void { + self::$data = $session_data; + self::$customer_id = $customer_id; + + add_filter( + 'woocommerce_session_handler', + function () { + return MemoryWcSession::class; + } + ); + } + + /** + * @inerhitDoc + */ + public function init_session_cookie() { + $this->_customer_id = self::$customer_id; + $this->_data = self::$data; + } + + /** + * @inerhitDoc + */ + public function get_session_data() { + return self::$data; + } + + /** + * @inerhitDoc + */ + public function forget_session() { + self::$data = array(); + + parent::forget_session(); + } +} diff --git a/modules/ppcp-session/src/SessionHandler.php b/modules/ppcp-session/src/SessionHandler.php index 7bceb34a6..d85c40b65 100644 --- a/modules/ppcp-session/src/SessionHandler.php +++ b/modules/ppcp-session/src/SessionHandler.php @@ -47,6 +47,13 @@ class SessionHandler { */ private $funding_source = null; + /** + * The checkout form data. + * + * @var array + */ + private $checkout_form = array(); + /** * Returns the order. * @@ -55,6 +62,8 @@ class SessionHandler { public function order() { $this->load_session(); + do_action( 'ppcp_session_get_order', $this->order, $this ); + return $this->order; } @@ -71,6 +80,30 @@ class SessionHandler { $this->store_session(); } + /** + * Returns the checkout form data. + * + * @return array + */ + public function checkout_form(): array { + $this->load_session(); + + return $this->checkout_form; + } + + /** + * Replaces the checkout form data. + * + * @param array $checkout_form The checkout form data. + */ + public function replace_checkout_form( array $checkout_form ): void { + $this->load_session(); + + $this->checkout_form = $checkout_form; + + $this->store_session(); + } + /** * Returns the BN Code. * @@ -151,6 +184,7 @@ class SessionHandler { $this->bn_code = ''; $this->insufficient_funding_tries = 0; $this->funding_source = null; + $this->checkout_form = array(); $this->store_session(); return $this; } @@ -188,6 +222,7 @@ class SessionHandler { if ( ! is_string( $this->funding_source ) ) { $this->funding_source = null; } + $this->checkout_form = $data['checkout_form'] ?? array(); } /** @@ -202,6 +237,7 @@ class SessionHandler { 'bn_code' => $obj->bn_code, 'insufficient_funding_tries' => $obj->insufficient_funding_tries, 'funding_source' => $obj->funding_source, + 'checkout_form' => $obj->checkout_form, ); } } diff --git a/modules/ppcp-session/src/SessionModule.php b/modules/ppcp-session/src/SessionModule.php index a27effb47..cecc1a98f 100644 --- a/modules/ppcp-session/src/SessionModule.php +++ b/modules/ppcp-session/src/SessionModule.php @@ -9,6 +9,11 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Session; +use Psr\Log\LoggerInterface; +use Throwable; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; +use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use WooCommerce\PayPalCommerce\Session\Cancellation\CancelController; @@ -19,6 +24,12 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; * Class SessionModule */ class SessionModule implements ModuleInterface { + /** + * A flag to avoid multiple requests to reload order. + * + * @var bool + */ + private $reloaded_order = false; /** * {@inheritDoc} @@ -46,6 +57,45 @@ class SessionModule implements ModuleInterface { $controller->run(); } ); + + add_action( + 'ppcp_session_get_order', + function ( ?Order $order, SessionHandler $session_handler ) use ( $c ): void { + if ( ! isset( WC()->session ) ) { + return; + } + + if ( $this->reloaded_order ) { + return; + } + + if ( ! $order ) { + return; + } + + if ( $order->status()->is( OrderStatus::APPROVED ) + || $order->status()->is( OrderStatus::COMPLETED ) + ) { + return; + } + + $order_endpoint = $c->get( 'api.endpoint.order' ); + assert( $order_endpoint instanceof OrderEndpoint ); + + $this->reloaded_order = true; + + try { + $session_handler->replace_order( $order_endpoint->order( $order->id() ) ); + } catch ( Throwable $exception ) { + $logger = $c->get( 'woocommerce.logger.woocommerce' ); + assert( $logger instanceof LoggerInterface ); + + $logger->warning( 'Failed to reload PayPal order in the session: ' . $exception->getMessage() ); + } + }, + 10, + 2 + ); } /** diff --git a/modules/ppcp-status-report/src/StatusReportModule.php b/modules/ppcp-status-report/src/StatusReportModule.php index abf84b83b..9e2df9599 100644 --- a/modules/ppcp-status-report/src/StatusReportModule.php +++ b/modules/ppcp-status-report/src/StatusReportModule.php @@ -120,11 +120,19 @@ class StatusReportModule implements ModuleInterface { 'value' => $this->bool_to_html( ! $last_webhook_storage->is_empty() ), ), array( - 'label' => esc_html__( 'Vault enabled', 'woocommerce-paypal-payments' ), - 'exported_label' => 'Vault enabled', - 'description' => esc_html__( 'Whether vaulting is enabled on PayPal account or not.', 'woocommerce-paypal-payments' ), + 'label' => esc_html__( 'PayPal Vault enabled', 'woocommerce-paypal-payments' ), + 'exported_label' => 'PayPal Vault enabled', + 'description' => esc_html__( 'Whether vaulting option is enabled on Standard Payments settings or not.', 'woocommerce-paypal-payments' ), 'value' => $this->bool_to_html( - $this->vault_enabled( $bearer ) + $settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' ) + ), + ), + array( + 'label' => esc_html__( 'ACDC Vault enabled', 'woocommerce-paypal-payments' ), + 'exported_label' => 'ACDC Vault enabled', + 'description' => esc_html__( 'Whether vaulting option is enabled on Advanced Card Processing settings or not.', 'woocommerce-paypal-payments' ), + 'value' => $this->bool_to_html( + $settings->has( 'vault_enabled_dcc' ) && $settings->get( 'vault_enabled_dcc' ) ), ), array( @@ -192,21 +200,6 @@ class StatusReportModule implements ModuleInterface { return $token->is_valid() && $current_state === $state::STATE_ONBOARDED; } - /** - * It returns whether vaulting is enabled or not. - * - * @param Bearer $bearer The bearer. - * @return bool - */ - private function vault_enabled( Bearer $bearer ): bool { - try { - $token = $bearer->bearer(); - return $token->vaulting_available(); - } catch ( RuntimeException $exception ) { - return false; - } - } - /** * Checks if reference transactions are enabled in account. * diff --git a/modules/ppcp-subscription/.gitignore b/modules/ppcp-subscription/.gitignore new file mode 100644 index 000000000..265c2208c --- /dev/null +++ b/modules/ppcp-subscription/.gitignore @@ -0,0 +1,2 @@ +node_modules +assets diff --git a/modules/ppcp-uninstall/services.php b/modules/ppcp-uninstall/services.php index 054a0d9dc..6f7cda722 100644 --- a/modules/ppcp-uninstall/services.php +++ b/modules/ppcp-uninstall/services.php @@ -33,6 +33,7 @@ return array( 'woocommerce-ppcp-version', WebhookSimulation::OPTION_ID, WebhookRegistrar::KEY, + 'ppcp_payment_tokens_migration_initialized', ); }, @@ -40,6 +41,7 @@ return array( return array( 'woocommerce_paypal_payments_check_pui_payment_captured', 'woocommerce_paypal_payments_check_saved_payment', + 'woocommerce_paypal_payments_payment_tokens_migration', ); }, diff --git a/modules/ppcp-vaulting/src/VaultingModule.php b/modules/ppcp-vaulting/src/VaultingModule.php index 824f206e8..5b7251abb 100644 --- a/modules/ppcp-vaulting/src/VaultingModule.php +++ b/modules/ppcp-vaulting/src/VaultingModule.php @@ -178,6 +178,9 @@ class VaultingModule implements ModuleInterface { } ); + /** + * Allows running migration externally via `do_action('pcp_migrate_payment_tokens')`. + */ add_action( 'pcp_migrate_payment_tokens', function() use ( $container ) { @@ -218,6 +221,10 @@ class VaultingModule implements ModuleInterface { * @return void */ public function migrate_payment_tokens( LoggerInterface $logger ): void { + $initialized = get_option( 'ppcp_payment_tokens_migration_initialized', null ); + if ( $initialized ) { + return; + } // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query $customers = new WP_User_Query( @@ -235,12 +242,19 @@ class VaultingModule implements ModuleInterface { return; } - $logger->info( 'Starting payment tokens migration for ' . (string) count( $customers ) . ' users' ); + $logger->info( 'Identified ' . (string) count( $customers ) . ' users with payment tokens. Initiating token migration.' ); + update_option( 'ppcp_payment_tokens_migration_initialized', true ); $interval_in_seconds = 5; $timestamp = time(); foreach ( $customers as $id ) { + $tokens = array_filter( get_user_meta( $id, 'ppcp-vault-token' ) ); + $skip_empty_key_migration = apply_filters( 'ppcp_skip_payment_tokens_empty_key_migration', true ); + if ( empty( $tokens ) && $skip_empty_key_migration ) { + continue; + } + /** * Function already exist in WooCommerce * diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 901c1f94a..478cc35e0 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -57,6 +57,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice; use WooCommerce\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice; use WooCommerce\PayPalCommerce\WcGateway\Notice\GatewayWithoutPayPalAdminNotice; +use WooCommerce\PayPalCommerce\WcGateway\Notice\UnsupportedCurrencyAdminNotice; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; @@ -208,6 +209,12 @@ return array( $settings = $container->get( 'wcgateway.settings' ); return new ConnectAdminNotice( $state, $settings ); }, + 'wcgateway.notice.currency-unsupported' => static function ( ContainerInterface $container ): UnsupportedCurrencyAdminNotice { + $state = $container->get( 'onboarding.state' ); + $shop_currency = $container->get( 'api.shop.currency' ); + $supported_currencies = $container->get( 'api.supported-currencies' ); + return new UnsupportedCurrencyAdminNotice( $state, $shop_currency, $supported_currencies ); + }, 'wcgateway.notice.dcc-without-paypal' => static function ( ContainerInterface $container ): GatewayWithoutPayPalAdminNotice { return new GatewayWithoutPayPalAdminNotice( CreditCardGateway::ID, @@ -223,7 +230,8 @@ return array( $container->get( 'onboarding.state' ), $container->get( 'wcgateway.settings' ), $container->get( 'wcgateway.is-wc-payments-page' ), - $container->get( 'wcgateway.is-ppcp-settings-page' ) + $container->get( 'wcgateway.is-ppcp-settings-page' ), + $container->get( 'wcgateway.settings.status' ) ); }, 'wcgateway.notice.authorize-order-action' => @@ -903,6 +911,12 @@ return array( 'paylater' => _x( 'Pay Later', 'Name of payment method', 'woocommerce-paypal-payments' ), ); }, + /** + * The sources that do not cause issues about redirecting (on mobile, ...) and sometimes not returning back. + */ + 'wcgateway.funding-sources-without-redirect' => static function( ContainerInterface $container ): array { + return array( 'paypal', 'paylater', 'venmo', 'card' ); + }, 'wcgateway.settings.funding-sources' => static function( ContainerInterface $container ): array { return array_diff_key( $container->get( 'wcgateway.all-funding-sources' ), @@ -938,11 +952,11 @@ return array( 'wcgateway.endpoint.return-url' => static function ( ContainerInterface $container ) : ReturnUrlEndpoint { $gateway = $container->get( 'wcgateway.paypal-gateway' ); $endpoint = $container->get( 'api.endpoint.order' ); - $prefix = $container->get( 'api.prefix' ); return new ReturnUrlEndpoint( $gateway, $endpoint, - $prefix + $container->get( 'session.handler' ), + $container->get( 'woocommerce.logger.woocommerce' ) ); }, diff --git a/modules/ppcp-wc-gateway/src/Checkout/DisableGateways.php b/modules/ppcp-wc-gateway/src/Checkout/DisableGateways.php index bbd9b30d3..08e263541 100644 --- a/modules/ppcp-wc-gateway/src/Checkout/DisableGateways.php +++ b/modules/ppcp-wc-gateway/src/Checkout/DisableGateways.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Checkout; +use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; @@ -20,6 +21,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; * Class DisableGateways */ class DisableGateways { + use ContextTrait; /** * The Session Handler. @@ -82,8 +84,11 @@ class DisableGateways { unset( $methods[ CreditCardGateway::ID ] ); } - if ( ! $this->settings_status->is_smart_button_enabled_for_location( 'checkout' ) && ! $this->session_handler->order() && is_checkout() ) { - unset( $methods[ PayPalGateway::ID ] ); + if ( ! $this->settings_status->is_smart_button_enabled_for_location( 'checkout' ) ) { + unset( $methods[ CardButtonGateway::ID ] ); + if ( ! $this->session_handler->order() && is_checkout() ) { + unset( $methods[ PayPalGateway::ID ] ); + } } if ( ! $this->needs_to_disable_gateways() ) { @@ -124,20 +129,6 @@ class DisableGateways { * @return bool */ private function needs_to_disable_gateways(): bool { - $order = $this->session_handler->order(); - if ( ! $order ) { - return false; - } - - $source = $order->payment_source(); - if ( $source && $source->card() ) { - return false; // DCC. - } - - if ( 'card' === $this->session_handler->funding_source() ) { - return false; // Card buttons. - } - - return true; + return $this->is_paypal_continuation(); } } diff --git a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php index 57c29c2a4..3efe16542 100644 --- a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php +++ b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php @@ -9,17 +9,18 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Endpoint; +use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; +use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXOGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; -use WooCommerce\PayPalCommerce\Webhooks\Handler\PrefixTrait; /** * Class ReturnUrlEndpoint */ class ReturnUrlEndpoint { - use PrefixTrait; const ENDPOINT = 'ppc-return-url'; /** @@ -36,17 +37,38 @@ class ReturnUrlEndpoint { */ private $order_endpoint; + /** + * The session handler + * + * @var SessionHandler + */ + protected $session_handler; + + /** + * The logger. + * + * @var LoggerInterface + */ + protected $logger; + /** * ReturnUrlEndpoint constructor. * - * @param PayPalGateway $gateway The PayPal Gateway. - * @param OrderEndpoint $order_endpoint The Order Endpoint. - * @param string $prefix The prefix. + * @param PayPalGateway $gateway The PayPal Gateway. + * @param OrderEndpoint $order_endpoint The Order Endpoint. + * @param SessionHandler $session_handler The session handler. + * @param LoggerInterface $logger The logger. */ - public function __construct( PayPalGateway $gateway, OrderEndpoint $order_endpoint, string $prefix ) { - $this->gateway = $gateway; - $this->order_endpoint = $order_endpoint; - $this->prefix = $prefix; + public function __construct( + PayPalGateway $gateway, + OrderEndpoint $order_endpoint, + SessionHandler $session_handler, + LoggerInterface $logger + ) { + $this->gateway = $gateway; + $this->order_endpoint = $order_endpoint; + $this->session_handler = $session_handler; + $this->logger = $logger; } /** @@ -63,13 +85,25 @@ class ReturnUrlEndpoint { // phpcs:enable WordPress.Security.NonceVerification.Recommended $order = $this->order_endpoint->order( $token ); - $wc_order_id = $this->sanitize_custom_id( $order->purchase_units()[0]->custom_id() ); + $wc_order_id = (int) $order->purchase_units()[0]->custom_id(); if ( ! $wc_order_id ) { + // We cannot finish processing here without WC order, but at least go into the continuation mode. + if ( $order->status()->is( OrderStatus::APPROVED ) + || $order->status()->is( OrderStatus::COMPLETED ) + ) { + $this->session_handler->replace_order( $order ); + + wp_safe_redirect( wc_get_checkout_url() ); + exit(); + } + + $this->logger->warning( "Return URL endpoint $token: no WC order ID." ); exit(); } $wc_order = wc_get_order( $wc_order_id ); if ( ! is_a( $wc_order, \WC_Order::class ) ) { + $this->logger->warning( "Return URL endpoint $token: WC order $wc_order_id not found." ); exit(); } diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php index 0a5835c31..c73d78a42 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php @@ -13,6 +13,7 @@ use Exception; use Psr\Log\LoggerInterface; use WC_Order; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; @@ -253,8 +254,13 @@ class PayPalGateway extends \WC_Payment_Gateway { $funding_source = $this->session_handler->funding_source(); if ( $funding_source ) { - $this->title = $this->funding_source_renderer->render_name( $funding_source ); - $this->description = $this->funding_source_renderer->render_description( $funding_source ); + $order = $this->session_handler->order(); + if ( $order && + ( $order->status()->is( OrderStatus::APPROVED ) || $order->status()->is( OrderStatus::COMPLETED ) ) + ) { + $this->title = $this->funding_source_renderer->render_name( $funding_source ); + $this->description = $this->funding_source_renderer->render_description( $funding_source ); + } } $this->init_form_fields(); @@ -271,6 +277,27 @@ class PayPalGateway extends \WC_Payment_Gateway { $this->order_endpoint = $order_endpoint; } + /** + * Return the gateway's title. + * + * @return string + */ + public function get_title() { + if ( is_admin() ) { + // $theorder and other things for retrieving the order or post info are not available + // in the constructor, so must do it here. + global $theorder; + if ( $theorder instanceof WC_Order ) { + $payment_method_title = $theorder->get_payment_method_title(); + if ( $payment_method_title ) { + $this->title = $payment_method_title; + } + } + } + + return parent::get_title(); + } + /** * Whether the Gateway needs to be setup. * @@ -533,12 +560,15 @@ class PayPalGateway extends \WC_Payment_Gateway { 'INSTRUMENT_DECLINED' => __( 'Instrument declined.', 'woocommerce-paypal-payments' ), 'PAYER_ACTION_REQUIRED' => __( 'Payer action required, possibly overcharge.', 'woocommerce-paypal-payments' ), ); - $retry_errors = array_filter( - array_keys( $retry_keys_messages ), - function ( string $key ) use ( $error ): bool { - return $error->has_detail( $key ); - } + $retry_errors = array_values( + array_filter( + array_keys( $retry_keys_messages ), + function ( string $key ) use ( $error ): bool { + return $error->has_detail( $key ); + } + ) ); + if ( $retry_errors ) { $retry_error_key = $retry_errors[0]; diff --git a/modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php b/modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php index 08a20651b..81b057423 100644 --- a/modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php +++ b/modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php @@ -13,11 +13,16 @@ use WC_Payment_Gateway; use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; /** * Creates the admin message about the gateway being enabled without the PayPal gateway. */ class GatewayWithoutPayPalAdminNotice { + private const NOTICE_OK = ''; + private const NOTICE_DISABLED_GATEWAY = 'disabled_gateway'; + private const NOTICE_DISABLED_LOCATION = 'disabled_location'; + /** * The gateway ID. * @@ -53,27 +58,37 @@ class GatewayWithoutPayPalAdminNotice { */ private $is_ppcp_settings_page; + /** + * The Settings status helper. + * + * @var SettingsStatus|null + */ + protected $settings_status; + /** * ConnectAdminNotice constructor. * - * @param string $id The gateway ID. - * @param State $state The state. - * @param ContainerInterface $settings The settings. - * @param bool $is_payments_page Whether the current page is the WC payment page. - * @param bool $is_ppcp_settings_page Whether the current page is the PPCP settings page. + * @param string $id The gateway ID. + * @param State $state The state. + * @param ContainerInterface $settings The settings. + * @param bool $is_payments_page Whether the current page is the WC payment page. + * @param bool $is_ppcp_settings_page Whether the current page is the PPCP settings page. + * @param SettingsStatus|null $settings_status The Settings status helper. */ public function __construct( string $id, State $state, ContainerInterface $settings, bool $is_payments_page, - bool $is_ppcp_settings_page + bool $is_ppcp_settings_page, + ?SettingsStatus $settings_status = null ) { $this->id = $id; $this->state = $state; $this->settings = $settings; $this->is_payments_page = $is_payments_page; $this->is_ppcp_settings_page = $is_ppcp_settings_page; + $this->settings_status = $settings_status; } /** @@ -82,8 +97,25 @@ class GatewayWithoutPayPalAdminNotice { * @return Message|null */ public function message(): ?Message { - if ( ! $this->should_display() ) { - return null; + $notice_type = $this->check(); + + switch ( $notice_type ) { + case self::NOTICE_DISABLED_GATEWAY: + /* translators: %1$s the gateway name, %2$s URL. */ + $text = __( + '%1$s cannot be used without the PayPal gateway. Enable the PayPal gateway.', + 'woocommerce-paypal-payments' + ); + break; + case self::NOTICE_DISABLED_LOCATION: + /* translators: %1$s the gateway name, %2$s URL. */ + $text = __( + '%1$s cannot be used without enabling the Checkout location for the PayPal gateway. Enable the Checkout location.', + 'woocommerce-paypal-payments' + ); + break; + default: + return null; } $gateway = $this->get_gateway(); @@ -94,11 +126,7 @@ class GatewayWithoutPayPalAdminNotice { $name = $gateway->get_method_title(); $message = sprintf( - /* translators: %1$s the gateway name, %2$s URL. */ - __( - '%1$s cannot be used without the PayPal gateway. Enable the PayPal gateway.', - 'woocommerce-paypal-payments' - ), + $text, $name, admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' ) ); @@ -106,22 +134,33 @@ class GatewayWithoutPayPalAdminNotice { } /** - * Whether the message should be displayed. + * Checks whether one of the messages should be displayed. * - * @return bool + * @return string One of the NOTICE_* constants. */ - protected function should_display(): bool { + protected function check(): string { if ( State::STATE_ONBOARDED !== $this->state->current_state() || ( ! $this->is_payments_page && ! $this->is_ppcp_settings_page ) ) { - return false; - } - if ( $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ) ) { - return false; + return self::NOTICE_OK; } - $gateway = $this->get_gateway(); + $gateway = $this->get_gateway(); + $gateway_enabled = $gateway && wc_string_to_bool( $gateway->get_option( 'enabled' ) ); - return $gateway && wc_string_to_bool( $gateway->get_option( 'enabled' ) ); + if ( ! $gateway_enabled ) { + return self::NOTICE_OK; + } + + $paypal_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ); + if ( ! $paypal_enabled ) { + return self::NOTICE_DISABLED_GATEWAY; + } + + if ( $this->settings_status && ! $this->settings_status->is_smart_button_enabled_for_location( 'checkout' ) ) { + return self::NOTICE_DISABLED_LOCATION; + } + + return self::NOTICE_OK; } /** diff --git a/modules/ppcp-wc-gateway/src/Notice/UnsupportedCurrencyAdminNotice.php b/modules/ppcp-wc-gateway/src/Notice/UnsupportedCurrencyAdminNotice.php new file mode 100644 index 000000000..27ef14f79 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Notice/UnsupportedCurrencyAdminNotice.php @@ -0,0 +1,97 @@ +state = $state; + $this->shop_currency = $shop_currency; + $this->supported_currencies = $supported_currencies; + } + + /** + * Returns the message. + * + * @return Message|null + */ + public function unsupported_currency_message() { + if ( ! $this->should_display() ) { + return null; + } + + $message = sprintf( + /* translators: %1$s the shop currency, 2$s the gateway name. */ + __( + 'Attention: Your current WooCommerce store currency (%1$s) is not supported by PayPal. Please update your store currency to one that is supported by PayPal to ensure smooth transactions. Visit the PayPal currency support page for more information on supported currencies.', + 'woocommerce-paypal-payments' + ), + $this->shop_currency, + 'https://developer.paypal.com/api/rest/reference/currency-codes/' + ); + return new Message( $message, 'warning' ); + } + + /** + * Whether the message should display. + * + * @return bool + */ + protected function should_display(): bool { + return $this->state->current_state() === State::STATE_ONBOARDED && ! $this->currency_supported(); + } + + /** + * Whether the currency is supported by PayPal. + * + * @return bool + */ + private function currency_supported(): bool { + $currency = $this->shop_currency; + $supported_currencies = $this->supported_currencies; + return in_array( $currency, $supported_currencies, true ); + } +} diff --git a/modules/ppcp-wc-gateway/src/Processor/OrderProcessor.php b/modules/ppcp-wc-gateway/src/Processor/OrderProcessor.php index 6cc9c7ce4..443fd1cbd 100644 --- a/modules/ppcp-wc-gateway/src/Processor/OrderProcessor.php +++ b/modules/ppcp-wc-gateway/src/Processor/OrderProcessor.php @@ -166,7 +166,7 @@ class OrderProcessor { // phpcs:ignore WordPress.Security.NonceVerification $order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY ) ?: wc_clean( wp_unslash( $_POST['paypal_order_id'] ?? '' ) ); $order = $this->session_handler->order(); - if ( ! $order && is_string( $order_id ) ) { + if ( ! $order && is_string( $order_id ) && $order_id ) { $order = $this->order_endpoint->order( $order_id ); } if ( ! $order ) { @@ -178,7 +178,7 @@ class OrderProcessor { $wc_order->get_id() ) ); - $this->last_error = __( 'Could not retrieve order. This browser may not be supported. Please try again with a different browser.', 'woocommerce-paypal-payments' ); + $this->last_error = __( 'Could not retrieve order. Maybe it was already completed or this browser is not supported. Please check your email or try again with a different browser.', 'woocommerce-paypal-payments' ); return false; } diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/pay-later-tab-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/pay-later-tab-fields.php index fab61ca83..6428b1389 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/pay-later-tab-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/pay-later-tab-fields.php @@ -33,11 +33,12 @@ return function ( ContainerInterface $container, array $fields ): array { $selected_country = $container->get( 'api.shop.country' ); $default_messaging_flex_color = $selected_country === 'US' ? 'white-no-border' : 'white'; - - $render_preview_element = function ( string $id, string $type ): string { + $button_message = __( 'Pay Later Button Preview', 'woocommerce-paypal-payments' ); + $messaging_message = __( 'Pay Later Messaging Preview', 'woocommerce-paypal-payments' ); + $render_preview_element = function ( string $id, string $type, string $message ): string { return '
-

' . __( 'Preview', 'woocommerce-paypal-payments' ) . '

+

' . $message . '

'; }; @@ -82,7 +83,7 @@ return function ( ContainerInterface $container, array $fields ): array { ), 'pay_later_button_preview' => array( 'type' => 'ppcp-text', - 'text' => $render_preview_element( 'ppcpPayLaterButtonPreview', 'button' ), + 'text' => $render_preview_element( 'ppcpPayLaterButtonPreview', 'button', $button_message ), 'screens' => array( State::STATE_ONBOARDED ), 'requirements' => array( 'messages' ), 'gateway' => Settings::PAY_LATER_TAB_ID, @@ -245,7 +246,7 @@ return function ( ContainerInterface $container, array $fields ): array { ), 'pay_later_general_message_preview' => array( 'type' => 'ppcp-text', - 'text' => $render_preview_element( 'ppcpGeneralMessagePreview', 'message' ), + 'text' => $render_preview_element( 'ppcpGeneralMessagePreview', 'message', $messaging_message ), 'screens' => array( State::STATE_ONBOARDED ), 'requirements' => array( 'messages' ), 'gateway' => Settings::PAY_LATER_TAB_ID, @@ -369,7 +370,7 @@ return function ( ContainerInterface $container, array $fields ): array { ), 'pay_later_product_message_preview' => array( 'type' => 'ppcp-text', - 'text' => $render_preview_element( 'ppcpProductMessagePreview', 'message' ), + 'text' => $render_preview_element( 'ppcpProductMessagePreview', 'message', $messaging_message ), 'screens' => array( State::STATE_ONBOARDED ), 'requirements' => array( 'messages' ), 'gateway' => Settings::PAY_LATER_TAB_ID, @@ -493,7 +494,7 @@ return function ( ContainerInterface $container, array $fields ): array { ), 'pay_later_cart_message_preview' => array( 'type' => 'ppcp-text', - 'text' => $render_preview_element( 'ppcpCartMessagePreview', 'message' ), + 'text' => $render_preview_element( 'ppcpCartMessagePreview', 'message', $messaging_message ), 'screens' => array( State::STATE_ONBOARDED ), 'requirements' => array( 'messages' ), 'gateway' => Settings::PAY_LATER_TAB_ID, @@ -617,7 +618,7 @@ return function ( ContainerInterface $container, array $fields ): array { ), 'pay_later_checkout_message_preview' => array( 'type' => 'ppcp-text', - 'text' => $render_preview_element( 'ppcpCheckoutMessagePreview', 'message' ), + 'text' => $render_preview_element( 'ppcpCheckoutMessagePreview', 'message', $messaging_message ), 'screens' => array( State::STATE_ONBOARDED ), 'requirements' => array( 'messages' ), 'gateway' => Settings::PAY_LATER_TAB_ID, diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/paypal-smart-button-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/paypal-smart-button-fields.php index f064b1e7b..82fa1d5f3 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/paypal-smart-button-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/paypal-smart-button-fields.php @@ -31,7 +31,7 @@ return function ( ContainerInterface $container, array $fields ): array { $render_preview_element = function ( string $id ): string { return '
-

' . __( 'Preview', 'woocommerce-paypal-payments' ) . '

+

' . __( 'Button Styling Preview', 'woocommerce-paypal-payments' ) . '

'; }; diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index f49fb8f08..0a9c4cdb0 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -39,6 +39,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceProductStatus; use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; use WooCommerce\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice; use WooCommerce\PayPalCommerce\WcGateway\Notice\GatewayWithoutPayPalAdminNotice; +use WooCommerce\PayPalCommerce\WcGateway\Notice\UnsupportedCurrencyAdminNotice; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Settings\HeaderRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer; @@ -197,6 +198,13 @@ class WCGatewayModule implements ModuleInterface { $notices[] = $connect_message; } + $notice = $c->get( 'wcgateway.notice.currency-unsupported' ); + assert( $notice instanceof UnsupportedCurrencyAdminNotice ); + $unsupported_currency_message = $notice->unsupported_currency_message(); + if ( $unsupported_currency_message ) { + $notices[] = $unsupported_currency_message; + } + foreach ( array( $c->get( 'wcgateway.notice.dcc-without-paypal' ), $c->get( 'wcgateway.notice.card-button-without-paypal' ), @@ -278,6 +286,15 @@ class WCGatewayModule implements ModuleInterface { $settings->set( 'products_dcc_enabled', false ); $settings->set( 'products_pui_enabled', false ); $settings->persist(); + + // Update caches. + $dcc_status = $c->get( 'wcgateway.helper.dcc-product-status' ); + assert( $dcc_status instanceof DCCProductStatus ); + $dcc_status->dcc_is_active(); + + $pui_status = $c->get( 'wcgateway.pay-upon-invoice-product-status' ); + assert( $pui_status instanceof PayUponInvoiceProductStatus ); + $pui_status->pui_is_active(); } ); @@ -615,7 +632,7 @@ class WCGatewayModule implements ModuleInterface { * @var OrderTablePaymentStatusColumn $payment_status_column */ $payment_status_column = $container->get( 'wcgateway.admin.orders-payment-status-column' ); - $payment_status_column->render( $column, intval( $wc_order_id ) ); + $payment_status_column->render( (string) $column, intval( $wc_order_id ) ); }, 10, 2 diff --git a/modules/ppcp-webhooks/services.php b/modules/ppcp-webhooks/services.php index ce0338228..cd0424a19 100644 --- a/modules/ppcp-webhooks/services.php +++ b/modules/ppcp-webhooks/services.php @@ -82,12 +82,18 @@ return array( $payment_token_factory = $container->get( 'vaulting.payment-token-factory' ); return array( - new CheckoutOrderApproved( $logger, $prefix, $order_endpoint ), - new CheckoutOrderCompleted( $logger, $prefix ), + new CheckoutOrderApproved( + $logger, + $order_endpoint, + $container->get( 'session.handler' ), + $container->get( 'wcgateway.funding-source.renderer' ), + $container->get( 'wcgateway.order-processor' ) + ), + new CheckoutOrderCompleted( $logger ), new CheckoutPaymentApprovalReversed( $logger ), - new PaymentCaptureRefunded( $logger, $prefix ), - new PaymentCaptureReversed( $logger, $prefix ), - new PaymentCaptureCompleted( $logger, $prefix, $order_endpoint ), + new PaymentCaptureRefunded( $logger ), + new PaymentCaptureReversed( $logger ), + new PaymentCaptureCompleted( $logger, $order_endpoint ), new VaultPaymentTokenCreated( $logger, $prefix, $authorized_payments_processor, $payment_token_factory ), new VaultPaymentTokenDeleted( $logger ), new PaymentCapturePending( $logger ), diff --git a/modules/ppcp-webhooks/src/CustomIds.php b/modules/ppcp-webhooks/src/CustomIds.php new file mode 100644 index 000000000..97a78e43d --- /dev/null +++ b/modules/ppcp-webhooks/src/CustomIds.php @@ -0,0 +1,18 @@ + false ); if ( is_null( $request['resource'] ) ) { - return new WP_REST_Response( $response ); + return $this->failure_response(); } $plan_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) ); @@ -92,7 +92,6 @@ class BillingPlanPricingChangeActivated implements RequestHandler { } } - $response['success'] = true; - return new WP_REST_Response( $response ); + return $this->success_response(); } } diff --git a/modules/ppcp-webhooks/src/Handler/BillingPlanUpdated.php b/modules/ppcp-webhooks/src/Handler/BillingPlanUpdated.php index 1c6aa14c9..b341a80c2 100644 --- a/modules/ppcp-webhooks/src/Handler/BillingPlanUpdated.php +++ b/modules/ppcp-webhooks/src/Handler/BillingPlanUpdated.php @@ -17,6 +17,7 @@ use WP_REST_Response; * Class BillingPlanUpdated */ class BillingPlanUpdated implements RequestHandler { + use RequestHandlerTrait; /** * The logger. @@ -64,9 +65,8 @@ class BillingPlanUpdated implements RequestHandler { * @return WP_REST_Response */ public function handle_request( WP_REST_Request $request ): WP_REST_Response { - $response = array( 'success' => false ); if ( is_null( $request['resource'] ) ) { - return new WP_REST_Response( $response ); + return $this->failure_response(); } $plan_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) ); @@ -109,7 +109,6 @@ class BillingPlanUpdated implements RequestHandler { } } - $response['success'] = true; - return new WP_REST_Response( $response ); + return $this->success_response(); } } diff --git a/modules/ppcp-webhooks/src/Handler/BillingSubscriptionCancelled.php b/modules/ppcp-webhooks/src/Handler/BillingSubscriptionCancelled.php index 8b06f06fe..0fc79f0d3 100644 --- a/modules/ppcp-webhooks/src/Handler/BillingSubscriptionCancelled.php +++ b/modules/ppcp-webhooks/src/Handler/BillingSubscriptionCancelled.php @@ -17,6 +17,7 @@ use WP_REST_Response; * Class BillingSubscriptionCancelled */ class BillingSubscriptionCancelled implements RequestHandler { + use RequestHandlerTrait; /** * The logger. @@ -64,9 +65,8 @@ class BillingSubscriptionCancelled implements RequestHandler { * @return WP_REST_Response */ public function handle_request( WP_REST_Request $request ): WP_REST_Response { - $response = array( 'success' => false ); if ( is_null( $request['resource'] ) ) { - return new WP_REST_Response( $response ); + return $this->failure_response(); } $subscription_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) ); @@ -87,7 +87,6 @@ class BillingSubscriptionCancelled implements RequestHandler { } } - $response['success'] = true; - return new WP_REST_Response( $response ); + return $this->success_response(); } } diff --git a/modules/ppcp-webhooks/src/Handler/CatalogProductUpdated.php b/modules/ppcp-webhooks/src/Handler/CatalogProductUpdated.php index 21e5423c3..dea42333d 100644 --- a/modules/ppcp-webhooks/src/Handler/CatalogProductUpdated.php +++ b/modules/ppcp-webhooks/src/Handler/CatalogProductUpdated.php @@ -17,6 +17,7 @@ use WP_REST_Response; * Class CatalogProductUpdated */ class CatalogProductUpdated implements RequestHandler { + use RequestHandlerTrait; /** * The logger. @@ -64,9 +65,8 @@ class CatalogProductUpdated implements RequestHandler { * @return WP_REST_Response */ public function handle_request( WP_REST_Request $request ): WP_REST_Response { - $response = array( 'success' => false ); if ( is_null( $request['resource'] ) ) { - return new WP_REST_Response( $response ); + return $this->failure_response(); } $product_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) ); @@ -104,7 +104,6 @@ class CatalogProductUpdated implements RequestHandler { } } - $response['success'] = true; - return new WP_REST_Response( $response ); + return $this->success_response(); } } diff --git a/modules/ppcp-webhooks/src/Handler/CheckoutOrderApproved.php b/modules/ppcp-webhooks/src/Handler/CheckoutOrderApproved.php index 350d0e168..60b51b513 100644 --- a/modules/ppcp-webhooks/src/Handler/CheckoutOrderApproved.php +++ b/modules/ppcp-webhooks/src/Handler/CheckoutOrderApproved.php @@ -9,18 +9,25 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Webhooks\Handler; +use WC_Checkout; +use WC_Order; +use WC_Session_Handler; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; -use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; +use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use Psr\Log\LoggerInterface; +use WooCommerce\PayPalCommerce\Session\MemoryWcSession; +use WooCommerce\PayPalCommerce\Session\SessionHandler; +use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer; use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXOGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway; +use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; /** * Class CheckoutOrderApproved */ class CheckoutOrderApproved implements RequestHandler { - use PrefixTrait; + use RequestHandlerTrait; /** * The logger. @@ -36,17 +43,48 @@ class CheckoutOrderApproved implements RequestHandler { */ private $order_endpoint; + /** + * The Session handler. + * + * @var SessionHandler + */ + private $session_handler; + + /** + * The funding source renderer. + * + * @var FundingSourceRenderer + */ + protected $funding_source_renderer; + + /** + * The processor for orders. + * + * @var OrderProcessor + */ + protected $order_processor; + /** * CheckoutOrderApproved constructor. * - * @param LoggerInterface $logger The logger. - * @param string $prefix The prefix. - * @param OrderEndpoint $order_endpoint The order endpoint. + * @param LoggerInterface $logger The logger. + * @param OrderEndpoint $order_endpoint The order endpoint. + * @param SessionHandler $session_handler The session handler. + * @param FundingSourceRenderer $funding_source_renderer The funding source renderer. + * @param OrderProcessor $order_processor The Order Processor. */ - public function __construct( LoggerInterface $logger, string $prefix, OrderEndpoint $order_endpoint ) { - $this->logger = $logger; - $this->prefix = $prefix; - $this->order_endpoint = $order_endpoint; + public function __construct( + LoggerInterface $logger, + OrderEndpoint $order_endpoint, + SessionHandler $session_handler, + FundingSourceRenderer $funding_source_renderer, + OrderProcessor $order_processor + ) { + $this->logger = $logger; + $this->order_endpoint = $order_endpoint; + $this->session_handler = $session_handler; + $this->funding_source_renderer = $funding_source_renderer; + $this->order_processor = $order_processor; } /** @@ -79,114 +117,94 @@ class CheckoutOrderApproved implements RequestHandler { * @return \WP_REST_Response */ public function handle_request( \WP_REST_Request $request ): \WP_REST_Response { - $response = array( 'success' => false ); - $custom_ids = array_filter( - array_map( - static function ( array $purchase_unit ): string { - return isset( $purchase_unit['custom_id'] ) ? - (string) $purchase_unit['custom_id'] : ''; - }, - isset( $request['resource']['purchase_units'] ) ? - (array) $request['resource']['purchase_units'] : array() - ), - static function ( string $order_id ): bool { - return ! empty( $order_id ); - } - ); - - if ( empty( $custom_ids ) ) { - $message = sprintf( - // translators: %s is the PayPal webhook Id. - __( - 'No order for webhook event %s was found.', - 'woocommerce-paypal-payments' - ), - isset( $request['id'] ) ? $request['id'] : '' - ); - $this->logger->log( - 'warning', - $message, - array( - 'request' => $request, + $order_id = isset( $request['resource']['id'] ) ? $request['resource']['id'] : null; + if ( ! $order_id ) { + return $this->failure_response( + sprintf( + 'No order ID in webhook event %s.', + $request['id'] ?: '' ) ); - $response['message'] = $message; - return rest_ensure_response( $response ); } - try { - $order = isset( $request['resource']['id'] ) ? - $this->order_endpoint->order( $request['resource']['id'] ) : null; - if ( ! $order ) { - $message = sprintf( - // translators: %s is the PayPal webhook Id. - __( - 'No paypal payment for webhook event %s was found.', - 'woocommerce-paypal-payments' - ), - isset( $request['id'] ) ? $request['id'] : '' - ); - $this->logger->log( - 'warning', - $message, - array( - 'request' => $request, + $order = $this->order_endpoint->order( $order_id ); + + $wc_orders = array(); + + $wc_order_ids = $this->get_wc_order_ids_from_request( $request ); + if ( empty( $wc_order_ids ) ) { + $customer_ids = $this->get_wc_customer_ids_from_request( $request ); + if ( empty( $customer_ids ) ) { + return $this->no_custom_ids_response( $request ); + } + + $customer_id = $customer_ids[0]; + + if ( $order->status()->is( OrderStatus::COMPLETED ) ) { + $this->logger->info( "Order {$order->id()} already completed." ); + return $this->success_response(); + } + + $wc_session = new WC_Session_Handler(); + + $session_data = $wc_session->get_session( $customer_id ); + if ( ! is_array( $session_data ) ) { + return $this->failure_response( "Failed to get session data {$customer_id}" ); + } + + MemoryWcSession::replace_session_handler( $session_data, $customer_id ); + + wc_load_cart(); + WC()->cart->get_cart_from_session(); + WC()->cart->calculate_shipping(); + + $form = $this->session_handler->checkout_form(); + + $checkout = new WC_Checkout(); + $wc_order_id = $checkout->create_order( $form ); + $wc_order = wc_get_order( $wc_order_id ); + if ( ! $wc_order instanceof WC_Order ) { + return $this->failure_response( + sprintf( + 'Failed to create WC order in webhook event %s.', + $request['id'] ?: '' ) ); - $response['message'] = $message; - return rest_ensure_response( $response ); } - if ( $order->intent() === 'CAPTURE' ) { - $order = $this->order_endpoint->capture( $order ); + $funding_source = $this->session_handler->funding_source(); + if ( $funding_source ) { + $wc_order->set_payment_method_title( $this->funding_source_renderer->render_name( $funding_source ) ); } - } catch ( RuntimeException $error ) { - $message = sprintf( - // translators: %s is the PayPal webhook Id. - __( - 'Could not capture payment for webhook event %s.', - 'woocommerce-paypal-payments' - ), - isset( $request['id'] ) ? $request['id'] : '' - ); - $this->logger->log( - 'warning', - $message, - array( - 'request' => $request, - ) - ); - $response['message'] = $message; - return rest_ensure_response( $response ); - } - $wc_order_ids = array_map( - array( - $this, - 'sanitize_custom_id', - ), - $custom_ids - ); - $args = array( - 'post__in' => $wc_order_ids, - 'limit' => -1, - ); - $wc_orders = wc_get_orders( $args ); - if ( ! $wc_orders ) { - $message = sprintf( - // translators: %s is the PayPal order Id. - __( 'Order for PayPal order %s not found.', 'woocommerce-paypal-payments' ), - isset( $request['resource']['id'] ) ? $request['resource']['id'] : '' + if ( is_numeric( $customer_id ) ) { + $wc_order->set_customer_id( (int) $customer_id ); + } + + $wc_order->save(); + + $wc_orders[] = $wc_order; + + add_action( + 'shutdown', + function () use ( $customer_id ): void { + $session = WC()->session; + assert( $session instanceof WC_Session_Handler ); + + /** + * Wrong type-hint. + * + * @psalm-suppress InvalidScalarArgument + */ + $session->delete_session( $customer_id ); + $session->forget_session(); + } ); - $this->logger->log( - 'warning', - $message, - array( - 'request' => $request, - ) - ); - $response['message'] = $message; - return rest_ensure_response( $response ); + } else { + $wc_orders = $this->get_wc_orders_from_custom_ids( $wc_order_ids ); + if ( ! $wc_orders ) { + return $this->no_wc_orders_response( $request ); + } } foreach ( $wc_orders as $wc_order ) { @@ -197,31 +215,24 @@ class CheckoutOrderApproved implements RequestHandler { if ( ! in_array( $wc_order->get_status(), array( 'pending', 'on-hold' ), true ) ) { continue; } - if ( $order->intent() === 'CAPTURE' ) { - $wc_order->payment_complete(); - } else { - $wc_order->update_status( - 'on-hold', - __( 'Payment can be captured.', 'woocommerce-paypal-payments' ) + + if ( ! $this->order_processor->process( $wc_order ) ) { + return $this->failure_response( + sprintf( + 'Failed to process WC order %s: %s.', + (string) $wc_order->get_id(), + $this->order_processor->last_error() + ) ); } - $this->logger->log( - 'info', + + $this->logger->info( sprintf( - // translators: %s is the order ID. - __( - 'Order %s has been updated through PayPal', - 'woocommerce-paypal-payments' - ), + 'WC order %s has been processed after approval in PayPal.', (string) $wc_order->get_id() - ), - array( - 'request' => $request, - 'order' => $wc_order, ) ); } - $response['success'] = true; - return rest_ensure_response( $response ); + return $this->success_response(); } } diff --git a/modules/ppcp-webhooks/src/Handler/CheckoutOrderCompleted.php b/modules/ppcp-webhooks/src/Handler/CheckoutOrderCompleted.php index 85bbd376d..6bf897720 100644 --- a/modules/ppcp-webhooks/src/Handler/CheckoutOrderCompleted.php +++ b/modules/ppcp-webhooks/src/Handler/CheckoutOrderCompleted.php @@ -19,7 +19,7 @@ use WP_REST_Response; */ class CheckoutOrderCompleted implements RequestHandler { - use PrefixTrait, RequestHandlerTrait; + use RequestHandlerTrait; /** * The logger. @@ -32,11 +32,9 @@ class CheckoutOrderCompleted implements RequestHandler { * CheckoutOrderCompleted constructor. * * @param LoggerInterface $logger The logger. - * @param string $prefix The prefix. */ - public function __construct( LoggerInterface $logger, string $prefix ) { + public function __construct( LoggerInterface $logger ) { $this->logger = $logger; - $this->prefix = $prefix; } /** @@ -69,16 +67,14 @@ class CheckoutOrderCompleted implements RequestHandler { * @return WP_REST_Response */ public function handle_request( WP_REST_Request $request ): WP_REST_Response { - $response = array( 'success' => false ); - - $custom_ids = $this->get_custom_ids_from_request( $request ); + $custom_ids = $this->get_wc_order_ids_from_request( $request ); if ( empty( $custom_ids ) ) { - return $this->no_custom_ids_from_request( $request, $response ); + return $this->no_custom_ids_response( $request ); } $wc_orders = $this->get_wc_orders_from_custom_ids( $custom_ids ); if ( ! $wc_orders ) { - return $this->no_wc_orders_from_custom_ids( $request, $response ); + return $this->no_wc_orders_response( $request ); } foreach ( $wc_orders as $wc_order ) { @@ -93,17 +89,12 @@ class CheckoutOrderCompleted implements RequestHandler { $this->logger->info( sprintf( - // translators: %s is the order ID. - __( - 'Order %s has been updated through PayPal', - 'woocommerce-paypal-payments' - ), + 'Order %s has been updated through PayPal', (string) $wc_order->get_id() ) ); } - $response['success'] = true; - return new WP_REST_Response( $response ); + return $this->success_response(); } } diff --git a/modules/ppcp-webhooks/src/Handler/CheckoutPaymentApprovalReversed.php b/modules/ppcp-webhooks/src/Handler/CheckoutPaymentApprovalReversed.php index 7377ec3bd..168d76382 100644 --- a/modules/ppcp-webhooks/src/Handler/CheckoutPaymentApprovalReversed.php +++ b/modules/ppcp-webhooks/src/Handler/CheckoutPaymentApprovalReversed.php @@ -18,7 +18,7 @@ use WP_REST_Response; */ class CheckoutPaymentApprovalReversed implements RequestHandler { - use RequestHandlerTrait, PrefixTrait; + use RequestHandlerTrait; /** * The logger. @@ -66,26 +66,20 @@ class CheckoutPaymentApprovalReversed implements RequestHandler { * @return WP_REST_Response */ public function handle_request( WP_REST_Request $request ): WP_REST_Response { - $response = array( 'success' => false ); - - $custom_ids = $this->get_custom_ids_from_request( $request ); + $custom_ids = $this->get_wc_order_ids_from_request( $request ); if ( empty( $custom_ids ) ) { - return $this->no_custom_ids_from_request( $request, $response ); + return $this->no_custom_ids_response( $request ); } $wc_orders = $this->get_wc_orders_from_custom_ids( $custom_ids ); if ( ! $wc_orders ) { - return $this->no_wc_orders_from_custom_ids( $request, $response ); + return $this->no_wc_orders_response( $request ); } foreach ( $wc_orders as $wc_order ) { if ( in_array( $wc_order->get_status(), array( 'pending', 'on-hold' ), true ) ) { $error_message = sprintf( - // translators: %1$s is the order id. - __( - 'Failed to capture order %1$s through PayPal.', - 'woocommerce-paypal-payments' - ), + 'Failed to capture order %1$s through PayPal.', (string) $wc_order->get_id() ); @@ -95,7 +89,6 @@ class CheckoutPaymentApprovalReversed implements RequestHandler { } } - $response['success'] = true; - return new WP_REST_Response( $response ); + return $this->success_response(); } } diff --git a/modules/ppcp-webhooks/src/Handler/PaymentCaptureCompleted.php b/modules/ppcp-webhooks/src/Handler/PaymentCaptureCompleted.php index eda546118..d8f927b42 100644 --- a/modules/ppcp-webhooks/src/Handler/PaymentCaptureCompleted.php +++ b/modules/ppcp-webhooks/src/Handler/PaymentCaptureCompleted.php @@ -22,7 +22,7 @@ use WP_REST_Response; */ class PaymentCaptureCompleted implements RequestHandler { - use PrefixTrait, TransactionIdHandlingTrait; + use TransactionIdHandlingTrait, RequestHandlerTrait; /** * The logger. @@ -42,16 +42,13 @@ class PaymentCaptureCompleted implements RequestHandler { * PaymentCaptureCompleted constructor. * * @param LoggerInterface $logger The logger. - * @param string $prefix The prefix. * @param OrderEndpoint $order_endpoint The order endpoint. */ public function __construct( LoggerInterface $logger, - string $prefix, OrderEndpoint $order_endpoint ) { $this->logger = $logger; - $this->prefix = $prefix; $this->order_endpoint = $order_endpoint; } @@ -83,33 +80,24 @@ class PaymentCaptureCompleted implements RequestHandler { * @return WP_REST_Response */ public function handle_request( \WP_REST_Request $request ): WP_REST_Response { - $response = array( 'success' => false ); - $webhook_id = (string) ( $request['id'] ?? '' ); $resource = $request['resource']; if ( ! is_array( $resource ) ) { $message = 'Resource data not found in webhook request.'; - $this->logger->warning( $message, array( 'request' => $request ) ); - $response['message'] = $message; - return new WP_REST_Response( $response ); + return $this->failure_response( $message ); } - $wc_order_id = isset( $resource['custom_id'] ) ? - $this->sanitize_custom_id( (string) $resource['custom_id'] ) : 0; + $wc_order_id = isset( $resource['custom_id'] ) ? (string) $resource['custom_id'] : 0; if ( ! $wc_order_id ) { $message = sprintf( 'No order for webhook event %s was found.', $webhook_id ); - $this->logger->warning( $message, array( 'request' => $request ) ); - $response['message'] = $message; - return new WP_REST_Response( $response ); + return $this->failure_response( $message ); } $wc_order = wc_get_order( $wc_order_id ); if ( ! is_a( $wc_order, \WC_Order::class ) ) { $message = sprintf( 'No order for webhook event %s was found.', $webhook_id ); - $this->logger->warning( $message, array( 'request' => $request ) ); - $response['message'] = $message; - return new WP_REST_Response( $response ); + return $this->failure_response( $message ); } $order_id = $resource['supplementary_data']['related_ids']['order_id'] ?? null; @@ -120,8 +108,7 @@ class PaymentCaptureCompleted implements RequestHandler { do_action( 'ppcp_payment_capture_completed_webhook_handler', $wc_order, $order_id ); if ( $wc_order->get_status() !== 'on-hold' ) { - $response['success'] = true; - return new WP_REST_Response( $response ); + return $this->success_response(); } $wc_order->add_order_note( __( 'Payment successfully captured.', 'woocommerce-paypal-payments' ) @@ -130,19 +117,10 @@ class PaymentCaptureCompleted implements RequestHandler { $wc_order->payment_complete(); $wc_order->update_meta_data( AuthorizedPaymentsProcessor::CAPTURED_META_KEY, 'true' ); $wc_order->save(); - $this->logger->log( - 'info', + $this->logger->info( sprintf( - // translators: %s is the order ID. - __( - 'Order %s has been updated through PayPal', - 'woocommerce-paypal-payments' - ), + 'Order %s has been updated through PayPal', (string) $wc_order->get_id() - ), - array( - 'request' => $request, - 'order' => $wc_order, ) ); @@ -159,7 +137,6 @@ class PaymentCaptureCompleted implements RequestHandler { } } - $response['success'] = true; - return new WP_REST_Response( $response ); + return $this->success_response(); } } diff --git a/modules/ppcp-webhooks/src/Handler/PaymentCapturePending.php b/modules/ppcp-webhooks/src/Handler/PaymentCapturePending.php index 225e70121..bbbc41dd8 100644 --- a/modules/ppcp-webhooks/src/Handler/PaymentCapturePending.php +++ b/modules/ppcp-webhooks/src/Handler/PaymentCapturePending.php @@ -18,7 +18,7 @@ use WP_REST_Response; */ class PaymentCapturePending implements RequestHandler { - use PrefixTrait; + use RequestHandlerTrait; /** * The logger. @@ -66,36 +66,21 @@ class PaymentCapturePending implements RequestHandler { * @return WP_REST_Response */ public function handle_request( WP_REST_Request $request ): WP_REST_Response { - $response = array( 'success' => false ); $order_id = $request['resource'] !== null && isset( $request['resource']['custom_id'] ) - ? $this->sanitize_custom_id( $request['resource']['custom_id'] ) + ? $request['resource']['custom_id'] : 0; if ( ! $order_id ) { $message = sprintf( - // translators: %s is the PayPal webhook Id. - __( - 'No order for webhook event %s was found.', - 'woocommerce-paypal-payments' - ), + 'No order for webhook event %s was found.', $request['id'] !== null && isset( $request['id'] ) ? $request['id'] : '' ); - $this->logger->log( - 'warning', - $message, - array( - 'request' => $request, - ) - ); - $response['message'] = $message; - return new WP_REST_Response( $response ); + return $this->failure_response( $message ); } $resource = $request['resource']; if ( ! is_array( $resource ) ) { $message = 'Resource data not found in webhook request.'; - $this->logger->warning( $message, array( 'request' => $request ) ); - $response['message'] = $message; - return new WP_REST_Response( $response ); + return $this->failure_response( $message ); } $wc_order = wc_get_order( $order_id ); @@ -105,10 +90,7 @@ class PaymentCapturePending implements RequestHandler { $request['resource'] !== null && isset( $request['resource']['id'] ) ? $request['resource']['id'] : '' ); - $this->logger->warning( $message ); - - $response['message'] = $message; - return new WP_REST_Response( $response ); + return $this->failure_response( $message ); } if ( $wc_order->get_status() === 'pending' ) { @@ -116,7 +98,6 @@ class PaymentCapturePending implements RequestHandler { } - $response['success'] = true; - return new WP_REST_Response( $response ); + return $this->success_response(); } } diff --git a/modules/ppcp-webhooks/src/Handler/PaymentCaptureRefunded.php b/modules/ppcp-webhooks/src/Handler/PaymentCaptureRefunded.php index d879a2345..ede513aa2 100644 --- a/modules/ppcp-webhooks/src/Handler/PaymentCaptureRefunded.php +++ b/modules/ppcp-webhooks/src/Handler/PaymentCaptureRefunded.php @@ -10,8 +10,10 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Webhooks\Handler; use Psr\Log\LoggerInterface; +use WC_Order; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundMetaTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait; +use WP_Error; use WP_REST_Request; use WP_REST_Response; @@ -20,7 +22,7 @@ use WP_REST_Response; */ class PaymentCaptureRefunded implements RequestHandler { - use PrefixTrait, TransactionIdHandlingTrait, RefundMetaTrait; + use TransactionIdHandlingTrait, RefundMetaTrait, RequestHandlerTrait; /** * The logger. @@ -33,11 +35,9 @@ class PaymentCaptureRefunded implements RequestHandler { * PaymentCaptureRefunded constructor. * * @param LoggerInterface $logger The logger. - * @param string $prefix The prefix. */ - public function __construct( LoggerInterface $logger, string $prefix ) { + public function __construct( LoggerInterface $logger ) { $this->logger = $logger; - $this->prefix = $prefix; } /** @@ -68,59 +68,32 @@ class PaymentCaptureRefunded implements RequestHandler { * @return WP_REST_Response */ public function handle_request( WP_REST_Request $request ): WP_REST_Response { - $response = array( 'success' => false ); $order_id = isset( $request['resource']['custom_id'] ) ? - $this->sanitize_custom_id( $request['resource']['custom_id'] ) : 0; + $request['resource']['custom_id'] : 0; $refund_id = (string) ( $request['resource']['id'] ?? '' ); if ( ! $order_id ) { $message = sprintf( - // translators: %s is the PayPal webhook Id. - __( - 'No order for webhook event %s was found.', - 'woocommerce-paypal-payments' - ), + 'No order for webhook event %s was found.', isset( $request['id'] ) ? $request['id'] : '' ); - $this->logger->log( - 'warning', - $message, - array( - 'request' => $request, - ) - ); - $response['message'] = $message; - return new WP_REST_Response( $response ); + return $this->failure_response( $message ); } $wc_order = wc_get_order( $order_id ); - if ( ! is_a( $wc_order, \WC_Order::class ) ) { + if ( ! is_a( $wc_order, WC_Order::class ) ) { $message = sprintf( - // translators: %s is the PayPal refund Id. - __( 'Order for PayPal refund %s not found.', 'woocommerce-paypal-payments' ), + 'Order for PayPal refund %s not found.', $refund_id ); - $this->logger->log( - 'warning', - $message, - array( - 'request' => $request, - ) - ); - $response['message'] = $message; - return new WP_REST_Response( $response ); + return $this->failure_response( $message ); } $already_added_refunds = $this->get_refunds_meta( $wc_order ); if ( in_array( $refund_id, $already_added_refunds, true ) ) { $this->logger->info( "Refund {$refund_id} is already handled." ); - return new WP_REST_Response( $response ); + return $this->success_response(); } - /** - * The WooCommerce order. - * - * @var \WC_Order $wc_order - */ $refund = wc_create_refund( array( 'order_id' => $wc_order->get_id(), @@ -128,37 +101,21 @@ class PaymentCaptureRefunded implements RequestHandler { ) ); if ( is_wp_error( $refund ) ) { - $this->logger->log( - 'warning', - sprintf( - // translators: %s is the order id. - __( 'Order %s could not be refunded', 'woocommerce-paypal-payments' ), - (string) $wc_order->get_id() - ), - array( - 'request' => $request, - 'error' => $refund, - ) + assert( $refund instanceof WP_Error ); + $message = sprintf( + 'Order %1$s could not be refunded. %2$s', + (string) $wc_order->get_id(), + $refund->get_error_message() ); - $response['message'] = $refund->get_error_message(); - return new WP_REST_Response( $response ); + return $this->failure_response( $message ); } - $this->logger->log( - 'info', + $this->logger->info( sprintf( - // translators: %1$s is the order id %2$s is the amount which has been refunded. - __( - 'Order %1$s has been refunded with %2$s through PayPal', - 'woocommerce-paypal-payments' - ), + 'Order %1$s has been refunded with %2$s through PayPal', (string) $wc_order->get_id(), (string) $refund->get_amount() - ), - array( - 'request' => $request, - 'order' => $wc_order, ) ); @@ -167,7 +124,6 @@ class PaymentCaptureRefunded implements RequestHandler { $this->add_refund_to_meta( $wc_order, $refund_id ); } - $response['success'] = true; - return new WP_REST_Response( $response ); + return $this->success_response(); } } diff --git a/modules/ppcp-webhooks/src/Handler/PaymentCaptureReversed.php b/modules/ppcp-webhooks/src/Handler/PaymentCaptureReversed.php index 5354e555c..5369a9ca3 100644 --- a/modules/ppcp-webhooks/src/Handler/PaymentCaptureReversed.php +++ b/modules/ppcp-webhooks/src/Handler/PaymentCaptureReversed.php @@ -19,7 +19,7 @@ use Psr\Log\LoggerInterface; */ class PaymentCaptureReversed implements RequestHandler { - use PrefixTrait; + use RequestHandlerTrait; /** * The logger. @@ -32,11 +32,9 @@ class PaymentCaptureReversed implements RequestHandler { * PaymentCaptureReversed constructor. * * @param LoggerInterface $logger The logger. - * @param string $prefix The prefix. */ - public function __construct( LoggerInterface $logger, string $prefix ) { + public function __construct( LoggerInterface $logger ) { $this->logger = $logger; - $this->prefix = $prefix; } /** @@ -71,45 +69,23 @@ class PaymentCaptureReversed implements RequestHandler { * @return \WP_REST_Response */ public function handle_request( \WP_REST_Request $request ): \WP_REST_Response { - $response = array( 'success' => false ); $order_id = isset( $request['resource']['custom_id'] ) ? - $this->sanitize_custom_id( $request['resource']['custom_id'] ) : 0; + $request['resource']['custom_id'] : 0; if ( ! $order_id ) { $message = sprintf( - // translators: %s is the PayPal webhook Id. - __( - 'No order for webhook event %s was found.', - 'woocommerce-paypal-payments' - ), + 'No order for webhook event %s was found.', isset( $request['id'] ) ? $request['id'] : '' ); - $this->logger->log( - 'warning', - $message, - array( - 'request' => $request, - ) - ); - $response['message'] = $message; - return rest_ensure_response( $response ); + return $this->failure_response( $message ); } $wc_order = wc_get_order( $order_id ); if ( ! is_a( $wc_order, \WC_Order::class ) ) { $message = sprintf( - // translators: %s is the PayPal refund Id. - __( 'Order for PayPal refund %s not found.', 'woocommerce-paypal-payments' ), + 'Order for PayPal refund %s not found.', isset( $request['resource']['id'] ) ? $request['resource']['id'] : '' ); - $this->logger->log( - 'warning', - $message, - array( - 'request' => $request, - ) - ); - $response['message'] = $message; - return rest_ensure_response( $response ); + return $this->failure_response( $message ); } /** @@ -117,33 +93,20 @@ class PaymentCaptureReversed implements RequestHandler { */ $note = apply_filters( 'ppcp_payment_capture_reversed_webhook_update_status_note', '', $wc_order, $request['event_type'] ); - /** - * The WooCommerce order. - * - * @var \WC_Order $wc_order - */ - $response['success'] = (bool) $wc_order->update_status( 'cancelled', $note ); + $is_success = $wc_order->update_status( 'cancelled', $note ); + if ( ! $is_success ) { + $message = sprintf( + 'Failed to cancel order %1$s cancelled through PayPal', + (string) $wc_order->get_id() + ); + return $this->failure_response( $message ); + } - $message = $response['success'] ? sprintf( - // translators: %1$s is the order id. - __( - 'Order %1$s has been cancelled through PayPal', - 'woocommerce-paypal-payments' - ), - (string) $wc_order->get_id() - ) : sprintf( - // translators: %1$s is the order id. - __( 'Failed to cancel order %1$s through PayPal', 'woocommerce-paypal-payments' ), + $message = sprintf( + 'Order %1$s has been cancelled through PayPal', (string) $wc_order->get_id() ); - $this->logger->log( - $response['success'] ? 'info' : 'warning', - $message, - array( - 'request' => $request, - 'order' => $wc_order, - ) - ); - return rest_ensure_response( $response ); + $this->logger->info( $message ); + return $this->success_response(); } } diff --git a/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php b/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php index 6f5cb491d..b606e2bd9 100644 --- a/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php +++ b/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php @@ -20,7 +20,7 @@ use WP_REST_Response; */ class PaymentSaleCompleted implements RequestHandler { - use TransactionIdHandlingTrait; + use TransactionIdHandlingTrait, RequestHandlerTrait; /** * The logger. @@ -67,17 +67,14 @@ class PaymentSaleCompleted implements RequestHandler { * @return WP_REST_Response */ public function handle_request( WP_REST_Request $request ): WP_REST_Response { - $response = array( 'success' => false ); if ( is_null( $request['resource'] ) ) { - return new WP_REST_Response( $response ); + return $this->failure_response(); } $billing_agreement_id = wc_clean( wp_unslash( $request['resource']['billing_agreement_id'] ?? '' ) ); if ( ! $billing_agreement_id ) { $message = 'Could not retrieve billing agreement id for subscription.'; - $this->logger->warning( $message, array( 'request' => $request ) ); - $response['message'] = $message; - return new WP_REST_Response( $response ); + return $this->failure_response( $message ); } $args = array( @@ -99,7 +96,6 @@ class PaymentSaleCompleted implements RequestHandler { } } - $response['success'] = true; - return new WP_REST_Response( $response ); + return $this->success_response(); } } diff --git a/modules/ppcp-webhooks/src/Handler/PaymentSaleRefunded.php b/modules/ppcp-webhooks/src/Handler/PaymentSaleRefunded.php index ff3c1dc6a..b3f2c06bb 100644 --- a/modules/ppcp-webhooks/src/Handler/PaymentSaleRefunded.php +++ b/modules/ppcp-webhooks/src/Handler/PaymentSaleRefunded.php @@ -21,7 +21,7 @@ use WP_REST_Response; */ class PaymentSaleRefunded implements RequestHandler { - use TransactionIdHandlingTrait, RefundMetaTrait; + use TransactionIdHandlingTrait, RefundMetaTrait, RequestHandlerTrait; /** * The logger. @@ -68,16 +68,15 @@ class PaymentSaleRefunded implements RequestHandler { * @return WP_REST_Response */ public function handle_request( WP_REST_Request $request ): WP_REST_Response { - $response = array( 'success' => false ); if ( is_null( $request['resource'] ) ) { - return new WP_REST_Response( $response ); + return $this->failure_response(); } $refund_id = (string) ( $request['resource']['id'] ?? '' ); $transaction_id = $request['resource']['sale_id'] ?? ''; $total_refunded_amount = $request['resource']['total_refunded_amount']['value'] ?? ''; if ( ! $refund_id || ! $transaction_id || ! $total_refunded_amount ) { - return new WP_REST_Response( $response ); + return $this->failure_response(); } $args = array( @@ -90,7 +89,7 @@ class PaymentSaleRefunded implements RequestHandler { $wc_orders = wc_get_orders( $args ); if ( ! is_array( $wc_orders ) ) { - return new WP_REST_Response( $response ); + return $this->failure_response(); } foreach ( $wc_orders as $wc_order ) { @@ -102,24 +101,17 @@ class PaymentSaleRefunded implements RequestHandler { ); if ( $refund instanceof WP_Error ) { - $this->logger->warning( - sprintf( - // translators: %s is the order id. - __( 'Order %s could not be refunded', 'woocommerce-paypal-payments' ), - (string) $wc_order->get_id() - ) + $message = sprintf( + 'Order %s could not be refunded. %s', + (string) $wc_order->get_id(), + $refund->get_error_message() ); - $response['message'] = $refund->get_error_message(); - return new WP_REST_Response( $response ); + return $this->failure_response( $message ); } $order_refunded_message = sprintf( - // translators: %1$s is the order id %2$s is the amount which has been refunded. - __( - 'Order %1$s has been refunded with %2$s through PayPal.', - 'woocommerce-paypal-payments' - ), + 'Order %1$s has been refunded with %2$s through PayPal.', (string) $wc_order->get_id(), (string) $total_refunded_amount ); @@ -130,7 +122,6 @@ class PaymentSaleRefunded implements RequestHandler { $this->add_refund_to_meta( $wc_order, $refund_id ); } - $response['success'] = true; - return new WP_REST_Response( $response ); + return $this->success_response(); } } diff --git a/modules/ppcp-webhooks/src/Handler/PrefixTrait.php b/modules/ppcp-webhooks/src/Handler/PrefixTrait.php deleted file mode 100644 index bc92d9e36..000000000 --- a/modules/ppcp-webhooks/src/Handler/PrefixTrait.php +++ /dev/null @@ -1,40 +0,0 @@ -prefix ) > 0 && 0 === strpos( $id, $this->prefix ) ) { - $id = substr( $id, strlen( $this->prefix ) ); - } - return (int) $id; - } -} diff --git a/modules/ppcp-webhooks/src/Handler/RequestHandlerTrait.php b/modules/ppcp-webhooks/src/Handler/RequestHandlerTrait.php index be155c0ee..933e202e4 100644 --- a/modules/ppcp-webhooks/src/Handler/RequestHandlerTrait.php +++ b/modules/ppcp-webhooks/src/Handler/RequestHandlerTrait.php @@ -9,8 +9,8 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Webhooks\Handler; -use stdClass; use WC_Order; +use WooCommerce\PayPalCommerce\Webhooks\CustomIds; use WP_REST_Request; use WP_REST_Response; @@ -19,22 +19,78 @@ trait RequestHandlerTrait { /** * Get available custom ids from the given request * - * @param \WP_REST_Request $request The request. - * @return array + * @param WP_REST_Request $request The request. + * @return string[] */ protected function get_custom_ids_from_request( WP_REST_Request $request ): array { - return array_filter( - array_map( + $resource = $request['resource']; + if ( ! is_array( $resource ) ) { + return array(); + } + + $ids = array(); + if ( isset( $resource['custom_id'] ) && ! empty( $resource['custom_id'] ) ) { + $ids[] = $resource['custom_id']; + } elseif ( isset( $resource['purchase_units'] ) ) { + $ids = array_map( static function ( array $purchase_unit ): string { - return isset( $purchase_unit['custom_id'] ) ? - (string) $purchase_unit['custom_id'] : ''; + return $purchase_unit['custom_id'] ?? ''; }, - $request['resource'] !== null && isset( $request['resource']['purchase_units'] ) ? - (array) $request['resource']['purchase_units'] : array() - ), - static function ( string $order_id ): bool { - return ! empty( $order_id ); - } + (array) $resource['purchase_units'] + ); + } + + return array_values( + array_filter( + $ids, + function ( string $id ): bool { + return ! empty( $id ); + } + ) + ); + } + + /** + * Get available WC order ids from the given request. + * + * @param WP_REST_Request $request The request. + * @return string[] + */ + protected function get_wc_order_ids_from_request( WP_REST_Request $request ): array { + $ids = $this->get_custom_ids_from_request( $request ); + + return array_values( + array_filter( + $ids, + function ( string $id ): bool { + return strpos( $id, CustomIds::CUSTOMER_ID_PREFIX ) === false; + } + ) + ); + } + + /** + * Get available WC customer ids from the given request. + * + * @param WP_REST_Request $request The request. + * @return string[] + */ + protected function get_wc_customer_ids_from_request( WP_REST_Request $request ): array { + $ids = $this->get_custom_ids_from_request( $request ); + + $customer_ids = array_values( + array_filter( + $ids, + function ( string $id ): bool { + return strpos( $id, CustomIds::CUSTOMER_ID_PREFIX ) === 0; + } + ) + ); + return array_map( + function ( string $str ): string { + return (string) substr( $str, strlen( CustomIds::CUSTOMER_ID_PREFIX ) ); + }, + $customer_ids ); } @@ -45,13 +101,7 @@ trait RequestHandlerTrait { * @return WC_Order[] */ protected function get_wc_orders_from_custom_ids( array $custom_ids ): array { - $order_ids = array_map( - array( - $this, - 'sanitize_custom_id', - ), - $custom_ids - ); + $order_ids = $custom_ids; $args = array( 'post__in' => $order_ids, 'limit' => -1, @@ -62,49 +112,62 @@ trait RequestHandlerTrait { } /** - * Return and log response for no custom ids found in request. + * Logs and returns response for no custom ids found in request. * * @param WP_REST_Request $request The request. - * @param array $response The response. * @return WP_REST_Response */ - protected function no_custom_ids_from_request( WP_REST_Request $request, array $response ): WP_REST_Response { + protected function no_custom_ids_response( WP_REST_Request $request ): WP_REST_Response { $message = sprintf( - // translators: %s is the PayPal webhook Id. - __( 'No order for webhook event %s was found.', 'woocommerce-paypal-payments' ), - $request['id'] !== null && isset( $request['id'] ) ? $request['id'] : '' - ); - - return $this->log_and_return_response( $message, $response ); - } - - /** - * Return and log response for no WC orders found in response. - * - * @param WP_REST_Request $request The request. - * @param array $response The response. - * @return WP_REST_Response - */ - protected function no_wc_orders_from_custom_ids( WP_REST_Request $request, array $response ): WP_REST_Response { - $message = sprintf( - // translators: %s is the PayPal order Id. - __( 'WC order for PayPal order %s not found.', 'woocommerce-paypal-payments' ), + 'WC order ID was not found in webhook event %s for PayPal order %s.', + (string) ( $request['id'] ?? '' ), + // Psalm 4.x does not seem to understand ?? with ArrayAccess correctly. $request['resource'] !== null && isset( $request['resource']['id'] ) ? $request['resource']['id'] : '' ); - return $this->log_and_return_response( $message, $response ); + return $this->failure_response( $message ); } /** - * Return and log response with the given message. + * Logs and returns response for no WC orders found via custom ids. * - * @param string $message The message. - * @param array $response The response. + * @param WP_REST_Request $request The request. * @return WP_REST_Response */ - private function log_and_return_response( string $message, array $response ): WP_REST_Response { - $this->logger->warning( $message ); - $response['message'] = $message; + protected function no_wc_orders_response( WP_REST_Request $request ): WP_REST_Response { + $message = sprintf( + 'WC order %s not found in webhook event %s for PayPal order %s.', + implode( ', ', $this->get_custom_ids_from_request( $request ) ), + (string) ( $request['id'] ?? '' ), + $request['resource'] !== null && isset( $request['resource']['id'] ) ? $request['resource']['id'] : '' + ); + + return $this->failure_response( $message ); + } + + /** + * Returns success response. + * + * @return WP_REST_Response + */ + protected function success_response(): WP_REST_Response { + return new WP_REST_Response( array( 'success' => true ) ); + } + + /** + * Logs and returns failure response with the given message. + * + * @param string $message The message. + * @return WP_REST_Response + */ + private function failure_response( string $message = '' ): WP_REST_Response { + $response = array( + 'success' => false, + ); + if ( $message ) { + $this->logger->warning( $message ); + $response['message'] = $message; + } return new WP_REST_Response( $response ); } diff --git a/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php b/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php index 28da46a32..8f2b1abce 100644 --- a/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php +++ b/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php @@ -24,6 +24,7 @@ use WP_REST_Response; * Class VaultPaymentTokenCreated */ class VaultPaymentTokenCreated implements RequestHandler { + use RequestHandlerTrait; /** * The logger. @@ -103,16 +104,12 @@ class VaultPaymentTokenCreated implements RequestHandler { * @return WP_REST_Response */ public function handle_request( WP_REST_Request $request ): WP_REST_Response { - $response = array( 'success' => false ); - $customer_id = null !== $request['resource'] && isset( $request['resource']['customer_id'] ) ? $request['resource']['customer_id'] : ''; if ( ! $customer_id ) { $message = 'No customer id was found.'; - $this->logger->warning( $message, array( 'request' => $request ) ); - $response['message'] = $message; - return new WP_REST_Response( $response ); + return $this->failure_response( $message ); } $wc_customer_id = (int) str_replace( $this->prefix, '', $customer_id ); @@ -150,7 +147,6 @@ class VaultPaymentTokenCreated implements RequestHandler { } } - $response['success'] = true; - return new WP_REST_Response( $response ); + return $this->success_response(); } } diff --git a/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenDeleted.php b/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenDeleted.php index 9a623081f..92dad5ed9 100644 --- a/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenDeleted.php +++ b/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenDeleted.php @@ -18,6 +18,7 @@ use WP_REST_Response; * Class VaultPaymentTokenDeleted */ class VaultPaymentTokenDeleted implements RequestHandler { + use RequestHandlerTrait; /** * The logger. @@ -65,8 +66,6 @@ class VaultPaymentTokenDeleted implements RequestHandler { * @return WP_REST_Response */ public function handle_request( WP_REST_Request $request ): WP_REST_Response { - $response = array( 'success' => false ); - if ( ! is_null( $request['resource'] ) && isset( $request['resource']['id'] ) ) { $token_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) ); @@ -89,7 +88,6 @@ class VaultPaymentTokenDeleted implements RequestHandler { } } - $response['success'] = true; - return new WP_REST_Response( $response ); + return $this->success_response(); } } diff --git a/modules/ppcp-webhooks/src/IncomingWebhookEndpoint.php b/modules/ppcp-webhooks/src/IncomingWebhookEndpoint.php index b228139e0..df556e0f0 100644 --- a/modules/ppcp-webhooks/src/IncomingWebhookEndpoint.php +++ b/modules/ppcp-webhooks/src/IncomingWebhookEndpoint.php @@ -17,12 +17,14 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookEventFactory; use WooCommerce\PayPalCommerce\Webhooks\Handler\RequestHandler; use Psr\Log\LoggerInterface; +use WooCommerce\PayPalCommerce\Webhooks\Handler\RequestHandlerTrait; use WooCommerce\PayPalCommerce\Webhooks\Status\WebhookSimulation; /** * Class IncomingWebhookEndpoint */ class IncomingWebhookEndpoint { + use RequestHandlerTrait; const NAMESPACE = 'paypal/v1'; const ROUTE = 'incoming'; @@ -211,26 +213,16 @@ class IncomingWebhookEndpoint { if ( $this->simulation->is_simulation_event( $event ) ) { $this->logger->info( 'Received simulated webhook.' ); $this->simulation->receive( $event ); - return rest_ensure_response( - array( - 'success' => true, - ) - ); + return $this->success_response(); } foreach ( $this->handlers as $handler ) { if ( $handler->responsible_for_request( $request ) ) { $response = $handler->handle_request( $request ); - $this->logger->log( - 'info', + $this->logger->info( sprintf( - // translators: %s is the event type. - __( 'Webhook has been handled by %s', 'woocommerce-paypal-payments' ), + 'Webhook has been handled by %s', ( $handler->event_types() ) ? current( $handler->event_types() ) : '' - ), - array( - 'request' => $request, - 'response' => $response, ) ); return $response; @@ -238,22 +230,10 @@ class IncomingWebhookEndpoint { } $message = sprintf( - // translators: %s is the request type. - __( 'Could not find handler for request type %s', 'woocommerce-paypal-payments' ), - $request['event_type'] + 'Could not find handler for request type %s', + $request['event_type'] ?: '' ); - $this->logger->log( - 'warning', - $message, - array( - 'request' => $request, - ) - ); - $response = array( - 'success' => false, - 'message' => $message, - ); - return rest_ensure_response( $response ); + return $this->failure_response( $message ); } /** diff --git a/package.json b/package.json index ccc69d50d..47a99da9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "woocommerce-paypal-payments", - "version": "2.1.0", + "version": "2.2.0", "description": "WooCommerce PayPal Payments", "repository": "https://github.com/woocommerce/woocommerce-paypal-payments", "license": "GPL-2.0", diff --git a/readme.txt b/readme.txt index b46a5283a..7c9d182c4 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: woocommerce, paypal, payments, ecommerce, e-commerce, store, sales, sell, Requires at least: 5.3 Tested up to: 6.2 Requires PHP: 7.2 -Stable tag: 2.1.0 +Stable tag: 2.2.0 License: GPLv2 License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -81,6 +81,27 @@ Follow the steps below to connect the plugin to your PayPal account: == Changelog == += 2.2.0 - TBD = +* Fix - Improve handling of APM payments when buyer did not return to Checkout #1233 +* Fix - Use order currency instead of shop currency on order-pay page #1363 +* Fix - Do not show broken card button gateway when no checkout location #1358 +* Fix - Smart buttons not greyed out/removed on single product when deselecting product variation #1469 +* Fix - Type error with advanced columns pro #1367 +* Fix - Undefined array key 0 when checking $retry_errors in process_payment method #1375 +* Fix - Advanced Card Processing gateway becomes invisible post-plugin update unless admin pages are accessed once #1432 +* Fix - Incompatibility with WooCommerce One Page Checkout (or similar use cases) in Version 2.1.0 #1473 +* Fix - Prevent Repetitive Token Migration and Database Overload After 2.1.0 Update #1461 +* Enhancement - Remove feature flag requirement for express cart/checkout block integration #1483 +* Enhancement - Add notice when shop currency is unsupported #1433 +* Enhancement - Improve ACDC error message when empty fields #1360 +* Enhancement - Do not exclude free items #1362 +* Enhancement - Trigger WC checkout_error event #1384 +* Enhancement - Update wording in buttons previews #1408 +* Enhancement - Filter to conditionally block the PayPal buttons #1485 +* Enhancement - Display funding source on the admin order page #1450 +* Enhancement - Update system report plugin status for Vaulting #1471 +* Enhancement - Revert Elementor Pro Checkout hook compatibility #1482 + = 2.1.0 - 2023-06-13 = * Fix - Performance issue #1182 * Fix - Webhooks not registered when onboarding with manual credentials #1223 diff --git a/tests/PHPUnit/ApiClient/Factory/PurchaseUnitFactoryTest.php b/tests/PHPUnit/ApiClient/Factory/PurchaseUnitFactoryTest.php index 54be500fc..a693bbb7b 100644 --- a/tests/PHPUnit/ApiClient/Factory/PurchaseUnitFactoryTest.php +++ b/tests/PHPUnit/ApiClient/Factory/PurchaseUnitFactoryTest.php @@ -258,7 +258,7 @@ class PurchaseUnitFactoryTest extends TestCase { $wcCustomer = Mockery::mock(\WC_Customer::class); expect('WC') - ->andReturn((object) ['customer' => $wcCustomer]); + ->andReturn((object) ['customer' => $wcCustomer, 'session' => null]); $wcCart = Mockery::mock(\WC_Cart::class); $amount = Mockery::mock(Amount::class); @@ -322,7 +322,7 @@ class PurchaseUnitFactoryTest extends TestCase public function testWcCartShippingGetsDroppendWhenNoCustomer() { expect('WC') - ->andReturn((object) ['customer' => null]); + ->andReturn((object) ['customer' => null, 'session' => null]); $wcCart = Mockery::mock(\WC_Cart::class); $amount = Mockery::mock(Amount::class); @@ -360,7 +360,7 @@ class PurchaseUnitFactoryTest extends TestCase public function testWcCartShippingGetsDroppendWhenNoCountryCode() { expect('WC') - ->andReturn((object) ['customer' => Mockery::mock(\WC_Customer::class)]); + ->andReturn((object) ['customer' => Mockery::mock(\WC_Customer::class), 'session' => null]); $wcCart = Mockery::mock(\WC_Cart::class); $amount = Mockery::mock(Amount::class); diff --git a/tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php b/tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php index d2fc1a4fa..79f80de3f 100644 --- a/tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php +++ b/tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php @@ -169,6 +169,7 @@ class CreateOrderEndpointTest extends TestCase false, ['checkout'], false, + ['paypal'], new NullLogger() ); return array($payer_factory, $testee); diff --git a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php index cfe7a2eec..a5a1dea3e 100644 --- a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php +++ b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php @@ -5,6 +5,8 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; +use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; @@ -75,6 +77,11 @@ class WcGatewayTest extends TestCase ->andReturnUsing(function () { return $this->fundingSource; }); + $order = Mockery::mock(Order::class); + $order->shouldReceive('status')->andReturn(new OrderStatus(OrderStatus::APPROVED)); + $this->sessionHandler + ->shouldReceive('order') + ->andReturn($order); $this->settings->shouldReceive('has')->andReturnFalse(); diff --git a/tests/playwright/place-order.spec.js b/tests/playwright/place-order.spec.js index b0028fafa..c0228c351 100644 --- a/tests/playwright/place-order.spec.js +++ b/tests/playwright/place-order.spec.js @@ -1,6 +1,6 @@ const {test, expect} = require('@playwright/test'); const {serverExec} = require("./utils/server"); -const {fillCheckoutForm, expectOrderReceivedPage} = require("./utils/checkout"); +const {fillCheckoutForm, expectOrderReceivedPage, acceptTerms} = require("./utils/checkout"); const {openPaypalPopup, loginIntoPaypal, waitForPaypalShippingList, completePaypalPayment} = require("./utils/paypal-popup"); const { @@ -11,9 +11,11 @@ const { PRODUCT_ID, CHECKOUT_URL, CHECKOUT_PAGE_ID, + CART_URL, BLOCK_CHECKOUT_URL, BLOCK_CHECKOUT_PAGE_ID, BLOCK_CART_URL, + APM_ID, } = process.env; async function completeBlockContinuation(page) { @@ -21,11 +23,25 @@ async function completeBlockContinuation(page) { await expect(page.locator('.component-frame')).toHaveCount(0); - await page.locator('.wc-block-components-checkout-place-order-button').click(); + await Promise.all( + page.waitForNavigation(), + page.locator('.wc-block-components-checkout-place-order-button').click(), + ); +} - await page.waitForNavigation(); +async function expectContinuation(page) { + await expect(page.locator('#payment_method_ppcp-gateway')).toBeChecked(); - await expectOrderReceivedPage(page); + await expect(page.locator('.component-frame')).toHaveCount(0); +} + +async function completeContinuation(page) { + await expectContinuation(page); + + await Promise.all([ + page.waitForNavigation(), + page.locator('#place_order').click(), + ]); } test.describe('Classic checkout', () => { @@ -44,10 +60,7 @@ test.describe('Classic checkout', () => { await fillCheckoutForm(page); - await Promise.all([ - page.waitForNavigation(), - page.locator('#place_order').click(), - ]); + await completeContinuation(page); await expectOrderReceivedPage(page); }); @@ -77,6 +90,47 @@ test.describe('Classic checkout', () => { await expectOrderReceivedPage(page); }); + + test('PayPal APM button place order', async ({page}) => { + await page.goto(CART_URL + '?add-to-cart=' + PRODUCT_ID); + + await page.goto(CHECKOUT_URL); + + await fillCheckoutForm(page); + + const popup = await openPaypalPopup(page, {fundingSource: APM_ID}); + + await popup.getByText('Continue', { exact: true }).click(); + await completePaypalPayment(popup, {selector: '[name="Successful"]'}); + + await expectOrderReceivedPage(page); + }); + + test('PayPal APM button place order when redirect fails', async ({page}) => { + await page.goto(CART_URL + '?add-to-cart=' + PRODUCT_ID); + + await page.goto(CHECKOUT_URL); + + await fillCheckoutForm(page); + + await page.evaluate('PayPalCommerceGateway.ajax.approve_order = null'); + + const popup = await openPaypalPopup(page, {fundingSource: APM_ID}); + + await popup.getByText('Continue', { exact: true }).click(); + await completePaypalPayment(popup, {selector: '[name="Successful"]'}); + + await expect(page.locator('.woocommerce-error')).toBeVisible(); + + await page.reload(); + await expectContinuation(page); + + await acceptTerms(page); + + await completeContinuation(page); + + await expectOrderReceivedPage(page); + }); }); test.describe('Block checkout', () => { @@ -97,6 +151,8 @@ test.describe('Block checkout', () => { await completePaypalPayment(popup); await completeBlockContinuation(page); + + await expectOrderReceivedPage(page); }); test('PayPal express block cart', async ({page}) => { @@ -109,6 +165,8 @@ test.describe('Block checkout', () => { await completePaypalPayment(popup); await completeBlockContinuation(page); + + await expectOrderReceivedPage(page); }); test.describe('Without review', () => { diff --git a/tests/playwright/utils/checkout.js b/tests/playwright/utils/checkout.js index 625c543fa..a93ba8b93 100644 --- a/tests/playwright/utils/checkout.js +++ b/tests/playwright/utils/checkout.js @@ -26,6 +26,10 @@ export const fillCheckoutForm = async (page) => { await differentShippingLocator.uncheck(); } + await acceptTerms(page); +} + +export const acceptTerms = async (page) => { const termsLocator = page.locator('[name="terms"]'); if (await termsLocator.count() > 0) { await termsLocator.check(); diff --git a/tests/playwright/utils/paypal-popup.js b/tests/playwright/utils/paypal-popup.js index a39e06759..3b84e7417 100644 --- a/tests/playwright/utils/paypal-popup.js +++ b/tests/playwright/utils/paypal-popup.js @@ -8,16 +8,24 @@ const { /** * Opens the PayPal popup by pressing the button, and returns the popup object. * @param page + * @param {{timeout: ?int, fundingSource: ?string}} options * @param {boolean} retry Retries the button click if the popup did not appear after timeout. - * @param {int} timeout */ -export const openPaypalPopup = async (page, retry = true, timeout = 5000) => { +export const openPaypalPopup = async (page, options = {}, retry = true) => { + options = { + ...{ + timeout: 5000, + fundingSource: 'paypal', + }, + ...options + }; + try { await page.locator('.component-frame').scrollIntoViewIfNeeded(); const [popup] = await Promise.all([ - page.waitForEvent('popup', {timeout}), - page.frameLocator('.component-frame').locator('[data-funding-source="paypal"]').click(), + page.waitForEvent('popup', {timeout: options.timeout}), + page.frameLocator('.component-frame').locator(`[data-funding-source="${options.fundingSource}"]`).click(), ]); await popup.waitForLoadState(); @@ -41,7 +49,7 @@ export const openPaypalPopup = async (page, retry = true, timeout = 5000) => { } if (retry) { - return openPaypalPopup(page, false); + return openPaypalPopup(page, options, false); } throw err; } @@ -83,9 +91,20 @@ export const waitForPaypalShippingList = async (popup) => { await expect(popup.locator('#shippingMethodsDropdown')).toBeVisible({timeout: 15000}); } -export const completePaypalPayment = async (popup) => { +/** + * @param popup + * @param {{timeout: ?int, selector: ?string}} options + */ +export const completePaypalPayment = async (popup, options) => { + options = { + ...{ + timeout: 20000, + selector: '#payment-submit-btn', + }, + ...options + }; await Promise.all([ - popup.waitForEvent('close', {timeout: 20000}), - popup.click('#payment-submit-btn'), + popup.waitForEvent('close', {timeout: options.timeout}), + popup.click(options.selector), ]); } diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php index 4fad31bcd..86a30e525 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -3,13 +3,13 @@ * Plugin Name: WooCommerce PayPal Payments * Plugin URI: https://woocommerce.com/products/woocommerce-paypal-payments/ * Description: PayPal's latest complete payments processing solution. Accept PayPal, Pay Later, credit/debit cards, alternative digital wallets local payment types and bank accounts. Turn on only PayPal options or process a full suite of payment methods. Enable global transaction with extensive currency and country coverage. - * Version: 2.1.0 + * Version: 2.2.0 * Author: WooCommerce * Author URI: https://woocommerce.com/ * License: GPL-2.0 * Requires PHP: 7.2 * WC requires at least: 3.9 - * WC tested up to: 7.7 + * WC tested up to: 7.8 * Text Domain: woocommerce-paypal-payments * * @package WooCommerce\PayPalCommerce @@ -23,7 +23,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; define( 'PAYPAL_API_URL', 'https://api.paypal.com' ); define( 'PAYPAL_SANDBOX_API_URL', 'https://api.sandbox.paypal.com' ); -define( 'PAYPAL_INTEGRATION_DATE', '2023-06-02' ); +define( 'PAYPAL_INTEGRATION_DATE', '2023-07-06' ); ! defined( 'CONNECT_WOO_CLIENT_ID' ) && define( 'CONNECT_WOO_CLIENT_ID', 'AcCAsWta_JTL__OfpjspNyH7c1GGHH332fLwonA5CwX4Y10mhybRZmHLA0GdRbwKwjQIhpDQy0pluX_P' ); ! defined( 'CONNECT_WOO_SANDBOX_CLIENT_ID' ) && define( 'CONNECT_WOO_SANDBOX_CLIENT_ID', 'AYmOHbt1VHg-OZ_oihPdzKEVbU3qg0qXonBcAztuzniQRaKE0w1Hr762cSFwd4n8wxOl-TCWohEa0XM_' );