Skip to content
129 changes: 129 additions & 0 deletions modules/ppcp-button/src/ButtonModule.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace WooCommerce\PayPalCommerce\Button;

use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ReturnUrlFactory;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint;
Expand All @@ -31,6 +32,7 @@
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;

/**
* Class ButtonModule
Expand Down Expand Up @@ -235,6 +237,10 @@ static function () use ( $container ) {
);
}

private static function is_cross_browser_order( WC_Order $wc_order ): bool {
return wc_string_to_bool( $wc_order->get_meta( PayPalGateway::CROSS_BROWSER_APPSWITCH_META_KEY ) );
}

private function register_appswitch_crossbrowser_handler( ContainerInterface $container ): void {
if ( ! $container->get( 'wcgateway.appswitch-enabled' ) ) {
return;
Expand Down Expand Up @@ -299,10 +305,133 @@ static function () use ( $container ) {

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

$wc_order->update_meta_data( PayPalGateway::CROSS_BROWSER_APPSWITCH_META_KEY, wc_bool_to_string( true ) );
$wc_order->save();

// 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>";
}
);

/**
* By default, WC asks to log in when opening a non-guest Pay for order page as a guest,
* so we disable this for cross-browser AppSwitch.
*
* @param array<string, bool> $allcaps Array of key/value pairs where keys represent a capability name
* and boolean values represent whether the user has that capability.
* @param string[] $caps Required primitive capabilities for the requested capability.
* @param array $args {
* Arguments that accompany the requested capability check.
*
* @type string $0 Requested capability.
* @type int $1 Concerned user ID.
* @type mixed ...$2 Optional second and further parameters, typically object ID.
* }
*
* @returns array<string, bool>
*
* @psalm-suppress MissingClosureParamType
*/
add_filter(
'user_has_cap',
static function ( $allcaps, $cap, $args ) {
if ( ! in_array( 'pay_for_order', $cap, true ) ) {
return $allcaps;
}

$wc_order_id = $args[2] ?? null;
if ( ! is_int( $wc_order_id ) ) {
return $allcaps;
}

$wc_order = wc_get_order( $wc_order_id );
if ( ! $wc_order instanceof WC_Order ) {
return $allcaps;
}

if ( ! self::is_cross_browser_order( $wc_order ) ) {
return $allcaps;
}

return array_merge(
$allcaps,
array(
'pay_for_order' => true,
)
);
},
10,
3
);

/**
* By default, WC asks to log in when opening a non-guest order received page as a guest,
* so we disable this for cross-browser AppSwitch.
*
* @param bool $result
*
* @psalm-suppress MissingClosureParamType
*/
add_filter(
'woocommerce_order_received_verify_known_shoppers',
static function ( $result ) {
if ( ! is_order_received_page() ) {
return $result;
}

$wc_order = null;
// phpcs:disable WordPress.Security.NonceVerification
if ( isset( $_GET['key'] ) && is_string( $_GET['key'] ) ) {
$wc_order_key = sanitize_text_field( wp_unslash( $_GET['key'] ) );
// phpcs:enable WordPress.Security.NonceVerification

$wc_order_id = wc_get_order_id_by_order_key( $wc_order_key );

$wc_order = wc_get_order( $wc_order_id );
}

if ( ! $wc_order instanceof WC_Order ) {
return $result;
}

if ( ! self::is_cross_browser_order( $wc_order ) ) {
return $result;
}

return false;
}
);

