Skip to content

Commit 27c1895

Browse files
authored
Merge pull request #4542 from woocommerce/dev/PCP-6639-insonsistent-pay-pal-button-behavior-for-subscriptions-on-cart
Insonsistent PayPal button behavior for subscriptions on cart (6639)
2 parents f94f8f1 + 5207862 commit 27c1895

8 files changed

Lines changed: 307 additions & 45 deletions

File tree

modules/ppcp-blocks/resources/js/Helper/Subscription.js

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @param {Object} scriptData
3-
* @return {boolean}
3+
* @return {boolean} Whether the store runs in PayPal Subscriptions mode.
44
*/
55
export const isPayPalSubscription = ( scriptData ) => {
66
return (
@@ -11,8 +11,58 @@ export const isPayPalSubscription = ( scriptData ) => {
1111

1212
/**
1313
* @param {Object} scriptData
14-
* @return {boolean}
14+
* @return {boolean} Whether the cart contains at least one subscription product.
1515
*/
1616
export const cartHasSubscriptionProducts = ( scriptData ) => {
1717
return !! scriptData?.locations_with_subscription_product?.cart;
1818
};
19+
20+
/**
21+
* Whether the PayPal button is allowed for the current (subscription) cart.
22+
*
23+
* Single, mode-aware rule shared by the block cart, classic cart and mini-cart
24+
* so the button is shown (or hidden) consistently. Prefers the authoritative
25+
* server flag `subscription_button_allowed`; the explicit checks act as a
26+
* fallback and also cover the free-trial guest case on the block cart.
27+
*
28+
* @param {Object} scriptData
29+
* @return {boolean} Whether the PayPal button may be displayed for this cart.
30+
*/
31+
export const paypalSubscriptionButtonAllowed = ( scriptData ) => {
32+
if ( ! cartHasSubscriptionProducts( scriptData ) ) {
33+
return true;
34+
}
35+
36+
// Don't show buttons on the block cart page if the user is not logged in
37+
// and the cart contains a free trial product.
38+
if (
39+
! scriptData.user?.is_logged &&
40+
scriptData.context === 'cart-block' &&
41+
scriptData.is_free_trial_cart
42+
) {
43+
return false;
44+
}
45+
46+
if ( typeof scriptData.subscription_button_allowed !== 'undefined' ) {
47+
return !! scriptData.subscription_button_allowed;
48+
}
49+
50+
// Vaulting mode but vaulting disabled.
51+
if (
52+
! isPayPalSubscription( scriptData ) &&
53+
! scriptData.can_save_vault_token
54+
) {
55+
return false;
56+
}
57+
58+
// PayPal Subscriptions mode but product not associated with a PayPal plan
59+
// (or the cart contains more than one item).
60+
if (
61+
isPayPalSubscription( scriptData ) &&
62+
! scriptData.subscription_product_allowed
63+
) {
64+
return false;
65+
}
66+
67+
return true;
68+
};
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { paypalSubscriptionButtonAllowed } from './Subscription';
2+
3+
const baseData = ( overrides = {} ) => ( {
4+
locations_with_subscription_product: { cart: true },
5+
user: { is_logged: true },
6+
context: 'cart',
7+
is_free_trial_cart: false,
8+
can_save_vault_token: false,
9+
subscription_product_allowed: false,
10+
data_client_id: {
11+
has_subscriptions: false,
12+
paypal_subscriptions_enabled: false,
13+
},
14+
...overrides,
15+
} );
16+
17+
describe( 'paypalSubscriptionButtonAllowed', () => {
18+
it( 'allows a cart without subscription products', () => {
19+
const data = baseData( {
20+
locations_with_subscription_product: { cart: false },
21+
} );
22+
expect( paypalSubscriptionButtonAllowed( data ) ).toBe( true );
23+
} );
24+
25+
it( 'hides for a guest with a free trial on the block cart', () => {
26+
const data = baseData( {
27+
user: { is_logged: false },
28+
context: 'cart-block',
29+
is_free_trial_cart: true,
30+
subscription_button_allowed: true,
31+
} );
32+
expect( paypalSubscriptionButtonAllowed( data ) ).toBe( false );
33+
} );
34+
35+
it( 'prefers the server subscription_button_allowed flag when present', () => {
36+
expect(
37+
paypalSubscriptionButtonAllowed(
38+
baseData( { subscription_button_allowed: false } )
39+
)
40+
).toBe( false );
41+
expect(
42+
paypalSubscriptionButtonAllowed(
43+
baseData( { subscription_button_allowed: true } )
44+
)
45+
).toBe( true );
46+
} );
47+
48+
it( 'falls back to hide in vaulting mode when vaulting is disabled', () => {
49+
const data = baseData( { can_save_vault_token: false } );
50+
expect( paypalSubscriptionButtonAllowed( data ) ).toBe( false );
51+
} );
52+
53+
it( 'falls back to show in vaulting mode when vaulting is enabled', () => {
54+
const data = baseData( { can_save_vault_token: true } );
55+
expect( paypalSubscriptionButtonAllowed( data ) ).toBe( true );
56+
} );
57+
58+
it( 'falls back to hide in subscriptions mode when the product is not allowed', () => {
59+
const data = baseData( {
60+
data_client_id: {
61+
has_subscriptions: true,
62+
paypal_subscriptions_enabled: true,
63+
},
64+
subscription_product_allowed: false,
65+
} );
66+
expect( paypalSubscriptionButtonAllowed( data ) ).toBe( false );
67+
} );
68+
69+
it( 'falls back to show in subscriptions mode when the product is allowed', () => {
70+
const data = baseData( {
71+
data_client_id: {
72+
has_subscriptions: true,
73+
paypal_subscriptions_enabled: true,
74+
},
75+
subscription_product_allowed: true,
76+
} );
77+
expect( paypalSubscriptionButtonAllowed( data ) ).toBe( true );
78+
} );
79+
} );

modules/ppcp-blocks/resources/js/checkout-block.js

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
import { __ } from '@wordpress/i18n';
66
import {
77
cartHasSubscriptionProducts,
8-
isPayPalSubscription,
8+
paypalSubscriptionButtonAllowed,
99
} from './Helper/Subscription';
1010
import { loadPayPalScript } from '../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading';
1111
import BlockCheckoutMessagesBootstrap from './Bootstrap/BlockCheckoutMessagesBootstrap';
@@ -25,31 +25,9 @@ const features = [ 'products' ];
2525
let blockEnabled = true;
2626

2727
if ( cartHasSubscriptionProducts( config.scriptData ) ) {
28-
// Don't show buttons on block cart page if user is not logged in and cart contains free trial product
29-
if (
30-
! config.scriptData.user.is_logged &&
31-
config.scriptData.context === 'cart-block' &&
32-
cartHasSubscriptionProducts( config.scriptData ) &&
33-
config.scriptData.is_free_trial_cart
34-
) {
35-
blockEnabled = false;
36-
}
37-
38-
// Don't render if vaulting disabled and is in vault subscription mode
39-
if (
40-
! isPayPalSubscription( config.scriptData ) &&
41-
! config.scriptData.can_save_vault_token
42-
) {
43-
blockEnabled = false;
44-
}
45-
46-
// Don't render buttons if in subscription mode and product not associated with a PayPal subscription
47-
if (
48-
isPayPalSubscription( config.scriptData ) &&
49-
! config.scriptData.subscription_product_allowed
50-
) {
51-
blockEnabled = false;
52-
}
28+
// Show the button only for subscription carts PayPal can process
29+
// (shared rule used by the classic cart and mini-cart as well).
30+
blockEnabled = paypalSubscriptionButtonAllowed( config.scriptData );
5331

5432
features.push( 'subscriptions' );
5533
}

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

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import CartActionHandler from '../ActionHandler/CartActionHandler';
22
import BootstrapHelper from '../Helper/BootstrapHelper';
3+
import { paypalSubscriptionButtonAllowed } from '@ppcp-blocks/Helper/Subscription';
34

45
class CartBootstrap {
56
constructor( gateway, renderer, errorHandler ) {
@@ -61,8 +62,23 @@ class CartBootstrap {
6162
if ( result.data.messages ) {
6263
newData.messages = result.data.messages;
6364
}
65+
// Keep the subscription gate in sync with the current cart.
66+
if (
67+
typeof result.data.subscription_button_allowed !==
68+
'undefined'
69+
) {
70+
newData.subscription_button_allowed =
71+
result.data.subscription_button_allowed;
72+
}
73+
if ( result.data.locations_with_subscription_product ) {
74+
newData.locations_with_subscription_product =
75+
result.data.locations_with_subscription_product;
76+
}
6477
if ( newData ) {
6578
BootstrapHelper.updateScriptData( this, newData );
79+
if ( this.shouldRender() ) {
80+
this.render();
81+
}
6682
this.handleButtonStatus();
6783
}
6884

@@ -87,11 +103,39 @@ class CartBootstrap {
87103
return BootstrapHelper.shouldEnable( this );
88104
}
89105

106+
/**
107+
* Whether the button is allowed for the current (subscription) cart.
108+
* Shared rule used by the block cart and mini-cart as well.
109+
*
110+
* @return {boolean} True when the button may be displayed.
111+
*/
112+
subscriptionButtonAllowed() {
113+
return paypalSubscriptionButtonAllowed( this.gateway );
114+
}
115+
116+
hide() {
117+
jQuery( this.gateway.button.wrapper ).hide();
118+
}
119+
120+
show() {
121+
jQuery( this.gateway.button.wrapper ).show();
122+
}
123+
90124
render() {
91125
if ( ! this.shouldRender() ) {
92126
return;
93127
}
94128

129+
// Hide the button for subscription carts PayPal cannot process, so the
130+
// classic and block carts behave consistently and the button does not
131+
// render only to be removed afterwards.
132+
if ( ! this.subscriptionButtonAllowed() ) {
133+
this.hide();
134+
return;
135+
}
136+
137+
this.show();
138+
95139
const actionHandler = new CartActionHandler(
96140
PayPalCommerceGateway,
97141
this.errorHandler
@@ -115,11 +159,6 @@ class CartBootstrap {
115159
actionHandler.subscriptionsConfiguration( subscription_plan_id )
116160
);
117161

118-
if ( ! PayPalCommerceGateway.subscription_product_allowed ) {
119-
this.gateway.button.is_disabled = true;
120-
this.handleButtonStatus();
121-
}
122-
123162
return;
124163
}
125164

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import CartActionHandler from '../ActionHandler/CartActionHandler';
22
import BootstrapHelper from '../Helper/BootstrapHelper';
3+
import { paypalSubscriptionButtonAllowed } from '@ppcp-blocks/Helper/Subscription';
4+
import { debounce } from '@ppcp-blocks/Helper/debounce';
35

46
class MiniCartBootstrap {
57
constructor( gateway, renderer, errorHandler ) {
68
this.gateway = gateway;
79
this.renderer = renderer;
810
this.errorHandler = errorHandler;
911
this.actionHandler = null;
12+
this.lastItemsCount = null;
1013
}
1114

1215
init() {
@@ -23,6 +26,7 @@ class MiniCartBootstrap {
2326
miniCartConfig,
2427
this.errorHandler
2528
);
29+
this.gateway = miniCartConfig;
2630
this.render();
2731
this.handleButtonStatus();
2832

@@ -34,6 +38,20 @@ class MiniCartBootstrap {
3438
}
3539
);
3640

41+
/*
42+
The block cart/mini-cart mutates the `wc/store/cart` data store instead of
43+
firing the jQuery fragment events above, so subscribe to it to keep the
44+
mini-cart button in sync (and clear it when the cart becomes empty).
45+
*/
46+
if ( typeof wp !== 'undefined' && wp.data?.subscribe ) {
47+
wp.data.subscribe(
48+
debounce( () => {
49+
this._handleStoreCartChange();
50+
}, 300 ),
51+
'wc/store/cart'
52+
);
53+
}
54+
3755
this.renderer.onButtonsInit(
3856
this.gateway.button.mini_cart_wrapper,
3957
() => {
@@ -43,6 +61,35 @@ class MiniCartBootstrap {
4361
);
4462
}
4563

64+
/**
65+
* @private
66+
*/
67+
_handleStoreCartChange() {
68+
if ( typeof wp === 'undefined' || ! wp.data?.select ) {
69+
return;
70+
}
71+
72+
const cart = wp.data.select( 'wc/store/cart' );
73+
if ( ! cart ) {
74+
return;
75+
}
76+
77+
const itemsCount =
78+
cart.getCartData?.()?.itemsCount ?? cart.getItemsCount?.() ?? null;
79+
if ( itemsCount === null || itemsCount === this.lastItemsCount ) {
80+
return;
81+
}
82+
this.lastItemsCount = itemsCount;
83+
84+
if ( itemsCount === 0 ) {
85+
this.hide();
86+
return;
87+
}
88+
89+
this.render();
90+
this.handleButtonStatus();
91+
}
92+
4693
handleButtonStatus() {
4794
BootstrapHelper.handleButtonStatus( this, {
4895
wrapper: this.gateway.button.mini_cart_wrapper,
@@ -66,11 +113,34 @@ class MiniCartBootstrap {
66113
} );
67114
}
68115

116+
/**
117+
* Whether the button is allowed for the current (subscription) cart.
118+
* Shared rule used by the classic cart and block cart as well.
119+
*
120+
* @return {boolean} True when the button may be displayed.
121+
*/
122+
subscriptionButtonAllowed() {
123+
return paypalSubscriptionButtonAllowed( this.gateway );
124+
}
125+
126+
hide() {
127+
jQuery( this.gateway.button.mini_cart_wrapper ).empty().hide();
128+
}
129+
69130
render() {
70131
if ( ! this.shouldRender() ) {
71132
return;
72133
}
73134

135+
// Hide the button for subscription carts PayPal cannot process, so the
136+
// mini-cart behaves consistently with the classic and block carts.
137+
if ( ! this.subscriptionButtonAllowed() ) {
138+
this.hide();
139+
return;
140+
}
141+
142+
jQuery( this.gateway.button.mini_cart_wrapper ).show();
143+
74144
this.renderer.render( this.actionHandler.configuration(), {
75145
button: {
76146
wrapper: this.gateway.button.mini_cart_wrapper,

0 commit comments

Comments
 (0)