diff --git a/includes/class-newspack.php b/includes/class-newspack.php index 513156fb45..14abffb2d8 100644 --- a/includes/class-newspack.php +++ b/includes/class-newspack.php @@ -224,6 +224,7 @@ private function includes() { include_once NEWSPACK_ABSPATH . 'includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription.php'; include_once NEWSPACK_ABSPATH . 'includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-api.php'; include_once NEWSPACK_ABSPATH . 'includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-invite.php'; + include_once NEWSPACK_ABSPATH . 'includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-myaccount.php'; include_once NEWSPACK_ABSPATH . 'includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php'; include_once NEWSPACK_ABSPATH . 'includes/plugins/class-woocommerce-gateway-stripe.php'; include_once NEWSPACK_ABSPATH . 'includes/plugins/class-teams-for-memberships.php'; diff --git a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-myaccount.php b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-myaccount.php new file mode 100644 index 0000000000..5d5001faa4 --- /dev/null +++ b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-myaccount.php @@ -0,0 +1,357 @@ +get_id(), + wc_get_page_permalink( 'myaccount' ) + ); + } + + /** + * Add manage members query var. + * + * @param array $query_vars Query vars. + * + * @return array + */ + public static function add_manage_members_endpoint( $query_vars ) { + $query_vars[ self::MANAGE_MEMBERS_ENDPOINT ] = self::MANAGE_MEMBERS_ENDPOINT; + return $query_vars; + } + + /** + * Render the group subscription members template. + */ + public static function render_group_subscription_members_template() { + $subscription_id = absint( get_query_var( self::MANAGE_MEMBERS_ENDPOINT ) ); + $subscription = WooCommerce_Subscriptions::sanitize_subscription( $subscription_id ); + if ( ! $subscription ) { + wp_safe_redirect( + add_query_arg( + [ + 'message' => __( 'Subscription not found.', 'newspack-plugin' ), + 'is_error' => true, + ], + wc_get_account_endpoint_url( 'edit-account' ) + ) + ); + exit; + } + $user_id = \get_current_user_id(); + if ( ! Group_Subscription::user_is_manager( $user_id, $subscription ) ) { + wp_safe_redirect( + add_query_arg( + [ + 'message' => __( 'You do not have permission to manage members of this subscription.', 'newspack-plugin' ), + 'is_error' => true, + ], + wc_get_account_endpoint_url( 'edit-account' ) + ) + ); + exit; + } + $args = [ + 'actions' => \wcs_get_all_user_actions_for_subscription( $subscription, $user_id ), + 'subscription' => $subscription, + 'view' => 'manage-members', + ]; + \wc_get_template( 'myaccount/group-subscription-members.php', $args ); + } + + /** + * Filter the actions a group manager or member can take on a subscription. + * + * Non-manager group members receive an empty actions array (view-only experience). + * Managers (subscription owners) receive an additional "Manage members" action. + * Non-group subscriptions and off-account-page requests pass through unchanged. + * + * @param array $actions Actions. + * @param \WC_Subscription $subscription Subscription. + * @param int $user_id The user ID. + * + * @return array + */ + public static function view_subscription_actions( $actions, $subscription, $user_id ) { + if ( ! function_exists( 'is_account_page' ) || ! \is_account_page() || ! Group_Subscription::is_group_subscription( $subscription ) ) { + return $actions; + } + + // Non-manager group members get a view-only experience: no actions. + if ( Group_Subscription::user_is_member( $user_id, $subscription ) ) { + return []; + } + + // Managers (subscription owners) get a "Manage members" action. + if ( Group_Subscription::user_is_manager( $user_id, $subscription ) ) { + $actions['manage_members'] = [ + 'url' => self::get_manage_members_url( $subscription ), + 'name' => __( 'Manage members', 'newspack-plugin' ), + ]; + } + + return $actions; + } + + /** + * Get subscription ID and redirect URL from POST data. + * + * @return array{ 0: int, 1: string } + */ + private static function get_subscription_context(): array { + $subscription_id = filter_input( INPUT_POST, 'subscription_id', FILTER_VALIDATE_INT ) ?? 0; + $redirect_url = wc_get_endpoint_url( self::MANAGE_MEMBERS_ENDPOINT, $subscription_id, wc_get_page_permalink( 'myaccount' ) ); + return [ $subscription_id, $redirect_url ]; + } + + /** + * Verify the current user has permission to manage the subscription, redirecting on failure. + * + * @param int $subscription_id Subscription ID. + * @param string $redirect_url URL to redirect to on failure. + * @param string $active_tab Active tab slug for the redirect. + * @param string|null $error_message Error message to display. + */ + private static function verify_permission( $subscription_id, $redirect_url, $active_tab, $error_message = null ): void { + if ( ! $error_message ) { + $error_message = __( 'You do not have permission to manage members for this group subscription.', 'newspack-plugin' ); + } + $request = new \WP_REST_Request(); + $request->set_param( 'subscription_id', $subscription_id ); + if ( ! Group_Subscription_API::permission_callback( $request ) ) { + self::redirect( + new \WP_Error( 'newspack_group_subscription_permission_denied', $error_message ), + $redirect_url, + $active_tab, + $error_message + ); + } + } + + /** + * Redirect with a success or error message depending on the action result. + * + * @param \WP_Error|mixed $result Result of the action. + * @param string $redirect_url URL to redirect to. + * @param string $active_tab Active tab slug for the redirect. + * @param string $success_message Success message to display. + */ + private static function redirect( $result, $redirect_url, $active_tab, $success_message ): never { + $query_args = [ + 'activeTab' => $active_tab, + 'message' => $success_message, + ]; + if ( is_wp_error( $result ) ) { + $query_args['is_error'] = true; + $query_args['message'] = $result->get_error_message(); + } else { + $query_args['is_success'] = true; + } + wp_safe_redirect( + add_query_arg( $query_args, $redirect_url ) + ); + exit; + } + + /** + * Handle the invite member form submission. + */ + public static function handle_invite_member() { + check_admin_referer( self::INVITE_NONCE_ACTION ); + [ $subscription_id, $redirect_url ] = self::get_subscription_context(); + self::verify_permission( $subscription_id, $redirect_url, 'invites' ); + + $email = filter_input( INPUT_POST, 'newspack-group-subscription-invite-email', FILTER_SANITIZE_EMAIL ) ?? ''; + $invite = Group_Subscription_Invite::generate_invite( $subscription_id, $email ); + + self::redirect( + $invite, + $redirect_url, + 'invites', + sprintf( + // translators: %s: The invited email address. + __( '%s has been invited to become a member of this group subscription.', 'newspack-plugin' ), + $email + ) + ); + } + + /** + * Handle the cancel invite form submission. + */ + public static function handle_cancel_invite() { + check_admin_referer( self::CANCEL_INVITE_NONCE_ACTION ); + [ $subscription_id, $redirect_url ] = self::get_subscription_context(); + self::verify_permission( $subscription_id, $redirect_url, 'invites' ); + + $email = filter_input( INPUT_POST, 'email', FILTER_SANITIZE_EMAIL ) ?? ''; + $result = Group_Subscription_Invite::cancel_invite( $subscription_id, $email ); + + self::redirect( + $result, + $redirect_url, + 'invites', + sprintf( + // translators: %s: The cancelled invitation's email address. + __( 'The invitation for %s has been cancelled.', 'newspack-plugin' ), + $email + ) + ); + } + + /** + * Inject group subscriptions the current user is a member of into the subscriptions list. + * + * Only runs on My Account pages to avoid side effects (e.g. trial limit checks) + * in non-account contexts. + * + * @param array $subscriptions Existing subscriptions keyed by subscription ID. + * @param int $user_id The user ID. + * + * @return array + */ + public static function inject_member_group_subscriptions( $subscriptions, $user_id ) { + if ( ! function_exists( 'is_account_page' ) || ! \is_account_page() ) { + return $subscriptions; + } + $existing_ids = array_keys( $subscriptions ); + $group_subscriptions = Group_Subscription::get_group_subscriptions_for_user( $user_id ); + foreach ( $group_subscriptions as $group_subscription ) { + if ( ! ( $group_subscription instanceof \WC_Subscription ) ) { + continue; + } + if ( $group_subscription->has_status( 'trash' ) ) { + continue; + } + if ( in_array( $group_subscription->get_id(), $existing_ids, true ) ) { + continue; + } + $subscriptions[ $group_subscription->get_id() ] = $group_subscription; + } + return $subscriptions; + } + + /** + * Grant the `view_order` capability to group subscription members on My Account pages. + * + * WCS checks current_user_can( 'view_order', $subscription->get_id() ) before rendering + * the view-subscription template. WC maps view_order → manage_woocommerce for non-owners. + * We override this to 'read' (a primitive cap all logged-in users have) for group members. + * + * @param string[] $caps Primitive capabilities required. + * @param string $cap The meta capability being checked. + * @param int $user_id The user ID. + * @param array $args Additional arguments; $args[0] is the post/order ID. + * + * @return string[] + */ + public static function grant_group_member_view_order_cap( $caps, $cap, $user_id, $args ) { + if ( 'view_order' !== $cap || ! function_exists( 'is_account_page' ) || ! \is_account_page() ) { + return $caps; + } + $order_id = isset( $args[0] ) ? absint( $args[0] ) : 0; + $subscription = WooCommerce_Subscriptions::sanitize_subscription( $order_id ); + if ( ! $subscription || $subscription->has_status( 'trash' ) ) { + return $caps; + } + if ( Group_Subscription::user_is_member( $user_id, $subscription ) ) { + return [ 'read' ]; + } + return $caps; + } + + /** + * Handle the remove member form submission. + */ + public static function handle_remove_member() { + check_admin_referer( self::REMOVE_MEMBER_NONCE_ACTION ); + [ $subscription_id, $redirect_url ] = self::get_subscription_context(); + self::verify_permission( $subscription_id, $redirect_url, 'members' ); + + $member_id = filter_input( INPUT_POST, 'member_id', FILTER_VALIDATE_INT ) ?? 0; + $member_data = get_userdata( $member_id ); + $result = Group_Subscription::update_members( $subscription_id, [], [ $member_id ] ); + + self::redirect( + $result, + $redirect_url, + 'members', + sprintf( + // translators: %s: The removed member's email address. + __( '%s has been removed from this group subscription.', 'newspack-plugin' ), + $member_data ? $member_data->user_email : $member_id + ) + ); + } +} +Group_Subscription_MyAccount::init(); diff --git a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription.php b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription.php index b14ce9c5e2..ed5fe6c384 100644 --- a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription.php +++ b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription.php @@ -157,29 +157,24 @@ public static function update_members( $subscription, $members_to_add, $members_ } /** - * Check if a user is a member or manager of a group subscription. + * Check if a user is a member (not manager) of a group subscription. * * @param int $user_id The user ID. * @param \WC_Subscription|int $subscription The subscription object or ID. * - * @return bool|null Whether the user has access to the group subscription, or null if not a group subscription. + * @return bool|null Whether the user is a member of the group subscription, or null if not a group subscription. */ public static function user_is_member( $user_id, $subscription ) { + $subscription = WooCommerce_Subscriptions::sanitize_subscription( $subscription ); if ( ! self::is_group_subscription( $subscription ) ) { return null; } - if ( ! is_a( $subscription, 'WC_Subscription' ) ) { - $subscription = \wcs_get_subscription( $subscription ); - } - if ( ! $subscription ) { - return null; - } - $is_member = in_array( $subscription->get_id(), self::get_group_subscriptions_for_user( $user_id, true ), true ) || in_array( $user_id, self::get_managers( $subscription ), true ); + $is_member = in_array( $subscription->get_id(), self::get_group_subscriptions_for_user( $user_id, true ), true ); /** - * Filter whether a user is a member or manager of a group subscription. + * Filter whether a user is a member (not manager) of a group subscription. * - * @param bool|null $is_member Whether the user is a member or manager of the group subscription, or null if not a group subscription. + * @param bool $is_member Whether the user is a member of the group subscription. * @param int $user_id The user ID. * @param \WC_Subscription|int $subscription The subscription object or ID. */ @@ -195,21 +190,16 @@ public static function user_is_member( $user_id, $subscription ) { * @return bool|null Whether the user is a manager of the group subscription, or null if not a group subscription. */ public static function user_is_manager( $user_id, $subscription ) { + $subscription = WooCommerce_Subscriptions::sanitize_subscription( $subscription ); if ( ! self::is_group_subscription( $subscription ) ) { return null; } - if ( ! is_a( $subscription, 'WC_Subscription' ) ) { - $subscription = \wcs_get_subscription( $subscription ); - } - if ( ! $subscription ) { - return null; - } $is_manager = in_array( $user_id, self::get_managers( $subscription ), true ); /** * Filter whether a user is a manager of a group subscription. * - * @param bool|null $is_manager Whether the user is a manager of the group subscription, or null if not a group subscription. + * @param bool $is_manager Whether the user is a manager of the group subscription. * @param int $user_id The user ID. * @param \WC_Subscription|int $subscription The subscription object or ID. */ diff --git a/includes/plugins/woocommerce/my-account/class-my-account-ui-v1.php b/includes/plugins/woocommerce/my-account/class-my-account-ui-v1.php index 26f02eb10d..b55bd1c31d 100644 --- a/includes/plugins/woocommerce/my-account/class-my-account-ui-v1.php +++ b/includes/plugins/woocommerce/my-account/class-my-account-ui-v1.php @@ -180,6 +180,8 @@ public static function wc_get_template( $template, $template_name ) { return __DIR__ . '/templates/v1/related-orders.php'; case 'myaccount/related-subscriptions.php': return __DIR__ . '/templates/v1/related-subscriptions.php'; + case 'myaccount/group-subscription-members.php': + return __DIR__ . '/templates/v1/group-subscription-members.php'; case 'order/order-again.php': return __DIR__ . '/templates/v1/order-again.php'; case 'notices/error.php': @@ -237,6 +239,19 @@ public static function my_account_menu_items( $items ) { return $items; } + /** + * Check if the current page is a subscription page. + * + * @param string $endpoint The current My Account page endpoint. + * @return bool Whether the current page is a subscription page. + */ + public static function is_subscription_page( $endpoint ) { + if ( 'subscriptions' !== $endpoint ) { + return false; + } + return function_exists( 'is_wc_endpoint_url' ) && ( is_wc_endpoint_url( 'view-subscription' ) || is_wc_endpoint_url( 'manage-members' ) ); + } + /** * Remove required fields from the account settings form. * diff --git a/includes/plugins/woocommerce/my-account/class-woocommerce-my-account.php b/includes/plugins/woocommerce/my-account/class-woocommerce-my-account.php index beddbc5051..aa50e015e8 100644 --- a/includes/plugins/woocommerce/my-account/class-woocommerce-my-account.php +++ b/includes/plugins/woocommerce/my-account/class-woocommerce-my-account.php @@ -59,6 +59,7 @@ public static function init() { // Reader Activation mods. if ( Reader_Activation::is_enabled() ) { + \add_action( 'wp_footer', [ __CLASS__, 'handle_messages' ] ); \add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue_scripts' ] ); \add_action( 'template_redirect', [ __CLASS__, 'handle_password_reset_request' ] ); \add_action( 'template_redirect', [ __CLASS__, 'handle_delete_account' ] ); @@ -146,6 +147,22 @@ public static function register_routes() { ); } + /** + * Handle messages in 'message' query param. + */ + public static function handle_messages() { + if ( ! function_exists( 'is_account_page' ) || ! \is_account_page() ) { + return; + } + $message = filter_input( INPUT_GET, 'message', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ?? false; + $is_success = filter_input( INPUT_GET, 'is_success', FILTER_VALIDATE_BOOLEAN ) ?? false; + $is_error = filter_input( INPUT_GET, 'is_error', FILTER_VALIDATE_BOOLEAN ) ?? false; + if ( $message ) { + \wc_add_notice( $message, $is_success ? 'success' : ( $is_error ? 'error' : 'notice' ) ); + \wc_print_notices(); + } + } + /** * Suppress ads on My Account pages. * diff --git a/includes/plugins/woocommerce/my-account/templates/v1/account-settings.php b/includes/plugins/woocommerce/my-account/templates/v1/account-settings.php index 4cda221ea6..e60026ca95 100644 --- a/includes/plugins/woocommerce/my-account/templates/v1/account-settings.php +++ b/includes/plugins/woocommerce/my-account/templates/v1/account-settings.php @@ -22,16 +22,6 @@ $newspack_reset_password_arg = WooCommerce_My_Account::RESET_PASSWORD_URL_PARAM; $newspack_delete_account_arg = WooCommerce_My_Account::DELETE_ACCOUNT_URL_PARAM; -$message = false; -if ( isset( $_GET['message'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $message = $_GET['message']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -} - -$is_error = false; -if ( isset( $_GET['is_error'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $is_error = $_GET['is_error']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -} - $without_password = true === Reader_Activation::is_reader_without_password( $user ); $is_reader = true === Reader_Activation::is_user_reader( $user ); $is_email_change_enabled = true === WooCommerce_My_Account::is_email_change_enabled(); @@ -39,13 +29,6 @@ $display_email = $is_pending_email_change ? $user->get( WooCommerce_My_Account::PENDING_EMAIL_CHANGE_META ) : $user->user_email; ?> - -

> diff --git a/includes/plugins/woocommerce/my-account/templates/v1/group-subscription-members.php b/includes/plugins/woocommerce/my-account/templates/v1/group-subscription-members.php new file mode 100644 index 0000000000..efe6680fdf --- /dev/null +++ b/includes/plugins/woocommerce/my-account/templates/v1/group-subscription-members.php @@ -0,0 +1,262 @@ + 0 && ( count( $members ) + count( $pending_invites ) ) >= $member_limit; +?> + + + diff --git a/includes/plugins/woocommerce/my-account/templates/v1/my-subscriptions.php b/includes/plugins/woocommerce/my-account/templates/v1/my-subscriptions.php index d76878a8df..7ca1cfc604 100644 --- a/includes/plugins/woocommerce/my-account/templates/v1/my-subscriptions.php +++ b/includes/plugins/woocommerce/my-account/templates/v1/my-subscriptions.php @@ -10,6 +10,8 @@ * @package Newspack */ +namespace Newspack; + if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } @@ -30,7 +32,11 @@ - $subscription ) : ?> + $subscription ) : + $is_group_subscription = Group_Subscription::is_group_subscription( $subscription ); + $is_group_member_subscription = $is_group_subscription && Group_Subscription::user_is_member( get_current_user_id(), $subscription ); + ?> get_name() ); } - ?> + if ( $is_group_subscription ) : + ?> + + + + get_status() ) ); ?> get_date_to_display( 'next_payment' ) ); ?> - is_manual() && $subscription->has_status( 'active' ) && $subscription->get_time( 'next_payment' ) > 0 ) : ?> + is_manual() && $subscription->has_status( 'active' ) && $subscription->get_time( 'next_payment' ) > 0 && ! $is_group_member_subscription ) : ?>
get_payment_method_to_display( 'customer' ) ); ?> diff --git a/includes/plugins/woocommerce/my-account/templates/v1/navigation.php b/includes/plugins/woocommerce/my-account/templates/v1/navigation.php index 0f146f7bcd..0e895ae972 100644 --- a/includes/plugins/woocommerce/my-account/templates/v1/navigation.php +++ b/includes/plugins/woocommerce/my-account/templates/v1/navigation.php @@ -7,6 +7,7 @@ * @package Newspack */ +use Newspack\My_Account_UI_V1; use Newspack\Newspack_UI_Icons; if ( ! defined( 'ABSPATH' ) ) { @@ -84,17 +85,10 @@