Merge branch 'trunk' into PCP-895-buttons-not-working-on-single-product-page-for-woo-commerce-bookings-product

This commit is contained in:
Pedro Silva 2023-07-05 09:34:11 +01:00
commit c00b5906f7
No known key found for this signature in database
GPG key ID: E2EE20C0669D24B3
55 changed files with 1039 additions and 683 deletions

View file

@ -27,7 +27,6 @@ web_environment:
- ADMIN_PASS=admin - ADMIN_PASS=admin
- ADMIN_EMAIL=admin@example.com - ADMIN_EMAIL=admin@example.com
- WC_VERSION=7.7.2 - WC_VERSION=7.7.2
- PCP_BLOCKS_ENABLED=1
# Key features of ddev's config.yaml: # Key features of ddev's config.yaml:

View file

@ -15,6 +15,8 @@ PRODUCT_ID=123
SUBSCRIPTION_URL="/product/sub" SUBSCRIPTION_URL="/product/sub"
APM_ID="sofort"
WP_MERCHANT_USER="admin" WP_MERCHANT_USER="admin"
WP_MERCHANT_PASSWORD="admin" WP_MERCHANT_PASSWORD="admin"

View file

@ -1,4 +1,7 @@
<?php <?php
if (!defined('PAYPAL_INTEGRATION_DATE')) {
define('PAYPAL_INTEGRATION_DATE', '2023-06-02');
}
if (!defined('EP_PAGES')) { if (!defined('EP_PAGES')) {
define('EP_PAGES', 4096); define('EP_PAGES', 4096);
} }

View file

@ -26,14 +26,8 @@ return function ( string $root_dir ): iterable {
( require "$modules_dir/ppcp-vaulting/module.php" )(), ( require "$modules_dir/ppcp-vaulting/module.php" )(),
( require "$modules_dir/ppcp-order-tracking/module.php" )(), ( require "$modules_dir/ppcp-order-tracking/module.php" )(),
( require "$modules_dir/ppcp-uninstall/module.php" )(), ( require "$modules_dir/ppcp-uninstall/module.php" )(),
( require "$modules_dir/ppcp-blocks/module.php" )(),
); );
if ( apply_filters(
'woocommerce_paypal_payments_blocks_enabled',
getenv( 'PCP_BLOCKS_ENABLED' ) === '1'
) ) {
$modules[] = ( require "$modules_dir/ppcp-blocks/module.php" )();
}
return $modules; return $modules;
}; };

View file

