Merge branch 'trunk' into PCP-591-save-and-display-vaulted-payment-methods-in-woo-commerce-native-endpoint

This commit is contained in:
Emili Castells Guasch 2023-02-28 10:00:35 +01:00
commit 4737f9d203
46 changed files with 1327 additions and 311 deletions

View file

@ -1 +1,10 @@
PPCP_E2E_WP_DIR=${ROOT_DIR}/.ddev/wordpress
BASEURL="https://woocommerce-paypal-payments.ddev.site"
CUSTOMER_EMAIL="customer@example.com"
CUSTOMER_PASSWORD="password"
CREDIT_CARD_NUMBER="1234567890"
CREDIT_CARD_EXPIRATION="01/2042"
CREDIT_CARD_CVV="123"

View file

@ -1,5 +1,28 @@
*** Changelog ***
= 2.0.3 - TBD =
* Fix - `DEVICE_DATA_NOT_AVAILABLE` error message when FraudNet is enabled #1177
* Fix - Redirect to connection tab after manual credentials input #1201
* Fix - Asking for address fields in checkout when not using them #1089
* Fix - Validate before free trial #1170
* Fix - Validate new user creation #1131
* Fix - After Updating to 2.0.2, Site Health reports REST API error #1195
* Fix - Do not send buyer-country for previews in live mode to avoid error #1186
* Fix - PPEC compatibility layer does not take over subscriptions #1193
* Enhancement - Save checkout form before free trial redirect #1135
* Enhancement - Add filter for controlling the ditching of items/breakdown #1146
* Enhancement - Add patch order data filter #1147
* Enhancement - Add filter for disabling fees on wc order admin pages #1153
* Enhancement - Use wp_loaded for fraudnet loading to avoid warnings #1172
* Enhancement - reCaptcha for WooCommerce support #1093
* Enhancement - Make it possible to hide missing funding resource Trustly #1155
* Enhancement - Add white color option #1167
* Enhancement - Checkout validation for other fields #861
* Enhancement - Mention PUI only for German shops and add line breaks #1169
* Enhancement - Add filter to fallback tracking_data['carrier'] #1188
* Enhancement - Error notices in checkout do not update / or are shown twice #1168
* Enhancement - capture authorized payment by changing order status (or programmatically) #587
= 2.0.2 - 2023-01-31 =
* Fix - Do not call PayPal get order by ID if it does not exist #1029
* Fix - Type check error conflict with German Market #1056

View file

