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