Merge branch 'trunk' into PCP-1429-device-data-not-available-error-message-is-showing-for-pay-upon-invoice

This commit is contained in:
emilicastells 2023-02-13 09:48:49 +01:00
commit 1f395701ed
No known key found for this signature in database
GPG key ID: 1520C07081754570
12 changed files with 305 additions and 32 deletions

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,7 @@ 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';
// 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 +25,18 @@ const buttonsSpinner = new Spinner(document.querySelector('.ppc-button-wrapper')
const cardsSpinner = new Spinner('#ppcp-hosted-fields');
const bootstrap = () => {
const checkoutFormSelector = 'form.woocommerce-checkout';
const errorHandler = new ErrorHandler(PayPalCommerceGateway.labels.error.generic);
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 freeTrialHandler = new FreeTrialHandler(PayPalCommerceGateway, checkoutFormSelector, formSaver, spinner, errorHandler);
jQuery('form.woocommerce-checkout input').on('keydown', e => {
if (e.key === 'Enter' && [
@ -88,7 +96,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

@ -1,44 +1,50 @@
import {PaymentMethods} from "../Helper/CheckoutMethodState";
import errorHandler from "../ErrorHandler";
class FreeTrialHandler {
constructor(
config,
formSelector,
formSaver,
spinner,
errorHandler
) {
this.config = config;
this.formSelector = formSelector;
this.formSaver = formSaver;
this.spinner = spinner;
this.errorHandler = errorHandler;
}
handle()
async handle()
{
this.spinner.block();
fetch(this.config.ajax.vault_paypal.endpoint, {
try {
await this.formSaver.save(document.querySelector(this.formSelector));
} catch (error) {
console.error(error);
}
try {
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
return_url: location.href,
}),
}).then(res => {
return res.json();
}).then(data => {
});
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

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

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Button\Assets\DisabledSmartButton;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButton;
@ -181,6 +183,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' );

View file

@ -21,6 +21,7 @@ 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\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
@ -777,6 +778,10 @@ class SmartButton implements SmartButtonInterface {
'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() ),
),
),
'enforce_vault' => $this->has_subscriptions(),
'can_save_vault_token' => $this->can_save_vault_token(),

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
@ -156,6 +157,16 @@ 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();
}
);
}
/**

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,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

@ -27,16 +27,20 @@ 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;
$_REQUEST[ $key ] = $value;
}
// Looks like without this WC()->shipping->get_packages() is empty which is used by some plugins.
WC()->cart->calculate_shipping();
// Some plugins/filters check is_checkout().
$is_checkout = function () {
return true;
};
// Some plugins/filters check is_checkout().
add_filter( 'woocommerce_is_checkout', $is_checkout );
try {
// And we must call get_posted_data because it handles the shipping address.
@ -49,8 +53,65 @@ class CheckoutFormValidator extends WC_Checkout {
remove_filter( 'woocommerce_is_checkout', $is_checkout );
}
if ( $errors->has_errors() ) {
throw new ValidationException( $errors->get_error_messages() );
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

@ -148,6 +148,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 );