@ -518,6 +518,11 @@ class OrderEndpoint {
}
}
/**
* The filter can be used to modify the order patching request body data (the final prices, items).
*/
$patches_array = apply_filters( 'ppcp_patch_order_request_body_data', $patches_array );
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/checkout/orders/' . $order_to_update->id();
$args = array(

View file

@ -284,7 +284,14 @@ class PurchaseUnit {
$this->items()
),
);
if ( $ditch_items_when_mismatch && $this->ditch_items_when_mismatch( $this->amount(), ...$this->items() ) ) {
$ditch = $ditch_items_when_mismatch && $this->ditch_items_when_mismatch( $this->amount(), ...$this->items() );
/**
* The filter can be used to control when the items and totals breakdown are removed from PayPal order info.
*/
$ditch = apply_filters( 'ppcp_ditch_items_breakdown', $ditch, $this );
if ( $ditch ) {
unset( $purchase_unit['items'] );
unset( $purchase_unit['amount']['breakdown'] );
}

View file

@ -17,6 +17,8 @@ import {
import {hide, setVisible, setVisibleByClass} from "./modules/Helper/Hiding";
import {isChangePaymentPage} from "./modules/Helper/Subscriptions";
import FreeTrialHandler from "./modules/ActionHandler/FreeTrialHandler";
import FormSaver from './modules/Helper/FormSaver';
import FormValidator from "./modules/Helper/FormValidator";
// TODO: could be a good idea to have a separate spinner for each gateway,
// but I think we care mainly about the script loading, so one spinner should be enough.
@ -24,11 +26,27 @@ const buttonsSpinner = new Spinner(document.querySelector('.ppc-button-wrapper')
const cardsSpinner = new Spinner('#ppcp-hosted-fields');
const bootstrap = () => {
const errorHandler = new ErrorHandler(PayPalCommerceGateway.labels.error.generic);
const checkoutFormSelector = 'form.woocommerce-checkout';
const errorHandler = new ErrorHandler(
PayPalCommerceGateway.labels.error.generic,
document.querySelector(checkoutFormSelector) ?? document.querySelector('.woocommerce-notices-wrapper')
);
const spinner = new Spinner();
const creditCardRenderer = new CreditCardRenderer(PayPalCommerceGateway, errorHandler, spinner);
const freeTrialHandler = new FreeTrialHandler(PayPalCommerceGateway, spinner, errorHandler);
const formSaver = new FormSaver(
PayPalCommerceGateway.ajax.save_checkout_form.endpoint,
PayPalCommerceGateway.ajax.save_checkout_form.nonce,
);
const formValidator = PayPalCommerceGateway.early_checkout_validation_enabled ?
new FormValidator(
PayPalCommerceGateway.ajax.validate_checkout.endpoint,
PayPalCommerceGateway.ajax.validate_checkout.nonce,
) : null;
const freeTrialHandler = new FreeTrialHandler(PayPalCommerceGateway, checkoutFormSelector, formSaver, formValidator, spinner, errorHandler);
jQuery('form.woocommerce-checkout input').on('keydown', e => {
if (e.key === 'Enter' && [
@ -88,7 +106,7 @@ const bootstrap = () => {
}
}
const form = document.querySelector('form.woocommerce-checkout');
const form = document.querySelector(checkoutFormSelector);
if (form) {
jQuery('#ppcp-funding-source-form-input').remove();
form.insertAdjacentHTML(

View file

@ -62,9 +62,9 @@ class CheckoutActionHandler {
if (data.data.errors?.length > 0) {
errorHandler.messages(data.data.errors);
} else if (data.data.details?.length > 0) {
errorHandler.message(data.data.details.map(d => `${d.issue} ${d.description}`).join('<br/>'), true);
errorHandler.message(data.data.details.map(d => `${d.issue} ${d.description}`).join('<br/>'));
} else {
errorHandler.message(data.data.message, true);
errorHandler.message(data.data.message);
}
}

View file

@ -1,44 +1,73 @@
import {PaymentMethods} from "../Helper/CheckoutMethodState";
import errorHandler from "../ErrorHandler";
class FreeTrialHandler {
/**
* @param config
* @param formSelector
* @param {FormSaver} formSaver
* @param {FormValidator|null} formValidator
* @param {Spinner} spinner
* @param {ErrorHandler} errorHandler
*/
constructor(
config,
formSelector,
formSaver,
formValidator,
spinner,
errorHandler
) {
this.config = config;
this.formSelector = formSelector;
this.formSaver = formSaver;
this.formValidator = formValidator;
this.spinner = spinner;
this.errorHandler = errorHandler;
}
handle()
async handle()
{
this.spinner.block();
fetch(this.config.ajax.vault_paypal.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: this.config.ajax.vault_paypal.nonce,
return_url: location.href
}),
}).then(res => {
return res.json();
}).then(data => {
try {
await this.formSaver.save(document.querySelector(this.formSelector));
} catch (error) {
console.error(error);
}
try {
if (this.formValidator) {
try {
const errors = await this.formValidator.validate(document.querySelector(this.formSelector));
if (errors.length > 0) {
this.spinner.unblock();
this.errorHandler.messages(errors);
return;
}
} catch (error) {
console.error(error);
}
}
const res = await fetch(this.config.ajax.vault_paypal.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: this.config.ajax.vault_paypal.nonce,
return_url: location.href,
}),
});
const data = await res.json();
if (!data.success) {
this.spinner.unblock();
console.error(data);
this.errorHandler.message(data.data.message);
throw Error(data.data.message);
}
location.href = data.data.approve_link;
}).catch(error => {
} catch (error) {
this.spinner.unblock();
console.error(error);
this.errorHandler.genericError();
});
this.errorHandler.message(data.data.message);
}
}
}
export default FreeTrialHandler;

View file

@ -1,47 +1,41 @@
class ErrorHandler {
constructor(genericErrorText)
/**
* @param {String} genericErrorText
* @param {Element} wrapper
*/
constructor(genericErrorText, wrapper)
{
this.genericErrorText = genericErrorText;
this.wrapper = document.querySelector('.woocommerce-notices-wrapper');
this.messagesList = document.querySelector('ul.woocommerce-error');
this.wrapper = wrapper;
}
genericError() {
if (this.wrapper.classList.contains('ppcp-persist')) {
return;
}
this.clear();
this.message(this.genericErrorText)
}
appendPreparedErrorMessageElement(errorMessageElement)
{
if (this.messagesList === null) {
this._prepareMessagesList();
}
this.messagesList.replaceWith(errorMessageElement);
this._getMessageContainer().replaceWith(errorMessageElement);
}
/**
* @param {String} text
* @param {Boolean} persist
*/
message(text, persist = false)
message(text)
{
this._addMessage(text, persist);
this._addMessage(text);
this._scrollToMessages();
}
/**
* @param {Array} texts
* @param {Boolean} persist
*/
messages(texts, persist = false)
messages(texts)
{
texts.forEach(t => this._addMessage(t, persist));
texts.forEach(t => this._addMessage(t));
this._scrollToMessages();
}
@ -49,26 +43,17 @@ class ErrorHandler {
/**
* @private
* @param {String} text
* @param {Boolean} persist
*/
_addMessage(text, persist = false)
_addMessage(text)
{
if(! typeof String || text.length === 0) {
throw new Error('A new message text must be a non-empty string.');
}
if (this.messagesList === null){
this._prepareMessagesList();
}
const messageContainer = this._getMessageContainer();
if (persist) {
this.wrapper.classList.add('ppcp-persist');
} else {
this.wrapper.classList.remove('ppcp-persist');
}
let messageNode = this._prepareMessagesListItem(text);
this.messagesList.appendChild(messageNode);
let messageNode = this._prepareMessageElement(text);
messageContainer.appendChild(messageNode);
}
/**
@ -76,26 +61,28 @@ class ErrorHandler {
*/
_scrollToMessages()
{
jQuery.scroll_to_notices(jQuery('.woocommerce-notices-wrapper'));
jQuery.scroll_to_notices(jQuery('.woocommerce-error'));
}
/**
* @private
*/
_prepareMessagesList()
_getMessageContainer()
{
if (this.messagesList === null) {
this.messagesList = document.createElement('ul');
this.messagesList.setAttribute('class', 'woocommerce-error');
this.messagesList.setAttribute('role', 'alert');
this.wrapper.appendChild(this.messagesList);
let messageContainer = document.querySelector('ul.woocommerce-error');
if (messageContainer === null) {
messageContainer = document.createElement('ul');
messageContainer.setAttribute('class', 'woocommerce-error');
messageContainer.setAttribute('role', 'alert');
jQuery(this.wrapper).prepend(messageContainer);
}
return messageContainer;
}
/**
* @private
*/
_prepareMessagesListItem(message)
_prepareMessageElement(message)
{
const li = document.createElement('li');
li.innerHTML = message;
@ -105,11 +92,7 @@ class ErrorHandler {
clear()
{
if (this.messagesList === null) {
return;
}
this.messagesList.innerHTML = '';
jQuery( '.woocommerce-error, .woocommerce-message' ).remove();
}
}

View file

@ -0,0 +1,26 @@
export default class FormSaver {
constructor(url, nonce) {
this.url = url;
this.nonce = nonce;
}
async save(form) {
const formData = new FormData(form);
const formJsonObj = Object.fromEntries(formData.entries());
const res = await fetch(this.url, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: this.nonce,
form: formJsonObj,
}),
});
const data = await res.json();
if (!data.success) {
throw Error(data.data.message);
}
}
}

View file

@ -0,0 +1,31 @@
export default class FormValidator {
constructor(url, nonce) {
this.url = url;
this.nonce = nonce;
}
async validate(form) {
const formData = new FormData(form);
const formJsonObj = Object.fromEntries(formData.entries());
const res = await fetch(this.url, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: this.nonce,
form: formJsonObj,
}),
});
const data = await res.json();
if (!data.success) {
if (data.data.errors) {
return data.data.errors;
}
throw Error(data.data.message);
}
return [];
}
}

View file

@ -234,15 +234,15 @@ class CreditCardRenderer {
this.errorHandler.clear();
if (err.data?.details?.length) {
this.errorHandler.message(err.data.details.map(d => `${d.issue} ${d.description}`).join('<br/>'), true);
this.errorHandler.message(err.data.details.map(d => `${d.issue} ${d.description}`).join('<br/>'));
} else if (err.details?.length) {
this.errorHandler.message(err.details.map(d => `${d.issue} ${d.description}`).join('<br/>'), true);
this.errorHandler.message(err.details.map(d => `${d.issue} ${d.description}`).join('<br/>'));
} else if (err.data?.errors?.length > 0) {
this.errorHandler.messages(err.data.errors);
} else if (err.data?.message) {
this.errorHandler.message(err.data.message, true);
this.errorHandler.message(err.data.message);
} else if (err.message) {
this.errorHandler.message(err.message, true);
this.errorHandler.message(err.message);
} else {
this.errorHandler.genericError();
}

View file

@ -9,6 +9,10 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button;
use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Button\Assets\DisabledSmartButton;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButton;
@ -104,6 +108,7 @@ return array(
$currency,
$container->get( 'wcgateway.all-funding-sources' ),
$container->get( 'button.basic-checkout-validation-enabled' ),
$container->get( 'button.early-wc-checkout-validation-enabled' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
@ -181,6 +186,16 @@ return array(
$logger
);
},
'button.checkout-form-saver' => static function ( ContainerInterface $container ): CheckoutFormSaver {
return new CheckoutFormSaver();
},
'button.endpoint.save-checkout-form' => static function ( ContainerInterface $container ): SaveCheckoutFormEndpoint {
return new SaveCheckoutFormEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'button.checkout-form-saver' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'button.endpoint.data-client-id' => static function( ContainerInterface $container ) : DataClientIdEndpoint {
$request_data = $container->get( 'button.request-data' );
$identity_token = $container->get( 'api.endpoint.identity-token' );
@ -198,6 +213,13 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'button.endpoint.validate-checkout' => static function ( ContainerInterface $container ): ValidateCheckoutEndpoint {
return new ValidateCheckoutEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'button.validation.wc-checkout-validator' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'button.helper.three-d-secure' => static function ( ContainerInterface $container ): ThreeDSecure {
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new ThreeDSecure( $logger );
@ -234,4 +256,7 @@ return array(
*/
return (bool) apply_filters( 'woocommerce_paypal_payments_early_wc_checkout_validation_enabled', true );
},
'button.validation.wc-checkout-validator' => static function ( ContainerInterface $container ): CheckoutFormValidator {
return new CheckoutFormValidator();
},
);

View file

@ -21,7 +21,10 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\DataClientIdEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\StartPayPalVaultingEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
@ -40,7 +43,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
*/
class SmartButton implements SmartButtonInterface {
use FreeTrialHandlerTrait;
use FreeTrialHandlerTrait, ContextTrait;
/**
* The Settings status helper.
@ -154,6 +157,13 @@ class SmartButton implements SmartButtonInterface {
*/
private $basic_checkout_validation_enabled;
/**
* Whether to execute WC validation of the checkout form.
*
* @var bool
*/
protected $early_validation_enabled;
/**
* The logger.
*
@ -187,6 +197,7 @@ class SmartButton implements SmartButtonInterface {
* @param string $currency 3-letter currency code of the shop.
* @param array $all_funding_sources All existing funding sources.
* @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 LoggerInterface $logger The logger.
*/
public function __construct(
@ -206,6 +217,7 @@ class SmartButton implements SmartButtonInterface {
string $currency,
array $all_funding_sources,
bool $basic_checkout_validation_enabled,
bool $early_validation_enabled,
LoggerInterface $logger
) {
@ -225,6 +237,7 @@ class SmartButton implements SmartButtonInterface {
$this->currency = $currency;
$this->all_funding_sources = $all_funding_sources;
$this->basic_checkout_validation_enabled = $basic_checkout_validation_enabled;
$this->early_validation_enabled = $early_validation_enabled;
$this->logger = $logger;
}
@ -761,22 +774,30 @@ class SmartButton implements SmartButtonInterface {
'redirect' => wc_get_checkout_url(),
'context' => $this->context(),
'ajax' => array(
'change_cart' => array(
'change_cart' => array(
'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ),
),
'create_order' => array(
'create_order' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreateOrderEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( CreateOrderEndpoint::nonce() ),
),
'approve_order' => array(
'approve_order' => array(
'endpoint' => \WC_AJAX::get_endpoint( ApproveOrderEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ApproveOrderEndpoint::nonce() ),
),
'vault_paypal' => array(
'vault_paypal' => array(
'endpoint' => \WC_AJAX::get_endpoint( StartPayPalVaultingEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( StartPayPalVaultingEndpoint::nonce() ),
),
'save_checkout_form' => array(
'endpoint' => \WC_AJAX::get_endpoint( SaveCheckoutFormEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( SaveCheckoutFormEndpoint::nonce() ),
),
'validate_checkout' => array(
'endpoint' => \WC_AJAX::get_endpoint( ValidateCheckoutEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ValidateCheckoutEndpoint::nonce() ),
),
),
'enforce_vault' => $this->has_subscriptions(),
'can_save_vault_token' => $this->can_save_vault_token(),
@ -866,6 +887,7 @@ class SmartButton implements SmartButtonInterface {
'single_product_buttons_enabled' => $this->settings_status->is_smart_button_enabled_for_location( 'product' ),
'mini_cart_buttons_enabled' => $this->settings_status->is_smart_button_enabled_for_location( 'mini-cart' ),
'basic_checkout_validation_enabled' => $this->basic_checkout_validation_enabled,
'early_checkout_validation_enabled' => $this->early_validation_enabled,
);
if ( $this->style_for_context( 'layout', 'mini-cart' ) !== 'horizontal' ) {
@ -1062,48 +1084,6 @@ class SmartButton implements SmartButtonInterface {
}
}
/**
* The current context.
*
* @return string
*/
private function context(): string {
$context = 'mini-cart';
if ( is_product() || wc_post_content_has_shortcode( 'product_page' ) ) {
$context = 'product';
}
if ( is_cart() ) {
$context = 'cart';
}
if ( is_checkout() && ! $this->is_paypal_continuation() ) {
$context = 'checkout';
}
if ( is_checkout_pay_page() ) {
$context = 'pay-now';
}
return $context;
}
/**
* Checks if PayPal payment was already initiated (on the product or cart pages).
*
* @return bool
*/
private function is_paypal_continuation(): bool {
$order = $this->session_handler->order();
if ( ! $order ) {
return false;
}
$source = $order->payment_source();
if ( $source && $source->card() ) {
return false; // Ignore for DCC.
}
if ( 'card' === $this->session_handler->funding_source() ) {
return false; // Ignore for card buttons.
}
return true;
}
/**
* Whether DCC is enabled or not.
*

View file

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
@ -95,7 +97,7 @@ class ButtonModule implements ModuleInterface {
*
* @param ContainerInterface $container The Container.
*/
private function register_ajax_endpoints( ContainerInterface $container ) {
private function register_ajax_endpoints( ContainerInterface $container ): void {
add_action(
'wc_ajax_' . DataClientIdEndpoint::ENDPOINT,
static function () use ( $container ) {
@ -156,6 +158,25 @@ class ButtonModule implements ModuleInterface {
$endpoint->handle_request();
}
);
add_action(
'wc_ajax_' . SaveCheckoutFormEndpoint::ENDPOINT,
static function () use ( $container ) {
$endpoint = $container->get( 'button.endpoint.save-checkout-form' );
assert( $endpoint instanceof SaveCheckoutFormEndpoint );
$endpoint->handle_request();
}
);
add_action(
'wc_ajax_' . ValidateCheckoutEndpoint::ENDPOINT,
static function () use ( $container ) {
$endpoint = $container->get( 'button.endpoint.validate-checkout' );
assert( $endpoint instanceof ValidateCheckoutEndpoint );
$endpoint->handle_request();
}
);
}
/**

View file

@ -0,0 +1,94 @@
<?php
/**
* Saves the form data to the WC customer and session.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver;
/**
* Class SaveCheckoutFormEndpoint
*/
class SaveCheckoutFormEndpoint implements EndpointInterface {
const ENDPOINT = 'ppc-save-checkout-form';
/**
* The Request Data Helper.
*
* @var RequestData
*/
private $request_data;
/**
* The checkout form saver.
*
* @var CheckoutFormSaver
*/
private $checkout_form_saver;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* SaveCheckoutFormEndpoint constructor.
*
* @param RequestData $request_data The Request Data Helper.
* @param CheckoutFormSaver $checkout_form_saver The checkout form saver.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
RequestData $request_data,
CheckoutFormSaver $checkout_form_saver,
LoggerInterface $logger
) {
$this->request_data = $request_data;
$this->checkout_form_saver = $checkout_form_saver;
$this->logger = $logger;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
*/
public function handle_request(): bool {
try {
$data = $this->request_data->read_request( $this->nonce() );
$this->checkout_form_saver->save( $data['form'] );
wp_send_json_success();
return true;
} catch ( Exception $error ) {
$this->logger->error( 'Checkout form saving failed: ' . $error->getMessage() );
wp_send_json_error(
array(
'message' => $error->getMessage(),
)
);
return false;
}
}
}

View file

@ -0,0 +1,107 @@
<?php
/**
* The endpoint for validating the checkout form.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Psr\Log\LoggerInterface;
use Throwable;
use WooCommerce\PayPalCommerce\Button\Exception\ValidationException;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
/**
* Class ValidateCheckoutEndpoint.
*/
class ValidateCheckoutEndpoint implements EndpointInterface {
const ENDPOINT = 'ppc-validate-checkout';
/**
* The Request Data Helper.
*
* @var RequestData
*/
private $request_data;
/**
* The CheckoutFormValidator.
*
* @var CheckoutFormValidator
*/
private $checkout_form_validator;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* ValidateCheckoutEndpoint constructor.
*
* @param RequestData $request_data The Request Data Helper.
* @param CheckoutFormValidator $checkout_form_validator The CheckoutFormValidator.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
RequestData $request_data,
CheckoutFormValidator $checkout_form_validator,
LoggerInterface $logger
) {
$this->request_data = $request_data;
$this->checkout_form_validator = $checkout_form_validator;
$this->logger = $logger;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
*/
public function handle_request(): bool {
try {
$data = $this->request_data->read_request( $this->nonce() );
$form_fields = $data['form'];
$this->checkout_form_validator->validate( $form_fields );
wp_send_json_success();
return true;
} catch ( ValidationException $exception ) {
wp_send_json_error(
array(
'message' => $exception->getMessage(),
'errors' => $exception->errors(),
)
);
return false;
} catch ( Throwable $error ) {
$this->logger->error( "Form validation execution failed. {$error->getMessage()} {$error->getFile()}:{$error->getLine()}" );
wp_send_json_error(
array(
'message' => $error->getMessage(),
)
);
return false;
}
}
}

View file

@ -0,0 +1,32 @@
<?php
/**
* Saves the form data to the WC customer and session.
*
* @package WooCommerce\PayPalCommerce\Button\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Helper;
use WC_Checkout;
/**
* Class CheckoutFormSaver
*/
class CheckoutFormSaver extends WC_Checkout {
/**
* Saves the form data to the WC customer and session.
*
* @param array $data The form data.
* @return void
*/
public function save( array $data ) {
foreach ( $data as $key => $value ) {
$_POST[ $key ] = $value;
}
$data = $this->get_posted_data();
$this->update_session( $data );
}
}

View file

@ -0,0 +1,62 @@
<?php
/**
* Helper trait for context.
*
* @package WooCommerce\PayPalCommerce\Button\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Helper;
trait ContextTrait {
/**
* The current context.
*
* @return string
*/
protected function context(): string {
$context = 'mini-cart';
if ( is_product() || wc_post_content_has_shortcode( 'product_page' ) ) {
$context = 'product';
}
if ( is_cart() ) {
$context = 'cart';
}
if ( is_checkout() && ! $this->is_paypal_continuation() ) {
$context = 'checkout';
}
if ( is_checkout_pay_page() ) {
$context = 'pay-now';
}
return $context;
}
/**
* Checks if PayPal payment was already initiated (on the product or cart pages).
*
* @return bool
*/
private function is_paypal_continuation(): bool {
$order = $this->session_handler->order();
if ( ! $order ) {
return false;
}
$source = $order->payment_source();
if ( $source && $source->card() ) {
return false; // Ignore for DCC.
}
if ( 'card' === $this->session_handler->funding_source() ) {
return false; // Ignore for card buttons.
}
return true;
}
}

View file

@ -27,20 +27,91 @@ class CheckoutFormValidator extends WC_Checkout {
public function validate( array $data ) {
$errors = new WP_Error();
// Some plugins check their fields using $_POST,
// Some plugins check their fields using $_POST or $_REQUEST,
// also WC terms checkbox https://github.com/woocommerce/woocommerce/issues/35328 .
foreach ( $data as $key => $value ) {
$_POST[ $key ] = $value;
$_POST[ $key ] = $value;
$_REQUEST[ $key ] = $value;
}
// And we must call get_posted_data because it handles the shipping address.
$data = $this->get_posted_data();
// It throws some notices when checking fields etc., also from other plugins via hooks.
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
@$this->validate_checkout( $data, $errors );
// Looks like without this WC()->shipping->get_packages() is empty which is used by some plugins.
WC()->cart->calculate_shipping();
if ( $errors->has_errors() ) {
throw new ValidationException( $errors->get_error_messages() );
// Some plugins/filters check is_checkout().
$is_checkout = function () {
return true;
};
add_filter( 'woocommerce_is_checkout', $is_checkout );
try {
// And we must call get_posted_data because it handles the shipping address.
$data = $this->get_posted_data();
// It throws some notices when checking fields etc., also from other plugins via hooks.
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
@$this->validate_checkout( $data, $errors );
} finally {
remove_filter( 'woocommerce_is_checkout', $is_checkout );
}
if (
apply_filters( 'woocommerce_paypal_payments_early_wc_checkout_account_creation_validation_enabled', true ) &&
! is_user_logged_in() && ( $this->is_registration_required() || ! empty( $data['createaccount'] ) )
) {
$username = ! empty( $data['account_username'] ) ? $data['account_username'] : '';
$email = $data['billing_email'] ?? '';
if ( email_exists( $email ) ) {
$errors->add(
'registration-error-email-exists',
apply_filters(
'woocommerce_registration_error_email_exists',
// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch
__( 'An account is already registered with your email address. <a href="#" class="showlogin">Please log in.</a>', 'woocommerce' ),
$email
)
);
}
if ( $username ) { // Empty username is already checked in validate_checkout, and it can be generated.
$username = sanitize_user( $username );
if ( empty( $username ) || ! validate_username( $username ) ) {
$errors->add(
'registration-error-invalid-username',
// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch
__( 'Please enter a valid account username.', 'woocommerce' )
);
}
if ( username_exists( $username ) ) {
$errors->add(
'registration-error-username-exists',
// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch
__( 'An account is already registered with that username. Please choose another.', 'woocommerce' )
);
}
}
}
// Some plugins call wc_add_notice directly.
// We should retrieve such notices, and also clear them to avoid duplicates later.
// TODO: Normally WC converts the messages from validate_checkout into notices,
// maybe we should do the same for consistency, but it requires lots of changes in the way we handle/output errors.
$messages = array_merge(
$errors->get_error_messages(),
array_map(
function ( array $notice ): string {
return $notice['notice'];
},
wc_get_notices( 'error' )
)
);
if ( wc_notice_count( 'error' ) > 0 ) {
wc_clear_notices();
}
if ( $messages ) {
throw new ValidationException( $messages );
}
}
}

View file

@ -155,7 +155,11 @@ class CompatModule implements ModuleInterface {
$provider = $shipment->get_shipping_provider();
if ( ! empty( $provider ) && $provider !== 'none' ) {
$tracking_data['carrier'] = 'DHL_DEUTSCHE_POST';
/**
* The filter allowing to change the default Germanized carrier for order tracking,
* such as DHL_DEUTSCHE_POST, DPD_DE, ...
*/
$tracking_data['carrier'] = (string) apply_filters( 'woocommerce_paypal_payments_default_gzd_carrier', 'DHL_DEUTSCHE_POST', $provider );
}
try {

View file

@ -142,7 +142,7 @@ class SettingsImporter {
$value = array_values(
array_intersect(
array_map( 'strtolower', is_array( $option_value ) ? $option_value : array() ),
array( 'card', 'sepa', 'bancontact', 'blik', 'eps', 'giropay', 'ideal', 'mercadopago', 'mybank', 'p24', 'sofort', 'venmo' )
array( 'card', 'sepa', 'bancontact', 'blik', 'eps', 'giropay', 'ideal', 'mercadopago', 'mybank', 'p24', 'sofort', 'venmo', 'trustly' )
)
);

View file

@ -183,18 +183,19 @@ class SubscriptionsHandler {
return true;
}
if ( function_exists( 'get_current_screen' ) && get_current_screen() ) {
// Are we on the WC > Subscriptions screen?
if ( 'edit-shop_subscription' === get_current_screen()->id ) {
return true;
}
// Are we on the WC > Subscriptions screen?
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$post_type = wc_clean( wp_unslash( $_GET['post_type'] ?? $_POST['post_type'] ?? '' ) );
if ( $post_type === 'shop_subscription' ) {
return true;
}
// Are we editing an order or subscription tied to PPEC?
if ( in_array( get_current_screen()->id, array( 'shop_subscription', 'shop_order' ), true ) ) {
$order = wc_get_order( $GLOBALS['post']->ID );
return ( $order && PPECHelper::PPEC_GATEWAY_ID === $order->get_payment_method() );
}
// Are we editing an order or subscription tied to PPEC?
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$order_id = wc_clean( wp_unslash( $_GET['post'] ?? $_POST['post_ID'] ?? '' ) );
if ( $order_id ) {
$order = wc_get_order( $order_id );
return ( $order && PPECHelper::PPEC_GATEWAY_ID === $order->get_payment_method() );
}
return false;

View file

@ -396,6 +396,10 @@ document.addEventListener(
{
value:'authorize',
selector:'#field-capture_for_virtual_only'
},
{
value:'authorize',
selector:'#field-capture_on_status_change'
}
]
);

View file

@ -114,10 +114,13 @@ import {setVisibleByClass, isVisible} from "../../../ppcp-button/resources/js/mo
'integration-date': PayPalCommerceGatewaySettings.integration_date,
'components': ['buttons', 'funding-eligibility', 'messages'],
'enable-funding': ['venmo', 'paylater'],
'buyer-country': PayPalCommerceGatewaySettings.country,
};
if(payLaterButtonPreview?.length) {
if (PayPalCommerceGatewaySettings.environment === 'sandbox') {
settings['buyer-country'] = PayPalCommerceGatewaySettings.country;
}
if (payLaterButtonPreview?.length) {
disabledSources = Object.keys(PayPalCommerceGatewaySettings.all_funding_sources);
}

View file

@ -36,6 +36,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\GatewayRepository;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXO;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXOEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXOGateway;
@ -538,6 +539,23 @@ return array(
'requirements' => array(),
'gateway' => 'paypal',
),
'capture_on_status_change' => array(
'title' => __( 'Capture On Status Change', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'default' => false,
'desc_tip' => true,
'description' => __(
'The transaction will be captured automatically when the order status changes to Processing or Completed.',
'woocommerce-paypal-payments'
),
'label' => __( 'Capture On Status Change', 'woocommerce-paypal-payments' ),
'screens' => array(
State::STATE_START,
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'paypal',
),
'capture_for_virtual_only' => array(
'title' => __( 'Capture Virtual-Only Orders ', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
@ -593,7 +611,7 @@ return array(
'type' => 'select',
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'gold',
'default' => ApplicationContext::LANDING_PAGE_LOGIN,
'desc_tip' => true,
'description' => __(
'Type of PayPal page to display.',
@ -862,6 +880,7 @@ return array(
'p24' => _x( 'Przelewy24', 'Name of payment method', 'woocommerce-paypal-payments' ),
'sofort' => _x( 'Sofort', 'Name of payment method', 'woocommerce-paypal-payments' ),
'venmo' => _x( 'Venmo', 'Name of payment method', 'woocommerce-paypal-payments' ),
'trustly' => _x( 'Trustly', 'Name of payment method', 'woocommerce-paypal-payments' ),
);
},
@ -1157,20 +1176,49 @@ return array(
return false;
},
'wcgateway.settings.tracking-label' => static function ( ContainerInterface $container ): string {
$tracking_label = sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__( 'Enable shipment tracking information to be sent to PayPal for seller protection features. Required when %1$sPay upon Invoice%2$s is used.', 'woocommerce-paypal-payments' ),
'<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#pay-upon-invoice-PUI" target="_blank">',
'wcgateway.settings.fraudnet-label' => static function ( ContainerInterface $container ): string {
$label = sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__( 'Manage online risk with %1$sFraudNet%2$s.', 'woocommerce-paypal-payments' ),
'<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#fraudnet" target="_blank">',
'</a>'
);
if ( 'DE' === $container->get( 'api.shop.country' ) ) {
$label .= '<br/>' . sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__( 'Required when %1$sPay upon Invoice%2$s is used.', 'woocommerce-paypal-payments' ),
'<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#pay-upon-invoice-PUI" target="_blank">',
'</a>'
);
}
return $label;
},
'wcgateway.settings.tracking-label' => static function ( ContainerInterface $container ): string {
$tracking_label = sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__( 'Enable %1$sshipment tracking information%2$s to be sent to PayPal for seller protection features.', 'woocommerce-paypal-payments' ),
'<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#shipment-tracking" target="_blank">',
'</a>'
);
if ( 'DE' === $container->get( 'api.shop.country' ) ) {
$tracking_label .= '<br/>' . sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__( 'Required when %1$sPay upon Invoice%2$s is used.', 'woocommerce-paypal-payments' ),
'<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#pay-upon-invoice-PUI" target="_blank">',
'</a>'
);
}
$is_tracking_available = $container->get( 'order-tracking.is-tracking-available' );
if ( $is_tracking_available ) {
return $tracking_label;
}
$tracking_label .= sprintf(
$tracking_label .= '<br/>' . sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__(
' To use tracking features, you must %1$senable tracking on your account%2$s.',
@ -1318,52 +1366,10 @@ return array(
OXXOGateway::ID,
);
},
'wcgateway.enabled-ppcp-gateways' => static function ( ContainerInterface $container ): array {
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
$ppcp_gateways = $container->get( 'wcgateway.ppcp-gateways' );
$enabled_ppcp_gateways = array();
foreach ( $ppcp_gateways as $gateway ) {
if ( ! isset( $available_gateways[ $gateway ] ) ) {
continue;
}
$enabled_ppcp_gateways[] = $gateway;
}
return $enabled_ppcp_gateways;
},
'wcgateway.is-paypal-continuation' => static function ( ContainerInterface $container ): bool {
$session_handler = $container->get( 'session.handler' );
assert( $session_handler instanceof SessionHandler );
$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;
},
'wcgateway.current-context' => static function ( ContainerInterface $container ): string {
$context = 'mini-cart';
if ( is_product() || wc_post_content_has_shortcode( 'product_page' ) ) {
$context = 'product';
}
if ( is_cart() ) {
$context = 'cart';
}
if ( is_checkout() && ! $container->get( 'wcgateway.is-paypal-continuation' ) ) {
$context = 'checkout';
}
if ( is_checkout_pay_page() ) {
$context = 'pay-now';
}
return $context;
'wcgateway.gateway-repository' => static function ( ContainerInterface $container ): GatewayRepository {
return new GatewayRepository(
$container->get( 'wcgateway.ppcp-gateways' )
);
},
'wcgateway.is-fraudnet-enabled' => static function ( ContainerInterface $container ): bool {
$settings = $container->get( 'wcgateway.settings' );
@ -1378,8 +1384,8 @@ return array(
$container->get( 'wcgateway.fraudnet' ),
$container->get( 'onboarding.environment' ),
$container->get( 'wcgateway.settings' ),
$container->get( 'wcgateway.enabled-ppcp-gateways' ),
$container->get( 'wcgateway.current-context' ),
$container->get( 'wcgateway.gateway-repository' ),
$container->get( 'session.handler' ),
$container->get( 'wcgateway.is-fraudnet-enabled' )
);
},

View file

@ -9,8 +9,12 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Assets;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNet;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\GatewayRepository;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
@ -20,6 +24,8 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
*/
class FraudNetAssets {
use ContextTrait;
/**
* The URL of this module.
*
@ -58,16 +64,23 @@ class FraudNetAssets {
/**
* The list of enabled PayPal gateways.
*
* @var string[]
* @var string[]|null
*/
protected $enabled_ppcp_gateways;
/**
* The current context.
* The GatewayRepository.
*
* @var string
* @var GatewayRepository
*/
protected $context;
protected $gateway_repository;
/**
* The session handler
*
* @var SessionHandler
*/
protected $session_handler;
/**
* True if FraudNet support is enabled in settings, otherwise false.
@ -79,14 +92,14 @@ class FraudNetAssets {
/**
* Assets constructor.
*
* @param string $module_url The url of this module.
* @param string $version The assets version.
* @param FraudNet $fraud_net The FraudNet entity.
* @param Environment $environment The environment.
* @param Settings $settings The Settings.
* @param string[] $enabled_ppcp_gateways The list of enabled PayPal gateways.
* @param string $context The current context.
* @param bool $is_fraudnet_enabled true if FraudNet support is enabled in settings, otherwise false.
* @param string $module_url The url of this module.
* @param string $version The assets version.
* @param FraudNet $fraud_net The FraudNet entity.
* @param Environment $environment The environment.
* @param Settings $settings The Settings.
* @param GatewayRepository $gateway_repository The GatewayRepository.
* @param SessionHandler $session_handler The session handler.
* @param bool $is_fraudnet_enabled true if FraudNet support is enabled in settings, otherwise false.
*/
public function __construct(
string $module_url,
@ -94,18 +107,18 @@ class FraudNetAssets {
FraudNet $fraud_net,
Environment $environment,
Settings $settings,
array $enabled_ppcp_gateways,
string $context,
GatewayRepository $gateway_repository,
SessionHandler $session_handler,
bool $is_fraudnet_enabled
) {
$this->module_url = $module_url;
$this->version = $version;
$this->fraud_net = $fraud_net;
$this->environment = $environment;
$this->settings = $settings;
$this->enabled_ppcp_gateways = $enabled_ppcp_gateways;
$this->context = $context;
$this->is_fraudnet_enabled = $is_fraudnet_enabled;
$this->module_url = $module_url;
$this->version = $version;
$this->fraud_net = $fraud_net;
$this->environment = $environment;
$this->settings = $settings;
$this->gateway_repository = $gateway_repository;
$this->session_handler = $session_handler;
$this->is_fraudnet_enabled = $is_fraudnet_enabled;
}
/**
@ -144,19 +157,18 @@ class FraudNetAssets {
* @return bool true if FraudNet script should be loaded, otherwise false.
*/
protected function should_load_fraudnet_script(): bool {
if ( empty( $this->enabled_ppcp_gateways ) ) {
if ( empty( $this->enabled_ppcp_gateways() ) ) {
return false;
}
$is_pui_gateway_enabled = in_array( PayUponInvoiceGateway::ID, $this->enabled_ppcp_gateways, true );
$is_only_standard_gateway_enabled = $this->enabled_ppcp_gateways === array( PayPalGateway::ID );
$is_pui_gateway_enabled = in_array( PayUponInvoiceGateway::ID, $this->enabled_ppcp_gateways(), true );
$is_only_standard_gateway_enabled = $this->enabled_ppcp_gateways() === array( PayPalGateway::ID );
if ( $this->context !== 'checkout' || $is_only_standard_gateway_enabled ) {
if ( $this->context() !== 'checkout' || $is_only_standard_gateway_enabled ) {
return $this->is_fraudnet_enabled && $this->are_buttons_enabled_for_context();
}
return $is_pui_gateway_enabled ? true : $this->is_fraudnet_enabled;
}
/**
@ -165,22 +177,35 @@ class FraudNetAssets {
* @return bool true if enabled, otherwise false.
*/
protected function are_buttons_enabled_for_context() : bool {
if ( ! in_array( PayPalGateway::ID, $this->enabled_ppcp_gateways, true ) ) {
if ( ! in_array( PayPalGateway::ID, $this->enabled_ppcp_gateways(), true ) ) {
return false;
}
try {
$button_locations = $this->settings->get( 'smart_button_locations' );
} catch ( NotFoundException $exception ) {
return false;
}
$location_prefix = $this->context === 'checkout' ? '' : "{$this->context}_";
$setting_name = "button_{$location_prefix}enabled";
$buttons_enabled_for_context = $this->settings->has( $setting_name ) && $this->settings->get( $setting_name );
if ( $this->context === 'product' ) {
return $buttons_enabled_for_context || $this->settings->has( 'mini-cart' ) && $this->settings->get( 'mini-cart' );
}
if ( $this->context === 'pay-now' ) {
if ( $this->context() === 'pay-now' ) {
return true;
}
return $buttons_enabled_for_context;
if ( $this->context() === 'product' ) {
return in_array( 'product', $button_locations, true ) || in_array( 'mini-cart', $button_locations, true );
}
return in_array( $this->context(), $button_locations, true );
}
/**
* Returns IDs of the currently enabled PPCP gateways.
*
* @return string[]
*/
protected function enabled_ppcp_gateways(): array {
if ( null === $this->enabled_ppcp_gateways ) {
$this->enabled_ppcp_gateways = $this->gateway_repository->get_enabled_ppcp_gateway_ids();
}
return $this->enabled_ppcp_gateways;
}
}

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Assets;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
/**
@ -58,6 +59,13 @@ class SettingsPageAssets {
*/
private $country;
/**
* The environment object.
*
* @var Environment
*/
private $environment;
/**
* Whether Pay Later button is enabled either for checkout, cart or product page.
*
@ -88,6 +96,7 @@ class SettingsPageAssets {
* @param string $client_id The PayPal SDK client ID.
* @param string $currency 3-letter currency code of the shop.
* @param string $country 2-letter country code of the shop.
* @param Environment $environment The environment object.
* @param bool $is_pay_later_button_enabled Whether Pay Later button is enabled either for checkout, cart or product page.
* @param array $disabled_sources The list of disabled funding sources.
* @param array $all_funding_sources The list of all existing funding sources.
@ -99,6 +108,7 @@ class SettingsPageAssets {
string $client_id,
string $currency,
string $country,
Environment $environment,
bool $is_pay_later_button_enabled,
array $disabled_sources,
array $all_funding_sources
@ -109,6 +119,7 @@ class SettingsPageAssets {
$this->client_id = $client_id;
$this->currency = $currency;
$this->country = $country;
$this->environment = $environment;
$this->is_pay_later_button_enabled = $is_pay_later_button_enabled;
$this->disabled_sources = $disabled_sources;
$this->all_funding_sources = $all_funding_sources;
@ -191,6 +202,7 @@ class SettingsPageAssets {
'client_id' => $this->client_id,
'currency' => $this->currency,
'country' => $this->country,
'environment' => $this->environment->current_environment(),
'integration_date' => PAYPAL_INTEGRATION_DATE,
'is_pay_later_button_enabled' => $this->is_pay_later_button_enabled,
'disabled_sources' => $this->disabled_sources,

View file

@ -0,0 +1,47 @@
<?php
/**
* Operations with the WC gateways.
*
* @package WooCommerce\PayPalCommerce\WcGateway\Gateway
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Gateway;
/**
* Class GatewayRepository
*/
class GatewayRepository {
/**
* IDs of our gateways.
*
* @var string[]
*/
protected $ppcp_gateway_ids;
/**
* GatewayRepository constructor.
*
* @param string[] $ppcp_gateway_ids IDs of our gateways.
*/
public function __construct( array $ppcp_gateway_ids ) {
$this->ppcp_gateway_ids = $ppcp_gateway_ids;
}
/**
* Returns IDs of the currently enabled PPCP gateways.
*
* @return array
*/
public function get_enabled_ppcp_gateway_ids(): array {
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
return array_filter(
$this->ppcp_gateway_ids,
function ( string $id ) use ( $available_gateways ): bool {
return isset( $available_gateways[ $id ] );
}
);
}
}

View file

@ -422,7 +422,7 @@ class PayUponInvoice {
*
* @psalm-suppress MissingClosureParamType
*/
function ( $methods ): array {
function ( $methods ) {
if ( ! is_array( $methods ) || State::STATE_ONBOARDED !== $this->state->current_state() ) {
return $methods;
}

View file

@ -398,12 +398,7 @@ return function ( ContainerInterface $container, array $fields ): array {
'title' => __( 'FraudNet', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'desc_tip' => true,
'label' => sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__( 'Manage online risk with %1$sFraudNet%2$s.', 'woocommerce-paypal-payments' ),
'<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#FraudNet" target="_blank">',
'</a>'
),
'label' => $container->get( 'wcgateway.settings.fraudnet-label' ),
'description' => __( 'FraudNet is a JavaScript library developed by PayPal and embedded into a merchants web page to collect browser-based data to help reduce fraud.', 'woocommerce-paypal-payments' ),
'default' => false,
'screens' => array(

View file

@ -49,7 +49,7 @@ return function ( ContainerInterface $container, array $fields ): array {
'description' => sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__(
'Customize the appearance of the PayPal smart buttons on the
'Customize the appearance of the PayPal smart buttons on the
%1$sCheckout page%5$s, %2$sSingle Product Page%5$s, %3$sCart page%5$s or on %4$sMini Cart%5$s.',
'woocommerce-paypal-payments'
),
@ -163,6 +163,7 @@ return function ( ContainerInterface $container, array $fields ): array {
'blue' => __( 'Blue', 'woocommerce-paypal-payments' ),
'silver' => __( 'Silver', 'woocommerce-paypal-payments' ),
'black' => __( 'Black', 'woocommerce-paypal-payments' ),
'white' => __( 'White', 'woocommerce-paypal-payments' ),
),
'screens' => array(
State::STATE_START,
@ -308,6 +309,7 @@ return function ( ContainerInterface $container, array $fields ): array {
'blue' => __( 'Blue', 'woocommerce-paypal-payments' ),
'silver' => __( 'Silver', 'woocommerce-paypal-payments' ),
'black' => __( 'Black', 'woocommerce-paypal-payments' ),
'white' => __( 'White', 'woocommerce-paypal-payments' ),
),
'screens' => array(
State::STATE_START,
@ -447,6 +449,7 @@ return function ( ContainerInterface $container, array $fields ): array {
'blue' => __( 'Blue', 'woocommerce-paypal-payments' ),
'silver' => __( 'Silver', 'woocommerce-paypal-payments' ),
'black' => __( 'Black', 'woocommerce-paypal-payments' ),
'white' => __( 'White', 'woocommerce-paypal-payments' ),
),
'screens' => array(
State::STATE_START,
@ -586,6 +589,7 @@ return function ( ContainerInterface $container, array $fields ): array {
'blue' => __( 'Blue', 'woocommerce-paypal-payments' ),
'silver' => __( 'Silver', 'woocommerce-paypal-payments' ),
'black' => __( 'Black', 'woocommerce-paypal-payments' ),
'white' => __( 'White', 'woocommerce-paypal-payments' ),
),
'screens' => array(
State::STATE_START,
@ -725,6 +729,7 @@ return function ( ContainerInterface $container, array $fields ): array {
'blue' => __( 'Blue', 'woocommerce-paypal-payments' ),
'silver' => __( 'Silver', 'woocommerce-paypal-payments' ),
'black' => __( 'Black', 'woocommerce-paypal-payments' ),
'white' => __( 'White', 'woocommerce-paypal-payments' ),
),
'screens' => array(
State::STATE_START,

View file

@ -193,10 +193,7 @@ class SettingsListener {
*/
do_action( 'woocommerce_paypal_payments_onboarding_before_redirect' );
/**
* The URL opened at the end of onboarding after saving the merchant ID/email.
*/
$redirect_url = apply_filters( 'woocommerce_paypal_payments_onboarding_redirect_url', admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway&ppcp-tab=' . Settings::CONNECTION_TAB_ID ) );
$redirect_url = $this->get_onboarding_redirect_url();
if ( ! $this->settings->has( 'client_id' ) || ! $this->settings->get( 'client_id' ) ) {
$redirect_url = add_query_arg( 'ppcp-onboarding-error', '1', $redirect_url );
}
@ -347,9 +344,17 @@ class SettingsListener {
$this->dcc_status_cache->delete( DCCProductStatus::DCC_STATUS_CACHE_KEY );
}
$redirect_url = false;
if ( self::CREDENTIALS_ADDED === $credentials_change_status ) {
$redirect_url = $this->get_onboarding_redirect_url();
}
if ( isset( $_GET['ppcp-onboarding-error'] ) ) {
$url = remove_query_arg( 'ppcp-onboarding-error' );
wp_safe_redirect( $url, 302 );
$redirect_url = remove_query_arg( 'ppcp-onboarding-error', $redirect_url );
}
if ( $redirect_url ) {
wp_safe_redirect( $redirect_url, 302 );
exit;
}
@ -357,6 +362,18 @@ class SettingsListener {
// phpcs:enable WordPress.Security.NonceVerification.Recommended
}
/**
* Returns the URL opened at the end of onboarding.
*
* @return string
*/
private function get_onboarding_redirect_url(): string {
/**
* The URL opened at the end of onboarding after saving the merchant ID/email.
*/
return apply_filters( 'woocommerce_paypal_payments_onboarding_redirect_url', admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway&ppcp-tab=' . Settings::CONNECTION_TAB_ID ) );
}
/**
* The actual used client credentials are stored in 'client_secret', 'client_id', 'merchant_id' and 'merchant_email'.
* This method populates those fields depending on the sandbox status.

View file

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway;
use Psr\Log\LoggerInterface;
use Throwable;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WC_Order;
@ -148,6 +150,12 @@ class WCGatewayModule implements ModuleInterface {
if ( ! $wc_order instanceof WC_Order ) {
return;
}
/**
* The filter can be used to remove the rows with PayPal fees in WC orders.
*/
if ( ! apply_filters( 'woocommerce_paypal_payments_show_fees_on_order_admin_page', true, $wc_order ) ) {
return;
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $fees_renderer->render( $wc_order );
@ -168,6 +176,7 @@ class WCGatewayModule implements ModuleInterface {
$c->get( 'button.client_id_for_admin' ),
$c->get( 'api.shop.currency' ),
$c->get( 'api.shop.country' ),
$c->get( 'onboarding.environment' ),
$settings_status->is_pay_later_button_enabled(),
$settings->has( 'disable_funding' ) ? $settings->get( 'disable_funding' ) : array(),
$c->get( 'wcgateway.all-funding-sources' )
@ -252,7 +261,7 @@ class WCGatewayModule implements ModuleInterface {
);
add_action(
'init',
'wp_loaded',
function () use ( $c ) {
if ( 'DE' === $c->get( 'api.shop.country' ) ) {
( $c->get( 'wcgateway.pay-upon-invoice' ) )->init();
@ -300,6 +309,57 @@ class WCGatewayModule implements ModuleInterface {
$endpoint->handle_request();
}
);
add_action(
'woocommerce_order_status_changed',
static function ( int $order_id, string $from, string $to ) use ( $c ) {
$wc_order = wc_get_order( $order_id );
if ( ! $wc_order instanceof WC_Order ) {
return;
}
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof ContainerInterface );
if ( ! $settings->has( 'capture_on_status_change' ) || ! $settings->get( 'capture_on_status_change' ) ) {
return;
}
$intent = strtoupper( (string) $wc_order->get_meta( PayPalGateway::INTENT_META_KEY ) );
$captured = wc_string_to_bool( $wc_order->get_meta( AuthorizedPaymentsProcessor::CAPTURED_META_KEY ) );
if ( $intent !== 'AUTHORIZE' || $captured ) {
return;
}
/**
* The filter returning the WC order statuses which trigger capturing of payment authorization.
*/
$capture_statuses = apply_filters( 'woocommerce_paypal_payments_auto_capture_statuses', array( 'processing', 'completed' ), $wc_order );
if ( ! in_array( $to, $capture_statuses, true ) ) {
return;
}
$authorized_payment_processor = $c->get( 'wcgateway.processor.authorized-payments' );
assert( $authorized_payment_processor instanceof AuthorizedPaymentsProcessor );
try {
if ( $authorized_payment_processor->capture_authorized_payment( $wc_order ) ) {
return;
}
} catch ( Throwable $error ) {
$logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
$logger->error( "Capture failed. {$error->getMessage()} {$error->getFile()}:{$error->getLine()}" );
}
$wc_order->update_status(
'failed',
__( 'Could not capture the payment.', 'woocommerce-paypal-payments' )
);
},
10,
3
);
}
/**

View file

@ -7,7 +7,6 @@
"author": "WooCommerce",
"scripts": {
"postinstall": "run-s install:modules:* && run-s build:modules",
"install:modules:ppcp-button": "cd modules/ppcp-button && yarn install",
"install:modules:ppcp-wc-gateway": "cd modules/ppcp-wc-gateway && yarn install",
"install:modules:ppcp-webhooks": "cd modules/ppcp-webhooks && yarn install",
@ -15,7 +14,6 @@
"install:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn install",
"install:modules:ppcp-compat": "cd modules/ppcp-compat && yarn install",
"install:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn install",
"build:modules:ppcp-button": "cd modules/ppcp-button && yarn run build",
"build:modules:ppcp-wc-gateway": "cd modules/ppcp-wc-gateway && yarn run build",
"build:modules:ppcp-webhooks": "cd modules/ppcp-webhooks && yarn run build",
@ -24,7 +22,6 @@
"build:modules:ppcp-compat": "cd modules/ppcp-compat && yarn run build",
"build:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn run build",
"build:modules": "run-p build:modules:*",
"watch:modules:ppcp-button": "cd modules/ppcp-button && yarn run watch",
"watch:modules:ppcp-wc-gateway": "cd modules/ppcp-wc-gateway && yarn run watch",
"watch:modules:ppcp-webhooks": "cd modules/ppcp-webhooks && yarn run watch",
@ -33,7 +30,6 @@
"watch:modules:ppcp-compat": "cd modules/ppcp-compat && yarn run watch",
"watch:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn run watch",
"watch:modules": "run-p watch:modules:*",
"ddev:setup": "ddev start && ddev orchestrate",
"ddev:start": "ddev start",
"ddev:stop": "ddev stop",
@ -44,7 +40,10 @@
"ddev:composer-update": "ddev composer update && ddev composer update --lock",
"ddev:unit-tests": "ddev exec phpunit",
"ddev:e2e-tests": "cp -n .env.e2e.example .env.e2e && ddev php tests/e2e/PHPUnit/setup.php && ddev exec phpunit -c tests/e2e/phpunit.xml.dist",
"ddev:test": "yarn run ddev:unit-tests && yarn run ddev:e2e-tests",
"ddev:pw-install": "ddev exec npx playwright install --with-deps",
"ddev:pw-tests-ci": "ddev exec npx playwright test --grep @ci",
"ddev:pw-tests-headed": "ddev exec npx playwright test --headed",
"ddev:test": "yarn run ddev:unit-tests && yarn run ddev:e2e-tests && yarn run ddev:pw-tests-ci",
"ddev:lint": "yarn ddev:phpcs && yarn ddev:psalm",
"ddev:phpcs": "ddev exec phpcs --parallel=8 -s",
"ddev:psalm": "ddev exec psalm --show-info=false --threads=8 --diff",
@ -52,19 +51,21 @@
"ddev:xdebug-on": "ddev xdebug",
"ddev:xdebug-off": "ddev xdebug",
"ddev:build-package": "ddev yarn build",
"prebuild": "rm -rf ./vendor && find . -name 'node_modules' -type d -maxdepth 3 -exec rm -rf {} +",
"build": "composer install --no-dev && yarn install && yarn run archive",
"prearchive": "rm -rf $npm_package_name.zip",
"archive": "zip -r $npm_package_name.zip . -x **.git/\\* **node_modules/\\*",
"postarchive": "yarn run archive:cleanup && rm -rf $npm_package_name && unzip $npm_package_name.zip -d $npm_package_name && rm $npm_package_name.zip && zip -r $npm_package_name.zip $npm_package_name && rm -rf $npm_package_name",
"archive:cleanup": "zip -d $npm_package_name.zip .env* .ddev/\\* \\*.idea/\\* .editorconfig tests/\\* .github/\\* .psalm/\\* wordpress_org_assets/\\* \\*.DS_Store \\*README.md \\*.gitattributes \\*.gitignore \\*composer.json \\*composer.lock patchwork.json phpunit.xml.dist .phpunit.result.cache phpcs.xml* psalm*.xml* \\*.babelrc \\*package.json \\*webpack.config.js \\*yarn.lock \\*.travis.yml"
"archive:cleanup": "zip -d $npm_package_name.zip .env* .ddev/\\* \\*.idea/\\* .editorconfig tests/\\* .github/\\* .psalm/\\* wordpress_org_assets/\\* \\*.DS_Store \\*README.md \\*.gitattributes \\*.gitignore \\*composer.json \\*composer.lock patchwork.json phpunit.xml.dist .phpunit.result.cache phpcs.xml* psalm*.xml* playwright.config.js \\*.babelrc \\*package.json \\*webpack.config.js \\*yarn.lock \\*.travis.yml\\"
},
"config": {
"wp_org_slug": "woocommerce-paypal-payments"
},
"dependencies": {
"dotenv": "^16.0.3",
"npm-run-all": "^4.1.5"
},
"devDependencies": {}
"devDependencies": {
"@playwright/test": "^1.31.1"
}
}

11
playwright.config.js Normal file
View file

@ -0,0 +1,11 @@
require('dotenv').config({ path: '.env.e2e' });
const config = {
testDir: './tests/playwright',
timeout: 30000,
use: {
baseURL: process.env.BASEURL,
},
};
module.exports = config;

View file

@ -4,7 +4,7 @@ Tags: woocommerce, paypal, payments, ecommerce, e-commerce, store, sales, sell,
Requires at least: 5.3
Tested up to: 6.1
Requires PHP: 7.2
Stable tag: 2.0.2
Stable tag: 2.0.3
License: GPLv2
License URI: http://www.gnu.org/licenses/gpl-2.0.html
@ -81,9 +81,32 @@ Follow the steps below to connect the plugin to your PayPal account:
== Changelog ==
= 2.0.2 =
= 2.0.3 =
* Fix - `DEVICE_DATA_NOT_AVAILABLE` error message when FraudNet is enabled #1177
* Fix - Redirect to connection tab after manual credentials input #1201
* Fix - Asking for address fields in checkout when not using them #1089
* Fix - Validate before free trial #1170
* Fix - Validate new user creation #1131
* Fix - After Updating to 2.0.2, Site Health reports REST API error #1195
* Fix - Do not send buyer-country for previews in live mode to avoid error #1186
* Fix - PPEC compatibility layer does not take over subscriptions #1193
* Enhancement - Save checkout form before free trial redirect #1135
* Enhancement - Add filter for controlling the ditching of items/breakdown #1146
* Enhancement - Add patch order data filter #1147
* Enhancement - Add filter for disabling fees on wc order admin pages #1153
* Enhancement - Use wp_loaded for fraudnet loading to avoid warnings #1172
* Enhancement - reCaptcha for WooCommerce support #1093
* Enhancement - Make it possible to hide missing funding resource Trustly #1155
* Enhancement - Add white color option #1167
* Enhancement - Checkout validation for other fields #861
* Enhancement - Mention PUI only for German shops and add line breaks #1169
* Enhancement - Add filter to fallback tracking_data['carrier'] #1188
* Enhancement - Error notices in checkout do not update / or are shown twice #1168
* Enhancement - capture authorized payment by changing order status (or programmatically) #587
= 2.0.2 - 2023-01-31 =
* Fix - Do not call PayPal get order by ID if it does not exist #1029
* Fix - Type check error conflict with German Market #1056
* Fix - Type check error conflict with German Market #1056
* Fix - Backend Storage for the PayPalRequestIdRepository does not scale #983
* Fix - Ensure WC()->payment_gateways is not null #1128
* Enhancement - Remove plugin data after uninstalling #1075
@ -93,7 +116,7 @@ Follow the steps below to connect the plugin to your PayPal account:
* Enhancement - When PUI is enabled FraudNet should be also enabled #1129
* Enhancement - Add PayPal-Request-Id if payment source exist #1132
= 2.0.1 =
= 2.0.1 - 2022-12-13 =
* Fix - Error while syncing tracking data to PayPal -> Sync GZD Tracking #1020
* Fix - Fix product price retrieval for variable product buttons #1000
* Fix - All tabs hidden on OXXO tab visit #1048
@ -106,12 +129,12 @@ Follow the steps below to connect the plugin to your PayPal account:
* Fix - Execute WC validation only for smart buttons in checkout #1074
* Enhancement - Param types removed in closure to avoid third-party issues #1046
= 2.0.0 =
* Add - Option to separate JSSDK APM payment buttons into individual WooCommerce gateways #671
* Add - OXXO APM (Alternative Payment Method) #684
= 2.0.0 - 2022-11-21 =
* Add - Option to separate JSSDK APM payment buttons into individual WooCommerce gateways #671
* Add - OXXO APM (Alternative Payment Method) #684
* Add - Pay Later tab #961
* Add - Button preview in settings #929
* Fix - Prevent Enter key submit for our non-standard button gateways #981
* Fix - Prevent Enter key submit for our non-standard button gateways #981
* Fix - Pay Upon Invoice - Stock correction on failed orders #964
* Fix - Check that WC session exists before using it #846
* Fix - Compatibility with One Page Checkout Extension #356
@ -120,11 +143,11 @@ Follow the steps below to connect the plugin to your PayPal account:
* Enhancement - PHP 8.1 warning: Constant FILTER_SANITIZE_STRING is deprecated #867
* Enhancement - Execute server-side WC validation when clicking button #942
* Enhancement - Update order with order note if payment failed after billing agreement canceled at PayPal #886
* Enhancement - Missing PUI refund functionality from WC order #937
* Enhancement - Missing PUI refund functionality from WC order #937
* Enhancement - Hide Pay upon Invoice tab if not available for merchant #978
* Enhancement - Handle synced sub without upfront payment like free trial #936
* Enhancement - Isolate container and modularity deps #972
**NOTE**: if you were extending/modifying the plugin using the modularity system,
**NOTE**: if you were extending/modifying the plugin using the modularity system,
you will need to add the `WooCommerce\PayPalCommerce\Vendor\` prefix for the container/modularity namespaces in your code,
that is `Psr\Container\ContainerInterface` becomes `WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface`,
and `Dhii\Modular\Module\ModuleInterface` becomes `WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface`.
@ -132,7 +155,7 @@ Follow the steps below to connect the plugin to your PayPal account:
* Enhancement - When Brand Name field is left empty, PUI purchase fails #916
* Enhancement - Improve styling when using separate buttons #996
= 1.9.5 =
= 1.9.5 - 2022-11-01 =
* Fix - Invalid tracking number in logs when adding tracking #903
* Fix - Tracking on Connection tab always enabled #900
* Fix - PUI payment instructions printed in the refund email #873
@ -148,7 +171,7 @@ Follow the steps below to connect the plugin to your PayPal account:
* Enhancement - Onboard with PUI Checkbox automatically set when shop is set to Germany #876
* Enhancement - Update all plugin strings #946
= 1.9.4 =
= 1.9.4 - 2022-10-11 =
* Add - Create new connection tab #801
* Add - Functionality to choose subscription failure behavior #728
* Fix - Virtual-only orders always move order status to completed #868
@ -160,7 +183,7 @@ Follow the steps below to connect the plugin to your PayPal account:
* Enhancement - PUI-relevant webhook not subscribed to #842
* Enhancement - Remove WC logo during onboarding #881
= 1.9.3 =
= 1.9.3 - 2022-08-31 =
* Add - Tracking API #792
* Fix - Improve compatibility with Siteground Optimizer plugin #797
* Fix - Transaction ID in order not updated when manually capturing authorized payment from WC #766
@ -175,7 +198,7 @@ Follow the steps below to connect the plugin to your PayPal account:
* Enhancement - PUI add option for a phone number field next to the Birth Date field #742
* Enhancement - PUI gateway availability on pay for order page with unsupported currency #744
= 1.9.2 =
= 1.9.2 - 2022-08-09 =
* Fix - Do not allow birth date older than 100 years for PUI. #743
* Fix - Store the customer id for vaulted payment method in usermeta to not lose vaulted methods after the invoice prefix change. #698
* Fix - Capture Virtual-Only Orders setting did not auto-capture subscription renewal payments. #626
@ -188,28 +211,28 @@ Follow the steps below to connect the plugin to your PayPal account:
* Enhancement - Improve Checkout Field Validation Message. #739
* Enhancement - Handle PAYER_ACTION_REQUIRED error. #759
= 1.9.1 =
= 1.9.1 - 2022-07-25 =
* Fix - ITEM_TOTAL_MISMATCH error when checking out with multiple products #721
* Fix - Unable to purchase a product with Credit card button in pay for order page #718
* Fix - Pay Later messaging only displayed when smart button is active on the same page #283
* Fix - Pay Later messaging displayed for out of stock variable products or with no variation selected #667
* Fix - Placeholders and card type detection not working for PayPal Card Processing (260) #685
* Fix - PUI gateway is displayed with unsupported store currency #711
* Fix - Wrong PUI locale sent causing error PAYMENT_SOURCE_CANNOT_BE_USED #741
* Fix - Wrong PUI locale sent causing error PAYMENT_SOURCE_CANNOT_BE_USED #741
* Enhancement - Missing PayPal fee in WC order details for PUI purchase #714
* Enhancement - Skip loading of PUI js file on all pages where PUI gateway is not displayed #723
* Enhancement - PUI feature capitalization not consistent #724
* Enhancement - PUI feature capitalization not consistent #724
= 1.9.0 =
= 1.9.0 - 2022-07-04 =
* Add - New Feature - Pay Upon Invoice (Germany only) #608
* Fix - Order not approved: payment via vaulted PayPal account fails #677
* Fix - Cant' refund : "ERROR Refund failed: No country given for address." #639
* Fix - Something went wrong error in Virtual products when using vaulted payment #673
* Fix - Something went wrong error in Virtual products when using vaulted payment #673
* Fix - PayPal smart buttons are not displayed for product variations when parent product is set to out of stock #669
* Fix - Pay Later messaging displayed for out of stock variable products or with no variation selected #667
* Fix - "Capture Virtual-Only Orders" intent sets virtual+downloadable product orders to "Processing" instead of "Completed" #665
* Fix - Free trial period causing incorrerct disable-funding parameters with DCC disabled #661
* Fix - Smart button not visible on single product page when product price is below 1 and decimal is "," #654
* Fix - Pay Later messaging displayed for out of stock variable products or with no variation selected #667
* Fix - "Capture Virtual-Only Orders" intent sets virtual+downloadable product orders to "Processing" instead of "Completed" #665
* Fix - Free trial period causing incorrerct disable-funding parameters with DCC disabled #661
* Fix - Smart button not visible on single product page when product price is below 1 and decimal is "," #654
* Fix - Checkout using an email address containing a + symbol results in a "[INVALID_REQUEST]" error #523
* Fix - Order details are sometimes empty in PayPal dashboard #689
* Fix - Incorrect TAX details on PayPal order overview #541
@ -217,7 +240,7 @@ Follow the steps below to connect the plugin to your PayPal account:
* Fix - DCC causes checkout continuation state after checkout validation error #695
* Enhancement - Improve checkout validation & order creation #513
= 1.8.1 =
= 1.8.1 - 2022-05-31 =
* Fix - Manual orders return an error for guest users when paying with PayPal Card Processing #530
* Fix - "No PayPal order found in the current WooCommerce session" error for guests on Pay for Order page #605
* Fix - Error on order discount by third-party plugins #548
@ -233,7 +256,7 @@ Follow the steps below to connect the plugin to your PayPal account:
* Enhancement - Add Fraud Processor Response as an order note #616
* Enhancement - Add the Paypal Fee to the Meta Custom Field for export purposes #591
= 1.8.0 =
= 1.8.0 - 2022-05-03 =
* Add - Allow free trial subscriptions #580
* Fix - The Card Processing does not appear as an available payment method when manually creating an order #562
* Fix - Express buttons & Pay Later visible on variable Subscription products /w disabled vaulting #281
@ -244,7 +267,7 @@ Follow the steps below to connect the plugin to your PayPal account:
* Enhancement - Onboarding errors improvements #558
* Enhancement - "Place order" button visible during gateway load time when DCC gateway is selected as the default #560
= 1.7.1 =
= 1.7.1 - 2022-04-06 =
* Fix - Hide smart buttons for free products and zero-sum carts #499
* Fix - Unprocessable Entity when paying with AMEX card #516
* Fix - Multisite path doubled in ajax URLs #528
@ -257,7 +280,7 @@ Follow the steps below to connect the plugin to your PayPal account:
* Enhancement - Improve payment token checking for subscriptions #525
* Enhancement - Add Spain and Italy to messaging #497
= 1.7.0 =
= 1.7.0 - 2022-02-28 =
* Fix - DCC orders randomly failing #503
* Fix - Multi-currency broke #481
* Fix - Address information from PayPal shortcut flow not loaded #451
@ -277,7 +300,7 @@ Follow the steps below to connect the plugin to your PayPal account:
* Enhancement - PayPal Payments doesn't set transaction fee metadata #467
* Enhancement - Show PayPal fee information in order #489
= 1.6.5 =
= 1.6.5 - 2022-01-31 =
* Fix - Allow guest users to purchase subscription products from checkout page #422
* Fix - Transaction ID missing for renewal order #424
* Fix - Save your credit card checkbox should be removed in pay for order for subscriptions #420
@ -291,7 +314,7 @@ Follow the steps below to connect the plugin to your PayPal account:
* Fix - Improve DCC error notice when not available #435
* Enhancement - Add View Logs link #416
= 1.6.4 =
= 1.6.4 - 2021-12-27 =
* Fix - Non admin user cannot save changes to the plugin settings #278
* Fix - Empty space in invoice prefix causes smart buttons to not load #390
* Fix - woocommerce_payment_complete action not triggered for payments completed via webhook #399
@ -300,7 +323,7 @@ Follow the steps below to connect the plugin to your PayPal account:
* Enhancement - Allow formatted text for the Description field #407
* Enhancement - Remove filter to prevent On-Hold emails #411
= 1.6.3 =
= 1.6.3 - 2021-12-14 =
* Fix - Payments fail when using custom order numbers #354
* Fix - Do not display saved payments on PayPal buttons if vault option is disabled #358
* Fix - Double "Place Order" button #362
@ -311,7 +334,7 @@ Follow the steps below to connect the plugin to your PayPal account:
* Fix - Error messages are not cleared even when checkout is re-attempted (DCC) #366
* Add - New additions for system report status #377
= 1.6.2 =
= 1.6.2 - 2021-11-22 =
* Fix - Order of WooCommerce checkout actions causing incompatibility with AvaTax address validation #335
* Fix - Can't checkout to certain countries with optional postcode #330
* Fix - Prevent subscription from being purchased when saving payment fails #308
@ -328,14 +351,14 @@ Follow the steps below to connect the plugin to your PayPal account:
* Fix - When paying for a subscription and vaulting fails, cart is cleared #367
* Fix - Fatal error when activating PayPal Checkout plugin #363
= 1.6.1 =
= 1.6.1 - 2021-10-12 =
* Fix - Handle authorization capture failures #312
* Fix - Handle denied payment authorization #302
* Fix - Handle failed authorizations when capturing order #303
* Fix - Transactions cannot be voided #293
* Fix - Fatal error: get_3ds_contingency() #310
= 1.6.0 =
= 1.6.0 - 2021-09-29 =
* Add - Webhook status. #246 #273
* Add - Show CC gateway in admin payments list. #236
* Add - Add 3d secure contingency settings. #230
@ -349,14 +372,14 @@ Follow the steps below to connect the plugin to your PayPal account:
* Fix - REFUND_CAPTURE_CURRENCY_MISMATCH on multicurrency sites. #225
* Fix - Can't checkout to certain countries with optional postcode. #224
= 1.5.1 =
= 1.5.1 - 2021-08-19 =
* Fix - Set 3DS contingencies to "SCA_WHEN_REQUIRED". #178
* Fix - Plugin conflict blocking line item details. #221
* Fix - WooCommerce orders left in "Pending Payment" after a decline. #222
* Fix - Do not send decimals when currency does not support them. #202
* Fix - Gateway can be activated without a connected PayPal account. #205
= 1.5.0 =
= 1.5.0 - 2021-08-09 =
* Add - Filter to modify plugin modules list. #203
* Add - Filters to move PayPal buttons and Pay Later messages. #203
* Fix - Remove redirection when enabling payment gateway with setup already done. #206
@ -365,7 +388,7 @@ Follow the steps below to connect the plugin to your PayPal account:
* Fix - Hide mini cart height field when mini cart is disabled. #213
* Fix - Address possible error on frontend pages due to an empty gateway description. #214
= 1.4.0 =
= 1.4.0 - 2021-07-27 =
* Add - Venmo update #169
* Add - Pay Later Button Global Expansion #182
* Add - Add Canada to advanced credit and debit card #180
@ -381,7 +404,7 @@ Follow the steps below to connect the plugin to your PayPal account:
* Fix - Remove merchant-id query parameter in JSSDK #179
* Fix - Error on Plugin activation with Zettle POS Integration for WooCommerce #195
= 1.3.2 =
= 1.3.2 - 2021-06-08 =
* Fix - Improve Subscription plugin support. #161
* Fix - Disable vault setting if vaulting feature is not available. #150
* Fix - Cast item get_quantity into int. #168
@ -392,10 +415,10 @@ Follow the steps below to connect the plugin to your PayPal account:
* Fix - Fix pay later messaging options. #141
* Fix - UI/UX for vaulting settings. #166
= 1.3.1 =
= 1.3.1 - 2021-04-30 =
* Fix - Fix Credit Card fields for non logged-in users. #152
= 1.3.0 =
= 1.3.0 - 2021-04-28 =
* Add - Client-side vaulting and allow WooCommerce Subscriptions product renewals through payment tokens. #134
* Add - Send transaction ids to woocommerce. #125
* Fix - Validate checkout form before sending request to PayPal #137
@ -405,31 +428,31 @@ Follow the steps below to connect the plugin to your PayPal account:
* Fix - Remove disabling credit for UK. #127
* Fix - Show WC message on account creating error. #136
= 1.2.1 =
= 1.2.1 - 2021-03-08 =
* Fix - Address compatibility issue with Jetpack.
= 1.2.0 =
= 1.2.0 - 2021-03-08 =
* Add - Rework onboarding code and add REST controller for integration with the OBW. #121
* Fix - Remove spinner on click, on cancel and on error. #124
= 1.1.0 =
= 1.1.0 - 2021-02-01 =
* Add - Buy Now Pay Later for UK. #104
* Add - DE now has 12 month installments. #106
* Fix - Check phone for empty string. #102
= 1.0.4 =
= 1.0.4 - 2021-01-18 =
* Fix - Check if WooCommerce is active before initialize. #99
* Fix - Payment buttons only visible on order-pay site when Mini Cart is enabled; payment fails. #96
* Fix - High volume of failed calls to /v1/notifications/webhooks #93
* Fix - GB country has ACDC blocked. #91
= 1.0.3 =
= 1.0.3 - 2020-11-30 =
* Fix - Order with Payment received when Hosted Fields transaction is declined. #88
= 1.0.2 =
= 1.0.2 - 2020-11-09 =
* Fix - Purchases over 1.000 USD fail. #84
= 1.0.1 =
= 1.0.1 - 2020-11-05 =
* Fix - PayPal Smart buttons don't load when using a production/live account and `WP_Debug` is turned on/true. #66
* Fix - [Card Processing] SCA/Visa Verification form loads underneath the Checkout blockUI element. #63
* Fix - Attempting to checkout without country selected results in unexpected error message. #67
@ -438,5 +461,5 @@ Follow the steps below to connect the plugin to your PayPal account:
* Fix - "The value of a field does not conform to the expected format" error when using certain e-mail addresses. #56
* Fix - HTML tags in Product description. #79
= 1.0.0 =
= 1.0.0 - 2020-10-15 =
* Initial release.

View file

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Mockery;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\Button\Exception\ValidationException;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
use WooCommerce\PayPalCommerce\TestCase;
use function Brain\Monkey\Functions\expect;
class ValidateCheckoutEndpointTest extends TestCase
{
private $requestData;
private $formValidator;
private $logger;
private $sut;
public function setUp(): void
{
parent::setUp();
$this->requestData = Mockery::mock(RequestData::class);
$this->formValidator = Mockery::mock(CheckoutFormValidator::class);
$this->logger = Mockery::mock(LoggerInterface::class);
$this->sut = new ValidateCheckoutEndpoint(
$this->requestData,
$this->formValidator,
$this->logger
);
$this->requestData->expects('read_request')->andReturn(['form' => ['field1' => 'value']]);
}
public function testValid()
{
$this->formValidator->expects('validate')->once();
expect('wp_send_json_success')->once();
$this->sut->handle_request();
}
public function testInvalid()
{
$exception = new ValidationException(['Invalid value']);
$this->formValidator->expects('validate')->once()
->andThrow($exception);
expect('wp_send_json_error')->once()
->with(['message' => $exception->getMessage(), 'errors' => ['Invalid value']]);
$this->sut->handle_request();
}
public function testFailure()
{
$exception = new Exception('BOOM');
$this->formValidator->expects('validate')->once()
->andThrow($exception);
expect('wp_send_json_error')->once()
->with(['message' => $exception->getMessage()]);
$this->logger->expects('error')->once();
$this->sut->handle_request();
}
}

View file

@ -3,6 +3,7 @@
namespace WooCommerce\PayPalCommerce\WcGateway\Assets;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\TestCase;
use function Brain\Monkey\Functions\when;
@ -23,6 +24,7 @@ class SettingsPagesAssetsTest extends TestCase
'123',
'EUR',
'DE',
Mockery::mock(Environment::class),
true,
array(),
array()

View file

@ -8,5 +8,6 @@ require_once ROOT_DIR . '/vendor/autoload.php';
require_once TESTS_ROOT_DIR . '/stubs/WC_Payment_Gateway.php';
require_once TESTS_ROOT_DIR . '/stubs/WC_Payment_Gateway_CC.php';
require_once TESTS_ROOT_DIR . '/stubs/WC_Ajax.php';
require_once TESTS_ROOT_DIR . '/stubs/WC_Checkout.php';
Hamcrest\Util::registerGlobalFunctions();

View file

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Tests\E2e\Validation;
use WooCommerce\PayPalCommerce\Button\Exception\ValidationException;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
use WooCommerce\PayPalCommerce\Tests\E2e\TestCase;
class ValidationTest extends TestCase
{
protected $container;
/**
* @var CheckoutFormValidator
*/
protected $sut;
public function setUp(): void
{
parent::setUp();
$this->container = $this->getContainer();
$this->sut = $this->container->get( 'button.validation.wc-checkout-validator' );
assert($this->sut instanceof CheckoutFormValidator);
}
public function testValid()
{
$this->sut->validate([
'billing_first_name'=>'John',
'billing_last_name'=>'Doe',
'billing_company'=>'',
'billing_country'=>'DE',
'billing_address_1'=>'1 Main St',
'billing_address_2'=>'city1',
'billing_postcode'=>'11111',
'billing_city'=>'city1',
'billing_state'=>'DE-BW',
'billing_phone'=>'12345678',
'billing_email'=>'a@gmail.com',
'terms-field'=>'1',
'terms'=>'on',
]);
}
public function testInvalid()
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessageMatches('/.+First name.+Postcode/i');
$this->sut->validate([
'billing_first_name'=>'',
'billing_postcode'=>'ABCDE',
'billing_last_name'=>'Doe',
'billing_company'=>'',
'billing_country'=>'DE',
'billing_address_1'=>'1 Main St',
'billing_address_2'=>'city1',
'billing_city'=>'city1',
'billing_state'=>'DE-BW',
'billing_phone'=>'12345678',
'billing_email'=>'a@gmail.com',
'terms-field'=>'1',
'terms'=>'on',
]);
}
}

3
tests/playwright/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
test-results/
playwright-report/
.cache/

View file

@ -0,0 +1,15 @@
# Playwright Testing
## Local Environment Variables
Allows using environment variables inside the tests.
- Duplicate `.env.e2e.example` and rename it as `.env.e2e`, set values and add new variables if needed.
## Run Tests
```
$ npx playwright test
$ npx playwright test --grep @ci
$ npx playwright test example.spec.js --headed
$ npx playwright test example.spec.js --debug
$ npx playwright test -g "Test name here"
```

View file

@ -0,0 +1,75 @@
const {test, expect} = require('@playwright/test');
const {
CUSTOMER_EMAIL,
CUSTOMER_PASSWORD,
CREDIT_CARD_NUMBER,
CREDIT_CARD_EXPIRATION,
CREDIT_CARD_CVV
} = process.env;
async function fillCheckoutForm(page) {
await page.fill('#billing_first_name', 'John');
await page.fill('#billing_last_name', 'Doe');
await page.selectOption('select#billing_country', 'DE');
await page.fill('#billing_address_1', 'Badensche Str. 24');
await page.fill('#billing_postcode', '10715');
await page.fill('#billing_city', '10715');
await page.fill('#billing_phone', '1234567890');
await page.fill('#billing_email', CUSTOMER_EMAIL);
}
test('PayPal button place order from Product page', async ({page}) => {
await page.goto('/product/product/');
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.frameLocator('.component-frame').locator('[data-funding-source="paypal"]').click(),
]);
await popup.waitForLoadState();
await popup.click("text=Log in");
await popup.fill('#email', CUSTOMER_EMAIL);
await popup.locator('#btnNext').click();
await popup.fill('#password', CUSTOMER_PASSWORD);
await popup.locator('#btnLogin').click();
await popup.locator('#payment-submit-btn').click();
await fillCheckoutForm(page);
await Promise.all([
page.waitForNavigation(),
page.locator('#place_order').click(),
]);
const title = await page.locator('.entry-title');
await expect(title).toHaveText('Order received');
});
test('Advanced Credit and Debit Card (ACDC) place order from Checkout page @ci', async ({page}) => {
await page.goto('/product/product/');
await page.locator('.single_add_to_cart_button').click();
await page.goto('/checkout/');
await fillCheckoutForm(page);
await page.click("text=Credit Cards");
const creditCardNumber = page.frameLocator('#braintree-hosted-field-number').locator('#credit-card-number');
await creditCardNumber.fill(CREDIT_CARD_NUMBER);
const expirationDate = page.frameLocator('#braintree-hosted-field-expirationDate').locator('#expiration');
await expirationDate.fill(CREDIT_CARD_EXPIRATION);
const cvv = page.frameLocator('#braintree-hosted-field-cvv').locator('#cvv');
await cvv.fill(CREDIT_CARD_CVV);
await Promise.all([
page.waitForNavigation(),
page.locator('.ppcp-dcc-order-button').click(),
]);
const title = await page.locator('.entry-title');
await expect(title).toHaveText('Order received');
});

View file

@ -0,0 +1,11 @@
<?php
declare( strict_types=1 );
class WC_Checkout {
public function get_posted_data() {
return [];
}
protected function validate_checkout( &$data, &$errors ) {
}
}

View file

@ -3,13 +3,13 @@
* Plugin Name: WooCommerce PayPal Payments
* Plugin URI: https://woocommerce.com/products/woocommerce-paypal-payments/
* Description: PayPal's latest complete payments processing solution. Accept PayPal, Pay Later, credit/debit cards, alternative digital wallets local payment types and bank accounts. Turn on only PayPal options or process a full suite of payment methods. Enable global transaction with extensive currency and country coverage.
* Version: 2.0.2
* Version: 2.0.3
* Author: WooCommerce
* Author URI: https://woocommerce.com/
* License: GPL-2.0
* Requires PHP: 7.2
* WC requires at least: 3.9
* WC tested up to: 7.2
* WC tested up to: 7.4
* Text Domain: woocommerce-paypal-payments
*
* @package WooCommerce\PayPalCommerce
@ -23,7 +23,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
define( 'PAYPAL_API_URL', 'https://api.paypal.com' );
define( 'PAYPAL_SANDBOX_API_URL', 'https://api.sandbox.paypal.com' );
define( 'PAYPAL_INTEGRATION_DATE', '2023-01-11' );
define( 'PAYPAL_INTEGRATION_DATE', '2023-02-21' );
define( 'PPCP_FLAG_SUBSCRIPTION', true );

View file

@ -2,6 +2,21 @@
# yarn lockfile v1
"@playwright/test@^1.31.1":
version "1.31.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.31.1.tgz#39d6873dc46af135f12451d79707db7d1357455d"
integrity sha512-IsytVZ+0QLDh1Hj83XatGp/GsI1CDJWbyDaBGbainsh0p2zC7F4toUocqowmjS6sQff2NGT3D9WbDj/3K2CJiA==
dependencies:
"@types/node" "*"
playwright-core "1.31.1"
optionalDependencies:
fsevents "2.3.2"
"@types/node@*":
version "18.14.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.1.tgz#90dad8476f1e42797c49d6f8b69aaf9f876fc69f"
integrity sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ==
ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
@ -75,6 +90,11 @@ define-properties@^1.1.3, define-properties@^1.1.4:
has-property-descriptors "^1.0.0"
object-keys "^1.1.1"
dotenv@^16.0.3:
version "16.0.3"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07"
integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==
error-ex@^1.3.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
@ -126,6 +146,11 @@ escape-string-regexp@^1.0.5:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
fsevents@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@ -422,6 +447,11 @@ pify@^3.0.0:
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==
playwright-core@1.31.1:
version "1.31.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.31.1.tgz#4deeebbb8fb73b512593fe24bea206d8fd85ff7f"
integrity sha512-JTyX4kV3/LXsvpHkLzL2I36aCdml4zeE35x+G5aPc4bkLsiRiQshU5lWeVpHFAuC8xAcbI6FDcw/8z3q2xtJSQ==
read-pkg@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"