Merge branch 'PCP-2815-allow-shipping-callback-and-skipping-confirmation-page-from-any-express-button' into PCP-3130-randomizer-for-shipping-callback

This commit is contained in:
Narek Zakarian 2024-05-17 16:53:45 +04:00
commit 859d05e190
No known key found for this signature in database
GPG key ID: 07AFD7E7A9C164A7
16 changed files with 582 additions and 48 deletions

View file

@ -37,6 +37,22 @@ return array(
return array_merge( array_slice( $array, 0, $pos ), $new, array_slice( $array, $pos ) );
};
$label = __(
'Enable this option to require customers to manually confirm express payments on the checkout page.
<p class="description">This ensures they can review the order, update shipping options, and fill in eventual custom fields necessary for the transaction.</p>
<p class="description">If this is disabled, the system will automatically synchronize shipping options with PayPal and bypass the final checkout confirmation. This expedites the checkout process but prevents buyers from filling in eventual custom fields and reviewing final details before finalizing the payment.</p>',
'woocommerce-paypal-payments'
);
if ( wc_terms_and_conditions_page_id() > 0 ) {
$label .= __(
'<div class="ppcp-notice ppcp-notice-warning"><p><span class="highlight">Important:</span> Your store has a <a href="/wp-admin/admin.php?page=wc-settings&tab=advanced" target="_blank">Terms and Conditions</a> page configured. Buyers who use a PayPal express payment method will not be able to consent to the terms on the <code>Classic Checkout</code>, as the final checkout confirmation will be skipped.</p></div>',
'woocommerce-paypal-payments'
);
}
$subscription_helper = $container->get( 'wc-subscriptions.helper' );
return $insert_after(
$fields,
'smart_button_locations',
@ -44,16 +60,13 @@ return array(
'blocks_final_review_enabled' => array(
'title' => __( 'Require final confirmation on checkout', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'label' => __(
'Require customers to confirm express payments from the Cart and Express Checkout on the checkout page.
<p class="description">If this setting is not enabled, <a href="https://woocommerce.com/document/woocommerce-paypal-payments/#blocks-faq" target="_blank">payment confirmation on the checkout will be skipped</a>.
Skipping the final confirmation on the checkout page may impact the buyer experience during the PayPal payment process.</p>',
'woocommerce-paypal-payments'
),
'default' => true,
'label' => $label,
'default' => false,
'screens' => array( State::STATE_START, State::STATE_ONBOARDED ),
'requirements' => array(),
'gateway' => 'paypal',
'class' => array( 'ppcp-grayed-out-text' ),
'input_class' => $subscription_helper->plugin_is_active() ? array( 'ppcp-disabled-checkbox' ) : array(),
),
)
);

View file

@ -139,7 +139,17 @@ export const paypalOrderToWcAddresses = (order) => {
billingAddress = paypalPayerToWc(order.payer);
// no billing address, such as if billing address retrieval is not allowed in the merchant account
if (!billingAddress.address_line_1) {
billingAddress = {...shippingAddress, ...paypalPayerToWc(order.payer)};
// use only non empty values from payer address, otherwise it will override shipping address
let payerAddress = Object.fromEntries(
Object.entries(billingAddress).filter(
([key, value]) => value !== '' && key !== 'country'
)
);
billingAddress = {
...shippingAddress,
...payerAddress
};
}
}

View file

@ -82,7 +82,7 @@ const PayPalComponent = ({
window.ppcpContinuationFilled = true;
}, [])
const createOrder = async () => {
const createOrder = async (data, actions) => {
try {
const res = await fetch(config.scriptData.ajax.create_order.endpoint, {
method: 'POST',
@ -93,7 +93,8 @@ const PayPalComponent = ({
context: config.scriptData.context,
payment_method: 'ppcp-gateway',
funding_source: window.ppcpFundingSource ?? 'paypal',
createaccount: false
createaccount: false,
payment_source: data.paymentSource
}),
});
@ -296,10 +297,15 @@ const PayPalComponent = ({
onClick();
};
const isVenmoAndVaultingEnabled = () => {
return window.ppcpFundingSource === 'venmo' && config.scriptData.vaultingEnabled;
}
let handleShippingOptionsChange = null;
let handleShippingAddressChange = null;
let handleSubscriptionShippingOptionsChange = null;
let handleSubscriptionShippingAddressChange = null;
if (shippingData.needsShipping && !config.finalReviewEnabled) {
handleShippingOptionsChange = async (data, actions) => {
try {

View file

@ -60,7 +60,8 @@ class CartActionHandler {
funding_source: window.ppcpFundingSource,
bn_code:bnCode,
payer,
context:this.config.context
context:this.config.context,
payment_source: data.paymentSource
}),
}).then(function(res) {
return res.json();

View file

@ -0,0 +1,126 @@
import {paypalAddressToWc} from "../../../../../ppcp-blocks/resources/js/Helper/Address.js";
import {convertKeysToSnakeCase} from "../../../../../ppcp-blocks/resources/js/Helper/Helper.js";
/**
* Handles the shipping option change in PayPal.
*
* @param data
* @param actions
* @param config
* @returns {Promise<void>}
*/
export const handleShippingOptionsChange = async (data, actions, config) => {
try {
const shippingOptionId = data.selectedShippingOption?.id;
if (shippingOptionId) {
await fetch(config.ajax.update_customer_shipping.shipping_options.endpoint, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-WC-Store-API-Nonce': config.ajax.update_customer_shipping.wp_rest_nonce,
},
body: JSON.stringify({
rate_id: shippingOptionId,
})
})
.then(response => {
return response.json();
})
.then(cardData => {
const shippingMethods = document.querySelectorAll('.shipping_method');
shippingMethods.forEach(function(method) {
if (method.value === shippingOptionId) {
method.checked = true;
}
});
})
}
const res = await fetch(config.ajax.update_shipping.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: config.ajax.update_shipping.nonce,
order_id: data.orderID,
})
});
const json = await res.json();
if (!json.success) {
throw new Error(json.data.message);
}
} catch (e) {
console.error(e);
actions.reject();
}
};
/**
* Handles the shipping address change in PayPal.
*
* @param data
* @param actions
* @param config
* @returns {Promise<void>}
*/
export const handleShippingAddressChange = async (data, actions, config) => {
try {
const address = paypalAddressToWc(convertKeysToSnakeCase(data.shippingAddress));
// Retrieve current cart contents
await fetch(config.ajax.update_customer_shipping.shipping_address.cart_endpoint)
.then(response => {
return response.json();
})
.then(cartData => {
// Update shipping address in the cart data
cartData.shipping_address.address_1 = address.address_1;
cartData.shipping_address.address_2 = address.address_2;
cartData.shipping_address.city = address.city;
cartData.shipping_address.state = address.state;
cartData.shipping_address.postcode = address.postcode;
cartData.shipping_address.country = address.country;
// Send update request
return fetch(config.ajax.update_customer_shipping.shipping_address.update_customer_endpoint, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-WC-Store-API-Nonce': config.ajax.update_customer_shipping.wp_rest_nonce,
},
body: JSON.stringify({
shipping_address: cartData.shipping_address,
})
}).then(function (res) {
return res.json();
}).then(function (customerData) {
jQuery(".cart_totals .shop_table").load(location.href + " " + ".cart_totals .shop_table" + ">*", "");
})
})
const res = await fetch(config.ajax.update_shipping.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: config.ajax.update_shipping.nonce,
order_id: data.orderID,
})
});
const json = await res.json();
if (!json.success) {
throw new Error(json.data.message);
}
} catch (e) {
console.error(e);
actions.reject();
}
};

