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;
+	}
+}