Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions modules/ppcp-api-client/src/Factory/ReturnUrlFactory.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
<?php
/**
* Factory for determining the appropriate return URL based on context.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/

declare(strict_types=1);

Expand All @@ -12,14 +7,34 @@
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;

/**
* Class ReturnUrlFactory
* Factory for determining the appropriate return URL based on context.
*/
class ReturnUrlFactory {
public const PCP_QUERY_ARG = 'pcp-return';

/**
* @param string $context The context, like in ContextTrait.
* @param array<string, mixed> $request_data The request parameters, if exist.
* 'order_id`, 'purchase_units' etc.
* @param array<string, string> $custom_query_args Additional query args to add into the URL.
*
* @throws RuntimeException When required data is missing for the context.
*/
public function from_context( string $context, array $request_data = array() ): string {
public function from_context(
string $context,
array $request_data = array(),
array $custom_query_args = array()
): string {
return add_query_arg(
array_merge(
array( self::PCP_QUERY_ARG => 'button' ),
$custom_query_args
),
$this->wc_url_from_context( $context, $request_data )
);
}

protected function wc_url_from_context( string $context, array $request_data = array() ): string {
switch ( $context ) {
case 'cart':
case 'cart-block':
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import {
getCurrentPaymentMethod,
PaymentMethods,
} from '../Helper/CheckoutMethodState';

const onApprove = ( context, errorHandler, spinner ) => {
return ( data, actions ) => {
spinner.block();
Expand Down Expand Up @@ -34,6 +39,13 @@ const onApprove = ( context, errorHandler, spinner ) => {
}
throw new Error( data.data.message );
}

if ( ! getCurrentPaymentMethod().startsWith( 'ppcp-' ) ) {
jQuery(
`input[name="payment_method"][value="${ PaymentMethods.PAYPAL }"]`
).prop( 'checked', true );
}

document.querySelector( '#place_order' ).click();
} );
};
Expand Down
14 changes: 13 additions & 1 deletion modules/ppcp-button/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Button\Helper\DisabledFundingSources;
use WooCommerce\PayPalCommerce\Button\Helper\WooCommerceOrderCreator;
use WooCommerce\PayPalCommerce\Button\Session\CartDataFactory;
use WooCommerce\PayPalCommerce\Button\Session\CartDataTransientStorage;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
Expand Down Expand Up @@ -239,6 +241,8 @@ public function get_context(): string {
$session_handler,
$settings,
$early_order_handler,
$container->get( 'button.session.factory.card-data' ),
$container->get( 'button.session.storage.card-data.transient' ),
$registration_needed,
$container->get( 'wcgateway.settings.card_billing_data_mode' ),
$container->get( 'button.early-wc-checkout-validation-enabled' ),
Expand Down Expand Up @@ -410,7 +414,15 @@ public function get_context(): string {
return new WooCommerceOrderCreator(
$container->get( 'wcgateway.funding-source.renderer' ),
$container->get( 'session.handler' ),
$container->get( 'wc-subscriptions.helper' )
$container->get( 'wc-subscriptions.helper' ),
$container->get( 'button.session.factory.card-data' )
);
},

'button.session.factory.card-data' => static function ( ContainerInterface $container ): CartDataFactory {
return new CartDataFactory();
},
'button.session.storage.card-data.transient' => static function ( ContainerInterface $container ): CartDataTransientStorage {
return new CartDataTransientStorage();
},
);
77 changes: 77 additions & 0 deletions modules/ppcp-button/src/ButtonModule.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

namespace WooCommerce\PayPalCommerce\Button;

use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ReturnUrlFactory;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
Expand All @@ -22,6 +24,8 @@
use WooCommerce\PayPalCommerce\Button\Endpoint\GetOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\StartPayPalVaultingEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
use WooCommerce\PayPalCommerce\Button\Helper\WooCommerceOrderCreator;
use WooCommerce\PayPalCommerce\Button\Session\CartDataTransientStorage;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
Expand Down Expand Up @@ -98,6 +102,8 @@ static function ( $value ) use ( $c ) {

$this->register_ajax_endpoints( $c );

$this->register_appswitch_crossbrowser_handler( $c );

return true;
}

Expand Down Expand Up @@ -228,4 +234,75 @@ static function () use ( $container ) {
}
);
}

