PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 2.0.0
Subscriptions for WooCommerce with Stripe Recurring Payments v2.0.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 1.4.1 1.4.2 1.5.0 1.5.1 All 61 releases
← All changes | includes/Admin/Menu.php +1017 -548 1.4.02.0.0 View file →
@@ -1,9 +1,15 @@
1 1 <?php
2 +/**
3 + * Admin menu + shared admin header/breadcrumb renderer.
4 + *
5 + * @package SpringDevs\Subscription\Admin
6 + */
2 7
3 8 namespace SpringDevs\Subscription\Admin;
4 9
5 -use function Avifinfo\read;
10 +use SpringDevs\Subscription\Illuminate\Helper;
11 +use SpringDevs\Subscription\Illuminate\Stats;
6 12
7 13 /**
8 14 * Menu class
9 15 *
@@ -10,576 +16,1039 @@
10 16 * @package SpringDevs\Subscription\Admin
11 17 */
12 18 class Menu {
13 19
14 - /**
15 - * Initialize the class
16 - */
17 - public function __construct() {
18 - add_action( 'admin_menu', array( $this, 'create_admin_menu' ) );
19 - add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
20 - }
20 + /**
21 + * Initialize the class
22 + */
23 + public function __construct() {
24 + add_action( 'admin_menu', array( $this, 'create_admin_menu' ) );
25 + add_action( 'admin_menu', array( $this, 'reorder_submenu' ), 999 );
26 + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
27 + add_action( 'wp_ajax_subscrpt_bulk_action', array( $this, 'handle_bulk_action_ajax' ) );
28 + add_action( 'admin_init', array( $this, 'maybe_onboarding_redirect' ) );
29 + }
21 30
22 - /**
23 - * Enqueue admin assets
24 - */
25 - public function enqueue_admin_assets() {
26 - wp_enqueue_style(
27 - 'wp-subscription-admin',
28 - WP_SUBSCRIPTION_ASSETS . '/css/admin.css',
29 - array(),
30 - WP_SUBSCRIPTION_VERSION
31 - );
32 - }
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 + }
33 47
34 - /**
35 - * Create Subscriptions Menu.
36 - */
37 - public function create_admin_menu() {
38 - $parent_slug = 'wp-subscription';
39 - // Determine if the menu is active
40 - $is_active = isset($_GET['page']) && strpos($_GET['page'], 'wp-subscription') === 0;
41 - $icon_url = $is_active
42 - ? WP_SUBSCRIPTION_ASSETS . '/images/icons/subscription-20.png'
43 - : WP_SUBSCRIPTION_ASSETS . '/images/icons/subscription-20-gray.png';
44 - // Main menu
45 - add_menu_page(
46 - __( 'WP Subscription', 'wp_subscription' ),
47 - __( 'WP Subscription', 'wp_subscription' ),
48 - 'manage_woocommerce',
49 - $parent_slug,
50 - array( $this, 'render_subscriptions_page' ),
51 - $icon_url,
52 - 40
53 - );
48 + if ( wp_doing_ajax() || is_network_admin() || ! current_user_can( 'manage_options' ) ) {
49 + return;
50 + }
54 51
55 - // Subscriptions List
56 - add_submenu_page(
57 - $parent_slug,
58 - __( 'Subscriptions', 'wp_subscription' ),
59 - __( 'Subscriptions', 'wp_subscription' ),
60 - 'manage_woocommerce',
61 - $parent_slug,
62 - array( $this, 'render_subscriptions_page' )
63 - );
52 + // Fire at most once, ever.
53 + if ( get_option( 'subscrpt_onboarding_seen' ) ) {
54 + return;
55 + }
56 + update_option( 'subscrpt_onboarding_seen', 1, false );
64 57
65 - // Stats Overview
66 - add_submenu_page(
67 - $parent_slug,
68 - __( 'Reports', 'wp_subscription' ),
69 - __( 'Reports', 'wp_subscription' ),
70 - 'manage_woocommerce',
71 - 'wp-subscription-stats',
72 - array( $this, 'render_stats_page' )
73 - );
58 + // Only first-time users with no plan yet.
59 + if ( ! empty( \SpringDevs\Subscription\Illuminate\Plans\PlanRepository::get_groups() ) ) {
60 + return;
61 + }
74 62
75 - // Settings
76 - add_submenu_page(
77 - $parent_slug,
78 - __( 'Settings', 'wp_subscription' ),
79 - __( 'Settings', 'wp_subscription' ),
80 - 'manage_woocommerce',
81 - 'wp-subscription-settings',
82 - array( $this, 'render_settings_page' )
83 - );
63 + wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription-onboarding' ) );
64 + exit;
65 + }
84 66
85 - // Support
86 - add_submenu_page(
87 - $parent_slug,
88 - __( 'Support', 'wp_subscription' ),
89 - __( 'Support', 'wp_subscription' ),
90 - 'manage_woocommerce',
91 - 'wp-subscription-support',
92 - array( $this, 'render_support_page' )
93 - );
67 + /**
68 + * Enqueue admin assets
69 + */
70 + public function enqueue_admin_assets() {
71 + wp_enqueue_style(
72 + 'wp-subscription-admin',
73 + SUBSCRPT_ASSETS . '/css/admin.css',
74 + array(),
75 + SUBSCRPT_VERSION
76 + );
94 77
95 - // Add WP Subscription link under WooCommerce menu
96 - add_submenu_page(
97 - 'woocommerce',
98 - __( 'WP Subscription', 'wp_subscription' ),
99 - __( 'WP Subscription', 'wp_subscription' ),
100 - 'manage_woocommerce',
101 - 'wp-subscription',
102 - array( $this, 'render_subscriptions_page' )
103 - );
104 - }
105 - /**
106 - * Render the admin header
107 - */
108 - private function render_admin_footer() {
109 - ?>
110 - <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
111 - Made with <span style="color:#e25555;font-size:1.1em;">♥</span> by the WP Subscription Team
112 - <div style="margin-top:6px;">
113 - <a href="https://wpsubscription.co/support" target="_blank" style="color:#2563eb;text-decoration:none;">Support</a>
114 - &nbsp;/&nbsp;
115 - <a href="https://wpsubscription.co/docs" target="_blank" style="color:#2563eb;text-decoration:none;">Docs</a>
116 - </div>
117 - </div>
118 - <?php
119 - }
120 - /**
121 - * Render the admin header
122 - */
123 - private function render_admin_header() {
124 - // Get current page slug
125 - $current = isset($_GET['page']) ? sanitize_text_field($_GET['page']) : 'wp-subscription';
126 - $menu_items = [
127 - [
128 - 'slug' => 'wp-subscription',
129 - 'label' => __('Subscriptions', 'wp_subscription'),
130 - 'url' => admin_url('admin.php?page=wp-subscription'),
131 - ],
132 - [
133 - 'slug' => 'wp-subscription-stats',
134 - 'label' => __('Reports', 'wp_subscription'),
135 - 'url' => admin_url('admin.php?page=wp-subscription-stats'),
136 - ],
137 - [
138 - 'slug' => 'wp-subscription-settings',
139 - 'label' => __('Settings', 'wp_subscription'),
140 - 'url' => admin_url('admin.php?page=wp-subscription-settings'),
141 - ],
142 - [
143 - 'slug' => 'wp-subscription-support',
144 - 'label' => __('Support', 'wp_subscription'),
145 - 'url' => admin_url('admin.php?page=wp-subscription-support'),
146 - ],
147 - ];
148 - // Allow pro plugin to inject menu items
149 - $menu_items = apply_filters('wp_subscription_admin_header_menu_items', $menu_items, $current);
150 - ?>
151 - <div class="wp-subscription-admin-header">
152 - <div style="width:1240px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;">
153 - <div class="wp-subscription-admin-header-left" style="display:flex;align-items:center;gap:14px;">
154 - <img style="height:30px;" src="<?php echo esc_url( WP_SUBSCRIPTION_ASSETS . '/images/logo-title.svg' ); ?>" alt="WP Subscription" class="wp-subscription-logo">
155 - <nav class="wp-subscription-admin-header-menu">
156 - <?php foreach ($menu_items as $item): ?>
157 - <a href="<?php echo esc_url($item['url']); ?>" class="<?php echo ($current === $item['slug']) ? 'current' : ''; ?>">
158 - <?php echo esc_html($item['label']); ?>
159 - </a>
160 - <?php endforeach; ?>
161 - </nav>
162 - </div>
163 - <div class="wp-subscription-admin-header-right">
164 - <?php if ( ! class_exists('Sdevs_Wc_Subscription_Pro') ) : ?>
165 - <a target="_blank" href="https://wpsubscription.co/?utm_source=plugin&utm_medium=admin&utm_campaign=upgrade_pro" class="wp-subscription-upgrade-btn"><?php _e( 'Upgrade to Pro', 'wp_subscription' ); ?></a>
166 - <?php endif; ?>
167 - </div>
168 - </div>
169 - </div>
170 - <?php
171 - }
78 + // Enqueue admin JavaScript for subscription list functionality
79 + wp_enqueue_script(
80 + 'sdevs_subscription_admin',
81 + SUBSCRPT_ASSETS . '/js/admin.js',
82 + array( 'jquery' ),
83 + SUBSCRPT_VERSION,
84 + true
85 + );
172 86
173 - /**
174 - * Render Subscriptions page
175 - */
176 - public function render_subscriptions_page() {
177 - $this->render_admin_header();
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 + );
178 94
179 - // Handle filters
180 - $status = isset($_GET['subscrpt_status']) ? sanitize_text_field($_GET['subscrpt_status']) : '';
181 - $search = isset($_GET['s']) ? sanitize_text_field($_GET['s']) : '';
182 - $date_filter = isset($_GET['date_filter']) ? sanitize_text_field($_GET['date_filter']) : '';
183 - $per_page = isset($_GET['per_page']) ? max(1, intval($_GET['per_page'])) : 20;
184 - $paged = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
95 + // Enqueue onboarding wizard JS (loaded on wizard page). Depends on the
96 + // admin components so the cadence picker (adv-select) is ready.
97 + wp_enqueue_script(
98 + 'subscrpt-onboarding-wizard',
99 + SUBSCRPT_ASSETS . '/js/admin/onboarding-wizard.js',
100 + array( 'jquery', 'subscrpt_admin_components' ),
101 + SUBSCRPT_VERSION,
102 + true
103 + );
104 + $subscrpt_wizard_has_products = (bool) wc_get_products(
105 + array(
106 + 'status' => array( 'publish', 'draft', 'pending', 'private' ),
107 + 'limit' => 1,
108 + 'return' => 'ids',
109 + )
110 + );
185 111
186 - // Handle duplicate action
187 - if (isset($_GET['action']) && $_GET['action'] === 'duplicate' && !empty($_GET['sub_id'])) {
188 - $sub_id = intval($_GET['sub_id']);
189 - $post = get_post($sub_id);
190 - if ($post && $post->post_type === 'subscrpt_order') {
191 - $new_post = [
192 - 'post_title' => $post->post_title . ' (Copy)',
193 - 'post_content' => $post->post_content,
194 - 'post_status' => 'draft',
195 - 'post_type' => 'subscrpt_order',
196 - ];
197 - $new_id = wp_insert_post($new_post);
198 - if ($new_id) {
199 - $meta = get_post_meta($sub_id);
200 - foreach ($meta as $key => $values) {
201 - foreach ($values as $value) {
202 - add_post_meta($new_id, $key, maybe_unserialize($value));
203 - }
204 - }
205 - }
206 - }
207 - wp_safe_redirect(admin_url('admin.php?page=wp-subscription'));
208 - exit;
209 - }
112 + wp_localize_script(
113 + 'subscrpt-onboarding-wizard',
114 + 'subscrpt_wizard',
115 + array(
116 + 'ajax_url' => admin_url( 'admin-ajax.php' ),
117 + 'subscriptions_url' => admin_url( 'admin.php?page=wp-subscription' ),
118 + 'dashboard_url' => admin_url( 'admin.php?page=wp-subscription' ),
119 + 'products_url' => admin_url( 'edit.php?post_type=product' ),
120 + 'plans_url' => admin_url( 'admin.php?page=wp-subscription-plans' ),
121 + 'rest_url' => rest_url( 'wpsubscription/v1/plans' ),
122 + 'rest_nonce' => wp_create_nonce( 'wp_rest' ),
123 + 'currency_symbol' => get_woocommerce_currency_symbol(),
124 + 'is_pro' => subscrpt_pro_activated(),
125 + 'has_products' => $subscrpt_wizard_has_products,
126 + )
127 + );
210 128
211 - $args = [
212 - 'post_type' => 'subscrpt_order',
213 - 'post_status' => 'any',
214 - 'posts_per_page' => $per_page,
215 - 'paged' => $paged,
216 - 'orderby' => 'date',
217 - 'order' => 'DESC',
218 - ];
129 + // Localize script for AJAX
130 + wp_localize_script(
131 + 'sdevs_subscription_admin',
132 + 'wp_subscription_ajax',
133 + array(
134 + 'nonce' => wp_create_nonce( 'subscrpt_bulk_action_nonce' ),
135 + 'ajaxurl' => admin_url( 'admin-ajax.php' ),
136 + )
137 + );
138 + }
219 139
220 - if ($status) {
221 - $args['post_status'] = $status;
222 - }
223 - // Search only by subscription ID
224 - if ($search !== '') {
225 - if (is_numeric($search)) {
226 - $args['p'] = intval($search);
227 - } else {
228 - // If not numeric, return no results
229 - $args['post__in'] = array(0);
230 - }
231 - }
232 - // Dynamic date filter (YYYY-MM)
233 - if ($date_filter && preg_match('/^\d{4}-\d{2}$/', $date_filter)) {
234 - $year = substr($date_filter, 0, 4);
235 - $month = substr($date_filter, 5, 2);
236 - $args['date_query'][] = [
237 - 'year' => intval($year),
238 - 'month' => intval($month),
239 - ];
240 - }
140 + /**
141 + * Create Subscriptions Menu.
142 + */
143 + public function create_admin_menu() {
144 + $parent_slug = 'wp-subscription';
145 + // Determine if the menu is active
146 + $is_active = isset( $_GET['page'] ) && strpos( sanitize_text_field( wp_unslash( $_GET['page'] ) ), 'wp-subscription' ) === 0;
147 + $icon_url = $is_active
148 + ? SUBSCRPT_ASSETS . '/images/icons/subscription-20.png'
149 + : SUBSCRPT_ASSETS . '/images/icons/subscription-20-gray.png';
241 150
242 - $query = new \WP_Query($args);
243 - $subscriptions = $query->posts;
244 - $total = $query->found_posts;
245 - $max_num_pages = $query->max_num_pages;
151 + $pro_text = __( 'WPSubscription Pro required', 'subscription' );
152 + $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>';
246 153
247 - // Get all possible statuses for filter dropdown
248 - $all_statuses = get_post_stati(['show_in_admin_all_list' => true], 'objects');
154 + // Main menu
155 + add_menu_page(
156 + __( 'WPSubscription', 'subscription' ),
157 + __( 'WPSubscription', 'subscription' ),
158 + 'manage_options',
159 + $parent_slug,
160 + array( $this, 'render_dashboard_page' ),
161 + $icon_url,
162 + 40
163 + );
249 164
250 - include dirname(__FILE__) . '/views/subscription-list.php';
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 WP Subscription Team
254 - <div style="margin-top:6px;">
255 - <a href="https://wpsubscription.co/support" target="_blank" style="color:#2563eb;text-decoration:none;">Support</a>
256 - &nbsp;/&nbsp;
257 - <a href="https://wpsubscription.co/docs" target="_blank" style="color:#2563eb;text-decoration:none;">Docs</a>
258 - </div>
259 - </div>
260 - <?php
261 - }
165 + // Onboarding Wizard (hidden from menu with CSS. can be accessed via direct URL: admin.php?page=wp-subscription-onboarding)
166 + // CSS Record: admin.css -> `.wp-submenu a[href*="page=wp-subscription-onboarding"]`
167 + add_submenu_page(
168 + $parent_slug,
169 + __( 'Setup Wizard', 'subscription' ),
170 + __( 'Setup Wizard', 'subscription' ),
171 + 'manage_options',
172 + 'wp-subscription-onboarding',
173 + array( $this, 'render_onboarding_wizard' )
174 + );
262 175
263 - /**
264 - * Render Stats page
265 - */
266 - public function render_stats_page() {
267 - $this->render_admin_header();
268 - // if ( ! class_exists('Sdevs_Wc_Subscription_Pro') ){
269 - // $this->render_admin_footer();
270 - // return;
271 - // }
176 + // Overview. WordPress makes the first submenu entry share the parent
177 + // slug, so this is the page the top-level item opens.
178 + add_submenu_page(
179 + $parent_slug,
180 + __( 'Overview', 'subscription' ),
181 + __( 'Overview', 'subscription' ),
182 + 'manage_options',
183 + $parent_slug,
184 + array( $this, 'render_dashboard_page' )
185 + );
272 186
273 - if ( ! class_exists('Sdevs_Wc_Subscription_Pro') ) { ?>
274 - <div class="wp-subscription-admin-content" style="max-width:1240px;margin:32px auto 0 auto">
275 - <div class="wp-subscription-hero-upgrade" style="margin-bottom:18px;">
276 - <div class="wp-subscription-hero-content">
277 - <span class="wp-subscription-hero-icon">✨</span>
278 - <span class="wp-subscription-hero-title">
279 - Unlock advanced features, priority support,<br>
280 - and more subscription control and reporting.
281 - </span>
282 - </div>
283 - <a href="https://wpsubscription.co/?utm_source=plugin&utm_medium=admin&utm_campaign=upgrade_pro"
284 - target="_blank"
285 - class="wp-subscription-hero-btn">
286 - UPGRADE TO PRO
287 - </a>
288 - </div>
289 - </div>
290 - <?php
291 - $this->render_admin_footer();
292 - return;
293 - }
187 + // Subscriptions List. Moved off the parent slug when the dashboard took
188 + // it; render_dashboard_page() redirects here when the request carries
189 + // list-only query arguments, so old bookmarks still work.
190 + add_submenu_page(
191 + $parent_slug,
192 + __( 'Subscriptions', 'subscription' ),
193 + __( 'Subscriptions', 'subscription' ),
194 + 'manage_options',
195 + 'wp-subscription-list',
196 + array( $this, 'render_subscriptions_page' )
197 + );
294 198
295 - global $wpdb;
296 - $months = [];
297 - $now = current_time('timestamp');
298 - for ($i = 11; $i >= 0; $i--) {
299 - $month = strtotime("-{$i} month", $now);
300 - $key = date('Y-m', $month);
301 - $months[$key] = [
302 - 'label' => date('M Y', $month),
303 - 'total' => 0,
304 - 'active' => 0,
305 - 'cancelled' => 0,
306 - 'revenue' => 0.0,
307 - ];
308 - }
309 - $start = strtotime('-11 months', $now);
310 - $start = strtotime(date('Y-m-01 00:00:00', $start));
311 - $end = strtotime(date('Y-m-t 23:59:59', $now));
312 - $subs = $wpdb->get_results($wpdb->prepare(
313 - "SELECT ID, post_status, post_date_gmt FROM {$wpdb->posts} WHERE post_type='subscrpt_order' AND post_status != 'trash' AND post_date_gmt >= %s AND post_date_gmt <= %s",
314 - gmdate('Y-m-d H:i:s', $start),
315 - gmdate('Y-m-d H:i:s', $end)
316 - ));
317 - // dd($subs);
318 - foreach ($subs as $sub) {
319 - $month = date('Y-m', strtotime($sub->post_date_gmt));
320 - if (!isset($months[$month])) continue;
321 - $months[$month]['total']++;
322 - if ($sub->post_status === 'active') $months[$month]['active']++;
323 - if ($sub->post_status === 'cancelled') $months[$month]['cancelled']++;
324 - // Revenue: get price meta
325 - $price = get_post_meta($sub->ID, '_subscrpt_price', true);
326 - $months[$month]['revenue'] += floatval($price);
327 - }
328 - // Next month projection: current active * avg revenue per active
329 - $active_now = $months[array_key_last($months)]['active'];
330 - $revenue_now = $months[array_key_last($months)]['revenue'];
331 - $avg_per_active = $active_now ? ($revenue_now / $active_now) : 0;
332 - $projection = round($active_now * $avg_per_active, 2);
333 - // Previous month
334 - $month_keys = array_keys($months);
335 - $prev_month_key = $month_keys[count($month_keys)-2] ?? null;
336 - $prev_month_label = $prev_month_key ? $months[$prev_month_key]['label'] : '';
337 - $prev_month_revenue = $prev_month_key ? $months[$prev_month_key]['revenue'] : 0;
338 - // Next month name
339 - $next_month_label = date('M Y', strtotime('+1 month', $now));
340 - ?>
341 - <div class="wp-subscription-admin-content" style="max-width:1240px;margin:32px auto 0 auto;">
342 - <div class="wp-subscription-admin-box" style="margin-bottom:32px;">
343 - <h1 class="wp-subscription-admin-title" style="margin-bottom:18px;">Monthly Subscription Report</h1>
344 - <div style="display:flex;gap:24px;flex-wrap:wrap;margin-bottom:50px;">
345 - <div style="flex:1;min-width:180px;background:#f7fafd;border-radius:8px;padding:18px 22px;text-align:center;">
346 - <div style="margin-bottom:10px;font-size:2em;font-weight:700;color:#2563eb;"><?php echo intval($months[array_key_last($months)]['total']); ?></div>
347 - <div style="color:#888;font-size:13px;">Subscriptions This Month</div>
348 - </div>
349 - <div style="flex:1;min-width:180px;background:#f7fafd;border-radius:8px;padding:18px 22px;text-align:center;">
350 - <div style="margin-bottom:10px;font-size:2em;font-weight:700;color:#27c775;"><?php echo intval($months[array_key_last($months)]['active']); ?></div>
351 - <div style="color:#888;font-size:13px;">Active Subscriptions</div>
352 - </div>
353 - <div style="flex:1;min-width:180px;background:#f7fafd;border-radius:8px;padding:18px 22px;text-align:center;">
354 - <div style="margin-bottom:10px;font-size:2em;font-weight:700;color:#d93025;"><?php echo intval($months[array_key_last($months)]['cancelled']); ?></div>
355 - <div style="color:#888;font-size:13px;">Cancelled This Month</div>
356 - </div>
357 - <div style="flex:1;min-width:180px;background:#f7fafd;border-radius:8px;padding:18px 22px;text-align:center;">
358 - <div style="margin-bottom:10px;font-size:2em;font-weight:700;color:#7f54b3;">$<?php echo number_format($months[array_key_last($months)]['revenue'], 2); ?></div>
359 - <div style="color:#888;font-size:13px;">Revenue This Month</div>
360 - </div>
361 - <div style="flex:1;min-width:180px;background:#f4f7fa;border-radius:8px;padding:18px 22px;text-align:center;">
362 - <div style="margin-bottom:10px;font-size:1.5em;font-weight:700;color:#7f54b3;">$<?php echo number_format($prev_month_revenue, 2); ?></div>
363 - <div style="font-size:1.1em;font-weight:600;color:#888;margin-bottom:2px;">Previous Month (<?php echo esc_html($prev_month_label); ?>)</div>
364 - </div>
365 - <div style="flex:1;min-width:180px;background:#e6f0fa;border-radius:8px;padding:18px 22px;text-align:center;">
366 - <div style="margin-bottom:10px;font-size:1.5em;font-weight:700;color:#2196f3;">$<?php echo number_format($projection, 2); ?></div>
367 - <div style="font-size:1.1em;font-weight:600;color:#888;margin-bottom:2px;">Next Month Projection (<?php echo esc_html($next_month_label); ?>)</div>
368 - </div>
369 - </div>
370 - <canvas id="wpsubscription-report-chart" height="110"></canvas>
371 - </div>
372 - </div>
373 -
374 - <?php $this->render_admin_footer(); ?>
199 + // Subscription Details (hidden from menu with CSS — accessed via admin.php?page=wp-subscription-details&id=ID).
200 + // CSS Record: admin.css -> `.wp-submenu a[href*="page=wp-subscription-details"]`
201 + add_submenu_page(
202 + $parent_slug,
203 + __( 'Subscription Details', 'subscription' ),
204 + __( 'Subscription Details', 'subscription' ),
205 + 'manage_options',
206 + 'wp-subscription-details',
207 + array( $this, 'render_subscription_details_page' )
208 + );
375 209
376 - <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
377 - <script>
378 - const ctx = document.getElementById('wpsubscription-report-chart').getContext('2d');
379 - const chart = new Chart(ctx, {
380 - type: 'bar',
381 - data: {
382 - labels: <?php echo json_encode(array_column($months, 'label')); ?>,
383 - datasets: [
384 - {
385 - label: 'Total',
386 - data: <?php echo json_encode(array_column($months, 'total')); ?>,
387 - backgroundColor: '#2563eb',
388 - },
389 - {
390 - label: 'Active',
391 - data: <?php echo json_encode(array_column($months, 'active')); ?>,
392 - backgroundColor: '#27c775',
393 - },
394 - {
395 - label: 'Cancelled',
396 - data: <?php echo json_encode(array_column($months, 'cancelled')); ?>,
397 - backgroundColor: '#d93025',
398 - },
399 - {
400 - label: 'Revenue',
401 - data: <?php echo json_encode(array_map('floatval', array_column($months, 'revenue'))); ?>,
402 - backgroundColor: '#7f54b3',
403 - type: 'line',
404 - yAxisID: 'y1',
405 - borderColor: '#7f54b3',
406 - fill: false,
407 - tension: 0.3,
408 - pointRadius: 3,
409 - pointBackgroundColor: '#7f54b3',
410 - }
411 - ]
412 - },
413 - options: {
414 - responsive: true,
415 - interaction: { mode: 'index', intersect: false },
416 - plugins: {
417 - legend: { position: 'top' },
418 - title: { display: false },
419 - tooltip: { enabled: true }
420 - },
421 - scales: {
422 - y: { beginAtZero: true, title: { display: true, text: 'Count' } },
423 - y1: {
424 - beginAtZero: true,
425 - position: 'right',
426 - grid: { drawOnChartArea: false },
427 - title: { display: true, text: 'Revenue ($)' }
428 - }
429 - }
430 - }
431 - });
432 - </script>
433 - <?php
434 - }
210 + // Stats Overview
211 + add_submenu_page(
212 + $parent_slug,
213 + __( 'Reports', 'subscription' ),
214 + __( 'Reports', 'subscription' ) . $pro_badge,
215 + 'manage_options',
216 + 'wp-subscription-stats',
217 + array( $this, 'render_stats_page' )
218 + );
435 219
436 - /**
437 - * Render Settings page
438 - */
439 - public function render_settings_page() {
440 - $this->render_admin_header();
441 - ?>
442 - <div class="wp-subscription-admin-content" style="max-width:1240px;margin:32px auto 0 auto">
443 - <div class="wp-subscription-admin-box">
444 - <?php include_once dirname(__FILE__) . '/views/settings.php'; ?>
445 - </div>
446 - </div>
447 - <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
448 - Made with <span style="color:#e25555;font-size:1.1em;">♥</span> by the WP Subscription Team
449 - <div style="margin-top:6px;">
450 - <a href="https://wpsubscription.co/support" target="_blank" style="color:#2563eb;text-decoration:none;">Support</a>
451 - &nbsp;/&nbsp;
452 - <a href="https://wpsubscription.co/docs" target="_blank" style="color:#2563eb;text-decoration:none;">Docs</a>
453 - </div>
454 - </div>
455 - <?php
456 - }
220 + // Subscription Health
221 + add_submenu_page(
222 + $parent_slug,
223 + __( 'Health', 'subscription' ),
224 + __( 'Health', 'subscription' ) . $pro_badge,
225 + 'manage_options',
226 + 'wp-subscription-health',
227 + array( $this, 'render_health_page' )
228 + );
457 229
458 - /**
459 - * Render Support page
460 - */
461 - public function render_support_page() {
462 - $this->render_admin_header();
463 - ?>
464 - <div class="wp-subscription-admin-content" style="max-width:1240px;margin:32px auto 0 auto">
465 - <?php if ( ! class_exists('Sdevs_Wc_Subscription_Pro') ) : ?>
466 - <!-- HERO VARIANT 1: Emoji -->
467 - <div class="wp-subscription-hero-upgrade" style="margin-bottom:18px;">
468 - <div class="wp-subscription-hero-content">
469 - <span class="wp-subscription-hero-icon">✨</span>
470 - <span class="wp-subscription-hero-title">
471 - Unlock advanced features, priority support,<br>
472 - and more subscription control and reporting.
473 - </span>
474 - </div>
475 - <a href="https://wpsubscription.co/?utm_source=plugin&utm_medium=admin&utm_campaign=upgrade_pro"
476 - target="_blank"
477 - class="wp-subscription-hero-btn">
478 - UPGRADE TO PRO
479 - </a>
480 - </div>
481 - <?php endif; ?>
482 -
483 - <!-- Product Overview & Video -->
484 - <div class="wp-subscription-admin-box" style="margin-bottom:24px;display:flex;gap:32px;align-items:flex-start;flex-wrap:wrap;">
485 - <div style="flex:2;min-width:260px;">
486 - <h3>Product Overview</h3>
487 - <p style="font-size:14px;line-height:1.7;margin:0 0 10px 0;">WP Subscription helps you manage WooCommerce subscriptions with ease. Enjoy a modern admin UI, powerful filters, detailed history, and seamless customer management. Designed for speed, clarity, and growth.</p>
488 - <ul style="font-size:14px;line-height:1.6;margin:0 0 0 18px;padding:0;list-style:disc;">
489 - <li>Modern, compact admin interface</li>
490 - <li>Advanced filtering & search</li>
491 - <li>Subscription history & activities</li>
492 - <li>Easy migration from other plugins</li>
493 - <li>Pro: Analytics, automation, integrations, and more!</li>
494 - </ul>
495 - </div>
496 - <div style="flex:1;min-width:220px;max-width:320px;">
497 - <div style="background:#f4f7fa;border-radius:8px;padding:10px 10px 6px 10px;box-shadow:0 2px 8px #e0e7ef;">
498 - <div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:6px;">
499 - <iframe src="https://www.youtube.com/embed/XXXXXXXX" title="WP Subscription Overview" frameborder="0" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;border-radius:6px;"></iframe>
500 - </div>
501 - <div style="text-align:center;font-size:13px;color:#888;margin-top:4px;">Watch: Quick Product Tour</div>
502 - </div>
503 - </div>
504 - </div>
230 + // Delivery Schedules
231 + add_submenu_page(
232 + $parent_slug,
233 + __( 'Delivery', 'subscription' ),
234 + __( 'Delivery', 'subscription' ) . $pro_badge,
235 + 'manage_options',
236 + 'wp-subscription-delivery',
237 + array( $this, 'render_delivery_page' )
238 + );
505 239
506 - <!-- PRO Features List -->
507 - <div class="wp-subscription-admin-box wp-subscription-pro-features" style="margin-bottom:32px;">
508 - <div style="font-size:1.3em;font-weight:600;margin-bottom:12px;display:flex;align-items:center;gap:10px;">
509 - <svg width="28" height="28" fill="none" viewBox="0 0 28 28"><circle cx="14" cy="14" r="14" fill="#2196f3"/><path d="M9 14.5l3 3 7-7" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
510 - <span>WP Subscription PRO Features</span>
511 - </div>
512 - <ul class="wp-subscription-pro-feature-list" style="list-style:none;padding:0;margin:0;display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:18px;">
513 - <li style="background:#f4f7fa;border-radius:8px;padding:18px 20px;display:flex;align-items:center;gap:14px;box-shadow:0 2px 8px #e0e7ef;">
514 - <span style="font-size:1.5em;color:#2196f3;">🔀</span>
515 - <span><b>Variable Product</b><br><span style="color:#555;font-size:0.98em;">Offer flexible subscription options for variable products.</span></span>
516 - </li>
517 - <li style="background:#f4f7fa;border-radius:8px;padding:18px 20px;display:flex;align-items:center;gap:14px;box-shadow:0 2px 8px #e0e7ef;">
518 - <span style="font-size:1.5em;color:#2196f3;">🚚</span>
519 - <span><b>Delivery Schedule</b><br><span style="color:#555;font-size:0.98em;">Set custom delivery intervals for each subscription.</span></span>
520 - </li>
521 - <li style="background:#f4f7fa;border-radius:8px;padding:18px 20px;display:flex;align-items:center;gap:14px;box-shadow:0 2px 8px #e0e7ef;">
522 - <span style="font-size:1.5em;color:#2196f3;">📜</span>
523 - <span><b>Subscription History</b><br><span style="color:#555;font-size:0.98em;">Track all changes and events for every subscription.</span></span>
524 - </li>
525 - <li style="background:#f4f7fa;border-radius:8px;padding:18px 20px;display:flex;align-items:center;gap:14px;box-shadow:0 2px 8px #e0e7ef;">
526 - <span style="font-size:1.5em;color:#2196f3;">⏳</span>
527 - <span><b>More Subscription Durations</b><br><span style="color:#555;font-size:0.98em;">Offer more flexible and custom subscription periods.</span></span>
528 - </li>
529 - <li style="background:#f4f7fa;border-radius:8px;padding:18px 20px;display:flex;align-items:center;gap:14px;box-shadow:0 2px 8px #e0e7ef;">
530 - <span style="font-size:1.5em;color:#2196f3;">💶</span>
531 - <span><b>Sign Up Fee</b><br><span style="color:#555;font-size:0.98em;">Charge a one-time sign up fee for new subscribers.</span></span>
532 - </li>
533 - <li style="background:#f4f7fa;border-radius:8px;padding:18px 20px;display:flex;align-items:center;gap:14px;box-shadow:0 2px 8px #e0e7ef;">
534 - <span style="font-size:1.5em;color:#2196f3;">⏩</span>
535 - <span><b>Early Renewal</b><br><span style="color:#555;font-size:0.98em;">Allow customers to renew their subscription before expiry.</span></span>
536 - </li>
537 - <li style="background:#f4f7fa;border-radius:8px;padding:18px 20px;display:flex;align-items:center;gap:14px;box-shadow:0 2px 8px #e0e7ef;">
538 - <span style="font-size:1.5em;color:#2196f3;">💳</span>
539 - <span><b>Renewal Price</b><br><span style="color:#555;font-size:0.98em;">Set a different price for subscription renewals.</span></span>
540 - </li>
541 - </ul>
542 - </div>
240 + // Help & Resources
241 + add_submenu_page(
242 + $parent_slug,
243 + __( 'Help', 'subscription' ),
244 + __( 'Help', 'subscription' ),
245 + 'manage_options',
246 + 'wp-subscription-support',
247 + array( $this, 'render_support_page' )
248 + );
543 249
544 - <!-- Support Resources: 2 rows, 2 columns, each in its own box -->
545 - <div class="wp-subscription-support-resources" style="display:grid;grid-template-columns:repeat(2,1fr);gap:24px;margin-bottom:24px;">
546 - <div class="wp-subscription-admin-box">
547 - <h3>Documentation</h3>
548 - <p style="font-size:14px;margin:0 0 8px 0;">Read our <a href="https://wpsubscription.co/docs\" target=\"_blank\" style=\"color:#2271b1;\">comprehensive docs</a> for setup, migration, and advanced usage.</p>
549 - <a href="https://wpsubscription.co/docs" target="_blank" class="button button-small" style="font-size:13px;padding:5px 14px;">View Docs</a>
550 - </div>
551 - <div class="wp-subscription-admin-box">
552 - <h3>Facing An Issue?</h3>
553 - <p style="font-size:14px;margin:0 0 8px 0;">If you have a problem, <a href="https://wpsubscription.co/support\" target=\"_blank\" style=\"color:#d93025;\">open a support ticket</a> or check our FAQ.</p>
554 - <a href="https://wpsubscription.co/support" target="_blank" class="button button-small" style="font-size:13px;padding:5px 14px;">Get Support</a>
555 - </div>
556 - <div class="wp-subscription-admin-box">
557 - <h3>Request a Feature</h3>
558 - <p style="font-size:14px;margin:0 0 8px 0;">Have an idea? <a href="https://wpsubscription.co/feature-request\" target=\"_blank\" style=\"color:#2271b1;\">Request a feature</a> or vote on others.</p>
559 - <a href="https://wpsubscription.co/feature-request" target="_blank" class="button button-small" style="font-size:13px;padding:5px 14px;">Request Feature</a>
560 - </div>
561 - <div class="wp-subscription-admin-box">
562 - <h3>Show Your Love</h3>
563 - <p style="font-size:14px;margin:0 0 8px 0;">Enjoying WP Subscription? <a href="https://wordpress.org/support/plugin/subscription/reviews/\" target=\"_blank\" style=\"color:#f59e42;\">Leave us a review</a> or share your experience!</p>
564 - <a href="https://wordpress.org/support/plugin/subscription/reviews/" target="_blank" class="button button-small" style="font-size:13px;padding:5px 14px;">Leave a Review</a>
565 - </div>
566 - </div>
567 - </div>
568 - <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
569 - Made with <span style="color:#e25555;font-size:1.1em;">♥</span> by the WP Subscription Team
570 - <div style="margin-top:6px;">
571 - <a href="https://wpsubscription.co/support" target="_blank" style="color:#2563eb;text-decoration:none;">Support</a>
572 - &nbsp;/&nbsp;
573 - <a href="https://wpsubscription.co/docs" target="_blank" style="color:#2563eb;text-decoration:none;">Docs</a>
574 - </div>
575 - </div>
576 - <?php
577 - }
250 + /*
251 + * Subscriptions link under the WooCommerce menu, opening the list.
252 + *
253 + * A link, not a page: the slug is the list's URL and there is no
254 + * callback, so WordPress registers no page and no hook for it.
255 + * Registering `wp-subscription-list` under `woocommerce` as well would
256 + * give the list a second hookname (`woocommerce_page_…` beside
257 + * `wpsubscription_page_…`), and whichever parent WordPress found first
258 + * would set the page's $hook_suffix, which its asset loading keys on.
259 + */
260 + add_submenu_page(
261 + 'woocommerce',
262 + __( 'Subscriptions', 'subscription' ),
263 + __( 'Subscriptions', 'subscription' ),
264 + 'manage_options',
265 + 'admin.php?page=wp-subscription-list'
266 + );
267 + }
578 268
579 - /**
580 - * Render the legacy subscriptions list page (WP_List_Table based)
581 - */
582 - public function render_legacy_subscriptions_page() {
583 - // No longer needed, as the menu now links directly to the post type list.
584 - }
269 + /**
270 + * Reorder the WPSubscription submenu after all items are registered.
271 + *
272 + * Runs at admin_menu priority 999 so every plugin has already inserted
273 + * its items. A filter lets the Pro plugin (or any extension) adjust the
274 + * slug order before sorting is applied.
275 + *
276 + * @do_action subscrpt_submenu_order {string[]} $order Ordered list of submenu page slugs.
277 + */
278 + public function reorder_submenu() {
279 + $parent = 'wp-subscription';
280 +
281 + global $submenu;
282 +
283 + if ( empty( $submenu[ $parent ] ) ) {
284 + return;
285 + }
286 +
287 + // slug => position. Use gaps of 10 so extensions can insert between items.
288 + //
289 + // The order is the same whether or not pro is active: a locked page
290 + // carries a pro badge but keeps its place, so the menu does not
291 + // rearrange itself the moment a licence is activated. Plans and
292 + // Cancellation Flow position themselves through the filter below.
293 + $default_order = [
294 + 'wp-subscription' => 5, // Overview
295 + 'wp-subscription-delivery' => 20, // Delivery (pro)
296 + 'wp-subscription-list' => 30, // Subscriptions
297 + 'wp-subscription-stats' => 40, // Reports
298 + 'wp-subscription-health' => 50, // Health
299 + 'wp-subscription-integrations' => 60, // Integrations
300 + 'wp-subscription-settings' => 998, // Settings
301 + 'wp-subscription-license' => 999, // License (pro)
302 + 'wp-subscription-support' => 1000, // Help & Resources
303 + ];
304 +
305 + /**
306 + * Filter the WPSubscription submenu slug order.
307 + *
308 + * Each entry is a slug => integer position pair. Lower positions appear
309 + * first. Use gaps of 10 between built-in positions so extensions can
310 + * insert their own slugs between existing items without renumbering.
311 + *
312 + * Example (an add-on placing its page between Reports and Health):
313 + * add_filter( 'subscrpt_submenu_order', function( $order ) {
314 + * $order['wp-subscription-my-addon'] = 45;
315 + * return $order;
316 + * } );
317 + *
318 + * @param array<string,int> $order Map of slug => position.
319 + */
320 + $order = apply_filters( 'subscrpt_submenu_order', $default_order );
321 +
322 + // Sort by position value, preserving slug keys.
323 + asort( $order );
324 +
325 + // Index current items by slug for fast lookup.
326 + $indexed = [];
327 + foreach ( $submenu[ $parent ] as $item ) {
328 + $indexed[ $item[2] ] = $item;
329 + }
330 +
331 + // Build sorted list from the ordered slugs.
332 + $sorted = [];
333 + foreach ( $order as $slug => $position ) {
334 + if ( isset( $indexed[ $slug ] ) ) {
335 + $sorted[] = $indexed[ $slug ];
336 + unset( $indexed[ $slug ] );
337 + }
338 + }
339 +
340 + // Append any remaining items not covered by the order list.
341 + foreach ( $indexed as $item ) {
342 + $sorted[] = $item;
343 + }
344 +
345 + $submenu[ $parent ] = $sorted; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Intentional reorder of submenu items.
346 + }
347 +
348 + /**
349 + * Render the admin header
350 + */
351 + public function render_admin_footer() {
352 + ?>
353 + <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
354 + Made with <span style="color:#e25555;font-size:1.1em;">♥</span> by the WPSubscription Team
355 + <div style="margin-top:6px;">
356 + <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>
357 + &nbsp;/&nbsp;
358 + <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>
359 + </div>
360 + </div>
361 + <?php
362 + }
363 + /**
364 + * Render the admin header.
365 + *
366 + * The breadcrumb after the home icon is built from $breadcrumbs when given —
367 + * an ordered trail supporting any number of levels — otherwise it falls back
368 + * to the single $title segment (backward compatible with existing callers).
369 + *
370 + * Each breadcrumb item is an array: `[ 'label' => string, 'url' => string ]`.
371 + * Items with a non-empty `url` render as links, except the last item, which is
372 + * always the current (non-linked) page.
373 + *
374 + * @param string $title Page title shown as the current segment when no
375 + * $breadcrumbs trail is supplied.
376 + * @param string $subtitle Optional subtitle (reserved; not rendered).
377 + * @param array $breadcrumbs Ordered trail of `[ 'label', 'url' ]` items.
378 + */
379 + public function render_admin_header( string $title = '', string $subtitle = '', array $breadcrumbs = [] ) {
380 + $current = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : 'wp-subscription';
381 +
382 + // Kept for backward compatibility — extensions may hook here for side-effects.
383 + $menu_items = apply_filters( 'subscrpt_admin_header_menu_items', [], $current );
384 + unset( $menu_items ); // Nav no longer rendered in header; navigation is in WP sidebar.
385 +
386 + // Normalize to a single trail. Explicit breadcrumbs win; otherwise the
387 + // legacy single $title segment is used.
388 + // A long breadcrumb label is truncated for display; the full text is kept
389 + // so the renderer can add it as a title attribute.
390 + $make_crumb = static function ( $label, $url ) {
391 + $label = (string) $label;
392 + return [
393 + 'label' => subscrpt_truncate_text( $label ),
394 + 'full' => $label,
395 + 'url' => (string) $url,
396 + ];
397 + };
398 +
399 + $trail = [];
400 + if ( ! empty( $breadcrumbs ) ) {
401 + foreach ( $breadcrumbs as $crumb ) {
402 + if ( is_array( $crumb ) && '' !== ( $crumb['label'] ?? '' ) ) {
403 + $trail[] = $make_crumb( $crumb['label'], $crumb['url'] ?? '' );
404 + }
405 + }
406 + } elseif ( '' !== $title ) {
407 + $trail[] = $make_crumb( $title, '' );
408 + }
409 +
410 + $last_index = count( $trail ) - 1;
411 + ?>
412 + <div class="wp-subscription-admin-header">
413 + <div class="wp-subscription-admin-header-inner">
414 + <div class="wp-subscription-admin-header-left">
415 + <nav class="wp-subscription-breadcrumb" aria-label="<?php esc_attr_e( 'Breadcrumb', 'subscription' ); ?>">
416 + <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' ); ?>">
417 + <span class="dashicons dashicons-admin-home"></span>
418 + </a>
419 + <?php foreach ( $trail as $index => $crumb ) : ?>
420 + <span class="wp-subscription-breadcrumb-sep" aria-hidden="true">/</span>
421 + <?php $crumb_title = $crumb['full'] !== $crumb['label'] ? ' title="' . esc_attr( $crumb['full'] ) . '"' : ''; ?>
422 + <?php if ( '' !== $crumb['url'] && $index < $last_index ) : ?>
423 + <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>
424 + <?php else : ?>
425 + <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>
426 + <?php endif; ?>
427 + <?php endforeach; ?>
428 + </nav>
429 + </div>
430 + <div class="wp-subscription-admin-header-right">
431 + <?php
432 + /**
433 + * Filters the pro licence state shown in the admin header.
434 + *
435 + * This plugin cannot ask the pro plugin directly — it runs alone
436 + * on nearly every install, so naming a symbol pro declares would
437 + * fatal there. Pro answers this filter when it is present; when
438 + * it is not, the value stays null and no badge is rendered.
439 + *
440 + * @since 1.11.3
441 + *
442 + * @param array|null $license {
443 + * Licence state, or null when pro is not installed.
444 + *
445 + * @type bool $active Whether the licence is valid.
446 + * @type string $url Admin URL of the licence page.
447 + * }
448 + */
449 + $license = apply_filters( 'subscrpt_admin_header_license', null );
450 +
451 + // Only the "Activate license" badge is shown; the "License active"
452 + // badge is intentionally hidden from the header.
453 + if ( is_array( $license ) && isset( $license['active'] ) && ! $license['active'] ) :
454 + $license_url = isset( $license['url'] ) ? (string) $license['url'] : '';
455 + ?>
456 + <a href="<?php echo esc_url( $license_url ); ?>" class="wpsubs-badge wpsubs-badge--warning wp-subscription-license-badge">
457 + <span class="wpsubs-badge__dot"></span>
458 + <?php esc_html_e( 'Activate license', 'subscription' ); ?>
459 + </a>
460 + <?php
461 + endif;
462 + ?>
463 +
464 + <?php if ( ! class_exists( 'Sdevs_Wc_Subscription_Pro' ) ) : ?>
465 + <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">
466 + <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>
467 + <?php esc_html_e( 'Upgrade to Pro', 'subscription' ); ?>
468 + </a>
469 + <?php endif; ?>
470 +<img src="<?php echo esc_url( SUBSCRPT_ASSETS . '/images/logo-title.svg' ); ?>" alt="WPSubscription" class="wp-subscription-logo">
471 + </div>
472 + </div>
473 + </div>
474 + <?php
475 + }
476 +
477 + /**
478 + * Render the dashboard, or hand off to the list.
479 + *
480 + * The subscriptions list used to live on this slug. Anything still linking
481 + * here with a list-only argument — a saved filter, a bookmarked search, a
482 + * pagination link — means the list, so it is sent there with its arguments
483 + * intact rather than landing on a dashboard that ignores them. The one
484 + * exception is `post_status`, renamed to the `subscrpt_status` the list reads.
485 + *
486 + * @return void
487 + */
488 + public function render_dashboard_page() {
489 + // phpcs:disable WordPress.Security.NonceVerification.Recommended
490 + $list_args = array( 'post_status', 's', 'paged', 'filter_action', 'orderby', 'order', 'subscrpt_status', 'date_filter', 'per_page', 'renewal_due' );
491 +
492 + foreach ( $list_args as $arg ) {
493 + if ( isset( $_GET[ $arg ] ) && '' !== $_GET[ $arg ] ) {
494 + $query = wp_unslash( $_GET );
495 + $query['page'] = 'wp-subscription-list';
496 +
497 + // The list filters on `subscrpt_status` and ignores `post_status`,
498 + // so forwarding the old name as-is lands on an unfiltered list.
499 + if ( isset( $query['post_status'] ) ) {
500 + if ( empty( $query['subscrpt_status'] ) ) {
501 + $query['subscrpt_status'] = $query['post_status'];
502 + }
503 + unset( $query['post_status'] );
504 + }
505 +
506 + wp_safe_redirect( add_query_arg( array_map( 'sanitize_text_field', $query ), admin_url( 'admin.php' ) ) );
507 + exit;
508 + }
509 + }
510 + // phpcs:enable WordPress.Security.NonceVerification.Recommended
511 +
512 + ( new Dashboard() )->render();
513 + }
514 +
515 + /**
516 + * Render Subscriptions page
517 + */
518 + public function render_subscriptions_page() {
519 + $this->render_admin_header( __( 'Subscriptions', 'subscription' ), __( 'Manage your subscriptions', 'subscription' ) );
520 +
521 + // Handle filters
522 + $status = isset( $_GET['subscrpt_status'] ) ? sanitize_text_field( wp_unslash( $_GET['subscrpt_status'] ) ) : '';
523 + $search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
524 + $date_filter = isset( $_GET['date_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['date_filter'] ) ) : '';
525 + $per_page = isset( $_GET['per_page'] ) ? max( 1, intval( $_GET['per_page'] ) ) : 20;
526 + $paged = isset( $_GET['paged'] ) ? max( 1, intval( $_GET['paged'] ) ) : 1;
527 + $renewal_due = isset( $_GET['renewal_due'] ) ? min( 366, absint( $_GET['renewal_due'] ) ) : 0;
528 +
529 + // Handle form submissions (both filters and bulk actions)
530 + $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
531 + if ( 'POST' === $request_method ) {
532 + // Verify nonce before processing any POST data.
533 + $nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '';
534 + if ( ! wp_verify_nonce( $nonce, 'subscrpt_list_action' ) ) {
535 + wp_die( esc_html__( 'Security check failed.', 'subscription' ) );
536 + }
537 + // Handle bulk actions
538 + if ( isset( $_POST['bulk_action'] ) || isset( $_POST['bulk_action2'] ) ) {
539 + $bulk_action = isset( $_POST['bulk_action'] ) ? sanitize_text_field( wp_unslash( $_POST['bulk_action'] ) ) : sanitize_text_field( wp_unslash( $_POST['bulk_action2'] ?? '' ) );
540 + $action = isset( $_POST['action'] ) ? sanitize_text_field( wp_unslash( $_POST['action'] ) ) : sanitize_text_field( wp_unslash( $_POST['action2'] ?? '' ) );
541 +
542 + if ( $bulk_action && $action && $action !== '-1' && isset( $_POST['subscription_ids'] ) && is_array( $_POST['subscription_ids'] ) ) {
543 + $subscription_ids = array_map( 'intval', $_POST['subscription_ids'] );
544 +
545 + if ( $action === 'trash' ) {
546 + foreach ( $subscription_ids as $sub_id ) {
547 + wp_trash_post( $sub_id );
548 + }
549 + } elseif ( $action === 'restore' ) {
550 + foreach ( $subscription_ids as $sub_id ) {
551 + wp_untrash_post( $sub_id );
552 + }
553 + } elseif ( $action === 'delete' ) {
554 + foreach ( $subscription_ids as $sub_id ) {
555 + wp_delete_post( $sub_id, true );
556 + }
557 + }
558 +
559 + wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription-list' ) );
560 + exit;
561 + }
562 + }
563 +
564 + // Handle filter form submission
565 + if ( isset( $_POST['filter_action'] ) ) {
566 + $filter_params = array();
567 +
568 + if ( ! empty( $_POST['subscrpt_status'] ) ) {
569 + $filter_params['subscrpt_status'] = sanitize_text_field( wp_unslash( $_POST['subscrpt_status'] ) );
570 + }
571 + if ( ! empty( $_POST['date_filter'] ) ) {
572 + $filter_params['date_filter'] = sanitize_text_field( wp_unslash( $_POST['date_filter'] ) );
573 + }
574 + if ( ! empty( $_POST['renewal_due'] ) ) {
575 + $filter_params['renewal_due'] = absint( $_POST['renewal_due'] );
576 + }
577 + if ( ! empty( $_POST['s'] ) ) {
578 + $filter_params['s'] = sanitize_text_field( wp_unslash( $_POST['s'] ) );
579 + }
580 + if ( ! empty( $_POST['per_page'] ) ) {
581 + $filter_params['per_page'] = intval( $_POST['per_page'] );
582 + }
583 +
584 + $redirect_url = add_query_arg( $filter_params, admin_url( 'admin.php?page=wp-subscription-list' ) );
585 + wp_safe_redirect( $redirect_url );
586 + exit;
587 + }
588 + }
589 +
590 + // Handle individual actions
591 + if ( isset( $_GET['action'] ) && ! empty( $_GET['sub_id'] ) ) {
592 + $sub_id = intval( $_GET['sub_id'] );
593 + $action = sanitize_text_field( wp_unslash( $_GET['action'] ) );
594 + $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
595 +
596 + // Clean trash action.
597 + if ( $action === 'clean_trash' ) {
598 + // Verify nonce for security.
599 + $nonce_action = 'wpsubs_action_clean_trash';
600 + if ( ! wp_verify_nonce( $nonce, $nonce_action ) ) {
601 + echo '<div class="notice notice-error"><p>' . esc_html__( 'Security check failed. Please try again.', 'subscription' ) . '</p></div>';
602 + wp_die();
603 + }
604 +
605 + // Clean all trash items.
606 + $trash_posts = get_posts(
607 + [
608 + 'post_type' => 'subscrpt_order',
609 + 'post_status' => 'trash',
610 + 'numberposts' => -1,
611 + 'fields' => 'ids',
612 + ]
613 + );
614 +
615 + foreach ( $trash_posts as $trash_id ) {
616 + wp_delete_post( $trash_id, true );
617 + }
618 +
619 + wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription-list&subscrpt_status=trash' ) );
620 + exit;
621 + } else {
622 + // For other actions, verify nonce with subscription ID.
623 + $nonce_action = 'wpsubs_action_' . $sub_id;
624 + if ( ! wp_verify_nonce( $nonce, $nonce_action ) ) {
625 + echo '<div class="notice notice-error"><p>' . esc_html__( 'Security check failed. Please try again.', 'subscription' ) . '</p></div>';
626 + wp_die();
627 + }
628 +
629 + $redirect_url = admin_url( 'admin.php?page=wp-subscription-list' );
630 +
631 + switch ( $action ) {
632 + case 'duplicate':
633 + $post = get_post( $sub_id );
634 + if ( $post && $post->post_type === 'subscrpt_order' ) {
635 + $new_post = [
636 + 'post_title' => $post->post_title . ' (Copy)',
637 + 'post_content' => $post->post_content,
638 + 'post_status' => 'draft',
639 + 'post_type' => 'subscrpt_order',
640 + ];
641 + $new_id = wp_insert_post( $new_post );
642 + if ( $new_id ) {
643 + $meta = get_post_meta( $sub_id );
644 + foreach ( $meta as $key => $values ) {
645 + foreach ( $values as $value ) {
646 + add_post_meta( $new_id, $key, maybe_unserialize( $value ) );
647 + }
648 + }
649 + }
650 + }
651 + break;
652 + case 'trash':
653 + wp_trash_post( $sub_id );
654 + break;
655 + case 'restore':
656 + wp_untrash_post( $sub_id );
657 + break;
658 + case 'delete':
659 + wp_delete_post( $sub_id, true );
660 + $redirect_url = admin_url( 'admin.php?page=wp-subscription-list&subscrpt_status=trash' );
661 + break;
662 + }
663 +
664 + wp_safe_redirect( $redirect_url );
665 + exit;
666 + }
667 + }
668 +
669 + $args = [
670 + 'post_type' => 'subscrpt_order',
671 + 'post_status' => 'any',
672 + 'posts_per_page' => $per_page,
673 + 'paged' => $paged,
674 + 'orderby' => 'date',
675 + 'order' => 'DESC',
676 + ];
677 +
678 + if ( $status ) {
679 + $args['post_status'] = $status;
680 + }
681 + // Search only by subscription ID
682 + if ( $search !== '' ) {
683 + if ( is_numeric( $search ) ) {
684 + $args['p'] = intval( $search );
685 + } else {
686 + // If not numeric, return no results
687 + $args['post__in'] = array( 0 );
688 + }
689 + }
690 + // Dynamic date filter (YYYY-MM)
691 + if ( $date_filter && preg_match( '/^\d{4}-\d{2}$/', $date_filter ) ) {
692 + $year = substr( $date_filter, 0, 4 );
693 + $month = substr( $date_filter, 5, 2 );
694 + $args['date_query'][] = [
695 + 'year' => intval( $year ),
696 + 'month' => intval( $month ),
697 + ];
698 + }
699 + // Next-renewal window, soonest first. The same arguments as the
700 + // Overview's "Renewals due" figure, so its 7-day rows equal its count.
701 + if ( $renewal_due ) {
702 + $due_args = Stats::renewals_due_args( $renewal_due );
703 +
704 + // Only an active subscription renews, so a renewal window combined
705 + // with any other status matches nothing — the filters AND together.
706 + if ( $status && $due_args['post_status'] !== $status ) {
707 + $args['post__in'] = array( 0 );
708 + }
709 +
710 + $args['post_status'] = $due_args['post_status'];
711 + $args['meta_query'] = $due_args['meta_query']; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
712 + $args['orderby'] = [ 'subscrpt_next_date' => 'ASC' ];
713 + }
714 +
715 + $query = new \WP_Query( $args );
716 + $subscriptions = $query->posts;
717 + $total = $query->found_posts;
718 + $max_num_pages = $query->max_num_pages;
719 +
720 + // Get all possible statuses for filter dropdown
721 + $all_statuses = get_post_stati( [ 'show_in_admin_all_list' => true ], 'objects' );
722 +
723 + include __DIR__ . '/views/subscription-list.php';
724 + ?>
725 + <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
726 + Made with <span style="color:#e25555;font-size:1.1em;">♥</span> by the WPSubscription Team
727 + <div style="margin-top:6px;">
728 + <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>
729 + &nbsp;/&nbsp;
730 + <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>
731 + </div>
732 + </div>
733 + <?php
734 + }
735 +
736 + /**
737 + * Render the standalone Subscription Details page.
738 + *
739 + * Replaces the legacy metabox edit screen. Mirrors the list-page design
740 + * shell (header + wpsubs components) and handles the status-change form.
741 + *
742 + * @return void
743 + */
744 + public function render_subscription_details_page() {
745 + $subscription_id = isset( $_GET['id'] ) ? absint( wp_unslash( $_GET['id'] ) ) : 0;
746 +
747 + if ( ! $subscription_id || 'subscrpt_order' !== get_post_type( $subscription_id ) ) {
748 + wp_die( esc_html__( 'Invalid subscription.', 'subscription' ) );
749 + }
750 +
751 + if ( ! current_user_can( 'edit_post', $subscription_id ) ) {
752 + wp_die( esc_html__( 'You do not have permission to view this subscription.', 'subscription' ) );
753 + }
754 +
755 + $list_url = admin_url( 'admin.php?page=wp-subscription-list' );
756 + $form_action = admin_url( 'admin.php?page=wp-subscription-details&id=' . $subscription_id );
757 +
758 + // Handle the status-change submission.
759 + $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
760 + if ( 'POST' === $request_method && isset( $_POST['subscrpt_order_action'] ) ) {
761 + $nonce = isset( $_POST['subscrpt_order_action_nonce_field'] ) ? sanitize_text_field( wp_unslash( $_POST['subscrpt_order_action_nonce_field'] ) ) : '';
762 + if ( ! wp_verify_nonce( $nonce, 'subscrpt_order_action_nonce' ) ) {
763 + wp_die( esc_html__( 'Security check failed.', 'subscription' ) );
764 + }
765 +
766 + $action = sanitize_text_field( wp_unslash( $_POST['subscrpt_order_action'] ) );
767 + if ( '' !== $action ) {
768 + Subscriptions::process_status_change( $subscription_id, $action );
769 + }
770 +
771 + wp_safe_redirect( $form_action );
772 + exit;
773 + }
774 +
775 + // Allowed actions for the current status (mirrors Subscriptions::subscrpt_order_save_post()).
776 + $actions_data = array(
777 + 'active' => array(
778 + 'label' => __( 'Activate Subscription', 'subscription' ),
779 + 'value' => 'active',
780 + ),
781 + 'pending' => array(
782 + 'label' => __( 'Pending Subscription', 'subscription' ),
783 + 'value' => 'pending',
784 + ),
785 + 'expire' => array(
786 + 'label' => __( 'Expire Subscription', 'subscription' ),
787 + 'value' => 'expired',
788 + ),
789 + 'pe_cancelled' => array(
790 + 'label' => __( 'Pending Cancel Subscription', 'subscription' ),
791 + 'value' => 'pe_cancelled',
792 + ),
793 + 'cancelled' => array(
794 + 'label' => __( 'Cancel Subscription', 'subscription' ),
795 + 'value' => 'cancelled',
796 + ),
797 + );
798 +
799 + $map_actions = array(
800 + 'pending' => array( 'active', 'cancelled' ),
801 + 'active' => array( 'pe_cancelled', 'cancelled' ),
802 + 'pe_cancelled' => array( 'active', 'cancelled' ),
803 + 'cancelled' => array( 'active' ),
804 + 'expired' => array( 'active', 'cancelled' ),
805 + 'completed' => array( 'cancelled' ),
806 + );
807 +
808 + $status = get_post_status( $subscription_id );
809 + $actions = $map_actions[ $status ] ?? array();
810 +
811 + // Gather subscription data for the view.
812 + $subscription_data = Helper::get_subscription_data( $subscription_id );
813 +
814 + $order_id = $subscription_data['order']['order_id'] ?? get_post_meta( $subscription_id, '_subscrpt_order_id', true );
815 + $order = $order_id ? wc_get_order( $order_id ) : null;
816 + $order_item_id = $subscription_data['order']['order_item_id'] ?? get_post_meta( $subscription_id, '_subscrpt_order_item_id', true );
817 + $order_item = ( $order && $order_item_id ) ? $order->get_item( $order_item_id ) : null;
818 +
819 + $rows = Subscriptions::get_info_rows( $subscription_id );
820 +
821 + // Related orders.
822 + $order_histories = Helper::get_related_orders( $subscription_id );
823 +
824 + $this->render_admin_header(
825 + '',
826 + '',
827 + array(
828 + array(
829 + 'label' => __( 'Subscriptions', 'subscription' ),
830 + 'url' => $list_url,
831 + ),
832 + array(
833 + 'label' => '#' . $subscription_id,
834 + 'url' => '',
835 + ),
836 + )
837 + );
838 +
839 + include __DIR__ . '/views/subscription-details.php';
840 +
841 + $this->render_admin_footer();
842 + }
843 +
844 + /**
845 + * Render Stats page
846 + */
847 + public function render_stats_page() {
848 + $this->render_admin_header( __( 'Reports', 'subscription' ), __( 'View your subscription analytics', 'subscription' ) );
849 +
850 + if ( ! subscrpt_pro_activated() ) {
851 + include 'views/reports-preview.php';
852 + } else {
853 + // Allow pro plugin to override the entire stats page content.
854 + do_action( 'subscrpt_render_stats_page' );
855 + }
856 +
857 + $this->render_admin_footer();
858 + }
859 +
860 + /**
861 + * Render Health page
862 + */
863 + public function render_health_page() {
864 + $this->render_admin_header( __( 'Health', 'subscription' ), __( 'Monitor your subscription health', 'subscription' ) );
865 +
866 + if ( ! subscrpt_pro_activated() ) {
867 + include 'views/health-preview.php';
868 + } else {
869 + // Allow pro plugin to render the full health page content.
870 + do_action( 'subscrpt_render_health_page' );
871 + }
872 +
873 + $this->render_admin_footer();
874 + }
875 +
876 + /**
877 + * Render Delivery Schedules page.
878 + * When pro is active, fires subscrpt_render_delivery_page for pro to handle.
879 + */
880 + public function render_delivery_page() {
881 + $this->render_admin_header( __( 'Delivery Schedules', 'subscription' ), __( 'Track and manage subscription delivery schedules.', 'subscription' ) );
882 +
883 + if ( ! subscrpt_pro_activated() ) {
884 + include 'views/delivery-preview.php';
885 + } else {
886 + // Allow pro plugin to render the full delivery page content.
887 + do_action( 'subscrpt_render_delivery_page' );
888 + }
889 +
890 + $this->render_admin_footer();
891 + }
892 +
893 + /**
894 + * Render Support page
895 + */
896 + public function render_support_page() {
897 + $this->render_admin_header( __( 'Help & Resources', 'subscription' ), __( 'Documentation, community links, and ways to get help with WPSubscription.', 'subscription' ) );
898 + include 'views/support.php';
899 + $this->render_admin_footer();
900 + }
901 +
902 + /**
903 + * Render Onboarding Wizard page
904 + * SPA-style: all sections rendered at once, JS controls visibility
905 + * Initial load always shows page 1 (JS handles transitions from there)
906 + */
907 + public function render_onboarding_wizard() {
908 + // Start session if not already started (used by the wizard reset handler).
909 + if ( ! session_id() && ! headers_sent() ) {
910 + session_start();
911 + }
912 +
913 + $this->render_admin_header( __( 'Setup Wizard', 'subscription' ), __( 'Create your first subscription plan', 'subscription' ) );
914 + include __DIR__ . '/views/onboarding-wizard.php';
915 + $this->render_admin_footer();
916 + }
917 +
918 + /**
919 + * Render the legacy subscriptions list page (WP_List_Table based)
920 + */
921 + public function render_legacy_subscriptions_page() {
922 + // No longer needed, as the menu now links directly to the post type list.
923 + }
924 +
925 + /**
926 + * Handle bulk action AJAX
927 + */
928 + public function handle_bulk_action_ajax() {
929 + // Verify nonce
930 + $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
931 + if ( ! wp_verify_nonce( $nonce, 'subscrpt_bulk_action_nonce' ) ) {
932 + wp_send_json_error( array( 'message' => __( 'Security check failed.', 'subscription' ) ) );
933 + }
934 +
935 + // Check permissions
936 + if ( ! current_user_can( 'manage_woocommerce' ) ) {
937 + wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'subscription' ) ) );
938 + }
939 +
940 + // Get action and subscription IDs
941 + $bulk_action = isset( $_POST['bulk_action'] ) ? sanitize_text_field( wp_unslash( $_POST['bulk_action'] ) ) : '';
942 + $subscription_ids = isset( $_POST['subscription_ids'] ) ? array_map( 'intval', $_POST['subscription_ids'] ) : array();
943 +
944 + if ( empty( $subscription_ids ) ) {
945 + wp_send_json_error( array( 'message' => __( 'No subscriptions selected.', 'subscription' ) ) );
946 + }
947 +
948 + $processed_count = 0;
949 + $errors = array();
950 +
951 + foreach ( $subscription_ids as $subscription_id ) {
952 + $post = get_post( $subscription_id );
953 +
954 + if ( ! $post || $post->post_type !== 'subscrpt_order' ) {
955 + // translators: Subscription ID.
956 + $errors[] = sprintf( __( 'Subscription #%d not found.', 'subscription' ), $subscription_id );
957 + continue;
958 + }
959 +
960 + try {
961 + switch ( $bulk_action ) {
962 + case 'trash':
963 + if ( wp_trash_post( $subscription_id ) ) {
964 + ++$processed_count;
965 + } else {
966 + $errors[] = sprintf(
967 + // translators: Subscription ID.
968 + __( 'Failed to move subscription #%d to trash.', 'subscription' ),
969 + $subscription_id
970 + );
971 + }
972 + break;
973 +
974 + case 'restore':
975 + if ( wp_untrash_post( $subscription_id ) ) {
976 + ++$processed_count;
977 + } else {
978 + $errors[] = sprintf(
979 + // translators: Subscription ID.
980 + __( 'Failed to restore subscription #%d.', 'subscription' ),
981 + $subscription_id
982 + );
983 + }
984 + break;
985 +
986 + case 'delete':
987 + if ( wp_delete_post( $subscription_id, true ) ) {
988 + ++$processed_count;
989 + } else {
990 + $errors[] = sprintf(
991 + // translators: Subscription ID.
992 + __( 'Failed to delete subscription #%d.', 'subscription' ),
993 + $subscription_id
994 + );
995 + }
996 + break;
997 +
998 + default:
999 + $errors[] = sprintf(
1000 + // translators: Bulk action.
1001 + __( 'Unknown action: %s', 'subscription' ),
1002 + $bulk_action
1003 + );
1004 + break;
1005 + }
1006 + } catch ( Exception $e ) {
1007 + $errors[] = sprintf(
1008 + // translators: Subscription ID, Error message.
1009 + __( 'Error processing subscription #%1$d: %2$s', 'subscription' ),
1010 + $subscription_id,
1011 + $e->getMessage()
1012 + );
1013 + }
1014 + }
1015 +
1016 + // Prepare response message
1017 + $message = '';
1018 + if ( $processed_count > 0 ) {
1019 + switch ( $bulk_action ) {
1020 + case 'trash':
1021 + $message = sprintf(
1022 + // translators: Number of subscriptions.
1023 + _n( '%d subscription moved to trash.', '%d subscriptions moved to trash.', $processed_count, 'subscription' ),
1024 + $processed_count
1025 + );
1026 + break;
1027 + case 'restore':
1028 + $message = sprintf(
1029 + // translators: Number of subscriptions.
1030 + _n( '%d subscription restored.', '%d subscriptions restored.', $processed_count, 'subscription' ),
1031 + $processed_count
1032 + );
1033 + break;
1034 + case 'delete':
1035 + $message = sprintf(
1036 + // translators: Number of subscriptions.
1037 + _n( '%d subscription permanently deleted.', '%d subscriptions permanently deleted.', $processed_count, 'subscription' ),
1038 + $processed_count
1039 + );
1040 + break;
1041 + }
1042 + }
1043 +
1044 + if ( ! empty( $errors ) ) {
1045 + $message .= ' ' . __( 'Some errors occurred:', 'subscription' ) . ' ' . implode( ', ', $errors );
1046 + }
1047 +
1048 + if ( $processed_count > 0 ) {
1049 + wp_send_json_success( array( 'message' => $message ) );
1050 + } else {
1051 + wp_send_json_error( array( 'message' => $message ? $message : __( 'No subscriptions were processed.', 'subscription' ) ) );
1052 + }
1053 + }
585 1054 }