PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 2.4.0
Subscriptions for WooCommerce with Stripe Recurring Payments v2.4.0
2.4.0 2.3.0 2.2.0 2.1.0 2.0.0 1.11.2 1.11.1 1.11.0 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.6 1.9.5 trunk 1.3.0 1.3.1 1.3.2 1.4.0 All 65 releases
← All changes | includes/Admin/Menu.php +592 -228 1.9.5 → 2.4.0 View file →
@@ -1,8 +1,16 @@
1 1 <?php
2 +/**
3 + * Admin menu + shared admin header/breadcrumb renderer.
4 + *
5 + * @package SpringDevs\Subscription\Admin
6 + */
2 7
3 8 namespace SpringDevs\Subscription\Admin;
4 9
10 +use SpringDevs\Subscription\Illuminate\Helper;
11 +use SpringDevs\Subscription\Illuminate\Stats;
12 +
5 13 /**
6 14 * Menu class
7 15 *
8 16 * @package SpringDevs\Subscription\Admin
@@ -13,13 +21,51 @@
13 21 * Initialize the class
14 22 */
15 23 public function __construct() {
16 24 add_action( 'admin_menu', array( $this, 'create_admin_menu' ) );
25 + add_action( 'admin_menu', array( $this, 'reorder_submenu' ), 999 );
17 26 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
18 27 add_action( 'wp_ajax_subscrpt_bulk_action', array( $this, 'handle_bulk_action_ajax' ) );
28 + add_action( 'admin_init', array( $this, 'maybe_onboarding_redirect' ) );
19 29 }
20 30
21 31 /**
32 + * Send a first-time user to the onboarding wizard when they open the
33 + * WPSubscription dashboard with no plan created yet.
34 + *
35 + * This runs on the dashboard visit rather than on activation, so it works
36 + * regardless of when WooCommerce gets installed (the plugin only loads its
37 + * admin once WooCommerce is active). A persistent "seen" flag makes it fire
38 + * at most once, so the user is never trapped away from the dashboard.
39 + *
40 + * @return void
41 + */
42 + public function maybe_onboarding_redirect() {
43 + // Only on the WPSubscription dashboard page.
44 + if ( ! isset( $_GET['page'] ) || 'wp-subscription' !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- menu navigation, no state change.
45 + return;
46 + }
47 +
48 + if ( wp_doing_ajax() || is_network_admin() || ! current_user_can( 'manage_options' ) ) {
49 + return;
50 + }
51 +
52 + // Fire at most once, ever.
53 + if ( get_option( 'subscrpt_onboarding_seen' ) ) {
54 + return;
55 + }
56 + update_option( 'subscrpt_onboarding_seen', 1, false );
57 +
58 + // Only first-time users with no plan yet.
59 + if ( ! empty( \SpringDevs\Subscription\Illuminate\Plans\PlanRepository::get_groups() ) ) {
60 + return;
61 + }
62 +
63 + wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription-onboarding' ) );
64 + exit;
65 + }
66 +
67 + /**
22 68 * Enqueue admin assets
23 69 */
24 70 public function enqueue_admin_assets() {
25 71 wp_enqueue_style(
@@ -32,13 +78,56 @@
32 78 // Enqueue admin JavaScript for subscription list functionality
33 79 wp_enqueue_script(
34 80 'sdevs_subscription_admin',
35 81 SUBSCRPT_ASSETS . '/js/admin.js',
36 - array( 'jquery' ),
82 + array( 'jquery', 'wp-i18n' ),
37 83 SUBSCRPT_VERSION,
38 84 true
39 85 );
40 86
87 + // Enqueue onboarding wizard styles.
88 + wp_enqueue_style(
89 + 'subscrpt-onboarding-wizard',
90 + SUBSCRPT_ASSETS . '/css/admin/onboarding-wizard.css',
91 + array(),
92 + SUBSCRPT_VERSION
93 + );
94 +
95 + // Enqueue onboarding wizard JS (loaded on wizard page). Depends on the
96 + // admin components so the cadence picker (adv-select) is ready.
97 + wp_enqueue_script(
98 + 'subscrpt-onboarding-wizard',
99 + SUBSCRPT_ASSETS . '/js/admin/onboarding-wizard.js',
100 + array( 'jquery', 'subscrpt_admin_components', 'wp-i18n' ),
101 + SUBSCRPT_VERSION,
102 + true
103 + );
104 + wp_set_script_translations( 'subscrpt-onboarding-wizard', 'subscription', SUBSCRPT_PATH . '/languages' );
105 + $subscrpt_wizard_has_products = (bool) wc_get_products(
106 + array(
107 + 'status' => array( 'publish', 'draft', 'pending', 'private' ),
108 + 'limit' => 1,
109 + 'return' => 'ids',
110 + )
111 + );
112 +
113 + wp_localize_script(
114 + 'subscrpt-onboarding-wizard',
115 + 'subscrpt_wizard',
116 + array(
117 + 'ajax_url' => admin_url( 'admin-ajax.php' ),
118 + 'subscriptions_url' => admin_url( 'admin.php?page=wp-subscription' ),
119 + 'dashboard_url' => admin_url( 'admin.php?page=wp-subscription' ),
120 + 'products_url' => admin_url( 'edit.php?post_type=product' ),
121 + 'plans_url' => admin_url( 'admin.php?page=wp-subscription-plans' ),
122 + 'rest_url' => rest_url( 'wpsubscription/v1/plans' ),
123 + 'rest_nonce' => wp_create_nonce( 'wp_rest' ),
124 + 'currency_symbol' => get_woocommerce_currency_symbol(),
125 + 'is_pro' => subscrpt_pro_activated(),
126 + 'has_products' => $subscrpt_wizard_has_products,
127 + )
128 + );
129 +
41 130 // Localize script for AJAX
42 131 wp_localize_script(
43 132 'sdevs_subscription_admin',
44 133 'wp_subscription_ajax',
@@ -58,27 +147,66 @@
58 147 $is_active = isset( $_GET['page'] ) && strpos( sanitize_text_field( wp_unslash( $_GET['page'] ) ), 'wp-subscription' ) === 0;
59 148 $icon_url = $is_active
60 149 ? SUBSCRPT_ASSETS . '/images/icons/subscription-20.png'
61 150 : SUBSCRPT_ASSETS . '/images/icons/subscription-20-gray.png';
151 +
152 + $pro_text = __( 'WPSubscription Pro required', 'subscription' );
153 + $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>';
154 +
62 155 // Main menu
63 156 add_menu_page(
64 - __( 'WP Subscription', 'subscription' ),
65 - __( 'WP Subscription', 'subscription' ),
66 - 'manage_woocommerce',
157 + __( 'WPSubscription', 'subscription' ),
158 + __( 'WPSubscription', 'subscription' ),
159 + 'manage_options',
67 160 $parent_slug,
68 - array( $this, 'render_subscriptions_page' ),
161 + array( $this, 'render_dashboard_page' ),
69 162 $icon_url,
70 163 40
71 164 );
72 165
73 - // Subscriptions List
166 + // Onboarding Wizard (hidden from menu with CSS. can be accessed via direct URL: admin.php?page=wp-subscription-onboarding)
167 + // CSS Record: admin.css -> `.wp-submenu a[href*="page=wp-subscription-onboarding"]`
74 168 add_submenu_page(
75 169 $parent_slug,
170 + __( 'Setup Wizard', 'subscription' ),
171 + __( 'Setup Wizard', 'subscription' ),
172 + 'manage_options',
173 + 'wp-subscription-onboarding',
174 + array( $this, 'render_onboarding_wizard' )
175 + );
176 +
177 + // Overview. WordPress makes the first submenu entry share the parent
178 + // slug, so this is the page the top-level item opens.
179 + add_submenu_page(
180 + $parent_slug,
181 + __( 'Overview', 'subscription' ),
182 + __( 'Overview', 'subscription' ),
183 + 'manage_options',
184 + $parent_slug,
185 + array( $this, 'render_dashboard_page' )
186 + );
187 +
188 + // Subscriptions List. Moved off the parent slug when the dashboard took
189 + // it; render_dashboard_page() redirects here when the request carries
190 + // list-only query arguments, so old bookmarks still work.
191 + add_submenu_page(
192 + $parent_slug,
76 193 __( 'Subscriptions', 'subscription' ),
77 194 __( 'Subscriptions', 'subscription' ),
78 - 'manage_woocommerce',
195 + 'manage_options',
196 + 'wp-subscription-list',
197 + array( $this, 'render_subscriptions_page' )
198 + );
199 +
200 + // Subscription Details (hidden from menu with CSS — accessed via admin.php?page=wp-subscription-details&id=ID).
201 + // CSS Record: admin.css -> `.wp-submenu a[href*="page=wp-subscription-details"]`
202 + add_submenu_page(
79 203 $parent_slug,
80 - array( $this, 'render_subscriptions_page' )
204 + __( 'Subscription Details', 'subscription' ),
205 + __( 'Subscription Details', 'subscription' ),
206 + 'manage_options',
207 + 'wp-subscription-details',
208 + array( $this, 'render_subscription_details_page' )
81 209 );
82 210
83 211 // Stats Overview
84 212 add_submenu_page(
@@ -83,125 +211,317 @@
83 211 // Stats Overview
84 212 add_submenu_page(
85 213 $parent_slug,
86 214 __( 'Reports', 'subscription' ),
87 - __( 'Reports', 'subscription' ),
88 - 'manage_woocommerce',
215 + __( 'Reports', 'subscription' ) . $pro_badge,
216 + 'manage_options',
89 217 'wp-subscription-stats',
90 218 array( $this, 'render_stats_page' )
91 219 );
92 220
93 - /*
94 - ! For god's sake, check existing code before implementing!
95 - // Settings
221 + // Subscription Health
96 222 add_submenu_page(
97 223 $parent_slug,
98 - __( 'Settings', 'subscription' ),
99 - __( 'Settings', 'subscription' ),
100 - 'manage_woocommerce',
101 - 'wp-subscription-settings',
102 - array( $this, 'render_settings_page' )
224 + __( 'Health', 'subscription' ),
225 + __( 'Health', 'subscription' ) . $pro_badge,
226 + 'manage_options',
227 + 'wp-subscription-health',
228 + array( $this, 'render_health_page' )
103 229 );
104 - */
105 230
106 - // Support
231 + // Delivery Schedules
107 232 add_submenu_page(
108 233 $parent_slug,
109 - __( 'Support', 'subscription' ),
110 - __( 'Support', 'subscription' ),
111 - 'manage_woocommerce',
234 + __( 'Delivery', 'subscription' ),
235 + __( 'Delivery', 'subscription' ) . $pro_badge,
236 + 'manage_options',
237 + 'wp-subscription-delivery',
238 + array( $this, 'render_delivery_page' )
239 + );
240 +
241 + // Help & Resources
242 + add_submenu_page(
243 + $parent_slug,
244 + __( 'Help', 'subscription' ),
245 + __( 'Help', 'subscription' ),
246 + 'manage_options',
112 247 'wp-subscription-support',
113 248 array( $this, 'render_support_page' )
114 249 );
115 250
116 - // Add WP Subscription link under WooCommerce menu
251 + /*
252 + * Subscriptions link under the WooCommerce menu, opening the list.
253 + *
254 + * A link, not a page: the slug is the list's URL and there is no
255 + * callback, so WordPress registers no page and no hook for it.
256 + * Registering `wp-subscription-list` under `woocommerce` as well would
257 + * give the list a second hookname (`woocommerce_page_…` beside
258 + * `wpsubscription_page_…`), and whichever parent WordPress found first
259 + * would set the page's $hook_suffix, which its asset loading keys on.
260 + */
117 261 add_submenu_page(
118 262 'woocommerce',
119 - __( 'WP Subscription', 'subscription' ),
120 - __( 'WP Subscription', 'subscription' ),
121 - 'manage_woocommerce',
122 - 'wp-subscription',
123 - array( $this, 'render_subscriptions_page' )
263 + __( 'Subscriptions', 'subscription' ),
264 + __( 'Subscriptions', 'subscription' ),
265 + 'manage_options',
266 + 'admin.php?page=wp-subscription-list'
124 267 );
125 268 }
269 +
126 270 /**
271 + * Reorder the WPSubscription submenu after all items are registered.
272 + *
273 + * Runs at admin_menu priority 999 so every plugin has already inserted
274 + * its items. A filter lets the Pro plugin (or any extension) adjust the
275 + * slug order before sorting is applied.
276 + *
277 + * @do_action subscrpt_submenu_order {string[]} $order Ordered list of submenu page slugs.
278 + */
279 + public function reorder_submenu() {
280 + $parent = 'wp-subscription';
281 +
282 + global $submenu;
283 +
284 + if ( empty( $submenu[ $parent ] ) ) {
285 + return;
286 + }
287 +
288 + // slug => position. Use gaps of 10 so extensions can insert between items.
289 + //
290 + // The order is the same whether or not pro is active: a locked page
291 + // carries a pro badge but keeps its place, so the menu does not
292 + // rearrange itself the moment a licence is activated. Plans and
293 + // Cancellation Flow position themselves through the filter below.
294 + $default_order = [
295 + 'wp-subscription' => 5, // Overview
296 + 'wp-subscription-delivery' => 20, // Delivery (pro)
297 + 'wp-subscription-list' => 30, // Subscriptions
298 + 'wp-subscription-stats' => 40, // Reports
299 + 'wp-subscription-health' => 50, // Health
300 + 'wp-subscription-integrations' => 60, // Integrations
301 + 'wp-subscription-settings' => 998, // Settings
302 + 'wp-subscription-license' => 999, // License (pro)
303 + 'wp-subscription-support' => 1000, // Help & Resources
304 + ];
305 +
306 + /**
307 + * Filter the WPSubscription submenu slug order.
308 + *
309 + * Each entry is a slug => integer position pair. Lower positions appear
310 + * first. Use gaps of 10 between built-in positions so extensions can
311 + * insert their own slugs between existing items without renumbering.
312 + *
313 + * Example (an add-on placing its page between Reports and Health):
314 + * add_filter( 'subscrpt_submenu_order', function( $order ) {
315 + * $order['wp-subscription-my-addon'] = 45;
316 + * return $order;
317 + * } );
318 + *
319 + * @param array<string,int> $order Map of slug => position.
320 + */
321 + $order = apply_filters( 'subscrpt_submenu_order', $default_order );
322 +
323 + // Sort by position value, preserving slug keys.
324 + asort( $order );
325 +
326 + // Index current items by slug for fast lookup.
327 + $indexed = [];
328 + foreach ( $submenu[ $parent ] as $item ) {
329 + $indexed[ $item[2] ] = $item;
330 + }
331 +
332 + // Build sorted list from the ordered slugs.
333 + $sorted = [];
334 + foreach ( $order as $slug => $position ) {
335 + if ( isset( $indexed[ $slug ] ) ) {
336 + $sorted[] = $indexed[ $slug ];
337 + unset( $indexed[ $slug ] );
338 + }
339 + }
340 +
341 + // Append any remaining items not covered by the order list.
342 + foreach ( $indexed as $item ) {
343 + $sorted[] = $item;
344 + }
345 +
346 + $submenu[ $parent ] = $sorted; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Intentional reorder of submenu items.
347 + }
348 +
349 + /**
127 350 * Render the admin header
128 351 */
129 352 public function render_admin_footer() {
130 353 ?>
131 354 <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
132 - Made with <span style="color:#e25555;font-size:1.1em;">♥</span> by the WP Subscription Team
355 + <?php
356 + /* translators: %s: a heart symbol. */
357 + printf( esc_html__( 'Made with %s by the WPSubscription Team', 'subscription' ), '<span style="color:#e25555;font-size:1.1em;">♥</span>' );
358 + ?>
133 359 <div style="margin-top:6px;">
134 - <a href="https://wpsubscription.co/contact" target="_blank" style="color:#2563eb;text-decoration:none;">Support</a>
360 + <a href="https://wpsubscription.co/contact?utm_source=plugin&utm_medium=admin&utm_campaign=support" target="_blank" style="color:#2563eb;text-decoration:none;"><?php esc_html_e( 'Support', 'subscription' ); ?></a>
135 361 &nbsp;/&nbsp;
136 - <a href="https://docs.converslabs.com/en" target="_blank" style="color:#2563eb;text-decoration:none;">Docs</a>
362 + <a href="https://docs.wpsubscription.co/en?utm_source=plugin&utm_medium=admin&utm_campaign=docs" target="_blank" style="color:#2563eb;text-decoration:none;"><?php esc_html_e( 'Docs', 'subscription' ); ?></a>
137 363 </div>
138 364 </div>
139 365 <?php
140 366 }
141 367 /**
142 - * Render the admin header
368 + * Render the admin header.
369 + *
370 + * The breadcrumb after the home icon is built from $breadcrumbs when given —
371 + * an ordered trail supporting any number of levels — otherwise it falls back
372 + * to the single $title segment (backward compatible with existing callers).
373 + *
374 + * Each breadcrumb item is an array: `[ 'label' => string, 'url' => string ]`.
375 + * Items with a non-empty `url` render as links, except the last item, which is
376 + * always the current (non-linked) page.
377 + *
378 + * @param string $title Page title shown as the current segment when no
379 + * $breadcrumbs trail is supplied.
380 + * @param string $subtitle Optional subtitle (reserved; not rendered).
381 + * @param array $breadcrumbs Ordered trail of `[ 'label', 'url' ]` items.
143 382 */
144 - public function render_admin_header() {
145 - // Get current page slug
146 - $current = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : 'wp-subscription';
147 - $menu_items = [
148 - [
149 - 'slug' => 'wp-subscription',
150 - 'label' => __( 'Subscriptions', 'subscription' ),
151 - 'url' => admin_url( 'admin.php?page=wp-subscription' ),
152 - ],
153 - [
154 - 'slug' => 'wp-subscription-stats',
155 - 'label' => __( 'Reports', 'subscription' ),
156 - 'url' => admin_url( 'admin.php?page=wp-subscription-stats' ),
157 - ],
158 - [
159 - 'slug' => 'wp-subscription-settings',
160 - 'label' => __( 'Settings', 'subscription' ),
161 - 'url' => admin_url( 'admin.php?page=wp-subscription-settings' ),
162 - ],
163 - ];
164 - // Allow pro plugin to inject menu items
165 - $menu_items = apply_filters( 'subscrpt_admin_header_menu_items', $menu_items, $current );
166 - $menu_items = array_merge(
167 - $menu_items,
168 - [
169 - [
170 - 'slug' => 'wp-subscription-support',
171 - 'label' => __( 'Support', 'subscription' ),
172 - 'url' => admin_url( 'admin.php?page=wp-subscription-support' ),
173 - ],
174 - ]
175 - );
383 + public function render_admin_header( string $title = '', string $subtitle = '', array $breadcrumbs = [] ) {
384 + $current = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : 'wp-subscription';
385 +
386 + // Kept for backward compatibility — extensions may hook here for side-effects.
387 + $menu_items = apply_filters( 'subscrpt_admin_header_menu_items', [], $current );
388 + unset( $menu_items ); // Nav no longer rendered in header; navigation is in WP sidebar.
389 +
390 + // Normalize to a single trail. Explicit breadcrumbs win; otherwise the
391 + // legacy single $title segment is used.
392 + // A long breadcrumb label is truncated for display; the full text is kept
393 + // so the renderer can add it as a title attribute.
394 + $make_crumb = static function ( $label, $url ) {
395 + $label = (string) $label;
396 + return [
397 + 'label' => subscrpt_truncate_text( $label ),
398 + 'full' => $label,
399 + 'url' => (string) $url,
400 + ];
401 + };
402 +
403 + $trail = [];
404 + if ( ! empty( $breadcrumbs ) ) {
405 + foreach ( $breadcrumbs as $crumb ) {
406 + if ( is_array( $crumb ) && '' !== ( $crumb['label'] ?? '' ) ) {
407 + $trail[] = $make_crumb( $crumb['label'], $crumb['url'] ?? '' );
408 + }
409 + }
410 + } elseif ( '' !== $title ) {
411 + $trail[] = $make_crumb( $title, '' );
412 + }
413 +
414 + $last_index = count( $trail ) - 1;
176 415 ?>
177 416 <div class="wp-subscription-admin-header">
178 - <div style="width:1240px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;">
179 - <div class="wp-subscription-admin-header-left" style="display:flex;align-items:center;gap:14px;">
180 - <img style="height:30px;" src="<?php echo esc_url( SUBSCRPT_ASSETS . '/images/logo-title.svg' ); ?>" alt="WP Subscription" class="wp-subscription-logo">
181 - <nav class="wp-subscription-admin-header-menu">
182 - <?php foreach ( $menu_items as $item ) : ?>
183 - <a href="<?php echo esc_url( $item['url'] ); ?>" class="<?php echo ( $current === $item['slug'] ) ? 'current' : ''; ?>">
184 - <?php echo esc_html( $item['label'] ); ?>
185 - </a>
186 - <?php endforeach; ?>
187 - </nav>
188 - </div>
189 - <div class="wp-subscription-admin-header-right">
190 - <?php if ( ! class_exists( 'Sdevs_Wc_Subscription_Pro' ) ) : ?>
191 - <a target="_blank" href="https://wpsubscription.co/?utm_source=plugin&utm_medium=admin&utm_campaign=upgrade_pro" class="wp-subscription-upgrade-btn"><?php esc_html_e( 'Upgrade to Pro', 'subscription' ); ?></a>
192 - <?php endif; ?>
193 - </div>
417 + <div class="wp-subscription-admin-header-inner">
418 + <div class="wp-subscription-admin-header-left">
419 + <nav class="wp-subscription-breadcrumb" aria-label="<?php esc_attr_e( 'Breadcrumb', 'subscription' ); ?>">
420 + <a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-subscription' ) ); ?>" class="wp-subscription-breadcrumb-home" aria-label="<?php esc_attr_e( 'WPSubscription Home', 'subscription' ); ?>">
421 + <span class="dashicons dashicons-admin-home"></span>
422 + </a>
423 + <?php foreach ( $trail as $index => $crumb ) : ?>
424 + <span class="wp-subscription-breadcrumb-sep" aria-hidden="true">/</span>
425 + <?php $crumb_title = $crumb['full'] !== $crumb['label'] ? ' title="' . esc_attr( $crumb['full'] ) . '"' : ''; ?>
426 + <?php if ( '' !== $crumb['url'] && $index < $last_index ) : ?>
427 + <a href="<?php echo esc_url( $crumb['url'] ); ?>" class="wp-subscription-breadcrumb-link"<?php echo $crumb_title; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- attribute pre-escaped. ?>><?php echo esc_html( $crumb['label'] ); ?></a>
428 + <?php else : ?>
429 + <span class="wp-subscription-breadcrumb-current"<?php echo $crumb_title; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- attribute pre-escaped. ?>><?php echo esc_html( $crumb['label'] ); ?></span>
430 + <?php endif; ?>
431 + <?php endforeach; ?>
432 + </nav>
194 433 </div>
434 + <div class="wp-subscription-admin-header-right">
435 + <?php
436 + /**
437 + * Filters the pro licence state shown in the admin header.
438 + *
439 + * This plugin cannot ask the pro plugin directly — it runs alone
440 + * on nearly every install, so naming a symbol pro declares would
441 + * fatal there. Pro answers this filter when it is present; when
442 + * it is not, the value stays null and no badge is rendered.
443 + *
444 + * @since 1.11.3
445 + *
446 + * @param array|null $license {
447 + * Licence state, or null when pro is not installed.
448 + *
449 + * @type bool $active Whether the licence is valid.
450 + * @type string $url Admin URL of the licence page.
451 + * }
452 + */
453 + $license = apply_filters( 'subscrpt_admin_header_license', null );
454 +
455 + // Only the "Activate license" badge is shown; the "License active"
456 + // badge is intentionally hidden from the header.
457 + if ( is_array( $license ) && isset( $license['active'] ) && ! $license['active'] ) :
458 + $license_url = isset( $license['url'] ) ? (string) $license['url'] : '';
459 + ?>
460 + <a href="<?php echo esc_url( $license_url ); ?>" class="wpsubs-badge wpsubs-badge--warning wp-subscription-license-badge">
461 + <span class="wpsubs-badge__dot"></span>
462 + <?php esc_html_e( 'Activate license', 'subscription' ); ?>
463 + </a>
464 + <?php
465 + endif;
466 + ?>
467 +
468 + <?php if ( ! class_exists( 'Sdevs_Wc_Subscription_Pro' ) ) : ?>
469 + <a target="_blank" href="https://wpsubscription.co/?utm_source=plugin&utm_medium=admin&utm_campaign=upgrade_pro" class="wpsubs-btn wpsubs-btn--primary wpsubs-btn--sm" rel="noreferrer noopener">
470 + <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" style="flex-shrink:0;"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19 19h-14c-.5 0 -.9 -.3 -1 -.8l-2 -10c0 -.4 .1 -.8 .5 -1.1c.4 -.2 .8 -.2 1.1 0l4.1 3.3l3.4 -5.1c.4 -.6 1.3 -.6 1.7 0l3.4 5.1l4.1 -3.3c.3 -.3 .8 -.3 1.1 0c.4 .2 .5 .6 .5 1.1l-2 10c0 .5 -.5 .8 -1 .8z"/></svg>
471 + <?php esc_html_e( 'Upgrade to Pro', 'subscription' ); ?>
472 + </a>
473 + <?php endif; ?>
474 +<img src="<?php echo esc_url( SUBSCRPT_ASSETS . '/images/logo-title.svg' ); ?>" alt="WPSubscription" class="wp-subscription-logo">
475 + </div>
476 + </div>
195 477 </div>
196 478 <?php
197 479 }
198 480
199 481 /**
482 + * Render the dashboard, or hand off to the list.
483 + *
484 + * The subscriptions list used to live on this slug. Anything still linking
485 + * here with a list-only argument — a saved filter, a bookmarked search, a
486 + * pagination link — means the list, so it is sent there with its arguments
487 + * intact rather than landing on a dashboard that ignores them. The one
488 + * exception is `post_status`, renamed to the `subscrpt_status` the list reads.
489 + *
490 + * @return void
491 + */
492 + public function render_dashboard_page() {
493 + // phpcs:disable WordPress.Security.NonceVerification.Recommended
494 + $list_args = array( 'post_status', 's', 'paged', 'filter_action', 'orderby', 'order', 'subscrpt_status', 'date_filter', 'per_page', 'renewal_due' );
495 +
496 + foreach ( $list_args as $arg ) {
497 + if ( isset( $_GET[ $arg ] ) && '' !== $_GET[ $arg ] ) {
498 + $query = wp_unslash( $_GET );
499 + $query['page'] = 'wp-subscription-list';
500 +
501 + // The list filters on `subscrpt_status` and ignores `post_status`,
502 + // so forwarding the old name as-is lands on an unfiltered list.
503 + if ( isset( $query['post_status'] ) ) {
504 + if ( empty( $query['subscrpt_status'] ) ) {
505 + $query['subscrpt_status'] = $query['post_status'];
506 + }
507 + unset( $query['post_status'] );
508 + }
509 +
510 + wp_safe_redirect( add_query_arg( array_map( 'sanitize_text_field', $query ), admin_url( 'admin.php' ) ) );
511 + exit;
512 + }
513 + }
514 + // phpcs:enable WordPress.Security.NonceVerification.Recommended
515 +
516 + ( new Dashboard() )->render();
517 + }
518 +
519 + /**
200 520 * Render Subscriptions page
201 521 */
202 522 public function render_subscriptions_page() {
203 - $this->render_admin_header();
523 + $this->render_admin_header( __( 'Subscriptions', 'subscription' ), __( 'Manage your subscriptions', 'subscription' ) );
204 524
205 525 // Handle filters
206 526 $status = isset( $_GET['subscrpt_status'] ) ? sanitize_text_field( wp_unslash( $_GET['subscrpt_status'] ) ) : '';
207 527 $search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
@@ -207,8 +527,9 @@
207 527 $search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
208 528 $date_filter = isset( $_GET['date_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['date_filter'] ) ) : '';
209 529 $per_page = isset( $_GET['per_page'] ) ? max( 1, intval( $_GET['per_page'] ) ) : 20;
210 530 $paged = isset( $_GET['paged'] ) ? max( 1, intval( $_GET['paged'] ) ) : 1;
531 + $renewal_due = isset( $_GET['renewal_due'] ) ? min( 366, absint( $_GET['renewal_due'] ) ) : 0;
211 532
212 533 // Handle form submissions (both filters and bulk actions)
213 534 $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
214 535 if ( 'POST' === $request_method ) {
@@ -238,9 +559,9 @@
238 559 wp_delete_post( $sub_id, true );
239 560 }
240 561 }
241 562
242 - wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription' ) );
563 + wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription-list' ) );
243 564 exit;
244 565 }
245 566 }
246 567
@@ -253,8 +574,11 @@
253 574 }
254 575 if ( ! empty( $_POST['date_filter'] ) ) {
255 576 $filter_params['date_filter'] = sanitize_text_field( wp_unslash( $_POST['date_filter'] ) );
256 577 }
578 + if ( ! empty( $_POST['renewal_due'] ) ) {
579 + $filter_params['renewal_due'] = absint( $_POST['renewal_due'] );
580 + }
257 581 if ( ! empty( $_POST['s'] ) ) {
258 582 $filter_params['s'] = sanitize_text_field( wp_unslash( $_POST['s'] ) );
259 583 }
260 584 if ( ! empty( $_POST['per_page'] ) ) {
@@ -260,9 +584,9 @@
260 584 if ( ! empty( $_POST['per_page'] ) ) {
261 585 $filter_params['per_page'] = intval( $_POST['per_page'] );
262 586 }
263 587
264 - $redirect_url = add_query_arg( $filter_params, admin_url( 'admin.php?page=wp-subscription' ) );
588 + $redirect_url = add_query_arg( $filter_params, admin_url( 'admin.php?page=wp-subscription-list' ) );
265 589 wp_safe_redirect( $redirect_url );
266 590 exit;
267 591 }
268 592 }
@@ -295,9 +619,9 @@
295 619 foreach ( $trash_posts as $trash_id ) {
296 620 wp_delete_post( $trash_id, true );
297 621 }
298 622
299 - wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription&subscrpt_status=trash' ) );
623 + wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription-list&subscrpt_status=trash' ) );
300 624 exit;
301 625 } else {
302 626 // For other actions, verify nonce with subscription ID.
303 627 $nonce_action = 'wpsubs_action_' . $sub_id;
@@ -305,9 +629,9 @@
305 629 echo '<div class="notice notice-error"><p>' . esc_html__( 'Security check failed. Please try again.', 'subscription' ) . '</p></div>';
306 630 wp_die();
307 631 }
308 632
309 - $redirect_url = admin_url( 'admin.php?page=wp-subscription' );
633 + $redirect_url = admin_url( 'admin.php?page=wp-subscription-list' );
310 634
311 635 switch ( $action ) {
312 636 case 'duplicate':
313 637 $post = get_post( $sub_id );
@@ -336,9 +660,9 @@
336 660 wp_untrash_post( $sub_id );
337 661 break;
338 662 case 'delete':
339 663 wp_delete_post( $sub_id, true );
340 - $redirect_url = admin_url( 'admin.php?page=wp-subscription&subscrpt_status=trash' );
664 + $redirect_url = admin_url( 'admin.php?page=wp-subscription-list&subscrpt_status=trash' );
341 665 break;
342 666 }
343 667
344 668 wp_safe_redirect( $redirect_url );
@@ -375,9 +699,24 @@
375 699 'year' => intval( $year ),
376 700 'month' => intval( $month ),
377 701 ];
378 702 }
703 + // Next-renewal window, soonest first. The same arguments as the
704 + // Overview's "Renewals due" figure, so its 7-day rows equal its count.
705 + if ( $renewal_due ) {
706 + $due_args = Stats::renewals_due_args( $renewal_due );
379 707
708 + // Only an active subscription renews, so a renewal window combined
709 + // with any other status matches nothing — the filters AND together.
710 + if ( $status && $due_args['post_status'] !== $status ) {
711 + $args['post__in'] = array( 0 );
712 + }
713 +
714 + $args['post_status'] = $due_args['post_status'];
715 + $args['meta_query'] = $due_args['meta_query']; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
716 + $args['orderby'] = [ 'subscrpt_next_date' => 'ASC' ];
717 + }
718 +
380 719 $query = new \WP_Query( $args );
381 720 $subscriptions = $query->posts;
382 721 $total = $query->found_posts;
383 722 $max_num_pages = $query->max_num_pages;
@@ -387,13 +726,16 @@
387 726
388 727 include __DIR__ . '/views/subscription-list.php';
389 728 ?>
390 729 <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
391 - Made with <span style="color:#e25555;font-size:1.1em;">♥</span> by the WP Subscription Team
730 + <?php
731 + /* translators: %s: a heart symbol. */
732 + printf( esc_html__( 'Made with %s by the WPSubscription Team', 'subscription' ), '<span style="color:#e25555;font-size:1.1em;">♥</span>' );
733 + ?>
392 734 <div style="margin-top:6px;">
393 - <a href="https://wpsubscription.co/contact" target="_blank" style="color:#2563eb;text-decoration:none;">Support</a>
735 + <a href="https://wpsubscription.co/contact?utm_source=plugin&utm_medium=admin&utm_campaign=support" target="_blank" style="color:#2563eb;text-decoration:none;"><?php esc_html_e( 'Support', 'subscription' ); ?></a>
394 736 &nbsp;/&nbsp;
395 - <a href="https://docs.converslabs.com/en" target="_blank" style="color:#2563eb;text-decoration:none;">Docs</a>
737 + <a href="https://docs.wpsubscription.co/en?utm_source=plugin&utm_medium=admin&utm_campaign=docs" target="_blank" style="color:#2563eb;text-decoration:none;"><?php esc_html_e( 'Docs', 'subscription' ); ?></a>
396 738 </div>
397 739 </div>
398 740 <?php
399 741 }
@@ -398,32 +740,123 @@
398 740 <?php
399 741 }
400 742
401 743 /**
744 + * Render the standalone Subscription Details page.
745 + *
746 + * Replaces the legacy metabox edit screen. Mirrors the list-page design
747 + * shell (header + wpsubs components) and handles the status-change form.
748 + *
749 + * @return void
750 + */
751 + public function render_subscription_details_page() {
752 + $subscription_id = isset( $_GET['id'] ) ? absint( wp_unslash( $_GET['id'] ) ) : 0;
753 +
754 + if ( ! $subscription_id || 'subscrpt_order' !== get_post_type( $subscription_id ) ) {
755 + wp_die( esc_html__( 'Invalid subscription.', 'subscription' ) );
756 + }
757 +
758 + if ( ! current_user_can( 'edit_post', $subscription_id ) ) {
759 + wp_die( esc_html__( 'You do not have permission to view this subscription.', 'subscription' ) );
760 + }
761 +
762 + $list_url = admin_url( 'admin.php?page=wp-subscription-list' );
763 + $form_action = admin_url( 'admin.php?page=wp-subscription-details&id=' . $subscription_id );
764 +
765 + // Handle the status-change submission.
766 + $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
767 + if ( 'POST' === $request_method && isset( $_POST['subscrpt_order_action'] ) ) {
768 + $nonce = isset( $_POST['subscrpt_order_action_nonce_field'] ) ? sanitize_text_field( wp_unslash( $_POST['subscrpt_order_action_nonce_field'] ) ) : '';
769 + if ( ! wp_verify_nonce( $nonce, 'subscrpt_order_action_nonce' ) ) {
770 + wp_die( esc_html__( 'Security check failed.', 'subscription' ) );
771 + }
772 +
773 + $action = sanitize_text_field( wp_unslash( $_POST['subscrpt_order_action'] ) );
774 + if ( '' !== $action ) {
775 + Subscriptions::process_status_change( $subscription_id, $action );
776 + }
777 +
778 + wp_safe_redirect( $form_action );
779 + exit;
780 + }
781 +
782 + // Allowed actions for the current status (mirrors Subscriptions::subscrpt_order_save_post()).
783 + $actions_data = array(
784 + 'active' => array(
785 + 'label' => __( 'Activate Subscription', 'subscription' ),
786 + 'value' => 'active',
787 + ),
788 + 'pending' => array(
789 + 'label' => __( 'Pending Subscription', 'subscription' ),
790 + 'value' => 'pending',
791 + ),
792 + 'expire' => array(
793 + 'label' => __( 'Expire Subscription', 'subscription' ),
794 + 'value' => 'expired',
795 + ),
796 + 'pe_cancelled' => array(
797 + 'label' => __( 'Pending Cancel Subscription', 'subscription' ),
798 + 'value' => 'pe_cancelled',
799 + ),
800 + 'cancelled' => array(
801 + 'label' => __( 'Cancel Subscription', 'subscription' ),
802 + 'value' => 'cancelled',
803 + ),
804 + );
805 +
806 + $map_actions = array(
807 + 'pending' => array( 'active', 'cancelled' ),
808 + 'active' => array( 'pe_cancelled', 'cancelled' ),
809 + 'pe_cancelled' => array( 'active', 'cancelled' ),
810 + 'cancelled' => array( 'active' ),
811 + 'expired' => array( 'active', 'cancelled' ),
812 + 'completed' => array( 'cancelled' ),
813 + );
814 +
815 + $status = get_post_status( $subscription_id );
816 + $actions = $map_actions[ $status ] ?? array();
817 +
818 + // Gather subscription data for the view.
819 + $subscription_data = Helper::get_subscription_data( $subscription_id );
820 +
821 + $order_id = $subscription_data['order']['order_id'] ?? get_post_meta( $subscription_id, '_subscrpt_order_id', true );
822 + $order = $order_id ? wc_get_order( $order_id ) : null;
823 + $order_item_id = $subscription_data['order']['order_item_id'] ?? get_post_meta( $subscription_id, '_subscrpt_order_item_id', true );
824 + $order_item = ( $order && $order_item_id ) ? $order->get_item( $order_item_id ) : null;
825 +
826 + $rows = Subscriptions::get_info_rows( $subscription_id );
827 +
828 + // Related orders.
829 + $order_histories = Helper::get_related_orders( $subscription_id );
830 +
831 + $this->render_admin_header(
832 + '',
833 + '',
834 + array(
835 + array(
836 + 'label' => __( 'Subscriptions', 'subscription' ),
837 + 'url' => $list_url,
838 + ),
839 + array(
840 + 'label' => '#' . $subscription_id,
841 + 'url' => '',
842 + ),
843 + )
844 + );
845 +
846 + include __DIR__ . '/views/subscription-details.php';
847 +
848 + $this->render_admin_footer();
849 + }
850 +
851 + /**
402 852 * Render Stats page
403 853 */
404 854 public function render_stats_page() {
405 - $this->render_admin_header();
855 + $this->render_admin_header( __( 'Reports', 'subscription' ), __( 'View your subscription analytics', 'subscription' ) );
406 856
407 - if ( ! class_exists( 'Sdevs_Wc_Subscription_Pro' ) ) {
408 - ?>
409 - <div class="wp-subscription-admin-content" style="max-width:1240px;margin:32px auto 0 auto">
410 - <div class="wp-subscription-hero-upgrade" style="margin-bottom:18px;">
411 - <div class="wp-subscription-hero-content">
412 - <span class="wp-subscription-hero-icon">✨</span>
413 - <span class="wp-subscription-hero-title">
414 - Unlock advanced features, priority support,<br>
415 - and more subscription control and reporting.
416 - </span>
417 - </div>
418 - <a href="https://wpsubscription.co/?utm_source=plugin&utm_medium=admin&utm_campaign=upgrade_pro"
419 - target="_blank"
420 - class="wp-subscription-hero-btn">
421 - UPGRADE TO PRO
422 - </a>
423 - </div>
424 - </div>
425 - <?php
857 + if ( ! subscrpt_pro_activated() ) {
858 + include 'views/reports-preview.php';
426 859 } else {
427 860 // Allow pro plugin to override the entire stats page content.
428 861 do_action( 'subscrpt_render_stats_page' );
429 862 }
@@ -428,135 +861,66 @@
428 861 do_action( 'subscrpt_render_stats_page' );
429 862 }
430 863
431 864 $this->render_admin_footer();
865 + }
432 866
433 - return;
867 + /**
868 + * Render Health page
869 + */
870 + public function render_health_page() {
871 + $this->render_admin_header( __( 'Health', 'subscription' ), __( 'Monitor your subscription health', 'subscription' ) );
872 +
873 + if ( ! subscrpt_pro_activated() ) {
874 + include 'views/health-preview.php';
875 + } else {
876 + // Allow pro plugin to render the full health page content.
877 + do_action( 'subscrpt_render_health_page' );
878 + }
879 +
880 + $this->render_admin_footer();
434 881 }
435 882
436 883 /**
884 + * Render Delivery Schedules page.
885 + * When pro is active, fires subscrpt_render_delivery_page for pro to handle.
886 + */
887 + public function render_delivery_page() {
888 + $this->render_admin_header( __( 'Delivery Schedules', 'subscription' ), __( 'Track and manage subscription delivery schedules.', 'subscription' ) );
889 +
890 + if ( ! subscrpt_pro_activated() ) {
891 + include 'views/delivery-preview.php';
892 + } else {
893 + // Allow pro plugin to render the full delivery page content.
894 + do_action( 'subscrpt_render_delivery_page' );
895 + }
896 +
897 + $this->render_admin_footer();
898 + }
899 +
900 + /**
437 901 * Render Support page
438 902 */
439 903 public function render_support_page() {
440 - $this->render_admin_header();
441 - ?>
442 - <div class="wp-subscription-admin-content" style="max-width:1240px;margin:32px auto 0 auto">
443 - <?php if ( ! class_exists( 'Sdevs_Wc_Subscription_Pro' ) ) : ?>
444 - <!-- HERO VARIANT 1: Emoji -->
445 - <div class="wp-subscription-hero-upgrade" style="margin-bottom:18px;">
446 - <div class="wp-subscription-hero-content">
447 - <span class="wp-subscription-hero-icon">✨</span>
448 - <span class="wp-subscription-hero-title">
449 - Unlock advanced features, priority support,<br>
450 - and more subscription control and reporting.
451 - </span>
452 - </div>
453 - <a href="https://wpsubscription.co/?utm_source=plugin&utm_medium=admin&utm_campaign=upgrade_pro"
454 - target="_blank"
455 - class="wp-subscription-hero-btn">
456 - UPGRADE TO PRO
457 - </a>
458 - </div>
459 - <?php endif; ?>
460 -
461 - <!-- Product Overview & Video -->
462 - <div class="wp-subscription-admin-box" style="margin-bottom:24px;display:flex;gap:32px;align-items:flex-start;flex-wrap:wrap;">
463 - <div style="flex:1;">
464 - <h3>Product Overview</h3>
465 - <p style="font-size:14px;line-height:1.7;margin:0 0 10px 0;">
466 - WPSubscription helps you to sell products and services on a recurring basis using your existing WooCommerce store. Whether you're offering digital licenses, physical product boxes, or ongoing service plans, this plugin provides the tools to build and manage subscription models.
467 - </p>
468 - <ul style="font-size:14px;line-height:1.6;margin:0 0 0 18px;padding:0;list-style:disc;">
469 - <li>Create simple or variable subscription products</li>
470 - <li>Set billing intervals (daily, weekly, monthly, yearly)</li>
471 - <li>Offer free trials and sign-up fees</li>
472 - <li>Allow customers to cancel or renew subscriptions manually</li>
473 - <li>View and manage subscriptions from the admin dashboard</li>
474 - <li>Customize subscription behavior and role assignment</li>
475 - <li>Integrate with payment gateways that support recurring billing (Stripe, PayPal)</li>
476 - </ul>
477 - </div>
478 - <div style="flex:1;">
479 - <div style="background:#f4f7fa;border-radius:8px;padding:10px 10px 6px 10px;box-shadow:0 2px 8px #e0e7ef;">
480 - <div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:6px;text-align: center;">
481 - <iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/o8usgcZp1nY?si=iPb5z7bvcy0rIyOQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
482 -ß </div>
483 - <!-- <div style="text-align:center;font-size:13px;color:#888;margin-top:4px;">Watch: Quick Product Tour</div> -->
484 - </div>
485 - </div>
486 - </div>
904 + $this->render_admin_header( __( 'Help & Resources', 'subscription' ), __( 'Documentation, community links, and ways to get help with WPSubscription.', 'subscription' ) );
905 + include 'views/support.php';
906 + $this->render_admin_footer();
907 + }
487 908
488 - <!-- PRO Features List -->
489 - <div class="wp-subscription-admin-box wp-subscription-pro-features" style="margin-bottom:32px;">
490 - <div style="font-size:1.3em;font-weight:600;margin-bottom:12px;display:flex;align-items:center;gap:10px;">
491 - <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>
492 - <span>WP Subscription PRO Features</span>
493 - </div>
494 - <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;">
495 - <li style="background:#f4f7fa;border-radius:8px;padding:18px 20px;display:flex;align-items:center;gap:14px;box-shadow:0 2px 8px #e0e7ef;">
496 - <span style="font-size:1.5em;color:#2196f3;">🔀</span>
497 - <span><b>Variable Product</b><br><span style="color:#555;font-size:0.98em;">Offer flexible subscription options for variable products.</span></span>
498 - </li>
499 - <li style="background:#f4f7fa;border-radius:8px;padding:18px 20px;display:flex;align-items:center;gap:14px;box-shadow:0 2px 8px #e0e7ef;">
500 - <span style="font-size:1.5em;color:#2196f3;">🚚</span>
501 - <span><b>Delivery Schedule</b><br><span style="color:#555;font-size:0.98em;">Set custom delivery intervals for each subscription.</span></span>
502 - </li>
503 - <li style="background:#f4f7fa;border-radius:8px;padding:18px 20px;display:flex;align-items:center;gap:14px;box-shadow:0 2px 8px #e0e7ef;">
504 - <span style="font-size:1.5em;color:#2196f3;">📜</span>
505 - <span><b>Subscription History</b><br><span style="color:#555;font-size:0.98em;">Track all changes and events for every subscription.</span></span>
506 - </li>
507 - <li style="background:#f4f7fa;border-radius:8px;padding:18px 20px;display:flex;align-items:center;gap:14px;box-shadow:0 2px 8px #e0e7ef;">
508 - <span style="font-size:1.5em;color:#2196f3;">⏳</span>
509 - <span><b>More Subscription Durations</b><br><span style="color:#555;font-size:0.98em;">Offer more flexible and custom subscription periods.</span></span>
510 - </li>
511 - <li style="background:#f4f7fa;border-radius:8px;padding:18px 20px;display:flex;align-items:center;gap:14px;box-shadow:0 2px 8px #e0e7ef;">
512 - <span style="font-size:1.5em;color:#2196f3;">💶</span>
513 - <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>
514 - </li>
515 - <li style="background:#f4f7fa;border-radius:8px;padding:18px 20px;display:flex;align-items:center;gap:14px;box-shadow:0 2px 8px #e0e7ef;">
516 - <span style="font-size:1.5em;color:#2196f3;">⏩</span>
517 - <span><b>Early Renewal</b><br><span style="color:#555;font-size:0.98em;">Allow customers to renew their subscription before expiry.</span></span>
518 - </li>
519 - <li style="background:#f4f7fa;border-radius:8px;padding:18px 20px;display:flex;align-items:center;gap:14px;box-shadow:0 2px 8px #e0e7ef;">
520 - <span style="font-size:1.5em;color:#2196f3;">💳</span>
521 - <span><b>Renewal Price</b><br><span style="color:#555;font-size:0.98em;">Set a different price for subscription renewals.</span></span>
522 - </li>
523 - </ul>
524 - </div>
909 + /**
910 + * Render Onboarding Wizard page
911 + * SPA-style: all sections rendered at once, JS controls visibility
912 + * Initial load always shows page 1 (JS handles transitions from there)
913 + */
914 + public function render_onboarding_wizard() {
915 + // Start session if not already started (used by the wizard reset handler).
916 + if ( ! session_id() && ! headers_sent() ) {
917 + session_start();
918 + }
525 919
526 - <!-- Support Resources: 2 rows, 2 columns, each in its own box -->
527 - <div class="wp-subscription-support-resources" style="display:grid;grid-template-columns:repeat(2,1fr);gap:24px;margin-bottom:24px;">
528 - <div class="wp-subscription-admin-box">
529 - <h3>Documentation</h3>
530 - <p style="font-size:14px;margin:0 0 8px 0;">Read our <a href="https://docs.converslabs.com/en\" target=\"_blank\" style=\"color:#2271b1;\">comprehensive docs</a> for setup, migration, and advanced usage.</p>
531 - <a href="https://docs.converslabs.com/en" target="_blank" class="button button-small" style="font-size:13px;padding:5px 14px;">View Docs</a>
532 - </div>
533 - <div class="wp-subscription-admin-box">
534 - <h3>Facing An Issue?</h3>
535 - <p style="font-size:14px;margin:0 0 8px 0;">If you have a problem, <a href="https://wpsubscription.co/contact\" target=\"_blank\" style=\"color:#d93025;\">open a support ticket</a> or check our FAQ.</p>
536 - <a href="https://wpsubscription.co/contact" target="_blank" class="button button-small" style="font-size:13px;padding:5px 14px;">Get Support</a>
537 - </div>
538 - <div class="wp-subscription-admin-box">
539 - <h3>Request a Feature</h3>
540 - <p style="font-size:14px;margin:0 0 8px 0;">Have an idea? <a href="https://wpsubscription.co/contact\" target=\"_blank\" style=\"color:#2271b1;\">Request a feature</a> or vote on others.</p>
541 - <a href="https://wpsubscription.co/contact" target="_blank" class="button button-small" style="font-size:13px;padding:5px 14px;">Request Feature</a>
542 - </div>
543 - <div class="wp-subscription-admin-box">
544 - <h3>Show Your Love</h3>
545 - <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>
546 - <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>
547 - </div>
548 - </div>
549 - </div>
550 - <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
551 - Made with <span style="color:#e25555;font-size:1.1em;">♥</span> by the WP Subscription Team
552 - <div style="margin-top:6px;">
553 - <a href="https://wpsubscription.co/contact" target="_blank" style="color:#2563eb;text-decoration:none;">Support</a>
554 - &nbsp;/&nbsp;
555 - <a href="https://docs.converslabs.com/en" target="_blank" style="color:#2563eb;text-decoration:none;">Docs</a>
556 - </div>
557 - </div>
558 - <?php
920 + $this->render_admin_header( __( 'Setup Wizard', 'subscription' ), __( 'Create your first subscription plan', 'subscription' ) );
921 + include __DIR__ . '/views/onboarding-wizard.php';
922 + $this->render_admin_footer();
559 923 }
560 924
561 925 /**
562 926 * Render the legacy subscriptions list page (WP_List_Table based)