diff --git a/.github/workflows/check-changelog.yml b/.github/workflows/check-changelog.yml index 97287d6a5ea..682710edbdd 100644 --- a/.github/workflows/check-changelog.yml +++ b/.github/workflows/check-changelog.yml @@ -5,6 +5,8 @@ on: branches: - develop - 'release/**' + paths-ignore: + - '.github/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/changelog/add-timeouts-to-direct-to-woopay-checkout b/changelog/add-timeouts-to-direct-to-woopay-checkout new file mode 100644 index 00000000000..7fcf95f0504 --- /dev/null +++ b/changelog/add-timeouts-to-direct-to-woopay-checkout @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Handle timeouts in direct to WooPay checkout flow. diff --git a/changelog/fix-7263-hooks-in-prb b/changelog/fix-7263-hooks-in-prb new file mode 100644 index 00000000000..058dd323a8a --- /dev/null +++ b/changelog/fix-7263-hooks-in-prb @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Removed hooks from payment request button classes + + diff --git a/changelog/fix-refunds-with-pms-disabled-on-checkout b/changelog/fix-refunds-with-pms-disabled-on-checkout new file mode 100644 index 00000000000..c6989fc7d67 --- /dev/null +++ b/changelog/fix-refunds-with-pms-disabled-on-checkout @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Ensure gateways accessibility for use cases which don't require the gateway to be enabled diff --git a/changelog/revert-return-rest-payments-survey-controller-to-avoid-update-error b/changelog/revert-return-rest-payments-survey-controller-to-avoid-update-error new file mode 100644 index 00000000000..8e0894f42eb --- /dev/null +++ b/changelog/revert-return-rest-payments-survey-controller-to-avoid-update-error @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Reverts removed REST controller class to prevent error on update from older versions of the plugin. diff --git a/client/checkout/woopay/direct-checkout/index.js b/client/checkout/woopay/direct-checkout/index.js index a75aed95281..e7c4b0cd7f2 100644 --- a/client/checkout/woopay/direct-checkout/index.js +++ b/client/checkout/woopay/direct-checkout/index.js @@ -14,11 +14,12 @@ window.addEventListener( 'load', async () => { const checkoutElements = WooPayDirectCheckout.getCheckoutRedirectElements(); if ( isThirdPartyCookieEnabled ) { if ( await WooPayDirectCheckout.isUserLoggedIn() ) { - WooPayDirectCheckout.redirectToWooPaySession( checkoutElements ); + WooPayDirectCheckout.redirectToWooPay( checkoutElements, false ); } return; } - WooPayDirectCheckout.redirectToWooPay( checkoutElements ); + // Pass true to append '&checkout_redirect=1' and let WooPay decide the checkout flow. + WooPayDirectCheckout.redirectToWooPay( checkoutElements, true ); } ); diff --git a/client/checkout/woopay/direct-checkout/woopay-direct-checkout.js b/client/checkout/woopay/direct-checkout/woopay-direct-checkout.js index 69cee1340cc..b7d24e4176d 100644 --- a/client/checkout/woopay/direct-checkout/woopay-direct-checkout.js +++ b/client/checkout/woopay/direct-checkout/woopay-direct-checkout.js @@ -87,19 +87,49 @@ class WoopayDirectCheckout { } /** - * Sends the session data to the WooPayConnectIframe. + * Resolves the redirect URL to the WooPay checkout page or throws an error if the request fails. * - * @return {Promise<*>} Resolves to the redirect URL. + * @return {string} The redirect URL. + * @throws {Error} If the session data could not be sent to WooPay. */ - static async sendRedirectSessionDataToWooPay() { - const woopaySession = await this.getWooPaySessionFromMerchant(); - const woopaySessionData = await this.getSessionConnect().sendRedirectSessionDataToWooPay( - woopaySession - ); + static async resolveWooPayRedirectUrl() { + // We're intentionally adding a try-catch block to catch any errors + // that might occur other than the known validation errors. + try { + const encryptedSessionData = await this.getEncryptedSessionData(); + if ( ! this.isValidEncryptedSessionData( encryptedSessionData ) ) { + throw new Error( + 'Could not retrieve encrypted session data from store.' + ); + } - const { redirect_url: redirectUrl } = await woopaySessionData; + const woopaySessionData = await this.getSessionConnect().sendRedirectSessionDataToWooPay( + encryptedSessionData + ); + if ( ! woopaySessionData?.redirect_url ) { + throw new Error( 'Could not retrieve WooPay checkout URL.' ); + } - return redirectUrl; + return woopaySessionData.redirect_url; + } catch ( error ) { + throw new Error( error.message ); + } + } + + /** + * Checks if the encrypted session object is valid. + * + * @param {Object} encryptedSessionData The encrypted session data. + * @return {boolean} True if the session is valid. + */ + static isValidEncryptedSessionData( encryptedSessionData ) { + return ( + encryptedSessionData && + encryptedSessionData?.blog_id && + encryptedSessionData?.data?.session && + encryptedSessionData?.data?.iv && + encryptedSessionData?.data?.hash + ); } /** @@ -120,46 +150,48 @@ class WoopayDirectCheckout { addElementBySelector( '.wc-proceed-to-checkout .checkout-button' ); // Blocks 'Proceed to Checkout' button. addElementBySelector( - '.wp-block-woocommerce-proceed-to-checkout-block' + '.wp-block-woocommerce-proceed-to-checkout-block a' ); return elements; } /** - * Adds a click-event listener that redirects to the WooPay checkout page to the given elements. + * Adds a click-event listener to the given elements that redirects to the WooPay checkout page. * * @param {*[]} elements The elements to add a click-event listener to. + * @param {boolean} useCheckoutRedirect Whether to use the `checkout_redirect` flag to let WooPay handle the checkout flow. */ - static redirectToWooPaySession( elements ) { + static redirectToWooPay( elements, useCheckoutRedirect ) { elements.forEach( ( element ) => { element.addEventListener( 'click', async ( event ) => { - event.preventDefault(); + // Store href before the async call to not lose the reference. + const currTargetHref = event.currentTarget.href; - const woopayRedirectUrl = await this.sendRedirectSessionDataToWooPay(); - this.teardown(); + // If there's no link where to redirect the user, do not break the expected behavior. + if ( ! currTargetHref ) { + this.teardown(); + return; + } - window.location.href = woopayRedirectUrl; - } ); - } ); - } - - /** - * Adds a click-event listener that redirects to WooPay and lets WooPay handle the checkout flow - * to the given elements. - * - * @param {*[]} elements The elements to add a click-event listener to. - */ - static redirectToWooPay( elements ) { - elements.forEach( ( element ) => { - element.addEventListener( 'click', async ( event ) => { event.preventDefault(); - const woopayRedirectUrl = await this.sendRedirectSessionDataToWooPay(); - this.teardown(); - - window.location.href = - woopayRedirectUrl + '&woopay_checkout_redirect=1'; + try { + let woopayRedirectUrl = await this.resolveWooPayRedirectUrl(); + if ( useCheckoutRedirect ) { + woopayRedirectUrl += '&checkout_redirect=1'; + } + + this.teardown(); + // TODO: Add telemetry as to _how long_ it took to get to this step. + window.location.href = woopayRedirectUrl; + } catch ( error ) { + // TODO: Add telemetry as to _why_ we've short-circuited the WooPay checkout flow. + console.warn( error ); // eslint-disable-line no-console + + this.teardown(); + window.location.href = currTargetHref; + } } ); } ); } @@ -169,7 +201,7 @@ class WoopayDirectCheckout { * * @return {Promise|*>} Resolves to the WooPay session response. */ - static async getWooPaySessionFromMerchant() { + static async getEncryptedSessionData() { return request( buildAjaxURL( getConfig( 'wcAjaxUrl' ), 'get_woopay_session' ), { diff --git a/dev/phpcs/ruleset.xml b/dev/phpcs/ruleset.xml index 27f2329cbb6..f9699a102f0 100644 --- a/dev/phpcs/ruleset.xml +++ b/dev/phpcs/ruleset.xml @@ -15,10 +15,6 @@ */includes/woopay-user/* */includes/class-wc-payments-order-success-page.php - - */includes/class-wc-payments-apple-pay-registration.php - */includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php - */includes/class-wc-payments-customer-service.php */includes/class-wc-payments-token-service.php diff --git a/includes/admin/class-wc-rest-payments-survey-controller.php b/includes/admin/class-wc-rest-payments-survey-controller.php new file mode 100644 index 00000000000..b670653cc48 --- /dev/null +++ b/includes/admin/class-wc-rest-payments-survey-controller.php @@ -0,0 +1,18 @@ +stripe_id, $this->get_payment_method_ids_enabled_at_checkout(), true ) ? true : false; + if ( ! $is_gateway_enabled ) { + return false; + } + return parent::is_available() && ! $this->needs_setup(); } diff --git a/includes/class-wc-payments-apple-pay-registration.php b/includes/class-wc-payments-apple-pay-registration.php index 546256f4815..276a6e65c62 100644 --- a/includes/class-wc-payments-apple-pay-registration.php +++ b/includes/class-wc-payments-apple-pay-registration.php @@ -71,7 +71,14 @@ public function __construct( WC_Payments_API_Client $payments_api_client, WC_Pay $this->payments_api_client = $payments_api_client; $this->account = $account; $this->gateway = $gateway; + } + /** + * Initializes this class's hooks. + * + * @return void + */ + public function init_hooks() { add_action( 'init', [ $this, 'add_domain_association_rewrite_rule' ], 5 ); add_action( 'woocommerce_woocommerce_payments_updated', [ $this, 'verify_domain_on_update' ] ); add_action( 'init', [ $this, 'init' ] ); @@ -433,6 +440,6 @@ public function display_error_notice() {

-init_hooks(); self::maybe_display_express_checkout_buttons(); @@ -734,7 +735,7 @@ public static function get_plugin_headers() { * @return array The list of payment gateways that will be available, including WooPayments' Gateway class. */ public static function register_gateway( $gateways ) { - $payment_methods = self::$card_gateway->get_payment_method_ids_enabled_at_checkout(); + $payment_methods = array_keys( self::get_payment_method_map() ); $key = array_search( 'link', $payment_methods, true ); @@ -1484,6 +1485,7 @@ public static function maybe_display_express_checkout_buttons() { $payment_request_button_handler = new WC_Payments_Payment_Request_Button_Handler( self::$account, self::get_gateway(), $express_checkout_helper ); $woopay_button_handler = new WC_Payments_WooPay_Button_Handler( self::$account, self::get_gateway(), self::$woopay_util, $express_checkout_helper ); $express_checkout_button_display_handler = new WC_Payments_Express_Checkout_Button_Display_Handler( self::get_gateway(), $payment_request_button_handler, $woopay_button_handler, $express_checkout_helper ); + $express_checkout_button_display_handler->init(); } } diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php index d2bacd56084..7f48ce2619b 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php @@ -55,7 +55,14 @@ public function __construct( WC_Payment_Gateway_WCPay $gateway, WC_Payments_Paym $this->payment_request_button_handler = $payment_request_button_handler; $this->platform_checkout_button_handler = $platform_checkout_button_handler; $this->express_checkout_helper = $express_checkout_helper; + } + /** + * Initializes this class, its dependencies, and its hooks. + * + * @return void + */ + public function init() { $this->platform_checkout_button_handler->init(); $this->payment_request_button_handler->init(); @@ -106,9 +113,9 @@ public function display_express_checkout_buttons() { ?>
express_checkout_helper->is_pay_for_order_page() || $this->is_pay_for_order_flow_supported() ) { - $this->platform_checkout_button_handler->display_woopay_button_html(); - } + if ( ! $this->express_checkout_helper->is_pay_for_order_page() || $this->is_pay_for_order_flow_supported() ) { + $this->platform_checkout_button_handler->display_woopay_button_html(); + } $this->payment_request_button_handler->display_payment_request_button_html(); ?>
@@ -148,7 +155,7 @@ public function add_pay_for_order_params_to_js_config() { $order = wc_get_order( $order_id ); - // phpcs:disable WordPress.Security.NonceVerification.Recommended + // phpcs:disable WordPress.Security.NonceVerification if ( isset( $_GET['pay_for_order'] ) && isset( $_GET['key'] ) && current_user_can( 'pay_for_order', $order_id ) ) { add_filter( 'wcpay_payment_fields_js_config', @@ -162,7 +169,7 @@ function( $js_config ) use ( $order ) { } // Silence the filter_input warning because we are sanitizing the input with sanitize_email(). - // nosemgrep: audit.php.lang.misc.filter-input-no-filter + // nosemgrep: audit.php.lang.misc.filter-input-no-filter. $user_email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( filter_input( INPUT_POST, 'email' ) ) ) : $session_email; $js_config['order_id'] = $order->get_id(); @@ -177,6 +184,6 @@ function( $js_config ) use ( $order ) { } ); } - // phpcs:enable WordPress.Security.NonceVerification.Recommended + // phpcs:enable WordPress.Security.NonceVerification } } diff --git a/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php b/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php index 1e3162dead3..22653a4cba9 100644 --- a/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php +++ b/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php @@ -293,7 +293,6 @@ public function set_up() { 'get_payment_method_ids_enabled_at_checkout', 'wc_payments_get_payment_gateway_by_id', 'get_selected_payment_method', - 'get_upe_enabled_payment_method_ids', ] ) ->getMock(); @@ -381,12 +380,7 @@ public function test_should_not_use_stripe_platform_on_checkout_page_for_upe() { public function test_link_payment_method_requires_mandate_data() { $mock_upe_gateway = $this->mock_payment_gateways[ Payment_Method::CARD ]; - $mock_upe_gateway - ->expects( $this->once() ) - ->method( 'get_upe_enabled_payment_method_ids' ) - ->will( - $this->returnValue( [ 'link' ] ) - ); + $mock_upe_gateway->update_option( 'upe_enabled_payment_method_ids', [ 'link' ] ); $this->assertTrue( $mock_upe_gateway->is_mandate_data_required() ); } @@ -422,11 +416,7 @@ public function test_process_payment_returns_correct_redirect_when_using_saved_p ->will( $this->returnValue( [ $user, $customer_id ] ) ); - $mock_card_payment_gateway->expects( $this->any() ) - ->method( 'get_upe_enabled_payment_method_ids' ) - ->will( - $this->returnValue( [ Payment_Method::CARD ] ) - ); + $mock_card_payment_gateway->update_option( 'upe_enabled_payment_method_ids', [ Payment_Method::CARD ] ); $this->mock_wcpay_request( Create_And_Confirm_Intention::class, 1 ) ->expects( $this->once() ) ->method( 'format_response' ) @@ -1216,7 +1206,6 @@ public function test_get_payment_methods_from_gateway_id_upe() { ) ->onlyMethods( [ - 'get_upe_enabled_payment_method_ids', 'get_payment_method_ids_enabled_at_checkout', ] ) @@ -1225,11 +1214,7 @@ public function test_get_payment_methods_from_gateway_id_upe() { $gateway = WC_Payments::get_gateway(); WC_Payments::set_gateway( $mock_upe_gateway ); - $mock_upe_gateway->expects( $this->any() ) - ->method( 'get_upe_enabled_payment_method_ids' ) - ->will( - $this->returnValue( [ Payment_Method::CARD, Payment_Method::LINK ] ) - ); + $mock_upe_gateway->update_option( 'upe_enabled_payment_method_ids', [ Payment_Method::CARD, Payment_Method::LINK ] ); $payment_methods = $mock_upe_gateway->get_payment_methods_from_gateway_id( WC_Payment_Gateway_WCPay::GATEWAY_ID . '_' . Payment_Method::BANCONTACT ); $this->assertSame( [ Payment_Method::BANCONTACT ], $payment_methods ); diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay.php b/tests/unit/test-class-wc-payment-gateway-wcpay.php index 6ecc387059b..a7202f5f549 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay.php @@ -821,8 +821,8 @@ public function test_exception_will_be_thrown_if_phone_number_is_invalid() { } public function test_remove_link_payment_method_if_card_disabled() { - $link_gateway = $this->get_gateway( Payment_Method::LINK ); - $link_gateway->settings['upe_enabled_payment_method_ids'] = [ 'link' ]; + $link_gateway = $this->get_gateway( Payment_Method::LINK ); + $link_gateway->settings['upe_enabled_payment_method_ids'] = [ 'link' ]; $this->mock_wcpay_account ->expects( $this->any() ) @@ -2622,6 +2622,22 @@ function ( $order ) { } } + public function test_gateway_enabled_when_payment_method_is_enabled() { + $afterpay = $this->get_gateway( Payment_Method::AFTERPAY ); + $afterpay->update_option( 'upe_enabled_payment_method_ids', [ Payment_Method::AFTERPAY, Payment_Method::CARD, Payment_Method::P24, Payment_Method::BANCONTACT ] ); + $this->prepare_gateway_for_availability_testing( $afterpay ); + + $this->assertTrue( $afterpay->is_available() ); + } + + public function test_gateway_disabled_when_payment_method_is_disabled() { + $afterpay = $this->get_gateway( Payment_Method::AFTERPAY ); + $afterpay->update_option( 'upe_enabled_payment_method_ids', [ Payment_Method::CARD, Payment_Method::P24, Payment_Method::BANCONTACT ] ); + $this->prepare_gateway_for_availability_testing( $afterpay ); + + $this->assertFalse( $afterpay->is_available() ); + } + public function test_process_payment_for_order_cc_payment_method() { $payment_method = 'woocommerce_payments'; $expected_upe_payment_method_for_pi_creation = 'card'; @@ -3374,6 +3390,48 @@ private function create_charge_object() { return new WC_Payments_API_Charge( $this->mock_charge_id, 1500, $created ); } + private function prepare_gateway_for_availability_testing( $gateway ) { + WC_Payments::mode()->test(); + $current_currency = strtolower( get_woocommerce_currency() ); + $this->mock_wcpay_account->expects( $this->once() )->method( 'get_account_customer_supported_currencies' )->will( + $this->returnValue( + [ + $current_currency, + ] + ) + ); + + $this->mock_wcpay_account + ->expects( $this->any() ) + ->method( 'get_cached_account_data' ) + ->willReturn( + [ + 'capabilities' => [ + 'afterpay_clearpay_payments' => 'active', + ], + 'capability_requirements' => [ + 'afterpay_clearpay_payments' => [], + ], + ] + ); + + $this->mock_wcpay_account + ->expects( $this->any() ) + ->method( 'is_stripe_connected' ) + ->willReturn( true ); + + $this->mock_wcpay_account + ->expects( $this->any() ) + ->method( 'get_account_status_data' ) + ->willReturn( + [ + 'paymentsEnabled' => true, + ] + ); + $gateway->update_option( WC_Payment_Gateway_WCPay::METHOD_ENABLED_KEY, 'yes' ); + $gateway->init_settings(); + } + private function init_payment_methods() { $payment_methods = []; diff --git a/tests/unit/test-class-wc-payments-apple-pay-registration.php b/tests/unit/test-class-wc-payments-apple-pay-registration.php index cfbf23d6154..7ce5f6496a5 100644 --- a/tests/unit/test-class-wc-payments-apple-pay-registration.php +++ b/tests/unit/test-class-wc-payments-apple-pay-registration.php @@ -64,6 +64,7 @@ public function set_up() { ->getMock(); $this->wc_apple_pay_registration = new WC_Payments_Apple_Pay_Registration( $this->mock_api_client, $this->mock_account, $mock_gateway ); + $this->wc_apple_pay_registration->init_hooks(); $this->file_name = 'apple-developer-merchantid-domain-association'; $this->initial_file_contents = file_get_contents( WCPAY_ABSPATH . '/' . $this->file_name ); // @codingStandardsIgnoreLine diff --git a/tests/unit/test-class-wc-payments-express-checkout-button-display-handler.php b/tests/unit/test-class-wc-payments-express-checkout-button-display-handler.php index 42acd644a62..8dacaf1f7a5 100644 --- a/tests/unit/test-class-wc-payments-express-checkout-button-display-handler.php +++ b/tests/unit/test-class-wc-payments-express-checkout-button-display-handler.php @@ -143,6 +143,7 @@ public function set_up() { ->getMock(); $this->express_checkout_button_display_handler = new WC_Payments_Express_Checkout_Button_Display_Handler( $this->mock_wcpay_gateway, $this->mock_payment_request_button_handler, $this->mock_woopay_button_handler, $this->mock_express_checkout_helper ); + $this->express_checkout_button_display_handler->init(); add_filter( 'woocommerce_available_payment_gateways', diff --git a/tests/unit/test-class-wc-payments.php b/tests/unit/test-class-wc-payments.php index d5e7da12239..972d1bfdd63 100644 --- a/tests/unit/test-class-wc-payments.php +++ b/tests/unit/test-class-wc-payments.php @@ -78,18 +78,9 @@ public function test_it_does_not_register_woopay_hooks_if_feature_flag_is_disabl } public function test_it_skips_stripe_link_gateway_registration() { - $this->mock_cache->method( 'get' )->willReturn( [ 'is_deferred_intent_creation_upe_enabled' => true ] ); + $all_gateways_before_registration = count( WC_Payments::get_payment_method_map() ); + $card_gateway_mock = $this->createMock( WC_Payment_Gateway_WCPay::class ); - $card_gateway_mock = $this->createMock( WC_Payment_Gateway_WCPay::class ); - $card_gateway_mock - ->expects( $this->once() ) - ->method( 'get_payment_method_ids_enabled_at_checkout' ) - ->willReturn( - [ - 'link', - 'card', - ] - ); $card_gateway_mock ->expects( $this->once() ) ->method( 'get_stripe_id' ) @@ -98,7 +89,7 @@ public function test_it_skips_stripe_link_gateway_registration() { $registered_gateways = WC_Payments::register_gateway( [] ); - $this->assertCount( 1, $registered_gateways ); + $this->assertCount( $all_gateways_before_registration - 1, $registered_gateways ); $this->assertInstanceOf( WC_Payment_Gateway_WCPay::class, $registered_gateways[0] ); $this->assertEquals( $registered_gateways[0]->get_stripe_id(), 'card' ); }