Subscriptions tab. * * Adds two WooCommerce My Account endpoints — a Subscriptions list and a * single-subscription view — where customers see and manage their Better * Payment subscriptions. All management actions reuse the existing * nonce-guarded POST handlers in {@see Subscriptions} (customer cancel, * auto-renewal toggle) and WooCommerce's own key-guarded order-pay URL for * past-due renewal invoices; this class never mutates subscription state * itself. * * Registration notes: * - The endpoints are added through `woocommerce_get_query_vars`, which * WooCommerce itself turns into rewrite endpoints — no direct * `add_rewrite_endpoint()` call is needed. * - Rewrite rules are flushed ONCE, keyed on a stored version option — * never unconditionally on every request. * - The endpoints stay registered even while the My Account Tab setting is * off; the setting gates the menu item and the rendered content, so * toggling it needs no rewrite flush. * - State-changing actions are POST forms with per-order nonces, handled on * `template_redirect` (proper redirect-after-post) — never GET links, and * never handled mid-template after headers are sent. * * @since 2.4.0 */ class MyAccount { /** * My Account endpoint slug for the subscriptions list. The endpoint * value, when present, is the list page number. */ const ENDPOINT = 'bp-subscriptions'; /** * My Account endpoint slug for the single-subscription view. The * endpoint value is the parent (subscription) order id. */ const VIEW_ENDPOINT = 'bp-view-subscription'; /** * Option storing the rewrite version the rules were last flushed for. */ const REWRITE_OPTION = 'better_payment_wc_myaccount_rewrite_version'; /** * Bump to force a one-time rewrite flush after an endpoint change. */ const REWRITE_VERSION = '1'; /** * Subscriptions shown per list page. */ const PER_PAGE = 10; /** * Register the My Account hooks. Called from Loader::register() — only * when WooCommerce is active. * * @return void */ public static function register() { // Endpoint registration. WooCommerce adds a rewrite endpoint for // every query var in this filter, so this is the single source of // registration for both endpoints. add_filter( 'woocommerce_get_query_vars', array( __CLASS__, 'add_query_vars' ) ); // One-time rewrite flush so the new endpoints resolve without a // manual Settings > Permalinks save. add_action( 'init', array( __CLASS__, 'maybe_flush_rewrite_rules' ), 50 ); // The Subscriptions tab, above Logout. add_filter( 'woocommerce_account_menu_items', array( __CLASS__, 'add_menu_item' ), 200 ); // Endpoint content + browser/page titles. add_action( 'woocommerce_account_' . self::ENDPOINT . '_endpoint', array( __CLASS__, 'render_list' ) ); add_action( 'woocommerce_account_' . self::VIEW_ENDPOINT . '_endpoint', array( __CLASS__, 'render_view' ) ); add_filter( 'woocommerce_endpoint_' . self::ENDPOINT . '_title', array( __CLASS__, 'list_title' ) ); add_filter( 'woocommerce_endpoint_' . self::VIEW_ENDPOINT . '_title', array( __CLASS__, 'view_title' ) ); // Status badge / table styling, on My Account pages only. add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_styles' ), 20 ); } /** * Whether the My Account Subscriptions tab is enabled (E-commerce > * Subscription > My Account Subscriptions Tab setting). Read live on * every request — never cached, never inferred. * * @return bool */ public static function enabled() { return 'yes' === Subscriptions::setting( 'myaccount_tab' ); } /** * Register both endpoints as WooCommerce query vars. WooCommerce turns * these into rewrite endpoints on `init`. * * @param array $vars WooCommerce query vars (internal name => slug). * @return array */ public static function add_query_vars( $vars ) { $vars[ self::ENDPOINT ] = self::ENDPOINT; $vars[ self::VIEW_ENDPOINT ] = self::VIEW_ENDPOINT; return $vars; } /** * Flush rewrite rules once per endpoint-schema version. Flushing them * unconditionally rebuilds and re-saves the whole rule set on every * request; keying on an option keeps this a one-time cost. * * @return void */ public static function maybe_flush_rewrite_rules() { if ( self::REWRITE_VERSION === get_option( self::REWRITE_OPTION ) ) { return; } flush_rewrite_rules(); update_option( self::REWRITE_OPTION, self::REWRITE_VERSION ); } /** * Add the Subscriptions tab to the My Account menu (gated on the * My Account Tab setting). * * @param array $items Menu items (endpoint => label). * @return array */ public static function add_menu_item( $items ) { if ( ! self::enabled() ) { return $items; } return self::insert_menu_item( (array) $items, __( 'Subscriptions', 'better-payment' ) ); } /** * Pure: insert the Subscriptions item before Logout, preserving the * existing order. Appends when there is no logout item (a theme may have * removed it), and never duplicates an existing entry. * * @param array $items Menu items (endpoint => label). * @param string $label Tab label. * @return array */ public static function insert_menu_item( $items, $label ) { if ( isset( $items[ self::ENDPOINT ] ) ) { return $items; } $updated = array(); foreach ( $items as $endpoint => $item_label ) { if ( 'customer-logout' === $endpoint ) { $updated[ self::ENDPOINT ] = $label; } $updated[ $endpoint ] = $item_label; } if ( ! isset( $updated[ self::ENDPOINT ] ) ) { $updated[ self::ENDPOINT ] = $label; } return $updated; } /** * Browser/page title for the list endpoint. * * @param string $title Default endpoint title. * @return string */ public static function list_title( $title ) { return __( 'Subscriptions', 'better-payment' ); } /** * Browser/page title for the single-subscription endpoint. * * @param string $title Default endpoint title. * @return string */ public static function view_title( $title ) { $order_id = absint( get_query_var( self::VIEW_ENDPOINT ) ); if ( $order_id > 0 ) { /* translators: %d: subscription (parent order) id */ return sprintf( __( 'Subscription #%d', 'better-payment' ), $order_id ); } return __( 'Subscription', 'better-payment' ); } /** * URL of the subscriptions list (optionally a specific page). * * @param int $page List page number (1 emits the bare endpoint URL). * @return string */ public static function list_url( $page = 1 ) { if ( ! function_exists( 'wc_get_endpoint_url' ) || ! function_exists( 'wc_get_page_permalink' ) ) { return ''; } $page = max( 1, (int) $page ); return wc_get_endpoint_url( self::ENDPOINT, ( $page > 1 ) ? (string) $page : '', wc_get_page_permalink( 'myaccount' ) ); } /** * URL of one subscription's My Account view. * * @param int $order_id Parent (subscription) order id. * @return string */ public static function view_url( $order_id ) { if ( ! function_exists( 'wc_get_endpoint_url' ) || ! function_exists( 'wc_get_page_permalink' ) ) { return ''; } return wc_get_endpoint_url( self::VIEW_ENDPOINT, (string) absint( $order_id ), wc_get_page_permalink( 'myaccount' ) ); } /** * Enqueue the subscription table/badge styles on My Account pages only — * never site-wide (a status stylesheet loaded on every frontend page is * pure weight on the pages that show no subscription). * * @return void */ public static function enqueue_styles() { if ( ! self::enabled() || ! function_exists( 'is_account_page' ) || ! is_account_page() ) { return; } wp_enqueue_style( 'better-payment-wc-subscriptions' ); } /* --------------------------------------------------------------------- * List view * ------------------------------------------------------------------- */ /** * Render the subscriptions list endpoint. * * Ownership is enforced at the query: only the current user's own * subscription parent orders are fetched. Logged-out visitors get * WooCommerce's own My Account login form before this ever runs, but the * guard stays anyway — an endpoint callback must never rely on the page * around it for authentication. * * @param mixed $current_page Endpoint value (list page number). * @return void */ public static function render_list( $current_page ) { if ( ! self::enabled() || ! is_user_logged_in() || ! function_exists( 'wc_get_orders' ) ) { return; } $page = max( 1, absint( $current_page ) ); $result = Subscriptions::customer_subscriptions( get_current_user_id(), $page, self::PER_PAGE ); if ( empty( $result['orders'] ) ) { self::render_empty_list(); return; } ?> get_meta( Subscriptions::STATUS_META ); ?>
get_order_number() ); ?>
1 ) : ?>
get_meta( Subscriptions::STATUS_META ) || get_current_user_id() !== $order->get_customer_id() ) { self::render_invalid_notice(); return; } $status = (string) $order->get_meta( Subscriptions::STATUS_META ); self::render_details_table( $order, $status ); self::render_actions( $order ); self::render_related_orders( $order ); ?>

