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 +544 -178 1.10.0 → 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 157 __( 'WPSubscription', 'subscription' ),
65 158 __( 'WPSubscription', 'subscription' ),
66 - 'manage_woocommerce',
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,10 +211,10 @@
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,58 +221,146 @@
93 221 // Subscription Health
94 222 add_submenu_page(
95 223 $parent_slug,
96 224 __( 'Health', 'subscription' ),
97 - __( 'Health', 'subscription' ),
98 - 'manage_woocommerce',
225 + __( 'Health', 'subscription' ) . $pro_badge,
226 + 'manage_options',
99 227 'wp-subscription-health',
100 228 array( $this, 'render_health_page' )
101 229 );
102 230
103 - /*
104 - ! For god's sake, check existing code before implementing!
105 - // Settings
231 + // Delivery Schedules
106 232 add_submenu_page(
107 233 $parent_slug,
108 - __( 'Settings', 'subscription' ),
109 - __( 'Settings', 'subscription' ),
110 - 'manage_woocommerce',
111 - 'wp-subscription-settings',
112 - array( $this, 'render_settings_page' )
234 + __( 'Delivery', 'subscription' ),
235 + __( 'Delivery', 'subscription' ) . $pro_badge,
236 + 'manage_options',
237 + 'wp-subscription-delivery',
238 + array( $this, 'render_delivery_page' )
113 239 );
114 - */
115 240
116 - // Support
241 + // Help & Resources
117 242 add_submenu_page(
118 243 $parent_slug,
119 - __( 'Support', 'subscription' ),
120 - __( 'Support', 'subscription' ),
121 - 'manage_woocommerce',
244 + __( 'Help', 'subscription' ),
245 + __( 'Help', 'subscription' ),
246 + 'manage_options',
122 247 'wp-subscription-support',
123 248 array( $this, 'render_support_page' )
124 249 );
125 250
126 - // Add WPSubscription 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 + */
127 261 add_submenu_page(
128 262 'woocommerce',
129 - __( 'WPSubscription', 'subscription' ),
130 - __( 'WPSubscription', 'subscription' ),
131 - 'manage_woocommerce',
132 - 'wp-subscription',
133 - array( $this, 'render_subscriptions_page' )
263 + __( 'Subscriptions', 'subscription' ),
264 + __( 'Subscriptions', 'subscription' ),
265 + 'manage_options',
266 + 'admin.php?page=wp-subscription-list'
134 267 );
135 268 }
269 +
136 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 + /**
137 350 * Render the admin header
138 351 */
139 352 public function render_admin_footer() {
140 353 ?>
141 354 <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
142 - Made with <span style="color:#e25555;font-size:1.1em;">♥</span> by the WPSubscription 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 + ?>
143 359 <div style="margin-top:6px;">
144 - <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>
145 361 &nbsp;/&nbsp;
146 - <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>
147 363 </div>
148 364 </div>
149 365 <?php
150 366 }
@@ -150,17 +366,53 @@
150 366 }
151 367 /**
152 368 * Render the admin header.
153 369 *
154 - * @param string $title Page title shown on the left.
155 - * @param string $subtitle Optional subtitle shown below the title.
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.
156 382 */
157 - public function render_admin_header( string $title = '', string $subtitle = '' ) {
383 + public function render_admin_header( string $title = '', string $subtitle = '', array $breadcrumbs = [] ) {
158 384 $current = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : 'wp-subscription';
159 385
160 386 // Kept for backward compatibility — extensions may hook here for side-effects.
161 387 $menu_items = apply_filters( 'subscrpt_admin_header_menu_items', [], $current );
162 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;
163 415 ?>
164 416 <div class="wp-subscription-admin-header">
165 417 <div class="wp-subscription-admin-header-inner">
166 418 <div class="wp-subscription-admin-header-left">
@@ -167,17 +419,58 @@
167 419 <nav class="wp-subscription-breadcrumb" aria-label="<?php esc_attr_e( 'Breadcrumb', 'subscription' ); ?>">
168 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' ); ?>">
169 421 <span class="dashicons dashicons-admin-home"></span>
170 422 </a>
171 - <?php if ( $title ) : ?>
423 + <?php foreach ( $trail as $index => $crumb ) : ?>
172 424 <span class="wp-subscription-breadcrumb-sep" aria-hidden="true">/</span>
173 - <span class="wp-subscription-breadcrumb-current"><?php echo esc_html( $title ); ?></span>
174 - <?php endif; ?>
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; ?>
175 432 </nav>
176 433 </div>
177 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 +
178 468 <?php if ( ! class_exists( 'Sdevs_Wc_Subscription_Pro' ) ) : ?>
179 - <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>
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>
180 473 <?php endif; ?>
181 474 <img src="<?php echo esc_url( SUBSCRPT_ASSETS . '/images/logo-title.svg' ); ?>" alt="WPSubscription" class="wp-subscription-logo">
182 475 </div>
183 476 </div>
@@ -185,8 +478,46 @@
185 478 <?php
186 479 }
187 480
188 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 + /**
189 520 * Render Subscriptions page
190 521 */
191 522 public function render_subscriptions_page() {
192 523 $this->render_admin_header( __( 'Subscriptions', 'subscription' ), __( 'Manage your subscriptions', 'subscription' ) );
@@ -196,8 +527,9 @@
196 527 $search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
197 528 $date_filter = isset( $_GET['date_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['date_filter'] ) ) : '';
198 529 $per_page = isset( $_GET['per_page'] ) ? max( 1, intval( $_GET['per_page'] ) ) : 20;
199 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;
200 532
201 533 // Handle form submissions (both filters and bulk actions)
202 534 $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
203 535 if ( 'POST' === $request_method ) {
@@ -227,9 +559,9 @@
227 559 wp_delete_post( $sub_id, true );
228 560 }
229 561 }
230 562
231 - wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription' ) );
563 + wp_safe_redirect( admin_url( 'admin.php?page=wp-subscription-list' ) );
232 564 exit;
233 565 }
234 566 }
235 567
@@ -242,8 +574,11 @@
242 574 }
243 575 if ( ! empty( $_POST['date_filter'] ) ) {
244 576 $filter_params['date_filter'] = sanitize_text_field( wp_unslash( $_POST['date_filter'] ) );
245 577 }
578 + if ( ! empty( $_POST['renewal_due'] ) ) {
579 + $filter_params['renewal_due'] = absint( $_POST['renewal_due'] );
580 + }
246 581 if ( ! empty( $_POST['s'] ) ) {
247 582 $filter_params['s'] = sanitize_text_field( wp_unslash( $_POST['s'] ) );
248 583 }
249 584 if ( ! empty( $_POST['per_page'] ) ) {
@@ -249,9 +584,9 @@
249 584 if ( ! empty( $_POST['per_page'] ) ) {
250 585 $filter_params['per_page'] = intval( $_POST['per_page'] );
251 586 }
252 587
253 - $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' ) );
254 589 wp_safe_redirect( $redirect_url );
255 590 exit;
256 591 }
257 592 }
@@ -284,9 +619,9 @@
284 619 foreach ( $trash_posts as $trash_id ) {
285 620 wp_delete_post( $trash_id, true );
286 621 }
287 622
288 - 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' ) );
289 624 exit;
290 625 } else {
291 626 // For other actions, verify nonce with subscription ID.
292 627 $nonce_action = 'wpsubs_action_' . $sub_id;
@@ -294,9 +629,9 @@
294 629 echo '<div class="notice notice-error"><p>' . esc_html__( 'Security check failed. Please try again.', 'subscription' ) . '</p></div>';
295 630 wp_die();
296 631 }
297 632
298 - $redirect_url = admin_url( 'admin.php?page=wp-subscription' );
633 + $redirect_url = admin_url( 'admin.php?page=wp-subscription-list' );
299 634
300 635 switch ( $action ) {
301 636 case 'duplicate':
302 637 $post = get_post( $sub_id );
@@ -325,9 +660,9 @@
325 660 wp_untrash_post( $sub_id );
326 661 break;
327 662 case 'delete':
328 663 wp_delete_post( $sub_id, true );
329 - $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' );
330 665 break;
331 666 }
332 667
333 668 wp_safe_redirect( $redirect_url );
@@ -364,9 +699,24 @@
364 699 'year' => intval( $year ),
365 700 'month' => intval( $month ),
366 701 ];
367 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 );
368 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 +
369 719 $query = new \WP_Query( $args );
370 720 $subscriptions = $query->posts;
371 721 $total = $query->found_posts;
372 722 $max_num_pages = $query->max_num_pages;
@@ -376,13 +726,16 @@
376 726
377 727 include __DIR__ . '/views/subscription-list.php';
378 728 ?>
379 729 <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
380 - Made with <span style="color:#e25555;font-size:1.1em;">♥</span> by the WPSubscription 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 + ?>
381 734 <div style="margin-top:6px;">
382 - <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>
383 736 &nbsp;/&nbsp;
384 - <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>
385 738 </div>
386 739 </div>
387 740 <?php
388 741 }
@@ -387,8 +740,116 @@
387 740 <?php
388 741 }
389 742
390 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 + /**
391 852 * Render Stats page
392 853 */
393 854 public function render_stats_page() {
394 855 $this->render_admin_header( __( 'Reports', 'subscription' ), __( 'View your subscription analytics', 'subscription' ) );
@@ -393,15 +854,9 @@
393 854 public function render_stats_page() {
394 855 $this->render_admin_header( __( 'Reports', 'subscription' ), __( 'View your subscription analytics', 'subscription' ) );
395 856
396 857 if ( ! subscrpt_pro_activated() ) {
397 - $args = [
398 - 'preview_image_url' => SUBSCRPT_ASSETS . '/images/previews/reports-page-preview.png',
399 - 'cta_title' => __( 'Unlock Subscription Reports', 'subscription' ),
400 - 'cta_description' => __( 'Subscription Reports require WPSubscription Pro. Unlock advanced features, priority support, and more with WPSubscription Pro.', 'subscription' ),
401 - ];
402 - $preview_content = subscrpt_render_page_preview( $args );
403 - echo $preview_content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Content is already escaped. Re-escaping will break the HTML structure.
858 + include 'views/reports-preview.php';
404 859 } else {
405 860 // Allow pro plugin to override the entire stats page content.
406 861 do_action( 'subscrpt_render_stats_page' );
407 862 }
@@ -415,15 +870,9 @@
415 870 public function render_health_page() {
416 871 $this->render_admin_header( __( 'Health', 'subscription' ), __( 'Monitor your subscription health', 'subscription' ) );
417 872
418 873 if ( ! subscrpt_pro_activated() ) {
419 - $args = [
420 - 'preview_image_url' => SUBSCRPT_ASSETS . '/images/previews/subscrpt-health-preview.png',
421 - 'cta_title' => __( 'Unlock Subscription Health', 'subscription' ),
422 - 'cta_description' => __( 'Subscription Health requires WPSubscription Pro. Unlock advanced features, priority support, and more with WPSubscription Pro.', 'subscription' ),
423 - ];
424 - $preview_content = subscrpt_render_page_preview( $args );
425 - echo $preview_content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Content is already escaped. Re-escaping will break the HTML structure.
874 + include 'views/health-preview.php';
426 875 } else {
427 876 // Allow pro plugin to render the full health page content.
428 877 do_action( 'subscrpt_render_health_page' );
429 878 }
@@ -431,130 +880,47 @@
431 880 $this->render_admin_footer();
432 881 }
433 882
434 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 + /**
435 901 * Render Support page
436 902 */
437 903 public function render_support_page() {
438 - $this->render_admin_header( __( 'Support', 'subscription' ), __( 'Get help and resources', 'subscription' ) );
439 - ?>
440 - <div class="wp-subscription-admin-content" style="max-width:1240px;margin:32px auto 0 auto">
441 - <?php if ( ! class_exists( 'Sdevs_Wc_Subscription_Pro' ) ) : ?>
442 - <!-- HERO VARIANT 1: Emoji -->
443 - <div class="wp-subscription-hero-upgrade" style="margin-bottom:18px;">
444 - <div class="wp-subscription-hero-content">
445 - <span class="wp-subscription-hero-icon">✨</span>
446 - <span class="wp-subscription-hero-title">
447 - Unlock advanced features, priority support,<br>
448 - and more subscription control and reporting.
449 - </span>
450 - </div>
451 - <a href="https://wpsubscription.co/?utm_source=plugin&utm_medium=admin&utm_campaign=upgrade_pro"
452 - target="_blank"
453 - class="wp-subscription-hero-btn">
454 - UPGRADE TO PRO
455 - </a>
456 - </div>
457 - <?php endif; ?>
458 -
459 - <!-- Product Overview & Video -->
460 - <div class="wp-subscription-admin-box" style="margin-bottom:24px;display:flex;gap:32px;align-items:flex-start;flex-wrap:wrap;">
461 - <div style="flex:1;">
462 - <h3>Product Overview</h3>
463 - <p style="font-size:14px;line-height:1.7;margin:0 0 10px 0;">
464 - 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.
465 - </p>
466 - <ul style="font-size:14px;line-height:1.6;margin:0 0 0 18px;padding:0;list-style:disc;">
467 - <li>Create simple or variable subscription products</li>
468 - <li>Set billing intervals (daily, weekly, monthly, yearly)</li>
469 - <li>Offer free trials and sign-up fees</li>
470 - <li>Allow customers to cancel or renew subscriptions manually</li>
471 - <li>View and manage subscriptions from the admin dashboard</li>
472 - <li>Customize subscription behavior and role assignment</li>
473 - <li>Integrate with payment gateways that support recurring billing (Stripe, PayPal)</li>
474 - </ul>
475 - </div>
476 - <div style="flex:1;">
477 - <div style="background:#f4f7fa;border-radius:8px;padding:10px 10px 6px 10px;box-shadow:0 2px 8px #e0e7ef;">
478 - <div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:6px;text-align: center;">
479 - <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>
480 -ß </div>
481 - <!-- <div style="text-align:center;font-size:13px;color:#888;margin-top:4px;">Watch: Quick Product Tour</div> -->
482 - </div>
483 - </div>
484 - </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 + }
485 908
486 - <!-- PRO Features List -->
487 - <div class="wp-subscription-admin-box wp-subscription-pro-features" style="margin-bottom:32px;">
488 - <div style="font-size:1.3em;font-weight:600;margin-bottom:12px;display:flex;align-items:center;gap:10px;">
489 - <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>
490 - <span>WPSubscription PRO Features</span>
491 - </div>
492 - <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;">
493 - <li style="background:#f4f7fa;border-radius:8px;padding:18px 20px;display:flex;align-items:center;gap:14px;box-shadow:0 2px 8px #e0e7ef;">
494 - <span style="font-size:1.5em;color:#2196f3;">🔀</span>
495 - <span><b>Variable Product</b><br><span style="color:#555;font-size:0.98em;">Offer flexible subscription options for variable products.</span></span>
496 - </li>
497 - <li style="background:#f4f7fa;border-radius:8px;padding:18px 20px;display:flex;align-items:center;gap:14px;box-shadow:0 2px 8px #e0e7ef;">
498 - <span style="font-size:1.5em;color:#2196f3;">🚚</span>
499 - <span><b>Delivery Schedule</b><br><span style="color:#555;font-size:0.98em;">Set custom delivery intervals for each subscription.</span></span>
500 - </li>
501 - <li style="background:#f4f7fa;border-radius:8px;padding:18px 20px;display:flex;align-items:center;gap:14px;box-shadow:0 2px 8px #e0e7ef;">
502 - <span style="font-size:1.5em;color:#2196f3;">📜</span>
503 - <span><b>Subscription History</b><br><span style="color:#555;font-size:0.98em;">Track all changes and events for every subscription.</span></span>
504 - </li>
505 - <li style="background:#f4f7fa;border-radius:8px;padding:18px 20px;display:flex;align-items:center;gap:14px;box-shadow:0 2px 8px #e0e7ef;">
506 - <span style="font-size:1.5em;color:#2196f3;">⏳</span>
507 - <span><b>More Subscription Durations</b><br><span style="color:#555;font-size:0.98em;">Offer more flexible and custom subscription periods.</span></span>
508 - </li>
509 - <li style="background:#f4f7fa;border-radius:8px;padding:18px 20px;display:flex;align-items:center;gap:14px;box-shadow:0 2px 8px #e0e7ef;">
510 - <span style="font-size:1.5em;color:#2196f3;">💶</span>
511 - <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>
512 - </li>
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>Early Renewal</b><br><span style="color:#555;font-size:0.98em;">Allow customers to renew their subscription before expiry.</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>Renewal Price</b><br><span style="color:#555;font-size:0.98em;">Set a different price for subscription renewals.</span></span>
520 - </li>
521 - </ul>
522 - </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 + }
523 919
524 - <!-- Support Resources: 2 rows, 2 columns, each in its own box -->
525 - <div class="wp-subscription-support-resources" style="display:grid;grid-template-columns:repeat(2,1fr);gap:24px;margin-bottom:24px;">
526 - <div class="wp-subscription-admin-box">
527 - <h3>Documentation</h3>
528 - <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>
529 - <a href="https://docs.converslabs.com/en" target="_blank" class="button button-small" style="font-size:13px;padding:5px 14px;">View Docs</a>
530 - </div>
531 - <div class="wp-subscription-admin-box">
532 - <h3>Facing An Issue?</h3>
533 - <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>
534 - <a href="https://wpsubscription.co/contact" target="_blank" class="button button-small" style="font-size:13px;padding:5px 14px;">Get Support</a>
535 - </div>
536 - <div class="wp-subscription-admin-box">
537 - <h3>Request a Feature</h3>
538 - <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>
539 - <a href="https://wpsubscription.co/contact" target="_blank" class="button button-small" style="font-size:13px;padding:5px 14px;">Request Feature</a>
540 - </div>
541 - <div class="wp-subscription-admin-box">
542 - <h3>Show Your Love</h3>
543 - <p style="font-size:14px;margin:0 0 8px 0;">Enjoying WPSubscription? <a href="https://wordpress.org/support/plugin/subscription/reviews/\" target=\"_blank\" style=\"color:#f59e42;\">Leave us a review</a> or share your experience!</p>
544 - <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>
545 - </div>
546 - </div>
547 - </div>
548 - <div style="text-align:center;margin:38px 0 0 0;font-size:14px;color:#888;">
549 - Made with <span style="color:#e25555;font-size:1.1em;">♥</span> by the WPSubscription Team
550 - <div style="margin-top:6px;">
551 - <a href="https://wpsubscription.co/contact" target="_blank" style="color:#2563eb;text-decoration:none;">Support</a>
552 - &nbsp;/&nbsp;
553 - <a href="https://docs.converslabs.com/en" target="_blank" style="color:#2563eb;text-decoration:none;">Docs</a>
554 - </div>
555 - </div>
556 - <?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();
557 923 }
558 924
559 925 /**
560 926 * Render the legacy subscriptions list page (WP_List_Table based)