From 88302adfc983bcc9452c71f04abcaf394325b3da Mon Sep 17 00:00:00 2001 From: Nagesh Pai <4162931+nagpai@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:46:09 +0530 Subject: [PATCH 01/31] Fix transactions search view when it has too many search terms (#8996) Co-authored-by: Nagesh Pai Co-authored-by: Jessy Pappachan <32092402+jessy-p@users.noreply.github.com> Co-authored-by: Francesco Co-authored-by: Timur Karimov Co-authored-by: Eric Jinks <3147296+Jinksi@users.noreply.github.com> --- changelog/fix-8781-transaction-search-fields | 4 ++ client/transactions/list/style.scss | 60 ++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 changelog/fix-8781-transaction-search-fields diff --git a/changelog/fix-8781-transaction-search-fields b/changelog/fix-8781-transaction-search-fields new file mode 100644 index 00000000000..ef142316156 --- /dev/null +++ b/changelog/fix-8781-transaction-search-fields @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Make the search box, and typed search term visible clearly on the 'Payments > Transactions' page, when there are too many existing search tags. diff --git a/client/transactions/list/style.scss b/client/transactions/list/style.scss index 90288f81f17..3788096bc00 100644 --- a/client/transactions/list/style.scss +++ b/client/transactions/list/style.scss @@ -134,4 +134,64 @@ $gap-small: 12px; height: auto; } } + + .components-card__header { + // These styles improve the overflow behaviour of the Search component within the TableCard, when many tags are selected. Used for transaction list views. See PR #8996 + .woocommerce-search.woocommerce-select-control + .woocommerce-select-control__listbox { + position: relative; + top: 5px; + } + .woocommerce-table__actions { + justify-content: space-between; + + & > div { + width: 85%; + margin-right: 0; + } + + button.woocommerce-table__download-button { + @include breakpoint( '<1040px' ) { + .woocommerce-table__download-button__label { + display: none; + } + } + } + + .woocommerce-select-control.is-focused + .woocommerce-select-control__control { + flex-wrap: wrap; + + .woocommerce-select-control__tags { + white-space: wrap; + } + } + .woocommerce-select-control__tags { + overflow-x: auto; + white-space: nowrap; + scrollbar-width: none; + margin-right: 25px; + + &::after { + content: ''; + position: absolute; + top: 0; + right: 0; + width: 25px; + height: 100%; + background: linear-gradient( + to right, + rgba( 255, 255, 255, 0 ), + rgba( 255, 255, 255, 1 ) 90% + ); + } + } + + @include breakpoint( '<960px' ) { + .woocommerce-search { + margin: 0; + } + } + } + } } From 1c74c0ceff17f209e5a714ba7f681dd5bfce5712 Mon Sep 17 00:00:00 2001 From: Eric Jinks <3147296+Jinksi@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:23:31 +1000 Subject: [PATCH 02/31] Fix transaction/document list advanced filter styling issue preventing dates to be input on mobile screens (#9013) --- ...-8782-transaction-list-data-filters-mobile | 4 + client/deposits/filters/index.js | 16 +- .../list/test/__snapshots__/index.tsx.snap | 208 +++++++++--------- client/disputes/filters/index.tsx | 16 +- .../test/__snapshots__/index.tsx.snap | 58 ++--- client/documents/filters/index.tsx | 1 - client/documents/filters/style.scss | 13 -- client/style.scss | 24 ++ client/transactions/filters/index.tsx | 1 - client/transactions/filters/style.scss | 13 -- 10 files changed, 185 insertions(+), 169 deletions(-) create mode 100644 changelog/fix-8782-transaction-list-data-filters-mobile delete mode 100644 client/documents/filters/style.scss delete mode 100644 client/transactions/filters/style.scss diff --git a/changelog/fix-8782-transaction-list-data-filters-mobile b/changelog/fix-8782-transaction-list-data-filters-mobile new file mode 100644 index 00000000000..f21f288da32 --- /dev/null +++ b/changelog/fix-8782-transaction-list-data-filters-mobile @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix transaction list and document list advanced filter styling issue preventing dates to be input on mobile screens. diff --git a/client/deposits/filters/index.js b/client/deposits/filters/index.js index 75b9081b489..34cc4bbdf52 100644 --- a/client/deposits/filters/index.js +++ b/client/deposits/filters/index.js @@ -33,13 +33,15 @@ export const DepositsFilters = ( props ) => { }; return ( - +
+ +
); }; diff --git a/client/deposits/list/test/__snapshots__/index.tsx.snap b/client/deposits/list/test/__snapshots__/index.tsx.snap index 46219760d21..e905e893318 100644 --- a/client/deposits/list/test/__snapshots__/index.tsx.snap +++ b/client/deposits/list/test/__snapshots__/index.tsx.snap @@ -5,71 +5,75 @@ exports[`Deposits list renders correctly a single deposit 1`] = `
-

- Filters -

+

+ Filters +

- - Deposit currency - : -
- +
+ + All + +
+ +
-
-
- - Show - : -
- +
+ + All deposits + +
+ +
@@ -551,71 +555,75 @@ exports[`Deposits list renders correctly with multiple currencies 1`] = `
-

- Filters -

+

+ Filters +

