From aa9c09583eb749dc60c001275dbf033485fc41f4 Mon Sep 17 00:00:00 2001
From: Hsing-yu Flowers
Date: Mon, 27 May 2024 19:53:53 -0400
Subject: [PATCH 01/52] Add order id mismatch exception (#8855)
---
changelog/add-order-id-mismatch-exception | 4 +++
includes/class-wc-payment-gateway-wcpay.php | 14 ++++++----
includes/class-wc-payments.php | 1 +
.../class-order-id-mismatch-exception.php | 16 ++++++++++++
.../test-class-wc-payment-gateway-wcpay.php | 26 +++++++++++++++++++
5 files changed, 56 insertions(+), 5 deletions(-)
create mode 100644 changelog/add-order-id-mismatch-exception
create mode 100644 includes/exceptions/class-order-id-mismatch-exception.php
diff --git a/changelog/add-order-id-mismatch-exception b/changelog/add-order-id-mismatch-exception
new file mode 100644
index 00000000000..22938c21145
--- /dev/null
+++ b/changelog/add-order-id-mismatch-exception
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Add Order_ID_Mismatch_Exception
diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php
index f20c1b15f1b..fdb12b247e3 100644
--- a/includes/class-wc-payment-gateway-wcpay.php
+++ b/includes/class-wc-payment-gateway-wcpay.php
@@ -18,7 +18,7 @@
use WCPay\Constants\Intent_Status;
use WCPay\Constants\Payment_Type;
use WCPay\Constants\Payment_Method;
-use WCPay\Exceptions\{ Add_Payment_Method_Exception, Amount_Too_Small_Exception, Process_Payment_Exception, Intent_Authentication_Exception, API_Exception, Invalid_Address_Exception, Fraud_Prevention_Enabled_Exception, Invalid_Phone_Number_Exception, Rate_Limiter_Enabled_Exception };
+use WCPay\Exceptions\{ Add_Payment_Method_Exception, Amount_Too_Small_Exception, Process_Payment_Exception, Intent_Authentication_Exception, API_Exception, Invalid_Address_Exception, Fraud_Prevention_Enabled_Exception, Invalid_Phone_Number_Exception, Rate_Limiter_Enabled_Exception, Order_ID_Mismatch_Exception, Order_Not_Found_Exception };
use WCPay\Core\Server\Request\Cancel_Intention;
use WCPay\Core\Server\Request\Capture_Intention;
use WCPay\Core\Server\Request\Create_And_Confirm_Intention;
@@ -1409,9 +1409,12 @@ public function update_saved_payment_method( $payment_method, $order_id, $is_tes
* @param WCPay\Payment_Information $payment_information Payment info.
* @param bool $scheduled_subscription_payment Used to determinate is scheduled subscription payment to add more fields into API request.
*
- * @return array|null An array with result of payment and redirect URL, or nothing.
+ * @return array|null An array with result of payment and redirect URL, or nothing.
* @throws API_Exception
- * @throws Intent_Authentication_Exception When the payment intent could not be authenticated.
+ * @throws Exception When amount too small.
+ * @throws Invalid_Address_Exception
+ * @throws Order_Not_Found_Exception
+ * @throws Order_ID_Mismatch_Exception When the payment intent could not be authenticated.
* @throws \WCPay\Core\Exceptions\Server\Request\Extend_Request_Exception When request class filter filed to extend request class because of incompatibility.
* @throws \WCPay\Core\Exceptions\Server\Request\Immutable_Parameter_Exception When immutable parameter gets changed in request class.
* @throws \WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception When you send incorrect request value via setters.
@@ -1514,7 +1517,7 @@ public function process_payment_for_order( $cart, $payment_information, $schedul
$intent_meta_order_id_raw = $intent->get_metadata()['order_id'] ?? '';
$intent_meta_order_id = is_numeric( $intent_meta_order_id_raw ) ? intval( $intent_meta_order_id_raw ) : 0;
if ( $intent_meta_order_id !== $order_id ) {
- throw new Intent_Authentication_Exception(
+ throw new Order_ID_Mismatch_Exception(
sprintf(
/* translators: %s: metadata. We do not need to translate WooPayMeta */
esc_html( __( 'We\'re not able to process this payment. Please try again later. WooPayMeta: intent_meta_order_id: %1$s, order_id: %2$s', 'woocommerce-payments' ) ),
@@ -1625,7 +1628,7 @@ public function process_payment_for_order( $cart, $payment_information, $schedul
$intent_meta_order_id = is_numeric( $intent_meta_order_id_raw ) ? intval( $intent_meta_order_id_raw ) : 0;
if ( $intent_meta_order_id !== $order_id ) {
- throw new Intent_Authentication_Exception(
+ throw new Order_ID_Mismatch_Exception(
__( "We're not able to process this payment. Please try again later.", 'woocommerce-payments' ),
'order_id_mismatch'
);
@@ -4487,6 +4490,7 @@ private function handle_afterpay_shipping_requirement( WC_Order $order, Create_A
* @param Create_And_Confirm_Intention $request The request object for creating and confirming intention.
* @param Payment_Information $payment_information The payment information object.
* @param WC_Order $order The order object.
+ * @throws Invalid_Address_Exception
*
* @return void
*/
diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php
index 63e8b079157..3a70e64758f 100644
--- a/includes/class-wc-payments.php
+++ b/includes/class-wc-payments.php
@@ -429,6 +429,7 @@ public static function init() {
include_once __DIR__ . '/exceptions/class-fraud-ruleset-exception.php';
include_once __DIR__ . '/exceptions/class-fraud-prevention-enabled-exception.php';
include_once __DIR__ . '/exceptions/class-order-not-found-exception.php';
+ include_once __DIR__ . '/exceptions/class-order-id-mismatch-exception.php';
include_once __DIR__ . '/exceptions/class-rate-limiter-enabled-exception.php';
include_once __DIR__ . '/constants/class-base-constant.php';
include_once __DIR__ . '/constants/class-country-code.php';
diff --git a/includes/exceptions/class-order-id-mismatch-exception.php b/includes/exceptions/class-order-id-mismatch-exception.php
new file mode 100644
index 00000000000..77f9519a581
--- /dev/null
+++ b/includes/exceptions/class-order-id-mismatch-exception.php
@@ -0,0 +1,16 @@
+card_gateway->process_payment_for_order( WC()->cart, $pi );
}
+ public function test_process_payment_for_order_rejects_with_order_id_mismatch() {
+ $order = WC_Helper_Order::create_order();
+ $intent_meta_order_id = 0;
+ $woopay_intent_id = 'woopay_invalid_intent_id_mock';
+ $payment_intent = WC_Helper_Intention::create_intention(
+ [
+ 'status' => 'success',
+ 'metadata' => [ 'order_id' => (string) $intent_meta_order_id ],
+ ]
+ );
+
+ $_POST['platform-checkout-intent'] = $woopay_intent_id;
+
+ $payment_information = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'card' );
+
+ $this->mock_wcpay_request( Get_Intention::class, 1, $woopay_intent_id )
+ ->expects( $this->once() )
+ ->method( 'format_response' )
+ ->willReturn( $payment_intent );
+
+ $this->expectException( 'WCPay\Exceptions\Order_ID_Mismatch_Exception' );
+ $this->expectExceptionMessage( 'We're not able to process this payment. Please try again later. WooPayMeta: intent_meta_order_id: ' . $intent_meta_order_id . ', order_id: ' . $order->get_id() );
+ $this->card_gateway->process_payment_for_order( WC()->cart, $payment_information );
+ }
+
public function test_set_mandate_data_to_payment_intent_if_not_required() {
$payment_method = 'woocommerce_payments_sepa_debit';
$order = WC_Helper_Order::create_order();
From 68b65c1f9ad2211c017a4a561dc6b515c2011cb4 Mon Sep 17 00:00:00 2001
From: Francesco
Date: Tue, 28 May 2024 21:09:11 +0300
Subject: [PATCH 02/52] chore: update incompatibility notice wrapping (#8866)
Co-authored-by: Brett Shumaker
---
changelog/chore-incompatibility-notice-wrapping | 4 ++++
client/settings/settings-warnings/incompatibility-notice.js | 4 +---
client/settings/settings-warnings/style.scss | 5 -----
3 files changed, 5 insertions(+), 8 deletions(-)
create mode 100644 changelog/chore-incompatibility-notice-wrapping
delete mode 100644 client/settings/settings-warnings/style.scss
diff --git a/changelog/chore-incompatibility-notice-wrapping b/changelog/chore-incompatibility-notice-wrapping
new file mode 100644
index 00000000000..cad615b4fd1
--- /dev/null
+++ b/changelog/chore-incompatibility-notice-wrapping
@@ -0,0 +1,4 @@
+Significance: patch
+Type: update
+
+chore: update incompatibility notice wrapping
diff --git a/client/settings/settings-warnings/incompatibility-notice.js b/client/settings/settings-warnings/incompatibility-notice.js
index 52d9d07f098..c3a388f8df6 100644
--- a/client/settings/settings-warnings/incompatibility-notice.js
+++ b/client/settings/settings-warnings/incompatibility-notice.js
@@ -7,13 +7,11 @@ import interpolateComponents from '@automattic/interpolate-components';
/**
* Internal dependencies
*/
-import './style.scss';
import InlineNotice from 'wcpay/components/inline-notice';
const IncompatibilityNotice = ( { message, learnMoreLinkHref } ) => (
- { message }
-
+ { message }{ ' ' }
{ interpolateComponents( {
mixedString: __(
'{{learnMoreLink}}Learn More{{/learnMoreLink}}',
diff --git a/client/settings/settings-warnings/style.scss b/client/settings/settings-warnings/style.scss
deleted file mode 100644
index e401acb1e5e..00000000000
--- a/client/settings/settings-warnings/style.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.components-notice {
- &__content {
- display: flex;
- }
-}
From 8481984317dbad3195a598c672d316e42af195bd Mon Sep 17 00:00:00 2001
From: Ricardo Metring
Date: Wed, 29 May 2024 09:06:32 +0200
Subject: [PATCH 03/52] Fix "Pay for order" infinite loading (#8886)
---
changelog/fix-8883-pay-for-order-blocked-ui | 4 +++
client/checkout/classic/payment-processing.js | 35 ++++++++++++-------
.../classic/test/payment-processing.test.js | 10 ++++++
3 files changed, 37 insertions(+), 12 deletions(-)
create mode 100644 changelog/fix-8883-pay-for-order-blocked-ui
diff --git a/changelog/fix-8883-pay-for-order-blocked-ui b/changelog/fix-8883-pay-for-order-blocked-ui
new file mode 100644
index 00000000000..b74ba9e179f
--- /dev/null
+++ b/changelog/fix-8883-pay-for-order-blocked-ui
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Fix "Pay for order" infinite loading when submitting form without payment details.
diff --git a/client/checkout/classic/payment-processing.js b/client/checkout/classic/payment-processing.js
index 74ab31c7e36..204a84adc10 100644
--- a/client/checkout/classic/payment-processing.js
+++ b/client/checkout/classic/payment-processing.js
@@ -1,3 +1,8 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
/**
* Internal dependencies
*/
@@ -63,8 +68,8 @@ async function initializeAppearance( api ) {
*
* @param {Object} $form The jQuery object for the form.
*/
-export function blockUI( $form ) {
- $form.addClass( 'processing' ).block( {
+export async function blockUI( $form ) {
+ await $form.addClass( 'processing' ).block( {
message: null,
overlayCSS: {
background: '#fff',
@@ -515,18 +520,24 @@ export const processPayment = (
return;
}
- const { elements, isPaymentInformationComplete } = gatewayUPEComponents[
- paymentMethodType
- ];
- if ( ! isPaymentInformationComplete ) {
- showErrorCheckout( 'Your payment information is incomplete.' );
- return false;
- }
-
- blockUI( $form );
-
( async () => {
try {
+ await blockUI( $form );
+
+ const {
+ elements,
+ isPaymentInformationComplete,
+ } = gatewayUPEComponents[ paymentMethodType ];
+
+ if ( ! isPaymentInformationComplete ) {
+ throw new Error(
+ __(
+ 'Your payment information is incomplete.',
+ 'woocommerce-payments'
+ )
+ );
+ }
+
await validateElements( elements );
const paymentMethodObject = await createStripePaymentMethod(
api,
diff --git a/client/checkout/classic/test/payment-processing.test.js b/client/checkout/classic/test/payment-processing.test.js
index 0dd6a9ef2a4..ad8a24315b9 100644
--- a/client/checkout/classic/test/payment-processing.test.js
+++ b/client/checkout/classic/test/payment-processing.test.js
@@ -457,6 +457,8 @@ describe( 'Payment processing', () => {
};
await processPayment( apiMock, checkoutForm, 'card' );
+ // Wait for promises to resolve.
+ await new Promise( ( resolve ) => setImmediate( resolve ) );
expect( mockCreatePaymentMethod ).toHaveBeenCalledWith( {
elements: expect.any( Object ),
@@ -499,6 +501,8 @@ describe( 'Payment processing', () => {
};
await processPayment( apiMock, checkoutForm, 'card' );
+ // Wait for promises to resolve.
+ await new Promise( ( resolve ) => setImmediate( resolve ) );
expect( mockCreatePaymentMethod ).toHaveBeenCalledWith( {
elements: expect.any( Object ),
@@ -537,6 +541,8 @@ describe( 'Payment processing', () => {
};
await processPayment( apiMock, checkoutForm, 'card' );
+ // Wait for promises to resolve.
+ await new Promise( ( resolve ) => setImmediate( resolve ) );
expect( mockCreatePaymentMethod ).toHaveBeenCalledWith( {
elements: expect.any( Object ),
@@ -572,6 +578,8 @@ describe( 'Payment processing', () => {
};
await processPayment( apiMock, checkoutForm, 'card' );
+ // Wait for promises to resolve.
+ await new Promise( ( resolve ) => setImmediate( resolve ) );
expect( mockCreatePaymentMethod ).toHaveBeenCalledWith( {
elements: expect.any( Object ),
@@ -605,6 +613,8 @@ describe( 'Payment processing', () => {
};
await processPayment( apiMock, addPaymentMethodForm, 'card' );
+ // Wait for promises to resolve.
+ await new Promise( ( resolve ) => setImmediate( resolve ) );
expect( mockCreatePaymentMethod ).toHaveBeenCalledWith( {
elements: expect.any( Object ),
From b6fb6b5ee140dfc6049f19582b6b7a179c392d64 Mon Sep 17 00:00:00 2001
From: Daniel Mallory
Date: Wed, 29 May 2024 09:18:00 +0100
Subject: [PATCH 04/52] Update to not open in new tab if onboarding not
complete (#8859)
Co-authored-by: Vlad Olaru
---
changelog/dev-finish-setup-cta | 4 ++++
.../components/account-status/account-tools/index.tsx | 1 -
.../test/__snapshots__/index.test.tsx.snap | 2 --
.../account-status/test/__snapshots__/index.js.snap | 1 -
.../task-list/tasks/update-business-details-task.tsx | 10 +++++++++-
5 files changed, 13 insertions(+), 5 deletions(-)
create mode 100644 changelog/dev-finish-setup-cta
diff --git a/changelog/dev-finish-setup-cta b/changelog/dev-finish-setup-cta
new file mode 100644
index 00000000000..0902b7fd4c6
--- /dev/null
+++ b/changelog/dev-finish-setup-cta
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Fixes some cases where redirects to the onboarding will open in a new tab.
diff --git a/client/components/account-status/account-tools/index.tsx b/client/components/account-status/account-tools/index.tsx
index e417d4349ed..34886cc95c3 100644
--- a/client/components/account-status/account-tools/index.tsx
+++ b/client/components/account-status/account-tools/index.tsx
@@ -56,7 +56,6 @@ export const AccountTools: React.FC< Props > = ( props: Props ) => {
)
}
href={ accountLink }
- target={ '_blank' }
>
{ strings.finish }
diff --git a/client/components/account-status/account-tools/test/__snapshots__/index.test.tsx.snap b/client/components/account-status/account-tools/test/__snapshots__/index.test.tsx.snap
index b5b1f5eb8fc..988d1e2d7fe 100644
--- a/client/components/account-status/account-tools/test/__snapshots__/index.test.tsx.snap
+++ b/client/components/account-status/account-tools/test/__snapshots__/index.test.tsx.snap
@@ -24,7 +24,6 @@ exports[`AccountTools should render in live mode 1`] = `
Finish setup
@@ -63,7 +62,6 @@ exports[`AccountTools should render in sandbox mode 1`] = `
Finish setup
diff --git a/client/components/account-status/test/__snapshots__/index.js.snap b/client/components/account-status/test/__snapshots__/index.js.snap
index 97a0ae5fc41..8c3367a63fc 100644
--- a/client/components/account-status/test/__snapshots__/index.js.snap
+++ b/client/components/account-status/test/__snapshots__/index.js.snap
@@ -189,7 +189,6 @@ exports[`AccountStatus renders normal status 1`] = `
Finish setup
diff --git a/client/overview/task-list/tasks/update-business-details-task.tsx b/client/overview/task-list/tasks/update-business-details-task.tsx
index 3d1061311be..2de66b9890f 100644
--- a/client/overview/task-list/tasks/update-business-details-task.tsx
+++ b/client/overview/task-list/tasks/update-business-details-task.tsx
@@ -27,6 +27,7 @@ export const getUpdateBusinessDetailsTask = (
const accountDetailsPastDue = 'restricted' === status && pastDue;
const hasMultipleErrors = 1 < errorMessages.length;
const hasSingleError = 1 === errorMessages.length;
+ const connectUrl = wcpaySettings.connectUrl;
const accountLinkWithSource = addQueryArgs( accountLink, {
source: 'overview-page__update-business-details-task',
} );
@@ -113,7 +114,14 @@ export const getUpdateBusinessDetailsTask = (
recordEvent( 'wcpay_account_details_link_clicked', {
source: 'overview-page__update-business-details-task',
} );
- window.open( accountLinkWithSource, '_blank' );
+
+ // If the onboarding isn't complete use the connectUrl instead,
+ // as the accountLink doesn't handle redirecting back to the overview page.
+ if ( ! detailsSubmitted ) {
+ window.location.href = connectUrl;
+ } else {
+ window.open( accountLinkWithSource, '_blank' );
+ }
}
};
From 9196a8dfd592323ea41450c60681af894a0e78f9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Krist=C3=B3fer=20Reykjal=C3=ADn?=
<13835680+reykjalin@users.noreply.github.com>
Date: Wed, 29 May 2024 13:16:13 -0400
Subject: [PATCH 05/52] Prevent WooPay button from submitting the checkout
forms when the customer presses "enter" on their keyboard (#8885)
---
changelog/fix-woopay-button-submit-when-enter-pressed | 4 ++++
.../woopay/express-button/woopay-express-checkout-button.js | 1 +
2 files changed, 5 insertions(+)
create mode 100644 changelog/fix-woopay-button-submit-when-enter-pressed
diff --git a/changelog/fix-woopay-button-submit-when-enter-pressed b/changelog/fix-woopay-button-submit-when-enter-pressed
new file mode 100644
index 00000000000..0b89f8eed90
--- /dev/null
+++ b/changelog/fix-woopay-button-submit-when-enter-pressed
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Make it so that the WooPay button is not triggered on Checkout pages when the "Enter" key is pressed on a keyboard.
diff --git a/client/checkout/woopay/express-button/woopay-express-checkout-button.js b/client/checkout/woopay/express-button/woopay-express-checkout-button.js
index 2220a1422e6..00b8a38b2c4 100644
--- a/client/checkout/woopay/express-button/woopay-express-checkout-button.js
+++ b/client/checkout/woopay/express-button/woopay-express-checkout-button.js
@@ -346,6 +346,7 @@ export const WoopayExpressCheckoutButton = ( {
data-width-type={ buttonWidthType }
style={ { height: `${ height }px` } }
disabled={ isLoading }
+ type="button"
>
{ isLoading ? (
From 1bd1b1d253cc8e5a7d3f57e4686abb5c6c7c1a91 Mon Sep 17 00:00:00 2001
From: Hsing-yu Flowers
Date: Wed, 29 May 2024 14:38:20 -0400
Subject: [PATCH 06/52] Add new process payment exception (#8834)
---
changelog/add-new-process-payment-exception | 4 +++
includes/class-wc-payment-gateway-wcpay.php | 15 +++++++---
includes/class-wc-payments.php | 1 +
.../class-new-process-payment-exception.php | 16 ++++++++++
.../test-class-wc-payment-gateway-wcpay.php | 29 +++++++++++++++++++
5 files changed, 61 insertions(+), 4 deletions(-)
create mode 100644 changelog/add-new-process-payment-exception
create mode 100644 includes/exceptions/class-new-process-payment-exception.php
diff --git a/changelog/add-new-process-payment-exception b/changelog/add-new-process-payment-exception
new file mode 100644
index 00000000000..f2ac469fefc
--- /dev/null
+++ b/changelog/add-new-process-payment-exception
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Add New_Process_Payment_Exception
diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php
index fdb12b247e3..0ca27dd3788 100644
--- a/includes/class-wc-payment-gateway-wcpay.php
+++ b/includes/class-wc-payment-gateway-wcpay.php
@@ -18,7 +18,7 @@
use WCPay\Constants\Intent_Status;
use WCPay\Constants\Payment_Type;
use WCPay\Constants\Payment_Method;
-use WCPay\Exceptions\{ Add_Payment_Method_Exception, Amount_Too_Small_Exception, Process_Payment_Exception, Intent_Authentication_Exception, API_Exception, Invalid_Address_Exception, Fraud_Prevention_Enabled_Exception, Invalid_Phone_Number_Exception, Rate_Limiter_Enabled_Exception, Order_ID_Mismatch_Exception, Order_Not_Found_Exception };
+use WCPay\Exceptions\{ Add_Payment_Method_Exception, Amount_Too_Small_Exception, Process_Payment_Exception, Intent_Authentication_Exception, API_Exception, Invalid_Address_Exception, Fraud_Prevention_Enabled_Exception, Invalid_Phone_Number_Exception, Rate_Limiter_Enabled_Exception, Order_ID_Mismatch_Exception, Order_Not_Found_Exception, New_Process_Payment_Exception };
use WCPay\Core\Server\Request\Cancel_Intention;
use WCPay\Core\Server\Request\Capture_Intention;
use WCPay\Core\Server\Request\Create_And_Confirm_Intention;
@@ -1072,8 +1072,8 @@ function_exists( 'wcs_order_contains_subscription' )
* and if the answer is yes, uses it and returns the result.
*
* @param WC_Order $order Order that needs payment.
- * @return array|null Array if processed, null if the new process is not supported.
- * @throws Exception If the payment process could not be completed.
+ * @return array|null Array if processed, null if the new process is not supported.
+ * @throws Exception Error processing the payment.
*/
public function new_process_payment( WC_Order $order ) {
$manual_capture = $this->get_capture_type() === Payment_Capture_Type::MANUAL();
@@ -1119,7 +1119,14 @@ public function new_process_payment( WC_Order $order ) {
];
}
- throw new Exception( __( 'The payment process could not be completed.', 'woocommerce-payments' ) );
+ throw new Exception(
+ __( 'The payment process could not be completed.', 'woocommerce-payments' ),
+ 0,
+ new New_Process_Payment_Exception(
+ __( 'The payment process could not be completed.', 'woocommerce-payments' ),
+ 'new_process_payment'
+ )
+ );
}
/**
diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php
index 3a70e64758f..c459d0cefec 100644
--- a/includes/class-wc-payments.php
+++ b/includes/class-wc-payments.php
@@ -428,6 +428,7 @@ public static function init() {
include_once __DIR__ . '/exceptions/class-invalid-price-exception.php';
include_once __DIR__ . '/exceptions/class-fraud-ruleset-exception.php';
include_once __DIR__ . '/exceptions/class-fraud-prevention-enabled-exception.php';
+ include_once __DIR__ . '/exceptions/class-new-process-payment-exception.php';
include_once __DIR__ . '/exceptions/class-order-not-found-exception.php';
include_once __DIR__ . '/exceptions/class-order-id-mismatch-exception.php';
include_once __DIR__ . '/exceptions/class-rate-limiter-enabled-exception.php';
diff --git a/includes/exceptions/class-new-process-payment-exception.php b/includes/exceptions/class-new-process-payment-exception.php
new file mode 100644
index 00000000000..742c1415b98
--- /dev/null
+++ b/includes/exceptions/class-new-process-payment-exception.php
@@ -0,0 +1,16 @@
+dev();
+
+ $mock_service = $this->createMock( PaymentProcessingService::class );
+ $mock_router = $this->createMock( Router::class );
+ $order = WC_Helper_Order::create_order();
+ $mock_state = $this->createMock( PaymentErrorState::class );
+
+ wcpay_get_test_container()->replace( PaymentProcessingService::class, $mock_service );
+ wcpay_get_test_container()->replace( Router::class, $mock_router );
+
+ $mock_router->expects( $this->once() )
+ ->method( 'should_use_new_payment_process' )
+ ->willReturn( true );
+
+ // Assert: The new service is called.
+ $mock_service->expects( $this->once() )
+ ->method( 'process_payment' )
+ ->with( $order->get_id() )
+ ->willReturn( $mock_state );
+
+ $this->expectException( Exception::class );
+ $this->expectExceptionMessage( 'The payment process could not be completed.' );
+
+ $this->card_gateway->process_payment( $order->get_id() );
+ }
+
public function test_process_payment_rate_limiter_enabled_throw_exception() {
$order = WC_Helper_Order::create_order();
From 08f4852564a41fe1391c93e271efdd3e09557547 Mon Sep 17 00:00:00 2001
From: Francesco
Date: Mon, 3 Jun 2024 13:11:54 +0300
Subject: [PATCH 07/52] fix: Store API tokenized cart PM title (#8842)
---
.../fix-tokenized-cart-payment-method-title | 4 ++
...ayments-payment-request-button-handler.php | 41 +++++++++++++++++++
2 files changed, 45 insertions(+)
create mode 100644 changelog/fix-tokenized-cart-payment-method-title
diff --git a/changelog/fix-tokenized-cart-payment-method-title b/changelog/fix-tokenized-cart-payment-method-title
new file mode 100644
index 00000000000..831ba5e453c
--- /dev/null
+++ b/changelog/fix-tokenized-cart-payment-method-title
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+fix: Store API tokenized cart payment method title
diff --git a/includes/class-wc-payments-payment-request-button-handler.php b/includes/class-wc-payments-payment-request-button-handler.php
index f89fdf39e2e..fd7de809e9a 100644
--- a/includes/class-wc-payments-payment-request-button-handler.php
+++ b/includes/class-wc-payments-payment-request-button-handler.php
@@ -110,6 +110,7 @@ public function init() {
add_filter( 'pre_option_wcpay_is_apple_pay_enabled', [ $this, 'get_option_is_apple_pay_enabled' ], 10, 1 );
if ( WC_Payments_Features::is_tokenized_cart_prb_enabled() ) {
+ add_action( 'woocommerce_store_api_checkout_update_order_from_request', [ $this, 'tokenized_cart_set_payment_method_type' ], 10, 2 );
add_filter( 'rest_pre_dispatch', [ $this, 'tokenized_cart_store_api_address_normalization' ], 10, 3 );
add_filter( 'rest_pre_dispatch', [ $this, 'tokenized_cart_store_api_nonce_overwrite' ], 10, 3 );
add_filter(
@@ -121,6 +122,46 @@ public function init() {
}
}
+ /**
+ * Updates the checkout order based on the request, to set the Apple Pay/Google Pay payment method title.
+ *
+ * @param \WC_Order $order The order to be updated.
+ * @param \WP_REST_Request $request Store API request to update the order.
+ */
+ public function tokenized_cart_set_payment_method_type( \WC_Order $order, \WP_REST_Request $request ) {
+ if ( ! isset( $request['payment_method'] ) || 'woocommerce_payments' !== $request['payment_method'] ) {
+ return;
+ }
+
+ if ( empty( $request['payment_data'] ) ) {
+ return;
+ }
+
+ $payment_data = [];
+ foreach ( $request['payment_data'] as $data ) {
+ $payment_data[ sanitize_key( $data['key'] ) ] = wc_clean( $data['value'] );
+ }
+
+ if ( empty( $payment_data['payment_request_type'] ) ) {
+ return;
+ }
+
+ $payment_request_type = wc_clean( wp_unslash( $payment_data['payment_request_type'] ) );
+
+ $payment_method_titles = [
+ 'apple_pay' => 'Apple Pay',
+ 'google_pay' => 'Google Pay',
+ ];
+
+ $suffix = apply_filters( 'wcpay_payment_request_payment_method_title_suffix', 'WooPayments' );
+ if ( ! empty( $suffix ) ) {
+ $suffix = " ($suffix)";
+ }
+
+ $payment_method_title = isset( $payment_method_titles[ $payment_request_type ] ) ? $payment_method_titles[ $payment_request_type ] : 'Payment Request';
+ $order->set_payment_method_title( $payment_method_title . $suffix );
+ }
+
/**
* The nonce supplied by the frontend can be overwritten in this middleware:
* https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-blocks/assets/js/middleware/store-api-nonce.js
From ef195c03b6c8e64b923e1ffc509024e0c881decf Mon Sep 17 00:00:00 2001
From: Rafael Zaleski
Date: Mon, 3 Jun 2024 10:45:09 -0300
Subject: [PATCH 08/52] Add payment processing using ECE on the Blocks checkout
and cart pages (#8884)
---
.../add-8773-ece-support-blocks-checkout-page | 4 +
client/checkout/api/index.js | 33 +
client/checkout/blocks/index.js | 7 +-
.../components/express-checkout-component.js | 52 ++
.../components/express-checkout-container.js | 28 +
.../blocks/express-checkout.js | 36 -
.../blocks/hooks/use-express-checkout.js | 93 ++
client/express-checkout/blocks/index.js | 9 +-
client/express-checkout/event-handlers.js | 61 ++
client/express-checkout/utils/index.js | 1 +
client/express-checkout/utils/normalize.js | 101 ++
client/utils/express-checkout/index.js | 39 +-
includes/class-wc-payment-gateway-wcpay.php | 6 +-
includes/class-wc-payments.php | 11 +-
...payments-express-checkout-ajax-handler.php | 195 ++++
...xpress-checkout-button-display-handler.php | 26 +-
...yments-express-checkout-button-handler.php | 400 +-------
...ayments-express-checkout-button-helper.php | 859 ++++++++++++++++--
.../services/class-checkout-service.php | 2 +-
src/Internal/Payment/Factor.php | 7 +
.../unit/src/Internal/Payment/FactorTest.php | 1 +
...xpress-checkout-button-display-handler.php | 33 +-
22 files changed, 1456 insertions(+), 548 deletions(-)
create mode 100644 changelog/add-8773-ece-support-blocks-checkout-page
create mode 100644 client/express-checkout/blocks/components/express-checkout-component.js
create mode 100644 client/express-checkout/blocks/components/express-checkout-container.js
delete mode 100644 client/express-checkout/blocks/express-checkout.js
create mode 100644 client/express-checkout/blocks/hooks/use-express-checkout.js
create mode 100644 client/express-checkout/event-handlers.js
create mode 100644 client/express-checkout/utils/index.js
create mode 100644 client/express-checkout/utils/normalize.js
create mode 100644 includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php
diff --git a/changelog/add-8773-ece-support-blocks-checkout-page b/changelog/add-8773-ece-support-blocks-checkout-page
new file mode 100644
index 00000000000..1e8174e125c
--- /dev/null
+++ b/changelog/add-8773-ece-support-blocks-checkout-page
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add payment processing using ECE in the Blocks checkout and cart pages.
diff --git a/client/checkout/api/index.js b/client/checkout/api/index.js
index 6a088a9c5c1..3775aa7f394 100644
--- a/client/checkout/api/index.js
+++ b/client/checkout/api/index.js
@@ -8,6 +8,8 @@ import {
getPaymentRequestData,
getPaymentRequestAjaxURL,
buildAjaxURL,
+ getExpressCheckoutAjaxURL,
+ getExpressCheckoutConfig,
} from 'utils/express-checkout';
/**
@@ -406,6 +408,37 @@ export default class WCPayAPI {
} );
}
+ /**
+ * Submits shipping address to get available shipping options
+ * from Express Checkout ECE payment method.
+ *
+ * @param {Object} shippingAddress Shipping details.
+ * @return {Promise} Promise for the request to the server.
+ */
+ expressCheckoutECECalculateShippingOptions( shippingAddress ) {
+ return this.request(
+ getExpressCheckoutAjaxURL( 'get_shipping_options' ),
+ {
+ security: getExpressCheckoutConfig( 'nonce' )?.shipping,
+ is_product_page: getExpressCheckoutConfig( 'is_product_page' ),
+ ...shippingAddress,
+ }
+ );
+ }
+
+ /**
+ * Creates order based on Express Checkout ECE payment method.
+ *
+ * @param {Object} paymentData Order data.
+ * @return {Promise} Promise for the request to the server.
+ */
+ expressCheckoutECECreateOrder( paymentData ) {
+ return this.request( getExpressCheckoutAjaxURL( 'create_order' ), {
+ _wpnonce: getExpressCheckoutConfig( 'nonce' )?.checkout,
+ ...paymentData,
+ } );
+ }
+
initWooPay( userEmail, woopayUserSession ) {
if ( ! this.isWooPayRequesting ) {
this.isWooPayRequesting = true;
diff --git a/client/checkout/blocks/index.js b/client/checkout/blocks/index.js
index 0e26e9ed4b1..9f858acd87d 100644
--- a/client/checkout/blocks/index.js
+++ b/client/checkout/blocks/index.js
@@ -154,8 +154,11 @@ if ( getUPEConfig( 'isWooPayEnabled' ) ) {
}
}
-registerExpressPaymentMethod( paymentRequestPaymentMethod( api ) );
-registerExpressPaymentMethod( expressCheckoutElementPaymentMethod( api ) );
+if ( getUPEConfig( 'isExpressCheckoutElementEnabled' ) ) {
+ registerExpressPaymentMethod( expressCheckoutElementPaymentMethod( api ) );
+} else {
+ registerExpressPaymentMethod( paymentRequestPaymentMethod( api ) );
+}
window.addEventListener( 'load', () => {
enqueueFraudScripts( getUPEConfig( 'fraudServices' ) );
addCheckoutTracking();
diff --git a/client/express-checkout/blocks/components/express-checkout-component.js b/client/express-checkout/blocks/components/express-checkout-component.js
new file mode 100644
index 00000000000..a8bbc56ba4b
--- /dev/null
+++ b/client/express-checkout/blocks/components/express-checkout-component.js
@@ -0,0 +1,52 @@
+/**
+ * External dependencies
+ */
+import { ExpressCheckoutElement } from '@stripe/react-stripe-js';
+import { shippingAddressChangeHandler } from '../../event-handlers';
+import { useExpressCheckout } from '../hooks/use-express-checkout';
+
+/**
+ * ExpressCheckout express payment method component.
+ *
+ * @param {Object} props PaymentMethodProps.
+ *
+ * @return {ReactNode} Stripe Elements component.
+ */
+const ExpressCheckoutComponent = ( {
+ api,
+ billing,
+ shippingData,
+ setExpressPaymentError,
+ onClick,
+ onClose,
+} ) => {
+ const {
+ buttonOptions,
+ onButtonClick,
+ onConfirm,
+ onCancel,
+ } = useExpressCheckout( {
+ api,
+ billing,
+ shippingData,
+ onClick,
+ onClose,
+ setExpressPaymentError,
+ } );
+
+ const onShippingAddressChange = ( event ) => {
+ shippingAddressChangeHandler( api, event );
+ };
+
+ return (
+
+ );
+};
+
+export default ExpressCheckoutComponent;
diff --git a/client/express-checkout/blocks/components/express-checkout-container.js b/client/express-checkout/blocks/components/express-checkout-container.js
new file mode 100644
index 00000000000..e25960c03cd
--- /dev/null
+++ b/client/express-checkout/blocks/components/express-checkout-container.js
@@ -0,0 +1,28 @@
+/**
+ * External dependencies
+ */
+import { Elements } from '@stripe/react-stripe-js';
+
+/**
+ * Internal dependencies
+ */
+import ExpressCheckoutComponent from './express-checkout-component';
+
+const ExpressCheckoutContainer = ( props ) => {
+ const { stripe, billing } = props;
+
+ const options = {
+ mode: 'payment',
+ paymentMethodCreation: 'manual',
+ amount: billing.cartTotal.value,
+ currency: billing.currency.code.toLowerCase(),
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default ExpressCheckoutContainer;
diff --git a/client/express-checkout/blocks/express-checkout.js b/client/express-checkout/blocks/express-checkout.js
deleted file mode 100644
index 5b4c47658bd..00000000000
--- a/client/express-checkout/blocks/express-checkout.js
+++ /dev/null
@@ -1,36 +0,0 @@
-/* global wcpayExpressCheckoutParams */
-
-/**
- * External dependencies
- */
-import { Elements, ExpressCheckoutElement } from '@stripe/react-stripe-js';
-
-/**
- * ExpressCheckout express payment method component.
- *
- * @param {Object} props PaymentMethodProps.
- *
- * @return {ReactNode} Stripe Elements component.
- */
-export const ExpressCheckout = ( props ) => {
- const { stripe } = props;
-
- const options = {
- mode: 'payment',
- amount: 1099,
- currency: 'usd',
- };
-
- const buttonOptions = {
- buttonType: {
- googlePay: wcpayExpressCheckoutParams.button.type,
- applePay: wcpayExpressCheckoutParams.button.type,
- },
- };
-
- return (
-
-
-
- );
-};
diff --git a/client/express-checkout/blocks/hooks/use-express-checkout.js b/client/express-checkout/blocks/hooks/use-express-checkout.js
new file mode 100644
index 00000000000..67dc33cc489
--- /dev/null
+++ b/client/express-checkout/blocks/hooks/use-express-checkout.js
@@ -0,0 +1,93 @@
+/* global wcpayExpressCheckoutParams */
+
+/**
+ * External dependencies
+ */
+import { useCallback } from '@wordpress/element';
+import { useStripe, useElements } from '@stripe/react-stripe-js';
+import { normalizeLineItems } from 'wcpay/express-checkout/utils';
+import { onConfirmHandler } from 'wcpay/express-checkout/event-handlers';
+
+export const useExpressCheckout = ( {
+ api,
+ billing,
+ shippingData,
+ onClick,
+ onClose,
+ setExpressPaymentError,
+} ) => {
+ const stripe = useStripe();
+ const elements = useElements();
+
+ const buttonOptions = {
+ paymentMethods: {
+ applePay: 'always',
+ googlePay: 'always',
+ link: 'auto',
+ },
+ buttonType: {
+ googlePay: wcpayExpressCheckoutParams.button.type,
+ applePay: wcpayExpressCheckoutParams.button.type,
+ },
+ };
+
+ const onCancel = () => {
+ onClose();
+ };
+
+ const completePayment = ( redirectUrl ) => {
+ window.location = redirectUrl;
+ };
+
+ const abortPayment = ( onConfirmEvent, message ) => {
+ onConfirmEvent.paymentFailed( 'fail' );
+ setExpressPaymentError( message );
+ };
+
+ const onButtonClick = useCallback(
+ ( event ) => {
+ const options = {
+ lineItems: normalizeLineItems( billing?.cartTotalItems ),
+ emailRequired: true,
+ shippingAddressRequired: shippingData?.needsShipping,
+ phoneNumberRequired:
+ wcpayExpressCheckoutParams?.checkout?.needs_payer_phone,
+ shippingRates: shippingData?.shippingRates[ 0 ]?.shipping_rates?.map(
+ ( r ) => {
+ return {
+ id: r.rate_id,
+ amount: parseInt( r.price, 10 ),
+ displayName: r.name,
+ };
+ }
+ ),
+ };
+ event.resolve( options );
+ onClick();
+ },
+ [
+ onClick,
+ billing.cartTotalItems,
+ shippingData.needsShipping,
+ shippingData.shippingRates,
+ ]
+ );
+
+ const onConfirm = async ( event ) => {
+ onConfirmHandler(
+ api,
+ stripe,
+ elements,
+ completePayment,
+ abortPayment,
+ event
+ );
+ };
+
+ return {
+ buttonOptions,
+ onButtonClick,
+ onConfirm,
+ onCancel,
+ };
+};
diff --git a/client/express-checkout/blocks/index.js b/client/express-checkout/blocks/index.js
index 17b2c1221dd..c7aac8d9b5c 100644
--- a/client/express-checkout/blocks/index.js
+++ b/client/express-checkout/blocks/index.js
@@ -4,13 +4,18 @@
* Internal dependencies
*/
import { PAYMENT_METHOD_NAME_EXPRESS_CHECKOUT_ELEMENT } from '../../checkout/constants';
-import { ExpressCheckout } from './express-checkout';
+import ExpressCheckoutContainer from './components/express-checkout-container';
import { getConfig } from '../../utils/checkout';
import ApplePayPreview from './apple-pay-preview';
const expressCheckoutElementPaymentMethod = ( api ) => ( {
name: PAYMENT_METHOD_NAME_EXPRESS_CHECKOUT_ELEMENT,
- content: ,
+ content: (
+
+ ),
edit: ,
paymentMethodId: PAYMENT_METHOD_NAME_EXPRESS_CHECKOUT_ELEMENT,
supports: {
diff --git a/client/express-checkout/event-handlers.js b/client/express-checkout/event-handlers.js
new file mode 100644
index 00000000000..e46931838a2
--- /dev/null
+++ b/client/express-checkout/event-handlers.js
@@ -0,0 +1,61 @@
+/**
+ * Internal dependencies
+ */
+import { normalizeOrderData, normalizeShippingAddress } from './utils';
+import { getErrorMessageFromNotice } from 'utils/express-checkout';
+
+export const shippingAddressChangeHandler = async ( api, event ) => {
+ const response = await api.expressCheckoutECECalculateShippingOptions(
+ normalizeShippingAddress( event.shippingAddress )
+ );
+ event.resolve( {
+ shippingRates: response.shipping_options,
+ } );
+};
+
+export const onConfirmHandler = async (
+ api,
+ stripe,
+ elements,
+ completePayment,
+ abortPayment,
+ event
+) => {
+ const { paymentMethod, error } = await stripe.createPaymentMethod( {
+ elements,
+ } );
+
+ if ( error ) {
+ abortPayment( event, error.message );
+ return;
+ }
+
+ // Kick off checkout processing step.
+ const createOrderResponse = await api.expressCheckoutECECreateOrder(
+ normalizeOrderData( event, paymentMethod.id )
+ );
+
+ if ( createOrderResponse.result !== 'success' ) {
+ return abortPayment(
+ event,
+ getErrorMessageFromNotice( createOrderResponse.messages )
+ );
+ }
+
+ try {
+ const confirmationRequest = api.confirmIntent(
+ createOrderResponse.redirect
+ );
+
+ // `true` means there is no intent to confirm.
+ if ( confirmationRequest === true ) {
+ completePayment( createOrderResponse.redirect );
+ } else {
+ const redirectUrl = await confirmationRequest;
+
+ completePayment( redirectUrl );
+ }
+ } catch ( e ) {
+ abortPayment( event, error.message );
+ }
+};
diff --git a/client/express-checkout/utils/index.js b/client/express-checkout/utils/index.js
new file mode 100644
index 00000000000..d29d7cccc32
--- /dev/null
+++ b/client/express-checkout/utils/index.js
@@ -0,0 +1 @@
+export * from './normalize';
diff --git a/client/express-checkout/utils/normalize.js b/client/express-checkout/utils/normalize.js
new file mode 100644
index 00000000000..fd44ca77b5c
--- /dev/null
+++ b/client/express-checkout/utils/normalize.js
@@ -0,0 +1,101 @@
+/**
+ * Normalizes incoming cart total items for use as a displayItems with the Stripe api.
+ *
+ * @param {Array} displayItems Items to normalize.
+ * @param {boolean} pending Whether to mark items as pending or not.
+ *
+ * @return {Array} An array of PaymentItems
+ */
+export const normalizeLineItems = ( displayItems ) => {
+ return displayItems
+ .filter( ( displayItem ) => {
+ return !! displayItem.value;
+ } )
+ .map( ( displayItem ) => {
+ return {
+ amount: displayItem.value,
+ name: displayItem.label,
+ };
+ } );
+};
+
+/**
+ * Normalize order data from Stripe's object to the expected format for WC.
+ *
+ * @param {Object} event Stripe's event object.
+ * @param {Object} paymentMethodId Stripe's payment method id.
+ *
+ * @return {Object} Order object in the format WooCommerce expects.
+ */
+export const normalizeOrderData = ( event, paymentMethodId ) => {
+ const name = event?.billingDetails?.name;
+ const email = event?.billingDetails?.email ?? '';
+ const phone = event?.billingDetails?.phone ?? '';
+ const billing = event?.billingDetails?.address ?? {};
+ const shipping = event?.shippingAddress ?? {};
+ const fraudPreventionTokenValue = window.wcpayFraudPreventionToken ?? '';
+
+ return {
+ billing_first_name:
+ name?.split( ' ' )?.slice( 0, 1 )?.join( ' ' ) ?? '',
+ billing_last_name: name?.split( ' ' )?.slice( 1 )?.join( ' ' ) || '-',
+ billing_company: billing?.organization ?? '',
+ billing_email: email ?? event?.payerEmail ?? '',
+ billing_phone:
+ phone ?? event?.payerPhone?.replace( '/[() -]/g', '' ) ?? '',
+ billing_country: billing?.country ?? '',
+ billing_address_1: billing?.line1 ?? '',
+ billing_address_2: billing?.line2 ?? '',
+ billing_city: billing?.city ?? '',
+ billing_state: billing?.state ?? '',
+ billing_postcode: billing?.postal_code ?? '',
+ shipping_first_name:
+ shipping?.name?.split( ' ' )?.slice( 0, 1 )?.join( ' ' ) ?? '',
+ shipping_last_name:
+ shipping?.name?.split( ' ' )?.slice( 1 )?.join( ' ' ) ?? '',
+ shipping_company: shipping?.organization ?? '',
+ shipping_country: shipping?.address?.country ?? '',
+ shipping_address_1: shipping?.address?.line1 ?? '',
+ shipping_address_2: shipping?.address?.line2 ?? '',
+ shipping_city: shipping?.address?.city ?? '',
+ shipping_state: shipping?.address?.state ?? '',
+ shipping_postcode: shipping?.address?.postal_code ?? '',
+ shipping_method: [ event?.shippingRate?.id ?? null ],
+ order_comments: '',
+ payment_method: 'woocommerce_payments',
+ ship_to_different_address: 1,
+ terms: 1,
+ 'wcpay-payment-method': paymentMethodId,
+ payment_request_type: event?.expressPaymentType,
+ express_payment_type: event?.expressPaymentType,
+ 'wcpay-fraud-prevention-token': fraudPreventionTokenValue,
+ };
+};
+
+/**
+ * Normalize shipping address information from Stripe's address object to
+ * the cart shipping address object shape.
+ *
+ * @param {Object} shippingAddress Stripe's shipping address item
+ *
+ * @return {Object} The shipping address in the shape expected by the cart.
+ */
+export const normalizeShippingAddress = ( shippingAddress ) => {
+ return {
+ first_name:
+ shippingAddress?.recipient
+ ?.split( ' ' )
+ ?.slice( 0, 1 )
+ ?.join( ' ' ) ?? '',
+ last_name:
+ shippingAddress?.recipient?.split( ' ' )?.slice( 1 )?.join( ' ' ) ??
+ '',
+ company: '',
+ address_1: shippingAddress?.addressLine?.[ 0 ] ?? '',
+ address_2: shippingAddress?.addressLine?.[ 1 ] ?? '',
+ city: shippingAddress?.city ?? '',
+ state: shippingAddress?.region ?? '',
+ country: shippingAddress?.country ?? '',
+ postcode: shippingAddress?.postalCode?.replace( ' ', '' ) ?? '',
+ };
+};
diff --git a/client/utils/express-checkout/index.js b/client/utils/express-checkout/index.js
index fb8fc89bc39..e0e93470d33 100644
--- a/client/utils/express-checkout/index.js
+++ b/client/utils/express-checkout/index.js
@@ -1,12 +1,12 @@
/* global wcpayPaymentRequestParams, wcpayExpressCheckoutParams */
/**
- * Retrieves payment request data from global variable.
+ * Retrieves express checkout config from global variable.
*
* @param {string} key The object property key.
* @return {mixed} Value of the object prop or null.
*/
-export const getPaymentRequestData = ( key ) => {
+export const getExpressCheckoutConfig = ( key ) => {
if (
typeof wcpayExpressCheckoutParams === 'object' &&
wcpayExpressCheckoutParams.hasOwnProperty( key )
@@ -23,16 +23,33 @@ export const getPaymentRequestData = ( key ) => {
};
/**
- * Get WC AJAX endpoint URL.
+ * Get WC AJAX endpoint URL for express checkout endpoints.
*
* @param {string} endpoint Endpoint.
* @return {string} URL with interpolated endpoint.
*/
-export const getPaymentRequestAjaxURL = ( endpoint ) =>
- getPaymentRequestData( 'wc_ajax_url' )
+export const getExpressCheckoutAjaxURL = ( endpoint ) =>
+ getExpressCheckoutConfig( 'wc_ajax_url' )
.toString()
.replace( '%%endpoint%%', 'wcpay_' + endpoint );
+/**
+ * Retrieves payment request data from global variable.
+ *
+ * @param {string} key The object property key.
+ * @return {mixed} Value of the object prop or null.
+ */
+export const getPaymentRequestData = ( key ) => getExpressCheckoutConfig( key );
+
+/**
+ * Get WC AJAX endpoint URL.
+ *
+ * @param {string} endpoint Endpoint.
+ * @return {string} URL with interpolated endpoint.
+ */
+export const getPaymentRequestAjaxURL = ( endpoint ) =>
+ getExpressCheckoutAjaxURL( endpoint );
+
/**
* Construct WC AJAX endpoint URL.
*
@@ -59,3 +76,15 @@ export const shouldUseGooglePayBrand = () => {
const isBrave = isChrome && window.navigator.brave;
return isChrome && ! isBrave;
};
+
+/**
+ * Get error messages from WooCommerce notice from server response.
+ *
+ * @param {string} notice Error notice.
+ * @return {string} Error messages.
+ */
+export const getErrorMessageFromNotice = ( notice ) => {
+ const div = document.createElement( 'div' );
+ div.innerHTML = notice.trim();
+ return div.firstChild ? div.firstChild.textContent : '';
+};
diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php
index 0ca27dd3788..fb5fa1633c3 100644
--- a/includes/class-wc-payment-gateway-wcpay.php
+++ b/includes/class-wc-payment-gateway-wcpay.php
@@ -1063,6 +1063,10 @@ function_exists( 'wcs_order_contains_subscription' )
$factors[] = Factor::PAYMENT_REQUEST();
}
+ if ( defined( 'WCPAY_EXPRESS_CHECKOUT_CHECKOUT' ) && WCPAY_EXPRESS_CHECKOUT_CHECKOUT ) {
+ $factors[] = Factor::EXPRESS_CHECKOUT_ELEMENT();
+ }
+
$router = wcpay_get_container()->get( Router::class );
return $router->should_use_new_payment_process( $factors );
}
@@ -1794,7 +1798,7 @@ public function process_payment_for_order( $cart, $payment_information, $schedul
$payment_method_type = $this->get_payment_method_type_for_setup_intent( $intent, $token );
}
- if ( empty( $_POST['payment_request_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
+ if ( empty( $_POST['payment_request_type'] ) || empty( $_POST['express_payment_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$this->set_payment_method_title_for_order( $order, $payment_method_type, $payment_method_details );
}
diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php
index c459d0cefec..d4716873ccf 100644
--- a/includes/class-wc-payments.php
+++ b/includes/class-wc-payments.php
@@ -410,6 +410,7 @@ public static function init() {
include_once __DIR__ . '/class-wc-payment-token-wcpay-sepa.php';
include_once __DIR__ . '/class-wc-payments-status.php';
include_once __DIR__ . '/class-wc-payments-token-service.php';
+ include_once __DIR__ . '/express-checkout/class-wc-payments-express-checkout-ajax-handler.php';
include_once __DIR__ . '/express-checkout/class-wc-payments-express-checkout-button-display-handler.php';
include_once __DIR__ . '/express-checkout/class-wc-payments-express-checkout-button-handler.php';
include_once __DIR__ . '/class-wc-payments-payment-request-button-handler.php';
@@ -1628,10 +1629,12 @@ public static function maybe_enqueue_woopay_common_config_script( $should_enqueu
*/
public static function maybe_display_express_checkout_buttons() {
if ( WC_Payments_Features::are_payments_enabled() ) {
- $payment_request_button_handler = new WC_Payments_Payment_Request_Button_Handler( self::$account, self::get_gateway(), self::get_express_checkout_helper() );
- $woopay_button_handler = new WC_Payments_WooPay_Button_Handler( self::$account, self::get_gateway(), self::$woopay_util, self::get_express_checkout_helper() );
- $express_checkout_element_button_handler = new WC_Payments_Express_Checkout_Button_Handler( self::$account, self::get_gateway(), self::get_express_checkout_helper() );
- $express_checkout_button_display_handler = new WC_Payments_Express_Checkout_Button_Display_Handler( self::get_gateway(), $payment_request_button_handler, $woopay_button_handler, $express_checkout_element_button_handler, self::get_express_checkout_helper() );
+ $payment_request_button_handler = new WC_Payments_Payment_Request_Button_Handler( self::$account, self::get_gateway(), self::get_express_checkout_helper() );
+ $woopay_button_handler = new WC_Payments_WooPay_Button_Handler( self::$account, self::get_gateway(), self::$woopay_util, self::get_express_checkout_helper() );
+
+ $express_checkout_ajax_handler = new WC_Payments_Express_Checkout_Ajax_Handler( self::get_express_checkout_helper() );
+ $express_checkout_element_button_handler = new WC_Payments_Express_Checkout_Button_Handler( self::$account, self::get_gateway(), self::get_express_checkout_helper(), $express_checkout_ajax_handler );
+ $express_checkout_button_display_handler = new WC_Payments_Express_Checkout_Button_Display_Handler( self::get_gateway(), $payment_request_button_handler, $woopay_button_handler, $express_checkout_element_button_handler, $express_checkout_ajax_handler, self::get_express_checkout_helper() );
$express_checkout_button_display_handler->init();
}
}
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
new file mode 100644
index 00000000000..773f3c5d61b
--- /dev/null
+++ b/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php
@@ -0,0 +1,195 @@
+express_checkout_button_helper = $express_checkout_button_helper;
+ }
+
+ /**
+ * Initialize hooks.
+ *
+ * @return void
+ */
+ public function init() {
+ add_action( 'wc_ajax_wcpay_create_order', [ $this, 'ajax_create_order' ] );
+ add_action( 'wc_ajax_wcpay_get_shipping_options', [ $this, 'ajax_get_shipping_options' ] );
+ }
+
+ /**
+ * Create order. Security is handled by WC.
+ */
+ public function ajax_create_order() {
+ if ( WC()->cart->is_empty() ) {
+ wp_send_json_error( __( 'Empty cart', 'woocommerce-payments' ), 400 );
+ }
+
+ if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) {
+ define( 'WOOCOMMERCE_CHECKOUT', true );
+ }
+
+ if ( ! defined( 'WCPAY_ECE_CHECKOUT' ) ) {
+ define( 'WCPAY_ECE_CHECKOUT', true );
+ }
+
+ // In case the state is required, but is missing, add a more descriptive error notice.
+ $this->express_checkout_button_helper->validate_state();
+
+ $this->express_checkout_button_helper->normalize_state();
+
+ WC()->checkout()->process_checkout();
+
+ die( 0 );
+ }
+
+ /**
+ * Get shipping options.
+ *
+ * @see WC_Cart::get_shipping_packages().
+ * @see WC_Shipping::calculate_shipping().
+ * @see WC_Shipping::get_packages().
+ */
+ public function ajax_get_shipping_options() {
+ check_ajax_referer( 'wcpay-payment-request-shipping', 'security' );
+
+ $shipping_address = filter_input_array(
+ INPUT_POST,
+ [
+ 'country' => FILTER_SANITIZE_SPECIAL_CHARS,
+ 'state' => FILTER_SANITIZE_SPECIAL_CHARS,
+ 'postcode' => FILTER_SANITIZE_SPECIAL_CHARS,
+ 'city' => FILTER_SANITIZE_SPECIAL_CHARS,
+ 'address_1' => FILTER_SANITIZE_SPECIAL_CHARS,
+ 'address_2' => FILTER_SANITIZE_SPECIAL_CHARS,
+ ]
+ );
+ $product_view_options = filter_input_array( INPUT_POST, [ 'is_product_page' => FILTER_SANITIZE_SPECIAL_CHARS ] );
+ $should_show_itemized_view = ! isset( $product_view_options['is_product_page'] ) ? true : filter_var( $product_view_options['is_product_page'], FILTER_VALIDATE_BOOLEAN );
+
+ $data = $this->express_checkout_button_helper->get_shipping_options( $shipping_address, $should_show_itemized_view );
+ wp_send_json( $data );
+ }
+
+ /**
+ * Adds the current product to the cart. Used on product detail page.
+ */
+ public function ajax_add_to_cart() {
+ check_ajax_referer( 'wcpay-add-to-cart', 'security' );
+
+ if ( ! defined( 'WOOCOMMERCE_CART' ) ) {
+ define( 'WOOCOMMERCE_CART', true );
+ }
+
+ WC()->shipping->reset_shipping();
+
+ $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : false;
+ $product = wc_get_product( $product_id );
+
+ if ( ! $product ) {
+ wp_send_json(
+ [
+ 'error' => [
+ 'code' => 'invalid_product_id',
+ 'message' => __( 'Invalid product id', 'woocommerce-payments' ),
+ ],
+ ],
+ 404
+ );
+ return;
+ }
+
+ $quantity = $this->express_checkout_button_helper->get_quantity();
+
+ $product_type = $product->get_type();
+
+ $is_add_to_cart_valid = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity );
+
+ if ( ! $is_add_to_cart_valid ) {
+ // Some extensions error messages needs to be
+ // submitted to show error messages.
+ wp_send_json(
+ [
+ 'error' => true,
+ 'submit' => true,
+ ],
+ 400
+ );
+ return;
+ }
+
+ // First empty the cart to prevent wrong calculation.
+ WC()->cart->empty_cart();
+
+ if ( ( 'variable' === $product_type || 'variable-subscription' === $product_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 );
+
+ WC()->cart->add_to_cart( $product->get_id(), $quantity, $variation_id, $attributes );
+ }
+
+ if ( in_array( $product_type, [ 'simple', 'variation', 'subscription', 'subscription_variation', 'booking', 'bundle', 'mix-and-match' ], true ) ) {
+ WC()->cart->add_to_cart( $product->get_id(), $quantity );
+ }
+
+ WC()->cart->calculate_totals();
+
+ if ( 'booking' === $product_type ) {
+ $booking_id = $this->express_checkout_button_helper->get_booking_id_from_cart();
+ }
+
+ $data = [];
+ $data += $this->express_checkout_button_helper->build_display_items();
+ $data['result'] = 'success';
+
+ if ( ! empty( $booking_id ) ) {
+ $data['bookingId'] = $booking_id;
+ }
+
+ wp_send_json( $data );
+ }
+
+ /**
+ * Empties the cart via AJAX. Used on the product page.
+ */
+ public function ajax_empty_cart() {
+ check_ajax_referer( 'wcpay-empty-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
+ }
+
+ wp_send_json( [ 'result' => 'success' ] );
+ }
+}
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 3fee5b1d9bd..ab132195f81 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
@@ -42,6 +42,13 @@ class WC_Payments_Express_Checkout_Button_Display_Handler {
*/
private $express_checkout_button_handler;
+ /**
+ * Express Checkout Helper instance.
+ *
+ * @var WC_Payments_Express_Checkout_Ajax_Handler
+ */
+ private $express_checkout_ajax_handler;
+
/**
* Express Checkout Helper instance.
*
@@ -56,19 +63,22 @@ class WC_Payments_Express_Checkout_Button_Display_Handler {
* @param WC_Payments_Payment_Request_Button_Handler $payment_request_button_handler Payment request button handler.
* @param WC_Payments_WooPay_Button_Handler $platform_checkout_button_handler Platform checkout button handler.
* @param WC_Payments_Express_Checkout_Button_Handler $express_checkout_button_handler Express Checkout Element button handler.
- * @param WC_Payments_Express_Checkout_Button_Helper $express_checkout_helper Express checkout helper.
+ * @param WC_Payments_Express_Checkout_Ajax_Handler $express_checkout_ajax_handler Express checkout ajax handlers.
+ * @param WC_Payments_Express_Checkout_Button_Helper $express_checkout_helper Express checkout button helper.
*/
public function __construct(
WC_Payment_Gateway_WCPay $gateway,
WC_Payments_Payment_Request_Button_Handler $payment_request_button_handler,
WC_Payments_WooPay_Button_Handler $platform_checkout_button_handler,
WC_Payments_Express_Checkout_Button_Handler $express_checkout_button_handler,
+ WC_Payments_Express_Checkout_Ajax_Handler $express_checkout_ajax_handler,
WC_Payments_Express_Checkout_Button_Helper $express_checkout_helper
) {
$this->gateway = $gateway;
$this->payment_request_button_handler = $payment_request_button_handler;
$this->platform_checkout_button_handler = $platform_checkout_button_handler;
$this->express_checkout_button_handler = $express_checkout_button_handler;
+ $this->express_checkout_ajax_handler = $express_checkout_ajax_handler;
$this->express_checkout_helper = $express_checkout_helper;
}
@@ -86,8 +96,8 @@ public function init() {
$is_payment_request_enabled = 'yes' === $this->gateway->get_option( 'payment_request' );
if ( $is_woopay_enabled || $is_payment_request_enabled ) {
- add_action( 'wc_ajax_wcpay_add_to_cart', [ $this->express_checkout_helper, 'ajax_add_to_cart' ] );
- add_action( 'wc_ajax_wcpay_empty_cart', [ $this->express_checkout_helper, 'ajax_empty_cart' ] );
+ add_action( 'wc_ajax_wcpay_add_to_cart', [ $this->express_checkout_ajax_handler, 'ajax_add_to_cart' ] );
+ add_action( 'wc_ajax_wcpay_empty_cart', [ $this->express_checkout_ajax_handler, 'ajax_empty_cart' ] );
add_action( 'woocommerce_after_add_to_cart_form', [ $this, 'display_express_checkout_buttons' ], 1 );
add_action( 'woocommerce_proceed_to_checkout', [ $this, 'display_express_checkout_buttons' ], 21 );
@@ -120,12 +130,14 @@ public function display_express_checkout_separator_if_necessary( $separator_star
* @return void
*/
public function display_express_checkout_buttons() {
- $should_show_woopay = $this->platform_checkout_button_handler->should_show_woopay_button();
- $should_show_payment_request = $this->payment_request_button_handler->should_show_payment_request_button();
+ $should_show_woopay = $this->platform_checkout_button_handler->should_show_woopay_button();
+ $should_show_payment_request = $this->payment_request_button_handler->should_show_payment_request_button();
+ $should_show_express_checkout_button = $this->express_checkout_helper->should_show_express_checkout_button();
+
// When Payment Request button is enabled, we need the separator markup on the page, but hidden in case the browser doesn't have any payment request methods to display.
// More details: https://github.com/Automattic/woocommerce-payments/pull/5399#discussion_r1073633776.
- $separator_starts_hidden = $should_show_payment_request && ! $should_show_woopay;
- if ( $should_show_woopay || $should_show_payment_request ) {
+ $separator_starts_hidden = ( $should_show_payment_request || $should_show_express_checkout_button ) && ! $should_show_woopay;
+ if ( $should_show_woopay || $should_show_payment_request || $should_show_express_checkout_button ) {
?>
account = $account;
- $this->gateway = $gateway;
- $this->express_checkout_helper = $express_checkout_helper;
+ public function __construct( WC_Payments_Account $account, WC_Payment_Gateway_WCPay $gateway, WC_Payments_Express_Checkout_Button_Helper $express_checkout_helper, WC_Payments_Express_Checkout_Ajax_Handler $express_checkout_ajax_handler ) {
+ $this->account = $account;
+ $this->gateway = $gateway;
+ $this->express_checkout_helper = $express_checkout_helper;
+ $this->express_checkout_ajax_handler = $express_checkout_ajax_handler;
}
/**
@@ -81,6 +88,8 @@ public function init() {
}
add_action( 'wp_enqueue_scripts', [ $this, 'scripts' ] );
+
+ $this->express_checkout_ajax_handler->init();
}
/**
@@ -100,159 +109,12 @@ public function get_button_settings() {
return array_merge( $common_settings, $payment_request_button_settings );
}
- /**
- * Checks whether Payment Request Button should be available on this page.
- *
- * @return bool
- */
- public function should_show_express_checkout_button() {
- // If account is not connected, then bail.
- if ( ! $this->account->is_stripe_connected( false ) ) {
- return false;
- }
-
- // If no SSL, bail.
- if ( ! WC_Payments::mode()->is_test() && ! is_ssl() ) {
- Logger::log( 'Stripe Payment Request live mode requires SSL.' );
-
- return false;
- }
-
- // Page not supported.
- if ( ! $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_cart() && ! $this->express_checkout_helper->is_checkout() ) {
- return false;
- }
-
- // Product page, but not available in settings.
- if ( $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_available_at( 'product', self::BUTTON_LOCATIONS ) ) {
- return false;
- }
-
- // Checkout page, but not available in settings.
- if ( $this->express_checkout_helper->is_checkout() && ! $this->express_checkout_helper->is_available_at( 'checkout', self::BUTTON_LOCATIONS ) ) {
- return false;
- }
-
- // Cart page, but not available in settings.
- if ( $this->express_checkout_helper->is_cart() && ! $this->express_checkout_helper->is_available_at( 'cart', self::BUTTON_LOCATIONS ) ) {
- return false;
- }
-
- // Product page, but has unsupported product type.
- if ( $this->express_checkout_helper->is_product() && ! $this->is_product_supported() ) {
- Logger::log( 'Product page has unsupported product type ( Payment Request button disabled )' );
- return false;
- }
-
- // Cart has unsupported product type.
- if ( ( $this->express_checkout_helper->is_checkout() || $this->express_checkout_helper->is_cart() ) && ! $this->has_allowed_items_in_cart() ) {
- Logger::log( 'Items in the cart have unsupported product type ( Payment Request button disabled )' );
- return false;
- }
-
- // Order total doesn't matter for Pay for Order page. Thus, this page should always display payment buttons.
- if ( $this->express_checkout_helper->is_pay_for_order_page() ) {
- return true;
- }
-
- // Cart total is 0 or is on product page and product price is 0.
- // Exclude pay-for-order pages from this check.
- if (
- ( ! $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_pay_for_order_page() && 0.0 === (float) WC()->cart->get_total( 'edit' ) ) ||
- ( $this->express_checkout_helper->is_product() && 0.0 === (float) $this->express_checkout_helper->get_product()->get_price() )
-
- ) {
- Logger::log( 'Order price is 0 ( Payment Request button disabled )' );
- return false;
- }
-
- return true;
- }
-
- /**
- * Checks to make sure product type is supported.
- *
- * @return array
- */
- public function supported_product_types() {
- return apply_filters(
- 'wcpay_payment_request_supported_types',
- [
- 'simple',
- 'variable',
- 'variation',
- 'subscription',
- 'variable-subscription',
- 'subscription_variation',
- 'booking',
- 'bundle',
- 'composite',
- 'mix-and-match',
- ]
- );
- }
-
- /**
- * Checks the cart to see if all items are allowed to be used.
- *
- * @return boolean
- *
- * @psalm-suppress UndefinedClass
- */
- public function has_allowed_items_in_cart() {
- /**
- * Pre Orders compatbility where we don't support charge upon release.
- *
- * @psalm-suppress UndefinedClass
- */
- if ( class_exists( 'WC_Pre_Orders_Cart' ) && WC_Pre_Orders_Cart::cart_contains_pre_order() && class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( WC_Pre_Orders_Cart::get_pre_order_product() ) ) {
- return false;
- }
-
- foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
- $_product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key );
-
- if ( ! in_array( $_product->get_type(), $this->supported_product_types(), true ) ) {
- return false;
- }
-
- /**
- * Filter whether product supports Payment Request Button on cart page.
- *
- * @since 6.9.0
- *
- * @param boolean $is_supported Whether product supports Payment Request Button on cart page.
- * @param object $_product Product object.
- */
- if ( ! apply_filters( 'wcpay_payment_request_is_cart_supported', true, $_product ) ) {
- return false;
- }
-
- /**
- * Trial subscriptions with shipping are not supported.
- *
- * @psalm-suppress UndefinedClass
- */
- if ( class_exists( 'WC_Subscriptions_Product' ) && WC_Subscriptions_Product::is_subscription( $_product ) && $_product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $_product ) > 0 ) {
- return false;
- }
- }
-
- // We don't support multiple packages with Payment Request Buttons because we can't offer a good UX.
- $packages = WC()->cart->get_shipping_packages();
- if ( 1 < ( is_countable( $packages ) ? count( $packages ) : 0 ) ) {
- return false;
- }
-
- return true;
- }
-
/**
* Load public scripts and styles.
*/
public function scripts() {
// Don't load scripts if page is not supported.
- if ( ! $this->should_show_express_checkout_button() ) {
+ if ( ! $this->express_checkout_helper->should_show_express_checkout_button() ) {
return;
}
@@ -288,7 +150,7 @@ public function scripts() {
'button_context' => $this->express_checkout_helper->get_button_context(),
'is_pay_for_order' => $this->express_checkout_helper->is_pay_for_order_page(),
'has_block' => has_block( 'woocommerce/cart' ) || has_block( 'woocommerce/checkout' ),
- 'product' => $this->get_product_data(),
+ 'product' => $this->express_checkout_helper->get_product_data(),
'total_label' => $this->express_checkout_helper->get_total_label(),
'is_checkout_page' => $this->express_checkout_helper->is_checkout(),
];
@@ -320,235 +182,11 @@ public function scripts() {
* Display the payment request button.
*/
public function display_express_checkout_button_html() {
- if ( ! $this->should_show_express_checkout_button() ) {
+ if ( ! $this->express_checkout_helper->should_show_express_checkout_button() ) {
return;
}
?>
express_checkout_helper->get_product();
- $is_supported = true;
-
- /**
- * Ignore undefined classes from 3rd party plugins.
- *
- * @psalm-suppress UndefinedClass
- */
- if ( is_null( $product )
- || ! is_object( $product )
- || ! in_array( $product->get_type(), $this->supported_product_types(), true )
- || ( class_exists( 'WC_Subscriptions_Product' ) && $product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $product ) > 0 ) // Trial subscriptions with shipping are not supported.
- || ( class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( $product ) ) // Pre Orders charge upon release not supported.
- || ( class_exists( 'WC_Composite_Products' ) && $product->is_type( 'composite' ) ) // Composite products are not supported on the product page.
- || ( class_exists( 'WC_Mix_and_Match' ) && $product->is_type( 'mix-and-match' ) ) // Mix and match products are not supported on the product page.
- ) {
- $is_supported = false;
- } elseif ( class_exists( 'WC_Product_Addons_Helper' ) ) {
- // File upload addon not supported.
- $product_addons = WC_Product_Addons_Helper::get_product_addons( $product->get_id() );
- foreach ( $product_addons as $addon ) {
- if ( 'file_upload' === $addon['type'] ) {
- $is_supported = false;
- break;
- }
- }
- }
-
- return apply_filters( 'wcpay_payment_request_is_product_supported', $is_supported, $product );
- }
-
- /**
- * Gets the product data for the currently viewed page.
- *
- * @return mixed Returns false if not on a product page, the product information otherwise.
- */
- public function get_product_data() {
- if ( ! $this->express_checkout_helper->is_product() ) {
- return false;
- }
-
- /** @var WC_Product_Variable $product */ // phpcs:ignore
- $product = $this->express_checkout_helper->get_product();
- $currency = get_woocommerce_currency();
-
- if ( 'variable' === $product->get_type() || 'variable-subscription' === $product->get_type() ) {
- $variation_attributes = $product->get_variation_attributes();
- $attributes = [];
-
- foreach ( $variation_attributes as $attribute_name => $attribute_values ) {
- $attribute_key = 'attribute_' . sanitize_title( $attribute_name );
-
- // Passed value via GET takes precedence. Otherwise get the default value for given attribute.
- $attributes[ $attribute_key ] = isset( $_GET[ $attribute_key ] ) // phpcs:ignore WordPress.Security.NonceVerification
- ? wc_clean( wp_unslash( $_GET[ $attribute_key ] ) ) // phpcs:ignore WordPress.Security.NonceVerification
- : $product->get_variation_default_attribute( $attribute_name );
- }
-
- $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 );
- }
- }
-
- try {
- $price = $this->get_product_price( $product );
- } catch ( Invalid_Price_Exception $e ) {
- Logger::log( $e->getMessage() );
- return false;
- }
-
- $data = [];
- $items = [];
-
- $items[] = [
- 'label' => $product->get_name(),
- 'amount' => WC_Payments_Utils::prepare_amount( $price, $currency ),
- ];
-
- $total_tax = 0;
- foreach ( $this->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() && 0 !== wc_get_shipping_method_count( true ) && $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' => apply_filters( 'wcpay_payment_request_total_label', $this->express_checkout_helper->get_total_label() ),
- 'amount' => WC_Payments_Utils::prepare_amount( $price + $total_tax, $currency ),
- 'pending' => true,
- ];
-
- $data['needs_shipping'] = ( wc_shipping_enabled() && 0 !== wc_get_shipping_method_count( true ) && $product->needs_shipping() );
- $data['currency'] = strtolower( $currency );
- $data['country_code'] = substr( get_option( 'woocommerce_default_country' ), 0, 2 );
-
- return apply_filters( 'wcpay_payment_request_product_data', $data, $product );
- }
-
- /**
- * Gets the product total price.
- *
- * @param object $product WC_Product_* object.
- * @param bool $is_deposit Whether customer is paying a deposit.
- * @param int $deposit_plan_id The ID of the deposit plan.
- * @return mixed Total price.
- *
- * @throws Invalid_Price_Exception Whenever a product has no price.
- *
- * @psalm-suppress UndefinedClass
- */
- public function get_product_price( $product, ?bool $is_deposit = null, int $deposit_plan_id = 0 ) {
- // If prices should include tax, using tax inclusive price.
- if ( $this->express_checkout_helper->cart_prices_include_tax() ) {
- $base_price = wc_get_price_including_tax( $product );
- } else {
- $base_price = wc_get_price_excluding_tax( $product );
- }
-
- // If WooCommerce Deposits is active, we need to get the correct price for the product.
- if ( class_exists( 'WC_Deposits_Product_Manager' ) && WC_Deposits_Product_Manager::deposits_enabled( $product->get_id() ) ) {
- if ( is_null( $is_deposit ) ) {
- /**
- * If is_deposit is null, we use the default deposit type for the product.
- *
- * @psalm-suppress UndefinedClass
- */
- $is_deposit = 'deposit' === WC_Deposits_Product_Manager::get_deposit_selected_type( $product->get_id() );
- }
- if ( $is_deposit ) {
- /**
- * Ignore undefined classes from 3rd party plugins.
- *
- * @psalm-suppress UndefinedClass
- */
- $deposit_type = WC_Deposits_Product_Manager::get_deposit_type( $product->get_id() );
- $available_plan_ids = WC_Deposits_Plans_Manager::get_plan_ids_for_product( $product->get_id() );
- // Default to first (default) plan if no plan is specified.
- if ( 'plan' === $deposit_type && 0 === $deposit_plan_id && ! empty( $available_plan_ids ) ) {
- $deposit_plan_id = $available_plan_ids[0];
- }
-
- // Ensure the selected plan is available for the product.
- if ( 0 === $deposit_plan_id || in_array( $deposit_plan_id, $available_plan_ids, true ) ) {
- $base_price = WC_Deposits_Product_Manager::get_deposit_amount( $product, $deposit_plan_id, 'display', $base_price );
- }
- }
- }
-
- // Add subscription sign-up fees to product price.
- $sign_up_fee = 0;
- $subscription_types = [
- 'subscription',
- 'subscription_variation',
- ];
- if ( in_array( $product->get_type(), $subscription_types, true ) && class_exists( 'WC_Subscriptions_Product' ) ) {
- // When there is no sign-up fee, `get_sign_up_fee` falls back to an int 0.
- $sign_up_fee = WC_Subscriptions_Product::get_sign_up_fee( $product );
- }
-
- if ( ! is_numeric( $base_price ) || ! is_numeric( $sign_up_fee ) ) {
- $error_message = sprintf(
- // Translators: %d is the numeric ID of the product without a price.
- __( 'Express checkout does not support products without prices! Please add a price to product #%d', 'woocommerce-payments' ),
- (int) $product->get_id()
- );
- throw new Invalid_Price_Exception(
- esc_html( $error_message )
- );
- }
-
- return $base_price + $sign_up_fee;
- }
-
- /**
- * Calculates taxes as displayed on cart, based on a product and a particular price.
- *
- * @param WC_Product $product The product, for retrieval of tax classes.
- * @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 ) {
- if ( ! wc_tax_enabled() || $this->express_checkout_helper->cart_prices_include_tax() ) {
- // Only proceed when taxes are enabled, but not included.
- return [];
- }
-
- // Follows the way `WC_Cart_Totals::get_item_tax_rates()` works.
- $tax_class = $product->get_tax_class();
- $rates = WC_Tax::get_rates( $tax_class );
- // No cart item, `woocommerce_cart_totals_get_item_tax_rates` can't be applied here.
-
- // Normally there should be a single tax, but `calc_tax` returns an array, let's use it.
- return WC_Tax::calc_tax( $price, $rates, false );
- }
}
\ No newline at end of file
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 75495a3b990..266e69e6f6d 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
@@ -7,6 +7,10 @@
defined( 'ABSPATH' ) || exit;
+use WCPay\Constants\Country_Code;
+use WCPay\Exceptions\Invalid_Price_Exception;
+use WCPay\Logger;
+
/**
* Express Checkout Button Helper class.
*/
@@ -36,86 +40,6 @@ public function __construct( WC_Payment_Gateway_WCPay $gateway, WC_Payments_Acco
$this->account = $account;
}
- /**
- * Adds the current product to the cart. Used on product detail page.
- */
- public function ajax_add_to_cart() {
- check_ajax_referer( 'wcpay-add-to-cart', 'security' );
-
- if ( ! defined( 'WOOCOMMERCE_CART' ) ) {
- define( 'WOOCOMMERCE_CART', true );
- }
-
- WC()->shipping->reset_shipping();
-
- $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : false;
- $product = wc_get_product( $product_id );
-
- if ( ! $product ) {
- wp_send_json(
- [
- 'error' => [
- 'code' => 'invalid_product_id',
- 'message' => __( 'Invalid product id', 'woocommerce-payments' ),
- ],
- ],
- 404
- );
- return;
- }
-
- $quantity = $this->get_quantity();
-
- $product_type = $product->get_type();
-
- $is_add_to_cart_valid = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity );
-
- if ( ! $is_add_to_cart_valid ) {
- // Some extensions error messages needs to be
- // submitted to show error messages.
- wp_send_json(
- [
- 'error' => true,
- 'submit' => true,
- ],
- 400
- );
- return;
- }
-
- // First empty the cart to prevent wrong calculation.
- WC()->cart->empty_cart();
-
- if ( ( 'variable' === $product_type || 'variable-subscription' === $product_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 );
-
- WC()->cart->add_to_cart( $product->get_id(), $quantity, $variation_id, $attributes );
- }
-
- if ( in_array( $product_type, [ 'simple', 'variation', 'subscription', 'subscription_variation', 'booking', 'bundle', 'mix-and-match' ], true ) ) {
- WC()->cart->add_to_cart( $product->get_id(), $quantity );
- }
-
- WC()->cart->calculate_totals();
-
- if ( 'booking' === $product_type ) {
- $booking_id = $this->get_booking_id_from_cart();
- }
-
- $data = [];
- $data += $this->build_display_items();
- $data['result'] = 'success';
-
- if ( ! empty( $booking_id ) ) {
- $data['bookingId'] = $booking_id;
- }
-
- wp_send_json( $data );
- }
-
/**
* Gets the booking id from the cart.
* It's expected that the cart only contains one item which was added via ajax_add_to_cart.
@@ -134,26 +58,6 @@ public function get_booking_id_from_cart() {
return false;
}
- /**
- * Empties the cart via AJAX. Used on the product page.
- */
- public function ajax_empty_cart() {
- check_ajax_referer( 'wcpay-empty-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
- }
-
- wp_send_json( [ 'result' => 'success' ] );
- }
-
/**
* Builds the line items to pass to Payment Request
*
@@ -277,7 +181,7 @@ public function get_total_label() {
*
* @return int
*/
- private function get_quantity() {
+ public function get_quantity() {
// Payment Request Button sends the quantity as qty. WooPay sends it as quantity.
if ( isset( $_POST['quantity'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
return absint( $_POST['quantity'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
@@ -433,4 +337,757 @@ public function is_product_subscription( WC_Product $product ): bool {
|| 'subscription_variation' === $product->get_type()
|| 'variable-subscription' === $product->get_type();
}
+
+ /**
+ * Checks whether Payment Request Button should be available on this page.
+ *
+ * @return bool
+ */
+ public function should_show_express_checkout_button() {
+ // If account is not connected, then bail.
+ if ( ! $this->account->is_stripe_connected( false ) ) {
+ return false;
+ }
+
+ // If no SSL, bail.
+ if ( ! WC_Payments::mode()->is_test() && ! is_ssl() ) {
+ Logger::log( 'Stripe Payment Request live mode requires SSL.' );
+
+ return false;
+ }
+
+ // Page not supported.
+ if ( ! $this->is_product() && ! $this->is_cart() && ! $this->is_checkout() ) {
+ return false;
+ }
+
+ // Product page, but not available in settings.
+ if ( $this->is_product() && ! $this->is_available_at( 'product', WC_Payments_Express_Checkout_Button_Handler::BUTTON_LOCATIONS ) ) {
+ return false;
+ }
+
+ // Checkout page, but not available in settings.
+ if ( $this->is_checkout() && ! $this->is_available_at( 'checkout', WC_Payments_Express_Checkout_Button_Handler::BUTTON_LOCATIONS ) ) {
+ return false;
+ }
+
+ // Cart page, but not available in settings.
+ if ( $this->is_cart() && ! $this->is_available_at( 'cart', WC_Payments_Express_Checkout_Button_Handler::BUTTON_LOCATIONS ) ) {
+ return false;
+ }
+
+ // Product page, but has unsupported product type.
+ if ( $this->is_product() && ! $this->is_product_supported() ) {
+ Logger::log( 'Product page has unsupported product type ( Payment Request button disabled )' );
+ return false;
+ }
+
+ // Cart has unsupported product type.
+ if ( ( $this->is_checkout() || $this->is_cart() ) && ! $this->has_allowed_items_in_cart() ) {
+ Logger::log( 'Items in the cart have unsupported product type ( Payment Request button disabled )' );
+ return false;
+ }
+
+ // Order total doesn't matter for Pay for Order page. Thus, this page should always display payment buttons.
+ if ( $this->is_pay_for_order_page() ) {
+ return true;
+ }
+
+ // Cart total is 0 or is on product page and product price is 0.
+ // Exclude pay-for-order pages from this check.
+ if (
+ ( ! $this->is_product() && ! $this->is_pay_for_order_page() && 0.0 === (float) WC()->cart->get_total( 'edit' ) ) ||
+ ( $this->is_product() && 0.0 === (float) $this->get_product()->get_price() )
+
+ ) {
+ Logger::log( 'Order price is 0 ( Payment Request button disabled )' );
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks to make sure product type is supported.
+ *
+ * @return array
+ */
+ public function supported_product_types() {
+ return apply_filters(
+ 'wcpay_payment_request_supported_types',
+ [
+ 'simple',
+ 'variable',
+ 'variation',
+ 'subscription',
+ 'variable-subscription',
+ 'subscription_variation',
+ 'booking',
+ 'bundle',
+ 'composite',
+ 'mix-and-match',
+ ]
+ );
+ }
+
+ /**
+ * Checks the cart to see if all items are allowed to be used.
+ *
+ * @return boolean
+ *
+ * @psalm-suppress UndefinedClass
+ */
+ public function has_allowed_items_in_cart() {
+ /**
+ * Pre Orders compatbility where we don't support charge upon release.
+ *
+ * @psalm-suppress UndefinedClass
+ */
+ if ( class_exists( 'WC_Pre_Orders_Cart' ) && WC_Pre_Orders_Cart::cart_contains_pre_order() && class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( WC_Pre_Orders_Cart::get_pre_order_product() ) ) {
+ return false;
+ }
+
+ foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
+ $_product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key );
+
+ if ( ! in_array( $_product->get_type(), $this->supported_product_types(), true ) ) {
+ return false;
+ }
+
+ /**
+ * Filter whether product supports Payment Request Button on cart page.
+ *
+ * @since 6.9.0
+ *
+ * @param boolean $is_supported Whether product supports Payment Request Button on cart page.
+ * @param object $_product Product object.
+ */
+ if ( ! apply_filters( 'wcpay_payment_request_is_cart_supported', true, $_product ) ) {
+ return false;
+ }
+
+ /**
+ * Trial subscriptions with shipping are not supported.
+ *
+ * @psalm-suppress UndefinedClass
+ */
+ if ( class_exists( 'WC_Subscriptions_Product' ) && WC_Subscriptions_Product::is_subscription( $_product ) && $_product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $_product ) > 0 ) {
+ return false;
+ }
+ }
+
+ // We don't support multiple packages with Payment Request Buttons because we can't offer a good UX.
+ $packages = WC()->cart->get_shipping_packages();
+ if ( 1 < ( is_countable( $packages ) ? count( $packages ) : 0 ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Gets shipping options available for specified shipping address
+ *
+ * @param array $shipping_address Shipping address.
+ * @param boolean $itemized_display_items Indicates whether to show subtotals or itemized views.
+ *
+ * @return array Shipping options data.
+ *
+ * phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag
+ */
+ public function get_shipping_options( $shipping_address, $itemized_display_items = false ) {
+ try {
+ // Set the shipping options.
+ $data = [];
+
+ // Remember current shipping method before resetting.
+ $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', [] );
+ $this->calculate_shipping( apply_filters( 'wcpay_payment_request_shipping_posted_values', $shipping_address ) );
+
+ $packages = WC()->shipping->get_packages();
+
+ if ( ! empty( $packages ) && WC()->customer->has_calculated_shipping() ) {
+ foreach ( $packages as $package_key => $package ) {
+ if ( empty( $package['rates'] ) ) {
+ throw new Exception( __( 'Unable to find shipping method for address.', 'woocommerce-payments' ) );
+ }
+
+ foreach ( $package['rates'] as $key => $rate ) {
+ $data['shipping_options'][] = [
+ 'id' => $rate->id,
+ 'displayName' => $rate->label,
+ 'amount' => WC_Payments_Utils::prepare_amount( $rate->cost, get_woocommerce_currency() ),
+ ];
+ }
+ }
+ } else {
+ throw new Exception( __( 'Unable to find shipping method for address.', 'woocommerce-payments' ) );
+ }
+
+ // The first shipping option is automatically applied on the client.
+ // Keep chosen shipping method by sorting shipping options if the method still available for new address.
+ // Fallback to the first available shipping method.
+ if ( isset( $data['shipping_options'][0] ) ) {
+ if ( isset( $chosen_shipping_methods[0] ) ) {
+ $chosen_method_id = $chosen_shipping_methods[0];
+ $compare_shipping_options = function ( $a, $b ) use ( $chosen_method_id ) {
+ if ( $a['id'] === $chosen_method_id ) {
+ return -1;
+ }
+
+ if ( $b['id'] === $chosen_method_id ) {
+ return 1;
+ }
+
+ return 0;
+ };
+ usort( $data['shipping_options'], $compare_shipping_options );
+ }
+
+ $first_shipping_method_id = $data['shipping_options'][0]['id'];
+ $this->update_shipping_method( [ $first_shipping_method_id ] );
+ }
+
+ WC()->cart->calculate_totals();
+
+ $this->maybe_restore_recurring_chosen_shipping_methods( $chosen_shipping_methods );
+
+ $data += $this->build_display_items( $itemized_display_items );
+ $data['result'] = 'success';
+ } catch ( Exception $e ) {
+ $data += $this->build_display_items( $itemized_display_items );
+ $data['result'] = 'invalid_shipping_address';
+ }
+
+ return $data;
+ }
+
+ /**
+ * Restores the shipping methods previously chosen for each recurring cart after shipping was reset and recalculated
+ * during the Payment Request get_shipping_options flow.
+ *
+ * When the cart contains multiple subscriptions with different billing periods, customers are able to select different shipping
+ * methods for each subscription, however, this is not supported when purchasing with Apple Pay and Google Pay as it's
+ * only concerned about handling the initial purchase.
+ *
+ * In order to avoid Woo Subscriptions's `WC_Subscriptions_Cart::validate_recurring_shipping_methods` throwing an error, we need to restore
+ * the previously chosen shipping methods for each recurring cart.
+ *
+ * This function needs to be called after `WC()->cart->calculate_totals()` is run, otherwise `WC()->cart->recurring_carts` won't exist yet.
+ *
+ * @param array $previous_chosen_methods The previously chosen shipping methods.
+ */
+ private function maybe_restore_recurring_chosen_shipping_methods( $previous_chosen_methods = [] ) {
+ if ( empty( WC()->cart->recurring_carts ) || ! method_exists( 'WC_Subscriptions_Cart', 'get_recurring_shipping_package_key' ) ) {
+ return;
+ }
+
+ $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', [] );
+
+ foreach ( WC()->cart->recurring_carts as $recurring_cart_key => $recurring_cart ) {
+ foreach ( $recurring_cart->get_shipping_packages() as $recurring_cart_package_index => $recurring_cart_package ) {
+ // phpcs:ignore
+ /**
+ * @psalm-suppress UndefinedClass
+ */
+ $package_key = WC_Subscriptions_Cart::get_recurring_shipping_package_key( $recurring_cart_key, $recurring_cart_package_index );
+
+ // If the recurring cart package key is found in the previous chosen methods, but not in the current chosen methods, restore it.
+ if ( isset( $previous_chosen_methods[ $package_key ] ) && ! isset( $chosen_shipping_methods[ $package_key ] ) ) {
+ $chosen_shipping_methods[ $package_key ] = $previous_chosen_methods[ $package_key ];
+ }
+ }
+ }
+
+ WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
+ }
+
+ /**
+ * Gets the product data for the currently viewed page.
+ *
+ * @return mixed Returns false if not on a product page, the product information otherwise.
+ */
+ public function get_product_data() {
+ if ( ! $this->is_product() ) {
+ return false;
+ }
+
+ /** @var WC_Product_Variable $product */ // phpcs:ignore
+ $product = $this->get_product();
+ $currency = get_woocommerce_currency();
+
+ if ( 'variable' === $product->get_type() || 'variable-subscription' === $product->get_type() ) {
+ $variation_attributes = $product->get_variation_attributes();
+ $attributes = [];
+
+ foreach ( $variation_attributes as $attribute_name => $attribute_values ) {
+ $attribute_key = 'attribute_' . sanitize_title( $attribute_name );
+
+ // Passed value via GET takes precedence. Otherwise get the default value for given attribute.
+ $attributes[ $attribute_key ] = isset( $_GET[ $attribute_key ] ) // phpcs:ignore WordPress.Security.NonceVerification
+ ? wc_clean( wp_unslash( $_GET[ $attribute_key ] ) ) // phpcs:ignore WordPress.Security.NonceVerification
+ : $product->get_variation_default_attribute( $attribute_name );
+ }
+
+ $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 );
+ }
+ }
+
+ try {
+ $price = $this->get_product_price( $product );
+ } catch ( Invalid_Price_Exception $e ) {
+ Logger::log( $e->getMessage() );
+ return false;
+ }
+
+ $data = [];
+ $items = [];
+
+ $items[] = [
+ 'label' => $product->get_name(),
+ 'amount' => WC_Payments_Utils::prepare_amount( $price, $currency ),
+ ];
+
+ $total_tax = 0;
+ foreach ( $this->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() && 0 !== wc_get_shipping_method_count( true ) && $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' => apply_filters( 'wcpay_payment_request_total_label', $this->get_total_label() ),
+ 'amount' => WC_Payments_Utils::prepare_amount( $price + $total_tax, $currency ),
+ 'pending' => true,
+ ];
+
+ $data['needs_shipping'] = ( wc_shipping_enabled() && 0 !== wc_get_shipping_method_count( true ) && $product->needs_shipping() );
+ $data['currency'] = strtolower( $currency );
+ $data['country_code'] = substr( get_option( 'woocommerce_default_country' ), 0, 2 );
+
+ return apply_filters( 'wcpay_payment_request_product_data', $data, $product );
+ }
+
+ /**
+ * Whether product page has a supported product.
+ *
+ * @return boolean
+ */
+ private function is_product_supported() {
+ $product = $this->get_product();
+ $is_supported = true;
+
+ /**
+ * Ignore undefined classes from 3rd party plugins.
+ *
+ * @psalm-suppress UndefinedClass
+ */
+ if ( is_null( $product )
+ || ! is_object( $product )
+ || ! in_array( $product->get_type(), $this->supported_product_types(), true )
+ || ( class_exists( 'WC_Subscriptions_Product' ) && $product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $product ) > 0 ) // Trial subscriptions with shipping are not supported.
+ || ( class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( $product ) ) // Pre Orders charge upon release not supported.
+ || ( class_exists( 'WC_Composite_Products' ) && $product->is_type( 'composite' ) ) // Composite products are not supported on the product page.
+ || ( class_exists( 'WC_Mix_and_Match' ) && $product->is_type( 'mix-and-match' ) ) // Mix and match products are not supported on the product page.
+ ) {
+ $is_supported = false;
+ } elseif ( class_exists( 'WC_Product_Addons_Helper' ) ) {
+ // File upload addon not supported.
+ $product_addons = WC_Product_Addons_Helper::get_product_addons( $product->get_id() );
+ foreach ( $product_addons as $addon ) {
+ if ( 'file_upload' === $addon['type'] ) {
+ $is_supported = false;
+ break;
+ }
+ }
+ }
+
+ return apply_filters( 'wcpay_payment_request_is_product_supported', $is_supported, $product );
+ }
+
+ /**
+ * Gets the product total price.
+ *
+ * @param object $product WC_Product_* object.
+ * @param bool $is_deposit Whether customer is paying a deposit.
+ * @param int $deposit_plan_id The ID of the deposit plan.
+ * @return mixed Total price.
+ *
+ * @throws Invalid_Price_Exception Whenever a product has no price.
+ *
+ * @psalm-suppress UndefinedClass
+ */
+ public function get_product_price( $product, ?bool $is_deposit = null, int $deposit_plan_id = 0 ) {
+ // If prices should include tax, using tax inclusive price.
+ if ( $this->cart_prices_include_tax() ) {
+ $base_price = wc_get_price_including_tax( $product );
+ } else {
+ $base_price = wc_get_price_excluding_tax( $product );
+ }
+
+ // If WooCommerce Deposits is active, we need to get the correct price for the product.
+ if ( class_exists( 'WC_Deposits_Product_Manager' ) && WC_Deposits_Product_Manager::deposits_enabled( $product->get_id() ) ) {
+ if ( is_null( $is_deposit ) ) {
+ /**
+ * If is_deposit is null, we use the default deposit type for the product.
+ *
+ * @psalm-suppress UndefinedClass
+ */
+ $is_deposit = 'deposit' === WC_Deposits_Product_Manager::get_deposit_selected_type( $product->get_id() );
+ }
+ if ( $is_deposit ) {
+ /**
+ * Ignore undefined classes from 3rd party plugins.
+ *
+ * @psalm-suppress UndefinedClass
+ */
+ $deposit_type = WC_Deposits_Product_Manager::get_deposit_type( $product->get_id() );
+ $available_plan_ids = WC_Deposits_Plans_Manager::get_plan_ids_for_product( $product->get_id() );
+ // Default to first (default) plan if no plan is specified.
+ if ( 'plan' === $deposit_type && 0 === $deposit_plan_id && ! empty( $available_plan_ids ) ) {
+ $deposit_plan_id = $available_plan_ids[0];
+ }
+
+ // Ensure the selected plan is available for the product.
+ if ( 0 === $deposit_plan_id || in_array( $deposit_plan_id, $available_plan_ids, true ) ) {
+ $base_price = WC_Deposits_Product_Manager::get_deposit_amount( $product, $deposit_plan_id, 'display', $base_price );
+ }
+ }
+ }
+
+ // Add subscription sign-up fees to product price.
+ $sign_up_fee = 0;
+ $subscription_types = [
+ 'subscription',
+ 'subscription_variation',
+ ];
+ if ( in_array( $product->get_type(), $subscription_types, true ) && class_exists( 'WC_Subscriptions_Product' ) ) {
+ // When there is no sign-up fee, `get_sign_up_fee` falls back to an int 0.
+ $sign_up_fee = WC_Subscriptions_Product::get_sign_up_fee( $product );
+ }
+
+ if ( ! is_numeric( $base_price ) || ! is_numeric( $sign_up_fee ) ) {
+ $error_message = sprintf(
+ // Translators: %d is the numeric ID of the product without a price.
+ __( 'Express checkout does not support products without prices! Please add a price to product #%d', 'woocommerce-payments' ),
+ (int) $product->get_id()
+ );
+ throw new Invalid_Price_Exception(
+ esc_html( $error_message )
+ );
+ }
+
+ return $base_price + $sign_up_fee;
+ }
+
+ /**
+ * Calculates taxes as displayed on cart, based on a product and a particular price.
+ *
+ * @param WC_Product $product The product, for retrieval of tax classes.
+ * @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 ) {
+ if ( ! wc_tax_enabled() || $this->cart_prices_include_tax() ) {
+ // Only proceed when taxes are enabled, but not included.
+ return [];
+ }
+
+ // Follows the way `WC_Cart_Totals::get_item_tax_rates()` works.
+ $tax_class = $product->get_tax_class();
+ $rates = WC_Tax::get_rates( $tax_class );
+ // No cart item, `woocommerce_cart_totals_get_item_tax_rates` can't be applied here.
+
+ // Normally there should be a single tax, but `calc_tax` returns an array, let's use it.
+ return WC_Tax::calc_tax( $price, $rates, false );
+ }
+
+ /**
+ * Gets the normalized state/county field because in some
+ * cases, the state/county field is formatted differently from
+ * what WC is expecting and throws an error. An example
+ * for Ireland, the county dropdown in Chrome shows "Co. Clare" format.
+ *
+ * @param string $state Full state name or an already normalized abbreviation.
+ * @param string $country Two-letter country code.
+ *
+ * @return string Normalized state abbreviation.
+ */
+ public function get_normalized_state( $state, $country ) {
+ // If it's empty or already normalized, skip.
+ if ( ! $state || $this->is_normalized_state( $state, $country ) ) {
+ return $state;
+ }
+
+ // Try to match state from the Payment Request API list of states.
+ $state = $this->get_normalized_state_from_pr_states( $state, $country );
+
+ // If it's normalized, return.
+ if ( $this->is_normalized_state( $state, $country ) ) {
+ return $state;
+ }
+
+ // If the above doesn't work, fallback to matching against the list of translated
+ // states from WooCommerce.
+ return $this->get_normalized_state_from_wc_states( $state, $country );
+ }
+
+ /**
+ * The Payment Request API provides its own validation for the address form.
+ * For some countries, it might not provide a state field, so we need to return a more descriptive
+ * error message, indicating that the Payment Request button is not supported for that country.
+ */
+ public static function validate_state() {
+ $wc_checkout = WC_Checkout::instance();
+ $posted_data = $wc_checkout->get_posted_data();
+ $checkout_fields = $wc_checkout->get_checkout_fields();
+ $countries = WC()->countries->get_countries();
+
+ $is_supported = true;
+ // Checks if billing state is missing and is required.
+ if ( ! empty( $checkout_fields['billing']['billing_state']['required'] ) && '' === $posted_data['billing_state'] ) {
+ $is_supported = false;
+ }
+
+ // Checks if shipping state is missing and is required.
+ if ( WC()->cart->needs_shipping_address() && ! empty( $checkout_fields['shipping']['shipping_state']['required'] ) && '' === $posted_data['shipping_state'] ) {
+ $is_supported = false;
+ }
+
+ if ( ! $is_supported ) {
+ wc_add_notice(
+ sprintf(
+ /* translators: %s: country. */
+ __( 'The payment request button is not supported in %s because some required fields couldn\'t be verified. Please proceed to the checkout page and try again.', 'woocommerce-payments' ),
+ $countries[ $posted_data['billing_country'] ] ?? $posted_data['billing_country']
+ ),
+ 'error'
+ );
+ }
+ }
+
+ /**
+ * Normalizes billing and shipping state fields.
+ */
+ public function normalize_state() {
+ check_ajax_referer( 'woocommerce-process_checkout', '_wpnonce' );
+
+ $billing_country = ! empty( $_POST['billing_country'] ) ? wc_clean( wp_unslash( $_POST['billing_country'] ) ) : '';
+ $shipping_country = ! empty( $_POST['shipping_country'] ) ? wc_clean( wp_unslash( $_POST['shipping_country'] ) ) : '';
+ $billing_state = ! empty( $_POST['billing_state'] ) ? wc_clean( wp_unslash( $_POST['billing_state'] ) ) : '';
+ $shipping_state = ! empty( $_POST['shipping_state'] ) ? wc_clean( wp_unslash( $_POST['shipping_state'] ) ) : '';
+
+ if ( $billing_state && $billing_country ) {
+ $_POST['billing_state'] = $this->get_normalized_state( $billing_state, $billing_country );
+ }
+
+ if ( $shipping_state && $shipping_country ) {
+ $_POST['shipping_state'] = $this->get_normalized_state( $shipping_state, $shipping_country );
+ }
+ }
+
+ /**
+ * Checks if given state is normalized.
+ *
+ * @param string $state State.
+ * @param string $country Two-letter country code.
+ *
+ * @return bool Whether state is normalized or not.
+ */
+ public function is_normalized_state( $state, $country ) {
+ $wc_states = WC()->countries->get_states( $country );
+ return is_array( $wc_states ) && array_key_exists( $state, $wc_states );
+ }
+
+ /**
+ * Get normalized state from Payment Request API dropdown list of states.
+ *
+ * @param string $state Full state name or state code.
+ * @param string $country Two-letter country code.
+ *
+ * @return string Normalized state or original state input value.
+ */
+ public function get_normalized_state_from_pr_states( $state, $country ) {
+ // Include Payment Request API State list for compatibility with WC countries/states.
+ include_once WCPAY_ABSPATH . 'includes/constants/class-payment-request-button-states.php';
+ $pr_states = \WCPay\Constants\Payment_Request_Button_States::STATES;
+
+ if ( ! isset( $pr_states[ $country ] ) ) {
+ return $state;
+ }
+
+ foreach ( $pr_states[ $country ] as $wc_state_abbr => $pr_state ) {
+ $sanitized_state_string = $this->sanitize_string( $state );
+ // Checks if input state matches with Payment Request state code (0), name (1) or localName (2).
+ if (
+ ( ! empty( $pr_state[0] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[0] ) ) ||
+ ( ! empty( $pr_state[1] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[1] ) ) ||
+ ( ! empty( $pr_state[2] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[2] ) )
+ ) {
+ return $wc_state_abbr;
+ }
+ }
+
+ return $state;
+ }
+
+ /**
+ * Get normalized state from WooCommerce list of translated states.
+ *
+ * @param string $state Full state name or state code.
+ * @param string $country Two-letter country code.
+ *
+ * @return string Normalized state or original state input value.
+ */
+ public function get_normalized_state_from_wc_states( $state, $country ) {
+ $wc_states = WC()->countries->get_states( $country );
+
+ if ( is_array( $wc_states ) ) {
+ foreach ( $wc_states as $wc_state_abbr => $wc_state_value ) {
+ if ( preg_match( '/' . preg_quote( $wc_state_value, '/' ) . '/i', $state ) ) {
+ return $wc_state_abbr;
+ }
+ }
+ }
+
+ return $state;
+ }
+
+ /**
+ * Normalizes postal code in case of redacted data from Apple Pay.
+ *
+ * @param string $postcode Postal code.
+ * @param string $country Country.
+ */
+ public function get_normalized_postal_code( $postcode, $country ) {
+ /**
+ * Currently, Apple Pay truncates the UK and Canadian postal codes to the first 4 and 3 characters respectively
+ * when passing it back from the shippingcontactselected object. This causes WC to invalidate
+ * the postal code and not calculate shipping zones correctly.
+ */
+ if ( Country_Code::UNITED_KINGDOM === $country ) {
+ // Replaces a redacted string with something like N1C0000.
+ return str_pad( preg_replace( '/\s+/', '', $postcode ), 7, '0' );
+ }
+ if ( Country_Code::CANADA === $country ) {
+ // Replaces a redacted string with something like H3B000.
+ return str_pad( preg_replace( '/\s+/', '', $postcode ), 6, '0' );
+ }
+
+ return $postcode;
+ }
+
+ /**
+ * Sanitize string for comparison.
+ *
+ * @param string $string String to be sanitized.
+ *
+ * @return string The sanitized string.
+ */
+ public function sanitize_string( $string ) {
+ return trim( wc_strtolower( remove_accents( $string ) ) );
+ }
+
+ /**
+ * Updates shipping method in WC session
+ *
+ * @param array $shipping_methods Array of selected shipping methods ids.
+ */
+ public function update_shipping_method( $shipping_methods ) {
+ $chosen_shipping_methods = (array) WC()->session->get( 'chosen_shipping_methods' );
+
+ if ( is_array( $shipping_methods ) ) {
+ foreach ( $shipping_methods as $i => $value ) {
+ $chosen_shipping_methods[ $i ] = wc_clean( $value );
+ }
+ }
+
+ WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
+ }
+
+ /**
+ * Calculate and set shipping method.
+ *
+ * @param array $address Shipping address.
+ */
+ protected function calculate_shipping( $address = [] ) {
+ $country = $address['country'];
+ $state = $address['state'];
+ $postcode = $address['postcode'];
+ $city = $address['city'];
+ $address_1 = $address['address_1'];
+ $address_2 = $address['address_2'];
+
+ // Normalizes state to calculate shipping zones.
+ $state = $this->get_normalized_state( $state, $country );
+
+ // Normalizes postal code in case of redacted data from Apple Pay.
+ $postcode = $this->get_normalized_postal_code( $postcode, $country );
+
+ WC()->shipping->reset_shipping();
+
+ if ( $postcode && WC_Validation::is_postcode( $postcode, $country ) ) {
+ $postcode = wc_format_postcode( $postcode, $country );
+ }
+
+ if ( $country ) {
+ WC()->customer->set_location( $country, $state, $postcode, $city );
+ WC()->customer->set_shipping_location( $country, $state, $postcode, $city );
+ } else {
+ WC()->customer->set_billing_address_to_base();
+ WC()->customer->set_shipping_address_to_base();
+ }
+
+ WC()->customer->set_calculated_shipping( true );
+ WC()->customer->save();
+
+ $packages = [];
+
+ $packages[0]['contents'] = WC()->cart->get_cart();
+ $packages[0]['contents_cost'] = 0;
+ $packages[0]['applied_coupons'] = WC()->cart->applied_coupons;
+ $packages[0]['user']['ID'] = get_current_user_id();
+ $packages[0]['destination']['country'] = $country;
+ $packages[0]['destination']['state'] = $state;
+ $packages[0]['destination']['postcode'] = $postcode;
+ $packages[0]['destination']['city'] = $city;
+ $packages[0]['destination']['address'] = $address_1;
+ $packages[0]['destination']['address_2'] = $address_2;
+
+ foreach ( WC()->cart->get_cart() as $item ) {
+ if ( $item['data']->needs_shipping() ) {
+ if ( isset( $item['line_total'] ) ) {
+ $packages[0]['contents_cost'] += $item['line_total'];
+ }
+ }
+ }
+
+ $packages = apply_filters( 'woocommerce_cart_shipping_packages', $packages );
+
+ WC()->shipping->calculate_shipping( $packages );
+ }
}
diff --git a/includes/woopay/services/class-checkout-service.php b/includes/woopay/services/class-checkout-service.php
index f10ca7bad7b..b776d505f27 100644
--- a/includes/woopay/services/class-checkout-service.php
+++ b/includes/woopay/services/class-checkout-service.php
@@ -64,7 +64,7 @@ public function create_and_confirm_setup_intention_request( Request $base_reques
*/
public function is_platform_payment_method( Payment_Information $payment_information ) {
// Return false for express checkout method.
- if ( isset( $_POST['payment_request_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
+ if ( isset( $_POST['payment_request_type'] ) || isset( $_POST['express_payment_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
return false;
}
diff --git a/src/Internal/Payment/Factor.php b/src/Internal/Payment/Factor.php
index 3a63e895738..957c131fd7a 100644
--- a/src/Internal/Payment/Factor.php
+++ b/src/Internal/Payment/Factor.php
@@ -100,6 +100,12 @@ class Factor extends Base_Constant {
*/
const PAYMENT_REQUEST = 'PAYMENT_REQUEST';
+ /**
+ * ECE buttons (Google Pay and Apple Pay)
+ * Type: Entry point
+ */
+ const EXPRESS_CHECKOUT_ELEMENT = 'EXPRESS_CHECKOUT_ELEMENT';
+
/**
* Returns all possible factors.
*
@@ -121,6 +127,7 @@ public static function get_all_factors() {
static::IPP_CAPTURE(),
static::STRIPE_LINK(),
static::PAYMENT_REQUEST(),
+ static::EXPRESS_CHECKOUT_ELEMENT(),
];
}
}
diff --git a/tests/unit/src/Internal/Payment/FactorTest.php b/tests/unit/src/Internal/Payment/FactorTest.php
index e1e1e14ba71..c570d6dcfe0 100644
--- a/tests/unit/src/Internal/Payment/FactorTest.php
+++ b/tests/unit/src/Internal/Payment/FactorTest.php
@@ -36,6 +36,7 @@ public function test_get_all_factors() {
'IPP_CAPTURE',
'STRIPE_LINK',
'PAYMENT_REQUEST',
+ 'EXPRESS_CHECKOUT_ELEMENT',
];
$result = Factor::get_all_factors();
diff --git a/tests/unit/test-class-wc-payments-express-checkout-button-display-handler.php b/tests/unit/test-class-wc-payments-express-checkout-button-display-handler.php
index 40329be06fd..2fc3948b716 100644
--- a/tests/unit/test-class-wc-payments-express-checkout-button-display-handler.php
+++ b/tests/unit/test-class-wc-payments-express-checkout-button-display-handler.php
@@ -79,6 +79,13 @@ class WC_Payments_Express_Checkout_Button_Display_Handler_Test extends WCPAY_Uni
*/
private $mock_express_checkout_helper;
+ /**
+ * Express Checkout Ajax Handler instance.
+ *
+ * @var WC_Payments_Express_Checkout_Ajax_Handler
+ */
+ private $mock_express_checkout_ajax_handler;
+
/**
* Sets up things all tests need.
*/
@@ -117,6 +124,14 @@ public function set_up() {
)
->getMock();
+ $this->mock_express_checkout_ajax_handler = $this->getMockBuilder( WC_Payments_Express_Checkout_Ajax_Handler::class )
+ ->setConstructorArgs(
+ [
+ $this->mock_express_checkout_helper,
+ ]
+ )
+ ->getMock();
+
$this->mock_woopay_button_handler = $this->getMockBuilder( WC_Payments_WooPay_Button_Handler::class )
->setConstructorArgs(
[
@@ -156,6 +171,7 @@ public function set_up() {
$this->mock_wcpay_account,
$this->mock_wcpay_gateway,
$this->mock_express_checkout_helper,
+ $this->mock_express_checkout_ajax_handler,
]
)
->setMethods(
@@ -165,14 +181,15 @@ public function set_up() {
)
->getMock();
- $this->express_checkout_button_display_handler = new WC_Payments_Express_Checkout_Button_Display_Handler(
- $this->mock_wcpay_gateway,
- $this->mock_payment_request_button_handler,
- $this->mock_woopay_button_handler,
- $this->mock_express_checkout_ece_button_handler,
- $this->mock_express_checkout_helper
- );
- $this->express_checkout_button_display_handler->init();
+ $this->express_checkout_button_display_handler = new WC_Payments_Express_Checkout_Button_Display_Handler(
+ $this->mock_wcpay_gateway,
+ $this->mock_payment_request_button_handler,
+ $this->mock_woopay_button_handler,
+ $this->mock_express_checkout_ece_button_handler,
+ $this->mock_express_checkout_ajax_handler,
+ $this->mock_express_checkout_helper
+ );
+ $this->express_checkout_button_display_handler->init();
add_filter(
'woocommerce_available_payment_gateways',
From 200a92e1eff7298e0651b0580cdcf2ceceae9b78 Mon Sep 17 00:00:00 2001
From: Timur Karimov
Date: Mon, 3 Jun 2024 22:31:26 +0200
Subject: [PATCH 09/52] Fix QIT errors reported by PHPStan (#8845)
---
changelog/fix-phpstan-errors | 4 ++
includes/class-wc-payments-account.php | 12 +----
...ayments-payment-request-button-handler.php | 2 +-
includes/class-wc-payments.php | 52 +++++++++----------
.../blocks/class-blocks-data-extractor.php | 4 ++
...ents-email-failed-authentication-retry.php | 11 +++-
...wc-payment-gateway-wcpay-subscriptions.php | 12 ++++-
includes/constants/class-base-constant.php | 2 +
...ayments-express-checkout-button-helper.php | 2 +-
.../WooCommerceNameYourPrice.php | 5 +-
.../WooCommerceProductAddOns.php | 20 ++++---
.../class-woopay-adapted-extensions.php | 23 ++++----
.../woopay/class-woopay-store-api-token.php | 1 +
.../Payment/State/AbstractPaymentState.php | 10 ++--
src/Internal/Payment/State/StateFactory.php | 10 ++--
tests/qit/README.md | 11 ++--
16 files changed, 108 insertions(+), 73 deletions(-)
create mode 100644 changelog/fix-phpstan-errors
diff --git a/changelog/fix-phpstan-errors b/changelog/fix-phpstan-errors
new file mode 100644
index 00000000000..173e71e8d88
--- /dev/null
+++ b/changelog/fix-phpstan-errors
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Fix PHPStan warnings.
diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php
index b3106527a2d..79b11b6646b 100644
--- a/includes/class-wc-payments-account.php
+++ b/includes/class-wc-payments-account.php
@@ -1675,7 +1675,7 @@ function () {
*
* @return void
*/
- public function update_cached_account_data( $property, $data ) {
+ public function update_account_data( $property, $data ) {
$account_data = $this->database_cache->get( Database_Cache::ACCOUNT_KEY );
$account_data[ $property ] = is_array( $data ) ? array_merge( $account_data[ $property ] ?? [], $data ) : $data;
@@ -1692,16 +1692,6 @@ public function refresh_account_data() {
return $this->get_cached_account_data( true );
}
- /**
- * Updates the account data.
- *
- * @param string $property Property to update.
- * @param mixed $data Data to update.
- */
- public function update_account_data( $property, $data ) {
- return $this->update_cached_account_data( $property, $data );
- }
-
/**
* Checks if the cached account can be used in the current plugin state.
*
diff --git a/includes/class-wc-payments-payment-request-button-handler.php b/includes/class-wc-payments-payment-request-button-handler.php
index fd7de809e9a..04a0fbdb83f 100644
--- a/includes/class-wc-payments-payment-request-button-handler.php
+++ b/includes/class-wc-payments-payment-request-button-handler.php
@@ -408,7 +408,7 @@ public function get_product_price( $product, ?bool $is_deposit = null, int $depo
}
// If WooCommerce Deposits is active, we need to get the correct price for the product.
- if ( class_exists( 'WC_Deposits_Product_Manager' ) && WC_Deposits_Product_Manager::deposits_enabled( $product->get_id() ) ) {
+ if ( class_exists( 'WC_Deposits_Product_Manager' ) && class_exists( 'WC_Deposits_Plans_Manager' ) && WC_Deposits_Product_Manager::deposits_enabled( $product->get_id() ) ) {
// If is_deposit is null, we use the default deposit type for the product.
if ( is_null( $is_deposit ) ) {
$is_deposit = 'deposit' === WC_Deposits_Product_Manager::get_deposit_selected_type( $product->get_id() );
diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php
index d4716873ccf..04b2e517c1f 100644
--- a/includes/class-wc-payments.php
+++ b/includes/class-wc-payments.php
@@ -1437,37 +1437,37 @@ public static function add_woo_admin_notes() {
}
if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '7.5', '<' ) && get_woocommerce_currency() === 'NOK' ) {
- /**
- * Shows an alert notice for Norwegian merchants on WooCommerce 7.4 and below
- */
- function wcpay_show_old_woocommerce_for_norway_notice() {
- ?>
-
-
- the plugins page.', 'woocommerce-payments' ),
- 'WooCommerce',
- 'WooPayments'
- ),
- [
- 'a1' => '',
- ]
- )
- ?>
-
-
-
+
+
+ the plugins page.', 'woocommerce-payments' ),
+ 'WooCommerce',
+ 'WooPayments'
+ ),
+ [
+ 'a1' => '',
+ ]
+ )
+ ?>
+
+
+ get( \MailPoet\WooCommerce\Subscription::class );
// phpcs:ignore
/**
* @psalm-suppress UndefinedClass
+ * @phpstan-ignore-next-line
*/
$settings_instance = \MailPoet\Settings\SettingsController::getInstance();
$settings = [
diff --git a/includes/compat/subscriptions/class-wc-payments-email-failed-authentication-retry.php b/includes/compat/subscriptions/class-wc-payments-email-failed-authentication-retry.php
index fb59585534f..7d4838bfceb 100644
--- a/includes/compat/subscriptions/class-wc-payments-email-failed-authentication-retry.php
+++ b/includes/compat/subscriptions/class-wc-payments-email-failed-authentication-retry.php
@@ -9,6 +9,8 @@
* @package WooCommerce\Payments
*/
+use WCPay\Logger;
+
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
@@ -18,6 +20,13 @@
*/
class WC_Payments_Email_Failed_Authentication_Retry extends WC_Email_Failed_Order {
+ /**
+ * The details of the last retry (if any) recorded for a given order
+ *
+ * @var WCS_Retry
+ */
+ private $retry;
+
/**
* Constructor
*/
@@ -71,7 +80,7 @@ public function trigger( $order_id, $order = null ) {
$this->retry = WCS_Retry_Manager::store()->get_last_retry_for_order( wcs_get_objects_property( $order, 'id' ) );
$this->replace['retry-time'] = wcs_get_human_time_diff( $this->retry->get_time() );
} else {
- WC_Stripe_Logger::log( 'WCS_Retry_Manager class or does not exist. Not able to send admin email about customer notification for authentication required for renewal payment.' );
+ Logger::log( 'WCS_Retry_Manager class or does not exist. Not able to send admin email about customer notification for authentication required for renewal payment.' );
return;
}
diff --git a/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php b/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php
index 09c1ac59c86..da7a1379c14 100644
--- a/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php
+++ b/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php
@@ -103,6 +103,14 @@ abstract protected function prepare_payment_information( $order );
*/
private static $has_attached_integration_hooks = false;
+ /**
+ * Used to temporary keep the state of the order_pay value on the Pay for order page with the SCA authorization flow.
+ * For more details, see remove_order_pay_var and restore_order_pay_var hooks.
+ *
+ * @var string|int
+ */
+ private $order_pay_var;
+
/**
* Initialize subscription support and hooks.
*/
@@ -941,7 +949,9 @@ public function get_mandate_params_for_order( WC_Order $order ): array {
if ( 1 < count( $subscriptions ) ) {
$result['card']['mandate_options']['amount_type'] = 'maximum';
$result['card']['mandate_options']['interval'] = 'sporadic';
- unset( $result['card']['mandate_options']['interval_count'] );
+ if ( isset( $result['card']['mandate_options']['interval_count'] ) ) {
+ unset( $result['card']['mandate_options']['interval_count'] );
+ }
}
return $result;
diff --git a/includes/constants/class-base-constant.php b/includes/constants/class-base-constant.php
index be1a5770922..876150b6581 100644
--- a/includes/constants/class-base-constant.php
+++ b/includes/constants/class-base-constant.php
@@ -98,6 +98,8 @@ public static function search( string $value ) {
*/
public static function __callStatic( $name, $arguments ) {
if ( ! isset( static::$object_cache[ $name ] ) ) {
+ // Instantiating constants by class name using the 'new static($name)' approach is integral to this method's functionality.
+ // @phpstan-ignore-next-line.
static::$object_cache[ $name ] = new static( $name );
}
return static::$object_cache[ $name ];
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 266e69e6f6d..401dc47a389 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
@@ -750,7 +750,7 @@ public function get_product_price( $product, ?bool $is_deposit = null, int $depo
}
// If WooCommerce Deposits is active, we need to get the correct price for the product.
- if ( class_exists( 'WC_Deposits_Product_Manager' ) && WC_Deposits_Product_Manager::deposits_enabled( $product->get_id() ) ) {
+ if ( class_exists( 'WC_Deposits_Product_Manager' ) && class_exists( 'WC_Deposits_Plans_Manager' ) && WC_Deposits_Product_Manager::deposits_enabled( $product->get_id() ) ) {
if ( is_null( $is_deposit ) ) {
/**
* If is_deposit is null, we use the default deposit type for the product.
diff --git a/includes/multi-currency/Compatibility/WooCommerceNameYourPrice.php b/includes/multi-currency/Compatibility/WooCommerceNameYourPrice.php
index 0095bc70559..155f99e1a4d 100644
--- a/includes/multi-currency/Compatibility/WooCommerceNameYourPrice.php
+++ b/includes/multi-currency/Compatibility/WooCommerceNameYourPrice.php
@@ -63,7 +63,7 @@ public function add_initial_currency( $cart_item, $product_id, $variation_id ) {
$nyp_id = $variation_id ? $variation_id : $product_id;
- if ( \WC_Name_Your_Price_Helpers::is_nyp( $nyp_id ) && isset( $cart_item['nyp'] ) ) {
+ if ( class_exists( '\WC_Name_Your_Price_Helpers' ) && \WC_Name_Your_Price_Helpers::is_nyp( $nyp_id ) && isset( $cart_item['nyp'] ) ) {
$currency = $this->multi_currency->get_selected_currency();
$cart_item['nyp_currency'] = $currency->get_code();
$cart_item['nyp_original'] = $cart_item['nyp'];
@@ -102,6 +102,7 @@ public function convert_cart_currency( $cart_item, $values ) {
$cart_item['nyp'] = $this->multi_currency->get_raw_conversion( $raw_price, $selected_currency->get_code(), $from_currency );
}
+ // @phpstan-ignore-next-line.
$cart_item = WC_Name_Your_Price()->cart->set_cart_item( $cart_item );
}
@@ -130,7 +131,7 @@ public function should_convert_product_price( bool $return, $product ): bool {
}
// Check to see if the product is a NYP product.
- if ( \WC_Name_Your_Price_Helpers::is_nyp( $product ) ) {
+ if ( class_exists( '\WC_Name_Your_Price_Helpers' ) && \WC_Name_Your_Price_Helpers::is_nyp( $product ) ) {
return false;
}
diff --git a/includes/multi-currency/Compatibility/WooCommerceProductAddOns.php b/includes/multi-currency/Compatibility/WooCommerceProductAddOns.php
index f3e04db64fc..adc5cb9dabb 100644
--- a/includes/multi-currency/Compatibility/WooCommerceProductAddOns.php
+++ b/includes/multi-currency/Compatibility/WooCommerceProductAddOns.php
@@ -125,8 +125,10 @@ public function get_item_data( $addon_data, $addon, $cart_item ): array {
// Quantity/multiplier add on needs to be split, calculated, then multiplied by input value.
$price = $this->multi_currency->get_price( $addon['price'] / $addon['value'], 'product' ) * $addon['value'];
}
- $price = \WC_Product_Addons_Helper::get_product_addon_price_for_display( $price, $cart_item['data'] );
- $name .= ' (' . wc_price( $price ) . ')';
+ if ( class_exists( '\WC_Product_Addons_Helper' ) ) {
+ $price = \WC_Product_Addons_Helper::get_product_addon_price_for_display( $price, $cart_item['data'] );
+ $name .= ' (' . wc_price( $price ) . ')';
+ }
} else {
// Get the percentage cost in the currency in use, and set the meta data on the product that the value was converted.
$_product = wc_get_product( $cart_item['product_id'] );
@@ -245,12 +247,14 @@ public function order_line_item_meta( array $meta_data, array $addon, \WC_Order_
// Convert all others.
$addon_price = $this->multi_currency->get_price( $addon['price'], 'product' );
}
- $price = html_entity_decode(
- wp_strip_all_tags( wc_price( \WC_Product_Addons_Helper::get_product_addon_price_for_display( $addon_price, $values['data'] ) ) ),
- ENT_QUOTES,
- get_bloginfo( 'charset' )
- );
- $addon['name'] .= ' (' . $price . ')';
+ if ( class_exists( '\WC_Product_Addons_Helper' ) ) {
+ $price = html_entity_decode(
+ wp_strip_all_tags( wc_price( \WC_Product_Addons_Helper::get_product_addon_price_for_display( $addon_price, $values['data'] ) ) ),
+ ENT_QUOTES,
+ get_bloginfo( 'charset' )
+ );
+ $addon['name'] .= ' (' . $price . ')';
+ }
}
if ( 'custom_price' === $addon['field_type'] ) {
diff --git a/includes/woopay/class-woopay-adapted-extensions.php b/includes/woopay/class-woopay-adapted-extensions.php
index 0180525f726..5b9c5c8643e 100644
--- a/includes/woopay/class-woopay-adapted-extensions.php
+++ b/includes/woopay/class-woopay-adapted-extensions.php
@@ -171,7 +171,7 @@ public function get_extension_data() {
];
}
- if ( $this->is_affiliate_for_woocommerce_enabled() ) {
+ if ( $this->is_affiliate_for_woocommerce_enabled() && function_exists( 'afwc_get_referrer_id' ) ) {
/**
* Suppress psalm warning.
*
@@ -207,12 +207,14 @@ public function update_order_extension_data( $order_id ) {
) {
$affiliate_id = (int) wc_clean( wp_unslash( $_GET['affiliate'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
- // phpcs:ignore
- /**
- * @psalm-suppress UndefinedClass
- */
- $affiliate_api = \AFWC_API::get_instance();
- $affiliate_api->track_conversion( $order_id, $affiliate_id, '', [ 'is_affiliate_eligible' => true ] );
+ if ( class_exists( '\AFWC_API' ) ) {
+ // phpcs:ignore
+ /**
+ * @psalm-suppress UndefinedClass
+ */
+ $affiliate_api = \AFWC_API::get_instance();
+ $affiliate_api->track_conversion( $order_id, $affiliate_id, '', [ 'is_affiliate_eligible' => true ] );
+ }
}
}
@@ -272,7 +274,10 @@ class_exists( '\AutomateWoo\Referrals\Referral_Manager' ) &&
* @return string|null
*/
private function get_automate_woo_advocate_id_from_cookie() {
- $advocate_from_key_cookie = \AutomateWoo\Referrals\Referral_Manager::get_advocate_key_from_cookie();
- return $advocate_from_key_cookie ? $advocate_from_key_cookie->get_advocate_id() : null;
+ if ( class_exists( '\AutomateWoo\Referrals\Referral_Manager' ) ) {
+ $advocate_from_key_cookie = \AutomateWoo\Referrals\Referral_Manager::get_advocate_key_from_cookie();
+ return $advocate_from_key_cookie ? $advocate_from_key_cookie->get_advocate_id() : null;
+ }
+ return null;
}
}
diff --git a/includes/woopay/class-woopay-store-api-token.php b/includes/woopay/class-woopay-store-api-token.php
index 0601b410350..b8dfb72a433 100644
--- a/includes/woopay/class-woopay-store-api-token.php
+++ b/includes/woopay/class-woopay-store-api-token.php
@@ -55,6 +55,7 @@ public function get_args() {
* @psalm-suppress UndefinedMethod
*/
public function get_cart_token() {
+ // @phpstan-ignore-next-line.
return parent::get_cart_token();
}
}
diff --git a/src/Internal/Payment/State/AbstractPaymentState.php b/src/Internal/Payment/State/AbstractPaymentState.php
index 19649b7eadb..942fec96ca2 100644
--- a/src/Internal/Payment/State/AbstractPaymentState.php
+++ b/src/Internal/Payment/State/AbstractPaymentState.php
@@ -79,6 +79,7 @@ public function get_context(): PaymentContext {
* @throws PaymentRequestException When data is not available or invalid.
*/
public function start_processing( PaymentRequest $request ) {
+ // @phpstan-ignore-next-line
$this->throw_unavailable_method_exception( __METHOD__ );
}
@@ -92,6 +93,7 @@ public function start_processing( PaymentRequest $request ) {
* @throws StateTransitionException
*/
public function complete_processing() {
+ // @phpstan-ignore-next-line
$this->throw_unavailable_method_exception( __METHOD__ );
}
// phpcs:enable Squiz.Commenting.FunctionComment.InvalidNoReturn
@@ -103,15 +105,15 @@ public function complete_processing() {
* This method should only be called whenever the process is ready to transition
* to the next state, as each new state will be considered the payment's latest one.
*
- * @template ConcreteState
- * @param class-string | string $state_class The class of the state to crate.
+ * @template ConcreteState of AbstractPaymentState
+ * @param class-string $state_class The class of the state to create.
*
- * @return AbstractPaymentState | ConcreteState
+ * @return ConcreteState The generated payment state instance.
*
* @throws StateTransitionException In case the new state could not be created.
* @throws ContainerException When the dependency container cannot instantiate the state.
*/
- protected function create_state( string $state_class ) {
+ protected function create_state( /*class-string*/ $state_class ): AbstractPaymentState {
$state = $this->state_factory->create_state( $state_class, $this->context );
// This is where logging will be added.
diff --git a/src/Internal/Payment/State/StateFactory.php b/src/Internal/Payment/State/StateFactory.php
index fa9554f4e73..7d0b77c375c 100644
--- a/src/Internal/Payment/State/StateFactory.php
+++ b/src/Internal/Payment/State/StateFactory.php
@@ -38,15 +38,15 @@ public function __construct( Container $container ) {
/**
* Creates a new state based on class name.
*
- * @template ConcreteState
- * @param class-string | string $state_class Name of the state class.
- * @param PaymentContext $context Context for the new state.
+ * @template ConcreteState of AbstractPaymentState
+ * @param class-string $state_class Name of the state class.
+ * @param PaymentContext $context Context for the new state.
*
- * @return AbstractPaymentState | ConcreteState The generated payment state instance.
+ * @return ConcreteState The generated payment state instance.
* @throws ContainerException When the dependency container cannot instantiate the state.
* @throws StateTransitionException When the class name is not a state.
*/
- public function create_state( string $state_class, PaymentContext $context ): AbstractPaymentState {
+ public function create_state( /*class-string*/ $state_class, PaymentContext $context ): AbstractPaymentState {
if ( ! is_subclass_of( $state_class, AbstractPaymentState::class ) ) {
throw new StateTransitionException(
esc_html(
diff --git a/tests/qit/README.md b/tests/qit/README.md
index a1d2e951709..896cc9b0ab9 100644
--- a/tests/qit/README.md
+++ b/tests/qit/README.md
@@ -1,18 +1,21 @@
## WooCommerce Payments QIT tests
-We currently only use the security tests from the [QIT toolkit](https://woocommerce.github.io/qit-documentation/#/) and these can be run locally.
+We currently only use the security tests from the [QIT toolkit](https://qit.woo.com/docs/) and these can be run locally.
#### Setup and running
- Create `local.env` inside the `tests/qit/config/` directory by copying the variables from `default.env`.
- To get the actual values for local config, refer to this [secret store](https://mc.a8c.com/secret-store/?secret_id=11043) link.
- Once configured, the first time you run the `npm` command, it should create a local auth file which will be used for subsequent runs.
-- For running, use:
+- Currently, two types of tests are available through the `npm` command: Security and PHPStan tests. PHPStan tests can also be run against the local development build.
+- For running, use one of the following commands based on your requirements:
```
- npm run test:qit
+ npm run test:qit-security
+ npm run test:qit-phpstan
+ npm run test:qit-phpstan-local
```
-- The command uses the `build:release` command to create `woocommerce-payments.zip` at the root of the directory which is then uploaded and used for the QIT tests.
+- The commands use the `build:release` to create `woocommerce-payments.zip` at the root of the directory which is then uploaded and used for the QIT tests.
#### Analysing results
From 2f30d7e47983bb672ef8e9e32ca76c168d000b7d Mon Sep 17 00:00:00 2001
From: Niels Lange
Date: Tue, 4 Jun 2024 10:41:17 +0700
Subject: [PATCH 10/52] Declare compatibility with the Cart and Checkout blocks
(#8890)
---
...update-declare-compatibility-width-cart-and-checkout-block | 4 ++++
woocommerce-payments.php | 3 ++-
2 files changed, 6 insertions(+), 1 deletion(-)
create mode 100644 changelog/update-declare-compatibility-width-cart-and-checkout-block
diff --git a/changelog/update-declare-compatibility-width-cart-and-checkout-block b/changelog/update-declare-compatibility-width-cart-and-checkout-block
new file mode 100644
index 00000000000..dff8698007d
--- /dev/null
+++ b/changelog/update-declare-compatibility-width-cart-and-checkout-block
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Declare compatibility with the Cart and Checkout blocks.
diff --git a/woocommerce-payments.php b/woocommerce-payments.php
index cdb84deb13a..46546f65539 100644
--- a/woocommerce-payments.php
+++ b/woocommerce-payments.php
@@ -423,7 +423,8 @@ function register_woopay_extension() {
'before_woocommerce_init',
function () {
if ( class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) ) {
- \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', 'woocommerce-payments/woocommerce-payments.php', true );
+ \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'cart_checkout_blocks', __FILE__, true );
+ \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
}
}
);
From 0fac547f86913073d1eadf39880cd47ef719c5a7 Mon Sep 17 00:00:00 2001
From: Eric Jinks <3147296+Jinksi@users.noreply.github.com>
Date: Tue, 4 Jun 2024 15:30:30 +1000
Subject: [PATCH 11/52] Skip flaky playwright e2e test: `payment gateways
disable confirmation` (#8899)
---
...able-flaky-e2e-test-payment-gateways-disable-confirmation | 5 +++++
.../merchant/merchant-payment-gateways-confirmation.spec.ts | 3 ++-
2 files changed, 7 insertions(+), 1 deletion(-)
create mode 100644 changelog/dev-8875-disable-flaky-e2e-test-payment-gateways-disable-confirmation
diff --git a/changelog/dev-8875-disable-flaky-e2e-test-payment-gateways-disable-confirmation b/changelog/dev-8875-disable-flaky-e2e-test-payment-gateways-disable-confirmation
new file mode 100644
index 00000000000..7d97fa86fc3
--- /dev/null
+++ b/changelog/dev-8875-disable-flaky-e2e-test-payment-gateways-disable-confirmation
@@ -0,0 +1,5 @@
+Significance: patch
+Type: dev
+Comment: Not user-facing: disable flaky playwright e2e test 'payment gateways disable confirmation'
+
+
diff --git a/tests/e2e-pw/specs/merchant/merchant-payment-gateways-confirmation.spec.ts b/tests/e2e-pw/specs/merchant/merchant-payment-gateways-confirmation.spec.ts
index 6197cfa5fcb..f4981c22798 100644
--- a/tests/e2e-pw/specs/merchant/merchant-payment-gateways-confirmation.spec.ts
+++ b/tests/e2e-pw/specs/merchant/merchant-payment-gateways-confirmation.spec.ts
@@ -8,7 +8,8 @@ import { test, expect, Page } from '@playwright/test';
*/
import { useMerchant } from '../../utils/helpers';
-test.describe( 'payment gateways disable confirmation', () => {
+// Skipping the test for now as it is flaky on GH action runs. See #8875.
+test.skip( 'payment gateways disable confirmation', () => {
useMerchant();
const getToggle = ( page: Page ) =>
From b73ef0649a6b98c68607de329567ef7cb0a856cb Mon Sep 17 00:00:00 2001
From: Chi-Hsuan Huang
Date: Tue, 4 Jun 2024 16:24:12 +0800
Subject: [PATCH 12/52] Improve the transition from the WCPay KYC to the WC
Admin Payments Task (#8893)
---
.../update-redirect-to-payment-screen-for-wcadmin-task | 4 ++++
includes/class-wc-payments-account.php | 10 ++++++----
2 files changed, 10 insertions(+), 4 deletions(-)
create mode 100644 changelog/update-redirect-to-payment-screen-for-wcadmin-task
diff --git a/changelog/update-redirect-to-payment-screen-for-wcadmin-task b/changelog/update-redirect-to-payment-screen-for-wcadmin-task
new file mode 100644
index 00000000000..f9c4a0dbe11
--- /dev/null
+++ b/changelog/update-redirect-to-payment-screen-for-wcadmin-task
@@ -0,0 +1,4 @@
+Significance: patch
+Type: update
+
+Improve the transition from the WCPay KYC to the WC Admin Payments Task
diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php
index 79b11b6646b..4b4a2fec01c 100644
--- a/includes/class-wc-payments-account.php
+++ b/includes/class-wc-payments-account.php
@@ -1235,9 +1235,13 @@ public static function get_connect_url( $wcpay_connect_from = '1' ) {
/**
* Payments task page url
*
+ * @deprecated 7.8.0
+ *
* @return string payments task page url
*/
public static function get_payments_task_page_url() {
+ wc_deprecated_function( __FUNCTION__, '7.8.0' );
+
return add_query_arg(
[
'page' => 'wc-admin',
@@ -1396,11 +1400,9 @@ private function get_onboarding_return_url( $wcpay_connect_from ) {
);
}
- // If connection originated on the WCADMIN payment task page, return there.
- // else goto the overview page, since now it is GA (earlier it was redirected to plugin settings page).
+ // Custom return URL for the connect page based on the source.
+ // Default goto the overview page, since now it is GA (earlier it was redirected to plugin settings page).
switch ( $wcpay_connect_from ) {
- case 'WCADMIN_PAYMENT_TASK':
- return static::get_payments_task_page_url();
case 'WC_SUBSCRIPTIONS_TABLE':
return admin_url( add_query_arg( [ 'post_type' => 'shop_subscription' ], 'edit.php' ) );
default:
From 8aa455c724e3eeaa7a31bbe536b6266296b0592a Mon Sep 17 00:00:00 2001
From: Alfredo Sumaran
Date: Tue, 4 Jun 2024 13:08:01 -0500
Subject: [PATCH 13/52] Disable WooPay for suspended and rejected accounts
(#8857)
---
...disable-woopay-rejected-suspended-accounts | 4 ++
includes/class-wc-payments-features.php | 10 ++++-
includes/class-wc-payments.php | 7 ++-
.../unit/test-class-wc-payments-features.php | 44 +++++++++++++++++++
4 files changed, 63 insertions(+), 2 deletions(-)
create mode 100644 changelog/as-disable-woopay-rejected-suspended-accounts
diff --git a/changelog/as-disable-woopay-rejected-suspended-accounts b/changelog/as-disable-woopay-rejected-suspended-accounts
new file mode 100644
index 00000000000..5c5d37f068f
--- /dev/null
+++ b/changelog/as-disable-woopay-rejected-suspended-accounts
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Disable WooPay for suspended and rejected accounts.
diff --git a/includes/class-wc-payments-features.php b/includes/class-wc-payments-features.php
index 568ee184b5a..42e2a19e38b 100644
--- a/includes/class-wc-payments-features.php
+++ b/includes/class-wc-payments-features.php
@@ -210,7 +210,15 @@ public static function is_woopay_eligible() {
// read directly from cache, ignore cache expiration check.
$account = WC_Payments::get_database_cache()->get( WCPay\Database_Cache::ACCOUNT_KEY, true );
- return is_array( $account ) && ( $account['platform_checkout_eligible'] ?? false );
+
+ $is_account_rejected = WC_Payments::get_account_service()->is_account_rejected();
+
+ $is_account_under_review = WC_Payments::get_account_service()->is_account_under_review();
+
+ return is_array( $account )
+ && ( $account['platform_checkout_eligible'] ?? false )
+ && ! $is_account_rejected
+ && ! $is_account_under_review;
}
/**
diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php
index 04b2e517c1f..d640cc30539 100644
--- a/includes/class-wc-payments.php
+++ b/includes/class-wc-payments.php
@@ -570,7 +570,9 @@ public static function init() {
// To avoid register the same hooks twice.
wcpay_get_container()->get( \WCPay\Internal\Service\DuplicatePaymentPreventionService::class )->init_hooks();
- self::maybe_register_woopay_hooks();
+ // Defer registering the WooPay hooks. Later on, $wp_rewrite is used and causes a fatal error every time the account cache is refreshed,
+ // given that $wp_rewrite is defined right after the `plugins_loaded` action is fired. See #8857.
+ add_action( 'setup_theme', [ __CLASS__, 'maybe_register_woopay_hooks' ] );
self::$apple_pay_registration = new WC_Payments_Apple_Pay_Registration( self::$api_client, self::$account, self::get_gateway() );
self::$apple_pay_registration->init_hooks();
@@ -1502,6 +1504,7 @@ public static function is_network_saved_cards_enabled() {
/**
* Registers woopay hooks if the woopay feature flag is enabled.
+ * Removes WooPay webhooks if the merchant is not eligible.
*
* @return void
*/
@@ -1551,6 +1554,8 @@ function ( $container ) {
}
new WooPay_Order_Status_Sync( self::$api_client, self::$account );
+ } else {
+ WooPay_Order_Status_Sync::remove_webhook();
}
}
diff --git a/tests/unit/test-class-wc-payments-features.php b/tests/unit/test-class-wc-payments-features.php
index a27ff2c432f..4e6d2258c13 100644
--- a/tests/unit/test-class-wc-payments-features.php
+++ b/tests/unit/test-class-wc-payments-features.php
@@ -18,6 +18,13 @@ class WC_Payments_Features_Test extends WCPAY_UnitTestCase {
*/
protected $mock_cache;
+ /**
+ * Mock WC_Payments_Account.
+ *
+ * @var WC_Payments_Account|MockObject
+ */
+ private $mock_wcpay_account;
+
const FLAG_OPTION_NAME_TO_FRONTEND_KEY_MAPPING = [
'_wcpay_feature_customer_multi_currency' => 'multiCurrency',
'_wcpay_feature_documents' => 'documents',
@@ -29,6 +36,17 @@ public function set_up() {
$this->_cache = WC_Payments::get_database_cache();
$this->mock_cache = $this->createMock( WCPay\Database_Cache::class );
WC_Payments::set_database_cache( $this->mock_cache );
+
+ // Mock the WCPay Account class to make sure the account is not restricted by default.
+ $this->mock_wcpay_account = $this->createMock( WC_Payments_Account::class );
+ $this->mock_wcpay_account
+ ->method( 'is_account_rejected' )
+ ->willReturn( false );
+ $this->mock_wcpay_account
+ ->method( 'is_account_under_review' )
+ ->willReturn( false );
+
+ WC_Payments::set_account_service( $this->mock_wcpay_account );
}
public function tear_down() {
@@ -91,6 +109,32 @@ public function test_is_woopay_eligible_returns_false() {
$this->assertFalse( WC_Payments_Features::is_woopay_eligible() );
}
+ public function test_is_woopay_eligible_when_account_is_suspended_returns_false() {
+ $mock_wcpay_account = $this->createMock( WC_Payments_Account::class );
+ $mock_wcpay_account
+ ->method( 'is_account_under_review' )
+ ->willReturn( true );
+
+ WC_Payments::set_account_service( $mock_wcpay_account );
+
+ $this->mock_cache->method( 'get' )->willReturn( [ 'platform_checkout_eligible' => true ] );
+
+ $this->assertFalse( WC_Payments_Features::is_woopay_eligible() );
+ }
+
+ public function test_is_woopay_eligible_when_account_is_rejected_returns_false() {
+ $mock_wcpay_account = $this->createMock( WC_Payments_Account::class );
+ $mock_wcpay_account
+ ->method( 'is_account_rejected' )
+ ->willReturn( true );
+
+ WC_Payments::set_account_service( $mock_wcpay_account );
+
+ $this->mock_cache->method( 'get' )->willReturn( [ 'platform_checkout_eligible' => true ] );
+
+ $this->assertFalse( WC_Payments_Features::is_woopay_eligible() );
+ }
+
public function test_is_documents_section_enabled_returns_true_when_flag_is_true() {
$this->mock_cache->method( 'get' )->willReturn( [ 'is_documents_enabled' => true ] );
$this->assertTrue( WC_Payments_Features::is_documents_section_enabled() );
From 282dfe64a53016875882cf22f7a8eb039858aa37 Mon Sep 17 00:00:00 2001
From: Cvetan Cvetanov
Date: Wed, 5 Jun 2024 16:56:22 +0300
Subject: [PATCH 14/52] Add survey modal on WooPayments deactivation (#8757)
---
.../add-1527-survey-modal-on-deactivation | 4 +
.../plugins-page/deactivation-survey/index.js | 50 ++++++++
.../deactivation-survey/style.scss | 38 ++++++
client/plugins-page/index.js | 108 ++++++++++++++++++
includes/admin/class-wc-payments-admin.php | 57 +++++++++
includes/class-wc-payments.php | 1 +
.../plugins-page/plugins-page-wrapper.php | 24 ++++
webpack/shared.js | 1 +
8 files changed, 283 insertions(+)
create mode 100644 changelog/add-1527-survey-modal-on-deactivation
create mode 100644 client/plugins-page/deactivation-survey/index.js
create mode 100644 client/plugins-page/deactivation-survey/style.scss
create mode 100644 client/plugins-page/index.js
create mode 100644 templates/plugins-page/plugins-page-wrapper.php
diff --git a/changelog/add-1527-survey-modal-on-deactivation b/changelog/add-1527-survey-modal-on-deactivation
new file mode 100644
index 00000000000..d2f860f88ed
--- /dev/null
+++ b/changelog/add-1527-survey-modal-on-deactivation
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add a feedback survey modal upon deactivation.
diff --git a/client/plugins-page/deactivation-survey/index.js b/client/plugins-page/deactivation-survey/index.js
new file mode 100644
index 00000000000..faa4eaf2228
--- /dev/null
+++ b/client/plugins-page/deactivation-survey/index.js
@@ -0,0 +1,50 @@
+/**
+ * External dependencies
+ */
+import React, { useState } from 'react';
+import { __ } from '@wordpress/i18n';
+import { Modal } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import './style.scss';
+import Loadable from 'wcpay/components/loadable';
+import WooPaymentsIcon from 'assets/images/woopayments.svg?asset';
+
+const PluginDisableSurvey = ( { onRequestClose } ) => {
+ const [ isLoading, setIsLoading ] = useState( true );
+
+ return (
+
+ }
+ isDismissible={ true }
+ shouldCloseOnClickOutside={ false } // Should be false because of the iframe.
+ shouldCloseOnEsc={ true }
+ onRequestClose={ onRequestClose }
+ className="woopayments-disable-survey"
+ >
+
+
+
+ );
+};
+
+export default PluginDisableSurvey;
diff --git a/client/plugins-page/deactivation-survey/style.scss b/client/plugins-page/deactivation-survey/style.scss
new file mode 100644
index 00000000000..ba8a2e116e0
--- /dev/null
+++ b/client/plugins-page/deactivation-survey/style.scss
@@ -0,0 +1,38 @@
+.woopayments-disable-survey {
+ @media ( min-width: 960px ) {
+ max-height: calc( 100% - 120px );
+ }
+
+ .components-modal__content {
+ padding: 0;
+ overflow: hidden;
+ }
+
+ &-iframe {
+ width: 100%;
+ height: 100%;
+
+ @media ( min-width: 600px ) {
+ width: 600px;
+ height: 650px;
+ }
+ }
+
+ &-logo {
+ height: 40px;
+ }
+}
+
+/**
+ * There is a bug with the Modal component that when the close X is hovered or focused, a tooltip
+ * appears outside of the view of the modal causing scrollbars. This is a work around to hide the
+ * tooltip until the bug is fixed.
+ * TODO: remove rule ones bug is closed
+ * https://github.com/WordPress/gutenberg/issues/15434
+ */
+.components-modal__content
+ .components-modal__header
+ .components-button
+ .components-tooltip {
+ display: none;
+}
diff --git a/client/plugins-page/index.js b/client/plugins-page/index.js
new file mode 100644
index 00000000000..24d59e65fa5
--- /dev/null
+++ b/client/plugins-page/index.js
@@ -0,0 +1,108 @@
+/**
+ * External dependencies
+ */
+import React, { useState, useEffect, useCallback } from 'react';
+import { useDispatch } from '@wordpress/data';
+import ReactDOM from 'react-dom';
+import { OPTIONS_STORE_NAME } from '@woocommerce/data';
+
+/**
+ * Internal dependencies
+ */
+import PluginDisableSurvey from './deactivation-survey';
+
+const PluginsPage = () => {
+ const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
+ const [ modalOpen, setModalOpen ] = useState( false );
+ const surveyModalTimestamp =
+ window.wcpayPluginsSettings?.exitSurveyLastShown ?? null;
+
+ const deactivationLink = document.querySelector(
+ '#deactivate-woocommerce-payments, #deactivate-woocommerce-payments-dev'
+ ); // ID set by WP on the deactivation link.
+
+ const deactivatePlugin = useCallback( () => {
+ // Abort if the deactivation link is not present.
+ if ( deactivationLink === null ) {
+ return null;
+ }
+
+ // Deactivate plugin
+ window.location.href = deactivationLink.getAttribute( 'href' );
+ }, [ deactivationLink ] );
+
+ const showModal = useCallback( () => {
+ setModalOpen( true );
+ }, [ setModalOpen ] );
+
+ const closeModal = async () => {
+ setModalOpen( false );
+
+ const currentDate = new Date();
+
+ // Update modal dismissed option.
+ await updateOptions( {
+ wcpay_exit_survey_last_shown: currentDate,
+ } );
+
+ window.wcpayPluginsSettings.exitSurveyLastShown = currentDate;
+
+ // Deactivate plugin
+ deactivatePlugin();
+ };
+
+ const isModalDismissed = useCallback( () => {
+ if ( surveyModalTimestamp ) {
+ const date1 = new Date( surveyModalTimestamp );
+ const date2 = new Date();
+ const diffTime = Math.abs( date2 - date1 );
+ const diffDays = Math.ceil( diffTime / ( 1000 * 60 * 60 * 24 ) );
+
+ if ( diffDays < 7 ) {
+ return true;
+ }
+ }
+
+ return false;
+ }, [ surveyModalTimestamp ] );
+
+ const handleLinkClick = useCallback(
+ ( e ) => {
+ e.preventDefault();
+ showModal();
+ },
+ [ showModal ]
+ );
+
+ useEffect( () => {
+ // If the survey is dismissed skip event listeners.
+ if ( isModalDismissed() ) {
+ return null;
+ }
+
+ // Abort if the deactivation link is not present.
+ if ( deactivationLink === null ) {
+ return null;
+ }
+
+ // Handle click event.
+ deactivationLink.addEventListener( 'click', handleLinkClick );
+
+ return () => {
+ deactivationLink.removeEventListener( 'click', handleLinkClick );
+ };
+ }, [ isModalDismissed, deactivationLink, handleLinkClick ] );
+
+ return (
+ <>
+ { ! isModalDismissed() && modalOpen && (
+
+ ) }
+ >
+ );
+};
+
+ReactDOM.render(
+ ,
+ document.querySelector( '#woopayments-plugins-page-app' )
+);
diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php
index f302580b0b7..aeb97880bfa 100644
--- a/includes/admin/class-wc-payments-admin.php
+++ b/includes/admin/class-wc-payments-admin.php
@@ -581,6 +581,17 @@ public function register_payments_scripts() {
WC_Payments::get_file_version( 'dist/payment-gateways.css' ),
'all'
);
+
+ WC_Payments::register_script_with_dependencies( 'WCPAY_PLUGINS_PAGE', 'dist/plugins-page', [ 'wp-api-request' ] );
+ wp_set_script_translations( 'WCPAY_PLUGINS_PAGE', 'woocommerce-payments' );
+
+ WC_Payments_Utils::register_style(
+ 'WCPAY_PLUGINS_PAGE',
+ plugins_url( 'dist/plugins-page.css', WCPAY_PLUGIN_FILE ),
+ [ 'wp-components', 'wc-components' ],
+ WC_Payments::get_file_version( 'dist/plugins-page.css' ),
+ 'all'
+ );
}
/**
@@ -669,6 +680,23 @@ public function enqueue_payments_scripts() {
}
$screen = get_current_screen();
+
+ // Only enqueue the scripts on the plugins page.
+ if ( in_array( $screen->id, [ 'plugins' ], true ) ) {
+ // Localize before actually enqueuing to avoid unnecessary settings generation.
+ // Most importantly, the destructive error transient handling.
+ wp_localize_script(
+ 'WCPAY_PLUGINS_PAGE',
+ 'wcpayPluginsSettings',
+ $this->get_plugins_page_js_settings()
+ );
+
+ wp_enqueue_script( 'WCPAY_PLUGINS_PAGE' );
+ wp_enqueue_style( 'WCPAY_PLUGINS_PAGE' );
+
+ add_action( 'admin_footer', [ $this, 'load_plugins_page_wrapper' ] );
+ }
+
if ( in_array( $screen->id, [ 'shop_order', 'woocommerce_page_wc-orders' ], true ) ) {
$order = wc_get_order();
@@ -719,6 +747,22 @@ public function enqueue_payments_scripts() {
}
}
+ /**
+ * Outputs the wrapper for the plugin modal
+ * Contents are loaded by React script
+ *
+ * @return void
+ */
+ public function load_plugins_page_wrapper() {
+ wc_get_template(
+ 'plugins-page/plugins-page-wrapper.php',
+ [],
+ '',
+ WCPAY_ABSPATH . 'templates/'
+ );
+ }
+
+
/**
* Get the WCPay settings to be sent to JS.
*
@@ -872,6 +916,19 @@ private function get_js_settings(): array {
return apply_filters( 'wcpay_js_settings', $this->wcpay_js_settings );
}
+ /**
+ * Get the WCPay plugins page settings to be sent to JS.
+ *
+ * @return array
+ */
+ private function get_plugins_page_js_settings(): array {
+ $plugins_page_settings = [
+ 'exitSurveyLastShown' => get_option( 'wcpay_exit_survey_last_shown', null ),
+ ];
+
+ return apply_filters( 'wcpay_plugins_page_js_settings', $plugins_page_settings );
+ }
+
/**
* Helper function to retrieve enabled UPE payment methods.
*
diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php
index d640cc30539..cf872d3a4f7 100644
--- a/includes/class-wc-payments.php
+++ b/includes/class-wc-payments.php
@@ -1833,6 +1833,7 @@ public static function add_wcpay_options_to_woocommerce_permissions_list( $permi
'wcpay_onboarding_eligibility_modal_dismissed',
'wcpay_next_deposit_notice_dismissed',
'wcpay_duplicate_payment_method_notices_dismissed',
+ 'wcpay_exit_survey_dismissed',
],
true
);
diff --git a/templates/plugins-page/plugins-page-wrapper.php b/templates/plugins-page/plugins-page-wrapper.php
new file mode 100644
index 00000000000..b89e5789c1a
--- /dev/null
+++ b/templates/plugins-page/plugins-page-wrapper.php
@@ -0,0 +1,24 @@
+
+
+
+
diff --git a/webpack/shared.js b/webpack/shared.js
index 60e6c3f333e..0a445ccf118 100644
--- a/webpack/shared.js
+++ b/webpack/shared.js
@@ -41,6 +41,7 @@ module.exports = {
'./client/subscription-product-onboarding/toast.js',
'product-details': './client/product-details/index.js',
'cart-block': './client/cart/blocks/index.js',
+ 'plugins-page': './client/plugins-page/index.js',
},
// Override webpack public path dynamically on every entry.
// Required for chunks loading to work on sites with JS concatenation.
From 64b88d06134e5b45d41c7fa76112da4192d6b9e5 Mon Sep 17 00:00:00 2001
From: Rua Haszard
Date: Thu, 6 Jun 2024 05:56:29 +1200
Subject: [PATCH 15/52] Support payment activity widget fetching & displaying
differnt deposit currencies (#8864)
Co-authored-by: Rua Haszard
Co-authored-by: Jessy Pappachan <32092402+jessy-p@users.noreply.github.com>
Co-authored-by: Jessy
---
.../add-deposit-currency-param-for-payment-widget | 5 +++++
client/components/payment-activity/index.tsx | 3 +++
client/data/payment-activity/test/hooks.test.ts | 1 +
client/data/payment-activity/test/resolver.test.ts | 3 ++-
client/data/payment-activity/types.d.ts | 9 +++++++++
...class-wc-rest-payments-reporting-controller.php | 1 +
.../class-get-reporting-payment-activity.md | 5 ++++-
.../class-get-reporting-payment-activity.php | 14 ++++++++++++++
8 files changed, 39 insertions(+), 2 deletions(-)
create mode 100644 changelog/add-deposit-currency-param-for-payment-widget
diff --git a/changelog/add-deposit-currency-param-for-payment-widget b/changelog/add-deposit-currency-param-for-payment-widget
new file mode 100644
index 00000000000..e0f22451ab2
--- /dev/null
+++ b/changelog/add-deposit-currency-param-for-payment-widget
@@ -0,0 +1,5 @@
+Significance: patch
+Type: add
+Comment: Behind feature flag. Adds few changes to support displaying multiple currencies for Payment Activity Widget.
+
+
diff --git a/client/components/payment-activity/index.tsx b/client/components/payment-activity/index.tsx
index 6a7e7bad00d..4c08aab66c2 100644
--- a/client/components/payment-activity/index.tsx
+++ b/client/components/payment-activity/index.tsx
@@ -66,6 +66,9 @@ const PaymentActivity: React.FC = () => {
wcpaySettings.isOverviewSurveySubmitted ?? false;
const { paymentActivityData, isLoading } = usePaymentActivityData( {
+ // In future this will be bound to currency picker via useSelectedCurrency().
+ // Can hard-code other store settings to test.
+ currency: wcpaySettings.accountDefaultCurrency,
...getDateRange(),
timezone: moment( new Date() ).format( 'Z' ),
} );
diff --git a/client/data/payment-activity/test/hooks.test.ts b/client/data/payment-activity/test/hooks.test.ts
index 4cd6114f69a..3bda3efb3cc 100644
--- a/client/data/payment-activity/test/hooks.test.ts
+++ b/client/data/payment-activity/test/hooks.test.ts
@@ -34,6 +34,7 @@ describe( 'usePaymentActivityData', () => {
);
const result = usePaymentActivityData( {
+ currency: 'jpy',
date_start: '2021-01-01',
date_end: '2021-01-31',
timezone: 'UTC',
diff --git a/client/data/payment-activity/test/resolver.test.ts b/client/data/payment-activity/test/resolver.test.ts
index a9a93977180..3f8ee5c3878 100644
--- a/client/data/payment-activity/test/resolver.test.ts
+++ b/client/data/payment-activity/test/resolver.test.ts
@@ -13,6 +13,7 @@ import { updatePaymentActivity } from '../actions';
import { getPaymentActivityData } from '../resolvers';
const query = {
+ currency: 'usd',
date_start: '2020-04-29T04:00:00',
date_end: '2020-04-29T03:59:59',
timezone: '+2:30',
@@ -21,7 +22,7 @@ const query = {
describe( 'getPaymentActivityData resolver', () => {
const successfulResponse: any = { amount: 3000 };
const expectedQueryString =
- 'date_start=2020-04-29T04%3A00%3A00&date_end=2020-04-29T03%3A59%3A59&timezone=%2B2%3A30';
+ 'currency=usd&date_start=2020-04-29T04%3A00%3A00&date_end=2020-04-29T03%3A59%3A59&timezone=%2B2%3A30';
const errorResponse = new Error(
'Error retrieving payment activity data.'
);
diff --git a/client/data/payment-activity/types.d.ts b/client/data/payment-activity/types.d.ts
index 76690dc459c..23eaddb6183 100644
--- a/client/data/payment-activity/types.d.ts
+++ b/client/data/payment-activity/types.d.ts
@@ -33,6 +33,13 @@ export interface PaymentActivityAction {
data: PaymentActivityData;
}
+/**
+ * Query parameters for fetching payment activity data for overview widget.
+ * Note that these are must match the query parameters for the REST API endpoint.
+ *
+ * @see Reporting_Service::get_payment_activity_totals() on WooPayments service.
+ * Musing: we could move all rest endpoint typedefs to a single place to make it clear that they are coupled to server code.
+ */
export interface PaymentActivityQuery {
/** The date range start datetime used to calculate transaction data, e.g. 2024-04-29T16:19:29 */
date_start: string;
@@ -40,4 +47,6 @@ export interface PaymentActivityQuery {
date_end: string;
/** The timezone used to calculate the transaction data date range, e.g. 'UTC' */
timezone: string;
+ /** The currency to display */
+ currency: string;
}
diff --git a/includes/admin/class-wc-rest-payments-reporting-controller.php b/includes/admin/class-wc-rest-payments-reporting-controller.php
index f2f292df683..56fce001f5e 100644
--- a/includes/admin/class-wc-rest-payments-reporting-controller.php
+++ b/includes/admin/class-wc-rest-payments-reporting-controller.php
@@ -48,6 +48,7 @@ public function get_payment_activity( $request ) {
$wcpay_request->set_date_start( $request->get_param( 'date_start' ) );
$wcpay_request->set_date_end( $request->get_param( 'date_end' ) );
$wcpay_request->set_timezone( $request->get_param( 'timezone' ) );
+ $wcpay_request->set_currency( $request->get_param( 'currency' ) );
return $wcpay_request->handle_rest_request();
}
}
diff --git a/includes/core/server/request/class-get-reporting-payment-activity.md b/includes/core/server/request/class-get-reporting-payment-activity.md
index 53f676f6d59..a47d9aed622 100644
--- a/includes/core/server/request/class-get-reporting-payment-activity.md
+++ b/includes/core/server/request/class-get-reporting-payment-activity.md
@@ -13,9 +13,11 @@ The `WCPay\Core\Server\Request\Get_Reporting_Payment_Activity` class is used to
| `date_start`| `set_date_start( string $date_start )` | No | Yes | - |
| `date_end` | `set_date_end( string $date_end )` | No | Yes | - |
| `timezone` | `set_timezone( string $timezone )` | No | Yes | - |
+| `currency` | `set_currency( string $currency )` | No | Yes | - |
The `date_start` and `date_end` parameters should be in the 'YYYY-MM-DDT00:00:00' format.
The `timezone` parameter can be passed as an offset or as a [timezone name](https://www.php.net/manual/en/timezones.php).
+The `currency` parameter should be a lower-cased ISO currency code of a store supported currency.
## Filter
@@ -30,9 +32,10 @@ $request = Get_Reporting_Payment_Activity::create();
$request->set_date_start( $date_start );
$request->set_date_end( $date_end );
$request->set_timezone( $timezone );
+$request->set_currency( $currency );
$request->send();
```
## Exceptions
-- `Invalid_Request_Parameter_Exception` - Thrown when the provided date or timezone is not in expected format.
\ No newline at end of file
+- `Invalid_Request_Parameter_Exception` - Thrown when the provided date or timezone is not in expected format.
diff --git a/includes/core/server/request/class-get-reporting-payment-activity.php b/includes/core/server/request/class-get-reporting-payment-activity.php
index f8697a198e8..244de31da2f 100644
--- a/includes/core/server/request/class-get-reporting-payment-activity.php
+++ b/includes/core/server/request/class-get-reporting-payment-activity.php
@@ -20,6 +20,7 @@ class Get_Reporting_Payment_Activity extends Request {
'date_start',
'date_end',
'timezone',
+ 'currency',
];
/**
@@ -97,4 +98,17 @@ public function set_timezone( string $timezone ) {
}
$this->set_param( 'timezone', $timezone );
}
+
+ /**
+ * Sets the currency arg for the request.
+ *
+ * @param string $currency A deposit currency code e.g. USD. (TODO lower or uppercase?).
+ * @return void
+ *
+ * @throws Invalid_Request_Parameter_Exception Exception if the arg is not in valid format.
+ */
+ public function set_currency( string $currency ) {
+ // Do we need validation here?
+ $this->set_param( 'currency', $currency );
+ }
}
From ea327ae26ba2dde7b0cb6b280b59faa781bec896 Mon Sep 17 00:00:00 2001
From: Ricardo Metring
Date: Wed, 5 Jun 2024 20:49:06 +0200
Subject: [PATCH 16/52] Fix credit card input errors and checkout errors E2E
tests (#8908)
---
...specific-field-checkout-failures-e2e-tests | 4 ++++
client/checkout/blocks/payment-processor.js | 14 +++++------
.../blocks/test/payment-processor.test.js | 24 +++++++++----------
client/checkout/classic/payment-processing.js | 19 +++++++--------
.../classic/test/payment-processing.test.js | 14 -----------
5 files changed, 31 insertions(+), 44 deletions(-)
create mode 100644 changelog/fix-specific-field-checkout-failures-e2e-tests
diff --git a/changelog/fix-specific-field-checkout-failures-e2e-tests b/changelog/fix-specific-field-checkout-failures-e2e-tests
new file mode 100644
index 00000000000..c885808e98c
--- /dev/null
+++ b/changelog/fix-specific-field-checkout-failures-e2e-tests
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Fix input-specific credit card errors.
diff --git a/client/checkout/blocks/payment-processor.js b/client/checkout/blocks/payment-processor.js
index aaaf3e21772..cbb1e8d412f 100644
--- a/client/checkout/blocks/payment-processor.js
+++ b/client/checkout/blocks/payment-processor.js
@@ -66,7 +66,7 @@ const PaymentProcessor = ( {
} ) => {
const stripe = useStripe();
const elements = useElements();
- const isPaymentInformationCompleteRef = useRef( false );
+ const hasLoadErrorRef = useRef( false );
const paymentMethodsConfig = getUPEConfig( 'paymentMethodsConfig' );
const isTestMode = getUPEConfig( 'testMode' );
@@ -140,11 +140,11 @@ const PaymentProcessor = ( {
return;
}
- if ( ! isPaymentInformationCompleteRef.current ) {
+ if ( hasLoadErrorRef.current ) {
return {
type: 'error',
message: __(
- 'Your payment information is incomplete.',
+ 'Invalid or missing payment details. Please ensure the provided payment method is correctly entered.',
'woocommerce-payments'
),
};
@@ -237,8 +237,9 @@ const PaymentProcessor = ( {
shouldSavePayment
);
- const setPaymentInformationCompletionStatus = ( event ) => {
- isPaymentInformationCompleteRef.current = event.complete;
+ const setHasLoadError = ( event ) => {
+ hasLoadErrorRef.current = true;
+ onLoadError( event );
};
return (
@@ -256,8 +257,7 @@ const PaymentProcessor = ( {
shouldSavePayment,
paymentMethodsConfig
) }
- onLoadError={ onLoadError }
- onChange={ setPaymentInformationCompletionStatus }
+ onLoadError={ setHasLoadError }
className="wcpay-payment-element"
/>
>
diff --git a/client/checkout/blocks/test/payment-processor.test.js b/client/checkout/blocks/test/payment-processor.test.js
index c94c7e432e9..39e0a754022 100644
--- a/client/checkout/blocks/test/payment-processor.test.js
+++ b/client/checkout/blocks/test/payment-processor.test.js
@@ -29,20 +29,12 @@ jest.mock( '@stripe/react-stripe-js', () => ( {
useStripe: jest.fn(),
} ) );
-const MockPaymentElement = ( { onChange } ) => {
- useEffect( () => {
- onChange( { complete: true } );
- }, [ onChange ] );
-
- return null;
-};
-
describe( 'PaymentProcessor', () => {
let mockApi;
let mockCreatePaymentMethod;
beforeEach( () => {
global.wcpay_upe_config = { paymentMethodsConfig: {} };
- PaymentElement.mockImplementation( MockPaymentElement );
+ PaymentElement.mockImplementation( () => null );
mockCreatePaymentMethod = jest
.fn()
.mockResolvedValue( { paymentMethod: {} } );
@@ -97,8 +89,14 @@ describe( 'PaymentProcessor', () => {
).not.toBeInTheDocument();
} );
- it( 'should return an error when the payment information is incomplete', async () => {
- PaymentElement.mockImplementation( () => null );
+ it( 'should return an error if the payment method could not be loaded', async () => {
+ PaymentElement.mockImplementation( ( { onLoadError } ) => {
+ useEffect( () => {
+ onLoadError();
+ }, [ onLoadError ] );
+
+ return null;
+ } );
let onPaymentSetupCallback;
render(
{
fingerprint=""
shouldSavePayment={ false }
upeMethods={ { card: 'woocommerce_payments' } }
+ onLoadError={ jest.fn() }
/>
);
expect( await onPaymentSetupCallback() ).toEqual( {
type: 'error',
- message: 'Your payment information is incomplete.',
+ message:
+ 'Invalid or missing payment details. Please ensure the provided payment method is correctly entered.',
} );
expect( mockCreatePaymentMethod ).not.toHaveBeenCalled();
} );
diff --git a/client/checkout/classic/payment-processing.js b/client/checkout/classic/payment-processing.js
index 204a84adc10..c3c50b60f33 100644
--- a/client/checkout/classic/payment-processing.js
+++ b/client/checkout/classic/payment-processing.js
@@ -39,7 +39,7 @@ for ( const paymentMethodType in getUPEConfig( 'paymentMethodsConfig' ) ) {
gatewayUPEComponents[ paymentMethodType ] = {
elements: null,
upeElement: null,
- isPaymentInformationComplete: false,
+ hasLoadError: false,
};
}
@@ -406,11 +406,9 @@ export async function mountStripePaymentElement( api, domElement ) {
gatewayUPEComponents[ paymentMethodType ].upeElement ||
( await createStripePaymentElement( api, paymentMethodType ) );
upeElement.mount( domElement );
- upeElement.on( 'change', ( e ) => {
- gatewayUPEComponents[ paymentMethodType ].isPaymentInformationComplete =
- e.complete;
- } );
upeElement.on( 'loaderror', ( e ) => {
+ // setting the flag to true to prevent the form from being submitted.
+ gatewayUPEComponents[ paymentMethodType ].hasLoadError = true;
// unset any styling to ensure the WC error message wrapper can take more width.
domElement.style.padding = '0';
// creating a new element to be added to the DOM, so that the message can be displayed.
@@ -524,15 +522,14 @@ export const processPayment = (
try {
await blockUI( $form );
- const {
- elements,
- isPaymentInformationComplete,
- } = gatewayUPEComponents[ paymentMethodType ];
+ const { elements, hasLoadError } = gatewayUPEComponents[
+ paymentMethodType
+ ];
- if ( ! isPaymentInformationComplete ) {
+ if ( hasLoadError ) {
throw new Error(
__(
- 'Your payment information is incomplete.',
+ 'Invalid or missing payment details. Please ensure the provided payment method is correctly entered.',
'woocommerce-payments'
)
);
diff --git a/client/checkout/classic/test/payment-processing.test.js b/client/checkout/classic/test/payment-processing.test.js
index ad8a24315b9..edcaf14107e 100644
--- a/client/checkout/classic/test/payment-processing.test.js
+++ b/client/checkout/classic/test/payment-processing.test.js
@@ -76,14 +76,6 @@ const mockCreateFunction = jest.fn( () => ( {
eventHandlersFromElementsCreate[ event ].push( handler );
},
} ) );
-const callAllCreateHandlersWith = ( event, ...args ) => {
- eventHandlersFromElementsCreate[ event ]?.forEach( ( handler ) => {
- handler.apply( null, args );
- } );
-};
-const markAllPaymentElementsAsComplete = () => {
- callAllCreateHandlersWith( 'change', { complete: true } );
-};
const mockSubmit = jest.fn( () => ( {
then: jest.fn(),
@@ -396,7 +388,6 @@ describe( 'Payment processing', () => {
mockDomElement.dataset.paymentMethodType = 'card';
await mountStripePaymentElement( apiMock, mockDomElement );
- markAllPaymentElementsAsComplete();
const mockJqueryForm = {
submit: jest.fn(),
@@ -443,7 +434,6 @@ describe( 'Payment processing', () => {
mockDomElement.dataset.paymentMethodType = 'card';
await mountStripePaymentElement( apiMock, mockDomElement );
- markAllPaymentElementsAsComplete();
const checkoutForm = {
submit: jest.fn(),
@@ -487,7 +477,6 @@ describe( 'Payment processing', () => {
mockDomElement.dataset.paymentMethodType = 'card';
await mountStripePaymentElement( apiMock, mockDomElement );
- markAllPaymentElementsAsComplete();
const checkoutForm = {
submit: jest.fn(),
@@ -527,7 +516,6 @@ describe( 'Payment processing', () => {
mockDomElement.dataset.paymentMethodType = 'card';
await mountStripePaymentElement( apiMock, mockDomElement );
- markAllPaymentElementsAsComplete();
const checkoutForm = {
submit: jest.fn(),
@@ -564,7 +552,6 @@ describe( 'Payment processing', () => {
mockDomElement.dataset.paymentMethodType = 'card';
await mountStripePaymentElement( apiMock, mockDomElement );
- markAllPaymentElementsAsComplete();
const checkoutForm = {
submit: jest.fn(),
@@ -599,7 +586,6 @@ describe( 'Payment processing', () => {
mockDomElement.dataset.paymentMethodType = 'card';
await mountStripePaymentElement( apiMock, mockDomElement );
- markAllPaymentElementsAsComplete();
const addPaymentMethodForm = {
submit: jest.fn(),
From 82b2515583771f7a4499fe278419811ea7433b7e Mon Sep 17 00:00:00 2001
From: Ricardo Metring
Date: Wed, 5 Jun 2024 20:55:15 +0200
Subject: [PATCH 17/52] Fix Klarna E2E tests (#8907)
---
changelog/fix-klarna-e2e-tests | 5 ++
.../shopper/shopper-klarna-checkout.spec.js | 55 ++++++-------------
2 files changed, 21 insertions(+), 39 deletions(-)
create mode 100644 changelog/fix-klarna-e2e-tests
diff --git a/changelog/fix-klarna-e2e-tests b/changelog/fix-klarna-e2e-tests
new file mode 100644
index 00000000000..576455458e1
--- /dev/null
+++ b/changelog/fix-klarna-e2e-tests
@@ -0,0 +1,5 @@
+Significance: patch
+Type: dev
+Comment: Fix Klarna E2E Tests.
+
+
diff --git a/tests/e2e/specs/wcpay/shopper/shopper-klarna-checkout.spec.js b/tests/e2e/specs/wcpay/shopper/shopper-klarna-checkout.spec.js
index 903d5d53593..1d4239fe590 100644
--- a/tests/e2e/specs/wcpay/shopper/shopper-klarna-checkout.spec.js
+++ b/tests/e2e/specs/wcpay/shopper/shopper-klarna-checkout.spec.js
@@ -102,70 +102,45 @@ describe( 'Klarna checkout', () => {
await shopper.placeOrder();
- // Klarna is rendered in an iframe, so we need to get its reference.
- // Sometimes the iframe is updated (or removed from the page),
- // this function has been created so that we always get the most updated reference.
- const getNewKlarnaIframe = async () => {
- const klarnaFrameHandle = await page.waitForSelector(
- '#klarna-apf-iframe'
- );
-
- return await klarnaFrameHandle.contentFrame();
- };
-
- let klarnaIframe = await getNewKlarnaIframe();
-
- const frameNavigationHandler = async ( frame ) => {
- if ( frame.url().includes( 'klarna.com' ) ) {
- const newKlarnaIframe = await getNewKlarnaIframe();
+ await page.waitForSelector( '#phone' );
- if ( frame === newKlarnaIframe ) {
- klarnaIframe = newKlarnaIframe;
- }
- }
- };
-
- // Add frame navigation event listener.
- page.on( 'framenavigated', frameNavigationHandler );
+ await page.waitFor( 2000 );
- // Waiting for the redirect & the Klarna iframe to load within the Stripe test page.
- // this is the "confirm phone number" page - we just click "continue".
- await klarnaIframe.waitForSelector( '#phone' );
- await klarnaIframe
+ await page
.waitForSelector( '#onContinue' )
.then( ( button ) => button.click() );
+ await page.waitFor( 2000 );
+
// This is where the OTP code is entered.
- await klarnaIframe.waitForSelector( '#phoneOtp' );
- await expect( klarnaIframe ).toFill( 'input#otp_field', '123456' );
+ await page.waitForSelector( '#phoneOtp' );
+
+ await page.waitFor( 2000 );
+
+ await expect( page ).toFill( 'input#otp_field', '123456' );
// Select Payment Plan - 4 weeks & click continue.
- await klarnaIframe
+ await page
.waitForSelector( 'button#pay_over_time__label' )
.then( ( button ) => button.click() );
await page.waitFor( 2000 );
- await klarnaIframe
+ await page
.waitForSelector( 'button[data-testid="select-payment-category"' )
.then( ( button ) => button.click() );
await page.waitFor( 2000 );
// Payment summary page. Click continue.
- await klarnaIframe
+ await page
.waitForSelector( 'button[data-testid="pick-plan"]' )
.then( ( button ) => button.click() );
await page.waitFor( 2000 );
- // At this point, the event listener is not needed anymore.
- page.removeListener( 'framenavigated', frameNavigationHandler );
-
- await page.waitFor( 2000 );
-
// Confirm payment.
- await klarnaIframe
+ await page
.waitForSelector( 'button#buy_button' )
.then( ( button ) => button.click() );
@@ -174,6 +149,8 @@ describe( 'Klarna checkout', () => {
waitUntil: 'networkidle0',
} );
+ await page.waitForSelector( 'h1.entry-title' );
+
await expect( page ).toMatch( 'Order received' );
} );
} );
From 91a340d821542db531ce07c9d6f0104f13391a53 Mon Sep 17 00:00:00 2001
From: Timur Karimov
Date: Thu, 6 Jun 2024 07:15:30 +0200
Subject: [PATCH 18/52] Fall back to credit card when a payment method is
toggled off (#8909)
---
changelog/fix-fallback-to-cc-when-hiding-pm | 4 +
client/checkout/utils/test/upe.test.js | 95 +++++++++++++++++++++
client/checkout/utils/upe.js | 7 ++
3 files changed, 106 insertions(+)
create mode 100644 changelog/fix-fallback-to-cc-when-hiding-pm
diff --git a/changelog/fix-fallback-to-cc-when-hiding-pm b/changelog/fix-fallback-to-cc-when-hiding-pm
new file mode 100644
index 00000000000..0c5b1dda236
--- /dev/null
+++ b/changelog/fix-fallback-to-cc-when-hiding-pm
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Fall back to credit card as default payment method when a payment method is toggled off.
diff --git a/client/checkout/utils/test/upe.test.js b/client/checkout/utils/test/upe.test.js
index 8af706e27d1..ec992e223bd 100644
--- a/client/checkout/utils/test/upe.test.js
+++ b/client/checkout/utils/test/upe.test.js
@@ -10,6 +10,7 @@ import {
getSelectedUPEGatewayPaymentMethod,
isUsingSavedPaymentMethod,
dispatchChangeEventFor,
+ togglePaymentMethodForCountry,
} from '../upe';
import { getPaymentMethodsConstants } from '../../constants';
import { getUPEConfig } from 'wcpay/utils/checkout';
@@ -125,6 +126,100 @@ describe( 'UPE checkout utils', () => {
} );
} );
+ describe( 'togglePaymentMethodForCountry', () => {
+ let container;
+
+ beforeAll( () => {
+ container = document.createElement( 'div' );
+ container.innerHTML = `
+
+
+ `;
+ document.body.appendChild( container );
+ } );
+
+ afterAll( () => {
+ document.body.removeChild( container );
+ container = null;
+ } );
+
+ beforeEach( () => {
+ jest.clearAllMocks();
+ getUPEConfig.mockImplementation( ( argument ) => {
+ if ( argument === 'paymentMethodsConfig' ) {
+ return {
+ card: { countries: [ 'US' ] },
+ bancontact: { countries: [ 'BE' ] },
+ };
+ }
+
+ if ( argument === 'gatewayId' ) {
+ return 'woocommerce_payments';
+ }
+ } );
+ window.wcpayCustomerData = { billing_country: 'BE' };
+ } );
+
+ afterEach( () => {
+ // document.getElementById('billing_country').value = '';
+ window.wcpayCustomerData = null;
+ } );
+
+ it( 'should show payment method if country is supported', () => {
+ const upeElement = document.querySelector(
+ '.payment_method_woocommerce_payments_card'
+ );
+ document.getElementById( 'billing_country' ).value = 'US';
+
+ togglePaymentMethodForCountry( upeElement );
+
+ expect( upeElement.style.display ).toBe( 'block' );
+ } );
+
+ it( 'should hide payment method if country is not supported', () => {
+ const upeElement = document.querySelector(
+ '.payment_method_woocommerce_payments_card'
+ );
+ document.getElementById( 'billing_country' ).value = 'BE';
+
+ togglePaymentMethodForCountry( upeElement );
+
+ expect( upeElement.style.display ).toBe( 'none' );
+ } );
+
+ it( 'should fall back to card as the default payment method if the selected payment method is toggled off', () => {
+ const input = document.querySelector(
+ '#payment_method_woocommerce_payments_bancontact'
+ );
+ input.checked = true;
+
+ const upeElement = document.querySelector(
+ '.payment_method_woocommerce_payments_bancontact'
+ );
+ document.getElementById( 'billing_country' ).value = 'US';
+
+ const cardPaymentMethod = document.querySelector(
+ '#payment_method_woocommerce_payments'
+ );
+ jest.spyOn( cardPaymentMethod, 'click' );
+
+ togglePaymentMethodForCountry( upeElement );
+
+ expect( upeElement.style.display ).toBe( 'none' );
+ expect( cardPaymentMethod.click ).toHaveBeenCalled();
+ } );
+ } );
+
describe( 'getUPESettings', () => {
afterEach( () => {
const checkboxElement = document.getElementById(
diff --git a/client/checkout/utils/upe.js b/client/checkout/utils/upe.js
index 65cdb5718aa..039f849e36c 100644
--- a/client/checkout/utils/upe.js
+++ b/client/checkout/utils/upe.js
@@ -311,6 +311,7 @@ export const togglePaymentMethodForCountry = ( upeElement ) => {
const paymentMethodType = upeElement.dataset.paymentMethodType;
const supportedCountries =
paymentMethodsConfig[ paymentMethodType ].countries;
+ const selectedPaymentMethod = getSelectedUPEGatewayPaymentMethod();
/* global wcpayCustomerData */
// in the case of "pay for order", there is no "billing country" input, so we need to rely on backend data.
@@ -326,5 +327,11 @@ export const togglePaymentMethodForCountry = ( upeElement ) => {
upeContainer.style.display = 'block';
} else {
upeContainer.style.display = 'none';
+ // if the toggled off payment method was selected, we need to fall back to credit card
+ if ( paymentMethodType === selectedPaymentMethod ) {
+ document
+ .querySelector( '#payment_method_woocommerce_payments' )
+ .click();
+ }
}
};
From 0703cc10e7bde81283fae4d03226e212c00b73ea Mon Sep 17 00:00:00 2001
From: Francesco
Date: Thu, 6 Jun 2024 09:21:17 +0200
Subject: [PATCH 19/52] chore: EPMs to always send shipping phone (#8902)
Co-authored-by: Brett Shumaker
---
changelog/chore-epms-always-send-shipping-phone | 4 ++++
client/express-checkout/utils/normalize.js | 10 +++++++---
client/payment-request/utils/normalize.js | 9 ++++++---
.../transformers/stripe-to-wc.js | 17 ++++++++++++-----
4 files changed, 29 insertions(+), 11 deletions(-)
create mode 100644 changelog/chore-epms-always-send-shipping-phone
diff --git a/changelog/chore-epms-always-send-shipping-phone b/changelog/chore-epms-always-send-shipping-phone
new file mode 100644
index 00000000000..1519b8a08ae
--- /dev/null
+++ b/changelog/chore-epms-always-send-shipping-phone
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+chore: EPMs to always send shipping phone
diff --git a/client/express-checkout/utils/normalize.js b/client/express-checkout/utils/normalize.js
index fd44ca77b5c..5e940d158de 100644
--- a/client/express-checkout/utils/normalize.js
+++ b/client/express-checkout/utils/normalize.js
@@ -30,19 +30,22 @@ export const normalizeLineItems = ( displayItems ) => {
export const normalizeOrderData = ( event, paymentMethodId ) => {
const name = event?.billingDetails?.name;
const email = event?.billingDetails?.email ?? '';
- const phone = event?.billingDetails?.phone ?? '';
const billing = event?.billingDetails?.address ?? {};
const shipping = event?.shippingAddress ?? {};
const fraudPreventionTokenValue = window.wcpayFraudPreventionToken ?? '';
+ const phone =
+ event?.billingDetails?.phone ??
+ event?.payerPhone?.replace( '/[() -]/g', '' ) ??
+ '';
+
return {
billing_first_name:
name?.split( ' ' )?.slice( 0, 1 )?.join( ' ' ) ?? '',
billing_last_name: name?.split( ' ' )?.slice( 1 )?.join( ' ' ) || '-',
billing_company: billing?.organization ?? '',
billing_email: email ?? event?.payerEmail ?? '',
- billing_phone:
- phone ?? event?.payerPhone?.replace( '/[() -]/g', '' ) ?? '',
+ billing_phone: phone,
billing_country: billing?.country ?? '',
billing_address_1: billing?.line1 ?? '',
billing_address_2: billing?.line2 ?? '',
@@ -54,6 +57,7 @@ export const normalizeOrderData = ( event, paymentMethodId ) => {
shipping_last_name:
shipping?.name?.split( ' ' )?.slice( 1 )?.join( ' ' ) ?? '',
shipping_company: shipping?.organization ?? '',
+ shipping_phone: phone,
shipping_country: shipping?.address?.country ?? '',
shipping_address_1: shipping?.address?.line1 ?? '',
shipping_address_2: shipping?.address?.line2 ?? '',
diff --git a/client/payment-request/utils/normalize.js b/client/payment-request/utils/normalize.js
index e55b78675c0..92a9414bc3b 100644
--- a/client/payment-request/utils/normalize.js
+++ b/client/payment-request/utils/normalize.js
@@ -32,7 +32,6 @@ export const normalizeOrderData = ( paymentData ) => {
paymentData?.paymentMethod?.billing_details?.name ??
paymentData.payerName;
const email = paymentData?.paymentMethod?.billing_details?.email ?? '';
- const phone = paymentData?.paymentMethod?.billing_details?.phone ?? '';
const billing = paymentData?.paymentMethod?.billing_details?.address ?? {};
const shipping = paymentData?.shippingAddress ?? {};
const fraudPreventionTokenValue = window.wcpayFraudPreventionToken ?? '';
@@ -44,14 +43,17 @@ export const normalizeOrderData = ( paymentData ) => {
paymentRequestType = 'google_pay';
}
+ const phone =
+ paymentData?.paymentMethod?.billing_details?.phone ??
+ paymentData?.payerPhone?.replace( '/[() -]/g', '' ) ??
+ '';
return {
billing_first_name:
name?.split( ' ' )?.slice( 0, 1 )?.join( ' ' ) ?? '',
billing_last_name: name?.split( ' ' )?.slice( 1 )?.join( ' ' ) || '-',
billing_company: billing?.organization ?? '',
billing_email: email ?? paymentData?.payerEmail ?? '',
- billing_phone:
- phone ?? paymentData?.payerPhone?.replace( '/[() -]/g', '' ) ?? '',
+ billing_phone: phone,
billing_country: billing?.country ?? '',
billing_address_1: billing?.line1 ?? '',
billing_address_2: billing?.line2 ?? '',
@@ -63,6 +65,7 @@ export const normalizeOrderData = ( paymentData ) => {
shipping_last_name:
shipping?.recipient?.split( ' ' )?.slice( 1 )?.join( ' ' ) ?? '',
shipping_company: shipping?.organization ?? '',
+ shipping_phone: phone,
shipping_country: shipping?.country ?? '',
shipping_address_1: shipping?.addressLine?.[ 0 ] ?? '',
shipping_address_2: shipping?.addressLine?.[ 1 ] ?? '',
diff --git a/client/tokenized-payment-request/transformers/stripe-to-wc.js b/client/tokenized-payment-request/transformers/stripe-to-wc.js
index b30c935b46d..f03a5a5d779 100644
--- a/client/tokenized-payment-request/transformers/stripe-to-wc.js
+++ b/client/tokenized-payment-request/transformers/stripe-to-wc.js
@@ -50,6 +50,10 @@ export const transformStripePaymentMethodForStoreApi = ( paymentData ) => {
const paymentRequestType =
paymentData.walletName === 'applePay' ? 'apple_pay' : 'google_pay';
+ const billingPhone =
+ paymentData.paymentMethod?.billing_details?.phone ??
+ paymentData.payerPhone?.replace( '/[() -]/g', '' ) ??
+ '';
return {
customer_note: paymentData.order_comments,
billing_address: {
@@ -66,13 +70,16 @@ export const transformStripePaymentMethodForStoreApi = ( paymentData ) => {
paymentData.paymentMethod?.billing_details?.email ??
paymentData.payerEmail ??
'',
- phone:
- paymentData.paymentMethod?.billing_details?.phone ??
- paymentData.payerPhone?.replace( '/[() -]/g', '' ) ??
- '',
+ phone: billingPhone,
},
// refreshing any shipping address data, now that the customer is placing the order.
- ...transformStripeShippingAddressForStoreApi( shipping ),
+ shipping_address: {
+ ...transformStripeShippingAddressForStoreApi( shipping )
+ .shipping_address,
+ // adding the phone number, because it might be needed.
+ // Stripe doesn't provide us with a different phone number for shipping, so we're going to use the same phone used for billing.
+ phone: billingPhone,
+ },
payment_method: 'woocommerce_payments',
payment_data: [
{
From 15fbb25444fa925ee0004843f0859ffe40de7e3b Mon Sep 17 00:00:00 2001
From: Alfredo Sumaran
Date: Thu, 6 Jun 2024 06:52:51 -0500
Subject: [PATCH 20/52] Revert "Disable WooPay for suspended and rejected
accounts (#8857)" (#8912)
---
...disable-woopay-rejected-suspended-accounts | 4 --
includes/class-wc-payments-features.php | 10 +----
includes/class-wc-payments.php | 7 +--
.../unit/test-class-wc-payments-features.php | 44 -------------------
4 files changed, 2 insertions(+), 63 deletions(-)
delete mode 100644 changelog/as-disable-woopay-rejected-suspended-accounts
diff --git a/changelog/as-disable-woopay-rejected-suspended-accounts b/changelog/as-disable-woopay-rejected-suspended-accounts
deleted file mode 100644
index 5c5d37f068f..00000000000
--- a/changelog/as-disable-woopay-rejected-suspended-accounts
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: fix
-
-Disable WooPay for suspended and rejected accounts.
diff --git a/includes/class-wc-payments-features.php b/includes/class-wc-payments-features.php
index 42e2a19e38b..568ee184b5a 100644
--- a/includes/class-wc-payments-features.php
+++ b/includes/class-wc-payments-features.php
@@ -210,15 +210,7 @@ public static function is_woopay_eligible() {
// read directly from cache, ignore cache expiration check.
$account = WC_Payments::get_database_cache()->get( WCPay\Database_Cache::ACCOUNT_KEY, true );
-
- $is_account_rejected = WC_Payments::get_account_service()->is_account_rejected();
-
- $is_account_under_review = WC_Payments::get_account_service()->is_account_under_review();
-
- return is_array( $account )
- && ( $account['platform_checkout_eligible'] ?? false )
- && ! $is_account_rejected
- && ! $is_account_under_review;
+ return is_array( $account ) && ( $account['platform_checkout_eligible'] ?? false );
}
/**
diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php
index cf872d3a4f7..e40ab0490a3 100644
--- a/includes/class-wc-payments.php
+++ b/includes/class-wc-payments.php
@@ -570,9 +570,7 @@ public static function init() {
// To avoid register the same hooks twice.
wcpay_get_container()->get( \WCPay\Internal\Service\DuplicatePaymentPreventionService::class )->init_hooks();
- // Defer registering the WooPay hooks. Later on, $wp_rewrite is used and causes a fatal error every time the account cache is refreshed,
- // given that $wp_rewrite is defined right after the `plugins_loaded` action is fired. See #8857.
- add_action( 'setup_theme', [ __CLASS__, 'maybe_register_woopay_hooks' ] );
+ self::maybe_register_woopay_hooks();
self::$apple_pay_registration = new WC_Payments_Apple_Pay_Registration( self::$api_client, self::$account, self::get_gateway() );
self::$apple_pay_registration->init_hooks();
@@ -1504,7 +1502,6 @@ public static function is_network_saved_cards_enabled() {
/**
* Registers woopay hooks if the woopay feature flag is enabled.
- * Removes WooPay webhooks if the merchant is not eligible.
*
* @return void
*/
@@ -1554,8 +1551,6 @@ function ( $container ) {
}
new WooPay_Order_Status_Sync( self::$api_client, self::$account );
- } else {
- WooPay_Order_Status_Sync::remove_webhook();
}
}
diff --git a/tests/unit/test-class-wc-payments-features.php b/tests/unit/test-class-wc-payments-features.php
index 4e6d2258c13..a27ff2c432f 100644
--- a/tests/unit/test-class-wc-payments-features.php
+++ b/tests/unit/test-class-wc-payments-features.php
@@ -18,13 +18,6 @@ class WC_Payments_Features_Test extends WCPAY_UnitTestCase {
*/
protected $mock_cache;
- /**
- * Mock WC_Payments_Account.
- *
- * @var WC_Payments_Account|MockObject
- */
- private $mock_wcpay_account;
-
const FLAG_OPTION_NAME_TO_FRONTEND_KEY_MAPPING = [
'_wcpay_feature_customer_multi_currency' => 'multiCurrency',
'_wcpay_feature_documents' => 'documents',
@@ -36,17 +29,6 @@ public function set_up() {
$this->_cache = WC_Payments::get_database_cache();
$this->mock_cache = $this->createMock( WCPay\Database_Cache::class );
WC_Payments::set_database_cache( $this->mock_cache );
-
- // Mock the WCPay Account class to make sure the account is not restricted by default.
- $this->mock_wcpay_account = $this->createMock( WC_Payments_Account::class );
- $this->mock_wcpay_account
- ->method( 'is_account_rejected' )
- ->willReturn( false );
- $this->mock_wcpay_account
- ->method( 'is_account_under_review' )
- ->willReturn( false );
-
- WC_Payments::set_account_service( $this->mock_wcpay_account );
}
public function tear_down() {
@@ -109,32 +91,6 @@ public function test_is_woopay_eligible_returns_false() {
$this->assertFalse( WC_Payments_Features::is_woopay_eligible() );
}
- public function test_is_woopay_eligible_when_account_is_suspended_returns_false() {
- $mock_wcpay_account = $this->createMock( WC_Payments_Account::class );
- $mock_wcpay_account
- ->method( 'is_account_under_review' )
- ->willReturn( true );
-
- WC_Payments::set_account_service( $mock_wcpay_account );
-
- $this->mock_cache->method( 'get' )->willReturn( [ 'platform_checkout_eligible' => true ] );
-
- $this->assertFalse( WC_Payments_Features::is_woopay_eligible() );
- }
-
- public function test_is_woopay_eligible_when_account_is_rejected_returns_false() {
- $mock_wcpay_account = $this->createMock( WC_Payments_Account::class );
- $mock_wcpay_account
- ->method( 'is_account_rejected' )
- ->willReturn( true );
-
- WC_Payments::set_account_service( $mock_wcpay_account );
-
- $this->mock_cache->method( 'get' )->willReturn( [ 'platform_checkout_eligible' => true ] );
-
- $this->assertFalse( WC_Payments_Features::is_woopay_eligible() );
- }
-
public function test_is_documents_section_enabled_returns_true_when_flag_is_true() {
$this->mock_cache->method( 'get' )->willReturn( [ 'is_documents_enabled' => true ] );
$this->assertTrue( WC_Payments_Features::is_documents_section_enabled() );
From 79bf183505d9db28afa3531bf0559719a4c64e70 Mon Sep 17 00:00:00 2001
From: Alfredo Sumaran
Date: Thu, 6 Jun 2024 09:40:24 -0500
Subject: [PATCH 21/52] Fix ECE not working without WooPay (#8905)
---
changelog/as-fix-ece-with-woopay | 4 ++++
.../blocks/components/express-checkout-container.js | 8 +++++---
client/express-checkout/blocks/index.js | 8 +-------
3 files changed, 10 insertions(+), 10 deletions(-)
create mode 100644 changelog/as-fix-ece-with-woopay
diff --git a/changelog/as-fix-ece-with-woopay b/changelog/as-fix-ece-with-woopay
new file mode 100644
index 00000000000..5bf422d22ea
--- /dev/null
+++ b/changelog/as-fix-ece-with-woopay
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Fix ECE not working without WooPay.
diff --git a/client/express-checkout/blocks/components/express-checkout-container.js b/client/express-checkout/blocks/components/express-checkout-container.js
index e25960c03cd..9285227bf63 100644
--- a/client/express-checkout/blocks/components/express-checkout-container.js
+++ b/client/express-checkout/blocks/components/express-checkout-container.js
@@ -19,9 +19,11 @@ const ExpressCheckoutContainer = ( props ) => {
};
return (
-
-
-
+
+
+
+
+
);
};
diff --git a/client/express-checkout/blocks/index.js b/client/express-checkout/blocks/index.js
index c7aac8d9b5c..79e54d80a0b 100644
--- a/client/express-checkout/blocks/index.js
+++ b/client/express-checkout/blocks/index.js
@@ -1,5 +1,3 @@
-/* global wcpayConfig, wcpayExpressCheckoutParams */
-
/**
* Internal dependencies
*/
@@ -26,11 +24,7 @@ const expressCheckoutElementPaymentMethod = ( api ) => ( {
return false;
}
- if ( typeof wcpayConfig !== 'undefined' ) {
- return wcpayConfig.isExpressCheckoutElementEnabled;
- }
-
- return false;
+ return true;
},
} );
From 3bb0caa396d3ed85b4372f1ce6f59e8a034c1a5d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?C=C3=A9sar=20Costa?=
<10233985+cesarcosta99@users.noreply.github.com>
Date: Thu, 6 Jun 2024 11:42:41 -0300
Subject: [PATCH 22/52] Add the WooPay Direct Checkout flow to the classic mini
cart widget (#8903)
---
...d-2688-woopay-direct-checkout-to-mini-cart | 4 ++
.../direct-checkout/woopay-direct-checkout.js | 5 ++
...ass-wc-payments-woopay-direct-checkout.php | 24 ++++++-
includes/class-wc-payments.php | 67 +++++++++++--------
4 files changed, 71 insertions(+), 29 deletions(-)
create mode 100644 changelog/add-2688-woopay-direct-checkout-to-mini-cart
diff --git a/changelog/add-2688-woopay-direct-checkout-to-mini-cart b/changelog/add-2688-woopay-direct-checkout-to-mini-cart
new file mode 100644
index 00000000000..02a5d4a0667
--- /dev/null
+++ b/changelog/add-2688-woopay-direct-checkout-to-mini-cart
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add the WooPay Direct Checkout flow to the classic mini cart widget.
diff --git a/client/checkout/woopay/direct-checkout/woopay-direct-checkout.js b/client/checkout/woopay/direct-checkout/woopay-direct-checkout.js
index b44db8d07e6..f52d5e03131 100644
--- a/client/checkout/woopay/direct-checkout/woopay-direct-checkout.js
+++ b/client/checkout/woopay/direct-checkout/woopay-direct-checkout.js
@@ -21,6 +21,8 @@ class WooPayDirectCheckout {
'.wp-block-woocommerce-proceed-to-checkout-block',
BLOCKS_MINI_CART_PROCEED_BUTTON:
'a.wp-block-woocommerce-mini-cart-checkout-button-block',
+ CLASSIC_MINI_CART_PROCEED_BUTTON:
+ '.widget_shopping_cart a.button.checkout',
};
/**
@@ -213,6 +215,9 @@ class WooPayDirectCheckout {
addElementBySelector(
this.redirectElements.BLOCKS_CART_PROCEED_BUTTON
);
+ addElementBySelector(
+ this.redirectElements.CLASSIC_MINI_CART_PROCEED_BUTTON
+ );
return elements;
}
diff --git a/includes/class-wc-payments-woopay-direct-checkout.php b/includes/class-wc-payments-woopay-direct-checkout.php
index 1cf3fbd35cb..6c2d170f4fe 100644
--- a/includes/class-wc-payments-woopay-direct-checkout.php
+++ b/includes/class-wc-payments-woopay-direct-checkout.php
@@ -6,6 +6,9 @@
* @package WooCommerce\Payments
*/
+use WCPay\WooPay\WooPay_Session;
+use WCPay\WooPay\WooPay_Utilities;
+
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
@@ -71,6 +74,13 @@ public function scripts() {
return;
}
+ // Enqueue the WCPay common config script only if it hasn't been enqueued yet.
+ // This may happen when Direct Checkout is being enqueued on pages that are not the cart page,
+ // such as the home and shop pages.
+ if ( function_exists( 'did_filter' ) && did_filter( 'wcpay_payment_fields_js_config' ) === 0 ) {
+ WC_Payments::enqueue_woopay_common_config_script();
+ }
+
WC_Payments::register_script_with_dependencies( 'WCPAY_WOOPAY_DIRECT_CHECKOUT', 'dist/woopay-direct-checkout' );
$direct_checkout_settings = [
@@ -94,11 +104,14 @@ public function scripts() {
* - The current page is the cart page.
* - The current page has a cart block.
* - The current page has the blocks mini cart widget, i.e 'woocommerce_blocks_cart_enqueue_data' has been fired.
+ * - The current page has the cart fragments script enqueued. which is enqueued by the shortcode mini cart widget.
*
* @return bool True if the scripts should be enqueued, false otherwise.
*/
private function should_enqueue_scripts(): bool {
- return $this->is_cart_page() || did_action( 'woocommerce_blocks_cart_enqueue_data' ) > 0;
+ return $this->is_cart_page()
+ || did_action( 'woocommerce_blocks_cart_enqueue_data' ) > 0
+ || ( wp_script_is( 'wc-cart-fragments', 'enqueued' ) && ! $this->is_checkout_page() );
}
/**
@@ -110,6 +123,15 @@ private function is_cart_page(): bool {
return is_cart() || has_block( 'woocommerce/cart' );
}
+ /**
+ * Check if the current page is the checkout page.
+ *
+ * @return bool True if the current page is the checkout page, false otherwise.
+ */
+ private function is_checkout_page(): bool {
+ return is_checkout() || has_block( 'woocommerce/checkout' );
+ }
+
/**
* Check if the current page is the product page.
*
diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php
index e40ab0490a3..2a20910b787 100644
--- a/includes/class-wc-payments.php
+++ b/includes/class-wc-payments.php
@@ -1569,41 +1569,52 @@ public static function maybe_init_woopay_direct_checkout() {
}
/**
- * Enqueues the common config script if the express checkout button is disabled on the cart page.
+ * Validates whether the common config script should be enqueued and enqueues it.
+ *
+ * If the express checkout button is disabled on the cart page, the common config
+ * script needs to be enqueued to ensure `wcpayConfig` is available on the cart page.
*
* @return void
*/
- public static function enqueue_woopay_common_config_script() {
+ public static function validate_and_enqueue_woopay_common_config_script() {
$is_express_button_disabled_on_cart = self::get_express_checkout_helper()->is_cart()
&& ! self::get_express_checkout_helper()->is_available_at( 'cart', WC_Payments_WooPay_Button_Handler::BUTTON_LOCATIONS );
- // If the express checkout button is disabled on the cart page, the common config
- // script needs to be enqueued to ensure wcpayConfig is available on the cart page.
+
if ( $is_express_button_disabled_on_cart ) {
- try {
- // is_test() throws if the class 'Mode' has not been initialized.
- $is_test_mode = self::mode()->is_test();
- } catch ( Exception $e ) {
- // Default to false if the class 'Mode' has not been initialized.
- $is_test_mode = false;
- }
+ self::enqueue_woopay_common_config_script();
+ }
+ }
- wp_register_script( 'WCPAY_WOOPAY_COMMON_CONFIG', '', [], WCPAY_VERSION_NUMBER, false );
- wp_localize_script(
- 'WCPAY_WOOPAY_COMMON_CONFIG',
- 'wcpayConfig',
- [
- 'woopayHost' => WooPay_Utilities::get_woopay_url(),
- 'testMode' => $is_test_mode,
- 'wcAjaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ),
- 'woopaySessionNonce' => wp_create_nonce( 'woopay_session_nonce' ),
- 'isWooPayDirectCheckoutEnabled' => WC_Payments_Features::is_woopay_direct_checkout_enabled(),
- 'platformTrackerNonce' => wp_create_nonce( 'platform_tracks_nonce' ),
- 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
- 'woopayMinimumSessionData' => WooPay_Session::get_woopay_minimum_session_data(),
- ]
- );
- wp_enqueue_script( 'WCPAY_WOOPAY_COMMON_CONFIG' );
+ /**
+ * Enqueues the common config script.
+ *
+ * @return void
+ */
+ public static function enqueue_woopay_common_config_script() {
+ try {
+ // is_test() throws if the class 'Mode' has not been initialized.
+ $is_test_mode = self::mode()->is_test();
+ } catch ( Exception $e ) {
+ // Default to false if the class 'Mode' has not been initialized.
+ $is_test_mode = false;
}
+
+ wp_register_script( 'WCPAY_WOOPAY_COMMON_CONFIG', '', [], WCPAY_VERSION_NUMBER, false );
+ wp_localize_script(
+ 'WCPAY_WOOPAY_COMMON_CONFIG',
+ 'wcpayConfig',
+ [
+ 'woopayHost' => WooPay_Utilities::get_woopay_url(),
+ 'testMode' => $is_test_mode,
+ 'wcAjaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ),
+ 'woopaySessionNonce' => wp_create_nonce( 'woopay_session_nonce' ),
+ 'isWooPayDirectCheckoutEnabled' => WC_Payments_Features::is_woopay_direct_checkout_enabled(),
+ 'platformTrackerNonce' => wp_create_nonce( 'platform_tracks_nonce' ),
+ 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
+ 'woopayMinimumSessionData' => WooPay_Session::get_woopay_minimum_session_data(),
+ ]
+ );
+ wp_enqueue_script( 'WCPAY_WOOPAY_COMMON_CONFIG' );
}
/**
@@ -1619,7 +1630,7 @@ public static function maybe_enqueue_woopay_common_config_script( $should_enqueu
return;
}
- add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue_woopay_common_config_script' ] );
+ add_action( 'wp_enqueue_scripts', [ __CLASS__, 'validate_and_enqueue_woopay_common_config_script' ] );
}
/**
From 4475663a55dfa06cd6f99c2b1114dc2bf9f6ab1b Mon Sep 17 00:00:00 2001
From: Francesco
Date: Thu, 6 Jun 2024 16:43:47 +0200
Subject: [PATCH 23/52] fix: itemized totals & pending amount on tokenized cart
(#8916)
---
...fix-pending-amount-missing-itemized-totals | 4 +++
.../transformers/wc-to-stripe.js | 31 +++++++++++++------
2 files changed, 25 insertions(+), 10 deletions(-)
create mode 100644 changelog/fix-pending-amount-missing-itemized-totals
diff --git a/changelog/fix-pending-amount-missing-itemized-totals b/changelog/fix-pending-amount-missing-itemized-totals
new file mode 100644
index 00000000000..e863c29b2e5
--- /dev/null
+++ b/changelog/fix-pending-amount-missing-itemized-totals
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+fix: itemized totals & pending amount on tokenized cart
diff --git a/client/tokenized-payment-request/transformers/wc-to-stripe.js b/client/tokenized-payment-request/transformers/wc-to-stripe.js
index 1d04026e7f5..a2e841b66b6 100644
--- a/client/tokenized-payment-request/transformers/wc-to-stripe.js
+++ b/client/tokenized-payment-request/transformers/wc-to-stripe.js
@@ -13,26 +13,37 @@ import { __ } from '@wordpress/i18n';
export const transformCartDataForDisplayItems = ( cartData ) => {
const displayItems = cartData.items.map( ( item ) => ( {
amount: parseInt( item.prices.price, 10 ),
- // TODO: should we also add variation attributes?
- label: [ item.name, item.quantity > 1 && ` (x${ item.quantity })` ]
+ label: [
+ item.name,
+ item.quantity > 1 && `(x${ item.quantity })`,
+ item.variation &&
+ item.variation
+ .map(
+ ( variation ) =>
+ `${ variation.attribute }: ${ variation.value }`
+ )
+ .join( ', ' ),
+ ]
.filter( Boolean )
- .join( '' ),
- pending: true,
+ .join( ' ' ),
} ) );
- if ( cartData.totals.total_tax ) {
+ const taxAmount = parseInt( cartData.totals.total_tax || '0', 10 );
+ if ( taxAmount ) {
displayItems.push( {
- amount: parseInt( cartData.totals.total_tax, 10 ),
+ amount: taxAmount,
label: __( 'Tax', 'woocommerce-payments' ),
- pending: true,
} );
}
- if ( cartData.totals.total_shipping ) {
+ const shippingAmount = parseInt(
+ cartData.totals.total_shipping || '0',
+ 10
+ );
+ if ( shippingAmount ) {
displayItems.push( {
- amount: parseInt( cartData.totals.total_shipping, 10 ),
+ amount: shippingAmount,
label: __( 'Shipping', 'woocommerce-payments' ),
- pending: true,
} );
}
From 6f33f984f0b13750220299ee914aa91044976ea4 Mon Sep 17 00:00:00 2001
From: Timur Karimov
Date: Thu, 6 Jun 2024 16:48:06 +0200
Subject: [PATCH 24/52] misc: remove redundant wrapper for getting account
default currency (#8302)
---
changelog/misc-remove-unnecessary-wrapper | 4 ++++
includes/class-wc-payment-gateway-wcpay.php | 11 +----------
2 files changed, 5 insertions(+), 10 deletions(-)
create mode 100644 changelog/misc-remove-unnecessary-wrapper
diff --git a/changelog/misc-remove-unnecessary-wrapper b/changelog/misc-remove-unnecessary-wrapper
new file mode 100644
index 00000000000..fb89b07d76e
--- /dev/null
+++ b/changelog/misc-remove-unnecessary-wrapper
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Remove redundant wrapper around method invocation.
diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php
index fb5fa1633c3..d6fe41edb75 100644
--- a/includes/class-wc-payment-gateway-wcpay.php
+++ b/includes/class-wc-payment-gateway-wcpay.php
@@ -2592,15 +2592,6 @@ public function get_account_statement_descriptor_kana( string $empty_value = ''
return $empty_value;
}
- /**
- * Gets account default currency.
- *
- * @return string Currency code.
- */
- public function get_account_default_currency(): string {
- return $this->account->get_account_default_currency();
- }
-
/**
* Gets connected account business name.
*
@@ -2790,7 +2781,7 @@ public function get_account_domestic_currency(): string {
$merchant_country
)
);
- return $this->get_account_default_currency();
+ return $this->account->get_account_default_currency();
}
return $country_locale_data['currency_code'];
From 95b74a487a7811604097acfd67dac428bfc0e149 Mon Sep 17 00:00:00 2001
From: Jessy Pappachan <32092402+jessy-p@users.noreply.github.com>
Date: Thu, 6 Jun 2024 22:16:55 +0530
Subject: [PATCH 25/52] Payment Activity data refreshes with currency selection
(#8915)
Co-authored-by: Jessy
---
...dd-8490-payment-activity-currency-selector | 5 +++++
client/components/payment-activity/index.tsx | 7 ++++---
client/data/payment-activity/hooks.ts | 19 ++++++++++++-------
3 files changed, 21 insertions(+), 10 deletions(-)
create mode 100644 changelog/add-8490-payment-activity-currency-selector
diff --git a/changelog/add-8490-payment-activity-currency-selector b/changelog/add-8490-payment-activity-currency-selector
new file mode 100644
index 00000000000..b3dafafae14
--- /dev/null
+++ b/changelog/add-8490-payment-activity-currency-selector
@@ -0,0 +1,5 @@
+Significance: patch
+Type: fix
+Comment: Behind a feature flag. Payment Activity Card updates according to currency selection.
+
+
diff --git a/client/components/payment-activity/index.tsx b/client/components/payment-activity/index.tsx
index 4c08aab66c2..33d89c418a6 100644
--- a/client/components/payment-activity/index.tsx
+++ b/client/components/payment-activity/index.tsx
@@ -16,6 +16,7 @@ import PaymentActivityDataComponent from './payment-activity-data';
import Survey from './survey';
import { WcPayOverviewSurveyContextProvider } from './survey/context';
import { usePaymentActivityData } from 'wcpay/data';
+import { useSelectedCurrency } from 'wcpay/overview/hooks';
import type { DateRange } from './types';
import './style.scss';
@@ -65,10 +66,10 @@ const PaymentActivity: React.FC = () => {
const isOverviewSurveySubmitted =
wcpaySettings.isOverviewSurveySubmitted ?? false;
+ const { selectedCurrency } = useSelectedCurrency();
+
const { paymentActivityData, isLoading } = usePaymentActivityData( {
- // In future this will be bound to currency picker via useSelectedCurrency().
- // Can hard-code other store settings to test.
- currency: wcpaySettings.accountDefaultCurrency,
+ currency: selectedCurrency ?? wcpaySettings.accountDefaultCurrency,
...getDateRange(),
timezone: moment( new Date() ).format( 'Z' ),
} );
diff --git a/client/data/payment-activity/hooks.ts b/client/data/payment-activity/hooks.ts
index c737c3a87c6..651c1dbd60f 100644
--- a/client/data/payment-activity/hooks.ts
+++ b/client/data/payment-activity/hooks.ts
@@ -14,11 +14,16 @@ import { PaymentActivityState, PaymentActivityQuery } from './types';
export const usePaymentActivityData = (
query: PaymentActivityQuery
): PaymentActivityState =>
- useSelect( ( select ) => {
- const { getPaymentActivityData, isResolving } = select( STORE_NAME );
+ useSelect(
+ ( select ) => {
+ const { getPaymentActivityData, isResolving } = select(
+ STORE_NAME
+ );
- return {
- paymentActivityData: getPaymentActivityData( query ),
- isLoading: isResolving( 'getPaymentActivityData', [ query ] ),
- };
- }, [] );
+ return {
+ paymentActivityData: getPaymentActivityData( query ),
+ isLoading: isResolving( 'getPaymentActivityData', [ query ] ),
+ };
+ },
+ [ query.currency ]
+ );
From 597aea4b67ee536c19fa188ad354e8a53bccabf0 Mon Sep 17 00:00:00 2001
From: Eric Jinks <3147296+Jinksi@users.noreply.github.com>
Date: Fri, 7 Jun 2024 14:06:35 +1000
Subject: [PATCH 26/52] Add a new `InlineLabelSelect` component for WCPay admin
filter select inputs (with inline label) (#8896)
Co-authored-by: Jessy Pappachan <32092402+jessy-p@users.noreply.github.com>
---
...788-select-component-for-currency-switcher | 4 +
.../components/inline-label-select/index.tsx | 250 ++++++++++++++
.../components/inline-label-select/style.scss | 95 ++++++
.../test/__snapshots__/index.test.tsx.snap | 312 ++++++++++++++++++
.../inline-label-select/test/index.test.tsx | 105 ++++++
5 files changed, 766 insertions(+)
create mode 100644 changelog/add-8788-select-component-for-currency-switcher
create mode 100644 client/components/inline-label-select/index.tsx
create mode 100644 client/components/inline-label-select/style.scss
create mode 100644 client/components/inline-label-select/test/__snapshots__/index.test.tsx.snap
create mode 100644 client/components/inline-label-select/test/index.test.tsx
diff --git a/changelog/add-8788-select-component-for-currency-switcher b/changelog/add-8788-select-component-for-currency-switcher
new file mode 100644
index 00000000000..a82d54d487d
--- /dev/null
+++ b/changelog/add-8788-select-component-for-currency-switcher
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add new select component to be used for reporting filters, e.g. Payments overview currency select
diff --git a/client/components/inline-label-select/index.tsx b/client/components/inline-label-select/index.tsx
new file mode 100644
index 00000000000..df1233ef890
--- /dev/null
+++ b/client/components/inline-label-select/index.tsx
@@ -0,0 +1,250 @@
+/**
+ * This is a copy of Gutenberg's CustomSelectControl component, found here:
+ * https://github.com/WordPress/gutenberg/tree/7aa042605ff42bb437e650c39132c0aa8eb4ef95/packages/components/src/custom-select-control
+ *
+ * It has been forked from the existing WooPayments copy of this component (client/components/custom-select-control)
+ * to match this specific select input design with an inline label and option hints.
+ */
+
+/**
+ * External Dependencies
+ */
+import React from 'react';
+import { Button } from '@wordpress/components';
+import { check, chevronDown, Icon } from '@wordpress/icons';
+import { useCallback } from '@wordpress/element';
+import classNames from 'classnames';
+import { __, sprintf } from '@wordpress/i18n';
+import { useSelect, UseSelectState } from 'downshift';
+
+/**
+ * Internal Dependencies
+ */
+import './style.scss';
+
+export interface SelectItem {
+ /** The unique key for the item. */
+ key: string;
+ /** The display name of the item. */
+ name?: string;
+ /** Descriptive hint for the item, displayed to the right of the name. */
+ hint?: string;
+ /** Additional class name to apply to the item. */
+ className?: string;
+ /** Additional inline styles to apply to the item. */
+ style?: React.CSSProperties;
+}
+
+export interface ControlProps< SelectItemType > {
+ /** The name attribute for the select input. */
+ name?: string;
+ /** Additional class name to apply to the select control. */
+ className?: string;
+ /** The label for the select control. */
+ label: string;
+ /** The ID of an element that describes the select control. */
+ describedBy?: string;
+ /** A list of options/items for the select control. */
+ options: SelectItemType[];
+ /** The currently selected option/item. */
+ value?: SelectItemType | null;
+ /** A placeholder to display when no item is selected. */
+ placeholder?: string;
+ /** Callback function to run when the selected item changes. */
+ onChange?: ( changes: Partial< UseSelectState< SelectItemType > > ) => void;
+ /** A function to render the children of the item. Takes an item as an argument, must return a JSX element. */
+ children?: ( item: SelectItemType ) => JSX.Element;
+}
+
+/**
+ * Converts a select option/item object to a string.
+ */
+const itemToString = ( item: { name?: string } | null ) => item?.name || '';
+
+/**
+ * State reducer for the select component.
+ * This is needed so that in Windows, where the menu does not necessarily open on
+ * key up/down, you can still switch between options with the menu closed.
+ */
+const stateReducer = (
+ { selectedItem }: any,
+ { type, changes, props: { items } }: any
+) => {
+ switch ( type ) {
+ case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowDown:
+ // If we already have a selected item, try to select the next one,
+ // without circular navigation. Otherwise, select the first item.
+ return {
+ selectedItem:
+ items[
+ selectedItem
+ ? Math.min(
+ items.indexOf( selectedItem ) + 1,
+ items.length - 1
+ )
+ : 0
+ ],
+ };
+ case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowUp:
+ // If we already have a selected item, try to select the previous one,
+ // without circular navigation. Otherwise, select the last item.
+ return {
+ selectedItem:
+ items[
+ selectedItem
+ ? Math.max( items.indexOf( selectedItem ) - 1, 0 )
+ : items.length - 1
+ ],
+ };
+ default:
+ return changes;
+ }
+};
+
+/**
+ * InlineLabelSelect component.
+ * A select control with a list of options, inline label, and option hints.
+ */
+function InlineLabelSelect< ItemType extends SelectItem >( {
+ name,
+ className,
+ label,
+ describedBy,
+ options: items,
+ onChange: onSelectedItemChange,
+ value,
+ placeholder,
+ children,
+}: ControlProps< ItemType > ): JSX.Element {
+ const {
+ getLabelProps,
+ getToggleButtonProps,
+ getMenuProps,
+ getItemProps,
+ isOpen,
+ highlightedIndex,
+ selectedItem,
+ } = useSelect( {
+ initialSelectedItem: items[ 0 ],
+ items,
+ itemToString,
+ onSelectedItemChange,
+ selectedItem: value || ( {} as ItemType ),
+ stateReducer,
+ } );
+
+ const itemString = itemToString( selectedItem );
+
+ function getDescribedBy() {
+ if ( describedBy ) {
+ return describedBy;
+ }
+
+ if ( ! itemString ) {
+ return __( 'No selection' );
+ }
+
+ // translators: %s: The selected option.
+ return sprintf( __( 'Currently selected: %s' ), itemString );
+ }
+
+ const menuProps = getMenuProps( {
+ className: 'wcpay-filter components-custom-select-control__menu',
+ 'aria-hidden': ! isOpen,
+ } );
+
+ const onKeyDownHandler = useCallback(
+ ( e ) => {
+ e.stopPropagation();
+ menuProps?.onKeyDown?.( e );
+ },
+ [ menuProps ]
+ );
+
+ // We need this here, because the null active descendant is not fully ARIA compliant.
+ if (
+ menuProps[ 'aria-activedescendant' ]?.startsWith( 'downshift-null' )
+ ) {
+ delete menuProps[ 'aria-activedescendant' ];
+ }
+ return (
+
+
+ { /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */ }
+
+ { isOpen &&
+ items.map( ( item, index ) => (
+ // eslint-disable-next-line react/jsx-key
+ -
+
+ { children ? children( item ) : item.name }
+ { item.hint && (
+
+ { item.hint }
+
+ ) }
+
+ ) ) }
+
+
+ );
+}
+
+export default InlineLabelSelect;
diff --git a/client/components/inline-label-select/style.scss b/client/components/inline-label-select/style.scss
new file mode 100644
index 00000000000..41f1cbf65fb
--- /dev/null
+++ b/client/components/inline-label-select/style.scss
@@ -0,0 +1,95 @@
+.wcpay-filter.components-custom-select-control {
+ font-size: 13px;
+ color: $gray-900;
+
+ .wcpay-filter.components-custom-select-control__label {
+ display: inline-block;
+ margin-bottom: 0;
+ color: $wp-gray-40;
+ margin-right: 0.2em;
+ white-space: nowrap;
+
+ &::after {
+ content: ':';
+ }
+ }
+
+ .wcpay-filter.components-custom-select-control__item {
+ padding: $gap-small;
+ margin: 0;
+ line-height: initial;
+ grid-template-columns: auto auto auto;
+ justify-content: start;
+ white-space: nowrap;
+ border-radius: 2px;
+
+ &.is-highlighted {
+ background: $gray-0;
+ }
+ }
+
+ .wcpay-filter.components-custom-select-control__item-icon {
+ margin-right: 0.2em;
+ }
+
+ .wcpay-filter.components-custom-select-control__item-hint {
+ margin-left: 1.8em;
+ text-align: left;
+ color: $gray-700;
+ }
+
+ button.wcpay-filter.components-custom-select-control__button {
+ width: 100%;
+ background-color: #fff;
+ margin: 0 1px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 6px 8px;
+ border: 1px solid $gray-200;
+ font-size: 13px;
+
+ &:hover,
+ &:focus {
+ color: initial;
+ background-color: $gray-0;
+ box-shadow: none;
+ }
+
+ &.placeholder {
+ color: $gray-50;
+ }
+
+ .wcpay-filter.components-custom-select-control__button-value {
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow-x: hidden;
+ }
+
+ svg {
+ fill: initial;
+ width: 18px;
+ flex: 0 0 18px;
+ }
+
+ &[aria-expanded='true'] {
+ .wcpay-filter.components-custom-select-control__button-value {
+ visibility: hidden;
+ }
+ .components-custom-select-control__label::after {
+ visibility: hidden;
+ }
+ }
+
+ @media screen and ( max-width: 782px ) {
+ font-size: 16px;
+ }
+ }
+
+ .wcpay-filter.components-custom-select-control__menu {
+ margin: -1px 1px;
+ border-color: $gray-300;
+ max-height: 300px;
+ padding: $grid-unit-10 $grid-unit-15;
+ }
+}
diff --git a/client/components/inline-label-select/test/__snapshots__/index.test.tsx.snap b/client/components/inline-label-select/test/__snapshots__/index.test.tsx.snap
new file mode 100644
index 00000000000..dd8eb1be580
--- /dev/null
+++ b/client/components/inline-label-select/test/__snapshots__/index.test.tsx.snap
@@ -0,0 +1,312 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`InlineLabelSelect renders options 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`InlineLabelSelect renders options with custom children 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`InlineLabelSelect renders with placeholder 1`] = `
+
+
+
+
+
+
+`;
diff --git a/client/components/inline-label-select/test/index.test.tsx b/client/components/inline-label-select/test/index.test.tsx
new file mode 100644
index 00000000000..be1f671e862
--- /dev/null
+++ b/client/components/inline-label-select/test/index.test.tsx
@@ -0,0 +1,105 @@
+/** @format */
+
+/**
+ * External dependencies
+ */
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import user from '@testing-library/user-event';
+
+/**
+ * Internal dependencies
+ */
+import InlineLabelSelect from '..';
+
+interface Item {
+ key: string;
+ name: string;
+ icon?: string;
+ hint?: string;
+}
+
+const options: Item[] = [
+ {
+ key: 'EUR',
+ name: 'EUR €',
+ icon: '💶',
+ },
+ {
+ key: 'JPY',
+ name: 'JPY ¥',
+ icon: '💴',
+ hint: 'Japanese Yen',
+ },
+ {
+ key: 'GBP',
+ name: 'GBP £',
+ icon: '💷',
+ hint: 'British Pound',
+ },
+];
+
+describe( 'InlineLabelSelect', () => {
+ test( 'renders options', () => {
+ const { container, getByText } = render(
+
+ );
+
+ user.click( screen.getByRole( 'button' ) );
+
+ // Option names should be visible.
+ getByText( 'JPY ¥' );
+ // Hints should be visible.
+ getByText( 'British Pound' );
+
+ expect( container ).toMatchSnapshot();
+ } );
+
+ test( 'renders options with custom children', () => {
+ const { container, getByText } = render(
+ (
+ <>
+ { item.icon }
+ { item.name }
+ >
+ ) }
+ />
+ );
+
+ user.click( screen.getByRole( 'button' ) );
+
+ // Option icons should be visible.
+ getByText( '💴' );
+
+ user.click( screen.getByRole( 'button' ) );
+
+ expect( container ).toMatchSnapshot();
+ } );
+
+ test( 'renders with placeholder', () => {
+ const { container } = render(
+
+ );
+
+ user.click( screen.getByRole( 'button' ) );
+
+ expect( container ).toMatchSnapshot();
+ } );
+} );
From a719acea46320614d408304f6847a51b6db70579 Mon Sep 17 00:00:00 2001
From: Eric Jinks <3147296+Jinksi@users.noreply.github.com>
Date: Fri, 7 Jun 2024 15:17:57 +1000
Subject: [PATCH 27/52] Reorganise payments overview currency switcher, welcome
message and balances card (#8791)
Co-authored-by: Jessy Pappachan <32092402+jessy-p@users.noreply.github.com>
---
...ate-8765-payments-overview-currency-select | 4 +
client/components/account-balances/index.tsx | 200 ++++++------------
client/components/account-balances/style.scss | 7 +-
.../test/__snapshots__/index.test.tsx.snap | 195 ++++++++---------
.../account-balances/test/index.test.tsx | 101 +--------
client/components/account-balances/utils.ts | 18 --
client/components/welcome/currency-select.tsx | 108 ++++++++++
client/components/welcome/index.tsx | 28 +--
client/components/welcome/style.scss | 18 +-
client/components/welcome/test/index.test.tsx | 158 +++++++++++++-
client/overview/index.js | 65 +++---
tests/e2e-pw/specs/basic.spec.ts | 5 +-
12 files changed, 488 insertions(+), 419 deletions(-)
create mode 100644 changelog/update-8765-payments-overview-currency-select
delete mode 100644 client/components/account-balances/utils.ts
create mode 100644 client/components/welcome/currency-select.tsx
diff --git a/changelog/update-8765-payments-overview-currency-select b/changelog/update-8765-payments-overview-currency-select
new file mode 100644
index 00000000000..f2808903a20
--- /dev/null
+++ b/changelog/update-8765-payments-overview-currency-select
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Update the Payments Overview screen with a new currency selection UI for stores with multiple deposit currencies
diff --git a/client/components/account-balances/index.tsx b/client/components/account-balances/index.tsx
index 6d65e661104..53010482c0f 100644
--- a/client/components/account-balances/index.tsx
+++ b/client/components/account-balances/index.tsx
@@ -2,14 +2,13 @@
* External dependencies
*/
import * as React from 'react';
-import { Flex, TabPanel } from '@wordpress/components';
+import { Flex } from '@wordpress/components';
/**
* Internal dependencies
*/
import { useAllDepositsOverviews } from 'wcpay/data';
import { useSelectedCurrency } from 'wcpay/overview/hooks';
-import { getCurrencyTabTitle } from './utils';
import BalanceBlock from './balance-block';
import {
TotalBalanceTooltip,
@@ -17,99 +16,55 @@ import {
} from './balance-tooltip';
import { fundLabelStrings } from './strings';
import InstantDepositButton from 'deposits/instant-deposits';
-import { recordEvent } from 'tracks';
import type * as AccountOverview from 'wcpay/types/account-overview';
import './style.scss';
/**
- * BalanceTabProps
- *
- * @typedef {Object} BalanceTab
- *
- * @param {string} name Name of the tab.
- * @param {string} title Title of the tab.
- * @param {string} currencyCode Currency code of the tab.
- * @param {number} availableFunds Available funds of the tab.
- * @param {number} pendingFunds Pending funds of the tab.
- * @param {number} delayDays The account's pending period in days.
- */
-type BalanceTabProps = {
- name: string;
- title: string;
- currencyCode: string;
- availableFunds: number;
- pendingFunds: number;
- delayDays: number;
- instantBalance?: AccountOverview.InstantBalance;
-};
-
-/**
- * Renders an account balances panel with tab navigation for each deposit currency.
- *
- * @return {JSX.Element} Rendered balances panel with tab navigation for each currency.
+ * Renders account balances for the selected currency.
*/
const AccountBalances: React.FC = () => {
const { overviews, isLoading } = useAllDepositsOverviews();
- const { selectedCurrency, setSelectedCurrency } = useSelectedCurrency();
+ const { selectedCurrency } = useSelectedCurrency();
if ( ! isLoading && overviews.currencies.length === 0 ) {
return null;
}
- const onTabSelect = ( tabName: BalanceTabProps[ 'name' ] ) => {
- setSelectedCurrency( tabName );
- recordEvent( 'wcpay_overview_balances_currency_tab_click', {
- selected_currency: tabName,
- } );
- };
-
if ( isLoading ) {
- // While the data is loading, we show a loading currency tab.
- const loadingTabs: BalanceTabProps[] = [
- {
- name: 'loading',
- title: getCurrencyTabTitle(
- wcpaySettings.accountDefaultCurrency
- ),
- currencyCode: wcpaySettings.accountDefaultCurrency,
- availableFunds: 0,
- pendingFunds: 0,
- delayDays: 0,
- },
- ];
+ // While the data is loading, we show a loading state for the balances.
+ const loadingData = {
+ name: 'loading',
+ currencyCode: wcpaySettings.accountDefaultCurrency,
+ availableFunds: 0,
+ pendingFunds: 0,
+ delayDays: 0,
+ };
+
return (
-
- { ( tab: BalanceTabProps ) => (
-
-
-
-
- ) }
-
+
+
+
+
);
}
const { currencies, account } = overviews;
- const depositCurrencyTabs = currencies.map(
+ const depositCurrencyOverviews = currencies.map(
( overview: AccountOverview.Overview ) => ( {
name: overview.currency,
- title: getCurrencyTabTitle( overview.currency ),
currencyCode: overview.currency,
availableFunds: overview.available?.amount ?? 0,
pendingFunds: overview.pending?.amount ?? 0,
@@ -118,65 +73,48 @@ const AccountBalances: React.FC = () => {
} )
);
- // Selected currency is not valid if it is not in the list of deposit currencies.
- const isSelectedCurrencyValid =
- selectedCurrency &&
- depositCurrencyTabs.some( ( tab ) => tab.name === selectedCurrency );
+ const selectedOverview =
+ depositCurrencyOverviews.find(
+ ( overview ) => overview.name === selectedCurrency
+ ) || depositCurrencyOverviews[ 0 ];
- return (
-
- { ( tab: BalanceTabProps ) => {
- const totalBalance = tab.availableFunds + tab.pendingFunds;
+ const totalBalance =
+ selectedOverview.availableFunds + selectedOverview.pendingFunds;
- return (
- <>
-
-
- }
- />
-
- }
- />
-
- { tab.instantBalance && tab.instantBalance.amount > 0 && (
-
-
-
- ) }
- >
- );
- } }
-
+ return (
+ <>
+
+ }
+ />
+
+ }
+ />
+
+ { selectedOverview.instantBalance &&
+ selectedOverview.instantBalance.amount > 0 && (
+
+
+
+ ) }
+ >
);
};
diff --git a/client/components/account-balances/style.scss b/client/components/account-balances/style.scss
index 1c33d4abdf9..22b44cd9591 100644
--- a/client/components/account-balances/style.scss
+++ b/client/components/account-balances/style.scss
@@ -1,7 +1,3 @@
-.wcpay-account-balances__balances {
- border-top: 1px solid $gray-200;
-}
-
.wcpay-account-balances__balances > * + * {
border-left: 1px solid $gray-200;
}
@@ -21,7 +17,7 @@
&__amount {
font-weight: 500;
font-size: 20px;
- line-height: 0px;
+ margin: 0;
color: $gray-900;
}
}
@@ -29,6 +25,7 @@
.wcpay-account-balances__balances__item__title {
display: flex;
align-items: center;
+ margin-top: 0;
}
.wcpay-account-balances__instant-deposit {
diff --git a/client/components/account-balances/test/__snapshots__/index.test.tsx.snap b/client/components/account-balances/test/__snapshots__/index.test.tsx.snap
index a8488b51be8..49b207843f8 100644
--- a/client/components/account-balances/test/__snapshots__/index.test.tsx.snap
+++ b/client/components/account-balances/test/__snapshots__/index.test.tsx.snap
@@ -2,127 +2,102 @@
exports[`AccountBalances renders the correct tooltip text for the total balance 1`] = `
-
+
-
-
-
-
-
+ Total balance
+
+
-
+
+
+
+
+ $300.00
+
+
+
+
+
+ Available funds
+
+
-
-
- Available funds
-
-
-
-
-
-
- $200.00
-
-
-
+
+
+
+
+
+
+
+
+
+ $200.00
+
diff --git a/client/components/account-balances/test/index.test.tsx b/client/components/account-balances/test/index.test.tsx
index 6c277094ae5..176488a605c 100644
--- a/client/components/account-balances/test/index.test.tsx
+++ b/client/components/account-balances/test/index.test.tsx
@@ -8,7 +8,6 @@ import { render, screen, fireEvent, within } from '@testing-library/react';
* Internal dependencies
*/
import AccountBalances from '..';
-import { getCurrencyTabTitle } from '../utils';
import { useAllDepositsOverviews, useInstantDeposit } from 'wcpay/data';
import { useSelectedCurrency } from 'wcpay/overview/hooks';
import type * as AccountOverview from 'wcpay/types/account-overview';
@@ -55,11 +54,6 @@ const mockWcPaySettings = {
},
};
-jest.mock( '../utils', () => ( {
- getTimeOfDayString: jest.fn(),
- getCurrencyTabTitle: jest.fn(),
-} ) );
-
jest.mock( 'wcpay/data', () => ( {
useAllDepositsOverviews: jest.fn(),
useInstantDeposit: jest.fn(),
@@ -69,9 +63,6 @@ jest.mock( 'wcpay/overview/hooks', () => ( {
useSelectedCurrency: jest.fn(),
} ) );
-const mockGetCurrencyTabTitle = getCurrencyTabTitle as jest.MockedFunction<
- typeof getCurrencyTabTitle
->;
const mockUseAllDepositsOverviews = useAllDepositsOverviews as jest.MockedFunction<
typeof useAllDepositsOverviews
>;
@@ -154,14 +145,12 @@ describe( 'AccountBalances', () => {
global.wcpaySettings = mockWcPaySettings;
} );
- test( 'renders the correct tab title and currency data', () => {
- mockGetCurrencyTabTitle.mockReturnValue( 'USD Balance' );
+ test( 'renders USD currency correctly', () => {
mockOverviews( [ createMockOverview( 'usd', 10000, 20000, 0 ) ] );
// Use a query method returned by the render function: (you could also use `container` which will represent `document`)
const { getByText, getByLabelText } = render( );
- // Check the tab title is rendered correctly.
getByText( 'Total balance' );
getByText( 'Available funds' );
@@ -174,12 +163,10 @@ describe( 'AccountBalances', () => {
} );
test( 'renders JPY currency correctly', () => {
- mockGetCurrencyTabTitle.mockReturnValue( 'JPY Balance' );
mockOverviews( [ createMockOverview( 'jpy', 12300, 4560, 0 ) ] );
const { getByText, getByLabelText } = render( );
- // Check the tab title is rendered correctly.
getByText( 'Total balance' );
getByText( 'Available funds' );
@@ -191,12 +178,7 @@ describe( 'AccountBalances', () => {
expect( availableAmount ).toHaveTextContent( 'Â¥46' );
} );
- test( 'renders with selected currency correctly', () => {
- mockGetCurrencyTabTitle.mockImplementation(
- ( currencyCode: string ) => {
- return `${ currencyCode.toUpperCase() } Balance`;
- }
- );
+ test( 'renders with selected currency correctly when multiple deposit currencies exist', () => {
mockOverviews( [
createMockOverview( 'eur', 7660, 2739, 0 ),
createMockOverview( 'usd', 84875, 47941, 0 ),
@@ -207,13 +189,7 @@ describe( 'AccountBalances', () => {
setSelectedCurrency: mockSetSelectedCurrency,
} );
- const { getByLabelText, getByRole } = render( );
-
- // Check the active tab is rendered correctly.
- getByRole( 'tab', {
- selected: true,
- name: /JPY Balance/,
- } );
+ const { getByLabelText } = render( );
const totalAmount = getByLabelText( 'Total balance' );
const availableAmount = getByLabelText( 'Available funds' );
@@ -223,12 +199,7 @@ describe( 'AccountBalances', () => {
expect( availableAmount ).toHaveTextContent( 'Â¥90' );
} );
- test( 'renders default tab with invalid selected currency', () => {
- mockGetCurrencyTabTitle.mockImplementation(
- ( currencyCode: string ) => {
- return `${ currencyCode.toUpperCase() } Balance`;
- }
- );
+ test( 'renders default currency when invalid selected currency', () => {
mockOverviews( [
createMockOverview( 'eur', 7660, 2739, 0 ),
createMockOverview( 'usd', 84875, 47941, 0 ),
@@ -240,13 +211,7 @@ describe( 'AccountBalances', () => {
setSelectedCurrency: mockSetSelectedCurrency,
} );
- const { getByLabelText, getByRole } = render( );
-
- // Check the default active tab is rendered correctly.
- getByRole( 'tab', {
- selected: true,
- name: /EUR Balance/,
- } );
+ const { getByLabelText } = render( );
const totalAmount = getByLabelText( 'Total balance' );
const availableAmount = getByLabelText( 'Available funds' );
@@ -256,62 +221,6 @@ describe( 'AccountBalances', () => {
expect( availableAmount ).toHaveTextContent( '€27.39' );
} );
- test( 'renders multiple currency tabs', () => {
- mockGetCurrencyTabTitle.mockImplementation(
- ( currencyCode: string ) => {
- return `${ currencyCode.toUpperCase() } Balance`;
- }
- );
- mockOverviews( [
- createMockOverview( 'eur', 7660, 2739, 0 ),
- createMockOverview( 'usd', 84875, 47941, 0 ),
- createMockOverview( 'jpy', 2000, 9000, 0 ),
- ] );
-
- const { getByLabelText } = render( );
-
- // Get all the tab elements to check the tab titles are rendered correctly and for testing tab switching.
- const tabTitles = screen.getAllByRole( 'tab' );
-
- expect( tabTitles[ 0 ] ).toHaveTextContent( 'EUR Balance' );
- expect( tabTitles[ 1 ] ).toHaveTextContent( 'USD Balance' );
- expect( tabTitles[ 2 ] ).toHaveTextContent( 'JPY Balance' );
-
- // Check the first tab (EUR).
- const eurTotalAmount = getByLabelText( 'Total balance' );
- const eurAvailableAmount = getByLabelText( 'Available funds' );
-
- // Check the total and available amounts are rendered correctly for the first tab.
- expect( eurTotalAmount ).toHaveTextContent( '€103.99' );
- expect( eurAvailableAmount ).toHaveTextContent( '€27.39' );
-
- /**
- * Change the tab to the second tab (USD).
- */
- fireEvent.click( tabTitles[ 1 ] );
- expect( mockSetSelectedCurrency ).toHaveBeenCalledTimes( 1 );
- expect( mockSetSelectedCurrency ).toHaveBeenCalledWith( 'usd' );
- const usdTotalAmount = getByLabelText( 'Total balance' );
- const usdAvailableAmount = getByLabelText( 'Available funds' );
-
- // Check the total and available amounts are rendered correctly for the first tab.
- expect( usdTotalAmount ).toHaveTextContent( '$1,328.16' );
- expect( usdAvailableAmount ).toHaveTextContent( '$479.41' );
-
- /**
- * Change the tab to the third tab (JPY).
- */
- fireEvent.click( tabTitles[ 2 ] );
- expect( mockSetSelectedCurrency ).toHaveBeenCalledTimes( 2 );
- expect( mockSetSelectedCurrency ).toHaveBeenLastCalledWith( 'jpy' );
- const jpyTotalAmount = getByLabelText( 'Total balance' );
- const jpyAvailableAmount = getByLabelText( 'Available funds' );
-
- // Check the total and available amounts are rendered correctly for the first tab.
- expect( jpyTotalAmount ).toHaveTextContent( 'Â¥110' );
- expect( jpyAvailableAmount ).toHaveTextContent( 'Â¥90' );
- } );
-
test( 'renders the correct tooltip text for the available balance', () => {
mockOverviews( [ createMockOverview( 'usd', 10000, 20000, 0 ) ] );
render( );
diff --git a/client/components/account-balances/utils.ts b/client/components/account-balances/utils.ts
deleted file mode 100644
index 56890cded1a..00000000000
--- a/client/components/account-balances/utils.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * External dependencies
- */
-import { __, sprintf } from '@wordpress/i18n';
-
-/**
- * Generates a currency tab title.
- *
- * @param {string} currencyCode The currency code.
- * @return {string} The currency tab title. Example: "USD balance"
- */
-export const getCurrencyTabTitle = ( currencyCode: string ): string => {
- return sprintf(
- /** translators: %s is the currency code, e.g. USD. */
- __( '%s Balance', 'woocommerce-payments' ),
- currencyCode.toUpperCase()
- );
-};
diff --git a/client/components/welcome/currency-select.tsx b/client/components/welcome/currency-select.tsx
new file mode 100644
index 00000000000..7f82c412313
--- /dev/null
+++ b/client/components/welcome/currency-select.tsx
@@ -0,0 +1,108 @@
+/**
+ * External dependencies
+ */
+import React, { useEffect } from 'react';
+import { decodeEntities } from '@wordpress/html-entities';
+
+/**
+ * Internal dependencies
+ */
+import { useSelectedCurrency } from 'overview/hooks';
+import { getCurrency } from 'utils/currency';
+import InlineLabelSelect from '../inline-label-select';
+import { recordEvent } from 'tracks';
+
+/**
+ * Returns an select option object for a currency select control.
+ */
+const getCurrencyOption = (
+ currency: string
+): {
+ name: string;
+ key: string;
+} => {
+ const { code, symbol } = getCurrency( currency )?.getCurrencyConfig() || {};
+ const currencySymbolDecoded = decodeEntities( symbol || '' );
+
+ if (
+ // Show just the currency the currency code is used as the name, e.g. 'EUR'
+ // if no currency config is found,
+ ! code ||
+ ! symbol ||
+ // or if the symbol is identical to the currency code, e.g. 'CHF CHF'.
+ currencySymbolDecoded === code
+ ) {
+ return {
+ name: currency.toUpperCase(),
+ key: currency,
+ };
+ }
+ return {
+ // A rendered name of the currency with symbol, e.g. `EUR €`.
+ name: `${ code } ${ currencySymbolDecoded }`,
+ key: currency,
+ };
+};
+
+/**
+ * Custom hook to get the selected currency from the URL query parameter 'selected_currency'.
+ * If no currency is selected, the store's default currency will be selected.
+ */
+const useSelectedCurrencyWithDefault = ( depositCurrencies: string[] ) => {
+ const { selectedCurrency, setSelectedCurrency } = useSelectedCurrency();
+
+ useEffect( () => {
+ // The selected currency is invalid if:
+ // * no currency is explicitly selected via URL query, or
+ // * no currency is found for the provided query parameter.
+ const isSelectedCurrencyValid =
+ selectedCurrency &&
+ depositCurrencies.find(
+ ( currency ) =>
+ currency.toLowerCase() === selectedCurrency.toLowerCase()
+ );
+
+ // Select the store's default currency if the selected currency is invalid.
+ if ( ! isSelectedCurrencyValid && depositCurrencies.length > 0 ) {
+ setSelectedCurrency( depositCurrencies[ 0 ].toLowerCase() );
+ }
+ }, [ depositCurrencies, selectedCurrency, setSelectedCurrency ] );
+
+ return { selectedCurrency, setSelectedCurrency };
+};
+
+/**
+ * Renders a currency select input used for the Payments Overview page.
+ * Should only be rendered if there are multiple deposit currencies available.
+ */
+export const CurrencySelect: React.FC< {
+ /** An array of available deposit currencies, e.g. ['usd', 'eur']. */
+ depositCurrencies: string[];
+} > = ( { depositCurrencies } ) => {
+ const currencyOptions = depositCurrencies.map( getCurrencyOption );
+ const {
+ selectedCurrency,
+ setSelectedCurrency,
+ } = useSelectedCurrencyWithDefault( depositCurrencies );
+
+ return (
+ option.key === selectedCurrency
+ ) }
+ options={ currencyOptions }
+ onChange={ ( { selectedItem } ) => {
+ if ( ! selectedItem ) {
+ return;
+ }
+
+ const currencyCode = selectedItem.key.toLowerCase();
+ setSelectedCurrency( currencyCode );
+ recordEvent( 'wcpay_overview_currency_select_change', {
+ selected_currency: currencyCode,
+ } );
+ } }
+ />
+ );
+};
diff --git a/client/components/welcome/index.tsx b/client/components/welcome/index.tsx
index 2a102592cd9..20ef939a4bf 100644
--- a/client/components/welcome/index.tsx
+++ b/client/components/welcome/index.tsx
@@ -8,8 +8,9 @@ import { __, sprintf } from '@wordpress/i18n';
/**
* Internal dependencies
*/
+import { useAllDepositsOverviews } from 'data';
import { useCurrentWpUser } from './hooks';
-import wooPaymentsLogo from 'assets/images/woopayments.svg?asset';
+import { CurrencySelect } from './currency-select';
import './style.scss';
type TimeOfDay = 'morning' | 'afternoon' | 'evening';
@@ -67,13 +68,16 @@ const getGreeting = ( name?: string, date: Date = new Date() ): string => {
};
/**
- * Renders a welcome card header with a greeting and the WooPayments logo.
- *
- * @return {JSX.Element} Rendered element with the account balances card header.
+ * Renders a welcome card header with a greeting and a currency select input if supported.
*/
const Welcome: React.FC = () => {
const { user } = useCurrentWpUser();
const greeting = getGreeting( user?.first_name );
+ const { overviews } = useAllDepositsOverviews();
+ const depositCurrencies =
+ overviews?.currencies.map( ( currencyObj ) => currencyObj.currency ) ||
+ [];
+ const renderCurrencySelect = depositCurrencies.length > 1;
return (
@@ -85,14 +89,14 @@ const Welcome: React.FC = () => {
{ greeting }
-
-
-
+
+ { renderCurrencySelect && (
+
+
+
+ ) }
);
diff --git a/client/components/welcome/style.scss b/client/components/welcome/style.scss
index cfb9ae0c098..0e91bf4c271 100644
--- a/client/components/welcome/style.scss
+++ b/client/components/welcome/style.scss
@@ -1,13 +1,8 @@
.wcpay-welcome.components-card__header {
- // Override the default padding to adjust for additional margins on child elements.
- padding-top: 8px;
- padding-bottom: 8px;
-}
-
-.wcpay-welcome__flex__logo {
- display: block;
- margin-top: 14px;
- margin-bottom: 10px;
+ // Override the Card Header styles – used to keep consistent header styles.
+ padding: 0;
+ margin-bottom: $grid-unit-30;
+ border: 0;
}
.wcpay-welcome__flex__greeting {
@@ -17,6 +12,9 @@
@media screen and ( max-width: $break-mobile ) {
.components-flex.wcpay-welcome__flex {
- flex-direction: column-reverse;
+ flex-direction: column;
+ }
+ .wcpay-welcome__flex__greeting {
+ margin-bottom: 24px;
}
}
diff --git a/client/components/welcome/test/index.test.tsx b/client/components/welcome/test/index.test.tsx
index 3f727b7cf85..d5bcd4681db 100644
--- a/client/components/welcome/test/index.test.tsx
+++ b/client/components/welcome/test/index.test.tsx
@@ -3,22 +3,119 @@
*/
import React from 'react';
import { render } from '@testing-library/react';
+import user from '@testing-library/user-event';
/**
* Internal dependencies
*/
import Welcome from '..';
import { useCurrentWpUser } from '../hooks';
+import { useAllDepositsOverviews } from 'data';
+import { useSelectedCurrency } from 'overview/hooks';
+import type { Overview } from 'types/account-overview';
+
+declare const global: {
+ wcpaySettings: {
+ accountDefaultCurrency: string;
+ zeroDecimalCurrencies: string[];
+ currencyData: Record< string, any >;
+ connect: {
+ country: string;
+ };
+ };
+};
jest.mock( '../hooks', () => ( {
useCurrentWpUser: jest.fn(),
} ) );
+jest.mock( 'wcpay/data', () => ( {
+ useAllDepositsOverviews: jest.fn(),
+} ) );
+jest.mock( 'wcpay/overview/hooks', () => ( {
+ useSelectedCurrency: jest.fn(),
+} ) );
+
+const mockUseAllDepositsOverviews = useAllDepositsOverviews as jest.MockedFunction<
+ typeof useAllDepositsOverviews
+>;
+const mockUseSelectedCurrency = useSelectedCurrency as jest.MockedFunction<
+ typeof useSelectedCurrency
+>;
+
+const mockAccountOverviewCurrencies: Partial< Overview >[] = [
+ {
+ currency: 'usd',
+ },
+];
+mockUseAllDepositsOverviews.mockReturnValue( {
+ overviews: {
+ account: null,
+ currencies: mockAccountOverviewCurrencies as Overview[],
+ },
+ isLoading: false,
+} );
+
+// Mocks the useSelectedCurrency hook to return no previously selected currency.
+const mockSetSelectedCurrency = jest.fn();
+mockUseSelectedCurrency.mockReturnValue( {
+ selectedCurrency: 'usd',
+ setSelectedCurrency: mockSetSelectedCurrency,
+} );
const mockUseCurrentWpUser = useCurrentWpUser as jest.MockedFunction<
typeof useCurrentWpUser
>;
+mockUseCurrentWpUser.mockReturnValue( {
+ user: {
+ id: 123,
+ first_name: 'Tester',
+ username: 'admin',
+ name: 'admin',
+ nickname: 'Tester-nickname',
+ last_name: 'Tester-lastname',
+ email: 'tester@test.com',
+ locale: 'en',
+ },
+ isLoading: false,
+} );
+
+describe( 'Welcome and Currency Select', () => {
+ beforeEach( () => {
+ global.wcpaySettings = {
+ accountDefaultCurrency: 'USD',
+ zeroDecimalCurrencies: [],
+ connect: {
+ country: 'US',
+ },
+ currencyData: {
+ US: {
+ code: 'USD',
+ symbol: '$',
+ symbolPosition: 'left',
+ thousandSeparator: ',',
+ decimalSeparator: '.',
+ precision: 2,
+ },
+ DE: {
+ code: 'EUR',
+ symbol: '€',
+ symbolPosition: 'right_space',
+ thousandSeparator: ' ',
+ decimalSeparator: ',',
+ precision: 2,
+ },
+ NO: {
+ code: 'NOK',
+ symbol: 'kr',
+ symbolPosition: 'right',
+ thousandSeparator: ' ',
+ decimalSeparator: ',',
+ precision: 2,
+ },
+ },
+ };
+ } );
-describe( 'Welcome', () => {
test( 'renders the correct greeting when the user first name exists', () => {
const mockUser = {
id: 123,
@@ -58,4 +155,63 @@ describe( 'Welcome', () => {
const { getByText } = render( );
getByText( expectedGreeting );
} );
+
+ test( 'renders the currency select control if multiple deposit currencies', () => {
+ mockUseAllDepositsOverviews.mockReturnValue( {
+ overviews: {
+ account: null,
+ currencies: [
+ {
+ currency: 'usd',
+ },
+ {
+ currency: 'eur',
+ },
+ {
+ currency: 'nok',
+ },
+ ] as Overview[],
+ },
+ isLoading: false,
+ } );
+ const { getByRole } = render( );
+ getByRole( 'button', {
+ name: /currency/i,
+ } );
+
+ // Check default selected currency.
+ const selectControl = getByRole( 'button', { name: /currency/i } );
+ expect( selectControl ).toHaveTextContent( /usd/i );
+
+ user.click( getByRole( 'button' ) );
+
+ // Currency options should be visible.
+ getByRole( 'option', { name: 'USD $' } );
+ getByRole( 'option', { name: 'EUR €' } );
+ getByRole( 'option', { name: 'NOK kr' } );
+
+ // Select a currency.
+ user.click( getByRole( 'option', { name: 'NOK kr' } ) );
+ expect( mockSetSelectedCurrency ).toHaveBeenCalledWith( 'nok' );
+ } );
+
+ test( 'does not render the currency select control if single deposit currency', () => {
+ mockUseAllDepositsOverviews.mockReturnValue( {
+ overviews: {
+ account: null,
+ currencies: [
+ {
+ currency: 'nok',
+ },
+ ] as Overview[],
+ },
+ isLoading: false,
+ } );
+ const { queryByRole } = render( );
+ expect(
+ queryByRole( 'button', {
+ name: /currency/i,
+ } )
+ ).toBeNull();
+ } );
} );
diff --git a/client/overview/index.js b/client/overview/index.js
index 669055b1813..ab6ca7e8404 100644
--- a/client/overview/index.js
+++ b/client/overview/index.js
@@ -234,40 +234,37 @@ const OverviewPage = () => {
{ showConnectionSuccess && }
{ ! accountRejected && ! accountUnderReview && (
- <>
- { showTaskList ? (
- <>
-
-
-
-
-
-
-
-
-
- >
- ) : (
-
-
-
-
- ) }
- {
- /* Show Payment Activity widget only when feature flag is set. To be removed before go live */
- isPaymentOverviewWidgetEnabled && (
-
-
-
- )
- }
-
- >
+
+
+ { showTaskList && (
+
+
+
+
+
+ ) }
+
+
+
+
+
+
+
+ {
+ /* Show Payment Activity widget only when feature flag is set. To be removed before go live */
+ isPaymentOverviewWidgetEnabled && (
+
+
+
+ )
+ }
+
+
) }
diff --git a/tests/e2e-pw/specs/basic.spec.ts b/tests/e2e-pw/specs/basic.spec.ts
index 910176fdc21..f8c49d71d70 100644
--- a/tests/e2e-pw/specs/basic.spec.ts
+++ b/tests/e2e-pw/specs/basic.spec.ts
@@ -22,8 +22,9 @@ test.describe(
'/wp-admin/admin.php?page=wc-admin&path=/payments/overview'
);
await page.waitForLoadState( 'domcontentloaded' );
- const logo = page.getByAltText( 'WooPayments logo' );
- await expect( logo ).toBeVisible();
+ await expect(
+ page.getByRole( 'heading', { name: 'Overview' } )
+ ).toBeVisible();
} );
} );
From bbdbafb3f91cc91b7acfe8191117de3a3d5c3e20 Mon Sep 17 00:00:00 2001
From: deepakpathania <68396823+deepakpathania@users.noreply.github.com>
Date: Fri, 7 Jun 2024 18:25:01 +0530
Subject: [PATCH 28/52] Validate route params before making request (#8901)
---
changelog/update-route-param-validation | 4 +++
...est-payments-authorizations-controller.php | 2 +-
...ss-wc-rest-payments-charges-controller.php | 4 +--
...s-wc-rest-payments-customer-controller.php | 2 +-
...s-wc-rest-payments-deposits-controller.php | 2 +-
...s-wc-rest-payments-disputes-controller.php | 6 ++---
...lass-wc-rest-payments-files-controller.php | 6 ++---
...est-payments-fraud-outcomes-controller.php | 2 +-
...ass-wc-rest-payments-orders-controller.php | 8 +++---
...st-payments-payment-intents-controller.php | 2 +-
...ass-wc-rest-payments-reader-controller.php | 4 +--
...payments-terminal-locations-controller.php | 6 ++---
...s-wc-rest-payments-timeline-controller.php | 2 +-
...-rest-payments-transactions-controller.php | 2 +-
...ents-reports-authorizations-controller.php | 2 +-
...yments-reports-transactions-controller.php | 2 +-
.../class-wc-payments-api-client.php | 26 +++++++++++++++++++
17 files changed, 56 insertions(+), 26 deletions(-)
create mode 100644 changelog/update-route-param-validation
diff --git a/changelog/update-route-param-validation b/changelog/update-route-param-validation
new file mode 100644
index 00000000000..4e92f0c3a2b
--- /dev/null
+++ b/changelog/update-route-param-validation
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Add validation for path variables.
diff --git a/includes/admin/class-wc-rest-payments-authorizations-controller.php b/includes/admin/class-wc-rest-payments-authorizations-controller.php
index 1b2993e3c71..a35f3fc85e3 100644
--- a/includes/admin/class-wc-rest-payments-authorizations-controller.php
+++ b/includes/admin/class-wc-rest-payments-authorizations-controller.php
@@ -46,7 +46,7 @@ public function register_routes() {
);
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P\w+)',
+ '/' . $this->rest_base . '/(?P(ch|pi|py)_[A-Za-z0-9]+)',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_authorization' ],
diff --git a/includes/admin/class-wc-rest-payments-charges-controller.php b/includes/admin/class-wc-rest-payments-charges-controller.php
index 34422cac647..2297e3bd13a 100644
--- a/includes/admin/class-wc-rest-payments-charges-controller.php
+++ b/includes/admin/class-wc-rest-payments-charges-controller.php
@@ -28,7 +28,7 @@ class WC_REST_Payments_Charges_Controller extends WC_Payments_REST_Controller {
public function register_routes() {
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P\w+)',
+ '/' . $this->rest_base . '/(?Pch_[A-Za-z0-9]+)',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_charge' ],
@@ -37,7 +37,7 @@ public function register_routes() {
);
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/order/(?P\w+)',
+ '/' . $this->rest_base . '/order/(?P[A-Za-z0-9_\-]+)',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'generate_charge_from_order' ],
diff --git a/includes/admin/class-wc-rest-payments-customer-controller.php b/includes/admin/class-wc-rest-payments-customer-controller.php
index c0f43a97003..5ccce5c99d6 100644
--- a/includes/admin/class-wc-rest-payments-customer-controller.php
+++ b/includes/admin/class-wc-rest-payments-customer-controller.php
@@ -49,7 +49,7 @@ public function __construct(
public function register_routes() {
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P\w+)/payment_methods',
+ '/' . $this->rest_base . '/(?P[A-Za-z0-9_\-]+)/payment_methods',
[
[
'methods' => WP_REST_Server::READABLE,
diff --git a/includes/admin/class-wc-rest-payments-deposits-controller.php b/includes/admin/class-wc-rest-payments-deposits-controller.php
index 1b5c808a314..da3b3c1afbc 100644
--- a/includes/admin/class-wc-rest-payments-deposits-controller.php
+++ b/includes/admin/class-wc-rest-payments-deposits-controller.php
@@ -55,7 +55,7 @@ public function register_routes() {
);
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P\w+)',
+ '/' . $this->rest_base . '/(?P[A-Za-z0-9_\-]+)',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_deposit' ],
diff --git a/includes/admin/class-wc-rest-payments-disputes-controller.php b/includes/admin/class-wc-rest-payments-disputes-controller.php
index 64e1e478a21..4dacf45ee5c 100644
--- a/includes/admin/class-wc-rest-payments-disputes-controller.php
+++ b/includes/admin/class-wc-rest-payments-disputes-controller.php
@@ -54,7 +54,7 @@ public function register_routes() {
);
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P\w+)',
+ '/' . $this->rest_base . '/(?P(dp|dispute)_[A-Za-z0-9]+)',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_dispute' ],
@@ -63,7 +63,7 @@ public function register_routes() {
);
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P\w+)',
+ '/' . $this->rest_base . '/(?P(dp|dispute)_[A-Za-z0-9]+)',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'update_dispute' ],
@@ -72,7 +72,7 @@ public function register_routes() {
);
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P\w+)/close',
+ '/' . $this->rest_base . '/(?P(dp|dispute)_[A-Za-z0-9]+)/close',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'close_dispute' ],
diff --git a/includes/admin/class-wc-rest-payments-files-controller.php b/includes/admin/class-wc-rest-payments-files-controller.php
index e66eedefffd..6720b52b959 100644
--- a/includes/admin/class-wc-rest-payments-files-controller.php
+++ b/includes/admin/class-wc-rest-payments-files-controller.php
@@ -35,7 +35,7 @@ public function register_routes() {
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P\w+)/details',
+ '/' . $this->rest_base . '/(?P[A-Za-z0-9_\-]+)/details',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_file_detail' ],
@@ -45,7 +45,7 @@ public function register_routes() {
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P\w+)/content',
+ '/' . $this->rest_base . '/(?P[A-Za-z0-9_\-]+)/content',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_file_content' ],
@@ -55,7 +55,7 @@ public function register_routes() {
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P\w+)',
+ '/' . $this->rest_base . '/(?P[A-Za-z0-9_\-]+)',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_file' ],
diff --git a/includes/admin/class-wc-rest-payments-fraud-outcomes-controller.php b/includes/admin/class-wc-rest-payments-fraud-outcomes-controller.php
index 7d0feb06024..199e62255cf 100644
--- a/includes/admin/class-wc-rest-payments-fraud-outcomes-controller.php
+++ b/includes/admin/class-wc-rest-payments-fraud-outcomes-controller.php
@@ -25,7 +25,7 @@ class WC_REST_Payments_Fraud_Outcomes_Controller extends WC_Payments_REST_Contro
public function register_routes() {
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P\w+)/latest',
+ '/' . $this->rest_base . '/(?P[A-Za-z0-9_\-]+)/latest',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_latest_fraud_outcome' ],
diff --git a/includes/admin/class-wc-rest-payments-orders-controller.php b/includes/admin/class-wc-rest-payments-orders-controller.php
index 04c86f54197..f79e0a5fcfd 100644
--- a/includes/admin/class-wc-rest-payments-orders-controller.php
+++ b/includes/admin/class-wc-rest-payments-orders-controller.php
@@ -68,7 +68,7 @@ public function __construct( WC_Payments_API_Client $api_client, WC_Payment_Gate
public function register_routes() {
register_rest_route(
$this->namespace,
- $this->rest_base . '/(?P\w+)/capture_terminal_payment',
+ $this->rest_base . '/(?P[A-Za-z0-9_\-]+)/capture_terminal_payment',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'capture_terminal_payment' ],
@@ -82,7 +82,7 @@ public function register_routes() {
);
register_rest_route(
$this->namespace,
- $this->rest_base . '/(?P\w+)/capture_authorization',
+ $this->rest_base . '/(?P[A-Za-z0-9_\-]+)/capture_authorization',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'capture_authorization' ],
@@ -96,7 +96,7 @@ public function register_routes() {
);
register_rest_route(
$this->namespace,
- $this->rest_base . '/(?P\w+)/cancel_authorization',
+ $this->rest_base . '/(?P[A-Za-z0-9_\-]+)/cancel_authorization',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'cancel_authorization' ],
@@ -110,7 +110,7 @@ public function register_routes() {
);
register_rest_route(
$this->namespace,
- $this->rest_base . '/(?P\w+)/create_terminal_intent',
+ $this->rest_base . '/(?P[A-Za-z0-9_\-]+)/create_terminal_intent',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'create_terminal_intent' ],
diff --git a/includes/admin/class-wc-rest-payments-payment-intents-controller.php b/includes/admin/class-wc-rest-payments-payment-intents-controller.php
index 670d15a3089..9ba1a39fa67 100644
--- a/includes/admin/class-wc-rest-payments-payment-intents-controller.php
+++ b/includes/admin/class-wc-rest-payments-payment-intents-controller.php
@@ -32,7 +32,7 @@ class WC_REST_Payments_Payment_Intents_Controller extends WC_Payments_REST_Contr
public function register_routes() {
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P\w+)',
+ '/' . $this->rest_base . '/(?P(ch|pi|py)_[A-Za-z0-9]+)',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_payment_intent' ],
diff --git a/includes/admin/class-wc-rest-payments-reader-controller.php b/includes/admin/class-wc-rest-payments-reader-controller.php
index fa6fa5192e0..569e49bc9ed 100644
--- a/includes/admin/class-wc-rest-payments-reader-controller.php
+++ b/includes/admin/class-wc-rest-payments-reader-controller.php
@@ -114,7 +114,7 @@ public function register_routes() {
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/charges/(?P\w+)',
+ '/' . $this->rest_base . '/charges/(?P[A-Za-z0-9_\-]+)',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_summary' ],
@@ -132,7 +132,7 @@ public function register_routes() {
);
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/receipts/(?P\w+)',
+ '/' . $this->rest_base . '/receipts/(?P(ch|pi|py)_[A-Za-z0-9]+)',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'generate_print_receipt' ],
diff --git a/includes/admin/class-wc-rest-payments-terminal-locations-controller.php b/includes/admin/class-wc-rest-payments-terminal-locations-controller.php
index c0f0f7754ee..90062ef3b8c 100644
--- a/includes/admin/class-wc-rest-payments-terminal-locations-controller.php
+++ b/includes/admin/class-wc-rest-payments-terminal-locations-controller.php
@@ -38,7 +38,7 @@ public function register_routes() {
);
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P\w+)',
+ '/' . $this->rest_base . '/(?P[A-Za-z0-9_\-]+)',
[
'methods' => WP_REST_Server::DELETABLE,
'callback' => [ $this, 'delete_location' ],
@@ -47,7 +47,7 @@ public function register_routes() {
);
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P\w+)',
+ '/' . $this->rest_base . '/(?P[A-Za-z0-9_\-]+)',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'update_location' ],
@@ -66,7 +66,7 @@ public function register_routes() {
);
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P\w+)',
+ '/' . $this->rest_base . '/(?P[A-Za-z0-9_\-]+)',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_location' ],
diff --git a/includes/admin/class-wc-rest-payments-timeline-controller.php b/includes/admin/class-wc-rest-payments-timeline-controller.php
index c2c0f5ac87a..23a1c4136d1 100644
--- a/includes/admin/class-wc-rest-payments-timeline-controller.php
+++ b/includes/admin/class-wc-rest-payments-timeline-controller.php
@@ -24,7 +24,7 @@ class WC_REST_Payments_Timeline_Controller extends WC_Payments_REST_Controller {
public function register_routes() {
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P\w+)',
+ '/' . $this->rest_base . '/(?P(ch|pi|py)_[A-Za-z0-9]+)',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_timeline' ],
diff --git a/includes/admin/class-wc-rest-payments-transactions-controller.php b/includes/admin/class-wc-rest-payments-transactions-controller.php
index c3a783943b8..5c72a32ed0e 100644
--- a/includes/admin/class-wc-rest-payments-transactions-controller.php
+++ b/includes/admin/class-wc-rest-payments-transactions-controller.php
@@ -100,7 +100,7 @@ public function register_routes() {
);
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P\w+)',
+ '/' . $this->rest_base . '/(?P[A-Za-z0-9_\-]+)',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_transaction' ],
diff --git a/includes/reports/class-wc-rest-payments-reports-authorizations-controller.php b/includes/reports/class-wc-rest-payments-reports-authorizations-controller.php
index 0834d078eeb..20cba8bc6cf 100644
--- a/includes/reports/class-wc-rest-payments-reports-authorizations-controller.php
+++ b/includes/reports/class-wc-rest-payments-reports-authorizations-controller.php
@@ -40,7 +40,7 @@ public function register_routes() {
);
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P\w+)',
+ '/' . $this->rest_base . '/(?P[A-Za-z0-9_\-]+)',
[
[
'methods' => WP_REST_Server::READABLE,
diff --git a/includes/reports/class-wc-rest-payments-reports-transactions-controller.php b/includes/reports/class-wc-rest-payments-reports-transactions-controller.php
index 1e38d6cd746..6e130400af2 100644
--- a/includes/reports/class-wc-rest-payments-reports-transactions-controller.php
+++ b/includes/reports/class-wc-rest-payments-reports-transactions-controller.php
@@ -39,7 +39,7 @@ public function register_routes() {
);
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P\w+)',
+ '/' . $this->rest_base . '/(?P[A-Za-z0-9_\-]+)',
[
[
'methods' => WP_REST_Server::READABLE,
diff --git a/includes/wc-payment-api/class-wc-payments-api-client.php b/includes/wc-payment-api/class-wc-payments-api-client.php
index 8c3e6c29ba9..41e8c35cad5 100644
--- a/includes/wc-payment-api/class-wc-payments-api-client.php
+++ b/includes/wc-payment-api/class-wc-payments-api-client.php
@@ -518,8 +518,17 @@ public function get_disputes( array $filters = [] ) {
*
* @param string $dispute_id id of requested dispute.
* @return array dispute object.
+ * @throws API_Exception - Exception thrown in case route validation fails.
*/
public function get_dispute( $dispute_id ) {
+ if ( ! preg_match( '/(dp|dispute)_[A-Za-z0-9]+/', $dispute_id ) ) {
+ throw new API_Exception(
+ __( 'Route param validation failed.', 'woocommerce-payments' ),
+ 'wcpay_route_validation_failure',
+ 400
+ );
+ }
+
$dispute = $this->request( [], self::DISPUTES_API . '/' . $dispute_id, self::GET );
if ( is_wp_error( $dispute ) ) {
@@ -726,8 +735,17 @@ public function create_token( $request ) {
* @return array
*
* @throws Exception - Exception thrown on request failure.
+ * @throws API_Exception - Exception thrown in case route validation fails.
*/
public function get_timeline( $id ) {
+ if ( ! preg_match( '/(ch|pi|py)_[A-Za-z0-9]+/', $id ) ) {
+ throw new API_Exception(
+ __( 'Route param validation failed.', 'woocommerce-payments' ),
+ 'wcpay_route_validation_failure',
+ 400
+ );
+ }
+
$timeline = $this->request( [], self::TIMELINE_API . '/' . $id, self::GET );
$has_fraud_outcome_event = false;
@@ -1199,6 +1217,14 @@ public function update_charge( string $charge_id, array $data = [] ) {
* @throws API_Exception
*/
public function get_charge( string $charge_id ) {
+ if ( ! preg_match( '/(ch|pi|py)_[A-Za-z0-9]+/', $charge_id ) ) {
+ throw new API_Exception(
+ __( 'Route param validation failed.', 'woocommerce-payments' ),
+ 'wcpay_route_validation_failure',
+ 400
+ );
+ }
+
return $this->request(
[],
self::CHARGES_API . '/' . $charge_id,
From bf558f8410585ce7db6b062affa504e926883e79 Mon Sep 17 00:00:00 2001
From: Hsing-yu Flowers
Date: Fri, 7 Jun 2024 16:59:26 -0400
Subject: [PATCH 29/52] Enable WP_DEBUG to be able to get previous Exception
class (#8920)
---
changelog/add-add-wc-return-previous-exceptions-filter | 4 ++++
includes/class-wc-payment-gateway-wcpay.php | 2 ++
2 files changed, 6 insertions(+)
create mode 100644 changelog/add-add-wc-return-previous-exceptions-filter
diff --git a/changelog/add-add-wc-return-previous-exceptions-filter b/changelog/add-add-wc-return-previous-exceptions-filter
new file mode 100644
index 00000000000..2ed58d22fff
--- /dev/null
+++ b/changelog/add-add-wc-return-previous-exceptions-filter
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+Add woocommerce-return-previous-exceptions filter
diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php
index d6fe41edb75..57988038e88 100644
--- a/includes/class-wc-payment-gateway-wcpay.php
+++ b/includes/class-wc-payment-gateway-wcpay.php
@@ -1289,6 +1289,8 @@ public function process_payment( $order_id ) {
$order->add_order_note( $note );
}
+ // This allows WC to check if WP_DEBUG mode is enabled before returning previous Exception and expose Exception class name to frontend.
+ add_filter( 'woocommerce_return_previous_exceptions', '__return_true' );
// Re-throw the exception after setting everything up.
// This makes the error notice show up both in the regular and block checkout.
throw new Exception( WC_Payments_Utils::get_filtered_error_message( $e, $blocked_by_fraud_rules ), 0, $e );
From cbeaa014e172aef319225a1074ad73bd2d3e784b Mon Sep 17 00:00:00 2001
From: Timur Karimov
Date: Mon, 10 Jun 2024 09:14:08 +0200
Subject: [PATCH 30/52] Reset duplicate notifications from new plugins (#8919)
---
...t-duplicate-notifications-from-new-plugins | 4 ++
client/components/duplicate-notice/index.tsx | 38 +++++++++++---
.../duplicate-notice/tests/index.test.tsx | 49 ++++++++++++++++---
.../payment-methods-list/payment-method.tsx | 5 +-
.../test/payment-method.test.tsx | 8 +--
client/globals.d.ts | 3 +-
client/payment-methods/test/index.js | 4 +-
.../apple-google-pay-item.tsx | 5 +-
.../duplicated-payment-methods-context.tsx | 5 +-
client/settings/settings-manager/index.js | 2 +-
.../class-duplicates-detection-service.php | 2 +-
...est-class-duplicates-detection-service.php | 8 +--
12 files changed, 99 insertions(+), 34 deletions(-)
create mode 100644 changelog/add-reset-duplicate-notifications-from-new-plugins
diff --git a/changelog/add-reset-duplicate-notifications-from-new-plugins b/changelog/add-reset-duplicate-notifications-from-new-plugins
new file mode 100644
index 00000000000..34be1090abf
--- /dev/null
+++ b/changelog/add-reset-duplicate-notifications-from-new-plugins
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Reset notifications about duplicate enabled payment methods when new plugins are enabling them.
diff --git a/client/components/duplicate-notice/index.tsx b/client/components/duplicate-notice/index.tsx
index 2a509147d0e..faca5adb573 100644
--- a/client/components/duplicate-notice/index.tsx
+++ b/client/components/duplicate-notice/index.tsx
@@ -8,21 +8,38 @@ import { __ } from '@wordpress/i18n';
import { getAdminUrl } from 'wcpay/utils';
import { useDispatch } from '@wordpress/data';
+export type PaymentMethodToPluginsMap = { [ key: string ]: string[] };
interface DuplicateNoticeProps {
paymentMethod: string;
- dismissedDuplicateNotices: string[];
- setDismissedDuplicateNotices: ( notices: string[] ) => void;
+ gatewaysEnablingPaymentMethod: string[];
+ dismissedNotices: PaymentMethodToPluginsMap;
+ setDismissedDuplicateNotices: (
+ notices: PaymentMethodToPluginsMap
+ ) => null;
}
function DuplicateNotice( {
paymentMethod,
- dismissedDuplicateNotices,
+ gatewaysEnablingPaymentMethod,
+ dismissedNotices,
setDismissedDuplicateNotices,
}: DuplicateNoticeProps ): JSX.Element | null {
const { updateOptions } = useDispatch( 'wc/admin/options' );
const handleDismiss = useCallback( () => {
- const updatedNotices = [ ...dismissedDuplicateNotices, paymentMethod ];
+ const updatedNotices = { ...dismissedNotices };
+ if ( updatedNotices[ paymentMethod ] ) {
+ // If there are existing dismissed notices for the payment method, append to the current array.
+ updatedNotices[ paymentMethod ] = [
+ ...new Set( [
+ ...updatedNotices[ paymentMethod ],
+ ...gatewaysEnablingPaymentMethod,
+ ] ),
+ ];
+ } else {
+ updatedNotices[ paymentMethod ] = gatewaysEnablingPaymentMethod;
+ }
+
setDismissedDuplicateNotices( updatedNotices );
updateOptions( {
wcpay_duplicate_payment_method_notices_dismissed: updatedNotices,
@@ -30,13 +47,20 @@ function DuplicateNotice( {
wcpaySettings.dismissedDuplicateNotices = updatedNotices;
}, [
paymentMethod,
- dismissedDuplicateNotices,
+ gatewaysEnablingPaymentMethod,
+ dismissedNotices,
setDismissedDuplicateNotices,
updateOptions,
] );
- if ( dismissedDuplicateNotices.includes( paymentMethod ) ) {
- return null;
+ if ( dismissedNotices?.[ paymentMethod ] ) {
+ const isNoticeDismissedForEveryGateway = gatewaysEnablingPaymentMethod.every(
+ ( value ) => dismissedNotices[ paymentMethod ].includes( value )
+ );
+
+ if ( isNoticeDismissedForEveryGateway ) {
+ return null;
+ }
}
return (
diff --git a/client/components/duplicate-notice/tests/index.test.tsx b/client/components/duplicate-notice/tests/index.test.tsx
index aacb7c3a90c..290f756d650 100644
--- a/client/components/duplicate-notice/tests/index.test.tsx
+++ b/client/components/duplicate-notice/tests/index.test.tsx
@@ -27,10 +27,14 @@ describe( 'DuplicateNotice', () => {
} );
test( 'does not render when the payment method is dismissed', () => {
+ const dismissedDuplicateNotices = {
+ bancontact: [ 'woocommerce_payments' ],
+ };
render(
);
@@ -41,11 +45,36 @@ describe( 'DuplicateNotice', () => {
).not.toBeInTheDocument();
} );
+ test( 'renders correctly when the payment method is dismissed by some plugins but not all', () => {
+ const dismissedDuplicateNotices = {
+ bancontact: [ 'woocommerce_payments' ],
+ };
+
+ render(
+
+ );
+ expect(
+ screen.getByText(
+ 'This payment method is enabled by other extensions. Review extensions to improve the shopper experience.'
+ )
+ ).toBeInTheDocument();
+ cleanup();
+ } );
+
test( 'renders correctly when the payment method is not dismissed', () => {
render(
);
@@ -61,7 +90,8 @@ describe( 'DuplicateNotice', () => {
const paymentMethod = 'ideal';
const props = {
paymentMethod: paymentMethod,
- dismissedDuplicateNotices: [],
+ gatewaysEnablingPaymentMethod: [ 'woocommerce_payments' ],
+ dismissedNotices: {},
setDismissedDuplicateNotices: jest.fn(),
};
const { container } = render( );
@@ -75,11 +105,13 @@ describe( 'DuplicateNotice', () => {
}
// Check if local state update function and Redux action dispatcher are called correctly
- expect( props.setDismissedDuplicateNotices ).toHaveBeenCalledWith( [
- paymentMethod,
- ] );
+ expect( props.setDismissedDuplicateNotices ).toHaveBeenCalledWith( {
+ [ paymentMethod ]: [ 'woocommerce_payments' ],
+ } );
expect( mockDispatch ).toHaveBeenCalledWith( {
- wcpay_duplicate_payment_method_notices_dismissed: [ paymentMethod ],
+ wcpay_duplicate_payment_method_notices_dismissed: {
+ [ paymentMethod ]: [ 'woocommerce_payments' ],
+ },
} );
} );
@@ -88,7 +120,8 @@ describe( 'DuplicateNotice', () => {
diff --git a/client/components/payment-methods-list/payment-method.tsx b/client/components/payment-methods-list/payment-method.tsx
index 7aa2bd7f8fa..76561c686f3 100644
--- a/client/components/payment-methods-list/payment-method.tsx
+++ b/client/components/payment-methods-list/payment-method.tsx
@@ -151,7 +151,7 @@ const PaymentMethod = ( {
dismissedDuplicateNotices,
setDismissedDuplicateNotices,
} = useContext( DuplicatedPaymentMethodsContext );
- const isDuplicate = duplicates.includes( id );
+ const isDuplicate = Object.keys( duplicates ).includes( id );
const needsOverlay =
( isManualCaptureEnabled && ! isAllowingManualCapture ) ||
@@ -368,7 +368,8 @@ const PaymentMethod = ( {
{ isDuplicate && (
{
render(
@@ -172,8 +172,8 @@ describe( 'PaymentMethod', () => {
render(
diff --git a/client/globals.d.ts b/client/globals.d.ts
index c64240cd04f..db7b9ce42a4 100644
--- a/client/globals.d.ts
+++ b/client/globals.d.ts
@@ -2,6 +2,7 @@
* Internal dependencies
*/
import type { MccsDisplayTreeItem, Country } from 'onboarding/types';
+import { PaymentMethodToPluginsMap } from './components/duplicate-notice';
declare global {
const wcpaySettings: {
@@ -90,7 +91,7 @@ declare global {
isEligibilityModalDismissed: boolean;
};
enabledPaymentMethods: string[];
- dismissedDuplicateNotices: string[];
+ dismissedDuplicateNotices: PaymentMethodToPluginsMap;
accountDefaultCurrency: string;
isFRTReviewFeatureActive: boolean;
frtDiscoverBannerSettings: string;
diff --git a/client/payment-methods/test/index.js b/client/payment-methods/test/index.js
index 6ab41a25c90..de01bb5d512 100644
--- a/client/payment-methods/test/index.js
+++ b/client/payment-methods/test/index.js
@@ -448,8 +448,8 @@ describe( 'PaymentMethods', () => {
render(
diff --git a/client/settings/express-checkout/apple-google-pay-item.tsx b/client/settings/express-checkout/apple-google-pay-item.tsx
index 46ca82907a2..a43af822015 100644
--- a/client/settings/express-checkout/apple-google-pay-item.tsx
+++ b/client/settings/express-checkout/apple-google-pay-item.tsx
@@ -34,7 +34,7 @@ const AppleGooglePayExpressCheckoutItem = (): React.ReactElement => {
dismissedDuplicateNotices,
setDismissedDuplicateNotices,
} = useContext( DuplicatedPaymentMethodsContext );
- const isDuplicate = duplicates.includes( id );
+ const isDuplicate = Object.keys( duplicates ).includes( id );
return (
{
{ isDuplicate && (
null,
} );
diff --git a/client/settings/settings-manager/index.js b/client/settings/settings-manager/index.js
index 5b79f668b32..a48b7fb9607 100644
--- a/client/settings/settings-manager/index.js
+++ b/client/settings/settings-manager/index.js
@@ -208,7 +208,7 @@ const SettingsManager = () => {
const [
dismissedDuplicateNotices,
setDismissedDuplicateNotices,
- ] = useState( wcpaySettings.dismissedDuplicateNotices || [] );
+ ] = useState( wcpaySettings.dismissedDuplicateNotices || {} );
return (
diff --git a/includes/class-duplicates-detection-service.php b/includes/class-duplicates-detection-service.php
index b494bd213b2..420545e0ce8 100644
--- a/includes/class-duplicates-detection-service.php
+++ b/includes/class-duplicates-detection-service.php
@@ -60,7 +60,7 @@ public function find_duplicates() {
->keep_duplicates_only();
// Return payment method IDs list so that front-end can successfully compare with its own list.
- return array_keys( $this->gateways_qualified_by_duplicates_detector );
+ return $this->gateways_qualified_by_duplicates_detector;
} catch ( \Exception $e ) {
Logger::warning( 'Duplicates detection service failed silently with the following error: ' . $e->getMessage() );
diff --git a/tests/unit/duplicate-detection/test-class-duplicates-detection-service.php b/tests/unit/duplicate-detection/test-class-duplicates-detection-service.php
index 2dd028d4203..23b696d751c 100644
--- a/tests/unit/duplicate-detection/test-class-duplicates-detection-service.php
+++ b/tests/unit/duplicate-detection/test-class-duplicates-detection-service.php
@@ -66,7 +66,7 @@ public function test_two_cc_both_enabled() {
$result = $this->service->find_duplicates();
$this->assertCount( 1, $result );
- $this->assertEquals( 'card', $result[0] );
+ $this->assertEquals( 'card', array_keys( $result )[0] );
}
public function test_two_cc_one_enabled() {
@@ -83,7 +83,7 @@ public function test_two_apms_enabled() {
$result = $this->service->find_duplicates();
$this->assertCount( 1, $result );
- $this->assertEquals( Giropay_Payment_Method::PAYMENT_METHOD_STRIPE_ID, $result[0] );
+ $this->assertEquals( Giropay_Payment_Method::PAYMENT_METHOD_STRIPE_ID, array_keys( $result )[0] );
}
public function test_two_bnpls_enabled() {
@@ -92,7 +92,7 @@ public function test_two_bnpls_enabled() {
$result = $this->service->find_duplicates();
$this->assertCount( 1, $result );
- $this->assertEquals( Klarna_Payment_Method::PAYMENT_METHOD_STRIPE_ID, $result[0] );
+ $this->assertEquals( Klarna_Payment_Method::PAYMENT_METHOD_STRIPE_ID, array_keys( $result )[0] );
}
public function test_two_prbs_enabled() {
@@ -103,7 +103,7 @@ public function test_two_prbs_enabled() {
$result = $this->service->find_duplicates();
- $this->assertEquals( 'apple_pay_google_pay', $result[0] );
+ $this->assertEquals( 'apple_pay_google_pay', array_keys( $result )[0] );
}
public function test_duplicate_not_enabled_in_woopayments() {
From 3281ce77bca6a09f73f94e88f35178f2d7b22bd3 Mon Sep 17 00:00:00 2001
From: Daniel Mallory
Date: Mon, 10 Jun 2024 09:56:46 +0100
Subject: [PATCH 31/52] Remove unneeded code from PO experiment (#8917)
---
changelog/dev-remove-unneeded-code | 4 ++
...wc-rest-payments-onboarding-controller.php | 45 +---------------
.../class-wc-payments-onboarding-service.php | 15 ------
.../class-wc-payments-api-client.php | 30 -----------
...wc-rest-payments-onboarding-controller.php | 53 -------------------
...t-class-wc-payments-onboarding-service.php | 15 ------
.../test-class-wc-payments-api-client.php | 19 -------
7 files changed, 5 insertions(+), 176 deletions(-)
create mode 100644 changelog/dev-remove-unneeded-code
diff --git a/changelog/dev-remove-unneeded-code b/changelog/dev-remove-unneeded-code
new file mode 100644
index 00000000000..a0f0aa36599
--- /dev/null
+++ b/changelog/dev-remove-unneeded-code
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Clean up and refactor some old code which is no longer in use.
diff --git a/includes/admin/class-wc-rest-payments-onboarding-controller.php b/includes/admin/class-wc-rest-payments-onboarding-controller.php
index 74cbac4dc9f..bfad476ce87 100644
--- a/includes/admin/class-wc-rest-payments-onboarding-controller.php
+++ b/includes/admin/class-wc-rest-payments-onboarding-controller.php
@@ -59,16 +59,6 @@ public function register_routes() {
]
);
- register_rest_route(
- $this->namespace,
- '/' . $this->rest_base . '/required_verification_information',
- [
- 'methods' => WP_REST_Server::READABLE,
- 'callback' => [ $this, 'get_required_verification_information' ],
- 'permission_callback' => [ $this, 'check_permission' ],
- ]
- );
-
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/router/po_eligible',
@@ -122,7 +112,7 @@ public function register_routes() {
],
'callback' => [ $this, 'get_progressive_onboarding_eligible' ],
'permission_callback' => [ $this, 'check_permission' ],
- ],
+ ]
);
}
@@ -138,39 +128,6 @@ public function get_business_types( WP_REST_Request $request ) {
return rest_ensure_response( [ 'data' => $business_types ] );
}
- /**
- * Get required verification information via API.
- *
- * @param WP_REST_Request $request Request object.
- *
- * @return WP_REST_Response|WP_Error
- *
- * @throws Rest_Request_Exception
- */
- public function get_required_verification_information( WP_REST_Request $request ) {
- $country_code = $request->get_param( 'country' ) ?? null;
- $type = $request->get_param( 'type' ) ?? null;
- $structure = $request->get_param( 'structure' ) ?? null;
-
- try {
- if ( ! $country_code || ! $type ) {
- throw new Rest_Request_Exception( __( 'Country or type parameter was missing', 'woocommerce-payments' ) );
- }
-
- $verification_info = $this->onboarding_service->get_required_verification_information( $country_code, $type, $structure );
-
- return rest_ensure_response(
- [
- 'data' => $verification_info,
- ]
- );
- } catch ( Rest_Request_Exception $e ) {
- return new WP_REST_Response( [ 'result' => self::RESULT_BAD_REQUEST ], 400 );
- } catch ( API_Exception $e ) {
- return new WP_Error( $e->get_error_code(), $e->getMessage() );
- }
- }
-
/**
* Get progressive onboarding eligibility via API.
*
diff --git a/includes/class-wc-payments-onboarding-service.php b/includes/class-wc-payments-onboarding-service.php
index eb7da0b288c..7a5440f57a2 100644
--- a/includes/class-wc-payments-onboarding-service.php
+++ b/includes/class-wc-payments-onboarding-service.php
@@ -142,21 +142,6 @@ function () {
return $business_types;
}
- /**
- * Get the required verification information for the selected country/type/structure combination from the API.
- *
- * @param string $country_code The currently selected country code.
- * @param string $type The currently selected business type.
- * @param string|null $structure The currently selected business structure (optional).
- *
- * @return array
- *
- * @throws API_Exception
- */
- public function get_required_verification_information( string $country_code, string $type, $structure = null ): array {
- return $this->payments_api_client->get_onboarding_required_verification_information( $country_code, $type, $structure );
- }
-
/**
* Check whether the business types fetched from the cache are valid.
*
diff --git a/includes/wc-payment-api/class-wc-payments-api-client.php b/includes/wc-payment-api/class-wc-payments-api-client.php
index 41e8c35cad5..4127e620606 100644
--- a/includes/wc-payment-api/class-wc-payments-api-client.php
+++ b/includes/wc-payment-api/class-wc-payments-api-client.php
@@ -972,36 +972,6 @@ public function get_onboarding_business_types(): array {
return $business_types;
}
- /**
- * Get the required verification information, needed for our KYC onboarding flow.
- *
- * @param string $country_code The country code.
- * @param string $type The business type.
- * @param string|null $structure The business structure (optional).
- *
- * @return array An array containing the required verification information.
- *
- * @throws API_Exception Exception thrown on request failure.
- */
- public function get_onboarding_required_verification_information( string $country_code, string $type, $structure = null ) {
- $params = [
- 'country' => $country_code,
- 'type' => $type,
- ];
-
- if ( ! is_null( $structure ) ) {
- $params = array_merge( $params, [ 'structure' => $structure ] );
- }
-
- return $this->request(
- $params,
- self::ONBOARDING_API . '/required_verification_information',
- self::GET,
- true,
- true
- );
- }
-
/**
* Get a link's details from the server.
*
diff --git a/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php b/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php
index 06606c14902..e7bc456c46b 100644
--- a/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php
+++ b/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php
@@ -64,59 +64,6 @@ public function test_get_business_types() {
$this->assertSame( [ 'data' => $mock_business_types ], $response->get_data() );
}
- public function test_get_required_verification_information_with_missing_params() {
- $request = new WP_REST_Request( 'GET', '', [ 'foo' => 'bar' ] );
- $response = $this->controller->get_required_verification_information( $request );
-
- $this->assertSame( 400, $response->status );
- $this->assertSame(
- [ 'result' => WC_REST_Payments_Onboarding_Controller::RESULT_BAD_REQUEST ],
- $response->get_data()
- );
- }
-
- public function test_get_required_verification_information() {
- $mock_requirements = [
- 'business_profile.url',
- 'business_profile.mcc',
- 'representative.first_name',
- 'representative.last_name',
- 'representative.dob.day',
- 'representative.dob.month',
- 'representative.dob.year',
- 'representative.phone',
- 'representative.email',
- 'representative.address.line1',
- 'representative.address.postal_code',
- 'representative.address.city',
- 'representative.address.state',
- 'representative.ssn_last_4',
- 'company.name',
- 'company.tax_id',
- 'tos_acceptance.ip',
- 'tos_acceptance.date',
- 'external_account',
- ];
-
- $this->mock_onboarding_service
- ->expects( $this->once() )
- ->method( 'get_required_verification_information' )
- ->willReturn( $mock_requirements );
-
- $request = new WP_REST_Request( 'GET' );
- $request->set_url_params(
- [
- 'country' => Country_Code::UNITED_STATES,
- 'type' => 'company',
- 'structure' => 'sole_proprietor',
- ]
- );
- $response = $this->controller->get_required_verification_information( $request );
-
- $this->assertSame( 200, $response->status );
- $this->assertSame( [ 'data' => $mock_requirements ], $response->get_data() );
- }
-
public function test_get_progressive_onboarding_eligible() {
$this->mock_api_client
->expects( $this->once() )
diff --git a/tests/unit/test-class-wc-payments-onboarding-service.php b/tests/unit/test-class-wc-payments-onboarding-service.php
index 7f32038c975..22a1274f9bb 100644
--- a/tests/unit/test-class-wc-payments-onboarding-service.php
+++ b/tests/unit/test-class-wc-payments-onboarding-service.php
@@ -141,21 +141,6 @@ public function test_filters_registered_properly() {
$this->assertNotFalse( has_filter( 'admin_body_class', [ $this->onboarding_service, 'add_admin_body_classes' ] ) );
}
- public function test_get_required_verification_information() {
- $mock_requirements = [ 'requirement1', 'requirement2', 'requirement3' ];
-
- $this->mock_api_client
- ->expects( $this->once() )
- ->method( 'get_onboarding_required_verification_information' )
- ->with( Country_Code::UNITED_STATES, 'company', 'sole_propietorship' )
- ->willReturn( $mock_requirements );
-
- $this->assertEquals(
- $mock_requirements,
- $this->onboarding_service->get_required_verification_information( Country_Code::UNITED_STATES, 'company', 'sole_propietorship' )
- );
- }
-
public function test_get_cached_business_types_with_no_server_connection() {
$this->mock_api_client
->expects( $this->once() )
diff --git a/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php b/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php
index e4a252c369a..ed7cc963b7e 100644
--- a/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php
+++ b/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php
@@ -347,25 +347,6 @@ public function test_get_onboarding_business_types() {
$this->payments_api_client->get_onboarding_business_types();
}
- /**
- * Test getting onboarding required verification information.
- *
- * @throws API_Exception
- */
- public function test_get_onboarding_required_verification_information() {
- $this->mock_http_client
- ->expects( $this->once() )
- ->method( 'remote_request' )
- ->with(
- $this->containsIdentical( 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/onboarding/required_verification_information?test_mode=0&country=country&type=type' ),
- null,
- true,
- true // get_onboarding_required_verification_information should use user token auth.
- );
-
- $this->payments_api_client->get_onboarding_required_verification_information( 'country', 'type' );
- }
-
public function test_get_link() {
$this->mock_http_client
->expects( $this->once() )
From 557030d067d0b32be9d38a376150de37000a4097 Mon Sep 17 00:00:00 2001
From: Timur Karimov
Date: Mon, 10 Jun 2024 11:01:01 +0200
Subject: [PATCH 32/52] Ensure sh compatibility of the pre-push script (#8931)
---
.husky/pre-push | 8 ++++----
changelog/misc-move-prepush-to-sh | 4 ++++
2 files changed, 8 insertions(+), 4 deletions(-)
create mode 100644 changelog/misc-move-prepush-to-sh
diff --git a/.husky/pre-push b/.husky/pre-push
index 05b3f2a4632..cb2c2d3005b 100755
--- a/.husky/pre-push
+++ b/.husky/pre-push
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# check if main stream (stdout and stderr) are attached to the terminal
@@ -7,12 +7,12 @@ if [ -t 1 ] && [ -t 2 ]; then
exec < /dev/tty
fi
-PROTECTED_BRANCH=("develop" "trunk")
+PROTECTED_BRANCH_LIST="develop trunk"
CURRENT_BRANCH=$(git branch --show-current)
-if [[ " ${PROTECTED_BRANCH[@]} " =~ " ${CURRENT_BRANCH} " ]]; then
+if echo "$PROTECTED_BRANCH_LIST" | grep -q -w "$CURRENT_BRANCH"; then
read -p "$CURRENT_BRANCH is a protected branch. Are you sure you want to push? (y/n): " confirmation
- if [ "$confirmation" != "y" ]; then
+ if [ "$confirmation" != "y" ]; then
echo "Push aborted"
exit 1
fi
diff --git a/changelog/misc-move-prepush-to-sh b/changelog/misc-move-prepush-to-sh
new file mode 100644
index 00000000000..bb1e71a81c8
--- /dev/null
+++ b/changelog/misc-move-prepush-to-sh
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Add sh support in pre-push husky script.
From 7a73407d347752638528ac8b423e99f9b9177eb0 Mon Sep 17 00:00:00 2001
From: Valery Sukhomlinov <683297+dmvrtx@users.noreply.github.com>
Date: Mon, 10 Jun 2024 13:52:30 +0200
Subject: [PATCH 33/52] Bump WooCommerce Tested To version to 8.9.2 (#8930)
---
changelog/2024-06-10-07-11-37-125865 | 4 ++++
woocommerce-payments.php | 2 +-
2 files changed, 5 insertions(+), 1 deletion(-)
create mode 100644 changelog/2024-06-10-07-11-37-125865
diff --git a/changelog/2024-06-10-07-11-37-125865 b/changelog/2024-06-10-07-11-37-125865
new file mode 100644
index 00000000000..f95872a8d2f
--- /dev/null
+++ b/changelog/2024-06-10-07-11-37-125865
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Bump WooCommerce Tested To version to 8.9.2
diff --git a/woocommerce-payments.php b/woocommerce-payments.php
index 46546f65539..0280deefa22 100644
--- a/woocommerce-payments.php
+++ b/woocommerce-payments.php
@@ -8,7 +8,7 @@
* Text Domain: woocommerce-payments
* Domain Path: /languages
* WC requires at least: 7.6
- * WC tested up to: 8.9.1
+ * WC tested up to: 8.9.2
* Requires at least: 6.0
* Requires PHP: 7.3
* Version: 7.7.0
From 342ea141413fa8e07309a9abdde4c8d11230927d Mon Sep 17 00:00:00 2001
From: Rafael Zaleski
Date: Mon, 10 Jun 2024 17:46:10 -0300
Subject: [PATCH 34/52] Fix PM title for PRBs not displaying correctly due to
ECE code (#8934)
---
changelog/fix-ece-payment-method-title | 4 ++++
includes/class-wc-payment-gateway-wcpay.php | 2 +-
2 files changed, 5 insertions(+), 1 deletion(-)
create mode 100644 changelog/fix-ece-payment-method-title
diff --git a/changelog/fix-ece-payment-method-title b/changelog/fix-ece-payment-method-title
new file mode 100644
index 00000000000..dd2eadcbdbc
--- /dev/null
+++ b/changelog/fix-ece-payment-method-title
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix Payment method title for PRBs not displaying correctly because of ECE code.
diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php
index 57988038e88..4d87ac67b9f 100644
--- a/includes/class-wc-payment-gateway-wcpay.php
+++ b/includes/class-wc-payment-gateway-wcpay.php
@@ -1800,7 +1800,7 @@ public function process_payment_for_order( $cart, $payment_information, $schedul
$payment_method_type = $this->get_payment_method_type_for_setup_intent( $intent, $token );
}
- if ( empty( $_POST['payment_request_type'] ) || empty( $_POST['express_payment_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
+ if ( empty( $_POST['payment_request_type'] ) && empty( $_POST['express_payment_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$this->set_payment_method_title_for_order( $order, $payment_method_type, $payment_method_details );
}
From 9f46bac31db133dda0ce9c31286a326210a4a6c5 Mon Sep 17 00:00:00 2001
From: Eric Jinks <3147296+Jinksi@users.noreply.github.com>
Date: Tue, 11 Jun 2024 11:19:23 +1000
Subject: [PATCH 35/52] Improve code quality of `RecentDepositsList` component,
including outdated `Next Deposit` references, complexity/nesting reduction
(#8887)
Co-authored-by: Shendy <73803630+shendy-a8c@users.noreply.github.com>
---
.../dev-code-quality-recent-deposits-list | 5 ++
.../recent-deposits-list.tsx | 49 +++++--------------
2 files changed, 18 insertions(+), 36 deletions(-)
create mode 100644 changelog/dev-code-quality-recent-deposits-list
diff --git a/changelog/dev-code-quality-recent-deposits-list b/changelog/dev-code-quality-recent-deposits-list
new file mode 100644
index 00000000000..4eb237a289c
--- /dev/null
+++ b/changelog/dev-code-quality-recent-deposits-list
@@ -0,0 +1,5 @@
+Significance: patch
+Type: dev
+Comment: Non-user-facing refactor: improve code quality of recent deposit list component
+
+
diff --git a/client/components/deposits-overview/recent-deposits-list.tsx b/client/components/deposits-overview/recent-deposits-list.tsx
index 119e7adc92f..1fed2448758 100644
--- a/client/components/deposits-overview/recent-deposits-list.tsx
+++ b/client/components/deposits-overview/recent-deposits-list.tsx
@@ -24,26 +24,26 @@ import { CachedDeposit } from 'wcpay/types/deposits';
import { formatCurrency } from 'wcpay/utils/currency';
import { getDetailsURL } from 'wcpay/components/details-link';
-interface DepositRowProps {
- deposit: CachedDeposit;
-}
-
interface RecentDepositsProps {
deposits: CachedDeposit[];
}
-const tableClass = 'wcpay-deposits-overview__table';
-
/**
- * Renders a recent deposits table row.
+ * Renders the Recent Deposit list component.
*
- * @return {JSX.Element} Deposit table row.
+ * This component includes the recent deposit heading, table and notice.
*/
-const DepositTableRow: React.FC< DepositRowProps > = ( {
- deposit,
-} ): JSX.Element => {
- return (
-
+const RecentDepositsList: React.FC< RecentDepositsProps > = ( {
+ deposits,
+} ) => {
+ if ( deposits.length === 0 ) {
+ return null;
+ }
+
+ const tableClass = 'wcpay-deposits-overview__table';
+
+ const depositRows = deposits.map( ( deposit ) => (
+
@@ -57,33 +57,10 @@ const DepositTableRow: React.FC< DepositRowProps > = ( {
{ formatCurrency( deposit.amount, deposit.currency ) }
- );
-};
-
-/**
- * Renders the Recent Deposit details component.
- *
- * This component includes the recent deposit heading, table and notice.
- *
- * @param {RecentDepositsProps} props Recent Deposit props.
- * @return {JSX.Element} Rendered element with Next Deposit details.
- */
-const RecentDepositsList: React.FC< RecentDepositsProps > = ( {
- deposits,
-} ): JSX.Element => {
- if ( deposits.length === 0 ) {
- return <>>;
- }
-
- const depositRows = deposits.map( ( deposit ) => (
-
-
-
) );
return (
<>
- { /* Next Deposit Table */ }
From cbc1e03ae3057f62e298217d16d15229be2f090d Mon Sep 17 00:00:00 2001
From: Eric Jinks <3147296+Jinksi@users.noreply.github.com>
Date: Tue, 11 Jun 2024 17:19:09 +1000
Subject: [PATCH 36/52] Payment Activity Widget: Select report currency when
clicking "View report" (#8941)
---
.../fix-8938-payment-activity-view-report-currency | 5 +++++
.../payment-activity/payment-activity-data.tsx | 4 ++++
.../test/__snapshots__/index.test.tsx.snap | 8 ++++----
.../components/payment-activity/test/index.test.tsx | 11 ++++++++++-
4 files changed, 23 insertions(+), 5 deletions(-)
create mode 100644 changelog/fix-8938-payment-activity-view-report-currency
diff --git a/changelog/fix-8938-payment-activity-view-report-currency b/changelog/fix-8938-payment-activity-view-report-currency
new file mode 100644
index 00000000000..4020259ca84
--- /dev/null
+++ b/changelog/fix-8938-payment-activity-view-report-currency
@@ -0,0 +1,5 @@
+Significance: patch
+Type: fix
+Comment: Behind feature flag: Ensure currency is set in Payment Activity "View report" urls
+
+
diff --git a/client/components/payment-activity/payment-activity-data.tsx b/client/components/payment-activity/payment-activity-data.tsx
index 32ba0ec522d..b75678e5f5e 100644
--- a/client/components/payment-activity/payment-activity-data.tsx
+++ b/client/components/payment-activity/payment-activity-data.tsx
@@ -112,6 +112,7 @@ const PaymentActivityDataComponent: React.FC< Props > = ( {
page: 'wc-admin',
path: '/payments/transactions',
filter: 'advanced',
+ store_currency_is: currency,
'date_between[0]': moment(
paymentActivityData?.date_start
).format( 'YYYY-MM-DD' ),
@@ -154,6 +155,7 @@ const PaymentActivityDataComponent: React.FC< Props > = ( {
page: 'wc-admin',
path: '/payments/transactions',
filter: 'advanced',
+ store_currency_is: currency,
'date_between[0]': moment(
paymentActivityData?.date_start
).format( 'YYYY-MM-DD' ),
@@ -176,6 +178,7 @@ const PaymentActivityDataComponent: React.FC< Props > = ( {
page: 'wc-admin',
path: '/payments/transactions',
filter: 'advanced',
+ store_currency_is: currency,
'date_between[0]': moment(
paymentActivityData?.date_start
).format( 'YYYY-MM-DD' ),
@@ -198,6 +201,7 @@ const PaymentActivityDataComponent: React.FC< Props > = ( {
page: 'wc-admin',
path: '/payments/transactions',
filter: 'advanced',
+ store_currency_is: currency,
'date_between[0]': moment(
paymentActivityData?.date_start
).format( 'YYYY-MM-DD' ),
diff --git a/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap b/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap
index 2a55f39d20a..34a62a77d21 100644
--- a/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap
+++ b/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap
@@ -76,7 +76,7 @@ exports[`PaymentActivity component should render 1`] = `
View report
@@ -136,7 +136,7 @@ exports[`PaymentActivity component should render 1`] = `
View report
@@ -165,7 +165,7 @@ exports[`PaymentActivity component should render 1`] = `
View report
@@ -194,7 +194,7 @@ exports[`PaymentActivity component should render 1`] = `
View report
diff --git a/client/components/payment-activity/test/index.test.tsx b/client/components/payment-activity/test/index.test.tsx
index cfd35de8451..0955df2a32d 100644
--- a/client/components/payment-activity/test/index.test.tsx
+++ b/client/components/payment-activity/test/index.test.tsx
@@ -122,7 +122,7 @@ describe( 'PaymentActivity component', () => {
} );
it( 'should render', () => {
- const { container, getByText, getByLabelText } = render(
+ const { container, getByText, getByLabelText, getAllByText } = render(
);
@@ -133,6 +133,15 @@ describe( 'PaymentActivity component', () => {
const tpvElement = getByLabelText( 'Total payment volume' );
expect( tpvElement ).toHaveTextContent( '€1.234,56' );
+ // Check the "View report" link is rendered with the correct currency query param.
+ const viewReportLinks = getAllByText( 'View report' );
+ viewReportLinks.forEach( ( link ) => {
+ expect( link ).toHaveAttribute(
+ 'href',
+ expect.stringContaining( 'store_currency_is=eur' )
+ );
+ } );
+
expect( container ).toMatchSnapshot();
} );
From 7b043b2b6e357d01154cc0cd931485a0b76f3c52 Mon Sep 17 00:00:00 2001
From: Jessy Pappachan <32092402+jessy-p@users.noreply.github.com>
Date: Tue, 11 Jun 2024 13:42:02 +0530
Subject: [PATCH 37/52] Adding adjustments to TPV Report (#8940)
Co-authored-by: Jessy
---
changelog/add-adjustments-tpv-report | 5 +++++
client/components/payment-activity/payment-activity-data.tsx | 1 +
.../payment-activity/test/__snapshots__/index.test.tsx.snap | 2 +-
3 files changed, 7 insertions(+), 1 deletion(-)
create mode 100644 changelog/add-adjustments-tpv-report
diff --git a/changelog/add-adjustments-tpv-report b/changelog/add-adjustments-tpv-report
new file mode 100644
index 00000000000..a10f64850ae
--- /dev/null
+++ b/changelog/add-adjustments-tpv-report
@@ -0,0 +1,5 @@
+Significance: patch
+Type: update
+Comment: Modified TPV report on Payment Activity Card. Behind Payment Activity Card Feature Flag.
+
+
diff --git a/client/components/payment-activity/payment-activity-data.tsx b/client/components/payment-activity/payment-activity-data.tsx
index b75678e5f5e..9ba4ce664eb 100644
--- a/client/components/payment-activity/payment-activity-data.tsx
+++ b/client/components/payment-activity/payment-activity-data.tsx
@@ -28,6 +28,7 @@ const searchTermsForViewReportLink = {
'dispute',
'dispute_reversal',
'card_reader_fee',
+ 'adjustment',
],
charge: [ 'charge', 'payment', 'adjustment' ],
diff --git a/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap b/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap
index 34a62a77d21..2db80f07baf 100644
--- a/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap
+++ b/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap
@@ -76,7 +76,7 @@ exports[`PaymentActivity component should render 1`] = `
View report
From fa1359b2bd1824b22ab5382b903e483b3131e724 Mon Sep 17 00:00:00 2001
From: Alefe Souza
Date: Tue, 11 Jun 2024 12:56:02 -0300
Subject: [PATCH 38/52] Prevent account creation during WooPay preflight
request (#8924)
---
changelog/fix-woopay-preflight-create-account | 4 +++
includes/class-wc-payment-gateway-wcpay.php | 3 ++
.../class-wc-payments-order-success-page.php | 30 +++++++++++++++++++
3 files changed, 37 insertions(+)
create mode 100644 changelog/fix-woopay-preflight-create-account
diff --git a/changelog/fix-woopay-preflight-create-account b/changelog/fix-woopay-preflight-create-account
new file mode 100644
index 00000000000..c21914ac58f
--- /dev/null
+++ b/changelog/fix-woopay-preflight-create-account
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent account creation during WooPay preflight request.
diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php
index 4d87ac67b9f..bd3c9ab913d 100644
--- a/includes/class-wc-payment-gateway-wcpay.php
+++ b/includes/class-wc-payment-gateway-wcpay.php
@@ -676,6 +676,9 @@ public function remove_all_actions_on_preflight_check( $response, $handler, $req
remove_all_actions( 'woocommerce_store_api_checkout_order_processed' );
// Avoid increasing coupon usage count during preflight check.
remove_all_actions( 'woocommerce_order_status_pending' );
+
+ // Avoid creating new accounts during preflight check.
+ remove_all_filters( 'woocommerce_checkout_registration_required' );
}
return $response;
diff --git a/includes/class-wc-payments-order-success-page.php b/includes/class-wc-payments-order-success-page.php
index f4aca6ebccb..8051d8aab1c 100644
--- a/includes/class-wc-payments-order-success-page.php
+++ b/includes/class-wc-payments-order-success-page.php
@@ -16,6 +16,7 @@ class WC_Payments_Order_Success_Page {
* Constructor.
*/
public function __construct() {
+ add_filter( 'woocommerce_order_received_verify_known_shoppers', [ $this, 'determine_woopay_order_received_verify_known_shoppers' ], 11 );
add_action( 'woocommerce_before_thankyou', [ $this, 'register_payment_method_override' ] );
add_action( 'woocommerce_order_details_before_order_table', [ $this, 'unregister_payment_method_override' ] );
add_filter( 'woocommerce_thankyou_order_received_text', [ $this, 'add_notice_previous_paid_order' ], 11 );
@@ -227,4 +228,33 @@ public function enqueue_scripts() {
'all',
);
}
+
+ /**
+ * Make sure we show the TYP page for orders paid with WooPay
+ * that create new user accounts, code mainly copied from
+ * WooCommerce WC_Shortcode_Checkout::order_received and
+ * WC_Shortcode_Checkout::guest_should_verify_email.
+ *
+ * @param bool $value The current value for this filter.
+ */
+ public function determine_woopay_order_received_verify_known_shoppers( $value ) {
+ global $wp;
+
+ $order_id = $wp->query_vars['order-received'];
+ $order_key = apply_filters( 'woocommerce_thankyou_order_key', empty( $_GET['key'] ) ? '' : wc_clean( wp_unslash( $_GET['key'] ) ) );
+ $order = wc_get_order( $order_id );
+
+ if ( ( ! $order instanceof WC_Order ) || ! $order->get_meta( 'is_woopay' ) || ! hash_equals( $order->get_order_key(), $order_key ) ) {
+ return $value;
+ }
+
+ $verification_grace_period = (int) apply_filters( 'woocommerce_order_email_verification_grace_period', 10 * MINUTE_IN_SECONDS, $order );
+ $date_created = $order->get_date_created();
+
+ // We do not need to verify the email address if we are within the grace period immediately following order creation.
+ $is_within_grace_period = is_a( $date_created, \WC_DateTime::class, true )
+ && time() - $date_created->getTimestamp() <= $verification_grace_period;
+
+ return ! $is_within_grace_period;
+ }
}
From 849d22bd755f8b8e342d9dc7455a1c2fcc81bbf5 Mon Sep 17 00:00:00 2001
From: Francesco
Date: Wed, 12 Jun 2024 08:35:42 +0200
Subject: [PATCH 39/52] fix: address normalization on checkout for tokenized
cart PRBs (#8918)
---
changelog/fix-tokenized-cart-place-order-uk | 4 ++
...ayments-payment-request-button-handler.php | 62 +++++++++++++++----
...ayments-payment-request-button-handler.php | 37 +++++++++++
3 files changed, 91 insertions(+), 12 deletions(-)
create mode 100644 changelog/fix-tokenized-cart-place-order-uk
diff --git a/changelog/fix-tokenized-cart-place-order-uk b/changelog/fix-tokenized-cart-place-order-uk
new file mode 100644
index 00000000000..99cfe4d4318
--- /dev/null
+++ b/changelog/fix-tokenized-cart-place-order-uk
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+fix: address normalization on checkout for tokenized cart PRBs
diff --git a/includes/class-wc-payments-payment-request-button-handler.php b/includes/class-wc-payments-payment-request-button-handler.php
index 04a0fbdb83f..6cbd3cf137c 100644
--- a/includes/class-wc-payments-payment-request-button-handler.php
+++ b/includes/class-wc-payments-payment-request-button-handler.php
@@ -110,7 +110,15 @@ public function init() {
add_filter( 'pre_option_wcpay_is_apple_pay_enabled', [ $this, 'get_option_is_apple_pay_enabled' ], 10, 1 );
if ( WC_Payments_Features::is_tokenized_cart_prb_enabled() ) {
- add_action( 'woocommerce_store_api_checkout_update_order_from_request', [ $this, 'tokenized_cart_set_payment_method_type' ], 10, 2 );
+ add_action(
+ 'woocommerce_store_api_checkout_update_order_from_request',
+ [
+ $this,
+ 'tokenized_cart_set_payment_method_type',
+ ],
+ 10,
+ 2
+ );
add_filter( 'rest_pre_dispatch', [ $this, 'tokenized_cart_store_api_address_normalization' ], 10, 3 );
add_filter( 'rest_pre_dispatch', [ $this, 'tokenized_cart_store_api_nonce_overwrite' ], 10, 3 );
add_filter(
@@ -209,16 +217,27 @@ public function tokenized_cart_store_api_address_normalization( $response, $serv
// This route is used to get shipping rates.
// GooglePay/ApplePay might provide us with "trimmed" zip codes.
// If that's the case, let's temporarily allow to skip the zip code validation, in order to get some shipping rates.
- if ( $request->get_route() === '/wc/store/v1/cart/update-customer' ) {
+ $is_update_customer_route = $request->get_route() === '/wc/store/v1/cart/update-customer';
+ if ( $is_update_customer_route ) {
add_filter( 'woocommerce_validate_postcode', [ $this, 'maybe_skip_postcode_validation' ], 10, 3 );
}
$request_data = $request->get_json_params();
if ( isset( $request_data['shipping_address'] ) ) {
- $request->set_param( 'shipping_address', $this->transform_prb_address_data( $request_data['shipping_address'] ) );
+ $request->set_param( 'shipping_address', $this->transform_prb_address_state_data( $request_data['shipping_address'] ) );
+ // on the "update customer" route, GooglePay/Apple pay might provide redacted postcode data.
+ // we need to modify the zip code to ensure that shipping zone identification still works.
+ if ( $is_update_customer_route ) {
+ $request->set_param( 'shipping_address', $this->transform_prb_address_postcode_data( $request_data['shipping_address'] ) );
+ }
}
if ( isset( $request_data['billing_address'] ) ) {
- $request->set_param( 'billing_address', $this->transform_prb_address_data( $request_data['billing_address'] ) );
+ $request->set_param( 'billing_address', $this->transform_prb_address_state_data( $request_data['billing_address'] ) );
+ // on the "update customer" route, GooglePay/Apple pay might provide redacted postcode data.
+ // we need to modify the zip code to ensure that shipping zone identification still works.
+ if ( $is_update_customer_route ) {
+ $request->set_param( 'billing_address', $this->transform_prb_address_postcode_data( $request_data['billing_address'] ) );
+ }
}
return $response;
@@ -260,7 +279,7 @@ public function maybe_skip_postcode_validation( $valid, $postcode, $country ) {
// We padded the string with `0` in the `get_normalized_postal_code` method.
// It's a flimsy check, but better than nothing.
// Plus, this check is only made for the scenarios outlined in the `tokenized_cart_store_api_address_normalization` method.
- if ( substr( $postcode, -1 ) === '0' ) {
+ if ( substr( $postcode, - 1 ) === '0' ) {
return true;
}
@@ -268,13 +287,13 @@ public function maybe_skip_postcode_validation( $valid, $postcode, $country ) {
}
/**
- * Transform a GooglePay/ApplePay address data fields into values that are valid for WooCommerce.
+ * Transform a GooglePay/ApplePay state address data fields into values that are valid for WooCommerce.
*
* @param array $address The address to normalize from the GooglePay/ApplePay request.
*
* @return array
*/
- private function transform_prb_address_data( $address ) {
+ private function transform_prb_address_state_data( $address ) {
$country = $address['country'] ?? '';
if ( empty( $country ) ) {
return $address;
@@ -286,6 +305,22 @@ private function transform_prb_address_data( $address ) {
$address['state'] = $this->get_normalized_state( $state, $country );
}
+ return $address;
+ }
+
+ /**
+ * Transform a GooglePay/ApplePay postcode address data fields into values that are valid for WooCommerce.
+ *
+ * @param array $address The address to normalize from the GooglePay/ApplePay request.
+ *
+ * @return array
+ */
+ private function transform_prb_address_postcode_data( $address ) {
+ $country = $address['country'] ?? '';
+ if ( empty( $country ) ) {
+ return $address;
+ }
+
// Normalizes postal code in case of redacted data from Apple Pay or Google Pay.
$postcode = $address['postcode'] ?? '';
if ( ! empty( $postcode ) ) {
@@ -395,6 +430,7 @@ public function get_button_settings() {
* @param object $product WC_Product_* object.
* @param bool $is_deposit Whether customer is paying a deposit.
* @param int $deposit_plan_id The ID of the deposit plan.
+ *
* @return mixed Total price.
*
* @throws Invalid_Price_Exception Whenever a product has no price.
@@ -441,7 +477,7 @@ public function get_product_price( $product, ?bool $is_deposit = null, int $depo
if ( ! is_numeric( $base_price ) || ! is_numeric( $sign_up_fee ) ) {
$error_message = sprintf(
- // Translators: %d is the numeric ID of the product without a price.
+ // Translators: %d is the numeric ID of the product without a price.
__( 'Express checkout does not support products without prices! Please add a price to product #%d', 'woocommerce-payments' ),
(int) $product->get_id()
);
@@ -492,6 +528,7 @@ public function get_product_data() {
$price = $this->get_product_price( $product );
} catch ( Invalid_Price_Exception $e ) {
Logger::log( $e->getMessage() );
+
return false;
}
@@ -572,7 +609,7 @@ public function display_pay_for_order_page_html( $order ) {
if ( $order->get_shipping_total() ) {
$shipping_label = sprintf(
- // Translators: %s is the name of the shipping method.
+ // Translators: %s is the name of the shipping method.
__( 'Shipping (%s)', 'woocommerce-payments' ),
$order->get_shipping_method()
);
@@ -619,7 +656,7 @@ public function get_cart_data() {
* Filters the gateway title to reflect Payment Request type
*
* @param string $title Gateway title.
- * @param string $id Gateway ID.
+ * @param string $id Gateway ID.
*/
public function filter_gateway_title( $title, $id ) {
if ( 'woocommerce_payments' !== $id || ! is_admin() ) {
@@ -1168,7 +1205,7 @@ public function get_shipping_options( $shipping_address, $itemized_display_items
$chosen_method_id = $chosen_shipping_methods[0];
$compare_shipping_options = function ( $a, $b ) use ( $chosen_method_id ) {
if ( $a['id'] === $chosen_method_id ) {
- return -1;
+ return - 1;
}
if ( $b['id'] === $chosen_method_id ) {
@@ -1439,6 +1476,7 @@ public function normalize_state() {
*/
public function is_normalized_state( $state, $country ) {
$wc_states = WC()->countries->get_states( $country );
+
return is_array( $wc_states ) && array_key_exists( $state, $wc_states );
}
@@ -1562,7 +1600,7 @@ public function validate_state() {
if ( ! $is_supported ) {
wc_add_notice(
sprintf(
- /* translators: %s: country. */
+ /* translators: %s: country. */
__( 'The payment request button is not supported in %s because some required fields couldn\'t be verified. Please proceed to the checkout page and try again.', 'woocommerce-payments' ),
$countries[ $posted_data['billing_country'] ] ?? $posted_data['billing_country']
),
diff --git a/tests/unit/test-class-wc-payments-payment-request-button-handler.php b/tests/unit/test-class-wc-payments-payment-request-button-handler.php
index e368d3c83ca..222fff3d742 100644
--- a/tests/unit/test-class-wc-payments-payment-request-button-handler.php
+++ b/tests/unit/test-class-wc-payments-payment-request-button-handler.php
@@ -366,6 +366,7 @@ public function test_tokenized_cart_address_state_normalization() {
public function test_tokenized_cart_address_postcode_normalization() {
$request = new WP_REST_Request();
+ $request->set_route( '/wc/store/v1/cart/update-customer' );
$request->set_header( 'X-WooPayments-Express-Payment-Request', 'true' );
$request->set_header( 'X-WooPayments-Express-Payment-Request-Nonce', wp_create_nonce( 'woopayments_tokenized_cart_nonce' ) );
$request->set_header( 'Content-Type', 'application/json' );
@@ -395,6 +396,42 @@ public function test_tokenized_cart_address_postcode_normalization() {
$this->assertSame( '90210', $billing_address['postcode'] );
}
+ public function test_tokenized_cart_avoid_address_postcode_normalization_if_route_incorrect() {
+ $request = new WP_REST_Request();
+ $request->set_route( '/wc/store/v1/checkout' );
+ $request->set_header( 'X-WooPayments-Express-Payment-Request', 'true' );
+ $request->set_header( 'X-WooPayments-Express-Payment-Request-Nonce', wp_create_nonce( 'woopayments_tokenized_cart_nonce' ) );
+ $request->set_header( 'Content-Type', 'application/json' );
+ $request->set_param(
+ 'shipping_address',
+ [
+ 'country' => 'CA',
+ 'postcode' => 'H3B',
+ 'state' => 'Colombie-Britannique',
+ ]
+ );
+ $request->set_param(
+ 'billing_address',
+ [
+ 'country' => 'CA',
+ 'postcode' => 'H3B',
+ 'state' => 'Colombie-Britannique',
+ ]
+ );
+
+ $this->pr->tokenized_cart_store_api_address_normalization( null, null, $request );
+
+ $shipping_address = $request->get_param( 'shipping_address' );
+ $billing_address = $request->get_param( 'billing_address' );
+
+ // this should be modified.
+ $this->assertSame( 'BC', $shipping_address['state'] );
+ $this->assertSame( 'BC', $billing_address['state'] );
+ // this shouldn't be modified.
+ $this->assertSame( 'H3B', $shipping_address['postcode'] );
+ $this->assertSame( 'H3B', $billing_address['postcode'] );
+ }
+
public function test_get_shipping_options_returns_shipping_options() {
$data = $this->pr->get_shipping_options( self::SHIPPING_ADDRESS );
From 983890425077bda2a354751e2352c8190882826c Mon Sep 17 00:00:00 2001
From: Nagesh Pai <4162931+nagpai@users.noreply.github.com>
Date: Wed, 12 Jun 2024 13:46:03 +0530
Subject: [PATCH 40/52] Reporting: Add preset date selector to the Payment
activity widget (#8927)
Co-authored-by: Nagesh Pai
Co-authored-by: Jessy
Co-authored-by: Eric Jinks <3147296+Jinksi@users.noreply.github.com>
---
changelog/8734-add-date-selector | 5 +
client/components/payment-activity/hooks.ts | 132 ++++++++++++++++++
client/components/payment-activity/index.tsx | 96 ++++++++++---
client/components/payment-activity/style.scss | 22 +++
.../test/__snapshots__/index.test.tsx.snap | 49 ++++++-
client/components/payment-activity/types.ts | 4 -
client/data/payment-activity/actions.ts | 10 +-
client/data/payment-activity/hooks.ts | 9 +-
client/data/payment-activity/reducer.ts | 19 ++-
client/data/payment-activity/resolvers.ts | 2 +-
client/data/payment-activity/selectors.ts | 11 +-
.../payment-activity/test/reducer.test.ts | 15 +-
.../payment-activity/test/resolver.test.ts | 2 +-
client/data/payment-activity/types.d.ts | 4 +-
14 files changed, 337 insertions(+), 43 deletions(-)
create mode 100644 changelog/8734-add-date-selector
create mode 100644 client/components/payment-activity/hooks.ts
delete mode 100644 client/components/payment-activity/types.ts
diff --git a/changelog/8734-add-date-selector b/changelog/8734-add-date-selector
new file mode 100644
index 00000000000..e6bb5d0d600
--- /dev/null
+++ b/changelog/8734-add-date-selector
@@ -0,0 +1,5 @@
+Significance: patch
+Type: add
+Comment: Changes behind a feature flag. The PR allows merchants to change the time period of stats shown in the payment activity widget.
+
+
diff --git a/client/components/payment-activity/hooks.ts b/client/components/payment-activity/hooks.ts
new file mode 100644
index 00000000000..2f5d04da975
--- /dev/null
+++ b/client/components/payment-activity/hooks.ts
@@ -0,0 +1,132 @@
+/**
+ * External dependencies
+ */
+import { useState } from 'react';
+import { __ } from '@wordpress/i18n';
+import moment from 'moment';
+
+interface DateRange {
+ /** The name of the date range preset. e.g. last_7_days */
+ preset_name: string;
+ /** The date range start datetime used to calculate transaction data, e.g. 2024-04-29T16:19:29 */
+ date_start: string;
+ /** The date range end datetime used to calculate transaction data, e.g. 2024-04-29T16:19:29 */
+ date_end: string;
+}
+
+/**
+ * Hook to manage the selected date range and date range presets for the payment activity widget.
+ */
+export const usePaymentActivityDateRangePresets = (): {
+ selectedDateRange: DateRange;
+ setSelectedDateRange: ( dateRange: DateRange ) => void;
+ dateRangePresets: {
+ [ key: string ]: {
+ start: moment.Moment;
+ end: moment.Moment;
+ displayKey: string;
+ };
+ };
+} => {
+ const now = moment();
+ const yesterdayEndOfDay = moment()
+ .clone()
+ .subtract( 1, 'd' )
+ .set( { hour: 23, minute: 59, second: 59, millisecond: 0 } );
+ const todayEndOfDay = moment()
+ .clone()
+ .set( { hour: 23, minute: 59, second: 59, millisecond: 0 } );
+
+ const dateRangePresets: {
+ [ key: string ]: {
+ start: moment.Moment;
+ end: moment.Moment;
+ displayKey: string;
+ };
+ } = {
+ today: {
+ start: now
+ .clone()
+ .set( { hour: 0, minute: 0, second: 0, millisecond: 0 } ),
+ end: todayEndOfDay,
+ displayKey: __( 'Today', 'woocommerce-payments' ),
+ },
+ last_7_days: {
+ start: now
+ .clone()
+ .subtract( 7, 'days' )
+ .set( { hour: 0, minute: 0, second: 0, millisecond: 0 } ),
+ end: yesterdayEndOfDay,
+ displayKey: __( 'Last 7 days', 'woocommerce-payments' ),
+ },
+ last_4_weeks: {
+ start: now
+ .clone()
+ .subtract( 4, 'weeks' )
+ .set( { hour: 0, minute: 0, second: 0, millisecond: 0 } ),
+ end: yesterdayEndOfDay,
+ displayKey: __( 'Last 4 weeks', 'woocommerce-payments' ),
+ },
+ last_3_months: {
+ start: now
+ .clone()
+ .subtract( 3, 'months' )
+ .set( { hour: 0, minute: 0, second: 0, millisecond: 0 } ),
+ end: yesterdayEndOfDay,
+ displayKey: __( 'Last 3 months', 'woocommerce-payments' ),
+ },
+ last_12_months: {
+ start: now
+ .clone()
+ .subtract( 12, 'months' )
+ .set( { hour: 0, minute: 0, second: 0, millisecond: 0 } ),
+ end: yesterdayEndOfDay,
+ displayKey: __( 'Last 12 months', 'woocommerce-payments' ),
+ },
+ month_to_date: {
+ start: now.clone().startOf( 'month' ),
+ end: todayEndOfDay,
+ displayKey: __( 'Month to date', 'woocommerce-payments' ),
+ },
+ quarter_to_date: {
+ start: now.clone().startOf( 'quarter' ),
+ end: todayEndOfDay,
+ displayKey: __( 'Quarter to date', 'woocommerce-payments' ),
+ },
+ year_to_date: {
+ start: now.clone().startOf( 'year' ),
+ end: todayEndOfDay,
+ displayKey: __( 'Year to date', 'woocommerce-payments' ),
+ },
+ all_time: {
+ start: moment(
+ wcpaySettings.accountStatus.created,
+ 'YYYY-MM-DD\\THH:mm:ss'
+ ),
+ end: todayEndOfDay,
+ displayKey: __( 'All time', 'woocommerce-payments' ),
+ },
+ };
+
+ const defaultDateRange = {
+ preset_name: 'last_7_days',
+ date_start: dateRangePresets.last_7_days.start.format(
+ 'YYYY-MM-DD\\THH:mm:ss'
+ ),
+ date_end: dateRangePresets.last_7_days.end.format(
+ 'YYYY-MM-DD\\THH:mm:ss'
+ ),
+ };
+
+ const [ selectedDateRange, setSelectedDateRange ] = useState( {
+ preset_name: defaultDateRange.preset_name,
+ date_start: defaultDateRange.date_start,
+ date_end: defaultDateRange.date_end,
+ } );
+
+ return {
+ selectedDateRange,
+ setSelectedDateRange,
+ dateRangePresets,
+ };
+};
diff --git a/client/components/payment-activity/index.tsx b/client/components/payment-activity/index.tsx
index 33d89c418a6..2e1f8fe2196 100644
--- a/client/components/payment-activity/index.tsx
+++ b/client/components/payment-activity/index.tsx
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import * as React from 'react';
+import React from 'react';
import { Card, CardBody, CardHeader } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import interpolateComponents from '@automattic/interpolate-components';
@@ -12,27 +12,16 @@ import moment from 'moment';
*/
import EmptyStateAsset from 'assets/images/payment-activity-empty-state.svg?asset';
+import InlineLabelSelect from '../inline-label-select';
import PaymentActivityDataComponent from './payment-activity-data';
import Survey from './survey';
-import { WcPayOverviewSurveyContextProvider } from './survey/context';
+import { recordEvent } from 'wcpay/tracks';
import { usePaymentActivityData } from 'wcpay/data';
+import { usePaymentActivityDateRangePresets } from './hooks';
import { useSelectedCurrency } from 'wcpay/overview/hooks';
-import type { DateRange } from './types';
+import { WcPayOverviewSurveyContextProvider } from './survey/context';
import './style.scss';
-/**
- * This will be replaces in the future with a dynamic date range picker.
- */
-const getDateRange = (): DateRange => {
- return {
- // Subtract 7 days from the current date.
- date_start: moment()
- .subtract( 7, 'd' )
- .format( 'YYYY-MM-DD\\THH:mm:ss' ),
- date_end: moment().format( 'YYYY-MM-DD\\THH:mm:ss' ),
- };
-};
-
const PaymentActivityEmptyState: React.FC = () => (
@@ -62,15 +51,40 @@ const PaymentActivityEmptyState: React.FC = () => (
);
+const formatDateRange = (
+ start: moment.Moment,
+ end: moment.Moment
+): string => {
+ // Today - show only today's date.
+ if ( start.isSame( end, 'day' ) ) {
+ return start.format( 'MMMM D, YYYY' );
+ }
+
+ // Different years - show year for both start and end
+ if ( ! start.isSame( end, 'year' ) ) {
+ return `${ start.format( 'MMMM D, YYYY' ) } - ${ end.format(
+ 'MMMM D, YYYY'
+ ) }`;
+ }
+
+ // Same year - show year only for end date.
+ return `${ start.format( 'MMMM D' ) } - ${ end.format( 'MMMM D, YYYY' ) }`;
+};
+
const PaymentActivity: React.FC = () => {
const isOverviewSurveySubmitted =
wcpaySettings.isOverviewSurveySubmitted ?? false;
const { selectedCurrency } = useSelectedCurrency();
-
+ const {
+ selectedDateRange,
+ setSelectedDateRange,
+ dateRangePresets,
+ } = usePaymentActivityDateRangePresets();
const { paymentActivityData, isLoading } = usePaymentActivityData( {
currency: selectedCurrency ?? wcpaySettings.accountDefaultCurrency,
- ...getDateRange(),
+ date_start: selectedDateRange.date_start,
+ date_end: selectedDateRange.date_end,
timezone: moment( new Date() ).format( 'Z' ),
} );
@@ -83,11 +97,53 @@ const PaymentActivity: React.FC = () => {
return <>>;
}
+ const options = Object.keys( dateRangePresets ).map( ( presetName ) => {
+ const preset = dateRangePresets[ presetName ];
+ return {
+ key: presetName,
+ name: preset.displayKey,
+ hint: formatDateRange( preset.start, preset.end ),
+ };
+ } );
+
return (
-
+
{ __( 'Your payment activity', 'woocommerce-payments' ) }
- { /* Filters go here */ }
+
+ option.key === selectedDateRange.preset_name
+ ) }
+ placeholder="Select an option..."
+ onChange={ ( changes ) => {
+ const selectedItem = changes.selectedItem;
+ if ( selectedItem ) {
+ const start = dateRangePresets[
+ selectedItem.key
+ ].start
+ .clone()
+ .format( 'YYYY-MM-DD\\THH:mm:ss' );
+ const end = dateRangePresets[ selectedItem.key ].end
+ .clone()
+ .format( 'YYYY-MM-DD\\THH:mm:ss' );
+ const { key: presetName } = selectedItem;
+ recordEvent(
+ 'wcpay_overview_payment_activity_period_change',
+ {
+ preset_name: presetName,
+ }
+ );
+ setSelectedDateRange( {
+ date_start: start,
+ date_end: end,
+ preset_name: presetName,
+ } );
+ }
+ } }
+ />
+): {
+ paymentActivityData: PaymentActivityData | undefined;
+ isLoading: boolean;
+} =>
useSelect(
( select ) => {
const { getPaymentActivityData, isResolving } = select(
@@ -25,5 +28,5 @@ export const usePaymentActivityData = (
isLoading: isResolving( 'getPaymentActivityData', [ query ] ),
};
},
- [ query.currency ]
+ [ query.currency, query.date_start, query.date_end ]
);
diff --git a/client/data/payment-activity/reducer.ts b/client/data/payment-activity/reducer.ts
index dfb53c7c95c..bf5d7c69aed 100644
--- a/client/data/payment-activity/reducer.ts
+++ b/client/data/payment-activity/reducer.ts
@@ -3,18 +3,33 @@
/**
* Internal dependencies
*/
+import { getResourceId } from 'wcpay/utils/data';
import TYPES from './action-types';
import { PaymentActivityAction, PaymentActivityState } from './types';
const receivePaymentActivity = (
state: PaymentActivityState = {},
- { type, data }: PaymentActivityAction
+ { type, query, data }: PaymentActivityAction
): PaymentActivityState => {
+ if ( ! query ) {
+ return state;
+ }
+
+ /*
+ Responses are stored in a key-value store where the key is a unique identifier for the query.
+ This is consistent with other query-based stores (i.e. transactions, disputes, etc.)
+ It allows us to temporarily cache responses to avoid re-fetching identical data.
+ For example, when a user is comparing two date ranges, we can store the responses for each date range
+ and switch between them without re-fetching.
+ This data is not persisted between browser sessions (e.g. on page refresh).
+ */
+ const index = getResourceId( query );
+
switch ( type ) {
case TYPES.SET_PAYMENT_ACTIVITY_DATA:
state = {
...state,
- paymentActivityData: data,
+ [ index ]: data,
};
break;
}
diff --git a/client/data/payment-activity/resolvers.ts b/client/data/payment-activity/resolvers.ts
index 64d9ade814a..d595b0ffe4b 100644
--- a/client/data/payment-activity/resolvers.ts
+++ b/client/data/payment-activity/resolvers.ts
@@ -31,7 +31,7 @@ export function* getPaymentActivityData(
try {
const results = yield apiFetch( { path } );
- yield updatePaymentActivity( results as PaymentActivityData );
+ yield updatePaymentActivity( results as PaymentActivityData, query );
} catch ( e ) {
yield controls.dispatch(
'core/notices',
diff --git a/client/data/payment-activity/selectors.ts b/client/data/payment-activity/selectors.ts
index 3432c80d9fc..0d5e7626873 100644
--- a/client/data/payment-activity/selectors.ts
+++ b/client/data/payment-activity/selectors.ts
@@ -3,11 +3,14 @@
/**
* Internal Dependencies
*/
-import { State } from 'wcpay/data/types';
-import { PaymentActivityData } from './types';
+import type { State } from 'wcpay/data/types';
+import type { PaymentActivityData, PaymentActivityQuery } from './types';
+import { getResourceId } from 'wcpay/utils/data';
export const getPaymentActivityData = (
- state: State
+ state: State,
+ query: PaymentActivityQuery
): PaymentActivityData | undefined => {
- return state?.paymentActivity?.paymentActivityData;
+ const index = getResourceId( query );
+ return state?.paymentActivity?.[ index ];
};
diff --git a/client/data/payment-activity/test/reducer.test.ts b/client/data/payment-activity/test/reducer.test.ts
index cbbbd1eb5b2..ae73a0ad717 100644
--- a/client/data/payment-activity/test/reducer.test.ts
+++ b/client/data/payment-activity/test/reducer.test.ts
@@ -3,7 +3,8 @@
*/
import receivePaymentActivity from '../reducer';
import types from '../action-types';
-import { PaymentActivityData } from '../types';
+import { PaymentActivityData, PaymentActivityAction } from '../types';
+import { getResourceId } from 'utils/data';
describe( 'receivePaymentActivity', () => {
const mockPaymentActivityData: PaymentActivityData = {
@@ -21,15 +22,23 @@ describe( 'receivePaymentActivity', () => {
test( 'should set payment activity data correctly', () => {
const initialState = {};
- const action = {
+ const query = {
+ currency: 'jpy',
+ date_start: '2024-01-01',
+ date_end: '2024-01-31',
+ timezone: 'UTC',
+ };
+ const action: PaymentActivityAction = {
type: types.SET_PAYMENT_ACTIVITY_DATA,
data: mockPaymentActivityData,
+ query,
};
const newState = receivePaymentActivity( initialState, action );
+ const stateIndex = getResourceId( query );
expect( newState ).toEqual( {
- paymentActivityData: action.data,
+ [ stateIndex ]: action.data,
} );
} );
} );
diff --git a/client/data/payment-activity/test/resolver.test.ts b/client/data/payment-activity/test/resolver.test.ts
index 3f8ee5c3878..2c744c71c8a 100644
--- a/client/data/payment-activity/test/resolver.test.ts
+++ b/client/data/payment-activity/test/resolver.test.ts
@@ -45,7 +45,7 @@ describe( 'getPaymentActivityData resolver', () => {
describe( 'on success', () => {
test( 'should update state with payment activity data', () => {
expect( generator.next( successfulResponse ).value ).toEqual(
- updatePaymentActivity( successfulResponse )
+ updatePaymentActivity( successfulResponse, query )
);
} );
} );
diff --git a/client/data/payment-activity/types.d.ts b/client/data/payment-activity/types.d.ts
index 23eaddb6183..11d5f736812 100644
--- a/client/data/payment-activity/types.d.ts
+++ b/client/data/payment-activity/types.d.ts
@@ -24,12 +24,12 @@ export interface PaymentActivityData {
}
export interface PaymentActivityState {
- paymentActivityData?: PaymentActivityData;
- isLoading?: boolean;
+ [ key: string ]: PaymentActivityData;
}
export interface PaymentActivityAction {
type: string;
+ query?: PaymentActivityQuery;
data: PaymentActivityData;
}
From 04b046546d34245a85fa2b29b1567b2cc25d72ac Mon Sep 17 00:00:00 2001
From: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com>
Date: Wed, 12 Jun 2024 16:20:52 +0300
Subject: [PATCH 41/52] Improve consistency of Manage button for different
WooPayments KYC states (#8900)
Co-authored-by: oaratovskyi
---
...ency-of-woopayments-manage-settings-button | 4 +++
includes/class-wc-payments-account.php | 25 ++++++++++++++++---
2 files changed, 26 insertions(+), 3 deletions(-)
create mode 100644 changelog/fix-8813-consistency-of-woopayments-manage-settings-button
diff --git a/changelog/fix-8813-consistency-of-woopayments-manage-settings-button b/changelog/fix-8813-consistency-of-woopayments-manage-settings-button
new file mode 100644
index 00000000000..887ffad262b
--- /dev/null
+++ b/changelog/fix-8813-consistency-of-woopayments-manage-settings-button
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Improve consistency of Manage button for different WooPayments KYC states
diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php
index 4b4a2fec01c..cc980eb9e45 100644
--- a/includes/class-wc-payments-account.php
+++ b/includes/class-wc-payments-account.php
@@ -1054,6 +1054,7 @@ public function maybe_handle_onboarding() {
}
if ( isset( $_GET['wcpay-connect'] ) && check_admin_referer( 'wcpay-connect' ) ) {
+ $wcpay_connect_param = sanitize_text_field( wp_unslash( $_GET['wcpay-connect'] ) );
$incentive = ! empty( $_GET['promo'] ) ? sanitize_text_field( wp_unslash( $_GET['promo'] ) ) : '';
$progressive = ! empty( $_GET['progressive'] ) && 'true' === $_GET['progressive'];
$create_builder_account = ! empty( $_GET['create_builder_account'] ) && 'true' === $_GET['create_builder_account'];
@@ -1095,7 +1096,27 @@ public function maybe_handle_onboarding() {
WC_Payments_Onboarding_Service::set_test_mode( false );
}
- $this->redirect_to_onboarding_flow_page( $connect_page_source );
+ if ( WC_Payments_Onboarding_Service::SOURCE_WCADMIN_SETTINGS_PAGE === $connect_page_source ) {
+ $this->redirect_to_onboarding_welcome_page();
+ } else {
+ $this->redirect_to_onboarding_flow_page( $connect_page_source );
+ }
+ } elseif ( WC_Payments_Onboarding_Service::SOURCE_WCADMIN_SETTINGS_PAGE === $connect_page_source && ! $this->is_details_submitted() ) {
+ try {
+ $this->init_stripe_onboarding(
+ $wcpay_connect_param,
+ [
+ 'promo' => $incentive,
+ 'progressive' => $progressive,
+ ]
+ );
+ } catch ( Exception $e ) {
+ Logger::error( 'Init Stripe onboarding flow failed. ' . $e );
+ $this->redirect_to_onboarding_welcome_page(
+ __( 'There was a problem redirecting you to the account connection page. Please try again.', 'woocommerce-payments' )
+ );
+ }
+ return;
} else {
// Accounts with Stripe account connected will be redirected to the overview page.
$this->redirect_to( static::get_overview_page_url() );
@@ -1160,8 +1181,6 @@ public function maybe_handle_onboarding() {
);
}
- $wcpay_connect_param = sanitize_text_field( wp_unslash( $_GET['wcpay-connect'] ) );
-
try {
$this->maybe_init_jetpack_connection(
$wcpay_connect_param,
From 4b59f3b1c0bb98ed4272bf355d5e4e7cb9335a45 Mon Sep 17 00:00:00 2001
From: Daniel Guerra <15204776+danielmx-dev@users.noreply.github.com>
Date: Wed, 12 Jun 2024 19:08:42 +0300
Subject: [PATCH 42/52] Dev: Improve test coverage of upe.js (#8945)
Co-authored-by: Daniel Guerra
---
changelog/dev-7387-add-tests-to-upe-js | 4 ++
client/checkout/classic/event-handlers.js | 4 +-
client/checkout/utils/test/upe.test.js | 58 ++++++++++++++++++++++-
client/checkout/utils/upe.js | 10 ++--
4 files changed, 69 insertions(+), 7 deletions(-)
create mode 100644 changelog/dev-7387-add-tests-to-upe-js
diff --git a/changelog/dev-7387-add-tests-to-upe-js b/changelog/dev-7387-add-tests-to-upe-js
new file mode 100644
index 00000000000..4cf93073d25
--- /dev/null
+++ b/changelog/dev-7387-add-tests-to-upe-js
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Improve test coverage of upe.js and rename isPaymentMethodRestrictedToLocation to hasPaymentMethodCountryRestrictions
diff --git a/client/checkout/classic/event-handlers.js b/client/checkout/classic/event-handlers.js
index 156b73e1942..c034aaccedd 100644
--- a/client/checkout/classic/event-handlers.js
+++ b/client/checkout/classic/event-handlers.js
@@ -9,7 +9,7 @@ import {
generateCheckoutEventNames,
getSelectedUPEGatewayPaymentMethod,
isLinkEnabled,
- isPaymentMethodRestrictedToLocation,
+ hasPaymentMethodCountryRestrictions,
isUsingSavedPaymentMethod,
togglePaymentMethodForCountry,
} from '../utils/upe';
@@ -228,7 +228,7 @@ jQuery( function ( $ ) {
}
function restrictPaymentMethodToLocation( upeElement ) {
- if ( isPaymentMethodRestrictedToLocation( upeElement ) ) {
+ if ( hasPaymentMethodCountryRestrictions( upeElement ) ) {
togglePaymentMethodForCountry( upeElement );
// this event only applies to the checkout form, but not "place order" or "add payment method" pages.
diff --git a/client/checkout/utils/test/upe.test.js b/client/checkout/utils/test/upe.test.js
index ec992e223bd..fb9f961d18f 100644
--- a/client/checkout/utils/test/upe.test.js
+++ b/client/checkout/utils/test/upe.test.js
@@ -8,6 +8,7 @@ import {
getStripeElementOptions,
blocksShowLinkButtonHandler,
getSelectedUPEGatewayPaymentMethod,
+ hasPaymentMethodCountryRestrictions,
isUsingSavedPaymentMethod,
dispatchChangeEventFor,
togglePaymentMethodForCountry,
@@ -126,6 +127,62 @@ describe( 'UPE checkout utils', () => {
} );
} );
+ describe( 'hasPaymentMethodCountryRestrictions', () => {
+ let container;
+
+ beforeAll( () => {
+ container = document.createElement( 'div' );
+ container.innerHTML = `
+
+ `;
+ document.body.appendChild( container );
+ } );
+
+ afterAll( () => {
+ document.body.removeChild( container );
+ container = null;
+ } );
+
+ beforeEach( () => {
+ jest.clearAllMocks();
+ getUPEConfig.mockImplementation( ( argument ) => {
+ if ( argument === 'paymentMethodsConfig' ) {
+ return {
+ card: { countries: [] },
+ bancontact: { countries: [ 'BE' ] },
+ };
+ }
+ } );
+ } );
+
+ it( 'should be true when the payment method is restricted to the location', () => {
+ const bancontactUpeElement = document.querySelector(
+ '.payment_method_woocommerce_payments_bancontact'
+ );
+
+ expect(
+ hasPaymentMethodCountryRestrictions( bancontactUpeElement )
+ ).toBe( true );
+ } );
+
+ it( 'should be false when the payment method is not restricted to the location', () => {
+ const cardUpeElement = document.querySelector(
+ '.payment_method_woocommerce_payments_card'
+ );
+
+ expect(
+ hasPaymentMethodCountryRestrictions( cardUpeElement )
+ ).toBe( false );
+ } );
+ } );
+
describe( 'togglePaymentMethodForCountry', () => {
let container;
@@ -171,7 +228,6 @@ describe( 'UPE checkout utils', () => {
} );
afterEach( () => {
- // document.getElementById('billing_country').value = '';
window.wcpayCustomerData = null;
} );
diff --git a/client/checkout/utils/upe.js b/client/checkout/utils/upe.js
index 039f849e36c..aa85be44436 100644
--- a/client/checkout/utils/upe.js
+++ b/client/checkout/utils/upe.js
@@ -292,19 +292,21 @@ export const blocksShowLinkButtonHandler = ( linkAutofill ) => {
};
/**
- * Hides payment method if it has set specific countries in the PHP class.
+ * Returns true if the payment method has configured with any country restrictions.
*
- * @param {Object} upeElement The selector of the DOM element of particular payment method to mount the UPE element to.
+ * @param {HTMLElement} upeElement The selector of the DOM element of particular payment method to mount the UPE element to.
* @return {boolean} Whether the payment method is restricted to selected billing country.
**/
-export const isPaymentMethodRestrictedToLocation = ( upeElement ) => {
+export const hasPaymentMethodCountryRestrictions = ( upeElement ) => {
const paymentMethodsConfig = getUPEConfig( 'paymentMethodsConfig' );
const paymentMethodType = upeElement.dataset.paymentMethodType;
return !! paymentMethodsConfig[ paymentMethodType ].countries.length;
};
/**
- * @param {Object} upeElement The selector of the DOM element of particular payment method to mount the UPE element to.
+ * Hides payment method if it has set specific countries in the PHP class.
+ *
+ * @param {HTMLElement} upeElement The selector of the DOM element of particular payment method to mount the UPE element to.
**/
export const togglePaymentMethodForCountry = ( upeElement ) => {
const paymentMethodsConfig = getUPEConfig( 'paymentMethodsConfig' );
From e8ba924f9b61eac42f814a1b94d584363ade4e12 Mon Sep 17 00:00:00 2001
From: Timur Karimov
Date: Thu, 13 Jun 2024 20:35:53 +0200
Subject: [PATCH 43/52] Fix unused parameter phpcs sniffs in checkout classes
(#8955)
---
changelog/fix-unused-param-phpcs-sniffs | 4 ++++
...lass-wc-payments-payment-request-button-handler.php | 4 ++--
...lass-wc-payments-express-checkout-button-helper.php | 10 ++++------
.../payment-methods/class-afterpay-payment-method.php | 4 +++-
includes/payment-methods/class-upe-payment-method.php | 4 ++++
5 files changed, 17 insertions(+), 9 deletions(-)
create mode 100644 changelog/fix-unused-param-phpcs-sniffs
diff --git a/changelog/fix-unused-param-phpcs-sniffs b/changelog/fix-unused-param-phpcs-sniffs
new file mode 100644
index 00000000000..e0562bb29ba
--- /dev/null
+++ b/changelog/fix-unused-param-phpcs-sniffs
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Fix unused parameter phpcs sniffs in checkout classes.
diff --git a/includes/class-wc-payments-payment-request-button-handler.php b/includes/class-wc-payments-payment-request-button-handler.php
index 6cbd3cf137c..894d650933d 100644
--- a/includes/class-wc-payments-payment-request-button-handler.php
+++ b/includes/class-wc-payments-payment-request-button-handler.php
@@ -1179,12 +1179,12 @@ public function get_shipping_options( $shipping_address, $itemized_display_items
$packages = WC()->shipping->get_packages();
if ( ! empty( $packages ) && WC()->customer->has_calculated_shipping() ) {
- foreach ( $packages as $package_key => $package ) {
+ foreach ( $packages as $package ) {
if ( empty( $package['rates'] ) ) {
throw new Exception( __( 'Unable to find shipping method for address.', 'woocommerce-payments' ) );
}
- foreach ( $package['rates'] as $key => $rate ) {
+ foreach ( $package['rates'] as $rate ) {
$data['shipping_options'][] = [
'id' => $rate->id,
'label' => $rate->label,
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 401dc47a389..08a2fad1014 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
@@ -69,15 +69,13 @@ public function build_display_items( $itemized_display_items = false ) {
}
$items = [];
- $subtotal = 0;
$discounts = 0;
$currency = get_woocommerce_currency();
// Default show only subtotal instead of itemization.
if ( ! apply_filters( 'wcpay_payment_request_hide_itemization', ! $itemized_display_items ) ) {
- foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
+ foreach ( WC()->cart->get_cart() as $cart_item ) {
$amount = $cart_item['line_subtotal'];
- $subtotal += $cart_item['line_subtotal'];
$quantity_label = 1 < $cart_item['quantity'] ? ' (x' . $cart_item['quantity'] . ')' : '';
$product_name = $cart_item['data']->get_name();
@@ -138,7 +136,7 @@ public function build_display_items( $itemized_display_items = false ) {
}
// Include fees and taxes as display items.
- foreach ( $cart_fees as $key => $fee ) {
+ foreach ( $cart_fees as $fee ) {
$items[] = [
'label' => $fee->name,
'amount' => WC_Payments_Utils::prepare_amount( $fee->amount, $currency ),
@@ -507,12 +505,12 @@ public function get_shipping_options( $shipping_address, $itemized_display_items
$packages = WC()->shipping->get_packages();
if ( ! empty( $packages ) && WC()->customer->has_calculated_shipping() ) {
- foreach ( $packages as $package_key => $package ) {
+ foreach ( $packages as $package ) {
if ( empty( $package['rates'] ) ) {
throw new Exception( __( 'Unable to find shipping method for address.', 'woocommerce-payments' ) );
}
- foreach ( $package['rates'] as $key => $rate ) {
+ foreach ( $package['rates'] as $rate ) {
$data['shipping_options'][] = [
'id' => $rate->id,
'displayName' => $rate->label,
diff --git a/includes/payment-methods/class-afterpay-payment-method.php b/includes/payment-methods/class-afterpay-payment-method.php
index 01690b64f1b..f4d1ef541aa 100644
--- a/includes/payment-methods/class-afterpay-payment-method.php
+++ b/includes/payment-methods/class-afterpay-payment-method.php
@@ -72,8 +72,10 @@ public function __construct( $token_service ) {
* Returns payment method title.
*
* @param string|null $account_country Country of merchants account.
- * @param array|false $payment_details Optional payment details from charge object.
+ * @param array|false $payment_details Payment details from charge object. Not used by this class.
* @return string|null
+ *
+ * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
*/
public function get_title( string $account_country = null, $payment_details = false ) {
if ( 'GB' === $account_country ) {
diff --git a/includes/payment-methods/class-upe-payment-method.php b/includes/payment-methods/class-upe-payment-method.php
index 51d091328fd..dabe8b1eeac 100644
--- a/includes/payment-methods/class-upe-payment-method.php
+++ b/includes/payment-methods/class-upe-payment-method.php
@@ -130,6 +130,8 @@ public function get_id() {
* @param array|false $payment_details Optional payment details from charge object.
*
* @return string
+ *
+ * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
*/
public function get_title( string $account_country = null, $payment_details = false ) {
return $this->title;
@@ -260,6 +262,8 @@ abstract public function get_testing_instructions();
*
* @param string|null $account_country Optional account country.
* @return string
+ *
+ * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
*/
public function get_icon( string $account_country = null ) {
return isset( $this->icon_url ) ? $this->icon_url : '';
From 6b5b9a65c9c2701d73c9b0c5f14f640276876cf3 Mon Sep 17 00:00:00 2001
From: Alefe Souza
Date: Thu, 13 Jun 2024 15:53:13 -0300
Subject: [PATCH 44/52] Fix Teams for WooCommerce Memberships on product WooPay
Express Checkout Button (#7239)
---
...hips-on-product-woopay-express-checkout-button | 4 ++++
...-wc-payments-express-checkout-ajax-handler.php | 15 ++++++++++++++-
2 files changed, 18 insertions(+), 1 deletion(-)
create mode 100644 changelog/fix-teams-for-wc-memberships-on-product-woopay-express-checkout-button
diff --git a/changelog/fix-teams-for-wc-memberships-on-product-woopay-express-checkout-button b/changelog/fix-teams-for-wc-memberships-on-product-woopay-express-checkout-button
new file mode 100644
index 00000000000..909793c0000
--- /dev/null
+++ b/changelog/fix-teams-for-wc-memberships-on-product-woopay-express-checkout-button
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix Teams for WooCommerce Memberships on product WooPay Express Checkout Button.
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 773f3c5d61b..8bf33263005 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
@@ -153,7 +153,20 @@ public function ajax_add_to_cart() {
}
if ( in_array( $product_type, [ 'simple', 'variation', 'subscription', 'subscription_variation', 'booking', 'bundle', 'mix-and-match' ], true ) ) {
- WC()->cart->add_to_cart( $product->get_id(), $quantity );
+ $allowed_item_data = [
+ // Teams for WooCommerce Memberships fields.
+ 'team_name',
+ 'team_owner_takes_seat',
+ ];
+ $item_data = [];
+
+ foreach ( $allowed_item_data as $item ) {
+ if ( isset( $_POST[ $item ] ) ) {
+ $item_data[ $item ] = wc_clean( wp_unslash( $_POST[ $item ] ) );
+ }
+ }
+
+ WC()->cart->add_to_cart( $product->get_id(), $quantity, 0, [], $item_data );
}
WC()->cart->calculate_totals();
From 26f3eabb5b15c19dda85fd5c30d8d96c7474d872 Mon Sep 17 00:00:00 2001
From: Valery Sukhomlinov <683297+dmvrtx@users.noreply.github.com>
Date: Fri, 14 Jun 2024 08:24:54 +0200
Subject: [PATCH 45/52] Bump WooCommerce Tested To version to 8.9.3 (#8952)
---
changelog/dev-bump-woocommerce-tested-up-to-8.9.3 | 4 ++++
woocommerce-payments.php | 2 +-
2 files changed, 5 insertions(+), 1 deletion(-)
create mode 100644 changelog/dev-bump-woocommerce-tested-up-to-8.9.3
diff --git a/changelog/dev-bump-woocommerce-tested-up-to-8.9.3 b/changelog/dev-bump-woocommerce-tested-up-to-8.9.3
new file mode 100644
index 00000000000..8a2eeb985ce
--- /dev/null
+++ b/changelog/dev-bump-woocommerce-tested-up-to-8.9.3
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Bump WooCommerce Tested To version to 8.9.3
diff --git a/woocommerce-payments.php b/woocommerce-payments.php
index 0280deefa22..cda1f3135cf 100644
--- a/woocommerce-payments.php
+++ b/woocommerce-payments.php
@@ -8,7 +8,7 @@
* Text Domain: woocommerce-payments
* Domain Path: /languages
* WC requires at least: 7.6
- * WC tested up to: 8.9.2
+ * WC tested up to: 8.9.3
* Requires at least: 6.0
* Requires PHP: 7.3
* Version: 7.7.0
From ae69970bf76a2462e7acad7c7f89f7e40752393f Mon Sep 17 00:00:00 2001
From: Nagesh Pai <4162931+nagpai@users.noreply.github.com>
Date: Fri, 14 Jun 2024 14:13:51 +0530
Subject: [PATCH 46/52] Reporting: Add test for date preset drowpdown in
PaymentActivity component (#8961)
Co-authored-by: Nagesh Pai
---
.../add-8943-test-for-date-selector-hook | 5 +
.../payment-activity/test/index.test.tsx | 122 +++++++++++++++++-
2 files changed, 126 insertions(+), 1 deletion(-)
create mode 100644 changelog/add-8943-test-for-date-selector-hook
diff --git a/changelog/add-8943-test-for-date-selector-hook b/changelog/add-8943-test-for-date-selector-hook
new file mode 100644
index 00000000000..eeff66837fe
--- /dev/null
+++ b/changelog/add-8943-test-for-date-selector-hook
@@ -0,0 +1,5 @@
+Significance: patch
+Type: dev
+Comment: Add tests for the date preset dropdown in the PaymentActivity component
+
+
diff --git a/client/components/payment-activity/test/index.test.tsx b/client/components/payment-activity/test/index.test.tsx
index 0955df2a32d..fe3d29376de 100644
--- a/client/components/payment-activity/test/index.test.tsx
+++ b/client/components/payment-activity/test/index.test.tsx
@@ -2,7 +2,8 @@
* External dependencies
*/
import React from 'react';
-import { render } from '@testing-library/react';
+import { render, fireEvent } from '@testing-library/react';
+import moment from 'moment';
/**
* Internal dependencies
@@ -64,6 +65,7 @@ declare const global: {
[ currencyCode: string ]: number;
};
};
+ created: string;
};
accountDefaultCurrency: string;
zeroDecimalCurrencies: string[];
@@ -87,6 +89,7 @@ describe( 'PaymentActivity component', () => {
usd: 500,
},
},
+ created: '2022-01-01T00:00:00Z',
},
accountDefaultCurrency: 'eur',
zeroDecimalCurrencies: [],
@@ -163,4 +166,121 @@ describe( 'PaymentActivity component', () => {
queryByText( 'Are these metrics helpful?' )
).not.toBeInTheDocument();
} );
+
+ describe( 'Date selector renders correct ranges', () => {
+ afterEach( () => {
+ Date.now = () => new Date().getTime();
+ } );
+
+ const mockDateNowTo = ( date: string ) => {
+ Date.now = jest.fn( () =>
+ moment.tz( new Date( date ).getTime(), 'UTC' ).valueOf()
+ );
+ };
+ const dataSet = [
+ {
+ // Ordinary case or Happy Path
+ dateNow: '2024-06-10T16:19:29',
+ expected: {
+ today: 'June 10, 2024',
+ last7Days: 'June 3 - June 9, 2024',
+ last4Weeks: 'May 13 - June 9, 2024',
+ last3Months: 'March 10 - June 9, 2024',
+ last12Months: 'June 10, 2023 - June 9, 2024',
+ monthToDate: 'June 1 - June 10, 2024',
+ quarterToDate: 'April 1 - June 10, 2024',
+ yearToDate: 'January 1 - June 10, 2024',
+ allTime: 'January 1, 2022 - June 10, 2024',
+ },
+ },
+ {
+ // Start of the year
+ dateNow: '2024-01-01T00:00:00',
+ expected: {
+ today: 'January 1, 2024',
+ last7Days: 'December 25 - December 31, 2023',
+ last4Weeks: 'December 4 - December 31, 2023',
+ last3Months: 'October 1 - December 31, 2023',
+ last12Months: 'January 1 - December 31, 2023',
+ monthToDate: 'January 1, 2024',
+ quarterToDate: 'January 1, 2024',
+ yearToDate: 'January 1, 2024',
+ allTime: 'January 1, 2022 - January 1, 2024',
+ },
+ },
+ {
+ // Leap year
+ dateNow: '2024-02-29T00:00:00',
+ expected: {
+ today: 'February 29, 2024',
+ last7Days: 'February 22 - February 28, 2024',
+ last4Weeks: 'February 1 - February 28, 2024',
+ last3Months: 'November 29, 2023 - February 28, 2024',
+ last12Months: 'February 28, 2023 - February 28, 2024',
+ monthToDate: 'February 1 - February 29, 2024',
+ quarterToDate: 'January 1 - February 29, 2024',
+ yearToDate: 'January 1 - February 29, 2024',
+ allTime: 'January 1, 2022 - February 29, 2024',
+ },
+ },
+ ];
+
+ it.each( dataSet )(
+ 'should render the correct date ranges',
+ ( { dateNow, expected } ) => {
+ mockDateNowTo( dateNow );
+
+ const { getByRole } = render( );
+
+ const dateSelectorButton = getByRole( 'button', {
+ name: 'Period',
+ } );
+ fireEvent.click( dateSelectorButton );
+
+ expect(
+ getByRole( 'option', { name: `Today ${ expected.today }` } )
+ ).toBeInTheDocument();
+ expect(
+ getByRole( 'option', {
+ name: `Last 7 days ${ expected.last7Days }`,
+ } )
+ ).toBeInTheDocument();
+ expect(
+ getByRole( 'option', {
+ name: `Last 4 weeks ${ expected.last4Weeks }`,
+ } )
+ ).toBeInTheDocument();
+ expect(
+ getByRole( 'option', {
+ name: `Last 3 months ${ expected.last3Months }`,
+ } )
+ ).toBeInTheDocument();
+ expect(
+ getByRole( 'option', {
+ name: `Last 12 months ${ expected.last12Months }`,
+ } )
+ ).toBeInTheDocument();
+ expect(
+ getByRole( 'option', {
+ name: `Month to date ${ expected.monthToDate }`,
+ } )
+ ).toBeInTheDocument();
+ expect(
+ getByRole( 'option', {
+ name: `Quarter to date ${ expected.quarterToDate }`,
+ } )
+ ).toBeInTheDocument();
+ expect(
+ getByRole( 'option', {
+ name: `Year to date ${ expected.yearToDate }`,
+ } )
+ ).toBeInTheDocument();
+ expect(
+ getByRole( 'option', {
+ name: `All time ${ expected.allTime }`,
+ } )
+ ).toBeInTheDocument();
+ }
+ );
+ } );
} );
From 13ea0cffabe45301b6f2850036784e389142559b Mon Sep 17 00:00:00 2001
From: Francesco
Date: Fri, 14 Jun 2024 17:21:59 +0200
Subject: [PATCH 47/52] feat: pay-for-order support w/ tokenized cart PRBs
(#8946)
---
.../feat-tokenized-cart-pay-for-order-flow | 4 +
client/tokenized-payment-request/index.js | 11 +-
client/tokenized-payment-request/order-api.js | 75 +++++++++
.../payment-request.js | 159 ++++++++++--------
.../test/order-api.js | 128 ++++++++++++++
.../test/payment-request.test.js | 1 +
.../transformers/wc-to-stripe.js | 8 +
...ayments-payment-request-button-handler.php | 3 +-
8 files changed, 313 insertions(+), 76 deletions(-)
create mode 100644 changelog/feat-tokenized-cart-pay-for-order-flow
create mode 100644 client/tokenized-payment-request/order-api.js
create mode 100644 client/tokenized-payment-request/test/order-api.js
diff --git a/changelog/feat-tokenized-cart-pay-for-order-flow b/changelog/feat-tokenized-cart-pay-for-order-flow
new file mode 100644
index 00000000000..6f408168dd0
--- /dev/null
+++ b/changelog/feat-tokenized-cart-pay-for-order-flow
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+feat: add pay-for-order support w/ tokenized cart PRBs
diff --git a/client/tokenized-payment-request/index.js b/client/tokenized-payment-request/index.js
index 19f78488477..003ba636d4f 100644
--- a/client/tokenized-payment-request/index.js
+++ b/client/tokenized-payment-request/index.js
@@ -7,8 +7,10 @@ import { doAction } from '@wordpress/hooks';
/**
* Internal dependencies
*/
+import { getUPEConfig } from 'wcpay/utils/checkout';
import WCPayAPI from '../checkout/api';
import PaymentRequestCartApi from './cart-api';
+import PaymentRequestOrderApi from './order-api';
import WooPaymentsPaymentRequest from './payment-request';
import paymentRequestButtonUi from './button-ui';
import { getPaymentRequestData } from './frontend-utils';
@@ -52,7 +54,14 @@ jQuery( ( $ ) => {
} );
}
);
- const paymentRequestCartApi = new PaymentRequestCartApi();
+ let paymentRequestCartApi = new PaymentRequestCartApi();
+ if ( getPaymentRequestData( 'button_context' ) === 'pay_for_order' ) {
+ paymentRequestCartApi = new PaymentRequestOrderApi( {
+ orderId: getUPEConfig( 'order_id' ),
+ key: getUPEConfig( 'key' ),
+ billingEmail: getUPEConfig( 'billing_email' ),
+ } );
+ }
const wooPaymentsPaymentRequest = new WooPaymentsPaymentRequest( {
wcpayApi: api,
diff --git a/client/tokenized-payment-request/order-api.js b/client/tokenized-payment-request/order-api.js
new file mode 100644
index 00000000000..5880af8b0ad
--- /dev/null
+++ b/client/tokenized-payment-request/order-api.js
@@ -0,0 +1,75 @@
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+import { addQueryArgs } from '@wordpress/url';
+import { getPaymentRequestData } from './frontend-utils';
+
+export default class PaymentRequestOrderApi {
+ // parameters used in every request, just in different ways.
+ orderId;
+ key;
+ billingEmail = '';
+
+ // needed to replay the cart data to the `placeOrder` endpoint when placing the order.
+ cachedCartData = {};
+
+ /**
+ * Creates an instance of class to query for order data.
+ *
+ * @param {string} orderId The order ID,
+ * @param {string} key The order key, used to verify the order ID.
+ * @param {string?} billingEmail The billing email address, used for guest orders.
+ */
+ constructor( { orderId, key, billingEmail = '' } ) {
+ this.orderId = orderId;
+ this.key = key;
+ this.billingEmail = billingEmail;
+ }
+
+ /**
+ * Creates an order from the cart object.
+ * See https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/StoreApi/docs/checkout-order.md
+ *
+ * @param {{
+ * billing_address: Object,
+ * shipping_address: Object,
+ * payment_method: string,
+ * payment_data: Array,
+ * }} paymentData Additional payment data to place the order.
+ * @return {Promise} Result of the order creation request.
+ */
+ async placeOrder( paymentData ) {
+ return await apiFetch( {
+ method: 'POST',
+ path: `/wc/store/v1/checkout/${ this.orderId }`,
+ headers: {
+ Nonce: getPaymentRequestData( 'nonce' ).tokenized_order_nonce,
+ },
+ data: {
+ ...paymentData,
+ key: this.key,
+ billing_email: this.billingEmail,
+ // preventing billing and shipping address from being overwritten in the request to the store - we don't want to update them
+ billing_address: this.cachedCartData.billing_address,
+ shipping_address: this.cachedCartData.shipping_address,
+ },
+ } );
+ }
+
+ /**
+ * Returns the customer's order object.
+ * See https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/StoreApi/docs/order.md
+ *
+ * @return {Promise} Cart response object.
+ */
+ async getCart() {
+ return ( this.cachedCartData = await apiFetch( {
+ method: 'GET',
+ path: addQueryArgs( `/wc/store/v1/order/${ this.orderId }`, {
+ key: this.key,
+ billing_email: this.billingEmail,
+ } ),
+ } ) );
+ }
+}
diff --git a/client/tokenized-payment-request/payment-request.js b/client/tokenized-payment-request/payment-request.js
index 4bf314b41b8..79e616ddaca 100644
--- a/client/tokenized-payment-request/payment-request.js
+++ b/client/tokenized-payment-request/payment-request.js
@@ -139,93 +139,104 @@ export default class WooPaymentsPaymentRequest {
} );
paymentRequestButtonUi.showButton( paymentRequestButton );
- this.attachPaymentRequestButtonEventListeners();
- removeAction(
- 'wcpay.payment-request.update-button-data',
- 'automattic/wcpay/payment-request'
- );
- addAction(
- 'wcpay.payment-request.update-button-data',
- 'automattic/wcpay/payment-request',
- async () => {
- const newCartData = await _self.getCartData();
- // checking if items needed shipping, before assigning new cart data.
- const didItemsNeedShipping =
- _self.initialProductData?.needs_shipping ||
- _self.cachedCartData?.needs_shipping;
-
- _self.cachedCartData = newCartData;
-
- /**
- * If the customer aborted the payment request, we need to re init the payment request button to ensure the shipping
- * options are re-fetched. If the customer didn't abort the payment request, and the product's shipping status is
- * consistent, we can simply update the payment request button with the new total and display items.
- */
- if (
- ! _self.isPaymentAborted &&
- didItemsNeedShipping === newCartData.needs_shipping
- ) {
- paymentRequest.update( {
- total: {
- label: getPaymentRequestData( 'total_label' ),
- amount: parseInt(
- newCartData.totals.total_price,
- 10
+ if ( getPaymentRequestData( 'button_context' ) === 'pay_for_order' ) {
+ paymentRequestButton.on( 'click', () => {
+ trackPaymentRequestButtonClick( 'pay_for_order' );
+ } );
+ } else {
+ this.attachPaymentRequestButtonEventListeners();
+ removeAction(
+ 'wcpay.payment-request.update-button-data',
+ 'automattic/wcpay/payment-request'
+ );
+ addAction(
+ 'wcpay.payment-request.update-button-data',
+ 'automattic/wcpay/payment-request',
+ async () => {
+ const newCartData = await _self.getCartData();
+ // checking if items needed shipping, before assigning new cart data.
+ const didItemsNeedShipping =
+ _self.initialProductData?.needs_shipping ||
+ _self.cachedCartData?.needs_shipping;
+
+ _self.cachedCartData = newCartData;
+
+ /**
+ * If the customer aborted the payment request, we need to re init the payment request button to ensure the shipping
+ * options are re-fetched. If the customer didn't abort the payment request, and the product's shipping status is
+ * consistent, we can simply update the payment request button with the new total and display items.
+ */
+ if (
+ ! _self.isPaymentAborted &&
+ didItemsNeedShipping === newCartData.needs_shipping
+ ) {
+ paymentRequest.update( {
+ total: {
+ label: getPaymentRequestData( 'total_label' ),
+ amount: parseInt(
+ newCartData.totals.total_price,
+ 10
+ ),
+ },
+ displayItems: transformCartDataForDisplayItems(
+ newCartData
),
- },
- displayItems: transformCartDataForDisplayItems(
- newCartData
- ),
- } );
- } else {
- _self.init().then( noop );
+ } );
+ } else {
+ _self.init().then( noop );
+ }
}
- }
- );
+ );
- const $addToCartButton = jQuery( '.single_add_to_cart_button' );
+ const $addToCartButton = jQuery( '.single_add_to_cart_button' );
- paymentRequestButton.on( 'click', ( event ) => {
- trackPaymentRequestButtonClick( 'product' );
+ paymentRequestButton.on( 'click', ( event ) => {
+ trackPaymentRequestButtonClick( 'product' );
- // If login is required for checkout, display redirect confirmation dialog.
- if ( getPaymentRequestData( 'login_confirmation' ) ) {
- event.preventDefault();
- displayLoginConfirmationDialog( buttonBranding );
- return;
- }
+ // If login is required for checkout, display redirect confirmation dialog.
+ if ( getPaymentRequestData( 'login_confirmation' ) ) {
+ event.preventDefault();
+ displayLoginConfirmationDialog( buttonBranding );
+ return;
+ }
- // First check if product can be added to cart.
- if ( $addToCartButton.is( '.disabled' ) ) {
- event.preventDefault(); // Prevent showing payment request modal.
- if ( $addToCartButton.is( '.wc-variation-is-unavailable' ) ) {
- window.alert(
- window.wc_add_to_cart_variation_params
- ?.i18n_unavailable_text ||
+ // First check if product can be added to cart.
+ if ( $addToCartButton.is( '.disabled' ) ) {
+ event.preventDefault(); // Prevent showing payment request modal.
+ 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(
__(
- 'Sorry, this product is unavailable. Please choose a different combination.',
+ 'Please select your product options before proceeding.',
'woocommerce-payments'
)
- );
- } else {
- window.alert(
- __(
- 'Please select your product options before proceeding.',
- 'woocommerce-payments'
- )
- );
+ );
+ }
+ return;
}
- return;
- }
- _self.paymentRequestCartApi.addProductToCart();
- } );
+ _self.paymentRequestCartApi.addProductToCart();
+ } );
+ }
paymentRequest.on( 'cancel', () => {
_self.isPaymentAborted = true;
- // clearing the cart to avoid issues with products with low or limited availability
- // being held hostage by customers cancelling the PRB.
- _self.paymentRequestCartApi.emptyCart();
+
+ if ( getPaymentRequestData( 'button_context' ) === 'product' ) {
+ // clearing the cart to avoid issues with products with low or limited availability
+ // being held hostage by customers cancelling the PRB.
+ _self.paymentRequestCartApi.emptyCart();
+ }
} );
paymentRequest.on( 'shippingaddresschange', async ( event ) => {
@@ -281,7 +292,7 @@ export default class WooPaymentsPaymentRequest {
} );
paymentRequest.on( 'paymentmethod', async ( event ) => {
- // TODO: this works for PDPs - need to handle checkout scenarios for pay-for-order, cart, checkout.
+ // TODO: this works for PDPs - need to handle checkout scenarios for cart, checkout.
try {
const response = await _self.paymentRequestCartApi.placeOrder( {
// adding extension data as a separate action,
diff --git a/client/tokenized-payment-request/test/order-api.js b/client/tokenized-payment-request/test/order-api.js
new file mode 100644
index 00000000000..2b15aae7f01
--- /dev/null
+++ b/client/tokenized-payment-request/test/order-api.js
@@ -0,0 +1,128 @@
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+
+/**
+ * Internal dependencies
+ */
+import PaymentRequestOrderApi from '../order-api';
+
+jest.mock( '@wordpress/api-fetch', () => jest.fn() );
+
+global.wcpayPaymentRequestParams = {};
+global.wcpayPaymentRequestParams.nonce = {};
+global.wcpayPaymentRequestParams.nonce.tokenized_order_nonce =
+ 'global_tokenized_order_nonce';
+
+describe( 'PaymentRequestOrderApi', () => {
+ afterEach( () => {
+ jest.resetAllMocks();
+ } );
+
+ it( 'gets order data with the provided arguments', async () => {
+ const api = new PaymentRequestOrderApi( {
+ orderId: '1',
+ key: 'key_123',
+ billingEmail: 'cheese@toast.com',
+ } );
+
+ await api.getCart();
+ expect( apiFetch ).toHaveBeenCalledWith(
+ expect.objectContaining( {
+ method: 'GET',
+ path: expect.stringMatching(
+ // I am using a regex to ensure the order of the parameters doesn't matter.
+ /(?=.*\/wc\/store\/v1\/order\/1)(?=.*billing_email=cheese%40toast.com)(?=.*key=key_123)/
+ ),
+ } )
+ );
+ } );
+
+ it( 'places an order', async () => {
+ const api = new PaymentRequestOrderApi( {
+ orderId: '1',
+ key: 'key_123',
+ billingEmail: 'cheese@toast.com',
+ } );
+
+ await api.placeOrder( {
+ billing_address: {
+ first_name: 'Fake',
+ },
+ shipping_address: {
+ first_name: 'Test',
+ },
+ anythingElse: 'passedThrough',
+ } );
+ expect( apiFetch ).toHaveBeenCalledWith(
+ expect.objectContaining( {
+ method: 'POST',
+ path: '/wc/store/v1/checkout/1',
+ headers: expect.objectContaining( {
+ Nonce: 'global_tokenized_order_nonce',
+ } ),
+ data: expect.objectContaining( {
+ key: 'key_123',
+ billing_email: 'cheese@toast.com',
+ billing_address: undefined,
+ shipping_address: undefined,
+ anythingElse: 'passedThrough',
+ } ),
+ } )
+ );
+ } );
+
+ it( 'places an order with the previous API request data', async () => {
+ const api = new PaymentRequestOrderApi( {
+ orderId: '1',
+ key: 'key_123',
+ billingEmail: 'cheese@toast.com',
+ } );
+
+ apiFetch.mockResolvedValueOnce( {
+ billing_address: {
+ first_name: 'Fake',
+ last_name: 'Test',
+ },
+ shipping_address: {
+ first_name: 'Test',
+ last_name: 'Fake',
+ },
+ } );
+ await api.getCart();
+
+ await api.placeOrder( {
+ billing_address: {
+ first_name: 'Fake',
+ },
+ shipping_address: {
+ first_name: 'Test',
+ },
+ anythingElse: 'passedThrough',
+ } );
+
+ expect( apiFetch ).toHaveBeenCalledWith(
+ expect.objectContaining( {
+ method: 'POST',
+ path: '/wc/store/v1/checkout/1',
+ headers: expect.objectContaining( {
+ Nonce: 'global_tokenized_order_nonce',
+ } ),
+ data: expect.objectContaining( {
+ key: 'key_123',
+ billing_email: 'cheese@toast.com',
+ billing_address: expect.objectContaining( {
+ first_name: 'Fake',
+ last_name: 'Test',
+ } ),
+ shipping_address: expect.objectContaining( {
+ first_name: 'Test',
+ last_name: 'Fake',
+ } ),
+ anythingElse: 'passedThrough',
+ } ),
+ } )
+ );
+ } );
+} );
diff --git a/client/tokenized-payment-request/test/payment-request.test.js b/client/tokenized-payment-request/test/payment-request.test.js
index dbad721f2be..a2f425f8dd0 100644
--- a/client/tokenized-payment-request/test/payment-request.test.js
+++ b/client/tokenized-payment-request/test/payment-request.test.js
@@ -133,6 +133,7 @@ describe( 'WooPaymentsPaymentRequest', () => {
expect( trackPaymentRequestButtonLoad ).toHaveBeenCalledWith( 'cart' );
doAction( 'wcpay.payment-request.update-button-data' );
+
await waitForAction( 'wcpay.payment-request.update-button-data' );
expect( paymentRequestAvailabilityCallback ).toHaveBeenCalledTimes( 1 );
diff --git a/client/tokenized-payment-request/transformers/wc-to-stripe.js b/client/tokenized-payment-request/transformers/wc-to-stripe.js
index a2e841b66b6..ae175bf9338 100644
--- a/client/tokenized-payment-request/transformers/wc-to-stripe.js
+++ b/client/tokenized-payment-request/transformers/wc-to-stripe.js
@@ -47,6 +47,14 @@ export const transformCartDataForDisplayItems = ( cartData ) => {
} );
}
+ const refundAmount = parseInt( cartData.totals.total_refund || '0', 10 );
+ if ( refundAmount ) {
+ displayItems.push( {
+ amount: -refundAmount,
+ label: __( 'Refund', 'woocommerce-payments' ),
+ } );
+ }
+
return displayItems;
};
diff --git a/includes/class-wc-payments-payment-request-button-handler.php b/includes/class-wc-payments-payment-request-button-handler.php
index 894d650933d..2a2f1a1827f 100644
--- a/includes/class-wc-payments-payment-request-button-handler.php
+++ b/includes/class-wc-payments-payment-request-button-handler.php
@@ -966,6 +966,7 @@ public function scripts() {
'platform_tracker' => wp_create_nonce( 'platform_tracks_nonce' ),
'pay_for_order' => wp_create_nonce( 'pay_for_order' ),
'tokenized_cart_nonce' => wp_create_nonce( 'woopayments_tokenized_cart_nonce' ),
+ 'tokenized_order_nonce' => wp_create_nonce( 'wc_store_api' ),
],
'checkout' => [
'currency_code' => strtolower( get_woocommerce_currency() ),
@@ -985,7 +986,7 @@ public function scripts() {
'is_checkout_page' => $this->express_checkout_helper->is_checkout(),
];
- if ( WC_Payments_Features::is_tokenized_cart_prb_enabled() && $this->express_checkout_helper->is_product() ) {
+ if ( WC_Payments_Features::is_tokenized_cart_prb_enabled() && ( $this->express_checkout_helper->is_product() || $this->express_checkout_helper->is_pay_for_order_page() ) ) {
WC_Payments::register_script_with_dependencies(
'WCPAY_PAYMENT_REQUEST',
'dist/tokenized-payment-request',
From 0948bf90a1f137ee14e9f4517cb53e75abcf6842 Mon Sep 17 00:00:00 2001
From: Hsing-yu Flowers
Date: Fri, 14 Jun 2024 15:44:41 -0400
Subject: [PATCH 48/52] Use FILTER_SANITIZE_EMAIL filter to sanitize email
input (#8957)
---
.../update-use-filter-sanitize-email-to-sanitize-email-input | 4 ++++
...ss-wc-payments-express-checkout-button-display-handler.php | 4 +---
2 files changed, 5 insertions(+), 3 deletions(-)
create mode 100644 changelog/update-use-filter-sanitize-email-to-sanitize-email-input
diff --git a/changelog/update-use-filter-sanitize-email-to-sanitize-email-input b/changelog/update-use-filter-sanitize-email-to-sanitize-email-input
new file mode 100644
index 00000000000..0e1c685b676
--- /dev/null
+++ b/changelog/update-use-filter-sanitize-email-to-sanitize-email-input
@@ -0,0 +1,4 @@
+Significance: patch
+Type: update
+
+Use FILTER_SANITIZE_EMAIL to sanitize email input
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 ab132195f81..66c203b818f 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
@@ -202,9 +202,7 @@ function ( $js_config ) use ( $order ) {
$session_email = is_array( $customer ) && isset( $customer['email'] ) ? $customer['email'] : '';
}
- // Silence the filter_input warning because we are sanitizing the input with sanitize_email().
- // nosemgrep: audit.php.lang.misc.filter-input-no-filter.
- $user_email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( filter_input( INPUT_POST, 'email' ) ) ) : $session_email;
+ $user_email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( filter_input( INPUT_POST, 'email', FILTER_SANITIZE_EMAIL ) ) ) : $session_email;
$js_config['order_id'] = $order->get_id();
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
From 25db37e303f2a47664e88b2526457be567dd14d0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?C=C3=A9sar=20Costa?=
<10233985+cesarcosta99@users.noreply.github.com>
Date: Fri, 14 Jun 2024 19:41:54 -0300
Subject: [PATCH 49/52] Add support for adapted extensions in Direct Checkout
(#8849)
---
.../add-2648-adapted-extensions-compatibility | 4 +++
.../checkout/woopay/connect/user-connect.js | 16 +++++++++
.../woopay/connect/woopay-connect-iframe.js | 4 ++-
.../direct-checkout/woopay-direct-checkout.js | 13 ++++++-
...lass-wc-rest-woopay-session-controller.php | 7 ++++
includes/class-wc-payments-features.php | 2 +-
includes/class-wc-payments.php | 1 +
includes/woopay/class-woopay-session.php | 35 ++++++++++++++++--
includes/woopay/class-woopay-utilities.php | 36 +++++++++++++++++++
.../unit/test-class-wc-payments-features.php | 7 ++--
10 files changed, 115 insertions(+), 10 deletions(-)
create mode 100644 changelog/add-2648-adapted-extensions-compatibility
diff --git a/changelog/add-2648-adapted-extensions-compatibility b/changelog/add-2648-adapted-extensions-compatibility
new file mode 100644
index 00000000000..6f4dfabe3d4
--- /dev/null
+++ b/changelog/add-2648-adapted-extensions-compatibility
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Enable adapted extensions compatibility with Direct Checkout.
diff --git a/client/checkout/woopay/connect/user-connect.js b/client/checkout/woopay/connect/user-connect.js
index 9e71772f086..ebae7e7ecf2 100644
--- a/client/checkout/woopay/connect/user-connect.js
+++ b/client/checkout/woopay/connect/user-connect.js
@@ -11,6 +11,7 @@ class WooPayUserConnect extends WoopayConnect {
this.listeners = {
...this.listeners,
getIsUserLoggedInCallback: () => {},
+ getEncryptedDataCallback: () => {},
};
}
@@ -26,6 +27,18 @@ class WooPayUserConnect extends WoopayConnect {
);
}
+ /**
+ * Retrieves encrypted data from WooPay.
+ *
+ * @return {Promise