Merge trunk

This commit is contained in:
Emili Castells Guasch 2024-06-11 10:52:10 +02:00
commit 73bc86f9f5
64 changed files with 1456 additions and 436 deletions

View file

@ -23,7 +23,8 @@ class CartActionHandler {
body: JSON.stringify({
nonce: this.config.ajax.approve_subscription.nonce,
order_id: data.orderID,
subscription_id: data.subscriptionID
subscription_id: data.subscriptionID,
should_create_wc_order: !context.config.vaultingEnabled || data.paymentSource !== 'venmo'
})
}).then((res)=>{
return res.json();
@ -33,7 +34,9 @@ class CartActionHandler {
throw Error(data.data.message);
}
location.href = this.config.redirect;
let orderReceivedUrl = data.data?.order_received_url
location.href = orderReceivedUrl ? orderReceivedUrl : context.config.redirect;
});
},
onError: (err) => {
@ -60,8 +63,7 @@ class CartActionHandler {
funding_source: window.ppcpFundingSource,
bn_code:bnCode,
payer,
context:this.config.context,
payment_source: data.paymentSource
context:this.config.context
}),
}).then(function(res) {
return res.json();

View file

@ -4,6 +4,7 @@ import widgetBuilder from "../Renderer/WidgetBuilder";
import merge from "deepmerge";
import {keysToCamelCase} from "./Utils";
import {getCurrentPaymentMethod} from "./CheckoutMethodState";
import { v4 as uuidv4 } from 'uuid';
// This component may be used by multiple modules. This assures that options are shared between all instances.
let options = window.ppcpWidgetBuilder = window.ppcpWidgetBuilder || {
@ -63,9 +64,10 @@ export const loadPaypalScript = (config, onLoaded, onError = null) => {
// Axo SDK options
const sdkClientToken = config?.axo?.sdk_client_token;
const uuid = uuidv4().replace(/-/g, '');
if(sdkClientToken) {
scriptOptions['data-sdk-client-token'] = sdkClientToken;
scriptOptions['data-client-metadata-id'] = 'ppcp-cm-id';
scriptOptions['data-client-metadata-id'] = uuid;
}
// Load PayPal script for special case with data-client-token

View file

@ -39,19 +39,21 @@ export const handleShippingOptionsChange = async (data, actions, config) => {
})
}
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,
})
});
if (!config.data_client_id.has_subscriptions) {
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();
const json = await res.json();
if (!json.success) {
throw new Error(json.data.message);
if (!json.success) {
throw new Error(json.data.message);
}
}
} catch (e) {
console.error(e);
@ -104,20 +106,20 @@ export const handleShippingAddressChange = async (data, actions, config) => {
})
})
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 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();
const json = await res.json();
if (!json.success) {
throw new Error(json.data.message);
}
if (!json.success) {
throw new Error(json.data.message);
}
} catch (e) {
console.error(e);

View file

@ -68,14 +68,6 @@ class Renderer {
}
}
shouldHandleShippingInPaypal = (venmoButtonClicked) => {
if (!this.defaultSettings.should_handle_shipping_in_paypal) {
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.
@ -93,7 +85,16 @@ class Renderer {
const options = {
style,
...contextConfig,
onClick: this.onSmartButtonClick,
onClick: (data, actions) => {
if (this.onSmartButtonClick) {
this.onSmartButtonClick(data, actions);
}
venmoButtonClicked = false;
if (data.fundingSource === 'venmo') {
venmoButtonClicked = true;
}
},
onInit: (data, actions) => {
if (this.onSmartButtonsInit) {
this.onSmartButtonsInit(data, actions);
@ -103,9 +104,17 @@ class Renderer {
};
// Check the condition and add the handler if needed
if (this.shouldHandleShippingInPaypal(venmoButtonClicked)) {
options.onShippingOptionsChange = (data, actions) => handleShippingOptionsChange(data, actions, this.defaultSettings);
options.onShippingAddressChange = (data, actions) => handleShippingAddressChange(data, actions, this.defaultSettings);
if (this.defaultSettings.should_handle_shipping_in_paypal) {
options.onShippingOptionsChange = (data, actions) => {
!this.isVenmoButtonClickedWhenVaultingIsEnabled(venmoButtonClicked)
? handleShippingOptionsChange(data, actions, this.defaultSettings)
: null;
}
options.onShippingAddressChange = (data, actions) => {
!this.isVenmoButtonClickedWhenVaultingIsEnabled(venmoButtonClicked)
? handleShippingAddressChange(data, actions, this.defaultSettings)
: null;
}
}
return options;
@ -139,6 +148,10 @@ class Renderer {
}
}
isVenmoButtonClickedWhenVaultingIsEnabled = (venmoButtonClicked) => {
return venmoButtonClicked && this.defaultSettings.vaultingEnabled;
}
isAlreadyRendered(wrapper, fundingSource) {
return this.renderedSources.has(wrapper + (fundingSource ?? ''));
}

View file

@ -239,7 +239,6 @@ return array(
$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' );
$subscription_helper = $container->get( 'wc-subscriptions.helper' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new ApproveOrderEndpoint(
$request_data,
@ -252,7 +251,6 @@ return array(
$final_review_enabled,
$gateway,
$wc_order_creator,
$subscription_helper,
$logger
);
},
@ -260,7 +258,10 @@ return array(
return new ApproveSubscriptionEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'session.handler' )
$container->get( 'session.handler' ),
$container->get( 'blocks.settings.final_review_enabled' ),
$container->get( 'button.helper.wc-order-creator' ),
$container->get( 'wcgateway.paypal-gateway' )
);
},
'button.checkout-form-saver' => static function ( ContainerInterface $container ): CheckoutFormSaver {
@ -362,6 +363,10 @@ return array(
},
'button.helper.wc-order-creator' => static function ( ContainerInterface $container ): WooCommerceOrderCreator {
return new WooCommerceOrderCreator( $container->get( 'wcgateway.funding-source.renderer' ), $container->get( 'session.handler' ) );
return new WooCommerceOrderCreator(
$container->get( 'wcgateway.funding-source.renderer' ),
$container->get( 'session.handler' ),
$container->get( 'wc-subscriptions.helper' )
);
},
);

View file

@ -1421,7 +1421,7 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
$disable_funding,
array_diff(
array_keys( $this->all_funding_sources ),
array( 'venmo', 'paylater', 'paypal' )
array( 'venmo', 'paylater', 'paypal', 'card' )
)
);
}

View file

@ -105,13 +105,6 @@ class ApproveOrderEndpoint implements EndpointInterface {
*/
protected $wc_order_creator;
/**
* The Subscription Helper.
*
* @var SubscriptionHelper
*/
protected $subscription_helper;
/**
* The logger.
*
@ -132,7 +125,6 @@ class ApproveOrderEndpoint implements EndpointInterface {
* @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 SubscriptionHelper $subscription_helper The subscription helper.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
@ -146,7 +138,6 @@ class ApproveOrderEndpoint implements EndpointInterface {
bool $final_review_enabled,
PayPalGateway $gateway,
WooCommerceOrderCreator $wc_order_creator,
SubscriptionHelper $subscription_helper,
LoggerInterface $logger
) {
@ -160,7 +151,6 @@ class ApproveOrderEndpoint implements EndpointInterface {
$this->final_review_enabled = $final_review_enabled;
$this->gateway = $gateway;
$this->wc_order_creator = $wc_order_creator;
$this->subscription_helper = $subscription_helper;
$this->logger = $logger;
}
@ -247,7 +237,7 @@ class ApproveOrderEndpoint implements EndpointInterface {
$this->session_handler->replace_order( $order );
if ( ! $this->subscription_helper->plugin_is_active() && apply_filters( 'woocommerce_paypal_payments_toggle_final_review_checkbox', false ) ) {
if ( apply_filters( 'woocommerce_paypal_payments_toggle_final_review_checkbox', false ) ) {
$this->toggle_final_review_enabled_setting();
}

View file

@ -11,13 +11,18 @@ namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Button\Helper\WooCommerceOrderCreator;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
* Class ApproveSubscriptionEndpoint
*/
class ApproveSubscriptionEndpoint implements EndpointInterface {
use ContextTrait;
const ENDPOINT = 'ppc-approve-subscription';
/**
@ -41,21 +46,51 @@ class ApproveSubscriptionEndpoint implements EndpointInterface {
*/
private $session_handler;
/**
* Whether the final review is enabled.
*
* @var bool
*/
protected $final_review_enabled;
/**
* The WooCommerce order creator.
*
* @var WooCommerceOrderCreator
*/
protected $wc_order_creator;
/**
* The WC gateway.
*
* @var PayPalGateway
*/
protected $gateway;
/**
* ApproveSubscriptionEndpoint constructor.
*
* @param RequestData $request_data The request data helper.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param SessionHandler $session_handler The session handler.
* @param RequestData $request_data The request data helper.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param SessionHandler $session_handler The session handler.
* @param bool $final_review_enabled Whether the final review is enabled.
* @param WooCommerceOrderCreator $wc_order_creator The WooCommerce order creator.
* @param PayPalGateway $gateway The WC gateway.
*/
public function __construct(
RequestData $request_data,
OrderEndpoint $order_endpoint,
SessionHandler $session_handler
SessionHandler $session_handler,
bool $final_review_enabled,
WooCommerceOrderCreator $wc_order_creator,
PayPalGateway $gateway
) {
$this->request_data = $request_data;
$this->order_endpoint = $order_endpoint;
$this->session_handler = $session_handler;
$this->request_data = $request_data;
$this->order_endpoint = $order_endpoint;
$this->session_handler = $session_handler;
$this->final_review_enabled = $final_review_enabled;
$this->wc_order_creator = $wc_order_creator;
$this->gateway = $gateway;
}
/**
@ -88,6 +123,15 @@ class ApproveSubscriptionEndpoint implements EndpointInterface {
WC()->session->set( 'ppcp_subscription_id', $data['subscription_id'] );
}
$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;
}

View file

@ -246,7 +246,6 @@ 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'] );
@ -262,7 +261,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->should_handle_shipping_in_paypal( $payment_source ) );
$this->purchase_unit = $this->purchase_unit_factory->from_wc_cart( null, $this->should_handle_shipping_in_paypal( $funding_source ) );
// Do not allow completion by webhooks when started via non-checkout buttons,
// it is needed only for some APMs in checkout.
@ -615,16 +614,16 @@ class CreateOrderEndpoint implements EndpointInterface {
/**
* Checks if the shipping should be handled in PayPal popup.
*
* @param string $payment_source The payment source.
* @param string $funding_source The funding 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 {
protected function should_handle_shipping_in_paypal( string $funding_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';
return ! $is_vaulting_enabled || $funding_source !== 'venmo';
}
}

View file

@ -14,12 +14,16 @@ use WC_Cart;
use WC_Order;
use WC_Order_Item_Product;
use WC_Order_Item_Shipping;
use WC_Subscription;
use WC_Subscriptions_Product;
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;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WP_Error;
/**
* Class WooCommerceOrderCreator
@ -40,18 +44,28 @@ class WooCommerceOrderCreator {
*/
protected $session_handler;
/**
* The subscription helper
*
* @var SubscriptionHelper
*/
protected $subscription_helper;
/**
* WooCommerceOrderCreator constructor.
*
* @param FundingSourceRenderer $funding_source_renderer The funding source renderer.
* @param SessionHandler $session_handler The session handler.
* @param SubscriptionHelper $subscription_helper The subscription helper.
*/
public function __construct(
FundingSourceRenderer $funding_source_renderer,
SessionHandler $session_handler
SessionHandler $session_handler,
SubscriptionHelper $subscription_helper
) {
$this->funding_source_renderer = $funding_source_renderer;
$this->session_handler = $session_handler;
$this->subscription_helper = $subscription_helper;
}
/**
@ -69,10 +83,13 @@ class WooCommerceOrderCreator {
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() );
$payer = $order->payer();
$shipping = $order->purchase_units()[0]->shipping();
$this->configure_payment_source( $wc_order );
$this->configure_customer( $wc_order );
$this->configure_line_items( $wc_order, $wc_cart, $payer, $shipping );
$this->configure_shipping( $wc_order, $payer, $shipping );
$this->configure_coupons( $wc_order, $wc_cart->get_applied_coupons() );
$wc_order->calculate_totals();
@ -84,11 +101,13 @@ class WooCommerceOrderCreator {
/**
* Configures the line items.
*
* @param WC_Order $wc_order The WC order.
* @param WC_Cart $wc_cart The Cart.
* @param WC_Order $wc_order The WC order.
* @param WC_Cart $wc_cart The Cart.
* @param Payer|null $payer The payer.
* @param Shipping|null $shipping The shipping.
* @return void
*/
protected function configure_line_items( WC_Order $wc_order, WC_Cart $wc_cart ): void {
protected function configure_line_items( WC_Order $wc_order, WC_Cart $wc_cart, ?Payer $payer, ?Shipping $shipping ): void {
$cart_contents = $wc_cart->get_cart();
foreach ( $cart_contents as $cart_item ) {
@ -111,9 +130,37 @@ class WooCommerceOrderCreator {
return;
}
$total = $product->get_price() * $quantity;
$item->set_name( $product->get_name() );
$item->set_subtotal( $product->get_price() * $quantity );
$item->set_total( $product->get_price() * $quantity );
$item->set_subtotal( $total );
$item->set_total( $total );
$product_id = $product->get_id();
if ( $this->is_subscription( $product_id ) ) {
$subscription = $this->create_subscription( $wc_order, $product_id );
$sign_up_fee = WC_Subscriptions_Product::get_sign_up_fee( $product );
$subscription_total = $total + $sign_up_fee;
$item->set_subtotal( $subscription_total );
$item->set_total( $subscription_total );
$subscription->add_product( $product );
$this->configure_shipping( $subscription, $payer, $shipping );
$this->configure_payment_source( $subscription );
$this->configure_coupons( $subscription, $wc_cart->get_applied_coupons() );
$dates = array(
'trial_end' => WC_Subscriptions_Product::get_trial_expiration_date( $product_id ),
'next_payment' => WC_Subscriptions_Product::get_first_renewal_payment_date( $product_id ),
'end' => WC_Subscriptions_Product::get_expiration_date( $product_id ),
);
$subscription->update_dates( $dates );
$subscription->calculate_totals();
$subscription->payment_complete_for_order( $wc_order );
}
$wc_order->add_item( $item );
}
@ -179,8 +226,18 @@ class WooCommerceOrderCreator {
$shipping->set_method_id( $shipping_options->id() );
$shipping->set_total( $shipping_options->amount()->value_str() );
$items = $wc_order->get_items();
$items_in_package = array();
foreach ( $items as $item ) {
$items_in_package[] = $item->get_name() . ' × ' . (string) $item->get_quantity();
}
$shipping->add_meta_data( __( 'Items', 'woocommerce-paypal-payments' ), implode( ', ', $items_in_package ) );
$wc_order->add_item( $shipping );
}
$wc_order->calculate_totals();
}
/**
@ -225,4 +282,43 @@ class WooCommerceOrderCreator {
}
}
/**
* Checks if the product with given ID is WC subscription.
*
* @param int $product_id The product ID.
* @return bool true if the product is subscription, otherwise false.
*/
protected function is_subscription( int $product_id ): bool {
if ( ! $this->subscription_helper->plugin_is_active() ) {
return false;
}
return WC_Subscriptions_Product::is_subscription( $product_id );
}
/**
* Creates WC subscription from given order and product ID.
*
* @param WC_Order $wc_order The WC order.
* @param int $product_id The product ID.
* @return WC_Subscription The subscription order
* @throws RuntimeException If problem creating.
*/
protected function create_subscription( WC_Order $wc_order, int $product_id ): WC_Subscription {
$subscription = wcs_create_subscription(
array(
'order_id' => $wc_order->get_id(),
'status' => 'pending',
'billing_period' => WC_Subscriptions_Product::get_period( $product_id ),
'billing_interval' => WC_Subscriptions_Product::get_interval( $product_id ),
'customer_id' => $wc_order->get_customer_id(),
)
);
if ( $subscription instanceof WP_Error ) {
throw new RuntimeException( $subscription->get_error_message() );
}
return $subscription;
}
}