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

Implement Stripe Express Checkout Elements (ECE) on the Product page #3441

Merged
merged 71 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
7d1d4b7
add ece feature flag
Mayisha Sep 2, 2024
a5cc1f0
update '@stripe/react-stripe-js' to latest
Mayisha Sep 3, 2024
26c0196
render ece button on block checkout
Mayisha Sep 3, 2024
fe572d5
display ece button if feature flag is enabled
Mayisha Sep 4, 2024
48e7e11
Merge branch 'develop' into task/3402-ece-ff
Mayisha Sep 9, 2024
122db02
check ece feature flag status from blocks data
Mayisha Sep 9, 2024
6a8e7ae
add min height to express checkout container
Mayisha Sep 9, 2024
34dc409
create 'WC_Stripe_Express_Checkout_Element' class
Mayisha Sep 10, 2024
ac13e24
fix callback function name
Mayisha Sep 10, 2024
0da1b05
register script for shortcode checkout
Mayisha Sep 10, 2024
9291021
move ajax functions to separate class
Mayisha Sep 10, 2024
8c6f05d
move helper functions to a separate class
Mayisha Sep 10, 2024
e3f68d3
include and initialize express checkout classes
Mayisha Sep 10, 2024
9502a5a
Merge branch 'develop' into task/ece-backend
Mayisha Sep 10, 2024
55e661c
make functions public in the helper class
Mayisha Sep 11, 2024
08c3205
fix lint issue
Mayisha Sep 11, 2024
7539f73
Merge branch 'develop' into task/ece-backend
Mayisha Sep 11, 2024
1f42347
use correct global variable
Mayisha Sep 11, 2024
632b63c
fix php lint issues
Mayisha Sep 11, 2024
1be4921
Integrating ECE to shortcode checkout
wjrosa Sep 13, 2024
f2b64f3
Adding extra contraints to show ECE
wjrosa Sep 13, 2024
1e901c9
Merge branch 'develop' into add/ece-for-shortcode-checkout
wjrosa Sep 17, 2024
b318c26
Importing additional implementations from WCPay
wjrosa Sep 17, 2024
8956e8e
Use our own Stripe tracking
mattallan Sep 17, 2024
0af7c3e
Fix typos in normalizeOrderData
mattallan Sep 17, 2024
acc8e92
Add missing client/api methods to handle ECE requests
mattallan Sep 18, 2024
afbb84e
Update ESLint config to not require await directly within async funct…
mattallan Sep 18, 2024
0cd28d5
Fix JS linting issues
mattallan Sep 18, 2024
d94bde5
Use startExpressCheckoutElement() to init the payment button
james-allan Sep 18, 2024
56225b3
Add getExpressCheckoutAjaxURL util function
mattallan Sep 18, 2024
9a40a5d
Update ECE API functions to use express checkout data
mattallan Sep 18, 2024
942af31
Move api const to top and add init function for page specific initial…
james-allan Sep 18, 2024
3efecab
Fix comment
james-allan Sep 18, 2024
3a24788
Adding changelog entry
wjrosa Sep 18, 2024
c07f00d
Putting display none back
wjrosa Sep 18, 2024
8244cf3
Multiple changes to the display logic based on WCPay
wjrosa Sep 18, 2024
99cceb4
Multiple changes to the display logic based on WCPay
wjrosa Sep 18, 2024
b894df0
Fix multiple issues + changing the main element ID
wjrosa Sep 18, 2024
ef6f49d
Removing unsupported coalesce operator
wjrosa Sep 18, 2024
1114804
Including missing methods
wjrosa Sep 18, 2024
d0055ea
Fix lint issues
wjrosa Sep 18, 2024
eeee466
Fix lint issues
wjrosa Sep 18, 2024
bc581aa
Updating ESLint version and requirement
wjrosa Sep 18, 2024
e839cd0
ECE instantiation options update
wjrosa Sep 18, 2024
bd7dacd
Fix get/update shipping option AJAX requests
mattallan Sep 19, 2024
1168099
Rename WooPayment related funtion and fix camelcase issue
james-allan Sep 19, 2024
a933b6e
Fetch proper requestShipping meta from product data
mattallan Sep 19, 2024
e49eb3a
Set 'wc-stripe-is-deferred-intent' in data submitted with the checkou…
mattallan Sep 19, 2024
2b02cf9
Fix incorrect product prices when store has price includes tax setting
mattallan Sep 19, 2024
75f7bea
Implement adding the product to the cart when clicking the ECE from t…
mattallan Sep 19, 2024
211498d
Fix tax and shipping values being displayed as NULL
mattallan Sep 19, 2024
92ed337
Don't send shipping line items if the store doesn't have shipping met…
mattallan Sep 19, 2024
0316b76
Initialize/start the Stripe Express Checkout Elements on the product …
mattallan Sep 19, 2024
fbb6a05
Fix changing quantities and variations, also add support for deposits
mattallan Sep 19, 2024
4818396
Merge branch 'develop' into add/3410-implement-ece-product-page
mattallan Sep 27, 2024
f7f7ca2
Merge branch 'develop' into add/3410-implement-ece-product-page
mattallan Sep 30, 2024
c6e271a
Use correct get shipping nonce key
mattallan Sep 30, 2024
dbef055
Add support for add bookings products to the cart
mattallan Sep 30, 2024
ebfbded
When changing booking dates, clear the cart and delete any 'in-cart' …
mattallan Sep 30, 2024
1fdf3df
Fix whitespace
mattallan Sep 30, 2024
381a5c8
Delete duplicate files that have moved to client/express-checkout
mattallan Sep 30, 2024
42ef0f5
Fix merge conflict errors
mattallan Sep 30, 2024
0e4539b
Bring fixes from develop to this branch due to bad handling of conflicts
mattallan Sep 30, 2024
277cb17
Bring fixes from develop to this branch due to bad handling of conflicts
mattallan Sep 30, 2024
616c084
Add changelog entry
mattallan Sep 30, 2024
538f1b5
Add deposits support by hiding itemization when cart contains deposits
mattallan Sep 30, 2024
eb4b04a
fix line items totals not matching total amount
mattallan Sep 30, 2024
030804e
send currency when fetching the stripe amount after selecting a varia…
mattallan Sep 30, 2024
a35bc3f
Merge branch 'develop' into add/3410-implement-ece-product-page
wjrosa Sep 30, 2024
fea0f12
Add changelog entry
mattallan Oct 1, 2024
0252c3e
Merge branch 'develop' into add/3410-implement-ece-product-page
mattallan Oct 1, 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
1 change: 0 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ module.exports = {
node: true,
},
rules: {
'require-await': 'error',
'react-hooks/exhaustive-deps': 'error',
'react-hooks/rules-of-hooks': 'error',
'react/jsx-curly-brace-presence': [
Expand Down
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* Add - Implemented the "Update all subscriptions payment methods" checkbox on My Account → Payment methods for UPE payment methods.
* Add - Add support for the new Stripe Checkout Element on the shortcode checkout page.
* Add - Add support for the new Stripe Checkout Element on the pay for order page.
* Add - Add support for the new Stripe Checkout Element on the product page.
* Dev - Introduces a new class with payment methods constants.
* Dev - Introduces a new class with currency codes constants.
* Dev - Improves the readability of the redirect URL generation code (UPE).
Expand Down
43 changes: 43 additions & 0 deletions client/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -548,4 +548,47 @@ export default class WCStripeAPI {
...paymentData,
} );
}

