PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 2.0.0
Subscriptions for WooCommerce with Stripe Recurring Payments v2.0.0
2.0.0 1.11.2 1.11.1 1.11.0 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.6 1.9.5 trunk 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 All 61 releases
← All changes | includes/Admin/Menu.php +382 -45 1.10.22.0.0 View file →
@@ -1,8 +1,16 @@
1 1 <?php
2 +/**
3 + * Admin menu + shared admin header/breadcrumb renderer.
4 + *
5 + * @package SpringDevs\Subscription\Admin
6 + */
2 7
3 8 namespace SpringDevs\Subscription\Admin;
4 9
10 +use SpringDevs\Subscription\Illuminate\Helper;
11 +use SpringDevs\Subscription\Illuminate\Stats;
12 +
5 13 /**
6 14 * Menu class
7 15 *
8 16 * @package SpringDevs\Subscription\Admin
@@ -16,11 +24,48 @@
16 24 add_action( 'admin_menu', array( $this, 'create_admin_menu' ) );
17 25 add_action( 'admin_menu', array( $this, 'reorder_submenu' ), 999 );
18 26 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
19 27 add_action( 'wp_ajax_subscrpt_bulk_action', array( $this, 'handle_bulk_action_ajax' ) );
28 + add_action( 'admin_init', array( $this, 'maybe_onboarding_redirect' ) );
20 29 }
21 30
22 31 /**
32 + * Send a first-time user to the onboarding wizard when they open the
33 + * WPSubscription dashboard with no plan created yet.
34 + *
35 + * This runs on the dashboard visit rather than on activation, so it works
36 + * regardless of when WooCommerce gets installed (the plugin only loads its
37 + * admin once WooCommerce is active). A persistent "seen" flag makes it fire
38 + * at most once, so the user is never trapped away from the dashboard.
39 + *
40 + * @return void
41 + */
42 + public function maybe_onboarding_redirect() {
43 + // Only on the WPSubscription dashboard page.
44 + if ( ! isset( $_GET['page'] ) || 'wp-subscription' !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- menu navigation, no state change.
45 + return;
46 + }
47 +
48 + if ( wp_doing_ajax() || is_network_admin() || ! current_user_can( 'manage_options' ) ) {
49 + return;
50 + }
51 +
52 + // Fire at most once, ever.
53 + if ( get_option( 'subscrpt_onboarding_seen' ) ) {
54 + return;
55 + }
56 + update_option( 'subscrpt_onboarding_seen', 1, false );
57 +
58 + // Only first-time users with no plan yet.
59 + if ( ! empty( \SpringDevs\Subscription\Illuminate\Plans\PlanRepository::get_groups() ) ) {
60 + return;
61 + }
62 +
63 + wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription-onboarding' ) );
64 + exit;
65 + }
66 +
67 + /**
23 68 * Enqueue admin assets
24 69 */
25 70 public function enqueue_admin_assets() {
26 71 wp_enqueue_style(
@@ -38,16 +83,33 @@
38 83 SUBSCRPT_VERSION,
39 84 true
40 85 );
41 86
42 - // Enqueue onboarding wizard JS (loaded on wizard page)
87 + // Enqueue onboarding wizard styles.
88 + wp_enqueue_style(
89 + 'subscrpt-onboarding-wizard',
90 + SUBSCRPT_ASSETS . '/css/admin/onboarding-wizard.css',
91 + array(),
92 + SUBSCRPT_VERSION
93 + );
94 +
95 + // Enqueue onboarding wizard JS (loaded on wizard page). Depends on the
96 + // admin components so the cadence picker (adv-select) is ready.
43 97 wp_enqueue_script(
44 98 'subscrpt-onboarding-wizard',
45 99 SUBSCRPT_ASSETS . '/js/admin/onboarding-wizard.js',
46 - array( 'jquery' ),
100 + array( 'jquery', 'subscrpt_admin_components' ),
47 101 SUBSCRPT_VERSION,
48 102 true
49 103 );
104 + $subscrpt_wizard_has_products = (bool) wc_get_products(
105 + array(
106 + 'status' => array( 'publish', 'draft', 'pending', 'private' ),
107 + 'limit' => 1,
108 + 'return' => 'ids',
109 + )
110 + );
111 +
50 112 wp_localize_script(
51 113 'subscrpt-onboarding-wizard',
52 114 'subscrpt_wizard',
53 115 array(
@@ -52,9 +114,16 @@
52 114 'subscrpt_wizard',
53 115 array(
54 116 'ajax_url' => admin_url( 'admin-ajax.php' ),
55 117 'subscriptions_url' => admin_url( 'admin.php?page=wp-subscription' ),
118 + 'dashboard_url' => admin_url( 'admin.php?page=wp-subscription' ),
119 + 'products_url' => admin_url( 'edit.php?post_type=product' ),
120 + 'plans_url' => admin_url( 'admin.php?page=wp-subscription-plans' ),
121 + 'rest_url' => rest_url( 'wpsubscription/v1/plans' ),
122 + 'rest_nonce' => wp_create_nonce( 'wp_rest' ),
56 123 'currency_symbol' => get_woocommerce_currency_symbol(),
124 + 'is_pro' => subscrpt_pro_activated(),
125 + 'has_products' => $subscrpt_wizard_has_products,
57 126 )
58 127 );
59 128
60 129 // Localize script for AJAX
@@ -87,14 +156,15 @@
87 156 __( 'WPSubscription', 'subscription' ),
88 157 __( 'WPSubscription', 'subscription' ),
89 158 'manage_options',
90 159 $parent_slug,
91 - array( $this, 'render_subscriptions_page' ),
160 + array( $this, 'render_dashboard_page' ),
92 161 $icon_url,
93 162 40
94 163 );
95 164
96 165 // Onboarding Wizard (hidden from menu with CSS. can be accessed via direct URL: admin.php?page=wp-subscription-onboarding)
166 + // CSS Record: admin.css -> `.wp-submenu a[href*="page=wp-subscription-onboarding"]`
97 167 add_submenu_page(
98 168 $parent_slug,
99 169 __( 'Setup Wizard', 'subscription' ),
100 170 __( 'Setup Wizard', 'subscription' ),
@@ -102,16 +172,40 @@
102 172 'wp-subscription-onboarding',
103 173 array( $this, 'render_onboarding_wizard' )
104 174 );
105 175
106 - // Subscriptions List
176 + // Overview. WordPress makes the first submenu entry share the parent
177 + // slug, so this is the page the top-level item opens.
107 178 add_submenu_page(
108 179 $parent_slug,
180 + __( 'Overview', 'subscription' ),
181 + __( 'Overview', 'subscription' ),
182 + 'manage_options',
183 + $parent_slug,
184 + array( $this, 'render_dashboard_page' )
185 + );
186 +
187 + // Subscriptions List. Moved off the parent slug when the dashboard took
188 + // it; render_dashboard_page() redirects here when the request carries
189 + // list-only query arguments, so old bookmarks still work.
190 + add_submenu_page(
191 + $parent_slug,
109 192 __( 'Subscriptions', 'subscription' ),
110 193 __( 'Subscriptions', 'subscription' ),
111 194 'manage_options',
195 + 'wp-subscription-list',
196 + array( $this, 'render_subscriptions_page' )
197 + );
198 +
199 + // Subscription Details (hidden from menu with CSS — accessed via admin.php?page=wp-subscription-details&id=ID).
200 + // CSS Record: admin.css -> `.wp-submenu a[href*="page=wp-subscription-details"]`
201 + add_submenu_page(
112 202 $parent_slug,
113 - array( $this, 'render_subscriptions_page' )
203 + __( 'Subscription Details', 'subscription' ),
204 + __( 'Subscription Details', 'subscription' ),
205 + 'manage_options',
206 + 'wp-subscription-details',
207 + array( $this, 'render_subscription_details_page' )
114 208 );
115 209
116 210 // Stats Overview
117 211 add_submenu_page(
@@ -152,16 +246,24 @@
152 246 'wp-subscription-support',
153 247 array( $this, 'render_support_page' )
154 248 );
155 249
156 - // Add WPSubscription link under WooCommerce menu
250 + /*
251 + * Subscriptions link under the WooCommerce menu, opening the list.
252 + *
253 + * A link, not a page: the slug is the list's URL and there is no
254 + * callback, so WordPress registers no page and no hook for it.
255 + * Registering `wp-subscription-list` under `woocommerce` as well would
256 + * give the list a second hookname (`woocommerce_page_…` beside
257 + * `wpsubscription_page_…`), and whichever parent WordPress found first
258 + * would set the page's $hook_suffix, which its asset loading keys on.
259 + */
157 260 add_submenu_page(
158 261 'woocommerce',
159 - __( 'WPSubscription', 'subscription' ),
160 - __( 'WPSubscription', 'subscription' ),
262 + __( 'Subscriptions', 'subscription' ),
263 + __( 'Subscriptions', 'subscription' ),
161 264 'manage_options',
162 - 'wp-subscription',
163 - array( $this, 'render_subscriptions_page' )
265 + 'admin.php?page=wp-subscription-list'
164 266 );
165 267 }
166 268
167 269 /**
@@ -182,26 +284,25 @@
182 284 return;
183 285 }
184 286
185 287 // slug => position. Use gaps of 10 so extensions can insert between items.
288 + //
289 + // The order is the same whether or not pro is active: a locked page
290 + // carries a pro badge but keeps its place, so the menu does not
291 + // rearrange itself the moment a licence is activated. Plans and
292 + // Cancellation Flow position themselves through the filter below.
186 293 $default_order = [
187 - 'wp-subscription' => 10, // Subscriptions
188 - 'wp-subscription-stats' => 20, // Reports
189 - 'wp-subscription-delivery' => 30, // Delivery (pro)
190 - 'wp-subscription-health' => 50, // Health
191 - 'wp-subscription-integrations' => 60, // Integrations
192 - 'wp-subscription-support' => 70, // Help & Resources
294 + 'wp-subscription' => 5, // Overview
295 + 'wp-subscription-delivery' => 20, // Delivery (pro)
296 + 'wp-subscription-list' => 30, // Subscriptions
297 + 'wp-subscription-stats' => 40, // Reports
298 + 'wp-subscription-health' => 50, // Health
299 + 'wp-subscription-integrations' => 60, // Integrations
193 300 'wp-subscription-settings' => 998, // Settings
194 301 'wp-subscription-license' => 999, // License (pro)
302 + 'wp-subscription-support' => 1000, // Help & Resources
195 303 ];
196 304
197 - // Place pro pages at the bottom if pro is not active.
198 - if ( ! subscrpt_pro_activated() ) {
199 - $default_order['wp-subscription-stats'] = 200;
200 - $default_order['wp-subscription-delivery'] = 210;
201 - $default_order['wp-subscription-health'] = 220;
202 - }
203 -
204 305 /**
205 306 * Filter the WPSubscription submenu slug order.
206 307 *
207 308 * Each entry is a slug => integer position pair. Lower positions appear
@@ -207,11 +308,11 @@
207 308 * Each entry is a slug => integer position pair. Lower positions appear
208 309 * first. Use gaps of 10 between built-in positions so extensions can
209 310 * insert their own slugs between existing items without renumbering.
210 311 *
211 - * Example (pro plugin adding Delivery at position 35):
312 + * Example (an add-on placing its page between Reports and Health):
212 313 * add_filter( 'subscrpt_submenu_order', function( $order ) {
213 - * $order['wp-subscription-delivery'] = 35;
314 + * $order['wp-subscription-my-addon'] = 45;
214 315 * return $order;
215 316 * } );
216 317 *
217 318 * @param array<string,int> $order Map of slug => position.
@@ -251,11 +352,11 @@
251 352 ?>
252 353 <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
253 354 Made with <span style="color:#e25555;font-size:1.1em;">♥</span> by the WPSubscription Team
254 355 <div style="margin-top:6px;">
255 - <a href="https://wpsubscription.co/contact" target="_blank" style="color:#2563eb;text-decoration:none;">Support</a>
356 + <a href="https://wpsubscription.co/contact?utm_source=plugin&utm_medium=admin&utm_campaign=support" target="_blank" style="color:#2563eb;text-decoration:none;">Support</a>
256 357 &nbsp;/&nbsp;
257 - <a href="https://docs.converslabs.com/en" target="_blank" style="color:#2563eb;text-decoration:none;">Docs</a>
358 + <a href="https://docs.wpsubscription.co/en?utm_source=plugin&utm_medium=admin&utm_campaign=docs" target="_blank" style="color:#2563eb;text-decoration:none;">Docs</a>
258 359 </div>
259 360 </div>
260 361 <?php
261 362 }
@@ -261,17 +362,53 @@
261 362 }
262 363 /**
263 364 * Render the admin header.
264 365 *
265 - * @param string $title Page title shown on the left.
266 - * @param string $subtitle Optional subtitle shown below the title.
366 + * The breadcrumb after the home icon is built from $breadcrumbs when given —
367 + * an ordered trail supporting any number of levels — otherwise it falls back
368 + * to the single $title segment (backward compatible with existing callers).
369 + *
370 + * Each breadcrumb item is an array: `[ 'label' => string, 'url' => string ]`.
371 + * Items with a non-empty `url` render as links, except the last item, which is
372 + * always the current (non-linked) page.
373 + *
374 + * @param string $title Page title shown as the current segment when no
375 + * $breadcrumbs trail is supplied.
376 + * @param string $subtitle Optional subtitle (reserved; not rendered).
377 + * @param array $breadcrumbs Ordered trail of `[ 'label', 'url' ]` items.
267 378 */
268 - public function render_admin_header( string $title = '', string $subtitle = '' ) {
379 + public function render_admin_header( string $title = '', string $subtitle = '', array $breadcrumbs = [] ) {
269 380 $current = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : 'wp-subscription';
270 381
271 382 // Kept for backward compatibility — extensions may hook here for side-effects.
272 383 $menu_items = apply_filters( 'subscrpt_admin_header_menu_items', [], $current );
273 384 unset( $menu_items ); // Nav no longer rendered in header; navigation is in WP sidebar.
385 +
386 + // Normalize to a single trail. Explicit breadcrumbs win; otherwise the
387 + // legacy single $title segment is used.
388 + // A long breadcrumb label is truncated for display; the full text is kept
389 + // so the renderer can add it as a title attribute.
390 + $make_crumb = static function ( $label, $url ) {
391 + $label = (string) $label;
392 + return [
393 + 'label' => subscrpt_truncate_text( $label ),
394 + 'full' => $label,
395 + 'url' => (string) $url,
396 + ];
397 + };
398 +
399 + $trail = [];
400 + if ( ! empty( $breadcrumbs ) ) {
401 + foreach ( $breadcrumbs as $crumb ) {
402 + if ( is_array( $crumb ) && '' !== ( $crumb['label'] ?? '' ) ) {
403 + $trail[] = $make_crumb( $crumb['label'], $crumb['url'] ?? '' );
404 + }
405 + }
406 + } elseif ( '' !== $title ) {
407 + $trail[] = $make_crumb( $title, '' );
408 + }
409 +
410 + $last_index = count( $trail ) - 1;
274 411 ?>
275 412 <div class="wp-subscription-admin-header">
276 413 <div class="wp-subscription-admin-header-inner">
277 414 <div class="wp-subscription-admin-header-left">
@@ -278,15 +415,53 @@
278 415 <nav class="wp-subscription-breadcrumb" aria-label="<?php esc_attr_e( 'Breadcrumb', 'subscription' ); ?>">
279 416 <a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-subscription' ) ); ?>" class="wp-subscription-breadcrumb-home" aria-label="<?php esc_attr_e( 'WPSubscription Home', 'subscription' ); ?>">
280 417 <span class="dashicons dashicons-admin-home"></span>
281 418 </a>
282 - <?php if ( $title ) : ?>
419 + <?php foreach ( $trail as $index => $crumb ) : ?>
283 420 <span class="wp-subscription-breadcrumb-sep" aria-hidden="true">/</span>
284 - <span class="wp-subscription-breadcrumb-current"><?php echo esc_html( $title ); ?></span>
285 - <?php endif; ?>
421 + <?php $crumb_title = $crumb['full'] !== $crumb['label'] ? ' title="' . esc_attr( $crumb['full'] ) . '"' : ''; ?>
422 + <?php if ( '' !== $crumb['url'] && $index < $last_index ) : ?>
423 + <a href="<?php echo esc_url( $crumb['url'] ); ?>" class="wp-subscription-breadcrumb-link"<?php echo $crumb_title; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- attribute pre-escaped. ?>><?php echo esc_html( $crumb['label'] ); ?></a>
424 + <?php else : ?>
425 + <span class="wp-subscription-breadcrumb-current"<?php echo $crumb_title; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- attribute pre-escaped. ?>><?php echo esc_html( $crumb['label'] ); ?></span>
426 + <?php endif; ?>
427 + <?php endforeach; ?>
286 428 </nav>
287 429 </div>
288 430 <div class="wp-subscription-admin-header-right">
431 + <?php
432 + /**
433 + * Filters the pro licence state shown in the admin header.
434 + *
435 + * This plugin cannot ask the pro plugin directly — it runs alone
436 + * on nearly every install, so naming a symbol pro declares would
437 + * fatal there. Pro answers this filter when it is present; when
438 + * it is not, the value stays null and no badge is rendered.
439 + *
440 + * @since 1.11.3
441 + *
442 + * @param array|null $license {
443 + * Licence state, or null when pro is not installed.
444 + *
445 + * @type bool $active Whether the licence is valid.
446 + * @type string $url Admin URL of the licence page.
447 + * }
448 + */
449 + $license = apply_filters( 'subscrpt_admin_header_license', null );
450 +
451 + // Only the "Activate license" badge is shown; the "License active"
452 + // badge is intentionally hidden from the header.
453 + if ( is_array( $license ) && isset( $license['active'] ) && ! $license['active'] ) :
454 + $license_url = isset( $license['url'] ) ? (string) $license['url'] : '';
455 + ?>
456 + <a href="<?php echo esc_url( $license_url ); ?>" class="wpsubs-badge wpsubs-badge--warning wp-subscription-license-badge">
457 + <span class="wpsubs-badge__dot"></span>
458 + <?php esc_html_e( 'Activate license', 'subscription' ); ?>
459 + </a>
460 + <?php
461 + endif;
462 + ?>
463 +
289 464 <?php if ( ! class_exists( 'Sdevs_Wc_Subscription_Pro' ) ) : ?>
290 465 <a target="_blank" href="https://wpsubscription.co/?utm_source=plugin&utm_medium=admin&utm_campaign=upgrade_pro" class="wpsubs-btn wpsubs-btn--primary wpsubs-btn--sm" rel="noreferrer noopener">
291 466 <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" style="flex-shrink:0;"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19 19h-14c-.5 0 -.9 -.3 -1 -.8l-2 -10c0 -.4 .1 -.8 .5 -1.1c.4 -.2 .8 -.2 1.1 0l4.1 3.3l3.4 -5.1c.4 -.6 1.3 -.6 1.7 0l3.4 5.1l4.1 -3.3c.3 -.3 .8 -.3 1.1 0c.4 .2 .5 .6 .5 1.1l-2 10c0 .5 -.5 .8 -1 .8z"/></svg>
292 467 <?php esc_html_e( 'Upgrade to Pro', 'subscription' ); ?>
@@ -299,8 +474,46 @@
299 474 <?php
300 475 }
301 476
302 477 /**
478 + * Render the dashboard, or hand off to the list.
479 + *
480 + * The subscriptions list used to live on this slug. Anything still linking
481 + * here with a list-only argument — a saved filter, a bookmarked search, a
482 + * pagination link — means the list, so it is sent there with its arguments
483 + * intact rather than landing on a dashboard that ignores them. The one
484 + * exception is `post_status`, renamed to the `subscrpt_status` the list reads.
485 + *
486 + * @return void
487 + */
488 + public function render_dashboard_page() {
489 + // phpcs:disable WordPress.Security.NonceVerification.Recommended
490 + $list_args = array( 'post_status', 's', 'paged', 'filter_action', 'orderby', 'order', 'subscrpt_status', 'date_filter', 'per_page', 'renewal_due' );
491 +
492 + foreach ( $list_args as $arg ) {
493 + if ( isset( $_GET[ $arg ] ) && '' !== $_GET[ $arg ] ) {
494 + $query = wp_unslash( $_GET );
495 + $query['page'] = 'wp-subscription-list';
496 +
497 + // The list filters on `subscrpt_status` and ignores `post_status`,
498 + // so forwarding the old name as-is lands on an unfiltered list.
499 + if ( isset( $query['post_status'] ) ) {
500 + if ( empty( $query['subscrpt_status'] ) ) {
501 + $query['subscrpt_status'] = $query['post_status'];
502 + }
503 + unset( $query['post_status'] );
504 + }
505 +
506 + wp_safe_redirect( add_query_arg( array_map( 'sanitize_text_field', $query ), admin_url( 'admin.php' ) ) );
507 + exit;
508 + }
509 + }
510 + // phpcs:enable WordPress.Security.NonceVerification.Recommended
511 +
512 + ( new Dashboard() )->render();
513 + }
514 +
515 + /**
303 516 * Render Subscriptions page
304 517 */
305 518 public function render_subscriptions_page() {
306 519 $this->render_admin_header( __( 'Subscriptions', 'subscription' ), __( 'Manage your subscriptions', 'subscription' ) );
@@ -310,8 +523,9 @@
310 523 $search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
311 524 $date_filter = isset( $_GET['date_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['date_filter'] ) ) : '';
312 525 $per_page = isset( $_GET['per_page'] ) ? max( 1, intval( $_GET['per_page'] ) ) : 20;
313 526 $paged = isset( $_GET['paged'] ) ? max( 1, intval( $_GET['paged'] ) ) : 1;
527 + $renewal_due = isset( $_GET['renewal_due'] ) ? min( 366, absint( $_GET['renewal_due'] ) ) : 0;
314 528
315 529 // Handle form submissions (both filters and bulk actions)
316 530 $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
317 531 if ( 'POST' === $request_method ) {
@@ -341,9 +555,9 @@
341 555 wp_delete_post( $sub_id, true );
342 556 }
343 557 }
344 558
345 - wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription' ) );
559 + wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription-list' ) );
346 560 exit;
347 561 }
348 562 }
349 563
@@ -356,8 +570,11 @@
356 570 }
357 571 if ( ! empty( $_POST['date_filter'] ) ) {
358 572 $filter_params['date_filter'] = sanitize_text_field( wp_unslash( $_POST['date_filter'] ) );
359 573 }
574 + if ( ! empty( $_POST['renewal_due'] ) ) {
575 + $filter_params['renewal_due'] = absint( $_POST['renewal_due'] );
576 + }
360 577 if ( ! empty( $_POST['s'] ) ) {
361 578 $filter_params['s'] = sanitize_text_field( wp_unslash( $_POST['s'] ) );
362 579 }
363 580 if ( ! empty( $_POST['per_page'] ) ) {
@@ -363,9 +580,9 @@
363 580 if ( ! empty( $_POST['per_page'] ) ) {
364 581 $filter_params['per_page'] = intval( $_POST['per_page'] );
365 582 }
366 583
367 - $redirect_url = add_query_arg( $filter_params, admin_url( 'admin.php?page=wp-subscription' ) );
584 + $redirect_url = add_query_arg( $filter_params, admin_url( 'admin.php?page=wp-subscription-list' ) );
368 585 wp_safe_redirect( $redirect_url );
369 586 exit;
370 587 }
371 588 }
@@ -398,9 +615,9 @@
398 615 foreach ( $trash_posts as $trash_id ) {
399 616 wp_delete_post( $trash_id, true );
400 617 }
401 618
402 - wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription&subscrpt_status=trash' ) );
619 + wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription-list&subscrpt_status=trash' ) );
403 620 exit;
404 621 } else {
405 622 // For other actions, verify nonce with subscription ID.
406 623 $nonce_action = 'wpsubs_action_' . $sub_id;
@@ -408,9 +625,9 @@
408 625 echo '<div class="notice notice-error"><p>' . esc_html__( 'Security check failed. Please try again.', 'subscription' ) . '</p></div>';
409 626 wp_die();
410 627 }
411 628
412 - $redirect_url = admin_url( 'admin.php?page=wp-subscription' );
629 + $redirect_url = admin_url( 'admin.php?page=wp-subscription-list' );
413 630
414 631 switch ( $action ) {
415 632 case 'duplicate':
416 633 $post = get_post( $sub_id );
@@ -439,9 +656,9 @@
439 656 wp_untrash_post( $sub_id );
440 657 break;
441 658 case 'delete':
442 659 wp_delete_post( $sub_id, true );
443 - $redirect_url = admin_url( 'admin.php?page=wp-subscription&subscrpt_status=trash' );
660 + $redirect_url = admin_url( 'admin.php?page=wp-subscription-list&subscrpt_status=trash' );
444 661 break;
445 662 }
446 663
447 664 wp_safe_redirect( $redirect_url );
@@ -478,9 +695,24 @@
478 695 'year' => intval( $year ),
479 696 'month' => intval( $month ),
480 697 ];
481 698 }
699 + // Next-renewal window, soonest first. The same arguments as the
700 + // Overview's "Renewals due" figure, so its 7-day rows equal its count.
701 + if ( $renewal_due ) {
702 + $due_args = Stats::renewals_due_args( $renewal_due );
482 703
704 + // Only an active subscription renews, so a renewal window combined
705 + // with any other status matches nothing — the filters AND together.
706 + if ( $status && $due_args['post_status'] !== $status ) {
707 + $args['post__in'] = array( 0 );
708 + }
709 +
710 + $args['post_status'] = $due_args['post_status'];
711 + $args['meta_query'] = $due_args['meta_query']; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
712 + $args['orderby'] = [ 'subscrpt_next_date' => 'ASC' ];
713 + }
714 +
483 715 $query = new \WP_Query( $args );
484 716 $subscriptions = $query->posts;
485 717 $total = $query->found_posts;
486 718 $max_num_pages = $query->max_num_pages;
@@ -492,11 +724,11 @@
492 724 ?>
493 725 <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
494 726 Made with <span style="color:#e25555;font-size:1.1em;">♥</span> by the WPSubscription Team
495 727 <div style="margin-top:6px;">
496 - <a href="https://wpsubscription.co/contact" target="_blank" style="color:#2563eb;text-decoration:none;">Support</a>
728 + <a href="https://wpsubscription.co/contact?utm_source=plugin&utm_medium=admin&utm_campaign=support" target="_blank" style="color:#2563eb;text-decoration:none;">Support</a>
497 729 &nbsp;/&nbsp;
498 - <a href="https://docs.converslabs.com/en" target="_blank" style="color:#2563eb;text-decoration:none;">Docs</a>
730 + <a href="https://docs.wpsubscription.co/en?utm_source=plugin&utm_medium=admin&utm_campaign=docs" target="_blank" style="color:#2563eb;text-decoration:none;">Docs</a>
499 731 </div>
500 732 </div>
501 733 <?php
502 734 }
@@ -501,8 +733,116 @@
501 733 <?php
502 734 }
503 735
504 736 /**
737 + * Render the standalone Subscription Details page.
738 + *
739 + * Replaces the legacy metabox edit screen. Mirrors the list-page design
740 + * shell (header + wpsubs components) and handles the status-change form.
741 + *
742 + * @return void
743 + */
744 + public function render_subscription_details_page() {
745 + $subscription_id = isset( $_GET['id'] ) ? absint( wp_unslash( $_GET['id'] ) ) : 0;
746 +
747 + if ( ! $subscription_id || 'subscrpt_order' !== get_post_type( $subscription_id ) ) {
748 + wp_die( esc_html__( 'Invalid subscription.', 'subscription' ) );
749 + }
750 +
751 + if ( ! current_user_can( 'edit_post', $subscription_id ) ) {
752 + wp_die( esc_html__( 'You do not have permission to view this subscription.', 'subscription' ) );
753 + }
754 +
755 + $list_url = admin_url( 'admin.php?page=wp-subscription-list' );
756 + $form_action = admin_url( 'admin.php?page=wp-subscription-details&id=' . $subscription_id );
757 +
758 + // Handle the status-change submission.
759 + $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
760 + if ( 'POST' === $request_method && isset( $_POST['subscrpt_order_action'] ) ) {
761 + $nonce = isset( $_POST['subscrpt_order_action_nonce_field'] ) ? sanitize_text_field( wp_unslash( $_POST['subscrpt_order_action_nonce_field'] ) ) : '';
762 + if ( ! wp_verify_nonce( $nonce, 'subscrpt_order_action_nonce' ) ) {
763 + wp_die( esc_html__( 'Security check failed.', 'subscription' ) );
764 + }
765 +
766 + $action = sanitize_text_field( wp_unslash( $_POST['subscrpt_order_action'] ) );
767 + if ( '' !== $action ) {
768 + Subscriptions::process_status_change( $subscription_id, $action );
769 + }
770 +
771 + wp_safe_redirect( $form_action );
772 + exit;
773 + }
774 +
775 + // Allowed actions for the current status (mirrors Subscriptions::subscrpt_order_save_post()).
776 + $actions_data = array(
777 + 'active' => array(
778 + 'label' => __( 'Activate Subscription', 'subscription' ),
779 + 'value' => 'active',
780 + ),
781 + 'pending' => array(
782 + 'label' => __( 'Pending Subscription', 'subscription' ),
783 + 'value' => 'pending',
784 + ),
785 + 'expire' => array(
786 + 'label' => __( 'Expire Subscription', 'subscription' ),
787 + 'value' => 'expired',
788 + ),
789 + 'pe_cancelled' => array(
790 + 'label' => __( 'Pending Cancel Subscription', 'subscription' ),
791 + 'value' => 'pe_cancelled',
792 + ),
793 + 'cancelled' => array(
794 + 'label' => __( 'Cancel Subscription', 'subscription' ),
795 + 'value' => 'cancelled',
796 + ),
797 + );
798 +
799 + $map_actions = array(
800 + 'pending' => array( 'active', 'cancelled' ),
801 + 'active' => array( 'pe_cancelled', 'cancelled' ),
802 + 'pe_cancelled' => array( 'active', 'cancelled' ),
803 + 'cancelled' => array( 'active' ),
804 + 'expired' => array( 'active', 'cancelled' ),
805 + 'completed' => array( 'cancelled' ),
806 + );
807 +
808 + $status = get_post_status( $subscription_id );
809 + $actions = $map_actions[ $status ] ?? array();
810 +
811 + // Gather subscription data for the view.
812 + $subscription_data = Helper::get_subscription_data( $subscription_id );
813 +
814 + $order_id = $subscription_data['order']['order_id'] ?? get_post_meta( $subscription_id, '_subscrpt_order_id', true );
815 + $order = $order_id ? wc_get_order( $order_id ) : null;
816 + $order_item_id = $subscription_data['order']['order_item_id'] ?? get_post_meta( $subscription_id, '_subscrpt_order_item_id', true );
817 + $order_item = ( $order && $order_item_id ) ? $order->get_item( $order_item_id ) : null;
818 +
819 + $rows = Subscriptions::get_info_rows( $subscription_id );
820 +
821 + // Related orders.
822 + $order_histories = Helper::get_related_orders( $subscription_id );
823 +
824 + $this->render_admin_header(
825 + '',
826 + '',
827 + array(
828 + array(
829 + 'label' => __( 'Subscriptions', 'subscription' ),
830 + 'url' => $list_url,
831 + ),
832 + array(
833 + 'label' => '#' . $subscription_id,
834 + 'url' => '',
835 + ),
836 + )
837 + );
838 +
839 + include __DIR__ . '/views/subscription-details.php';
840 +
841 + $this->render_admin_footer();
842 + }
843 +
844 + /**
505 845 * Render Stats page
506 846 */
507 847 public function render_stats_page() {
508 848 $this->render_admin_header( __( 'Reports', 'subscription' ), __( 'View your subscription analytics', 'subscription' ) );
@@ -564,17 +904,14 @@
564 904 * SPA-style: all sections rendered at once, JS controls visibility
565 905 * Initial load always shows page 1 (JS handles transitions from there)
566 906 */
567 907 public function render_onboarding_wizard() {
568 - // Start session if not already started
908 + // Start session if not already started (used by the wizard reset handler).
569 909 if ( ! session_id() && ! headers_sent() ) {
570 910 session_start();
571 911 }
572 912
573 - // Always start at page 1 on direct load (SPA behavior — JS drives page transitions)
574 - $GLOBALS['wizard_page'] = 1;
575 -
576 - $this->render_admin_header( __( 'Setup Wizard', 'subscription' ), __( 'Create your first subscription product', 'subscription' ) );
913 + $this->render_admin_header( __( 'Setup Wizard', 'subscription' ), __( 'Create your first subscription plan', 'subscription' ) );
577 914 include __DIR__ . '/views/onboarding-wizard.php';
578 915 $this->render_admin_footer();
579 916 }
580 917