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 +392 -48 1.10.2 → 2.4.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(
@@ -33,21 +78,39 @@
33 78 // Enqueue admin JavaScript for subscription list functionality
34 79 wp_enqueue_script(
35 80 'sdevs_subscription_admin',
36 81 SUBSCRPT_ASSETS . '/js/admin.js',
37 - array( 'jquery' ),
82 + array( 'jquery', 'wp-i18n' ),
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', 'wp-i18n' ),
47 101 SUBSCRPT_VERSION,
48 102 true
49 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 +
50 113 wp_localize_script(
51 114 'subscrpt-onboarding-wizard',
52 115 'subscrpt_wizard',
53 116 array(
@@ -52,9 +115,16 @@
52 115 'subscrpt_wizard',
53 116 array(
54 117 'ajax_url' => admin_url( 'admin-ajax.php' ),
55 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' ),
56 124 'currency_symbol' => get_woocommerce_currency_symbol(),
125 + 'is_pro' => subscrpt_pro_activated(),
126 + 'has_products' => $subscrpt_wizard_has_products,
57 127 )
58 128 );
59 129
60 130 // Localize script for AJAX
@@ -87,14 +157,15 @@
87 157 __( 'WPSubscription', 'subscription' ),
88 158 __( 'WPSubscription', 'subscription' ),
89 159 'manage_options',
90 160 $parent_slug,
91 - array( $this, 'render_subscriptions_page' ),
161 + array( $this, 'render_dashboard_page' ),
92 162 $icon_url,
93 163 40
94 164 );
95 165
96 166 // Onboarding Wizard (hidden from menu with CSS. can be accessed via direct URL: admin.php?page=wp-subscription-onboarding)
167 + // CSS Record: admin.css -> `.wp-submenu a[href*="page=wp-subscription-onboarding"]`
97 168 add_submenu_page(
98 169 $parent_slug,
99 170 __( 'Setup Wizard', 'subscription' ),
100 171 __( 'Setup Wizard', 'subscription' ),
@@ -102,16 +173,40 @@
102 173 'wp-subscription-onboarding',
103 174 array( $this, 'render_onboarding_wizard' )
104 175 );
105 176
106 - // 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.
107 179 add_submenu_page(
108 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,
109 193 __( 'Subscriptions', 'subscription' ),
110 194 __( 'Subscriptions', 'subscription' ),
111 195 'manage_options',
196 + 'wp-subscription-list',
197 + array( $this, 'render_subscriptions_page' )
198 + );
199 +
200 + // Subscription Details (hidden from menu with CSS — accessed via admin.php?page=wp-subscription-details&id=ID).
201 + // CSS Record: admin.css -> `.wp-submenu a[href*="page=wp-subscription-details"]`
202 + add_submenu_page(
112 203 $parent_slug,
113 - array( $this, 'render_subscriptions_page' )
204 + __( 'Subscription Details', 'subscription' ),
205 + __( 'Subscription Details', 'subscription' ),
206 + 'manage_options',
207 + 'wp-subscription-details',
208 + array( $this, 'render_subscription_details_page' )
114 209 );
115 210
116 211 // Stats Overview
117 212 add_submenu_page(
@@ -152,16 +247,24 @@
152 247 'wp-subscription-support',
153 248 array( $this, 'render_support_page' )
154 249 );
155 250
156 - // 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 + */
157 261 add_submenu_page(
158 262 'woocommerce',
159 - __( 'WPSubscription', 'subscription' ),
160 - __( 'WPSubscription', 'subscription' ),
263 + __( 'Subscriptions', 'subscription' ),
264 + __( 'Subscriptions', 'subscription' ),
161 265 'manage_options',
162 - 'wp-subscription',
163 - array( $this, 'render_subscriptions_page' )
266 + 'admin.php?page=wp-subscription-list'
164 267 );
165 268 }
166 269
167 270 /**
@@ -182,26 +285,25 @@
182 285 return;
183 286 }
184 287
185 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.
186 294 $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
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
193 301 'wp-subscription-settings' => 998, // Settings
194 302 'wp-subscription-license' => 999, // License (pro)
303 + 'wp-subscription-support' => 1000, // Help & Resources
195 304 ];
196 305
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 306 /**
205 307 * Filter the WPSubscription submenu slug order.
206 308 *
207 309 * Each entry is a slug => integer position pair. Lower positions appear
@@ -207,11 +309,11 @@
207 309 * Each entry is a slug => integer position pair. Lower positions appear
208 310 * first. Use gaps of 10 between built-in positions so extensions can
209 311 * insert their own slugs between existing items without renumbering.
210 312 *
211 - * Example (pro plugin adding Delivery at position 35):
313 + * Example (an add-on placing its page between Reports and Health):
212 314 * add_filter( 'subscrpt_submenu_order', function( $order ) {
213 - * $order['wp-subscription-delivery'] = 35;
315 + * $order['wp-subscription-my-addon'] = 45;
214 316 * return $order;
215 317 * } );
216 318 *
217 319 * @param array<string,int> $order Map of slug => position.
@@ -249,13 +351,16 @@
249 351 */
250 352 public function render_admin_footer() {
251 353 ?>
252 354 <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
253 - 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 + ?>
254 359 <div style="margin-top:6px;">
255 - <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>
256 361 &nbsp;/&nbsp;
257 - <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>
258 363 </div>
259 364 </div>
260 365 <?php
261 366 }
@@ -261,17 +366,53 @@
261 366 }
262 367 /**
263 368 * Render the admin header.
264 369 *
265 - * @param string $title Page title shown on the left.
266 - * @param string $subtitle Optional subtitle shown below the title.
370 + * The breadcrumb after the home icon is built from $breadcrumbs when given —
371 + * an ordered trail supporting any number of levels — otherwise it falls back
372 + * to the single $title segment (backward compatible with existing callers).
373 + *
374 + * Each breadcrumb item is an array: `[ 'label' => string, 'url' => string ]`.
375 + * Items with a non-empty `url` render as links, except the last item, which is
376 + * always the current (non-linked) page.
377 + *
378 + * @param string $title Page title shown as the current segment when no
379 + * $breadcrumbs trail is supplied.
380 + * @param string $subtitle Optional subtitle (reserved; not rendered).
381 + * @param array $breadcrumbs Ordered trail of `[ 'label', 'url' ]` items.
267 382 */
268 - public function render_admin_header( string $title = '', string $subtitle = '' ) {
383 + public function render_admin_header( string $title = '', string $subtitle = '', array $breadcrumbs = [] ) {
269 384 $current = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : 'wp-subscription';
270 385
271 386 // Kept for backward compatibility — extensions may hook here for side-effects.
272 387 $menu_items = apply_filters( 'subscrpt_admin_header_menu_items', [], $current );
273 388 unset( $menu_items ); // Nav no longer rendered in header; navigation is in WP sidebar.
389 +
390 + // Normalize to a single trail. Explicit breadcrumbs win; otherwise the
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 +
403 + $trail = [];
404 + if ( ! empty( $breadcrumbs ) ) {
405 + foreach ( $breadcrumbs as $crumb ) {
406 + if ( is_array( $crumb ) && '' !== ( $crumb['label'] ?? '' ) ) {
407 + $trail[] = $make_crumb( $crumb['label'], $crumb['url'] ?? '' );
408 + }
409 + }
410 + } elseif ( '' !== $title ) {
411 + $trail[] = $make_crumb( $title, '' );
412 + }
413 +
414 + $last_index = count( $trail ) - 1;
274 415 ?>
275 416 <div class="wp-subscription-admin-header">
276 417 <div class="wp-subscription-admin-header-inner">
277 418 <div class="wp-subscription-admin-header-left">
@@ -278,15 +419,53 @@
278 419 <nav class="wp-subscription-breadcrumb" aria-label="<?php esc_attr_e( 'Breadcrumb', 'subscription' ); ?>">
279 420 <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 421 <span class="dashicons dashicons-admin-home"></span>
281 422 </a>
282 - <?php if ( $title ) : ?>
423 + <?php foreach ( $trail as $index => $crumb ) : ?>
283 424 <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; ?>
425 + <?php $crumb_title = $crumb['full'] !== $crumb['label'] ? ' title="' . esc_attr( $crumb['full'] ) . '"' : ''; ?>
426 + <?php if ( '' !== $crumb['url'] && $index < $last_index ) : ?>
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>
428 + <?php else : ?>
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>
430 + <?php endif; ?>
431 + <?php endforeach; ?>
286 432 </nav>
287 433 </div>
288 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 +
289 468 <?php if ( ! class_exists( 'Sdevs_Wc_Subscription_Pro' ) ) : ?>
290 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">
291 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>
292 471 <?php esc_html_e( 'Upgrade to Pro', 'subscription' ); ?>
@@ -299,8 +478,46 @@
299 478 <?php
300 479 }
301 480
302 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 + /**
303 520 * Render Subscriptions page
304 521 */
305 522 public function render_subscriptions_page() {
306 523 $this->render_admin_header( __( 'Subscriptions', 'subscription' ), __( 'Manage your subscriptions', 'subscription' ) );
@@ -310,8 +527,9 @@
310 527 $search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
311 528 $date_filter = isset( $_GET['date_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['date_filter'] ) ) : '';
312 529 $per_page = isset( $_GET['per_page'] ) ? max( 1, intval( $_GET['per_page'] ) ) : 20;
313 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;
314 532
315 533 // Handle form submissions (both filters and bulk actions)
316 534 $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
317 535 if ( 'POST' === $request_method ) {
@@ -341,9 +559,9 @@
341 559 wp_delete_post( $sub_id, true );
342 560 }
343 561 }
344 562
345 - wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription' ) );
563 + wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription-list' ) );
346 564 exit;
347 565 }
348 566 }
349 567
@@ -356,8 +574,11 @@
356 574 }
357 575 if ( ! empty( $_POST['date_filter'] ) ) {
358 576 $filter_params['date_filter'] = sanitize_text_field( wp_unslash( $_POST['date_filter'] ) );
359 577 }
578 + if ( ! empty( $_POST['renewal_due'] ) ) {
579 + $filter_params['renewal_due'] = absint( $_POST['renewal_due'] );
580 + }
360 581 if ( ! empty( $_POST['s'] ) ) {
361 582 $filter_params['s'] = sanitize_text_field( wp_unslash( $_POST['s'] ) );
362 583 }
363 584 if ( ! empty( $_POST['per_page'] ) ) {
@@ -363,9 +584,9 @@
363 584 if ( ! empty( $_POST['per_page'] ) ) {
364 585 $filter_params['per_page'] = intval( $_POST['per_page'] );
365 586 }
366 587
367 - $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' ) );
368 589 wp_safe_redirect( $redirect_url );
369 590 exit;
370 591 }
371 592 }
@@ -398,9 +619,9 @@
398 619 foreach ( $trash_posts as $trash_id ) {
399 620 wp_delete_post( $trash_id, true );
400 621 }
401 622
402 - 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' ) );
403 624 exit;
404 625 } else {
405 626 // For other actions, verify nonce with subscription ID.
406 627 $nonce_action = 'wpsubs_action_' . $sub_id;
@@ -408,9 +629,9 @@
408 629 echo '<div class="notice notice-error"><p>' . esc_html__( 'Security check failed. Please try again.', 'subscription' ) . '</p></div>';
409 630 wp_die();
410 631 }
411 632
412 - $redirect_url = admin_url( 'admin.php?page=wp-subscription' );
633 + $redirect_url = admin_url( 'admin.php?page=wp-subscription-list' );
413 634
414 635 switch ( $action ) {
415 636 case 'duplicate':
416 637 $post = get_post( $sub_id );
@@ -439,9 +660,9 @@
439 660 wp_untrash_post( $sub_id );
440 661 break;
441 662 case 'delete':
442 663 wp_delete_post( $sub_id, true );
443 - $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' );
444 665 break;
445 666 }
446 667
447 668 wp_safe_redirect( $redirect_url );
@@ -478,9 +699,24 @@
478 699 'year' => intval( $year ),
479 700 'month' => intval( $month ),
480 701 ];
481 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 );
482 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 +
483 719 $query = new \WP_Query( $args );
484 720 $subscriptions = $query->posts;
485 721 $total = $query->found_posts;
486 722 $max_num_pages = $query->max_num_pages;
@@ -490,13 +726,16 @@
490 726
491 727 include __DIR__ . '/views/subscription-list.php';
492 728 ?>
493 729 <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
494 - 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 + ?>
495 734 <div style="margin-top:6px;">
496 - <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>
497 736 &nbsp;/&nbsp;
498 - <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>
499 738 </div>
500 739 </div>
501 740 <?php
502 741 }
@@ -501,8 +740,116 @@
501 740 <?php
502 741 }
503 742
504 743 /**
744 + * Render the standalone Subscription Details page.
745 + *
746 + * Replaces the legacy metabox edit screen. Mirrors the list-page design
747 + * shell (header + wpsubs components) and handles the status-change form.
748 + *
749 + * @return void
750 + */
751 + public function render_subscription_details_page() {
752 + $subscription_id = isset( $_GET['id'] ) ? absint( wp_unslash( $_GET['id'] ) ) : 0;
753 +
754 + if ( ! $subscription_id || 'subscrpt_order' !== get_post_type( $subscription_id ) ) {
755 + wp_die( esc_html__( 'Invalid subscription.', 'subscription' ) );
756 + }
757 +
758 + if ( ! current_user_can( 'edit_post', $subscription_id ) ) {
759 + wp_die( esc_html__( 'You do not have permission to view this subscription.', 'subscription' ) );
760 + }
761 +
762 + $list_url = admin_url( 'admin.php?page=wp-subscription-list' );
763 + $form_action = admin_url( 'admin.php?page=wp-subscription-details&id=' . $subscription_id );
764 +
765 + // Handle the status-change submission.
766 + $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
767 + if ( 'POST' === $request_method && isset( $_POST['subscrpt_order_action'] ) ) {
768 + $nonce = isset( $_POST['subscrpt_order_action_nonce_field'] ) ? sanitize_text_field( wp_unslash( $_POST['subscrpt_order_action_nonce_field'] ) ) : '';
769 + if ( ! wp_verify_nonce( $nonce, 'subscrpt_order_action_nonce' ) ) {
770 + wp_die( esc_html__( 'Security check failed.', 'subscription' ) );
771 + }
772 +
773 + $action = sanitize_text_field( wp_unslash( $_POST['subscrpt_order_action'] ) );
774 + if ( '' !== $action ) {
775 + Subscriptions::process_status_change( $subscription_id, $action );
776 + }
777 +
778 + wp_safe_redirect( $form_action );
779 + exit;
780 + }
781 +
782 + // Allowed actions for the current status (mirrors Subscriptions::subscrpt_order_save_post()).
783 + $actions_data = array(
784 + 'active' => array(
785 + 'label' => __( 'Activate Subscription', 'subscription' ),
786 + 'value' => 'active',
787 + ),
788 + 'pending' => array(
789 + 'label' => __( 'Pending Subscription', 'subscription' ),
790 + 'value' => 'pending',
791 + ),
792 + 'expire' => array(
793 + 'label' => __( 'Expire Subscription', 'subscription' ),
794 + 'value' => 'expired',
795 + ),
796 + 'pe_cancelled' => array(
797 + 'label' => __( 'Pending Cancel Subscription', 'subscription' ),
798 + 'value' => 'pe_cancelled',
799 + ),
800 + 'cancelled' => array(
801 + 'label' => __( 'Cancel Subscription', 'subscription' ),
802 + 'value' => 'cancelled',
803 + ),
804 + );
805 +
806 + $map_actions = array(
807 + 'pending' => array( 'active', 'cancelled' ),
808 + 'active' => array( 'pe_cancelled', 'cancelled' ),
809 + 'pe_cancelled' => array( 'active', 'cancelled' ),
810 + 'cancelled' => array( 'active' ),
811 + 'expired' => array( 'active', 'cancelled' ),
812 + 'completed' => array( 'cancelled' ),
813 + );
814 +
815 + $status = get_post_status( $subscription_id );
816 + $actions = $map_actions[ $status ] ?? array();
817 +
818 + // Gather subscription data for the view.
819 + $subscription_data = Helper::get_subscription_data( $subscription_id );
820 +
821 + $order_id = $subscription_data['order']['order_id'] ?? get_post_meta( $subscription_id, '_subscrpt_order_id', true );
822 + $order = $order_id ? wc_get_order( $order_id ) : null;
823 + $order_item_id = $subscription_data['order']['order_item_id'] ?? get_post_meta( $subscription_id, '_subscrpt_order_item_id', true );
824 + $order_item = ( $order && $order_item_id ) ? $order->get_item( $order_item_id ) : null;
825 +
826 + $rows = Subscriptions::get_info_rows( $subscription_id );
827 +
828 + // Related orders.
829 + $order_histories = Helper::get_related_orders( $subscription_id );
830 +
831 + $this->render_admin_header(
832 + '',
833 + '',
834 + array(
835 + array(
836 + 'label' => __( 'Subscriptions', 'subscription' ),
837 + 'url' => $list_url,
838 + ),
839 + array(
840 + 'label' => '#' . $subscription_id,
841 + 'url' => '',
842 + ),
843 + )
844 + );
845 +
846 + include __DIR__ . '/views/subscription-details.php';
847 +
848 + $this->render_admin_footer();
849 + }
850 +
851 + /**
505 852 * Render Stats page
506 853 */
507 854 public function render_stats_page() {
508 855 $this->render_admin_header( __( 'Reports', 'subscription' ), __( 'View your subscription analytics', 'subscription' ) );
@@ -564,17 +911,14 @@
564 911 * SPA-style: all sections rendered at once, JS controls visibility
565 912 * Initial load always shows page 1 (JS handles transitions from there)
566 913 */
567 914 public function render_onboarding_wizard() {
568 - // Start session if not already started
915 + // Start session if not already started (used by the wizard reset handler).
569 916 if ( ! session_id() && ! headers_sent() ) {
570 917 session_start();
571 918 }
572 919
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' ) );
920 + $this->render_admin_header( __( 'Setup Wizard', 'subscription' ), __( 'Create your first subscription plan', 'subscription' ) );
577 921 include __DIR__ . '/views/onboarding-wizard.php';
578 922 $this->render_admin_footer();
579 923 }
580 924