View file

@ -10,6 +10,7 @@ const onApprove = (context, errorHandler) => {
nonce: context.config.ajax.approve_order.nonce,
order_id:data.orderID,
funding_source: window.ppcpFundingSource,
should_create_wc_order: !context.config.vaultingEnabled || data.paymentSource !== 'venmo'
})
}).then((res)=>{
return res.json();
@ -20,7 +21,11 @@ const onApprove = (context, errorHandler) => {
errorHandler.genericError();
});
}
location.href = context.config.redirect;
let orderReceivedUrl = data.data?.order_received_url
location.href = orderReceivedUrl ? orderReceivedUrl : context.config.redirect;
});
}

View file

@ -3,6 +3,10 @@ import {loadScript} from "@paypal/paypal-js";
import {keysToCamelCase} from "../Helper/Utils";
import widgetBuilder from "./WidgetBuilder";
import {normalizeStyleForFundingSource} from "../Helper/Style";
import {
handleShippingOptionsChange,
handleShippingAddressChange,
} from "../Helper/ShippingHandler.js";
class Renderer {
constructor(creditCardRenderer, defaultSettings, onSmartButtonClick, onSmartButtonsInit) {
@ -64,6 +68,15 @@ class Renderer {
}
}
shouldHandleShippingInPaypal = (venmoButtonClicked) => {
if (!this.defaultSettings.should_handle_shipping_in_paypal) {
console.log('no')
return false;
}
return !venmoButtonClicked || !this.defaultSettings.vaultingEnabled;
}
renderButtons(wrapper, style, contextConfig, hasEnabledSeparateGateways, fundingSource = null) {
if (! document.querySelector(wrapper) || this.isAlreadyRendered(wrapper, fundingSource, hasEnabledSeparateGateways) ) {
// Try to render registered buttons again in case they were removed from the DOM by an external source.
@ -75,17 +88,24 @@ class Renderer {
contextConfig.fundingSource = fundingSource;
}
let venmoButtonClicked = false;
const buttonsOptions = () => {
return {
style,
...contextConfig,
onClick: this.onSmartButtonClick,
onClick: (data, actions) => {
venmoButtonClicked = data.fundingSource === 'venmo'
this.onSmartButtonClick
},
onInit: (data, actions) => {
if (this.onSmartButtonsInit) {
this.onSmartButtonsInit(data, actions);
}
this.handleOnButtonsInit(wrapper, data, actions);
},
onShippingOptionsChange: (data, actions) => this.shouldHandleShippingInPaypal(venmoButtonClicked) ? handleShippingOptionsChange(data, actions, this.defaultSettings) : null,
onShippingAddressChange: (data, actions) => this.shouldHandleShippingInPaypal(venmoButtonClicked) ? handleShippingAddressChange(data, actions, this.defaultSettings) : null,
}
}

View file

@ -16,6 +16,7 @@ use WooCommerce\PayPalCommerce\Button\Helper\CartProductsHelper;
use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Button\Helper\WooCommerceOrderCreator;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
@ -147,7 +148,8 @@ return array(
$container->get( 'wcgateway.funding-sources-without-redirect' ),
$container->get( 'vaulting.vault-v3-enabled' ),
$container->get( 'api.endpoint.payment-tokens' ),
$container->get( 'woocommerce.logger.woocommerce' )
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'button.handle-shipping-in-paypal' )
);
},
'button.url' => static function ( ContainerInterface $container ): string {
@ -157,7 +159,13 @@ return array(
);
},
'button.pay-now-contexts' => static function ( ContainerInterface $container ): array {
return array( 'checkout', 'pay-now' );
$defaults = array( 'checkout', 'pay-now' );
if ( $container->get( 'button.handle-shipping-in-paypal' ) ) {
return array_merge( $defaults, array( 'cart', 'product', 'mini-cart' ) );
}
return $defaults;
},
'button.request-data' => static function ( ContainerInterface $container ): RequestData {
return new RequestData();
@ -221,14 +229,17 @@ return array(
return new EarlyOrderHandler( $state, $order_processor, $session_handler );
},
'button.endpoint.approve-order' => static function ( ContainerInterface $container ): ApproveOrderEndpoint {
$request_data = $container->get( 'button.request-data' );
$order_endpoint = $container->get( 'api.endpoint.order' );
$session_handler = $container->get( 'session.handler' );
$three_d_secure = $container->get( 'button.helper.three-d-secure' );
$settings = $container->get( 'wcgateway.settings' );
$dcc_applies = $container->get( 'api.helpers.dccapplies' );
$order_helper = $container->get( 'api.order-helper' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
$request_data = $container->get( 'button.request-data' );
$order_endpoint = $container->get( 'api.endpoint.order' );
$session_handler = $container->get( 'session.handler' );
$three_d_secure = $container->get( 'button.helper.three-d-secure' );
$settings = $container->get( 'wcgateway.settings' );
$dcc_applies = $container->get( 'api.helpers.dccapplies' );
$order_helper = $container->get( 'api.order-helper' );
$final_review_enabled = $container->get( 'blocks.settings.final_review_enabled' );
$wc_order_creator = $container->get( 'button.helper.wc-order-creator' );
$gateway = $container->get( 'wcgateway.paypal-gateway' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new ApproveOrderEndpoint(
$request_data,
$order_endpoint,
@ -237,6 +248,9 @@ return array(
$settings,
$dcc_applies,
$order_helper,
$final_review_enabled,
$gateway,
$wc_order_creator,
$logger
);
},
@ -342,6 +356,10 @@ return array(
* May result in slower popup performance, additional loading.
*/
'button.handle-shipping-in-paypal' => static function ( ContainerInterface $container ): bool {
return false;
return ! $container->get( 'blocks.settings.final_review_enabled' );
},
'button.helper.wc-order-creator' => static function ( ContainerInterface $container ): WooCommerceOrderCreator {
return new WooCommerceOrderCreator( $container->get( 'wcgateway.funding-source.renderer' ), $container->get( 'session.handler' ) );
},
);

View file

@ -19,6 +19,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Blocks\Endpoint\UpdateShippingEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
@ -217,6 +218,13 @@ class SmartButton implements SmartButtonInterface {
*/
private $logger;
/**
* Whether the shipping should be handled in PayPal.
*
* @var bool
*/
private $should_handle_shipping_in_paypal;
/**
* SmartButton constructor.
*
@ -242,6 +250,7 @@ class SmartButton implements SmartButtonInterface {
* @param bool $vault_v3_enabled Whether Vault v3 module is enabled.
* @param PaymentTokensEndpoint $payment_tokens_endpoint Payment tokens endpoint.
* @param LoggerInterface $logger The logger.
* @param bool $should_handle_shipping_in_paypal Whether the shipping should be handled in PayPal.
*/
public function __construct(
string $module_url,
@ -265,7 +274,8 @@ class SmartButton implements SmartButtonInterface {
array $funding_sources_without_redirect,
bool $vault_v3_enabled,
PaymentTokensEndpoint $payment_tokens_endpoint,
LoggerInterface $logger
LoggerInterface $logger,
bool $should_handle_shipping_in_paypal
) {
$this->module_url = $module_url;
@ -290,6 +300,7 @@ class SmartButton implements SmartButtonInterface {
$this->vault_v3_enabled = $vault_v3_enabled;
$this->logger = $logger;
$this->payment_tokens_endpoint = $payment_tokens_endpoint;
$this->should_handle_shipping_in_paypal = $should_handle_shipping_in_paypal;
}
/**
@ -1133,6 +1144,21 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
'endpoint' => \WC_AJAX::get_endpoint( CreatePaymentTokenForGuest::ENDPOINT ),
'nonce' => wp_create_nonce( CreatePaymentTokenForGuest::nonce() ),
),
'update_shipping' => array(
'endpoint' => \WC_AJAX::get_endpoint( UpdateShippingEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( UpdateShippingEndpoint::nonce() ),
),
'update_customer_shipping' => array(
'shipping_options' => array(
'endpoint' => '/wp-json/wc/store/cart/select-shipping-rate',
),
'shipping_address' => array(
'cart_endpoint' => '/wp-json/wc/store/cart/',
'update_customer_endpoint' => '/wp-json/wc/store/v1/cart/update-customer/',
),
'wp_rest_nonce' => wp_create_nonce( 'wc_store_api' ),
'update_shipping_method' => \WC_AJAX::get_endpoint( 'update_shipping_method' ),
),
),
'cart_contains_subscription' => $this->subscription_helper->cart_contains_subscription(),
'subscription_plan_id' => $this->subscription_helper->paypal_subscription_id(),
@ -1253,6 +1279,8 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
'user' => array(
'is_logged' => is_user_logged_in(),
),
'should_handle_shipping_in_paypal' => $this->should_handle_shipping_in_paypal && ! $this->is_checkout(),
'vaultingEnabled' => $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' ),
);
if ( 'pay-now' === $this->context() ) {
@ -1333,7 +1361,7 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
'integration-date' => PAYPAL_INTEGRATION_DATE,
'components' => implode( ',', $this->components() ),
'vault' => ( $this->can_save_vault_token() || $this->subscription_helper->need_subscription_intent( $subscription_mode ) ) ? 'true' : 'false',
'commit' => in_array( $context, $this->pay_now_contexts, true ) ? 'true' : 'false',
'commit' => 'false',
'intent' => $intent,
);

View file

@ -14,13 +14,15 @@ use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderHelper;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure;
use WooCommerce\PayPalCommerce\Button\Helper\WooCommerceOrderCreator;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
@ -28,6 +30,8 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
*/
class ApproveOrderEndpoint implements EndpointInterface {
use ContextTrait;
const ENDPOINT = 'ppc-approve-order';
/**
@ -79,6 +83,27 @@ class ApproveOrderEndpoint implements EndpointInterface {
*/
protected $order_helper;
/**
* Whether the final review is enabled.
*
* @var bool
*/
protected $final_review_enabled;
/**
* The WC gateway.
*
* @var PayPalGateway
*/
protected $gateway;
/**
* The WooCommerce order creator.
*
* @var WooCommerceOrderCreator
*/
protected $wc_order_creator;
/**
* The logger.
*
@ -89,14 +114,17 @@ class ApproveOrderEndpoint implements EndpointInterface {
/**
* ApproveOrderEndpoint constructor.
*
* @param RequestData $request_data The request data helper.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param SessionHandler $session_handler The session handler.
* @param ThreeDSecure $three_d_secure The 3d secure helper object.
* @param Settings $settings The settings.
* @param DccApplies $dcc_applies The DCC applies object.
* @param OrderHelper $order_helper The order helper.
* @param LoggerInterface $logger The logger.
* @param RequestData $request_data The request data helper.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param SessionHandler $session_handler The session handler.
* @param ThreeDSecure $three_d_secure The 3d secure helper object.
* @param Settings $settings The settings.
* @param DccApplies $dcc_applies The DCC applies object.
* @param OrderHelper $order_helper The order helper.
* @param bool $final_review_enabled Whether the final review is enabled.
* @param PayPalGateway $gateway The WC gateway.
* @param WooCommerceOrderCreator $wc_order_creator The WooCommerce order creator.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
RequestData $request_data,
@ -106,17 +134,23 @@ class ApproveOrderEndpoint implements EndpointInterface {
Settings $settings,
DccApplies $dcc_applies,
OrderHelper $order_helper,
bool $final_review_enabled,
PayPalGateway $gateway,
WooCommerceOrderCreator $wc_order_creator,
LoggerInterface $logger
) {
$this->request_data = $request_data;
$this->api_endpoint = $order_endpoint;
$this->session_handler = $session_handler;
$this->threed_secure = $three_d_secure;
$this->settings = $settings;
$this->dcc_applies = $dcc_applies;
$this->order_helper = $order_helper;
$this->logger = $logger;
$this->request_data = $request_data;
$this->api_endpoint = $order_endpoint;
$this->session_handler = $session_handler;
$this->threed_secure = $three_d_secure;
$this->settings = $settings;
$this->dcc_applies = $dcc_applies;
$this->order_helper = $order_helper;
$this->final_review_enabled = $final_review_enabled;
$this->gateway = $gateway;
$this->wc_order_creator = $wc_order_creator;
$this->logger = $logger;
}
/**
@ -182,6 +216,7 @@ class ApproveOrderEndpoint implements EndpointInterface {
);
}
$this->session_handler->replace_order( $order );
wp_send_json_success();
}
@ -200,6 +235,15 @@ class ApproveOrderEndpoint implements EndpointInterface {
$this->session_handler->replace_funding_source( $funding_source );
$this->session_handler->replace_order( $order );
$should_create_wc_order = $data['should_create_wc_order'] ?? false;
if ( ! $this->final_review_enabled && ! $this->is_checkout() && $should_create_wc_order ) {
$wc_order = $this->wc_order_creator->create_from_paypal_order( $order, WC()->cart );
$this->gateway->process_payment( $wc_order->get_id() );
$order_received_url = $wc_order->get_checkout_order_received_url();
wp_send_json_success( array( 'order_received_url' => $order_received_url ) );
}
wp_send_json_success();
return true;
} catch ( Exception $error ) {

View file

@ -246,6 +246,7 @@ class CreateOrderEndpoint implements EndpointInterface {
$this->parsed_request_data = $data;
$payment_method = $data['payment_method'] ?? '';
$funding_source = $data['funding_source'] ?? '';
$payment_source = $data['payment_source'] ?? '';
$wc_order = null;
if ( 'pay-now' === $data['context'] ) {
$wc_order = wc_get_order( (int) $data['order_id'] );
@ -261,7 +262,7 @@ class CreateOrderEndpoint implements EndpointInterface {
}
$this->purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order );
} else {
$this->purchase_unit = $this->purchase_unit_factory->from_wc_cart( null, $this->handle_shipping_in_paypal );
$this->purchase_unit = $this->purchase_unit_factory->from_wc_cart( null, $this->should_handle_shipping_in_paypal( $payment_source ) );
// Do not allow completion by webhooks when started via non-checkout buttons,
// it is needed only for some APMs in checkout.
@ -610,4 +611,20 @@ class CreateOrderEndpoint implements EndpointInterface {
'custom_id' => $order->purchase_units()[0]->custom_id(),
);
}
/**
* Checks if the shipping should be handled in PayPal popup.
*
* @param string $payment_source The payment source.
* @return bool true if the shipping should be handled in PayPal popup, otherwise false.
*/
protected function should_handle_shipping_in_paypal( string $payment_source ): bool {
$is_vaulting_enabled = $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' );
if ( ! $this->handle_shipping_in_paypal ) {
return false;
}
return ! $is_vaulting_enabled || $payment_source !== 'venmo';
}
}

View file

@ -0,0 +1,228 @@
<?php
/**
* Can create WC orders.
*
* @package WooCommerce\PayPalCommerce\Button\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Helper;
use RuntimeException;
use WC_Cart;
use WC_Order;
use WC_Order_Item_Product;
use WC_Order_Item_Shipping;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Shipping;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
* Class WooCommerceOrderCreator
*/
class WooCommerceOrderCreator {
/**
* The funding source renderer.
*
* @var FundingSourceRenderer
*/
protected $funding_source_renderer;
/**
* The Session handler.
*
* @var SessionHandler
*/
protected $session_handler;
/**
* WooCommerceOrderCreator constructor.
*
* @param FundingSourceRenderer $funding_source_renderer The funding source renderer.
* @param SessionHandler $session_handler The session handler.
*/
public function __construct(
FundingSourceRenderer $funding_source_renderer,
SessionHandler $session_handler
) {
$this->funding_source_renderer = $funding_source_renderer;
$this->session_handler = $session_handler;
}
/**
* Creates WC order based on given PayPal order.
*
* @param Order $order The PayPal order.
* @param WC_Cart $wc_cart The Cart.
* @return WC_Order The WC order.
* @throws RuntimeException If problem creating.
*/
public function create_from_paypal_order( Order $order, WC_Cart $wc_cart ): WC_Order {
$wc_order = wc_create_order();
if ( ! $wc_order instanceof WC_Order ) {
throw new RuntimeException( 'Problem creating WC order.' );
}
$this->configure_line_items( $wc_order, $wc_cart );
$this->configure_shipping( $wc_order, $order->payer(), $order->purchase_units()[0]->shipping() );
$this->configure_payment_source( $wc_order );
$this->configure_customer( $wc_order );
$this->configure_coupons( $wc_order, $wc_cart->get_applied_coupons() );
$wc_order->calculate_totals();
$wc_order->save();
return $wc_order;
}
/**
* Configures the line items.
*
* @param WC_Order $wc_order The WC order.
* @param WC_Cart $wc_cart The Cart.
* @return void
*/
protected function configure_line_items( WC_Order $wc_order, WC_Cart $wc_cart ): void {
$cart_contents = $wc_cart->get_cart();
foreach ( $cart_contents as $cart_item ) {
$product_id = $cart_item['product_id'] ?? 0;
$variation_id = $cart_item['variation_id'] ?? 0;
$quantity = $cart_item['quantity'] ?? 0;
$variation_attributes = $cart_item['variation'];
$item = new WC_Order_Item_Product();
$item->set_product_id( $product_id );
$item->set_quantity( $quantity );
if ( $variation_id ) {
$item->set_variation_id( $variation_id );
$item->set_variation( $variation_attributes );
}
$product = wc_get_product( $variation_id ?: $product_id );
if ( ! $product ) {
return;
}
$item->set_name( $product->get_name() );
$item->set_subtotal( $product->get_price() * $quantity );
$item->set_total( $product->get_price() * $quantity );
$wc_order->add_item( $item );
}
}
/**
* Configures the shipping & billing addresses for WC order from given payer.
*
* @param WC_Order $wc_order The WC order.
* @param Payer|null $payer The payer.
* @param Shipping|null $shipping The shipping.
* @return void
*/
protected function configure_shipping( WC_Order $wc_order, ?Payer $payer, ?Shipping $shipping ): void {
$shipping_address = null;
$billing_address = null;
$shipping_options = null;
if ( $payer ) {
$address = $payer->address();
$payer_name = $payer->name();
$billing_address = array(
'first_name' => $payer_name ? $payer_name->given_name() : '',
'last_name' => $payer_name ? $payer_name->surname() : '',
'address_1' => $address ? $address->address_line_1() : '',
'address_2' => $address ? $address->address_line_2() : '',
'city' => $address ? $address->admin_area_2() : '',
'state' => $address ? $address->admin_area_1() : '',
'postcode' => $address ? $address->postal_code() : '',
'country' => $address ? $address->country_code() : '',
);
}
if ( $shipping ) {
$address = $shipping->address();
$shipping_address = array(
'first_name' => $shipping->name(),
'last_name' => '',
'address_1' => $address->address_line_1(),
'address_2' => $address->address_line_2(),
'city' => $address->admin_area_2(),
'state' => $address->admin_area_1(),
'postcode' => $address->postal_code(),
'country' => $address->country_code(),
);
$shipping_options = $shipping->options()[0] ?? '';
}
if ( $shipping_address ) {
$wc_order->set_shipping_address( $shipping_address );
}
if ( $billing_address || $shipping_address ) {
$wc_order->set_billing_address( $billing_address ?: $shipping_address );
}
if ( $shipping_options ) {
$shipping = new WC_Order_Item_Shipping();
$shipping->set_method_title( $shipping_options->label() );
$shipping->set_method_id( $shipping_options->id() );
$shipping->set_total( $shipping_options->amount()->value_str() );
$wc_order->add_item( $shipping );
}
}
/**
* Configures the payment source.
*
* @param WC_Order $wc_order The WC order.
* @return void
*/
protected function configure_payment_source( WC_Order $wc_order ): void {
$funding_source = $this->session_handler->funding_source();
$wc_order->set_payment_method( PayPalGateway::ID );
if ( $funding_source ) {
$wc_order->set_payment_method_title( $this->funding_source_renderer->render_name( $funding_source ) );
}
}
/**
* Configures the customer ID.
*
* @param WC_Order $wc_order The WC order.
* @return void
*/
protected function configure_customer( WC_Order $wc_order ): void {
$current_user = wp_get_current_user();
if ( $current_user->ID !== 0 ) {
$wc_order->set_customer_id( $current_user->ID );
}
}
/**
* Configures the applied coupons.
*
* @param WC_Order $wc_order The WC order.
* @param string[] $coupons The list of applied coupons.
* @return void
*/
protected function configure_coupons( WC_Order $wc_order, array $coupons ): void {
foreach ( $coupons as $coupon_code ) {
$wc_order->apply_coupon( $coupon_code );
}
}
}

View file

@ -61,8 +61,8 @@ $background-ident-color: #fbfbfb;
border: 1px solid #c3c4c7;
border-left-width: 4px;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
margin: 5px 15px 2px;
padding: 1px 12px;
margin: 5px 0px 2px;
padding: 0px 12px 4px 12px;
}
.ppcp-notice-warning {

View file

@ -347,6 +347,7 @@ return array(
$container->get( 'api.partner_merchant_id-production' ),
$container->get( 'api.partner_merchant_id-sandbox' ),
$container->get( 'api.endpoint.billing-agreements' ),
$container->get( 'wc-subscriptions.helper' ),
$logger
);
},

View file

@ -23,6 +23,7 @@ use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCProductStatus;
use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceProductStatus;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\WooCommerce\Logging\Logger\NullLogger;
@ -160,6 +161,13 @@ class SettingsListener {
*/
private $billing_agreements_endpoint;
/**
* The subscription helper
*
* @var SubscriptionHelper
*/
protected $subscription_helper;
/**
* The logger.
*
@ -185,6 +193,7 @@ class SettingsListener {
* @param string $partner_merchant_id_production Partner merchant ID production.
* @param string $partner_merchant_id_sandbox Partner merchant ID sandbox.
* @param BillingAgreementsEndpoint $billing_agreements_endpoint Billing Agreements endpoint.
* @param SubscriptionHelper $subscription_helper The subscription helper.
* @param ?LoggerInterface $logger The logger.
*/
public function __construct(
@ -203,6 +212,7 @@ class SettingsListener {
string $partner_merchant_id_production,
string $partner_merchant_id_sandbox,
BillingAgreementsEndpoint $billing_agreements_endpoint,
SubscriptionHelper $subscription_helper,
LoggerInterface $logger = null
) {
@ -221,6 +231,7 @@ class SettingsListener {
$this->partner_merchant_id_production = $partner_merchant_id_production;
$this->partner_merchant_id_sandbox = $partner_merchant_id_sandbox;
$this->billing_agreements_endpoint = $billing_agreements_endpoint;
$this->subscription_helper = $subscription_helper;
$this->logger = $logger ?: new NullLogger();
}
@ -392,6 +403,11 @@ class SettingsListener {
$this->settings->persist();
}
if ( $this->subscription_helper->plugin_is_active() ) {
$this->settings->set( 'blocks_final_review_enabled', true );
$this->settings->persist();
}
if ( $subscription_mode === 'disable_paypal_subscriptions' && $vault_enabled === '1' ) {
$this->settings->set( 'vault_enabled', false );
$this->settings->persist();

View file

@ -3,16 +3,15 @@
namespace WooCommerce\PayPalCommerce\WcGateway\Settings;
use Psr\Log\LoggerInterface;
use Requests_Utility_CaseInsensitiveDictionary;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\Helper\RedirectorStub;
use WooCommerce\PayPalCommerce\Helper\StubRedirectionException;
use WooCommerce\PayPalCommerce\ModularTestCase;
use WooCommerce\PayPalCommerce\Onboarding\State;
use Mockery;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar;
use function Brain\Monkey\Functions\when;
@ -42,6 +41,7 @@ class SettingsListenerTest extends ModularTestCase
$pui_status_cache = Mockery::mock(Cache::class);
$dcc_status_cache = Mockery::mock(Cache::class);
$billing_agreement_endpoint = Mockery::mock(BillingAgreementsEndpoint::class);
$subscription_helper = Mockery::mock(SubscriptionHelper::class);
$logger = Mockery::mock(LoggerInterface::class);
$testee = new SettingsListener(
@ -60,6 +60,7 @@ class SettingsListenerTest extends ModularTestCase
'',
'',
$billing_agreement_endpoint,
$subscription_helper,
$logger
);