PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 2.4.0
Subscriptions for WooCommerce with Stripe Recurring Payments v2.4.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 +233 -53 1.11.0 → 2.4.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(
@@ -35,9 +78,9 @@
35 78 // Enqueue admin JavaScript for subscription list functionality
36 79 wp_enqueue_script(
37 80 'sdevs_subscription_admin',
38 81 SUBSCRPT_ASSETS . '/js/admin.js',
39 - array( 'jquery' ),
82 + array( 'jquery', 'wp-i18n' ),
40 83 SUBSCRPT_VERSION,
41 84 true
42 85 );
43 86
@@ -48,16 +91,26 @@
48 91 array(),
49 92 SUBSCRPT_VERSION
50 93 );
51 94
52 - // Enqueue onboarding wizard JS (loaded on wizard page)
95 + // Enqueue onboarding wizard JS (loaded on wizard page). Depends on the
96 + // admin components so the cadence picker (adv-select) is ready.
53 97 wp_enqueue_script(
54 98 'subscrpt-onboarding-wizard',
55 99 SUBSCRPT_ASSETS . '/js/admin/onboarding-wizard.js',
56 - array( 'jquery' ),
100 + array( 'jquery', 'subscrpt_admin_components', 'wp-i18n' ),
57 101 SUBSCRPT_VERSION,
58 102 true
59 103 );
104 + wp_set_script_translations( 'subscrpt-onboarding-wizard', 'subscription', SUBSCRPT_PATH . '/languages' );
105 + $subscrpt_wizard_has_products = (bool) wc_get_products(
106 + array(
107 + 'status' => array( 'publish', 'draft', 'pending', 'private' ),
108 + 'limit' => 1,
109 + 'return' => 'ids',
110 + )
111 + );
112 +
60 113 wp_localize_script(
61 114 'subscrpt-onboarding-wizard',
62 115 'subscrpt_wizard',
63 116 array(
@@ -62,9 +115,16 @@
62 115 'subscrpt_wizard',
63 116 array(
64 117 'ajax_url' => admin_url( 'admin-ajax.php' ),
65 118 'subscriptions_url' => admin_url( 'admin.php?page=wp-subscription' ),
119 + 'dashboard_url' => admin_url( 'admin.php?page=wp-subscription' ),
120 + 'products_url' => admin_url( 'edit.php?post_type=product' ),
121 + 'plans_url' => admin_url( 'admin.php?page=wp-subscription-plans' ),
122 + 'rest_url' => rest_url( 'wpsubscription/v1/plans' ),
123 + 'rest_nonce' => wp_create_nonce( 'wp_rest' ),
66 124 'currency_symbol' => get_woocommerce_currency_symbol(),
125 + 'is_pro' => subscrpt_pro_activated(),
126 + 'has_products' => $subscrpt_wizard_has_products,
67 127 )
68 128 );
69 129
70 130 // Localize script for AJAX
@@ -97,9 +157,9 @@
97 157 __( 'WPSubscription', 'subscription' ),
98 158 __( 'WPSubscription', 'subscription' ),
99 159 'manage_options',
100 160 $parent_slug,
101 - array( $this, 'render_subscriptions_page' ),
161 + array( $this, 'render_dashboard_page' ),
102 162 $icon_url,
103 163 40
104 164 );
105 165
@@ -113,15 +173,28 @@
113 173 'wp-subscription-onboarding',
114 174 array( $this, 'render_onboarding_wizard' )
115 175 );
116 176
117 - // Subscriptions List
177 + // Overview. WordPress makes the first submenu entry share the parent
178 + // slug, so this is the page the top-level item opens.
118 179 add_submenu_page(
119 180 $parent_slug,
181 + __( 'Overview', 'subscription' ),
182 + __( 'Overview', 'subscription' ),
183 + 'manage_options',
184 + $parent_slug,
185 + array( $this, 'render_dashboard_page' )
186 + );
187 +
188 + // Subscriptions List. Moved off the parent slug when the dashboard took
189 + // it; render_dashboard_page() redirects here when the request carries
190 + // list-only query arguments, so old bookmarks still work.
191 + add_submenu_page(
192 + $parent_slug,
120 193 __( 'Subscriptions', 'subscription' ),
121 194 __( 'Subscriptions', 'subscription' ),
122 195 'manage_options',
123 - $parent_slug,
196 + 'wp-subscription-list',
124 197 array( $this, 'render_subscriptions_page' )
125 198 );
126 199
127 200 // Subscription Details (hidden from menu with CSS — accessed via admin.php?page=wp-subscription-details&id=ID).
@@ -174,16 +247,24 @@
174 247 'wp-subscription-support',
175 248 array( $this, 'render_support_page' )
176 249 );
177 250
178 - // Add WPSubscription link under WooCommerce menu
251 + /*
252 + * Subscriptions link under the WooCommerce menu, opening the list.
253 + *
254 + * A link, not a page: the slug is the list's URL and there is no
255 + * callback, so WordPress registers no page and no hook for it.
256 + * Registering `wp-subscription-list` under `woocommerce` as well would
257 + * give the list a second hookname (`woocommerce_page_…` beside
258 + * `wpsubscription_page_…`), and whichever parent WordPress found first
259 + * would set the page's $hook_suffix, which its asset loading keys on.
260 + */
179 261 add_submenu_page(
180 262 'woocommerce',
181 - __( 'WPSubscription', 'subscription' ),
182 - __( 'WPSubscription', 'subscription' ),
263 + __( 'Subscriptions', 'subscription' ),
264 + __( 'Subscriptions', 'subscription' ),
183 265 'manage_options',
184 - 'wp-subscription',
185 - array( $this, 'render_subscriptions_page' )
266 + 'admin.php?page=wp-subscription-list'
186 267 );
187 268 }
188 269
189 270 /**
@@ -204,26 +285,25 @@
204 285 return;
205 286 }
206 287
207 288 // slug => position. Use gaps of 10 so extensions can insert between items.
289 + //
290 + // The order is the same whether or not pro is active: a locked page
291 + // carries a pro badge but keeps its place, so the menu does not
292 + // rearrange itself the moment a licence is activated. Plans and
293 + // Cancellation Flow position themselves through the filter below.
208 294 $default_order = [
209 - 'wp-subscription' => 10, // Subscriptions
210 - 'wp-subscription-stats' => 20, // Reports
211 - 'wp-subscription-delivery' => 30, // Delivery (pro)
212 - 'wp-subscription-health' => 50, // Health
213 - 'wp-subscription-integrations' => 60, // Integrations
214 - 'wp-subscription-support' => 70, // Help & Resources
295 + 'wp-subscription' => 5, // Overview
296 + 'wp-subscription-delivery' => 20, // Delivery (pro)
297 + 'wp-subscription-list' => 30, // Subscriptions
298 + 'wp-subscription-stats' => 40, // Reports
299 + 'wp-subscription-health' => 50, // Health
300 + 'wp-subscription-integrations' => 60, // Integrations
215 301 'wp-subscription-settings' => 998, // Settings
216 302 'wp-subscription-license' => 999, // License (pro)
303 + 'wp-subscription-support' => 1000, // Help & Resources
217 304 ];
218 305
219 - // Place pro pages at the bottom if pro is not active.
220 - if ( ! subscrpt_pro_activated() ) {
221 - $default_order['wp-subscription-stats'] = 200;
222 - $default_order['wp-subscription-delivery'] = 210;
223 - $default_order['wp-subscription-health'] = 220;
224 - }
225 -
226 306 /**
227 307 * Filter the WPSubscription submenu slug order.
228 308 *
229 309 * Each entry is a slug => integer position pair. Lower positions appear
@@ -229,11 +309,11 @@
229 309 * Each entry is a slug => integer position pair. Lower positions appear
230 310 * first. Use gaps of 10 between built-in positions so extensions can
231 311 * insert their own slugs between existing items without renumbering.
232 312 *
233 - * Example (pro plugin adding Delivery at position 35):
313 + * Example (an add-on placing its page between Reports and Health):
234 314 * add_filter( 'subscrpt_submenu_order', function( $order ) {
235 - * $order['wp-subscription-delivery'] = 35;
315 + * $order['wp-subscription-my-addon'] = 45;
236 316 * return $order;
237 317 * } );
238 318 *
239 319 * @param array<string,int> $order Map of slug => position.
@@ -271,13 +351,16 @@
271 351 */
272 352 public function render_admin_footer() {
273 353 ?>
274 354 <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
275 - Made with <span style="color:#e25555;font-size:1.1em;">♥</span> by the WPSubscription Team
355 + <?php
356 + /* translators: %s: a heart symbol. */
357 + printf( esc_html__( 'Made with %s by the WPSubscription Team', 'subscription' ), '<span style="color:#e25555;font-size:1.1em;">♥</span>' );
358 + ?>
276 359 <div style="margin-top:6px;">
277 - <a href="https://wpsubscription.co/contact" target="_blank" style="color:#2563eb;text-decoration:none;">Support</a>
360 + <a href="https://wpsubscription.co/contact?utm_source=plugin&utm_medium=admin&utm_campaign=support" target="_blank" style="color:#2563eb;text-decoration:none;"><?php esc_html_e( 'Support', 'subscription' ); ?></a>
278 361 &nbsp;/&nbsp;
279 - <a href="https://docs.wpsubscription.co/en" target="_blank" style="color:#2563eb;text-decoration:none;">Docs</a>
362 + <a href="https://docs.wpsubscription.co/en?utm_source=plugin&utm_medium=admin&utm_campaign=docs" target="_blank" style="color:#2563eb;text-decoration:none;"><?php esc_html_e( 'Docs', 'subscription' ); ?></a>
280 363 </div>
281 364 </div>
282 365 <?php
283 366 }
@@ -305,23 +388,28 @@
305 388 unset( $menu_items ); // Nav no longer rendered in header; navigation is in WP sidebar.
306 389
307 390 // Normalize to a single trail. Explicit breadcrumbs win; otherwise the
308 391 // legacy single $title segment is used.
392 + // A long breadcrumb label is truncated for display; the full text is kept
393 + // so the renderer can add it as a title attribute.
394 + $make_crumb = static function ( $label, $url ) {
395 + $label = (string) $label;
396 + return [
397 + 'label' => subscrpt_truncate_text( $label ),
398 + 'full' => $label,
399 + 'url' => (string) $url,
400 + ];
401 + };
402 +
309 403 $trail = [];
310 404 if ( ! empty( $breadcrumbs ) ) {
311 405 foreach ( $breadcrumbs as $crumb ) {
312 406 if ( is_array( $crumb ) && '' !== ( $crumb['label'] ?? '' ) ) {
313 - $trail[] = [
314 - 'label' => (string) $crumb['label'],
315 - 'url' => isset( $crumb['url'] ) ? (string) $crumb['url'] : '',
316 - ];
407 + $trail[] = $make_crumb( $crumb['label'], $crumb['url'] ?? '' );
317 408 }
318 409 }
319 410 } elseif ( '' !== $title ) {
320 - $trail[] = [
321 - 'label' => $title,
322 - 'url' => '',
323 - ];
411 + $trail[] = $make_crumb( $title, '' );
324 412 }
325 413
326 414 $last_index = count( $trail ) - 1;
327 415 ?>
@@ -333,17 +421,51 @@
333 421 <span class="dashicons dashicons-admin-home"></span>
334 422 </a>
335 423 <?php foreach ( $trail as $index => $crumb ) : ?>
336 424 <span class="wp-subscription-breadcrumb-sep" aria-hidden="true">/</span>
425 + <?php $crumb_title = $crumb['full'] !== $crumb['label'] ? ' title="' . esc_attr( $crumb['full'] ) . '"' : ''; ?>
337 426 <?php if ( '' !== $crumb['url'] && $index < $last_index ) : ?>
338 - <a href="<?php echo esc_url( $crumb['url'] ); ?>" class="wp-subscription-breadcrumb-link"><?php echo esc_html( $crumb['label'] ); ?></a>
427 + <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>
339 428 <?php else : ?>
340 - <span class="wp-subscription-breadcrumb-current"><?php echo esc_html( $crumb['label'] ); ?></span>
429 + <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>
341 430 <?php endif; ?>
342 431 <?php endforeach; ?>
343 432 </nav>
344 433 </div>
345 434 <div class="wp-subscription-admin-header-right">
435 + <?php
436 + /**
437 + * Filters the pro licence state shown in the admin header.
438 + *
439 + * This plugin cannot ask the pro plugin directly — it runs alone
440 + * on nearly every install, so naming a symbol pro declares would
441 + * fatal there. Pro answers this filter when it is present; when
442 + * it is not, the value stays null and no badge is rendered.
443 + *
444 + * @since 1.11.3
445 + *
446 + * @param array|null $license {
447 + * Licence state, or null when pro is not installed.
448 + *
449 + * @type bool $active Whether the licence is valid.
450 + * @type string $url Admin URL of the licence page.
451 + * }
452 + */
453 + $license = apply_filters( 'subscrpt_admin_header_license', null );
454 +
455 + // Only the "Activate license" badge is shown; the "License active"
456 + // badge is intentionally hidden from the header.
457 + if ( is_array( $license ) && isset( $license['active'] ) && ! $license['active'] ) :
458 + $license_url = isset( $license['url'] ) ? (string) $license['url'] : '';
459 + ?>
460 + <a href="<?php echo esc_url( $license_url ); ?>" class="wpsubs-badge wpsubs-badge--warning wp-subscription-license-badge">
461 + <span class="wpsubs-badge__dot"></span>
462 + <?php esc_html_e( 'Activate license', 'subscription' ); ?>
463 + </a>
464 + <?php
465 + endif;
466 + ?>
467 +
346 468 <?php if ( ! class_exists( 'Sdevs_Wc_Subscription_Pro' ) ) : ?>
347 469 <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">
348 470 <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>
349 471 <?php esc_html_e( 'Upgrade to Pro', 'subscription' ); ?>
@@ -356,8 +478,46 @@
356 478 <?php
357 479 }
358 480
359 481 /**
482 + * Render the dashboard, or hand off to the list.
483 + *
484 + * The subscriptions list used to live on this slug. Anything still linking
485 + * here with a list-only argument — a saved filter, a bookmarked search, a
486 + * pagination link — means the list, so it is sent there with its arguments
487 + * intact rather than landing on a dashboard that ignores them. The one
488 + * exception is `post_status`, renamed to the `subscrpt_status` the list reads.
489 + *
490 + * @return void
491 + */
492 + public function render_dashboard_page() {
493 + // phpcs:disable WordPress.Security.NonceVerification.Recommended
494 + $list_args = array( 'post_status', 's', 'paged', 'filter_action', 'orderby', 'order', 'subscrpt_status', 'date_filter', 'per_page', 'renewal_due' );
495 +
496 + foreach ( $list_args as $arg ) {
497 + if ( isset( $_GET[ $arg ] ) && '' !== $_GET[ $arg ] ) {
498 + $query = wp_unslash( $_GET );
499 + $query['page'] = 'wp-subscription-list';
500 +
501 + // The list filters on `subscrpt_status` and ignores `post_status`,
502 + // so forwarding the old name as-is lands on an unfiltered list.
503 + if ( isset( $query['post_status'] ) ) {
504 + if ( empty( $query['subscrpt_status'] ) ) {
505 + $query['subscrpt_status'] = $query['post_status'];
506 + }
507 + unset( $query['post_status'] );
508 + }
509 +
510 + wp_safe_redirect( add_query_arg( array_map( 'sanitize_text_field', $query ), admin_url( 'admin.php' ) ) );
511 + exit;
512 + }
513 + }
514 + // phpcs:enable WordPress.Security.NonceVerification.Recommended
515 +
516 + ( new Dashboard() )->render();
517 + }
518 +
519 + /**
360 520 * Render Subscriptions page
361 521 */
362 522 public function render_subscriptions_page() {
363 523 $this->render_admin_header( __( 'Subscriptions', 'subscription' ), __( 'Manage your subscriptions', 'subscription' ) );
@@ -367,8 +527,9 @@
367 527 $search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
368 528 $date_filter = isset( $_GET['date_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['date_filter'] ) ) : '';
369 529 $per_page = isset( $_GET['per_page'] ) ? max( 1, intval( $_GET['per_page'] ) ) : 20;
370 530 $paged = isset( $_GET['paged'] ) ? max( 1, intval( $_GET['paged'] ) ) : 1;
531 + $renewal_due = isset( $_GET['renewal_due'] ) ? min( 366, absint( $_GET['renewal_due'] ) ) : 0;
371 532
372 533 // Handle form submissions (both filters and bulk actions)
373 534 $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
374 535 if ( 'POST' === $request_method ) {
@@ -398,9 +559,9 @@
398 559 wp_delete_post( $sub_id, true );
399 560 }
400 561 }
401 562
402 - wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription' ) );
563 + wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription-list' ) );
403 564 exit;
404 565 }
405 566 }
406 567
@@ -413,8 +574,11 @@
413 574 }
414 575 if ( ! empty( $_POST['date_filter'] ) ) {
415 576 $filter_params['date_filter'] = sanitize_text_field( wp_unslash( $_POST['date_filter'] ) );
416 577 }
578 + if ( ! empty( $_POST['renewal_due'] ) ) {
579 + $filter_params['renewal_due'] = absint( $_POST['renewal_due'] );
580 + }
417 581 if ( ! empty( $_POST['s'] ) ) {
418 582 $filter_params['s'] = sanitize_text_field( wp_unslash( $_POST['s'] ) );
419 583 }
420 584 if ( ! empty( $_POST['per_page'] ) ) {
@@ -420,9 +584,9 @@
420 584 if ( ! empty( $_POST['per_page'] ) ) {
421 585 $filter_params['per_page'] = intval( $_POST['per_page'] );
422 586 }
423 587
424 - $redirect_url = add_query_arg( $filter_params, admin_url( 'admin.php?page=wp-subscription' ) );
588 + $redirect_url = add_query_arg( $filter_params, admin_url( 'admin.php?page=wp-subscription-list' ) );
425 589 wp_safe_redirect( $redirect_url );
426 590 exit;
427 591 }
428 592 }
@@ -455,9 +619,9 @@
455 619 foreach ( $trash_posts as $trash_id ) {
456 620 wp_delete_post( $trash_id, true );
457 621 }
458 622
459 - wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription&subscrpt_status=trash' ) );
623 + wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription-list&subscrpt_status=trash' ) );
460 624 exit;
461 625 } else {
462 626 // For other actions, verify nonce with subscription ID.
463 627 $nonce_action = 'wpsubs_action_' . $sub_id;
@@ -465,9 +629,9 @@
465 629 echo '<div class="notice notice-error"><p>' . esc_html__( 'Security check failed. Please try again.', 'subscription' ) . '</p></div>';
466 630 wp_die();
467 631 }
468 632
469 - $redirect_url = admin_url( 'admin.php?page=wp-subscription' );
633 + $redirect_url = admin_url( 'admin.php?page=wp-subscription-list' );
470 634
471 635 switch ( $action ) {
472 636 case 'duplicate':
473 637 $post = get_post( $sub_id );
@@ -496,9 +660,9 @@
496 660 wp_untrash_post( $sub_id );
497 661 break;
498 662 case 'delete':
499 663 wp_delete_post( $sub_id, true );
500 - $redirect_url = admin_url( 'admin.php?page=wp-subscription&subscrpt_status=trash' );
664 + $redirect_url = admin_url( 'admin.php?page=wp-subscription-list&subscrpt_status=trash' );
501 665 break;
502 666 }
503 667
504 668 wp_safe_redirect( $redirect_url );
@@ -535,9 +699,24 @@
535 699 'year' => intval( $year ),
536 700 'month' => intval( $month ),
537 701 ];
538 702 }
703 + // Next-renewal window, soonest first. The same arguments as the
704 + // Overview's "Renewals due" figure, so its 7-day rows equal its count.
705 + if ( $renewal_due ) {
706 + $due_args = Stats::renewals_due_args( $renewal_due );
539 707
708 + // Only an active subscription renews, so a renewal window combined
709 + // with any other status matches nothing — the filters AND together.
710 + if ( $status && $due_args['post_status'] !== $status ) {
711 + $args['post__in'] = array( 0 );
712 + }
713 +
714 + $args['post_status'] = $due_args['post_status'];
715 + $args['meta_query'] = $due_args['meta_query']; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
716 + $args['orderby'] = [ 'subscrpt_next_date' => 'ASC' ];
717 + }
718 +
540 719 $query = new \WP_Query( $args );
541 720 $subscriptions = $query->posts;
542 721 $total = $query->found_posts;
543 722 $max_num_pages = $query->max_num_pages;
@@ -547,13 +726,16 @@
547 726
548 727 include __DIR__ . '/views/subscription-list.php';
549 728 ?>
550 729 <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
551 - Made with <span style="color:#e25555;font-size:1.1em;">♥</span> by the WPSubscription Team
730 + <?php
731 + /* translators: %s: a heart symbol. */
732 + printf( esc_html__( 'Made with %s by the WPSubscription Team', 'subscription' ), '<span style="color:#e25555;font-size:1.1em;">♥</span>' );
733 + ?>
552 734 <div style="margin-top:6px;">
553 - <a href="https://wpsubscription.co/contact" target="_blank" style="color:#2563eb;text-decoration:none;">Support</a>
735 + <a href="https://wpsubscription.co/contact?utm_source=plugin&utm_medium=admin&utm_campaign=support" target="_blank" style="color:#2563eb;text-decoration:none;"><?php esc_html_e( 'Support', 'subscription' ); ?></a>
554 736 &nbsp;/&nbsp;
555 - <a href="https://docs.wpsubscription.co/en" target="_blank" style="color:#2563eb;text-decoration:none;">Docs</a>
737 + <a href="https://docs.wpsubscription.co/en?utm_source=plugin&utm_medium=admin&utm_campaign=docs" target="_blank" style="color:#2563eb;text-decoration:none;"><?php esc_html_e( 'Docs', 'subscription' ); ?></a>
556 738 </div>
557 739 </div>
558 740 <?php
559 741 }
@@ -576,9 +758,9 @@
576 758 if ( ! current_user_can( 'edit_post', $subscription_id ) ) {
577 759 wp_die( esc_html__( 'You do not have permission to view this subscription.', 'subscription' ) );
578 760 }
579 761
580 - $list_url = admin_url( 'admin.php?page=wp-subscription' );
762 + $list_url = admin_url( 'admin.php?page=wp-subscription-list' );
581 763 $form_action = admin_url( 'admin.php?page=wp-subscription-details&id=' . $subscription_id );
582 764
583 765 // Handle the status-change submission.
584 766 $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
@@ -626,8 +808,9 @@
626 808 'active' => array( 'pe_cancelled', 'cancelled' ),
627 809 'pe_cancelled' => array( 'active', 'cancelled' ),
628 810 'cancelled' => array( 'active' ),
629 811 'expired' => array( 'active', 'cancelled' ),
812 + 'completed' => array( 'cancelled' ),
630 813 );
631 814
632 815 $status = get_post_status( $subscription_id );
633 816 $actions = $map_actions[ $status ] ?? array();
@@ -728,17 +911,14 @@
728 911 * SPA-style: all sections rendered at once, JS controls visibility
729 912 * Initial load always shows page 1 (JS handles transitions from there)
730 913 */
731 914 public function render_onboarding_wizard() {
732 - // Start session if not already started
915 + // Start session if not already started (used by the wizard reset handler).
733 916 if ( ! session_id() && ! headers_sent() ) {
734 917 session_start();
735 918 }
736 919
737 - // Always start at page 1 on direct load (SPA behavior — JS drives page transitions)
738 - $GLOBALS['wizard_page'] = 1;
739 -
740 - $this->render_admin_header( __( 'Setup Wizard', 'subscription' ), __( 'Create your first subscription product', 'subscription' ) );
920 + $this->render_admin_header( __( 'Setup Wizard', 'subscription' ), __( 'Create your first subscription plan', 'subscription' ) );
741 921 include __DIR__ . '/views/onboarding-wizard.php';
742 922 $this->render_admin_footer();
743 923 }
744 924