private function register_appswitch_crossbrowser_handler( ContainerInterface $container ): void {
if ( ! $container->get( 'wcgateway.appswitch-enabled' ) ) {
return;
}

// After returning from cross-browser AppSwitch (started in non-default browser, then redirected to the default one)
// we need to retrieve the saved cart and PayPal order, create a WC order and redirect to Pay for order.
add_action(
'wp',
static function () use ( $container ) {
// phpcs:ignore WordPress.Security.NonceVerification
if ( ! isset( $_GET[ ReturnUrlFactory::PCP_QUERY_ARG ] ) ) {
return;
}

if ( is_checkout_pay_page() ) {
return;
}

// phpcs:ignore WordPress.Security.NonceVerification
if ( ! isset( $_GET[ CreateOrderEndpoint::RETURN_URL_CART_QUERY_ARG ] ) ) {
return;
}

// phpcs:ignore WordPress.Security.NonceVerification
$cart_key = wc_clean( wp_unslash( $_GET[ CreateOrderEndpoint::RETURN_URL_CART_QUERY_ARG ] ) );
if ( ! is_string( $cart_key ) ) {
return;
}

$card_data_storage = $container->get( 'button.session.storage.card-data.transient' );
assert( $card_data_storage instanceof CartDataTransientStorage );

$cart_data = $card_data_storage->get( $cart_key );
if ( ! $cart_data ) {
return;
}

// Delete the data to avoid accidentally triggering it again, duplicating orders etc.
$card_data_storage->remove( $cart_data );

if ( ! WC()->cart ) {
return;
}
// The current cart is the same, so we don't need to do anything (probably not cross-browser).
if ( WC()->cart->get_cart_hash() === $cart_data->cart_hash() ) {
return;
}

$paypal_order_id = $cart_data->paypal_order_id();
if ( empty( $paypal_order_id ) ) {
return;
}

$order_endpoint = $container->get( 'api.endpoint.order' );
assert( $order_endpoint instanceof OrderEndpoint );

$paypal_order = $order_endpoint->order( $paypal_order_id );

$wc_order_creator = $container->get( 'button.helper.wc-order-creator' );
assert( $wc_order_creator instanceof WooCommerceOrderCreator );

$wc_order = $wc_order_creator->create_from_paypal_order( $paypal_order, $cart_data );

// Redirect via JS because we need to keep the # parameters which are not accessible on the server side.
// phpcs:ignore WordPress.Security.EscapeOutput
echo "<script>location.href = '" . $wc_order->get_checkout_payment_url() . "' + location.hash;</script>";
}
);
}
}
66 changes: 55 additions & 11 deletions modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
use WooCommerce\PayPalCommerce\ApiClient\Factory\ReturnUrlFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\Button\Exception\ValidationException;
use WooCommerce\PayPalCommerce\Button\Session\CartDataFactory;
use WooCommerce\PayPalCommerce\Button\Session\CartDataTransientStorage;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
Expand All @@ -50,6 +52,8 @@ class CreateOrderEndpoint implements EndpointInterface {

const ENDPOINT = 'ppc-create-order';

public const RETURN_URL_CART_QUERY_ARG = 'pcp-cart';

/**
* The request data helper.
*
Expand Down Expand Up @@ -118,6 +122,10 @@ class CreateOrderEndpoint implements EndpointInterface {
*/
private $early_order_handler;

protected CartDataFactory $cart_data_factory;

protected CartDataTransientStorage $cart_data_transient_storage;

/**
* Data from the request.
*
Expand Down Expand Up @@ -207,6 +215,8 @@ class CreateOrderEndpoint implements EndpointInterface {
* @param SessionHandler $session_handler The SessionHandler object.
* @param Settings $settings The Settings object.
* @param EarlyOrderHandler $early_order_handler The EarlyOrderHandler object.
* @param CartDataFactory $cart_data_factory
* @param CartDataTransientStorage $cart_data_transient_storage
* @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.
Expand All @@ -228,6 +238,8 @@ public function __construct(
SessionHandler $session_handler,
Settings $settings,
EarlyOrderHandler $early_order_handler,
CartDataFactory $cart_data_factory,
CartDataTransientStorage $cart_data_transient_storage,
bool $registration_needed,
string $card_billing_data_mode,
bool $early_validation_enabled,
Expand All @@ -249,6 +261,8 @@ public function __construct(
$this->session_handler = $session_handler;
$this->settings = $settings;
$this->early_order_handler = $early_order_handler;
$this->cart_data_factory = $cart_data_factory;
$this->cart_data_transient_storage = $cart_data_transient_storage;
$this->registration_needed = $registration_needed;
$this->card_billing_data_mode = $card_billing_data_mode;
$this->early_validation_enabled = $early_validation_enabled;
Expand Down Expand Up @@ -475,33 +489,52 @@ private function create_paypal_order( ?WC_Order $wc_order = null, string $paymen
$payment_source_key
);

$custom_args = array();

$cart_data = null;
// Save the cart to be able to access it after cross-browser AppSwitch.
if ( 'pay-now' !== $data['context'] ) {
try {
$cart_data = $this->cart_data_factory->from_current_cart();
$cart_data->generate_key();

$key = $cart_data->key();
assert( ! empty( $key ) );

$custom_args[ self::RETURN_URL_CART_QUERY_ARG ] = $key;
} catch ( Throwable $exception ) {
$this->logger->error( 'Failed to serialize cart: ' . $exception->getMessage() );
}
}

$return_url = $this->return_url_factory->from_context(
$this->parsed_request_data['context'],
$this->parsed_request_data,
$custom_args
);

$experience_context = $this->experience_context_builder
->with_default_paypal_config( $shipping_preference, $action )
->with_contact_preference( $contact_preference );
->with_contact_preference( $contact_preference )
->with_custom_return_url( $return_url )
->with_custom_cancel_url( $return_url );

if ( $this->server_side_shipping_callback_enabled
&& $shipping_preference === ExperienceContext::SHIPPING_PREFERENCE_GET_FROM_FILE ) {
$experience_context = $experience_context->with_shipping_callback();
}

$return_url = $this->return_url_factory->from_context(
$this->parsed_request_data['context'],
$this->parsed_request_data
);

$payment_source = new PaymentSource(
$payment_source_key,
(object) array(
'experience_context' => $experience_context
->with_custom_return_url( $return_url )
->with_custom_cancel_url( $return_url )
->build()
->to_array(),
)
);

try {
return $this->api_endpoint->create(
$order = $this->api_endpoint->create(
array( $this->purchase_unit ),
$shipping_preference,
$payer,
Expand All @@ -524,18 +557,29 @@ function ( stdClass $detail ): bool {

$this->purchase_unit->set_shipping( null );

return $this->api_endpoint->create(
$order = $this->api_endpoint->create(
array( $this->purchase_unit ),
$shipping_preference,
$payer,
$payment_method,
$data,
$payment_source
);
} else {
throw $exception;
}
}

throw $exception;
if ( $cart_data ) {
try {
$cart_data->set_paypal_order_id( $order->id() );
$this->cart_data_transient_storage->save( $cart_data );
} catch ( Throwable $exception ) {
$this->logger->error( 'Failed to save cart: ' . $exception->getMessage() );
}
}

return $order;
}

/**
Expand Down
Loading
Loading