Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/woocommerce-payments/dist/blocks-checkout.asset.php
+++ b/woocommerce-payments/dist/blocks-checkout.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'react', 'react-dom', 'wc-blocks-checkout', 'wc-blocks-registry', 'wp-api-fetch', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-polyfill', 'wp-url'), 'version' => '24cda7661f313f8074d2');
+<?php return array('dependencies' => array('lodash', 'react', 'react-dom', 'wc-blocks-checkout', 'wc-blocks-registry', 'wp-api-fetch', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-polyfill', 'wp-url'), 'version' => '11ed0c2a2446a934b825');
--- a/woocommerce-payments/dist/cart-block.asset.php
+++ b/woocommerce-payments/dist/cart-block.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'wp-data', 'wp-polyfill'), 'version' => '2a117c383d6b3b970e3b');
+<?php return array('dependencies' => array('react', 'wp-data', 'wp-polyfill'), 'version' => '4c3a6e146c4abcc89e62');
--- a/woocommerce-payments/dist/checkout.asset.php
+++ b/woocommerce-payments/dist/checkout.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('wp-dom-ready', 'wp-i18n', 'wp-polyfill'), 'version' => '55870132dc1e52d5ebc3');
+<?php return array('dependencies' => array('wp-dom-ready', 'wp-i18n', 'wp-polyfill'), 'version' => '28d5576899a7c39d8e39');
--- a/woocommerce-payments/dist/express-checkout.asset.php
+++ b/woocommerce-payments/dist/express-checkout.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'wp-api-fetch', 'wp-dom-ready', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-polyfill', 'wp-url'), 'version' => 'ebbe7df1387435783f21');
+<?php return array('dependencies' => array('lodash', 'wp-api-fetch', 'wp-dom-ready', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-polyfill', 'wp-url'), 'version' => 'b7224986b444129cca6f');
--- a/woocommerce-payments/dist/index.asset.php
+++ b/woocommerce-payments/dist/index.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wc-components', 'wc-csv', 'wc-currency', 'wc-experimental', 'wc-navigation', 'wc-number', 'wc-settings', 'wc-store-data', 'wc-tracks', 'wp-a11y', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-data-controls', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-mediaelement', 'wp-polyfill', 'wp-primitives', 'wp-url'), 'version' => '7bfb72a6000a44a92f47');
+<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wc-components', 'wc-csv', 'wc-currency', 'wc-experimental', 'wc-navigation', 'wc-number', 'wc-settings', 'wc-store-data', 'wc-tracks', 'wp-a11y', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-data-controls', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-mediaelement', 'wp-polyfill', 'wp-primitives', 'wp-url'), 'version' => '3d7247a3b047cfaaaf0d');
--- a/woocommerce-payments/dist/multi-currency-async-renderer.asset.php
+++ b/woocommerce-payments/dist/multi-currency-async-renderer.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array('wp-polyfill'), 'version' => '659ee11d6afd84a258ee');
--- a/woocommerce-payments/dist/multi-currency-switcher-block.asset.php
+++ b/woocommerce-payments/dist/multi-currency-switcher-block.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'moment', 'react', 'wc-currency', 'wc-number', 'wc-settings', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-data-controls', 'wp-date', 'wp-dom-ready', 'wp-i18n', 'wp-polyfill', 'wp-url'), 'version' => '75ee90da24b76aba2ab0');
+<?php return array('dependencies' => array('lodash', 'moment', 'react', 'wc-currency', 'wc-number', 'wc-settings', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-data-controls', 'wp-date', 'wp-dom-ready', 'wp-i18n', 'wp-polyfill', 'wp-url'), 'version' => '0b1131f8fe12bfad1a80');
--- a/woocommerce-payments/dist/multi-currency.asset.php
+++ b/woocommerce-payments/dist/multi-currency.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wc-components', 'wc-currency', 'wc-number', 'wc-settings', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-data-controls', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-primitives', 'wp-url'), 'version' => 'f0a2373531c5363800fa');
+<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wc-components', 'wc-currency', 'wc-number', 'wc-settings', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-data-controls', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-primitives', 'wp-url'), 'version' => '7c8d22188cf23696584b');
--- a/woocommerce-payments/dist/order.asset.php
+++ b/woocommerce-payments/dist/order.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wc-components', 'wc-currency', 'wc-number', 'wc-settings', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-data-controls', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-primitives', 'wp-url'), 'version' => '43fc809cdd384debfa03');
+<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wc-components', 'wc-currency', 'wc-number', 'wc-settings', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-data-controls', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-primitives', 'wp-url'), 'version' => '433d411b434a58ee883f');
--- a/woocommerce-payments/dist/product-details.asset.php
+++ b/woocommerce-payments/dist/product-details.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('wp-polyfill'), 'version' => '6be418606a7c97d3f65e');
+<?php return array('dependencies' => array('wp-polyfill'), 'version' => 'afce5e2525d57634610a');
--- a/woocommerce-payments/dist/settings.asset.php
+++ b/woocommerce-payments/dist/settings.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wc-components', 'wc-currency', 'wc-navigation', 'wc-number', 'wc-settings', 'wp-a11y', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-data-controls', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-html-entities', 'wp-i18n', 'wp-polyfill', 'wp-primitives', 'wp-url'), 'version' => 'afd29e8fff24bc00f41b');
+<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wc-components', 'wc-currency', 'wc-navigation', 'wc-number', 'wc-settings', 'wp-a11y', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-data-controls', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-html-entities', 'wp-i18n', 'wp-polyfill', 'wp-primitives', 'wp-url'), 'version' => '6252039206631ef28a63');
--- a/woocommerce-payments/dist/subscription-edit-page.asset.php
+++ b/woocommerce-payments/dist/subscription-edit-page.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'react-dom', 'wp-i18n', 'wp-polyfill'), 'version' => 'fa39c2fe831093b6ef98');
+<?php return array('dependencies' => array('react', 'react-dom', 'wp-i18n', 'wp-polyfill'), 'version' => 'f2bbc81a778730442a5c');
--- a/woocommerce-payments/dist/wc-payments-review-prompt.asset.php
+++ b/woocommerce-payments/dist/wc-payments-review-prompt.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array('lodash', 'react', 'react-dom', 'wc-store-data', 'wp-a11y', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-primitives'), 'version' => 'd1547a271b1069ffe8e9');
--- a/woocommerce-payments/dist/wc-payments-settings-spotlight.asset.php
+++ b/woocommerce-payments/dist/wc-payments-settings-spotlight.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wc-currency', 'wc-number', 'wc-settings', 'wp-a11y', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-data-controls', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-primitives', 'wp-url'), 'version' => 'ca8e25db67a28be595c9');
+<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wc-currency', 'wc-number', 'wc-settings', 'wp-a11y', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-data-controls', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-primitives', 'wp-url'), 'version' => '06f02da80da84a01d310');
--- a/woocommerce-payments/dist/woopay-express-button.asset.php
+++ b/woocommerce-payments/dist/woopay-express-button.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'react-dom', 'wp-dom-ready', 'wp-i18n', 'wp-polyfill'), 'version' => 'f7659cfffe7fdbc89434');
+<?php return array('dependencies' => array('react', 'react-dom', 'wp-dom-ready', 'wp-i18n', 'wp-polyfill'), 'version' => 'd4926962382460ed321f');
--- a/woocommerce-payments/includes/admin/class-wc-payments-admin-settings.php
+++ b/woocommerce-payments/includes/admin/class-wc-payments-admin-settings.php
@@ -97,12 +97,23 @@
?>
</b>
<?php
+ if ( WC_Payments::mode()->is_dev() ) {
+ printf(
+ /* translators: 1: Anchor opening tag; 2: Anchor closing tag; 3: Anchor opening tag; 4: Anchor closing tag */
+ esc_html__( 'Test mode is active because your store is running in a development or staging environment. To disable it, switch to a production %1$sWordPress environment%2$s or remove the WCPAY_DEV_MODE constant. %3$sLearn more%4$s', 'woocommerce-payments' ),
+ '<a href="' . esc_url( 'https://make.wordpress.org/core/2020/08/27/wordpress-environment-types/' ) . '" target="_blank" rel="noreferrer noopener">',
+ '</a>',
+ '<a href="' . esc_url( 'https://woocommerce.com/document/woopayments/testing-and-troubleshooting/testing/' ) . '" target="_blank" rel="noreferrer noopener">',
+ '</a>'
+ );
+ } else {
printf(
/* translators: 1: Anchor opening tag; 2: Anchor closing tag */
esc_html__( 'You can use %1$stest card numbers%2$s to simulate various types of transactions.', 'woocommerce-payments' ),
'<a href="' . esc_url( 'https://woocommerce.com/document/woopayments/testing-and-troubleshooting/testing/#test-cards' ) . '" target="_blank" rel="noreferrer noopener">',
'</a>'
);
+ }
?>
</p>
</div>
--- a/woocommerce-payments/includes/admin/class-wc-payments-admin.php
+++ b/woocommerce-payments/includes/admin/class-wc-payments-admin.php
@@ -185,6 +185,7 @@
add_action( 'admin_init', [ $this, 'add_css_classes' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_wc_payment_settings_spotlight' ] );
add_action( 'admin_footer', [ $this, 'inject_payment_settings_spotlight_container' ] );
+ add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_wc_payments_review_prompt' ] );
}
/**
@@ -688,6 +689,17 @@
WC_Payments::get_file_version( 'dist/wc-payments-settings-spotlight.css' ),
'all'
);
+
+ WC_Payments::register_script_with_dependencies( 'WCPAY_REVIEW_PROMPT', 'dist/wc-payments-review-prompt' );
+ wp_set_script_translations( 'WCPAY_REVIEW_PROMPT', 'woocommerce-payments' );
+
+ WC_Payments_Utils::register_style(
+ 'WCPAY_REVIEW_PROMPT',
+ plugins_url( 'dist/wc-payments-review-prompt.css', WCPAY_PLUGIN_FILE ),
+ [],
+ WC_Payments::get_file_version( 'dist/wc-payments-review-prompt.css' ),
+ 'all'
+ );
}
/**
@@ -1016,6 +1028,17 @@
'isWooPayGlobalThemeSupportEligible' => WC_Payments_Features::is_woopay_global_theme_support_eligible(),
'dateFormat' => wc_date_format(),
'timeFormat' => get_option( 'time_format' ),
+ 'formattedStoreAddress' => WC()->countries->get_formatted_address(
+ [
+ 'address_1' => get_option( 'woocommerce_store_address', '' ),
+ 'address_2' => get_option( 'woocommerce_store_address_2', '' ),
+ 'city' => get_option( 'woocommerce_store_city', '' ),
+ 'state' => WC()->countries->get_base_state(),
+ 'postcode' => get_option( 'woocommerce_store_postcode', '' ),
+ 'country' => WC()->countries->get_base_country(),
+ ],
+ ', '
+ ),
];
/**
@@ -1262,9 +1285,16 @@
*/
public function display_wcpay_transaction_fee( $order_id ) {
$order = wc_get_order( $order_id );
- if ( ! $order || ! $order->get_meta( '_wcpay_transaction_fee' ) || Intent_Status::REQUIRES_CAPTURE === $order->get_meta( WC_Payments_Order_Service::INTENTION_STATUS_META_KEY ) ) {
+ if ( ! $order || Intent_Status::REQUIRES_CAPTURE === $order->get_meta( WC_Payments_Order_Service::INTENTION_STATUS_META_KEY ) ) {
+ return;
+ }
+
+ $transaction_fee = $order->get_meta( '_wcpay_transaction_fee' );
+
+ if ( ! $transaction_fee ) {
return;
}
+
?>
<tr>
<td class="label wcpay-transaction-fee">
@@ -1282,7 +1312,7 @@
</td>
<td width="1%"></td>
<td class="total">
- -<?php echo wp_kses( wc_price( $order->get_meta( '_wcpay_transaction_fee' ), [ 'currency' => $order->get_currency() ] ), 'post' ); ?>
+ -<?php echo wp_kses( wc_price( $transaction_fee, [ 'currency' => $order->get_currency() ] ), 'post' ); ?>
</td>
</tr>
<?php
@@ -1476,7 +1506,7 @@
}
/**
- * Check if we're on the WooCommerce Payments Settings page.
+ * Check if we're on the WooCommerce Payments Settings page (general payments tab, no specific section).
*
* @return bool True if on the WC payment settings page.
*/
@@ -1488,4 +1518,72 @@
&& is_admin();
// phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
}
+
+ /**
+ * Check if the review prompt should be shown based on eligibility and user state.
+ *
+ * @return bool True if the prompt should be shown, false otherwise.
+ */
+ public function should_show_review_prompt() {
+ // Only show on top-level Payments Settings page.
+ if ( ! $this->is_wc_admin_payments_settings_page() ) {
+ return false;
+ }
+
+ // Check account eligibility.
+ if ( ! $this->account->is_review_prompt_eligible() ) {
+ return false;
+ }
+
+ // Check user dismissal/cooldown state.
+ $user_id = get_current_user_id();
+ $dismissed = (int) get_user_meta( $user_id, 'woocommerce_admin_wc_payments_review_prompt_dismissed', true );
+ $maybe_later = (int) get_user_meta( $user_id, 'woocommerce_admin_wc_payments_review_prompt_maybe_later', true );
+
+ // If dismissed permanently, don't show.
+ if ( $dismissed > 0 ) {
+ return false;
+ }
+
+ // If cooldown is active (within 10 days), don't show.
+ if ( $maybe_later > 0 ) {
+ $cooldown_seconds = 10 * DAY_IN_SECONDS;
+ $now = time();
+ if ( $now < ( $maybe_later + $cooldown_seconds ) ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Enqueue the review prompt script on top-level Payments Settings page.
+ */
+ public function enqueue_wc_payments_review_prompt() {
+ if ( ! $this->should_show_review_prompt() ) {
+ return;
+ }
+
+ add_action( 'admin_footer', [ $this, 'inject_review_prompt_container' ] );
+
+ wp_localize_script(
+ 'WCPAY_REVIEW_PROMPT',
+ 'wcpayReviewPromptSettings',
+ [
+ 'isLive' => WC_Payments::mode()->is_live(),
+ 'version' => WCPAY_VERSION_NUMBER,
+ ]
+ );
+
+ wp_enqueue_script( 'WCPAY_REVIEW_PROMPT' );
+ wp_enqueue_style( 'WCPAY_REVIEW_PROMPT' );
+ }
+
+ /**
+ * Inject the container div for the review prompt on top-level Payments settings page.
+ */
+ public function inject_review_prompt_container() {
+ echo '<div id="wcpay-review-prompt"></div>';
+ }
}
--- a/woocommerce-payments/includes/admin/class-wc-rest-payments-onboarding-controller.php
+++ b/woocommerce-payments/includes/admin/class-wc-rest-payments-onboarding-controller.php
@@ -88,6 +88,12 @@
],
],
],
+ 'mode' => [
+ 'description' => 'The account mode the user selected: live or test. Overrides environment-based auto-detection (except dev mode).',
+ 'type' => 'string',
+ 'required' => false,
+ 'enum' => [ 'live', 'test' ],
+ ],
],
]
);
@@ -217,10 +223,12 @@
public function create_embedded_kyc_session( WP_REST_Request $request ) {
$self_assessment_data = ! empty( $request->get_param( 'self_assessment' ) ) ? wc_clean( wp_unslash( $request->get_param( 'self_assessment' ) ) ) : [];
$capabilities = ! empty( $request->get_param( 'capabilities' ) ) ? wc_clean( wp_unslash( $request->get_param( 'capabilities' ) ) ) : [];
+ $mode = ! empty( $request->get_param( 'mode' ) ) ? sanitize_text_field( $request->get_param( 'mode' ) ) : null;
$account_session = $this->onboarding_service->create_embedded_kyc_session(
$self_assessment_data,
- $capabilities
+ $capabilities,
+ $mode
);
if ( empty( $account_session ) ) {
--- a/woocommerce-payments/includes/admin/class-wc-rest-payments-orders-controller.php
+++ b/woocommerce-payments/includes/admin/class-wc-rest-payments-orders-controller.php
@@ -190,6 +190,7 @@
$order->set_payment_method( WC_Payment_Gateway_WCPay::GATEWAY_ID );
$order->set_payment_method_title( __( 'WooCommerce In-Person Payments', 'woocommerce-payments' ) );
$this->order_service->attach_intent_info_to_order( $order, $intent );
+
$this->order_service->update_order_status_from_intent( $order, $intent );
// Certain payments (eg. Interac) are captured on the client-side (mobile app).
--- a/woocommerce-payments/includes/admin/class-wc-rest-payments-settings-controller.php
+++ b/woocommerce-payments/includes/admin/class-wc-rest-payments-settings-controller.php
@@ -227,8 +227,8 @@
'type' => 'boolean',
'validate_callback' => 'rest_validate_request_arg',
],
- 'is_apple_google_pay_in_payment_methods_options_enabled' => [
- 'description' => __( 'If Apple Pay / Google Pay should be enabled as an option in the payment methods list.', 'woocommerce-payments' ),
+ 'is_express_checkout_in_payment_methods_enabled' => [
+ 'description' => __( 'If express checkout methods (Apple Pay, Google Pay, Amazon Pay) should be enabled as options in the payment methods list.', 'woocommerce-payments' ),
'type' => 'boolean',
'validate_callback' => 'rest_validate_request_arg',
],
@@ -270,11 +270,6 @@
'type' => 'boolean',
'validate_callback' => 'rest_validate_request_arg',
],
- 'is_amazon_pay_enabled' => [
- 'description' => __( 'If Amazon Pay should be enabled.', 'woocommerce-payments' ),
- 'type' => 'boolean',
- 'validate_callback' => 'rest_validate_request_arg',
- ],
'woopay_custom_message' => [
'description' => __( 'Custom message to display to WooPay customers.', 'woocommerce-payments' ),
'type' => 'string',
@@ -560,7 +555,7 @@
'account_domestic_currency' => $this->wcpay_gateway->get_option( 'account_domestic_currency' ),
'account_communications_email' => $this->wcpay_gateway->get_option( 'account_communications_email' ),
'is_payment_request_enabled' => $this->wcpay_gateway->is_payment_request_enabled(),
- 'is_apple_google_pay_in_payment_methods_options_enabled' => 'yes' === $this->wcpay_gateway->get_option( 'apple_google_pay_in_payment_methods_options' ),
+ 'is_express_checkout_in_payment_methods_enabled' => 'yes' === $this->wcpay_gateway->get_option( 'express_checkout_in_payment_methods' ),
'is_debug_log_enabled' => 'yes' === $this->wcpay_gateway->get_option( 'enable_logging' ),
'payment_request_button_size' => $this->wcpay_gateway->get_option( 'payment_request_button_size' ),
'payment_request_button_type' => $this->wcpay_gateway->get_option( 'payment_request_button_type' ),
@@ -569,7 +564,6 @@
'is_saved_cards_enabled' => $this->wcpay_gateway->is_saved_cards_enabled(),
'is_card_present_eligible' => $this->wcpay_gateway->is_card_present_eligible() && isset( WC()->payment_gateways()->get_available_payment_gateways()['cod'] ),
'is_woopay_enabled' => WC_Payments_Features::is_woopay_eligible() && 'yes' === $this->wcpay_gateway->get_option( 'platform_checkout' ),
- 'is_amazon_pay_enabled' => $this->is_amazon_pay_enabled(),
'show_woopay_incompatibility_notice' => get_option( 'woopay_invalid_extension_found', false ),
'woopay_custom_message' => $this->wcpay_gateway->get_option( 'platform_checkout_custom_message' ),
'woopay_store_logo' => $this->wcpay_gateway->get_option( 'platform_checkout_store_logo' ),
@@ -606,11 +600,10 @@
$this->update_is_multi_currency_enabled( $request );
$this->update_is_wcpay_subscriptions_enabled( $request );
$this->update_is_payment_request_enabled( $request );
- $this->update_is_apple_google_pay_in_payment_methods_options_enabled( $request );
+ $this->update_is_express_checkout_in_payment_methods_enabled( $request );
$this->update_payment_request_appearance( $request );
$this->update_is_saved_cards_enabled( $request );
$this->update_is_woopay_enabled( $request );
- $this->update_is_amazon_pay_enabled( $request );
$this->update_is_woopay_global_theme_support_enabled( $request );
$this->update_woopay_store_logo( $request );
$this->update_woopay_custom_message( $request );
@@ -938,18 +931,20 @@
}
/**
- * Updates the "Apple Pay / Google Pay in payment methods options" enable/disable settings.
+ * Updates the "express checkout in payment methods" enable/disable settings.
+ * This controls whether Apple Pay, Google Pay, and Amazon Pay appear as options
+ * in the payment methods list instead of as separate express checkout buttons.
*
* @param WP_REST_Request $request Request object.
*/
- private function update_is_apple_google_pay_in_payment_methods_options_enabled( WP_REST_Request $request ) {
- if ( ! $request->has_param( 'is_apple_google_pay_in_payment_methods_options_enabled' ) ) {
+ private function update_is_express_checkout_in_payment_methods_enabled( WP_REST_Request $request ) {
+ if ( ! $request->has_param( 'is_express_checkout_in_payment_methods_enabled' ) ) {
return;
}
- $is_apple_google_pay_in_payment_methods_options_enabled = $request->get_param( 'is_apple_google_pay_in_payment_methods_options_enabled' );
+ $is_express_checkout_in_payment_methods_enabled = $request->get_param( 'is_express_checkout_in_payment_methods_enabled' );
- $this->wcpay_gateway->update_option( 'apple_google_pay_in_payment_methods_options', $is_apple_google_pay_in_payment_methods_options_enabled ? 'yes' : 'no' );
+ $this->wcpay_gateway->update_option( 'express_checkout_in_payment_methods', $is_express_checkout_in_payment_methods_enabled ? 'yes' : 'no' );
}
/**
@@ -1005,41 +1000,6 @@
}
/**
- * Updates the "Amazon Pay" enable/disable settings.
- *
- * @param WP_REST_Request $request Request object.
- */
- private function update_is_amazon_pay_enabled( WP_REST_Request $request ) {
- if ( ! $request->has_param( 'is_amazon_pay_enabled' ) ) {
- return;
- }
-
- $amazon_pay_gateway = WC_Payments::get_payment_gateway_by_id( WCPayPaymentMethodsConfigsDefinitionsAmazonPayDefinition::get_id() );
- if ( ! $amazon_pay_gateway ) {
- return;
- }
-
- $is_amazon_pay_enabled = $request->get_param( 'is_amazon_pay_enabled' );
- if ( $is_amazon_pay_enabled ) {
- $amazon_pay_gateway->enable();
- $this->request_unrequested_payment_methods( [ 'amazon_pay' ] );
- } else {
- $amazon_pay_gateway->disable();
- }
- }
-
- /**
- * Checks if Amazon Pay is enabled.
- *
- * @return bool
- */
- private function is_amazon_pay_enabled(): bool {
- $amazon_pay_gateway = WC_Payments::get_payment_gateway_by_id( WCPayPaymentMethodsConfigsDefinitionsAmazonPayDefinition::get_id() );
-
- return $amazon_pay_gateway && $amazon_pay_gateway->is_enabled();
- }
-
- /**
* Updates the WooPay Global Theme Support enable/disable settings.
*
* @param WP_REST_Request $request Request object.
--- a/woocommerce-payments/includes/admin/class-wc-rest-payments-settings-option-controller.php
+++ b/woocommerce-payments/includes/admin/class-wc-rest-payments-settings-option-controller.php
@@ -27,7 +27,7 @@
'wcpay_onboarding_eligibility_modal_dismissed' => 'bool',
'wcpay_connection_success_modal_dismissed' => 'bool',
'wcpay_next_deposit_notice_dismissed' => 'bool',
- 'wcpay_duplicate_payment_method_notices_dismissed' => 'bool',
+ 'wcpay_duplicate_payment_method_notices_dismissed' => 'array',
'wcpay_instant_deposit_notice_dismissed' => 'bool',
'wcpay_exit_survey_last_shown' => 'string',
];
--- a/woocommerce-payments/includes/class-database-cache.php
+++ b/woocommerce-payments/includes/class-database-cache.php
@@ -213,10 +213,14 @@
unset( $this->in_memory_cache[ $key ] );
// Remove from the DB cache.
- if ( delete_option( $key ) ) {
- // Clear the WP object cache to ensure the new data is fetched by other processes.
- wp_cache_delete( $key, 'options' );
- }
+ delete_option( $key );
+
+ // Always clear the WP object cache, even if the DB row didn't exist.
+ // wp_cache_delete on a missing key is a no-op, but skipping it when the
+ // DB row is gone while Memcached still has a stale entry causes persistent
+ // stale data across requests (see #8601 for the original fix, #9639 for
+ // the regression).
+ wp_cache_delete( $key, 'options' );
}
/**
--- a/woocommerce-payments/includes/class-wc-payment-gateway-wcpay.php
+++ b/woocommerce-payments/includes/class-wc-payment-gateway-wcpay.php
@@ -37,6 +37,7 @@
use WCPayCoreServerRequestCapture_Intention;
use WCPayCoreServerRequestCreate_And_Confirm_Intention;
use WCPayCoreServerRequestCreate_And_Confirm_Setup_Intention;
+use WCPayCoreServerRequestCreate_Setup_Intention;
use WCPayCoreServerRequestGet_Charge;
use WCPayCoreServerRequestGet_Intention;
use WCPayCoreServerRequestGet_Setup_Intention;
@@ -111,37 +112,7 @@
*/
const USER_FORMATTED_TOKENS_LIMIT = 100;
- const PROCESS_REDIRECT_ORDER_MISMATCH_ERROR_CODE = 'upe_process_redirect_order_id_mismatched';
- const UPE_APPEARANCE_TRANSIENT = 'wcpay_upe_appearance';
- const UPE_ADD_PAYMENT_METHOD_APPEARANCE_TRANSIENT = 'wcpay_upe_add_payment_method_appearance';
- const WC_BLOCKS_UPE_APPEARANCE_TRANSIENT = 'wcpay_wc_blocks_upe_appearance';
- const UPE_BNPL_PRODUCT_PAGE_APPEARANCE_TRANSIENT = 'wcpay_upe_bnpl_product_page_appearance';
- const UPE_BNPL_CLASSIC_CART_APPEARANCE_TRANSIENT = 'wcpay_upe_bnpl_classic_cart_appearance';
- const UPE_BNPL_CART_BLOCK_APPEARANCE_TRANSIENT = 'wcpay_upe_bnpl_cart_block_appearance';
- const UPE_APPEARANCE_THEME_TRANSIENT = 'wcpay_upe_appearance_theme';
- const UPE_ADD_PAYMENT_METHOD_APPEARANCE_THEME_TRANSIENT = 'wcpay_upe_add_payment_method_appearance_theme';
- const WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT = 'wcpay_wc_blocks_upe_appearance_theme';
- const UPE_BNPL_PRODUCT_PAGE_APPEARANCE_THEME_TRANSIENT = 'wcpay_upe_bnpl_product_page_appearance_theme';
- const UPE_BNPL_CLASSIC_CART_APPEARANCE_THEME_TRANSIENT = 'wcpay_upe_bnpl_classic_cart_appearance_theme';
- const UPE_BNPL_CART_BLOCK_APPEARANCE_THEME_TRANSIENT = 'wcpay_upe_bnpl_cart_block_appearance_theme';
-
- /**
- * The locations of appearance transients.
- */
- const APPEARANCE_THEME_TRANSIENTS = [
- 'checkout' => [
- 'blocks' => self::WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT,
- 'classic' => self::UPE_APPEARANCE_THEME_TRANSIENT,
- ],
- 'product_page' => [
- 'blocks' => self::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_THEME_TRANSIENT,
- 'classic' => self::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_THEME_TRANSIENT,
- ],
- 'cart' => [
- 'blocks' => self::UPE_BNPL_CART_BLOCK_APPEARANCE_THEME_TRANSIENT,
- 'classic' => self::UPE_BNPL_CLASSIC_CART_APPEARANCE_THEME_TRANSIENT,
- ],
- ];
+ const PROCESS_REDIRECT_ORDER_MISMATCH_ERROR_CODE = 'upe_process_redirect_order_id_mismatched';
/**
* Client for making requests to the WooCommerce Payments API
@@ -364,6 +335,13 @@
if ( $this->is_saved_cards_enabled() ) {
array_push( $this->supports, 'tokenization', 'add_payment_method' );
}
+
+ // enabling the custom place order button for express checkout methods (Apple Pay, Google Pay, Amazon Pay)
+ // only when the feature is available. Other payment methods like WooPay or card will return `false` for `is_express_checkout()`.
+ if ( property_exists( $this, 'has_custom_place_order_button' ) && $this->payment_method->is_express_checkout() && WC_Payments::get_gateway()->is_express_checkout_in_payment_methods_enabled() ) {
+ $this->has_custom_place_order_button = true;
+ $this->has_fields = false;
+ }
}
/**
@@ -882,6 +860,26 @@
* @return bool Whether the gateway is enabled and ready to accept payments.
*/
public function is_available() {
+ // Express checkout methods (Apple Pay, Google Pay, Amazon Pay) are only available
+ // in the payment methods list when the feature is enabled. Otherwise, they appear
+ // as separate express checkout buttons.
+ if ( $this->payment_method->is_express_checkout() && ! is_admin() ) {
+ if ( ! WC_Payments::get_gateway()->is_express_checkout_in_payment_methods_enabled() ) {
+ return false;
+ }
+ }
+
+ return $this->check_base_availability();
+ }
+
+ /**
+ * Checks base availability without checkout-page-specific restrictions.
+ * Used by is_available_for_express_checkout() for payment methods that are
+ * only available via express checkout (e.g., Amazon Pay).
+ *
+ * @return bool
+ */
+ protected function check_base_availability() {
if ( ! WC_Payments::get_gateway()->is_enabled() ) {
return false;
}
@@ -925,7 +923,7 @@
}
// Disable the gateway if it should not be displayed on the checkout page.
- $is_gateway_enabled = in_array( $this->stripe_id, $this->get_payment_method_ids_enabled_at_checkout(), true ) ? true : false;
+ $is_gateway_enabled = in_array( $this->stripe_id, $this->get_payment_method_ids_enabled_at_checkout(), true );
if ( ! $is_gateway_enabled ) {
return false;
}
@@ -934,6 +932,25 @@
}
/**
+ * Checks if the gateway is available for express checkout.
+ * This bypasses checkout-page-specific restrictions for payment methods
+ * that are only available via express checkout buttons.
+ *
+ * @return bool
+ */
+ public function is_available_for_express_checkout() {
+ if ( is_admin() ) {
+ // In admin context (e.g. block editor preview), skip full availability
+ // checks. check_base_availability() includes runtime checks (HTTPS,
+ // currency, capability status) that can fail without an active cart
+ // or customer session. A simple enabled check is sufficient here.
+ return WC_Payments::get_gateway()->is_enabled() && $this->is_enabled();
+ }
+
+ return $this->check_base_availability();
+ }
+
+ /**
* Overrides the parent method by adding an additional check to see if the tokens list is empty.
* If it is, the method avoids displaying the HTML element with an empty line to maintain a clean user interface and remove unnecessary space.
*
@@ -977,6 +994,20 @@
}
/**
+ * Whether express checkout methods should appear in the payment methods list
+ * instead of as separate express buttons.
+ *
+ * Requires both the dynamic checkout place order button feature flag
+ * and the express_checkout_in_payment_methods gateway setting.
+ *
+ * @return bool
+ */
+ public function is_express_checkout_in_payment_methods_enabled(): bool {
+ return WC_Payments_Features::is_dynamic_checkout_place_order_button_enabled()
+ && 'yes' === $this->get_option( 'express_checkout_in_payment_methods' );
+ }
+
+ /**
* Check if account is eligible for card present.
*
* @return bool
@@ -1716,26 +1747,32 @@
}
// For $0 orders, we need to save the payment method using a setup intent.
- $request = Create_And_Confirm_Setup_Intention::create();
- $request->set_customer( $customer_id );
-
- // Setting the credential based on what was provided.
$payment_credential = $payment_information->get_payment_method();
+
+ // For confirmation tokens (e.g.: through the ECE), we must create an unconfirmed `SetupIntent`
+ // and let the frontend confirm it with the confirmation token.
+ // Stripe's SetupIntent API doesn't support confirmation_token with confirm=true in the same way `PaymentIntent`s do.
if ( $payment_information->is_using_confirmation_token() ) {
- $request->set_confirmation_token( $payment_credential );
+ $request = Create_Setup_Intention::create();
+ $request->set_customer( $customer_id );
+ $request->set_payment_method_types( $this->get_payment_method_types( $payment_information ) );
+ $request->set_metadata( $metadata );
+ $request->assign_hook( 'wcpay_create_setup_intention_request' );
} else {
+ $request = Create_And_Confirm_Setup_Intention::create();
+ $request->set_customer( $customer_id );
$request->set_payment_method( $payment_credential );
- }
- $request->set_metadata( $metadata );
- $request->assign_hook( 'wcpay_create_and_confirm_setup_intention_request' );
- $request->set_hook_args( $payment_information, false, $save_user_in_woopay );
-
- if (
- Payment_Method::CARD === $this->get_selected_stripe_payment_type_id() &&
- in_array( Payment_Method::LINK, $this->get_upe_enabled_payment_method_ids(), true )
+ $request->set_metadata( $metadata );
+ $request->assign_hook( 'wcpay_create_and_confirm_setup_intention_request' );
+ $request->set_hook_args( $payment_information, false, $save_user_in_woopay );
+
+ if (
+ Payment_Method::CARD === $this->get_selected_stripe_payment_type_id() &&
+ in_array( Payment_Method::LINK, $this->get_upe_enabled_payment_method_ids(), true )
) {
- $request->set_payment_method_types( $this->get_payment_method_types( $payment_information ) );
- $request->set_mandate_data( $this->get_mandate_data() );
+ $request->set_payment_method_types( $this->get_payment_method_types( $payment_information ) );
+ $request->set_mandate_data( $this->get_mandate_data() );
+ }
}
/** @var WC_Payments_API_Setup_Intention $intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
@@ -1811,7 +1848,19 @@
}
}
- if ( Intent_Status::REQUIRES_ACTION === $status ) {
+ $needs_frontend_confirmation = (
+ Intent_Status::REQUIRES_ACTION === $status
+ || Intent_Status::REQUIRES_CONFIRMATION === $status
+ || (
+ // For SetupIntents with confirmation tokens, the status will be 'requires_payment_method'
+ // since no payment method is attached yet (the confirmation token will be used on frontend).
+ Intent_Status::REQUIRES_PAYMENT_METHOD === $status
+ && $payment_information->is_using_confirmation_token()
+ && ! $payment_needed
+ )
+ );
+
+ if ( $needs_frontend_confirmation ) {
$next_action_type = $next_action['type'] ?? null;
if ( 'redirect_to_url' === $next_action_type && ! empty( $next_action[ $next_action_type ]['url'] ) ) {
$response = [
@@ -1827,17 +1876,26 @@
$next_action[ $next_action_type ]['expires_at']
);
} else {
+ // Build the redirect URL with the confirmation token for `SetupIntent`s requested through the ECE.
+ // Format: #wcpay-confirm-{si|pi}:{orderId}:{clientSecret}:{nonce}[:{confirmationToken}].
+ $redirect_hash_parts = [
+ $payment_needed ? 'pi' : 'si',
+ $order_id,
+ $client_secret,
+ wp_create_nonce( 'wcpay_update_order_status_nonce' ),
+ ];
+
+ // For ECE SetupIntents, include the confirmation token so the frontend can
+ // use it with confirmSetup() to complete the confirmation.
+ if ( ! $payment_needed && $payment_information->is_using_confirmation_token() ) {
+ $redirect_hash_parts[] = $payment_information->get_payment_method();
+ }
+
$response = [
'result' => 'success',
// Include a new nonce for update_order_status to ensure the update order
// status call works when a guest user creates an account during checkout.
- 'redirect' => sprintf(
- '#wcpay-confirm-%s:%s:%s:%s',
- $payment_needed ? 'pi' : 'si',
- $order_id,
- $client_secret,
- wp_create_nonce( 'wcpay_update_order_status_nonce' ),
- ),
+ 'redirect' => '#wcpay-confirm-' . implode( ':', $redirect_hash_parts ),
// Include the payment method ID so the Blocks integration can save cards.
'payment_method' => $payment_information->get_payment_method(),
];
@@ -1892,7 +1950,7 @@
// ensuring the payment method title is set before any early return paths to avoid incomplete order data.
$this->set_payment_method_title_for_order( $order, $payment_method_type, $payment_method_details );
- if ( isset( $status ) && Intent_Status::REQUIRES_ACTION === $status && $this->is_changing_payment_method_for_subscription() ) {
+ if ( isset( $status ) && ( Intent_Status::REQUIRES_ACTION === $status || Intent_Status::REQUIRES_CONFIRMATION === $status ) && $this->is_changing_payment_method_for_subscription() ) {
// Because we're filtering woocommerce_subscriptions_update_payment_via_pay_shortcode, we need to manually set this delayed update all flag here.
if ( isset( $_POST['update_all_subscriptions_payment_method'] ) && wc_clean( wp_unslash( $_POST['update_all_subscriptions_payment_method'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
$order->update_meta_data( '_delayed_update_payment_method_all', wc_clean( wp_unslash( $_POST['payment_method'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
@@ -2207,7 +2265,15 @@
// If $gateway_id begins with `woocommerce_payments_` payment method is a split UPE LPM.
// Otherwise, $gateway_id must be `woocommerce_payments`.
if ( substr( $gateway_id, 0, strlen( $split_upe_gateway_prefix ) ) === $split_upe_gateway_prefix ) {
- return [ str_replace( $split_upe_gateway_prefix, '', $gateway_id ) ];
+ $payment_method = str_replace( $split_upe_gateway_prefix, '', $gateway_id );
+
+ // Apple Pay and Google Pay are wrappers around card payments for Stripe.
+ $card_wrappers = [ Payment_Method::APPLE_PAY, Payment_Method::GOOGLE_PAY ];
+ if ( in_array( $payment_method, $card_wrappers, true ) ) {
+ return [ Payment_Method::CARD ];
+ }
+
+ return [ $payment_method ];
}
$eligible_payment_methods = WC_Payments::get_gateway()->get_payment_method_ids_enabled_at_checkout( $order_id, true );
@@ -2672,7 +2738,27 @@
*/
public function init_settings() {
parent::init_settings();
- $this->enabled = ! empty( $this->settings[ static::METHOD_ENABLED_KEY ] ) && 'yes' === $this->settings[ static::METHOD_ENABLED_KEY ] ? 'yes' : 'no';
+
+ // Get the basic enabled value from settings.
+ $is_enabled = ! empty( $this->settings[ static::METHOD_ENABLED_KEY ] ) && 'yes' === $this->settings[ static::METHOD_ENABLED_KEY ];
+
+ // Card and express checkout methods are not in the UPE enabled list,
+ // so they only need the basic enabled setting check. Without this
+ // early return, they would fall through to the UPE list verification
+ // below and always end up disabled.
+ if ( 'card' === $this->stripe_id || $this->payment_method->is_express_checkout() ) {
+ return;
+ }
+
+ // For split gateways, also verify the method is in the UPE enabled list.
+ // This prevents sync issues where a gateway has enabled=yes but isn't
+ // actually configured for checkout in the UPE settings.
+ if ( $is_enabled ) {
+ $upe_enabled_methods = $this->get_upe_enabled_payment_method_ids();
+ $this->enabled = in_array( $this->stripe_id, $upe_enabled_methods, true ) ? 'yes' : 'no';
+ } else {
+ $this->enabled = 'no';
+ }
}
/**
@@ -3682,13 +3768,39 @@
// For $0 orders, fetch the Setup Intent instead.
$setup_intent_request = Get_Setup_Intention::create( $intent_id );
/** @var WC_Payments_API_Setup_Intention $setup_intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
- $intent = $setup_intent_request->send();
- $status = $intent->get_status();
- $charge_id = '';
+ $intent = $setup_intent_request->send();
+ $status = $intent->get_status();
+
+ // For $0 orders (free trials), directly complete the order when SetupIntent succeeds.
+ // This is similar to how WC Stripe Gateway handles it - calling payment_complete()
+ // directly ensures the order transitions to the correct status and activates subscriptions.
+ // Otherwise, the order would be in a "Pending payment" state and the subscription would be "Pending".
+ if ( Intent_Status::SUCCEEDED === $status && ! $order->is_paid() ) {
+ $order->payment_complete( $intent_id );
+
+ // Add a success note similar to mark_payment_completed().
+ $note = sprintf(
+ /* translators: %1: the successfully charged amount, %2: WooPayments, %3: transaction ID of the payment */
+ __( 'A payment of %1$s was successfully charged using %2$s (%3$s).', 'woocommerce-payments' ),
+ wc_price( $order->get_total(), [ 'currency' => $order->get_currency() ] ),
+ 'WooPayments',
+ $intent_id
+ );
+ $order->add_order_note( $note );
+ $this->order_service->set_intention_status_for_order( $order, $status );
+ $order->save();
+ }
}
$payment_method_id = $intent->get_payment_method_id();
+ // For SetupIntents confirmed via frontend (e.g., ECE with confirmation tokens),
+ // store the payment method ID in order meta. This ensures subscription renewals
+ // can find the payment method even if token creation fails later.
+ if ( ! empty( $payment_method_id ) ) {
+ $this->order_service->set_payment_method_id_for_order( $order, $payment_method_id );
+ }
+
if ( Intent_Status::SUCCEEDED === $status ) {
$this->duplicate_payment_prevention_service->remove_session_processing_order( $order->get_id() );
}
@@ -3710,8 +3822,16 @@
$this->set_payment_method_title_for_order( $order, $payment_method_type, $payment_method_details );
}
} catch ( Exception $e ) {
- // If saving the token fails, log the error message but catch the error to avoid crashing the checkout flow.
Logger::log( 'Error when saving payment method: ' . $e->getMessage() );
+
+ // For subscription orders, token creation failure is critical - renewals will fail.
+ // Re-throw the exception so the customer sees an error instead of a successful
+ // checkout that will fail on the first renewal.
+ if ( $is_subscription ) {
+ throw new Exception(
+ __( 'Unable to save payment method for subscription. Please try again or use a different payment method.', 'woocommerce-payments' )
+ );
+ }
}
}
@@ -3965,24 +4085,34 @@
/**
* Returns a formatted token list for a user.
*
- * @param int $user_id The user ID.
+ * @param int $user_id The user ID.
+ * @param string|null $gateway_id Optional gateway ID to filter tokens. Defaults to card gateway.
*/
- protected function get_user_formatted_tokens_array( $user_id ) {
+ protected function get_user_formatted_tokens_array( $user_id, $gateway_id = null ) {
$tokens = WC_Payment_Tokens::get_tokens(
[
'user_id' => $user_id,
- 'gateway_id' => self::GATEWAY_ID,
+ 'gateway_id' => $gateway_id ?? self::GATEWAY_ID,
'limit' => self::USER_FORMATTED_TOKENS_LIMIT,
]
);
return array_map(
static function ( WC_Payment_Token $token ): array {
+ // ensures that Google Pay/Apple Pay methods display "Google Pay Visa ending in 1234",
+ // instead of just "Visa ending in 1234".
+ $wallet_type = $token->get_meta( '_wcpay_wallet_type', true );
+ $name = $token->get_display_name();
+ $payment_method = WC_Payments::get_payment_method_by_id( $wallet_type );
+ if ( $payment_method && method_exists( $payment_method, 'get_title' ) ) {
+ $name = join( ' ', [ $payment_method->get_title(), $name ] );
+ }
+
return [
'tokenId' => $token->get_id(),
'paymentMethodId' => $token->get_token(),
'isDefault' => $token->get_is_default(),
- 'displayName' => $token->get_display_name(),
+ 'displayName' => $name,
];
},
array_values( $tokens )
@@ -4215,102 +4345,7 @@
return array_values( array_intersect( $available_methods, $methods_with_fees ) );
}
- /**
- * Handle AJAX request for saving UPE appearance value to transient.
- *
- * @throws Exception - If nonce or setup intent is invalid.
- */
- public function save_upe_appearance_ajax() {
- try {
- $is_nonce_valid = check_ajax_referer( 'wcpay_save_upe_appearance_nonce', false, false );
- if ( ! $is_nonce_valid ) {
- throw new Exception(
- __( 'Unable to update UPE appearance values at this time.', 'woocommerce-payments' )
- );
- }
-
- $elements_location = isset( $_POST['elements_location'] ) ? wc_clean( wp_unslash( $_POST['elements_location'] ) ) : null;
- $appearance = isset( $_POST['appearance'] ) ? json_decode( wc_clean( wp_unslash( $_POST['appearance'] ) ) ) : null;
-
- $valid_locations = [ 'blocks_checkout', 'shortcode_checkout', 'bnpl_product_page', 'bnpl_classic_cart', 'bnpl_cart_block', 'add_payment_method' ];
- if ( ! $elements_location || ! in_array( $elements_location, $valid_locations, true ) ) {
- throw new Exception(
- __( 'Unable to update UPE appearance values at this time.', 'woocommerce-payments' )
- );
- }
-
- if ( in_array( $elements_location, [ 'blocks_checkout', 'shortcode_checkout' ], true ) ) {
- $is_blocks_checkout = 'blocks_checkout' === $elements_location;
- /**
- * This filter is only called on "save" of the appearance, to avoid calling it on every page load.
- * If you apply changes through this filter, you'll need to clear the transient data to see them at checkout.
- *
- * @deprecated 7.4.0 Use {@see 'wcpay_elements_appearance'} instead.
- * @since 7.3.0
- */
- $appearance = apply_filters_deprecated( 'wcpay_upe_appearance', [ $appearance, $is_blocks_checkout ], '7.4.0', 'wcpay_elements_appearance' );
- }
-
- /**
- * This filter is only called on "save" of the appearance, to avoid calling it on every page load.
- * If you apply changes through this filter, you'll need to clear the transient data to see them at checkout.
- * $elements_location can be 'blocks_checkout', 'shortcode_checkout', 'bnpl_product_page', 'bnpl_classic_cart', 'bnpl_cart_block', 'add_payment_method'.
- *
- * @since 7.4.0
- */
- $appearance = apply_filters( 'wcpay_elements_appearance', $appearance, $elements_location );
-
- $appearance_transient = [
- 'shortcode_checkout' => self::UPE_APPEARANCE_TRANSIENT,
- 'add_payment_method' => self::UPE_ADD_PAYMENT_METHOD_APPEARANCE_TRANSIENT,
- 'blocks_checkout' => self::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT,
- 'bnpl_product_page' => self::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_TRANSIENT,
- 'bnpl_classic_cart' => self::UPE_BNPL_CLASSIC_CART_APPEARANCE_TRANSIENT,
- 'bnpl_cart_block' => self::UPE_BNPL_CART_BLOCK_APPEARANCE_TRANSIENT,
- ][ $elements_location ];
- $appearance_theme_transient = [
- 'shortcode_checkout' => self::UPE_APPEARANCE_THEME_TRANSIENT,
- 'add_payment_method' => self::UPE_ADD_PAYMENT_METHOD_APPEARANCE_THEME_TRANSIENT,
- 'blocks_checkout' => self::WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT,
- 'bnpl_product_page' => self::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_THEME_TRANSIENT,
- 'bnpl_classic_cart' => self::UPE_BNPL_CLASSIC_CART_APPEARANCE_THEME_TRANSIENT,
- 'bnpl_cart_block' => self::UPE_BNPL_CART_BLOCK_APPEARANCE_THEME_TRANSIENT,
- ][ $elements_location ];
-
- if ( null !== $appearance ) {
- set_transient( $appearance_transient, $appearance, DAY_IN_SECONDS );
- set_transient( $appearance_theme_transient, $appearance->theme, DAY_IN_SECONDS );
- }
-
- wp_send_json_success( $appearance, 200 );
- } catch ( Exception $e ) {
- // Send back error so it can be displayed to the customer.
- wp_send_json_error(
- [
- 'error' => [
- 'message' => WC_Payments_Utils::get_filtered_error_message( $e ),
- ],
- ],
- WC_Payments_Utils::get_filtered_error_status_code( $e )
- );
- }
- }
- /**
- * Clear the saved UPE appearance transient value.
- */
- public function clear_upe_appearance_transient() {
- delete_transient( self::UPE_APPEARANCE_TRANSIENT );
- delete_transient( self::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT );
- delete_transient( self::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_TRANSIENT );
- delete_transient( self::UPE_BNPL_CLASSIC_CART_APPEARANCE_TRANSIENT );
- delete_transient( self::UPE_BNPL_CART_BLOCK_APPEARANCE_TRANSIENT );
- delete_transient( self::UPE_APPEARANCE_THEME_TRANSIENT );
- delete_transient( self::WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT );
- delete_transient( self::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_THEME_TRANSIENT );
- delete_transient( self::UPE_BNPL_CLASSIC_CART_APPEARANCE_THEME_TRANSIENT );
- delete_transient( self::UPE_BNPL_CART_BLOCK_APPEARANCE_THEME_TRANSIENT );
- }
/**
* Returns true if the code returned from the API represents an error that should be rate-limited.
@@ -4486,15 +4521,11 @@
}
/**
- * Checks if UPE appearance theme is set and returns appropriate icon URL.
+ * Returns the appropriate icon URL for the payment method.
*
* @return string
*/
public function get_theme_icon() {
- $upe_appearance_theme = get_transient( self::UPE_APPEARANCE_THEME_TRANSIENT );
- if ( $upe_appearance_theme ) {
- return 'night' === $upe_appearance_theme ? $this->payment_method->get_dark_icon() : $this->payment_method->get_icon();
- }
return $this->payment_method->get_icon();
}
--- a/woocommerce-payments/includes/class-wc-payment-token-wcpay-amazon-pay.php
+++ b/woocommerce-payments/includes/class-wc-payment-token-wcpay-amazon-pay.php
@@ -0,0 +1,122 @@
+<?php
+/**
+ * Class WC_Payment_Token_WCPay_Amazon_Pay
+ *
+ * @package WooCommercePayments
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit; // Exit if accessed directly.
+}
+
+/**
+ * WooCommerce Amazon Pay Payment Token.
+ *
+ * Representation of a payment token for Amazon Pay.
+ *
+ * @class WC_Payment_Token_WCPay_Amazon_Pay
+ */
+class WC_Payment_Token_WCPay_Amazon_Pay extends WC_Payment_Token {
+
+ /**
+ * Class Constant so other code can be unambiguous.
+ *
+ * @type string
+ */
+ const TYPE = 'wcpay_amazon_pay';
+
+ /**
+ * The payment method type of this token.
+ *
+ * @var string
+ */
+ protected $type = self::TYPE;
+
+ /**
+ * Stores Amazon Pay payment token data.
+ *
+ * @var array
+ */
+ protected $extra_data = [
+ 'email' => '',
+ ];
+
+ /**
+ * Get payment method type to display to user.
+ *
+ * @param string $deprecated Deprecated since WooCommerce 3.0.
+ * @return string
+ */
+ public function get_display_name( $deprecated = '' ) {
+ $email = $this->get_email();
+ if ( ! empty( $email ) ) {
+ return sprintf(
+ /* translators: %s: redacted customer email */
+ __( 'Amazon Pay (%s)', 'woocommerce-payments' ),
+ $email
+ );
+ }
+
+ return __( 'Amazon Pay', 'woocommerce-payments' );
+ }
+
+ /**
+ * Hook prefix.
+ */
+ protected function get_hook_prefix() {
+ return 'woocommerce_payments_token_wcpay_amazon_pay_get_';
+ }
+
+ /**
+ * Returns the redacted customer email.
+ * Note: The email is stored in redacted format for privacy.
+ *
+ * @param string $context What the value is for. Valid values are view and edit.
+ *
+ * @return string Redacted customer email.
+ */
+ public function get_email( $context = 'view' ) {
+ $email = $this->get_prop( 'email', $context );
+
+ return $email ?? '';
+ }
+
+ /**
+ * Set the customer email. The email is automatically redacted for privacy.
+ *
+ * @param string $email Customer email (will be redacted before storage).
+ */
+ public function set_email( $email ) {
+ $this->set_prop( 'email', $this->redact_email_address( $email ) );
+ }
+
+ /**
+ * Returns the type of this payment token.
+ *
+ * @param string $deprecated Deprecated since WooCommerce 3.0.
+ * @return string Payment Token Type.
+ */
+ public function get_type( $deprecated = '' ) {
+ return self::TYPE;
+ }
+
+ /**
+ * Transforms email address into redacted/shortened format like ***xxxx@domain.com.
+ * Using shortened length of four characters to mimic CC last-4 digits.
+ *
+ * @param string $email Email address.
+ * @return string Redacted/shortened email address.
+ */
+ private function redact_email_address( $email ) {
+ if ( empty( $email ) || false === strpos( $email, '@' ) ) {
+ return $email;
+ }
+
+ $placeholder = '***';
+ $shortened_length = 4;
+ list( $handle, $domain ) = explode( '@', $email );
+ $redacted_handle = strlen( $handle ) > $shortened_length ? substr( $handle, - $shortened_length ) : $handle;
+
+ return "$placeholder$redacted_handle@$domain";
+ }
+}
--- a/woocommerce-payments/includes/class-wc-payments-account.php
+++ b/woocommerce-payments/includes/class-wc-payments-account.php
@@ -382,7 +382,9 @@
// Campaigns are temporary flags that are used to enable/disable features for a limited time.
'campaigns' => [
// The flag for the WordPress.org merchant review campaign in 2025. Eligibility is determined per-account on transact-platform-server.
- 'wporgReview2025' => $account['eligibility_wporg_review_campaign_2025'] ?? false,
+ 'wporgReview2025' => $account['eligibility_wporg_review_campaign_2025'] ?? false,
+ // The flag for the payments settings review prompt (Phase 0). Eligibility is determined per-account on transact-platform-server.
+ 'reviewPromptPhase0' => $account['eligibility_review_prompt_phase_0'] ?? false,
],
];
}
@@ -2573,6 +2575,7 @@
*/
public function handle_loan_approved_inbox_note( $account ) {
require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-loan-approved.php';
+ require_once WCPAY_ABSPATH . 'includes/class-wc-payments-explicit-price-formatter.php';
// If the account cache is empty, don't try to create an inbox note.
if ( empty( $account ) ) {
@@ -2653,6 +2656,16 @@
}
/**
+ * Checks if the account is eligible for the review prompt (Phase 0).
+ *
+ * @return bool
+ */
+ public function is_review_prompt_eligible(): bool {
+ $account = $this->get_cached_account_data();
+ return $account['eligibility_review_prompt_phase_0'] ?? false;
+ }
+
+ /**
* Gather the latest store setup state and send it to the Transact Platform.
*
* @return void
@@ -2714,14 +2727,14 @@
return [
// The WooPayments setup details.
- 'gateway' => [
+ 'gateway' => [
'enabled' => $gateway->is_enabled(),
'test_mode' => WC_Payments::mode()->is_test(),
'test_mode_onboarding' => WC_Payments::mode()->is_test_mode_onboarding(),
],
// Payment methods setup.
- 'payment_methods' => [
+ 'payment_methods' => [
'available' => $payment_methods_available,
'enabled' => $payment_methods_enabled,
'disabled' => $payment_methods_disabled,
@@ -2729,18 +2742,18 @@
],
// Payment methods mapped to capabilities, for flexibility with the Transact Platform.
// E.g. 'card_payments' capability corresponds to 'card' payment method.
- 'provider_capabilities' => [
+ 'provider_capabilities' => [
'available' => $provider_capabilities_available,
'enabled' => $provider_capabilities_enabled,
'disabled' => $provider_capabilities_disabled,
],
- 'apple_google_pay_in_payment_methods_options_enabled' => $gateway->get_option( 'apple_google_pay_in_payment_methods_options' ),
+ 'express_checkout_in_payment_methods_enabled' => $gateway->get_option( 'express_checkout_in_payment_methods' ),
- 'saved_cards_enabled' => $gateway->is_saved_cards_enabled(),
- 'manual_capture_enabled' => 'yes' === $gateway->get_option( 'manual_capture' ),
- 'debug_log_enabled' => 'yes' === $gateway->get_option( 'enable_logging' ),
+ 'saved_cards_enabled' => $gateway->is_saved_cards_enabled(),
+ 'manual_capture_enabled' => 'yes' === $gateway->get_option( 'manual_capture' ),
+ 'debug_log_enabled' => 'yes' === $gateway->get_option( 'enable_logging' ),
- 'payment_request' => [
+ 'payment_request' => [
'enabled' => $gateway->is_payment_request_enabled(),
'enabled_locations' => $this->get_express_checkout_method_locations( $gateway, 'payment_request' ),
'button_type' => $gateway->get_option( 'payment_request_button_type' ),
@@ -2749,7 +2762,7 @@
'button_border_radius' => $gateway->get_option( 'payment_request_button_border_radius' ),
],
- 'woopay' => [
+ 'woopay' => [
'enabled' => WC_Payments_Features::is_woopay_enabled(),
'enabled_locations' => $this->get_express_checkout_method_locations( $gateway, 'woopay' ),
'store_logo' => $gateway->get_option( 'platform_checkout_store_logo' ),
@@ -2758,17 +2771,17 @@
],
// WooPayments features.
- 'multi_currency_enabled' => WC_Payments_Features::is_customer_multi_currency_enabled(),
- 'stripe_billing_enabled' => WC_Payments_Features::is_stripe_billing_enabled(),
+ 'multi_currency_enabled' => WC_Payments_Features::is_customer_multi_currency_enabled(),
+ 'stripe_billing_enabled' => WC_Payments_Features::is_stripe_billing_enabled(),
// Other WooPayments details.
- 'plugin' => [
+ 'plugin' => [
'version' => defined( 'WCPAY_VERSION_NUMBER' ) ? explode( '-', WCPAY_VERSION_NUMBER, 2 )[0] : '',
'activation_timestamp' => get_option( 'wcpay_activation_timestamp', null ),
],
// Other store setup details.
- 'wp_setup' => [
+ 'wp_setup' => [
'name' => get_bloginfo( 'name' ),
'url' => home_url(),
'active_theme' => $this->get_store_theme_details(),
@@ -2776,7 +2789,7 @@
'version' => get_bloginfo( 'version' ),
'locale' => get_locale(),
],
- 'wc_setup' => [
+ 'wc_setup' => [
'version' => defined( 'WC_VERSION' ) ? explode( '-', WC_VERSION, 2 )[0] : '',
'store_id' => ( class_exists( 'WC_Install' ) && defined( 'WC_Install::STORE_ID_OPTION' ) ) ? get_option( WC_Install::STORE_ID_OPTION, null ) : null,
'currency' => get_woocommerce_currency(),
--- a/woocommerce-payments/includes/class-wc-payments-checkout.php
+++ b/woocommerce-payments/includes/class-wc-payments-checkout.php
@@ -96,14 +96,11 @@
add_action( 'wc_payments_set_gateway', [ $this, 'set_gateway' ] );
add_action( 'wc_payments_add_upe_payment_fields', [ $this, 'payment_fields' ] );
add_action( 'wp', [ $this->gateway, 'maybe_process_upe_redirect' ] );
- add_action( 'wp_ajax_save_upe_appearance', [ $this->gateway, 'save_upe_appearance_ajax' ] );
- add_action( 'wp_ajax_nopriv_save_upe_appearanc