- - Deposit currency - : -
- +
+ + All + +
+ +
-
-
- - Show - : -
- +
+ + All deposits + +
+ +
diff --git a/client/disputes/filters/index.tsx b/client/disputes/filters/index.tsx index 2b89197d48f..6e8288f2611 100644 --- a/client/disputes/filters/index.tsx +++ b/client/disputes/filters/index.tsx @@ -42,13 +42,15 @@ export const DisputesFilters = ( { }; return ( - +
+ +
); }; diff --git a/client/disputes/test/__snapshots__/index.tsx.snap b/client/disputes/test/__snapshots__/index.tsx.snap index 1f41882728e..2bfe0e98c1e 100644 --- a/client/disputes/test/__snapshots__/index.tsx.snap +++ b/client/disputes/test/__snapshots__/index.tsx.snap @@ -5,43 +5,47 @@ exports[`Disputes list renders correctly 1`] = `
-

- Filters -

+

+ Filters +

- - Show - : -
- +
+ + All disputes + +
+ +
diff --git a/client/documents/filters/index.tsx b/client/documents/filters/index.tsx index 81b11bcde9d..7ceda5758ea 100644 --- a/client/documents/filters/index.tsx +++ b/client/documents/filters/index.tsx @@ -9,7 +9,6 @@ import { getQuery } from '@woocommerce/navigation'; * Internal dependencies */ import { filters, advancedFilters } from './config'; -import './style.scss'; export const DocumentsFilters = (): JSX.Element => { return ( diff --git a/client/documents/filters/style.scss b/client/documents/filters/style.scss deleted file mode 100644 index a5915afcadf..00000000000 --- a/client/documents/filters/style.scss +++ /dev/null @@ -1,13 +0,0 @@ -.woocommerce-filters-documents { - .woocommerce-filters-advanced { - &__fieldset { - display: grid; - grid-template-columns: 100px auto 1fr; - } - } - .components-select-control - .components-input-control__container - .components-select-control__input { - padding-right: var( --main-gap ); - } -} diff --git a/client/style.scss b/client/style.scss index 2dcdbb2a7dd..7fe3269109f 100644 --- a/client/style.scss +++ b/client/style.scss @@ -147,3 +147,27 @@ img { } } } + +/** + * This styling fixes the appearance of advanced filters on list screens. + * In particular, it adds a gap between advanced date filter inputs on viewports >= 783px. + * See https://github.com/woocommerce/woocommerce/blob/892afbe1b9ee7bd1d20e7c34ad6180d91b8b94d9/packages/js/components/src/advanced-filters/style.scss#L119 + */ +.woocommerce-filters-deposits, +.woocommerce-filters-disputes, +.woocommerce-filters-documents, +.woocommerce-filters-transactions { + @media screen and ( min-width: 783px ) { + .woocommerce-filters-advanced { + &__fieldset { + display: grid; + grid-template-columns: 100px auto 1fr; + } + } + .components-select-control + .components-input-control__container + .components-select-control__input { + padding-right: var( --main-gap ); + } + } +} diff --git a/client/transactions/filters/index.tsx b/client/transactions/filters/index.tsx index dd5f5712ba6..cbb729ce5d9 100644 --- a/client/transactions/filters/index.tsx +++ b/client/transactions/filters/index.tsx @@ -10,7 +10,6 @@ import { getQuery } from '@woocommerce/navigation'; */ import { getFilters, getAdvancedFilters } from './config'; import { formatCurrencyName } from '../../utils/currency'; -import './style.scss'; import { recordEvent } from 'tracks'; interface TransactionsFiltersProps { diff --git a/client/transactions/filters/style.scss b/client/transactions/filters/style.scss deleted file mode 100644 index e29aa8cbf4a..00000000000 --- a/client/transactions/filters/style.scss +++ /dev/null @@ -1,13 +0,0 @@ -.woocommerce-filters-transactions { - .woocommerce-filters-advanced { - &__fieldset { - display: grid; - grid-template-columns: 100px auto 1fr; - } - } - .components-select-control - .components-input-control__container - .components-select-control__input { - padding-right: var( --main-gap ); - } -} From 471bcbacc29ec01359e1fc132896726395ebd643 Mon Sep 17 00:00:00 2001 From: Vlad Olaru Date: Wed, 26 Jun 2024 11:16:34 +0300 Subject: [PATCH 03/31] Change plugin WP.org author to WooCommerce (#8388) --- changelog/update-8385-plugin-wporg-author | 5 +++++ woocommerce-payments.php | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelog/update-8385-plugin-wporg-author diff --git a/changelog/update-8385-plugin-wporg-author b/changelog/update-8385-plugin-wporg-author new file mode 100644 index 00000000000..b3847e87826 --- /dev/null +++ b/changelog/update-8385-plugin-wporg-author @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: Just changed the WP.org display author to WooCommerce. + + diff --git a/woocommerce-payments.php b/woocommerce-payments.php index ac10ffab61c..51e29ba399c 100644 --- a/woocommerce-payments.php +++ b/woocommerce-payments.php @@ -3,7 +3,7 @@ * Plugin Name: WooPayments * Plugin URI: https://woocommerce.com/payments/ * Description: Accept payments via credit card. Manage transactions within WordPress. - * Author: Automattic + * Author: WooCommerce * Author URI: https://woocommerce.com/ * Text Domain: woocommerce-payments * Domain Path: /languages From 56b35cf83c77ceef8c532ae6edbd57ff355b0123 Mon Sep 17 00:00:00 2001 From: Vlad Olaru Date: Wed, 26 Jun 2024 12:02:26 +0300 Subject: [PATCH 04/31] Minor updates to Tracks onboarding events props (#9015) --- changelog/update-tracks-onboarding-events-props | 5 +++++ client/connect-account-page/index.tsx | 1 + includes/class-wc-payments-account.php | 4 ++++ includes/class-wc-payments-onboarding-service.php | 9 +++++++-- tests/unit/test-class-wc-payments-account.php | 4 ++-- 5 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 changelog/update-tracks-onboarding-events-props diff --git a/changelog/update-tracks-onboarding-events-props b/changelog/update-tracks-onboarding-events-props new file mode 100644 index 00000000000..a1b8d03dbba --- /dev/null +++ b/changelog/update-tracks-onboarding-events-props @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: These changes are only related to Tracks events props updates. + + diff --git a/client/connect-account-page/index.tsx b/client/connect-account-page/index.tsx index 7cb4fdf9a15..a273565059c 100644 --- a/client/connect-account-page/index.tsx +++ b/client/connect-account-page/index.tsx @@ -105,6 +105,7 @@ const ConnectAccountPage: React.FC = () => { incentive_id: incentive.id, } ), sandbox_mode: sandboxMode, + path: 'payments_connect_v2', } ); }; diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index cc980eb9e45..807f64750e8 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -830,6 +830,7 @@ public function maybe_redirect_to_wcpay_connect(): bool { [ 'wcpay-connect' => '1', '_wpnonce' => wp_create_nonce( 'wcpay-connect' ), + 'from' => 'WCPAY_KYC_REMINDER', ], admin_url( 'admin.php' ) ); @@ -871,6 +872,7 @@ public function maybe_redirect_settings_to_connect_or_overview(): bool { [ 'page' => 'wc-admin', 'path' => '/payments/connect', + 'from' => 'WCADMIN_PAYMENT_SETTINGS', ], 'admin.php' ) @@ -890,6 +892,7 @@ public function maybe_redirect_settings_to_connect_or_overview(): bool { [ 'page' => 'wc-admin', 'path' => '/payments/overview', + 'from' => 'WCADMIN_PAYMENT_SETTINGS', ], 'admin.php' ) @@ -942,6 +945,7 @@ public function maybe_redirect_onboarding_flow_to_overview(): bool { [ 'page' => 'wc-admin', 'path' => '/payments/overview', + 'from' => 'WCPAY_ONBOARDING_FLOW', ], 'admin.php' ) diff --git a/includes/class-wc-payments-onboarding-service.php b/includes/class-wc-payments-onboarding-service.php index 7a5440f57a2..e6467fd670f 100644 --- a/includes/class-wc-payments-onboarding-service.php +++ b/includes/class-wc-payments-onboarding-service.php @@ -245,8 +245,12 @@ public static function is_test_mode_enabled(): bool { * @return string The source or empty string if the source is unsupported. */ public static function get_source( string $referer, array $get_params ): string { - $wcpay_connect_param = sanitize_text_field( wp_unslash( $get_params['wcpay-connect'] ) ); - if ( 'WCADMIN_PAYMENT_TASK' === $wcpay_connect_param ) { + $wcpay_connect_param = isset( $get_params['wcpay-connect'] ) ? sanitize_text_field( wp_unslash( $get_params['wcpay-connect'] ) ) : ''; + $from_param = isset( $get_params['from'] ) ? sanitize_text_field( wp_unslash( $get_params['from'] ) ) : ''; + + // Sometimes we have it in the `wcpay-connect` param and other times in the `from` one. + if ( 'WCADMIN_PAYMENT_TASK' === $wcpay_connect_param + || 'WCADMIN_PAYMENT_TASK' === $from_param ) { return self::SOURCE_WCADMIN_PAYMENT_TASK; } // Payments tab in Woo Admin Settings page. @@ -266,6 +270,7 @@ public static function get_source( string $referer, array $get_params ): string if ( isset( $get_params['wcpay-reset-account'] ) ) { return self::SOURCE_WCPAY_RESET_ACCOUNT; } + return ''; } } diff --git a/tests/unit/test-class-wc-payments-account.php b/tests/unit/test-class-wc-payments-account.php index 016fc286fcb..a3725e2c412 100644 --- a/tests/unit/test-class-wc-payments-account.php +++ b/tests/unit/test-class-wc-payments-account.php @@ -586,7 +586,7 @@ public function data_maybe_redirect_settings_to_connect_or_overview() { 'section' => 'woocommerce_payments', ], true, - 'connect', + 'connect&from=WCADMIN_PAYMENT_SETTINGS', ], 'account_partially_onboarded' => [ 1, @@ -597,7 +597,7 @@ public function data_maybe_redirect_settings_to_connect_or_overview() { 'section' => 'woocommerce_payments', ], false, - 'overview', + 'overview&from=WCADMIN_PAYMENT_SETTINGS', ], 'account_fully_onboarded' => [ 0, From c17029a4bec068c13374d978d2e5ad107202ffb7 Mon Sep 17 00:00:00 2001 From: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com> Date: Thu, 27 Jun 2024 12:00:29 +0300 Subject: [PATCH 05/31] Refactor redirects logic in payments (#8590) Co-authored-by: oaratovskyi --- ...-7654-refactor-redirects-logic-in-payments | 4 + includes/admin/class-wc-payments-admin.php | 55 +-- includes/class-wc-payments-account.php | 442 +++++------------- .../class-wc-payments-redirect-service.php | 189 ++++++++ includes/class-wc-payments.php | 11 +- .../admin/test-class-wc-payments-admin.php | 113 +---- ...test-class-wc-payments-account-capital.php | 84 +--- .../test-class-wc-payments-account-link.php | 65 +-- tests/unit/test-class-wc-payments-account.php | 206 ++++---- ...est-class-wc-payments-redirect-service.php | 174 +++++++ 10 files changed, 633 insertions(+), 710 deletions(-) create mode 100644 changelog/dev-7654-refactor-redirects-logic-in-payments create mode 100644 includes/class-wc-payments-redirect-service.php create mode 100644 tests/unit/test-class-wc-payments-redirect-service.php diff --git a/changelog/dev-7654-refactor-redirects-logic-in-payments b/changelog/dev-7654-refactor-redirects-logic-in-payments new file mode 100644 index 00000000000..988c5ba661e --- /dev/null +++ b/changelog/dev-7654-refactor-redirects-logic-in-payments @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Refactor redirects logic in payments diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index 27604515859..38dbe0d0db5 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -6,7 +6,6 @@ */ use Automattic\Jetpack\Identity_Crisis as Jetpack_Identity_Crisis; -use Automattic\WooCommerce\Admin\PageController; use WCPay\Core\Server\Request; use WCPay\Database_Cache; use WCPay\Logger; @@ -203,8 +202,7 @@ public function init_hooks() { // Add menu items. add_action( 'admin_menu', [ $this, 'add_payments_menu' ], 0 ); - add_action( 'admin_init', [ $this, 'maybe_redirect_to_onboarding' ], 11 ); // Run this after the WC setup wizard and onboarding redirection logic. - add_action( 'admin_enqueue_scripts', [ $this, 'maybe_redirect_overview_to_connect' ], 1 ); // Run this late (after `admin_init`) but before any scripts are actually enqueued. + add_action( 'admin_init', [ $this, 'maybe_redirect_from_payments_admin_child_pages' ], 11 ); // Run this after the WC setup wizard and onboarding redirection logic. add_action( 'admin_enqueue_scripts', [ $this, 'register_payments_scripts' ], 9 ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_payments_scripts' ], 9 ); add_action( 'woocommerce_admin_field_payment_gateways', [ $this, 'payment_gateways_container' ] ); @@ -1086,10 +1084,10 @@ private function get_settings_menu_item_name() { } /** - * Checks if Stripe account is connected and redirects to the onboarding page - * if it is not and the user is attempting to view a WCPay admin page. + * If the user is attempting to view a WCPay admin page without a connected Stripe account, + * redirect them to the connect account page. */ - public function maybe_redirect_to_onboarding() { + public function maybe_redirect_from_payments_admin_child_pages() { if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } @@ -1126,51 +1124,6 @@ public function maybe_redirect_to_onboarding() { $this->account->redirect_to_onboarding_welcome_page(); } - /** - * Avoid WC Admin /payments/overview error page when the current account associated Stripe account is not valid. - * - * The errored page happens because we don't register a /payments/overview WC admin page when the Stripe account - * is not valid and register only a /payments/connect top level menu page. - * - * Places around our plugin redirect merchants to the overview page by default (or using it for the Stripe KYC - * return URL) leading to poor UX. - * This is a safety net to prevent that from happening. - * - * @see self::add_payments_menu() - */ - public function maybe_redirect_overview_to_connect() { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return; - } - if ( wp_doing_ajax() ) { - return; - } - - // If the current page is registered, let it pass. - if ( wc_admin_is_registered_page() ) { - return; - } - - $url_params = wp_unslash( $_GET ); // phpcs:ignore WordPress.Security.NonceVerification - if ( empty( $url_params['page'] ) || 'wc-admin' !== $url_params['page'] - || empty( $url_params['path'] ) || '/payments/overview' !== $url_params['path'] ) { - return; - } - - /** - * Determine the path of the top level menu page since that can change between payments/connect and payments/overview. - * - * @see self::add_payments_menu() - */ - $top_level_page_path = PageController::get_instance()->get_path_from_id( 'wc-payments' ); - // If the top level page path is not the payments/connect one, bail. - if ( 'wc-admin&path=/payments/connect' !== $top_level_page_path ) { - return; - } - - $this->account->redirect_to_onboarding_welcome_page(); - } - /** * Add woopay as a payment method to the edit order on admin. * diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 807f64750e8..e8369a00d15 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -14,8 +14,6 @@ use WCPay\Constants\Country_Code; use WCPay\Constants\Currency_Code; use WCPay\Core\Server\Request\Get_Account; -use WCPay\Core\Server\Request\Get_Account_Capital_Link; -use WCPay\Core\Server\Request\Get_Account_Login_Data; use WCPay\Core\Server\Request; use WCPay\Core\Server\Request\Update_Account; use WCPay\Exceptions\API_Exception; @@ -68,6 +66,13 @@ class WC_Payments_Account { */ private $session_service; + /** + * WC_Payments_Redirect_Service instance for handling redirects business logic + * + * @var WC_Payments_Redirect_Service + */ + private $redirect_service; + /** * Class constructor * @@ -75,17 +80,20 @@ class WC_Payments_Account { * @param Database_Cache $database_cache Database cache util. * @param WC_Payments_Action_Scheduler_Service $action_scheduler_service Action scheduler service. * @param WC_Payments_Session_Service $session_service Session service. + * @param WC_Payments_Redirect_Service $redirect_service Redirect service. */ public function __construct( WC_Payments_API_Client $payments_api_client, Database_Cache $database_cache, WC_Payments_Action_Scheduler_Service $action_scheduler_service, - WC_Payments_Session_Service $session_service + WC_Payments_Session_Service $session_service, + WC_Payments_Redirect_Service $redirect_service ) { $this->payments_api_client = $payments_api_client; $this->database_cache = $database_cache; $this->action_scheduler_service = $action_scheduler_service; $this->session_service = $session_service; + $this->redirect_service = $redirect_service; } /** @@ -96,13 +104,12 @@ public function __construct( public function init_hooks() { // Add admin init hooks. add_action( 'admin_init', [ $this, 'maybe_handle_onboarding' ] ); - add_action( 'admin_init', [ $this, 'maybe_redirect_to_onboarding' ], 11 ); // Run this after the WC setup wizard and onboarding redirection logic. - add_action( 'admin_init', [ $this, 'maybe_redirect_to_wcpay_connect' ], 12 ); // Run this after the redirect to onboarding logic. - add_action( 'admin_init', [ $this, 'maybe_redirect_to_capital_offer' ] ); - add_action( 'admin_init', [ $this, 'maybe_redirect_to_server_link' ] ); - add_action( 'admin_init', [ $this, 'maybe_redirect_settings_to_connect_or_overview' ] ); - add_action( 'admin_init', [ $this, 'maybe_redirect_onboarding_flow_to_overview' ] ); - add_action( 'admin_init', [ $this, 'maybe_redirect_onboarding_flow_to_connect' ] ); + + add_action( 'admin_init', [ $this, 'maybe_redirect_after_plugin_activation' ], 11 ); // Run this after the WC setup wizard and onboarding redirection logic. + add_action( 'admin_init', [ $this, 'maybe_redirect_by_get_param' ], 12 ); // Run this after the redirect to onboarding logic. + add_action( 'admin_init', [ $this, 'maybe_redirect_from_settings_page' ] ); + add_action( 'admin_init', [ $this, 'maybe_redirect_from_onboarding_page' ] ); + add_action( 'admin_init', [ $this, 'maybe_activate_woopay' ] ); // Add handlers for inbox notes and reminders. @@ -600,126 +607,83 @@ public function get_is_live() { } /** - * Checks if the request is for the Capital view offer redirection page, and redirects to the offer if so. + * Checks if the request contains specific get param to redirect further, and redirects to the relevant link if so. * * Only admins are be able to perform this action. The redirect doesn't happen if the request is an AJAX request. - * This method will end execution after the redirect if the user requests and is allowed to view the loan offer. */ - public function maybe_redirect_to_capital_offer() { - if ( wp_doing_ajax() ) { - return; - } - + public function maybe_redirect_by_get_param() { // Safety check to prevent non-admin users to be redirected to the view offer page. - if ( ! current_user_can( 'manage_woocommerce' ) ) { + if ( wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) { return; } - // This is an automatic redirection page, used to authenticate users that come from the offer email. For this reason + // This is an automatic redirection page, used to authenticate users that come from the KYC reminder email. For this reason // we're not using a nonce. The GET parameter accessed here is just to indicate that we should process the redirection. // phpcs:disable WordPress.Security.NonceVerification.Recommended - if ( ! isset( $_GET['wcpay-loan-offer'] ) ) { - return; - } + if ( isset( $_GET['wcpay-connect-redirect'] ) ) { + $params = [ + 'page' => 'wc-admin', + 'path' => '/payments/connect', + ]; - $return_url = static::get_overview_page_url(); - $refresh_url = add_query_arg( [ 'wcpay-loan-offer' => '' ], admin_url( 'admin.php' ) ); + // We're not in the connect page, don't redirect. + if ( count( $params ) !== count( array_intersect_assoc( $_GET, $params ) ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended + return; + } - try { - $request = Get_Account_Capital_Link::create(); - $type = 'capital_financing_offer'; - $request->set_type( $type ); - $request->set_return_url( $return_url ); - $request->set_refresh_url( $refresh_url ); - - $capital_link = $request->send(); - $this->redirect_to( $capital_link['url'] ); - } catch ( Exception $e ) { - $error_url = add_query_arg( - [ 'wcpay-loan-offer-error' => '1' ], - self::get_overview_page_url() - ); + $redirect_param = sanitize_text_field( wp_unslash( $_GET['wcpay-connect-redirect'] ) ); + + // Let's record in Tracks merchants returning via the KYC reminder email. + if ( 'initial' === $redirect_param ) { + $offset = 1; + $description = 'initial'; + } elseif ( 'second' === $redirect_param ) { + $offset = 3; + $description = 'second'; + } else { + $follow_number = in_array( $redirect_param, [ '1', '2', '3', '4' ], true ) ? $redirect_param : '0'; + // offset is recorded in days, $follow_number maps to the week number. + $offset = (int) $follow_number * 7; + $description = 'weekly-' . $follow_number; + } - $this->redirect_to( $error_url ); - } - } + $track_props = [ + 'offset' => $offset, + 'description' => $description, + ]; + $this->tracks_event( self::TRACKS_EVENT_KYC_REMINDER_MERCHANT_RETURNED, $track_props ); - /** - * Checks if the request is for the server links handler, and redirects to the link if it's valid. - * - * Only admins are be able to perform this action. The redirect doesn't happen if the request is an AJAX request. - * This method will end execution after the redirect if the user is allowed to view the link and the link is valid. - */ - public function maybe_redirect_to_server_link() { - if ( wp_doing_ajax() ) { - return; + $this->redirect_service->redirect_to_wcpay_connect( 'WCPAY_KYC_REMINDER' ); } - // Safety check to prevent non-admin users to be redirected to the view offer page. - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return; + // This is an automatic redirection page, used to authenticate users that come from the capitcal offer email. For this reason + // we're not using a nonce. The GET parameter accessed here is just to indicate that we should process the redirection. + // phpcs:disable WordPress.Security.NonceVerification.Recommended + if ( isset( $_GET['wcpay-loan-offer'] ) ) { + $this->redirect_service->redirect_to_capital_view_offer_page(); } // This is an automatic redirection page, used to authenticate users that come from an email link. For this reason // we're not using a nonce. The GET parameter accessed here is just to indicate that we should process the redirection. // phpcs:disable WordPress.Security.NonceVerification.Recommended - if ( ! isset( $_GET['wcpay-link-handler'] ) ) { - return; - } - - // Get all request arguments to be forwarded and remove the link handler identifier. - $args = $_GET; - unset( $args['wcpay-link-handler'] ); - - $this->redirect_to_account_link( $args ); - } - - /** - * Function to immediately redirect to the account link. - * - * @param array $args The arguments to be sent with the link request. - */ - private function redirect_to_account_link( array $args ) { - try { - $link = $this->payments_api_client->get_link( $args ); - - if ( isset( $args['type'] ) && 'complete_kyc_link' === $args['type'] && isset( $link['state'] ) ) { - set_transient( 'wcpay_stripe_onboarding_state', $link['state'], DAY_IN_SECONDS ); - } + if ( isset( $_GET['wcpay-link-handler'] ) ) { + // Get all request arguments to be forwarded and remove the link handler identifier. + $args = $_GET; + unset( $args['wcpay-link-handler'] ); - $this->redirect_to( $link['url'] ); - } catch ( API_Exception $e ) { - $error_url = add_query_arg( - [ 'wcpay-server-link-error' => '1' ], - self::get_overview_page_url() - ); - - $this->redirect_to( $error_url ); + $this->redirect_service->redirect_to_account_link( $args ); } } /** - * Utility function to immediately redirect to the main "Welcome to WooPayments" onboarding page. + * Proxy method that's called in other classes that have access to account (not redirect_service) + * to immediately redirect to the main "Welcome to WooPayments" onboarding page. * Note that this function immediately ends the execution. * - * @param string $error_message Optional error message to show in a notice. + * @param string|null $error_message Optional error message to show in a notice. */ public function redirect_to_onboarding_welcome_page( $error_message = null ) { - if ( isset( $error_message ) ) { - set_transient( self::ERROR_MESSAGE_TRANSIENT, $error_message, 30 ); - } - - $params = [ - 'page' => 'wc-admin', - 'path' => '/payments/connect', - ]; - if ( count( $params ) === count( array_intersect_assoc( $_GET, $params ) ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended - // We are already in the onboarding page, do nothing. - return; - } - - wp_safe_redirect( admin_url( add_query_arg( $params, 'admin.php' ) ) ); - exit(); + $this->redirect_service->redirect_to_connect_page( $error_message ); } /** @@ -727,7 +691,7 @@ public function redirect_to_onboarding_welcome_page( $error_message = null ) { * * @return bool True if the redirection happened. */ - public function maybe_redirect_to_onboarding() { + public function maybe_redirect_after_plugin_activation() { if ( wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) { return false; } @@ -765,77 +729,11 @@ public function maybe_redirect_to_onboarding() { // Redirect directly to onboarding page if come from WC Admin task. $http_referer = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ?? '' ) ); if ( 0 < strpos( $http_referer, 'task=payments' ) ) { - $this->redirect_to_onboarding_flow_page( WC_Payments_Onboarding_Service::SOURCE_WCADMIN_PAYMENT_TASK ); + $this->redirect_to_onboarding_page_or_start_server_connection( WC_Payments_Onboarding_Service::SOURCE_WCADMIN_PAYMENT_TASK ); } // Redirect if not connected. - $this->redirect_to_onboarding_welcome_page(); - return true; - } - - /** - * Redirects to the wcpay-connect URL, which then redirects to the KYC flow. - * - * This URL is used by the KYC reminder email. We can't take the merchant - * directly to the wcpay-connect URL because it's nonced, and the - * nonce will likely be expired by the time the user follows the link. - * That's why we need this middleman instead. - * - * @return bool True if the redirection happened, false otherwise. - */ - public function maybe_redirect_to_wcpay_connect(): bool { - if ( wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) { - return false; - } - - $params = [ - 'page' => 'wc-admin', - 'path' => '/payments/connect', - ]; - - // We're not in the onboarding page, don't redirect. - if ( count( $params ) !== count( array_intersect_assoc( $_GET, $params ) ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended - return false; - } - - if ( ! isset( $_GET['wcpay-connect-redirect'] ) ) { - return false; - } - - $redirect_param = sanitize_text_field( wp_unslash( $_GET['wcpay-connect-redirect'] ) ); - - // Let's record in Tracks merchants returning via the KYC reminder email. - if ( 'initial' === $redirect_param ) { - $offset = 1; - $description = 'initial'; - } elseif ( 'second' === $redirect_param ) { - $offset = 3; - $description = 'second'; - } else { - $follow_number = in_array( $redirect_param, [ '1', '2', '3', '4' ], true ) ? $redirect_param : '0'; - // offset is recorded in days, $follow_number maps to the week number. - $offset = (int) $follow_number * 7; - $description = 'weekly-' . $follow_number; - } - - $track_props = [ - 'offset' => $offset, - 'description' => $description, - ]; - $this->tracks_event( self::TRACKS_EVENT_KYC_REMINDER_MERCHANT_RETURNED, $track_props ); - - // Take the user to the 'wcpay-connect' URL. - // We handle creating and redirecting to the account link there. - $connect_url = add_query_arg( - [ - 'wcpay-connect' => '1', - '_wpnonce' => wp_create_nonce( 'wcpay-connect' ), - 'from' => 'WCPAY_KYC_REMINDER', - ], - admin_url( 'admin.php' ) - ); - - $this->redirect_to( $connect_url ); + $this->redirect_service->redirect_to_connect_page(); return true; } @@ -848,7 +746,7 @@ public function maybe_redirect_to_wcpay_connect(): bool { * * @return bool True if a redirection happened, false otherwise. */ - public function maybe_redirect_settings_to_connect_or_overview(): bool { + public function maybe_redirect_from_settings_page(): bool { if ( wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) { return false; } @@ -866,18 +764,7 @@ public function maybe_redirect_settings_to_connect_or_overview(): bool { // Not able to establish Stripe connection, redirect to the Connect page. if ( ! $this->is_stripe_connected() ) { - $this->redirect_to( - admin_url( - add_query_arg( - [ - 'page' => 'wc-admin', - 'path' => '/payments/connect', - 'from' => 'WCADMIN_PAYMENT_SETTINGS', - ], - 'admin.php' - ) - ) - ); + $this->redirect_service->redirect_to_connect_page( null, 'WCADMIN_PAYMENT_SETTINGS' ); return true; } @@ -886,21 +773,9 @@ public function maybe_redirect_settings_to_connect_or_overview(): bool { return false; } else { // Account not yet fully onboarded so redirect to overview page. - $this->redirect_to( - admin_url( - add_query_arg( - [ - 'page' => 'wc-admin', - 'path' => '/payments/overview', - 'from' => 'WCADMIN_PAYMENT_SETTINGS', - ], - 'admin.php' - ) - ) - ); + $this->redirect_service->redirect_to_overview_page( 'WCADMIN_PAYMENT_SETTINGS' ); + return true; } - - return true; } /** @@ -911,7 +786,7 @@ public function maybe_redirect_settings_to_connect_or_overview(): bool { * * @return bool True if the redirection happened, false otherwise. */ - public function maybe_redirect_onboarding_flow_to_overview(): bool { + public function maybe_redirect_from_onboarding_page(): bool { if ( wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) { return false; } @@ -926,6 +801,28 @@ public function maybe_redirect_onboarding_flow_to_overview(): bool { return false; } + // Prevent access to onboarding flow if the server is not connected. Redirect back to the connect page with an error message. + if ( ! $this->payments_api_client->is_server_connected() ) { + $referer = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ?? '' ) ); + + // Track unsuccessful Jetpack connection. + if ( strpos( $referer, 'wordpress.com' ) ) { + $this->tracks_event( + self::TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_FAILURE, + [ 'mode' => WC_Payments::mode()->is_test() ? 'test' : 'live' ] + ); + } + + $this->redirect_service->redirect_to_connect_page( + sprintf( + /* translators: %s: WooPayments */ + __( 'Please connect to WordPress.com to start using %s.', 'woocommerce-payments' ), + 'WooPayments' + ) + ); + return true; + } + // We check it here after refreshing the cache, because merchant might have clicked back in browser (after Stripe KYC). // That will mean that no redirect from Stripe happened and user might be able to go through onboarding again if no webhook processed yet. // That might cause issues if user selects sandbox onboarding after live one. @@ -939,67 +836,11 @@ public function maybe_redirect_onboarding_flow_to_overview(): bool { return false; } - $this->redirect_to( - admin_url( - add_query_arg( - [ - 'page' => 'wc-admin', - 'path' => '/payments/overview', - 'from' => 'WCPAY_ONBOARDING_FLOW', - ], - 'admin.php' - ) - ) - ); + $this->redirect_service->redirect_to_overview_page( 'WCPAY_ONBOARDING_FLOW' ); return true; } - /** - * Prevent access to onboarding flow if the server is not connected. - * Redirect back to the connect page with an error message. - * - * @return void - */ - public function maybe_redirect_onboarding_flow_to_connect(): void { - if ( wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) { - return; - } - - $params = [ - 'page' => 'wc-admin', - 'path' => '/payments/onboarding', - ]; - - // We're not in the onboarding flow page, don't redirect. - if ( count( $params ) !== count( array_intersect_assoc( $_GET, $params ) ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended - return; - } - - // Server is connected, don't redirect. - if ( $this->payments_api_client->is_server_connected() ) { - return; - } - - $referer = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ?? '' ) ); - - // Track unsuccessful Jetpack connection. - if ( strpos( $referer, 'wordpress.com' ) ) { - $this->tracks_event( - self::TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_FAILURE, - [ 'mode' => WC_Payments::mode()->is_test() ? 'test' : 'live' ] - ); - } - - $this->redirect_to_onboarding_welcome_page( - sprintf( - /* translators: %s: WooPayments */ - __( 'Please connect to WordPress.com to start using %s.', 'woocommerce-payments' ), - 'WooPayments' - ) - ); - } - /** * Filter function to add Stripe to the list of allowed redirect hosts * @@ -1031,20 +872,16 @@ public function maybe_handle_onboarding() { $args['is_progressive_onboarding'] = $this->is_progressive_onboarding_in_progress() ?? false; } - $this->redirect_to_account_link( $args ); + $this->redirect_service->redirect_to_account_link( $args ); } - $this->redirect_to_login(); + // Clear account transient when generating Stripe dashboard's login link. + $this->clear_cache(); + $this->redirect_service->redirect_to_login(); } catch ( Exception $e ) { Logger::error( 'Failed redirect_to_login: ' . $e ); - wp_safe_redirect( - add_query_arg( - [ 'wcpay-login-error' => '1' ], - self::get_overview_page_url() - ) - ); - exit; + $this->redirect_service->redirect_to_overview_page_with_error( [ 'wcpay-login-error' => '1' ] ); } return; } @@ -1101,9 +938,9 @@ public function maybe_handle_onboarding() { } if ( WC_Payments_Onboarding_Service::SOURCE_WCADMIN_SETTINGS_PAGE === $connect_page_source ) { - $this->redirect_to_onboarding_welcome_page(); + $this->redirect_service->redirect_to_connect_page(); } else { - $this->redirect_to_onboarding_flow_page( $connect_page_source ); + $this->redirect_to_onboarding_page_or_start_server_connection( $connect_page_source ); } } elseif ( WC_Payments_Onboarding_Service::SOURCE_WCADMIN_SETTINGS_PAGE === $connect_page_source && ! $this->is_details_submitted() ) { try { @@ -1116,14 +953,14 @@ public function maybe_handle_onboarding() { ); } catch ( Exception $e ) { Logger::error( 'Init Stripe onboarding flow failed. ' . $e ); - $this->redirect_to_onboarding_welcome_page( + $this->redirect_service->redirect_to_connect_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() ); + $this->redirect_service->redirect_to_overview_page(); } } @@ -1138,7 +975,7 @@ public function maybe_handle_onboarding() { // Set the test mode to false now that we are handling a real onboarding. WC_Payments_Onboarding_Service::set_test_mode( false ); - $this->redirect_to_onboarding_flow_page( $connect_page_source ); + $this->redirect_to_onboarding_page_or_start_server_connection( $connect_page_source ); return; } @@ -1147,7 +984,7 @@ public function maybe_handle_onboarding() { // Delete the account. $this->payments_api_client->delete_account( $test_mode ); - $this->redirect_to_onboarding_flow_page( $connect_page_source ); + $this->redirect_to_onboarding_page_or_start_server_connection( $connect_page_source ); return; } @@ -1162,7 +999,7 @@ public function maybe_handle_onboarding() { $event_properties ); - $this->redirect_to_onboarding_welcome_page( + $this->redirect_service->redirect_to_connect_page( sprintf( /* translators: %s: WooPayments */ __( 'Connection to WordPress.com failed. Please connect to WordPress.com to start using %s.', 'woocommerce-payments' ), @@ -1194,7 +1031,7 @@ public function maybe_handle_onboarding() { ] ); } catch ( Exception $e ) { - $this->redirect_to_onboarding_welcome_page( + $this->redirect_service->redirect_to_connect_page( /* translators: error message. */ sprintf( __( 'There was a problem connecting this site to WordPress.com: "%s"', 'woocommerce-payments' ), $e->getMessage() ) ); @@ -1211,7 +1048,7 @@ public function maybe_handle_onboarding() { ); } catch ( Exception $e ) { Logger::error( 'Init Stripe onboarding flow failed. ' . $e ); - $this->redirect_to_onboarding_welcome_page( + $this->redirect_service->redirect_to_connect_page( __( 'There was a problem redirecting you to the account connection page. Please try again.', 'woocommerce-payments' ) ); } @@ -1343,18 +1180,6 @@ public static function is_on_boarding_disabled() { return get_transient( self::ON_BOARDING_DISABLED_TRANSIENT ); } - /** - * Calls wp_safe_redirect and exit. - * - * This method will end the execution immediately after the redirection. - * - * @param string $location The URL to redirect to. - */ - protected function redirect_to( $location ) { - wp_safe_redirect( $location ); - exit; - } - /** * Starts the Jetpack connection flow if it's not already fully connected. * @@ -1386,23 +1211,6 @@ private function maybe_init_jetpack_connection( $wcpay_connect_from, $additional $this->payments_api_client->start_server_connection( $redirect ); } - /** - * For the connected account, fetches the login url from the API and redirects to it - */ - private function redirect_to_login() { - // Clear account transient when generating Stripe dashboard's login link. - $this->clear_cache(); - $redirect_url = static::get_overview_page_url(); - - $request = Get_Account_Login_Data::create(); - $request->set_redirect_url( $redirect_url ); - - $response = $request->send(); - $login_data = $response->to_array(); - wp_safe_redirect( $login_data['url'] ); - exit; - } - /** * Builds the URL to return the user to after the Jetpack/Onboarding flow. * @@ -1441,7 +1249,7 @@ private function get_onboarding_return_url( $wcpay_connect_from ) { */ private function init_stripe_onboarding( $wcpay_connect_from, $additional_args = [] ) { if ( get_transient( self::ON_BOARDING_STARTED_TRANSIENT ) ) { - $this->redirect_to_onboarding_welcome_page( + $this->redirect_service->redirect_to_connect_page( __( 'There was a duplicate attempt to initiate account setup. Please wait a few seconds and try again.', 'woocommerce-payments' ) ); return; @@ -1546,20 +1354,17 @@ private function init_stripe_onboarding( $wcpay_connect_from, $additional_args = if ( false === $onboarding_data['url'] ) { WC_Payments::get_gateway()->update_option( 'enabled', 'yes' ); update_option( '_wcpay_onboarding_stripe_connected', [ 'is_existing_stripe_account' => true ] ); - wp_safe_redirect( - add_query_arg( - [ 'wcpay-connection-success' => '1' ], - $return_url - ) + $redirect_url = add_query_arg( + [ 'wcpay-connection-success' => '1' ], + $return_url ); - exit; + $this->redirect_service->redirect_to( $redirect_url ); } set_transient( 'woopay_enabled_by_default', $onboarding_data['woopay_enabled_by_default'], DAY_IN_SECONDS ); set_transient( 'wcpay_stripe_onboarding_state', $onboarding_data['state'], DAY_IN_SECONDS ); - wp_safe_redirect( $onboarding_data['url'] ); - exit; + $this->redirect_service->redirect_to( $onboarding_data['url'] ); } /** @@ -1584,7 +1389,7 @@ public function maybe_activate_woopay() { */ private function finalize_connection( $state, $mode ) { if ( get_transient( 'wcpay_stripe_onboarding_state' ) !== $state ) { - $this->redirect_to_onboarding_welcome_page( + $this->redirect_service->redirect_to_connect_page( __( 'There was a problem processing your account data. Please try again.', 'woocommerce-payments' ) ); return; @@ -1623,8 +1428,7 @@ private function finalize_connection( $state, $mode ) { $params['wcpay-connection-success'] = '1'; } - wp_safe_redirect( add_query_arg( $params ) ); - exit; + $this->redirect_service->redirect_to( add_query_arg( $params ) ); } /** @@ -2053,20 +1857,20 @@ function (): array { * * @return void */ - private function redirect_to_onboarding_flow_page( string $source ) { + private function redirect_to_onboarding_page_or_start_server_connection( string $source ) { if ( ! WC_Payments_Utils::should_use_new_onboarding_flow() ) { return; } - // Track the Jetpack connection start. - $this->tracks_event( self::TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_START ); - $onboarding_url = add_query_arg( [ 'source' => $source ], admin_url( 'admin.php?page=wc-admin&path=/payments/onboarding' ) ); if ( ! $this->payments_api_client->is_server_connected() ) { + // TODO extract it to redirect service when we have a chance to refactor tracks events. + // Track the Jetpack connection start. + $this->tracks_event( self::TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_START ); try { $this->payments_api_client->start_server_connection( $onboarding_url ); } catch ( API_Exception $e ) { @@ -2074,7 +1878,7 @@ private function redirect_to_onboarding_flow_page( string $source ) { return; } } else { - $this->redirect_to( $onboarding_url ); + $this->redirect_service->redirect_to( $onboarding_url ); } } diff --git a/includes/class-wc-payments-redirect-service.php b/includes/class-wc-payments-redirect-service.php new file mode 100644 index 00000000000..a07d135e454 --- /dev/null +++ b/includes/class-wc-payments-redirect-service.php @@ -0,0 +1,189 @@ +payments_api_client = $payments_api_client; + } + + /** + * Calls wp_safe_redirect and exit. + * + * This method will end the execution immediately after the redirection. + * + * @param string $location The URL to redirect to. + */ + public function redirect_to( string $location ): void { + wp_safe_redirect( $location ); + exit; + } + + /** + * Redirects to the wcpay-connect URL, which then redirects to the KYC flow. + * + * This URL is used by the KYC reminder email. We can't take the merchant + * directly to the wcpay-connect URL because it's nonced, and the + * nonce will likely be expired by the time the user follows the link. + * That's why we need this middleman instead. + * + * @param string $from Source of the redirect. + */ + public function redirect_to_wcpay_connect( string $from = '' ): void { + // Take the user to the 'wcpay-connect' URL. + // We handle creating and redirecting to the account link there. + $params = [ + 'wcpay-connect' => '1', + '_wpnonce' => wp_create_nonce( 'wcpay-connect' ), + ]; + if ( '' !== $from ) { + $params['from'] = $from; + } + $connect_url = add_query_arg( + $params, + admin_url( 'admin.php' ) + ); + + $this->redirect_to( $connect_url ); + } + + /** + * Redirects to the capital view offer page or overview page with error message. + */ + public function redirect_to_capital_view_offer_page(): void { + $return_url = WC_Payments_Account::get_overview_page_url(); + $refresh_url = add_query_arg( [ 'wcpay-loan-offer' => '' ], admin_url( 'admin.php' ) ); + + try { + $request = Get_Account_Capital_Link::create(); + $type = 'capital_financing_offer'; + $request->set_type( $type ); + $request->set_return_url( $return_url ); + $request->set_refresh_url( $refresh_url ); + + $capital_link = $request->send(); + $this->redirect_to( $capital_link['url'] ); + } catch ( Exception $e ) { + + $this->redirect_to_overview_page_with_error( [ 'wcpay-loan-offer-error' => '1' ] ); + } + } + + /** + * Function to immediately redirect to the account link. + * + * @param array $args The arguments to be sent with the link request. + */ + public function redirect_to_account_link( array $args ): void { + try { + $link = $this->payments_api_client->get_link( $args ); + + if ( isset( $args['type'] ) && 'complete_kyc_link' === $args['type'] && isset( $link['state'] ) ) { + set_transient( 'wcpay_stripe_onboarding_state', $link['state'], DAY_IN_SECONDS ); + } + + $this->redirect_to( $link['url'] ); + } catch ( API_Exception $e ) { + $this->redirect_to_overview_page_with_error( [ 'wcpay-server-link-error' => '1' ] ); + } + } + + /** + * Function to immediately redirect to the main "Welcome to WooPayments" connect page. + * Note that this function immediately ends the execution. + * + * @param string|null $error_message Optional error message to show in a notice. + * @param string $from Optional source of the redirect. + */ + public function redirect_to_connect_page( ?string $error_message = null, string $from = '' ): void { + if ( isset( $error_message ) ) { + set_transient( WC_Payments_Account::ERROR_MESSAGE_TRANSIENT, $error_message, 30 ); + } + + $params = [ + 'page' => 'wc-admin', + 'path' => '/payments/connect', + ]; + + if ( count( $params ) === count( array_intersect_assoc( $_GET, $params ) ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended + // We are already in the onboarding page, do nothing. + return; + } + + if ( '' !== $from ) { + $params['from'] = $from; + } + + $this->redirect_to( admin_url( add_query_arg( $params, 'admin.php' ) ) ); + } + + /** + * Redirect to the overview page. + * + * @param string $from Source of the redirect. + */ + public function redirect_to_overview_page( string $from = '' ): void { + $overview_page_url = WC_Payments_Account::get_overview_page_url(); + if ( '' !== $from ) { + $overview_page_url = add_query_arg( 'from', $from, $overview_page_url ); + } + $this->redirect_to( $overview_page_url ); + } + + /** + * Redirect to the overview page with an error message. + * + * @param array $error The error data to show. + */ + public function redirect_to_overview_page_with_error( array $error ): void { + $overview_url_with_error = add_query_arg( + $error, + WC_Payments_Account::get_overview_page_url() + ); + $this->redirect_to( $overview_url_with_error ); + } + + /** + * For the connected account, fetches the login url from the API and redirects to it. + */ + public function redirect_to_login(): void { + + $redirect_url = WC_Payments_Account::get_overview_page_url(); + + $request = Get_Account_Login_Data::create(); + $request->set_redirect_url( $redirect_url ); + + $response = $request->send(); + $login_data = $response->to_array(); + $this->redirect_to( $login_data['url'] ); + } +} diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index afa27c4f4aa..862135efbad 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -82,6 +82,13 @@ class WC_Payments { */ private static $session_service; + /** + * Instance of WC_Payments_Redirect_Service, created in init function. + * + * @var WC_Payments_Redirect_Service + */ + private static $redirect_service; + /** * Instance of WC_Payments_Customer_Service, created in init function. * @@ -385,6 +392,7 @@ public static function init() { include_once __DIR__ . '/compat/subscriptions/trait-wc-payments-subscriptions-utilities.php'; include_once __DIR__ . '/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php'; include_once __DIR__ . '/class-wc-payments-session-service.php'; + include_once __DIR__ . '/class-wc-payments-redirect-service.php'; include_once __DIR__ . '/class-wc-payments-account.php'; include_once __DIR__ . '/class-wc-payments-customer-service.php'; include_once __DIR__ . '/class-logger.php'; @@ -495,7 +503,8 @@ public static function init() { self::$order_service = new WC_Payments_Order_Service( self::$api_client ); self::$action_scheduler_service = new WC_Payments_Action_Scheduler_Service( self::$api_client, self::$order_service ); self::$session_service = new WC_Payments_Session_Service( self::$api_client ); - self::$account = new WC_Payments_Account( self::$api_client, self::$database_cache, self::$action_scheduler_service, self::$session_service ); + self::$redirect_service = new WC_Payments_Redirect_Service( self::$api_client ); + self::$account = new WC_Payments_Account( self::$api_client, self::$database_cache, self::$action_scheduler_service, self::$session_service, self::$redirect_service ); self::$customer_service = new WC_Payments_Customer_Service( self::$api_client, self::$account, self::$database_cache, self::$session_service, self::$order_service ); self::$token_service = new WC_Payments_Token_Service( self::$api_client, self::$customer_service ); self::$remote_note_service = new WC_Payments_Remote_Note_Service( WC_Data_Store::load( 'admin-note' ) ); diff --git a/tests/unit/admin/test-class-wc-payments-admin.php b/tests/unit/admin/test-class-wc-payments-admin.php index ca7917968cb..e4a9ca98bd2 100644 --- a/tests/unit/admin/test-class-wc-payments-admin.php +++ b/tests/unit/admin/test-class-wc-payments-admin.php @@ -221,9 +221,9 @@ private function mock_current_user_is_admin() { } /** - * @dataProvider data_maybe_redirect_to_onboarding + * @dataProvider data_maybe_redirect_from_payments_admin_child_pages */ - public function test_maybe_redirect_to_onboarding( $expected_times_redirect_called, $is_stripe_connected, $get_params ) { + public function test_maybe_redirect_from_payments_admin_child_pages( $expected_times_redirect_called, $is_stripe_connected, $get_params ) { $this->mock_current_user_is_admin(); $_GET = $get_params; @@ -235,13 +235,13 @@ public function test_maybe_redirect_to_onboarding( $expected_times_redirect_call ->expects( $this->exactly( $expected_times_redirect_called ) ) ->method( 'redirect_to_onboarding_welcome_page' ); - $this->payments_admin->maybe_redirect_to_onboarding(); + $this->payments_admin->maybe_redirect_from_payments_admin_child_pages(); } /** - * Data provider for test_maybe_redirect_to_onboarding + * Data provider for test_maybe_redirect_from_payments_admin_child_pages */ - public function data_maybe_redirect_to_onboarding() { + public function data_maybe_redirect_from_payments_admin_child_pages() { return [ 'no_get_params' => [ 0, @@ -297,109 +297,6 @@ public function data_maybe_redirect_to_onboarding() { ]; } - /** - * @dataProvider data_maybe_redirect_overview_to_connect - */ - public function test_maybe_redirect_overview_to_connect( $expected_times_redirect_called, $is_wc_registered_page, $get_params ) { - global $wp_actions; - $this->mock_current_user_is_admin(); - // Avoid WP doing_it_wrong warnings. - // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - $wp_actions['current_screen'] = true; - - $_GET = $get_params; - - // Register the Payments > Connect page as the top level menu item. - wc_admin_register_page( - [ - 'id' => 'wc-payments', - 'title' => __( 'Payments', 'woocommerce-payments' ), - 'capability' => 'manage_woocommerce', - 'path' => '/payments/connect', - 'position' => '55.7', // After WooCommerce & Product menu items. - 'icon' => '', - 'nav_args' => [ - 'title' => 'WooPayments', - 'is_category' => false, - 'menuId' => 'plugins', - 'is_top_level' => true, - ], - ] - ); - - // Whether the current page should be treated as a registered WC admin page or not. - if ( $is_wc_registered_page ) { - add_filter( 'woocommerce_navigation_is_registered_page', '__return_true', 999 ); - } - - $this->mock_account - ->expects( $this->exactly( $expected_times_redirect_called ) ) - ->method( 'redirect_to_onboarding_welcome_page' ); - - $this->payments_admin->maybe_redirect_overview_to_connect(); - - remove_filter( 'woocommerce_navigation_is_registered_page', '__return_true', 999 ); - } - - /** - * Data provider for test_maybe_redirect_overview_to_connect - */ - public function data_maybe_redirect_overview_to_connect() { - return [ - 'no_get_params' => [ - 0, - false, - [], - ], - 'empty_page_param' => [ - 0, - false, - [ - 'path' => '/payments/overview', - ], - ], - 'incorrect_page_param' => [ - 0, - false, - [ - 'page' => 'wc-settings', - 'path' => '/payments/overview', - ], - ], - 'empty_path_param' => [ - 0, - false, - [ - 'page' => 'wc-admin', - ], - ], - 'incorrect_path_param' => [ - 0, - false, - [ - 'page' => 'wc-admin', - 'path' => '/payments/does-not-exist', - ], - ], - 'wc registered page' => [ - 0, - true, - [ - 'page' => 'wc-admin', - 'path' => '/payments/overview', - ], - ], - 'happy_path' => [ - 1, - false, - [ - 'page' => 'wc-admin', - 'path' => '/payments/overview', - ], - ], - ]; - } - /** * Tests WC_Payments_Admin::add_disputes_notification_badge() */ diff --git a/tests/unit/test-class-wc-payments-account-capital.php b/tests/unit/test-class-wc-payments-account-capital.php index a32610f1625..240d2bc2e51 100644 --- a/tests/unit/test-class-wc-payments-account-capital.php +++ b/tests/unit/test-class-wc-payments-account-capital.php @@ -56,6 +56,13 @@ class WC_Payments_Account_Capital_Test extends WCPAY_UnitTestCase { */ private $mock_session_service; + /** + * Mock WC_Payments_Redirect_Service. + * + * @var WC_Payments_Redirect_Service|MockObject + */ + private $mock_redirect_service; + /** * Pre-test setup */ @@ -74,11 +81,12 @@ public function set_up() { $this->mock_database_cache = $this->createMock( Database_Cache::class ); $this->mock_action_scheduler_service = $this->createMock( WC_Payments_Action_Scheduler_Service::class ); $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class ); + $this->mock_redirect_service = $this->createMock( WC_Payments_Redirect_Service::class ); // Mock WC_Payments_Account without redirect_to to prevent headers already sent error. $this->wcpay_account = $this->getMockBuilder( WC_Payments_Account::class ) - ->setMethods( [ 'redirect_to', 'init_hooks' ] ) - ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service ] ) + ->setMethods( [ 'init_hooks' ] ) + ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service, $this->mock_redirect_service ] ) ->getMock(); $this->wcpay_account->init_hooks(); } @@ -94,93 +102,45 @@ public function tear_down() { parent::tear_down(); } - public function test_maybe_redirect_to_capital_offer_will_run() { + public function test_maybe_redirect_by_get_param_will_run() { $wcpay_account = $this->getMockBuilder( WC_Payments_Account::class ) - ->setMethodsExcept( [ 'maybe_redirect_to_capital_offer', 'init_hooks' ] ) - ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service ] ) + ->setMethodsExcept( [ 'init_hooks' ] ) + ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service, $this->mock_redirect_service ] ) ->getMock(); $wcpay_account->init_hooks(); $this->assertNotFalse( - has_action( 'admin_init', [ $wcpay_account, 'maybe_redirect_to_capital_offer' ] ) + has_action( 'admin_init', [ $wcpay_account, 'maybe_redirect_by_get_param' ] ) ); } public function test_maybe_redirect_to_capital_offer_skips_ajax_requests() { add_filter( 'wp_doing_ajax', '__return_true' ); - $this->mock_wcpay_request( Get_Account_Capital_Link::class, 0 ); + $this->mock_redirect_service->expects( $this->never() )->method( 'redirect_to_capital_view_offer_page' ); - $this->wcpay_account->maybe_redirect_to_capital_offer(); + $this->wcpay_account->maybe_redirect_by_get_param(); } public function test_maybe_redirect_to_capital_offer_skips_non_admin_users() { wp_set_current_user( 0 ); - $this->mock_wcpay_request( Get_Account_Capital_Link::class, 0 ); + $this->mock_redirect_service->expects( $this->never() )->method( 'redirect_to_capital_view_offer_page' ); - $this->wcpay_account->maybe_redirect_to_capital_offer(); + $this->wcpay_account->maybe_redirect_by_get_param(); } public function test_maybe_redirect_to_capital_offer_skips_regular_requests() { unset( $_GET['wcpay-loan-offer'] ); - $this->mock_wcpay_request( Get_Account_Capital_Link::class, 0 ); + $this->mock_redirect_service->expects( $this->never() )->method( 'redirect_to_capital_view_offer_page' ); - $this->wcpay_account->maybe_redirect_to_capital_offer(); + $this->wcpay_account->maybe_redirect_by_get_param(); } public function test_maybe_redirect_to_capital_offer_redirects_to_capital_offer() { - $request = $this->mock_wcpay_request( Get_Account_Capital_Link::class ); - $request - ->expects( $this->once() ) - ->method( 'set_type' ) - ->with( 'capital_financing_offer' ); - - $request - ->expects( $this->once() ) - ->method( 'set_return_url' ) - ->with( 'http://example.org/wp-admin/admin.php?page=wc-admin&path=/payments/overview' ); - - $request - ->expects( $this->once() ) - ->method( 'set_refresh_url' ) - ->with( 'http://example.org/wp-admin/admin.php?wcpay-loan-offer' ); - - $request->expects( $this->once() ) - ->method( 'format_response' ) - ->willReturn( new Response( [ 'url' => 'https://capital.url' ] ) ); - - $this->wcpay_account->expects( $this->once() )->method( 'redirect_to' )->with( 'https://capital.url' ); - - $this->wcpay_account->maybe_redirect_to_capital_offer(); - } - - public function test_maybe_redirect_to_capital_offer_redirects_to_overview_on_error() { - $request = $this->mock_wcpay_request( Get_Account_Capital_Link::class ); - $request - ->expects( $this->once() ) - ->method( 'set_type' ) - ->with( 'capital_financing_offer' ); - - $request - ->expects( $this->once() ) - ->method( 'set_return_url' ) - ->with( 'http://example.org/wp-admin/admin.php?page=wc-admin&path=/payments/overview' ); - - $request - ->expects( $this->once() ) - ->method( 'set_refresh_url' ) - ->with( 'http://example.org/wp-admin/admin.php?wcpay-loan-offer' ); - - $request->expects( $this->once() ) - ->method( 'format_response' ) - ->willThrowException( - new API_Exception( 'Error: This account has no offer of financing from Capital.', 'invalid_request_error', 400 ) - ); - - $this->wcpay_account->expects( $this->once() )->method( 'redirect_to' )->with( 'http://example.org/wp-admin/admin.php?page=wc-admin&path=%2Fpayments%2Foverview&wcpay-loan-offer-error=1' ); + $this->mock_redirect_service->expects( $this->once() )->method( 'redirect_to_capital_view_offer_page' ); - $this->wcpay_account->maybe_redirect_to_capital_offer(); + $this->wcpay_account->maybe_redirect_by_get_param(); } } diff --git a/tests/unit/test-class-wc-payments-account-link.php b/tests/unit/test-class-wc-payments-account-link.php index 09ec6e4a039..4ab2c92a8af 100644 --- a/tests/unit/test-class-wc-payments-account-link.php +++ b/tests/unit/test-class-wc-payments-account-link.php @@ -54,6 +54,13 @@ class WC_Payments_Account_Server_Links_Test extends WCPAY_UnitTestCase { */ private $mock_session_service; + /** + * Mock WC_Payments_Redirect_Service. + * + * @var WC_Payments_Redirect_Service|MockObject + */ + private $mock_redirect_service; + /** * Pre-test setup */ @@ -72,11 +79,12 @@ public function set_up() { $this->mock_database_cache = $this->createMock( Database_Cache::class ); $this->mock_action_scheduler_service = $this->createMock( WC_Payments_Action_Scheduler_Service::class ); $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class ); + $this->mock_redirect_service = $this->createMock( WC_Payments_Redirect_Service::class ); // Mock WC_Payments_Account without redirect_to to prevent headers already sent error. $this->wcpay_account = $this->getMockBuilder( WC_Payments_Account::class ) - ->setMethods( [ 'redirect_to' ] ) - ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service ] ) + ->setMethods( [ 'init_hooks' ] ) + ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service, $this->mock_redirect_service ] ) ->getMock(); $this->wcpay_account->init_hooks(); @@ -93,47 +101,28 @@ public function tear_down() { parent::tear_down(); } - public function test_maybe_redirect_to_server_link_will_run() { - $this->assertNotFalse( - has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_to_server_link' ] ) - ); - } - public function test_maybe_redirect_to_server_link_skips_ajax_requests() { add_filter( 'wp_doing_ajax', '__return_true' ); - $this->mock_api_client->expects( $this->never() )->method( 'get_link' ); + $this->mock_redirect_service->expects( $this->never() )->method( 'redirect_to_account_link' ); - $this->wcpay_account->maybe_redirect_to_server_link(); + $this->wcpay_account->maybe_redirect_by_get_param(); } public function test_maybe_redirect_to_server_link_skips_non_admin_users() { wp_set_current_user( 0 ); - $this->mock_api_client->expects( $this->never() )->method( 'get_link' ); + $this->mock_redirect_service->expects( $this->never() )->method( 'redirect_to_account_link' ); - $this->wcpay_account->maybe_redirect_to_server_link(); + $this->wcpay_account->maybe_redirect_by_get_param(); } public function test_maybe_redirect_to_server_link_skips_regular_requests() { unset( $_GET['wcpay-link-handler'] ); - $this->mock_api_client->expects( $this->never() )->method( 'get_link' ); - - $this->wcpay_account->maybe_redirect_to_server_link(); - } - - public function test_maybe_redirect_to_server_link_redirects_to_link() { - $this->mock_api_client - ->method( 'get_link' ) - ->willReturn( [ 'url' => 'https://link.url' ] ); - - $this->wcpay_account - ->expects( $this->once() ) - ->method( 'redirect_to' ) - ->with( 'https://link.url' ); + $this->mock_redirect_service->expects( $this->never() )->method( 'redirect_to_account_link' ); - $this->wcpay_account->maybe_redirect_to_server_link(); + $this->wcpay_account->maybe_redirect_by_get_param(); } public function test_maybe_redirect_to_server_link_forwards_all_arguments() { @@ -141,31 +130,17 @@ public function test_maybe_redirect_to_server_link_forwards_all_arguments() { $_GET['id'] = 'link_id'; $_GET['random_arg'] = 'random_arg'; - $this->mock_api_client + $this->mock_redirect_service ->expects( $this->once() ) - ->method( 'get_link' ) + ->method( 'redirect_to_account_link' ) ->with( [ 'type' => 'login_link', 'id' => 'link_id', 'random_arg' => 'random_arg', ] - ) - ->willReturn( [ 'url' => 'https://link.url' ] ); - - $this->wcpay_account->maybe_redirect_to_server_link(); - } - - public function test_maybe_redirect_to_server_link_redirects_to_overview_on_error() { - $this->mock_api_client - ->method( 'get_link' ) - ->willThrowException( new API_Exception( 'Error: The requested link is invalid.', 'invalid_request_error', 400 ) ); - - $this->wcpay_account - ->expects( $this->once() ) - ->method( 'redirect_to' ) - ->with( 'http://example.org/wp-admin/admin.php?page=wc-admin&path=%2Fpayments%2Foverview&wcpay-server-link-error=1' ); + ); - $this->wcpay_account->maybe_redirect_to_server_link(); + $this->wcpay_account->maybe_redirect_by_get_param(); } } diff --git a/tests/unit/test-class-wc-payments-account.php b/tests/unit/test-class-wc-payments-account.php index a3725e2c412..e285d8a609a 100644 --- a/tests/unit/test-class-wc-payments-account.php +++ b/tests/unit/test-class-wc-payments-account.php @@ -57,6 +57,13 @@ class WC_Payments_Account_Test extends WCPAY_UnitTestCase { */ private $mock_session_service; + /** + * Mock WC_Payments_Redirect_Service. + * + * @var WC_Payments_Redirect_Service|MockObject + */ + private $mock_redirect_service; + /** * Pre-test setup */ @@ -77,8 +84,9 @@ public function set_up() { $this->mock_database_cache = $this->createMock( Database_Cache::class ); $this->mock_action_scheduler_service = $this->createMock( WC_Payments_Action_Scheduler_Service::class ); $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class ); + $this->mock_redirect_service = $this->createMock( WC_Payments_Redirect_Service::class ); - $this->wcpay_account = new WC_Payments_Account( $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service ); + $this->wcpay_account = new WC_Payments_Account( $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service, $this->mock_redirect_service ); $this->wcpay_account->init_hooks(); } @@ -90,13 +98,10 @@ public function tear_down() { public function test_filters_registered_properly() { $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_handle_onboarding' ] ), 'maybe_handle_onboarding action does not exist.' ); - $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_to_onboarding' ] ), 'maybe_redirect_to_onboarding action does not exist.' ); - $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_to_wcpay_connect' ] ), 'maybe_redirect_to_wcpay_connect action does not exist.' ); - $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_to_capital_offer' ] ), 'maybe_redirect_to_capital_offer action does not exist.' ); - $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_to_server_link' ] ), 'maybe_redirect_to_server_link action does not exist.' ); - $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_settings_to_connect_or_overview' ] ), 'maybe_redirect_settings_to_connect_or_overview action does not exist.' ); - $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_onboarding_flow_to_overview' ] ), 'maybe_redirect_onboarding_flow_to_overview action does not exist.' ); - $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_onboarding_flow_to_connect' ] ), 'maybe_redirect_onboarding_flow_to_connect action does not exist.' ); + $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_after_plugin_activation' ] ), 'maybe_redirect_after_plugin_activation action does not exist.' ); + $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_by_get_param' ] ), 'maybe_redirect_by_get_param action does not exist.' ); + $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_from_settings_page' ] ), 'maybe_redirect_from_settings_page action does not exist.' ); + $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_from_onboarding_page' ] ), 'maybe_redirect_from_onboarding_page action does not exist.' ); $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_activate_woopay' ] ), 'maybe_activate_woopay action does not exist.' ); $this->assertNotFalse( has_action( 'woocommerce_payments_account_refreshed', [ $this->wcpay_account, 'handle_instant_deposits_inbox_note' ] ), 'handle_instant_deposits_inbox_note action does not exist.' ); $this->assertNotFalse( has_action( 'woocommerce_payments_account_refreshed', [ $this->wcpay_account, 'handle_loan_approved_inbox_note' ] ), 'handle_loan_approved_inbox_note action does not exist.' ); @@ -123,7 +128,7 @@ public function test_maybe_redirect_to_onboarding_stripe_disconnected_redirects( new API_Exception( 'test', 'wcpay_account_not_found', 401 ) ); - $this->assertTrue( $this->wcpay_account->maybe_redirect_to_onboarding() ); + $this->assertTrue( $this->wcpay_account->maybe_redirect_after_plugin_activation() ); $this->assertFalse( WC_Payments_Account::is_on_boarding_disabled() ); // The option should be updated. $this->assertFalse( (bool) get_option( 'wcpay_should_redirect_to_onboarding', false ) ); @@ -149,7 +154,7 @@ public function test_maybe_redirect_to_onboarding_stripe_disconnected_and_on_boa ) ); - $this->assertTrue( $this->wcpay_account->maybe_redirect_to_onboarding() ); + $this->assertTrue( $this->wcpay_account->maybe_redirect_after_plugin_activation() ); $this->assertTrue( WC_Payments_Account::is_on_boarding_disabled() ); // The option should be updated. $this->assertFalse( (bool) get_option( 'wcpay_should_redirect_to_onboarding', false ) ); @@ -173,7 +178,7 @@ public function test_maybe_redirect_to_onboarding_account_error() { $this->expectException( Exception::class ); - $this->assertFalse( $this->wcpay_account->maybe_redirect_to_onboarding() ); + $this->assertFalse( $this->wcpay_account->maybe_redirect_after_plugin_activation() ); // Should not update the option. $this->assertTrue( (bool) get_option( 'wcpay_should_redirect_to_onboarding', false ) ); } @@ -203,7 +208,7 @@ public function test_maybe_redirect_to_onboarding_account_connected() { ) ); - $this->assertFalse( $this->wcpay_account->maybe_redirect_to_onboarding() ); + $this->assertFalse( $this->wcpay_account->maybe_redirect_after_plugin_activation() ); // The option should be updated. $this->assertFalse( (bool) get_option( 'wcpay_should_redirect_to_onboarding', false ) ); } @@ -219,7 +224,7 @@ public function test_maybe_redirect_to_onboarding_with_non_admin_user() { $this->mock_wcpay_request( Get_Account::class, 0 ); - $this->assertFalse( $this->wcpay_account->maybe_redirect_to_onboarding() ); + $this->assertFalse( $this->wcpay_account->maybe_redirect_after_plugin_activation() ); // The option should be updated. $this->assertTrue( (bool) get_option( 'wcpay_should_redirect_to_onboarding', false ) ); } @@ -249,9 +254,9 @@ public function test_maybe_redirect_to_onboarding_checks_the_account_once() { ) ); - $this->assertFalse( $this->wcpay_account->maybe_redirect_to_onboarding() ); + $this->assertFalse( $this->wcpay_account->maybe_redirect_after_plugin_activation() ); // call the method twice but use the mock_api_client to make sure the account has been retrieved only once. - $this->assertFalse( $this->wcpay_account->maybe_redirect_to_onboarding() ); + $this->assertFalse( $this->wcpay_account->maybe_redirect_after_plugin_activation() ); // The option should be updated. $this->assertFalse( (bool) get_option( 'wcpay_should_redirect_to_onboarding', false ) ); } @@ -298,14 +303,14 @@ public function test_maybe_redirect_to_onboarding_returns_true_and_on_boarding_r update_option( 'wcpay_should_redirect_to_onboarding', true ); // First call, on-boarding is disabled. - $this->wcpay_account->maybe_redirect_to_onboarding(); + $this->wcpay_account->maybe_redirect_after_plugin_activation(); $this->assertTrue( WC_Payments_Account::is_on_boarding_disabled() ); // Simulate the situation where the redirect has not happened yet. update_option( 'wcpay_should_redirect_to_onboarding', true ); // Second call, on-boarding re-enabled. - $this->wcpay_account->maybe_redirect_to_onboarding(); + $this->wcpay_account->maybe_redirect_after_plugin_activation(); $this->assertFalse( WC_Payments_Account::is_on_boarding_disabled() ); } @@ -316,15 +321,9 @@ public function test_maybe_redirect_to_wcpay_connect_do_redirect() { // Set the redirection parameter. $_GET['wcpay-connect-redirect'] = 1; - // Mock WC_Payments_Account without redirect_to to prevent headers already sent error. - $mock_wcpay_account = $this->getMockBuilder( WC_Payments_Account::class ) - ->setMethods( [ 'redirect_to' ] ) - ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service ] ) - ->getMock(); - - $mock_wcpay_account->expects( $this->once() )->method( 'redirect_to' ); + $this->mock_redirect_service->expects( $this->once() )->method( 'redirect_to_wcpay_connect' ); - $this->assertTrue( $mock_wcpay_account->maybe_redirect_to_wcpay_connect() ); + $this->wcpay_account->maybe_redirect_by_get_param(); } public function test_maybe_redirect_to_wcpay_connect_unauthorized_user() { @@ -332,7 +331,9 @@ public function test_maybe_redirect_to_wcpay_connect_unauthorized_user() { $editor_user = $this->factory()->user->create( [ 'role' => 'editor' ] ); wp_set_current_user( $editor_user ); - $this->assertFalse( $this->wcpay_account->maybe_redirect_to_wcpay_connect() ); + $this->mock_redirect_service->expects( $this->never() )->method( 'redirect_to' ); + + $this->wcpay_account->maybe_redirect_by_get_param(); } public function test_maybe_redirect_to_wcpay_connect_doing_ajax() { @@ -345,7 +346,9 @@ public function test_maybe_redirect_to_wcpay_connect_doing_ajax() { // Simulate we're in an AJAX request. add_filter( 'wp_doing_ajax', '__return_true' ); - $this->assertFalse( $this->wcpay_account->maybe_redirect_to_wcpay_connect() ); + $this->mock_redirect_service->expects( $this->never() )->method( 'redirect_to' ); + + $this->wcpay_account->maybe_redirect_by_get_param(); // Cleaning up. remove_filter( 'wp_doing_ajax', '__return_true' ); @@ -360,13 +363,15 @@ public function test_maybe_redirect_to_wcpay_connect_wrong_page() { $_GET['path'] = '/payments/overview'; - $this->assertFalse( $this->wcpay_account->maybe_redirect_to_wcpay_connect() ); + $this->mock_redirect_service->expects( $this->never() )->method( 'redirect_to' ); + + $this->wcpay_account->maybe_redirect_by_get_param(); } /** - * @dataProvider data_maybe_redirect_onboarding_flow_to_overview + * @dataProvider data_maybe_redirect_from_onboarding_page */ - public function test_maybe_redirect_onboarding_flow_to_overview( $expected_redirect_to_count, $stripe_account_connected, $get_params ) { + public function test_maybe_redirect_from_onboarding_page( $expected_redirect_to_count, $expected_method, $stripe_account_connected, $is_server_connected, $get_params ) { wp_set_current_user( 1 ); $_GET = $get_params; @@ -379,68 +384,6 @@ public function test_maybe_redirect_onboarding_flow_to_overview( $expected_redir ); } - // Mock WC_Payments_Account without redirect_to to prevent headers already sent error. - $mock_wcpay_account = $this->getMockBuilder( WC_Payments_Account::class ) - ->setMethods( [ 'redirect_to' ] ) - ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service ] ) - ->getMock(); - - $mock_wcpay_account->expects( $this->exactly( $expected_redirect_to_count ) )->method( 'redirect_to' ); - - $mock_wcpay_account->maybe_redirect_onboarding_flow_to_overview(); - } - - /** - * Data provider for test_maybe_redirect_onboarding_flow_to_overview - */ - public function data_maybe_redirect_onboarding_flow_to_overview() { - return [ - 'no_get_params' => [ - 0, - false, - [], - ], - 'missing_param' => [ - 0, - false, - [ - 'page' => 'wc-admin', - ], - ], - 'incorrect_param' => [ - 0, - false, - [ - 'page' => 'wc-settings', - 'path' => '/payments/onboarding', - ], - ], - 'account_fully_onboarded' => [ - 0, - false, - [ - 'page' => 'wc-admin', - 'path' => '/payments/onboarding', - ], - ], - 'happy_path' => [ - 1, - true, - [ - 'page' => 'wc-admin', - 'path' => '/payments/onboarding', - ], - ], - ]; - } - - /** - * @dataProvider data_maybe_redirect_onboarding_flow_to_connect - */ - public function test_maybe_redirect_onboarding_flow_to_connect( $expected_times_redirect_called, $is_server_connected, $get_params ) { - wp_set_current_user( 1 ); - $_GET = $get_params; - $this->mock_api_client = $this->getMockBuilder( 'WC_Payments_API_Client' ) ->disableOriginalConstructor() ->getMock(); @@ -449,39 +392,39 @@ public function test_maybe_redirect_onboarding_flow_to_connect( $expected_times_ ->method( 'is_server_connected' ) ->willReturn( $is_server_connected ); - // Mock WC_Payments_Account without redirect_to_onboarding_welcome_page to prevent headers already sent error. - $this->wcpay_account = $this->getMockBuilder( WC_Payments_Account::class ) - ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service ] ) - ->onlyMethods( [ 'redirect_to_onboarding_welcome_page' ] ) - ->getMock(); + $this->wcpay_account = new WC_Payments_Account( $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service, $this->mock_redirect_service ); - $this->wcpay_account - ->expects( $this->exactly( $expected_times_redirect_called ) ) - ->method( 'redirect_to_onboarding_welcome_page' ); + $this->mock_redirect_service->expects( $this->exactly( $expected_redirect_to_count ) )->method( $expected_method ); - $this->wcpay_account->maybe_redirect_onboarding_flow_to_connect(); + $this->wcpay_account->maybe_redirect_from_onboarding_page(); } /** - * Data provider for test_maybe_redirect_onboarding_flow_to_connect + * Data provider for test_maybe_redirect_from_onboarding_page */ - public function data_maybe_redirect_onboarding_flow_to_connect() { + public function data_maybe_redirect_from_onboarding_page() { return [ 'no_get_params' => [ 0, + 'redirect_to_connect_page', false, + true, [], ], - 'empty_page_param' => [ + 'missing_param' => [ 0, + 'redirect_to_connect_page', false, + true, [ - 'path' => '/payments/onboarding', + 'page' => 'wc-admin', ], ], - 'incorrect_page_param' => [ + 'incorrect_param' => [ 0, + 'redirect_to_connect_page', false, + true, [ 'page' => 'wc-settings', 'path' => '/payments/onboarding', @@ -489,21 +432,37 @@ public function data_maybe_redirect_onboarding_flow_to_connect() { ], 'empty_path_param' => [ 0, + 'redirect_to_connect_page', false, + true, [ 'page' => 'wc-admin', ], ], 'incorrect_path_param' => [ 0, + 'redirect_to_connect_page', false, + true, [ 'page' => 'wc-admin', 'path' => '/payments/does-not-exist', ], ], - 'server_connected' => [ + 'server_not_connected' => [ + 1, + 'redirect_to_connect_page', + false, + false, + [ + 'page' => 'wc-admin', + 'path' => '/payments/onboarding', + ], + ], + 'stripe not connected' => [ 0, + 'redirect_to_connect_page', + false, true, [ 'page' => 'wc-admin', @@ -512,7 +471,9 @@ public function data_maybe_redirect_onboarding_flow_to_connect() { ], 'happy_path' => [ 1, - false, + 'redirect_to_overview_page', + true, + true, [ 'page' => 'wc-admin', 'path' => '/payments/onboarding', @@ -522,9 +483,9 @@ public function data_maybe_redirect_onboarding_flow_to_connect() { } /** - * @dataProvider data_maybe_redirect_settings_to_connect_or_overview + * @dataProvider data_maybe_redirect_from_settings_page */ - public function test_maybe_redirect_settings_to_connect_or_overview( $expected_redirect_to_count, $details_submitted, $get_params, $no_account = false, $path = null ) { + public function test_maybe_redirect_from_settings_page( $expected_redirect_to_count, $expected_method, $details_submitted, $get_params, $no_account = false ) { wp_set_current_user( 1 ); $_GET = $get_params; @@ -537,31 +498,26 @@ public function test_maybe_redirect_settings_to_connect_or_overview( $expected_r ] ); } - // Mock WC_Payments_Account without redirect_to to prevent headers already sent error. - $mock_wcpay_account = $this->getMockBuilder( WC_Payments_Account::class ) - ->setMethods( [ 'redirect_to' ] ) - ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service ] ) - ->getMock(); - - $mock_wcpay_account->expects( $this->exactly( $expected_redirect_to_count ) ) - ->method( 'redirect_to' ) - ->with( "http://example.org/wp-admin/admin.php?page=wc-admin&path=/payments/$path" ); + $this->mock_redirect_service->expects( $this->exactly( $expected_redirect_to_count ) ) + ->method( $expected_method ); - $mock_wcpay_account->maybe_redirect_settings_to_connect_or_overview(); + $this->wcpay_account->maybe_redirect_from_settings_page(); } /** - * Data provider for test_maybe_redirect_settings_to_connect_or_overview + * Data provider for test_maybe_redirect_from_settings_page */ - public function data_maybe_redirect_settings_to_connect_or_overview() { + public function data_maybe_redirect_from_settings_page() { return [ 'no_get_params' => [ 0, + 'redirect_to_connect_page', false, [], ], 'missing_param' => [ 0, + 'redirect_to_connect_page', false, [ 'page' => 'wc-settings', @@ -570,6 +526,7 @@ public function data_maybe_redirect_settings_to_connect_or_overview() { ], 'incorrect_param' => [ 0, + 'redirect_to_connect_page', false, [ 'page' => 'wc-admin', @@ -579,6 +536,7 @@ public function data_maybe_redirect_settings_to_connect_or_overview() { ], 'no_account' => [ 1, + 'redirect_to_connect_page', false, [ 'page' => 'wc-settings', @@ -586,10 +544,10 @@ public function data_maybe_redirect_settings_to_connect_or_overview() { 'section' => 'woocommerce_payments', ], true, - 'connect&from=WCADMIN_PAYMENT_SETTINGS', ], 'account_partially_onboarded' => [ 1, + 'redirect_to_overview_page', false, [ 'page' => 'wc-settings', @@ -597,10 +555,10 @@ public function data_maybe_redirect_settings_to_connect_or_overview() { 'section' => 'woocommerce_payments', ], false, - 'overview&from=WCADMIN_PAYMENT_SETTINGS', ], 'account_fully_onboarded' => [ 0, + 'redirect_to_connect_page', true, [ 'page' => 'wc-settings', diff --git a/tests/unit/test-class-wc-payments-redirect-service.php b/tests/unit/test-class-wc-payments-redirect-service.php new file mode 100644 index 00000000000..f122ba615ed --- /dev/null +++ b/tests/unit/test-class-wc-payments-redirect-service.php @@ -0,0 +1,174 @@ +previous_user_id = get_current_user_id(); + // Set admin as the current user. + wp_set_current_user( 1 ); + + $this->mock_api_client = $this->createMock( WC_Payments_API_Client::class ); + + $this->redirect_service = $this->getMockBuilder( WC_Payments_Redirect_Service::class ) + ->setMethods( [ 'redirect_to' ] ) + ->setConstructorArgs( [ $this->mock_api_client ] ) + ->getMock(); + } + + public function tear_down() { + wp_set_current_user( $this->previous_user_id ); + + parent::tear_down(); + } + + public function test_maybe_redirect_to_capital_offer_redirects_to_capital_offer() { + // Set the request as if the user is requesting to view a capital offer. + $request = $this->mock_wcpay_request( Get_Account_Capital_Link::class ); + $request + ->expects( $this->once() ) + ->method( 'set_type' ) + ->with( 'capital_financing_offer' ); + + $request + ->expects( $this->once() ) + ->method( 'set_return_url' ) + ->with( 'http://example.org/wp-admin/admin.php?page=wc-admin&path=/payments/overview' ); + + $request + ->expects( $this->once() ) + ->method( 'set_refresh_url' ) + ->with( 'http://example.org/wp-admin/admin.php?wcpay-loan-offer' ); + + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( new Response( [ 'url' => 'https://capital.url' ] ) ); + + $this->redirect_service->expects( $this->once() )->method( 'redirect_to' )->with( 'https://capital.url' ); + + $this->redirect_service->redirect_to_capital_view_offer_page(); + } + + public function test_maybe_redirect_to_capital_offer_redirects_to_overview_on_error() { + $request = $this->mock_wcpay_request( Get_Account_Capital_Link::class ); + $request + ->expects( $this->once() ) + ->method( 'set_type' ) + ->with( 'capital_financing_offer' ); + + $request + ->expects( $this->once() ) + ->method( 'set_return_url' ) + ->with( 'http://example.org/wp-admin/admin.php?page=wc-admin&path=/payments/overview' ); + + $request + ->expects( $this->once() ) + ->method( 'set_refresh_url' ) + ->with( 'http://example.org/wp-admin/admin.php?wcpay-loan-offer' ); + + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->willThrowException( + new API_Exception( 'Error: This account has no offer of financing from Capital.', 'invalid_request_error', 400 ) + ); + + $this->redirect_service->expects( $this->once() )->method( 'redirect_to' )->with( 'http://example.org/wp-admin/admin.php?page=wc-admin&path=%2Fpayments%2Foverview&wcpay-loan-offer-error=1' ); + + $this->redirect_service->redirect_to_capital_view_offer_page(); + } + + public function test_redirect_to_account_link_success() { + $this->mock_api_client + ->method( 'get_link' ) + ->willReturn( [ 'url' => 'https://link.url' ] ); + + $this->redirect_service + ->expects( $this->once() ) + ->method( 'redirect_to' ) + ->with( 'https://link.url' ); + + $this->redirect_service->redirect_to_account_link( + [ + 'type' => 'login_link', + 'id' => 'link_id', + 'random_arg' => 'random_arg', + ] + ); + } + + public function test_redirect_to_account_link_to_overview_on_error() { + $this->mock_api_client + ->method( 'get_link' ) + ->willThrowException( new API_Exception( 'Error: The requested link is invalid.', 'invalid_request_error', 400 ) ); + + $this->redirect_service + ->expects( $this->once() ) + ->method( 'redirect_to' ) + ->with( 'http://example.org/wp-admin/admin.php?page=wc-admin&path=%2Fpayments%2Foverview&wcpay-server-link-error=1' ); + + $this->redirect_service->redirect_to_account_link( + [ + 'type' => 'login_link', + 'id' => 'link_id', + 'random_arg' => 'random_arg', + ] + ); + } + + public function test_redirect_to_login_success() { + $request = $this->mock_wcpay_request( Get_Account_Login_Data::class ); + + $request->expects( $this->once() ) + ->method( 'set_redirect_url' ) + ->with( 'http://example.org/wp-admin/admin.php?page=wc-admin&path=/payments/overview' ); + + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( new Response( [ 'url' => 'https://login.url' ] ) ); + + $this->redirect_service + ->expects( $this->once() ) + ->method( 'redirect_to' ) + ->with( 'https://login.url' ); + + $this->redirect_service->redirect_to_login(); + } +} From 858ad7eaaeb57ff5da45e4a23d3833379e3c5584 Mon Sep 17 00:00:00 2001 From: Rafael Zaleski Date: Thu, 27 Jun 2024 17:57:44 -0300 Subject: [PATCH 06/31] Add Telemetry Events to ECE (#8993) --- changelog/add-8948-ece-telemetry-events | 4 +++ .../components/express-checkout-component.js | 2 ++ .../blocks/hooks/use-express-checkout.js | 23 +++++++++--- client/express-checkout/event-handlers.js | 29 ++++++++++++++- client/express-checkout/index.js | 5 +++ client/express-checkout/tracking.js | 36 +++++++++++++++++++ 6 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 changelog/add-8948-ece-telemetry-events create mode 100644 client/express-checkout/tracking.js diff --git a/changelog/add-8948-ece-telemetry-events b/changelog/add-8948-ece-telemetry-events new file mode 100644 index 00000000000..51a4cc03c24 --- /dev/null +++ b/changelog/add-8948-ece-telemetry-events @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add telemetry events from PRBs into ECE. diff --git a/client/express-checkout/blocks/components/express-checkout-component.js b/client/express-checkout/blocks/components/express-checkout-component.js index 3c53945c2a0..58ea5d96dfc 100644 --- a/client/express-checkout/blocks/components/express-checkout-component.js +++ b/client/express-checkout/blocks/components/express-checkout-component.js @@ -27,6 +27,7 @@ const ExpressCheckoutComponent = ( { buttonOptions, onButtonClick, onConfirm, + onReady, onCancel, elements, } = useExpressCheckout( { @@ -49,6 +50,7 @@ const ExpressCheckoutComponent = ( { options={ buttonOptions } onClick={ onButtonClick } onConfirm={ onConfirm } + onReady={ onReady } onCancel={ onCancel } onShippingAddressChange={ onShippingAddressChange } onShippingRateChange={ onShippingRateChange } diff --git a/client/express-checkout/blocks/hooks/use-express-checkout.js b/client/express-checkout/blocks/hooks/use-express-checkout.js index ee5f7be6ed4..a587fd7a9e9 100644 --- a/client/express-checkout/blocks/hooks/use-express-checkout.js +++ b/client/express-checkout/blocks/hooks/use-express-checkout.js @@ -1,15 +1,21 @@ -/* global wcpayExpressCheckoutParams */ - /** * External dependencies */ import { useCallback } from '@wordpress/element'; import { useStripe, useElements } from '@stripe/react-stripe-js'; +/** + * Internal dependencies + */ import { getExpressCheckoutButtonStyleSettings, + getExpressCheckoutData, normalizeLineItems, } from 'wcpay/express-checkout/utils'; -import { onConfirmHandler } from 'wcpay/express-checkout/event-handlers'; +import { + onClickHandler, + onConfirmHandler, + onReadyHandler, +} from 'wcpay/express-checkout/event-handlers'; export const useExpressCheckout = ( { api, @@ -44,7 +50,8 @@ export const useExpressCheckout = ( { emailRequired: true, shippingAddressRequired: shippingData?.needsShipping, phoneNumberRequired: - wcpayExpressCheckoutParams?.checkout?.needs_payer_phone, + getExpressCheckoutData( 'checkout' )?.needs_payer_phone ?? + false, shippingRates: shippingData?.shippingRates[ 0 ]?.shipping_rates?.map( ( r ) => { return { @@ -55,8 +62,13 @@ export const useExpressCheckout = ( { } ), }; - event.resolve( options ); + + // Click event from WC Blocks. onClick(); + // Global click event handler from WooPayments to ECE. + onClickHandler( event ); + + event.resolve( options ); }, [ onClick, @@ -81,6 +93,7 @@ export const useExpressCheckout = ( { buttonOptions, onButtonClick, onConfirm, + onReady: onReadyHandler, onCancel, elements, }; diff --git a/client/express-checkout/event-handlers.js b/client/express-checkout/event-handlers.js index cd45606992b..ad8b23f52e4 100644 --- a/client/express-checkout/event-handlers.js +++ b/client/express-checkout/event-handlers.js @@ -2,11 +2,16 @@ * Internal dependencies */ import { + getErrorMessageFromNotice, normalizeOrderData, normalizeShippingAddress, normalizeLineItems, + getExpressCheckoutData, } from './utils'; -import { getErrorMessageFromNotice } from './utils/index'; +import { + trackExpressCheckoutButtonClick, + trackExpressCheckoutButtonLoad, +} from './tracking'; export const shippingAddressChangeHandler = async ( api, event, elements ) => { try { @@ -99,3 +104,25 @@ export const onConfirmHandler = async ( return abortPayment( event, e.message ); } }; + +export const onReadyHandler = async function ( { availablePaymentMethods } ) { + if ( availablePaymentMethods ) { + const enabledMethods = Object.entries( availablePaymentMethods ) + // eslint-disable-next-line no-unused-vars + .filter( ( [ _, isEnabled ] ) => isEnabled ) + // eslint-disable-next-line no-unused-vars + .map( ( [ methodName, _ ] ) => methodName ); + + trackExpressCheckoutButtonLoad( { + paymentMethods: enabledMethods, + source: getExpressCheckoutData( 'button_context' ), + } ); + } +}; + +export const onClickHandler = async function ( { expressPaymentType } ) { + trackExpressCheckoutButtonClick( + expressPaymentType, + getExpressCheckoutData( 'button_context' ) + ); +}; diff --git a/client/express-checkout/index.js b/client/express-checkout/index.js index 45fea02f042..e994a81bd8e 100644 --- a/client/express-checkout/index.js +++ b/client/express-checkout/index.js @@ -13,7 +13,9 @@ import { normalizeLineItems, } from './utils/index'; import { + onClickHandler, onConfirmHandler, + onReadyHandler, shippingAddressChangeHandler, shippingRateChangeHandler, } from './event-handlers'; @@ -262,6 +264,7 @@ jQuery( ( $ ) => { shippingRates, }; wcpayECE.block(); + onClickHandler( event ); event.resolve( clickOptions ); } ); @@ -287,6 +290,8 @@ jQuery( ( $ ) => { eceButton.on( 'cancel', async () => { wcpayECE.unblock(); } ); + + eceButton.on( 'ready', onReadyHandler ); }, getSelectedProductData: () => { diff --git a/client/express-checkout/tracking.js b/client/express-checkout/tracking.js new file mode 100644 index 00000000000..862f6fc587e --- /dev/null +++ b/client/express-checkout/tracking.js @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import { debounce } from 'lodash'; +import { recordUserEvent } from 'tracks'; + +// Track the button click event. +export const trackExpressCheckoutButtonClick = ( paymentMethod, source ) => { + const expressPaymentTypeEvents = { + google_pay: 'gpay_button_click', + apple_pay: 'applepay_button_click', + }; + + const event = expressPaymentTypeEvents[ paymentMethod ]; + if ( ! event ) return; + + recordUserEvent( event, { source } ); +}; + +// Track the button load event. +export const trackExpressCheckoutButtonLoad = debounce( + ( { paymentMethods, source } ) => { + const expressPaymentTypeEvents = { + googlePay: 'gpay_button_load', + applePay: 'applepay_button_load', + }; + + for ( const paymentMethod of paymentMethods ) { + const event = expressPaymentTypeEvents[ paymentMethod ]; + if ( ! event ) continue; + + recordUserEvent( event, { source } ); + } + }, + 1000 +); From 50a2090375813418c8bb51ae49c822a57ed6d9d4 Mon Sep 17 00:00:00 2001 From: Timur Karimov Date: Fri, 28 Jun 2024 10:29:15 +0200 Subject: [PATCH 07/31] Display payment error message in the Payment context with Blocks (#9017) --- .../fix-error-message-location-in-blocks | 4 + client/checkout/blocks/hooks.js | 22 ++ client/checkout/blocks/payment-processor.js | 6 +- client/checkout/blocks/test/hooks.test.js | 113 +++++++++ .../blocks/test/payment-processor.test.js | 1 + includes/class-wc-payment-gateway-wcpay.php | 35 ++- .../test-class-upe-payment-gateway.php | 11 +- ...-payment-gateway-wcpay-process-payment.php | 217 ++++++++---------- .../test-class-wc-payment-gateway-wcpay.php | 55 +++-- 9 files changed, 302 insertions(+), 162 deletions(-) create mode 100644 changelog/fix-error-message-location-in-blocks create mode 100644 client/checkout/blocks/test/hooks.test.js diff --git a/changelog/fix-error-message-location-in-blocks b/changelog/fix-error-message-location-in-blocks new file mode 100644 index 00000000000..ff40aaa06a9 --- /dev/null +++ b/changelog/fix-error-message-location-in-blocks @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Display payment error message in the Payment context with Blocks. diff --git a/client/checkout/blocks/hooks.js b/client/checkout/blocks/hooks.js index 34a7a6f504d..67f8159d7ce 100644 --- a/client/checkout/blocks/hooks.js +++ b/client/checkout/blocks/hooks.js @@ -37,6 +37,28 @@ export const usePaymentCompleteHandler = ( ); }; +/** + * Handles onCheckoutFail event emitter which fires after Blocks checkout processor responds with error. + * + * Displays the error message returned from checkout processor in the noticeContexts.PAYMENTS area. + * + * @param {Function} onCheckoutFail The onCheckoutFail event emitter. + * @param {Object} emitResponse Various helpers for usage with observer. + */ +export const usePaymentFailHandler = ( onCheckoutFail, emitResponse ) => { + useEffect( + () => + onCheckoutFail( ( { processingResponse: { paymentDetails } } ) => { + return { + type: 'failure', + message: paymentDetails.errorMessage, + messageContext: emitResponse.noticeContexts.PAYMENTS, + }; + } ), + [ onCheckoutFail, emitResponse ] + ); +}; + export const useFingerprint = () => { const [ fingerprint, setFingerprint ] = useState( '' ); const [ error, setError ] = useState( null ); diff --git a/client/checkout/blocks/payment-processor.js b/client/checkout/blocks/payment-processor.js index cbb1e8d412f..1779c4b7b2c 100644 --- a/client/checkout/blocks/payment-processor.js +++ b/client/checkout/blocks/payment-processor.js @@ -16,7 +16,7 @@ import { useEffect, useRef } from 'react'; /** * Internal dependencies */ -import { usePaymentCompleteHandler } from './hooks'; +import { usePaymentCompleteHandler, usePaymentFailHandler } from './hooks'; import { getStripeElementOptions, blocksShowLinkButtonHandler, @@ -55,7 +55,7 @@ const PaymentProcessor = ( { api, activePaymentMethod, testingInstructions, - eventRegistration: { onPaymentSetup, onCheckoutSuccess }, + eventRegistration: { onPaymentSetup, onCheckoutSuccess, onCheckoutFail }, emitResponse, paymentMethodId, upeMethods, @@ -237,6 +237,8 @@ const PaymentProcessor = ( { shouldSavePayment ); + usePaymentFailHandler( onCheckoutFail, emitResponse ); + const setHasLoadError = ( event ) => { hasLoadErrorRef.current = true; onLoadError( event ); diff --git a/client/checkout/blocks/test/hooks.test.js b/client/checkout/blocks/test/hooks.test.js new file mode 100644 index 00000000000..8e75908b6cb --- /dev/null +++ b/client/checkout/blocks/test/hooks.test.js @@ -0,0 +1,113 @@ +/** + * External dependencies + */ +import { renderHook, act } from '@testing-library/react-hooks'; + +/** + * Internal dependencies + */ +import { usePaymentFailHandler, useFingerprint } from '../hooks'; +import * as fingerprintModule from '../../utils/fingerprint'; +// import { act } from '@testing-library/react'; + +describe( 'usePaymentFailHandler', () => { + let mockOnCheckoutFail; + let mockEmitResponse; + + beforeEach( () => { + mockOnCheckoutFail = jest.fn(); + mockEmitResponse = { + noticeContexts: { + PAYMENTS: 'payments_context', + }, + }; + } ); + + it( 'should return the correct failure response checkout processor payment failure', () => { + const errorMessage = 'Your card was declined.'; + const paymentDetails = { + errorMessage: errorMessage, + }; + + renderHook( () => + usePaymentFailHandler( mockOnCheckoutFail, mockEmitResponse ) + ); + + expect( mockOnCheckoutFail ).toHaveBeenCalled(); + const failureResponse = mockOnCheckoutFail.mock.calls[ 0 ][ 0 ]( { + processingResponse: { paymentDetails }, + } ); + + expect( failureResponse ).toEqual( { + type: 'failure', + message: errorMessage, + messageContext: 'payments_context', + } ); + } ); +} ); + +describe( 'useFingerprint', () => { + it( 'should return fingerprint', async () => { + const mockVisitorId = 'test-visitor-id'; + const mockGetFingerprint = jest + .fn() + .mockResolvedValue( { visitorId: mockVisitorId } ); + + jest.spyOn( fingerprintModule, 'getFingerprint' ).mockImplementation( + mockGetFingerprint + ); + + let hook; + + await act( async () => { + hook = renderHook( () => useFingerprint() ); + } ); + + const [ fingerprint, error ] = hook.result.current; + + expect( mockGetFingerprint ).toHaveBeenCalledTimes( 1 ); + expect( fingerprint ).toBe( mockVisitorId ); + expect( error ).toBeNull(); + } ); + + it( 'should handle errors when getting fingerprint fails', async () => { + const mockError = new Error( 'Test error' ); + const mockGetFingerprint = jest.fn().mockRejectedValue( mockError ); + + jest.spyOn( fingerprintModule, 'getFingerprint' ).mockImplementation( + mockGetFingerprint + ); + + let hook; + + await act( async () => { + hook = renderHook( () => useFingerprint() ); + } ); + + const [ fingerprint, error ] = hook.result.current; + + expect( mockGetFingerprint ).toHaveBeenCalledTimes( 1 ); + expect( fingerprint ).toBe( '' ); + expect( error ).toBe( mockError.message ); + } ); + + it( 'should use generic error message when error has no message', async () => { + const mockGetFingerprint = jest.fn().mockRejectedValue( {} ); + + jest.spyOn( fingerprintModule, 'getFingerprint' ).mockImplementation( + mockGetFingerprint + ); + + let hook; + + await act( async () => { + hook = renderHook( () => useFingerprint() ); + } ); + + const [ fingerprint, error ] = hook.result.current; + + expect( mockGetFingerprint ).toHaveBeenCalledTimes( 1 ); + expect( fingerprint ).toBe( '' ); + expect( error ).toBe( fingerprintModule.FINGERPRINT_GENERIC_ERROR ); + } ); +} ); diff --git a/client/checkout/blocks/test/payment-processor.test.js b/client/checkout/blocks/test/payment-processor.test.js index 39e0a754022..2fad681cd40 100644 --- a/client/checkout/blocks/test/payment-processor.test.js +++ b/client/checkout/blocks/test/payment-processor.test.js @@ -17,6 +17,7 @@ jest.mock( 'wcpay/checkout/blocks/utils', () => ( { } ) ); jest.mock( '../hooks', () => ( { usePaymentCompleteHandler: () => null, + usePaymentFailHandler: () => null, } ) ); jest.mock( '@woocommerce/blocks-registry', () => ( { getPaymentMethods: () => ( { diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 0fe8b9c4dad..8b38ef31643 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -9,6 +9,8 @@ exit; // Exit if accessed directly. } +use Automattic\WooCommerce\StoreApi\Payments\PaymentContext; +use Automattic\WooCommerce\StoreApi\Payments\PaymentResult; use WCPay\Constants\Country_Code; use WCPay\Constants\Fraud_Meta_Box_Type; use WCPay\Constants\Order_Mode; @@ -560,6 +562,7 @@ public function init_hooks() { add_filter( 'woocommerce_billing_fields', [ $this, 'checkout_update_email_field_priority' ], 50 ); add_action( 'woocommerce_update_order', [ $this, 'schedule_order_tracking' ], 10, 2 ); + add_action( 'woocommerce_rest_checkout_process_payment_with_context', [ $this, 'setup_payment_error_handler' ], 10, 2 ); add_filter( 'rest_request_before_callbacks', [ $this, 'remove_all_actions_on_preflight_check' ], 10, 3 ); @@ -1296,9 +1299,13 @@ public function process_payment( $order_id ) { // 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 ); + wc_add_notice( wp_strip_all_tags( WC_Payments_Utils::get_filtered_error_message( $e, $blocked_by_fraud_rules ) ), 'error' ); + do_action( 'update_payment_result_on_error', $e, $order ); + + return [ + 'result' => 'fail', + 'redirect' => '', + ]; } } @@ -1365,6 +1372,28 @@ public function update_customer_with_order_data( $order, $customer_id, $is_test_ $this->customer_service->update_customer_for_user( $customer_id, $user, $customer_data ); } + /** + * Sets up a handler to add error details to the payment result. + * Registers an action to handle 'update_payment_result_on_error', + * using the payment result object from 'woocommerce_rest_checkout_process_payment_with_context'. + * + * @param PaymentContext $context The payment context. + * @param PaymentResult $result The payment result, passed by reference. + */ + public function setup_payment_error_handler( PaymentContext $context, PaymentResult &$result ) { + add_action( + 'update_payment_result_on_error', + function ( $error ) use ( &$result ) { + $result->set_payment_details( + array_merge( + $result->payment_details, + [ 'errorMessage' => wp_strip_all_tags( $error->getMessage() ) ] + ) + ); + } + ); + } + /** * Manages customer details held on WCPay server for WordPress user associated with an order. * diff --git a/tests/unit/payment-methods/test-class-upe-payment-gateway.php b/tests/unit/payment-methods/test-class-upe-payment-gateway.php index 137a283a944..91000a6490c 100644 --- a/tests/unit/payment-methods/test-class-upe-payment-gateway.php +++ b/tests/unit/payment-methods/test-class-upe-payment-gateway.php @@ -927,13 +927,16 @@ public function test_create_token_from_setup_intent_adds_token() { $this->assertEquals( $mock_token, $this->mock_gateway->create_token_from_setup_intent( $mock_setup_intent_id, $mock_user ) ); } - public function test_exception_will_be_thrown_if_phone_number_is_invalid() { + public function test_failure_result_returned_if_phone_number_is_invalid() { $order = WC_Helper_Order::create_order(); $order->set_billing_phone( '+1123456789123456789123' ); $order->save(); - $this->expectException( Exception::class ); - $this->expectExceptionMessage( 'Invalid phone number.' ); - $this->mock_gateway->process_payment( $order->get_id() ); + $result = $this->mock_gateway->process_payment( $order->get_id() ); + $this->assertEquals( 'fail', $result['result'] ); + $error_notices = WC()->session->get( 'wc_notices' ); + $this->assertNotEmpty( $error_notices ); + $this->assertEquals( 'Invalid phone number.', $error_notices['error'][0]['notice'] ); + WC()->session->set( 'wc_notices', [] ); } public function test_remove_link_payment_method_if_card_disabled() { diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php b/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php index 19e49543b18..0faa39f9b8f 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php @@ -219,6 +219,7 @@ public function set_up() { public function tear_down() { parent::tear_down(); WC_Payments::set_gateway( $this->wcpay_gateway ); + WC()->session->set( 'wc_notices', [] ); } /** @@ -499,11 +500,9 @@ public function test_intent_status_requires_capture() { $this->assertEquals( $this->return_url, $result['redirect'] ); } - public function test_exception_thrown() { + public function test_error_notice_added_on_failure() { // Arrange: Reusable data. $error_message = 'Error: No such customer: 123.'; - $order_id = 123; - $total = 12.23; // Arrange: Create an order to test with. $order = WC_Helper_Order::create_order(); @@ -526,46 +525,41 @@ public function test_exception_thrown() { ) ) ); - $this->expectException( 'Exception' ); // Act: process payment. - $this->expectException( Exception::class ); - try { - $this->mock_wcpay_gateway->process_payment( $order->get_id(), false ); - } catch ( Exception $e ) { - $result_order = wc_get_order( $order->get_id() ); - - // Assert: Order status was updated. - $this->assertEquals( Order_Status::FAILED, $result_order->get_status() ); - - // Assert: Order transaction ID was not set. - $this->assertEquals( '', $result_order->get_transaction_id() ); - - // Assert: Order meta was not updated with charge ID, intention status, or intent ID. - $this->assertEquals( '', $result_order->get_meta( '_intent_id' ) ); - $this->assertEquals( '', $result_order->get_meta( '_charge_id' ) ); - $this->assertEquals( '', $result_order->get_meta( '_intention_status' ) ); - - // Assert: No order note was added, besides the status change and failed transaction details. - $notes = wc_get_order_notes( [ 'order_id' => $result_order->get_id() ] ); - $this->assertCount( 2, $notes ); - $this->assertEquals( 'Order status changed from Pending payment to Failed.', $notes[1]->content ); - $this->assertStringContainsString( 'A payment of $50.00 failed to complete with the following message: Error: No such customer: 123.', strip_tags( $notes[0]->content, '' ) ); - - // Assert: A WooCommerce notice was added. - $this->assertSame( $error_message, $e->getMessage() ); - - throw $e; - } + $result = $this->mock_wcpay_gateway->process_payment( $order->get_id(), false ); + + $this->assertEquals( 'fail', $result['result'] ); + $error_notices = WC()->session->get( 'wc_notices' ); + $this->assertNotEmpty( $error_notices ); + $this->assertEquals( $error_message, $error_notices['error'][0]['notice'] ); + + $result_order = wc_get_order( $order->get_id() ); + + // Assert: Order status was updated. + $this->assertEquals( Order_Status::FAILED, $result_order->get_status() ); + + // Assert: Order transaction ID was not set. + $this->assertEquals( '', $result_order->get_transaction_id() ); + + // Assert: Order meta was not updated with charge ID, intention status, or intent ID. + $this->assertEquals( '', $result_order->get_meta( '_intent_id' ) ); + $this->assertEquals( '', $result_order->get_meta( '_charge_id' ) ); + $this->assertEquals( '', $result_order->get_meta( '_intention_status' ) ); + + // Assert: No order note was added, besides the status change and failed transaction details. + $notes = wc_get_order_notes( [ 'order_id' => $result_order->get_id() ] ); + $this->assertCount( 2, $notes ); + $this->assertEquals( 'Order status changed from Pending payment to Failed.', $notes[1]->content ); + $this->assertStringContainsString( 'A payment of $50.00 failed to complete with the following message: Error: No such customer: 123.', strip_tags( $notes[0]->content, '' ) ); } - public function test_exception_will_be_thrown_if_phone_number_is_invalid() { + public function test_failure_result_returned_if_phone_number_is_invalid() { $order = WC_Helper_Order::create_order(); $order->set_billing_phone( '+1123456789123456789123' ); $order->save(); - $this->expectException( Exception::class ); - $this->expectExceptionMessage( 'Invalid phone number.' ); - $this->mock_wcpay_gateway->process_payment( $order->get_id() ); + $result = $this->mock_wcpay_gateway->process_payment( $order->get_id() ); + $this->assertEquals( 'fail', $result['result'] ); } public function test_connection_exception_thrown() { @@ -595,30 +589,25 @@ public function test_connection_exception_thrown() { ) ) ); - // Arrange: Prepare for the upcoming exception. - $this->expectException( 'Exception' ); // Act: process payment. - $this->expectException( Exception::class ); - try { - $this->mock_wcpay_gateway->process_payment( $order->get_id(), false ); - } catch ( Exception $e ) { - $result_order = wc_get_order( $order->get_id() ); - - // Assert: Order status was updated. - $this->assertEquals( Order_Status::FAILED, $result_order->get_status() ); - - // Assert: No order note was added, besides the status change and failed transaction details. - $notes = wc_get_order_notes( [ 'order_id' => $result_order->get_id() ] ); - $this->assertCount( 2, $notes ); - $this->assertEquals( 'Order status changed from Pending payment to Failed.', $notes[1]->content ); - $this->assertStringContainsString( 'A payment of $50.00 failed to complete with the following message: Test error.', strip_tags( $notes[0]->content, '' ) ); - - // Assert: A WooCommerce notice was added. - $this->assertSame( $error_notice, $e->getMessage() ); - - throw $e; - } + $result = $this->mock_wcpay_gateway->process_payment( $order->get_id(), false ); + + $this->assertEquals( 'fail', $result['result'] ); + $error_notices = WC()->session->get( 'wc_notices' ); + $this->assertNotEmpty( $error_notices ); + $this->assertEquals( $error_notice, $error_notices['error'][0]['notice'] ); + + $result_order = wc_get_order( $order->get_id() ); + + // Assert: Order status was updated. + $this->assertEquals( Order_Status::FAILED, $result_order->get_status() ); + + // Assert: No order note was added, besides the status change and failed transaction details. + $notes = wc_get_order_notes( [ 'order_id' => $result_order->get_id() ] ); + $this->assertCount( 2, $notes ); + $this->assertEquals( 'Order status changed from Pending payment to Failed.', $notes[1]->content ); + $this->assertStringContainsString( 'A payment of $50.00 failed to complete with the following message: Test error.', strip_tags( $notes[0]->content, '' ) ); } /** @@ -626,7 +615,7 @@ public function test_connection_exception_thrown() { * * @dataProvider rate_limiter_error_code_provider */ - public function test_failed_transaction_rate_limiter_bumped( $error_code ) { + public function test_failed_transaction_rate_limiter_bumped( $error_message, $error_code ) { $order = WC_Helper_Order::create_order(); $this->mock_rate_limiter @@ -646,7 +635,7 @@ public function test_failed_transaction_rate_limiter_bumped( $error_code ) { ->will( $this->throwException( new API_Exception( - 'test error', + $error_message, $error_code, 400, 'card_error' @@ -654,17 +643,19 @@ public function test_failed_transaction_rate_limiter_bumped( $error_code ) { ) ); - $this->expectException( Exception::class ); - // Act: process payment. $this->mock_wcpay_gateway->process_payment( $order->get_id() ); + + $error_notices = WC()->session->get( 'wc_notices' ); + $this->assertNotEmpty( $error_notices ); + $this->assertEquals( $error_message, $error_notices['error'][0]['notice'] ); } public function rate_limiter_error_code_provider() { return [ - [ 'card_declined' ], - [ 'incorrect_number' ], - [ 'incorrect_cvc' ], + 'Card declined' => [ 'Your card was declined.', 'card_declined' ], + 'Incorrect number' => [ 'Your card number is incorrect.', 'incorrect_number' ], + 'Incorrect CVC' => [ 'Your card security code is incorrect.', 'incorrect_cvc' ], ]; } @@ -678,27 +669,18 @@ public function test_failed_transaction_rate_limiter_is_limited() { ->method( 'is_limited' ) ->willReturn( true ); - // Arrange: Prepare for the upcoming exception. - $this->expectException( 'Exception' ); - // Act: process payment. - $this->expectException( Exception::class ); - try { - $this->mock_wcpay_gateway->process_payment( $order->get_id(), false ); - } catch ( Exception $e ) { - $result_order = wc_get_order( $order->get_id() ); - - // Assert: Order status was updated. - $this->assertEquals( Order_Status::FAILED, $result_order->get_status() ); - - // Assert: No order note was added, besides the status change and failed transaction details. - $notes = wc_get_order_notes( [ 'order_id' => $result_order->get_id() ] ); - $this->assertCount( 2, $notes ); - $this->assertEquals( 'Order status changed from Pending payment to Failed.', $notes[1]->content ); - $this->assertStringContainsString( 'A payment of $50.00 failed to complete because of too many failed transactions. A rate limiter was enabled for the user to prevent more attempts temporarily.', strip_tags( $notes[0]->content, '' ) ); - - throw $e; - } + $this->mock_wcpay_gateway->process_payment( $order->get_id(), false ); + $result_order = wc_get_order( $order->get_id() ); + + // Assert: Order status was updated. + $this->assertEquals( Order_Status::FAILED, $result_order->get_status() ); + + // Assert: No order note was added, besides the status change and failed transaction details. + $notes = wc_get_order_notes( [ 'order_id' => $result_order->get_id() ] ); + $this->assertCount( 2, $notes ); + $this->assertEquals( 'Order status changed from Pending payment to Failed.', $notes[1]->content ); + $this->assertStringContainsString( 'A payment of $50.00 failed to complete because of too many failed transactions. A rate limiter was enabled for the user to prevent more attempts temporarily.', strip_tags( $notes[0]->content, '' ) ); } /** @@ -821,30 +803,23 @@ public function test_bad_request_exception_thrown() { ) ); - // Arrange: Prepare for the upcoming exception. - $this->expectException( 'Exception' ); - // Act: process payment. - $this->expectException( Exception::class ); - try { - $this->mock_wcpay_gateway->process_payment( $order->get_id(), false ); - } catch ( Exception $e ) { - $result_order = wc_get_order( $order->get_id() ); - - // Assert: Order status was updated. - $this->assertEquals( Order_Status::FAILED, $result_order->get_status() ); - - // Assert: No order note was added, besides the status change and failed transaction details. - $notes = wc_get_order_notes( [ 'order_id' => $result_order->get_id() ] ); - $this->assertCount( 2, $notes ); - $this->assertEquals( 'Order status changed from Pending payment to Failed.', $notes[1]->content ); - $this->assertStringContainsString( "A payment of $50.00 failed to complete with the following message: $error_message", strip_tags( $notes[0]->content, '' ) ); - - // Assert: A WooCommerce notice was added. - $this->assertSame( $error_notice, $e->getMessage() ); - - throw $e; - } + $this->mock_wcpay_gateway->process_payment( $order->get_id(), false ); + $result_order = wc_get_order( $order->get_id() ); + + // Assert: Order status was updated. + $this->assertEquals( Order_Status::FAILED, $result_order->get_status() ); + + // Assert: No order note was added, besides the status change and failed transaction details. + $notes = wc_get_order_notes( [ 'order_id' => $result_order->get_id() ] ); + $this->assertCount( 2, $notes ); + $this->assertEquals( 'Order status changed from Pending payment to Failed.', $notes[1]->content ); + $this->assertStringContainsString( "A payment of $50.00 failed to complete with the following message: $error_message", strip_tags( $notes[0]->content, '' ) ); + + // Assert: A WooCommerce notice was added. + $error_notices = WC()->session->get( 'wc_notices' ); + $this->assertNotEmpty( $error_notices ); + $this->assertEquals( $error_notice, $error_notices['error'][0]['notice'] ); } public function test_incorrect_zip_exception_thrown() { @@ -874,22 +849,16 @@ public function test_incorrect_zip_exception_thrown() { ); // Act: process payment. - $this->expectException( Exception::class ); - try { - $this->mock_wcpay_gateway->process_payment( $order->get_id(), false ); - } catch ( Exception $e ) { - $result_order = wc_get_order( $order->get_id() ); - - // Assert: No order note was added, besides the status change and failed transaction details. - $notes = wc_get_order_notes( [ 'order_id' => $result_order->get_id() ] ); - - // Assert: Correct order notes are added. - $this->assertCount( 2, $notes ); - $this->assertEquals( 'Order status changed from Pending payment to Failed.', $notes[1]->content ); - $this->assertStringContainsString( "A payment of $50.00 failed. $error_note", strip_tags( $notes[0]->content, '' ) ); - - throw $e; - } + $this->mock_wcpay_gateway->process_payment( $order->get_id(), false ); + $result_order = wc_get_order( $order->get_id() ); + + // Assert: No order note was added, besides the status change and failed transaction details. + $notes = wc_get_order_notes( [ 'order_id' => $result_order->get_id() ] ); + + // Assert: Correct order notes are added. + $this->assertCount( 2, $notes ); + $this->assertEquals( 'Order status changed from Pending payment to Failed.', $notes[1]->content ); + $this->assertStringContainsString( "A payment of $50.00 failed. $error_note", strip_tags( $notes[0]->content, '' ) ); } /** diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay.php b/tests/unit/test-class-wc-payment-gateway-wcpay.php index e39a0c73b91..6879500a7b2 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay.php @@ -304,6 +304,7 @@ public function tear_down() { } wcpay_get_test_container()->reset_all_replacements(); + WC()->session->set( 'wc_notices', [] ); } public function test_process_redirect_payment_intent_processing() { @@ -827,13 +828,11 @@ public function test_exception_will_be_thrown_if_phone_number_is_invalid() { $order = WC_Helper_Order::create_order(); $order->set_billing_phone( '+1123456789123456789123' ); $order->save(); - try { - $this->card_gateway->process_payment( $order->get_id() ); - } catch ( Exception $e ) { - $this->assertEquals( 'Exception', get_class( $e ) ); - $this->assertEquals( 'Invalid phone number.', $e->getMessage() ); - $this->assertEquals( 'WCPay\Exceptions\Invalid_Phone_Number_Exception', get_class( $e->getPrevious() ) ); - } + $result = $this->card_gateway->process_payment( $order->get_id() ); + $this->assertEquals( 'fail', $result['result'] ); + $error_notices = WC()->session->get( 'wc_notices' ); + $this->assertNotEmpty( $error_notices ); + $this->assertEquals( 'Invalid phone number.', $error_notices['error'][0]['notice'] ); } public function test_remove_link_payment_method_if_card_disabled() { @@ -2869,17 +2868,12 @@ public function test_process_payment_caches_mimimum_amount_and_displays_error_up $request->expects( $this->once() ) ->method( 'format_response' ) ->will( $this->throwException( new Amount_Too_Small_Exception( 'Error: Amount must be at least $60 usd', 6000, 'usd', 400 ) ) ); - $this->expectException( Exception::class ); - $price = html_entity_decode( wp_strip_all_tags( wc_price( 60, [ 'currency' => 'USD' ] ) ) ); - $message = 'The selected payment method requires a total amount of at least ' . $price . '.'; - $this->expectExceptionMessage( $message ); - try { - $this->card_gateway->process_payment( $order->get_id() ); - } catch ( Exception $e ) { - $this->assertEquals( '6000', get_transient( 'wcpay_minimum_amount_usd' ) ); - throw $e; - } + $this->card_gateway->process_payment( $order->get_id() ); + $error_notices = WC()->session->get( 'wc_notices' ); + $this->assertNotEmpty( $error_notices ); + $this->assertEquals( 'The selected payment method requires a total amount of at least $60.00.', $error_notices['error'][0]['notice'] ); + $this->assertEquals( '6000', get_transient( 'wcpay_minimum_amount_usd' ) ); } public function test_process_payment_rejects_if_missing_fraud_prevention_token() { @@ -2979,10 +2973,11 @@ public function test_process_payment_marks_order_as_blocked_for_fraud() { ->method( 'process_payment_for_order' ) ->willThrowException( new API_Exception( $error_message, 'wcpay_blocked_by_fraud_rule', 400, 'card_error' ) ); - $this->expectException( Exception::class ); - $this->expectExceptionMessage( $error_message ); - - $mock_wcpay_gateway->process_payment( $order->get_id() ); + $result = $mock_wcpay_gateway->process_payment( $order->get_id() ); + $this->assertEquals( 'fail', $result['result'] ); + $error_notices = WC()->session->get( 'wc_notices' ); + $this->assertNotEmpty( $error_notices ); + $this->assertEquals( $error_message, $error_notices['error'][0]['notice'] ); } public function test_process_payment_marks_order_as_blocked_for_fraud_avs_mismatch() { @@ -3049,10 +3044,11 @@ public function test_process_payment_marks_order_as_blocked_for_fraud_avs_mismat ->method( 'process_payment_for_order' ) ->willThrowException( new API_Exception( $error_message, 'incorrect_zip', 400, 'card_error' ) ); - $this->expectException( Exception::class ); - $this->expectExceptionMessage( $error_message ); - - $mock_wcpay_gateway->process_payment( $order->get_id() ); + $result = $mock_wcpay_gateway->process_payment( $order->get_id() ); + $this->assertEquals( 'fail', $result['result'] ); + $error_notices = WC()->session->get( 'wc_notices' ); + $this->assertNotEmpty( $error_notices ); + $this->assertEquals( $error_message, $error_notices['error'][0]['notice'] ); delete_transient( 'wcpay_fraud_protection_settings' ); } @@ -3121,10 +3117,11 @@ public function test_process_payment_marks_order_as_blocked_for_postal_code_mism ->method( 'process_payment_for_order' ) ->willThrowException( new API_Exception( $error_message, 'incorrect_zip', 400, 'card_error' ) ); - $this->expectException( Exception::class ); - $this->expectExceptionMessage( $error_message ); - - $mock_wcpay_gateway->process_payment( $order->get_id() ); + $result = $mock_wcpay_gateway->process_payment( $order->get_id() ); + $this->assertEquals( 'fail', $result['result'] ); + $error_notices = WC()->session->get( 'wc_notices' ); + $this->assertNotEmpty( $error_notices ); + $this->assertEquals( $error_message, $error_notices['error'][0]['notice'] ); delete_transient( 'wcpay_fraud_protection_settings' ); } From 3ae722744ea92487ffec3cd8a342a4c6da9d0293 Mon Sep 17 00:00:00 2001 From: Vlad Olaru Date: Tue, 2 Jul 2024 18:43:12 +0300 Subject: [PATCH 08/31] Updates to Tracks onboarding events props - Take 2 (#9036) --- ...pdate-tracks-onboarding-events-props-take2 | 5 + client/connect-account-page/index.tsx | 20 +++ includes/class-wc-payments-account.php | 44 +++++-- .../class-wc-payments-onboarding-service.php | 8 +- .../class-wc-payments-redirect-service.php | 14 ++- ...est-class-wc-payments-redirect-service.php | 118 ++++++++++++++++-- 6 files changed, 182 insertions(+), 27 deletions(-) create mode 100644 changelog/update-tracks-onboarding-events-props-take2 diff --git a/changelog/update-tracks-onboarding-events-props-take2 b/changelog/update-tracks-onboarding-events-props-take2 new file mode 100644 index 00000000000..b83c62d7f76 --- /dev/null +++ b/changelog/update-tracks-onboarding-events-props-take2 @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: It is just about improving Tracks events data consistency. + + diff --git a/client/connect-account-page/index.tsx b/client/connect-account-page/index.tsx index a273565059c..1dc81e0a497 100644 --- a/client/connect-account-page/index.tsx +++ b/client/connect-account-page/index.tsx @@ -56,12 +56,31 @@ const ConnectAccountPage: React.FC = () => { const isCountrySupported = !! availableCountries[ country ]; + const determineTrackingSource = () => { + const urlParams = new URLSearchParams( window.location.search ); + const from = urlParams.get( 'from' ) || ''; + + // Determine where the user came from. + let source = 'wcadmin'; + switch ( from ) { + case 'WCADMIN_PAYMENT_TASK': + source = 'wcadmin-payment-task'; + break; + case 'WCADMIN_PAYMENT_SETTINGS': + source = 'wcadmin-settings-page'; + break; + } + + return source; + }; + useEffect( () => { recordEvent( 'page_view', { path: 'payments_connect_v2', ...( incentive && { incentive_id: incentive.id, } ), + source: determineTrackingSource(), } ); // We only want to run this once. // eslint-disable-next-line react-hooks/exhaustive-deps @@ -106,6 +125,7 @@ const ConnectAccountPage: React.FC = () => { } ), sandbox_mode: sandboxMode, path: 'payments_connect_v2', + source: determineTrackingSource(), } ); }; diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index e8369a00d15..1f7e2f9b083 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -803,13 +803,18 @@ public function maybe_redirect_from_onboarding_page(): bool { // Prevent access to onboarding flow if the server is not connected. Redirect back to the connect page with an error message. if ( ! $this->payments_api_client->is_server_connected() ) { - $referer = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ?? '' ) ); + $referer = sanitize_text_field( wp_get_raw_referer() ); // Track unsuccessful Jetpack connection. if ( strpos( $referer, 'wordpress.com' ) ) { $this->tracks_event( self::TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_FAILURE, - [ 'mode' => WC_Payments::mode()->is_test() ? 'test' : 'live' ] + [ + 'mode' => WC_Payments::mode()->is_test() ? 'test' : 'live', + // Capture the user source of the connection attempt originating page. + // This is the same source that is used to track the onboarding flow origin. + 'source' => isset( $_GET['source'] ) ? sanitize_text_field( wp_unslash( $_GET['source'] ) ) : '', + ] ); } @@ -818,7 +823,8 @@ public function maybe_redirect_from_onboarding_page(): bool { /* translators: %s: WooPayments */ __( 'Please connect to WordPress.com to start using %s.', 'woocommerce-payments' ), 'WooPayments' - ) + ), + 'WCPAY_ONBOARDING_FLOW' ); return true; } @@ -938,7 +944,7 @@ public function maybe_handle_onboarding() { } if ( WC_Payments_Onboarding_Service::SOURCE_WCADMIN_SETTINGS_PAGE === $connect_page_source ) { - $this->redirect_service->redirect_to_connect_page(); + $this->redirect_service->redirect_to_connect_page( null, 'WCADMIN_PAYMENT_SETTINGS' ); } else { $this->redirect_to_onboarding_page_or_start_server_connection( $connect_page_source ); } @@ -992,6 +998,12 @@ public function maybe_handle_onboarding() { update_option( 'wcpay_menu_badge_hidden', 'yes' ); if ( isset( $_GET['wcpay-connect-jetpack-success'] ) ) { + $test_mode = isset( $_GET['test_mode'] ) && wc_clean( wp_unslash( $_GET['test_mode'] ) ); + $event_properties = [ + 'incentive' => $incentive, + 'mode' => $test_mode || WC_Payments::mode()->is_test() ? 'test' : 'live', + ]; + if ( ! $this->payments_api_client->is_server_connected() ) { // Track unsuccessful Jetpack connection. $this->tracks_event( @@ -1011,11 +1023,6 @@ public function maybe_handle_onboarding() { } // Track successful Jetpack connection. - $test_mode = isset( $_GET['test_mode'] ) ? boolval( wc_clean( wp_unslash( $_GET['test_mode'] ) ) ) : false; - $event_properties = [ - 'incentive' => $incentive, - 'mode' => $test_mode || WC_Payments::mode()->is_test() ? 'test' : 'live', - ]; $this->tracks_event( self::TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_SUCCESS, $event_properties @@ -1081,15 +1088,26 @@ private function get_login_url() { } /** - * Get Stripe connect url + * Get connect url. * * @see WC_Payments_Account::get_onboarding_return_url(). The $wcpay_connect_from param relies on this function returning the corresponding URL. - * @param string $wcpay_connect_from Optional. A page ID representing where the user should be returned to after connecting. Default is '1' - redirects back to the WC Payments overview page. * - * @return string Stripe account login url. + * @param string $wcpay_connect_from Optional. A page ID representing where the user should be returned to after connecting. + * Default is '1' - redirects back to the WooPayments overview page. + * + * @return string Connect URL. */ public static function get_connect_url( $wcpay_connect_from = '1' ) { - return wp_nonce_url( add_query_arg( [ 'wcpay-connect' => $wcpay_connect_from ], admin_url( 'admin.php' ) ), 'wcpay-connect' ); + $url_params = [ + 'wcpay-connect' => $wcpay_connect_from, + ]; + + // Maintain the `from` param from the request URL, if present. + if ( isset( $_GET['from'] ) ) { + $url_params['from'] = sanitize_text_field( wp_unslash( $_GET['from'] ) ); + } + + return wp_nonce_url( add_query_arg( $url_params, admin_url( 'admin.php' ) ), 'wcpay-connect' ); } /** diff --git a/includes/class-wc-payments-onboarding-service.php b/includes/class-wc-payments-onboarding-service.php index e6467fd670f..5318951dac8 100644 --- a/includes/class-wc-payments-onboarding-service.php +++ b/includes/class-wc-payments-onboarding-service.php @@ -254,11 +254,13 @@ public static function get_source( string $referer, array $get_params ): string return self::SOURCE_WCADMIN_PAYMENT_TASK; } // Payments tab in Woo Admin Settings page. - if ( false !== strpos( $referer, 'page=wc-settings&tab=checkout' ) ) { + if ( false !== strpos( $referer, 'page=wc-settings&tab=checkout' ) + || 'WCADMIN_PAYMENT_SETTINGS' === $from_param ) { return self::SOURCE_WCADMIN_SETTINGS_PAGE; } - // Payments tab in the sidebar. - if ( false !== strpos( $referer, 'path=%2Fwc-pay-welcome-page' ) ) { + // Payments incentive page. + if ( false !== strpos( $referer, 'path=%2Fwc-pay-welcome-page' ) + || 'WCADMIN_PAYMENT_INCENTIVE' === $from_param ) { return self::SOURCE_WCADMIN_INCENTIVE_PAGE; } if ( false !== strpos( $referer, 'path=%2Fpayments%2Fconnect' ) ) { diff --git a/includes/class-wc-payments-redirect-service.php b/includes/class-wc-payments-redirect-service.php index a07d135e454..9ec74cb4f6d 100644 --- a/includes/class-wc-payments-redirect-service.php +++ b/includes/class-wc-payments-redirect-service.php @@ -122,9 +122,10 @@ public function redirect_to_account_link( array $args ): void { * Note that this function immediately ends the execution. * * @param string|null $error_message Optional error message to show in a notice. - * @param string $from Optional source of the redirect. + * @param string|null $from Optional source of the redirect. + * Will fall back to keeping the `from` parameter in the current request URL, if present. */ - public function redirect_to_connect_page( ?string $error_message = null, string $from = '' ): void { + public function redirect_to_connect_page( ?string $error_message = null, ?string $from = null ): void { if ( isset( $error_message ) ) { set_transient( WC_Payments_Account::ERROR_MESSAGE_TRANSIENT, $error_message, 30 ); } @@ -135,11 +136,16 @@ public function redirect_to_connect_page( ?string $error_message = null, string ]; if ( count( $params ) === count( array_intersect_assoc( $_GET, $params ) ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended - // We are already in the onboarding page, do nothing. + // We are already on the Connect page. Do nothing. return; } - if ( '' !== $from ) { + // If we were not given a source, try to get it from the request URL. + if ( ! isset( $from ) && isset( $_GET['from'] ) ) { + $from = sanitize_text_field( wp_unslash( $_GET['from'] ) ); + } + + if ( ! empty( $from ) ) { $params['from'] = $from; } diff --git a/tests/unit/test-class-wc-payments-redirect-service.php b/tests/unit/test-class-wc-payments-redirect-service.php index f122ba615ed..c1b1b0823ef 100644 --- a/tests/unit/test-class-wc-payments-redirect-service.php +++ b/tests/unit/test-class-wc-payments-redirect-service.php @@ -49,7 +49,7 @@ public function set_up() { $this->mock_api_client = $this->createMock( WC_Payments_API_Client::class ); $this->redirect_service = $this->getMockBuilder( WC_Payments_Redirect_Service::class ) - ->setMethods( [ 'redirect_to' ] ) + ->onlyMethods( [ 'redirect_to' ] ) ->setConstructorArgs( [ $this->mock_api_client ] ) ->getMock(); } @@ -71,12 +71,12 @@ public function test_maybe_redirect_to_capital_offer_redirects_to_capital_offer( $request ->expects( $this->once() ) ->method( 'set_return_url' ) - ->with( 'http://example.org/wp-admin/admin.php?page=wc-admin&path=/payments/overview' ); + ->with( admin_url( 'admin.php?page=wc-admin&path=/payments/overview' ) ); $request ->expects( $this->once() ) ->method( 'set_refresh_url' ) - ->with( 'http://example.org/wp-admin/admin.php?wcpay-loan-offer' ); + ->with( admin_url( 'admin.php?wcpay-loan-offer' ) ); $request->expects( $this->once() ) ->method( 'format_response' ) @@ -97,12 +97,12 @@ public function test_maybe_redirect_to_capital_offer_redirects_to_overview_on_er $request ->expects( $this->once() ) ->method( 'set_return_url' ) - ->with( 'http://example.org/wp-admin/admin.php?page=wc-admin&path=/payments/overview' ); + ->with( admin_url( 'admin.php?page=wc-admin&path=/payments/overview' ) ); $request ->expects( $this->once() ) ->method( 'set_refresh_url' ) - ->with( 'http://example.org/wp-admin/admin.php?wcpay-loan-offer' ); + ->with( admin_url( 'admin.php?wcpay-loan-offer' ) ); $request->expects( $this->once() ) ->method( 'format_response' ) @@ -142,7 +142,7 @@ public function test_redirect_to_account_link_to_overview_on_error() { $this->redirect_service ->expects( $this->once() ) ->method( 'redirect_to' ) - ->with( 'http://example.org/wp-admin/admin.php?page=wc-admin&path=%2Fpayments%2Foverview&wcpay-server-link-error=1' ); + ->with( admin_url( 'admin.php?page=wc-admin&path=%2Fpayments%2Foverview&wcpay-server-link-error=1' ) ); $this->redirect_service->redirect_to_account_link( [ @@ -158,7 +158,7 @@ public function test_redirect_to_login_success() { $request->expects( $this->once() ) ->method( 'set_redirect_url' ) - ->with( 'http://example.org/wp-admin/admin.php?page=wc-admin&path=/payments/overview' ); + ->with( admin_url( 'admin.php?page=wc-admin&path=/payments/overview' ) ); $request->expects( $this->once() ) ->method( 'format_response' ) @@ -171,4 +171,108 @@ public function test_redirect_to_login_success() { $this->redirect_service->redirect_to_login(); } + + public function test_redirect_to_connect_page_no_redirect() { + // Arrange. + // Set the request as if the user is already on the Connect page. + $_GET = [ + 'page' => 'wc-admin', + 'path' => '/payments/connect', + ]; + + // Assert. + $this->redirect_service + ->expects( $this->never() ) + ->method( 'redirect_to' ); + + // Act. + $this->redirect_service->redirect_to_connect_page(); + + // Cleanup. + unset( $_GET ); + } + + public function test_redirect_to_connect_page_sets_transient_on_error_message() { + // Arrange. + // Set the request as if the user is already on the Connect page. + $_GET = [ + 'page' => 'wc-admin', + 'path' => '/payments/connect', + ]; + + // Assert. + $this->redirect_service + ->expects( $this->never() ) + ->method( 'redirect_to' ); + + // Act. + $this->redirect_service->redirect_to_connect_page( 'Error message' ); + + // Assert. + $this->assertEquals( 'Error message', get_transient( WC_Payments_Account::ERROR_MESSAGE_TRANSIENT ) ); + + // Cleanup. + unset( $_GET ); + } + + public function test_redirect_to_connect_page_redirects() { + // Arrange. + $_GET = [ + 'page' => 'wc-admin', + 'path' => '/some-other-path', + ]; + + // Assert. + $this->redirect_service + ->expects( $this->once() ) + ->method( 'redirect_to' ) + ->with( admin_url( 'admin.php?page=wc-admin&path=/payments/connect' ) ); + + // Act. + $this->redirect_service->redirect_to_connect_page(); + + // Cleanup. + unset( $_GET ); + } + + public function test_redirect_to_connect_page_redirects_with_from() { + // Assert. + $this->redirect_service + ->expects( $this->once() ) + ->method( 'redirect_to' ) + ->with( admin_url( 'admin.php?page=wc-admin&path=/payments/connect&from=FROM_SOMEWHERE' ) ); + + // Act. + $this->redirect_service->redirect_to_connect_page( null, 'FROM_SOMEWHERE' ); + } + + public function test_redirect_to_connect_page_redirects_with_from_param_from_get() { + // Arrange. + $_GET = [ + 'from' => 'FROM_SOMEWHERE', + ]; + + // Assert. + $this->redirect_service + ->expects( $this->once() ) + ->method( 'redirect_to' ) + ->with( admin_url( 'admin.php?page=wc-admin&path=/payments/connect&from=FROM_SOMEWHERE' ) ); + + // Act. + $this->redirect_service->redirect_to_connect_page(); + + // Cleanup. + unset( $_GET ); + } + + public function test_redirect_to_connect_page_redirects_without_from_when_empty() { + // Assert. + $this->redirect_service + ->expects( $this->once() ) + ->method( 'redirect_to' ) + ->with( admin_url( 'admin.php?page=wc-admin&path=/payments/connect' ) ); + + // Act. + $this->redirect_service->redirect_to_connect_page( null, '' ); + } } From 90992934c100ef6ea48d3429ed12360c74416bdf Mon Sep 17 00:00:00 2001 From: Daniel Guerra <15204776+danielmx-dev@users.noreply.github.com> Date: Tue, 2 Jul 2024 20:08:32 +0300 Subject: [PATCH 09/31] Fix: Use a filter instead of an action hook to properly wait for payment request button updates (#9022) --- ...tokenized-prb-shortcode-cart-total-delayed | 4 +++ .../compatibility/wc-product-variations.js | 9 +++--- .../frontend-utils.js | 20 ------------ client/tokenized-payment-request/index.js | 15 ++++++--- .../payment-request.js | 32 ++++++++++--------- .../test/payment-request.test.js | 19 ++++++----- 6 files changed, 46 insertions(+), 53 deletions(-) create mode 100644 changelog/fix-9000-tokenized-prb-shortcode-cart-total-delayed diff --git a/changelog/fix-9000-tokenized-prb-shortcode-cart-total-delayed b/changelog/fix-9000-tokenized-prb-shortcode-cart-total-delayed new file mode 100644 index 00000000000..dbc646e6e8a --- /dev/null +++ b/changelog/fix-9000-tokenized-prb-shortcode-cart-total-delayed @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Properly wait for tokenized cart data updates before refreshing PRB data. diff --git a/client/tokenized-payment-request/compatibility/wc-product-variations.js b/client/tokenized-payment-request/compatibility/wc-product-variations.js index a20123e039a..775a894fec4 100644 --- a/client/tokenized-payment-request/compatibility/wc-product-variations.js +++ b/client/tokenized-payment-request/compatibility/wc-product-variations.js @@ -3,17 +3,18 @@ /** * External dependencies */ -import { addFilter, doAction } from '@wordpress/hooks'; +import { addFilter, applyFilters } from '@wordpress/hooks'; import paymentRequestButtonUi from '../button-ui'; -import { waitForAction } from '../frontend-utils'; jQuery( ( $ ) => { $( document.body ).on( 'woocommerce_variation_has_changed', async () => { try { paymentRequestButtonUi.blockButton(); - doAction( 'wcpay.payment-request.update-button-data' ); - await waitForAction( 'wcpay.payment-request.update-button-data' ); + await applyFilters( + 'wcpay.payment-request.update-button-data', + Promise.resolve() + ); paymentRequestButtonUi.unblockButton(); } catch ( e ) { diff --git a/client/tokenized-payment-request/frontend-utils.js b/client/tokenized-payment-request/frontend-utils.js index 4c7344fc888..8ea7f87556c 100644 --- a/client/tokenized-payment-request/frontend-utils.js +++ b/client/tokenized-payment-request/frontend-utils.js @@ -1,8 +1,4 @@ /* global wcpayPaymentRequestParams */ -/** - * External dependencies - */ -import { doingAction } from '@wordpress/hooks'; /** * Internal dependencies @@ -111,19 +107,3 @@ export const displayLoginConfirmationDialog = ( paymentRequestType ) => { )?.redirect_url; } }; - -/** - * Waiting for a specific WP action to finish completion. - * - * @param {string} hookName The name of the action to wait for. - * @return {Promise} Resolves when the action is completed. - */ -export const waitForAction = ( hookName ) => - new Promise( ( resolve ) => { - const interval = setInterval( () => { - if ( doingAction( hookName ) === false ) { - clearInterval( interval ); - resolve(); - } - }, 500 ); - } ); diff --git a/client/tokenized-payment-request/index.js b/client/tokenized-payment-request/index.js index ca2ec02904b..f1797b725e2 100644 --- a/client/tokenized-payment-request/index.js +++ b/client/tokenized-payment-request/index.js @@ -2,7 +2,7 @@ /** * External dependencies */ -import { doAction } from '@wordpress/hooks'; +import { applyFilters } from '@wordpress/hooks'; /** * Internal dependencies @@ -72,12 +72,19 @@ jQuery( ( $ ) => { wooPaymentsPaymentRequest.init(); // When the cart is updated, the PRB is removed from the page and needs to be re-initialized. - $( document.body ).on( 'updated_cart_totals', () => { + $( document.body ).on( 'updated_cart_totals', async () => { + await applyFilters( + 'wcpay.payment-request.update-button-data', + Promise.resolve() + ); wooPaymentsPaymentRequest.init(); } ); // We need to refresh payment request data when total is updated. - $( document.body ).on( 'updated_checkout', () => { - doAction( 'wcpay.payment-request.update-button-data' ); + $( document.body ).on( 'updated_checkout', async () => { + await applyFilters( + 'wcpay.payment-request.update-button-data', + Promise.resolve() + ); } ); } ); diff --git a/client/tokenized-payment-request/payment-request.js b/client/tokenized-payment-request/payment-request.js index 93381769892..0102095c648 100644 --- a/client/tokenized-payment-request/payment-request.js +++ b/client/tokenized-payment-request/payment-request.js @@ -5,9 +5,9 @@ import { __ } from '@wordpress/i18n'; import { doAction, - addAction, - removeAction, applyFilters, + removeFilter, + addFilter, } from '@wordpress/hooks'; /** @@ -32,7 +32,6 @@ import { getPaymentRequest, displayLoginConfirmationDialog, getPaymentRequestData, - waitForAction, } from './frontend-utils'; import PaymentRequestCartApi from './cart-api'; import debounce from './debounce'; @@ -150,14 +149,17 @@ export default class WooPaymentsPaymentRequest { this.attachPaymentRequestButtonEventListeners(); } - removeAction( + removeFilter( 'wcpay.payment-request.update-button-data', 'automattic/wcpay/payment-request' ); - addAction( + addFilter( 'wcpay.payment-request.update-button-data', 'automattic/wcpay/payment-request', - async () => { + async ( previousPromise ) => { + // Wait for previous filters + await previousPromise; + const newCartData = await _self.getCartData(); // checking if items needed shipping, before assigning new cart data. const didItemsNeedShipping = @@ -192,7 +194,7 @@ export default class WooPaymentsPaymentRequest { ), } ); } else { - _self.init().then( noop ); + await _self.init(); } } ); @@ -400,9 +402,9 @@ export default class WooPaymentsPaymentRequest { 'input', '.qty', debounce( 250, async () => { - doAction( 'wcpay.payment-request.update-button-data' ); - await waitForAction( - 'wcpay.payment-request.update-button-data' + await applyFilters( + 'wcpay.payment-request.update-button-data', + Promise.resolve() ); paymentRequestButtonUi.unblockButton(); } ) @@ -441,14 +443,14 @@ export default class WooPaymentsPaymentRequest { } } - this.startPaymentRequest().then( noop ); - - // After initializing a new payment request, we need to reset the isPaymentAborted flag. - this.isPaymentAborted = false; - // once cart data has been fetched, we can safely clear cached product data. if ( this.cachedCartData ) { this.initialProductData = undefined; } + + await this.startPaymentRequest(); + + // After initializing a new payment request, we need to reset the isPaymentAborted flag. + this.isPaymentAborted = false; } } diff --git a/client/tokenized-payment-request/test/payment-request.test.js b/client/tokenized-payment-request/test/payment-request.test.js index 3c697def3e0..fab94af0429 100644 --- a/client/tokenized-payment-request/test/payment-request.test.js +++ b/client/tokenized-payment-request/test/payment-request.test.js @@ -2,7 +2,7 @@ * External dependencies */ import apiFetch from '@wordpress/api-fetch'; -import { addAction, doAction, doingAction } from '@wordpress/hooks'; +import { addAction, applyFilters, doAction } from '@wordpress/hooks'; /** * Internal dependencies @@ -10,7 +10,6 @@ import { addAction, doAction, doingAction } from '@wordpress/hooks'; import PaymentRequestCartApi from '../cart-api'; import WooPaymentsPaymentRequest from '../payment-request'; import { trackPaymentRequestButtonLoad } from '../tracking'; -import { waitFor } from '@testing-library/react'; jest.mock( '@wordpress/api-fetch', () => jest.fn() ); jest.mock( '../tracking', () => ( { @@ -55,9 +54,6 @@ const jQueryMock = ( selector ) => { }; jQueryMock.blockUI = () => null; -const waitForAction = async ( hookName ) => - await waitFor( () => doingAction( hookName ) === false ); - describe( 'WooPaymentsPaymentRequest', () => { let wcpayApi; @@ -140,16 +136,19 @@ describe( 'WooPaymentsPaymentRequest', () => { ); expect( trackPaymentRequestButtonLoad ).toHaveBeenCalledWith( 'cart' ); - doAction( 'wcpay.payment-request.update-button-data' ); - - await waitForAction( 'wcpay.payment-request.update-button-data' ); + await applyFilters( + 'wcpay.payment-request.update-button-data', + Promise.resolve() + ); expect( paymentRequestAvailabilityCallback ).toHaveBeenCalledTimes( 1 ); // firing this should initialize the button again. doAction( 'payment-request-test.registered-action.cancel' ); - doAction( 'wcpay.payment-request.update-button-data' ); - await waitForAction( 'wcpay.payment-request.update-button-data' ); + await applyFilters( + 'wcpay.payment-request.update-button-data', + Promise.resolve() + ); expect( paymentRequestAvailabilityCallback ).toHaveBeenCalledTimes( 2 ); } ); } ); From 05e7d8629eefb351b15a57e9af609781e5fa21e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Costa?= <10233985+cesarcosta99@users.noreply.github.com> Date: Tue, 2 Jul 2024 23:11:57 -0300 Subject: [PATCH 10/31] Add Pay for Order support in ECE (#8994) --- .../add-8869-pay-for-order-support-in-ece | 4 + client/checkout/api/index.js | 15 ++ client/checkout/api/test/index.test.js | 22 ++- client/express-checkout/event-handlers.js | 28 ++-- client/express-checkout/index.js | 34 ++++- .../express-checkout/test/event-handlers.js | 134 ++++++++++++++++++ client/express-checkout/utils/normalize.js | 16 +++ .../express-checkout/utils/test/normalize.js | 57 ++++++++ ...payments-express-checkout-ajax-handler.php | 98 +++++++++++++ ...yments-express-checkout-button-handler.php | 62 +++++++- 10 files changed, 451 insertions(+), 19 deletions(-) create mode 100644 changelog/add-8869-pay-for-order-support-in-ece diff --git a/changelog/add-8869-pay-for-order-support-in-ece b/changelog/add-8869-pay-for-order-support-in-ece new file mode 100644 index 00000000000..ddf5cba6463 --- /dev/null +++ b/changelog/add-8869-pay-for-order-support-in-ece @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add Pay for Order support in Express Checkout Elements. diff --git a/client/checkout/api/index.js b/client/checkout/api/index.js index 3775aa7f394..a5fa5991fbe 100644 --- a/client/checkout/api/index.js +++ b/client/checkout/api/index.js @@ -439,6 +439,21 @@ export default class WCPayAPI { } ); } + /** + * Pays for an order based on the Express Checkout payment method. + * + * @param {integer} order The order ID. + * @param {Object} paymentData Order data. + * @return {Promise} Promise for the request to the server. + */ + expressCheckoutECEPayForOrder( order, paymentData ) { + return this.request( getExpressCheckoutAjaxURL( 'pay_for_order' ), { + _wpnonce: getExpressCheckoutConfig( 'nonce' )?.pay_for_order, + order, + ...paymentData, + } ); + } + initWooPay( userEmail, woopayUserSession ) { if ( ! this.isWooPayRequesting ) { this.isWooPayRequesting = true; diff --git a/client/checkout/api/test/index.test.js b/client/checkout/api/test/index.test.js index c725985fedd..93b135555dd 100644 --- a/client/checkout/api/test/index.test.js +++ b/client/checkout/api/test/index.test.js @@ -3,7 +3,11 @@ */ import WCPayAPI from '..'; import request from 'wcpay/checkout/utils/request'; -import { buildAjaxURL } from 'wcpay/utils/express-checkout'; +import { + buildAjaxURL, + getExpressCheckoutAjaxURL, + getExpressCheckoutConfig, +} from 'wcpay/utils/express-checkout'; import { getConfig } from 'wcpay/utils/checkout'; jest.mock( 'wcpay/checkout/utils/request', () => @@ -11,6 +15,8 @@ jest.mock( 'wcpay/checkout/utils/request', () => ); jest.mock( 'wcpay/utils/express-checkout', () => ( { buildAjaxURL: jest.fn(), + getExpressCheckoutAjaxURL: jest.fn(), + getExpressCheckoutConfig: jest.fn(), } ) ); jest.mock( 'wcpay/utils/checkout', () => ( { getConfig: jest.fn(), @@ -62,4 +68,18 @@ describe( 'WCPayAPI', () => { } ); expect( api.isWooPayRequesting ).toBe( false ); } ); + + test( 'express checkout pay for order is initialized correctly', async () => { + getExpressCheckoutAjaxURL.mockReturnValue( 'https://example.org/' ); + getExpressCheckoutConfig.mockReturnValue( { pay_for_order: '1234' } ); + + const api = new WCPayAPI( {}, request ); + await api.expressCheckoutECEPayForOrder( '12', { foo: 'bar' } ); + + expect( request ).toHaveBeenLastCalledWith( 'https://example.org/', { + _wpnonce: '1234', + order: '12', + foo: 'bar', + } ); + } ); } ); diff --git a/client/express-checkout/event-handlers.js b/client/express-checkout/event-handlers.js index ad8b23f52e4..21c1199bce3 100644 --- a/client/express-checkout/event-handlers.js +++ b/client/express-checkout/event-handlers.js @@ -4,6 +4,7 @@ import { getErrorMessageFromNotice, normalizeOrderData, + normalizePayForOrderData, normalizeShippingAddress, normalizeLineItems, getExpressCheckoutData, @@ -60,7 +61,8 @@ export const onConfirmHandler = async ( elements, completePayment, abortPayment, - event + event, + order = 0 // Order ID for the pay for order flow. ) => { const { error: submitError } = await elements.submit(); if ( submitError ) { @@ -76,25 +78,31 @@ export const onConfirmHandler = async ( } // Kick off checkout processing step. - const createOrderResponse = await api.expressCheckoutECECreateOrder( - normalizeOrderData( event, paymentMethod.id ) - ); + let orderResponse; + if ( ! order ) { + orderResponse = await api.expressCheckoutECECreateOrder( + normalizeOrderData( event, paymentMethod.id ) + ); + } else { + orderResponse = await api.expressCheckoutECEPayForOrder( + order, + normalizePayForOrderData( event, paymentMethod.id ) + ); + } - if ( createOrderResponse.result !== 'success' ) { + if ( orderResponse.result !== 'success' ) { return abortPayment( event, - getErrorMessageFromNotice( createOrderResponse.messages ) + getErrorMessageFromNotice( orderResponse.messages ) ); } try { - const confirmationRequest = api.confirmIntent( - createOrderResponse.redirect - ); + const confirmationRequest = api.confirmIntent( orderResponse.redirect ); // `true` means there is no intent to confirm. if ( confirmationRequest === true ) { - completePayment( createOrderResponse.redirect ); + completePayment( orderResponse.redirect ); } else { const redirectUrl = await confirmationRequest; diff --git a/client/express-checkout/index.js b/client/express-checkout/index.js index e994a81bd8e..94be0031230 100644 --- a/client/express-checkout/index.js +++ b/client/express-checkout/index.js @@ -1,4 +1,4 @@ -/* global jQuery, wcpayExpressCheckoutParams */ +/* global jQuery, wcpayExpressCheckoutParams, wcpayECEPayForOrderParams */ import { __ } from '@wordpress/i18n'; /** @@ -276,16 +276,19 @@ jQuery( ( $ ) => { shippingRateChangeHandler( api, event, elements ) ); - eceButton.on( 'confirm', async ( event ) => - onConfirmHandler( + eceButton.on( 'confirm', async ( event ) => { + const order = options.order ?? 0; + + return onConfirmHandler( api, api.getStripe(), elements, wcpayECE.completePayment, wcpayECE.abortPayment, - event - ) - ); + event, + order + ); + } ); eceButton.on( 'cancel', async () => { wcpayECE.unblock(); @@ -402,7 +405,24 @@ jQuery( ( $ ) => { return; } - wcpayECE.startExpressCheckoutElement(); + const { + total: { amount: total }, + displayItems, + order, + } = wcpayECEPayForOrderParams; + + wcpayECE.startExpressCheckoutElement( { + mode: 'payment', + total, + currency: getExpressCheckoutData( 'checkout' ) + ?.currency_code, + requestShipping: false, + requestPhone: + getExpressCheckoutData( 'checkout' ) + ?.needs_payer_phone ?? false, + displayItems, + order, + } ); } else if ( wcpayExpressCheckoutParams.is_product_page ) { wcpayECE.startExpressCheckoutElement( { mode: 'payment', diff --git a/client/express-checkout/test/event-handlers.js b/client/express-checkout/test/event-handlers.js index 338ca2a3da1..5458a4acbec 100644 --- a/client/express-checkout/test/event-handlers.js +++ b/client/express-checkout/test/event-handlers.js @@ -10,6 +10,7 @@ import { normalizeLineItems, normalizeShippingAddress, normalizeOrderData, + normalizePayForOrderData, } from '../utils'; describe( 'Express checkout event handlers', () => { @@ -213,10 +214,12 @@ describe( 'Express checkout event handlers', () => { let completePayment; let abortPayment; let event; + let order; beforeEach( () => { api = { expressCheckoutECECreateOrder: jest.fn(), + expressCheckoutECEPayForOrder: jest.fn(), confirmIntent: jest.fn(), }; stripe = { @@ -257,6 +260,7 @@ describe( 'Express checkout event handlers', () => { shippingRate: { id: 'rate_1' }, expressPaymentType: 'express', }; + order = 123; global.window.wcpayFraudPreventionToken = 'token123'; } ); @@ -433,5 +437,135 @@ describe( 'Express checkout event handlers', () => { ); expect( completePayment ).not.toHaveBeenCalled(); } ); + + test( 'should abort payment if expressCheckoutECEPayForOrder fails', async () => { + elements.submit.mockResolvedValue( {} ); + stripe.createPaymentMethod.mockResolvedValue( { + paymentMethod: { id: 'pm_123' }, + } ); + api.expressCheckoutECEPayForOrder.mockResolvedValue( { + result: 'error', + messages: 'Order creation error', + } ); + + await onConfirmHandler( + api, + stripe, + elements, + completePayment, + abortPayment, + event, + order + ); + + const expectedOrderData = normalizePayForOrderData( + event, + 'pm_123' + ); + expect( api.expressCheckoutECEPayForOrder ).toHaveBeenCalledWith( + 123, + expectedOrderData + ); + expect( abortPayment ).toHaveBeenCalledWith( + event, + 'Order creation error' + ); + expect( completePayment ).not.toHaveBeenCalled(); + } ); + + test( 'should complete payment (pay for order) if confirmationRequest is true', async () => { + elements.submit.mockResolvedValue( {} ); + stripe.createPaymentMethod.mockResolvedValue( { + paymentMethod: { id: 'pm_123' }, + } ); + api.expressCheckoutECEPayForOrder.mockResolvedValue( { + result: 'success', + redirect: 'https://example.com/redirect', + } ); + api.confirmIntent.mockReturnValue( true ); + + await onConfirmHandler( + api, + stripe, + elements, + completePayment, + abortPayment, + event, + order + ); + + expect( api.confirmIntent ).toHaveBeenCalledWith( + 'https://example.com/redirect' + ); + expect( completePayment ).toHaveBeenCalledWith( + 'https://example.com/redirect' + ); + expect( abortPayment ).not.toHaveBeenCalled(); + } ); + + test( 'should complete payment (pay for order) if confirmationRequest returns a redirect URL', async () => { + elements.submit.mockResolvedValue( {} ); + stripe.createPaymentMethod.mockResolvedValue( { + paymentMethod: { id: 'pm_123' }, + } ); + api.expressCheckoutECEPayForOrder.mockResolvedValue( { + result: 'success', + redirect: 'https://example.com/redirect', + } ); + api.confirmIntent.mockResolvedValue( + 'https://example.com/confirmation_redirect' + ); + + await onConfirmHandler( + api, + stripe, + elements, + completePayment, + abortPayment, + event, + order + ); + + expect( api.confirmIntent ).toHaveBeenCalledWith( + 'https://example.com/redirect' + ); + expect( completePayment ).toHaveBeenCalledWith( + 'https://example.com/confirmation_redirect' + ); + expect( abortPayment ).not.toHaveBeenCalled(); + } ); + + test( 'should abort payment (pay for order) if confirmIntent throws an error', async () => { + elements.submit.mockResolvedValue( {} ); + stripe.createPaymentMethod.mockResolvedValue( { + paymentMethod: { id: 'pm_123' }, + } ); + api.expressCheckoutECEPayForOrder.mockResolvedValue( { + result: 'success', + redirect: 'https://example.com/redirect', + } ); + api.confirmIntent.mockRejectedValue( + new Error( 'Intent confirmation error' ) + ); + + await onConfirmHandler( + api, + stripe, + elements, + completePayment, + abortPayment, + event, + order + ); + + expect( api.confirmIntent ).toHaveBeenCalledWith( + 'https://example.com/redirect' + ); + expect( abortPayment ).toHaveBeenCalledWith( + event, + 'Intent confirmation error' + ); + expect( completePayment ).not.toHaveBeenCalled(); + } ); } ); } ); diff --git a/client/express-checkout/utils/normalize.js b/client/express-checkout/utils/normalize.js index 576a2109ea3..e07cc89e450 100644 --- a/client/express-checkout/utils/normalize.js +++ b/client/express-checkout/utils/normalize.js @@ -74,6 +74,22 @@ export const normalizeOrderData = ( event, paymentMethodId ) => { }; }; +/** + * Normalize Pay for Order data from Stripe's object to the expected format for WC. + * + * @param {Object} event Stripe's event object. + * @param {string} paymentMethodId Stripe's payment method id. + * + * @return {Object} Order object in the format WooCommerce expects. + */ +export const normalizePayForOrderData = ( event, paymentMethodId ) => { + return { + payment_method: 'woocommerce_payments', + 'wcpay-payment-method': paymentMethodId, + express_payment_type: event?.expressPaymentType, + }; +}; + /** * Normalize shipping address information from Stripe's address object to * the cart shipping address object shape. diff --git a/client/express-checkout/utils/test/normalize.js b/client/express-checkout/utils/test/normalize.js index 6bd88b47b0b..96dca4c5b49 100644 --- a/client/express-checkout/utils/test/normalize.js +++ b/client/express-checkout/utils/test/normalize.js @@ -4,6 +4,7 @@ import { normalizeLineItems, normalizeOrderData, + normalizePayForOrderData, normalizeShippingAddress, } from '../normalize'; @@ -263,6 +264,62 @@ describe( 'Express checkout normalization', () => { } ); } ); + describe( 'normalizePayForOrderData', () => { + test( 'should normalize pay for order data with complete event and paymentMethodId', () => { + window.wcpayFraudPreventionToken = 'token123'; + + const event = { + billingDetails: { + name: 'John Doe', + email: 'john.doe@example.com', + address: { + organization: 'Some Company', + country: 'US', + line1: '123 Main St', + line2: 'Apt 4B', + city: 'New York', + state: 'NY', + postal_code: '10001', + }, + phone: '(123) 456-7890', + }, + shippingAddress: { + name: 'John Doe', + organization: 'Some Company', + address: { + country: 'US', + line1: '123 Main St', + line2: 'Apt 4B', + city: 'New York', + state: 'NY', + postal_code: '10001', + }, + }, + shippingRate: { id: 'rate_1' }, + expressPaymentType: 'express', + }; + + expect( normalizePayForOrderData( event, 'pm_123456' ) ).toEqual( { + payment_method: 'woocommerce_payments', + 'wcpay-payment-method': 'pm_123456', + express_payment_type: 'express', + } ); + } ); + + test( 'should normalize pay for order data with empty event and empty payment method', () => { + const event = {}; + const paymentMethodId = ''; + + expect( + normalizePayForOrderData( event, paymentMethodId ) + ).toEqual( { + payment_method: 'woocommerce_payments', + 'wcpay-payment-method': '', + express_payment_type: undefined, + } ); + } ); + } ); + describe( 'normalizeShippingAddress', () => { test( 'should normalize shipping address with all fields present', () => { const shippingAddress = { 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 9063ef39edf..94482930a62 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 @@ -36,6 +36,7 @@ public function __construct( WC_Payments_Express_Checkout_Button_Helper $express */ public function init() { add_action( 'wc_ajax_wcpay_create_order', [ $this, 'ajax_create_order' ] ); + add_action( 'wc_ajax_wcpay_pay_for_order', [ $this, 'ajax_pay_for_order' ] ); add_action( 'wc_ajax_wcpay_get_shipping_options', [ $this, 'ajax_get_shipping_options' ] ); add_action( 'wc_ajax_wcpay_get_cart_details', [ $this, 'ajax_get_cart_details' ] ); add_action( 'wc_ajax_wcpay_update_shipping_method', [ $this, 'ajax_update_shipping_method' ] ); @@ -67,6 +68,72 @@ public function ajax_create_order() { die( 0 ); } + /** + * Handles payment requests on the Pay for Order page. + * + * @throws Exception All exceptions are handled within the method. + */ + public function ajax_pay_for_order() { + check_ajax_referer( 'pay_for_order' ); + + if ( + ! isset( $_POST['payment_method'] ) || 'woocommerce_payments' !== $_POST['payment_method'] + || ! isset( $_POST['order'] ) || ! intval( $_POST['order'] ) + || ! isset( $_POST['wcpay-payment-method'] ) || empty( $_POST['wcpay-payment-method'] ) + ) { + // Incomplete request. + $response = [ + 'result' => 'error', + 'messages' => __( 'Invalid request', 'woocommerce-payments' ), + ]; + wp_send_json( $response, 400 ); + + return; + } + + try { + // Set up an environment, similar to core checkout. + wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true ); + wc_set_time_limit( 0 ); + + // Load the order. + $order_id = intval( $_POST['order'] ); + $order = wc_get_order( $order_id ); + + if ( ! is_a( $order, WC_Order::class ) ) { + throw new Exception( __( 'Invalid order!', 'woocommerce-payments' ) ); + } + + if ( ! $order->needs_payment() ) { + throw new Exception( __( 'This order does not require payment!', 'woocommerce-payments' ) ); + } + + $this->add_order_meta( $order_id ); + + // Load the gateway. + $all_gateways = WC()->payment_gateways->get_available_payment_gateways(); + $gateway = $all_gateways['woocommerce_payments']; + $result = $gateway->process_payment( $order_id ); + + // process_payment() should only return `success` or throw an exception. + if ( ! is_array( $result ) || ! isset( $result['result'] ) || 'success' !== $result['result'] || ! isset( $result['redirect'] ) ) { + throw new Exception( __( 'Unable to determine payment success.', 'woocommerce-payments' ) ); + } + + // Include the order ID in the result. + $result['order_id'] = $order_id; + + $result = apply_filters( 'woocommerce_payment_successful_result', $result, $order_id ); + } catch ( Exception $e ) { + $result = [ + 'result' => 'error', + 'messages' => $e->getMessage(), + ]; + } + + wp_send_json( $result ); + } + /** * Get shipping options. * @@ -257,4 +324,35 @@ public function ajax_empty_cart() { wp_send_json( [ 'result' => 'success' ] ); } + + /** + * Add needed order meta + * + * @param integer $order_id The order ID. + * + * @return void + */ + public function add_order_meta( $order_id ) { + if ( empty( $_POST['express_payment_type'] ) || ! isset( $_POST['payment_method'] ) || 'woocommerce_payments' !== $_POST['payment_method'] ) { // phpcs:ignore WordPress.Security.NonceVerification + return; + } + + $order = wc_get_order( $order_id ); + + $express_payment_type = wc_clean( wp_unslash( $_POST['express_payment_type'] ) ); // phpcs:ignore WordPress.Security.NonceVerification + + $express_payment_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( $express_payment_titles[ $express_payment_type ] ) ? $express_payment_titles[ $express_payment_type ] : 'Express Payment'; + $order->set_payment_method_title( $payment_method_title . $suffix ); + $order->save(); + } } diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php index 1868fd3dcaa..31a75ffbe0c 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php @@ -88,6 +88,7 @@ public function init() { } add_action( 'wp_enqueue_scripts', [ $this, 'scripts' ] ); + add_action( 'before_woocommerce_pay_form', [ $this, 'display_pay_for_order_page_html' ], 1 ); $this->express_checkout_ajax_handler->init(); } @@ -189,4 +190,63 @@ public function display_express_checkout_button_html() {
get_items() as $item ) { + if ( method_exists( $item, 'get_total' ) ) { + $items[] = [ + 'label' => $item->get_name(), + 'amount' => WC_Payments_Utils::prepare_amount( $item->get_total(), $currency ), + ]; + } + } + + if ( $order->get_total_tax() ) { + $items[] = [ + 'label' => __( 'Tax', 'woocommerce-payments' ), + 'amount' => WC_Payments_Utils::prepare_amount( $order->get_total_tax(), $currency ), + ]; + } + + if ( $order->get_shipping_total() ) { + $shipping_label = sprintf( + // Translators: %s is the name of the shipping method. + __( 'Shipping (%s)', 'woocommerce-payments' ), + $order->get_shipping_method() + ); + + $items[] = [ + 'label' => $shipping_label, + 'amount' => WC_Payments_Utils::prepare_amount( $order->get_shipping_total(), $currency ), + ]; + } + + foreach ( $order->get_fees() as $fee ) { + $items[] = [ + 'label' => $fee->get_name(), + 'amount' => WC_Payments_Utils::prepare_amount( $fee->get_amount(), $currency ), + ]; + } + + $data['order'] = $order->get_id(); + $data['displayItems'] = $items; + $data['needs_shipping'] = false; // This should be already entered/prepared. + $data['total'] = [ + 'label' => apply_filters( 'wcpay_payment_request_total_label', $this->express_checkout_helper->get_total_label() ), + 'amount' => WC_Payments_Utils::prepare_amount( $order->get_total(), $currency ), + 'pending' => true, + ]; + + wp_localize_script( 'WCPAY_EXPRESS_CHECKOUT_ECE', 'wcpayECEPayForOrderParams', $data ); + } +} From 3767a171bef4833b3fd577dbc2860737c62bb8dc Mon Sep 17 00:00:00 2001 From: Valery Sukhomlinov <683297+dmvrtx@users.noreply.github.com> Date: Wed, 3 Jul 2024 10:00:34 +0200 Subject: [PATCH 11/31] Remove printed receipts mentions from WooPayments - those are hanled by WooCommerce now. (#9031) --- .../change-9025-no-printed-receipts-in-wcpay | 4 + client/card-readers/preview-receipt/index.tsx | 27 ---- .../preview-receipt/previewer.tsx | 61 -------- .../card-readers/preview-receipt/receipt.tsx | 104 ------------- .../card-readers/preview-receipt/style.scss | 11 -- .../preview-receipt.test.tsx.snap | 142 ------------------ .../printed-receipt-previewer.test.tsx.snap | 11 -- .../test/preview-receipt.test.tsx | 123 --------------- .../test/printed-receipt-previewer.test.tsx | 30 ---- client/card-readers/settings/index.tsx | 12 +- .../test/__snapshots__/index.test.tsx.snap | 8 +- client/index.js | 10 -- includes/admin/class-wc-payments-admin.php | 10 -- 13 files changed, 6 insertions(+), 547 deletions(-) create mode 100644 changelog/change-9025-no-printed-receipts-in-wcpay delete mode 100644 client/card-readers/preview-receipt/index.tsx delete mode 100644 client/card-readers/preview-receipt/previewer.tsx delete mode 100644 client/card-readers/preview-receipt/receipt.tsx delete mode 100644 client/card-readers/preview-receipt/style.scss delete mode 100644 client/card-readers/preview-receipt/test/__snapshots__/preview-receipt.test.tsx.snap delete mode 100644 client/card-readers/preview-receipt/test/__snapshots__/printed-receipt-previewer.test.tsx.snap delete mode 100644 client/card-readers/preview-receipt/test/preview-receipt.test.tsx delete mode 100644 client/card-readers/preview-receipt/test/printed-receipt-previewer.test.tsx diff --git a/changelog/change-9025-no-printed-receipts-in-wcpay b/changelog/change-9025-no-printed-receipts-in-wcpay new file mode 100644 index 00000000000..6829af078d5 --- /dev/null +++ b/changelog/change-9025-no-printed-receipts-in-wcpay @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update payment receipt settings to remove mention of the printed receipts. diff --git a/client/card-readers/preview-receipt/index.tsx b/client/card-readers/preview-receipt/index.tsx deleted file mode 100644 index 481d79807f5..00000000000 --- a/client/card-readers/preview-receipt/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { Card, CardBody } from '@wordpress/components'; -import Page from 'components/page'; -/** - * Internal dependencies. - */ -import PreviewReceipt from './receipt'; -import ErrorBoundary from 'components/error-boundary'; - -export const PreviewPrintReceipt = (): JSX.Element => { - return ( - - - - - - - - - - ); -}; - -export default PreviewPrintReceipt; diff --git a/client/card-readers/preview-receipt/previewer.tsx b/client/card-readers/preview-receipt/previewer.tsx deleted file mode 100644 index a92aa7a3214..00000000000 --- a/client/card-readers/preview-receipt/previewer.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/** - * External dependencies - */ -import { useState } from '@wordpress/element'; -import React from 'react'; -import { createPortal } from 'react-dom'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import './style.scss'; - -interface PrintedReceiptPreviewerProps { - receiptHtml: string; -} - -interface IFrameComponentProps { - title: string; - children: React.ReactNode; -} - -const IFrameComponent = ( { title, children }: IFrameComponentProps ) => { - const [ iframeBody, setIframeBody ] = useState< HTMLElement | null >( - null - ); - const handleLoad = ( event: React.SyntheticEvent ) => { - const iframe = event.target as HTMLIFrameElement; - if ( iframe?.contentDocument ) { - setIframeBody( iframe.contentDocument.body ); - } - }; - - return ( - - ); -}; - -const PrintedReceiptPreviewer = ( { - receiptHtml, -}: PrintedReceiptPreviewerProps ): JSX.Element => { - return ( - - { - // eslint-disable-next-line react/no-danger -
- } - - ); -}; - -export default PrintedReceiptPreviewer; diff --git a/client/card-readers/preview-receipt/receipt.tsx b/client/card-readers/preview-receipt/receipt.tsx deleted file mode 100644 index df4383316b5..00000000000 --- a/client/card-readers/preview-receipt/receipt.tsx +++ /dev/null @@ -1,104 +0,0 @@ -/** - * External dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { useState, useEffect } from '@wordpress/element'; -import { Notice } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import React from 'react'; -/** - * Internal dependencies - */ -import { LoadableBlock } from 'components/loadable'; -import PrintedReceiptPreviewer from 'wcpay/card-readers/preview-receipt/previewer'; -import { - useAccountBusinessSupportAddress, - useAccountBusinessName, - useAccountBusinessURL, - useAccountBusinessSupportEmail, - useAccountBusinessSupportPhone, -} from '../../data'; -import { FetchReceiptPayload } from 'wcpay/types/card-readers'; - -interface PreviewReceiptHtmlContent { - html_content: string; -} - -async function fetchReceiptHtml( - payload: FetchReceiptPayload -): Promise< PreviewReceiptHtmlContent > { - const path = '/wc/v3/payments/readers/receipts/preview'; - return apiFetch( { path, data: payload, method: 'post' } ); -} - -const PreviewReceipt = (): JSX.Element => { - const [ receiptHtml, setReceiptHtml ] = useState< string >( '' ); - const [ isLoading, setIsLoading ] = useState< boolean >( true ); - const [ isErrorFetchingReceipt, setIsErrorFetchingReceipt ] = useState< - boolean - >( false ); - - const [ - accountBusinessSupportAddress, - ] = useAccountBusinessSupportAddress(); - const [ accountBusinessName ] = useAccountBusinessName(); - const [ accountBusinessURL ] = useAccountBusinessURL(); - const [ accountBusinessSupportEmail ] = useAccountBusinessSupportEmail(); - const [ accountBusinessSupportPhone ] = useAccountBusinessSupportPhone(); - - useEffect( () => { - let didCancel = false; - async function fetchReceiptHtmlAPI() { - try { - const data = await fetchReceiptHtml( { - accountBusinessName, - accountBusinessSupportAddress, - accountBusinessURL, - accountBusinessSupportEmail, - accountBusinessSupportPhone, - } as FetchReceiptPayload ); - - if ( ! didCancel && data ) { - setIsLoading( false ); - setReceiptHtml( data.html_content ); - } - } catch ( error ) { - setIsLoading( false ); - setIsErrorFetchingReceipt( true ); - } - } - fetchReceiptHtmlAPI(); - return () => { - didCancel = true; - }; - }, [ - accountBusinessName, - accountBusinessSupportAddress, - accountBusinessSupportEmail, - accountBusinessSupportPhone, - accountBusinessURL, - ] ); - - return ( - <> - { isLoading && ( -

{ __( 'Generating preview.', 'woocommerce-payments' ) }

- ) } - - { isErrorFetchingReceipt && ( - - { __( - 'There was a problem generating the receipt preview. Please try again later.', - 'woocommerce-payments' - ) } - - ) } - { ! isErrorFetchingReceipt && ( - - ) } - - - ); -}; - -export default PreviewReceipt; diff --git a/client/card-readers/preview-receipt/style.scss b/client/card-readers/preview-receipt/style.scss deleted file mode 100644 index b9c0881fcf7..00000000000 --- a/client/card-readers/preview-receipt/style.scss +++ /dev/null @@ -1,11 +0,0 @@ -.wcpay-card-readers-preview-receipt-page { - min-width: 340px; - - .card-readers-preview-receipt__preview { - display: block; - height: 454px; - margin: auto; - box-shadow: 0 10px 5px -10px rgba( 0, 0, 0, 0.2 ); - border: 1px solid rgba( 0, 0, 0, 0.1 ); - } -} diff --git a/client/card-readers/preview-receipt/test/__snapshots__/preview-receipt.test.tsx.snap b/client/card-readers/preview-receipt/test/__snapshots__/preview-receipt.test.tsx.snap deleted file mode 100644 index 3a05b1dd3cb..00000000000 --- a/client/card-readers/preview-receipt/test/__snapshots__/preview-receipt.test.tsx.snap +++ /dev/null @@ -1,142 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PreviewReceipt should render error when fetch from API fails 1`] = ` -
-
-
-
-
-
-
- There was a problem generating the receipt preview. Please try again later. -
-
-
-
-
- -
-`; - -exports[`PreviewReceipt should render loading block while fetching from API 1`] = ` -
-
-
-
-
-

- Generating preview. -

- -

- Block placeholder -

-
-
-
- -
-`; - -exports[`PreviewReceipt should render preview when fetch from API succeeds 1`] = ` -
-
-
-
-
-