Commit a71953e3e4 for woocommerce
commit a71953e3e4459407ab5e930ae35df0ac549c82d0
Author: Ahmed <ahmed.el.azzabi@automattic.com>
Date: Mon Nov 17 22:21:06 2025 +0100
Redirect &task=payments and &task=woocommerce-payments to the Payments Settings page (#61715)
* Redirect to checkout settings
* Revert "Redirect to checkout settings"
This reverts commit e3e3f2084482d6797092e37ec21e6e236cd71616.
* add redirect to settings
* Add changefile(s) from automation for the following project(s): woocommerce
* Adjust payments task redirect logic
* More redirect adjustments
* Add user cap check
* Lint fix
* Add unit tests
* Fix linting errors
* set current screen in tearDown
* Another attempt to fix tests
* test: A new attempt at fixing them
---------
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Cvetan Cvetanov <cvetan.cvetanov@automattic.com>
Co-authored-by: Vlad Olaru <vlad.olaru@automattic.com>
Co-authored-by: Vlad Olaru <vlad@pixelgrade.com>
diff --git a/plugins/woocommerce/changelog/61715-fix-redirect-for-mobile b/plugins/woocommerce/changelog/61715-fix-redirect-for-mobile
new file mode 100644
index 0000000000..08fa4691e2
--- /dev/null
+++ b/plugins/woocommerce/changelog/61715-fix-redirect-for-mobile
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Redirect payments-related tasks to the Payments Settings page.
\ No newline at end of file
diff --git a/plugins/woocommerce/src/Admin/PageController.php b/plugins/woocommerce/src/Admin/PageController.php
index 0fdea6ce10..6c27180465 100644
--- a/plugins/woocommerce/src/Admin/PageController.php
+++ b/plugins/woocommerce/src/Admin/PageController.php
@@ -70,6 +70,8 @@ class PageController {
// priority is 20 to run after https://github.com/woocommerce/woocommerce/blob/a55ae325306fc2179149ba9b97e66f32f84fdd9c/includes/admin/class-wc-admin-menus.php#L165.
add_action( 'admin_head', array( $this, 'remove_app_entry_page_menu_item' ), 20 );
+ // Using low priority to run before other hooks.
+ add_action( 'admin_init', array( $this, 'maybe_redirect_payment_tasks_to_settings' ), 1 );
}
/**
@@ -612,4 +614,76 @@ class PageController {
public static function is_modern_settings_page() {
return self::is_settings_page() && Features::is_enabled( 'settings' );
}
+
+ /**
+ * Redirect payment tasks to the settings page.
+ *
+ * Redirects both 'payments' and 'woocommerce-payments' tasks to the Payments settings page,
+ * when it is safe to do so in terms of backwards compatibility.
+ */
+ public function maybe_redirect_payment_tasks_to_settings() {
+ // Bail if we are not in the WP admin or not on a WC admin page.
+ if ( ! is_admin() || ! self::is_admin_page() ) {
+ return;
+ }
+
+ // Bail if we are not requesting a page for a WooCommerce task.
+ // phpcs:ignore WordPress.Security.NonceVerification
+ if ( empty( $_GET['task'] ) ) {
+ return;
+ }
+
+ // Only sufficiently capable users should be redirected.
+ if ( ! current_user_can( 'manage_woocommerce' ) ) {
+ return;
+ }
+
+ // Get the current task ID.
+ // phpcs:ignore WordPress.Security.NonceVerification
+ $task_id = wc_clean( wp_unslash( $_GET['task'] ) );
+
+ // Bail if the task is not a payments task.
+ if ( ! in_array( $task_id, array( 'payments', 'woocommerce-payments' ), true ) ) {
+ return;
+ }
+
+ $redirect_url = admin_url( 'admin.php?page=wc-settings&tab=checkout&from=WCADMIN_PAYMENT_TASK' );
+
+ // The WooPayments task is always redirected to the settings page.
+ if ( 'woocommerce-payments' === $task_id ) {
+ wp_safe_redirect( $redirect_url );
+ exit;
+ }
+
+ // The generic payments task is only redirected if the request is a regular user request,
+ // not part of an onboarding flow or other special case.
+ $special_request_params = array(
+ // This is used by the legacy, Payments task-based suggestions onboarding flow.
+ // Nobody should be using this anymore, but just in case.
+ 'connection-return',
+ // This is used by the legacy, Payments task-based suggestions onboarding flow.
+ // Nobody should be using this anymore, but just in case.
+ 'id',
+ // Some params for gateway IDs, just in case.
+ 'gateway_id',
+ 'gateway-id',
+ // Sometimes the gateway is referred to as 'method'. Stay clear of it.
+ 'method',
+ // If there is a success or error param, better not redirect.
+ 'success',
+ 'error',
+ // If the URL is nonced, better not redirect.
+ '_wpnonce',
+ );
+ foreach ( $special_request_params as $param ) {
+ // phpcs:ignore WordPress.Security.NonceVerification
+ if ( isset( $_GET[ $param ] ) ) {
+ return;
+ }
+ }
+
+ // If we reach this point, we can safely redirect to the settings page.
+ wp_safe_redirect( $redirect_url );
+ exit;
+ }
}
diff --git a/plugins/woocommerce/tests/php/src/Admin/PageControllerTest.php b/plugins/woocommerce/tests/php/src/Admin/PageControllerTest.php
new file mode 100644
index 0000000000..c422c94d78
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Admin/PageControllerTest.php
@@ -0,0 +1,549 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Tests\Admin;
+
+use Automattic\WooCommerce\Admin\PageController;
+use WC_Unit_Test_Case;
+
+/**
+ * Unit tests for PageController redirect functionality.
+ *
+ * @covers \Automattic\WooCommerce\Admin\PageController
+ */
+class PageControllerTest extends WC_Unit_Test_Case {
+ /**
+ * PageController instance.
+ *
+ * @var PageController
+ */
+ private $sut;
+
+ /**
+ * Admin user ID.
+ *
+ * @var int
+ */
+ private $admin_user_id;
+
+ /**
+ * Shop manager user ID.
+ *
+ * @var int
+ */
+ private $shop_manager_user_id;
+
+ /**
+ * Customer user ID.
+ *
+ * @var int
+ */
+ private $customer_user_id;
+
+ /**
+ * Backup object of $GLOBALS['current_screen'].
+ *
+ * @var object
+ */
+ private $current_screen_backup;
+
+ /**
+ * Holds the URL of the last attempted redirect.
+ *
+ * @var string
+ */
+ private $redirected_to = '';
+
+ /**
+ * Set things up before each test case.
+ *
+ * @return void
+ */
+ public function setUp(): void {
+ // Mock screen.
+ $this->current_screen_backup = $GLOBALS['current_screen'] ?? null;
+ $GLOBALS['current_screen'] = $this->get_screen_mock(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+ if ( ! did_action( 'current_screen' ) ) {
+ do_action( 'current_screen', $GLOBALS['current_screen'] ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+ }
+
+ parent::setUp();
+
+ // Create test users with different capabilities.
+ $this->admin_user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
+ $this->shop_manager_user_id = $this->factory->user->create( array( 'role' => 'shop_manager' ) );
+ $this->customer_user_id = $this->factory->user->create( array( 'role' => 'customer' ) );
+
+ $this->sut = PageController::get_instance();
+
+ // Start watching for redirects.
+ $this->redirected_to = '';
+ add_filter( 'wp_redirect', array( $this, 'watch_and_anull_redirects' ) );
+ }
+
+ /**
+ * Tear down after each test case.
+ *
+ * @return void
+ */
+ public function tearDown(): void {
+ // Remove redirect listener.
+ remove_filter( 'wp_redirect', array( $this, 'watch_and_anull_redirects' ) );
+
+ // Clean up users.
+ wp_delete_user( $this->admin_user_id );
+ wp_delete_user( $this->shop_manager_user_id );
+ wp_delete_user( $this->customer_user_id );
+
+ // Reset global state.
+ unset( $_GET['page'], $_GET['task'], $_GET['connection-return'] );
+
+ // Restore screen backup.
+ if ( $this->current_screen_backup ) {
+ $GLOBALS['current_screen'] = $this->current_screen_backup; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+ }
+
+ parent::tearDown();
+ }
+
+ /**
+ * Captures the attempted redirect location, and stops the redirect from taking place.
+ *
+ * @param string $url Redirect location.
+ *
+ * @throws \WPAjaxDieContinueException To prevent exit() from being called after redirect.
+ * @return void
+ */
+ public function watch_and_anull_redirects( string $url ) {
+ $this->redirected_to = $url;
+ // Throw exception to prevent exit() from being called after wp_safe_redirect().
+ throw new \WPAjaxDieContinueException();
+ }
+
+ /**
+ * Supplies the URL of the last attempted redirect, then resets ready for the next test.
+ *
+ * @return string
+ */
+ private function get_redirect_attempt(): string {
+ $return = $this->redirected_to;
+ $this->redirected_to = '';
+ return $return;
+ }
+
+ /**
+ * Trigger the redirect method and catch the exception to prevent exit().
+ * Temporarily defines WP_ADMIN for this specific call only.
+ *
+ * @return void
+ */
+ private function trigger_redirect_check(): void {
+ try {
+ $this->sut->maybe_redirect_payment_tasks_to_settings();
+ } catch ( \WPAjaxDieContinueException $e ) {
+ // Expected - this prevents exit() from killing the test.
+ unset( $e );
+ }
+ }
+
+ /**
+ * Test redirect happens for basic task=payments request.
+ */
+ public function test_redirect_for_payments_task(): void {
+ // Set up admin user.
+ wp_set_current_user( $this->admin_user_id );
+
+ // Set up request.
+ $_GET['page'] = 'wc-admin';
+ $_GET['task'] = 'payments';
+
+ // Trigger redirect.
+ $this->trigger_redirect_check();
+
+ // Verify redirect occurred.
+ $redirect_url = $this->get_redirect_attempt();
+ $this->assertNotEmpty( $redirect_url, 'A redirect should occur for the payments task.' );
+ $this->assertEquals(
+ admin_url( 'admin.php?page=wc-settings&tab=checkout&from=WCADMIN_PAYMENT_TASK' ),
+ $redirect_url,
+ 'Redirect URL should match expected settings page URL.'
+ );
+ }
+
+ /**
+ * Test redirect happens for task=woocommerce-payments request.
+ */
+ public function test_redirect_for_woocommerce_payments_task(): void {
+ // Set up admin user.
+ wp_set_current_user( $this->admin_user_id );
+
+ // Set up request.
+ $_GET['page'] = 'wc-admin';
+ $_GET['task'] = 'woocommerce-payments';
+
+ // Trigger redirect.
+ $this->trigger_redirect_check();
+
+ // Verify redirect occurred.
+ $redirect_url = $this->get_redirect_attempt();
+ $this->assertNotEmpty( $redirect_url, 'A redirect should occur for the woocommerce-payments task.' );
+ $this->assertEquals(
+ admin_url( 'admin.php?page=wc-settings&tab=checkout&from=WCADMIN_PAYMENT_TASK' ),
+ $redirect_url,
+ 'Redirect URL should match expected settings page URL.'
+ );
+ }
+
+ /**
+ * Test no redirect when connection-return parameter is present.
+ */
+ public function test_no_redirect_with_connection_return_param(): void {
+ // Set up admin user.
+ wp_set_current_user( $this->admin_user_id );
+
+ // Set up request with connection-return parameter.
+ $_GET['page'] = 'wc-admin';
+ $_GET['task'] = 'payments';
+ $_GET['connection-return'] = '1';
+
+ // Trigger redirect check.
+ $this->trigger_redirect_check();
+
+ // Verify no redirect occurred.
+ $this->assertEmpty(
+ $this->get_redirect_attempt(),
+ 'No redirect should occur when connection-return parameter is present.'
+ );
+ }
+
+ /**
+ * Test no redirect when id parameter is present.
+ */
+ public function test_no_redirect_with_id_param(): void {
+ // Set up admin user.
+ wp_set_current_user( $this->admin_user_id );
+
+ // Set up request with id parameter.
+ $_GET['page'] = 'wc-admin';
+ $_GET['task'] = 'payments';
+ $_GET['id'] = 'some-gateway';
+
+ // Trigger redirect check.
+ $this->trigger_redirect_check();
+
+ // Verify no redirect occurred.
+ $this->assertEmpty(
+ $this->get_redirect_attempt(),
+ 'No redirect should occur when id parameter is present.'
+ );
+ }
+
+ /**
+ * Test no redirect when gateway_id parameter is present.
+ */
+ public function test_no_redirect_with_gateway_id_param(): void {
+ // Set up admin user.
+ wp_set_current_user( $this->admin_user_id );
+
+ // Set up request with gateway_id parameter.
+ $_GET['page'] = 'wc-admin';
+ $_GET['task'] = 'payments';
+ $_GET['gateway_id'] = 'stripe';
+
+ // Trigger redirect check.
+ $this->trigger_redirect_check();
+
+ // Verify no redirect occurred.
+ $this->assertEmpty(
+ $this->get_redirect_attempt(),
+ 'No redirect should occur when gateway_id parameter is present.'
+ );
+ }
+
+ /**
+ * Test no redirect when gateway-id parameter is present.
+ */
+ public function test_no_redirect_with_gateway_hyphen_id_param(): void {
+ // Set up admin user.
+ wp_set_current_user( $this->admin_user_id );
+
+ // Set up request with gateway-id parameter.
+ $_GET['page'] = 'wc-admin';
+ $_GET['task'] = 'payments';
+ $_GET['gateway-id'] = 'stripe';
+
+ // Trigger redirect check.
+ $this->trigger_redirect_check();
+
+ // Verify no redirect occurred.
+ $this->assertEmpty(
+ $this->get_redirect_attempt(),
+ 'No redirect should occur when gateway-id parameter is present.'
+ );
+ }
+
+ /**
+ * Test no redirect when method parameter is present.
+ */
+ public function test_no_redirect_with_method_param(): void {
+ // Set up admin user.
+ wp_set_current_user( $this->admin_user_id );
+
+ // Set up request with method parameter.
+ $_GET['page'] = 'wc-admin';
+ $_GET['task'] = 'payments';
+ $_GET['method'] = 'card';
+
+ // Trigger redirect check.
+ $this->trigger_redirect_check();
+
+ // Verify no redirect occurred.
+ $this->assertEmpty(
+ $this->get_redirect_attempt(),
+ 'No redirect should occur when method parameter is present.'
+ );
+ }
+
+ /**
+ * Test no redirect when success parameter is present.
+ */
+ public function test_no_redirect_with_success_param(): void {
+ // Set up admin user.
+ wp_set_current_user( $this->admin_user_id );
+
+ // Set up request with success parameter.
+ $_GET['page'] = 'wc-admin';
+ $_GET['task'] = 'payments';
+ $_GET['success'] = '1';
+
+ // Trigger redirect check.
+ $this->trigger_redirect_check();
+
+ // Verify no redirect occurred.
+ $this->assertEmpty(
+ $this->get_redirect_attempt(),
+ 'No redirect should occur when success parameter is present.'
+ );
+ }
+
+ /**
+ * Test no redirect when error parameter is present.
+ */
+ public function test_no_redirect_with_error_param(): void {
+ // Set up admin user.
+ wp_set_current_user( $this->admin_user_id );
+
+ // Set up request with error parameter.
+ $_GET['page'] = 'wc-admin';
+ $_GET['task'] = 'payments';
+ $_GET['error'] = 'some-error';
+
+ // Trigger redirect check.
+ $this->trigger_redirect_check();
+
+ // Verify no redirect occurred.
+ $this->assertEmpty(
+ $this->get_redirect_attempt(),
+ 'No redirect should occur when error parameter is present.'
+ );
+ }
+
+ /**
+ * Test no redirect when _wpnonce parameter is present.
+ */
+ public function test_no_redirect_with_wpnonce_param(): void {
+ // Set up admin user.
+ wp_set_current_user( $this->admin_user_id );
+
+ // Set up request with _wpnonce parameter.
+ $_GET['page'] = 'wc-admin';
+ $_GET['task'] = 'payments';
+ $_GET['_wpnonce'] = wp_create_nonce( 'test-action' );
+
+ // Trigger redirect check.
+ $this->trigger_redirect_check();
+
+ // Verify no redirect occurred.
+ $this->assertEmpty(
+ $this->get_redirect_attempt(),
+ 'No redirect should occur when _wpnonce parameter is present.'
+ );
+ }
+
+ /**
+ * Test no redirect for users without manage_woocommerce capability.
+ */
+ public function test_no_redirect_without_manage_woocommerce_capability(): void {
+ // Set up customer user (no manage_woocommerce capability).
+ wp_set_current_user( $this->customer_user_id );
+
+ // Set up request.
+ $_GET['page'] = 'wc-admin';
+ $_GET['task'] = 'payments';
+
+ // Trigger redirect check.
+ $this->trigger_redirect_check();
+
+ // Verify no redirect occurred.
+ $this->assertEmpty(
+ $this->get_redirect_attempt(),
+ 'No redirect should occur for users without manage_woocommerce capability.'
+ );
+ }
+
+ /**
+ * Test redirect works for shop_manager role.
+ */
+ public function test_redirect_works_for_shop_manager(): void {
+ // Set up shop manager user.
+ wp_set_current_user( $this->shop_manager_user_id );
+
+ // Set up request.
+ $_GET['page'] = 'wc-admin';
+ $_GET['task'] = 'payments';
+
+ // Trigger redirect.
+ $this->trigger_redirect_check();
+
+ // Verify redirect occurred.
+ $redirect_url = $this->get_redirect_attempt();
+ $this->assertNotEmpty( $redirect_url, 'A redirect should occur for shop_manager users.' );
+ $this->assertEquals(
+ admin_url( 'admin.php?page=wc-settings&tab=checkout&from=WCADMIN_PAYMENT_TASK' ),
+ $redirect_url,
+ 'Redirect URL should match expected settings page URL for shop_manager.'
+ );
+ }
+
+ /**
+ * Test no redirect when not on wc-admin page.
+ */
+ public function test_no_redirect_when_not_on_wc_admin_page(): void {
+ // Set up admin user.
+ wp_set_current_user( $this->admin_user_id );
+
+ // Set up request without wc-admin page.
+ $_GET['page'] = 'wc-settings';
+ $_GET['task'] = 'payments';
+
+ // Trigger redirect check.
+ $this->trigger_redirect_check();
+
+ // Verify no redirect occurred.
+ $this->assertEmpty(
+ $this->get_redirect_attempt(),
+ 'No redirect should occur when not on wc-admin page.'
+ );
+ }
+
+ /**
+ * Test no redirect when task parameter is missing.
+ */
+ public function test_no_redirect_when_task_param_missing(): void {
+ // Set up admin user.
+ wp_set_current_user( $this->admin_user_id );
+
+ // Set up request without task parameter.
+ $_GET['page'] = 'wc-admin';
+
+ // Trigger redirect check.
+ $this->trigger_redirect_check();
+
+ // Verify no redirect occurred.
+ $this->assertEmpty(
+ $this->get_redirect_attempt(),
+ 'No redirect should occur when task parameter is missing.'
+ );
+ }
+
+ /**
+ * Test no redirect for non-payment tasks.
+ */
+ public function test_no_redirect_for_non_payment_tasks(): void {
+ // Set up admin user.
+ wp_set_current_user( $this->admin_user_id );
+
+ // Set up request with different task.
+ $_GET['page'] = 'wc-admin';
+ $_GET['task'] = 'products';
+
+ // Trigger redirect check.
+ $this->trigger_redirect_check();
+
+ // Verify no redirect occurred.
+ $this->assertEmpty(
+ $this->get_redirect_attempt(),
+ 'No redirect should occur for non-payment tasks.'
+ );
+ }
+
+ /**
+ * Test woocommerce-payments task redirects even with special parameters.
+ *
+ * The woocommerce-payments task should always redirect, unlike the generic payments task.
+ */
+ public function test_woocommerce_payments_redirects_with_special_params(): void {
+ // Set up admin user.
+ wp_set_current_user( $this->admin_user_id );
+
+ // Set up request with special parameters.
+ $_GET['page'] = 'wc-admin';
+ $_GET['task'] = 'woocommerce-payments';
+ $_GET['connection-return'] = '1';
+
+ // Trigger redirect.
+ $this->trigger_redirect_check();
+
+ // Verify redirect occurred even with special params.
+ $redirect_url = $this->get_redirect_attempt();
+ $this->assertNotEmpty( $redirect_url, 'woocommerce-payments task should redirect even with special parameters.' );
+ $this->assertEquals(
+ admin_url( 'admin.php?page=wc-settings&tab=checkout&from=WCADMIN_PAYMENT_TASK' ),
+ $redirect_url,
+ 'Redirect URL should match expected settings page URL for woocommerce-payments task.'
+ );
+ }
+
+ /**
+ * Test redirect URL contains expected parameters.
+ */
+ public function test_redirect_url_contains_expected_parameters(): void {
+ // Set up admin user.
+ wp_set_current_user( $this->admin_user_id );
+
+ // Set up request.
+ $_GET['page'] = 'wc-admin';
+ $_GET['task'] = 'payments';
+
+ // Trigger redirect.
+ $this->trigger_redirect_check();
+
+ // Get redirect URL.
+ $redirect_url = $this->get_redirect_attempt();
+
+ // Parse URL to verify parameters.
+ $parsed_url = wp_parse_url( $redirect_url );
+ parse_str( $parsed_url['query'], $params );
+
+ // Verify parameters.
+ $this->assertEquals( 'wc-settings', $params['page'], 'Redirect should go to wc-settings page.' );
+ $this->assertEquals( 'checkout', $params['tab'], 'Redirect should go to checkout tab.' );
+ $this->assertEquals( 'WCADMIN_PAYMENT_TASK', $params['from'], 'Redirect should include from parameter.' );
+ }
+
+ /**
+ * Returns an object mocking what we need from \WP_Screen.
+ *
+ * @return object
+ */
+ private function get_screen_mock() {
+ $screen_mock = $this->getMockBuilder( \stdClass::class )->setMethods( array( 'in_admin', 'add_option' ) )->getMock();
+ $screen_mock->method( 'in_admin' )->willReturn( true );
+ foreach ( array( 'id', 'base', 'action', 'post_type' ) as $key ) {
+ $screen_mock->{$key} = '';
+ }
+
+ return $screen_mock;
+ }
+}