diff --git a/includes/class-newspack-ui-icons.php b/includes/class-newspack-ui-icons.php index 719178a2f4..6c2a6e1860 100644 --- a/includes/class-newspack-ui-icons.php +++ b/includes/class-newspack-ui-icons.php @@ -174,6 +174,10 @@ public static function sanitize_svgs() { '', + 'lineSolid' => + '', 'login' => ' Checkbox/Radio Lists
  • Order table
  • Buttons
  • +
  • Tabs
  • Buttons Icon
  • Modals
  • @@ -896,6 +897,55 @@ public static function return_demo_content() {
    +

    Tabs

    +

    Underline-style tabs. Built on Ghost button + bottom box-shadow. Drop-in replacement for newspack-ui__segmented-control where the underline aesthetic is preferred.

    + +

    Default

    +
    +
    + + + +
    +
    + +

    With count badges

    +
    +
    + + +
    +
    + +

    Small — with panels

    +
    +
    + + +
    +
    +

    +

    +
    +
    + +

    X-Small — stretch (equal width)

    +
    +
    + + + +
    +
    + +
    +

    Buttons Icon

    Uses the same classes as the newspack-ui__button but we add an extra class to it newspack-ui__button--icon

    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 index 7eec758800..46dc2d18d1 100644 --- a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-myaccount.php +++ b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-myaccount.php @@ -18,6 +18,11 @@ class Group_Subscription_MyAccount { */ const MANAGE_MEMBERS_ENDPOINT = 'manage-members'; + /** + * Group page endpoint slug. + */ + const GROUP_ENDPOINT = 'group'; + /** * Nonce action for the invite member form. */ @@ -33,6 +38,11 @@ class Group_Subscription_MyAccount { */ const REMOVE_MEMBER_NONCE_ACTION = 'newspack_group_subscription_remove_member'; + /** + * Nonce action for the leave-group (self-removal) form. + */ + const LEAVE_GROUP_NONCE_ACTION = 'newspack_group_subscription_leave_group'; + /** * Initialize hooks and filters. */ @@ -43,20 +53,24 @@ public static function init() { } add_action( 'init', [ __CLASS__, 'flush_rewrite_rules' ] ); add_filter( 'woocommerce_get_query_vars', [ __CLASS__, 'add_manage_members_endpoint' ] ); - add_action( 'woocommerce_account_' . self::MANAGE_MEMBERS_ENDPOINT . '_endpoint', [ __CLASS__, 'render_group_subscription_members_template' ] ); + add_filter( 'woocommerce_get_query_vars', [ __CLASS__, 'add_group_endpoint' ] ); + add_action( 'woocommerce_account_' . self::GROUP_ENDPOINT . '_endpoint', [ __CLASS__, 'resolve_group_landing' ] ); + // Keep the legacy endpoint addressable; redirect to the new one. + add_action( 'woocommerce_account_' . self::MANAGE_MEMBERS_ENDPOINT . '_endpoint', [ __CLASS__, 'render_manage_members_template_redirect' ] ); add_filter( 'wcs_get_users_subscriptions', [ __CLASS__, 'inject_member_group_subscriptions' ], 15, 2 ); add_filter( 'map_meta_cap', [ __CLASS__, 'grant_group_member_view_order_cap' ], 15, 4 ); add_filter( 'wcs_view_subscription_actions', [ __CLASS__, 'view_subscription_actions' ], 13, 3 ); add_action( 'admin_post_' . self::INVITE_NONCE_ACTION, [ __CLASS__, 'handle_invite_member' ] ); add_action( 'admin_post_' . self::CANCEL_INVITE_NONCE_ACTION, [ __CLASS__, 'handle_cancel_invite' ] ); add_action( 'admin_post_' . self::REMOVE_MEMBER_NONCE_ACTION, [ __CLASS__, 'handle_remove_member' ] ); + add_action( 'admin_post_' . self::LEAVE_GROUP_NONCE_ACTION, [ __CLASS__, 'handle_leave_group' ] ); } /** * Flush rewrite rules for My Account endpoints for group subscriptions. */ public static function flush_rewrite_rules() { - $rewrite_rules_updated_option_name = 'newspack_group_subscription_rewrite_rules_updated'; + $rewrite_rules_updated_option_name = 'newspack_group_subscription_rewrite_rules_updated_v2'; if ( false === get_option( $rewrite_rules_updated_option_name ) ) { flush_rewrite_rules(); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.flush_rewrite_rules_flush_rewrite_rules update_option( $rewrite_rules_updated_option_name, true ); @@ -64,16 +78,19 @@ public static function flush_rewrite_rules() { } /** - * Get the URL to manage members of a group subscription. + * Build a URL to a specific group's page. * - * @param \WC_Subscription $subscription Subscription. + * @param \WC_Subscription|int $subscription Subscription or subscription ID. * * @return string The URL. */ - private static function get_manage_members_url( $subscription ) { + public static function get_group_url( $subscription ) { + $subscription_id = $subscription instanceof \WC_Subscription + ? $subscription->get_id() + : absint( $subscription ); return wc_get_endpoint_url( - self::MANAGE_MEMBERS_ENDPOINT, - $subscription->get_id(), + self::GROUP_ENDPOINT, + $subscription_id, wc_get_page_permalink( 'myaccount' ) ); } @@ -91,49 +108,104 @@ public static function add_manage_members_endpoint( $query_vars ) { } /** - * Render the group subscription members template. + * Add group query var. + * + * @param array $query_vars Query vars. + * + * @return array + */ + public static function add_group_endpoint( $query_vars ) { + $query_vars[ self::GROUP_ENDPOINT ] = self::GROUP_ENDPOINT; + return $query_vars; + } + + /** + * Handle the new `group` endpoint. + * + * @param mixed $value Subscription ID passed as the endpoint value, if any. */ - 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' ) - ) - ); + public static function resolve_group_landing( $value ) { + $user_id = \get_current_user_id(); + $subscription_id = absint( $value ); + + if ( $subscription_id ) { + $subscription = WooCommerce_Subscriptions::sanitize_subscription( $subscription_id ); + if ( ! $subscription || ! Group_Subscription::user_is_manager( $user_id, $subscription ) ) { + wp_safe_redirect( + add_query_arg( + [ + 'message' => __( 'You do not have permission to manage this group.', 'newspack-plugin' ), + 'is_error' => true, + ], + wc_get_account_endpoint_url( 'dashboard' ) + ) + ); + exit; + } + self::render_group_page( $subscription ); + return; + } + + $managed = Group_Subscription::get_managed_subscriptions_for_user( $user_id ); + if ( 0 === count( $managed ) ) { + wp_safe_redirect( wc_get_account_endpoint_url( 'dashboard' ) ); 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' ) - ) - ); + if ( 1 === count( $managed ) ) { + wp_safe_redirect( self::get_group_url( $managed[0] ) ); exit; } + self::render_group_picker( $managed ); + } + + /** + * Render the group page shell. + * + * @param \WC_Subscription $subscription Subscription. + */ + public static function render_group_page( $subscription ) { $args = [ - 'actions' => \wcs_get_all_user_actions_for_subscription( $subscription, $user_id ), 'subscription' => $subscription, - 'view' => 'manage-members', + 'actions' => \wcs_get_all_user_actions_for_subscription( $subscription, \get_current_user_id() ), ]; - \wc_get_template( 'myaccount/group-subscription-members.php', $args ); + \wc_get_template( 'myaccount/group.php', $args ); + } + + /** + * Render the multi-group picker. + * + * @param \WC_Subscription[] $managed Managed group subscriptions. + */ + public static function render_group_picker( $managed ) { + \wc_get_template( 'myaccount/group-picker.php', [ 'managed' => $managed ] ); + } + + /** + * Redirect the legacy manage-members endpoint to the new group endpoint. + * + * @param mixed $value Subscription ID passed as the endpoint value. + */ + public static function render_manage_members_template_redirect( $value ) { + $subscription_id = absint( $value ); + if ( ! $subscription_id ) { + wp_safe_redirect( wc_get_endpoint_url( self::GROUP_ENDPOINT, '', wc_get_page_permalink( 'myaccount' ) ) ); + exit; + } + wp_safe_redirect( + wc_get_endpoint_url( + self::GROUP_ENDPOINT, + $subscription_id, + wc_get_page_permalink( 'myaccount' ) + ), + 308 + ); + exit; } /** * 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. @@ -152,14 +224,7 @@ public static function view_subscription_actions( $actions, $subscription, $user 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' ), - ]; - } - + // Managers reach Members via the new Group sidebar entry / tab — no action button needed. return $actions; } @@ -170,10 +235,47 @@ public static function view_subscription_actions( $actions, $subscription, $user */ 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' ) ); + $redirect_url = self::get_group_url( $subscription_id ); return [ $subscription_id, $redirect_url ]; } + /** + * Whether the subscription is in a state that accepts manager-driven changes + * (invite, cancel-invite, remove-member). Terminal statuses block all writes + * — there's no point inviting someone to a sub that no longer grants access. + * + * @param int|\WC_Subscription $subscription Subscription or ID. + * + * @return bool + */ + public static function is_subscription_manageable( $subscription ): bool { + $subscription = WooCommerce_Subscriptions::sanitize_subscription( $subscription ); + if ( ! $subscription instanceof \WC_Subscription ) { + return false; + } + return ! $subscription->has_status( [ 'cancelled', 'expired', 'trash' ] ); + } + + /** + * Verify the subscription accepts manager changes, redirecting with an error 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. + */ + private static function verify_manageable( $subscription_id, $redirect_url, $active_tab ): void { + if ( self::is_subscription_manageable( $subscription_id ) ) { + return; + } + $error_message = __( 'This group subscription is no longer active, so its members can\'t be changed.', 'newspack-plugin' ); + self::redirect( + new \WP_Error( 'newspack_group_subscription_inactive', $error_message ), + $redirect_url, + $active_tab, + $error_message + ); + } + /** * Verify the current user has permission to manage the subscription, redirecting on failure. * @@ -230,6 +332,7 @@ 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' ); + self::verify_manageable( $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 ); @@ -253,6 +356,7 @@ 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' ); + self::verify_manageable( $subscription_id, $redirect_url, 'invites' ); $email = filter_input( INPUT_POST, 'email', FILTER_SANITIZE_EMAIL ) ?? ''; $result = Group_Subscription_Invite::cancel_invite( $subscription_id, $email ); @@ -337,6 +441,39 @@ public static function grant_group_member_view_order_cap( $caps, $cap, $user_id, return $caps; } + /** + * Handle the leave-group form submission (a member removing themselves). + * + * Unlike manager-driven mutations, this is allowed even on cancelled + * subscriptions — a member should always be able to walk away. + */ + public static function handle_leave_group() { + check_admin_referer( self::LEAVE_GROUP_NONCE_ACTION ); + $subscription_id = isset( $_POST['subscription_id'] ) ? absint( wp_unslash( $_POST['subscription_id'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing + $user_id = get_current_user_id(); + $dashboard_url = function_exists( 'wc_get_account_endpoint_url' ) + ? wc_get_account_endpoint_url( 'dashboard' ) + : home_url(); + + if ( ! $user_id || ! Group_Subscription::user_is_member( $user_id, $subscription_id ) ) { + self::redirect( + new \WP_Error( 'newspack_group_subscription_not_a_member', __( 'You are not a member of this group subscription.', 'newspack-plugin' ) ), + $dashboard_url, + '', + __( 'You are not a member of this group subscription.', 'newspack-plugin' ) + ); + } + + $result = Group_Subscription::update_members( $subscription_id, [], [ $user_id ] ); + + self::redirect( + $result, + $dashboard_url, + '', + __( 'You have left the group subscription.', 'newspack-plugin' ) + ); + } + /** * Handle the remove member form submission. */ @@ -344,19 +481,25 @@ 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' ); + self::verify_manageable( $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 ] ); + $member_id = filter_input( INPUT_POST, 'member_id', FILTER_VALIDATE_INT ) ?? 0; + $result = Group_Subscription::update_members( $subscription_id, [], [ $member_id ] ); + + $member_label = newspack_get_user_display_label( $member_id ); + if ( '' === $member_label ) { + $member_label = (string) $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 + /* translators: 1: removed member's name or email, 2: lowercase singular group label. */ + __( '%1$s has been removed from this %2$s.', 'newspack-plugin' ), + $member_label, + mb_strtolower( Group_Subscription::get_label( 'singular' ) ) ) ); } diff --git a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php index 16f90892ef..165806a62b 100644 --- a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php +++ b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php @@ -66,6 +66,11 @@ public static function init() { \add_filter( 'woocommerce_order_table_search_query_meta_keys', [ __CLASS__, 'add_group_name_hpos_search_field' ] ); \add_filter( 'posts_join', [ __CLASS__, 'search_group_name_join' ], 10, 2 ); \add_filter( 'posts_search', [ __CLASS__, 'search_group_name_where' ], 10, 2 ); + + // Publisher-configurable group label settings. The editing UI lives in the + // Audience wizard's Groups tab; this registration only guards storage + // validation when the options are written directly. + \add_action( 'admin_init', [ __CLASS__, 'register_label_settings' ] ); } /** @@ -254,7 +259,6 @@ public static function get_subscription_settings( $subscription ) { return self::DEFAULT_SETTINGS; } $product_id = WooCommerce_Subscriptions::get_subscription_product_id( $subscription ); - $owner_name = trim( $subscription->get_formatted_billing_full_name() ); $settings = self::get_product_settings( $product_id ); $enabled_meta = $subscription->get_meta( self::GROUP_SUBSCRIPTION_META_PREFIX . 'enabled', true ); $limit_meta = $subscription->get_meta( self::GROUP_SUBSCRIPTION_META_PREFIX . 'limit', true ); @@ -263,14 +267,10 @@ public static function get_subscription_settings( $subscription ) { $settings['limit'] = '' !== $limit_meta ? (int) $limit_meta : $settings['limit']; // Empty string means the meta is unset; any other value, including '0', is a real override. if ( $name_meta ) { $settings['name'] = $name_meta; - } elseif ( $owner_name ) { - $settings['name'] = sprintf( - /* translators: %s: The subscription owner's name. */ - __( '%s’s Group', 'newspack-plugin' ), - $owner_name - ); } else { - $settings['name'] = __( 'Unnamed group', 'newspack-plugin' ); + $product = $product_id ? \wc_get_product( $product_id ) : null; + $product_name = $product ? trim( (string) $product->get_name() ) : ''; + $settings['name'] = '' !== $product_name ? $product_name : Group_Subscription::get_label( 'singular' ); } /** @@ -852,5 +852,32 @@ private static function apply_group_filter( $args, $filter, $group_ids ) { return $args; } + + /** + * Register the publisher-configurable group label settings. + */ + public static function register_label_settings() { + // Group name is unused (no settings_fields() form); registers sanitize_callback via update_option(). + \register_setting( + 'newspack_group_subscription', + 'newspack_group_subscription_label_singular', + [ + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'default' => '', + 'show_in_rest' => false, + ] + ); + \register_setting( + 'newspack_group_subscription', + 'newspack_group_subscription_label_plural', + [ + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'default' => '', + 'show_in_rest' => false, + ] + ); + } } Group_Subscription_Settings::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 fe1bbfc9d6..1f1375a4fe 100644 --- a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription.php +++ b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription.php @@ -18,6 +18,40 @@ class Group_Subscription { */ const GROUP_SUBSCRIPTION_USER_META_KEY = '_newspack_group_subscription'; + /** + * Per-membership join timestamp meta key prefix. + * Full key is `{$prefix}{$subscription_id}` storing a Unix timestamp. + */ + const GROUP_SUBSCRIPTION_JOINED_META_KEY_PREFIX = '_newspack_group_subscription_joined_'; + + /** + * Build the per-subscription joined-at user_meta key. + * + * @param int $subscription_id Subscription ID. + * + * @return string Meta key. + */ + public static function get_member_joined_meta_key( $subscription_id ) { + return self::GROUP_SUBSCRIPTION_JOINED_META_KEY_PREFIX . absint( $subscription_id ); + } + + /** + * Get the Unix timestamp at which a user joined a group subscription. + * + * @param int $user_id The user ID. + * @param \WC_Subscription|int $subscription The subscription object or ID. + * + * @return int|null Unix timestamp, or null if no record exists. + */ + public static function get_member_joined_at( $user_id, $subscription ) { + $subscription = WooCommerce_Subscriptions::sanitize_subscription( $subscription ); + if ( ! $subscription || ! $user_id ) { + return null; + } + $stored = \get_user_meta( $user_id, self::get_member_joined_meta_key( $subscription->get_id() ), true ); + return $stored ? (int) $stored : null; + } + /** * Per-request cache of [sub_id => decoded_name] maps, keyed by user_id + product filter. * @@ -77,6 +111,25 @@ public static function is_group_subscription( $subscription ) { return $settings['enabled']; } + /** + * Get the publisher-configurable container label. + * + * @param string $variant Either 'singular' or 'plural'. Unknown variants fall back to singular. + * + * @return string The override if the publisher has set a non-blank one, otherwise the translated default. + */ + public static function get_label( $variant = 'singular' ) { + $variant = 'plural' === $variant ? 'plural' : 'singular'; + $option_key = 'newspack_group_subscription_label_' . $variant; + $override = trim( (string) \get_option( $option_key, '' ) ); + if ( '' !== $override ) { + return $override; + } + return 'plural' === $variant + ? __( 'Groups', 'newspack-plugin' ) + : __( 'Group', 'newspack-plugin' ); + } + /** * Get the managers of a group subscription. * @@ -97,6 +150,51 @@ public static function get_managers( $subscription ) { return apply_filters( 'newspack_group_subscription_managers', [ $subscription ? $subscription->get_user_id() : 0 ], $subscription ); } + /** + * Get the group subscriptions a user manages (owns). + * + * Mirrors the data-layer side of `get_managers()` — a manager is any user + * who owns a group-enabled subscription. Filters out non-group-enabled subs + * and gifted subscriptions where the user isn't the owner. + * + * @param int $user_id The user ID. + * @param bool $ids_only If true, return only subscription IDs instead of objects. + * + * @return \WC_Subscription[]|int[] The group subscriptions the user manages. + */ + public static function get_managed_subscriptions_for_user( $user_id, $ids_only = false ) { + $user_id = (int) $user_id; + if ( ! $user_id || ! function_exists( 'wcs_get_users_subscriptions' ) ) { + return []; + } + $owned = \wcs_get_users_subscriptions( $user_id ); + $managed = []; + foreach ( $owned as $sub ) { + if ( ! $sub instanceof \WC_Subscription ) { + continue; + } + // wcs_get_users_subscriptions() is filtered to inject subs the user + // is only a *member* of on account pages. Manager detection must + // only accept subs the user actually owns. + if ( (int) $sub->get_customer_id() !== $user_id ) { + continue; + } + $settings = Group_Subscription_Settings::get_subscription_settings( $sub ); + if ( empty( $settings['enabled'] ) ) { + continue; + } + $managed[] = $ids_only ? $sub->get_id() : $sub; + } + + /** + * Filter the group subscriptions a user manages. + * + * @param \WC_Subscription[]|int[] $managed Managed group subscriptions or IDs. + * @param int $user_id The user ID. + */ + return apply_filters( 'newspack_group_subscriptions_managed_for_user', $managed, $user_id ); + } + /** * Get the members of a group subscription. * @@ -167,6 +265,7 @@ public static function update_members( $subscription, $members_to_add, $members_ continue; } if ( \delete_user_meta( $member_id, self::GROUP_SUBSCRIPTION_USER_META_KEY, $subscription->get_id() ) ) { + \delete_user_meta( $member_id, self::get_member_joined_meta_key( $subscription->get_id() ) ); $members_removed[ $member_id ] = [ 'email' => \get_userdata( $member_id )->user_email, 'url' => \get_edit_user_link( $member_id ), @@ -191,6 +290,7 @@ public static function update_members( $subscription, $members_to_add, $members_ continue; } if ( \add_user_meta( $member_id, self::GROUP_SUBSCRIPTION_USER_META_KEY, $subscription->get_id() ) ) { + \update_user_meta( $member_id, self::get_member_joined_meta_key( $subscription->get_id() ), time() ); $members_added[ $member_id ] = [ 'email' => \get_userdata( $member_id )->user_email, 'url' => \get_edit_user_link( $member_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 97133d38b0..5a5bd7fec2 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 @@ -7,6 +7,8 @@ namespace Newspack; +use Newspack\Group_Subscription; +use Newspack\Memberships; use Newspack\Reader_Activation; use Newspack\Reader_Data; use Newspack\WooCommerce_Connection; @@ -40,6 +42,8 @@ public static function init() { \add_filter( 'option_woocommerce_myaccount_add_payment_method_endpoint', [ __CLASS__, 'add_payment_method_endpoint' ] ); \add_filter( 'default_option_woocommerce_myaccount_add_payment_method_endpoint', [ __CLASS__, 'add_payment_method_endpoint' ] ); \add_action( 'template_redirect', [ __CLASS__, 'redirect_payment_information_endpoint' ] ); + \add_action( 'template_redirect', [ __CLASS__, 'redirect_empty_orders_endpoint' ] ); + \add_action( 'template_redirect', [ __CLASS__, 'redirect_empty_payment_methods_endpoint' ] ); \add_action( 'newspack_woocommerce_after_account_payment_methods', [ __CLASS__, 'add_payment_method_modal' ] ); \add_action( 'newspack_woocommerce_after_account_payment_methods', [ __CLASS__, 'delete_payment_method_modals' ] ); \add_action( 'newspack_woocommerce_after_account_addresses', [ __CLASS__, 'add_address_modals' ] ); @@ -207,6 +211,10 @@ 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-picker.php': + return __DIR__ . '/templates/v1/group-picker.php'; + case 'myaccount/group.php': + return __DIR__ . '/templates/v1/group.php'; case 'myaccount/group-subscription-members.php': return __DIR__ . '/templates/v1/group-subscription-members.php'; case 'order/order-again.php': @@ -267,6 +275,40 @@ public static function my_account_menu_items( $items ) { // Remove "Addresses" (replaced by custom "Payment information" page). unset( $items['edit-address'] ); + // Hide "Orders" for anyone with no orders — even admins/non-readers shouldn't see a tab + // that only renders the "No order has been made yet" empty state. + if ( isset( $items['orders'] ) && ! self::current_user_has_orders() ) { + unset( $items['orders'] ); + } + + // Hide "Payment information" for group members who have no orders of their own — + // they're accessing content via someone else's subscription. + if ( isset( $items['payment-methods'] ) && self::current_user_is_group_member_without_orders() ) { + unset( $items['payment-methods'] ); + } + + // Sidebar entry for native group management. Visibility gated by manager-of-at-least-one-group + // AND the existing `Memberships::is_active()` suppression already used by group-subscription UI. + // Inserted immediately after Subscriptions so the two related entries stay adjacent. + if ( ! Memberships::is_active() ) { + $managed = Group_Subscription::get_managed_subscriptions_for_user( \get_current_user_id() ); + $count = count( $managed ); + if ( $count > 0 ) { + $label_variant = $count > 1 ? 'plural' : 'singular'; + $group_entry = [ 'group' => Group_Subscription::get_label( $label_variant ) ]; + if ( isset( $items['subscriptions'] ) ) { + $position = array_search( 'subscriptions', array_keys( $items ), true ); + $items = array_merge( + array_slice( $items, 0, $position + 1, true ), + $group_entry, + array_slice( $items, $position + 1, null, true ) + ); + } else { + $items = array_merge( $items, $group_entry ); + } + } + } + return $items; } @@ -280,7 +322,7 @@ 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' ) ); + return function_exists( 'is_wc_endpoint_url' ) && is_wc_endpoint_url( 'view-subscription' ); } /** @@ -347,7 +389,7 @@ public static function delete_account_modal() { endif; if ( ! empty( $active_subscriptions ) || $active_donations ) : ?> -
    +

    @@ -360,7 +402,7 @@ public static function delete_account_modal() {
    -
    +

    @@ -381,7 +423,7 @@ public static function delete_account_modal() { 'id' => 'delete-account', 'title' => __( 'Delete account', 'newspack-plugin' ), 'content' => $content_send_email, - 'size' => 'medium', + 'size' => 'small', 'actions' => [ 'confirm' => [ 'label' => __( 'Delete account', 'newspack-plugin' ), @@ -616,6 +658,88 @@ public static function redirect_payment_information_endpoint() { } } + /** + * Redirect the Orders endpoint to the My Account dashboard for group members with no orders. + * Pairs with hiding the menu item in `my_account_menu_items()` so bookmarks / typed URLs + * don't dead-end on the empty state. + */ + public static function redirect_empty_orders_endpoint() { + self::maybe_redirect_dead_end_endpoint( 'orders' ); + } + + /** + * Redirect the Payment Methods endpoint to the dashboard for group members with no orders. + */ + public static function redirect_empty_payment_methods_endpoint() { + self::maybe_redirect_dead_end_endpoint( 'payment-methods' ); + } + + /** + * Shared redirect helper: bounce a My Account endpoint to the dashboard when the current + * user is a group-only member (no orders of their own). + * + * @param string $endpoint WooCommerce account endpoint slug. + */ + private static function maybe_redirect_dead_end_endpoint( $endpoint ) { + if ( ! function_exists( 'is_account_page' ) || ! \is_account_page() ) { + return; + } + global $wp; + $current_url = \trailingslashit( \home_url( $wp->request ) ); + if ( \trailingslashit( \wc_get_account_endpoint_url( $endpoint ) ) !== $current_url ) { + return; + } + if ( ! self::current_user_is_group_member_without_orders() ) { + return; + } + \wp_safe_redirect( \wc_get_page_permalink( 'myaccount' ) ); + exit; + } + + /** + * Whether the current user belongs to at least one group subscription and has never + * placed an order of their own. Per-request memoized. + * + * @return bool + */ + private static function current_user_is_group_member_without_orders() { + $user_id = \get_current_user_id(); + if ( ! $user_id || ! class_exists( __NAMESPACE__ . '\\Group_Subscription' ) ) { + return false; + } + $group_subscription_ids = Group_Subscription::get_group_subscriptions_for_user( $user_id, true ); + if ( empty( $group_subscription_ids ) ) { + return false; + } + return ! self::current_user_has_orders(); + } + + /** + * Whether the current user has any orders. Per-request memoized. + * + * @return bool + */ + private static function current_user_has_orders() { + static $cached = null; + if ( null !== $cached ) { + return $cached; + } + $user_id = \get_current_user_id(); + if ( ! $user_id || ! function_exists( 'wc_get_orders' ) ) { + $cached = false; + return $cached; + } + $cached = (bool) \wc_get_orders( + [ + 'customer_id' => $user_id, + 'limit' => 1, + 'return' => 'ids', + 'status' => 'any', + ] + ); + return $cached; + } + /** * Render confirmation modals for deleting saved payment methods. * 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 aa50e015e8..ce6012a79d 100644 --- a/includes/plugins/woocommerce/my-account/class-woocommerce-my-account.php +++ b/includes/plugins/woocommerce/my-account/class-woocommerce-my-account.php @@ -432,10 +432,13 @@ public static function my_account_menu_items( $items ) { } } - // Move "Account Details" and "Subscriptions" to the top of the menu. + // Move "Account settings", "Newsletters", and "Subscriptions" to the top of the menu (in that order). if ( isset( $items['subscriptions'] ) ) { $items = [ 'subscriptions' => $items['subscriptions'] ] + $items; } + if ( isset( $items['newsletters'] ) ) { + $items = [ 'newsletters' => $items['newsletters'] ] + $items; + } if ( isset( $items['edit-account'] ) ) { $items = [ 'edit-account' => $items['edit-account'] ] + $items; } diff --git a/includes/plugins/woocommerce/my-account/templates/v1/group-picker.php b/includes/plugins/woocommerce/my-account/templates/v1/group-picker.php new file mode 100644 index 0000000000..1a41f20dd4 --- /dev/null +++ b/includes/plugins/woocommerce/my-account/templates/v1/group-picker.php @@ -0,0 +1,87 @@ +get_date_created() ? $a->get_date_created()->getTimestamp() : 0; + $tb = $b->get_date_created() ? $b->get_date_created()->getTimestamp() : 0; + return $tb <=> $ta; + } +); +?> + 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 index a5aa0bedd5..3a4884afca 100644 --- a/includes/plugins/woocommerce/my-account/templates/v1/group-subscription-members.php +++ b/includes/plugins/woocommerce/my-account/templates/v1/group-subscription-members.php @@ -10,7 +10,6 @@ namespace Newspack; -use Newspack\WooCommerce_Subscriptions; use Newspack\Newspack_UI_Icons; defined( 'ABSPATH' ) || exit; @@ -24,108 +23,57 @@ $current_user_id = get_current_user_id(); $invite_link = Group_Subscription_Invite::get_link_invite( $subscription, $current_user_id ); $invite_link_url = $invite_link ? Group_Subscription_Invite::get_link_invite_url( $subscription->get_id(), $current_user_id, $invite_link['key'] ) : ''; -$is_at_limit = $member_limit > 0 && ( count( $members ) + count( $pending_invites ) ) >= $member_limit; -$active_tab = ( isset( $_GET['activeTab'] ) && 'invites' === sanitize_key( wp_unslash( $_GET['activeTab'] ) ) ) ? 'invites' : 'members'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended +$is_at_limit = $member_limit > 0 && ( count( $members ) + count( $pending_invites ) ) >= $member_limit; +$is_manageable = Group_Subscription_MyAccount::is_subscription_manageable( $subscription ); +$active_tab = ( isset( $_GET['activeTab'] ) && 'invites' === sanitize_key( wp_unslash( $_GET['activeTab'] ) ) ) ? 'invites' : 'members'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended +$group_label_lower = mb_strtolower( Group_Subscription::get_label( 'singular' ) ); +$is_completely_empty = empty( $members ) && empty( $all_invites ); ?> - - -