Skip to content

Commit 5a4d7f3

Browse files
authored
Merge pull request #3569 from woocommerce/PCP-4992-cross-browser-appswitch
Cross browser AppSwitch
2 parents 2a7eedc + fe59ab0 commit 5a4d7f3

File tree

18 files changed

+492
-68
lines changed

18 files changed

+492
-68
lines changed

modules/ppcp-api-client/src/Factory/ReturnUrlFactory.php

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
11
<?php
2-
/**
3-
* Factory for determining the appropriate return URL based on context.
4-
*
5-
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
6-
*/
72

83
declare(strict_types=1);
94

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

149
/**
15-
* Class ReturnUrlFactory
10+
* Factory for determining the appropriate return URL based on context.
1611
*/
1712
class ReturnUrlFactory {
13+
public const PCP_QUERY_ARG = 'pcp-return';
1814

1915
/**
16+
* @param string $context The context, like in ContextTrait.
17+
* @param array<string, mixed> $request_data The request parameters, if exist.
18+
* 'order_id`, 'purchase_units' etc.
19+
* @param array<string, string> $custom_query_args Additional query args to add into the URL.
20+
*
2021
* @throws RuntimeException When required data is missing for the context.
2122
*/
22-
public function from_context( string $context, array $request_data = array() ): string {
23+
public function from_context(
24+
string $context,
25+
array $request_data = array(),
26+
array $custom_query_args = array()
27+
): string {
28+
return add_query_arg(
29+
array_merge(
30+
array( self::PCP_QUERY_ARG => 'button' ),
31+
$custom_query_args
32+
),
33+
$this->wc_url_from_context( $context, $request_data )
34+
);
35+
}
36+
37+
protected function wc_url_from_context( string $context, array $request_data = array() ): string {
2338
switch ( $context ) {
2439
case 'cart':
2540
case 'cart-block':

modules/ppcp-blocks/resources/js/Components/paypal.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
onApproveSavePayment,
2222
} from '../paypal-config';
2323
import { useRef } from 'react';
24+
import Spinner from '../../../../ppcp-button/resources/js/modules/Helper/Spinner';
2425

2526
const PAYPAL_GATEWAY_ID = 'ppcp-gateway';
2627

@@ -52,8 +53,11 @@ export const PayPalComponent = ( {
5253
useState( false );
5354

5455
const [ paypalScriptLoaded, setPaypalScriptLoaded ] = useState( false );
56+
const [ isFullPageSpinnerActive, setIsFullPageSpinnerActive ] =
57+
useState( false );
5558

5659
const paypalButtonRef = useRef( null );
60+
const spinnerRef = useRef( null );
5761

5862
if ( ! paypalScriptLoaded ) {
5963
if ( ! paypalScriptPromise ) {
@@ -70,6 +74,18 @@ export const PayPalComponent = ( {
7074
? `${ config.id }-${ fundingSource }`
7175
: config.id;
7276

77+
// Full-page spinner used to block UI interactions during flows like AppSwitch.
78+
useEffect( () => {
79+
if ( isFullPageSpinnerActive ) {
80+
if ( ! spinnerRef.current ) {
81+
spinnerRef.current = Spinner.fullPage();
82+
}
83+
spinnerRef.current.block();
84+
} else if ( spinnerRef.current ) {
85+
spinnerRef.current.unblock();
86+
}
87+
}, [ isFullPageSpinnerActive ] );
88+
7389
useEffect( () => {
7490
// fill the form if in continuation (for product or mini-cart buttons)
7591
if ( continuationFilled || ! config.scriptData.continuation?.order ) {
@@ -286,6 +302,14 @@ export const PayPalComponent = ( {
286302
};
287303
}, [ onPaymentSetup, paypalOrder, activePaymentMethod ] );
288304

305+
useEffect( () => {
306+
const unsubscribe = onCheckoutFail( () => {
307+
setIsFullPageSpinnerActive( false );
308+
} );
309+
310+
return unsubscribe;
311+
}, [ onCheckoutFail ] );
312+
289313
useEffect( () => {
290314
if ( activePaymentMethod !== methodId ) {
291315
return;
@@ -336,7 +360,8 @@ export const PayPalComponent = ( {
336360
setGotoContinuationOnError,
337361
onSubmit,
338362
onError,
339-
onClose
363+
onClose,
364+
setIsFullPageSpinnerActive
340365
);
341366
},
342367
}
@@ -491,7 +516,8 @@ export const PayPalComponent = ( {
491516
setGotoContinuationOnError,
492517
onSubmit,
493518
onError,
494-
onClose
519+
onClose,
520+
setIsFullPageSpinnerActive
495521
)
496522
}
497523
onShippingOptionsChange={ getOnShippingOptionsChange(

modules/ppcp-blocks/resources/js/paypal-config.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,11 @@ export const handleApprove = async (
6464
setGotoContinuationOnError,
6565
onSubmit,
6666
onError,
67-
onClose
67+
onClose,
68+
setIsFullPageSpinnerActive
6869
) => {
70+
setIsFullPageSpinnerActive( true );
71+
6972
try {
7073
let order;
7174

@@ -168,6 +171,8 @@ export const handleApprove = async (
168171
} catch ( err ) {
169172
console.error( err );
170173

174+
setIsFullPageSpinnerActive( false );
175+
171176
onError( err.message );
172177

173178
onClose();

modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstrap.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import SimulateCart from '../Helper/SimulateCart';
88
import { strRemoveWord, strAddWord, throttle } from '../Helper/Utils';
99
import merge from 'deepmerge';
1010
import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounce';
11+
import ResumeFlowHelper from '../Helper/ResumeFlowHelper';
1112

1213
class SingleProductBootstrap {
1314
constructor( gateway, renderer, errorHandler ) {
@@ -53,7 +54,10 @@ class SingleProductBootstrap {
5354
return;
5455
}
5556

56-
this.render();
57+
// Avoid re-rendering during the resume flow to prevent duplicate onApprove callbacks.
58+
if ( ! ResumeFlowHelper.isResumeFlow() ) {
59+
this.render();
60+
}
5761

5862
this.renderer.enableSmartButtons( this.gateway.button.wrapper );
5963
show( this.gateway.button.wrapper );

modules/ppcp-button/resources/js/modules/Helper/Spinner.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@ class Spinner {
1414
background: '#fff',
1515
opacity: 0.6,
1616
},
17+
baseZ: 10000,
1718
} );
1819
}
1920

2021
unblock() {
2122
jQuery( this.target ).unblock();
2223
}
23-
}
2424

25+
static fullPage() {
26+
return new Spinner( window );
27+
}
28+
}
2529
export default Spinner;

modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import Spinner from '../Helper/Spinner';
2+
13
const initiateRedirect = ( successUrl ) => {
24
/**
35
* Notice how this step initiates a redirect to a new page using a plain
@@ -15,6 +17,10 @@ const initiateRedirect = ( successUrl ) => {
1517

1618
const onApprove = ( context, errorHandler ) => {
1719
return ( data, actions ) => {
20+
// Block the entire page during approval process
21+
const spinner = Spinner.fullPage();
22+
spinner.block();
23+
1824
const canCreateOrder =
1925
! context.config.vaultingEnabled || data.paymentSource !== 'venmo';
2026

@@ -50,6 +56,9 @@ const onApprove = ( context, errorHandler ) => {
5056

5157
const orderReceivedUrl = approveData.data?.order_received_url;
5258
initiateRedirect( orderReceivedUrl || context.config.redirect );
59+
} )
60+
.finally( () => {
61+
spinner.unblock();
5362
} );
5463
};
5564
};

modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import {
2+
getCurrentPaymentMethod,
3+
PaymentMethods,
4+
} from '../Helper/CheckoutMethodState';
5+
16
const onApprove = ( context, errorHandler, spinner ) => {
27
return ( data, actions ) => {
38
spinner.block();
@@ -34,6 +39,15 @@ const onApprove = ( context, errorHandler, spinner ) => {
3439
}
3540
throw new Error( data.data.message );
3641
}
42+
43+
// in some cases a different method may get selected,
44+
// such as when returning from AppSwitch in a different browser and PayPal is not default
45+
if ( ! getCurrentPaymentMethod().startsWith( 'ppcp-' ) ) {
46+
jQuery(
47+
`input[name="payment_method"][value="${ PaymentMethods.PAYPAL }"]`
48+
).prop( 'checked', true );
49+
}
50+
3751
document.querySelector( '#place_order' ).click();
3852
} );
3953
};

modules/ppcp-button/services.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
1919
use WooCommerce\PayPalCommerce\Button\Helper\DisabledFundingSources;
2020
use WooCommerce\PayPalCommerce\Button\Helper\WooCommerceOrderCreator;
21+
use WooCommerce\PayPalCommerce\Button\Session\CartDataFactory;
22+
use WooCommerce\PayPalCommerce\Button\Session\CartDataTransientStorage;
2123
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
2224
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
2325
use WooCommerce\PayPalCommerce\Session\SessionHandler;
@@ -236,6 +238,8 @@ public function get_context(): string {
236238
$session_handler,
237239
$settings,
238240
$early_order_handler,
241+
$container->get( 'button.session.factory.card-data' ),
242+
$container->get( 'button.session.storage.card-data.transient' ),
239243
$registration_needed,
240244
$container->get( 'wcgateway.settings.card_billing_data_mode' ),
241245
$container->get( 'button.early-wc-checkout-validation-enabled' ),
@@ -407,7 +411,15 @@ public function get_context(): string {
407411
return new WooCommerceOrderCreator(
408412
$container->get( 'wcgateway.funding-source.renderer' ),
409413
$container->get( 'session.handler' ),
410-
$container->get( 'wc-subscriptions.helper' )
414+
$container->get( 'wc-subscriptions.helper' ),
415+
$container->get( 'button.session.factory.card-data' )
411416
);
412417
},
418+
419+
'button.session.factory.card-data' => static function ( ContainerInterface $container ): CartDataFactory {
420+
return new CartDataFactory();
421+
},
422+
'button.session.storage.card-data.transient' => static function ( ContainerInterface $container ): CartDataTransientStorage {
423+
return new CartDataTransientStorage();
424+
},
413425
);

modules/ppcp-button/src/ButtonModule.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
namespace WooCommerce\PayPalCommerce\Button;
1111

12+
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
13+
use WooCommerce\PayPalCommerce\ApiClient\Factory\ReturnUrlFactory;
1214
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint;
1315
use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
1416
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
@@ -22,6 +24,8 @@
2224
use WooCommerce\PayPalCommerce\Button\Endpoint\GetOrderEndpoint;
2325
use WooCommerce\PayPalCommerce\Button\Endpoint\StartPayPalVaultingEndpoint;
2426
use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
27+
use WooCommerce\PayPalCommerce\Button\Helper\WooCommerceOrderCreator;
28+
use WooCommerce\PayPalCommerce\Button\Session\CartDataTransientStorage;
2529
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
2630
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
2731
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
@@ -98,6 +102,8 @@ static function ( $value ) use ( $c ) {
98102

99103
$this->register_ajax_endpoints( $c );
100104

105+
$this->register_appswitch_crossbrowser_handler( $c );
106+
101107
return true;
102108
}
103109

@@ -228,4 +234,75 @@ static function () use ( $container ) {
228234
}
229235
);
230236
}
237+
238+
private function register_appswitch_crossbrowser_handler( ContainerInterface $container ): void {
239+
if ( ! $container->get( 'wcgateway.appswitch-enabled' ) ) {
240+
return;
241+
}
242+
243+
// After returning from cross-browser AppSwitch (started in non-default browser, then redirected to the default one)
244+
// we need to retrieve the saved cart and PayPal order, create a WC order and redirect to Pay for order.
245+
add_action(
246+
'wp',
247+
static function () use ( $container ) {
248+
// phpcs:ignore WordPress.Security.NonceVerification
249+
if ( ! isset( $_GET[ ReturnUrlFactory::PCP_QUERY_ARG ] ) ) {
250+
return;
251+
}
252+
253+
if ( is_checkout_pay_page() ) {
254+
return;
255+
}
256+
257+
// phpcs:ignore WordPress.Security.NonceVerification
258+
if ( ! isset( $_GET[ CreateOrderEndpoint::RETURN_URL_CART_QUERY_ARG ] ) ) {
259+
return;
260+
}
261+
262+
// phpcs:ignore WordPress.Security.NonceVerification
263+
$cart_key = wc_clean( wp_unslash( $_GET[ CreateOrderEndpoint::RETURN_URL_CART_QUERY_ARG ] ) );
264+
if ( ! is_string( $cart_key ) ) {
265+
return;
266+
}
267+
268+
$card_data_storage = $container->get( 'button.session.storage.card-data.transient' );
269+
assert( $card_data_storage instanceof CartDataTransientStorage );
270+
271+
$cart_data = $card_data_storage->get( $cart_key );
272+
if ( ! $cart_data ) {
273+
return;
274+
}
275+
276+
// Delete the data to avoid accidentally triggering it again, duplicating orders etc.
277+
$card_data_storage->remove( $cart_data );
278+
279+
if ( ! WC()->cart ) {
280+
return;
281+
}
282+
// The current cart is the same, so we don't need to do anything (probably not cross-browser).
283+
if ( WC()->cart->get_cart_hash() === $cart_data->cart_hash() ) {
284+
return;
285+
}
286+
287+
$paypal_order_id = $cart_data->paypal_order_id();
288+
if ( empty( $paypal_order_id ) ) {
289+
return;
290+
}
291+
292+
$order_endpoint = $container->get( 'api.endpoint.order' );
293+
assert( $order_endpoint instanceof OrderEndpoint );
294+
295+
$paypal_order = $order_endpoint->order( $paypal_order_id );
296+
297+
$wc_order_creator = $container->get( 'button.helper.wc-order-creator' );
298+
assert( $wc_order_creator instanceof WooCommerceOrderCreator );
299+
300+
$wc_order = $wc_order_creator->create_from_paypal_order( $paypal_order, $cart_data );
301+
302+
// Redirect via JS because we need to keep the # parameters which are not accessible on the server side.
303+
// phpcs:ignore WordPress.Security.EscapeOutput
304+
echo "<script>location.href = '" . $wc_order->get_checkout_payment_url() . "' + location.hash;</script>";
305+
}
306+
);
307+
}
231308
}

0 commit comments

Comments
 (0)