Plugin Directory

Changeset 3651653


Ignore:
Timestamp:
08/17/2026 07:50:43 PM (2 months ago)
Author:
subscriptionplugin
Message:

Bump version to 1.0.22 - Maintenance release: admin fixes, checkout stability, and feature toggles

Location:
subscription-system/trunk
Files:
5 added
4 deleted
21 edited

Legend:

Unmodified
Added
Removed
  • subscription-system/trunk/README.txt

    r3650727 r3651653  
    66Tested up to: 7.0
    77Requires PHP: 8.0
    8 Stable tag: 1.0.21
     8Stable tag: 1.0.22
    99License: GPLv2 or later
    1010License URI: http://www.gnu.org/licenses/gpl-2.0.html
    … …  
    8686== Changelog ==
    8787
     88= 1.0.22 =
     89**Maintenance release — admin fixes, checkout stability, and feature toggles**
     90* FIX: Subscribers admin page stylesheet now loads correctly (fixed screen ID mismatch that caused overlapping layout).
     91* FIX: Login, register, account, and checkout redirect URLs now respect custom Pages Settings instead of always sending visitors to wp-login.php.
     92* FIX: Stale Stripe customer IDs no longer crash checkout when a stored customer was deleted or belongs to a different Stripe mode.
     93* FIX: Content notification popup buttons ("Subscribe" / "Remind me later") now work reliably; consolidated into content-notification.js.
     94* FIX: Database updater self-heals missing columns on upgrade (Stripe mode stamping, plan-switch scheduling, content subscription payment fields).
     95* FIX: Plan category labels no longer render blank when only one of the category_name/name columns is populated.
     96* FIX: Featured plan badge no longer fails when plan index is stored as a float string.
     97* NEW: Features tab in Settings — enable or disable Subscription Plans and Content Subscriptions independently; admin menu shows only enabled features.
     98* NEW: Per-plan customizable button label (e.g. "Buy Now" instead of "Subscribe Now").
     99* NEW: Improved front-end asset cache busting (file modification time appended to script/style version strings).
     100* IMPROVED: Admin menu reorganized into logical feature blocks (Plans, Content, shared settings).
     101* REMOVED: The "This content is for subscribers" paywall. Gated posts no longer show a teaser + subscribe prompt — full content is always rendered. Existing installs are cleaned up automatically on update (the paywall's stored setting is deleted); plans, checkout, webhooks and all other subscription features are unaffected.
     102
    88103= 1.0.21 =
     104* Version bump for deployment tracking.
     105
     106= 1.1.0 =
    89107**Centralized plans, and the content paywall — a core rebuild**
    90108* NEW: Content actually gets locked now. A the_content-based paywall shows a teaser + subscribe prompt on gated posts and grants full access only once Stripe confirms payment — previously every "gated" post was fully readable by anyone.
    … …  
    276294== Upgrade Notice ==
    277295
    278 = 1.0.21 =
    279 Major rebuild: new content paywall, unified Plans system, consolidated Stripe/webhook flow, and several security fixes (removed client-side Stripe secret key, fixed authorization gaps on subscription/admin actions). Recommended update.
     296= 1.0.22 =
     297Recommended update. Fixes Subscribers admin layout, checkout crashes from stale Stripe customers, content notification popup buttons, and Pages Settings login/register redirects. Adds feature toggles for Plans vs Content subscriptions and per-plan button labels.
    280298
    281299= 1.0.20 =
  • subscription-system/trunk/admin/partials/appearance-settings-display.php

    r3650727 r3651653  
    44 *
    55 * Every picker maps to a design token (--ss-*) that all public stylesheets
    6  * read, so a change here recolors buttons, headings, plans, paywalls, modals
     6 * read, so a change here recolors buttons, headings, plans, modals
    77 * and emails at once. Pale "tint" shades are derived automatically from the
    88 * color they belong to.
    … …  
    2323    <h2><?php esc_html_e('Global Colors', 'subscription-system'); ?></h2>
    2424    <p class="description" style="max-width: 640px;">
    25         <?php esc_html_e('These colors apply to everything the plugin shows to visitors — buttons, headings, pricing plans, paywalls, modals, account pages and notification emails. Pale background shades are derived automatically from the colors you pick.', 'subscription-system'); ?>
     25        <?php esc_html_e('These colors apply to everything the plugin shows to visitors — buttons, headings, pricing plans, modals, account pages and notification emails. Pale background shades are derived automatically from the colors you pick.', 'subscription-system'); ?>
    2626    </p>
    2727
  • subscription-system/trunk/admin/partials/plans-display.php

    r3650727 r3651653  
    3737}
    3838wp_enqueue_style( 'subs-sys-tokens', SUBS_SYS_PLUGIN_URL . 'public/css/subscription-system-tokens.css', array(), SUBS_SYS_VERSION );
    39 wp_enqueue_style( 'subscription-pricing-' . $preview_layout, SUBS_SYS_PLUGIN_URL . 'public/css/pricing-' . $preview_layout . '.css', array( 'subs-sys-tokens' ), SUBS_SYS_VERSION );
     39wp_enqueue_style( 'subs-sys-pricing-shared', SUBS_SYS_PLUGIN_URL . 'public/css/pricing-shared.css', array( 'subs-sys-tokens' ), subs_sys_asset_version( 'public/css/pricing-shared.css' ) );
     40wp_enqueue_style( 'subscription-pricing-' . $preview_layout, SUBS_SYS_PLUGIN_URL . 'public/css/pricing-' . $preview_layout . '.css', array( 'subs-sys-tokens', 'subs-sys-pricing-shared' ), subs_sys_asset_version( 'public/css/pricing-' . $preview_layout . '.css' ) );
     41wp_add_inline_style( 'subs-sys-tokens', Subs_Sys_Colors::inline_css() . Subs_Sys_Layout_Settings::inline_css() );
    4042
    4143$notice = '';
    … …  
    447449                                        ?>
    448450                                        <div class="ss-preview-panel__stage">
    449                                                 <div class="subscription-pricing-plans <?php echo esc_attr( $preview_layout ); ?> ss-responsive">
     451                                                <div class="subscription-pricing-plans <?php echo esc_attr( $preview_layout ); ?> ss-responsive <?php echo esc_attr( Subs_Sys_Layout_Settings::wrapper_classes( 'vertical' ) ); ?>">
    450452                                                        <div class="ss-pricing-container ss-<?php echo esc_attr( $preview_layout ); ?>">
    451453                                                                <div class="ss-pricing-plans">
    … …  
    611613
    612614        <style>
     615        /* The editable price digits sit in a span so the form can update them
     616         * live; keep them at full price size — only the period label is small. */
     617        .ss-preview-panel__stage .ss-price #ss-preview-price {
     618                font-size: inherit;
     619                font-weight: inherit;
     620                color: inherit;
     621                letter-spacing: inherit;
     622        }
     623
    613624        .ss-mode-heading {
    614625                margin: 28px 0 8px;
  • subscription-system/trunk/admin/partials/pricing-settings-display.php

    r3650727 r3651653  
    11<?php
    22/**
    3  * Provide a admin area view for pricing plan settings
     3 * Pricing Plan Settings — layout choice, design options, display behavior,
     4 * and live previews of all three layouts.
    45 *
    5  * @link       https://yourwebsite.com
    6  * @since      1.0.0
     6 * Every design control here writes to Subs_Sys_Layout_Settings; the previews
     7 * below are drawn by the real frontend stylesheets and re-painted live as
     8 * controls change, so what the admin sees is what the visitor gets.
    79 *
    810 * @package    Subscription_System
    … …  
    2426// below are styled by the real thing rather than a hand-copied approximation.
    2527wp_enqueue_style('subs-sys-tokens', SUBS_SYS_PLUGIN_URL . 'public/css/subscription-system-tokens.css', array(), SUBS_SYS_VERSION);
    26 wp_enqueue_style('subscription-pricing-layout1', SUBS_SYS_PLUGIN_URL . 'public/css/pricing-layout1.css', array('subs-sys-tokens'), SUBS_SYS_VERSION);
    27 wp_enqueue_style('subscription-pricing-layout2', SUBS_SYS_PLUGIN_URL . 'public/css/pricing-layout2.css', array('subs-sys-tokens'), SUBS_SYS_VERSION);
    28 wp_enqueue_style('subscription-pricing-layout3', SUBS_SYS_PLUGIN_URL . 'public/css/pricing-layout3.css', array('subs-sys-tokens'), SUBS_SYS_VERSION);
     28wp_enqueue_style('subs-sys-pricing-shared', SUBS_SYS_PLUGIN_URL . 'public/css/pricing-shared.css', array('subs-sys-tokens'), subs_sys_asset_version('public/css/pricing-shared.css'));
     29wp_enqueue_style('subscription-pricing-layout1', SUBS_SYS_PLUGIN_URL . 'public/css/pricing-layout1.css', array('subs-sys-tokens', 'subs-sys-pricing-shared'), subs_sys_asset_version('public/css/pricing-layout1.css'));
     30wp_enqueue_style('subscription-pricing-layout2', SUBS_SYS_PLUGIN_URL . 'public/css/pricing-layout2.css', array('subs-sys-tokens', 'subs-sys-pricing-shared'), subs_sys_asset_version('public/css/pricing-layout2.css'));
     31wp_enqueue_style('subscription-pricing-layout3', SUBS_SYS_PLUGIN_URL . 'public/css/pricing-layout3.css', array('subs-sys-tokens', 'subs-sys-pricing-shared'), subs_sys_asset_version('public/css/pricing-layout3.css'));
     32
     33// The saved Appearance colors and design variables, exactly as the front end
     34// prints them.
     35wp_add_inline_style('subs-sys-tokens', Subs_Sys_Colors::inline_css() . Subs_Sys_Layout_Settings::inline_css());
    2936
    3037// Get current settings
    3138$default_layout = get_option('subscription_default_layout', 'layout1');
    3239
    33 // Add this at the top of the file
    34 global $wp_settings_fields, $wp_settings_sections, $wp_registered_settings;
    35 
    36 // At the top of the file, after checking for direct access
     40// Save both the default layout and the design settings from one form.
    3741if (isset($_POST['save_layout_settings']) && check_admin_referer('save_layout_settings_nonce')) {
    3842    $layout = isset($_POST['subscription_default_layout']) ? sanitize_text_field(wp_unslash($_POST['subscription_default_layout'])) : 'layout1';
    3943    update_option('subscription_default_layout', $layout);
    40    
    41     // Show success message
     44
     45    if (isset($_POST['ssl']) && is_array($_POST['ssl'])) {
     46        update_option(Subs_Sys_Layout_Settings::OPTION, Subs_Sys_Layout_Settings::sanitize(wp_unslash($_POST['ssl']))); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- sanitized field-by-field in sanitize().
     47    }
     48
    4249    echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__('Settings saved successfully.', 'subscription-system') . '</p></div>';
    43    
    44     // Refresh the default layout variable
     50
    4551    $default_layout = get_option('subscription_default_layout', 'layout1');
    46 }
     52    // Re-print the variables with the values just saved.
     53    wp_add_inline_style('subs-sys-tokens', Subs_Sys_Layout_Settings::inline_css());
     54}
     55
     56$ss  = Subs_Sys_Layout_Settings::all();
     57$ss_wrapper_classes = Subs_Sys_Layout_Settings::wrapper_classes();
     58
     59/**
     60 * Small field helpers, local to this screen.
     61 */
     62$ss_select = function ($key, $choices) use ($ss) {
     63    $out = '<select name="ssl[' . esc_attr($key) . ']" class="ss-design-control" data-key="' . esc_attr($key) . '">';
     64    foreach ($choices as $value => $label) {
     65        $out .= '<option value="' . esc_attr($value) . '"' . selected($ss[$key], $value, false) . '>' . esc_html($label) . '</option>';
     66    }
     67    return $out . '</select>';
     68};
     69$ss_check = function ($key, $label) use ($ss) {
     70    return '<label><input type="checkbox" name="ssl[' . esc_attr($key) . ']" value="1" class="ss-design-control" data-key="' . esc_attr($key) . '"' . checked($ss[$key], '1', false) . '> ' . esc_html($label) . '</label>';
     71};
     72$ss_range = function ($key, $min, $max, $unit) use ($ss) {
     73    return '<span class="ss-range-wrap"><input type="range" name="ssl[' . esc_attr($key) . ']" min="' . esc_attr($min) . '" max="' . esc_attr($max) . '" value="' . esc_attr($ss[$key]) . '" class="ss-design-control" data-key="' . esc_attr($key) . '"> <output>' . esc_html($ss[$key]) . '</output>' . esc_html($unit) . '</span>';
     74};
    4775?>
    4876
    4977<div class="wrap subscription-settings-page">
    5078    <h1><?php echo esc_html__('Pricing Plan Settings', 'subscription-system'); ?></h1>
    51    
     79
    5280    <!-- Shortcode Info Box -->
    5381    <div class="notice notice-info is-dismissible">
    5482        <h3><?php echo esc_html__('Subscription Plans Shortcode:', 'subscription-system'); ?></h3>
    5583        <p><?php echo esc_html__('Use the following shortcode to display your subscription plans:', 'subscription-system'); ?></p>
    56         <code>[pricing_plans]</code> <?php echo esc_html__('– This will use the default layout set in the admin settings.', 'subscription-system'); ?>
    57         <p><code>[pricing_plans layout="layout1" code="2"]</code> <?php echo esc_html__('– This will display plans in Layout 1 filtered by category ID.', 'subscription-system'); ?></p>
    58         <p><code>[pricing_plans layout="layout2" code="3"]</code> <?php echo esc_html__('– This will display plans in Layout 2 filtered by category ID.', 'subscription-system'); ?></p>
    59         <p><code>[pricing_plans layout="layout3" orientation="horizontal"]</code> <?php echo esc_html__('– This will display plans in Layout 3 with horizontal orientation.', 'subscription-system'); ?></p>
    60         <p><?php echo esc_html__('Customize your subscription plans display by adding this shortcode to any post or page.', 'subscription-system'); ?></p>
     84        <code>[pricing_plans]</code> <?php echo esc_html__('– This will use the default layout and design settings below.', 'subscription-system'); ?>
     85        <p><code>[pricing_plans layout="layout2" code="3"]</code> <?php echo esc_html__('– Layout 2, filtered by category ID.', 'subscription-system'); ?></p>
     86        <p><code>[pricing_plans behavior="carousel"]</code> <?php echo esc_html__('– Override the display behavior for one page (auto, horizontal, vertical, scroll, carousel).', 'subscription-system'); ?></p>
    6187    </div>
    62    
     88
    6389    <form method="post" action="">
    6490        <?php wp_nonce_field('save_layout_settings_nonce'); ?>
    65        
     91
    6692        <div class="layout-settings-section">
    6793            <h2><?php echo esc_html__('Layout Settings', 'subscription-system'); ?></h2>
    … …  
    7197                    <td>
    7298                        <select name="subscription_default_layout" id="default-layout">
    73                             <option value="layout1" <?php selected($default_layout, 'layout1'); ?>><?php echo esc_html__('Layout 1', 'subscription-system'); ?></option>
    74                             <option value="layout2" <?php selected($default_layout, 'layout2'); ?>><?php echo esc_html__('Layout 2', 'subscription-system'); ?></option>
    75                             <option value="layout3" <?php selected($default_layout, 'layout3'); ?>><?php echo esc_html__('Layout 3', 'subscription-system'); ?></option>
     99                            <option value="layout1" <?php selected($default_layout, 'layout1'); ?>><?php echo esc_html__('Layout 1 — Classic cards', 'subscription-system'); ?></option>
     100                            <option value="layout2" <?php selected($default_layout, 'layout2'); ?>><?php echo esc_html__('Layout 2 — Ledger sheet', 'subscription-system'); ?></option>
     101                            <option value="layout3" <?php selected($default_layout, 'layout3'); ?>><?php echo esc_html__('Layout 3 — Spotlight stage', 'subscription-system'); ?></option>
    76102                        </select>
    77103                    </td>
    78104                </tr>
    79105            </table>
    80             <p class="submit">
    81                 <input type="submit" name="save_layout_settings" class="button button-primary" value="<?php esc_attr_e('Save Changes', 'subscription-system'); ?>">
    82             </p>
    83106        </div>
    84        
     107
     108        <div class="ss-design-settings">
     109            <h2><?php esc_html_e('Design Options', 'subscription-system'); ?></h2>
     110            <p class="description"><?php esc_html_e('These options apply to all three layouts. Colors come from Settings → Appearance. Changes preview live below — nothing is applied until you save.', 'subscription-system'); ?></p>
     111
     112            <div class="ss-design-grid">
     113
     114                <fieldset class="ss-design-group">
     115                    <legend><?php esc_html_e('Featured badge', 'subscription-system'); ?></legend>
     116                    <p><?php echo $ss_check('badge_enabled', __('Show a badge on the featured plan', 'subscription-system')); // phpcs:ignore WordPress.Security.EscapeOutput ?></p>
     117                    <p>
     118                        <label for="ssl-badge-text"><?php esc_html_e('Badge text', 'subscription-system'); ?></label><br>
     119                        <input type="text" id="ssl-badge-text" name="ssl[badge_text]" value="<?php echo esc_attr($ss['badge_text']); ?>" maxlength="40" list="ssl-badge-suggestions" class="regular-text ss-design-control" data-key="badge_text">
     120                        <datalist id="ssl-badge-suggestions">
     121                            <option value="<?php esc_attr_e('Most Popular', 'subscription-system'); ?>"></option>
     122                            <option value="<?php esc_attr_e('Best Value', 'subscription-system'); ?>"></option>
     123                            <option value="<?php esc_attr_e('Recommended', 'subscription-system'); ?>"></option>
     124                            <option value="<?php esc_attr_e('Featured', 'subscription-system'); ?>"></option>
     125                            <option value="<?php esc_attr_e('Editor’s Choice', 'subscription-system'); ?>"></option>
     126                        </datalist>
     127                    </p>
     128                    <p>
     129                        <label><?php esc_html_e('Badge style', 'subscription-system'); ?><br>
     130                        <?php echo $ss_select('badge_style', array( // phpcs:ignore WordPress.Security.EscapeOutput
     131                            'auto'   => __('Match the layout (recommended)', 'subscription-system'),
     132                            'pill'   => __('Floating pill', 'subscription-system'),
     133                            'ribbon' => __('Corner ribbon', 'subscription-system'),
     134                            'stripe' => __('Top stripe', 'subscription-system'),
     135                        )); ?></label>
     136                    </p>
     137                    <p>
     138                        <label><?php esc_html_e('Badge color', 'subscription-system'); ?><br>
     139                        <?php echo $ss_select('badge_color', array( // phpcs:ignore WordPress.Security.EscapeOutput
     140                            'accent'  => __('Accent color', 'subscription-system'),
     141                            'primary' => __('Primary color', 'subscription-system'),
     142                        )); ?></label>
     143                    </p>
     144                </fieldset>
     145
     146                <fieldset class="ss-design-group">
     147                    <legend><?php esc_html_e('Card style', 'subscription-system'); ?></legend>
     148                    <p><label><?php esc_html_e('Shadow', 'subscription-system'); ?><br>
     149                        <?php echo $ss_select('card_shadow', array('none' => __('None', 'subscription-system'), 'soft' => __('Soft', 'subscription-system'), 'medium' => __('Medium', 'subscription-system'), 'bold' => __('Bold', 'subscription-system'))); // phpcs:ignore WordPress.Security.EscapeOutput ?></label></p>
     150                    <p><label><?php esc_html_e('Border width', 'subscription-system'); ?><br>
     151                        <?php echo $ss_select('card_border', array('0' => __('None', 'subscription-system'), '1' => __('Hairline (1px)', 'subscription-system'), '2' => __('Strong (2px)', 'subscription-system'))); // phpcs:ignore WordPress.Security.EscapeOutput ?></label></p>
     152                    <p><label><?php esc_html_e('Corner radius', 'subscription-system'); ?><br>
     153                        <?php echo $ss_range('card_radius', 0, 32, 'px'); // phpcs:ignore WordPress.Security.EscapeOutput ?></label></p>
     154                    <p><label><?php esc_html_e('Card background', 'subscription-system'); ?><br>
     155                        <?php echo $ss_select('card_bg', array('surface' => __('Card color (from Appearance)', 'subscription-system'), 'tint' => __('Primary tint', 'subscription-system'), 'transparent' => __('Transparent', 'subscription-system'))); // phpcs:ignore WordPress.Security.EscapeOutput ?></label></p>
     156                    <p><label><?php esc_html_e('Card padding', 'subscription-system'); ?><br>
     157                        <?php echo $ss_select('card_padding', array('compact' => __('Compact', 'subscription-system'), 'cozy' => __('Comfortable', 'subscription-system'), 'spacious' => __('Spacious', 'subscription-system'))); // phpcs:ignore WordPress.Security.EscapeOutput ?></label></p>
     158                    <p><label><?php esc_html_e('Space between cards', 'subscription-system'); ?><br>
     159                        <?php echo $ss_range('card_gap', 0, 64, 'px'); // phpcs:ignore WordPress.Security.EscapeOutput ?></label></p>
     160                    <p><label><?php esc_html_e('Hover effect', 'subscription-system'); ?><br>
     161                        <?php echo $ss_select('hover_effect', array('none' => __('None', 'subscription-system'), 'lift' => __('Lift', 'subscription-system'), 'glow' => __('Glow', 'subscription-system'), 'scale' => __('Scale up', 'subscription-system'))); // phpcs:ignore WordPress.Security.EscapeOutput ?></label></p>
     162                    <p><label><?php esc_html_e('Text alignment', 'subscription-system'); ?><br>
     163                        <?php echo $ss_select('align', array('auto' => __('Match the layout (recommended)', 'subscription-system'), 'left' => __('Left', 'subscription-system'), 'center' => __('Center', 'subscription-system'))); // phpcs:ignore WordPress.Security.EscapeOutput ?></label></p>
     164                </fieldset>
     165
     166                <fieldset class="ss-design-group">
     167                    <legend><?php esc_html_e('Feature icons', 'subscription-system'); ?></legend>
     168                    <p><label><?php esc_html_e('Icon', 'subscription-system'); ?><br>
     169                        <?php echo $ss_select('icon_style', array('check' => __('Check ✓', 'subscription-system'), 'arrow' => __('Arrow →', 'subscription-system'), 'star' => __('Star ★', 'subscription-system'), 'dot' => __('Dot •', 'subscription-system'), 'none' => __('No icon', 'subscription-system'))); // phpcs:ignore WordPress.Security.EscapeOutput ?></label></p>
     170                    <p><label><?php esc_html_e('Icon size', 'subscription-system'); ?><br>
     171                        <?php echo $ss_select('icon_size', array('sm' => __('Small', 'subscription-system'), 'md' => __('Medium', 'subscription-system'), 'lg' => __('Large', 'subscription-system'))); // phpcs:ignore WordPress.Security.EscapeOutput ?></label></p>
     172                    <p><label><?php esc_html_e('Icon position', 'subscription-system'); ?><br>
     173                        <?php echo $ss_select('icon_position', array('left' => __('Before the text', 'subscription-system'), 'right' => __('At the right edge', 'subscription-system'))); // phpcs:ignore WordPress.Security.EscapeOutput ?></label></p>
     174                </fieldset>
     175
     176                <fieldset class="ss-design-group">
     177                    <legend><?php esc_html_e('Subscribe button', 'subscription-system'); ?></legend>
     178                    <p><label><?php esc_html_e('Style', 'subscription-system'); ?><br>
     179                        <?php echo $ss_select('button_style', array('solid' => __('Solid', 'subscription-system'), 'outline' => __('Outline', 'subscription-system'), 'soft' => __('Soft tint', 'subscription-system'))); // phpcs:ignore WordPress.Security.EscapeOutput ?></label></p>
     180                    <p><label><?php esc_html_e('Shape', 'subscription-system'); ?><br>
     181                        <?php echo $ss_select('button_shape', array('pill' => __('Pill', 'subscription-system'), 'rounded' => __('Rounded', 'subscription-system'), 'square' => __('Square', 'subscription-system'))); // phpcs:ignore WordPress.Security.EscapeOutput ?></label></p>
     182                    <p><label><?php esc_html_e('Size', 'subscription-system'); ?><br>
     183                        <?php echo $ss_select('button_size', array('sm' => __('Small', 'subscription-system'), 'md' => __('Medium', 'subscription-system'), 'lg' => __('Large', 'subscription-system'))); // phpcs:ignore WordPress.Security.EscapeOutput ?></label></p>
     184                    <p><?php echo $ss_check('button_full', __('Stretch to the card’s full width', 'subscription-system')); // phpcs:ignore WordPress.Security.EscapeOutput ?></p>
     185                </fieldset>
     186
     187                <fieldset class="ss-design-group">
     188                    <legend><?php esc_html_e('Display behavior', 'subscription-system'); ?></legend>
     189                    <p><label><?php esc_html_e('How the cards occupy the page', 'subscription-system'); ?><br>
     190                        <?php echo $ss_select('behavior', array( // phpcs:ignore WordPress.Security.EscapeOutput
     191                            'auto'       => __('Auto responsive (recommended)', 'subscription-system'),
     192                            'horizontal' => __('Horizontal row', 'subscription-system'),
     193                            'vertical'   => __('Vertical / stacked', 'subscription-system'),
     194                            'scroll'     => __('Scrollable strip', 'subscription-system'),
     195                            'carousel'   => __('Slider / carousel', 'subscription-system'),
     196                        )); ?></label></p>
     197                    <p class="description"><?php esc_html_e('Auto responsive lays cards out in a grid on large screens and switches to one swipeable card per view on phones.', 'subscription-system'); ?></p>
     198
     199                    <div class="ss-carousel-options" <?php echo ('carousel' === $ss['behavior']) ? '' : 'hidden'; ?>>
     200                        <p><label><?php esc_html_e('Cards visible at once (desktop)', 'subscription-system'); ?><br>
     201                            <?php echo $ss_range('carousel_visible', 1, 4, ''); // phpcs:ignore WordPress.Security.EscapeOutput ?></label></p>
     202                        <p><?php echo $ss_check('carousel_arrows', __('Show previous / next arrows', 'subscription-system')); // phpcs:ignore WordPress.Security.EscapeOutput ?></p>
     203                        <p><?php echo $ss_check('carousel_dots', __('Show dots', 'subscription-system')); // phpcs:ignore WordPress.Security.EscapeOutput ?></p>
     204                        <p><?php echo $ss_check('carousel_loop', __('Loop back to the first plan at the end', 'subscription-system')); // phpcs:ignore WordPress.Security.EscapeOutput ?></p>
     205                        <p><?php echo $ss_check('carousel_autoplay', __('Advance automatically', 'subscription-system')); // phpcs:ignore WordPress.Security.EscapeOutput ?></p>
     206                        <p><label><?php esc_html_e('Autoplay speed (milliseconds)', 'subscription-system'); ?><br>
     207                            <input type="number" name="ssl[carousel_speed]" min="1500" max="20000" step="500" value="<?php echo esc_attr($ss['carousel_speed']); ?>" class="small-text ss-design-control" data-key="carousel_speed"></label></p>
     208                        <p class="description"><?php esc_html_e('Swiping is always available on touch screens. Tablets show at most 2 cards, phones 1.', 'subscription-system'); ?></p>
     209                    </div>
     210                </fieldset>
     211
     212            </div>
     213        </div>
     214
     215        <p class="submit">
     216            <input type="submit" name="save_layout_settings" class="button button-primary" value="<?php esc_attr_e('Save Changes', 'subscription-system'); ?>">
     217        </p>
     218
    85219        <!-- Layout Previews -->
    86220        <div class="layout-previews-section">
    87221            <h2><?php echo esc_html__('Layout Previews', 'subscription-system'); ?></h2>
    88             <p><?php echo esc_html__('Current default layout:', 'subscription-system'); ?> <strong><?php echo esc_html(ucfirst($default_layout)); ?></strong></p>
    89            
     222            <p><?php echo esc_html__('Current default layout:', 'subscription-system'); ?> <strong><?php echo esc_html(ucfirst($default_layout)); ?></strong>
     223               <span class="description"><?php esc_html_e('— previews repaint as you change the options above.', 'subscription-system'); ?></span></p>
     224
    90225            <div class="layout-tabs">
    91                 <div class="layout-tab <?php echo ($default_layout === 'layout1') ? 'active' : ''; ?>" data-layout="layout1"><?php echo esc_html__('Layout 1', 'subscription-system'); ?></div>
    92                 <div class="layout-tab <?php echo ($default_layout === 'layout2') ? 'active' : ''; ?>" data-layout="layout2"><?php echo esc_html__('Layout 2', 'subscription-system'); ?></div>
    93                 <div class="layout-tab <?php echo ($default_layout === 'layout3') ? 'active' : ''; ?>" data-layout="layout3"><?php echo esc_html__('Layout 3', 'subscription-system'); ?></div>
     226                <div class="layout-tab <?php echo ($default_layout === 'layout1') ? 'active' : ''; ?>" data-layout="layout1"><?php echo esc_html__('Layout 1 · Classic', 'subscription-system'); ?></div>
     227                <div class="layout-tab <?php echo ($default_layout === 'layout2') ? 'active' : ''; ?>" data-layout="layout2"><?php echo esc_html__('Layout 2 · Ledger', 'subscription-system'); ?></div>
     228                <div class="layout-tab <?php echo ($default_layout === 'layout3') ? 'active' : ''; ?>" data-layout="layout3"><?php echo esc_html__('Layout 3 · Spotlight', 'subscription-system'); ?></div>
    94229            </div>
    95            
     230
    96231            <?php
    97232            // Sample plans used by all three previews below.
    … …  
    101236                array('name' => 'PREMIUM', 'price' => '49.99', 'featured' => false, 'features' => array('50GB Disk Space', '50 Domain Names', '20 E-Mail Address', '300GB Monthly Bandwidth', 'Fully Support')),
    102237            );
    103             // Markup below is deliberately identical to what each
    104             // public/partials/pricing-plans-layoutN.php template renders,
    105             // wrapped in the same .subscription-pricing-plans.layoutN
    106             // class the real CSS files are scoped to — so this preview is
    107             // styled by the exact same stylesheet the front end uses,
    108             // not a hand-copied approximation of it.
    109             $cards_markup = function ($plans, $button_class) {
     238            // Markup below is deliberately identical to what
     239            // components/pricing-plan-card.php renders on the front end,
     240            // wrapped in the same classes the real CSS files are scoped to —
     241            // so this preview is styled by the exact stylesheets the visitor
     242            // sees, not a hand-copied approximation of them.
     243            $cards_markup = function ($plans) {
    110244                ob_start();
    111245                foreach ($plans as $plan) :
    … …  
    113247                ?>
    114248                    <div class="ss-pricing-plan <?php echo esc_attr($featured_class); ?>">
     249                        <?php if ($plan['featured']) { echo wp_kses_post(Subs_Sys_Layout_Settings::badge_html()); } ?>
    115250                        <div class="ss-plan-header">
    116251                            <h3><?php echo esc_html($plan['name']); ?></h3>
    … …  
    125260                        </div>
    126261                        <div class="ss-plan-footer">
    127                             <a href="#" class="<?php echo esc_attr($button_class); ?>" onclick="return false;">Order Now</a>
     262                            <a href="#" class="ss-button" onclick="return false;">Order Now</a>
    128263                        </div>
    129264                    </div>
    … …  
    133268            ?>
    134269
    135             <div class="layout-preview layout1-preview <?php echo ($default_layout === 'layout1') ? 'active' : ''; ?>">
    136                 <h3><?php echo esc_html__('Layout 1 Preview', 'subscription-system'); ?></h3>
    137                 <div class="subscription-pricing-plans layout1">
    138                     <div class="ss-pricing-container ss-layout1">
    139                         <div class="ss-pricing-plans">
    140                             <?php echo $cards_markup($sample_plans, 'ss-button'); ?>
     270            <?php foreach (array('layout1', 'layout2', 'layout3') as $ss_preview_layout) : ?>
     271                <div class="layout-preview <?php echo esc_attr($ss_preview_layout); ?>-preview <?php echo ($default_layout === $ss_preview_layout) ? 'active' : ''; ?>">
     272                    <div class="subscription-pricing-plans <?php echo esc_attr($ss_preview_layout); ?> ss-responsive <?php echo esc_attr($ss_wrapper_classes); ?> ss-admin-preview">
     273                        <div class="ss-pricing-container ss-<?php echo esc_attr($ss_preview_layout); ?>">
     274                            <div class="ss-pricing-plans">
     275                                <?php echo $cards_markup($sample_plans); // phpcs:ignore WordPress.Security.EscapeOutput -- built from escaped parts above. ?>
     276                            </div>
    141277                        </div>
    142278                    </div>
    143279                </div>
    144             </div>
    145 
    146             <div class="layout-preview layout2-preview <?php echo ($default_layout === 'layout2') ? 'active' : ''; ?>">
    147                 <h3><?php echo esc_html__('Layout 2 Preview', 'subscription-system'); ?></h3>
    148                 <div class="subscription-pricing-plans layout2">
    149                     <div class="ss-pricing-container ss-layout2">
    150                         <?php echo $cards_markup($sample_plans, 'ss-button'); ?>
    151                     </div>
    152                 </div>
    153             </div>
    154 
    155             <div class="layout-preview layout3-preview <?php echo ($default_layout === 'layout3') ? 'active' : ''; ?>">
    156                 <h3><?php echo esc_html__('Layout 3 Preview', 'subscription-system'); ?></h3>
    157                 <div class="subscription-pricing-plans layout3">
    158                     <div class="ss-pricing-container ss-layout3">
    159                         <?php echo $cards_markup($sample_plans, 'ss-pricing-button'); ?>
    160                     </div>
    161                 </div>
    162             </div>
     280            <?php endforeach; ?>
     281            <p class="description ss-carousel-preview-note" hidden><?php esc_html_e('Carousel arrows, dots and autoplay run on the live site; the preview shows the strip.', 'subscription-system'); ?></p>
    163282        </div>
    164283    </form>
    … …  
    166285
    167286<style>
     287/* Design option groups */
     288.ss-design-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; max-width: 1400px; }
     289.ss-design-group { background: #fff; border: 1px solid var(--ss-border, #E1E6EA); border-radius: 8px; padding: 4px 18px 12px; margin: 0; }
     290.ss-design-group legend { font-weight: 600; padding: 0 6px; }
     291.ss-design-group label { font-weight: 500; }
     292.ss-design-group .description { color: #667085; }
     293.ss-range-wrap { display: inline-flex; align-items: center; gap: 8px; }
     294.ss-range-wrap input[type="range"] { width: 140px; }
     295.ss-range-wrap output { min-width: 2ch; font-variant-numeric: tabular-nums; font-weight: 600; }
     296.ss-carousel-options { border-top: 1px dashed var(--ss-border, #E1E6EA); margin-top: 10px; padding-top: 6px; }
     297
    168298/* Layout Preview Styles */
    169 .layout-previews-section {
    170     margin-top: 30px;
    171 }
    172 
    173 .layout-tabs {
    174     display: flex;
    175     margin-bottom: 0;
    176     border-bottom: 1px solid var(--ss-border);
    177 }
     299.layout-previews-section { margin-top: 30px; }
     300
     301.layout-tabs { display: flex; margin-bottom: 0; border-bottom: 1px solid var(--ss-border); }
    178302
    179303.layout-tab {
    … …  
    189313}
    190314
    191 .layout-tab:hover {
    192     background-color: var(--ss-bg);
    193 }
    194 
    195315.layout-tab.active {
    196316    background-color: var(--ss-surface);
    … …  
    204324    display: none;
    205325    padding: 30px;
    206     background-color: var(--ss-surface);
     326    background-color: var(--ss-bg);
    207327    border: 1px solid var(--ss-border);
    208328    border-top: none;
    … …  
    211331}
    212332
    213 .layout-preview.active {
    214     display: block;
    215 }
    216 
    217 .layout-preview h3 {
    218     margin-top: 0;
    219     margin-bottom: 20px;
    220     font-size: 18px;
    221     color: var(--ss-ink);
    222     font-weight: 500;
    223 }
    224 
     333.layout-preview.active { display: block; }
     334
     335/* Give badge pills room above the cards inside the preview boxes. */
     336.layout-preview .subscription-pricing-plans { padding-top: 10px; }
    225337</style>
    226338
    227339<script>
    228340jQuery(document).ready(function($) {
    229     // Switch tabs
     341    // ── Preview tabs ────────────────────────────────────────────────────
    230342    $('.layout-tab').on('click', function() {
    231343        var layout = $(this).data('layout');
    232        
    233         // Update tabs
    234344        $('.layout-tab').removeClass('active');
    235345        $(this).addClass('active');
    236        
    237         // Update previews
    238346        $('.layout-preview').removeClass('active');
    239347        $('.' + layout + '-preview').addClass('active');
    240        
    241         // Update select
    242348        $('#default-layout').val(layout);
    243349    });
    244    
    245     // Update active tab when changing default layout select
     350
    246351    $('#default-layout').on('change', function() {
    247352        var layout = $(this).val();
    248353        $('.layout-tab').removeClass('active');
    249354        $('.layout-tab[data-layout="' + layout + '"]').addClass('active');
    250        
    251355        $('.layout-preview').removeClass('active');
    252356        $('.' + layout + '-preview').addClass('active');
    253357    });
     358
     359    // ── Live design preview ─────────────────────────────────────────────
     360    // Mirrors Subs_Sys_Layout_Settings::inline_css() + wrapper_classes():
     361    // controls repaint the preview wrappers without a round trip.
     362    var previews = $('.ss-admin-preview');
     363
     364    var shadows = {
     365        none:   ['none', 'none'],
     366        soft:   ['0 4px 14px rgba(17,24,39,0.07)',  '0 10px 24px rgba(17,24,39,0.11)'],
     367        medium: ['0 8px 28px rgba(17,24,39,0.12)',  '0 16px 40px rgba(17,24,39,0.16)'],
     368        bold:   ['0 16px 44px rgba(17,24,39,0.18)', '0 24px 60px rgba(17,24,39,0.22)']
     369    };
     370    var paddings  = { compact: '16px', cozy: '24px', spacious: '34px' };
     371    var iconSizes = { sm: '14px', md: '17px', lg: '21px' };
     372    var btnPad    = { sm: '8px 18px', md: '12px 26px', lg: '16px 34px' };
     373    var btnFont   = { sm: '12px', md: '14px', lg: '16px' };
     374    var btnRadius = { pill: '999px', rounded: '8px', square: '0px' };
     375
     376    var classGroups = {
     377        card_bg:       'ss-card-bg-',
     378        hover_effect:  'ss-hover-',
     379        align:         'ss-align-',
     380        icon_style:    'ss-icon-',
     381        icon_position: 'ss-icon-pos-',
     382        button_style:  'ss-btn-',
     383        badge_style:   'ss-badge-',
     384        behavior:      'ss-behave-'
     385    };
     386    // ss-icon-pos-* must be swapped before ss-icon-* could eat its prefix,
     387    // so each group strips only its own exact classes:
     388    var classValues = {
     389        card_bg:       ['surface', 'tint', 'transparent'],
     390        hover_effect:  ['none', 'lift', 'glow', 'scale'],
     391        align:         ['auto', 'left', 'center'],
     392        icon_style:    ['check', 'arrow', 'star', 'dot', 'none'],
     393        icon_position: ['left', 'right'],
     394        button_style:  ['solid', 'outline', 'soft'],
     395        badge_style:   ['auto', 'pill', 'ribbon', 'stripe'],
     396        behavior:      ['auto', 'horizontal', 'vertical', 'scroll', 'carousel']
     397    };
     398
     399    function setVar(name, value) {
     400        previews.each(function() { this.style.setProperty(name, value); });
     401    }
     402
     403    function swapClass(group, value) {
     404        var prefix = classGroups[group];
     405        classValues[group].forEach(function(v) { previews.removeClass(prefix + v); });
     406        previews.addClass(prefix + value);
     407    }
     408
     409    function apply(key, control) {
     410        var $c = $(control);
     411        var value = control.type === 'checkbox' ? ($c.prop('checked') ? '1' : '0') : String($c.val());
     412
     413        switch (key) {
     414            case 'card_shadow':
     415                setVar('--ssl-shadow', shadows[value][0]);
     416                setVar('--ssl-shadow-hover', shadows[value][1]);
     417                break;
     418            case 'card_border':   setVar('--ssl-border-w', value + 'px'); break;
     419            case 'card_radius':   setVar('--ssl-radius', value + 'px'); $c.siblings('output').text(value); break;
     420            case 'card_gap':      setVar('--ssl-gap', value + 'px'); $c.siblings('output').text(value); break;
     421            case 'card_padding':  setVar('--ssl-pad', paddings[value]); break;
     422            case 'icon_size':     setVar('--ssl-icon-size', iconSizes[value]); break;
     423            case 'button_size':
     424                setVar('--ssl-btn-pad', btnPad[value]);
     425                setVar('--ssl-btn-font', btnFont[value]);
     426                break;
     427            case 'button_shape':  setVar('--ssl-btn-radius', btnRadius[value]); break;
     428            case 'badge_color':   setVar('--ssl-badge-bg', value === 'primary' ? 'var(--ss-primary)' : 'var(--ss-accent)'); break;
     429            case 'badge_text':    $('.ss-admin-preview .ss-plan-badge').text(value); break;
     430            case 'badge_enabled': $('.ss-admin-preview .ss-plan-badge').toggle(value === '1'); break;
     431            case 'button_full':   previews.toggleClass('ss-btn-full', value === '1'); break;
     432            case 'carousel_visible': $c.siblings('output').text(value); break;
     433            case 'behavior':
     434                swapClass(key, value);
     435                $('.ss-carousel-options').prop('hidden', value !== 'carousel');
     436                $('.ss-carousel-preview-note').prop('hidden', value !== 'carousel');
     437                break;
     438            default:
     439                if (classGroups[key]) { swapClass(key, value); }
     440        }
     441    }
     442
     443    $('.ss-design-control').on('input change', function() {
     444        apply($(this).data('key'), this);
     445    });
    254446});
    255 </script> 
     447</script>
  • subscription-system/trunk/includes/class-subs-sys-access.php

    r3650727 r3651653  
    22/**
    33 * Centralized access-control API — the single place every shortcode, template,
    4  * the paywall content filter, and AJAX handler asks "can this user see this?"
     4 * and AJAX handler asks "can this user see this?"
    55 * instead of each re-deriving its own version of that logic (README §10.4).
    66 *
  • subscription-system/trunk/includes/class-subs-sys-checkout.php

    r3650727 r3651653  
    22/**
    33 * The one checkout entry point. Every current call site that builds a Stripe
    4  * Checkout Session (the [pricing_plans] AJAX handlers, the paywall CTA, and any
    5  * future entry point) calls Subs_Sys_Checkout::start() instead of each
     4 * Checkout Session (the [pricing_plans] AJAX handlers, and any future entry
     5 * point) calls Subs_Sys_Checkout::start() instead of each
    66 * constructing its own session with copy-pasted, sometimes-hardcoded logic
    77 * (README §6.2).
  • subscription-system/trunk/includes/class-subs-sys-db-updater.php

    r3650727 r3651653  
    3131         * Current database version
    3232         */
    33         const DB_VERSION = '1.6.0';
     33        const DB_VERSION = '1.6.1';
    3434
    3535        /**
    … …  
    312312                                Subs_Sys_Content_Schema::create_tables();
    313313                        }
     314                }
     315
     316                // Paywall removal (1.6.1): the "This content is for subscribers" content
     317                // gate no longer exists, so sites upgrading from a version that had it
     318                // must not keep its orphaned setting around.
     319                if ( version_compare( $old_version, '1.6.1', '<' ) ) {
     320                        delete_option( 'ss_paywall_teaser_word_count' );
    314321                }
    315322
  • subscription-system/trunk/includes/class-subs-sys-shortcodes.php

    r3650727 r3651653  
    139139                'columns' => '3',
    140140                'orientation' => 'vertical', // New attribute for orientation
     141                'behavior' => '', // auto|horizontal|vertical|scroll|carousel — overrides the saved display behavior
    141142                'filters' => '', // "yes" renders the category filter bar
    142143            ),
    … …  
    213214        // Add horizontal class if orientation is horizontal
    214215        $orientation_class = ($atts['orientation'] === 'horizontal') ? 'horizontal' : '';
     216
     217        // Per-shortcode display behavior. The legacy orientation="horizontal"
     218        // attribute maps onto the horizontal behavior so old embeds keep
     219        // their meaning under the new behavior system.
     220        $ss_behavior = strtolower(trim((string) $atts['behavior']));
     221        if ('' === $ss_behavior && 'horizontal' === $atts['orientation']) {
     222            $ss_behavior = 'horizontal';
     223        }
    215224       
    216225        // Include the appropriate template based on layout
  • subscription-system/trunk/includes/class-subs-sys-table-registry.php

    r3650727 r3651653  
    5757                                'ss_subscriptions'               => array( 'status' => 'active', 'feature' => __( 'Site subscriptions (unified)', 'subscription-system' ), 'note' => __( 'Written by checkout/webhooks; read by access control and the account page.', 'subscription-system' ) ),
    5858                                'ss_orders'                      => array( 'status' => 'active', 'feature' => __( 'Orders (unified)', 'subscription-system' ), 'note' => __( 'Payment records from the unified checkout.', 'subscription-system' ) ),
    59                                 'ss_entitlements'                => array( 'status' => 'active', 'feature' => __( 'Access entitlements', 'subscription-system' ), 'note' => __( 'What each subscription unlocks; read by the paywall.', 'subscription-system' ) ),
     59                                'ss_entitlements'                => array( 'status' => 'active', 'feature' => __( 'Access entitlements', 'subscription-system' ), 'note' => __( 'What each subscription unlocks; read by access checks.', 'subscription-system' ) ),
    6060                                'ss_entitlement_rules'           => array( 'status' => 'active', 'feature' => __( 'Entitlement rules', 'subscription-system' ), 'note' => __( 'Which content each plan gates.', 'subscription-system' ) ),
    6161                                'ss_webhook_events'              => array( 'status' => 'active', 'feature' => __( 'Stripe webhook idempotency ledger', 'subscription-system' ), 'note' => __( 'Stops a replayed webhook from acting twice.', 'subscription-system' ) ),
  • subscription-system/trunk/includes/class-subs-sys.php

    r3650727 r3651653  
    223223        require_once __DIR__ . '/class-subs-sys-colors.php';
    224224
     225        // Pricing layout design settings (badge, card chrome, display behavior) —
     226        // read by the pricing templates, the Pricing Settings screen, and the
     227        // public side that prints the --ssl-* variables.
     228        require_once __DIR__ . '/class-subs-sys-layout-settings.php';
     229
    225230        // Auto-attached Login/Register items in the site navigation.
    226231        require_once __DIR__ . '/class-subs-sys-menu.php';
    … …  
    257262        require_once __DIR__ . '/class-subs-sys-reconciler.php';
    258263        Subs_Sys_Reconciler::init();
    259 
    260         /**
    261          * The content paywall - the plugin's core stated purpose, previously absent
    262          * from the codebase entirely.
    263          */
    264         require_once __DIR__ . '/class-subs-sys-paywall.php';
    265264
    266265        /**
    … …  
    411410                $plugin_webhook = new Subs_Sys_Webhook();
    412411
    413                 // The single unified webhook endpoint + the content paywall.
     412                // The single unified webhook endpoint.
    414413                $webhook_v2 = new Subs_Sys_Webhook_V2();
    415                 $paywall    = new Subs_Sys_Paywall();
    416414
    417415                // Keep ss_entitlements in sync whenever a subscription's status changes
    418416                // (fired by Subs_Sys_Webhook_V2's event handlers).
    419417                $this->loader->add_action( 'ss_subscription_status_changed', $this, 'on_subscription_status_changed', 10, 3 );
    420 
    421                 // Single checkout entry point for the paywall CTA.
    422                 $this->loader->add_action( 'wp_ajax_ss_start_checkout', $this, 'ajax_start_checkout' );
    423418
    424419                // Initialize content subscription manager
    … …  
    488483                        Subs_Sys_Access::refresh_entitlements_for_user( (int) $user_id );
    489484                }
    490         }
    491 
    492         /**
    493          * AJAX: start a Stripe Checkout Session for a plan price (the paywall CTA's
    494          * "Subscribe to unlock" button).
    495          */
    496         public function ajax_start_checkout() {
    497                 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'ss_paywall_checkout' ) ) {
    498                         wp_send_json_error( array( 'message' => __( 'Security check failed.', 'subscription-system' ) ) );
    499                         return;
    500                 }
    501                 if ( ! is_user_logged_in() ) {
    502                         wp_send_json_error( array( 'message' => __( 'Please log in to subscribe.', 'subscription-system' ) ) );
    503                         return;
    504                 }
    505 
    506                 $plan_price_id = isset( $_POST['plan_price_id'] ) ? absint( $_POST['plan_price_id'] ) : 0;
    507                 if ( ! $plan_price_id ) {
    508                         wp_send_json_error( array( 'message' => __( 'Invalid plan.', 'subscription-system' ) ) );
    509                         return;
    510                 }
    511 
    512                 $context = array();
    513                 if ( ! empty( $_POST['return_post_id'] ) ) {
    514                         $post_id                       = absint( $_POST['return_post_id'] );
    515                         $context['return_post_id']     = $post_id;
    516                         $context['success_url']        = get_permalink( $post_id ) ?: home_url( '/' );
    517                         $context['cancel_url']         = get_permalink( $post_id ) ?: home_url( '/' );
    518                 }
    519 
    520                 $result = Subs_Sys_Checkout::start( get_current_user_id(), $plan_price_id, $context );
    521 
    522                 if ( is_wp_error( $result ) ) {
    523                         wp_send_json_error( array( 'message' => $result->get_error_message() ) );
    524                         return;
    525                 }
    526 
    527                 wp_send_json_success( array( 'url' => $result['url'] ) );
    528485        }
    529486
  • subscription-system/trunk/public/class-subs-sys-public.php

    r3650727 r3651653  
    8686        if ( $color_overrides ) {
    8787            wp_add_inline_style( $this->plugin_name . '-tokens', $color_overrides );
     88        }
     89
     90        // Pricing layout design settings (--ssl-* chrome variables) print in
     91        // the same slot, so every pricing stylesheet can rely on them.
     92        if ( class_exists( 'Subs_Sys_Layout_Settings' ) ) {
     93            wp_add_inline_style( $this->plugin_name . '-tokens', Subs_Sys_Layout_Settings::inline_css() );
    8894        }
    8995
  • subscription-system/trunk/public/css/interest-subscribe.css

    r3650727 r3651653  
    276276}
    277277
     278/* Feedback alert: hidden until it has a state, then a full-width tinted
     279 * panel with an icon — not a bare line of colored text a reader can miss. */
    278280.ss-interest-message {
    279         min-height: 20px;
    280         margin-top: 8px;
    281         font-size: 14px;
     281        display: none;
     282        align-items: flex-start;
     283        gap: 10px;
     284        margin-top: 14px;
     285        padding: 12px 14px;
     286        border-radius: 8px;
     287        border: 1px solid transparent;
     288        font-size: 13px;
     289        font-weight: 500;
     290        line-height: 1.5;
     291        animation: ss-interest-msg-in 0.25s ease;
     292}
     293
     294.ss-interest-message.is-error,
     295.ss-interest-message.is-success {
     296        display: flex;
     297}
     298
     299.ss-interest-message::before {
     300        flex: 0 0 auto;
     301        width: 18px;
     302        height: 18px;
     303        margin-top: 1px;
     304        border-radius: 50%;
     305        display: inline-flex;
     306        align-items: center;
     307        justify-content: center;
     308        font-size: 11px;
     309        font-weight: 700;
     310        line-height: 1;
     311        color: #fff;
     312}
     313
     314.ss-interest-message.is-success {
     315        background: var(--ss-success-tint);
     316        border-color: var(--ss-success);
     317        color: var(--ss-success);
     318}
     319
     320.ss-interest-message.is-success::before {
     321        content: "\2713"; /* ✓ */
     322        background: var(--ss-success);
    282323}
    283324
    284325.ss-interest-message.is-error {
     326        background: var(--ss-danger-tint);
     327        border-color: var(--ss-danger);
    285328        color: var(--ss-danger);
    286329}
    287330
    288 .ss-interest-message.is-success {
    289         color: var(--ss-success);
     331.ss-interest-message.is-error::before {
     332        content: "!";
     333        background: var(--ss-danger);
     334}
     335
     336@keyframes ss-interest-msg-in {
     337        from {
     338                opacity: 0;
     339                transform: translateY(4px);
     340        }
     341        to {
     342                opacity: 1;
     343                transform: none;
     344        }
     345}
     346
     347@media (prefers-reduced-motion: reduce) {
     348        .ss-interest-message {
     349                animation: none;
     350        }
    290351}
    291352
    … …  
    357418        opacity: 0.65;
    358419        cursor: not-allowed;
     420}
     421
     422/* Success state after the subscription completes — the button confirms the
     423 * outcome instead of sitting on its "Setting up…" busy label. !important so
     424 * it outranks the theme-armor rule painting the primary button. */
     425.ss-interest-btn--primary.ss-interest-btn--done,
     426.ss-interest-btn--primary.ss-interest-btn--done:disabled {
     427        background: var(--ss-success) !important;
     428        color: #fff !important;
     429        opacity: 1;
     430        cursor: default;
    359431}
    360432
  • subscription-system/trunk/public/css/pricing-layout1.css

    r3650727 r3651653  
    11/**
    2  * Pricing Layout 1 - Clean Modern Style
     2 * Pricing Layout 1 — "Classic"
    33 *
    4  * This is the ONLY stylesheet for this layout — the template
    5  * (pricing-plans-layout1.php) used to also echo a duplicate inline
    6  * <style> block after this file's <link>, which fought it for control
    7  * of .ss-pricing-plan's background (both rules used !important at tied
    8  * specificity, so the later-in-document inline block always won,
    9  * making every card render transparent instead of white). That inline
    10  * block has been removed; its design (rounded pill buttons, hover
    11  * lift) has been merged in here instead, so there is exactly one rule
    12  * that can win for any given property.
     4 * Centered floating cards: uppercase plan name, big tabular price over a
     5 * hairline divider, check list, full-width CTA. The badge (settings-driven,
     6 * see pricing-shared.css) floats through the featured card's top edge.
     7 *
     8 * Card chrome (radius, border, shadow, padding, gap), icons, buttons, badge
     9 * and display behaviors all come from pricing-shared.css + the --ssl-* vars;
     10 * this file only supplies Layout 1's own structure and type rhythm.
    1311 */
    1412
    15 *,
    16 *::before,
    17 *::after {
    18     box-sizing: border-box;
    19 }
    20 
    21 /* Container and base layout */
    2213.subscription-pricing-plans.layout1 {
    2314    padding: 40px 20px;
    2415    background: transparent !important;
    25     min-height: auto;
    26     display: flex;
    27     align-items: center;
    28     justify-content: center;
    2916}
    3017
    … …  
    4027    display: grid;
    4128    grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
    42     align-items: start;
     29    align-items: stretch;
    4330    justify-content: center;
    4431    max-width: 1200px;
    4532    width: 100%;
    4633    margin: 0 auto;
    47     gap: 20px;
    4834}
    4935
    50 /* Individual plan cards — size to their own content (no forced
    51  * equal-height stretch across a row). */
    5236.subscription-pricing-plans.layout1 .ss-pricing-plan {
    53     max-width: 350px;
     37    max-width: 360px;
    5438    min-width: 0;
    5539    width: 100%;
    5640    justify-self: center;
    57     background-color: var(--ss-surface);
    58     border-radius: 8px;
    59     box-shadow: 0 5px 15px rgba(24, 38, 32, 0.08);
    60     overflow: hidden;
    61     transition: all 0.3s ease;
    62     border: 1px solid var(--ss-border);
    63     position: relative;
    6441}
    6542
    66 .subscription-pricing-plans.layout1 .ss-pricing-plan:hover {
    67     transform: translateY(-5px);
    68     box-shadow: 0 8px 25px rgba(24, 38, 32, 0.12);
     43/* Featured plan: keyline in the brand color, gently raised. */
     44.subscription-pricing-plans.layout1 .ss-pricing-plan.ss-featured {
     45    border-color: var(--ss-primary);
     46    border-width: max(var(--ssl-border-w, 1px), 2px);
    6947}
    7048
    71 /* Featured plan styling — gold is reserved for exactly this moment. */
    72 .subscription-pricing-plans.layout1 .ss-pricing-plan.ss-featured {
    73     border: 2px solid var(--ss-accent);
    74     position: relative;
    75 }
    76 
    77 .subscription-pricing-plans.layout1 .ss-pricing-plan.ss-featured::after {
    78     content: 'POPULAR';
    79     position: absolute;
    80     top: 15px;
    81     right: 15px;
    82     background-color: var(--ss-accent);
    83     color: white;
    84     padding: 5px 10px;
    85     font-size: 12px;
    86     border-radius: 20px;
    87     font-weight: bold;
    88 }
    89 
    90 /* Plan header */
     49/* Header */
    9150.subscription-pricing-plans.layout1 .ss-plan-header {
    92     padding: 25px 20px;
     51    padding: calc(var(--ssl-pad, 24px) + 6px) var(--ssl-pad, 24px) calc(var(--ssl-pad, 24px) - 4px);
    9352    text-align: center;
    9453    border-bottom: 1px solid var(--ss-border);
    95     position: relative;
    9654}
    9755
    9856.subscription-pricing-plans.layout1 .ss-plan-header h3 {
    9957    margin: 0;
    100     font-size: 20px;
    101     font-weight: 600;
     58    font-size: 14px;
     59    font-weight: 700;
    10260    text-transform: uppercase;
    103     letter-spacing: 1px;
     61    letter-spacing: 0.14em;
    10462    color: var(--ss-ink);
    10563}
    … …  
    11068
    11169.subscription-pricing-plans.layout1 .ss-price {
    112     font-size: 34px;
    113     font-weight: bold;
    114     margin-top: 10px;
    115     color: var(--ss-primary);
     70    margin-top: 12px;
     71    font-size: 40px;
     72    font-weight: 800;
     73    line-height: 1;
     74    letter-spacing: -0.02em;
     75    font-variant-numeric: tabular-nums;
     76    color: var(--ss-ink);
    11677}
    11778
    11879.subscription-pricing-plans.layout1 .ss-price span {
    119     font-size: 16px;
    120     font-weight: normal;
    121     opacity: 0.8;
     80    font-size: 15px;
     81    font-weight: 500;
     82    letter-spacing: 0;
     83    color: var(--ss-muted);
    12284}
    12385
    124 /* Plan features */
     86/* Features */
    12587.subscription-pricing-plans.layout1 .ss-plan-features {
    126     padding: 0;
    127 }
    128 
    129 .subscription-pricing-plans.layout1 .ss-plan-features ul {
    130     list-style: none;
    131     padding: 0;
    132     margin: 0;
     88    padding: calc(var(--ssl-pad, 24px) - 8px) var(--ssl-pad, 24px);
    13389}
    13490
    13591.subscription-pricing-plans.layout1 .ss-plan-features li {
    136     padding: 15px 20px;
    137     border-bottom: 1px solid var(--ss-border);
    138     text-align: center;
     92    padding: 8px 0;
    13993    font-size: 14px;
    140     color: var(--ss-body);
    14194}
    14295
    143 /* Plan footer and button */
     96/* Footer */
    14497.subscription-pricing-plans.layout1 .ss-plan-footer {
    145     padding: 25px;
     98    padding: calc(var(--ssl-pad, 24px) - 6px) var(--ssl-pad, 24px) var(--ssl-pad, 24px);
    14699    text-align: center;
    147     background-color: transparent;
    148100}
    149101
    150 .subscription-pricing-plans.layout1 .ss-button,
    151 .subscription-pricing-plans.layout1 .ss-pricing-button {
    152     display: inline-block;
    153     padding: 12px 30px;
    154     border-radius: 50px;
    155     text-decoration: none;
    156     font-weight: 600;
    157     transition: all 0.3s ease;
    158     font-size: 15px;
    159     letter-spacing: 0.5px;
    160     text-transform: uppercase;
    161     background-color: var(--ss-primary);
    162     color: white;
    163     border: none;
    164     cursor: pointer;
    165 }
    166 
    167 .subscription-pricing-plans.layout1 .ss-button:hover,
    168 .subscription-pricing-plans.layout1 .ss-pricing-button:hover {
    169     transform: translateY(-2px);
    170     box-shadow: 0 5px 15px rgba(24, 38, 32, 0.15);
    171     background-color: var(--ss-primary-dark);
    172 }
    173 
    174 /* Responsive Design
    175  * Card width adapts to the container; no forced column count.
    176 @media (max-width: 992px) {
    177     .subscription-pricing-plans.layout1 .ss-pricing-plan {
    178         width: 100%;
    179         max-width: 450px;
    180     }
    181 }
    182 
     102/* Responsive type + spacing. Structure changes (the mobile swipe strip,
     103 * stacking, carousels) are handled by the behavior classes in
     104 * pricing-shared.css. */
    183105@media (max-width: 768px) {
    184106    .subscription-pricing-plans.layout1 {
    … …  
    186108    }
    187109
    188     .subscription-pricing-plans.layout1 .ss-pricing-plan {
    189         max-width: 100%;
    190         margin: 10px 0;
    191     }
    192 
    193     .subscription-pricing-plans.layout1 .ss-plan-header h3 {
    194         font-size: 1.5rem;
    195     }
    196 
    197110    .subscription-pricing-plans.layout1 .ss-price {
    198         font-size: 1.8rem;
    199     }
    200 
    201     .subscription-pricing-plans.layout1 .ss-price span {
    202         font-size: 0.9rem;
    203     }
    204 
    205     .subscription-pricing-plans.layout1 .ss-plan-features li {
    206         font-size: 0.9rem;
    207         padding: 8px 10px;
     111        font-size: 34px;
    208112    }
    209113}
    210 
    211 @media (max-width: 480px) {
    212     .subscription-pricing-plans.layout1 .ss-plan-header h3 {
    213         font-size: 1.3rem;
    214     }
    215 
    216     .subscription-pricing-plans.layout1 .ss-price {
    217         font-size: 1.6rem;
    218     }
    219 }
    220 
    221 /* Feature list: check marks + tighter rhythm, so a one-line plan
    222  * doesn't render as a mostly-empty panel. */
    223 .subscription-pricing-plans.layout1 .ss-plan-features {
    224     padding: 6px 0;
    225 }
    226 
    227 .subscription-pricing-plans.layout1 .ss-plan-features li {
    228     display: flex;
    229     align-items: flex-start;
    230     gap: 10px;
    231     text-align: left;
    232     padding: 10px 24px;
    233     border-bottom: none;
    234     line-height: 1.5;
    235 }
    236 
    237 .subscription-pricing-plans.layout1 .ss-plan-features li::before {
    238     content: "✓";
    239     color: var(--ss-primary);
    240     font-weight: 700;
    241     line-height: 1.4;
    242     flex-shrink: 0;
    243 }
    244 
    245 .subscription-pricing-plans.layout1 .ss-plan-footer {
    246     padding: 18px 24px 26px;
    247 }
    248 
    249 .subscription-pricing-plans.layout1 .ss-button,
    250 .subscription-pricing-plans.layout1 .ss-pricing-button {
    251     width: 100%;
    252 }
    253 
    254 /* "Current Plan" — a plan the logged-in user already owns. Disabled, so they
    255    can see they have it and cannot buy it twice. */
    256 .ss-button--owned,
    257 .ss-button--owned:hover {
    258         background: var(--ss-success-tint, #d8f2e0) !important;
    259         color: var(--ss-success, #14603a) !important;
    260         border: 1px solid var(--ss-success, #86c9a3) !important;
    261         cursor: not-allowed;
    262         box-shadow: none !important;
    263         transform: none !important;
    264 }
  • subscription-system/trunk/public/css/pricing-layout2.css

    r3650727 r3651653  
    11/**
    2  * Pricing Layout 2 - Elegant Style
     2 * Pricing Layout 2 — "Ledger"
    33 *
    4  * This is the ONLY stylesheet for this layout — the template
    5  * (pricing-plans-layout2.php) used to also echo a duplicate inline
    6  * <style> block after this file's <link>. Because the inline block's
    7  * base-card rule used a shorter selector than this file's, it lost for
    8  * ordinary cards but WON for the featured card (tied specificity,
    9  * later in the document) — so only the "Popular" card rendered
    10  * transparent instead of white. That inline block has been removed
    11  * entirely; there is exactly one rule that can win for any property.
     4 * A single flat, hairline-ruled sheet: the plans are columns of one
     5 * continuous panel rather than three floating cards. Left-aligned editorial
     6 * type — small tracked eyebrow for the plan name, a large baseline-set
     7 * price, thin rules between features. The featured plan is a fully
     8 * inverted column in the brand color.
     9 *
     10 * The sheet only holds together in the grid behaviors; when a display
     11 * behavior turns the set into a strip, stack or carousel, each column
     12 * gets its own card chrome back (see the overrides at the bottom).
    1213 */
    1314
    14 *,
    15 *::before,
    16 *::after {
    17     box-sizing: border-box;
    18 }
    19 
    2015.subscription-pricing-plans.layout2 {
     16    padding: 40px 20px;
    2117    background: transparent !important;
    22     padding: 40px 20px;
    2318}
    2419
    … …  
    2722    justify-content: center;
    2823    width: 100%;
    29     padding: 20px 0;
    3024    background: transparent !important;
    3125}
    3226
    33 .subscription-pricing-plans.layout2 .ss-pricing-plans,
    34 .subscription-pricing-plans.layout2 .ss-pricing-container {
     27/* The sheet: one bordered panel; the cards inside are flat columns. */
     28.subscription-pricing-plans.layout2 .ss-pricing-plans {
    3529    display: grid;
    36     grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
    37     align-items: start;
    38     justify-content: center;
    39     max-width: 1200px;
     30    grid-template-columns: repeat(auto-fit, minmax(min(100%, 270px), 1fr));
     31    align-items: stretch;
     32    max-width: 1160px;
    4033    width: 100%;
    41     margin: 0 auto;
    42     gap: 20px;
    43 }
    44 
    45 /* Individual plan cards — size to their own content (no forced
    46  * equal-height stretch across a row). */
     34    gap: 0;
     35    background: var(--ss-surface);
     36    border: 1px solid var(--ss-border);
     37    border-radius: var(--ssl-radius, 12px);
     38    box-shadow: var(--ssl-shadow, none);
     39    overflow: hidden;
     40}
     41
    4742.subscription-pricing-plans.layout2 .ss-pricing-plan {
    48     max-width: 350px;
    49     width: 100%;
    50     justify-self: center;
    51     background-color: var(--ss-surface);
    52     border: 1px solid var(--ss-border);
    53     box-shadow: 0 8px 20px rgba(24, 38, 32, 0.06);
    54     border-radius: 8px;
    55     overflow: hidden;
    56     transition: all 0.3s ease;
    57     position: relative;
    58 }
    59 
    60 .subscription-pricing-plans.layout2 .ss-pricing-plan:hover {
    61     transform: translateY(-5px);
    62     box-shadow: 0 15px 30px rgba(24, 38, 32, 0.1);
    63 }
    64 
    65 /* Featured plan styling — gold is reserved for exactly this moment. */
     43    background: transparent;
     44    border: none;
     45    border-left: 1px solid var(--ss-border);
     46    border-radius: 0;
     47    box-shadow: none;
     48    max-width: none;
     49    padding: calc(var(--ssl-pad, 24px) + 8px) var(--ssl-pad, 24px);
     50}
     51
     52.subscription-pricing-plans.layout2 .ss-pricing-plan:first-child {
     53    border-left: none;
     54}
     55
     56/* Columns don't lift out of the sheet — the sheet is the object. */
     57.subscription-pricing-plans.ss-responsive.ss-hover-lift.layout2 .ss-pricing-plan:hover,
     58.subscription-pricing-plans.ss-responsive.ss-hover-scale.layout2 .ss-pricing-plan:hover {
     59    transform: none;
     60    box-shadow: none;
     61}
     62
     63.subscription-pricing-plans.ss-responsive.ss-hover-glow.layout2 .ss-pricing-plan:hover {
     64    box-shadow: inset 0 0 0 2px var(--ss-primary);
     65    border-color: var(--ss-border);
     66}
     67
     68/* Header: eyebrow name, then the price set big on its baseline. */
     69.subscription-pricing-plans.layout2 .ss-plan-header {
     70    text-align: left;
     71    padding: 0 0 18px;
     72    border-bottom: 1px solid var(--ss-border);
     73}
     74
     75.subscription-pricing-plans.layout2 .ss-plan-header h3 {
     76    margin: 0 0 14px;
     77    font-size: 12px;
     78    font-weight: 700;
     79    text-transform: uppercase;
     80    letter-spacing: 0.18em;
     81    color: var(--ss-muted);
     82}
     83
     84.subscription-pricing-plans.layout2 .ss-price {
     85    font-size: 44px;
     86    font-weight: 750;
     87    line-height: 1;
     88    letter-spacing: -0.03em;
     89    font-variant-numeric: tabular-nums;
     90    color: var(--ss-ink);
     91}
     92
     93.subscription-pricing-plans.layout2 .ss-price span {
     94    font-size: 14px;
     95    font-weight: 500;
     96    letter-spacing: 0;
     97    color: var(--ss-muted);
     98    margin-left: 2px;
     99}
     100
     101/* Features: compact ruled list. */
     102.subscription-pricing-plans.layout2 .ss-plan-features {
     103    padding: 10px 0 22px;
     104}
     105
     106.subscription-pricing-plans.layout2 .ss-plan-features li {
     107    padding: 10px 0;
     108    font-size: 14px;
     109    border-bottom: 1px solid color-mix(in srgb, var(--ss-border) 55%, transparent);
     110}
     111
     112.subscription-pricing-plans.layout2 .ss-plan-features li:last-child {
     113    border-bottom: none;
     114}
     115
     116.subscription-pricing-plans.layout2 .ss-plan-footer {
     117    text-align: left;
     118    padding: 0;
     119}
     120
     121/* ── The inverted featured column ───────────────────────────────────────── */
     122
    66123.subscription-pricing-plans.layout2 .ss-pricing-plan.ss-featured {
    67     background-color: var(--ss-surface);
    68     border: 2px solid var(--ss-accent);
    69     transform: scale(1.05);
    70     z-index: 1;
    71     box-shadow: 0 15px 30px rgba(192, 139, 30, 0.25);
    72     position: relative;
    73 }
    74 
    75 .subscription-pricing-plans.layout2 .ss-pricing-plan.ss-featured::before {
    76     content: 'POPULAR';
    77     position: absolute;
    78     top: 15px;
    79     right: 15px;
    80     background-color: var(--ss-accent);
    81     color: white;
    82     padding: 5px 10px;
    83     font-size: 12px;
    84     border-radius: 20px;
    85     font-weight: bold;
    86 }
    87 
    88 .subscription-pricing-plans.layout2 .ss-plan-header {
    89     padding: 30px 20px;
    90     text-align: center;
    91     border-bottom: 1px solid var(--ss-border);
    92     position: relative;
    93 }
    94 
    95 .subscription-pricing-plans.layout2 .ss-plan-header h3 {
    96     margin: 0;
    97     font-size: 20px;
    98     font-weight: 600;
    99     text-transform: uppercase;
    100     letter-spacing: 1px;
    101     color: var(--ss-ink);
     124    background: var(--ss-primary);
     125    border-left-color: var(--ss-primary);
     126}
     127
     128.subscription-pricing-plans.layout2 .ss-featured + .ss-pricing-plan {
     129    border-left-color: var(--ss-primary);
     130}
     131
     132.subscription-pricing-plans.layout2 .ss-featured .ss-plan-header {
     133    border-bottom-color: rgba(255, 255, 255, 0.25);
    102134}
    103135
    104136.subscription-pricing-plans.layout2 .ss-featured .ss-plan-header h3 {
     137    color: rgba(255, 255, 255, 0.75);
     138}
     139
     140.subscription-pricing-plans.layout2 .ss-featured .ss-price,
     141.subscription-pricing-plans.layout2 .ss-featured .ss-price span {
     142    color: #fff;
     143}
     144
     145.subscription-pricing-plans.layout2 .ss-featured .ss-price span {
     146    color: rgba(255, 255, 255, 0.72);
     147}
     148
     149.subscription-pricing-plans.layout2 .ss-featured .ss-plan-features li {
     150    color: rgba(255, 255, 255, 0.88);
     151    border-bottom-color: rgba(255, 255, 255, 0.18);
     152}
     153
     154.subscription-pricing-plans.ss-responsive.layout2 .ss-featured .ss-plan-features li::before {
     155    color: #fff;
     156}
     157
     158/* Badge chip on the inverted column: white chip, primary text. */
     159.subscription-pricing-plans.ss-responsive.layout2 .ss-featured .ss-plan-badge {
     160    background: #fff;
    105161    color: var(--ss-primary);
    106162}
    107163
    108 .subscription-pricing-plans.layout2 .ss-price {
    109     font-size: 36px;
    110     font-weight: bold;
    111     margin-top: 10px;
     164/* The featured CTA must read on the primary background whatever the global
     165 * button variant is: white button, primary label. */
     166.subscription-pricing-plans.ss-responsive.layout2 .ss-featured .ss-button,
     167.subscription-pricing-plans.ss-responsive.layout2 .ss-featured .ss-pricing-button {
     168    background: #fff;
     169    border-color: #fff;
    112170    color: var(--ss-primary);
    113171}
    114172
    115 .subscription-pricing-plans.layout2 .ss-price span {
    116     font-size: 16px;
    117     font-weight: normal;
    118     opacity: 0.8;
    119 }
    120 
    121 .subscription-pricing-plans.layout2 .ss-plan-features {
    122     padding: 0;
    123 }
    124 
    125 .subscription-pricing-plans.layout2 .ss-plan-features ul {
    126     list-style: none;
    127     padding: 0;
    128     margin: 0;
    129 }
    130 
    131 .subscription-pricing-plans.layout2 .ss-plan-features li {
    132     padding: 15px 20px;
    133     border-bottom: 1px solid var(--ss-border);
    134     text-align: center;
    135     font-size: 14px;
    136     color: var(--ss-body);
    137 }
    138 
    139 .subscription-pricing-plans.layout2 .ss-plan-footer {
    140     padding: 30px;
    141     text-align: center;
    142     background-color: transparent;
    143 }
    144 
    145 .subscription-pricing-plans.layout2 .ss-button,
    146 .subscription-pricing-plans.layout2 .ss-pricing-button {
    147     display: inline-block;
    148     padding: 12px 30px;
    149     border-radius: 50px;
    150     text-decoration: none;
    151     font-weight: 600;
    152     transition: all 0.3s ease;
    153     font-size: 15px;
    154     letter-spacing: 0.5px;
    155     text-transform: uppercase;
     173.subscription-pricing-plans.ss-responsive.layout2 .ss-featured .ss-button:hover,
     174.subscription-pricing-plans.ss-responsive.layout2 .ss-featured .ss-pricing-button:hover {
     175    background: rgba(255, 255, 255, 0.88);
     176    border-color: rgba(255, 255, 255, 0.88);
     177    color: var(--ss-primary-dark);
     178}
     179
     180/* ── When a behavior breaks up the sheet, columns become cards again ────── */
     181
     182.subscription-pricing-plans.layout2:is(.ss-behave-vertical, .ss-behave-horizontal, .ss-behave-scroll, .ss-behave-carousel) .ss-pricing-plans {
     183    background: transparent;
     184    border: none;
     185    border-radius: 0;
     186    box-shadow: none;
     187    overflow: visible;
     188    gap: var(--ssl-gap, 24px);
     189}
     190
     191.subscription-pricing-plans.layout2:is(.ss-behave-vertical, .ss-behave-horizontal, .ss-behave-scroll, .ss-behave-carousel) .ss-pricing-plan,
     192.subscription-pricing-plans.layout2:is(.ss-behave-vertical, .ss-behave-horizontal, .ss-behave-scroll, .ss-behave-carousel) .ss-pricing-plan:first-child {
     193    background: var(--ss-surface);
     194    border: var(--ssl-border-w, 1px) solid var(--ss-border);
     195    border-radius: var(--ssl-radius, 12px);
     196    box-shadow: var(--ssl-shadow, none);
     197}
     198
     199.subscription-pricing-plans.layout2:is(.ss-behave-vertical, .ss-behave-horizontal, .ss-behave-scroll, .ss-behave-carousel) .ss-pricing-plan.ss-featured {
    156200    background: var(--ss-primary);
    157     color: white;
    158     border: none;
    159     cursor: pointer;
    160     box-shadow: var(--ss-shadow);
    161 }
    162 
    163 .subscription-pricing-plans.layout2 .ss-featured .ss-button,
    164 .subscription-pricing-plans.layout2 .ss-featured .ss-pricing-button {
    165     background: var(--ss-primary);
    166     color: white;
    167 }
    168 
    169 .subscription-pricing-plans.layout2 .ss-button:hover,
    170 .subscription-pricing-plans.layout2 .ss-pricing-button:hover {
    171     background: var(--ss-primary-dark);
    172     transform: translateY(-3px);
    173     box-shadow: 0 8px 20px rgba(24, 38, 32, 0.18);
    174 }
    175 
    176 /* Responsive styles */
    177 @media screen and (max-width: 992px) {
    178     .subscription-pricing-plans.layout2 .ss-pricing-plan {
    179         max-width: 100%;
    180     }
    181 
    182     .subscription-pricing-plans.layout2 .ss-pricing-plan.ss-featured {
    183         transform: none;
    184     }
    185 
    186     .subscription-pricing-plans.layout2 .ss-button {
    187         width: 100%;
    188         padding: 12px 10px;
    189     }
    190 }
    191 
    192 /* Responsive type + spacing.
    193 @media screen and (max-width: 768px) {
    194     .subscription-pricing-plans.layout2 .ss-plan-header h3 {
    195         font-size: 1.5rem;
     201    border-color: var(--ss-primary);
     202}
     203
     204/* Same again for the automatic mobile swipe strip. */
     205@media (max-width: 767px) {
     206    .subscription-pricing-plans.layout2.ss-behave-auto .ss-pricing-plans {
     207        background: transparent;
     208        border: none;
     209        border-radius: 0;
     210        box-shadow: none;
     211        overflow: visible;
     212        gap: var(--ssl-gap, 24px);
     213    }
     214
     215    .subscription-pricing-plans.layout2.ss-behave-auto .ss-pricing-plan,
     216    .subscription-pricing-plans.layout2.ss-behave-auto .ss-pricing-plan:first-child {
     217        background: var(--ss-surface);
     218        border: var(--ssl-border-w, 1px) solid var(--ss-border);
     219        border-radius: var(--ssl-radius, 12px);
     220        box-shadow: var(--ssl-shadow, none);
     221    }
     222
     223    .subscription-pricing-plans.layout2.ss-behave-auto .ss-pricing-plan.ss-featured {
     224        background: var(--ss-primary);
     225        border-color: var(--ss-primary);
     226    }
     227
     228    .subscription-pricing-plans.layout2 {
     229        padding: 20px 10px;
    196230    }
    197231
    198232    .subscription-pricing-plans.layout2 .ss-price {
    199         font-size: 1.8rem;
    200     }
    201 
    202     .subscription-pricing-plans.layout2 .ss-price span {
    203         font-size: 0.9rem;
    204     }
    205 
    206     .subscription-pricing-plans.layout2 .ss-plan-features li {
    207         font-size: 0.9rem;
    208         padding: 8px 10px;
    209     }
    210 
    211     .subscription-pricing-plans.layout2 .ss-button {
    212         padding: 14px 20px;
    213         font-size: 1rem;
    214     }
    215 
    216     .subscription-pricing-plans.layout2 .ss-plan-header,
    217     .subscription-pricing-plans.layout2 .ss-plan-features {
    218         margin-bottom: 15px;
    219     }
    220 }
    221 
    222 @media screen and (max-width: 480px) {
    223     .subscription-pricing-plans.layout2 .ss-plan-header h3 {
    224         font-size: 1.3rem;
    225     }
    226 
    227     .subscription-pricing-plans.layout2 .ss-price {
    228         font-size: 1.6rem;
    229     }
    230 }
    231 
    232 /* Feature list: check marks + tighter rhythm. */
    233 .subscription-pricing-plans.layout2 .ss-plan-features li {
    234     display: flex;
    235     align-items: flex-start;
    236     gap: 10px;
    237     text-align: left;
    238     padding: 10px 24px;
    239     border-bottom: none;
    240     line-height: 1.5;
    241 }
    242 
    243 .subscription-pricing-plans.layout2 .ss-plan-features li::before {
    244     content: "✓";
    245     color: var(--ss-primary);
    246     font-weight: 700;
    247     line-height: 1.4;
    248     flex-shrink: 0;
    249     margin-right: 0;
    250 }
    251 
    252 .subscription-pricing-plans.layout2 .ss-plan-footer {
    253     padding: 18px 24px 26px;
    254 }
    255 
    256 .subscription-pricing-plans.layout2 .ss-button,
    257 .subscription-pricing-plans.layout2 .ss-pricing-button {
    258     width: 100%;
    259 }
    260 
    261 /* "Current Plan" — a plan the logged-in user already owns. Disabled, so they
    262    can see they have it and cannot buy it twice. */
    263 .ss-button--owned,
    264 .ss-button--owned:hover {
    265         background: var(--ss-success-tint, #d8f2e0) !important;
    266         color: var(--ss-success, #14603a) !important;
    267         border: 1px solid var(--ss-success, #86c9a3) !important;
    268         cursor: not-allowed;
    269         box-shadow: none !important;
    270         transform: none !important;
    271 }
     233        font-size: 36px;
     234    }
     235}
  • subscription-system/trunk/public/css/pricing-layout3.css

    r3650727 r3651653  
    11/**
    2  * Pricing Layout 3 - Modern Cards Style
     2 * Pricing Layout 3 — "Spotlight"
    33 *
    4  * This is the ONLY stylesheet for this layout — the template
    5  * (pricing-plans-layout3.php) used to also echo a duplicate inline
    6  * <style> block after this file's <link>, with the same specificity
    7  * mismatch as layout 2 (base rule loses, featured-card rule wins) —
    8  * so only the "Best Value" card rendered transparent instead of
    9  * white. That inline block has been removed entirely.
     4 * A premium dark stage: the whole strip sits on a deep panel mixed from the
     5 * ink and primary tokens. Non-featured plans are translucent glass cards on
     6 * the dark; the featured plan is the one card in the light — a solid surface
     7 * card with an accent keyline, raised and gently scaled. Check icons go
     8 * accent-colored here so they read on the dark ground.
     9 *
     10 * Everything still derives from the Appearance tokens: repaint the palette
     11 * and the stage repaints with it.
    1012 */
    1113
    12 *,
    13 *::before,
    14 *::after {
    15     box-sizing: border-box;
    16 }
    17 
    1814.subscription-pricing-plans.layout3 {
     15    padding: 40px 20px;
    1916    background: transparent !important;
    20     padding: 40px 20px;
    21 }
    22 
     17}
     18
     19/* The stage. */
    2320.subscription-pricing-plans.layout3 .ss-pricing-container {
    24     display: flex;
    25     justify-content: center;
    26     width: 100%;
    27     padding: 20px 0;
    28     background: transparent !important;
    29 }
    30 
    31 .subscription-pricing-plans.layout3 .ss-pricing-plans,
    32 .subscription-pricing-plans.layout3 .ss-pricing-container {
     21    position: relative;
     22    max-width: 1200px;
     23    margin: 0 auto;
     24    padding: clamp(28px, 5vw, 56px) clamp(20px, 4vw, 48px);
     25    border-radius: max(var(--ssl-radius, 12px), 18px);
     26    background: var(--ss-ink);
     27    background: linear-gradient(
     28        140deg,
     29        color-mix(in srgb, var(--ss-ink) 94%, var(--ss-primary) 6%) 0%,
     30        color-mix(in srgb, var(--ss-ink) 62%, var(--ss-primary-dark) 38%) 100%
     31    );
     32    overflow: hidden;
     33}
     34
     35/* A faint top-light so the panel reads lit, not flat. */
     36.subscription-pricing-plans.layout3 .ss-pricing-container::before {
     37    content: "";
     38    position: absolute;
     39    inset: 0;
     40    background: radial-gradient(60% 45% at 50% 0%, rgba(255, 255, 255, 0.10) 0%, transparent 70%);
     41    pointer-events: none;
     42}
     43
     44.subscription-pricing-plans.layout3 .ss-pricing-plans {
     45    position: relative;
    3346    display: grid;
    34     grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
    35     align-items: start;
    36     justify-content: center;
    37     max-width: 1200px;
    38     width: 100%;
    39     margin: 0 auto;
    40     gap: 20px;
    41 }
    42 
    43 /* Individual plan cards — size to their own content (no forced
    44  * equal-height stretch across a row). */
     47    grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
     48    align-items: center;
     49}
     50
     51/* Glass cards on the dark. */
    4552.subscription-pricing-plans.layout3 .ss-pricing-plan {
    46     max-width: 350px;
     53    background: rgba(255, 255, 255, 0.06);
     54    border: 1px solid rgba(255, 255, 255, 0.14);
     55    box-shadow: none;
     56    max-width: 380px;
    4757    width: 100%;
    4858    justify-self: center;
    49     background-color: var(--ss-surface);
    50     border-radius: 15px;
    51     border: 1px solid var(--ss-border);
    52     transition: all 0.3s ease;
    53     box-shadow: 0 10px 25px rgba(24, 38, 32, 0.08);
    54     overflow: hidden;
    55     position: relative;
    56 }
    57 
    58 .subscription-pricing-plans.layout3 .ss-pricing-plan:hover {
    59     transform: translateY(-10px);
    60     box-shadow: 0 20px 40px rgba(24, 38, 32, 0.12);
    61 }
    62 
    63 /* Featured plan styling — gold is reserved for exactly this moment. */
    64 .subscription-pricing-plans.layout3 .ss-pricing-plan.ss-featured {
    65     background-color: var(--ss-surface);
    66     border: 2px solid var(--ss-accent);
    67     box-shadow: 0 15px 30px rgba(192, 139, 30, 0.25);
    68     position: relative;
    69 }
    70 
    71 .subscription-pricing-plans.layout3 .ss-pricing-plan.ss-featured::before {
    72     content: 'BEST VALUE';
    73     position: absolute;
    74     top: 15px;
    75     right: -35px;
    76     background-color: var(--ss-accent);
    77     color: white;
    78     padding: 5px 40px;
    79     font-size: 12px;
    80     transform: rotate(45deg);
    81     font-weight: bold;
    82     box-shadow: 0 2px 5px rgba(24, 38, 32, 0.1);
    83     z-index: 1;
    84 }
    85 
     59    padding: var(--ssl-pad, 24px);
     60}
     61
     62.subscription-pricing-plans.ss-responsive.ss-hover-lift.layout3 .ss-pricing-plan:hover,
     63.subscription-pricing-plans.ss-responsive.ss-hover-glow.layout3 .ss-pricing-plan:hover {
     64    background: rgba(255, 255, 255, 0.09);
     65    border-color: rgba(255, 255, 255, 0.26);
     66    box-shadow: 0 14px 34px rgba(0, 0, 0, 0.35);
     67}
     68
     69/* Header */
    8670.subscription-pricing-plans.layout3 .ss-plan-header {
    87     padding: 30px 20px;
    88     position: relative;
    89     overflow: hidden;
    90     border-bottom: none;
    9171    text-align: center;
     72    padding: 8px 0 18px;
     73    border-bottom: 1px solid rgba(255, 255, 255, 0.14);
    9274}
    9375
    9476.subscription-pricing-plans.layout3 .ss-plan-header h3 {
    9577    margin: 0;
    96     font-size: 20px;
     78    font-size: 13px;
    9779    font-weight: 600;
    9880    text-transform: uppercase;
    99     letter-spacing: 1px;
    100     color: var(--ss-ink);
     81    letter-spacing: 0.16em;
     82    color: rgba(255, 255, 255, 0.82);
     83}
     84
     85.subscription-pricing-plans.layout3 .ss-price {
     86    margin-top: 14px;
     87    font-size: 46px;
     88    font-weight: 800;
     89    line-height: 1;
     90    letter-spacing: -0.02em;
     91    font-variant-numeric: tabular-nums;
     92    color: #fff;
     93}
     94
     95.subscription-pricing-plans.layout3 .ss-price span {
     96    font-size: 14px;
     97    font-weight: 500;
     98    letter-spacing: 0;
     99    color: rgba(255, 255, 255, 0.6);
     100}
     101
     102/* Features */
     103.subscription-pricing-plans.layout3 .ss-plan-features {
     104    padding: 16px 4px;
     105}
     106
     107.subscription-pricing-plans.layout3 .ss-plan-features li {
     108    padding: 8px 0;
     109    font-size: 14px;
     110    color: rgba(255, 255, 255, 0.78);
     111}
     112
     113.subscription-pricing-plans.ss-responsive.layout3 .ss-plan-features li::before {
     114    color: var(--ss-accent);
     115}
     116
     117/* CTA on glass cards: ghost white, whatever the global variant. */
     118.subscription-pricing-plans.ss-responsive.layout3 .ss-button,
     119.subscription-pricing-plans.ss-responsive.layout3 .ss-pricing-button {
     120    background: transparent;
     121    border-color: rgba(255, 255, 255, 0.45);
     122    color: #fff;
     123}
     124
     125.subscription-pricing-plans.ss-responsive.layout3 .ss-button:hover,
     126.subscription-pricing-plans.ss-responsive.layout3 .ss-pricing-button:hover {
     127    background: #fff;
     128    border-color: #fff;
     129    color: var(--ss-primary-dark);
     130}
     131
     132.subscription-pricing-plans.layout3 .ss-plan-footer {
     133    text-align: center;
     134    padding: 6px 0 4px;
     135}
     136
     137/* ── The card in the spotlight ──────────────────────────────────────────── */
     138
     139.subscription-pricing-plans.layout3 .ss-pricing-plan.ss-featured {
     140    background: var(--ss-surface);
     141    border-color: var(--ss-surface);
     142    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
     143    transform: scale(1.045);
     144    z-index: 1;
     145    padding-top: calc(var(--ssl-pad, 24px) + 6px);
     146    padding-bottom: calc(var(--ssl-pad, 24px) + 6px);
     147}
     148
     149/* Accent keyline across the featured card's top. */
     150.subscription-pricing-plans.layout3 .ss-pricing-plan.ss-featured::before {
     151    content: "";
     152    position: absolute;
     153    top: 0;
     154    left: 0;
     155    right: 0;
     156    height: 4px;
     157    background: linear-gradient(90deg, var(--ss-accent), var(--ss-primary));
     158}
     159
     160.subscription-pricing-plans.ss-responsive.ss-hover-lift.layout3 .ss-featured:hover,
     161.subscription-pricing-plans.ss-responsive.ss-hover-scale.layout3 .ss-featured:hover,
     162.subscription-pricing-plans.ss-responsive.ss-hover-glow.layout3 .ss-featured:hover {
     163    background: var(--ss-surface);
     164    transform: scale(1.045) translateY(-6px);
     165    box-shadow: 0 30px 70px rgba(0, 0, 0, 0.5);
     166    border-color: var(--ss-surface);
     167}
     168
     169.subscription-pricing-plans.layout3 .ss-featured .ss-plan-header {
     170    border-bottom-color: var(--ss-border);
    101171}
    102172
    … …  
    105175}
    106176
    107 .subscription-pricing-plans.layout3 .ss-price {
    108     font-size: 34px;
    109     font-weight: bold;
    110     margin-top: 10px;
     177.subscription-pricing-plans.layout3 .ss-featured .ss-price {
     178    color: var(--ss-ink);
     179}
     180
     181.subscription-pricing-plans.layout3 .ss-featured .ss-price span {
     182    color: var(--ss-muted);
     183}
     184
     185.subscription-pricing-plans.layout3 .ss-featured .ss-plan-features li {
     186    color: var(--ss-body);
     187}
     188
     189.subscription-pricing-plans.ss-responsive.layout3 .ss-featured .ss-plan-features li::before {
    111190    color: var(--ss-primary);
    112191}
    113192
    114 .subscription-pricing-plans.layout3 .ss-price span {
    115     font-size: 16px;
    116     font-weight: normal;
    117     opacity: 0.8;
    118 }
    119 
    120 .subscription-pricing-plans.layout3 .ss-plan-features {
    121     padding: 0;
    122 }
    123 
    124 .subscription-pricing-plans.layout3 .ss-plan-features ul {
    125     list-style: none;
    126     padding: 0;
    127     margin: 0;
    128 }
    129 
    130 .subscription-pricing-plans.layout3 .ss-plan-features li {
    131     padding: 15px 20px;
    132     border-bottom: 1px solid var(--ss-border);
    133     text-align: left;
    134     display: flex;
    135     align-items: center;
    136     font-size: 14px;
    137     color: var(--ss-body);
    138 }
    139 
    140 .subscription-pricing-plans.layout3 .ss-plan-features li:before {
    141     content: "✓";
    142     color: var(--ss-primary);
    143     margin-right: 15px;
    144     font-weight: bold;
    145 }
    146 
    147 .subscription-pricing-plans.layout3 .ss-plan-footer {
    148     padding: 30px;
    149     text-align: center;
    150     background-color: transparent;
    151 }
    152 
    153 .subscription-pricing-plans.layout3 .ss-button,
    154 .subscription-pricing-plans.layout3 .ss-pricing-button {
    155     display: inline-block;
    156     padding: 14px 35px;
    157     background-color: var(--ss-primary);
    158     color: white;
    159     border-radius: 30px;
    160     text-decoration: none;
    161     font-weight: 600;
    162     font-size: 15px;
    163     letter-spacing: 0.5px;
    164     text-transform: uppercase;
    165     transition: all 0.3s ease;
    166     box-shadow: var(--ss-shadow);
    167     border: none;
    168     cursor: pointer;
    169 }
    170 
    171 .subscription-pricing-plans.layout3 .ss-featured .ss-button,
    172 .subscription-pricing-plans.layout3 .ss-featured .ss-pricing-button {
    173     background-color: var(--ss-primary);
    174     color: white;
    175 }
    176 
    177 .subscription-pricing-plans.layout3 .ss-button:hover,
    178 .subscription-pricing-plans.layout3 .ss-pricing-button:hover {
    179     transform: translateY(-3px);
    180     box-shadow: 0 8px 20px rgba(24, 38, 32, 0.18);
    181     background-color: var(--ss-primary-dark);
    182 }
    183 
    184 /* Responsive styles */
    185 @media screen and (max-width: 992px) {
    186     .subscription-pricing-plans.layout3 .ss-pricing-plan {
    187         max-width: 100%;
     193/* Featured CTA: solid primary, full strength. */
     194.subscription-pricing-plans.ss-responsive.layout3 .ss-featured .ss-button,
     195.subscription-pricing-plans.ss-responsive.layout3 .ss-featured .ss-pricing-button {
     196    background: var(--ss-primary);
     197    border-color: var(--ss-primary);
     198    color: #fff;
     199}
     200
     201.subscription-pricing-plans.ss-responsive.layout3 .ss-featured .ss-button:hover,
     202.subscription-pricing-plans.ss-responsive.layout3 .ss-featured .ss-pricing-button:hover {
     203    background: var(--ss-primary-dark);
     204    border-color: var(--ss-primary-dark);
     205}
     206
     207/* Owned state must stay readable on both card types. */
     208.subscription-pricing-plans.ss-responsive.layout3 .ss-button--owned,
     209.subscription-pricing-plans.ss-responsive.layout3 .ss-button--owned:hover {
     210    background: var(--ss-success-tint, #D8F2E0) !important;
     211    color: var(--ss-success, #14603A) !important;
     212    border-color: var(--ss-success, #86C9A3) !important;
     213}
     214
     215/* ── Behaviors inside the stage ─────────────────────────────────────────── */
     216
     217/* The scale transform fights carousel/scroll geometry — level the featured
     218 * card when the set becomes a strip or slider. */
     219.subscription-pricing-plans.layout3:is(.ss-behave-scroll, .ss-behave-carousel, .ss-behave-horizontal, .ss-behave-vertical) .ss-pricing-plan.ss-featured {
     220    transform: none;
     221}
     222
     223@media (max-width: 767px) {
     224    .subscription-pricing-plans.layout3.ss-behave-auto .ss-pricing-plan.ss-featured {
     225        transform: none;
    188226    }
    189 }
    190 
    191 /* Responsive type + spacing.
    192 @media screen and (max-width: 768px) {
    193     .subscription-pricing-plans.layout3 .ss-pricing-plan.ss-featured {
    194         order: -1; /* Move featured plan to the top on mobile */
     227
     228    .subscription-pricing-plans.layout3 {
     229        padding: 20px 10px;
    195230    }
    196231
    197     .subscription-pricing-plans.layout3 .ss-plan-header h3 {
    198         font-size: 1.5rem;
     232    .subscription-pricing-plans.layout3 .ss-price {
     233        font-size: 38px;
    199234    }
    200 
    201     .subscription-pricing-plans.layout3 .ss-price {
    202         font-size: 1.8rem;
    203     }
    204 
    205     .subscription-pricing-plans.layout3 .ss-price span {
    206         font-size: 0.9rem;
    207     }
    208 
    209     .subscription-pricing-plans.layout3 .ss-plan-features li {
    210         font-size: 0.9rem;
    211         padding: 8px 10px;
    212     }
    213 
    214     .subscription-pricing-plans.layout3 .ss-pricing-button {
    215         padding: 10px 15px;
    216     }
    217 }
    218 
    219 @media screen and (max-width: 480px) {
    220     .subscription-pricing-plans.layout3 .ss-plan-header h3 {
    221         font-size: 1.3rem;
    222     }
    223 
    224     .subscription-pricing-plans.layout3 .ss-price {
    225         font-size: 1.6rem;
    226     }
    227 }
    228 
    229 /* Feature list: check marks + tighter rhythm. */
    230 .subscription-pricing-plans.layout3 .ss-plan-features li {
    231     display: flex;
    232     align-items: flex-start;
    233     gap: 10px;
    234     text-align: left;
    235     padding: 10px 24px;
    236     border-bottom: none;
    237     line-height: 1.5;
    238 }
    239 
    240 .subscription-pricing-plans.layout3 .ss-plan-features li::before {
    241     content: "✓";
    242     color: var(--ss-primary);
    243     font-weight: 700;
    244     line-height: 1.4;
    245     flex-shrink: 0;
    246     margin-right: 0;
    247 }
    248 
    249 .subscription-pricing-plans.layout3 .ss-plan-footer {
    250     padding: 18px 24px 26px;
    251 }
    252 
    253 .subscription-pricing-plans.layout3 .ss-button,
    254 .subscription-pricing-plans.layout3 .ss-pricing-button {
    255     width: 100%;
    256 }
    257 
    258 /* "Current Plan" — a plan the logged-in user already owns. Disabled, so they
    259    can see they have it and cannot buy it twice. */
    260 .ss-button--owned,
    261 .ss-button--owned:hover {
    262         background: var(--ss-success-tint, #d8f2e0) !important;
    263         color: var(--ss-success, #14603a) !important;
    264         border: 1px solid var(--ss-success, #86c9a3) !important;
    265         cursor: not-allowed;
    266         box-shadow: none !important;
    267         transform: none !important;
    268 }
     235}
     236
     237/* Filter bar sits above the dark stage, keep it outside the panel look. */
     238.subscription-pricing-plans.layout3 .ss-plan-filters {
     239    margin-bottom: 18px;
     240}
  • subscription-system/trunk/public/js/interest-subscribe.js

    r3650727 r3651653  
    7272                $('body').addClass('ss-interest-modal-open');
    7373                $modal.find('.ss-interest-message').removeClass('is-error is-success').text('');
     74
     75                // A previous run may have left the submit button disabled (busy) or
     76                // in its green done state; reopening starts a fresh attempt. Only
     77                // buttons WE disabled are re-enabled — the "no plans exist" disable
     78                // set server-side carries no ss-busy flag and stays off.
     79                var $submit = $modal.find('.ss-interest-form [type="submit"]');
     80                if ($submit.hasClass('ss-interest-btn--done') || $submit.data('ssBusy')) {
     81                        $submit.prop('disabled', false).removeClass('ss-interest-btn--done').removeData('ssBusy');
     82                }
    7483
    7584                // Always open on step 1, even if a previous run left it on the auth step.
    … …  
    279288                var $plan = getSelectedPlan($form);
    280289
    281                 $submit.prop('disabled', true).text(i18n().finishing || 'Setting up your subscription…');
     290                $submit.prop('disabled', true).data('ssBusy', 1).text(i18n().finishing || 'Setting up your subscription…');
    282291
    283292                $.ajax({
    … …  
    296305                                if (response.data.checkoutUrl) {
    297306                                        showMessage($form, i18n().to_checkout || 'Taking you to secure checkout…', 'success');
     307                                        $submit.text(i18n().to_checkout || 'Taking you to secure checkout…');
    298308                                        window.location.href = response.data.checkoutUrl;
    299309                                        return;
    … …  
    302312                                // then land on the success page — same destination the paid
    303313                                // flow reaches via Stripe — so what they subscribed to and
    304                                 // its benefits are played back to them.
     314                                // its benefits are played back to them. The button flips to a
     315                                // green done state so it agrees with the message instead of
     316                                // staying stuck on its "Setting up…" busy label.
    305317                                showMessage($form, response.data.message || 'You are subscribed.', 'success');
     318                                $submit.addClass('ss-interest-btn--done').text(i18n().done || 'All set ✓');
    306319                                if (response.data.redirectUrl) {
    307320                                        setTimeout(function () { window.location.href = response.data.redirectUrl; }, 1200);
    … …  
    311324                        } else {
    312325                                showMessage($form, (response.data && response.data.message) || i18n().error || 'Error.', 'error');
    313                                 $submit.prop('disabled', false); refreshSubmitLabel($form);
     326                                $submit.prop('disabled', false).removeData('ssBusy'); refreshSubmitLabel($form);
    314327                        }
    315328                }).fail(function () {
    316329                        showMessage($form, i18n().error || 'Error.', 'error');
    317                         $submit.prop('disabled', false); refreshSubmitLabel($form);
     330                        $submit.prop('disabled', false).removeData('ssBusy'); refreshSubmitLabel($form);
    318331                });
    319332        }
    … …  
    348361                                subs_sys_hp: ''
    349362                        };
    350                         $submit.prop('disabled', true).text(i18n().registering || 'Creating your account…');
     363                        $submit.prop('disabled', true).data('ssBusy', 1).text(i18n().registering || 'Creating your account…');
    351364                } else {
    352365                        if (!val('login_identifier') || !val('login_password')) {
    … …  
    360373                                password: val('login_password')
    361374                        };
    362                         $submit.prop('disabled', true).text(i18n().logging_in || 'Signing in…');
     375                        $submit.prop('disabled', true).data('ssBusy', 1).text(i18n().logging_in || 'Signing in…');
    363376                }
    364377
    … …  
    383396                                        }
    384397                                        showMessage($form, (response && response.message) || i18n().error || 'Error.', 'error');
    385                                         $submit.prop('disabled', false); refreshSubmitLabel($form);
     398                                        $submit.prop('disabled', false).removeData('ssBusy'); refreshSubmitLabel($form);
    386399                                }
    387400                        })
    388401                        .fail(function () {
    389402                                showMessage($form, i18n().error || 'Error.', 'error');
    390                                 $submit.prop('disabled', false); refreshSubmitLabel($form);
     403                                $submit.prop('disabled', false).removeData('ssBusy'); refreshSubmitLabel($form);
    391404                        });
    392405        }
  • subscription-system/trunk/public/partials/pricing-plans-layout1.php

    r3650727 r3651653  
    11<?php
    22/**
    3  * Template for displaying pricing plans (Layout 1)
     3 * Template for displaying pricing plans (Layout 1 — "Classic")
     4 *
     5 * Centered floating cards. Structure is shared with the other layouts via
     6 * components/pricing-plan-card.php; this file only sets the wrapper.
    47 *
    58 * @package    Subscription_System
    … …  
    1114}
    1215
    13 // Enqueue the layout-specific CSS
    14 wp_enqueue_style('subscription-pricing-layout1', plugin_dir_url(dirname(__FILE__)) . 'css/pricing-layout1.css', array('subs-sys-tokens'), '1.0.0', 'all');
     16$layout      = 'layout1';
     17$ss_behavior = isset($ss_behavior) ? $ss_behavior : '';
     18require __DIR__ . '/components/pricing-assets.php';
    1519
    16 // Add responsive viewport meta tag if not already present
    17  
    18 
    19 // Enqueue Stripe JS
    20 wp_enqueue_script('stripe-js', 'https://js.stripe.com/v3/', array(), null, true);
    21 
    22 // Enqueue our custom JS
    23 wp_enqueue_script('subscription-pricing-js', plugin_dir_url(dirname(__FILE__)) . 'js/subscription-pricing.js', array('jquery', 'stripe-js'), '1.0.0', true);
    24 
    25 // Localize script with necessary data
    26 wp_localize_script('subscription-pricing-js', 'subscription_data', array(
    27     'ajax_url' => esc_url(admin_url('admin-ajax.php')),
    28     'stripe_publishable_key' => esc_attr( class_exists('Subs_Sys_Stripe_Sync') ? Subs_Sys_Stripe_Sync::get_publishable_key() : get_option('subscription_stripe_publishable_key') ),
    29     'checkout_nonce' => wp_create_nonce('subscription_checkout_nonce'),
    30     'is_user_logged_in' => is_user_logged_in(),
    31     'register_page_url' => esc_url(get_permalink(get_option('register_page'))),
    32     'account_page_url' => esc_url(get_permalink(get_option('account_page'))),
    33     'thank_you_page_url' => esc_url(get_permalink(get_option('thank_you_page'))),
    34     'pricing_plans_page_url' => esc_url(get_permalink(get_option('pricing_plans_page'))),
    35     'nonce' => esc_attr(wp_create_nonce('subscription_check_nonce')),
    36     'subscribe_now_text' => esc_html__('Subscribe Now', 'subscription-system')
    37 ));
    38 
    39  
     20$ss_design_classes = class_exists('Subs_Sys_Layout_Settings') ? Subs_Sys_Layout_Settings::wrapper_classes($ss_behavior) : '';
     21$ss_design_attrs   = class_exists('Subs_Sys_Layout_Settings') ? Subs_Sys_Layout_Settings::data_attrs() : '';
    4022?>
    4123
    42 <div class="subscription-pricing-plans layout1 <?php echo esc_attr($orientation_class); ?> columns-<?php echo esc_attr($atts['columns']); ?> ss-responsive">
     24<div class="subscription-pricing-plans layout1 <?php echo esc_attr($orientation_class); ?> columns-<?php echo esc_attr($atts['columns']); ?> ss-responsive <?php echo esc_attr($ss_design_classes); ?>"<?php echo $ss_design_attrs; // phpcs:ignore WordPress.Security.EscapeOutput -- escaped attr-by-attr in data_attrs(). ?>>
    4325    <?php if (empty($plans)) : ?>
    4426        <p class="no-plans"><?php esc_html_e('No subscription plans available.', 'subscription-system'); ?></p>
    … …  
    4729        <div class="ss-pricing-container ss-layout1">
    4830            <div class="ss-pricing-plans">
    49                 <?php
    50                 // Determine which plan should be featured (middle plan by default)
     31                <?php
    5132                $featured_index = subs_sys_featured_plan_index($plans);
    52                
    53                 // Loop through plans
    54                 foreach ($plans as $index => $plan) :
     33
     34                foreach ($plans as $index => $plan) :
    5535                    $is_featured = ($index === $featured_index) ? 'ss-featured' : '';
     36                    include __DIR__ . '/components/pricing-plan-card.php';
     37                endforeach;
    5638                ?>
    57                     <div class="ss-pricing-plan <?php echo esc_attr($is_featured); ?>" data-ss-category="<?php echo esc_attr($plan->category_name ?? ''); ?>">
    58                         <div class="ss-plan-header">
    59                             <h3><?php echo esc_html($plan->plan_name); ?></h3>
    60                             <div class="ss-price">$<?php echo esc_html(number_format($plan->price, 2)); ?><span><?php echo esc_html(subs_sys_plan_period_label($plan)); ?></span></div>
    61                         </div>
    62                         <div class="ss-plan-features">
    63                             <ul>
    64                                 <?php
    65                                 // If plan has features, display them
    66                                 if (!empty($plan->description)) {
    67                                     // Clean up the description string - remove escaped quotes
    68                                     $clean_description = str_replace('\"', '"', $plan->description);
    69                                    
    70                                     // Try to decode the JSON
    71                                     $features = json_decode($clean_description);
    72                                    
    73                                     // If JSON decode failed, try other methods
    74                                     if ($features === null) {
    75                                         // Try to unserialize
    76                                         $features = maybe_unserialize($clean_description);
    77                                        
    78                                         // If still not an array, use as single item
    79                                         if (!is_array($features)) {
    80                                             $features = array($clean_description);
    81                                         }
    82                                     }
    83                                    
    84                                     // Display features
    85                                     if (is_array($features) || is_object($features)) {
    86                                         foreach ($features as $feature) {
    87                                             echo '<li>' . esc_html($feature) . '</li>';
    88                                         }
    89                                     } else {
    90                                         echo '<li>' . esc_html($clean_description) . '</li>';
    91                                     }
    92                                 } else {
    93                                     echo '<!-- Debug: Plan description is empty -->';
    94                                 }
    95                                 ?>
    96                             </ul>
    97                         </div>
    98                         <div class="ss-plan-footer">
    99                             <?php $ss_owned = is_user_logged_in() && class_exists('Subs_Sys_Subscription_Repository') && Subs_Sys_Subscription_Repository::owns_plan(get_current_user_id(), $plan->id); ?>
    100                             <?php if ($ss_owned) : ?>
    101                                 <button class="ss-button subscribe-button ss-button--owned" disabled aria-disabled="true">
    102                                     <?php esc_html_e('Current Plan', 'subscription-system'); ?>
    103                                 </button>
    104                             <?php else : ?>
    105                                 <button class="ss-button subscribe-button" data-plan-id="<?php echo esc_attr($plan->id); ?>">
    106                                     <?php echo esc_html(subs_sys_plan_button_label($plan)); ?>
    107                                 </button>
    108                             <?php endif; ?>
    109                         </div>
    110                     </div>
    111                 <?php endforeach; ?>
    11239            </div>
    11340        </div>
  • subscription-system/trunk/public/partials/pricing-plans-layout2.php

    r3650727 r3651653  
    22/**
    33 * Template for displaying pricing plans (Layout 2)
     4 *
     5 * The "Ledger": one flat ruled sheet, left-aligned type, inverted featured column. Structure is shared with the other layouts via
     6 * components/pricing-plan-card.php; this file only sets the wrapper.
    47 *
    58 * @package    Subscription_System
    … …  
    1114}
    1215
    13 // Enqueue the layout-specific CSS
    14 wp_enqueue_style('subscription-pricing-layout2', plugin_dir_url(dirname(__FILE__)) . 'css/pricing-layout2.css', array('subs-sys-tokens'), '1.0.0', 'all');
     16$layout      = 'layout2';
     17$ss_behavior = isset($ss_behavior) ? $ss_behavior : '';
     18require __DIR__ . '/components/pricing-assets.php';
    1519
    16 // Add responsive viewport meta tag if not already present
    17  
    18 
    19 // Enqueue Stripe JS
    20 wp_enqueue_script('stripe-js', 'https://js.stripe.com/v3/', array(), null, true);
    21 
    22 // Enqueue our custom JS
    23 wp_enqueue_script('subscription-pricing-js', plugin_dir_url(dirname(__FILE__)) . 'js/subscription-pricing.js', array('jquery', 'stripe-js'), '1.0.0', true);
    24 
    25 // Localize script with necessary data
    26 wp_localize_script('subscription-pricing-js', 'subscription_data', array(
    27     'ajax_url' => admin_url('admin-ajax.php'),
    28     'stripe_publishable_key' => class_exists('Subs_Sys_Stripe_Sync') ? Subs_Sys_Stripe_Sync::get_publishable_key() : get_option('subscription_stripe_publishable_key'),
    29     'checkout_nonce' => wp_create_nonce('subscription_checkout_nonce'),
    30     'is_user_logged_in' => is_user_logged_in(),
    31     'register_page_url' => get_permalink(get_option('register_page')),
    32     'account_page_url' => get_permalink(get_option('account_page')),
    33     'thank_you_page_url' => get_permalink(get_option('thank_you_page')),
    34     'pricing_plans_page_url' => get_permalink(get_option('pricing_plans_page')),
    35     'nonce' => wp_create_nonce('subscription_check_nonce'),
    36     'subscribe_now_text' => esc_html__('Subscribe Now', 'subscription-system')
    37 ));
     20$ss_design_classes = class_exists('Subs_Sys_Layout_Settings') ? Subs_Sys_Layout_Settings::wrapper_classes($ss_behavior) : '';
     21$ss_design_attrs   = class_exists('Subs_Sys_Layout_Settings') ? Subs_Sys_Layout_Settings::data_attrs() : '';
    3822?>
    3923
    40 <div class="subscription-pricing-plans layout2 <?php echo esc_attr($orientation_class); ?> columns-<?php echo esc_attr($atts['columns']); ?> ss-responsive">
     24<div class="subscription-pricing-plans layout2 <?php echo esc_attr($orientation_class); ?> columns-<?php echo esc_attr($atts['columns']); ?> ss-responsive <?php echo esc_attr($ss_design_classes); ?>"<?php echo $ss_design_attrs; // phpcs:ignore WordPress.Security.EscapeOutput -- escaped attr-by-attr in data_attrs(). ?>>
    4125    <?php if (empty($plans)) : ?>
    4226        <p class="no-plans"><?php esc_html_e('No subscription plans available.', 'subscription-system'); ?></p>
    … …  
    4428        <?php include __DIR__ . '/components/plan-filter-bar.php'; ?>
    4529        <div class="ss-pricing-container ss-layout2">
    46             <?php
    47             // Determine which plan should be featured (middle plan by default)
    48             $featured_index = subs_sys_featured_plan_index($plans);
     30            <div class="ss-pricing-plans">
     31                <?php
     32                $featured_index = subs_sys_featured_plan_index($plans);
    4933
    50             foreach ($plans as $index => $plan) :
    51                 $is_featured = ($index === $featured_index) ? 'ss-featured' : '';
    52             ?>
    53                 <div class="ss-pricing-plan <?php echo esc_attr($is_featured); ?>" data-ss-category="<?php echo esc_attr($plan->category_name ?? ''); ?>">
    54                     <div class="ss-plan-header">
    55                         <h3><?php echo esc_html($plan->plan_name); ?></h3>
    56                         <div class="ss-price">$<?php echo esc_html(number_format($plan->price, 2)); ?><span><?php echo esc_html(subs_sys_plan_period_label($plan)); ?></span></div>
    57                     </div>
    58                     <div class="ss-plan-features">
    59                         <ul>
    60                             <?php
    61                             // If plan has features, display them
    62                             if (!empty($plan->description)) {
    63                                 // Clean up the description string - remove escaped quotes
    64                                 $clean_description = str_replace('\"', '"', $plan->description);
    65                                
    66                                 // Try to decode the JSON
    67                                 $features = json_decode($clean_description);
    68                                
    69                                 // If JSON decode failed, try other methods
    70                                 if ($features === null) {
    71                                     // Try to unserialize
    72                                     $features = maybe_unserialize($clean_description);
    73                                    
    74                                     // If still not an array, use as single item
    75                                     if (!is_array($features)) {
    76                                         $features = array($clean_description);
    77                                     }
    78                                 }
    79                                
    80                                 // Display features
    81                                 if (is_array($features) || is_object($features)) {
    82                                     foreach ($features as $feature) {
    83                                         echo '<li>' . esc_html($feature) . '</li>';
    84                                     }
    85                                 } else {
    86                                     echo '<li>' . esc_html($clean_description) . '</li>';
    87                                 }
    88                             }
    89                             ?>
    90                         </ul>
    91                     </div>
    92                     <div class="ss-plan-footer">
    93                         <?php $ss_owned = is_user_logged_in() && class_exists('Subs_Sys_Subscription_Repository') && Subs_Sys_Subscription_Repository::owns_plan(get_current_user_id(), $plan->id); ?>
    94                         <button class="ss-button subscribe-button<?php echo $ss_owned ? ' ss-button--owned' : ''; ?>"<?php echo $ss_owned ? ' disabled aria-disabled="true"' : ''; ?> data-plan-id="<?php echo esc_attr($plan->id); ?>">
    95                             <?php echo $ss_owned ? esc_html__("Current Plan", "subscription-system") : esc_html(subs_sys_plan_button_label($plan)); ?>
    96                         </button>
    97                     </div>
    98                 </div>
    99             <?php endforeach; ?>
     34                foreach ($plans as $index => $plan) :
     35                    $is_featured = ($index === $featured_index) ? 'ss-featured' : '';
     36                    include __DIR__ . '/components/pricing-plan-card.php';
     37                endforeach;
     38                ?>
     39            </div>
    10040        </div>
    10141    <?php endif; ?>
    10242</div>
    103 
    104 <script>
    105 jQuery(document).ready(function($) {
    106     // Initialize Stripe
    107     var stripe = Stripe('<?php echo esc_js( class_exists('Subs_Sys_Stripe_Sync') ? Subs_Sys_Stripe_Sync::get_publishable_key() : get_option('subscription_stripe_publishable_key') ); ?>');
    108    
    109     $('.subscribe-button').on('click', function() {
    110         var planId = $(this).data('plan-id');
    111         var button = $(this);
    112         var planName = button.closest('.ss-pricing-plan').find('h3').text();
    113         var planPrice = button.closest('.ss-pricing-plan').find('.ss-price').text().replace(/[^0-9.]/g, '');
    114        
    115         // Check if user is logged in
    116         <?php if (!is_user_logged_in()): ?>
    117             // User is not logged in, redirect to register page
    118             window.location.href = '<?php echo esc_js(get_permalink(get_option('register_page'))); ?>?plan_id=' + planId;
    119             return;
    120         <?php else: ?>
    121             // Check if user already has this subscription
    122             $.ajax({
    123                 url: '<?php echo esc_url(admin_url('admin-ajax.php')); ?>',
    124                 type: 'POST',
    125                 data: {
    126                     action: 'check_user_subscription',
    127                     plan_id: planId,
    128                     nonce: '<?php echo esc_attr(wp_create_nonce('subscription_check_nonce')); ?>'
    129                 },
    130                 success: function(response) {
    131                     if (response.success && response.data.has_subscription) {
    132                         // User already has this subscription, redirect to account page
    133                         window.location.href = '<?php echo esc_js(get_permalink(get_option('account_page'))); ?>?tab=subscription';
    134                     } else if (response.success && response.data.has_other_plan) {
    135                         // One active plan at a time: this click is a switch, not a purchase.
    136                         var current = response.data.current_plan_name || 'your current plan';
    137                         if (confirm('You are currently on "' + current + '". Switch to "' + planName + '" instead?\n\nUpgrades apply immediately with prorated billing. Downgrades keep your current benefits until the end of the period you already paid for.')) {
    138                             proceedWithPayment(planId, button, planName, planPrice);
    139                         }
    140                     } else {
    141                         // User is logged in but doesn't have this subscription, proceed with payment
    142                         proceedWithPayment(planId, button, planName, planPrice);
    143                     }
    144                 },
    145                 error: function() {
    146                     // On error, proceed with payment flow
    147                     proceedWithPayment(planId, button, planName, planPrice);
    148                 }
    149             });
    150         <?php endif; ?>
    151     });
    152    
    153     function proceedWithPayment(planId, button, planName, planPrice) {
    154         // Remember this plan's own wording — each plan may override its button
    155         // label, so restoring a hardcoded "Subscribe Now" here would quietly
    156         // rewrite the label the site owner chose.
    157         var originalLabel = button.text();
    158 
    159         // Disable button and show loading state
    160         button.prop('disabled', true).addClass('loading').text('Processing...');
    161 
    162         // Ask our own server to create the Checkout Session. The Stripe secret key
    163         // never leaves the server - the browser only ever receives back a checkout_url.
    164         $.ajax({
    165             url: '<?php echo esc_url(admin_url('admin-ajax.php')); ?>',
    166             type: 'POST',
    167             data: {
    168                 action: 'create_checkout_session',
    169                 plan_id: planId,
    170                 nonce: '<?php echo esc_attr(wp_create_nonce('subscription_checkout_nonce')); ?>'
    171             },
    172             success: function(response) {
    173                 if (response.success && response.data.checkout_url) {
    174                     window.location.href = response.data.checkout_url;
    175                 } else {
    176                     var message = (response.data && response.data.message) ? response.data.message : 'Unknown error occurred';
    177                     alert('Error: ' + message);
    178                     button.prop('disabled', false).removeClass('loading').text(originalLabel);
    179                 }
    180             },
    181             error: function() {
    182                 alert('Error: Unable to start checkout. Please try again.');
    183                 button.prop('disabled', false).removeClass('loading').text(originalLabel);
    184             }
    185         });
    186     }
    187 });
    188 </script>
  • subscription-system/trunk/public/partials/pricing-plans-layout3.php

    r3650727 r3651653  
    22/**
    33 * Template for displaying pricing plans (Layout 3)
     4 *
     5 * The "Spotlight": dark stage panel with one lit, elevated featured card. Structure is shared with the other layouts via
     6 * components/pricing-plan-card.php; this file only sets the wrapper.
    47 *
    58 * @package    Subscription_System
    … …  
    1114}
    1215
    13 // Enqueue the layout-specific CSS
    14 wp_enqueue_style('subscription-pricing-layout3', plugin_dir_url(dirname(__FILE__)) . 'css/pricing-layout3.css', array('subs-sys-tokens'), '1.0.0', 'all');
     16$layout      = 'layout3';
     17$ss_behavior = isset($ss_behavior) ? $ss_behavior : '';
     18require __DIR__ . '/components/pricing-assets.php';
    1519
    16 // Add responsive viewport meta tag if not already present
    17  
    18 
    19 // Enqueue Stripe JS
    20 wp_enqueue_script('stripe-js', 'https://js.stripe.com/v3/', array(), null, true);
    21 
    22 // Enqueue our custom JS
    23 wp_enqueue_script('subscription-pricing-js', plugin_dir_url(dirname(__FILE__)) . 'js/subscription-pricing.js', array('jquery', 'stripe-js'), '1.0.0', true);
    24 
    25 // Localize script with necessary data
    26 wp_localize_script('subscription-pricing-js', 'subscription_data', array(
    27     'ajax_url' => admin_url('admin-ajax.php'),
    28     'stripe_publishable_key' => class_exists('Subs_Sys_Stripe_Sync') ? Subs_Sys_Stripe_Sync::get_publishable_key() : get_option('subscription_stripe_publishable_key'),
    29     'checkout_nonce' => wp_create_nonce('subscription_checkout_nonce'),
    30     'is_user_logged_in' => is_user_logged_in(),
    31     'register_page_url' => get_permalink(get_option('register_page')),
    32     'account_page_url' => get_permalink(get_option('account_page')),
    33     'thank_you_page_url' => get_permalink(get_option('thank_you_page')),
    34     'pricing_plans_page_url' => get_permalink(get_option('pricing_plans_page')),
    35     'nonce' => wp_create_nonce('subscription_check_nonce'),
    36     'subscribe_now_text' => esc_html__('Subscribe Now', 'subscription-system')
    37 ));
     20$ss_design_classes = class_exists('Subs_Sys_Layout_Settings') ? Subs_Sys_Layout_Settings::wrapper_classes($ss_behavior) : '';
     21$ss_design_attrs   = class_exists('Subs_Sys_Layout_Settings') ? Subs_Sys_Layout_Settings::data_attrs() : '';
    3822?>
    3923
    40 <div class="subscription-pricing-plans layout3 <?php echo esc_attr($orientation_class); ?> columns-<?php echo esc_attr($atts['columns']); ?> ss-responsive">
     24<div class="subscription-pricing-plans layout3 <?php echo esc_attr($orientation_class); ?> columns-<?php echo esc_attr($atts['columns']); ?> ss-responsive <?php echo esc_attr($ss_design_classes); ?>"<?php echo $ss_design_attrs; // phpcs:ignore WordPress.Security.EscapeOutput -- escaped attr-by-attr in data_attrs(). ?>>
    4125    <?php if (empty($plans)) : ?>
    4226        <p class="no-plans"><?php esc_html_e('No subscription plans available.', 'subscription-system'); ?></p>
    … …  
    4428        <?php include __DIR__ . '/components/plan-filter-bar.php'; ?>
    4529        <div class="ss-pricing-container ss-layout3">
    46             <?php
    47             $featured_index = subs_sys_featured_plan_index($plans); // Middle plan is featured
    48             foreach ($plans as $index => $plan) :
    49                 $is_featured = ($index === $featured_index) ? 'ss-featured' : '';
    50             ?>
    51                 <div class="ss-pricing-plan <?php echo esc_attr($is_featured); ?>" data-ss-category="<?php echo esc_attr($plan->category_name ?? ''); ?>">
    52                     <div class="ss-plan-header">
    53                         <h3><?php echo esc_html($plan->plan_name); ?></h3>
    54                         <div class="ss-price">$<?php echo esc_html(number_format($plan->price, 2)); ?><span><?php echo esc_html(subs_sys_plan_period_label($plan)); ?></span></div>
    55                     </div>
    56                     <div class="ss-plan-features">
    57                         <ul>
    58                             <?php
    59                            
    60                             // If plan has features, display them
    61                             if (!empty($plan->description)) {
    62                                 try {
    63                                     // Clean up the description string - remove escaped quotes
    64                                     $clean_description = str_replace('\"', '"', $plan->description);
    65                                    
    66                                     // Try to decode the JSON
    67                                     $features = json_decode($clean_description);
    68                                    
    69                                     // If JSON decode failed, try other methods
    70                                     if ($features === null) {
    71                                         // Try to unserialize
    72                                         $features = maybe_unserialize($clean_description);
    73                                        
    74                                         // If still not an array, use as single item
    75                                         if (!is_array($features)) {
    76                                             $features = array($clean_description);
    77                                         }
    78                                     }
    79                                    
    80                                     // Display features without dashicons
    81                                     if (is_array($features) || is_object($features)) {
    82                                         foreach ($features as $feature) {
    83                                             echo '<li>' . esc_html($feature) . '</li>';
    84                                         }
    85                                     } else {
    86                                         echo '<li>' . esc_html($clean_description) . '</li>';
    87                                     }
    88                                 } catch (Exception $e) {
    89                                     // Fallback if any error occurs
    90                                     echo '<li>' . esc_html($plan->description) . '</li>';
    91                                 }
    92                             }
    93                             ?>
    94                         </ul>
    95                     </div>
    96                     <div class="ss-plan-footer">
    97                         <?php $ss_owned = is_user_logged_in() && class_exists('Subs_Sys_Subscription_Repository') && Subs_Sys_Subscription_Repository::owns_plan(get_current_user_id(), $plan->id); ?>
    98                         <button class="ss-pricing-button subscribe-button<?php echo $ss_owned ? ' ss-button--owned' : ''; ?>"<?php echo $ss_owned ? ' disabled aria-disabled="true"' : ''; ?> data-plan-id="<?php echo esc_attr($plan->id); ?>">
    99                             <?php echo $ss_owned ? esc_html__("Current Plan", "subscription-system") : esc_html(subs_sys_plan_button_label($plan)); ?>
    100                         </button>
    101                     </div>
    102                 </div>
    103             <?php endforeach; ?>
     30            <div class="ss-pricing-plans">
     31                <?php
     32                $featured_index = subs_sys_featured_plan_index($plans);
     33
     34                foreach ($plans as $index => $plan) :
     35                    $is_featured = ($index === $featured_index) ? 'ss-featured' : '';
     36                    include __DIR__ . '/components/pricing-plan-card.php';
     37                endforeach;
     38                ?>
     39            </div>
    10440        </div>
    10541    <?php endif; ?>
    10642</div>
    107 <script>
    108 jQuery(document).ready(function($) {
    109     // Initialize Stripe
    110     var stripe = Stripe('<?php echo esc_js( class_exists('Subs_Sys_Stripe_Sync') ? Subs_Sys_Stripe_Sync::get_publishable_key() : get_option('subscription_stripe_publishable_key') ); ?>');
    111    
    112     $('.subscribe-button').on('click', function() {
    113         var planId = $(this).data('plan-id');
    114         var button = $(this);
    115         var planName = button.closest('.ss-pricing-plan').find('h3').text();
    116         var planPrice = button.closest('.ss-pricing-plan').find('.ss-price').text().replace(/[^0-9.]/g, '');
    117        
    118         // Check if user is logged in
    119         <?php if (!is_user_logged_in()): ?>
    120             // User is not logged in, redirect to register page
    121             window.location.href = '<?php echo esc_js(get_permalink(get_option('register_page'))); ?>?plan_id=' + planId;
    122             return;
    123         <?php else: ?>
    124             // Check if user already has this subscription
    125             $.ajax({
    126                 url: '<?php echo esc_url(admin_url('admin-ajax.php')); ?>',
    127                 type: 'POST',
    128                 data: {
    129                     action: 'check_user_subscription',
    130                     plan_id: planId,
    131                     nonce: '<?php echo esc_attr(wp_create_nonce('subscription_check_nonce')); ?>'
    132                 },
    133                 success: function(response) {
    134                     if (response.success && response.data.has_subscription) {
    135                         // User already has this subscription, redirect to account page
    136                         window.location.href = '<?php echo esc_js(get_permalink(get_option('account_page'))); ?>?tab=subscription';
    137                     } else if (response.success && response.data.has_other_plan) {
    138                         // One active plan at a time: this click is a switch, not a purchase.
    139                         var current = response.data.current_plan_name || 'your current plan';
    140                         if (confirm('You are currently on "' + current + '". Switch to "' + planName + '" instead?\n\nUpgrades apply immediately with prorated billing. Downgrades keep your current benefits until the end of the period you already paid for.')) {
    141                             proceedWithPayment(planId, button, planName, planPrice);
    142                         }
    143                     } else {
    144                         // User is logged in but doesn't have this subscription, proceed with payment
    145                         proceedWithPayment(planId, button, planName, planPrice);
    146                     }
    147                 },
    148                 error: function() {
    149                     // On error, proceed with payment flow
    150                     proceedWithPayment(planId, button, planName, planPrice);
    151                 }
    152             });
    153         <?php endif; ?>
    154     });
    155    
    156     function proceedWithPayment(planId, button, planName, planPrice) {
    157         // Remember this plan's own wording — each plan may override its button
    158         // label, so restoring a hardcoded "Subscribe Now" here would quietly
    159         // rewrite the label the site owner chose.
    160         var originalLabel = button.text();
    161 
    162         // Disable button and show loading state
    163         button.prop('disabled', true).addClass('loading').text('Processing...');
    164 
    165         // Ask our own server to create the Checkout Session. The Stripe secret key
    166         // never leaves the server - the browser only ever receives back a checkout_url.
    167         $.ajax({
    168             url: '<?php echo esc_url(admin_url('admin-ajax.php')); ?>',
    169             type: 'POST',
    170             data: {
    171                 action: 'create_checkout_session',
    172                 plan_id: planId,
    173                 nonce: '<?php echo esc_attr(wp_create_nonce('subscription_checkout_nonce')); ?>'
    174             },
    175             success: function(response) {
    176                 if (response.success && response.data.checkout_url) {
    177                     window.location.href = response.data.checkout_url;
    178                 } else {
    179                     var message = (response.data && response.data.message) ? response.data.message : '<?php esc_html_e('Unknown error occurred', 'subscription-system'); ?>';
    180                     alert(message);
    181                     button.prop('disabled', false).removeClass('loading').text(originalLabel);
    182                 }
    183             },
    184             error: function() {
    185                 alert('<?php esc_html_e('Error connecting to server. Please try again.', 'subscription-system'); ?>');
    186                 button.prop('disabled', false).removeClass('loading').text(originalLabel);
    187             }
    188         });
    189     }
    190 });
    191 </script>
  • subscription-system/trunk/subscription-system.php

    r3650727 r3651653  
    1717 * Plugin URI:        https://mediahubster.in/subscription-system
    1818 * Description:       A comprehensive subscription management system for WordPress.
    19  * Version:           1.0.21
     19 * Version:           1.0.22
    2020 * Author:            Joe Youngblood
    2121 * Author URI:        https://mediahubster.in
    … …  
    9797
    9898// Define plugin constants
    99 define('SUBS_SYS_VERSION', '1.0.21');
     99define('SUBS_SYS_VERSION', '1.0.22');
    100100define('SUBS_SYS_PLUGIN_DIR', plugin_dir_path(__FILE__));
    101101define('SUBS_SYS_PLUGIN_URL', plugin_dir_url(__FILE__));
    … …  
    227227
    228228    return (int) ceil( $count / 2 ) - 1;
     229}
     230
     231/**
     232 * A plan's feature lines as a flat array of strings.
     233 *
     234 * The description column has held three shapes over the plugin's life: a JSON
     235 * array, a PHP-serialized array, and free text. Each layout template used to
     236 * carry its own copy of this decoding cascade; one copy lives here now so the
     237 * card component (and anything else) always parses identically.
     238 *
     239 * @param object $plan Plan row.
     240 * @return string[] Feature lines, possibly empty.
     241 */
     242function subs_sys_plan_features( $plan ) {
     243    $raw = isset( $plan->description ) ? trim( (string) $plan->description ) : '';
     244
     245    if ( '' === $raw ) {
     246        return array();
     247    }
     248
     249    $clean    = str_replace( '\\"', '"', $raw );
     250    $features = json_decode( $clean );
     251
     252    if ( null === $features ) {
     253        $features = maybe_unserialize( $clean );
     254    }
     255
     256    if ( ! is_array( $features ) && ! is_object( $features ) ) {
     257        $features = array( $clean );
     258    }
     259
     260    $out = array();
     261    foreach ( (array) $features as $feature ) {
     262        $feature = trim( (string) $feature );
     263        if ( '' !== $feature ) {
     264            $out[] = $feature;
     265        }
     266    }
     267    return $out;
    229268}
    230269
    … …  
    487526                'finishing'         => __( 'Setting up your subscription…', 'subscription-system' ),
    488527                'to_checkout'       => __( 'Taking you to secure checkout…', 'subscription-system' ),
     528                'done'              => __( 'All set ✓', 'subscription-system' ),
    489529                'auth_required'     => __( 'Please log in or create an account to continue.', 'subscription-system' ),
    490530                'login_subscribe'   => __( 'Log in & Subscribe', 'subscription-system' ),
  • subscription-system/trunk/uninstall.php

    r3650727 r3651653  
    6262    'ss_migration_completed_v1',
    6363    'ss_unified_webhook_secret',
     64    'ss_paywall_teaser_word_count', // Removed paywall feature; may linger on old installs.
    6465    'enable_stripe',
    6566    'stripe_mode',
Note: See TracChangeset for help on using the changeset viewer.