Skip to content

Commit

Permalink
feat: tokenized cart PRBs on PDPs (#8644)
Browse files Browse the repository at this point in the history
  • Loading branch information
frosso authored May 17, 2024
1 parent 9eab5e4 commit b0436e9
Show file tree
Hide file tree
Showing 24 changed files with 1,814 additions and 47 deletions.
4 changes: 4 additions & 0 deletions changelog/refactor-pdp-payment-request-tokenized-cart
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: add

feat: tokenized cart PRBs on PDPs via feature flag.
47 changes: 47 additions & 0 deletions client/tokenized-payment-request/button-ui.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* global jQuery */

let $wcpayPaymentRequestContainer = null;

const paymentRequestButtonUi = {
init: ( { $container } ) => {
$wcpayPaymentRequestContainer = $container;
},

getElements: () => {
return jQuery(
'.wcpay-payment-request-wrapper,#wcpay-payment-request-button-separator'
);
},

blockButton: () => {
// check if element isn't already blocked before calling block() to avoid blinking overlay issues
// blockUI.isBlocked is either undefined or 0 when element is not blocked
if ( $wcpayPaymentRequestContainer.data( 'blockUI.isBlocked' ) ) {
return;
}

$wcpayPaymentRequestContainer.block( { message: null } );
},

unblockButton: () => {
paymentRequestButtonUi.show();
$wcpayPaymentRequestContainer.unblock();
},

showButton: ( paymentRequestButton ) => {
if ( $wcpayPaymentRequestContainer.length ) {
paymentRequestButtonUi.show();
paymentRequestButton.mount( '#wcpay-payment-request-button' );
}
},

hide: () => {
paymentRequestButtonUi.getElements().hide();
},

show: () => {
paymentRequestButtonUi.getElements().show();
},
};

export default paymentRequestButtonUi;
200 changes: 200 additions & 0 deletions client/tokenized-payment-request/cart-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/* global jQuery */

/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import { applyFilters } from '@wordpress/hooks';

/**
* Internal dependencies
*/
import { getPaymentRequestData } from './frontend-utils';

export default class PaymentRequestCartApi {
// Used on product pages to interact with an anonymous cart.
// This anonymous cart is separate from the customer's cart, which might contain additional products.
// This functionality is also useful to calculate product/shipping pricing (and shipping needs)
// for compatibility scenarios with other plugins (like WC Bookings, Product Add-Ons, WC Deposits, etc.).
cartRequestHeaders = {};

/**
* Creates an order from the cart object.
* See https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/StoreApi/docs/checkout.md#process-order-and-payment
*
* @param {{
* billing_address: Object,
* shipping_address: Object,
* customer_note: string?,
* payment_method: string,
* payment_data: Array,
* }} paymentData Additional payment data to place the order.
* @return {Promise} Result of the order creation request.
*/
async placeOrder( paymentData ) {
return await apiFetch( {
method: 'POST',
path: '/wc/store/v1/checkout',
credentials: 'omit',
headers: {
'X-WooPayments-Express-Payment-Request': true,
'X-WooPayments-Express-Payment-Request-Nonce':
getPaymentRequestData( 'nonce' ).tokenized_cart_nonce ||
undefined,
...this.cartRequestHeaders,
},
data: paymentData,
} );
}

/**
* Returns the customer's cart object.
* See https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/StoreApi/docs/cart.md#get-cart
*
* @return {Promise} Cart response object.
*/
async getCart() {
return await apiFetch( {
method: 'GET',
path: '/wc/store/v1/cart',
headers: {
...this.cartRequestHeaders,
},
} );
}

/**
* Creates and returns a new cart object. The response type is the same as `getCart()`.
*
* @return {Promise} Cart response object.
*/
async createAnonymousCart() {
const response = await apiFetch( {
method: 'GET',
path: '/wc/store/v1/cart',
// omitting credentials, to create a new cart object separate from the user's cart.
credentials: 'omit',
// parse: false to ensure we can get the response headers
parse: false,
} );

this.cartRequestHeaders = {
Nonce: response.headers.get( 'Nonce' ),
'Cart-Token': response.headers.get( 'Cart-Token' ),
'X-WooPayments-Express-Payment-Request-Nonce': response.headers.get(
'X-WooPayments-Express-Payment-Request-Nonce'
),
};
}

/**
* Update customer data and return the full cart response, or an error.
* See https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/StoreApi/docs/cart.md#update-customer
*
* @param {{
* billing_address: Object?,
* shipping_address: Object?,
* }} customerData Customer data to update.
* @return {Promise} Cart Response on success, or an Error Response on failure.
*/
async updateCustomer( customerData ) {
return await apiFetch( {
method: 'POST',
path: '/wc/store/v1/cart/update-customer',
credentials: 'omit',
headers: {
'X-WooPayments-Express-Payment-Request': true,
'X-WooPayments-Express-Payment-Request-Nonce':
getPaymentRequestData( 'nonce' ).tokenized_cart_nonce ||
undefined,
...this.cartRequestHeaders,
},
data: customerData,
} );
}

/**
* Selects an available shipping rate for a package, then returns the full cart response, or an error
* See https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/StoreApi/docs/cart.md#select-shipping-rate
*
* @param {{rate_id: string, package_id: integer}} shippingRate The selected shipping rate.
* @return {Promise} Cart Response on success, or an Error Response on failure.
*/
async selectShippingRate( shippingRate ) {
return await apiFetch( {
method: 'POST',
path: '/wc/store/v1/cart/select-shipping-rate',
credentials: 'omit',
headers: {
...this.cartRequestHeaders,
},
data: shippingRate,
} );
}

/**
* Add an item to the cart and return the full cart response, or an error.
* See https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/StoreApi/docs/cart.md#add-item
*
* @return {Promise} Cart Response on success, or an Error Response on failure.
*/
async addProductToCart() {
const productData = {
// can be modified in case of variable products, WC bookings plugin, etc.
id: jQuery( '.single_add_to_cart_button' ).val(),
quantity: parseInt( jQuery( '.quantity .qty' ).val(), 10 ) || 1,
// can be modified in case of variable products, WC bookings plugin, etc.
variation: [],
};

return await apiFetch( {
method: 'POST',
path: '/wc/store/v1/cart/add-item',
credentials: 'omit',
headers: {
...this.cartRequestHeaders,
},
data: applyFilters(
'wcpay.payment-request.cart-add-item',
productData
),
} );
}

/**
* Removes all items from the cart and clears the cart headers.
* See https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/StoreApi/docs/cart.md#remove-item
*
* @return {undefined}
*/
async emptyCart() {
try {
const cartData = await apiFetch( {
method: 'GET',
path: '/wc/store/v1/cart',
credentials: 'omit',
headers: {
...this.cartRequestHeaders,
},
} );

const removeItemsPromises = cartData.items.map( ( item ) => {
return apiFetch( {
method: 'POST',
path: '/wc/store/v1/cart/remove-item',
credentials: 'omit',
headers: {
...this.cartRequestHeaders,
},
data: {
key: item.key,
},
} );
} );

await Promise.all( removeItemsPromises );
} catch ( e ) {
// let's ignore the error, it's likely not going to be relevant.
}
}
}
15 changes: 15 additions & 0 deletions client/tokenized-payment-request/compatibility/wc-deposits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* global jQuery */
jQuery( ( $ ) => {
// WooCommerce Deposits support.
// Trigger the "woocommerce_variation_has_changed" event when the deposit option is changed.
$( 'input[name=wc_deposit_option],input[name=wc_deposit_payment_plan]' ).on(
'change',
() => {
$( 'form' )
.has(
'input[name=wc_deposit_option],input[name=wc_deposit_payment_plan]'
)
.trigger( 'woocommerce_variation_has_changed' );
}
);
} );
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* global jQuery */

/**
* External dependencies
*/
import { addFilter } from '@wordpress/hooks';

addFilter(
'wcpay.payment-request.cart-place-order-extension-data',
'automattic/wcpay/payment-request',
( extensionData ) => {
const orderAttributionValues = jQuery(
'#wcpay-express-checkout__order-attribution-inputs input'
);

if ( ! orderAttributionValues.length ) {
return extensionData;
}

const orderAttributionData = {};
orderAttributionValues.each( function () {
const name = jQuery( this )
.attr( 'name' )
.replace( 'wc_order_attribution_', '' );
const value = jQuery( this ).val();

if ( name && value ) {
orderAttributionData[ name ] = value;
}
} );

return {
...extensionData,
'woocommerce/order-attribution': orderAttributionData,
};
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* global jQuery */

/**
* External dependencies
*/
import { addFilter, doAction } from '@wordpress/hooks';
import paymentRequestButtonUi from '../button-ui';
import { waitForAction } from '../frontend-utils';

jQuery( ( $ ) => {
$( document.body ).on( 'woocommerce_variation_has_changed', async () => {
try {
paymentRequestButtonUi.blockButton();

doAction( 'wcpay.payment-request.update-button-data' );
await waitForAction( 'wcpay.payment-request.update-button-data' );

paymentRequestButtonUi.unblockButton();
} catch ( e ) {
paymentRequestButtonUi.hide();
}
} );
} );

addFilter(
'wcpay.payment-request.cart-add-item',
'automattic/wcpay/payment-request',
( productData ) => {
const $variationInformation = jQuery( '.single_variation_wrap' );
if ( ! $variationInformation.length ) {
return productData;
}

const productId = $variationInformation
.find( 'input[name="product_id"]' )
.val();
return {
...productData,
id: parseInt( productId, 10 ),
};
}
);
addFilter(
'wcpay.payment-request.cart-add-item',
'automattic/wcpay/payment-request',
( productData ) => {
const $variationsForm = jQuery( '.variations_form' );
if ( ! $variationsForm.length ) {
return productData;
}

const attributes = [];
const $variationSelectElements = $variationsForm.find(
'.variations select'
);
$variationSelectElements.each( function () {
const $select = jQuery( this );
const attributeName =
$select.data( 'attribute_name' ) || $select.attr( 'name' );

attributes.push( {
// The Store API accepts the variable attribute's label, rather than an internal identifier:
// https://github.com/woocommerce/woocommerce-blocks/blob/trunk/src/StoreApi/docs/cart.md#add-item
// It's an unfortunate hack that doesn't work when labels have special characters in them.
attribute: document.querySelector(
`label[for="${ attributeName.replace(
'attribute_',
''
) }"]`
).innerHTML,
value: $select.val() || '',
} );
} );

return {
...productData,
variation: [ ...productData.variation, ...attributes ],
};
}
);
Loading

0 comments on commit b0436e9

Please sign in to comment.