/**
* Add product to cart from product page.
*
* @param {Object} productData Product data.
* @return {Promise} Promise for the request to the server.
*/
expressCheckoutAddToCart( productData ) {
return this.request( getExpressCheckoutAjaxURL( 'add_to_cart' ), {
security: getExpressCheckoutData( 'nonce' )?.add_to_cart,
...productData,
} );
}

/**
* Get selected product data from variable product page.
*
* @param {Object} productData Product data.
* @return {Promise} Promise for the request to the server.
*/
expressCheckoutGetSelectedProductData( productData ) {
return this.request(
getExpressCheckoutAjaxURL( 'get_selected_product_data' ),
{
security: getExpressCheckoutData( 'nonce' )
?.get_selected_product_data,
...productData,
}
);
}

/**
* Empty the cart.
*
* @param {number} bookingId Booking ID.
* @return {Promise} Promise for the request to the server.
*/
expressCheckoutEmptyCart( bookingId ) {
return this.request( getExpressCheckoutAjaxURL( 'clear_cart' ), {
security: getExpressCheckoutData( 'nonce' )?.clear_cart,
booking_id: bookingId,
} );
}
}
179 changes: 178 additions & 1 deletion client/entrypoints/express-checkout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ jQuery( function ( $ ) {
}

const publishableKey = getExpressCheckoutData( 'stripe' ).publishable_key;
const quantityInputSelector = '.quantity .qty[type=number]';

if ( ! publishableKey ) {
// If no configuration is present, probably this is not the checkout page.
return;
Expand Down Expand Up @@ -285,7 +287,19 @@ jQuery( function ( $ ) {
order,
} );
} else if ( getExpressCheckoutData( 'is_product_page' ) ) {
// Product page specific initialization.
wcStripeECE.startExpressCheckoutElement( {
mode: 'payment',
total: getExpressCheckoutData( 'product' )?.total.amount,
currency: getExpressCheckoutData( 'product' )?.currency,
requestShipping:
getExpressCheckoutData( 'product' )?.requestShipping ??
false,
requestPhone:
getExpressCheckoutData( 'checkout' )
?.needs_payer_phone ?? false,
displayItems: getExpressCheckoutData( 'product' )
.displayItems,
} );
} else {
// Cart and Checkout page specific initialization.
api.expressCheckoutGetCartDetails().then( ( cart ) => {
Expand All @@ -306,6 +320,131 @@ jQuery( function ( $ ) {
wcStripeECE.paymentAborted = false;
},

getAttributes: () => {
const select = $( '.variations_form' ).find( '.variations select' );
const data = {};
let count = 0;
let chosen = 0;

select.each( function () {
const attributeName =
$( this ).data( 'attribute_name' ) ||
$( this ).attr( 'name' );
const value = $( this ).val() || '';

if ( value.length > 0 ) {
chosen++;
}

count++;
data[ attributeName ] = value;
} );

return {
count,
chosenCount: chosen,
data,
};
},

getSelectedProductData: () => {
let productId = $( '.single_add_to_cart_button' ).val();

// Check if product is a variable product.
if ( $( '.single_variation_wrap' ).length ) {
productId = $( '.single_variation_wrap' )
.find( 'input[name="product_id"]' )
.val();
}

// WC Bookings Support.
if ( $( '.wc-bookings-booking-form' ).length ) {
productId = $( '.wc-booking-product-id' ).val();
}

const addons =
$( '#product-addons-total' ).data( 'price_data' ) || [];
const addonValue = addons.reduce(
( sum, addon ) => sum + addon.cost,
0
);

// WC Deposits Support.
const depositObject = {};
if ( $( 'input[name=wc_deposit_option]' ).length ) {
depositObject.wc_deposit_option = $(
'input[name=wc_deposit_option]:checked'
).val();
}
if ( $( 'input[name=wc_deposit_payment_plan]' ).length ) {
depositObject.wc_deposit_payment_plan = $(
'input[name=wc_deposit_payment_plan]:checked'
).val();
}

const data = {
product_id: productId,
qty: $( quantityInputSelector ).val(),
attributes: $( '.variations_form' ).length
? wcStripeECE.getAttributes().data
: [],
addon_value: addonValue,
...depositObject,
};

return api.expressCheckoutGetSelectedProductData( data );
},

/**
* Adds the item to the cart and return cart details.
*
* @return {Promise} Promise for the request to the server.
*/
addToCart: () => {
let productId = $( '.single_add_to_cart_button' ).val();

// Check if product is a variable product.
if ( $( '.single_variation_wrap' ).length ) {
productId = $( '.single_variation_wrap' )
.find( 'input[name="product_id"]' )
.val();
}

if ( $( '.wc-bookings-booking-form' ).length ) {
productId = $( '.wc-booking-product-id' ).val();
}

const data = {
product_id: productId,
qty: $( quantityInputSelector ).val(),
attributes: $( '.variations_form' ).length
? wcStripeECE.getAttributes().data
: [],
};

// Add extension data to the POST body
const formData = $( 'form.cart' ).serializeArray();
$.each( formData, ( i, field ) => {
if ( /^(addon-|wc_)/.test( field.name ) ) {
if ( /\[\]$/.test( field.name ) ) {
const fieldName = field.name.substring(
0,
field.name.length - 2
);
if ( data[ fieldName ] ) {
data[ fieldName ].push( field.value );
} else {
data[ fieldName ] = [ field.value ];
}
} else {
data[ field.name ] = field.value;
}
}
} );

return api.expressCheckoutAddToCart( data );
},

/**
* Complete payment.
*
Expand Down Expand Up @@ -480,4 +619,42 @@ jQuery( function ( $ ) {
};

wcStripeECE.init();

// Handle bookable products on the product page.
let wcBookingFormChanged = false;

$( document.body )
.off( 'wc_booking_form_changed' )
.on( 'wc_booking_form_changed', () => {
wcBookingFormChanged = true;
} );

// Listen for the WC Bookings wc_bookings_calculate_costs event to complete
// and add the bookable product to the cart, using the response to update the
// payment request request params with correct totals.
$( document ).ajaxComplete( function ( event, xhr, settings ) {
if ( wcBookingFormChanged ) {
if (
settings.url === window.booking_form_params.ajax_url &&
settings.data.includes( 'wc_bookings_calculate_costs' ) &&
xhr.responseText.includes( 'SUCCESS' )
) {
wcStripeECE.blockExpressCheckoutButton();
wcBookingFormChanged = false;

return wcStripeECE.addToCart().then( ( response ) => {
getExpressCheckoutData( 'product' ).total = response.total;
getExpressCheckoutData( 'product' ).displayItems =
response.displayItems;

// Empty the cart to avoid having 2 products in the cart when payment request is not used.
api.expressCheckoutEmptyCart( response.bookingId );

wcStripeECE.init();

wcStripeECE.unblockExpressCheckoutButton();
} );
}
}
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public function ajax_add_to_cart() {
WC()->cart->add_to_cart( $product->get_id(), $qty, $variation_id, $attributes );
}

if ( in_array( $product_type, [ 'simple', 'variation', 'subscription', 'subscription_variation' ], true ) ) {
if ( in_array( $product_type, [ 'simple', 'variation', 'subscription', 'subscription_variation', 'booking' ], true ) ) {
WC()->cart->add_to_cart( $product->get_id(), $qty );
}

Expand All @@ -110,6 +110,14 @@ public function ajax_add_to_cart() {
$data += $this->express_checkout_helper->build_display_items();
$data['result'] = 'success';

if ( 'booking' === $product_type ) {
$booking_id = $this->express_checkout_helper->get_booking_id_from_cart();

if ( ! empty( $booking_id ) ) {
$data['bookingId'] = $booking_id;
}
}

// @phpstan-ignore-next-line (return statement is added)
wp_send_json( $data );
}
Expand All @@ -120,7 +128,17 @@ public function ajax_add_to_cart() {
public function ajax_clear_cart() {
check_ajax_referer( 'wc-stripe-clear-cart', 'security' );

$booking_id = isset( $_POST['booking_id'] ) ? absint( $_POST['booking_id'] ) : null;

WC()->cart->empty_cart();

if ( $booking_id ) {
// When a bookable product is added to the cart, a 'booking' is create with status 'in-cart'.
// This status is used to prevent the booking from being booked by another customer
// and should be removed when the cart is emptied for PRB purposes.
do_action( 'wc-booking-remove-inactive-cart', $booking_id ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
}

exit;
}

Expand Down Expand Up @@ -186,11 +204,14 @@ public function ajax_get_selected_product_data() {
check_ajax_referer( 'wc-stripe-get-selected-product-data', 'security' );

try { // @phpstan-ignore-line (return statement is added)
$product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0;
$qty = ! isset( $_POST['qty'] ) ? 1 : apply_filters( 'woocommerce_add_to_cart_quantity', absint( $_POST['qty'] ), $product_id );
$addon_value = isset( $_POST['addon_value'] ) ? max( floatval( $_POST['addon_value'] ), 0 ) : 0;
$product = wc_get_product( $product_id );
$variation_id = null;
$product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0;
$qty = ! isset( $_POST['qty'] ) ? 1 : apply_filters( 'woocommerce_add_to_cart_quantity', absint( $_POST['qty'] ), $product_id );
$addon_value = isset( $_POST['addon_value'] ) ? max( floatval( $_POST['addon_value'] ), 0 ) : 0;
$product = wc_get_product( $product_id );
$variation_id = null;
$currency = get_woocommerce_currency();
$is_deposit = isset( $_POST['wc_deposit_option'] ) ? 'yes' === sanitize_text_field( wp_unslash( $_POST['wc_deposit_option'] ) ) : null;
$deposit_plan_id = isset( $_POST['wc_deposit_payment_plan'] ) ? absint( $_POST['wc_deposit_payment_plan'] ) : 0;

if ( ! is_a( $product, 'WC_Product' ) ) {
/* translators: 1) The product Id */
Expand Down Expand Up @@ -222,27 +243,35 @@ public function ajax_get_selected_product_data() {
throw new Exception( sprintf( __( 'You cannot add that amount of "%1$s"; to the cart because there is not enough stock (%2$s remaining).', 'woocommerce-gateway-stripe' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity(), $product ) ) );
}

$total = $qty * $this->express_checkout_helper->get_product_price( $product ) + $addon_value;
$price = $this->express_checkout_helper->get_product_price( $product, $is_deposit, $deposit_plan_id );
$total = $qty * $price + $addon_value;

$quantity_label = 1 < $qty ? ' (x' . $qty . ')' : '';

$data = [];
$items = [];
$data = [
'currency' => strtolower( $currency ),
'country_code' => substr( get_option( 'woocommerce_default_country' ), 0, 2 ),
'requestShipping' => wc_shipping_enabled() && 0 !== wc_get_shipping_method_count( true ) && $product->needs_shipping(),
];

$items[] = [
'label' => $product->get_name() . $quantity_label,
'amount' => WC_Stripe_Helper::get_stripe_amount( $total ),
];

if ( wc_tax_enabled() ) {
$total_tax = 0;
foreach ( $this->express_checkout_helper->get_taxes_like_cart( $product, $price ) as $tax ) {
$total_tax += $tax;

$items[] = [
'label' => __( 'Tax', 'woocommerce-gateway-stripe' ),
'amount' => 0,
'pending' => true,
'amount' => WC_Stripe_Helper::get_stripe_amount( $tax, $currency ),
'pending' => 0 === $tax,
];
}

if ( wc_shipping_enabled() && $product->needs_shipping() ) {
if ( true === $data['requestShipping'] ) {
$items[] = [
'label' => __( 'Shipping', 'woocommerce-gateway-stripe' ),
'amount' => 0,
Expand All @@ -260,13 +289,9 @@ public function ajax_get_selected_product_data() {
$data['displayItems'] = $items;
$data['total'] = [
'label' => $this->express_checkout_helper->get_total_label(),
'amount' => WC_Stripe_Helper::get_stripe_amount( $total ),
'amount' => WC_Stripe_Helper::get_stripe_amount( $total + $total_tax, $currency ),
];

$data['requestShipping'] = ( wc_shipping_enabled() && $product->needs_shipping() );
$data['currency'] = strtolower( get_woocommerce_currency() );
$data['country_code'] = substr( get_option( 'woocommerce_default_country' ), 0, 2 );

wp_send_json( $data );
} catch ( Exception $e ) {
wp_send_json( [ 'error' => wp_strip_all_tags( $e->getMessage() ) ] );
Expand Down
Loading
Loading