Skip to content

Commit 75aace4

Browse files
committed
Migrate AppSwitch cross-browser handling logic to frontend
1 parent 9c861d4 commit 75aace4

File tree

8 files changed

+291
-117
lines changed

8 files changed

+291
-117
lines changed

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@ public function from_context(
2525
array $request_data = array(),
2626
array $custom_query_args = array()
2727
): 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 )
28+
$base_url = $this->wc_url_from_context( $context, $request_data );
29+
30+
$hash_params = array_merge(
31+
array( self::PCP_QUERY_ARG => 'button' ),
32+
$custom_query_args
3433
);
34+
35+
return $base_url . '#' . http_build_query( $hash_params );
3536
}
3637

3738
protected function wc_url_from_context( string $context, array $request_data = array() ): string {

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { loadPaypalScript } from './modules/Helper/ScriptLoading';
2525
import buttonModuleWatcher from './modules/ButtonModuleWatcher';
2626
import MessagesBootstrap from './modules/ContextBootstrap/MessagesBootstrap';
2727
import { apmButtonsInit } from './modules/Helper/ApmButtons';
28+
import CrossBrowserCartRestorer from './modules/Helper/CrossBrowserCartRestorer';
2829

2930
// TODO: could be a good idea to have a separate spinner for each gateway,
3031
// but I think we care mainly about the script loading, so one spinner should be enough.
@@ -45,6 +46,16 @@ const bootstrap = () => {
4546
);
4647
const spinner = new Spinner();
4748

49+
// Check if we need to handle cross-browser AppSwitch
50+
const crossBrowserCartRestorer = new CrossBrowserCartRestorer(
51+
PayPalCommerceGateway
52+
);
53+
54+
if ( crossBrowserCartRestorer.shouldRestore() ) {
55+
crossBrowserCartRestorer.restore();
56+
return; // Stop bootstrap - we're handling cross-browser order creation
57+
}
58+
4859
const formSaver = new FormSaver(
4960
PayPalCommerceGateway.ajax.save_checkout_form.endpoint,
5061
PayPalCommerceGateway.ajax.save_checkout_form.nonce
@@ -90,7 +101,10 @@ const bootstrap = () => {
90101
};
91102

92103
const doBasicCheckoutValidation = () => {
93-
if ( PayPalCommerceGateway.basic_checkout_validation_enabled || PayPalCommerceGateway.is_free_trial_cart ) {
104+
if (
105+
PayPalCommerceGateway.basic_checkout_validation_enabled ||
106+
PayPalCommerceGateway.is_free_trial_cart
107+
) {
94108
// A quick fix to get the errors about empty form fields before attempting PayPal order,
95109
// it should solve #513 for most of the users, but it is not a proper solution.
96110
// Currently it is disabled by default because a better solution is now implemented
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import ResumeFlowHelper from './ResumeFlowHelper';
2+
import Spinner from './Spinner';
3+
4+
/**
5+
* Handles cross-browser cart restoration for AppSwitch flows
6+
*/
7+
class CrossBrowserCartRestorer {
8+
constructor( config ) {
9+
this.config = config;
10+
}
11+
12+
/**
13+
* Check if we should restore (create cross-browser order)
14+
*
15+
* @return {boolean} True if this is a cross-browser AppSwitch return
16+
*/
17+
shouldRestore() {
18+
if ( ! this.isAppSwitchReturn() ) {
19+
return false;
20+
}
21+
22+
const cartKey = this.getCartKeyFromHash();
23+
const savedCartHash = this.getSavedCartHashFromHash();
24+
25+
if ( ! cartKey || ! savedCartHash ) {
26+
return false;
27+
}
28+
29+
const currentCartHash = this.config.cart_hash;
30+
31+
// If cart hashes match, no need for cross-browser handling.
32+
return currentCartHash !== savedCartHash;
33+
}
34+
35+
restore() {
36+
const cartKey = this.getCartKeyFromHash();
37+
this.createCrossBrowserOrder( cartKey );
38+
}
39+
40+
createCrossBrowserOrder( cartKey ) {
41+
const endpoint = this.config.ajax?.create_cross_browser_order;
42+
43+
if ( ! endpoint ) {
44+
console.error(
45+
'Create cross-browser order endpoint not configured'
46+
);
47+
return;
48+
}
49+
50+
const spinner = Spinner.fullPage();
51+
spinner.block();
52+
53+
fetch( endpoint, {
54+
method: 'POST',
55+
headers: {
56+
'Content-Type': 'application/json',
57+
},
58+
credentials: 'same-origin',
59+
body: JSON.stringify( {
60+
cart_key: cartKey,
61+
} ),
62+
} )
63+
.then( ( response ) => response.json() )
64+
.then( ( response ) => {
65+
const { success, data } = response;
66+
67+
if ( ! success ) {
68+
console.error(
69+
'Cross-browser order creation failed:',
70+
data?.message
71+
);
72+
return;
73+
}
74+
75+
if ( ! data.redirect ) {
76+
console.error(
77+
'Missing redirect URL in cross-browser order creation response'
78+
);
79+
return;
80+
}
81+
82+
this.cleanCrossBrowserAppSwitchParams();
83+
window.location.href = data.redirect + window.location.hash;
84+
} )
85+
.catch( ( error ) => {
86+
console.error( 'Error creating cross-browser order:', error );
87+
} )
88+
.finally( () => spinner.unblock() );
89+
}
90+
91+
/**
92+
* Clean cross-browser AppSwitch params from hash while preserving PayPal's params
93+
*/
94+
cleanCrossBrowserAppSwitchParams() {
95+
if ( ! window.location.hash ) {
96+
return;
97+
}
98+
99+
const CROSS_BROWSER_APPSWITCH_PARAMS = [
100+
'pcp-return',
101+
'pcp-cart',
102+
'pcp-cart-hash',
103+
];
104+
105+
const hashString = window.location.hash.substring( 1 );
106+
const params = new URLSearchParams( hashString );
107+
108+
CROSS_BROWSER_APPSWITCH_PARAMS.forEach( ( param ) =>
109+
params.delete( param )
110+
);
111+
112+
const baseUrl = window.location.pathname + window.location.search;
113+
const newHashString = params.toString();
114+
const newUrl = baseUrl + ( newHashString ? '#' + newHashString : '' );
115+
116+
window.history.replaceState( null, '', newUrl );
117+
}
118+
119+
isAppSwitchReturn() {
120+
const params = this.getHashParams();
121+
return params[ 'pcp-return' ] === 'button';
122+
}
123+
124+
getCartKeyFromHash() {
125+
const params = this.getHashParams();
126+
return params[ 'pcp-cart' ] || null;
127+
}
128+
129+
getSavedCartHashFromHash() {
130+
const params = this.getHashParams();
131+
return params[ 'pcp-cart-hash' ] || null;
132+
}
133+
134+
getHashParams() {
135+
if ( ! window.location.hash ) {
136+
return {};
137+
}
138+
139+
const hashString = window.location.hash.substring( 1 );
140+
const params = new URLSearchParams( hashString );
141+
142+
return Object.fromEntries( params );
143+
}
144+
}
145+
146+
export default CrossBrowserCartRestorer;

modules/ppcp-button/services.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
2929
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveOrderEndpoint;
3030
use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint;
31+
use WooCommerce\PayPalCommerce\Button\Endpoint\CreateCrossBrowserOrderEndpoint;
3132
use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
3233
use WooCommerce\PayPalCommerce\Button\Endpoint\DataClientIdEndpoint;
3334
use WooCommerce\PayPalCommerce\Button\Endpoint\GetOrderEndpoint;
@@ -262,6 +263,17 @@
262263
$context
263264
);
264265
},
266+
'button.endpoint.create-cross-browser-order' => static function ( ContainerInterface $container ): CreateCrossBrowserOrderEndpoint {
267+
$cart_data_storage = $container->get( 'button.session.storage.card-data.transient' );
268+
$order_endpoint = $container->get( 'api.endpoint.order' );
269+
$wc_order_creator = $container->get( 'button.helper.wc-order-creator' );
270+
271+
return new CreateCrossBrowserOrderEndpoint(
272+
$cart_data_storage,
273+
$order_endpoint,
274+
$wc_order_creator
275+
);
276+
},
265277
'button.endpoint.approve-subscription' => static function ( ContainerInterface $container ): ApproveSubscriptionEndpoint {
266278
return new ApproveSubscriptionEndpoint(
267279
$container->get( 'button.request-data' ),

modules/ppcp-button/src/Assets/SmartButton.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint;
2828
use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
2929
use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint;
30+
use WooCommerce\PayPalCommerce\Button\Endpoint\CreateCrossBrowserOrderEndpoint;
3031
use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
3132
use WooCommerce\PayPalCommerce\Button\Endpoint\DataClientIdEndpoint;
3233
use WooCommerce\PayPalCommerce\Button\Endpoint\GetOrderEndpoint;
@@ -1247,6 +1248,9 @@ public function script_data(): array {
12471248
'wp_rest_nonce' => wp_create_nonce( 'wc_store_api' ),
12481249
'update_shipping_method' => \WC_AJAX::get_endpoint( 'update_shipping_method' ),
12491250
),
1251+
'create_cross_browser_order' => array(
1252+
'endpoint' => \WC_AJAX::get_endpoint( CreateCrossBrowserOrderEndpoint::ENDPOINT ),
1253+
),
12501254
),
12511255
'cart_contains_subscription' => $this->subscription_helper->cart_contains_subscription(),
12521256
'subscription_plan_id' => $this->subscription_helper->paypal_subscription_id(),
@@ -1380,6 +1384,7 @@ public function script_data(): array {
13801384
'productType' => null,
13811385
'manualRenewalEnabled' => $this->subscription_helper->accept_manual_renewals(),
13821386
'final_review_enabled' => $this->final_review_enabled,
1387+
'cart_hash' => WC()->cart ? WC()->cart->get_cart_hash() : '',
13831388
);
13841389

13851390
if ( is_product() ) {

0 commit comments

Comments
 (0)