/**
* Disabling the email prompt on Pay for order page for guests.
* Should not affect anything in most cases because it is skipped for just created orders (< 10 min).
*
* @param bool $email_verification_required
* @param WC_Order $order
*
* @returns bool
*
* @psalm-suppress MissingClosureParamType
*/
add_filter(
Copy link
Collaborator

@hmouhtar hmouhtar Aug 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AppSwitch flows are short-lived. If the email prompt is already disabled for orders that were created within the last 10 minutes, I think that's enough, and I don't see much value in extending the support timeframe compared to the side effects that removing this prompt might have, since essentially all orders would be marked with CROSS_BROWSER_APPSWITCH_META_KEY. I'm not even sure AppSwitch will work 10 minutes after it was triggered, so I would recommend removing this.

Not a blocker anyway, thanks for working on this @AlexP11223

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I encountered it some time ago, though not sure how. It can be useful not for AppSwitch itself but when it fails and the customer retries using different methods.

essentially all orders would be marked

all orders? 🤔 Only cross-browser are supposed to be marked, which should be quite rare.

'woocommerce_order_email_verification_required',
static function (
$email_verification_required,
$order
) {
if ( ! $order instanceof WC_Order ) {
return $email_verification_required;
}

if ( ! self::is_cross_browser_order( $order ) ) {
return $email_verification_required;
}

return false;
},
10,
2
);
}
}
13 changes: 8 additions & 5 deletions modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public function create_from_paypal_order( Order $order, $cart ): WC_Order {
$shipping = ! empty( $purchase_units ) ? $purchase_units[0]->shipping() : null;

$this->configure_payment_source( $wc_order );
$this->configure_customer( $wc_order );
$this->configure_customer( $wc_order, $cart_data );
$this->configure_line_items( $wc_order, $cart_data, $payer, $shipping );
$this->configure_addresses( $wc_order, $payer, $shipping, $cart_data->needs_shipping() );
$this->configure_coupons( $wc_order, $cart_data->coupons() );
Expand Down Expand Up @@ -293,15 +293,18 @@ protected function configure_payment_source( WC_Order $wc_order ): void {

/**
* Configures the customer ID.
*
* @param WC_Order $wc_order The WC order.
* @return void
*/
protected function configure_customer( WC_Order $wc_order ): void {
protected function configure_customer( WC_Order $wc_order, CartData $cart_data ): void {
$current_user = wp_get_current_user();

if ( $current_user->ID !== 0 ) {
$wc_order->set_customer_id( $current_user->ID );
return;
}

$saved_user_id = $cart_data->user_id();
if ( $saved_user_id !== 0 ) {
$wc_order->set_customer_id( $saved_user_id );
}
}

Expand Down
11 changes: 11 additions & 0 deletions modules/ppcp-button/src/Session/CartData.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class CartData {

protected bool $needs_shipping;

protected int $user_id;

protected string $cart_hash;

protected ?string $paypal_order_id = null;
Expand All @@ -30,17 +32,20 @@ class CartData {
* @param array<string, array<string, mixed>> $items The cart items like in $cart->get_cart_for_session() or $cart->get_cart().
* @param string[] $coupons
* @param bool $needs_shipping
* @param int $user_id
* @param string $cart_hash
*/
public function __construct(
array $items,
array $coupons,
bool $needs_shipping,
int $user_id,
string $cart_hash
) {
$this->items = $items;
$this->coupons = $coupons;
$this->needs_shipping = $needs_shipping;
$this->user_id = $user_id;
$this->cart_hash = $cart_hash;
}

Expand Down Expand Up @@ -78,6 +83,10 @@ public function needs_shipping(): bool {
return $this->needs_shipping;
}

public function user_id(): int {
return $this->user_id;
}

public function cart_hash(): string {
return $this->cart_hash;
}
Expand All @@ -95,6 +104,7 @@ public function to_array(): array {
'items' => $this->items,
'coupons' => $this->coupons,
'needs_shipping' => $this->needs_shipping,
'user_id' => $this->user_id,
'cart_hash' => $this->cart_hash,
'paypal_order_id' => $this->paypal_order_id,
);
Expand All @@ -105,6 +115,7 @@ public static function from_array( array $data, ?string $key = null ): CartData
$data['items'] ?? array(),
$data['coupons'] ?? array(),
(bool) ( $data['needs_shipping'] ?? false ),
(int) ( $data['user_id'] ?? 0 ),
$data['cart_hash'] ?? ''
);
$cart_data->paypal_order_id = $data['paypal_order_id'] ?? null;
Expand Down
1 change: 1 addition & 0 deletions modules/ppcp-button/src/Session/CartDataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public function from_current_cart( ?WC_Cart $cart = null ): CartData {
$cart->get_cart_for_session(),
$cart->get_applied_coupons(),
$cart->needs_shipping(),
get_current_user_id(),
$cart->get_cart_hash()
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,13 +244,13 @@ public function get(): array {
),
'priority' => 14,
),
'pay_later_messaging_is_auto_enabled' => array(
'pay_later_messaging_is_auto_enabled' => array(
'title' => __( 'PayPal Pay Later messaging successfully enabled', 'woocommerce-paypal-payments' ),
'description' => __( 'PayPal is now displaying this flexible payment option earlier in the shopping experience. This update was made based on your “Stay Updated” preference and the messaging can be customized or disabled through the Pay Later settings.', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['pay_later_messaging_is_auto_enabled'],
'action' => array(
'type' => 'tab',
'tab' => 'pay_later_messaging',
'type' => 'tab',
'tab' => 'pay_later_messaging',
),
'priority' => 15,
),
Expand Down
2 changes: 2 additions & 0 deletions modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ class PayPalGateway extends \WC_Payment_Gateway {
public const ORIGINAL_EMAIL_META_KEY = '_ppcp_paypal_billing_email';
public const ORIGINAL_PHONE_META_KEY = '_ppcp_paypal_billing_phone';

public const CROSS_BROWSER_APPSWITCH_META_KEY = '_ppcp_cross_browser_appswitch';

/**
* List of payment sources for which we are expected to store the payer email in the WC Order metadata.
*/
Expand Down
Loading