@ -9,10 +9,12 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory; namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WC_Session_Handler;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Item; use WooCommerce\PayPalCommerce\ApiClient\Entity\Item;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit; use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository; use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository;
use WooCommerce\PayPalCommerce\Webhooks\CustomIds;
/** /**
* Class PurchaseUnitFactory * Class PurchaseUnitFactory
@ -187,7 +189,14 @@ class PurchaseUnitFactory {
$payee = $this->payee_repository->payee(); $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 = ''; $invoice_id = '';
$soft_descriptor = ''; $soft_descriptor = '';
$purchase_unit = new PurchaseUnit( $purchase_unit = new PurchaseUnit(

View file

@ -28,6 +28,8 @@ const cardsSpinner = new Spinner('#ppcp-hosted-fields');
const bootstrap = () => { const bootstrap = () => {
const checkoutFormSelector = 'form.woocommerce-checkout'; const checkoutFormSelector = 'form.woocommerce-checkout';
const context = PayPalCommerceGateway.context;
const errorHandler = new ErrorHandler( const errorHandler = new ErrorHandler(
PayPalCommerceGateway.labels.error.generic, PayPalCommerceGateway.labels.error.generic,
document.querySelector(checkoutFormSelector) ?? document.querySelector('.woocommerce-notices-wrapper') 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; window.ppcpFundingSource = data.fundingSource;
const requiredFields = jQuery('form.woocommerce-checkout .validate-required:visible :input'); const requiredFields = jQuery('form.woocommerce-checkout .validate-required:visible :input');
requiredFields.each((i, input) => { requiredFields.each((i, input) => {
@ -120,25 +122,21 @@ const bootstrap = () => {
freeTrialHandler.handle(); freeTrialHandler.handle();
return actions.reject(); return actions.reject();
} }
};
let smartButtonsOptions = { if (context === 'checkout' && !PayPalCommerceGateway.funding_sources_without_redirect.includes(data.fundingSource)) {
onInit: null, try {
init: function (actions) { await formSaver.save(form);
this.actions = actions; } catch (error) {
if (typeof this.onInit === 'function') { console.error(error);
this.onInit();
} }
} }
}; };
const onSmartButtonsInit = (data, actions) => { const onSmartButtonsInit = () => {
buttonsSpinner.unblock(); buttonsSpinner.unblock();
smartButtonsOptions.init(actions);
}; };
const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway, onSmartButtonClick, onSmartButtonsInit, smartButtonsOptions); const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway, onSmartButtonClick, onSmartButtonsInit);
const messageRenderer = new MessageRenderer(PayPalCommerceGateway.messages); const messageRenderer = new MessageRenderer(PayPalCommerceGateway.messages);
const context = PayPalCommerceGateway.context;
if (context === 'mini-cart' || context === 'product') { if (context === 'mini-cart' || context === 'product') {
if (PayPalCommerceGateway.mini_cart_buttons_enabled === '1') { if (PayPalCommerceGateway.mini_cart_buttons_enabled === '1') {
const miniCartBootstrap = new MiniCartBootstap( const miniCartBootstrap = new MiniCartBootstap(

View file

@ -1,4 +1,5 @@
import CartActionHandler from '../ActionHandler/CartActionHandler'; import CartActionHandler from '../ActionHandler/CartActionHandler';
import BootstrapHelper from "../Helper/BootstrapHelper";
import {setVisible} from "../Helper/Hiding"; import {setVisible} from "../Helper/Hiding";
class CartBootstrap { class CartBootstrap {
@ -6,6 +7,10 @@ class CartBootstrap {
this.gateway = gateway; this.gateway = gateway;
this.renderer = renderer; this.renderer = renderer;
this.errorHandler = errorHandler; this.errorHandler = errorHandler;
this.renderer.onButtonsInit(this.gateway.button.wrapper, () => {
this.handleButtonStatus();
}, true);
} }
init() { init() {
@ -14,9 +19,11 @@ class CartBootstrap {
} }
this.render(); this.render();
this.handleButtonStatus();
jQuery(document.body).on('updated_cart_totals updated_checkout', () => { jQuery(document.body).on('updated_cart_totals updated_checkout', () => {
this.render(); this.render();
this.handleButtonStatus();
fetch( fetch(
this.gateway.ajax.cart_script_params.endpoint, this.gateway.ajax.cart_script_params.endpoint,
@ -40,10 +47,18 @@ class CartBootstrap {
}); });
} }
handleButtonStatus() {
BootstrapHelper.handleButtonStatus(this);
}
shouldRender() { shouldRender() {
return document.querySelector(this.gateway.button.wrapper) !== null; return document.querySelector(this.gateway.button.wrapper) !== null;
} }
shouldEnable() {
return BootstrapHelper.shouldEnable(this);
}
render() { render() {
const actionHandler = new CartActionHandler( const actionHandler = new CartActionHandler(
PayPalCommerceGateway, PayPalCommerceGateway,

View file

@ -5,6 +5,8 @@ import {
isSavedCardSelected, ORDER_BUTTON_SELECTOR, isSavedCardSelected, ORDER_BUTTON_SELECTOR,
PaymentMethods PaymentMethods
} from "../Helper/CheckoutMethodState"; } from "../Helper/CheckoutMethodState";
import BootstrapHelper from "../Helper/BootstrapHelper";
import {disable, enable} from "../Helper/ButtonDisabler";
class CheckoutBootstap { class CheckoutBootstap {
constructor(gateway, renderer, messages, spinner, errorHandler) { constructor(gateway, renderer, messages, spinner, errorHandler) {
@ -15,10 +17,15 @@ class CheckoutBootstap {
this.errorHandler = errorHandler; this.errorHandler = errorHandler;
this.standardOrderButtonSelector = ORDER_BUTTON_SELECTOR; this.standardOrderButtonSelector = ORDER_BUTTON_SELECTOR;
this.renderer.onButtonsInit(this.gateway.button.wrapper, () => {
this.handleButtonStatus();
}, true);
} }
init() { init() {
this.render(); this.render();
this.handleButtonStatus();
// Unselect saved card. // Unselect saved card.
// WC saves form values, so with our current UI it would be a bit weird // 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', () => { jQuery(document.body).on('updated_checkout', () => {
this.render() this.render()
this.handleButtonStatus();
}); });
jQuery(document.body).on('updated_checkout payment_method_selected', () => { jQuery(document.body).on('updated_checkout payment_method_selected', () => {
@ -43,6 +51,10 @@ class CheckoutBootstap {
this.updateUi(); this.updateUi();
} }
handleButtonStatus() {
BootstrapHelper.handleButtonStatus(this);
}
shouldRender() { shouldRender() {
if (document.querySelector(this.gateway.button.cancel_wrapper)) { if (document.querySelector(this.gateway.button.cancel_wrapper)) {
return false; return false;
@ -51,6 +63,10 @@ class CheckoutBootstap {
return document.querySelector(this.gateway.button.wrapper) !== null || document.querySelector(this.gateway.hosted_fields.wrapper) !== null; return document.querySelector(this.gateway.button.wrapper) !== null || document.querySelector(this.gateway.hosted_fields.wrapper) !== null;
} }
shouldEnable() {
return BootstrapHelper.shouldEnable(this);
}
render() { render() {
if (!this.shouldRender()) { if (!this.shouldRender()) {
return; return;

View file

@ -1,4 +1,5 @@
import CartActionHandler from '../ActionHandler/CartActionHandler'; import CartActionHandler from '../ActionHandler/CartActionHandler';
import BootstrapHelper from "../Helper/BootstrapHelper";
class MiniCartBootstap { class MiniCartBootstap {
constructor(gateway, renderer, errorHandler) { constructor(gateway, renderer, errorHandler) {
@ -15,9 +16,22 @@ class MiniCartBootstap {
this.errorHandler, this.errorHandler,
); );
this.render(); this.render();
this.handleButtonStatus();
jQuery(document.body).on('wc_fragments_loaded wc_fragments_refreshed', () => { jQuery(document.body).on('wc_fragments_loaded wc_fragments_refreshed', () => {
this.render(); 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; || document.querySelector(this.gateway.hosted_fields.mini_cart_wrapper) !== null;
} }
shouldEnable() {
return BootstrapHelper.shouldEnable(this, {
isDisabled: !!this.gateway.button.is_mini_cart_disabled
});
}
render() { render() {
if (!this.shouldRender()) { if (!this.shouldRender()) {
return; return;

View file

@ -1,7 +1,7 @@
import UpdateCart from "../Helper/UpdateCart"; import UpdateCart from "../Helper/UpdateCart";
import SingleProductActionHandler from "../ActionHandler/SingleProductActionHandler"; import SingleProductActionHandler from "../ActionHandler/SingleProductActionHandler";
import {hide, show} from "../Helper/Hiding"; import {hide, show} from "../Helper/Hiding";
import {disable, enable} from "../Helper/ButtonDisabler"; import BootstrapHelper from "../Helper/BootstrapHelper";
class SingleProductBootstap { class SingleProductBootstap {
constructor(gateway, renderer, messages, errorHandler) { constructor(gateway, renderer, messages, errorHandler) {
@ -12,11 +12,9 @@ class SingleProductBootstap {
this.mutationObserver = new MutationObserver(this.handleChange.bind(this)); this.mutationObserver = new MutationObserver(this.handleChange.bind(this));
this.formSelector = 'form.cart'; this.formSelector = 'form.cart';
if (this.renderer && this.renderer.smartButtonsOptions) { this.renderer.onButtonsInit(this.gateway.button.wrapper, () => {
this.renderer.smartButtonsOptions.onInit = () => { this.handleChange();
this.handleChange(); }, true);
};
}
} }
form() { form() {
@ -25,7 +23,7 @@ class SingleProductBootstap {
handleChange() { handleChange() {
if (!this.shouldRender()) { if (!this.shouldRender()) {
this.renderer.disableSmartButtons(); this.renderer.disableSmartButtons(this.gateway.button.wrapper);
hide(this.gateway.button.wrapper, this.formSelector); hide(this.gateway.button.wrapper, this.formSelector);
hide(this.gateway.messages.wrapper); hide(this.gateway.messages.wrapper);
return; return;
@ -33,7 +31,7 @@ class SingleProductBootstap {
this.render(); this.render();
this.renderer.enableSmartButtons(); this.renderer.enableSmartButtons(this.gateway.button.wrapper);
show(this.gateway.button.wrapper); show(this.gateway.button.wrapper);
show(this.gateway.messages.wrapper); show(this.gateway.messages.wrapper);
@ -41,15 +39,9 @@ class SingleProductBootstap {
} }
handleButtonStatus() { handleButtonStatus() {
if (!this.shouldEnable()) { BootstrapHelper.handleButtonStatus(this, {
this.renderer.disableSmartButtons(); formSelector: this.formSelector
disable(this.gateway.button.wrapper, this.formSelector); });
disable(this.gateway.messages.wrapper);
return;
}
this.renderer.enableSmartButtons();
enable(this.gateway.button.wrapper);
enable(this.gateway.messages.wrapper);
} }
init() { init() {
@ -94,7 +86,7 @@ class SingleProductBootstap {
const form = this.form(); const form = this.form();
const addToCartButton = form ? form.querySelector('.single_add_to_cart_button') : null; const addToCartButton = form ? form.querySelector('.single_add_to_cart_button') : null;
return this.shouldRender() return BootstrapHelper.shouldEnable(this)
&& !this.priceAmountIsZero() && !this.priceAmountIsZero()
&& ((null === addToCartButton) || !addToCartButton.classList.contains('disabled')); && ((null === addToCartButton) || !addToCartButton.classList.contains('disabled'));
} }

View file

@ -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;
}
}

View file

@ -36,7 +36,10 @@ export const setEnabled = (selectorOrElement, enable, form = null) => {
if (form) { if (form) {
// Trigger form submit to show the error message // Trigger form submit to show the error message
jQuery(form).find(':submit').trigger('click'); let $form = jQuery(form);
if ($form.find('.single_add_to_cart_button').hasClass('disabled')) {
$form.find(':submit').trigger('click');
}
} }
}) })
.find('> *') .find('> *')

View file

@ -1,12 +1,14 @@
import merge from "deepmerge"; import merge from "deepmerge";
class Renderer { class Renderer {
constructor(creditCardRenderer, defaultSettings, onSmartButtonClick, onSmartButtonsInit, smartButtonsOptions) { constructor(creditCardRenderer, defaultSettings, onSmartButtonClick, onSmartButtonsInit) {
this.defaultSettings = defaultSettings; this.defaultSettings = defaultSettings;
this.creditCardRenderer = creditCardRenderer; this.creditCardRenderer = creditCardRenderer;
this.onSmartButtonClick = onSmartButtonClick; this.onSmartButtonClick = onSmartButtonClick;
this.onSmartButtonsInit = onSmartButtonsInit; this.onSmartButtonsInit = onSmartButtonsInit;
this.smartButtonsOptions = smartButtonsOptions;
this.buttonsOptions = {};
this.onButtonsInitListeners = {};
this.renderedSources = new Set(); this.renderedSources = new Set();
} }
@ -78,7 +80,10 @@ class Renderer {
style, style,
...contextConfig, ...contextConfig,
onClick: this.onSmartButtonClick, onClick: this.onSmartButtonClick,
onInit: this.onSmartButtonsInit, onInit: (data, actions) => {
this.onSmartButtonsInit(data, actions);
this.handleOnButtonsInit(wrapper, data, actions);
},
}); });
if (!btn.isEligible()) { if (!btn.isEligible()) {
return; return;
@ -108,18 +113,42 @@ class Renderer {
this.creditCardRenderer.enableFields(); this.creditCardRenderer.enableFields();
} }
disableSmartButtons() { onButtonsInit(wrapper, handler, reset) {
if (!this.smartButtonsOptions || !this.smartButtonsOptions.actions) { this.onButtonsInitListeners[wrapper] = reset ? [] : (this.onButtonsInitListeners[wrapper] || []);
return; this.onButtonsInitListeners[wrapper].push(handler);
}
this.smartButtonsOptions.actions.disable();
} }
enableSmartButtons() { handleOnButtonsInit(wrapper, data, actions) {
if (!this.smartButtonsOptions || !this.smartButtonsOptions.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; return;
} }
this.smartButtonsOptions.actions.enable(); this.buttonsOptions[wrapper].actions.disable();
}
enableSmartButtons(wrapper) {
if (!this.buttonsOptions[wrapper]) {
return;
}
this.buttonsOptions[wrapper].actions.enable();
} }
} }

View file

@ -67,23 +67,6 @@ return array(
return $dummy_ids[ $shop_country ] ?? $container->get( 'button.client_id' ); 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 { 'button.smart-button' => static function ( ContainerInterface $container ): SmartButtonInterface {
$state = $container->get( 'onboarding.state' ); $state = $container->get( 'onboarding.state' );
if ( $state->current_state() !== State::STATE_ONBOARDED ) { if ( $state->current_state() !== State::STATE_ONBOARDED ) {
@ -125,6 +108,7 @@ return array(
$container->get( 'button.basic-checkout-validation-enabled' ), $container->get( 'button.basic-checkout-validation-enabled' ),
$container->get( 'button.early-wc-checkout-validation-enabled' ), $container->get( 'button.early-wc-checkout-validation-enabled' ),
$container->get( 'button.pay-now-contexts' ), $container->get( 'button.pay-now-contexts' ),
$container->get( 'wcgateway.funding-sources-without-redirect' ),
$container->get( 'woocommerce.logger.woocommerce' ) $container->get( 'woocommerce.logger.woocommerce' )
); );
}, },
@ -176,6 +160,7 @@ return array(
$container->get( 'button.early-wc-checkout-validation-enabled' ), $container->get( 'button.early-wc-checkout-validation-enabled' ),
$container->get( 'button.pay-now-contexts' ), $container->get( 'button.pay-now-contexts' ),
$container->get( 'button.handle-shipping-in-paypal' ), $container->get( 'button.handle-shipping-in-paypal' ),
$container->get( 'wcgateway.funding-sources-without-redirect' ),
$logger $logger
); );
}, },
@ -184,8 +169,7 @@ return array(
$state = $container->get( 'onboarding.state' ); $state = $container->get( 'onboarding.state' );
$order_processor = $container->get( 'wcgateway.order-processor' ); $order_processor = $container->get( 'wcgateway.order-processor' );
$session_handler = $container->get( 'session.handler' ); $session_handler = $container->get( 'session.handler' );
$prefix = $container->get( 'api.prefix' ); return new EarlyOrderHandler( $state, $order_processor, $session_handler );
return new EarlyOrderHandler( $state, $order_processor, $session_handler, $prefix );
}, },
'button.endpoint.approve-order' => static function ( ContainerInterface $container ): ApproveOrderEndpoint { 'button.endpoint.approve-order' => static function ( ContainerInterface $container ): ApproveOrderEndpoint {
$request_data = $container->get( 'button.request-data' ); $request_data = $container->get( 'button.request-data' );
@ -215,7 +199,9 @@ return array(
); );
}, },
'button.checkout-form-saver' => static function ( ContainerInterface $container ): CheckoutFormSaver { '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 { 'button.endpoint.save-checkout-form' => static function ( ContainerInterface $container ): SaveCheckoutFormEndpoint {
return new SaveCheckoutFormEndpoint( return new SaveCheckoutFormEndpoint(

View file

@ -174,6 +174,13 @@ class SmartButton implements SmartButtonInterface {
*/ */
private $pay_now_contexts; 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. * The logger.
* *
@ -209,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 $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 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 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. * @param LoggerInterface $logger The logger.
*/ */
public function __construct( public function __construct(
@ -230,6 +238,7 @@ class SmartButton implements SmartButtonInterface {
bool $basic_checkout_validation_enabled, bool $basic_checkout_validation_enabled,
bool $early_validation_enabled, bool $early_validation_enabled,
array $pay_now_contexts, array $pay_now_contexts,
array $funding_sources_without_redirect,
LoggerInterface $logger LoggerInterface $logger
) { ) {
@ -251,6 +260,7 @@ class SmartButton implements SmartButtonInterface {
$this->basic_checkout_validation_enabled = $basic_checkout_validation_enabled; $this->basic_checkout_validation_enabled = $basic_checkout_validation_enabled;
$this->early_validation_enabled = $early_validation_enabled; $this->early_validation_enabled = $early_validation_enabled;
$this->pay_now_contexts = $pay_now_contexts; $this->pay_now_contexts = $pay_now_contexts;
$this->funding_sources_without_redirect = $funding_sources_without_redirect;
$this->logger = $logger; $this->logger = $logger;
} }
@ -857,10 +867,12 @@ class SmartButton implements SmartButtonInterface {
'bn_codes' => $this->bn_codes(), 'bn_codes' => $this->bn_codes(),
'payer' => $this->payerData(), 'payer' => $this->payerData(),
'button' => array( 'button' => array(
'wrapper' => '#ppc-button-' . PayPalGateway::ID, 'wrapper' => '#ppc-button-' . PayPalGateway::ID,
'mini_cart_wrapper' => '#ppc-button-minicart', 'is_disabled' => $this->is_button_disabled(),
'cancel_wrapper' => '#ppcp-cancel', 'mini_cart_wrapper' => '#ppc-button-minicart',
'mini_cart_style' => array( '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' ), 'layout' => $this->style_for_context( 'layout', 'mini-cart' ),
'color' => $this->style_for_context( 'color', 'mini-cart' ), 'color' => $this->style_for_context( 'color', 'mini-cart' ),
'shape' => $this->style_for_context( 'shape', 'mini-cart' ), 'shape' => $this->style_for_context( 'shape', 'mini-cart' ),
@ -868,7 +880,7 @@ class SmartButton implements SmartButtonInterface {
'tagline' => $this->style_for_context( 'tagline', 'mini-cart' ), '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, '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() ), 'layout' => $this->style_for_context( 'layout', $this->context() ),
'color' => $this->style_for_context( 'color', $this->context() ), 'color' => $this->style_for_context( 'color', $this->context() ),
'shape' => $this->style_for_context( 'shape', $this->context() ), 'shape' => $this->style_for_context( 'shape', $this->context() ),
@ -942,6 +954,7 @@ class SmartButton implements SmartButtonInterface {
'mini_cart_buttons_enabled' => $this->settings_status->is_smart_button_enabled_for_location( 'mini-cart' ), 'mini_cart_buttons_enabled' => $this->settings_status->is_smart_button_enabled_for_location( 'mini-cart' ),
'basic_checkout_validation_enabled' => $this->basic_checkout_validation_enabled, 'basic_checkout_validation_enabled' => $this->basic_checkout_validation_enabled,
'early_checkout_validation_enabled' => $this->early_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' ) { if ( $this->style_for_context( 'layout', 'mini-cart' ) !== 'horizontal' ) {
@ -1003,7 +1016,7 @@ class SmartButton implements SmartButtonInterface {
); );
if ( if (
$this->environment->current_environment_is( Environment::SANDBOX ) $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() && WC()->customer instanceof \WC_Customer && WC()->customer->get_billing_country()
&& 2 === strlen( WC()->customer->get_billing_country() ) && 2 === strlen( WC()->customer->get_billing_country() )
) { ) {
@ -1350,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. * Retrieves all payment tokens for the user, via API or cached if already queried.
* *

View file

@ -152,6 +152,13 @@ class CreateOrderEndpoint implements EndpointInterface {
*/ */
private $handle_shipping_in_paypal; 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. * 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 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 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 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. * @param LoggerInterface $logger The logger.
*/ */
public function __construct( public function __construct(
@ -191,23 +199,25 @@ class CreateOrderEndpoint implements EndpointInterface {
bool $early_validation_enabled, bool $early_validation_enabled,
array $pay_now_contexts, array $pay_now_contexts,
bool $handle_shipping_in_paypal, bool $handle_shipping_in_paypal,
array $funding_sources_without_redirect,
LoggerInterface $logger LoggerInterface $logger
) { ) {
$this->request_data = $request_data; $this->request_data = $request_data;
$this->purchase_unit_factory = $purchase_unit_factory; $this->purchase_unit_factory = $purchase_unit_factory;
$this->shipping_preference_factory = $shipping_preference_factory; $this->shipping_preference_factory = $shipping_preference_factory;
$this->api_endpoint = $order_endpoint; $this->api_endpoint = $order_endpoint;
$this->payer_factory = $payer_factory; $this->payer_factory = $payer_factory;
$this->session_handler = $session_handler; $this->session_handler = $session_handler;
$this->settings = $settings; $this->settings = $settings;
$this->early_order_handler = $early_order_handler; $this->early_order_handler = $early_order_handler;
$this->registration_needed = $registration_needed; $this->registration_needed = $registration_needed;
$this->card_billing_data_mode = $card_billing_data_mode; $this->card_billing_data_mode = $card_billing_data_mode;
$this->early_validation_enabled = $early_validation_enabled; $this->early_validation_enabled = $early_validation_enabled;
$this->pay_now_contexts = $pay_now_contexts; $this->pay_now_contexts = $pay_now_contexts;
$this->handle_shipping_in_paypal = $handle_shipping_in_paypal; $this->handle_shipping_in_paypal = $handle_shipping_in_paypal;
$this->logger = $logger; $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 ( '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 ( if (
! $this->early_order_handler->should_create_early_order() ! $this->early_order_handler->should_create_early_order()
|| $this->registration_needed || $this->registration_needed

View file

@ -10,11 +10,30 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Helper; namespace WooCommerce\PayPalCommerce\Button\Helper;
use WC_Checkout; use WC_Checkout;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
/** /**
* Class CheckoutFormSaver * Class CheckoutFormSaver
*/ */
class CheckoutFormSaver extends WC_Checkout { 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. * Saves the form data to the WC customer and session.
* *
@ -28,5 +47,7 @@ class CheckoutFormSaver extends WC_Checkout {
$data = $this->get_posted_data(); $data = $this->get_posted_data();
$this->update_session( $data ); $this->update_session( $data );
$this->session_handler->replace_checkout_form( $data );
} }
} }

View file

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Helper; namespace WooCommerce\PayPalCommerce\Button\Helper;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
trait ContextTrait { trait ContextTrait {
/** /**
@ -18,6 +20,13 @@ trait ContextTrait {
*/ */
protected function context(): string { protected function context(): string {
if ( is_product() || wc_post_content_has_shortcode( 'product_page' ) ) { 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'; return 'product';
} }
@ -56,6 +65,12 @@ trait ContextTrait {
return false; return false;
} }
if ( ! $order->status()->is( OrderStatus::APPROVED )
&& ! $order->status()->is( OrderStatus::COMPLETED )
) {
return false;
}
$source = $order->payment_source(); $source = $order->payment_source();
if ( $source && $source->card() ) { if ( $source && $source->card() ) {
return false; // Ignore for DCC. return false; // Ignore for DCC.

View file

@ -15,15 +15,12 @@ use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\Webhooks\Handler\PrefixTrait;
/** /**
* Class EarlyOrderHandler * Class EarlyOrderHandler
*/ */
class EarlyOrderHandler { class EarlyOrderHandler {
use PrefixTrait;
/** /**
* The State. * The State.
* *
@ -51,19 +48,16 @@ class EarlyOrderHandler {
* @param State $state The State. * @param State $state The State.
* @param OrderProcessor $order_processor The Order Processor. * @param OrderProcessor $order_processor The Order Processor.
* @param SessionHandler $session_handler The Session Handler. * @param SessionHandler $session_handler The Session Handler.
* @param string $prefix The Prefix.
*/ */
public function __construct( public function __construct(
State $state, State $state,
OrderProcessor $order_processor, OrderProcessor $order_processor,
SessionHandler $session_handler, SessionHandler $session_handler
string $prefix
) { ) {
$this->state = $state; $this->state = $state;
$this->order_processor = $order_processor; $this->order_processor = $order_processor;
$this->session_handler = $session_handler; $this->session_handler = $session_handler;
$this->prefix = $prefix;
} }
/** /**
@ -101,7 +95,7 @@ class EarlyOrderHandler {
$order_id = false; $order_id = false;
foreach ( $order->purchase_units() as $purchase_unit ) { foreach ( $order->purchase_units() as $purchase_unit ) {
if ( $purchase_unit->custom_id() === sanitize_text_field( wp_unslash( $_REQUEST['ppcp-resume-order'] ) ) ) { 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 ) { if ( $order_id === $resume_order_id ) {

View file

@ -339,14 +339,6 @@ class CompatModule implements ModuleInterface {
}, },
5 5
); );
add_filter(
'woocommerce_paypal_payments_checkout_button_renderer_hook',
function(): string {
return 'woocommerce_review_order_after_submit';
},
5
);
} }
} }

View file

@ -9,12 +9,14 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Session\Cancellation; namespace WooCommerce\PayPalCommerce\Session\Cancellation;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler;
/** /**
* Class CancelController * Class CancelController
*/ */
class CancelController { class CancelController {
use ContextTrait;
public const NONCE = 'ppcp-cancel'; public const NONCE = 'ppcp-cancel';
@ -50,7 +52,7 @@ class CancelController {
/** /**
* Runs the controller. * Runs the controller.
*/ */
public function run() { public function run(): void {
$param_name = self::NONCE; $param_name = self::NONCE;
if ( isset( $_GET[ $param_name ] ) && // Input var ok. if ( isset( $_GET[ $param_name ] ) && // Input var ok.
wp_verify_nonce( wp_verify_nonce(
@ -61,20 +63,10 @@ class CancelController {
$this->session_handler->destroy_session_data(); $this->session_handler->destroy_session_data();
} }
$order = $this->session_handler->order(); if ( ! $this->is_paypal_continuation() ) {
if ( ! $order ) {
return; 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() ); $url = add_query_arg( array( $param_name => wp_create_nonce( self::NONCE ) ), wc_get_checkout_url() );
add_action( add_action(
'woocommerce_review_order_after_submit', 'woocommerce_review_order_after_submit',

View file

@ -0,0 +1,75 @@
<?php
/**
* A WC_Session_Handler subclass for loading the session when it is normally not available (e.g. in webhooks).
*
* @package WooCommerce\PayPalCommerce\Session
*
* phpcs:disable Generic.Commenting.DocComment
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Session;
use WC_Session_Handler;
/**
* MemoryWcSession class.
*/
class MemoryWcSession extends WC_Session_Handler {
/**
* The session data (from WC()->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();
}
}

View file

@ -47,6 +47,13 @@ class SessionHandler {
*/ */
private $funding_source = null; private $funding_source = null;
/**
* The checkout form data.
*
* @var array
*/
private $checkout_form = array();
/** /**
* Returns the order. * Returns the order.
* *
@ -55,6 +62,8 @@ class SessionHandler {
public function order() { public function order() {
$this->load_session(); $this->load_session();
do_action( 'ppcp_session_get_order', $this->order, $this );
return $this->order; return $this->order;
} }
@ -71,6 +80,30 @@ class SessionHandler {
$this->store_session(); $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. * Returns the BN Code.
* *
@ -151,6 +184,7 @@ class SessionHandler {
$this->bn_code = ''; $this->bn_code = '';
$this->insufficient_funding_tries = 0; $this->insufficient_funding_tries = 0;
$this->funding_source = null; $this->funding_source = null;
$this->checkout_form = array();
$this->store_session(); $this->store_session();
return $this; return $this;
} }
@ -188,6 +222,7 @@ class SessionHandler {
if ( ! is_string( $this->funding_source ) ) { if ( ! is_string( $this->funding_source ) ) {
$this->funding_source = null; $this->funding_source = null;
} }
$this->checkout_form = $data['checkout_form'] ?? array();
} }
/** /**
@ -202,6 +237,7 @@ class SessionHandler {
'bn_code' => $obj->bn_code, 'bn_code' => $obj->bn_code,
'insufficient_funding_tries' => $obj->insufficient_funding_tries, 'insufficient_funding_tries' => $obj->insufficient_funding_tries,
'funding_source' => $obj->funding_source, 'funding_source' => $obj->funding_source,
'checkout_form' => $obj->checkout_form,
); );
} }
} }

View file

@ -9,6 +9,11 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Session; 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\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Session\Cancellation\CancelController; use WooCommerce\PayPalCommerce\Session\Cancellation\CancelController;
@ -19,6 +24,12 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
* Class SessionModule * Class SessionModule
*/ */
class SessionModule implements ModuleInterface { class SessionModule implements ModuleInterface {
/**
* A flag to avoid multiple requests to reload order.
*
* @var bool
*/
private $reloaded_order = false;
/** /**
* {@inheritDoc} * {@inheritDoc}
@ -46,6 +57,45 @@ class SessionModule implements ModuleInterface {
$controller->run(); $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
);
} }
/** /**

View file

@ -911,6 +911,12 @@ return array(
'paylater' => _x( 'Pay Later', 'Name of payment method', 'woocommerce-paypal-payments' ), '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 { 'wcgateway.settings.funding-sources' => static function( ContainerInterface $container ): array {
return array_diff_key( return array_diff_key(
$container->get( 'wcgateway.all-funding-sources' ), $container->get( 'wcgateway.all-funding-sources' ),
@ -946,11 +952,11 @@ return array(
'wcgateway.endpoint.return-url' => static function ( ContainerInterface $container ) : ReturnUrlEndpoint { 'wcgateway.endpoint.return-url' => static function ( ContainerInterface $container ) : ReturnUrlEndpoint {
$gateway = $container->get( 'wcgateway.paypal-gateway' ); $gateway = $container->get( 'wcgateway.paypal-gateway' );
$endpoint = $container->get( 'api.endpoint.order' ); $endpoint = $container->get( 'api.endpoint.order' );
$prefix = $container->get( 'api.prefix' );
return new ReturnUrlEndpoint( return new ReturnUrlEndpoint(
$gateway, $gateway,
$endpoint, $endpoint,
$prefix $container->get( 'session.handler' ),
$container->get( 'woocommerce.logger.woocommerce' )
); );
}, },

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Checkout; namespace WooCommerce\PayPalCommerce\WcGateway\Checkout;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
@ -20,6 +21,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
* Class DisableGateways * Class DisableGateways
*/ */
class DisableGateways { class DisableGateways {
use ContextTrait;
/** /**
* The Session Handler. * The Session Handler.
@ -127,20 +129,6 @@ class DisableGateways {
* @return bool * @return bool
*/ */
private function needs_to_disable_gateways(): bool { private function needs_to_disable_gateways(): bool {
$order = $this->session_handler->order(); return $this->is_paypal_continuation();
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;
} }
} }

View file

@ -9,17 +9,18 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Endpoint; namespace WooCommerce\PayPalCommerce\WcGateway\Endpoint;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; 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\OXXO\OXXOGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\Webhooks\Handler\PrefixTrait;
/** /**
* Class ReturnUrlEndpoint * Class ReturnUrlEndpoint
*/ */
class ReturnUrlEndpoint { class ReturnUrlEndpoint {
use PrefixTrait;
const ENDPOINT = 'ppc-return-url'; const ENDPOINT = 'ppc-return-url';
/** /**
@ -36,17 +37,38 @@ class ReturnUrlEndpoint {
*/ */
private $order_endpoint; private $order_endpoint;
/**
* The session handler
*
* @var SessionHandler
*/
protected $session_handler;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/** /**
* ReturnUrlEndpoint constructor. * ReturnUrlEndpoint constructor.
* *
* @param PayPalGateway $gateway The PayPal Gateway. * @param PayPalGateway $gateway The PayPal Gateway.
* @param OrderEndpoint $order_endpoint The Order Endpoint. * @param OrderEndpoint $order_endpoint The Order Endpoint.
* @param string $prefix The prefix. * @param SessionHandler $session_handler The session handler.
* @param LoggerInterface $logger The logger.
*/ */
public function __construct( PayPalGateway $gateway, OrderEndpoint $order_endpoint, string $prefix ) { public function __construct(
$this->gateway = $gateway; PayPalGateway $gateway,
$this->order_endpoint = $order_endpoint; OrderEndpoint $order_endpoint,
$this->prefix = $prefix; 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 // phpcs:enable WordPress.Security.NonceVerification.Recommended
$order = $this->order_endpoint->order( $token ); $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 ) { 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(); exit();
} }
$wc_order = wc_get_order( $wc_order_id ); $wc_order = wc_get_order( $wc_order_id );
if ( ! is_a( $wc_order, \WC_Order::class ) ) { if ( ! is_a( $wc_order, \WC_Order::class ) ) {
$this->logger->warning( "Return URL endpoint $token: WC order $wc_order_id not found." );
exit(); exit();
} }

View file

@ -13,6 +13,7 @@ use Exception;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WC_Order; use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
@ -253,8 +254,13 @@ class PayPalGateway extends \WC_Payment_Gateway {
$funding_source = $this->session_handler->funding_source(); $funding_source = $this->session_handler->funding_source();
if ( $funding_source ) { if ( $funding_source ) {
$this->title = $this->funding_source_renderer->render_name( $funding_source ); $order = $this->session_handler->order();
$this->description = $this->funding_source_renderer->render_description( $funding_source ); 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(); $this->init_form_fields();
@ -554,12 +560,15 @@ class PayPalGateway extends \WC_Payment_Gateway {
'INSTRUMENT_DECLINED' => __( 'Instrument declined.', 'woocommerce-paypal-payments' ), 'INSTRUMENT_DECLINED' => __( 'Instrument declined.', 'woocommerce-paypal-payments' ),
'PAYER_ACTION_REQUIRED' => __( 'Payer action required, possibly overcharge.', 'woocommerce-paypal-payments' ), 'PAYER_ACTION_REQUIRED' => __( 'Payer action required, possibly overcharge.', 'woocommerce-paypal-payments' ),
); );
$retry_errors = array_filter( $retry_errors = array_values(
array_keys( $retry_keys_messages ), array_filter(
function ( string $key ) use ( $error ): bool { array_keys( $retry_keys_messages ),
return $error->has_detail( $key ); function ( string $key ) use ( $error ): bool {
} return $error->has_detail( $key );
}
)
); );
if ( $retry_errors ) { if ( $retry_errors ) {
$retry_error_key = $retry_errors[0]; $retry_error_key = $retry_errors[0];

View file

@ -166,7 +166,7 @@ class OrderProcessor {
// phpcs:ignore WordPress.Security.NonceVerification // 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_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY ) ?: wc_clean( wp_unslash( $_POST['paypal_order_id'] ?? '' ) );
$order = $this->session_handler->order(); $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 ); $order = $this->order_endpoint->order( $order_id );
} }
if ( ! $order ) { if ( ! $order ) {
@ -178,7 +178,7 @@ class OrderProcessor {
$wc_order->get_id() $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; return false;
} }

View file

@ -82,12 +82,18 @@ return array(
$payment_token_factory = $container->get( 'vaulting.payment-token-factory' ); $payment_token_factory = $container->get( 'vaulting.payment-token-factory' );
return array( return array(
new CheckoutOrderApproved( $logger, $prefix, $order_endpoint ), new CheckoutOrderApproved(
new CheckoutOrderCompleted( $logger, $prefix ), $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 CheckoutPaymentApprovalReversed( $logger ),
new PaymentCaptureRefunded( $logger, $prefix ), new PaymentCaptureRefunded( $logger ),
new PaymentCaptureReversed( $logger, $prefix ), new PaymentCaptureReversed( $logger ),
new PaymentCaptureCompleted( $logger, $prefix, $order_endpoint ), new PaymentCaptureCompleted( $logger, $order_endpoint ),
new VaultPaymentTokenCreated( $logger, $prefix, $authorized_payments_processor, $payment_token_factory ), new VaultPaymentTokenCreated( $logger, $prefix, $authorized_payments_processor, $payment_token_factory ),
new VaultPaymentTokenDeleted( $logger ), new VaultPaymentTokenDeleted( $logger ),
new PaymentCapturePending( $logger ), new PaymentCapturePending( $logger ),

View file

@ -0,0 +1,18 @@
<?php
/**
* The constants for handling custom_id in the webhook requests.
*
* @package WooCommerce\PayPalCommerce\Webhooks
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks;
/**
* Interface CustomIds
*/
interface CustomIds {
public const CUSTOMER_ID_PREFIX = 'pcp_customer_';
}

View file

@ -17,6 +17,7 @@ use WP_REST_Response;
* Class BillingPlanPricingChangeActivated * Class BillingPlanPricingChangeActivated
*/ */
class BillingPlanPricingChangeActivated implements RequestHandler { class BillingPlanPricingChangeActivated implements RequestHandler {
use RequestHandlerTrait;
/** /**
* The logger. * The logger.
@ -64,9 +65,8 @@ class BillingPlanPricingChangeActivated implements RequestHandler {
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function handle_request( WP_REST_Request $request ): WP_REST_Response { public function handle_request( WP_REST_Request $request ): WP_REST_Response {
$response = array( 'success' => false );
if ( is_null( $request['resource'] ) ) { if ( is_null( $request['resource'] ) ) {
return new WP_REST_Response( $response ); return $this->failure_response();
} }
$plan_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) ); $plan_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) );
@ -92,7 +92,6 @@ class BillingPlanPricingChangeActivated implements RequestHandler {
} }
} }
$response['success'] = true; return $this->success_response();
return new WP_REST_Response( $response );
} }
} }

View file

@ -17,6 +17,7 @@ use WP_REST_Response;
* Class BillingPlanUpdated * Class BillingPlanUpdated
*/ */
class BillingPlanUpdated implements RequestHandler { class BillingPlanUpdated implements RequestHandler {
use RequestHandlerTrait;
/** /**
* The logger. * The logger.
@ -64,9 +65,8 @@ class BillingPlanUpdated implements RequestHandler {
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function handle_request( WP_REST_Request $request ): WP_REST_Response { public function handle_request( WP_REST_Request $request ): WP_REST_Response {
$response = array( 'success' => false );
if ( is_null( $request['resource'] ) ) { if ( is_null( $request['resource'] ) ) {
return new WP_REST_Response( $response ); return $this->failure_response();
} }
$plan_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) ); $plan_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) );
@ -109,7 +109,6 @@ class BillingPlanUpdated implements RequestHandler {
} }
} }
$response['success'] = true; return $this->success_response();
return new WP_REST_Response( $response );
} }
} }

View file

@ -17,6 +17,7 @@ use WP_REST_Response;
* Class BillingSubscriptionCancelled * Class BillingSubscriptionCancelled
*/ */
class BillingSubscriptionCancelled implements RequestHandler { class BillingSubscriptionCancelled implements RequestHandler {
use RequestHandlerTrait;
/** /**
* The logger. * The logger.
@ -64,9 +65,8 @@ class BillingSubscriptionCancelled implements RequestHandler {
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function handle_request( WP_REST_Request $request ): WP_REST_Response { public function handle_request( WP_REST_Request $request ): WP_REST_Response {
$response = array( 'success' => false );
if ( is_null( $request['resource'] ) ) { if ( is_null( $request['resource'] ) ) {
return new WP_REST_Response( $response ); return $this->failure_response();
} }
$subscription_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) ); $subscription_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) );
@ -87,7 +87,6 @@ class BillingSubscriptionCancelled implements RequestHandler {
} }
} }
$response['success'] = true; return $this->success_response();
return new WP_REST_Response( $response );
} }
} }

View file

@ -17,6 +17,7 @@ use WP_REST_Response;
* Class CatalogProductUpdated * Class CatalogProductUpdated
*/ */
class CatalogProductUpdated implements RequestHandler { class CatalogProductUpdated implements RequestHandler {
use RequestHandlerTrait;
/** /**
* The logger. * The logger.
@ -64,9 +65,8 @@ class CatalogProductUpdated implements RequestHandler {
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function handle_request( WP_REST_Request $request ): WP_REST_Response { public function handle_request( WP_REST_Request $request ): WP_REST_Response {
$response = array( 'success' => false );
if ( is_null( $request['resource'] ) ) { if ( is_null( $request['resource'] ) ) {
return new WP_REST_Response( $response ); return $this->failure_response();
} }
$product_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) ); $product_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) );
@ -104,7 +104,6 @@ class CatalogProductUpdated implements RequestHandler {
} }
} }
$response['success'] = true; return $this->success_response();
return new WP_REST_Response( $response );
} }
} }

View file

@ -9,18 +9,25 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks\Handler; 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\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use Psr\Log\LoggerInterface; 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\OXXO\OXXOGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
/** /**
* Class CheckoutOrderApproved * Class CheckoutOrderApproved
*/ */
class CheckoutOrderApproved implements RequestHandler { class CheckoutOrderApproved implements RequestHandler {
use PrefixTrait; use RequestHandlerTrait;
/** /**
* The logger. * The logger.
@ -36,17 +43,48 @@ class CheckoutOrderApproved implements RequestHandler {
*/ */
private $order_endpoint; 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. * CheckoutOrderApproved constructor.
* *
* @param LoggerInterface $logger The logger. * @param LoggerInterface $logger The logger.
* @param string $prefix The prefix. * @param OrderEndpoint $order_endpoint The order endpoint.
* @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 ) { public function __construct(
$this->logger = $logger; LoggerInterface $logger,
$this->prefix = $prefix; OrderEndpoint $order_endpoint,
$this->order_endpoint = $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 * @return \WP_REST_Response
*/ */
public function handle_request( \WP_REST_Request $request ): \WP_REST_Response { public function handle_request( \WP_REST_Request $request ): \WP_REST_Response {
$response = array( 'success' => false ); $order_id = isset( $request['resource']['id'] ) ? $request['resource']['id'] : null;
$custom_ids = array_filter( if ( ! $order_id ) {
array_map( return $this->failure_response(
static function ( array $purchase_unit ): string { sprintf(
return isset( $purchase_unit['custom_id'] ) ? 'No order ID in webhook event %s.',
(string) $purchase_unit['custom_id'] : ''; $request['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,
) )
); );
$response['message'] = $message;
return rest_ensure_response( $response );
} }
try { $order = $this->order_endpoint->order( $order_id );
$order = isset( $request['resource']['id'] ) ?
$this->order_endpoint->order( $request['resource']['id'] ) : null; $wc_orders = array();
if ( ! $order ) {
$message = sprintf( $wc_order_ids = $this->get_wc_order_ids_from_request( $request );
// translators: %s is the PayPal webhook Id. if ( empty( $wc_order_ids ) ) {
__( $customer_ids = $this->get_wc_customer_ids_from_request( $request );
'No paypal payment for webhook event %s was found.', if ( empty( $customer_ids ) ) {
'woocommerce-paypal-payments' return $this->no_custom_ids_response( $request );
), }
isset( $request['id'] ) ? $request['id'] : ''
); $customer_id = $customer_ids[0];
$this->logger->log(
'warning', if ( $order->status()->is( OrderStatus::COMPLETED ) ) {
$message, $this->logger->info( "Order {$order->id()} already completed." );
array( return $this->success_response();
'request' => $request, }
$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' ) { $funding_source = $this->session_handler->funding_source();
$order = $this->order_endpoint->capture( $order ); 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( if ( is_numeric( $customer_id ) ) {
array( $wc_order->set_customer_id( (int) $customer_id );
$this, }
'sanitize_custom_id',
), $wc_order->save();
$custom_ids
); $wc_orders[] = $wc_order;
$args = array(
'post__in' => $wc_order_ids, add_action(
'limit' => -1, 'shutdown',
); function () use ( $customer_id ): void {
$wc_orders = wc_get_orders( $args ); $session = WC()->session;
if ( ! $wc_orders ) { assert( $session instanceof WC_Session_Handler );
$message = sprintf(
// translators: %s is the PayPal order Id. /**
__( 'Order for PayPal order %s not found.', 'woocommerce-paypal-payments' ), * Wrong type-hint.
isset( $request['resource']['id'] ) ? $request['resource']['id'] : '' *
* @psalm-suppress InvalidScalarArgument
*/
$session->delete_session( $customer_id );
$session->forget_session();
}
); );
$this->logger->log( } else {
'warning', $wc_orders = $this->get_wc_orders_from_custom_ids( $wc_order_ids );
$message, if ( ! $wc_orders ) {
array( return $this->no_wc_orders_response( $request );
'request' => $request, }
)
);
$response['message'] = $message;
return rest_ensure_response( $response );
} }
foreach ( $wc_orders as $wc_order ) { 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 ) ) { if ( ! in_array( $wc_order->get_status(), array( 'pending', 'on-hold' ), true ) ) {
continue; continue;
} }
if ( $order->intent() === 'CAPTURE' ) {
$wc_order->payment_complete(); if ( ! $this->order_processor->process( $wc_order ) ) {
} else { return $this->failure_response(
$wc_order->update_status( sprintf(
'on-hold', 'Failed to process WC order %s: %s.',
__( 'Payment can be captured.', 'woocommerce-paypal-payments' ) (string) $wc_order->get_id(),
$this->order_processor->last_error()
)
); );
} }
$this->logger->log(
'info', $this->logger->info(
sprintf( sprintf(
// translators: %s is the order ID. 'WC order %s has been processed after approval in PayPal.',
__(
'Order %s has been updated through PayPal',
'woocommerce-paypal-payments'
),
(string) $wc_order->get_id() (string) $wc_order->get_id()
),
array(
'request' => $request,
'order' => $wc_order,
) )
); );
} }
$response['success'] = true; return $this->success_response();
return rest_ensure_response( $response );
} }
} }

View file

@ -19,7 +19,7 @@ use WP_REST_Response;
*/ */
class CheckoutOrderCompleted implements RequestHandler { class CheckoutOrderCompleted implements RequestHandler {
use PrefixTrait, RequestHandlerTrait; use RequestHandlerTrait;
/** /**
* The logger. * The logger.
@ -32,11 +32,9 @@ class CheckoutOrderCompleted implements RequestHandler {
* CheckoutOrderCompleted constructor. * CheckoutOrderCompleted constructor.
* *
* @param LoggerInterface $logger The logger. * @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->logger = $logger;
$this->prefix = $prefix;
} }
/** /**
@ -69,16 +67,14 @@ class CheckoutOrderCompleted implements RequestHandler {
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function handle_request( WP_REST_Request $request ): WP_REST_Response { public function handle_request( WP_REST_Request $request ): WP_REST_Response {
$response = array( 'success' => false ); $custom_ids = $this->get_wc_order_ids_from_request( $request );
$custom_ids = $this->get_custom_ids_from_request( $request );
if ( empty( $custom_ids ) ) { 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 ); $wc_orders = $this->get_wc_orders_from_custom_ids( $custom_ids );
if ( ! $wc_orders ) { 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 ) { foreach ( $wc_orders as $wc_order ) {
@ -93,17 +89,12 @@ class CheckoutOrderCompleted implements RequestHandler {
$this->logger->info( $this->logger->info(
sprintf( sprintf(
// translators: %s is the order ID. 'Order %s has been updated through PayPal',
__(
'Order %s has been updated through PayPal',
'woocommerce-paypal-payments'
),
(string) $wc_order->get_id() (string) $wc_order->get_id()
) )
); );
} }
$response['success'] = true; return $this->success_response();
return new WP_REST_Response( $response );
} }
} }

View file

@ -18,7 +18,7 @@ use WP_REST_Response;
*/ */
class CheckoutPaymentApprovalReversed implements RequestHandler { class CheckoutPaymentApprovalReversed implements RequestHandler {
use RequestHandlerTrait, PrefixTrait; use RequestHandlerTrait;
/** /**
* The logger. * The logger.
@ -66,26 +66,20 @@ class CheckoutPaymentApprovalReversed implements RequestHandler {
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function handle_request( WP_REST_Request $request ): WP_REST_Response { public function handle_request( WP_REST_Request $request ): WP_REST_Response {
$response = array( 'success' => false ); $custom_ids = $this->get_wc_order_ids_from_request( $request );
$custom_ids = $this->get_custom_ids_from_request( $request );
if ( empty( $custom_ids ) ) { 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 ); $wc_orders = $this->get_wc_orders_from_custom_ids( $custom_ids );
if ( ! $wc_orders ) { 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 ) { foreach ( $wc_orders as $wc_order ) {
if ( in_array( $wc_order->get_status(), array( 'pending', 'on-hold' ), true ) ) { if ( in_array( $wc_order->get_status(), array( 'pending', 'on-hold' ), true ) ) {
$error_message = sprintf( $error_message = sprintf(
// translators: %1$s is the order id. 'Failed to capture order %1$s through PayPal.',
__(
'Failed to capture order %1$s through PayPal.',
'woocommerce-paypal-payments'
),
(string) $wc_order->get_id() (string) $wc_order->get_id()
); );
@ -95,7 +89,6 @@ class CheckoutPaymentApprovalReversed implements RequestHandler {
} }
} }
$response['success'] = true; return $this->success_response();
return new WP_REST_Response( $response );
} }
} }

View file

@ -22,7 +22,7 @@ use WP_REST_Response;
*/ */
class PaymentCaptureCompleted implements RequestHandler { class PaymentCaptureCompleted implements RequestHandler {
use PrefixTrait, TransactionIdHandlingTrait; use TransactionIdHandlingTrait, RequestHandlerTrait;
/** /**
* The logger. * The logger.
@ -42,16 +42,13 @@ class PaymentCaptureCompleted implements RequestHandler {
* PaymentCaptureCompleted constructor. * PaymentCaptureCompleted constructor.
* *
* @param LoggerInterface $logger The logger. * @param LoggerInterface $logger The logger.
* @param string $prefix The prefix.
* @param OrderEndpoint $order_endpoint The order endpoint. * @param OrderEndpoint $order_endpoint The order endpoint.
*/ */
public function __construct( public function __construct(
LoggerInterface $logger, LoggerInterface $logger,
string $prefix,
OrderEndpoint $order_endpoint OrderEndpoint $order_endpoint
) { ) {
$this->logger = $logger; $this->logger = $logger;
$this->prefix = $prefix;
$this->order_endpoint = $order_endpoint; $this->order_endpoint = $order_endpoint;
} }
@ -83,33 +80,24 @@ class PaymentCaptureCompleted implements RequestHandler {
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function handle_request( \WP_REST_Request $request ): WP_REST_Response { public function handle_request( \WP_REST_Request $request ): WP_REST_Response {
$response = array( 'success' => false );
$webhook_id = (string) ( $request['id'] ?? '' ); $webhook_id = (string) ( $request['id'] ?? '' );
$resource = $request['resource']; $resource = $request['resource'];
if ( ! is_array( $resource ) ) { if ( ! is_array( $resource ) ) {
$message = 'Resource data not found in webhook request.'; $message = 'Resource data not found in webhook request.';
$this->logger->warning( $message, array( 'request' => $request ) ); return $this->failure_response( $message );
$response['message'] = $message;
return new WP_REST_Response( $response );
} }
$wc_order_id = isset( $resource['custom_id'] ) ? $wc_order_id = isset( $resource['custom_id'] ) ? (string) $resource['custom_id'] : 0;
$this->sanitize_custom_id( (string) $resource['custom_id'] ) : 0;
if ( ! $wc_order_id ) { if ( ! $wc_order_id ) {
$message = sprintf( 'No order for webhook event %s was found.', $webhook_id ); $message = sprintf( 'No order for webhook event %s was found.', $webhook_id );
$this->logger->warning( $message, array( 'request' => $request ) ); return $this->failure_response( $message );
$response['message'] = $message;
return new WP_REST_Response( $response );
} }
$wc_order = wc_get_order( $wc_order_id ); $wc_order = wc_get_order( $wc_order_id );
if ( ! is_a( $wc_order, \WC_Order::class ) ) { if ( ! is_a( $wc_order, \WC_Order::class ) ) {
$message = sprintf( 'No order for webhook event %s was found.', $webhook_id ); $message = sprintf( 'No order for webhook event %s was found.', $webhook_id );
$this->logger->warning( $message, array( 'request' => $request ) ); return $this->failure_response( $message );
$response['message'] = $message;
return new WP_REST_Response( $response );
} }
$order_id = $resource['supplementary_data']['related_ids']['order_id'] ?? null; $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 ); do_action( 'ppcp_payment_capture_completed_webhook_handler', $wc_order, $order_id );
if ( $wc_order->get_status() !== 'on-hold' ) { if ( $wc_order->get_status() !== 'on-hold' ) {
$response['success'] = true; return $this->success_response();
return new WP_REST_Response( $response );
} }
$wc_order->add_order_note( $wc_order->add_order_note(
__( 'Payment successfully captured.', 'woocommerce-paypal-payments' ) __( 'Payment successfully captured.', 'woocommerce-paypal-payments' )
@ -130,19 +117,10 @@ class PaymentCaptureCompleted implements RequestHandler {
$wc_order->payment_complete(); $wc_order->payment_complete();
$wc_order->update_meta_data( AuthorizedPaymentsProcessor::CAPTURED_META_KEY, 'true' ); $wc_order->update_meta_data( AuthorizedPaymentsProcessor::CAPTURED_META_KEY, 'true' );
$wc_order->save(); $wc_order->save();
$this->logger->log( $this->logger->info(
'info',
sprintf( sprintf(
// translators: %s is the order ID. 'Order %s has been updated through PayPal',
__(
'Order %s has been updated through PayPal',
'woocommerce-paypal-payments'
),
(string) $wc_order->get_id() (string) $wc_order->get_id()
),
array(
'request' => $request,
'order' => $wc_order,
) )
); );
@ -159,7 +137,6 @@ class PaymentCaptureCompleted implements RequestHandler {
} }
} }
$response['success'] = true; return $this->success_response();
return new WP_REST_Response( $response );
} }
} }

View file

@ -18,7 +18,7 @@ use WP_REST_Response;
*/ */
class PaymentCapturePending implements RequestHandler { class PaymentCapturePending implements RequestHandler {
use PrefixTrait; use RequestHandlerTrait;
/** /**
* The logger. * The logger.
@ -66,36 +66,21 @@ class PaymentCapturePending implements RequestHandler {
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function handle_request( WP_REST_Request $request ): 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'] ) $order_id = $request['resource'] !== null && isset( $request['resource']['custom_id'] )
? $this->sanitize_custom_id( $request['resource']['custom_id'] ) ? $request['resource']['custom_id']
: 0; : 0;
if ( ! $order_id ) { if ( ! $order_id ) {
$message = sprintf( $message = sprintf(
// translators: %s is the PayPal webhook Id. 'No order for webhook event %s was found.',
__(
'No order for webhook event %s was found.',
'woocommerce-paypal-payments'
),
$request['id'] !== null && isset( $request['id'] ) ? $request['id'] : '' $request['id'] !== null && isset( $request['id'] ) ? $request['id'] : ''
); );
$this->logger->log( return $this->failure_response( $message );
'warning',
$message,
array(
'request' => $request,
)
);
$response['message'] = $message;
return new WP_REST_Response( $response );
} }
$resource = $request['resource']; $resource = $request['resource'];
if ( ! is_array( $resource ) ) { if ( ! is_array( $resource ) ) {
$message = 'Resource data not found in webhook request.'; $message = 'Resource data not found in webhook request.';
$this->logger->warning( $message, array( 'request' => $request ) ); return $this->failure_response( $message );
$response['message'] = $message;
return new WP_REST_Response( $response );
} }
$wc_order = wc_get_order( $order_id ); $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'] : '' $request['resource'] !== null && isset( $request['resource']['id'] ) ? $request['resource']['id'] : ''
); );
$this->logger->warning( $message ); return $this->failure_response( $message );
$response['message'] = $message;
return new WP_REST_Response( $response );
} }
if ( $wc_order->get_status() === 'pending' ) { if ( $wc_order->get_status() === 'pending' ) {
@ -116,7 +98,6 @@ class PaymentCapturePending implements RequestHandler {
} }
$response['success'] = true; return $this->success_response();
return new WP_REST_Response( $response );
} }
} }

View file

@ -10,8 +10,10 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks\Handler; namespace WooCommerce\PayPalCommerce\Webhooks\Handler;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WC_Order;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundMetaTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundMetaTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
use WP_Error;
use WP_REST_Request; use WP_REST_Request;
use WP_REST_Response; use WP_REST_Response;
@ -20,7 +22,7 @@ use WP_REST_Response;
*/ */
class PaymentCaptureRefunded implements RequestHandler { class PaymentCaptureRefunded implements RequestHandler {
use PrefixTrait, TransactionIdHandlingTrait, RefundMetaTrait; use TransactionIdHandlingTrait, RefundMetaTrait, RequestHandlerTrait;
/** /**
* The logger. * The logger.
@ -33,11 +35,9 @@ class PaymentCaptureRefunded implements RequestHandler {
* PaymentCaptureRefunded constructor. * PaymentCaptureRefunded constructor.
* *
* @param LoggerInterface $logger The logger. * @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->logger = $logger;
$this->prefix = $prefix;
} }
/** /**
@ -68,59 +68,32 @@ class PaymentCaptureRefunded implements RequestHandler {
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function handle_request( WP_REST_Request $request ): 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'] ) ? $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'] ?? '' ); $refund_id = (string) ( $request['resource']['id'] ?? '' );
if ( ! $order_id ) { if ( ! $order_id ) {
$message = sprintf( $message = sprintf(
// translators: %s is the PayPal webhook Id. 'No order for webhook event %s was found.',
__(
'No order for webhook event %s was found.',
'woocommerce-paypal-payments'
),
isset( $request['id'] ) ? $request['id'] : '' isset( $request['id'] ) ? $request['id'] : ''
); );
$this->logger->log( return $this->failure_response( $message );
'warning',
$message,
array(
'request' => $request,
)
);
$response['message'] = $message;
return new WP_REST_Response( $response );
} }
$wc_order = wc_get_order( $order_id ); $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( $message = sprintf(
// translators: %s is the PayPal refund Id. 'Order for PayPal refund %s not found.',
__( 'Order for PayPal refund %s not found.', 'woocommerce-paypal-payments' ),
$refund_id $refund_id
); );
$this->logger->log( return $this->failure_response( $message );
'warning',
$message,
array(
'request' => $request,
)
);
$response['message'] = $message;
return new WP_REST_Response( $response );
} }
$already_added_refunds = $this->get_refunds_meta( $wc_order ); $already_added_refunds = $this->get_refunds_meta( $wc_order );
if ( in_array( $refund_id, $already_added_refunds, true ) ) { if ( in_array( $refund_id, $already_added_refunds, true ) ) {
$this->logger->info( "Refund {$refund_id} is already handled." ); $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( $refund = wc_create_refund(
array( array(
'order_id' => $wc_order->get_id(), 'order_id' => $wc_order->get_id(),
@ -128,37 +101,21 @@ class PaymentCaptureRefunded implements RequestHandler {
) )
); );
if ( is_wp_error( $refund ) ) { if ( is_wp_error( $refund ) ) {
$this->logger->log( assert( $refund instanceof WP_Error );
'warning', $message = sprintf(
sprintf( 'Order %1$s could not be refunded. %2$s',
// translators: %s is the order id. (string) $wc_order->get_id(),
__( 'Order %s could not be refunded', 'woocommerce-paypal-payments' ), $refund->get_error_message()
(string) $wc_order->get_id()
),
array(
'request' => $request,
'error' => $refund,
)
); );
$response['message'] = $refund->get_error_message(); return $this->failure_response( $message );
return new WP_REST_Response( $response );
} }
$this->logger->log( $this->logger->info(
'info',
sprintf( 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',
__(
'Order %1$s has been refunded with %2$s through PayPal',
'woocommerce-paypal-payments'
),
(string) $wc_order->get_id(), (string) $wc_order->get_id(),
(string) $refund->get_amount() (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 ); $this->add_refund_to_meta( $wc_order, $refund_id );
} }
$response['success'] = true; return $this->success_response();
return new WP_REST_Response( $response );
} }
} }

View file

@ -19,7 +19,7 @@ use Psr\Log\LoggerInterface;
*/ */
class PaymentCaptureReversed implements RequestHandler { class PaymentCaptureReversed implements RequestHandler {
use PrefixTrait; use RequestHandlerTrait;
/** /**
* The logger. * The logger.
@ -32,11 +32,9 @@ class PaymentCaptureReversed implements RequestHandler {
* PaymentCaptureReversed constructor. * PaymentCaptureReversed constructor.
* *
* @param LoggerInterface $logger The logger. * @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->logger = $logger;
$this->prefix = $prefix;
} }
/** /**
@ -71,45 +69,23 @@ class PaymentCaptureReversed implements RequestHandler {
* @return \WP_REST_Response * @return \WP_REST_Response
*/ */
public function handle_request( \WP_REST_Request $request ): \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'] ) ? $order_id = isset( $request['resource']['custom_id'] ) ?
$this->sanitize_custom_id( $request['resource']['custom_id'] ) : 0; $request['resource']['custom_id'] : 0;
if ( ! $order_id ) { if ( ! $order_id ) {
$message = sprintf( $message = sprintf(
// translators: %s is the PayPal webhook Id. 'No order for webhook event %s was found.',
__(
'No order for webhook event %s was found.',
'woocommerce-paypal-payments'
),
isset( $request['id'] ) ? $request['id'] : '' isset( $request['id'] ) ? $request['id'] : ''
); );
$this->logger->log( return $this->failure_response( $message );
'warning',
$message,
array(
'request' => $request,
)
);
$response['message'] = $message;
return rest_ensure_response( $response );
} }
$wc_order = wc_get_order( $order_id ); $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( $message = sprintf(
// translators: %s is the PayPal refund Id. 'Order for PayPal refund %s not found.',
__( 'Order for PayPal refund %s not found.', 'woocommerce-paypal-payments' ),
isset( $request['resource']['id'] ) ? $request['resource']['id'] : '' isset( $request['resource']['id'] ) ? $request['resource']['id'] : ''
); );
$this->logger->log( return $this->failure_response( $message );
'warning',
$message,
array(
'request' => $request,
)
);
$response['message'] = $message;
return rest_ensure_response( $response );
} }
/** /**
@ -117,33 +93,20 @@ class PaymentCaptureReversed implements RequestHandler {
*/ */
$note = apply_filters( 'ppcp_payment_capture_reversed_webhook_update_status_note', '', $wc_order, $request['event_type'] ); $note = apply_filters( 'ppcp_payment_capture_reversed_webhook_update_status_note', '', $wc_order, $request['event_type'] );
/** $is_success = $wc_order->update_status( 'cancelled', $note );
* The WooCommerce order. if ( ! $is_success ) {
* $message = sprintf(
* @var \WC_Order $wc_order 'Failed to cancel order %1$s cancelled through PayPal',
*/ (string) $wc_order->get_id()
$response['success'] = (bool) $wc_order->update_status( 'cancelled', $note ); );
return $this->failure_response( $message );
}
$message = $response['success'] ? sprintf( $message = sprintf(
// translators: %1$s is the order id. 'Order %1$s has been cancelled through PayPal',
__(
'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' ),
(string) $wc_order->get_id() (string) $wc_order->get_id()
); );
$this->logger->log( $this->logger->info( $message );
$response['success'] ? 'info' : 'warning', return $this->success_response();
$message,
array(
'request' => $request,
'order' => $wc_order,
)
);
return rest_ensure_response( $response );
} }
} }

View file

@ -20,7 +20,7 @@ use WP_REST_Response;
*/ */
class PaymentSaleCompleted implements RequestHandler { class PaymentSaleCompleted implements RequestHandler {
use TransactionIdHandlingTrait; use TransactionIdHandlingTrait, RequestHandlerTrait;
/** /**
* The logger. * The logger.
@ -67,17 +67,14 @@ class PaymentSaleCompleted implements RequestHandler {
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function handle_request( WP_REST_Request $request ): WP_REST_Response { public function handle_request( WP_REST_Request $request ): WP_REST_Response {
$response = array( 'success' => false );
if ( is_null( $request['resource'] ) ) { 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'] ?? '' ) ); $billing_agreement_id = wc_clean( wp_unslash( $request['resource']['billing_agreement_id'] ?? '' ) );
if ( ! $billing_agreement_id ) { if ( ! $billing_agreement_id ) {
$message = 'Could not retrieve billing agreement id for subscription.'; $message = 'Could not retrieve billing agreement id for subscription.';
$this->logger->warning( $message, array( 'request' => $request ) ); return $this->failure_response( $message );
$response['message'] = $message;
return new WP_REST_Response( $response );
} }
$args = array( $args = array(
@ -99,7 +96,6 @@ class PaymentSaleCompleted implements RequestHandler {
} }
} }
$response['success'] = true; return $this->success_response();
return new WP_REST_Response( $response );
} }
} }

View file

@ -21,7 +21,7 @@ use WP_REST_Response;
*/ */
class PaymentSaleRefunded implements RequestHandler { class PaymentSaleRefunded implements RequestHandler {
use TransactionIdHandlingTrait, RefundMetaTrait; use TransactionIdHandlingTrait, RefundMetaTrait, RequestHandlerTrait;
/** /**
* The logger. * The logger.
@ -68,16 +68,15 @@ class PaymentSaleRefunded implements RequestHandler {
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function handle_request( WP_REST_Request $request ): WP_REST_Response { public function handle_request( WP_REST_Request $request ): WP_REST_Response {
$response = array( 'success' => false );
if ( is_null( $request['resource'] ) ) { if ( is_null( $request['resource'] ) ) {
return new WP_REST_Response( $response ); return $this->failure_response();
} }
$refund_id = (string) ( $request['resource']['id'] ?? '' ); $refund_id = (string) ( $request['resource']['id'] ?? '' );
$transaction_id = $request['resource']['sale_id'] ?? ''; $transaction_id = $request['resource']['sale_id'] ?? '';
$total_refunded_amount = $request['resource']['total_refunded_amount']['value'] ?? ''; $total_refunded_amount = $request['resource']['total_refunded_amount']['value'] ?? '';
if ( ! $refund_id || ! $transaction_id || ! $total_refunded_amount ) { if ( ! $refund_id || ! $transaction_id || ! $total_refunded_amount ) {
return new WP_REST_Response( $response ); return $this->failure_response();
} }
$args = array( $args = array(
@ -90,7 +89,7 @@ class PaymentSaleRefunded implements RequestHandler {
$wc_orders = wc_get_orders( $args ); $wc_orders = wc_get_orders( $args );
if ( ! is_array( $wc_orders ) ) { if ( ! is_array( $wc_orders ) ) {
return new WP_REST_Response( $response ); return $this->failure_response();
} }
foreach ( $wc_orders as $wc_order ) { foreach ( $wc_orders as $wc_order ) {
@ -102,24 +101,17 @@ class PaymentSaleRefunded implements RequestHandler {
); );
if ( $refund instanceof WP_Error ) { if ( $refund instanceof WP_Error ) {
$this->logger->warning( $message = sprintf(
sprintf( 'Order %s could not be refunded. %s',
// translators: %s is the order id. (string) $wc_order->get_id(),
__( 'Order %s could not be refunded', 'woocommerce-paypal-payments' ), $refund->get_error_message()
(string) $wc_order->get_id()
)
); );
$response['message'] = $refund->get_error_message(); return $this->failure_response( $message );
return new WP_REST_Response( $response );
} }
$order_refunded_message = sprintf( $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.',
__(
'Order %1$s has been refunded with %2$s through PayPal.',
'woocommerce-paypal-payments'
),
(string) $wc_order->get_id(), (string) $wc_order->get_id(),
(string) $total_refunded_amount (string) $total_refunded_amount
); );
@ -130,7 +122,6 @@ class PaymentSaleRefunded implements RequestHandler {
$this->add_refund_to_meta( $wc_order, $refund_id ); $this->add_refund_to_meta( $wc_order, $refund_id );
} }
$response['success'] = true; return $this->success_response();
return new WP_REST_Response( $response );
} }
} }

View file

@ -1,40 +0,0 @@
<?php
/**
* Trait which helps to remove the prefix of IDs.
*
* @package WooCommerce\PayPalCommerce\Webhooks\Handler
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks\Handler;
/**
* Trait PrefixTrait
*/
trait PrefixTrait {
/**
* The prefix.
*
* @var string
*/
private $prefix = '';
/**
* Removes the prefix from a given Id.
*
* @param string $custom_id The custom id.
*
* @return int
*/
private function sanitize_custom_id( string $custom_id ): int {
$id = $custom_id;
if ( strlen( $this->prefix ) > 0 && 0 === strpos( $id, $this->prefix ) ) {
$id = substr( $id, strlen( $this->prefix ) );
}
return (int) $id;
}
}

View file

@ -9,8 +9,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks\Handler; namespace WooCommerce\PayPalCommerce\Webhooks\Handler;
use stdClass;
use WC_Order; use WC_Order;
use WooCommerce\PayPalCommerce\Webhooks\CustomIds;
use WP_REST_Request; use WP_REST_Request;
use WP_REST_Response; use WP_REST_Response;
@ -19,22 +19,78 @@ trait RequestHandlerTrait {
/** /**
* Get available custom ids from the given request * Get available custom ids from the given request
* *
* @param \WP_REST_Request $request The request. * @param WP_REST_Request $request The request.
* @return array * @return string[]
*/ */
protected function get_custom_ids_from_request( WP_REST_Request $request ): array { protected function get_custom_ids_from_request( WP_REST_Request $request ): array {
return array_filter( $resource = $request['resource'];
array_map( 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 { static function ( array $purchase_unit ): string {
return isset( $purchase_unit['custom_id'] ) ? return $purchase_unit['custom_id'] ?? '';
(string) $purchase_unit['custom_id'] : '';
}, },
$request['resource'] !== null && isset( $request['resource']['purchase_units'] ) ? (array) $resource['purchase_units']
(array) $request['resource']['purchase_units'] : array() );
), }
static function ( string $order_id ): bool {
return ! empty( $order_id ); 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[] * @return WC_Order[]
*/ */
protected function get_wc_orders_from_custom_ids( array $custom_ids ): array { protected function get_wc_orders_from_custom_ids( array $custom_ids ): array {
$order_ids = array_map( $order_ids = $custom_ids;
array(
$this,
'sanitize_custom_id',
),
$custom_ids
);
$args = array( $args = array(
'post__in' => $order_ids, 'post__in' => $order_ids,
'limit' => -1, '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 WP_REST_Request $request The request.
* @param array $response The response.
* @return WP_REST_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( $message = sprintf(
// translators: %s is the PayPal webhook Id. 'WC order ID was not found in webhook event %s for PayPal order %s.',
__( 'No order for webhook event %s was found.', 'woocommerce-paypal-payments' ), (string) ( $request['id'] ?? '' ),
$request['id'] !== null && isset( $request['id'] ) ? $request['id'] : '' // Psalm 4.x does not seem to understand ?? with ArrayAccess correctly.
);
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' ),
$request['resource'] !== null && isset( $request['resource']['id'] ) ? $request['resource']['id'] : '' $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 WP_REST_Request $request The request.
* @param array $response The response.
* @return WP_REST_Response * @return WP_REST_Response
*/ */
private function log_and_return_response( string $message, array $response ): WP_REST_Response { protected function no_wc_orders_response( WP_REST_Request $request ): WP_REST_Response {
$this->logger->warning( $message ); $message = sprintf(
$response['message'] = $message; '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 ); return new WP_REST_Response( $response );
} }

View file

@ -24,6 +24,7 @@ use WP_REST_Response;
* Class VaultPaymentTokenCreated * Class VaultPaymentTokenCreated
*/ */
class VaultPaymentTokenCreated implements RequestHandler { class VaultPaymentTokenCreated implements RequestHandler {
use RequestHandlerTrait;
/** /**
* The logger. * The logger.
@ -103,16 +104,12 @@ class VaultPaymentTokenCreated implements RequestHandler {
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function handle_request( WP_REST_Request $request ): 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'] ) $customer_id = null !== $request['resource'] && isset( $request['resource']['customer_id'] )
? $request['resource']['customer_id'] ? $request['resource']['customer_id']
: ''; : '';
if ( ! $customer_id ) { if ( ! $customer_id ) {
$message = 'No customer id was found.'; $message = 'No customer id was found.';
$this->logger->warning( $message, array( 'request' => $request ) ); return $this->failure_response( $message );
$response['message'] = $message;
return new WP_REST_Response( $response );
} }
$wc_customer_id = (int) str_replace( $this->prefix, '', $customer_id ); $wc_customer_id = (int) str_replace( $this->prefix, '', $customer_id );
@ -150,7 +147,6 @@ class VaultPaymentTokenCreated implements RequestHandler {
} }
} }
$response['success'] = true; return $this->success_response();
return new WP_REST_Response( $response );
} }
} }

View file

@ -18,6 +18,7 @@ use WP_REST_Response;
* Class VaultPaymentTokenDeleted * Class VaultPaymentTokenDeleted
*/ */
class VaultPaymentTokenDeleted implements RequestHandler { class VaultPaymentTokenDeleted implements RequestHandler {
use RequestHandlerTrait;
/** /**
* The logger. * The logger.
@ -65,8 +66,6 @@ class VaultPaymentTokenDeleted implements RequestHandler {
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function handle_request( WP_REST_Request $request ): 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'] ) ) { if ( ! is_null( $request['resource'] ) && isset( $request['resource']['id'] ) ) {
$token_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) ); $token_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) );
@ -89,7 +88,6 @@ class VaultPaymentTokenDeleted implements RequestHandler {
} }
} }
$response['success'] = true; return $this->success_response();
return new WP_REST_Response( $response );
} }
} }

View file

@ -17,12 +17,14 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookEventFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookEventFactory;
use WooCommerce\PayPalCommerce\Webhooks\Handler\RequestHandler; use WooCommerce\PayPalCommerce\Webhooks\Handler\RequestHandler;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\Webhooks\Handler\RequestHandlerTrait;
use WooCommerce\PayPalCommerce\Webhooks\Status\WebhookSimulation; use WooCommerce\PayPalCommerce\Webhooks\Status\WebhookSimulation;
/** /**
* Class IncomingWebhookEndpoint * Class IncomingWebhookEndpoint
*/ */
class IncomingWebhookEndpoint { class IncomingWebhookEndpoint {
use RequestHandlerTrait;
const NAMESPACE = 'paypal/v1'; const NAMESPACE = 'paypal/v1';
const ROUTE = 'incoming'; const ROUTE = 'incoming';
@ -211,26 +213,16 @@ class IncomingWebhookEndpoint {
if ( $this->simulation->is_simulation_event( $event ) ) { if ( $this->simulation->is_simulation_event( $event ) ) {
$this->logger->info( 'Received simulated webhook.' ); $this->logger->info( 'Received simulated webhook.' );
$this->simulation->receive( $event ); $this->simulation->receive( $event );
return rest_ensure_response( return $this->success_response();
array(
'success' => true,
)
);
} }
foreach ( $this->handlers as $handler ) { foreach ( $this->handlers as $handler ) {
if ( $handler->responsible_for_request( $request ) ) { if ( $handler->responsible_for_request( $request ) ) {
$response = $handler->handle_request( $request ); $response = $handler->handle_request( $request );
$this->logger->log( $this->logger->info(
'info',
sprintf( sprintf(
// translators: %s is the event type. 'Webhook has been handled by %s',
__( 'Webhook has been handled by %s', 'woocommerce-paypal-payments' ),
( $handler->event_types() ) ? current( $handler->event_types() ) : '' ( $handler->event_types() ) ? current( $handler->event_types() ) : ''
),
array(
'request' => $request,
'response' => $response,
) )
); );
return $response; return $response;
@ -238,22 +230,10 @@ class IncomingWebhookEndpoint {
} }
$message = sprintf( $message = sprintf(
// translators: %s is the request type. 'Could not find handler for request type %s',
__( 'Could not find handler for request type %s', 'woocommerce-paypal-payments' ), $request['event_type'] ?: ''
$request['event_type']
); );
$this->logger->log( return $this->failure_response( $message );
'warning',
$message,
array(
'request' => $request,
)
);
$response = array(
'success' => false,
'message' => $message,
);
return rest_ensure_response( $response );
} }
/** /**

View file

@ -258,7 +258,7 @@ class PurchaseUnitFactoryTest extends TestCase
{ {
$wcCustomer = Mockery::mock(\WC_Customer::class); $wcCustomer = Mockery::mock(\WC_Customer::class);
expect('WC') expect('WC')
->andReturn((object) ['customer' => $wcCustomer]); ->andReturn((object) ['customer' => $wcCustomer, 'session' => null]);
$wcCart = Mockery::mock(\WC_Cart::class); $wcCart = Mockery::mock(\WC_Cart::class);
$amount = Mockery::mock(Amount::class); $amount = Mockery::mock(Amount::class);
@ -322,7 +322,7 @@ class PurchaseUnitFactoryTest extends TestCase
public function testWcCartShippingGetsDroppendWhenNoCustomer() public function testWcCartShippingGetsDroppendWhenNoCustomer()
{ {
expect('WC') expect('WC')
->andReturn((object) ['customer' => null]); ->andReturn((object) ['customer' => null, 'session' => null]);
$wcCart = Mockery::mock(\WC_Cart::class); $wcCart = Mockery::mock(\WC_Cart::class);
$amount = Mockery::mock(Amount::class); $amount = Mockery::mock(Amount::class);
@ -360,7 +360,7 @@ class PurchaseUnitFactoryTest extends TestCase
public function testWcCartShippingGetsDroppendWhenNoCountryCode() public function testWcCartShippingGetsDroppendWhenNoCountryCode()
{ {
expect('WC') 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); $wcCart = Mockery::mock(\WC_Cart::class);
$amount = Mockery::mock(Amount::class); $amount = Mockery::mock(Amount::class);

View file

@ -169,6 +169,7 @@ class CreateOrderEndpointTest extends TestCase
false, false,
['checkout'], ['checkout'],
false, false,
['paypal'],
new NullLogger() new NullLogger()
); );
return array($payer_factory, $testee); return array($payer_factory, $testee);

View file

@ -5,6 +5,8 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Gateway;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; 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\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler;
@ -75,6 +77,11 @@ class WcGatewayTest extends TestCase
->andReturnUsing(function () { ->andReturnUsing(function () {
return $this->fundingSource; 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(); $this->settings->shouldReceive('has')->andReturnFalse();

View file

@ -1,6 +1,6 @@
const {test, expect} = require('@playwright/test'); const {test, expect} = require('@playwright/test');
const {serverExec} = require("./utils/server"); 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 {openPaypalPopup, loginIntoPaypal, waitForPaypalShippingList, completePaypalPayment} = require("./utils/paypal-popup");
const { const {
@ -11,9 +11,11 @@ const {
PRODUCT_ID, PRODUCT_ID,
CHECKOUT_URL, CHECKOUT_URL,
CHECKOUT_PAGE_ID, CHECKOUT_PAGE_ID,
CART_URL,
BLOCK_CHECKOUT_URL, BLOCK_CHECKOUT_URL,
BLOCK_CHECKOUT_PAGE_ID, BLOCK_CHECKOUT_PAGE_ID,
BLOCK_CART_URL, BLOCK_CART_URL,
APM_ID,
} = process.env; } = process.env;
async function completeBlockContinuation(page) { async function completeBlockContinuation(page) {
@ -21,11 +23,25 @@ async function completeBlockContinuation(page) {
await expect(page.locator('.component-frame')).toHaveCount(0); 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', () => { test.describe('Classic checkout', () => {
@ -44,10 +60,7 @@ test.describe('Classic checkout', () => {
await fillCheckoutForm(page); await fillCheckoutForm(page);
await Promise.all([ await completeContinuation(page);
page.waitForNavigation(),
page.locator('#place_order').click(),
]);
await expectOrderReceivedPage(page); await expectOrderReceivedPage(page);
}); });
@ -77,6 +90,47 @@ test.describe('Classic checkout', () => {
await expectOrderReceivedPage(page); await expectOrderReceivedPage(page);
}); });
test('PayPal APM button place order', async ({page}) => {
await page.goto(CART_URL + '?add-to-cart=' + PRODUCT_ID);
await page.goto(CHECKOUT_URL);
await fillCheckoutForm(page);
const popup = await openPaypalPopup(page, {fundingSource: APM_ID});
await popup.getByText('Continue', { exact: true }).click();
await completePaypalPayment(popup, {selector: '[name="Successful"]'});
await expectOrderReceivedPage(page);
});
test('PayPal APM button place order when redirect fails', async ({page}) => {
await page.goto(CART_URL + '?add-to-cart=' + PRODUCT_ID);
await page.goto(CHECKOUT_URL);
await fillCheckoutForm(page);
await page.evaluate('PayPalCommerceGateway.ajax.approve_order = null');
const popup = await openPaypalPopup(page, {fundingSource: APM_ID});
await popup.getByText('Continue', { exact: true }).click();
await completePaypalPayment(popup, {selector: '[name="Successful"]'});
await expect(page.locator('.woocommerce-error')).toBeVisible();
await page.reload();
await expectContinuation(page);
await acceptTerms(page);
await completeContinuation(page);
await expectOrderReceivedPage(page);
});
}); });
test.describe('Block checkout', () => { test.describe('Block checkout', () => {
@ -97,6 +151,8 @@ test.describe('Block checkout', () => {
await completePaypalPayment(popup); await completePaypalPayment(popup);
await completeBlockContinuation(page); await completeBlockContinuation(page);
await expectOrderReceivedPage(page);
}); });
test('PayPal express block cart', async ({page}) => { test('PayPal express block cart', async ({page}) => {
@ -109,6 +165,8 @@ test.describe('Block checkout', () => {
await completePaypalPayment(popup); await completePaypalPayment(popup);
await completeBlockContinuation(page); await completeBlockContinuation(page);
await expectOrderReceivedPage(page);
}); });
test.describe('Without review', () => { test.describe('Without review', () => {

View file

@ -26,6 +26,10 @@ export const fillCheckoutForm = async (page) => {
await differentShippingLocator.uncheck(); await differentShippingLocator.uncheck();
} }
await acceptTerms(page);
}
export const acceptTerms = async (page) => {
const termsLocator = page.locator('[name="terms"]'); const termsLocator = page.locator('[name="terms"]');
if (await termsLocator.count() > 0) { if (await termsLocator.count() > 0) {
await termsLocator.check(); await termsLocator.check();

View file

@ -8,16 +8,24 @@ const {
/** /**
* Opens the PayPal popup by pressing the button, and returns the popup object. * Opens the PayPal popup by pressing the button, and returns the popup object.
* @param page * @param page
* @param {{timeout: ?int, fundingSource: ?string}} options
* @param {boolean} retry Retries the button click if the popup did not appear after timeout. * @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 { try {
await page.locator('.component-frame').scrollIntoViewIfNeeded(); await page.locator('.component-frame').scrollIntoViewIfNeeded();
const [popup] = await Promise.all([ const [popup] = await Promise.all([
page.waitForEvent('popup', {timeout}), page.waitForEvent('popup', {timeout: options.timeout}),
page.frameLocator('.component-frame').locator('[data-funding-source="paypal"]').click(), page.frameLocator('.component-frame').locator(`[data-funding-source="${options.fundingSource}"]`).click(),
]); ]);
await popup.waitForLoadState(); await popup.waitForLoadState();
@ -41,7 +49,7 @@ export const openPaypalPopup = async (page, retry = true, timeout = 5000) => {
} }
if (retry) { if (retry) {
return openPaypalPopup(page, false); return openPaypalPopup(page, options, false);
} }
throw err; throw err;
} }
@ -83,9 +91,20 @@ export const waitForPaypalShippingList = async (popup) => {
await expect(popup.locator('#shippingMethodsDropdown')).toBeVisible({timeout: 15000}); 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([ await Promise.all([
popup.waitForEvent('close', {timeout: 20000}), popup.waitForEvent('close', {timeout: options.timeout}),
popup.click('#payment-submit-btn'), popup.click(options.selector),
]); ]);
} }