diff --git a/changelog/9001-tokenized-prb-order-origin b/changelog/9001-tokenized-prb-order-origin
new file mode 100644
index 00000000000..82d4c4aac15
--- /dev/null
+++ b/changelog/9001-tokenized-prb-order-origin
@@ -0,0 +1,5 @@
+Significance: patch
+Type: fix
+Comment: Bug fix for PRB feature behind a flag.
+
+
diff --git a/changelog/as-8871-ece-on-supported-product-types b/changelog/as-8871-ece-on-supported-product-types
new file mode 100644
index 00000000000..5aa499e2693
--- /dev/null
+++ b/changelog/as-8871-ece-on-supported-product-types
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add ECE support for multiple product types.
diff --git a/client/express-checkout/event-handlers.js b/client/express-checkout/event-handlers.js
index 21c1199bce3..f0ae3359376 100644
--- a/client/express-checkout/event-handlers.js
+++ b/client/express-checkout/event-handlers.js
@@ -13,6 +13,7 @@ import {
trackExpressCheckoutButtonClick,
trackExpressCheckoutButtonLoad,
} from './tracking';
+import { __ } from '@wordpress/i18n';
export const shippingAddressChangeHandler = async ( api, event, elements ) => {
try {
@@ -77,27 +78,27 @@ export const onConfirmHandler = async (
return abortPayment( event, error.message );
}
- // Kick off checkout processing step.
- let orderResponse;
- if ( ! order ) {
- orderResponse = await api.expressCheckoutECECreateOrder(
- normalizeOrderData( event, paymentMethod.id )
- );
- } else {
- orderResponse = await api.expressCheckoutECEPayForOrder(
- order,
- normalizePayForOrderData( event, paymentMethod.id )
- );
- }
+ try {
+ // Kick off checkout processing step.
+ let orderResponse;
+ if ( ! order ) {
+ orderResponse = await api.expressCheckoutECECreateOrder(
+ normalizeOrderData( event, paymentMethod.id )
+ );
+ } else {
+ orderResponse = await api.expressCheckoutECEPayForOrder(
+ order,
+ normalizePayForOrderData( event, paymentMethod.id )
+ );
+ }
- if ( orderResponse.result !== 'success' ) {
- return abortPayment(
- event,
- getErrorMessageFromNotice( orderResponse.messages )
- );
- }
+ if ( orderResponse.result !== 'success' ) {
+ return abortPayment(
+ event,
+ getErrorMessageFromNotice( orderResponse.messages )
+ );
+ }
- try {
const confirmationRequest = api.confirmIntent( orderResponse.redirect );
// `true` means there is no intent to confirm.
@@ -109,7 +110,14 @@ export const onConfirmHandler = async (
completePayment( redirectUrl );
}
} catch ( e ) {
- return abortPayment( event, e.message );
+ return abortPayment(
+ event,
+ e.message ??
+ __(
+ 'There was a problem processing the order.',
+ 'woocommerce-payments'
+ )
+ );
}
};
diff --git a/client/express-checkout/index.js b/client/express-checkout/index.js
index 94be0031230..71d2e75135e 100644
--- a/client/express-checkout/index.js
+++ b/client/express-checkout/index.js
@@ -1,5 +1,6 @@
/* global jQuery, wcpayExpressCheckoutParams, wcpayECEPayForOrderParams */
import { __ } from '@wordpress/i18n';
+import { debounce } from 'lodash';
/**
* Internal dependencies
@@ -19,6 +20,7 @@ import {
shippingAddressChangeHandler,
shippingRateChangeHandler,
} from './event-handlers';
+import { displayLoginConfirmation } from './utils';
jQuery( ( $ ) => {
// Don't load if blocks checkout is being loaded.
@@ -30,6 +32,7 @@ jQuery( ( $ ) => {
}
const publishableKey = wcpayExpressCheckoutParams.stripe.publishableKey;
+ const quantityInputSelector = '.quantity .qty[type=number]';
if ( ! publishableKey ) {
// If no configuration is present, probably this is not the checkout page.
@@ -50,6 +53,12 @@ jQuery( ( $ ) => {
}
);
+ let wcPayECEError = '';
+ const defaultErrorMessage = __(
+ 'There was an error getting the product information.',
+ 'woocommerce-payments'
+ );
+
/**
* Object to handle Stripe payment forms.
*/
@@ -156,7 +165,7 @@ jQuery( ( $ ) => {
const data = {
product_id: productId,
- qty: $( '.quantity .qty' ).val(),
+ qty: $( quantityInputSelector ).val(),
attributes: $( '.variations_form' ).length
? wcpayECE.getAttributes().data
: [],
@@ -250,9 +259,45 @@ jQuery( ( $ ) => {
wcpayECE.showButton( eceButton );
eceButton.on( 'click', function ( event ) {
- // TODO: handle cases where we need login confirmation.
+ // If login is required for checkout, display redirect confirmation dialog.
+ if ( getExpressCheckoutData( 'login_confirmation' ) ) {
+ displayLoginConfirmation( event.expressPaymentType );
+ return;
+ }
if ( getExpressCheckoutData( 'is_product_page' ) ) {
+ const addToCartButton = $( '.single_add_to_cart_button' );
+
+ // First check if product can be added to cart.
+ if ( addToCartButton.is( '.disabled' ) ) {
+ if (
+ addToCartButton.is( '.wc-variation-is-unavailable' )
+ ) {
+ window.alert(
+ window?.wc_add_to_cart_variation_params
+ ?.i18n_unavailable_text ||
+ __(
+ 'Sorry, this product is unavailable. Please choose a different combination.',
+ 'woocommerce-payments'
+ )
+ );
+ } else {
+ window.alert(
+ __(
+ 'Please select your product options before proceeding.',
+ 'woocommerce-payments'
+ )
+ );
+ }
+ return;
+ }
+
+ if ( wcPayECEError ) {
+ window.alert( wcPayECEError );
+ return;
+ }
+
+ // Add products to the cart if everything is right.
wcpayECE.addToCart();
}
@@ -291,10 +336,15 @@ jQuery( ( $ ) => {
} );
eceButton.on( 'cancel', async () => {
+ wcpayECE.paymentAborted = true;
wcpayECE.unblock();
} );
eceButton.on( 'ready', onReadyHandler );
+
+ if ( getExpressCheckoutData( 'is_product_page' ) ) {
+ wcpayECE.attachProductPageEventListeners( elements );
+ }
},
getSelectedProductData: () => {
@@ -333,7 +383,7 @@ jQuery( ( $ ) => {
const data = {
product_id: productId,
- qty: $( '.quantity .qty' ).val(),
+ qty: $( quantityInputSelector ).val(),
attributes: $( '.variations_form' ).length
? wcpayECE.getAttributes().data
: [],
@@ -356,44 +406,132 @@ jQuery( ( $ ) => {
return elements.create( 'expressCheckout', options );
},
- getElements: () => {
- return $(
- '.wcpay-payment-request-wrapper,#wcpay-express-checkout-button-separator'
- );
- },
-
- hide: () => {
- wcpayECE.getElements().hide();
- },
+ attachProductPageEventListeners: ( elements ) => {
+ $( document.body )
+ .off( 'woocommerce_variation_has_changed' )
+ .on( 'woocommerce_variation_has_changed', () => {
+ wcpayECE.blockExpressCheckoutButton();
+
+ $.when( wcpayECE.getSelectedProductData() )
+ .then( ( response ) => {
+ /**
+ * If the customer aborted the express checkout,
+ * we need to re init the express checkout button to ensure the shipping
+ * options are refetched. If the customer didn't abort the express checkout,
+ * and the product's shipping status is consistent,
+ * we can simply update the express checkout button with the new total and display items.
+ */
+ if (
+ ! wcpayECE.paymentAborted &&
+ getExpressCheckoutData( 'product' )
+ .needs_shipping === response.needs_shipping
+ ) {
+ elements.update( {
+ amount: response.total.amount,
+ displayItems: response.displayItems,
+ } );
+ } else {
+ wcpayECE.reInitExpressCheckoutElement(
+ response
+ );
+ }
+ } )
+ .catch( () => {
+ wcpayECE.hide();
+ } )
+ .always( () => {
+ wcpayECE.unblockExpressCheckoutButton();
+ } );
+ } );
- show: () => {
- wcpayECE.getElements().show();
+ $( '.quantity' )
+ .off( 'input', '.qty' )
+ .on(
+ 'input',
+ '.qty',
+ debounce( () => {
+ wcpayECE.blockExpressCheckoutButton();
+ wcPayECEError = '';
+
+ $.when( wcpayECE.getSelectedProductData() )
+ .then(
+ ( response ) => {
+ // In case the server returns an unexpected response
+ if ( typeof response !== 'object' ) {
+ wcPayECEError = defaultErrorMessage;
+ }
+
+ if (
+ ! wcpayECE.paymentAborted &&
+ getExpressCheckoutData( 'product' )
+ .needs_shipping ===
+ response.needs_shipping
+ ) {
+ elements.update( {
+ amount: response.total.amount,
+ } );
+ } else {
+ wcpayECE.reInitExpressCheckoutElement(
+ response
+ );
+ }
+ },
+ ( response ) => {
+ wcPayECEError =
+ response.responseJSON?.error ??
+ defaultErrorMessage;
+ }
+ )
+ .always( function () {
+ wcpayECE.unblockExpressCheckoutButton();
+ } );
+ }, 250 )
+ );
},
- showButton: ( eceButton ) => {
- if ( $( '#wcpay-express-checkout-element' ).length ) {
- wcpayECE.show();
- eceButton.mount( '#wcpay-express-checkout-element' );
- }
+ reInitExpressCheckoutElement: ( response ) => {
+ wcpayExpressCheckoutParams.product.needs_shipping =
+ response.needs_shipping;
+ wcpayExpressCheckoutParams.product.total = response.total;
+ wcpayExpressCheckoutParams.product.displayItems =
+ response.displayItems;
+ wcpayECE.init();
},
- blockButton: () => {
+ blockExpressCheckoutButton: () => {
// 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 (
- $( '#wcpay-express-checkout-button' ).data(
+ $( '#wcpay-express-checkout-element' ).data(
'blockUI.isBlocked'
)
) {
return;
}
- $( '#wcpay-express-checkout-button' ).block( { message: null } );
+ $( '#wcpay-express-checkout-element' ).block( { message: null } );
},
- unblockButton: () => {
+ unblockExpressCheckoutButton: () => {
wcpayECE.show();
- $( '#wcpay-express-checkout-button' ).unblock();
+ $( '#wcpay-express-checkout-element' ).unblock();
+ },
+
+ getElements: () => {
+ return $(
+ '.wcpay-payment-request-wrapper,#wcpay-express-checkout-button-separator'
+ );
+ },
+
+ show: () => {
+ wcpayECE.getElements().show();
+ },
+
+ showButton: ( eceButton ) => {
+ if ( $( '#wcpay-express-checkout-element' ).length ) {
+ wcpayECE.show();
+ eceButton.mount( '#wcpay-express-checkout-element' );
+ }
},
/**
@@ -454,6 +592,9 @@ jQuery( ( $ ) => {
} );
} );
}
+
+ // After initializing a new express checkout button, we need to reset the paymentAborted flag.
+ wcpayECE.paymentAborted = false;
},
};
diff --git a/client/express-checkout/utils/index.ts b/client/express-checkout/utils/index.ts
index b945ba92974..2d8317bd0c7 100644
--- a/client/express-checkout/utils/index.ts
+++ b/client/express-checkout/utils/index.ts
@@ -4,6 +4,12 @@
export * from './normalize';
import { getDefaultBorderRadius } from 'wcpay/utils/express-checkout';
+interface MyWindow extends Window {
+ wcpayExpressCheckoutParams: WCPayExpressCheckoutParams;
+}
+
+declare let window: MyWindow;
+
/**
* An /incomplete/ representation of the data that is loaded into the frontend for the Express Checkout.
*/
@@ -77,6 +83,12 @@ export interface WCPayExpressCheckoutParams {
amount: number;
};
};
+
+ /**
+ * Settings for the user authentication dialog and redirection.
+ */
+ login_confirmation: { message: string; redirect_url: string } | false;
+
stripe: {
accountId: string;
locale: string;
@@ -97,11 +109,7 @@ export const getExpressCheckoutData = <
>(
key: K
) => {
- if ( window.wcpayExpressCheckoutParams ) {
- return window.wcpayExpressCheckoutParams?.[ key ];
- }
-
- return null;
+ return window.wcpayExpressCheckoutParams?.[ key ] ?? null;
};
/**
@@ -116,6 +124,51 @@ export const getErrorMessageFromNotice = ( notice: string ) => {
return div.firstChild ? div.firstChild.textContent : '';
};
+type ExpressPaymentType =
+ | 'apple_pay'
+ | 'google_pay'
+ | 'amazon_pay'
+ | 'paypal'
+ | 'link';
+
+/**
+ * Displays a `confirm` dialog which leads to a redirect.
+ *
+ * @param expressPaymentType Can be either 'apple_pay', 'google_pay', 'amazon_pay', 'paypal' or 'link'.
+ */
+export const displayLoginConfirmation = (
+ expressPaymentType: ExpressPaymentType
+) => {
+ const loginConfirmation = getExpressCheckoutData( 'login_confirmation' );
+
+ if ( ! loginConfirmation ) {
+ return;
+ }
+
+ const paymentTypesMap = {
+ apple_pay: 'Apple Pay',
+ google_pay: 'Google Pay',
+ amazon_pay: 'Amazon Pay',
+ paypal: 'PayPal',
+ link: 'Link',
+ };
+ let message = loginConfirmation.message;
+
+ // Replace dialog text with specific express checkout type.
+ message = message.replace(
+ /\*\*.*?\*\*/,
+ paymentTypesMap[ expressPaymentType ]
+ );
+
+ // Remove asterisks from string.
+ message = message.replace( /\*\*/g, '' );
+
+ if ( confirm( message ) ) {
+ // Redirect to my account page.
+ window.location.href = loginConfirmation.redirect_url;
+ }
+};
+
/**
* Returns the appearance settings for the Express Checkout buttons.
* Currently only configures border radius for the buttons.
diff --git a/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php
index 94482930a62..c4ba8cff02b 100644
--- a/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php
+++ b/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php
@@ -9,6 +9,9 @@
exit;
}
+use WCPay\Exceptions\Invalid_Price_Exception;
+use WCPay\Logger;
+
/**
* WC_Payments_Express_Checkout_Ajax_Handler class.
*/
@@ -40,6 +43,7 @@ public function init() {
add_action( 'wc_ajax_wcpay_get_shipping_options', [ $this, 'ajax_get_shipping_options' ] );
add_action( 'wc_ajax_wcpay_get_cart_details', [ $this, 'ajax_get_cart_details' ] );
add_action( 'wc_ajax_wcpay_update_shipping_method', [ $this, 'ajax_update_shipping_method' ] );
+ add_action( 'wc_ajax_wcpay_get_selected_product_data', [ $this, 'ajax_get_selected_product_data' ] );
}
/**
@@ -212,6 +216,109 @@ public function ajax_update_shipping_method() {
wp_send_json( $data );
}
+ /**
+ * Gets the selected product data.
+ *
+ * @throws Exception If product or stock is unavailable - caught inside function.
+ */
+ public function ajax_get_selected_product_data() {
+ check_ajax_referer( 'wcpay-get-selected-product-data', 'security' );
+
+ try {
+ $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : false;
+ $qty = ! isset( $_POST['qty'] ) ? 1 : apply_filters( 'woocommerce_add_to_cart_quantity', absint( $_POST['qty'] ), $product_id );
+ $addon_value = isset( $_POST['addon_value'] ) ? max( (float) $_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: product ID */
+ throw new Exception( sprintf( __( 'Product with the ID (%d) cannot be found.', 'woocommerce-payments' ), $product_id ) );
+ }
+
+ if ( ( 'variable' === $product->get_type() || 'variable-subscription' === $product->get_type() ) && isset( $_POST['attributes'] ) ) {
+ $attributes = wc_clean( wp_unslash( $_POST['attributes'] ) );
+
+ $data_store = WC_Data_Store::load( 'product' );
+ $variation_id = $data_store->find_matching_product_variation( $product, $attributes );
+
+ if ( ! empty( $variation_id ) ) {
+ $product = wc_get_product( $variation_id );
+ }
+ }
+
+ // Force quantity to 1 if sold individually and check for existing item in cart.
+ if ( $product->is_sold_individually() ) {
+ $qty = apply_filters( 'wcpay_payment_request_add_to_cart_sold_individually_quantity', 1, $qty, $product_id, $variation_id );
+ }
+
+ if ( ! $product->has_enough_stock( $qty ) ) {
+ /* translators: 1: product name 2: quantity in stock */
+ 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-payments' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity(), $product ) ) );
+ }
+
+ $price = $this->express_checkout_button_helper->get_product_price( $product, $is_deposit, $deposit_plan_id );
+ $total = $qty * $price + $addon_value;
+
+ $quantity_label = 1 < $qty ? ' (x' . $qty . ')' : '';
+
+ $data = [];
+ $items = [];
+
+ $items[] = [
+ 'label' => $product->get_name() . $quantity_label,
+ 'amount' => WC_Payments_Utils::prepare_amount( $total, $currency ),
+ ];
+
+ $total_tax = 0;
+ foreach ( $this->express_checkout_button_helper->get_taxes_like_cart( $product, $price ) as $tax ) {
+ $total_tax += $tax;
+
+ $items[] = [
+ 'label' => __( 'Tax', 'woocommerce-payments' ),
+ 'amount' => WC_Payments_Utils::prepare_amount( $tax, $currency ),
+ 'pending' => 0 === $tax,
+ ];
+ }
+
+ if ( wc_shipping_enabled() && $product->needs_shipping() ) {
+ $items[] = [
+ 'label' => __( 'Shipping', 'woocommerce-payments' ),
+ 'amount' => 0,
+ 'pending' => true,
+ ];
+
+ $data['shippingOptions'] = [
+ 'id' => 'pending',
+ 'label' => __( 'Pending', 'woocommerce-payments' ),
+ 'detail' => '',
+ 'amount' => 0,
+ ];
+ }
+
+ $data['displayItems'] = $items;
+ $data['total'] = [
+ 'label' => $this->express_checkout_button_helper->get_total_label(),
+ 'amount' => WC_Payments_Utils::prepare_amount( $total + $total_tax, $currency ),
+ 'pending' => true,
+ ];
+
+ $data['needs_shipping'] = ( 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 ) {
+ if ( is_a( $e, Invalid_Price_Exception::class ) ) {
+ Logger::log( $e->getMessage() );
+ }
+ wp_send_json( [ 'error' => wp_strip_all_tags( $e->getMessage() ) ], 500 );
+ }
+ }
+
/**
* Adds the current product to the cart. Used on product detail page.
*/
diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php
index 66c203b818f..ba1e0c5bff9 100644
--- a/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php
+++ b/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php
@@ -150,14 +150,29 @@ public function display_express_checkout_buttons() {
} else {
$this->payment_request_button_handler->display_payment_request_button_html();
}
+
+ if ( is_cart() ) {
+ add_action( 'woocommerce_after_cart', [ $this, 'add_order_attribution_inputs' ], 1 );
+ } else {
+ $this->add_order_attribution_inputs();
+ }
+
?>
-
display_express_checkout_separator_if_necessary( $separator_starts_hidden );
}
}
+ /**
+ * Add order attribution inputs to the page.
+ *
+ * @return void
+ */
+ public function add_order_attribution_inputs() {
+ echo '';
+ }
+
/**
* Check if the pay-for-order flow is supported.
*
diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php
index 444977bfb90..6a8d66d1bd8 100644
--- a/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php
+++ b/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php
@@ -88,6 +88,10 @@ public function init() {
return;
}
+ add_action( 'template_redirect', [ $this, 'set_session' ] );
+ add_action( 'template_redirect', [ $this, 'handle_express_checkout_redirect' ] );
+ add_filter( 'woocommerce_login_redirect', [ $this, 'get_login_redirect_url' ], 10, 3 );
+ add_filter( 'woocommerce_registration_redirect', [ $this, 'get_login_redirect_url' ], 10, 3 );
add_action( 'wp_enqueue_scripts', [ $this, 'scripts' ] );
add_action( 'before_woocommerce_pay_form', [ $this, 'display_pay_for_order_page_html' ], 1 );
@@ -111,6 +115,99 @@ public function get_button_settings() {
return array_merge( $common_settings, $payment_request_button_settings );
}
+ /**
+ * Settings array for the user authentication dialog and redirection.
+ *
+ * @return array|false
+ */
+ public function get_login_confirmation_settings() {
+ if ( is_user_logged_in() || ! $this->is_authentication_required() ) {
+ return false;
+ }
+
+ /* translators: The text encapsulated in `**` can be replaced with "Apple Pay" or "Google Pay". Please translate this text, but don't remove the `**`. */
+ $message = __( 'To complete your transaction with **the selected payment method**, you must log in or create an account with our site.', 'woocommerce-payments' );
+ $redirect_url = add_query_arg(
+ [
+ '_wpnonce' => wp_create_nonce( 'wcpay-set-redirect-url' ),
+ 'wcpay_express_checkout_redirect_url' => rawurlencode( home_url( add_query_arg( [] ) ) ),
+ // Current URL to redirect to after login.
+ ],
+ home_url()
+ );
+
+ return [ // nosemgrep: audit.php.wp.security.xss.query-arg -- home_url passed in to add_query_arg.
+ 'message' => $message,
+ 'redirect_url' => $redirect_url,
+ ];
+ }
+
+ /**
+ * Checks whether authentication is required for checkout.
+ *
+ * @return bool
+ */
+ public function is_authentication_required() {
+ // If guest checkout is disabled and account creation is not possible, authentication is required.
+ if ( 'no' === get_option( 'woocommerce_enable_guest_checkout', 'yes' ) && ! $this->is_account_creation_possible() ) {
+ return true;
+ }
+ // If cart contains subscription and account creation is not posible, authentication is required.
+ if ( $this->has_subscription_product() && ! $this->is_account_creation_possible() ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks whether cart contains a subscription product or this is a subscription product page.
+ *
+ * @return boolean
+ */
+ public function has_subscription_product() {
+ if ( ! class_exists( 'WC_Subscriptions_Product' ) || ! class_exists( 'WC_Subscriptions_Cart' ) ) {
+ return false;
+ }
+
+ if ( $this->express_checkout_helper->is_product() ) {
+ $product = $this->express_checkout_helper->get_product();
+ if ( WC_Subscriptions_Product::is_subscription( $product ) ) {
+ return true;
+ }
+ }
+
+ if ( $this->express_checkout_helper->is_checkout() || $this->express_checkout_helper->is_cart() ) {
+ if ( WC_Subscriptions_Cart::cart_contains_subscription() ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks whether account creation is possible during checkout.
+ *
+ * @return bool
+ */
+ public function is_account_creation_possible() {
+ $is_signup_from_checkout_allowed = 'yes' === get_option( 'woocommerce_enable_signup_and_login_from_checkout', 'no' );
+
+ // If a subscription is being purchased, check if account creation is allowed for subscriptions.
+ if ( ! $is_signup_from_checkout_allowed && $this->has_subscription_product() ) {
+ $is_signup_from_checkout_allowed = 'yes' === get_option( 'woocommerce_enable_signup_from_checkout_for_subscriptions', 'no' );
+ }
+
+ // If automatically generate username/password are disabled, the Payment Request API
+ // can't include any of those fields, so account creation is not possible.
+ return (
+ $is_signup_from_checkout_allowed &&
+ 'yes' === get_option( 'woocommerce_registration_generate_username', 'yes' ) &&
+ 'yes' === get_option( 'woocommerce_registration_generate_password', 'yes' )
+ );
+ }
+
/**
* Load public scripts and styles.
*/
@@ -147,7 +244,7 @@ public function scripts() {
'needs_payer_phone' => 'required' === get_option( 'woocommerce_checkout_phone_field', 'required' ),
],
'button' => $this->get_button_settings(),
- 'login_confirmation' => '',
+ 'login_confirmation' => $this->get_login_confirmation_settings(),
'is_product_page' => $this->express_checkout_helper->is_product(),
'button_context' => $this->express_checkout_helper->get_button_context(),
'is_pay_for_order' => $this->express_checkout_helper->is_pay_for_order_page(),
@@ -250,4 +347,59 @@ public function display_pay_for_order_page_html( $order ) {
wp_localize_script( 'WCPAY_EXPRESS_CHECKOUT_ECE', 'wcpayECEPayForOrderParams', $data );
}
+
+ /**
+ * Sets the WC customer session if one is not set.
+ * This is needed so nonces can be verified by AJAX Request.
+ *
+ * @return void
+ */
+ public function set_session() {
+ // Don't set session cookies on product pages to allow for caching when express checkout
+ // buttons are disabled. But keep cookies if there is already an active WC session in place.
+ if (
+ ! ( $this->express_checkout_helper->is_product() && $this->express_checkout_helper->should_show_express_checkout_button() )
+ || ( isset( WC()->session ) && WC()->session->has_session() )
+ ) {
+ return;
+ }
+
+ WC()->session->set_customer_session_cookie( true );
+ }
+
+ /**
+ * Handles express checkout redirect when the redirect dialog "Continue" button is clicked.
+ */
+ public function handle_express_checkout_redirect() {
+ if (
+ ! empty( $_GET['wcpay_express_checkout_redirect_url'] )
+ && ! empty( $_GET['_wpnonce'] )
+ && wp_verify_nonce( $_GET['_wpnonce'], 'wcpay-set-redirect-url' ) // @codingStandardsIgnoreLine
+ ) {
+ $url = rawurldecode( esc_url_raw( wp_unslash( $_GET['wcpay_express_checkout_redirect_url'] ) ) );
+ // Sets a redirect URL cookie for 10 minutes, which we will redirect to after authentication.
+ // Users will have a 10 minute timeout to login/create account, otherwise redirect URL expires.
+ wc_setcookie( 'wcpay_express_checkout_redirect_url', $url, time() + MINUTE_IN_SECONDS * 10 );
+ // Redirects to "my-account" page.
+ wp_safe_redirect( get_permalink( get_option( 'woocommerce_myaccount_page_id' ) ) );
+ }
+ }
+
+ /**
+ * Returns the login redirect URL.
+ *
+ * @param string $redirect Default redirect URL.
+ *
+ * @return string Redirect URL.
+ */
+ public function get_login_redirect_url( $redirect ) {
+ $url = esc_url_raw( wp_unslash( $_COOKIE['wcpay_express_checkout_redirect_url'] ?? '' ) );
+
+ if ( empty( $url ) ) {
+ return $redirect;
+ }
+ wc_setcookie( 'wcpay_express_checkout_redirect_url', '' );
+
+ return $url;
+ }
}
diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php
index 378a2c4d7c5..8f9d0d7ac9c 100644
--- a/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php
+++ b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php
@@ -815,7 +815,7 @@ public function get_product_price( $product, ?bool $is_deposit = null, int $depo
* @param float $price The price, which to calculate taxes for.
* @return array An array of final taxes.
*/
- private function get_taxes_like_cart( $product, $price ) {
+ public function get_taxes_like_cart( $product, $price ) {
if ( ! wc_tax_enabled() || $this->cart_prices_include_tax() ) {
// Only proceed when taxes are enabled, but not included.
return [];
diff --git a/includes/fraud-prevention/class-fraud-prevention-service.php b/includes/fraud-prevention/class-fraud-prevention-service.php
index abab5b9f454..9bef49875c5 100644
--- a/includes/fraud-prevention/class-fraud-prevention-service.php
+++ b/includes/fraud-prevention/class-fraud-prevention-service.php
@@ -66,6 +66,7 @@ public static function get_instance( $session = null, $gateway = null ): self {
/**
* Appends the fraud prevention token to the JS context if the protection is enabled, and a session exists.
+ * This token will also be used by express checkouts.
*
* @return void
*/
@@ -86,9 +87,9 @@ public static function maybe_append_fraud_prevention_token() {
return;
}
- // Don't add the token if the user isn't on the cart or checkout page.
- // Checking the cart page too because the user can pay quickly via the payment buttons on that page.
- if ( ! is_checkout() && ! is_cart() ) {
+ // Don't add the token if the user isn't on the cart, checkout or product page.
+ // Checking the product and cart page too because the user can pay quickly via the payment buttons on that page.
+ if ( ! is_checkout() && ! is_cart() && ! is_product() ) {
return;
}
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index b358e46babb..751f04cb47f 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -31,6 +31,11 @@
WC_Subscriptions_Cart
+
+
+ WC_Subscriptions_Cart
+
+
WC_Subscriptions_Product