Skip to content

Commit 7ec511e

Browse files
authored
Merge pull request #4473 from woocommerce/dev/PCP-4438-make-cart-sumulation-disabling-filter-work-for-apple-pay-and-google-pay
Make cart simulation disabling filter work for Apple Pay and Google Pay (4438)
2 parents 658e627 + df6b926 commit 7ec511e

7 files changed

Lines changed: 245 additions & 20 deletions

File tree

modules/ppcp-applepay/resources/js/Context/SingleProductHandler.js

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import SingleProductActionHandler from '@ppcp-button/ActionHandler/SingleProductActionHandler';
2-
import SimulateCart from '@ppcp-button/Helper/SimulateCart';
2+
import SimulateCart, { isSimulateCartEnabled } from '@ppcp-button/Helper/SimulateCart';
33
import ErrorHandler from '@ppcp-button/ErrorHandler';
44
import UpdateCart from '@ppcp-button/Helper/UpdateCart';
55
import BaseHandler from '@ppcp-applepay/Context/BaseHandler';
@@ -13,6 +13,12 @@ class SingleProductHandler extends BaseHandler {
1313
}
1414

1515
transactionInfo() {
16+
// Simulation is this method's only mechanism for fetching product data;
17+
// reject early to avoid a pointless AJAX call.
18+
if ( ! isSimulateCartEnabled( this.ppcpConfig ) ) {
19+
return Promise.reject( new Error( 'Cart simulation is disabled.' ) );
20+
}
21+
1622
const errorHandler = new ErrorHandler(
1723
this.ppcpConfig.labels.error.generic,
1824
document.querySelector( '.woocommerce-notices-wrapper' )
@@ -37,19 +43,17 @@ class SingleProductHandler extends BaseHandler {
3743
? actionHandler.getSubscriptionProducts()
3844
: actionHandler.getProducts();
3945

40-
return new Promise( ( resolve, reject ) => {
41-
new SimulateCart(
42-
this.ppcpConfig.ajax.simulate_cart.endpoint,
43-
this.ppcpConfig.ajax.simulate_cart.nonce
44-
).simulate( ( data ) => {
45-
resolve( {
46-
countryCode: data.country_code,
47-
currencyCode: data.currency_code,
48-
totalPriceStatus: 'FINAL',
49-
totalPrice: data.total,
50-
} );
51-
}, products );
52-
} );
46+
return new SimulateCart(
47+
this.ppcpConfig.ajax?.simulate_cart?.endpoint,
48+
this.ppcpConfig.ajax?.simulate_cart?.nonce
49+
).simulate( ( data ) => {
50+
return {
51+
countryCode: data.country_code,
52+
currencyCode: data.currency_code,
53+
totalPriceStatus: 'FINAL',
54+
totalPrice: data.total,
55+
};
56+
}, products );
5357
}
5458

5559
createOrder() {
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/* global describe, test, expect, jest, beforeEach */
2+
3+
jest.mock( '@ppcp-button/ActionHandler/SingleProductActionHandler' );
4+
jest.mock( '@ppcp-button/Helper/SimulateCart', () => {
5+
const { isSimulateCartEnabled } = jest.requireActual(
6+
'@ppcp-button/Helper/SimulateCart'
7+
);
8+
return { __esModule: true, default: jest.fn(), isSimulateCartEnabled };
9+
} );
10+
jest.mock( '@ppcp-button/ErrorHandler', () => jest.fn() );
11+
jest.mock( '@ppcp-button/Helper/UpdateCart', () => jest.fn() );
12+
13+
import SingleProductHandler from './SingleProductHandler';
14+
import SimulateCart from '@ppcp-button/Helper/SimulateCart';
15+
import SingleProductActionHandler from '@ppcp-button/ActionHandler/SingleProductActionHandler';
16+
17+
describe( 'SingleProductHandler', () => {
18+
let handler;
19+
let mockSimulate;
20+
let mockGetProducts;
21+
let mockGetSubscriptionProducts;
22+
const mockProducts = [ { id: 64, quantity: 1 } ];
23+
24+
const ppcpConfig = {
25+
labels: { error: { generic: 'An error occurred.' } },
26+
simulate_cart: {
27+
enabled: true,
28+
},
29+
ajax: {
30+
simulate_cart: {
31+
endpoint: '/ppcp/simulate-cart',
32+
nonce: 'test-nonce',
33+
},
34+
},
35+
};
36+
37+
beforeEach( () => {
38+
jest.clearAllMocks();
39+
40+
global.PayPalCommerceGateway = {
41+
data_client_id: {
42+
has_subscriptions: false,
43+
paypal_subscriptions_enabled: false,
44+
},
45+
};
46+
47+
mockGetProducts = jest.fn().mockReturnValue( mockProducts );
48+
mockGetSubscriptionProducts = jest.fn().mockReturnValue( [] );
49+
SingleProductActionHandler.mockImplementation( () => ( {
50+
getProducts: mockGetProducts,
51+
getSubscriptionProducts: mockGetSubscriptionProducts,
52+
} ) );
53+
54+
mockSimulate = jest.fn();
55+
SimulateCart.mockImplementation( () => ( {
56+
simulate: mockSimulate,
57+
} ) );
58+
59+
handler = new SingleProductHandler( {}, ppcpConfig );
60+
} );
61+
62+
describe( 'validateContext()', () => {
63+
test( 'returns true by default', () => {
64+
expect( handler.validateContext() ).toBe( true );
65+
} );
66+
} );
67+
68+
describe( 'transactionInfo()', () => {
69+
describe( 'simulate_cart.enabled guard', () => {
70+
test( 'rejects immediately when simulate_cart is disabled', async () => {
71+
const disabledHandler = new SingleProductHandler(
72+
{},
73+
{ ...ppcpConfig, simulate_cart: { enabled: false } }
74+
);
75+
76+
await expect( disabledHandler.transactionInfo() ).rejects.toThrow(
77+
'Cart simulation is disabled.'
78+
);
79+
expect( mockSimulate ).not.toHaveBeenCalled();
80+
} );
81+
82+
test( 'proceeds when simulate_cart key is absent (defaults to enabled)', async () => {
83+
const { simulate_cart: _, ...configWithoutKey } = ppcpConfig;
84+
const defaultHandler = new SingleProductHandler(
85+
{},
86+
configWithoutKey
87+
);
88+
mockSimulate.mockResolvedValue( {} );
89+
90+
await defaultHandler.transactionInfo();
91+
92+
expect( mockSimulate ).toHaveBeenCalled();
93+
} );
94+
} );
95+
96+
test( 'calls simulate_cart with products when enabled', async () => {
97+
mockSimulate.mockImplementation( ( onResolve ) => {
98+
return Promise.resolve(
99+
onResolve( {
100+
total: 45,
101+
country_code: 'US',
102+
currency_code: 'USD',
103+
} )
104+
);
105+
} );
106+
107+
await handler.transactionInfo();
108+
109+
expect( mockSimulate ).toHaveBeenCalledWith(
110+
expect.any( Function ),
111+
mockProducts
112+
);
113+
} );
114+
115+
test( 'maps simulation response to Apple Pay transaction info format', async () => {
116+
mockSimulate.mockImplementation( ( onResolve ) => {
117+
return Promise.resolve(
118+
onResolve( {
119+
total: 45,
120+
country_code: 'US',
121+
currency_code: 'USD',
122+
} )
123+
);
124+
} );
125+
126+
const result = await handler.transactionInfo();
127+
128+
expect( result ).toEqual( {
129+
countryCode: 'US',
130+
currencyCode: 'USD',
131+
totalPriceStatus: 'FINAL',
132+
totalPrice: 45,
133+
} );
134+
} );
135+
136+
test( 'propagates rejection from simulate_cart', async () => {
137+
const serverError = { message: 'Server error.' };
138+
mockSimulate.mockRejectedValue( serverError );
139+
140+
await expect( handler.transactionInfo() ).rejects.toEqual(
141+
serverError
142+
);
143+
} );
144+
145+
test( 'passes subscription products when subscriptions are enabled', async () => {
146+
const mockSubscriptionProducts = [ { id: 155, quantity: 1 } ];
147+
mockGetSubscriptionProducts.mockReturnValue( mockSubscriptionProducts );
148+
global.PayPalCommerceGateway = {
149+
data_client_id: {
150+
has_subscriptions: true,
151+
paypal_subscriptions_enabled: true,
152+
},
153+
};
154+
mockSimulate.mockImplementation( ( onResolve ) => {
155+
return Promise.resolve(
156+
onResolve( { total: 0, country_code: 'US', currency_code: 'USD' } )
157+
);
158+
} );
159+
160+
await handler.transactionInfo();
161+
162+
expect( mockGetSubscriptionProducts ).toHaveBeenCalled();
163+
expect( mockGetProducts ).not.toHaveBeenCalled();
164+
expect( mockSimulate ).toHaveBeenCalledWith(
165+
expect.any( Function ),
166+
mockSubscriptionProducts
167+
);
168+
} );
169+
} );
170+
} );

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
export const isSimulateCartEnabled = ( ppcpConfig ) =>
2+
//Fallback to true because it preserves behavior existing earlier, and misconfigured state fails loudly at the AJAX call.
3+
ppcpConfig?.simulate_cart?.enabled ?? true;
4+
15
class SimulateCart {
26
constructor( endpoint, nonce ) {
37
this.endpoint = endpoint;

modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import SingleProductActionHandler from '@ppcp-button/ActionHandler/SingleProductActionHandler';
2-
import SimulateCart from '@ppcp-button/Helper/SimulateCart';
2+
import SimulateCart, { isSimulateCartEnabled } from '@ppcp-button/Helper/SimulateCart';
33
import ErrorHandler from '@ppcp-button/ErrorHandler';
44
import UpdateCart from '@ppcp-button/Helper/UpdateCart';
55
import BaseHandler from './BaseHandler';
@@ -14,6 +14,12 @@ class SingleProductHandler extends BaseHandler {
1414
}
1515

1616
transactionInfo() {
17+
// Simulation is this method's only mechanism for fetching product data;
18+
// reject early to avoid a pointless AJAX call.
19+
if ( ! isSimulateCartEnabled( this.ppcpConfig ) ) {
20+
return Promise.reject( new Error( 'Cart simulation is disabled.' ) );
21+
}
22+
1723
const form = document.querySelector( 'form.cart' );
1824
const variationIdInput = form?.querySelector(
1925
'input[name="variation_id"]'

modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.test.js

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
/* global describe, test, expect, jest, beforeEach */
22

33
jest.mock( '@ppcp-button/ActionHandler/SingleProductActionHandler' );
4-
jest.mock( '@ppcp-button/Helper/SimulateCart' );
4+
jest.mock( '@ppcp-button/Helper/SimulateCart', () => {
5+
const { isSimulateCartEnabled } = jest.requireActual(
6+
'@ppcp-button/Helper/SimulateCart'
7+
);
8+
return { __esModule: true, default: jest.fn(), isSimulateCartEnabled };
9+
} );
510
jest.mock( '@ppcp-button/ErrorHandler', () => jest.fn() );
611
jest.mock( '@ppcp-button/Helper/UpdateCart', () => jest.fn() );
712
jest.mock( '@ppcp-googlepay/Helper/TransactionInfo' );
@@ -20,6 +25,9 @@ describe( 'SingleProductHandler', () => {
2025

2126
const ppcpConfig = {
2227
labels: { error: { generic: 'An error occurred.' } },
28+
simulate_cart: {
29+
enabled: true,
30+
},
2331
ajax: {
2432
simulate_cart: {
2533
endpoint: '/ppcp/simulate-cart',
@@ -74,6 +82,36 @@ describe( 'SingleProductHandler', () => {
7482
} );
7583

7684
describe( 'transactionInfo()', () => {
85+
describe( 'simulate_cart.enabled guard', () => {
86+
test( 'rejects immediately when simulate_cart is disabled', async () => {
87+
const disabledHandler = new SingleProductHandler(
88+
{},
89+
{ ...ppcpConfig, simulate_cart: { enabled: false } },
90+
null
91+
);
92+
93+
await expect( disabledHandler.transactionInfo() ).rejects.toThrow(
94+
'Cart simulation is disabled.'
95+
);
96+
expect( mockSimulate ).not.toHaveBeenCalled();
97+
} );
98+
99+
test( 'proceeds when simulate_cart key is absent (defaults to enabled)', async () => {
100+
const { simulate_cart: _, ...configWithoutKey } = ppcpConfig;
101+
const defaultHandler = new SingleProductHandler(
102+
{},
103+
configWithoutKey,
104+
null
105+
);
106+
document.body.innerHTML = `<form class="cart"></form>`;
107+
mockSimulate.mockResolvedValue( {} );
108+
109+
await defaultHandler.transactionInfo();
110+
111+
expect( mockSimulate ).toHaveBeenCalled();
112+
} );
113+
} );
114+
77115
describe( 'variation_id guard', () => {
78116
test( 'rejects immediately when variation_id is empty', async () => {
79117
document.body.innerHTML = `

modules/ppcp-googlepay/resources/js/GooglepayManager.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,13 @@ class GooglepayManager {
5151
button.init();
5252
};
5353

54-
// Initialize button only if googlePayConfig is already fetched.
55-
if ( this.googlePayConfig ) {
54+
// Initialize button only if googlePayConfig and transactionInfo are already fetched.
55+
if ( this.googlePayConfig && this.transactionInfo ) {
5656
initButton();
5757
} else {
5858
await this.init();
5959

60-
if ( this.googlePayConfig ) {
60+
if ( this.googlePayConfig && this.transactionInfo ) {
6161
initButton();
6262
}
6363
}
@@ -79,6 +79,8 @@ class GooglepayManager {
7979

8080
if ( ! this.googlePayConfig ) {
8181
console.error( 'No GooglePayConfig received during init' );
82+
} else if ( ! this.transactionInfo ) {
83+
console.warn( 'No transaction info available, skipping button init.' );
8284
} else {
8385
for ( const button of this.buttons ) {
8486
button.configure(

tests/js/jest.config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"^@ppcp-blocks/(.*)$": "<rootDir>/modules/ppcp-blocks/resources/js/$1",
1111
"^@ppcp-card-fields/(.*)$": "<rootDir>/modules/ppcp-card-fields/resources/js/$1",
1212
"^@ppcp-paylater-block/(.*)$": "<rootDir>/modules/ppcp-paylater-block/resources/js/$1",
13-
"^@ppcp-googlepay/(.*)$": "<rootDir>/modules/ppcp-googlepay/resources/js/$1"
13+
"^@ppcp-googlepay/(.*)$": "<rootDir>/modules/ppcp-googlepay/resources/js/$1",
14+
"^@ppcp-applepay/(.*)$": "<rootDir>/modules/ppcp-applepay/resources/js/$1"
1415
},
1516
"testPathIgnorePatterns": [
1617
"<rootDir>/tests/",

0 commit comments

Comments
 (0)