Skip to content

Commit

Permalink
feat: tokenized cart ECE base implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
frosso committed Nov 15, 2024
1 parent f358e9d commit 1d3fc0f
Show file tree
Hide file tree
Showing 16 changed files with 532 additions and 704 deletions.
5 changes: 5 additions & 0 deletions changelog/refactor-tokenized-ece-base-implementation
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Significance: patch
Type: update
Comment: feat: tokenized cart ECE base implementation


4 changes: 2 additions & 2 deletions client/express-checkout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
getExpressCheckoutButtonStyleSettings,
getExpressCheckoutData,
normalizeLineItems,
} from './utils/index';
displayLoginConfirmation,
} from './utils';
import {
onAbortPaymentHandler,
onCancelHandler,
Expand All @@ -23,7 +24,6 @@ import {
shippingAddressChangeHandler,
shippingRateChangeHandler,
} from './event-handlers';
import { displayLoginConfirmation } from './utils';

jQuery( ( $ ) => {
// Don't load if blocks checkout is being loaded.
Expand Down
3 changes: 3 additions & 0 deletions client/express-checkout/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ export interface WCPayExpressCheckoutParams {
platform_tracker: string;
shipping: string;
update_shipping: string;
tokenized_cart_nonce: string;
tokenized_cart_session_nonce: string;
store_api_nonce: string;
};

/**
Expand Down
196 changes: 196 additions & 0 deletions client/tokenized-express-checkout/__tests__/cart-api.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';

/**
* Internal dependencies
*/
import ExpressCheckoutCartApi from '../cart-api';

jest.mock( '@wordpress/api-fetch', () => jest.fn() );

global.wcpayExpressCheckoutParams = {};
global.wcpayExpressCheckoutParams.nonce = {};
global.wcpayExpressCheckoutParams.nonce.store_api_nonce =
'global_store_api_nonce';
global.wcpayExpressCheckoutParams.nonce.tokenized_cart_nonce =
'global_tokenized_cart_nonce';
global.wcpayExpressCheckoutParams.nonce.tokenized_cart_session_nonce =
'global_tokenized_cart_session_nonce';
global.wcpayExpressCheckoutParams.checkout = {};
global.wcpayExpressCheckoutParams.checkout.currency_code = 'USD';

describe( 'ExpressCheckoutCartApi', () => {
afterEach( () => {
jest.resetAllMocks();
} );

it( 'should allow to create an anonymous cart for a specific class instance, without affecting other instances', async () => {
global.wcpayExpressCheckoutParams.button_context = 'product';
const headers = new Headers();
headers.append(
'X-WooPayments-Tokenized-Cart-Session',
'tokenized_cart_session'
);
headers.append( 'Nonce', 'nonce-value' );
apiFetch.mockResolvedValue( {
headers: headers,
json: () => Promise.resolve( {} ),
} );

const api = new ExpressCheckoutCartApi();
const anotherApi = new ExpressCheckoutCartApi();

api.useSeparateCart();
await api.getCart();

expect( apiFetch ).toHaveBeenCalledWith(
expect.objectContaining( {
method: 'GET',
path: expect.stringContaining( '/wc/store/v1/cart' ),
headers: expect.objectContaining( {
'X-WooPayments-Tokenized-Cart-Session': '',
'X-WooPayments-Tokenized-Cart-Session-Nonce':
'global_tokenized_cart_session_nonce',
'X-WooPayments-Tokenized-Cart-Nonce':
'global_tokenized_cart_nonce',
Nonce: 'global_store_api_nonce',
} ),
} )
);

apiFetch.mockClear();
apiFetch.mockResolvedValue( {
headers: new Headers(),
json: () => Promise.resolve( {} ),
} );

await api.updateCustomer( {
billing_address: { first_name: 'First' },
} );
expect( apiFetch ).toHaveBeenLastCalledWith(
expect.objectContaining( {
method: 'POST',
path: expect.stringContaining(
'/wc/store/v1/cart/update-customer'
),
headers: expect.objectContaining( {
'X-WooPayments-Tokenized-Cart': true,
'X-WooPayments-Tokenized-Cart-Session-Nonce':
'global_tokenized_cart_session_nonce',
'X-WooPayments-Tokenized-Cart-Nonce':
'global_tokenized_cart_nonce',
'X-WooPayments-Tokenized-Cart-Session':
'tokenized_cart_session',
Nonce: 'nonce-value',
} ),
data: expect.objectContaining( {
billing_address: { first_name: 'First' },
} ),
} )
);

apiFetch.mockClear();
apiFetch.mockResolvedValue( {
headers: new Headers(),
json: () => Promise.resolve( {} ),
} );
await anotherApi.updateCustomer( {
billing_address: { last_name: 'Last' },
} );
expect( apiFetch ).toHaveBeenLastCalledWith(
expect.objectContaining( {
method: 'POST',
path: expect.stringContaining(
'/wc/store/v1/cart/update-customer'
),
// in this case, no additional headers should have been submitted.
headers: expect.objectContaining( {
'X-WooPayments-Tokenized-Cart': true,
'X-WooPayments-Tokenized-Cart-Nonce':
'global_tokenized_cart_nonce',
Nonce: 'global_store_api_nonce',
} ),
data: expect.objectContaining( {
billing_address: { last_name: 'Last' },
} ),
} )
);
} );

it( 'should call `/cart/update-customer` with the global headers if the cart is not anonymous', async () => {
global.wcpayExpressCheckoutParams.button_context = 'cart';
apiFetch.mockResolvedValue( {
headers: new Headers(),
json: () => Promise.resolve( {} ),
} );
const api = new ExpressCheckoutCartApi();

await api.updateCustomer( {
billing_address: { last_name: 'Last' },
} );
expect( apiFetch ).toHaveBeenCalledWith(
expect.objectContaining( {
method: 'POST',
path: expect.stringContaining(
'/wc/store/v1/cart/update-customer'
),
// in this case, no additional headers should have been submitted.
headers: expect.objectContaining( {
'X-WooPayments-Tokenized-Cart': true,
'X-WooPayments-Tokenized-Cart-Nonce':
'global_tokenized_cart_nonce',
} ),
data: expect.objectContaining( {
billing_address: { last_name: 'Last' },
} ),
} )
);
} );

it( 'should store received header information for subsequent usage', async () => {
global.wcpayExpressCheckoutParams.button_context = 'cart';
const headers = new Headers();
headers.append( 'Nonce', 'nonce-value' );
apiFetch.mockResolvedValue( {
headers,
json: () => Promise.resolve( {} ),
} );
const api = new ExpressCheckoutCartApi();

await api.getCart();

expect( apiFetch ).toHaveBeenCalledWith(
expect.objectContaining( {
method: 'GET',
path: expect.stringContaining( '/wc/store/v1/cart' ),
headers: expect.objectContaining( {
'X-WooPayments-Tokenized-Cart-Session-Nonce': undefined,
'X-WooPayments-Tokenized-Cart-Nonce':
'global_tokenized_cart_nonce',
} ),
} )
);

await api.updateCustomer( {
billing_address: { last_name: 'Last' },
} );
expect( apiFetch ).toHaveBeenCalledWith(
expect.objectContaining( {
method: 'POST',
path: expect.stringContaining(
'/wc/store/v1/cart/update-customer'
),
// in this case, no additional headers should have been submitted.
headers: expect.objectContaining( {
'X-WooPayments-Tokenized-Cart-Session-Nonce': undefined,
'X-WooPayments-Tokenized-Cart': true,
'X-WooPayments-Tokenized-Cart-Nonce':
'global_tokenized_cart_nonce',
Nonce: 'nonce-value',
} ),
} )
);
} );
} );
128 changes: 128 additions & 0 deletions client/tokenized-express-checkout/__tests__/order-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';

/**
* Internal dependencies
*/
import ExpressCheckoutOrderApi from '../order-api';

jest.mock( '@wordpress/api-fetch', () => jest.fn() );

global.wcpayExpressCheckoutParams = {};
global.wcpayExpressCheckoutParams.nonce = {};
global.wcpayExpressCheckoutParams.nonce.store_api_nonce =
'global_store_api_nonce';

describe( 'ExpressCheckoutOrderApi', () => {
afterEach( () => {
jest.resetAllMocks();
} );

it( 'gets order data with the provided arguments', async () => {
const api = new ExpressCheckoutOrderApi( {
orderId: '1',
key: 'key_123',
billingEmail: '[email protected]',
} );

await api.getCart();
expect( apiFetch ).toHaveBeenCalledWith(
expect.objectContaining( {
method: 'GET',
path: expect.stringMatching(
// I am using a regex to ensure the order of the parameters doesn't matter.
/(?=.*\/wc\/store\/v1\/order\/1)(?=.*billing_email=cheese%40toast.com)(?=.*key=key_123)/
),
} )
);
} );

it( 'places an order', async () => {
const api = new ExpressCheckoutOrderApi( {
orderId: '1',
key: 'key_123',
billingEmail: '[email protected]',
} );

await api.placeOrder( {
billing_address: {
first_name: 'Fake',
},
shipping_address: {
first_name: 'Test',
},
anythingElse: 'passedThrough',
} );
expect( apiFetch ).toHaveBeenCalledWith(
expect.objectContaining( {
method: 'POST',
path: '/wc/store/v1/checkout/1',
headers: expect.objectContaining( {
Nonce: 'global_store_api_nonce',
} ),
data: expect.objectContaining( {
key: 'key_123',
billing_email: '[email protected]',
billing_address: undefined,
shipping_address: undefined,
anythingElse: 'passedThrough',
} ),
} )
);
} );

it( 'places an order with the previous API request data', async () => {
const api = new ExpressCheckoutOrderApi( {
orderId: '1',
key: 'key_123',
billingEmail: '[email protected]',
} );

apiFetch.mockResolvedValueOnce( {
billing_address: {
first_name: 'Fake',
last_name: 'Test',
},
shipping_address: {
first_name: 'Test',
last_name: 'Fake',
},
} );
await api.getCart();

await api.placeOrder( {
billing_address: {
first_name: 'Fake',
},
shipping_address: {
first_name: 'Test',
},
anythingElse: 'passedThrough',
} );

expect( apiFetch ).toHaveBeenCalledWith(
expect.objectContaining( {
method: 'POST',
path: '/wc/store/v1/checkout/1',
headers: expect.objectContaining( {
Nonce: 'global_store_api_nonce',
} ),
data: expect.objectContaining( {
key: 'key_123',
billing_email: '[email protected]',
billing_address: expect.objectContaining( {
first_name: 'Fake',
last_name: 'Test',
} ),
shipping_address: expect.objectContaining( {
first_name: 'Test',
last_name: 'Fake',
} ),
anythingElse: 'passedThrough',
} ),
} )
);
} );
} );
2 changes: 1 addition & 1 deletion client/tokenized-express-checkout/blocks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { __ } from '@wordpress/i18n';
import { PAYMENT_METHOD_NAME_EXPRESS_CHECKOUT_ELEMENT } from 'wcpay/checkout/constants';
import { getConfig } from 'wcpay/utils/checkout';
import ExpressCheckoutContainer from './components/express-checkout-container';
import { checkPaymentMethodIsAvailable } from '../utils/checkPaymentMethodIsAvailable';
import { checkPaymentMethodIsAvailable } from './checkPaymentMethodIsAvailable';

export const tokenizedExpressCheckoutElementApplePay = ( api ) => ( {
paymentMethodId: PAYMENT_METHOD_NAME_EXPRESS_CHECKOUT_ELEMENT,
Expand Down
Loading

0 comments on commit 1d3fc0f

Please sign in to comment.