Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: tokenized cart PRBs on PDPs #8644

Merged
merged 56 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from 53 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
6ab91f1
feat: tokenized cart PRBs on PDPs
frosso Apr 15, 2024
ad45e23
WIP variations & compatibility work
frosso Apr 17, 2024
28f0416
Merge branch 'develop' into refactor/pdp-payment-request-tokenized-cart
frosso Apr 19, 2024
962a5b3
WIP
frosso Apr 19, 2024
1cfa7b4
WIP
frosso Apr 22, 2024
fe74f7a
WIP
frosso Apr 23, 2024
bb7dea8
Merge branch 'develop' into refactor/pdp-payment-request-tokenized-cart
frosso Apr 24, 2024
b80e946
WIP
frosso Apr 24, 2024
519983a
WIP
frosso Apr 24, 2024
5507a7f
WIP
frosso Apr 24, 2024
f83a0c6
WIP
frosso Apr 24, 2024
9e39d42
WIP
frosso Apr 24, 2024
6702bcf
WIP
frosso Apr 24, 2024
32f6aaf
WIP
frosso Apr 26, 2024
3660d72
WIP
frosso Apr 26, 2024
3b3867c
Merge branch 'develop' into refactor/pdp-payment-request-tokenized-cart
frosso Apr 26, 2024
468e3d9
WIP
frosso Apr 26, 2024
74be525
WIP
frosso Apr 26, 2024
b63b92e
WIP
frosso Apr 26, 2024
c50e21b
WIP
frosso Apr 26, 2024
a1b99a6
better transformers organization
frosso Apr 30, 2024
e983e0e
WIP
frosso Apr 30, 2024
3635550
fix variable product page load
frosso Apr 30, 2024
87d6460
revert some unrelated changes
frosso Apr 30, 2024
d66eaf4
revert some unrelated changes
frosso Apr 30, 2024
cf071ff
WIP
frosso Apr 30, 2024
5e998af
WIP
frosso May 2, 2024
5f1b216
Merge branch 'develop' into refactor/pdp-payment-request-tokenized-cart
frosso May 2, 2024
792665d
WIP
frosso May 2, 2024
ad0d9fc
WIP
frosso May 2, 2024
05d9e9f
tests
frosso May 2, 2024
ce1bcf6
WIP
frosso May 2, 2024
93bdadf
WIP
frosso May 2, 2024
94478da
PR review feedback
frosso May 6, 2024
2b9e553
Merge branch 'develop' into refactor/pdp-payment-request-tokenized-cart
frosso May 6, 2024
7bc82f9
JSDocs
frosso May 6, 2024
3965245
typos
frosso May 6, 2024
823c8d0
WooPayments <> WcPay
frosso May 6, 2024
57e3055
boolean prefix
frosso May 6, 2024
6540977
better variable naming
frosso May 6, 2024
83989e1
Merge branch 'develop' into refactor/pdp-payment-request-tokenized-cart
frosso May 6, 2024
b42101d
order attribution values
frosso May 6, 2024
913339f
Merge branch 'develop' into refactor/pdp-payment-request-tokenized-cart
frosso May 7, 2024
74fe219
Merge branch 'develop' into refactor/pdp-payment-request-tokenized-cart
frosso May 8, 2024
b8c2402
save point
frosso May 9, 2024
f9919d9
Merge branch 'develop' into refactor/pdp-payment-request-tokenized-cart
frosso May 13, 2024
76e847a
Merge branch 'develop' into refactor/pdp-payment-request-tokenized-cart
frosso May 14, 2024
172ee33
adding await, just because
frosso May 14, 2024
9ec44c7
Merge branch 'develop' into refactor/pdp-payment-request-tokenized-cart
frosso May 15, 2024
de28d7f
adding additional header to Store API response
frosso May 15, 2024
86991a1
do not enqueue woopay checkout scripts on product pages
frosso May 16, 2024
7f6941f
Merge branch 'develop' into refactor/pdp-payment-request-tokenized-cart
frosso May 16, 2024
77e0ae1
remove payment_request_api TODO
frosso May 16, 2024
b92ed0f
reuse of getPaymentRequestData
frosso May 17, 2024
fd4085c
adding await to avoid issues with fast clickers
frosso May 17, 2024
91a26d1
Merge branch 'develop' into refactor/pdp-payment-request-tokenized-cart
frosso May 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
195 changes: 195 additions & 0 deletions client/tokenized-payment-request/cart-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/* global jQuery */

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

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':
window.wcpayPaymentRequestParams.nonce
.tokenized_cart_nonce || undefined,
frosso marked this conversation as resolved.
Show resolved Hide resolved
...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() {
frosso marked this conversation as resolved.
Show resolved Hide resolved
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':
window.wcpayPaymentRequestParams.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
Loading