Merge branch 'trunk' into PCP-1544-pay-order-currency

This commit is contained in:
Alex P 2023-06-06 15:31:46 +03:00
commit d2a5ecf3b8
No known key found for this signature in database
GPG key ID: 54487A734A204D71
125 changed files with 9483 additions and 789 deletions

View file

@ -9,6 +9,39 @@ class CartActionHandler {
this.errorHandler = errorHandler;
}
subscriptionsConfiguration() {
return {
createSubscription: (data, actions) => {
return actions.subscription.create({
'plan_id': this.config.subscription_plan_id
});
},
onApprove: (data, actions) => {
fetch(this.config.ajax.approve_subscription.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: this.config.ajax.approve_subscription.nonce,
order_id: data.orderID,
subscription_id: data.subscriptionID
})
}).then((res)=>{
return res.json();
}).then((data) => {
if (!data.success) {
console.log(data)
throw Error(data.data.message);
}
location.href = this.config.redirect;
});
},
onError: (err) => {
console.error(err);
}
}
}
configuration() {
const createOrder = (data, actions) => {
const payer = payerData();

View file

@ -11,6 +11,34 @@ class CheckoutActionHandler {
this.spinner = spinner;
}
subscriptionsConfiguration() {
return {
createSubscription: (data, actions) => {
return actions.subscription.create({
'plan_id': this.config.subscription_plan_id
});
},
onApprove: (data, actions) => {
fetch(this.config.ajax.approve_subscription.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: this.config.ajax.approve_subscription.nonce,
order_id: data.orderID,
subscription_id: data.subscriptionID
})
}).then((res)=>{
return res.json();
}).then((data) => {
document.querySelector('#place_order').click();
});
},
onError: (err) => {
console.error(err);
}
}
}
configuration() {
const spinner = this.spinner;
const createOrder = (data, actions) => {
@ -81,7 +109,7 @@ class CheckoutActionHandler {
const input = document.createElement('input');
input.setAttribute('type', 'hidden');
input.setAttribute('name', 'ppcp-resume-order');
input.setAttribute('value', data.data.purchase_units[0].custom_id);
input.setAttribute('value', data.data.custom_id);
document.querySelector(formSelector).appendChild(input);
return data.data.id;
});

View file

@ -18,6 +18,56 @@ class SingleProductActionHandler {
this.errorHandler = errorHandler;
}
subscriptionsConfiguration() {
return {
createSubscription: (data, actions) => {
return actions.subscription.create({
'plan_id': this.config.subscription_plan_id
});
},
onApprove: (data, actions) => {
fetch(this.config.ajax.approve_subscription.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: this.config.ajax.approve_subscription.nonce,
order_id: data.orderID,
subscription_id: data.subscriptionID
})
}).then((res)=>{
return res.json();
}).then(() => {
const id = document.querySelector('[name="add-to-cart"]').value;
const products = [new Product(id, 1, null)];
fetch(this.config.ajax.change_cart.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify({
nonce: this.config.ajax.change_cart.nonce,
products,
})
}).then((result) => {
return result.json();
}).then((result) => {
if (!result.success) {
console.log(result)
throw Error(result.data.message);
}
location.href = this.config.redirect;
})
});
},
onError: (err) => {
console.error(err);
}
}
}
configuration()
{
return {

View file

@ -50,6 +50,14 @@ class CartBootstrap {
this.errorHandler,
);
if(
PayPalCommerceGateway.data_client_id.has_subscriptions
&& PayPalCommerceGateway.data_client_id.paypal_subscriptions_enabled
) {
this.renderer.render(actionHandler.subscriptionsConfiguration());
return;
}
this.renderer.render(
actionHandler.configuration()
);

View file

@ -64,9 +64,15 @@ class CheckoutBootstap {
this.spinner
);
this.renderer.render(
actionHandler.configuration()
);
if(
PayPalCommerceGateway.data_client_id.has_subscriptions
&& PayPalCommerceGateway.data_client_id.paypal_subscriptions_enabled
) {
this.renderer.render(actionHandler.subscriptionsConfiguration(), {}, actionHandler.configuration());
return;
}
this.renderer.render(actionHandler.configuration(), {}, actionHandler.configuration());
}
updateUi() {

View file

@ -110,6 +110,14 @@ class SingleProductBootstap {
this.errorHandler,
);
if(
PayPalCommerceGateway.data_client_id.has_subscriptions
&& PayPalCommerceGateway.data_client_id.paypal_subscriptions_enabled
) {
this.renderer.render(actionHandler.subscriptionsConfiguration());
return;
}
this.renderer.render(
actionHandler.configuration()
);

View file

@ -10,7 +10,7 @@ class Renderer {
this.renderedSources = new Set();
}
render(contextConfig, settingsOverride = {}) {
render(contextConfig, settingsOverride = {}, contextConfigOverride = () => {}) {
const settings = merge(this.defaultSettings, settingsOverride);
const enabledSeparateGateways = Object.fromEntries(Object.entries(
@ -50,7 +50,7 @@ class Renderer {
}
if (this.creditCardRenderer) {
this.creditCardRenderer.render(settings.hosted_fields.wrapper, contextConfig);
this.creditCardRenderer.render(settings.hosted_fields.wrapper, contextConfigOverride);
}
for (const [fundingSource, data] of Object.entries(enabledSeparateGateways)) {

View file

@ -9,11 +9,13 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
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\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Button\Assets\DisabledSmartButton;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButton;
@ -65,25 +67,37 @@ return array(
return $dummy_ids[ $shop_country ] ?? $container->get( 'button.client_id' );
},
'button.smart-button' => static function ( ContainerInterface $container ): SmartButtonInterface {
'button.is_paypal_continuation' => static function( ContainerInterface $container ): bool {
$session_handler = $container->get( 'session.handler' );
$order = $session_handler->order();
if ( ! $order ) {
return false;
}
$source = $order->payment_source();
if ( $source && $source->card() ) {
return false; // Ignore for DCC.
}
if ( 'card' === $session_handler->funding_source() ) {
return false; // Ignore for card buttons.
}
return true;
},
'button.smart-button' => static function ( ContainerInterface $container ): SmartButtonInterface {
$state = $container->get( 'onboarding.state' );
/**
* The state.
*
* @var State $state
*/
if ( $state->current_state() !== State::STATE_ONBOARDED ) {
return new DisabledSmartButton();
}
$settings = $container->get( 'wcgateway.settings' );
$paypal_disabled = ! $settings->has( 'enabled' ) || ! $settings->get( 'enabled' );
if ( $paypal_disabled ) {
return new DisabledSmartButton();
}
$payer_factory = $container->get( 'api.factory.payer' );
$request_data = $container->get( 'button.request-data' );
$client_id = $container->get( 'button.client_id' );
$dcc_applies = $container->get( 'api.helpers.dccapplies' );
$subscription_helper = $container->get( 'subscription.helper' );
@ -110,6 +124,7 @@ return array(
$container->get( 'wcgateway.all-funding-sources' ),
$container->get( 'button.basic-checkout-validation-enabled' ),
$container->get( 'button.early-wc-checkout-validation-enabled' ),
$container->get( 'button.pay-now-contexts' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
@ -119,6 +134,9 @@ return array(
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
'button.pay-now-contexts' => static function ( ContainerInterface $container ): array {
return array( 'checkout', 'pay-now' );
},
'button.request-data' => static function ( ContainerInterface $container ): RequestData {
return new RequestData();
},
@ -156,6 +174,8 @@ return array(
$registration_needed,
$container->get( 'wcgateway.settings.card_billing_data_mode' ),
$container->get( 'button.early-wc-checkout-validation-enabled' ),
$container->get( 'button.pay-now-contexts' ),
$container->get( 'button.handle-shipping-in-paypal' ),
$logger
);
},
@ -187,6 +207,13 @@ return array(
$logger
);
},
'button.endpoint.approve-subscription' => static function( ContainerInterface $container ): ApproveSubscriptionEndpoint {
return new ApproveSubscriptionEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'session.handler' )
);
},
'button.checkout-form-saver' => static function ( ContainerInterface $container ): CheckoutFormSaver {
return new CheckoutFormSaver();
},
@ -266,4 +293,12 @@ return array(
'button.validation.wc-checkout-validator' => static function ( ContainerInterface $container ): CheckoutFormValidator {
return new CheckoutFormValidator();
},
/**
* If true, the shipping methods are sent to PayPal allowing the customer to select it inside the popup.
* May result in slower popup performance, additional loading.
*/
'button.handle-shipping-in-paypal' => static function ( ContainerInterface $container ): bool {
return false;
},
);

View file

@ -26,7 +26,7 @@ class DisabledSmartButton implements SmartButtonInterface {
/**
* Whether the scripts should be loaded.
*/
public function should_load(): bool {
public function should_load_ppcp_script(): bool {
return false;
}

View file

@ -18,6 +18,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
@ -68,13 +69,6 @@ class SmartButton implements SmartButtonInterface {
*/
private $version;
/**
* The Session Handler.
*
* @var SessionHandler
*/
private $session_handler;
/**
* The settings.
*
@ -166,13 +160,6 @@ class SmartButton implements SmartButtonInterface {
*/
protected $early_validation_enabled;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* Cached payment tokens.
*
@ -180,12 +167,33 @@ class SmartButton implements SmartButtonInterface {
*/
private $payment_tokens = null;
/**
* The contexts that should have the Pay Now button.
*
* @var string[]
*/
private $pay_now_contexts;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* Session handler.
*
* @var SessionHandler
*/
private $session_handler;
/**
* SmartButton constructor.
*
* @param string $module_url The URL to the module.
* @param string $version The assets version.
* @param SessionHandler $session_handler The Session Handler.
* @param string $version The assets version.
* @param SessionHandler $session_handler The Session handler.
* @param Settings $settings The Settings.
* @param PayerFactory $payer_factory The Payer factory.
* @param string $client_id The client ID.
@ -200,6 +208,7 @@ class SmartButton implements SmartButtonInterface {
* @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 array $pay_now_contexts The contexts that should have the Pay Now button.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
@ -220,6 +229,7 @@ class SmartButton implements SmartButtonInterface {
array $all_funding_sources,
bool $basic_checkout_validation_enabled,
bool $early_validation_enabled,
array $pay_now_contexts,
LoggerInterface $logger
) {
@ -240,6 +250,7 @@ class SmartButton implements SmartButtonInterface {
$this->all_funding_sources = $all_funding_sources;
$this->basic_checkout_validation_enabled = $basic_checkout_validation_enabled;
$this->early_validation_enabled = $early_validation_enabled;
$this->pay_now_contexts = $pay_now_contexts;
$this->logger = $logger;
}
@ -254,10 +265,6 @@ class SmartButton implements SmartButtonInterface {
$this->render_message_wrapper_registrar();
}
if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) {
return false;
}
if (
$this->settings->has( 'dcc_enabled' )
&& $this->settings->get( 'dcc_enabled' )
@ -284,7 +291,7 @@ class SmartButton implements SmartButtonInterface {
add_filter(
'woocommerce_credit_card_form_fields',
function ( array $default_fields, $id ) use ( $subscription_helper ) : array {
if ( is_user_logged_in() && $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' ) && CreditCardGateway::ID === $id ) {
if ( is_user_logged_in() && $this->settings->has( 'vault_enabled_dcc' ) && $this->settings->get( 'vault_enabled_dcc' ) && CreditCardGateway::ID === $id ) {
$default_fields['card-vault'] = sprintf(
'<p class="form-row form-row-wide"><label for="ppcp-credit-card-vault"><input class="ppcp-credit-card-vault" type="checkbox" id="ppcp-credit-card-vault" name="vault">%s</label></p>',
@ -455,10 +462,6 @@ class SmartButton implements SmartButtonInterface {
add_action(
$this->mini_cart_button_renderer_hook(),
function () {
if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) {
return;
}
if ( $this->is_cart_price_total_zero() || $this->is_free_trial_cart() ) {
return;
}
@ -509,31 +512,70 @@ class SmartButton implements SmartButtonInterface {
}
/**
* Whether the scripts should be loaded.
* Whether any of our scripts (for DCC or product, mini-cart, non-block cart/checkout) should be loaded.
*/
public function should_load(): bool {
public function should_load_ppcp_script(): bool {
$buttons_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' );
if ( ! is_checkout() && ! $buttons_enabled ) {
if ( ! $buttons_enabled ) {
return false;
}
return true;
if ( in_array( $this->context(), array( 'checkout-block', 'cart-block' ), true ) ) {
return false;
}
return $this->should_load_buttons() || $this->can_render_dcc();
}
/**
* Enqueues the scripts.
* Determines whether the button component should be loaded.
*/
public function enqueue(): void {
if ( ! $this->should_load() ) {
return;
public function should_load_buttons() : bool {
$buttons_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' );
if ( ! $buttons_enabled ) {
return false;
}
$load_script = false;
if ( is_checkout() && $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) ) {
$load_script = true;
$smart_button_enabled_for_current_location = $this->settings_status->is_smart_button_enabled_for_location( $this->context() );
$smart_button_enabled_for_mini_cart = $this->settings_status->is_smart_button_enabled_for_location( 'mini-cart' );
$messaging_enabled_for_current_location = $this->settings_status->is_pay_later_messaging_enabled_for_location( $this->context() );
switch ( $this->context() ) {
case 'checkout':
case 'cart':
case 'pay-now':
return $smart_button_enabled_for_current_location || $messaging_enabled_for_current_location;
case 'checkout-block':
case 'cart-block':
return $smart_button_enabled_for_current_location;
case 'product':
return $smart_button_enabled_for_current_location || $messaging_enabled_for_current_location || $smart_button_enabled_for_mini_cart;
default:
return $smart_button_enabled_for_mini_cart;
}
if ( $this->load_button_component() ) {
$load_script = true;
}
/**
* Whether DCC fields can be rendered.
*/
public function can_render_dcc() : bool {
return $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' )
&& $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' )
&& $this->dcc_applies->for_country_currency()
&& in_array( $this->context(), array( 'checkout', 'pay-now' ), true );
}
/**
* Enqueues our scripts/styles (for DCC and product, mini-cart and non-block cart/checkout)
*/
public function enqueue(): void {
if ( $this->can_render_dcc() ) {
wp_enqueue_style(
'ppcp-hosted-fields',
untrailingslashit( $this->module_url ) . '/assets/css/hosted-fields.css',
array(),
$this->version
);
}
if ( in_array( $this->context(), array( 'pay-now', 'checkout' ), true ) ) {
@ -543,31 +585,21 @@ class SmartButton implements SmartButtonInterface {
array(),
$this->version
);
if ( $this->can_render_dcc() ) {
wp_enqueue_style(
'ppcp-hosted-fields',
untrailingslashit( $this->module_url ) . '/assets/css/hosted-fields.css',
array(),
$this->version
);
}
}
if ( $load_script ) {
wp_enqueue_script(
'ppcp-smart-button',
untrailingslashit( $this->module_url ) . '/assets/js/button.js',
array( 'jquery' ),
$this->version,
true
);
wp_localize_script(
'ppcp-smart-button',
'PayPalCommerceGateway',
$this->script_data()
);
}
wp_enqueue_script(
'ppcp-smart-button',
untrailingslashit( $this->module_url ) . '/assets/js/button.js',
array( 'jquery' ),
$this->version,
true
);
wp_localize_script(
'ppcp-smart-button',
'PayPalCommerceGateway',
$this->script_data()
);
}
/**
@ -577,10 +609,6 @@ class SmartButton implements SmartButtonInterface {
*/
public function button_renderer( string $gateway_id ) {
if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) {
return;
}
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
if ( ! isset( $available_gateways[ $gateway_id ] ) ) {
@ -596,9 +624,6 @@ class SmartButton implements SmartButtonInterface {
* Renders the HTML for the credit messaging.
*/
public function message_renderer() {
if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) {
return false;
}
$product = wc_get_product();
@ -666,18 +691,6 @@ class SmartButton implements SmartButtonInterface {
}
/**
* Whether DCC fields can be rendered.
*
* @return bool
* @throws NotFoundException When a setting was not found.
*/
private function can_render_dcc() : bool {
$can_render = $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) && $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' ) && $this->dcc_applies->for_country_currency();
return $can_render;
}
/**
* Renders the HTML for the DCC fields.
*/
@ -710,7 +723,6 @@ class SmartButton implements SmartButtonInterface {
* Whether we can store vault tokens or not.
*
* @return bool
* @throws NotFoundException If a setting hasn't been found.
*/
public function can_save_vault_token(): bool {
@ -744,6 +756,25 @@ class SmartButton implements SmartButtonInterface {
return $this->subscription_helper->cart_contains_subscription();
}
/**
* Whether PayPal subscriptions is enabled or not.
*
* @return bool
*/
private function paypal_subscriptions_enabled(): bool {
if ( defined( 'PPCP_FLAG_SUBSCRIPTIONS_API' ) && ! PPCP_FLAG_SUBSCRIPTIONS_API ) {
return false;
}
try {
$subscriptions_mode = $this->settings->get( 'subscriptions_mode' );
} catch ( NotFoundException $exception ) {
return false;
}
return $subscriptions_mode === 'subscriptions_api';
}
/**
* Retrieves the 3D Secure contingency settings.
*
@ -764,7 +795,6 @@ class SmartButton implements SmartButtonInterface {
* The configuration for the smart buttons.
*
* @return array
* @throws NotFoundException If a setting hasn't been found.
*/
public function script_data(): array {
$is_free_trial_cart = $this->is_free_trial_cart();
@ -777,43 +807,49 @@ class SmartButton implements SmartButtonInterface {
'url_params' => $url_params,
'script_attributes' => $this->attributes(),
'data_client_id' => array(
'set_attribute' => ( is_checkout() && $this->dcc_is_enabled() ) || $this->can_save_vault_token(),
'endpoint' => \WC_AJAX::get_endpoint( DataClientIdEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( DataClientIdEndpoint::nonce() ),
'user' => get_current_user_id(),
'has_subscriptions' => $this->has_subscriptions(),
'set_attribute' => ( is_checkout() && $this->dcc_is_enabled() ) || $this->can_save_vault_token(),
'endpoint' => \WC_AJAX::get_endpoint( DataClientIdEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( DataClientIdEndpoint::nonce() ),
'user' => get_current_user_id(),
'has_subscriptions' => $this->has_subscriptions(),
'paypal_subscriptions_enabled' => $this->paypal_subscriptions_enabled(),
),
'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(
'approve_subscription' => array(
'endpoint' => \WC_AJAX::get_endpoint( ApproveSubscriptionEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ApproveSubscriptionEndpoint::nonce() ),
),
'vault_paypal' => array(
'endpoint' => \WC_AJAX::get_endpoint( StartPayPalVaultingEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( StartPayPalVaultingEndpoint::nonce() ),
),
'save_checkout_form' => array(
'save_checkout_form' => array(
'endpoint' => \WC_AJAX::get_endpoint( SaveCheckoutFormEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( SaveCheckoutFormEndpoint::nonce() ),
),
'validate_checkout' => array(
'validate_checkout' => array(
'endpoint' => \WC_AJAX::get_endpoint( ValidateCheckoutEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ValidateCheckoutEndpoint::nonce() ),
),
'cart_script_params' => array(
'cart_script_params' => array(
'endpoint' => \WC_AJAX::get_endpoint( CartScriptParamsEndpoint::ENDPOINT ),
),
),
'subscription_plan_id' => $this->paypal_subscription_id(),
'enforce_vault' => $this->has_subscriptions(),
'can_save_vault_token' => $this->can_save_vault_token(),
'is_free_trial_cart' => $is_free_trial_cart,
@ -911,6 +947,15 @@ class SmartButton implements SmartButtonInterface {
$localize['button']['style']['tagline'] = false;
}
if ( $this->is_paypal_continuation() ) {
$order = $this->session_handler->order();
assert( $order !== null );
$localize['continuation'] = array(
'order_id' => $order->id(),
);
}
$this->request_data->dequeue_nonce_fix();
return $localize;
}
@ -933,21 +978,24 @@ class SmartButton implements SmartButtonInterface {
* The JavaScript SDK url parameters.
*
* @return array
* @throws NotFoundException If a setting was not found.
*/
private function url_params(): array {
$intent = ( $this->settings->has( 'intent' ) ) ? $this->settings->get( 'intent' ) : 'capture';
$product_intent = $this->subscription_helper->current_product_is_subscription() ? 'authorize' : $intent;
$other_context_intent = $this->subscription_helper->cart_contains_subscription() ? 'authorize' : $intent;
$context = $this->context();
try {
$intent = $this->intent();
} catch ( NotFoundException $exception ) {
$intent = 'capture';
}
$params = array(
$subscription_mode = $this->settings->has( 'subscriptions_mode' ) ? $this->settings->get( 'subscriptions_mode' ) : '';
$params = array(
'client-id' => $this->client_id,
'currency' => $this->currency,
'integration-date' => PAYPAL_INTEGRATION_DATE,
'components' => implode( ',', $this->components() ),
'vault' => $this->can_save_vault_token() ? 'true' : 'false',
'commit' => is_checkout() ? 'true' : 'false',
'intent' => $this->context() === 'product' ? $product_intent : $other_context_intent,
'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',
'intent' => $intent,
);
if (
$this->environment->current_environment_is( Environment::SANDBOX )
@ -991,6 +1039,13 @@ class SmartButton implements SmartButtonInterface {
}
}
if ( in_array( $context, array( 'checkout-block', 'cart-block' ), true ) ) {
$disable_funding = array_diff(
array_keys( $this->all_funding_sources ),
array( 'venmo', 'paylater' )
);
}
if ( $this->is_free_trial_cart() ) {
$all_sources = array_keys( $this->all_funding_sources );
if ( $is_dcc_enabled || $is_separate_card_enabled ) {
@ -1001,8 +1056,8 @@ class SmartButton implements SmartButtonInterface {
$enable_funding = array( 'venmo' );
if ( $this->settings_status->is_pay_later_button_enabled_for_location( $this->context() ) ||
$this->settings_status->is_pay_later_messaging_enabled_for_location( $this->context() )
if ( $this->settings_status->is_pay_later_button_enabled_for_location( $context ) ||
$this->settings_status->is_pay_later_messaging_enabled_for_location( $context )
) {
$enable_funding[] = 'paylater';
} else {
@ -1071,7 +1126,7 @@ class SmartButton implements SmartButtonInterface {
private function components(): array {
$components = array();
if ( $this->load_button_component() ) {
if ( $this->should_load_buttons() ) {
$components[] = 'buttons';
$components[] = 'funding-eligibility';
}
@ -1087,34 +1142,10 @@ class SmartButton implements SmartButtonInterface {
return $components;
}
/**
* Determines whether the button component should be loaded.
*
* @return bool
* @throws NotFoundException If a setting has not been found.
*/
private function load_button_component() : bool {
$smart_button_enabled_for_current_location = $this->settings_status->is_smart_button_enabled_for_location( $this->context() );
$smart_button_enabled_for_mini_cart = $this->settings_status->is_smart_button_enabled_for_location( 'mini-cart' );
$messaging_enabled_for_current_location = $this->settings_status->is_pay_later_messaging_enabled_for_location( $this->context() );
switch ( $this->context() ) {
case 'checkout':
case 'cart':
case 'pay-now':
return $smart_button_enabled_for_current_location || $messaging_enabled_for_current_location;
case 'product':
return $smart_button_enabled_for_current_location || $messaging_enabled_for_current_location || $smart_button_enabled_for_mini_cart;
default:
return $smart_button_enabled_for_mini_cart;
}
}
/**
* Whether DCC is enabled or not.
*
* @return bool
* @throws NotFoundException If a setting has not been found.
*/
private function dcc_is_enabled(): bool {
if ( ! is_checkout() ) {
@ -1143,9 +1174,11 @@ class SmartButton implements SmartButtonInterface {
* @param string $context The context.
*
* @return string
* @throws NotFoundException When a setting hasn't been found.
*/
private function style_for_context( string $style, string $context ): string {
// Use the cart/checkout styles for blocks.
$context = str_replace( '-block', '', $context );
$defaults = array(
'layout' => 'vertical',
'size' => 'responsive',
@ -1362,6 +1395,57 @@ class SmartButton implements SmartButtonInterface {
return false;
}
/**
* Returns PayPal subscription plan id from WC subscription product.
*
* @return string
*/
private function paypal_subscription_id(): string {
if ( $this->subscription_helper->current_product_is_subscription() ) {
$product = wc_get_product();
assert( $product instanceof WC_Product );
if ( $product->get_type() === 'subscription' && $product->meta_exists( 'ppcp_subscription_plan' ) ) {
return $product->get_meta( 'ppcp_subscription_plan' )['id'];
}
}
$cart = WC()->cart ?? null;
if ( ! $cart || $cart->is_empty() ) {
return '';
}
$items = $cart->get_cart_contents();
foreach ( $items as $item ) {
$product = wc_get_product( $item['product_id'] );
assert( $product instanceof WC_Product );
if ( $product->get_type() === 'subscription' && $product->meta_exists( 'ppcp_subscription_plan' ) ) {
return $product->get_meta( 'ppcp_subscription_plan' )['id'];
}
}
return '';
}
/**
* Returns the intent.
*
* @return string
* @throws NotFoundException If intent is not found.
*/
private function intent(): string {
$intent = ( $this->settings->has( 'intent' ) ) ? $this->settings->get( 'intent' ) : 'capture';
$product_intent = $this->subscription_helper->current_product_is_subscription() ? 'authorize' : $intent;
$other_context_intent = $this->subscription_helper->cart_contains_subscription() ? 'authorize' : $intent;
$subscription_mode = $this->settings->has( 'subscriptions_mode' ) ? $this->settings->get( 'subscriptions_mode' ) : '';
if ( $this->subscription_helper->need_subscription_intent( $subscription_mode ) ) {
return 'subscription';
}
return $this->context() === 'product' ? $product_intent : $other_context_intent;
}
/**
* Returns the ID of WC order on the order-pay page, or 0.
*

View file

@ -22,22 +22,15 @@ interface SmartButtonInterface {
public function render_wrapper(): bool;
/**
* Whether the scripts should be loaded.
* Whether any of our scripts (for DCC or product, mini-cart, non-block cart/checkout) should be loaded.
*/
public function should_load(): bool;
public function should_load_ppcp_script(): bool;
/**
* Enqueues the necessary scripts.
* Enqueues our scripts/styles (for DCC and product, mini-cart and non-block cart/checkout)
*/
public function enqueue(): void;
/**
* Whether the running installation could save vault tokens or not.
*
* @return bool
*/
public function can_save_vault_token(): bool;
/**
* The configuration for the smart buttons.
*

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
@ -63,14 +64,12 @@ class ButtonModule implements ModuleInterface {
add_action(
'wp_enqueue_scripts',
static function () use ( $c ) {
$smart_button = $c->get( 'button.smart-button' );
/**
* The Smart Button.
*
* @var SmartButtonInterface $smart_button
*/
$smart_button->enqueue();
assert( $smart_button instanceof SmartButtonInterface );
if ( $smart_button->should_load_ppcp_script() ) {
$smart_button->enqueue();
}
}
);
@ -147,6 +146,16 @@ class ButtonModule implements ModuleInterface {
}
);
add_action(
'wc_ajax_' . ApproveSubscriptionEndpoint::ENDPOINT,
static function () use ( $container ) {
$endpoint = $container->get( 'button.endpoint.approve-subscription' );
assert( $endpoint instanceof ApproveSubscriptionEndpoint );
$endpoint->handle_request();
}
);
add_action(
'wc_ajax_' . CreateOrderEndpoint::ENDPOINT,
static function () use ( $container ) {

View file

@ -180,7 +180,7 @@ class ApproveOrderEndpoint implements EndpointInterface {
);
}
$this->session_handler->replace_order( $order );
wp_send_json_success( $order );
wp_send_json_success();
}
if ( $this->order_helper->contains_physical_goods( $order ) && ! $order->status()->is( OrderStatus::APPROVED ) && ! $order->status()->is( OrderStatus::CREATED ) ) {
@ -198,7 +198,7 @@ class ApproveOrderEndpoint implements EndpointInterface {
$this->session_handler->replace_funding_source( $funding_source );
$this->session_handler->replace_order( $order );
wp_send_json_success( $order );
wp_send_json_success();
return true;
} catch ( Exception $error ) {
$this->logger->error( 'Order approve failed: ' . $error->getMessage() );

View file

@ -0,0 +1,94 @@
<?php
/**
* Endpoint to handle PayPal Subscription created.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
/**
* Class ApproveSubscriptionEndpoint
*/
class ApproveSubscriptionEndpoint implements EndpointInterface {
const ENDPOINT = 'ppc-approve-subscription';
/**
* The request data helper.
*
* @var RequestData
*/
private $request_data;
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
private $order_endpoint;
/**
* The session handler.
*
* @var SessionHandler
*/
private $session_handler;
/**
* ApproveSubscriptionEndpoint constructor.
*
* @param RequestData $request_data The request data helper.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param SessionHandler $session_handler The session handler.
*/
public function __construct(
RequestData $request_data,
OrderEndpoint $order_endpoint,
SessionHandler $session_handler
) {
$this->request_data = $request_data;
$this->order_endpoint = $order_endpoint;
$this->session_handler = $session_handler;
}
/**
* The nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
* @throws RuntimeException When order not found or handling failed.
*/
public function handle_request(): bool {
$data = $this->request_data->read_request( $this->nonce() );
if ( ! isset( $data['order_id'] ) ) {
throw new RuntimeException(
__( 'No order id given', 'woocommerce-paypal-payments' )
);
}
$order = $this->order_endpoint->order( $data['order_id'] );
$this->session_handler->replace_order( $order );
if ( isset( $data['subscription_id'] ) ) {
WC()->session->set( 'ppcp_subscription_id', $data['subscription_id'] );
}
wp_send_json_success();
return true;
}
}

View file

@ -138,6 +138,20 @@ class CreateOrderEndpoint implements EndpointInterface {
*/
protected $early_validation_enabled;
/**
* The contexts that should have the Pay Now button.
*
* @var string[]
*/
private $pay_now_contexts;
/**
* If true, the shipping methods are sent to PayPal allowing the customer to select it inside the popup.
*
* @var bool
*/
private $handle_shipping_in_paypal;
/**
* The logger.
*
@ -159,6 +173,8 @@ class CreateOrderEndpoint implements EndpointInterface {
* @param bool $registration_needed Whether a new user must be registered during checkout.
* @param string $card_billing_data_mode The value of card_billing_data_mode from the settings.
* @param bool $early_validation_enabled Whether to execute WC validation of the checkout form.
* @param string[] $pay_now_contexts The contexts that should have the Pay Now button.
* @param bool $handle_shipping_in_paypal If true, the shipping methods are sent to PayPal allowing the customer to select it inside the popup.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
@ -173,6 +189,8 @@ class CreateOrderEndpoint implements EndpointInterface {
bool $registration_needed,
string $card_billing_data_mode,
bool $early_validation_enabled,
array $pay_now_contexts,
bool $handle_shipping_in_paypal,
LoggerInterface $logger
) {
@ -187,6 +205,8 @@ class CreateOrderEndpoint implements EndpointInterface {
$this->registration_needed = $registration_needed;
$this->card_billing_data_mode = $card_billing_data_mode;
$this->early_validation_enabled = $early_validation_enabled;
$this->pay_now_contexts = $pay_now_contexts;
$this->handle_shipping_in_paypal = $handle_shipping_in_paypal;
$this->logger = $logger;
}
@ -226,7 +246,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();
$this->purchase_unit = $this->purchase_unit_factory->from_wc_cart( null, $this->handle_shipping_in_paypal );
// The cart does not have any info about payment method, so we must handle free trial here.
if ( (
@ -272,7 +292,7 @@ class CreateOrderEndpoint implements EndpointInterface {
! $this->early_order_handler->should_create_early_order()
|| $this->registration_needed
|| isset( $data['createaccount'] ) && '1' === $data['createaccount'] ) {
wp_send_json_success( $order->to_array() );
wp_send_json_success( $this->make_response( $order ) );
}
$this->early_order_handler->register_for_order( $order );
@ -284,7 +304,7 @@ class CreateOrderEndpoint implements EndpointInterface {
$wc_order->save_meta_data();
}
wp_send_json_success( $order->to_array() );
wp_send_json_success( $this->make_response( $order ) );
return true;
} catch ( ValidationException $error ) {
@ -342,7 +362,7 @@ class CreateOrderEndpoint implements EndpointInterface {
* during the "onApprove"-JS callback or the webhook listener.
*/
if ( ! $this->early_order_handler->should_create_early_order() ) {
wp_send_json_success( $order->to_array() );
wp_send_json_success( $this->make_response( $order ) );
}
$this->early_order_handler->register_for_order( $order );
return $data;
@ -385,6 +405,9 @@ class CreateOrderEndpoint implements EndpointInterface {
$funding_source
);
$action = in_array( $this->parsed_request_data['context'], $this->pay_now_contexts, true ) ?
ApplicationContext::USER_ACTION_PAY_NOW : ApplicationContext::USER_ACTION_CONTINUE;
if ( 'card' === $funding_source ) {
if ( CardBillingMode::MINIMAL_INPUT === $this->card_billing_data_mode ) {
if ( ApplicationContext::SHIPPING_PREFERENCE_SET_PROVIDED_ADDRESS === $shipping_preference ) {
@ -410,7 +433,9 @@ class CreateOrderEndpoint implements EndpointInterface {
$shipping_preference,
$payer,
null,
$this->payment_method()
$this->payment_method(),
'',
$action
);
} catch ( PayPalApiException $exception ) {
// Looks like currently there is no proper way to validate the shipping address for PayPal,
@ -544,4 +569,17 @@ class CreateOrderEndpoint implements EndpointInterface {
);
}
}
/**
* Returns the response data for success response.
*
* @param Order $order The PayPal order.
* @return array
*/
private function make_response( Order $order ): array {
return array(
'id' => $order->id(),
'custom_id' => $order->purchase_units()[0]->custom_id(),
);
}
}

View file

@ -17,24 +17,32 @@ trait ContextTrait {
* @return string
*/
protected function context(): string {
$context = 'mini-cart';
if ( is_product() || wc_post_content_has_shortcode( 'product_page' ) ) {
$context = 'product';
return 'product';
}
// has_block may not work if called too early, such as during the block registration.
if ( has_block( 'woocommerce/cart' ) ) {
return 'cart-block';
}
if ( is_cart() ) {
$context = 'cart';
}
if ( is_checkout() && ! $this->is_paypal_continuation() ) {
$context = 'checkout';
return 'cart';
}
if ( is_checkout_pay_page() ) {
$context = 'pay-now';
return 'pay-now';
}
return $context;
if ( has_block( 'woocommerce/checkout' ) ) {
return 'checkout-block';
}
if ( ( is_checkout() ) && ! $this->is_paypal_continuation() ) {
return 'checkout';
}
return 'mini-cart';
}
/**