PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / trunk
Subscriptions for WooCommerce with Stripe Recurring Payments vtrunk
1.11.2 1.11.1 1.11.0 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.6 1.9.5 trunk 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 All 60 releases
subscription / includes / Admin / Menu.php

Menu.php in Subscriptions for WooCommerce with Stripe Recurring Payments trunk, at includes/Admin/Menu.php

882 lines 30.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SpringDevs\Subscription\Admin;
4
5 use SpringDevs\Subscription\Illuminate\Helper;
6
7 /**
8 * Menu class
9 *
10 * @package SpringDevs\Subscription\Admin
11 */
12 class Menu {
13
14 /**
15 * Initialize the class
16 */
17 public function __construct() {
18 add_action( 'admin_menu', array( $this, 'create_admin_menu' ) );
19 add_action( 'admin_menu', array( $this, 'reorder_submenu' ), 999 );
20 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
21 add_action( 'wp_ajax_subscrpt_bulk_action', array( $this, 'handle_bulk_action_ajax' ) );
22 }
23
24 /**
25 * Enqueue admin assets
26 */
27 public function enqueue_admin_assets() {
28 wp_enqueue_style(
29 'wp-subscription-admin',
30 SUBSCRPT_ASSETS . '/css/admin.css',
31 array(),
32 SUBSCRPT_VERSION
33 );
34
35 // Enqueue admin JavaScript for subscription list functionality
36 wp_enqueue_script(
37 'sdevs_subscription_admin',
38 SUBSCRPT_ASSETS . '/js/admin.js',
39 array( 'jquery' ),
40 SUBSCRPT_VERSION,
41 true
42 );
43
44 // Enqueue onboarding wizard styles.
45 wp_enqueue_style(
46 'subscrpt-onboarding-wizard',
47 SUBSCRPT_ASSETS . '/css/admin/onboarding-wizard.css',
48 array(),
49 SUBSCRPT_VERSION
50 );
51
52 // Enqueue onboarding wizard JS (loaded on wizard page)
53 wp_enqueue_script(
54 'subscrpt-onboarding-wizard',
55 SUBSCRPT_ASSETS . '/js/admin/onboarding-wizard.js',
56 array( 'jquery' ),
57 SUBSCRPT_VERSION,
58 true
59 );
60 wp_localize_script(
61 'subscrpt-onboarding-wizard',
62 'subscrpt_wizard',
63 array(
64 'ajax_url' => admin_url( 'admin-ajax.php' ),
65 'subscriptions_url' => admin_url( 'admin.php?page=wp-subscription' ),
66 'currency_symbol' => get_woocommerce_currency_symbol(),
67 )
68 );
69
70 // Localize script for AJAX
71 wp_localize_script(
72 'sdevs_subscription_admin',
73 'wp_subscription_ajax',
74 array(
75 'nonce' => wp_create_nonce( 'subscrpt_bulk_action_nonce' ),
76 'ajaxurl' => admin_url( 'admin-ajax.php' ),
77 )
78 );
79 }
80
81 /**
82 * Create Subscriptions Menu.
83 */
84 public function create_admin_menu() {
85 $parent_slug = 'wp-subscription';
86 // Determine if the menu is active
87 $is_active = isset( $_GET['page'] ) && strpos( sanitize_text_field( wp_unslash( $_GET['page'] ) ), 'wp-subscription' ) === 0;
88 $icon_url = $is_active
89 ? SUBSCRPT_ASSETS . '/images/icons/subscription-20.png'
90 : SUBSCRPT_ASSETS . '/images/icons/subscription-20-gray.png';
91
92 $pro_text = __( 'WPSubscription Pro required', 'subscription' );
93 $pro_badge = subscrpt_pro_activated() ? '' : ' <span title="' . $pro_text . '"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" style="fill:var(--wpsubs-brand);vertical-align:middle;margin-bottom:2.2px;flex-shrink:0;" aria-hidden="true"><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></span>';
94
95 // Main menu
96 add_menu_page(
97 __( 'WPSubscription', 'subscription' ),
98 __( 'WPSubscription', 'subscription' ),
99 'manage_options',
100 $parent_slug,
101 array( $this, 'render_subscriptions_page' ),
102 $icon_url,
103 40
104 );
105
106 // Onboarding Wizard (hidden from menu with CSS. can be accessed via direct URL: admin.php?page=wp-subscription-onboarding)
107 // CSS Record: admin.css -> `.wp-submenu a[href*="page=wp-subscription-onboarding"]`
108 add_submenu_page(
109 $parent_slug,
110 __( 'Setup Wizard', 'subscription' ),
111 __( 'Setup Wizard', 'subscription' ),
112 'manage_options',
113 'wp-subscription-onboarding',
114 array( $this, 'render_onboarding_wizard' )
115 );
116
117 // Subscriptions List
118 add_submenu_page(
119 $parent_slug,
120 __( 'Subscriptions', 'subscription' ),
121 __( 'Subscriptions', 'subscription' ),
122 'manage_options',
123 $parent_slug,
124 array( $this, 'render_subscriptions_page' )
125 );
126
127 // Subscription Details (hidden from menu with CSS — accessed via admin.php?page=wp-subscription-details&id=ID).
128 // CSS Record: admin.css -> `.wp-submenu a[href*="page=wp-subscription-details"]`
129 add_submenu_page(
130 $parent_slug,
131 __( 'Subscription Details', 'subscription' ),
132 __( 'Subscription Details', 'subscription' ),
133 'manage_options',
134 'wp-subscription-details',
135 array( $this, 'render_subscription_details_page' )
136 );
137
138 // Stats Overview
139 add_submenu_page(
140 $parent_slug,
141 __( 'Reports', 'subscription' ),
142 __( 'Reports', 'subscription' ) . $pro_badge,
143 'manage_options',
144 'wp-subscription-stats',
145 array( $this, 'render_stats_page' )
146 );
147
148 // Subscription Health
149 add_submenu_page(
150 $parent_slug,
151 __( 'Health', 'subscription' ),
152 __( 'Health', 'subscription' ) . $pro_badge,
153 'manage_options',
154 'wp-subscription-health',
155 array( $this, 'render_health_page' )
156 );
157
158 // Delivery Schedules
159 add_submenu_page(
160 $parent_slug,
161 __( 'Delivery', 'subscription' ),
162 __( 'Delivery', 'subscription' ) . $pro_badge,
163 'manage_options',
164 'wp-subscription-delivery',
165 array( $this, 'render_delivery_page' )
166 );
167
168 // Help & Resources
169 add_submenu_page(
170 $parent_slug,
171 __( 'Help', 'subscription' ),
172 __( 'Help', 'subscription' ),
173 'manage_options',
174 'wp-subscription-support',
175 array( $this, 'render_support_page' )
176 );
177
178 // Add WPSubscription link under WooCommerce menu
179 add_submenu_page(
180 'woocommerce',
181 __( 'WPSubscription', 'subscription' ),
182 __( 'WPSubscription', 'subscription' ),
183 'manage_options',
184 'wp-subscription',
185 array( $this, 'render_subscriptions_page' )
186 );
187 }
188
189 /**
190 * Reorder the WPSubscription submenu after all items are registered.
191 *
192 * Runs at admin_menu priority 999 so every plugin has already inserted
193 * its items. A filter lets the Pro plugin (or any extension) adjust the
194 * slug order before sorting is applied.
195 *
196 * @do_action subscrpt_submenu_order {string[]} $order Ordered list of submenu page slugs.
197 */
198 public function reorder_submenu() {
199 $parent = 'wp-subscription';
200
201 global $submenu;
202
203 if ( empty( $submenu[ $parent ] ) ) {
204 return;
205 }
206
207 // slug => position. Use gaps of 10 so extensions can insert between items.
208 $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
215 'wp-subscription-settings' => 998, // Settings
216 'wp-subscription-license' => 999, // License (pro)
217 ];
218
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 /**
227 * Filter the WPSubscription submenu slug order.
228 *
229 * Each entry is a slug => integer position pair. Lower positions appear
230 * first. Use gaps of 10 between built-in positions so extensions can
231 * insert their own slugs between existing items without renumbering.
232 *
233 * Example (pro plugin adding Delivery at position 35):
234 * add_filter( 'subscrpt_submenu_order', function( $order ) {
235 * $order['wp-subscription-delivery'] = 35;
236 * return $order;
237 * } );
238 *
239 * @param array<string,int> $order Map of slug => position.
240 */
241 $order = apply_filters( 'subscrpt_submenu_order', $default_order );
242
243 // Sort by position value, preserving slug keys.
244 asort( $order );
245
246 // Index current items by slug for fast lookup.
247 $indexed = [];
248 foreach ( $submenu[ $parent ] as $item ) {
249 $indexed[ $item[2] ] = $item;
250 }
251
252 // Build sorted list from the ordered slugs.
253 $sorted = [];
254 foreach ( $order as $slug => $position ) {
255 if ( isset( $indexed[ $slug ] ) ) {
256 $sorted[] = $indexed[ $slug ];
257 unset( $indexed[ $slug ] );
258 }
259 }
260
261 // Append any remaining items not covered by the order list.
262 foreach ( $indexed as $item ) {
263 $sorted[] = $item;
264 }
265
266 $submenu[ $parent ] = $sorted; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Intentional reorder of submenu items.
267 }
268
269 /**
270 * Render the admin header
271 */
272 public function render_admin_footer() {
273 ?>
274 <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
276 <div style="margin-top:6px;">
277 <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>
278 &nbsp;/&nbsp;
279 <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>
280 </div>
281 </div>
282 <?php
283 }
284 /**
285 * Render the admin header.
286 *
287 * The breadcrumb after the home icon is built from $breadcrumbs when given —
288 * an ordered trail supporting any number of levels — otherwise it falls back
289 * to the single $title segment (backward compatible with existing callers).
290 *
291 * Each breadcrumb item is an array: `[ 'label' => string, 'url' => string ]`.
292 * Items with a non-empty `url` render as links, except the last item, which is
293 * always the current (non-linked) page.
294 *
295 * @param string $title Page title shown as the current segment when no
296 * $breadcrumbs trail is supplied.
297 * @param string $subtitle Optional subtitle (reserved; not rendered).
298 * @param array $breadcrumbs Ordered trail of `[ 'label', 'url' ]` items.
299 */
300 public function render_admin_header( string $title = '', string $subtitle = '', array $breadcrumbs = [] ) {
301 $current = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : 'wp-subscription';
302
303 // Kept for backward compatibility — extensions may hook here for side-effects.
304 $menu_items = apply_filters( 'subscrpt_admin_header_menu_items', [], $current );
305 unset( $menu_items ); // Nav no longer rendered in header; navigation is in WP sidebar.
306
307 // Normalize to a single trail. Explicit breadcrumbs win; otherwise the
308 // legacy single $title segment is used.
309 $trail = [];
310 if ( ! empty( $breadcrumbs ) ) {
311 foreach ( $breadcrumbs as $crumb ) {
312 if ( is_array( $crumb ) && '' !== ( $crumb['label'] ?? '' ) ) {
313 $trail[] = [
314 'label' => (string) $crumb['label'],
315 'url' => isset( $crumb['url'] ) ? (string) $crumb['url'] : '',
316 ];
317 }
318 }
319 } elseif ( '' !== $title ) {
320 $trail[] = [
321 'label' => $title,
322 'url' => '',
323 ];
324 }
325
326 $last_index = count( $trail ) - 1;
327 ?>
328 <div class="wp-subscription-admin-header">
329 <div class="wp-subscription-admin-header-inner">
330 <div class="wp-subscription-admin-header-left">
331 <nav class="wp-subscription-breadcrumb" aria-label="<?php esc_attr_e( 'Breadcrumb', 'subscription' ); ?>">
332 <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' ); ?>">
333 <span class="dashicons dashicons-admin-home"></span>
334 </a>
335 <?php foreach ( $trail as $index => $crumb ) : ?>
336 <span class="wp-subscription-breadcrumb-sep" aria-hidden="true">/</span>
337 <?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>
339 <?php else : ?>
340 <span class="wp-subscription-breadcrumb-current"><?php echo esc_html( $crumb['label'] ); ?></span>
341 <?php endif; ?>
342 <?php endforeach; ?>
343 </nav>
344 </div>
345 <div class="wp-subscription-admin-header-right">
346 <?php if ( ! class_exists( 'Sdevs_Wc_Subscription_Pro' ) ) : ?>
347 <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 <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 <?php esc_html_e( 'Upgrade to Pro', 'subscription' ); ?>
350 </a>
351 <?php endif; ?>
352 <img src="<?php echo esc_url( SUBSCRPT_ASSETS . '/images/logo-title.svg' ); ?>" alt="WPSubscription" class="wp-subscription-logo">
353 </div>
354 </div>
355 </div>
356 <?php
357 }
358
359 /**
360 * Render Subscriptions page
361 */
362 public function render_subscriptions_page() {
363 $this->render_admin_header( __( 'Subscriptions', 'subscription' ), __( 'Manage your subscriptions', 'subscription' ) );
364
365 // Handle filters
366 $status = isset( $_GET['subscrpt_status'] ) ? sanitize_text_field( wp_unslash( $_GET['subscrpt_status'] ) ) : '';
367 $search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
368 $date_filter = isset( $_GET['date_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['date_filter'] ) ) : '';
369 $per_page = isset( $_GET['per_page'] ) ? max( 1, intval( $_GET['per_page'] ) ) : 20;
370 $paged = isset( $_GET['paged'] ) ? max( 1, intval( $_GET['paged'] ) ) : 1;
371
372 // Handle form submissions (both filters and bulk actions)
373 $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
374 if ( 'POST' === $request_method ) {
375 // Verify nonce before processing any POST data.
376 $nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '';
377 if ( ! wp_verify_nonce( $nonce, 'subscrpt_list_action' ) ) {
378 wp_die( esc_html__( 'Security check failed.', 'subscription' ) );
379 }
380 // Handle bulk actions
381 if ( isset( $_POST['bulk_action'] ) || isset( $_POST['bulk_action2'] ) ) {
382 $bulk_action = isset( $_POST['bulk_action'] ) ? sanitize_text_field( wp_unslash( $_POST['bulk_action'] ) ) : sanitize_text_field( wp_unslash( $_POST['bulk_action2'] ?? '' ) );
383 $action = isset( $_POST['action'] ) ? sanitize_text_field( wp_unslash( $_POST['action'] ) ) : sanitize_text_field( wp_unslash( $_POST['action2'] ?? '' ) );
384
385 if ( $bulk_action && $action && $action !== '-1' && isset( $_POST['subscription_ids'] ) && is_array( $_POST['subscription_ids'] ) ) {
386 $subscription_ids = array_map( 'intval', $_POST['subscription_ids'] );
387
388 if ( $action === 'trash' ) {
389 foreach ( $subscription_ids as $sub_id ) {
390 wp_trash_post( $sub_id );
391 }
392 } elseif ( $action === 'restore' ) {
393 foreach ( $subscription_ids as $sub_id ) {
394 wp_untrash_post( $sub_id );
395 }
396 } elseif ( $action === 'delete' ) {
397 foreach ( $subscription_ids as $sub_id ) {
398 wp_delete_post( $sub_id, true );
399 }
400 }
401
402 wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription' ) );
403 exit;
404 }
405 }
406
407 // Handle filter form submission
408 if ( isset( $_POST['filter_action'] ) ) {
409 $filter_params = array();
410
411 if ( ! empty( $_POST['subscrpt_status'] ) ) {
412 $filter_params['subscrpt_status'] = sanitize_text_field( wp_unslash( $_POST['subscrpt_status'] ) );
413 }
414 if ( ! empty( $_POST['date_filter'] ) ) {
415 $filter_params['date_filter'] = sanitize_text_field( wp_unslash( $_POST['date_filter'] ) );
416 }
417 if ( ! empty( $_POST['s'] ) ) {
418 $filter_params['s'] = sanitize_text_field( wp_unslash( $_POST['s'] ) );
419 }
420 if ( ! empty( $_POST['per_page'] ) ) {
421 $filter_params['per_page'] = intval( $_POST['per_page'] );
422 }
423
424 $redirect_url = add_query_arg( $filter_params, admin_url( 'admin.php?page=wp-subscription' ) );
425 wp_safe_redirect( $redirect_url );
426 exit;
427 }
428 }
429
430 // Handle individual actions
431 if ( isset( $_GET['action'] ) && ! empty( $_GET['sub_id'] ) ) {
432 $sub_id = intval( $_GET['sub_id'] );
433 $action = sanitize_text_field( wp_unslash( $_GET['action'] ) );
434 $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
435
436 // Clean trash action.
437 if ( $action === 'clean_trash' ) {
438 // Verify nonce for security.
439 $nonce_action = 'wpsubs_action_clean_trash';
440 if ( ! wp_verify_nonce( $nonce, $nonce_action ) ) {
441 echo '<div class="notice notice-error"><p>' . esc_html__( 'Security check failed. Please try again.', 'subscription' ) . '</p></div>';
442 wp_die();
443 }
444
445 // Clean all trash items.
446 $trash_posts = get_posts(
447 [
448 'post_type' => 'subscrpt_order',
449 'post_status' => 'trash',
450 'numberposts' => -1,
451 'fields' => 'ids',
452 ]
453 );
454
455 foreach ( $trash_posts as $trash_id ) {
456 wp_delete_post( $trash_id, true );
457 }
458
459 wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription&subscrpt_status=trash' ) );
460 exit;
461 } else {
462 // For other actions, verify nonce with subscription ID.
463 $nonce_action = 'wpsubs_action_' . $sub_id;
464 if ( ! wp_verify_nonce( $nonce, $nonce_action ) ) {
465 echo '<div class="notice notice-error"><p>' . esc_html__( 'Security check failed. Please try again.', 'subscription' ) . '</p></div>';
466 wp_die();
467 }
468
469 $redirect_url = admin_url( 'admin.php?page=wp-subscription' );
470
471 switch ( $action ) {
472 case 'duplicate':
473 $post = get_post( $sub_id );
474 if ( $post && $post->post_type === 'subscrpt_order' ) {
475 $new_post = [
476 'post_title' => $post->post_title . ' (Copy)',
477 'post_content' => $post->post_content,
478 'post_status' => 'draft',
479 'post_type' => 'subscrpt_order',
480 ];
481 $new_id = wp_insert_post( $new_post );
482 if ( $new_id ) {
483 $meta = get_post_meta( $sub_id );
484 foreach ( $meta as $key => $values ) {
485 foreach ( $values as $value ) {
486 add_post_meta( $new_id, $key, maybe_unserialize( $value ) );
487 }
488 }
489 }
490 }
491 break;
492 case 'trash':
493 wp_trash_post( $sub_id );
494 break;
495 case 'restore':
496 wp_untrash_post( $sub_id );
497 break;
498 case 'delete':
499 wp_delete_post( $sub_id, true );
500 $redirect_url = admin_url( 'admin.php?page=wp-subscription&subscrpt_status=trash' );
501 break;
502 }
503
504 wp_safe_redirect( $redirect_url );
505 exit;
506 }
507 }
508
509 $args = [
510 'post_type' => 'subscrpt_order',
511 'post_status' => 'any',
512 'posts_per_page' => $per_page,
513 'paged' => $paged,
514 'orderby' => 'date',
515 'order' => 'DESC',
516 ];
517
518 if ( $status ) {
519 $args['post_status'] = $status;
520 }
521 // Search only by subscription ID
522 if ( $search !== '' ) {
523 if ( is_numeric( $search ) ) {
524 $args['p'] = intval( $search );
525 } else {
526 // If not numeric, return no results
527 $args['post__in'] = array( 0 );
528 }
529 }
530 // Dynamic date filter (YYYY-MM)
531 if ( $date_filter && preg_match( '/^\d{4}-\d{2}$/', $date_filter ) ) {
532 $year = substr( $date_filter, 0, 4 );
533 $month = substr( $date_filter, 5, 2 );
534 $args['date_query'][] = [
535 'year' => intval( $year ),
536 'month' => intval( $month ),
537 ];
538 }
539
540 $query = new \WP_Query( $args );
541 $subscriptions = $query->posts;
542 $total = $query->found_posts;
543 $max_num_pages = $query->max_num_pages;
544
545 // Get all possible statuses for filter dropdown
546 $all_statuses = get_post_stati( [ 'show_in_admin_all_list' => true ], 'objects' );
547
548 include __DIR__ . '/views/subscription-list.php';
549 ?>
550 <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
552 <div style="margin-top:6px;">
553 <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>
554 &nbsp;/&nbsp;
555 <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>
556 </div>
557 </div>
558 <?php
559 }
560
561 /**
562 * Render the standalone Subscription Details page.
563 *
564 * Replaces the legacy metabox edit screen. Mirrors the list-page design
565 * shell (header + wpsubs components) and handles the status-change form.
566 *
567 * @return void
568 */
569 public function render_subscription_details_page() {
570 $subscription_id = isset( $_GET['id'] ) ? absint( wp_unslash( $_GET['id'] ) ) : 0;
571
572 if ( ! $subscription_id || 'subscrpt_order' !== get_post_type( $subscription_id ) ) {
573 wp_die( esc_html__( 'Invalid subscription.', 'subscription' ) );
574 }
575
576 if ( ! current_user_can( 'edit_post', $subscription_id ) ) {
577 wp_die( esc_html__( 'You do not have permission to view this subscription.', 'subscription' ) );
578 }
579
580 $list_url = admin_url( 'admin.php?page=wp-subscription' );
581 $form_action = admin_url( 'admin.php?page=wp-subscription-details&id=' . $subscription_id );
582
583 // Handle the status-change submission.
584 $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
585 if ( 'POST' === $request_method && isset( $_POST['subscrpt_order_action'] ) ) {
586 $nonce = isset( $_POST['subscrpt_order_action_nonce_field'] ) ? sanitize_text_field( wp_unslash( $_POST['subscrpt_order_action_nonce_field'] ) ) : '';
587 if ( ! wp_verify_nonce( $nonce, 'subscrpt_order_action_nonce' ) ) {
588 wp_die( esc_html__( 'Security check failed.', 'subscription' ) );
589 }
590
591 $action = sanitize_text_field( wp_unslash( $_POST['subscrpt_order_action'] ) );
592 if ( '' !== $action ) {
593 Subscriptions::process_status_change( $subscription_id, $action );
594 }
595
596 wp_safe_redirect( $form_action );
597 exit;
598 }
599
600 // Allowed actions for the current status (mirrors Subscriptions::subscrpt_order_save_post()).
601 $actions_data = array(
602 'active' => array(
603 'label' => __( 'Activate Subscription', 'subscription' ),
604 'value' => 'active',
605 ),
606 'pending' => array(
607 'label' => __( 'Pending Subscription', 'subscription' ),
608 'value' => 'pending',
609 ),
610 'expire' => array(
611 'label' => __( 'Expire Subscription', 'subscription' ),
612 'value' => 'expired',
613 ),
614 'pe_cancelled' => array(
615 'label' => __( 'Pending Cancel Subscription', 'subscription' ),
616 'value' => 'pe_cancelled',
617 ),
618 'cancelled' => array(
619 'label' => __( 'Cancel Subscription', 'subscription' ),
620 'value' => 'cancelled',
621 ),
622 );
623
624 $map_actions = array(
625 'pending' => array( 'active', 'cancelled' ),
626 'active' => array( 'pe_cancelled', 'cancelled' ),
627 'pe_cancelled' => array( 'active', 'cancelled' ),
628 'cancelled' => array( 'active' ),
629 'expired' => array( 'active', 'cancelled' ),
630 );
631
632 $status = get_post_status( $subscription_id );
633 $actions = $map_actions[ $status ] ?? array();
634
635 // Gather subscription data for the view.
636 $subscription_data = Helper::get_subscription_data( $subscription_id );
637
638 $order_id = $subscription_data['order']['order_id'] ?? get_post_meta( $subscription_id, '_subscrpt_order_id', true );
639 $order = $order_id ? wc_get_order( $order_id ) : null;
640 $order_item_id = $subscription_data['order']['order_item_id'] ?? get_post_meta( $subscription_id, '_subscrpt_order_item_id', true );
641 $order_item = ( $order && $order_item_id ) ? $order->get_item( $order_item_id ) : null;
642
643 $rows = Subscriptions::get_info_rows( $subscription_id );
644
645 // Related orders.
646 $order_histories = Helper::get_related_orders( $subscription_id );
647
648 $this->render_admin_header(
649 '',
650 '',
651 array(
652 array(
653 'label' => __( 'Subscriptions', 'subscription' ),
654 'url' => $list_url,
655 ),
656 array(
657 'label' => '#' . $subscription_id,
658 'url' => '',
659 ),
660 )
661 );
662
663 include __DIR__ . '/views/subscription-details.php';
664
665 $this->render_admin_footer();
666 }
667
668 /**
669 * Render Stats page
670 */
671 public function render_stats_page() {
672 $this->render_admin_header( __( 'Reports', 'subscription' ), __( 'View your subscription analytics', 'subscription' ) );
673
674 if ( ! subscrpt_pro_activated() ) {
675 include 'views/reports-preview.php';
676 } else {
677 // Allow pro plugin to override the entire stats page content.
678 do_action( 'subscrpt_render_stats_page' );
679 }
680
681 $this->render_admin_footer();
682 }
683
684 /**
685 * Render Health page
686 */
687 public function render_health_page() {
688 $this->render_admin_header( __( 'Health', 'subscription' ), __( 'Monitor your subscription health', 'subscription' ) );
689
690 if ( ! subscrpt_pro_activated() ) {
691 include 'views/health-preview.php';
692 } else {
693 // Allow pro plugin to render the full health page content.
694 do_action( 'subscrpt_render_health_page' );
695 }
696
697 $this->render_admin_footer();
698 }
699
700 /**
701 * Render Delivery Schedules page.
702 * When pro is active, fires subscrpt_render_delivery_page for pro to handle.
703 */
704 public function render_delivery_page() {
705 $this->render_admin_header( __( 'Delivery Schedules', 'subscription' ), __( 'Track and manage subscription delivery schedules.', 'subscription' ) );
706
707 if ( ! subscrpt_pro_activated() ) {
708 include 'views/delivery-preview.php';
709 } else {
710 // Allow pro plugin to render the full delivery page content.
711 do_action( 'subscrpt_render_delivery_page' );
712 }
713
714 $this->render_admin_footer();
715 }
716
717 /**
718 * Render Support page
719 */
720 public function render_support_page() {
721 $this->render_admin_header( __( 'Help & Resources', 'subscription' ), __( 'Documentation, community links, and ways to get help with WPSubscription.', 'subscription' ) );
722 include 'views/support.php';
723 $this->render_admin_footer();
724 }
725
726 /**
727 * Render Onboarding Wizard page
728 * SPA-style: all sections rendered at once, JS controls visibility
729 * Initial load always shows page 1 (JS handles transitions from there)
730 */
731 public function render_onboarding_wizard() {
732 // Start session if not already started
733 if ( ! session_id() && ! headers_sent() ) {
734 session_start();
735 }
736
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' ) );
741 include __DIR__ . '/views/onboarding-wizard.php';
742 $this->render_admin_footer();
743 }
744
745 /**
746 * Render the legacy subscriptions list page (WP_List_Table based)
747 */
748 public function render_legacy_subscriptions_page() {
749 // No longer needed, as the menu now links directly to the post type list.
750 }
751
752 /**
753 * Handle bulk action AJAX
754 */
755 public function handle_bulk_action_ajax() {
756 // Verify nonce
757 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
758 if ( ! wp_verify_nonce( $nonce, 'subscrpt_bulk_action_nonce' ) ) {
759 wp_send_json_error( array( 'message' => __( 'Security check failed.', 'subscription' ) ) );
760 }
761
762 // Check permissions
763 if ( ! current_user_can( 'manage_woocommerce' ) ) {
764 wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'subscription' ) ) );
765 }
766
767 // Get action and subscription IDs
768 $bulk_action = isset( $_POST['bulk_action'] ) ? sanitize_text_field( wp_unslash( $_POST['bulk_action'] ) ) : '';
769 $subscription_ids = isset( $_POST['subscription_ids'] ) ? array_map( 'intval', $_POST['subscription_ids'] ) : array();
770
771 if ( empty( $subscription_ids ) ) {
772 wp_send_json_error( array( 'message' => __( 'No subscriptions selected.', 'subscription' ) ) );
773 }
774
775 $processed_count = 0;
776 $errors = array();
777
778 foreach ( $subscription_ids as $subscription_id ) {
779 $post = get_post( $subscription_id );
780
781 if ( ! $post || $post->post_type !== 'subscrpt_order' ) {
782 // translators: Subscription ID.
783 $errors[] = sprintf( __( 'Subscription #%d not found.', 'subscription' ), $subscription_id );
784 continue;
785 }
786
787 try {
788 switch ( $bulk_action ) {
789 case 'trash':
790 if ( wp_trash_post( $subscription_id ) ) {
791 ++$processed_count;
792 } else {
793 $errors[] = sprintf(
794 // translators: Subscription ID.
795 __( 'Failed to move subscription #%d to trash.', 'subscription' ),
796 $subscription_id
797 );
798 }
799 break;
800
801 case 'restore':
802 if ( wp_untrash_post( $subscription_id ) ) {
803 ++$processed_count;
804 } else {
805 $errors[] = sprintf(
806 // translators: Subscription ID.
807 __( 'Failed to restore subscription #%d.', 'subscription' ),
808 $subscription_id
809 );
810 }
811 break;
812
813 case 'delete':
814 if ( wp_delete_post( $subscription_id, true ) ) {
815 ++$processed_count;
816 } else {
817 $errors[] = sprintf(
818 // translators: Subscription ID.
819 __( 'Failed to delete subscription #%d.', 'subscription' ),
820 $subscription_id
821 );
822 }
823 break;
824
825 default:
826 $errors[] = sprintf(
827 // translators: Bulk action.
828 __( 'Unknown action: %s', 'subscription' ),
829 $bulk_action
830 );
831 break;
832 }
833 } catch ( Exception $e ) {
834 $errors[] = sprintf(
835 // translators: Subscription ID, Error message.
836 __( 'Error processing subscription #%1$d: %2$s', 'subscription' ),
837 $subscription_id,
838 $e->getMessage()
839 );
840 }
841 }
842
843 // Prepare response message
844 $message = '';
845 if ( $processed_count > 0 ) {
846 switch ( $bulk_action ) {
847 case 'trash':
848 $message = sprintf(
849 // translators: Number of subscriptions.
850 _n( '%d subscription moved to trash.', '%d subscriptions moved to trash.', $processed_count, 'subscription' ),
851 $processed_count
852 );
853 break;
854 case 'restore':
855 $message = sprintf(
856 // translators: Number of subscriptions.
857 _n( '%d subscription restored.', '%d subscriptions restored.', $processed_count, 'subscription' ),
858 $processed_count
859 );
860 break;
861 case 'delete':
862 $message = sprintf(
863 // translators: Number of subscriptions.
864 _n( '%d subscription permanently deleted.', '%d subscriptions permanently deleted.', $processed_count, 'subscription' ),
865 $processed_count
866 );
867 break;
868 }
869 }
870
871 if ( ! empty( $errors ) ) {
872 $message .= ' ' . __( 'Some errors occurred:', 'subscription' ) . ' ' . implode( ', ', $errors );
873 }
874
875 if ( $processed_count > 0 ) {
876 wp_send_json_success( array( 'message' => $message ) );
877 } else {
878 wp_send_json_error( array( 'message' => $message ? $message : __( 'No subscriptions were processed.', 'subscription' ) ) );
879 }
880 }
881 }
882