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 +241 -53 1.10.7 → 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,21 +78,39 @@
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
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', 'wp-i18n' ),
49 101 SUBSCRPT_VERSION,
50 102 true
51 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 +
52 113 wp_localize_script(
53 114 'subscrpt-onboarding-wizard',
54 115 'subscrpt_wizard',
55 116 array(
@@ -54,9 +115,16 @@
54 115 'subscrpt_wizard',
55 116 array(
56 117 'ajax_url' => admin_url( 'admin-ajax.php' ),
57 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' ),
58 124 'currency_symbol' => get_woocommerce_currency_symbol(),
125 + 'is_pro' => subscrpt_pro_activated(),
126 + 'has_products' => $subscrpt_wizard_has_products,
59 127 )
60 128 );
61 129
62 130 // Localize script for AJAX
@@ -89,9 +157,9 @@
89 157 __( 'WPSubscription', 'subscription' ),
90 158 __( 'WPSubscription', 'subscription' ),
91 159 'manage_options',
92 160 $parent_slug,
93 - array( $this, 'render_subscriptions_page' ),
161 + array( $this, 'render_dashboard_page' ),
94 162 $icon_url,
95 163 40
96 164 );
97 165
@@ -105,15 +173,28 @@
105 173 'wp-subscription-onboarding',
106 174 array( $this, 'render_onboarding_wizard' )
107 175 );
108 176
109 - // 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.
110 179 add_submenu_page(
111 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,
112 193 __( 'Subscriptions', 'subscription' ),
113 194 __( 'Subscriptions', 'subscription' ),
114 195 'manage_options',
115 - $parent_slug,
196 + 'wp-subscription-list',
116 197 array( $this, 'render_subscriptions_page' )
117 198 );
118 199
119 200 // Subscription Details (hidden from menu with CSS — accessed via admin.php?page=wp-subscription-details&id=ID).
@@ -166,16 +247,24 @@
166 247 'wp-subscription-support',
167 248 array( $this, 'render_support_page' )
168 249 );
169 250
170 - // 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 + */
171 261 add_submenu_page(
172 262 'woocommerce',
173 - __( 'WPSubscription', 'subscription' ),
174 - __( 'WPSubscription', 'subscription' ),
263 + __( 'Subscriptions', 'subscription' ),
264 + __( 'Subscriptions', 'subscription' ),
175 265 'manage_options',
176 - 'wp-subscription',
177 - array( $this, 'render_subscriptions_page' )
266 + 'admin.php?page=wp-subscription-list'
178 267 );
179 268 }
180 269
181 270 /**
@@ -196,26 +285,25 @@
196 285 return;
197 286 }
198 287
199 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.
200 294 $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
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
207 301 'wp-subscription-settings' => 998, // Settings
208 302 'wp-subscription-license' => 999, // License (pro)
303 + 'wp-subscription-support' => 1000, // Help & Resources
209 304 ];
210 305
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 306 /**
219 307 * Filter the WPSubscription submenu slug order.
220 308 *
221 309 * Each entry is a slug => integer position pair. Lower positions appear
@@ -221,11 +309,11 @@
221 309 * Each entry is a slug => integer position pair. Lower positions appear
222 310 * first. Use gaps of 10 between built-in positions so extensions can
223 311 * insert their own slugs between existing items without renumbering.
224 312 *
225 - * Example (pro plugin adding Delivery at position 35):
313 + * Example (an add-on placing its page between Reports and Health):
226 314 * add_filter( 'subscrpt_submenu_order', function( $order ) {
227 - * $order['wp-subscription-delivery'] = 35;
315 + * $order['wp-subscription-my-addon'] = 45;
228 316 * return $order;
229 317 * } );
230 318 *
231 319 * @param array<string,int> $order Map of slug => position.
@@ -263,13 +351,16 @@
263 351 */
264 352 public function render_admin_footer() {
265 353 ?>
266 354 <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
267 - 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 + ?>
268 359 <div style="margin-top:6px;">
269 - <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>
270 361 &nbsp;/&nbsp;
271 - <a href="https://docs.converslabs.com/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>
272 363 </div>
273 364 </div>
274 365 <?php
275 366 }
@@ -297,23 +388,28 @@
297 388 unset( $menu_items ); // Nav no longer rendered in header; navigation is in WP sidebar.
298 389
299 390 // Normalize to a single trail. Explicit breadcrumbs win; otherwise the
300 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 +
301 403 $trail = [];
302 404 if ( ! empty( $breadcrumbs ) ) {
303 405 foreach ( $breadcrumbs as $crumb ) {
304 406 if ( is_array( $crumb ) && '' !== ( $crumb['label'] ?? '' ) ) {
305 - $trail[] = [
306 - 'label' => (string) $crumb['label'],
307 - 'url' => isset( $crumb['url'] ) ? (string) $crumb['url'] : '',
308 - ];
407 + $trail[] = $make_crumb( $crumb['label'], $crumb['url'] ?? '' );
309 408 }
310 409 }
311 410 } elseif ( '' !== $title ) {
312 - $trail[] = [
313 - 'label' => $title,
314 - 'url' => '',
315 - ];
411 + $trail[] = $make_crumb( $title, '' );
316 412 }
317 413
318 414 $last_index = count( $trail ) - 1;
319 415 ?>
@@ -325,17 +421,51 @@
325 421 <span class="dashicons dashicons-admin-home"></span>
326 422 </a>
327 423 <?php foreach ( $trail as $index => $crumb ) : ?>
328 424 <span class="wp-subscription-breadcrumb-sep" aria-hidden="true">/</span>
425 + <?php $crumb_title = $crumb['full'] !== $crumb['label'] ? ' title="' . esc_attr( $crumb['full'] ) . '"' : ''; ?>
329 426 <?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>
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>
331 428 <?php else : ?>
332 - <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>
333 430 <?php endif; ?>
334 431 <?php endforeach; ?>
335 432 </nav>
336 433 </div>
337 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 +
338 468 <?php if ( ! class_exists( 'Sdevs_Wc_Subscription_Pro' ) ) : ?>
339 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">
340 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>
341 471 <?php esc_html_e( 'Upgrade to Pro', 'subscription' ); ?>
@@ -348,8 +478,46 @@
348 478 <?php
349 479 }
350 480
351 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 + /**
352 520 * Render Subscriptions page
353 521 */
354 522 public function render_subscriptions_page() {
355 523 $this->render_admin_header( __( 'Subscriptions', 'subscription' ), __( 'Manage your subscriptions', 'subscription' ) );
@@ -359,8 +527,9 @@
359 527 $search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
360 528 $date_filter = isset( $_GET['date_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['date_filter'] ) ) : '';
361 529 $per_page = isset( $_GET['per_page'] ) ? max( 1, intval( $_GET['per_page'] ) ) : 20;
362 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;
363 532
364 533 // Handle form submissions (both filters and bulk actions)
365 534 $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
366 535 if ( 'POST' === $request_method ) {
@@ -390,9 +559,9 @@
390 559 wp_delete_post( $sub_id, true );
391 560 }
392 561 }
393 562
394 - wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription' ) );
563 + wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription-list' ) );
395 564 exit;
396 565 }
397 566 }
398 567
@@ -405,8 +574,11 @@
405 574 }
406 575 if ( ! empty( $_POST['date_filter'] ) ) {
407 576 $filter_params['date_filter'] = sanitize_text_field( wp_unslash( $_POST['date_filter'] ) );
408 577 }
578 + if ( ! empty( $_POST['renewal_due'] ) ) {
579 + $filter_params['renewal_due'] = absint( $_POST['renewal_due'] );
580 + }
409 581 if ( ! empty( $_POST['s'] ) ) {
410 582 $filter_params['s'] = sanitize_text_field( wp_unslash( $_POST['s'] ) );
411 583 }
412 584 if ( ! empty( $_POST['per_page'] ) ) {
@@ -412,9 +584,9 @@
412 584 if ( ! empty( $_POST['per_page'] ) ) {
413 585 $filter_params['per_page'] = intval( $_POST['per_page'] );
414 586 }
415 587
416 - $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' ) );
417 589 wp_safe_redirect( $redirect_url );
418 590 exit;
419 591 }
420 592 }
@@ -447,9 +619,9 @@
447 619 foreach ( $trash_posts as $trash_id ) {
448 620 wp_delete_post( $trash_id, true );
449 621 }
450 622
451 - 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' ) );
452 624 exit;
453 625 } else {
454 626 // For other actions, verify nonce with subscription ID.
455 627 $nonce_action = 'wpsubs_action_' . $sub_id;
@@ -457,9 +629,9 @@
457 629 echo '<div class="notice notice-error"><p>' . esc_html__( 'Security check failed. Please try again.', 'subscription' ) . '</p></div>';
458 630 wp_die();
459 631 }
460 632
461 - $redirect_url = admin_url( 'admin.php?page=wp-subscription' );
633 + $redirect_url = admin_url( 'admin.php?page=wp-subscription-list' );
462 634
463 635 switch ( $action ) {
464 636 case 'duplicate':
465 637 $post = get_post( $sub_id );
@@ -488,9 +660,9 @@
488 660 wp_untrash_post( $sub_id );
489 661 break;
490 662 case 'delete':
491 663 wp_delete_post( $sub_id, true );
492 - $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' );
493 665 break;
494 666 }
495 667
496 668 wp_safe_redirect( $redirect_url );
@@ -527,9 +699,24 @@
527 699 'year' => intval( $year ),
528 700 'month' => intval( $month ),
529 701 ];
530 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 );
531 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 +
532 719 $query = new \WP_Query( $args );
533 720 $subscriptions = $query->posts;
534 721 $total = $query->found_posts;
535 722 $max_num_pages = $query->max_num_pages;
@@ -539,13 +726,16 @@
539 726
540 727 include __DIR__ . '/views/subscription-list.php';
541 728 ?>
542 729 <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
543 - 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 + ?>
544 734 <div style="margin-top:6px;">
545 - <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>
546 736 &nbsp;/&nbsp;
547 - <a href="https://docs.converslabs.com/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>
548 738 </div>
549 739 </div>
550 740 <?php
551 741 }
@@ -568,9 +758,9 @@
568 758 if ( ! current_user_can( 'edit_post', $subscription_id ) ) {
569 759 wp_die( esc_html__( 'You do not have permission to view this subscription.', 'subscription' ) );
570 760 }
571 761
572 - $list_url = admin_url( 'admin.php?page=wp-subscription' );
762 + $list_url = admin_url( 'admin.php?page=wp-subscription-list' );
573 763 $form_action = admin_url( 'admin.php?page=wp-subscription-details&id=' . $subscription_id );
574 764
575 765 // Handle the status-change submission.
576 766 $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
@@ -618,8 +808,9 @@
618 808 'active' => array( 'pe_cancelled', 'cancelled' ),
619 809 'pe_cancelled' => array( 'active', 'cancelled' ),
620 810 'cancelled' => array( 'active' ),
621 811 'expired' => array( 'active', 'cancelled' ),
812 + 'completed' => array( 'cancelled' ),
622 813 );
623 814
624 815 $status = get_post_status( $subscription_id );
625 816 $actions = $map_actions[ $status ] ?? array();
@@ -720,17 +911,14 @@
720 911 * SPA-style: all sections rendered at once, JS controls visibility
721 912 * Initial load always shows page 1 (JS handles transitions from there)
722 913 */
723 914 public function render_onboarding_wizard() {
724 - // Start session if not already started
915 + // Start session if not already started (used by the wizard reset handler).
725 916 if ( ! session_id() && ! headers_sent() ) {
726 917 session_start();
727 918 }
728 919
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' ) );
920 + $this->render_admin_header( __( 'Setup Wizard', 'subscription' ), __( 'Create your first subscription plan', 'subscription' ) );
733 921 include __DIR__ . '/views/onboarding-wizard.php';
734 922 $this->render_admin_footer();
735 923 }
736 924