mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-08-30 05:00:51 +08:00
Show shipping methods in paypal if express without review
This commit is contained in:
parent
8efa0e6da3
commit
9d638a57e3
12 changed files with 355 additions and 16 deletions
|
@ -9,6 +9,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient;
|
||||
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory;
|
||||
use WooCommerce\PayPalCommerce\Session\SessionHandler;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
|
||||
|
@ -289,12 +290,19 @@ return array(
|
|||
);
|
||||
},
|
||||
'api.factory.shipping' => static function ( ContainerInterface $container ): ShippingFactory {
|
||||
$address_factory = $container->get( 'api.factory.address' );
|
||||
return new ShippingFactory( $address_factory );
|
||||
return new ShippingFactory(
|
||||
$container->get( 'api.factory.address' ),
|
||||
$container->get( 'api.factory.shipping-option' )
|
||||
);
|
||||
},
|
||||
'api.factory.shipping-preference' => static function ( ContainerInterface $container ): ShippingPreferenceFactory {
|
||||
return new ShippingPreferenceFactory();
|
||||
},
|
||||
'api.factory.shipping-option' => static function ( ContainerInterface $container ): ShippingOptionFactory {
|
||||
return new ShippingOptionFactory(
|
||||
$container->get( 'api.factory.money' )
|
||||
);
|
||||
},
|
||||
'api.factory.amount' => static function ( ContainerInterface $container ): AmountFactory {
|
||||
$item_factory = $container->get( 'api.factory.item' );
|
||||
return new AmountFactory(
|
||||
|
|
|
@ -28,15 +28,24 @@ class Shipping {
|
|||
*/
|
||||
private $address;
|
||||
|
||||
/**
|
||||
* Shipping methods.
|
||||
*
|
||||
* @var ShippingOption[]
|
||||
*/
|
||||
private $options;
|
||||
|
||||
/**
|
||||
* Shipping constructor.
|
||||
*
|
||||
* @param string $name The name.
|
||||
* @param Address $address The address.
|
||||
* @param string $name The name.
|
||||
* @param Address $address The address.
|
||||
* @param ShippingOption[] $options Shipping methods.
|
||||
*/
|
||||
public function __construct( string $name, Address $address ) {
|
||||
public function __construct( string $name, Address $address, array $options = array() ) {
|
||||
$this->name = $name;
|
||||
$this->address = $address;
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -57,17 +66,35 @@ class Shipping {
|
|||
return $this->address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shipping methods.
|
||||
*
|
||||
* @return ShippingOption[]
|
||||
*/
|
||||
public function options(): array {
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the object as array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array(
|
||||
$result = array(
|
||||
'name' => array(
|
||||
'full_name' => $this->name(),
|
||||
),
|
||||
'address' => $this->address()->to_array(),
|
||||
);
|
||||
if ( $this->options ) {
|
||||
$result['options'] = array_map(
|
||||
function ( ShippingOption $opt ): array {
|
||||
return $opt->to_array();
|
||||
},
|
||||
$this->options
|
||||
);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
|
139
modules/ppcp-api-client/src/Entity/ShippingOption.php
Normal file
139
modules/ppcp-api-client/src/Entity/ShippingOption.php
Normal file
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
/**
|
||||
* The ShippingOption object.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
|
||||
|
||||
/**
|
||||
* Class ShippingOption
|
||||
*/
|
||||
class ShippingOption {
|
||||
const TYPE_SHIPPING = 'SHIPPING';
|
||||
const TYPE_PICKUP = 'PICKUP';
|
||||
|
||||
/**
|
||||
* The name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* The label.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $label;
|
||||
|
||||
/**
|
||||
* Whether the method is selected by default.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $selected;
|
||||
|
||||
/**
|
||||
* The price.
|
||||
*
|
||||
* @var Money
|
||||
*/
|
||||
private $amount;
|
||||
|
||||
/**
|
||||
* SHIPPING or PICKUP.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* ShippingOption constructor.
|
||||
*
|
||||
* @param string $id The name.
|
||||
* @param string $label The label.
|
||||
* @param bool $selected Whether the method is selected by default.
|
||||
* @param Money $amount The price.
|
||||
* @param string $type SHIPPING or PICKUP.
|
||||
*/
|
||||
public function __construct( string $id, string $label, bool $selected, Money $amount, string $type ) {
|
||||
$this->id = $id;
|
||||
$this->label = $label;
|
||||
$this->selected = $selected;
|
||||
$this->amount = $amount;
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function id(): string {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* The label.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function label(): string {
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the method is selected by default.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function selected(): bool {
|
||||
return $this->selected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the method is selected by default.
|
||||
*
|
||||
* @param bool $selected The value to be set.
|
||||
*/
|
||||
public function set_selected( bool $selected ): void {
|
||||
$this->selected = $selected;
|
||||
}
|
||||
|
||||
/**
|
||||
* The price.
|
||||
*
|
||||
* @return Money
|
||||
*/
|
||||
public function amount(): Money {
|
||||
return $this->amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* SHIPPING or PICKUP.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function type(): string {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the object as array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array(
|
||||
'id' => $this->id,
|
||||
'label' => $this->label,
|
||||
'selected' => $this->selected,
|
||||
'amount' => $this->amount->to_array(),
|
||||
'type' => $this->type,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -153,10 +153,11 @@ class PurchaseUnitFactory {
|
|||
* Creates a PurchaseUnit based off a WooCommerce cart.
|
||||
*
|
||||
* @param \WC_Cart|null $cart The cart.
|
||||
* @param bool $with_shipping_options Include WC shipping methods.
|
||||
*
|
||||
* @return PurchaseUnit
|
||||
*/
|
||||
public function from_wc_cart( ?\WC_Cart $cart = null ): PurchaseUnit {
|
||||
public function from_wc_cart( ?\WC_Cart $cart = null, bool $with_shipping_options = false ): PurchaseUnit {
|
||||
if ( ! $cart ) {
|
||||
$cart = WC()->cart ?? new \WC_Cart();
|
||||
}
|
||||
|
@ -172,7 +173,7 @@ class PurchaseUnitFactory {
|
|||
$shipping = null;
|
||||
$customer = \WC()->customer;
|
||||
if ( $this->shipping_needed( ... array_values( $items ) ) && is_a( $customer, \WC_Customer::class ) ) {
|
||||
$shipping = $this->shipping_factory->from_wc_customer( \WC()->customer );
|
||||
$shipping = $this->shipping_factory->from_wc_customer( \WC()->customer, $with_shipping_options );
|
||||
if (
|
||||
2 !== strlen( $shipping->address()->country_code() ) ||
|
||||
( ! $shipping->address()->postal_code() && ! $this->country_without_postal_code( $shipping->address()->country_code() ) )
|
||||
|
|
|
@ -10,6 +10,7 @@ declare(strict_types=1);
|
|||
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
|
||||
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Shipping;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\ShippingOption;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
|
@ -24,23 +25,33 @@ class ShippingFactory {
|
|||
*/
|
||||
private $address_factory;
|
||||
|
||||
/**
|
||||
* The shipping option factory.
|
||||
*
|
||||
* @var ShippingOptionFactory
|
||||
*/
|
||||
private $shipping_option_factory;
|
||||
|
||||
/**
|
||||
* ShippingFactory constructor.
|
||||
*
|
||||
* @param AddressFactory $address_factory The address factory.
|
||||
* @param AddressFactory $address_factory The address factory.
|
||||
* @param ShippingOptionFactory $shipping_option_factory The shipping option factory.
|
||||
*/
|
||||
public function __construct( AddressFactory $address_factory ) {
|
||||
$this->address_factory = $address_factory;
|
||||
public function __construct( AddressFactory $address_factory, ShippingOptionFactory $shipping_option_factory ) {
|
||||
$this->address_factory = $address_factory;
|
||||
$this->shipping_option_factory = $shipping_option_factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a shipping object based off a WooCommerce customer.
|
||||
*
|
||||
* @param \WC_Customer $customer The WooCommerce customer.
|
||||
* @param bool $with_shipping_options Include WC shipping methods.
|
||||
*
|
||||
* @return Shipping
|
||||
*/
|
||||
public function from_wc_customer( \WC_Customer $customer ): Shipping {
|
||||
public function from_wc_customer( \WC_Customer $customer, bool $with_shipping_options = false ): Shipping {
|
||||
// Replicates the Behavior of \WC_Order::get_formatted_shipping_full_name().
|
||||
$full_name = sprintf(
|
||||
// translators: %1$s is the first name and %2$s is the second name. wc translation.
|
||||
|
@ -51,7 +62,8 @@ class ShippingFactory {
|
|||
$address = $this->address_factory->from_wc_customer( $customer );
|
||||
return new Shipping(
|
||||
$full_name,
|
||||
$address
|
||||
$address,
|
||||
$with_shipping_options ? $this->shipping_option_factory->from_wc_cart() : array()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -91,9 +103,14 @@ class ShippingFactory {
|
|||
);
|
||||
}
|
||||
$address = $this->address_factory->from_paypal_response( $data->address );
|
||||
$options = array_map(
|
||||
array( $this->shipping_option_factory, 'from_paypal_response' ),
|
||||
$data->options ?? array()
|
||||
);
|
||||
return new Shipping(
|
||||
$data->name->full_name,
|
||||
$address
|
||||
$address,
|
||||
$options
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
111
modules/ppcp-api-client/src/Factory/ShippingOptionFactory.php
Normal file
111
modules/ppcp-api-client/src/Factory/ShippingOptionFactory.php
Normal file
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
/**
|
||||
* The shipping options factory.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
|
||||
|
||||
use stdClass;
|
||||
use WC_Cart;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\ShippingOption;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* Class ShippingOptionFactory
|
||||
*/
|
||||
class ShippingOptionFactory {
|
||||
|
||||
/**
|
||||
* The Money factory.
|
||||
*
|
||||
* @var MoneyFactory
|
||||
*/
|
||||
private $money_factory;
|
||||
|
||||
/**
|
||||
* ShippingOptionFactory constructor.
|
||||
*
|
||||
* @param MoneyFactory $money_factory The Money factory.
|
||||
*/
|
||||
public function __construct( MoneyFactory $money_factory ) {
|
||||
$this->money_factory = $money_factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an array of ShippingOption objects for the shipping methods available in the cart.
|
||||
*
|
||||
* @param WC_Cart|null $cart The cart.
|
||||
* @return ShippingOption[]
|
||||
*/
|
||||
public function from_wc_cart( ?WC_Cart $cart = null ): array {
|
||||
if ( ! $cart ) {
|
||||
$cart = WC()->cart ?? new WC_Cart();
|
||||
}
|
||||
|
||||
$cart->calculate_shipping();
|
||||
|
||||
$chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', array() );
|
||||
if ( ! is_array( $chosen_shipping_methods ) ) {
|
||||
$chosen_shipping_methods = array();
|
||||
}
|
||||
|
||||
$packages = WC()->shipping()->get_packages();
|
||||
$options = array();
|
||||
foreach ( $packages as $package ) {
|
||||
$rates = $package['rates'] ?? array();
|
||||
foreach ( $rates as $rate ) {
|
||||
if ( ! $rate instanceof \WC_Shipping_Rate ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$options[] = new ShippingOption(
|
||||
$rate->get_id(),
|
||||
$rate->get_label(),
|
||||
in_array( $rate->get_id(), $chosen_shipping_methods, true ),
|
||||
new Money(
|
||||
(float) $rate->get_cost(),
|
||||
get_woocommerce_currency()
|
||||
),
|
||||
ShippingOption::TYPE_SHIPPING
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $chosen_shipping_methods && $options ) {
|
||||
$options[0]->set_selected( true );
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ShippingOption object from the PayPal JSON object.
|
||||
*
|
||||
* @param stdClass $data The JSON object.
|
||||
*
|
||||
* @return ShippingOption
|
||||
* @throws RuntimeException When JSON object is malformed.
|
||||
*/
|
||||
public function from_paypal_response( stdClass $data ): ShippingOption {
|
||||
if ( ! isset( $data->id ) ) {
|
||||
throw new RuntimeException( 'No id was given for shipping option.' );
|
||||
}
|
||||
if ( ! isset( $data->amount ) ) {
|
||||
throw new RuntimeException( 'Shipping option amount not found' );
|
||||
}
|
||||
|
||||
$amount = $this->money_factory->from_paypal_response( $data->amount );
|
||||
return new ShippingOption(
|
||||
$data->id,
|
||||
$data->label ?? '',
|
||||
isset( $data->selected ) ? (bool) $data->selected : false,
|
||||
$amount,
|
||||
$data->type ?? ShippingOption::TYPE_SHIPPING
|
||||
);
|
||||
}
|
||||
}
|
|
@ -63,4 +63,8 @@ return array(
|
|||
|
||||
return $contexts;
|
||||
},
|
||||
|
||||
'button.handle-shipping-in-paypal' => function ( ContainerInterface $container ): bool {
|
||||
return ! $container->get( 'blocks.settings.final_review_enabled' );
|
||||
},
|
||||
);
|
||||
|
|
|
@ -15,6 +15,7 @@ const PayPalComponent = ({
|
|||
eventRegistration,
|
||||
emitResponse,
|
||||
activePaymentMethod,
|
||||
shippingData,
|
||||
}) => {
|
||||
const {onPaymentSetup} = eventRegistration;
|
||||
const {responseTypes} = emitResponse;
|
||||
|
@ -129,6 +130,16 @@ const PayPalComponent = ({
|
|||
onClick();
|
||||
};
|
||||
|
||||
let handleShippingChange = null;
|
||||
if (shippingData.needsShipping && !config.finalReviewEnabled) {
|
||||
handleShippingChange = (data) => {
|
||||
console.log(data)
|
||||
|
||||
const shippingOptionId = data.selected_shipping_option.id;
|
||||
shippingData.setSelectedRates(shippingOptionId)
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (activePaymentMethod !== config.id) {
|
||||
return;
|
||||
|
@ -187,6 +198,7 @@ const PayPalComponent = ({
|
|||
onError={onClose}
|
||||
createOrder={createOrder}
|
||||
onApprove={handleApprove}
|
||||
onShippingChange={handleShippingChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -161,6 +161,7 @@ return array(
|
|||
$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
|
||||
);
|
||||
},
|
||||
|
@ -271,4 +272,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;
|
||||
},
|
||||
);
|
||||
|
|
|
@ -145,6 +145,13 @@ class CreateOrderEndpoint implements EndpointInterface {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -167,6 +174,7 @@ class CreateOrderEndpoint implements EndpointInterface {
|
|||
* @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(
|
||||
|
@ -182,6 +190,7 @@ class CreateOrderEndpoint implements EndpointInterface {
|
|||
string $card_billing_data_mode,
|
||||
bool $early_validation_enabled,
|
||||
array $pay_now_contexts,
|
||||
bool $handle_shipping_in_paypal,
|
||||
LoggerInterface $logger
|
||||
) {
|
||||
|
||||
|
@ -197,6 +206,7 @@ class CreateOrderEndpoint implements EndpointInterface {
|
|||
$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;
|
||||
}
|
||||
|
||||
|
@ -236,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 ( (
|
||||
|
|
|
@ -294,7 +294,7 @@ class PurchaseUnitFactoryTest extends TestCase
|
|||
$shippingFactory = Mockery::mock(ShippingFactory::class);
|
||||
$shippingFactory
|
||||
->expects('from_wc_customer')
|
||||
->with($wcCustomer)
|
||||
->with($wcCustomer, false)
|
||||
->andReturn($shipping);
|
||||
$paymentsFacory = Mockery::mock(PaymentsFactory::class);
|
||||
$testee = new PurchaseUnitFactory(
|
||||
|
|
|
@ -168,6 +168,7 @@ class CreateOrderEndpointTest extends TestCase
|
|||
CardBillingMode::MINIMAL_INPUT,
|
||||
false,
|
||||
['checkout'],
|
||||
false,
|
||||
new NullLogger()
|
||||
);
|
||||
return array($payer_factory, $testee);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue