PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 1.10.5
Subscriptions for WooCommerce with Stripe Recurring Payments v1.10.5
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
subscription / includes / Admin / Menu.php

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

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