diff --git a/includes/reader-revenue/my-account/class-woocommerce-my-account.php b/includes/reader-revenue/my-account/class-woocommerce-my-account.php index 9eb617cb31..c99c451905 100644 --- a/includes/reader-revenue/my-account/class-woocommerce-my-account.php +++ b/includes/reader-revenue/my-account/class-woocommerce-my-account.php @@ -8,6 +8,7 @@ namespace Newspack; use Newspack\Reader_Activation; +use Newspack\WooCommerce_Connection; use WP_Error; defined( 'ABSPATH' ) || exit; @@ -28,6 +29,7 @@ class WooCommerce_My_Account { * @codeCoverageIgnore */ public static function init() { + \add_action( 'rest_api_init', [ __CLASS__, 'register_routes' ] ); \add_filter( 'woocommerce_account_menu_items', [ __CLASS__, 'my_account_menu_items' ], 1000 ); \add_filter( 'woocommerce_default_address_fields', [ __CLASS__, 'required_address_fields' ] ); \add_filter( 'woocommerce_billing_fields', [ __CLASS__, 'required_address_fields' ] ); @@ -56,6 +58,36 @@ public static function init() { } } + /** + * Register routes. + */ + public static function register_routes() { + \register_rest_route( + NEWSPACK_API_NAMESPACE, + '/check-rate', + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [ __CLASS__, 'api_check_rate_limit' ], + 'permission_callback' => '__return_true', + ] + ); + } + + /** + * REST API handler for rate limit check. + */ + public static function api_check_rate_limit() { + $is_rate_limited = WooCommerce_Connection::rate_limit_by_user( 'add_payment_method', __( 'Please wait a moment before trying to add a new payment method.', 'newspack-plugin' ), true ); + $response = [ 'success' => false ]; + if ( ! \is_wp_error( $is_rate_limited ) && ! $is_rate_limited ) { + $response['success'] = true; + } + if ( \is_wp_error( $is_rate_limited ) ) { + $response['error'] = $is_rate_limited->get_error_message(); + } + return new \WP_REST_Response( $response ); + } + /** * Enqueue front-end scripts. */ @@ -72,9 +104,12 @@ public static function enqueue_scripts() { 'my-account', 'newspack_my_account', [ - 'labels' => [ + 'labels' => [ 'cancel_subscription_message' => __( 'Are you sure you want to cancel this subscription?', 'newspack-plugin' ), ], + 'rest_url' => get_rest_url(), + 'should_rate_limit' => WooCommerce_Connection::rate_limiting_enabled(), + 'nonce' => wp_create_nonce( 'wp_rest' ), ] ); \wp_enqueue_style( diff --git a/includes/reader-revenue/my-account/index.js b/includes/reader-revenue/my-account/index.js index 0f3fa4e2c3..4a3e641706 100644 --- a/includes/reader-revenue/my-account/index.js +++ b/includes/reader-revenue/my-account/index.js @@ -29,11 +29,12 @@ function domReady( callback ) { domReady( function () { const cancelButton = document.querySelector( '.subscription_details .button.cancel' ); + const { labels, nonce, rest_url, should_rate_limit } = newspack_my_account || {}; if ( cancelButton ) { const confirmCancel = event => { const message = - newspack_my_account?.labels?.cancel_subscription_message || + labels?.cancel_subscription_message || 'Are you sure you want to cancel this subscription?'; // eslint-disable-next-line no-alert @@ -43,4 +44,53 @@ domReady( function () { }; cancelButton.addEventListener( 'click', confirmCancel ); } + + const addPaymentForm = document.getElementById( 'add_payment_method' ); + if ( addPaymentForm && Boolean( should_rate_limit ) ) { + const errorContainer = document.querySelector( '.woocommerce-notices-wrapper' ); + const submitButton = addPaymentForm.querySelector( 'input[type="submit"], button[type="submit"]' ); + const rateLimit = function( e ) { + if ( addPaymentForm.hasAttribute( 'data-check-rate-limit' ) ) { + errorContainer.textContent = ''; + submitButton.setAttribute( 'disabled', '' ); + e.preventDefault(); + const xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + // Return if the request is completed. + if ( xhr.readyState !== 4 ) { + return; + } + + // Call onSuccess with parsed JSON if the request is successful. + if ( xhr.status >= 200 && xhr.status < 300 ) { + submitButton.removeAttribute( 'disabled' ); + const data = JSON.parse( xhr.responseText ); + if ( data?.success ) { + addPaymentForm.removeAttribute( 'data-check-rate-limit' ); + addPaymentForm.requestSubmit( submitButton ); + addPaymentForm.setAttribute( 'data-check-rate-limit', '1' ); + } + if ( data?.error ) { + const error = document.createElement( 'div' ); + const errorUl = document.createElement( 'ul' ); + const errorLi = document.createElement( 'li' ); + errorUl.classList.add( 'woocommerce-error' ); + errorLi.textContent = data.error; + error.appendChild( errorUl ); + errorUl.appendChild( errorLi ); + errorContainer.appendChild( error ); + errorContainer.scrollIntoView( { behavior: 'smooth' } ); + } + } + }; + + xhr.open( 'GET', rest_url + 'newspack/v1/check-rate' ); + xhr.setRequestHeader( 'X-WP-Nonce', nonce ); + xhr.send(); + } + }; + addPaymentForm.setAttribute( 'data-check-rate-limit', '1' ); + addPaymentForm.addEventListener( 'submit' , rateLimit, true ); + submitButton.addEventListener( 'click', rateLimit, true ); + } } ); diff --git a/includes/reader-revenue/woocommerce/class-woocommerce-connection.php b/includes/reader-revenue/woocommerce/class-woocommerce-connection.php index 6b0fec58d6..7250e9dd00 100644 --- a/includes/reader-revenue/woocommerce/class-woocommerce-connection.php +++ b/includes/reader-revenue/woocommerce/class-woocommerce-connection.php @@ -177,6 +177,15 @@ private static function get_client_ip() { return null; } + /** + * Check if rate limiting by user should be enabled for checkout and payment methods. + * + * @return bool + */ + public static function rate_limiting_enabled() { + return defined( 'NEWSPACK_CHECKOUT_RATE_LIMIT' ) && is_int( NEWSPACK_CHECKOUT_RATE_LIMIT ) && 0 !== NEWSPACK_CHECKOUT_RATE_LIMIT && class_exists( 'WC_Rate_Limiter' ); + } + /** * Check the rate limit for the current user or IP. * Currently locked behind a NEWSPACK_CHECKOUT_RATE_LIMIT environment constant, for controlled rollout. @@ -189,7 +198,7 @@ private static function get_client_ip() { */ public static function rate_limit_by_user( $action_name, $error_message = '', $return_error = false ) { $rate_limited = false; - if ( ! defined( 'NEWSPACK_CHECKOUT_RATE_LIMIT' ) || ! class_exists( 'WC_Rate_Limiter' ) ) { + if ( ! self::rate_limiting_enabled() ) { return $rate_limited; } if ( ! $error_message ) {