PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 1.10.3
Subscriptions for WooCommerce with Stripe Recurring Payments v1.10.3
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 1.4.1 1.4.2 1.5.0 1.5.1 All 61 releases
subscription / includes / Admin / Menu.php

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

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