PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 2.2.0
Subscriptions for WooCommerce with Stripe Recurring Payments v2.2.0
2.4.0 2.3.0 2.2.0 2.1.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 All 65 releases
← All changes | includes/Admin/Menu.php +231 -50 1.10.5 → 2.2.0 View file →
@@ -1,9 +1,15 @@
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
5 10 use SpringDevs\Subscription\Illuminate\Helper;
11 +use SpringDevs\Subscription\Illuminate\Stats;
6 12
7 13 /**
8 14 * Menu class
9 15 *
@@ -18,11 +24,48 @@
18 24 add_action( 'admin_menu', array( $this, 'create_admin_menu' ) );
19 25 add_action( 'admin_menu', array( $this, 'reorder_submenu' ), 999 );
20 26 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
21 27 add_action( 'wp_ajax_subscrpt_bulk_action', array( $this, 'handle_bulk_action_ajax' ) );
28 + add_action( 'admin_init', array( $this, 'maybe_onboarding_redirect' ) );
22 29 }
23 30
24 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 + /**
25 68 * Enqueue admin assets
26 69 */
27 70 public function enqueue_admin_assets() {
28 71 wp_enqueue_style(
@@ -40,16 +83,33 @@
40 83 SUBSCRPT_VERSION,
41 84 true
42 85 );
43 86
44 - // 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.
45 97 wp_enqueue_script(
46 98 'subscrpt-onboarding-wizard',
47 99 SUBSCRPT_ASSETS . '/js/admin/onboarding-wizard.js',
48 - array( 'jquery' ),
100 + array( 'jquery', 'subscrpt_admin_components' ),
49 101 SUBSCRPT_VERSION,
50 102 true
51 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 +
52 112 wp_localize_script(
53 113 'subscrpt-onboarding-wizard',
54 114 'subscrpt_wizard',
55 115 array(
@@ -54,9 +114,16 @@
54 114 'subscrpt_wizard',
55 115 array(
56 116 'ajax_url' => admin_url( 'admin-ajax.php' ),
57 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' ),
58 123 'currency_symbol' => get_woocommerce_currency_symbol(),
124 + 'is_pro' => subscrpt_pro_activated(),
125 + 'has_products' => $subscrpt_wizard_has_products,
59 126 )
60 127 );
61 128
62 129 // Localize script for AJAX
@@ -89,9 +156,9 @@
89 156 __( 'WPSubscription', 'subscription' ),
90 157 __( 'WPSubscription', 'subscription' ),
91 158 'manage_options',
92 159 $parent_slug,
93 - array( $this, 'render_subscriptions_page' ),
160 + array( $this, 'render_dashboard_page' ),
94 161 $icon_url,
95 162 40
96 163 );
97 164
@@ -105,15 +172,28 @@
105 172 'wp-subscription-onboarding',
106 173 array( $this, 'render_onboarding_wizard' )
107 174 );
108 175
109 - // 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.
110 178 add_submenu_page(
111 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,
112 192 __( 'Subscriptions', 'subscription' ),
113 193 __( 'Subscriptions', 'subscription' ),
114 194 'manage_options',
115 - $parent_slug,
195 + 'wp-subscription-list',
116 196 array( $this, 'render_subscriptions_page' )
117 197 );
118 198
119 199 // Subscription Details (hidden from menu with CSS — accessed via admin.php?page=wp-subscription-details&id=ID).
@@ -166,16 +246,24 @@
166 246 'wp-subscription-support',
167 247 array( $this, 'render_support_page' )
168 248 );
169 249
170 - // 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 + */
171 260 add_submenu_page(
172 261 'woocommerce',
173 - __( 'WPSubscription', 'subscription' ),
174 - __( 'WPSubscription', 'subscription' ),
262 + __( 'Subscriptions', 'subscription' ),
263 + __( 'Subscriptions', 'subscription' ),
175 264 'manage_options',
176 - 'wp-subscription',
177 - array( $this, 'render_subscriptions_page' )
265 + 'admin.php?page=wp-subscription-list'
178 266 );
179 267 }
180 268
181 269 /**
@@ -196,26 +284,25 @@
196 284 return;
197 285 }
198 286
199 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.
200 293 $default_order = [
201 - 'wp-subscription' => 10, // Subscriptions
202 - 'wp-subscription-stats' => 20, // Reports
203 - 'wp-subscription-delivery' => 30, // Delivery (pro)
204 - 'wp-subscription-health' => 50, // Health
205 - 'wp-subscription-integrations' => 60, // Integrations
206 - '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
207 300 'wp-subscription-settings' => 998, // Settings
208 301 'wp-subscription-license' => 999, // License (pro)
302 + 'wp-subscription-support' => 1000, // Help & Resources
209 303 ];
210 304
211 - // Place pro pages at the bottom if pro is not active.
212 - if ( ! subscrpt_pro_activated() ) {
213 - $default_order['wp-subscription-stats'] = 200;
214 - $default_order['wp-subscription-delivery'] = 210;
215 - $default_order['wp-subscription-health'] = 220;
216 - }
217 -
218 305 /**
219 306 * Filter the WPSubscription submenu slug order.
220 307 *
221 308 * Each entry is a slug => integer position pair. Lower positions appear
@@ -221,11 +308,11 @@
221 308 * Each entry is a slug => integer position pair. Lower positions appear
222 309 * first. Use gaps of 10 between built-in positions so extensions can
223 310 * insert their own slugs between existing items without renumbering.
224 311 *
225 - * Example (pro plugin adding Delivery at position 35):
312 + * Example (an add-on placing its page between Reports and Health):
226 313 * add_filter( 'subscrpt_submenu_order', function( $order ) {
227 - * $order['wp-subscription-delivery'] = 35;
314 + * $order['wp-subscription-my-addon'] = 45;
228 315 * return $order;
229 316 * } );
230 317 *
231 318 * @param array<string,int> $order Map of slug => position.
@@ -265,11 +352,11 @@
265 352 ?>
266 353 <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
267 354 Made with <span style="color:#e25555;font-size:1.1em;">♥</span> by the WPSubscription Team
268 355 <div style="margin-top:6px;">
269 - <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>
270 357 &nbsp;/&nbsp;
271 - <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>
272 359 </div>
273 360 </div>
274 361 <?php
275 362 }
@@ -297,23 +384,28 @@
297 384 unset( $menu_items ); // Nav no longer rendered in header; navigation is in WP sidebar.
298 385
299 386 // Normalize to a single trail. Explicit breadcrumbs win; otherwise the
300 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 +
301 399 $trail = [];
302 400 if ( ! empty( $breadcrumbs ) ) {
303 401 foreach ( $breadcrumbs as $crumb ) {
304 402 if ( is_array( $crumb ) && '' !== ( $crumb['label'] ?? '' ) ) {
305 - $trail[] = [
306 - 'label' => (string) $crumb['label'],
307 - 'url' => isset( $crumb['url'] ) ? (string) $crumb['url'] : '',
308 - ];
403 + $trail[] = $make_crumb( $crumb['label'], $crumb['url'] ?? '' );
309 404 }
310 405 }
311 406 } elseif ( '' !== $title ) {
312 - $trail[] = [
313 - 'label' => $title,
314 - 'url' => '',
315 - ];
407 + $trail[] = $make_crumb( $title, '' );
316 408 }
317 409
318 410 $last_index = count( $trail ) - 1;
319 411 ?>
@@ -325,17 +417,51 @@
325 417 <span class="dashicons dashicons-admin-home"></span>
326 418 </a>
327 419 <?php foreach ( $trail as $index => $crumb ) : ?>
328 420 <span class="wp-subscription-breadcrumb-sep" aria-hidden="true">/</span>
421 + <?php $crumb_title = $crumb['full'] !== $crumb['label'] ? ' title="' . esc_attr( $crumb['full'] ) . '"' : ''; ?>
329 422 <?php if ( '' !== $crumb['url'] && $index < $last_index ) : ?>
330 - <a href="<?php echo esc_url( $crumb['url'] ); ?>" class="wp-subscription-breadcrumb-link"><?php echo esc_html( $crumb['label'] ); ?></a>
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>
331 424 <?php else : ?>
332 - <span class="wp-subscription-breadcrumb-current"><?php echo esc_html( $crumb['label'] ); ?></span>
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>
333 426 <?php endif; ?>
334 427 <?php endforeach; ?>
335 428 </nav>
336 429 </div>
337 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 +
338 464 <?php if ( ! class_exists( 'Sdevs_Wc_Subscription_Pro' ) ) : ?>
339 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">
340 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>
341 467 <?php esc_html_e( 'Upgrade to Pro', 'subscription' ); ?>
@@ -348,8 +474,46 @@
348 474 <?php
349 475 }
350 476
351 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 + /**
352 516 * Render Subscriptions page
353 517 */
354 518 public function render_subscriptions_page() {
355 519 $this->render_admin_header( __( 'Subscriptions', 'subscription' ), __( 'Manage your subscriptions', 'subscription' ) );
@@ -359,8 +523,9 @@
359 523 $search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
360 524 $date_filter = isset( $_GET['date_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['date_filter'] ) ) : '';
361 525 $per_page = isset( $_GET['per_page'] ) ? max( 1, intval( $_GET['per_page'] ) ) : 20;
362 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;
363 528
364 529 // Handle form submissions (both filters and bulk actions)
365 530 $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
366 531 if ( 'POST' === $request_method ) {
@@ -390,9 +555,9 @@
390 555 wp_delete_post( $sub_id, true );
391 556 }
392 557 }
393 558
394 - wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription' ) );
559 + wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription-list' ) );
395 560 exit;
396 561 }
397 562 }
398 563
@@ -405,8 +570,11 @@
405 570 }
406 571 if ( ! empty( $_POST['date_filter'] ) ) {
407 572 $filter_params['date_filter'] = sanitize_text_field( wp_unslash( $_POST['date_filter'] ) );
408 573 }
574 + if ( ! empty( $_POST['renewal_due'] ) ) {
575 + $filter_params['renewal_due'] = absint( $_POST['renewal_due'] );
576 + }
409 577 if ( ! empty( $_POST['s'] ) ) {
410 578 $filter_params['s'] = sanitize_text_field( wp_unslash( $_POST['s'] ) );
411 579 }
412 580 if ( ! empty( $_POST['per_page'] ) ) {
@@ -412,9 +580,9 @@
412 580 if ( ! empty( $_POST['per_page'] ) ) {
413 581 $filter_params['per_page'] = intval( $_POST['per_page'] );
414 582 }
415 583
416 - $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' ) );
417 585 wp_safe_redirect( $redirect_url );
418 586 exit;
419 587 }
420 588 }
@@ -447,9 +615,9 @@
447 615 foreach ( $trash_posts as $trash_id ) {
448 616 wp_delete_post( $trash_id, true );
449 617 }
450 618
451 - 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' ) );
452 620 exit;
453 621 } else {
454 622 // For other actions, verify nonce with subscription ID.
455 623 $nonce_action = 'wpsubs_action_' . $sub_id;
@@ -457,9 +625,9 @@
457 625 echo '<div class="notice notice-error"><p>' . esc_html__( 'Security check failed. Please try again.', 'subscription' ) . '</p></div>';
458 626 wp_die();
459 627 }
460 628
461 - $redirect_url = admin_url( 'admin.php?page=wp-subscription' );
629 + $redirect_url = admin_url( 'admin.php?page=wp-subscription-list' );
462 630
463 631 switch ( $action ) {
464 632 case 'duplicate':
465 633 $post = get_post( $sub_id );
@@ -488,9 +656,9 @@
488 656 wp_untrash_post( $sub_id );
489 657 break;
490 658 case 'delete':
491 659 wp_delete_post( $sub_id, true );
492 - $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' );
493 661 break;
494 662 }
495 663
496 664 wp_safe_redirect( $redirect_url );
@@ -527,9 +695,24 @@
527 695 'year' => intval( $year ),
528 696 'month' => intval( $month ),
529 697 ];
530 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 );
531 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 +
532 715 $query = new \WP_Query( $args );
533 716 $subscriptions = $query->posts;
534 717 $total = $query->found_posts;
535 718 $max_num_pages = $query->max_num_pages;
@@ -541,11 +724,11 @@
541 724 ?>
542 725 <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
543 726 Made with <span style="color:#e25555;font-size:1.1em;">♥</span> by the WPSubscription Team
544 727 <div style="margin-top:6px;">
545 - <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>
546 729 &nbsp;/&nbsp;
547 - <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>
548 731 </div>
549 732 </div>
550 733 <?php
551 734 }
@@ -568,9 +751,9 @@
568 751 if ( ! current_user_can( 'edit_post', $subscription_id ) ) {
569 752 wp_die( esc_html__( 'You do not have permission to view this subscription.', 'subscription' ) );
570 753 }
571 754
572 - $list_url = admin_url( 'admin.php?page=wp-subscription' );
755 + $list_url = admin_url( 'admin.php?page=wp-subscription-list' );
573 756 $form_action = admin_url( 'admin.php?page=wp-subscription-details&id=' . $subscription_id );
574 757
575 758 // Handle the status-change submission.
576 759 $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
@@ -618,8 +801,9 @@
618 801 'active' => array( 'pe_cancelled', 'cancelled' ),
619 802 'pe_cancelled' => array( 'active', 'cancelled' ),
620 803 'cancelled' => array( 'active' ),
621 804 'expired' => array( 'active', 'cancelled' ),
805 + 'completed' => array( 'cancelled' ),
622 806 );
623 807
624 808 $status = get_post_status( $subscription_id );
625 809 $actions = $map_actions[ $status ] ?? array();
@@ -720,17 +904,14 @@
720 904 * SPA-style: all sections rendered at once, JS controls visibility
721 905 * Initial load always shows page 1 (JS handles transitions from there)
722 906 */
723 907 public function render_onboarding_wizard() {
724 - // Start session if not already started
908 + // Start session if not already started (used by the wizard reset handler).
725 909 if ( ! session_id() && ! headers_sent() ) {
726 910 session_start();
727 911 }
728 912
729 - // Always start at page 1 on direct load (SPA behavior — JS drives page transitions)
730 - $GLOBALS['wizard_page'] = 1;
731 -
732 - $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' ) );
733 914 include __DIR__ . '/views/onboarding-wizard.php';
734 915 $this->render_admin_footer();
735 916 }
736 917