diff --git a/includes/class-magic-link.php b/includes/class-magic-link.php index 00adce3cf5..4987d6cdf0 100644 --- a/includes/class-magic-link.php +++ b/includes/class-magic-link.php @@ -516,11 +516,27 @@ public static function send_email( $user, $redirect_to = '', $use_otp = true ) { 'value' => $token_data['otp']['code'], ]; } - return Emails::send_email( + + /** + * Filters the email config name for magic link/OTP authentication emails. + * Allows overriding the email template used based on the current context. + * + * @param string $email_config_name The email config name (e.g., 'reader-activation-otp-authentication'). + * @param string $email_type The email type key (e.g., 'OTP_AUTH', 'MAGIC_LINK'). + * @param \WP_User $user The user receiving the email. + * @param string $redirect_to The redirect URL after authentication. + * @param array $token_data The token data including OTP information. + */ + $email_config_name = \apply_filters( + 'newspack_magic_link_email_config', Reader_Activation_Emails::EMAIL_TYPES[ $email_type ], - $user->user_email, - $email_placeholders + $email_type, + $user, + $redirect_to, + $token_data ); + + return Emails::send_email( $email_config_name, $user->user_email, $email_placeholders ); } /** diff --git a/includes/reader-activation/class-reader-activation.php b/includes/reader-activation/class-reader-activation.php index b75c8cebfe..696cc3a9fe 100644 --- a/includes/reader-activation/class-reader-activation.php +++ b/includes/reader-activation/class-reader-activation.php @@ -2712,7 +2712,7 @@ public static function login_after_password_reset( $user ) { * @param string $url The URL to check. * @return bool True if the URL path contains an OAuth route. */ - private static function is_oauth_redirect( $url ) { + public static function is_oauth_redirect( $url ) { $url_path = \wp_parse_url( $url, PHP_URL_PATH ) ?? ''; /** diff --git a/src/reader-activation-auth/auth-form.js b/src/reader-activation-auth/auth-form.js index 372b266922..16c2a53c58 100644 --- a/src/reader-activation-auth/auth-form.js +++ b/src/reader-activation-auth/auth-form.js @@ -39,6 +39,17 @@ window.newspackRAS.push( function ( readerActivation ) { const resendCodeButton = container.querySelector( '[data-resend-code]' ); const messageContentElement = container.querySelector( '.response' ); + /** + * Check if the current URL has a redirect parameter. + * Used to determine if we should pass the full URL to the backend during OAuth flows. + * + * @return {boolean} True if URL has a 'redirect' query parameter. + */ + const hasRedirectInUrl = () => { + const urlParams = new URLSearchParams( window.location.search ); + return urlParams.has( 'redirect' ); + }; + /** * Set action listener on the given item. */ @@ -216,9 +227,11 @@ window.newspackRAS.push( function ( readerActivation ) { body.set( 'npe', emailInput.value ); body.set( 'action', 'link' ); const pendingCheckout = getPendingCheckout(); - if ( pendingCheckout ) { + if ( pendingCheckout || hasRedirectInUrl() ) { const url = new URL( window.location.href ); - url.searchParams.set( 'checkout', 1 ); + if ( pendingCheckout ) { + url.searchParams.set( 'checkout', 1 ); + } body.set( 'redirect_url', url.toString() ); } fetch( form.getAttribute( 'action' ) || window.location.pathname, { @@ -410,9 +423,11 @@ window.newspackRAS.push( function ( readerActivation ) { return form.endLoginFlow( newspack_reader_activation_labels.invalid_email, 400 ); } const pendingCheckout = getPendingCheckout(); - if ( pendingCheckout ) { + if ( pendingCheckout || hasRedirectInUrl() ) { const url = new URL( window.location.href ); - url.searchParams.set( 'checkout', 1 ); + if ( pendingCheckout ) { + url.searchParams.set( 'checkout', 1 ); + } body.set( 'redirect_url', url.toString() ); } diff --git a/tests/unit-tests/magic-link.php b/tests/unit-tests/magic-link.php index 109be76b48..c47098de64 100644 --- a/tests/unit-tests/magic-link.php +++ b/tests/unit-tests/magic-link.php @@ -315,4 +315,40 @@ public function test_expired_token() { $this->assertTrue( is_wp_error( $validation ) ); $this->assertEquals( 'invalid_token', $validation->get_error_code() ); } + + /** + * Test that newspack_magic_link_email_config filter can override the email config. + */ + public function test_email_config_filter() { + $filter_called = false; + $received_params = []; + + // Add a filter to capture the parameters and modify the config name. + $filter_callback = function ( $email_config_name, $email_type, $user, $redirect_to, $token_data ) use ( &$filter_called, &$received_params ) { + $filter_called = true; + $received_params = [ + 'email_config_name' => $email_config_name, + 'email_type' => $email_type, + 'user' => $user, + 'redirect_to' => $redirect_to, + 'token_data' => $token_data, + ]; + // Return a custom config name. + return 'custom-email-config'; + }; + + add_filter( 'newspack_magic_link_email_config', $filter_callback, 10, 5 ); + + // Trigger send_email (it will fail to send since we don't have email setup, but filter should still fire). + $user = get_user_by( 'id', self::$user_id ); + Magic_Link::send_email( $user, 'https://example.com/redirect' ); + + remove_filter( 'newspack_magic_link_email_config', $filter_callback, 10 ); + + $this->assertTrue( $filter_called, 'The newspack_magic_link_email_config filter was called.' ); + $this->assertEquals( 'OTP_AUTH', $received_params['email_type'], 'Filter received the correct email type.' ); + $this->assertEquals( $user->ID, $received_params['user']->ID, 'Filter received the correct user.' ); + $this->assertEquals( 'https://example.com/redirect', $received_params['redirect_to'], 'Filter received the correct redirect_to.' ); + $this->assertIsArray( $received_params['token_data'], 'Filter received token data as array.' ); + } } diff --git a/tests/unit-tests/reader-activation.php b/tests/unit-tests/reader-activation.php index c7f30e7e63..07442a87ff 100644 --- a/tests/unit-tests/reader-activation.php +++ b/tests/unit-tests/reader-activation.php @@ -142,4 +142,33 @@ public function test_restricted_roles() { $this->assertFalse( Reader_Activation::is_user_reader( $user ) ); wp_delete_user( $reader_id ); // Clean up. } + + /** + * Test is_oauth_redirect detection. + */ + public function test_is_oauth_redirect() { + $this->assertTrue( Reader_Activation::is_oauth_redirect( 'https://example.com/oauth/authorize?client_id=123' ) ); + $this->assertFalse( Reader_Activation::is_oauth_redirect( 'https://example.com/my-account/' ) ); + $this->assertFalse( Reader_Activation::is_oauth_redirect( 'https://example.com/' ) ); + $this->assertFalse( Reader_Activation::is_oauth_redirect( '' ) ); + } + + /** + * Test is_oauth_redirect filter can extend routes. + */ + public function test_is_oauth_redirect_filter() { + // Add a custom OAuth route via filter. + add_filter( + 'newspack_ras_oauth_redirect_routes', + function ( $routes ) { + $routes[] = '/custom-oauth/'; + return $routes; + } + ); + + $this->assertTrue( + Reader_Activation::is_oauth_redirect( 'https://example.com/custom-oauth/?param=value' ), + 'Custom OAuth route should be detected after adding via filter.' + ); + } }