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_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 );
?>
|
|
|
|
|
|
|
getTimestamp() ) : '—' ); ?> |
|
|
|
|
get_meta( Subscriptions::TRIAL_APPLIED_META ) ) : ?>
|
|
|
|
get_payment_method_title() ) : ?>
|
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 ) )
);
}
}