get_date_created(); $next_payment = self::next_payment_text( $order, $status ); $count = (int) $order->get_meta( Subscriptions::RENEWAL_COUNT_META ); ?>

get_meta( Subscriptions::TRIAL_APPLIED_META ) ) : ?> get_payment_method_title() ) : ?>
getTimestamp() ) : '—' ); ?>
get_payment_method_title() ); ?>
get_order_number() ); ?>
get_customer_id() ) { ?>

get_id(), 'woo' ); if ( empty( $relations ) ) { return; } $rows = array(); foreach ( $relations as $relation ) { $order = wc_get_order( absint( $relation->order_id ) ); if ( ! $order instanceof \WC_Order || get_current_user_id() !== $order->get_customer_id() ) { continue; } $rows[] = array( 'order' => $order, 'type' => (string) $relation->type, ); } if ( empty( $rows ) ) { return; } ?>

get_date_created(); ?>
get_order_number() ); ?> getTimestamp() ) : '—' ); ?> get_status() ) : ucfirst( $order->get_status() ) ); ?> get_formatted_order_total() ); ?>
get_name(); } return implode( ', ', $names ); } /** * "$10.00 / month" — the order total plus the billing schedule * snapshotted on the parent order. * * @param \WC_Order $order Parent (subscription) order. * @return string HTML (wc_price output). */ protected static function amount_text( $order ) { $interval = max( 1, (int) $order->get_meta( Subscriptions::INTERVAL_META ) ); $period = Subscriptions::sanitize_period( $order->get_meta( Subscriptions::PERIOD_META ) ); $price = function_exists( 'wc_price' ) ? wc_price( $order->get_total(), array( 'currency' => $order->get_currency() ) ) : $order->get_currency() . ' ' . $order->get_total(); return sprintf( /* translators: 1: formatted price, 2: billing schedule ("month", "every 3 months") */ __( '%1$s / %2$s', 'better-payment' ), $price, Subscriptions::describe_schedule( $interval, $period ) ); } /** * Next-payment date text — empty on terminal statuses (a cancelled * subscription has no next charge, and a stale date reads as one). * * @param \WC_Order $order Parent (subscription) order. * @param string $status Raw subscription status. * @return string */ protected static function next_payment_text( $order, $status ) { $next = (int) $order->get_meta( Subscriptions::NEXT_PAYMENT_META ); if ( $next <= 0 || ! in_array( $status, array( 'active', 'past_due' ), true ) ) { return ''; } return date_i18n( get_option( 'date_format' ), $next ); } /** * Status badge markup. * * @param string $status Raw subscription status. * @return void */ protected static function render_status_badge( $status ) { $status = strtolower( (string) $status ); printf( '%2$s', esc_attr( sanitize_html_class( $status, 'unknown' ) ), esc_html( Subscriptions::customer_status_label( $status ) ) ); } }