Plugin Directory

Changeset 3598150


Ignore:
Timestamp:
07/06/2026 05:21:45 PM (4 months ago)
Author:
subscriptionplugin
Message:

Release version 1.0.18

Location:
subscription-system
Files:
12 added
21 edited
5 copied

Legend:

Unmodified
Added
Removed
  • subscription-system/tags/1.0.18/README.txt

    r3587496 r3598150  
    66Tested up to: 7.0
    77Requires PHP: 8.0
    8 Stable tag: 1.0.17
     8Stable tag: 1.0.18
    99License: GPLv2 or later
    1010License URI: http://www.gnu.org/licenses/gpl-2.0.html
    … …  
    8585
    8686== Changelog ==
     87
     88= 1.0.18 =
     89**Subscribe Button & Modal — Category / Tag Interest Subscriptions**
     90* Added new "Subscribe Button & Modal" admin settings tab with full trigger, display, and context configuration
     91* Floating subscribe button with configurable position (bottom-right, bottom-left, bottom-center)
     92* Auto-open modal on page load with configurable delay
     93* Shortcode `[subscriptionsystem_subscribe]` to place a subscribe button anywhere
     94* Context-aware modal: automatically detects and pre-selects the current post's categories and tags
     95* Logged-in user experience: hides name/email fields, greets user by name, and auto-fills their details
     96* Guest (not-logged-in) modal now shows a "Log in / Not now" panel instead of the full form
     97* After login, modal automatically reopens on the same page via `?ss_subscribe=1` URL parameter
     98* Session parameter is silently removed from the URL via `history.replaceState` after modal opens
     99
     100**Content Plans (Subscribe to Updates)**
     101* Modal now sources plans exclusively from **Subscription → Content Plans** instead of generic plans
     102* Strict plan filtering: only shows plans whose categories/tags exactly match the current post
     103* Each plan card now displays which categories or tags it covers (e.g. "Category: Technology")
     104* Conflict prevention: categories and tags already assigned to one active plan are shown as disabled (greyed out with "Used by: [Plan Name]" badge) when creating/editing another plan
     105* Backend validation blocks saving a plan with duplicate categories/tags — clear error message lists every conflict
     106* Content Plans admin list now has a **Stripe** column showing connection status (Connected / Mock / None)
     107* "Sync with Stripe" link on each Content Plan — creates/updates Stripe Product and Price, saves IDs back to DB
     108* Free content plans skip Stripe price creation (product only)
     109
     110**Checkout & Payment**
     111* Interest subscription checkout now creates a real **Stripe Checkout Session** and redirects users to Stripe's hosted payment page
     112* Plan lookup fixed to read from `subscription_content_plans` (or falls back to `subscription_plans` for legacy records)
     113* Uses pre-synced `stripe_price_id` when available; falls back to inline `price_data` for unsynced plans
     114* New `/?action=interest_checkout_success` handler: verifies Stripe session, activates the subscription, and shows a styled success page
     115* Cancelled Stripe sessions return to the checkout info page with a clear "Payment was cancelled" notice
     116* Fallback info page (when Stripe is not configured) now shows an admin-only "Configure Stripe →" button
     117
     118**Subscriber Experience Improvements**
     119* Modal categories/tags list filtered to show only terms assigned to the current post (not all site terms)
     120* Section labels update contextually: "Categories in this post" / "Tags in this post" vs. "Select Categories"
     121* All post-specific categories and tags are pre-checked by default
     122* Intro text adapts: "Subscribe to get notified when new content is published in these topics."
     123* Empty categories or tags sections are hidden rather than showing a placeholder
     124
     125**Plan Management (Subscription Plans)**
     126* Add/Edit plan forms include Status (Active/Inactive), Max Categories, and Max Tags fields
     127* Plans list table shows Interest Limits ("Cat: X, Tags: Y") and Status badge
     128* Backend correctly saves `features` JSON with `max_categories`, `max_tags`, and `status` on both create and update
    87129
    88130= 1.0.17 =
    … …  
    215257== Upgrade Notice ==
    216258
     259= 1.0.18 =
     260Major feature update. Introduces the Subscribe Button & Modal system with floating buttons, auto-open, and context-aware category/tag subscriptions. Content Plans now power the modal with strict per-post plan filtering, Stripe sync, and a full Stripe Checkout payment flow. Guest users see a "Log in / Not now" prompt. Includes plan conflict prevention, improved plan cards, and a polished post-payment success page.
     261
    217262= 1.0.17 =
    218263Recommended update for all users. Improves compatibility with the latest WordPress versions (including WordPress 7.0), enhances codebase structure, and includes general stability and performance improvements.
  • subscription-system/tags/1.0.18/admin/class-subs-sys-admin.php

    r3463757 r3598150  
    9797        // 6. Content Subscribers (content-specific subscribers)
    9898        $this->add_content_subscribers_submenu();
     99
     100        // 6b. Interest Subscribers (category/tag popup subscriptions)
     101        $this->add_interest_subscribers_submenu();
    99102       
    100103        // 7. Channels
    … …  
    991994
    992995    /**
     996     * Add Interest Subscribers submenu.
     997     */
     998    public function add_interest_subscribers_submenu() {
     999        add_submenu_page(
     1000            'subscription-dashboard',
     1001            esc_html__( 'Interest Subscribers', 'subscription-system' ),
     1002            esc_html__( 'Interest Subscribers', 'subscription-system' ),
     1003            'manage_options',
     1004            'interest-subscribers',
     1005            array( $this, 'render_interest_subscribers_page' )
     1006        );
     1007    }
     1008
     1009    /**
     1010     * Render Interest Subscribers admin page.
     1011     */
     1012    public function render_interest_subscribers_page() {
     1013        include plugin_dir_path( dirname( __FILE__ ) ) . 'admin/partials/interest-subscribers-display.php';
     1014    }
     1015
     1016    /**
    9931017     * Add Email Logs submenu to the Subscriptions menu
    9941018     */
    … …  
    10871111            )
    10881112        );
     1113
     1114        // Subscribe Button & Modal settings
     1115        register_setting( 'subscription_modal_settings', 'subs_modal_auto_open', array( 'sanitize_callback' => 'absint', 'default' => 0 ) );
     1116        register_setting( 'subscription_modal_settings', 'subs_modal_auto_open_delay', array( 'sanitize_callback' => 'absint', 'default' => 3 ) );
     1117        register_setting( 'subscription_modal_settings', 'subs_modal_floating_btn', array( 'sanitize_callback' => 'absint', 'default' => 0 ) );
     1118        register_setting( 'subscription_modal_settings', 'subs_modal_floating_btn_text', array( 'sanitize_callback' => 'sanitize_text_field', 'default' => 'Subscribe' ) );
     1119        register_setting( 'subscription_modal_settings', 'subs_modal_floating_btn_position', array( 'sanitize_callback' => 'sanitize_text_field', 'default' => 'bottom-right' ) );
     1120        register_setting( 'subscription_modal_settings', 'subs_modal_display_all_posts', array( 'sanitize_callback' => 'absint', 'default' => 0 ) );
     1121        register_setting( 'subscription_modal_settings', 'subs_modal_display_all_pages', array( 'sanitize_callback' => 'absint', 'default' => 0 ) );
     1122        register_setting( 'subscription_modal_settings', 'subs_modal_display_single_posts', array( 'sanitize_callback' => 'absint', 'default' => 0 ) );
     1123        register_setting( 'subscription_modal_settings', 'subs_modal_display_single_pages', array( 'sanitize_callback' => 'absint', 'default' => 0 ) );
     1124        register_setting( 'subscription_modal_settings', 'subs_modal_display_post_types', array(
     1125            'sanitize_callback' => function( $val ) {
     1126                return is_array( $val ) ? array_map( 'sanitize_key', $val ) : array();
     1127            },
     1128            'default' => array(),
     1129        ) );
     1130        register_setting( 'subscription_modal_settings', 'subs_modal_display_specific_pages', array(
     1131            'sanitize_callback' => function( $val ) {
     1132                return is_array( $val ) ? array_map( 'absint', $val ) : array();
     1133            },
     1134            'default' => array(),
     1135        ) );
     1136        register_setting( 'subscription_modal_settings', 'subs_modal_context_categories', array( 'sanitize_callback' => 'absint', 'default' => 1 ) );
     1137        register_setting( 'subscription_modal_settings', 'subs_modal_context_tags', array( 'sanitize_callback' => 'absint', 'default' => 1 ) );
     1138        register_setting( 'subscription_modal_settings', 'subs_modal_hide_fields_logged_in', array( 'sanitize_callback' => 'absint', 'default' => 1 ) );
    10891139
    10901140        // Pause subscription setting
    … …  
    13661416    public function register_ajax_handlers() {
    13671417        add_action('wp_ajax_sync_plan_with_stripe', array($this, 'ajax_sync_plan_with_stripe'));
     1418        add_action('wp_ajax_sync_content_plan_with_stripe', array($this, 'ajax_sync_content_plan_with_stripe'));
    13681419        add_action('wp_ajax_test_stripe_connection', array($this, 'ajax_test_stripe_connection'));
    13691420        add_action('wp_ajax_install_stripe_sdk', array($this, 'ajax_install_stripe_sdk'));
    … …  
    13911442           
    13921443            // Get form data
     1444            $raw_features = isset( $_POST['plan_features_json'] ) ? sanitize_text_field( wp_unslash( $_POST['plan_features_json'] ) ) : '';
     1445            $features_arr = json_decode( $raw_features, true );
     1446            if ( ! is_array( $features_arr ) ) {
     1447                $features_arr = array( 'max_categories' => 0, 'max_tags' => 0 );
     1448            }
     1449            $features_arr['max_categories'] = max( 0, (int) ( $features_arr['max_categories'] ?? 0 ) );
     1450            $features_arr['max_tags']       = max( 0, (int) ( $features_arr['max_tags']       ?? 0 ) );
     1451
    13931452            $plan_data = array(
    1394                 'plan_name' => isset($_POST['plan_name']) ? sanitize_text_field($_POST['plan_name']) : '',
     1453                'plan_name'   => isset($_POST['plan_name']) ? sanitize_text_field($_POST['plan_name']) : '',
    13951454                'description' => isset($_POST['description']) ? wp_kses_post($_POST['description']) : '',
    1396                 'price' => isset($_POST['price']) ? floatval($_POST['price']) : 0,
    1397                 'duration' => isset($_POST['duration']) ? intval($_POST['duration']) : 30,
     1455                'price'       => isset($_POST['price']) ? floatval($_POST['price']) : 0,
     1456                'duration'    => isset($_POST['duration']) ? intval($_POST['duration']) : 30,
    13981457                'category_id' => isset($_POST['category_id']) ? intval($_POST['category_id']) : 0,
    1399                 'status' => isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'active'
     1458                'status'      => isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'active',
     1459                'features'    => wp_json_encode( $features_arr ),
    14001460            );
    14011461           
    … …  
    14401500            }
    14411501           
     1502            $raw_features_edit = isset( $_POST['plan_features_json'] ) ? sanitize_text_field( wp_unslash( $_POST['plan_features_json'] ) ) : '';
     1503            $features_arr_edit = json_decode( $raw_features_edit, true );
     1504            if ( ! is_array( $features_arr_edit ) ) {
     1505                $features_arr_edit = array( 'max_categories' => 0, 'max_tags' => 0 );
     1506            }
     1507            $features_arr_edit['max_categories'] = max( 0, (int) ( $features_arr_edit['max_categories'] ?? 0 ) );
     1508            $features_arr_edit['max_tags']       = max( 0, (int) ( $features_arr_edit['max_tags']       ?? 0 ) );
     1509
    14421510            $plan_data = array(
    1443                 'plan_name' => isset($_POST['plan_name']) ? sanitize_text_field($_POST['plan_name']) : '',
     1511                'plan_name'   => isset($_POST['plan_name']) ? sanitize_text_field($_POST['plan_name']) : '',
    14441512                'description' => isset($_POST['description']) ? wp_kses_post($_POST['description']) : '',
    1445                 'price' => isset($_POST['price']) ? floatval($_POST['price']) : 0,
    1446                 'duration' => isset($_POST['duration']) ? intval($_POST['duration']) : 30,
     1513                'price'       => isset($_POST['price']) ? floatval($_POST['price']) : 0,
     1514                'duration'    => isset($_POST['duration']) ? intval($_POST['duration']) : 30,
    14471515                'category_id' => isset($_POST['category_id']) ? intval($_POST['category_id']) : 0,
    1448                 'status' => isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'active',
    1449                 'updated_at' => current_time('mysql')
     1516                'status'      => isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'active',
     1517                'features'    => wp_json_encode( $features_arr_edit ),
     1518                'updated_at'  => current_time('mysql'),
    14501519            );
    14511520           
    … …  
    22412310        // Prepare the data
    22422311        $data = array(
    2243             'plan_name' => sanitize_text_field($plan_data['plan_name']),
     2312            'plan_name'   => sanitize_text_field($plan_data['plan_name']),
    22442313            'description' => isset($plan_data['description']) ? wp_kses_post($plan_data['description']) : '',
    2245             'price' => floatval($plan_data['price']),
    2246             'duration' => isset($plan_data['duration']) ? intval($plan_data['duration']) : 30,
     2314            'price'       => floatval($plan_data['price']),
     2315            'duration'    => isset($plan_data['duration']) ? intval($plan_data['duration']) : 30,
    22472316            'category_id' => isset($plan_data['category_id']) ? intval($plan_data['category_id']) : 0,
    2248             'status' => isset($plan_data['status']) ? sanitize_text_field($plan_data['status']) : 'active',
    2249             'created_at' => current_time('mysql'),
    2250             'updated_at' => current_time('mysql')
     2317            'status'      => isset($plan_data['status']) ? sanitize_text_field($plan_data['status']) : 'active',
     2318            'features'    => isset($plan_data['features']) ? $plan_data['features'] : wp_json_encode( array( 'max_categories' => 0, 'max_tags' => 0 ) ),
     2319            'created_at'  => current_time('mysql'),
     2320            'updated_at'  => current_time('mysql'),
    22512321        );
    22522322       
    … …  
    22572327            $table_name,
    22582328            $data,
    2259             array('%s', '%s', '%f', '%d', '%d', '%s', '%s', '%s')
     2329            array('%s', '%s', '%f', '%d', '%d', '%s', '%s', '%s', '%s')
    22602330        );
    22612331       
    … …  
    26142684
    26152685    /**
     2686     * AJAX handler: sync a content plan with Stripe.
     2687     * Mirrors ajax_sync_plan_with_stripe but operates on subscription_content_plans.
     2688     */
     2689    public function ajax_sync_content_plan_with_stripe() {
     2690        // Nonce check.
     2691        $nonce_ok = ( isset( $_POST['nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'sync_content_plan_with_stripe' ) );
     2692        if ( ! $nonce_ok ) {
     2693            wp_send_json_error( array( 'message' => 'Security check failed.', 'mock_mode' => true ) );
     2694            return;
     2695        }
     2696
     2697        if ( ! current_user_can( 'manage_options' ) ) {
     2698            wp_send_json_error( array( 'message' => 'Insufficient permissions.', 'mock_mode' => true ) );
     2699            return;
     2700        }
     2701
     2702        $plan_id = isset( $_POST['plan_id'] ) ? absint( $_POST['plan_id'] ) : 0;
     2703        if ( ! $plan_id ) {
     2704            wp_send_json_error( array( 'message' => 'Invalid plan ID.', 'mock_mode' => true ) );
     2705            return;
     2706        }
     2707
     2708        global $wpdb;
     2709        $table = $wpdb->prefix . 'subscription_content_plans';
     2710        $plan  = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table WHERE id = %d", $plan_id ) );
     2711
     2712        if ( ! $plan ) {
     2713            wp_send_json_error( array( 'message' => 'Content plan not found.', 'mock_mode' => true ) );
     2714            return;
     2715        }
     2716
     2717        // Helper: save IDs back to DB and return success.
     2718        $save_and_respond = function( $product_id, $price_id, $mock_mode, $message, $extra = array() ) use ( $wpdb, $table, $plan_id ) {
     2719            $wpdb->update(
     2720                $table,
     2721                array( 'stripe_product_id' => $product_id, 'stripe_price_id' => $price_id ),
     2722                array( 'id' => $plan_id ),
     2723                array( '%s', '%s' ),
     2724                array( '%d' )
     2725            );
     2726            wp_send_json_success( array_merge( array(
     2727                'product_id' => $product_id,
     2728                'price_id'   => $price_id,
     2729                'mock_mode'  => $mock_mode,
     2730                'message'    => $message,
     2731            ), $extra ) );
     2732        };
     2733
     2734        // Determine billing interval from duration.
     2735        $duration       = (int) ( $plan->duration ?? 30 );
     2736        $interval       = 'month';
     2737        $interval_count = 1;
     2738        if ( $duration <= 31 )       { $interval = 'month'; $interval_count = 1; }
     2739        elseif ( $duration <= 93 )   { $interval = 'month'; $interval_count = 3; }
     2740        elseif ( $duration <= 186 )  { $interval = 'month'; $interval_count = 6; }
     2741        else                         { $interval = 'year';  $interval_count = 1; }
     2742
     2743        // Check Stripe is enabled.
     2744        $stripe_enabled = get_option( 'subscription_stripe_enabled', 0 );
     2745
     2746        if ( ! $stripe_enabled ) {
     2747            $mock_prod  = 'mock_prod_'  . substr( md5( 'cpln_' . $plan_id ), 0, 10 );
     2748            $mock_price = 'mock_price_' . substr( md5( 'cpln_' . $plan_id . '_price' ), 0, 10 );
     2749            $save_and_respond( $mock_prod, $mock_price, true, 'Content plan synced with mock Stripe. Enable Stripe in Payment Settings for real integration.' );
     2750            return;
     2751        }
     2752
     2753        // Resolve API key.
     2754        $test_mode = get_option( 'subscription_stripe_test_mode', 1 );
     2755        $api_key   = $test_mode
     2756            ? get_option( 'subscription_stripe_test_secret_key', '' )
     2757            : get_option( 'subscription_stripe_live_secret_key', '' );
     2758        if ( empty( $api_key ) ) {
     2759            $api_key = get_option( 'subscription_stripe_secret_key', '' );
     2760        }
     2761
     2762        if ( empty( $api_key ) ) {
     2763            wp_send_json_error( array( 'message' => 'Stripe API key is not set. Please configure it in Payment Settings.', 'mock_mode' => true ) );
     2764            return;
     2765        }
     2766
     2767        // Load Stripe SDK.
     2768        if ( ! class_exists( '\Stripe\Stripe' ) ) {
     2769            $this->ensure_stripe_sdk_loaded();
     2770        }
     2771
     2772        if ( ! class_exists( '\Stripe\Stripe' ) ) {
     2773            $mock_prod  = 'mock_prod_'  . substr( md5( 'cpln_' . $plan_id ), 0, 10 );
     2774            $mock_price = 'mock_price_' . substr( md5( 'cpln_' . $plan_id . '_price' ), 0, 10 );
     2775            $save_and_respond( $mock_prod, $mock_price, true, 'Content plan synced with mock Stripe. SDK could not be loaded.' );
     2776            return;
     2777        }
     2778
     2779        try {
     2780            \Stripe\Stripe::setApiKey( $api_key );
     2781
     2782            $product_id = 'prod_' . substr( md5( 'cpln_' . $plan_id ), 0, 10 );
     2783
     2784            // Create or update the Stripe product.
     2785            try {
     2786                $product       = \Stripe\Product::retrieve( $product_id );
     2787                $product->name = $plan->plan_name;
     2788                if ( ! empty( $plan->description ) ) {
     2789                    $product->description = wp_strip_all_tags( $plan->description );
     2790                }
     2791                $product->save();
     2792            } catch ( \Exception $e ) {
     2793                $product = \Stripe\Product::create( array(
     2794                    'id'          => $product_id,
     2795                    'name'        => $plan->plan_name,
     2796                    'description' => wp_strip_all_tags( $plan->description ?? '' ),
     2797                    'metadata'    => array(
     2798                        'content_plan_id' => $plan_id,
     2799                        'content_type'    => $plan->content_type,
     2800                    ),
     2801                ) );
     2802            }
     2803
     2804            // Free plans: no Stripe price needed.
     2805            if ( (float) $plan->price <= 0 ) {
     2806                $save_and_respond( $product->id, 'free', false, 'Content plan (free) synced with Stripe as a product.' );
     2807                return;
     2808            }
     2809
     2810            $price_id = 'price_' . substr( md5( 'cpln_' . $plan_id . '_' . $plan->price ), 0, 10 );
     2811
     2812            // Create or reuse the Stripe price.
     2813            try {
     2814                $price = \Stripe\Price::retrieve( $price_id );
     2815                // If amount changed, create a new price.
     2816                if ( $price->unit_amount !== (int) ( $plan->price * 100 ) ) {
     2817                    $price = \Stripe\Price::create( array(
     2818                        'product'   => $product->id,
     2819                        'unit_amount' => (int) ( $plan->price * 100 ),
     2820                        'currency'  => strtolower( get_option( 'subscription_currency', 'usd' ) ),
     2821                        'recurring' => array( 'interval' => $interval, 'interval_count' => $interval_count ),
     2822                        'metadata'  => array( 'content_plan_id' => $plan_id ),
     2823                    ) );
     2824                }
     2825            } catch ( \Exception $e ) {
     2826                $price = \Stripe\Price::create( array(
     2827                    'product'   => $product->id,
     2828                    'unit_amount' => (int) ( $plan->price * 100 ),
     2829                    'currency'  => strtolower( get_option( 'subscription_currency', 'usd' ) ),
     2830                    'recurring' => array( 'interval' => $interval, 'interval_count' => $interval_count ),
     2831                    'metadata'  => array( 'content_plan_id' => $plan_id ),
     2832                ) );
     2833            }
     2834
     2835            $save_and_respond( $product->id, $price->id, false, 'Content plan successfully synced with Stripe.' );
     2836
     2837        } catch ( \Exception $e ) {
     2838            $mock_prod  = 'mock_prod_'  . substr( md5( 'cpln_' . $plan_id ), 0, 10 );
     2839            $mock_price = 'mock_price_' . substr( md5( 'cpln_' . $plan_id . '_price' ), 0, 10 );
     2840            $save_and_respond( $mock_prod, $mock_price, true,
     2841                'Content plan synced with mock Stripe. Error: ' . $e->getMessage(),
     2842                array( 'error' => $e->getMessage() )
     2843            );
     2844        }
     2845    }
     2846
     2847    /**
    26162848     * AJAX handler for checking the Stripe API key
    26172849     */
    … …  
    34273659        // Handle add/edit content plan
    34283660        if (isset($_POST['submit_content_plan']) && wp_verify_nonce($_POST['content_plan_nonce'], 'content_plan_action')) {
     3661            $content_type = sanitize_text_field($_POST['content_type']);
     3662            $content_ids  = isset($_POST['content_ids']) ? array_map('sanitize_text_field', $_POST['content_ids']) : array();
     3663            $plan_id      = isset($_POST['plan_id']) ? intval($_POST['plan_id']) : 0;
     3664
     3665            // Conflict check: category and tag IDs must be unique across all active plans.
     3666            if ( in_array( $content_type, array( 'category', 'tag' ), true ) && ! empty( $content_ids ) ) {
     3667                global $wpdb;
     3668                $cp_table    = $wpdb->prefix . 'subscription_content_plans';
     3669                $other_plans = $wpdb->get_results( $wpdb->prepare(
     3670                    "SELECT id, plan_name, content_ids FROM $cp_table WHERE content_type = %s AND status = 'active'" . ( $plan_id ? ' AND id != ' . intval( $plan_id ) : '' ),
     3671                    $content_type
     3672                ) );
     3673
     3674                $conflicts = array();
     3675                foreach ( $other_plans as $op ) {
     3676                    $op_ids = json_decode( $op->content_ids, true ) ?: array();
     3677                    $overlapping = array_intersect( array_map( 'intval', $content_ids ), array_map( 'intval', $op_ids ) );
     3678                    if ( ! empty( $overlapping ) ) {
     3679                        // Resolve names for display.
     3680                        $type_fn    = $content_type === 'category' ? 'get_category' : 'get_tag';
     3681                        $term_names = array();
     3682                        foreach ( $overlapping as $tid ) {
     3683                            $term = $type_fn( (int) $tid );
     3684                            $term_names[] = $term ? $term->name : "#$tid";
     3685                        }
     3686                        $conflicts[] = sprintf(
     3687                            /* translators: 1: term names list, 2: conflicting plan name */
     3688                            __( '%1$s (already used by "%2$s")', 'subscription-system' ),
     3689                            implode( ', ', $term_names ),
     3690                            $op->plan_name
     3691                        );
     3692                    }
     3693                }
     3694
     3695                if ( ! empty( $conflicts ) ) {
     3696                    $label = $content_type === 'category' ? __( 'categories', 'subscription-system' ) : __( 'tags', 'subscription-system' );
     3697                    add_settings_error(
     3698                        'subscription_messages',
     3699                        'content_plan_conflict',
     3700                        sprintf(
     3701                            /* translators: 1: content type label, 2: conflict details */
     3702                            __( 'The following %1$s are already assigned to another plan and cannot be reused: %2$s', 'subscription-system' ),
     3703                            $label,
     3704                            implode( '; ', $conflicts )
     3705                        ),
     3706                        'error'
     3707                    );
     3708                    return;
     3709                }
     3710            }
     3711
    34293712            $plan_data = array(
    34303713                'plan_name' => sanitize_text_field($_POST['plan_name']),
    … …  
    34333716                'duration' => intval($_POST['duration']),
    34343717                'duration_unit' => sanitize_text_field($_POST['duration_unit']),
    3435                 'content_type' => sanitize_text_field($_POST['content_type']),
    3436                 'content_ids' => isset($_POST['content_ids']) ? array_map('sanitize_text_field', $_POST['content_ids']) : array(),
     3718                'content_type' => $content_type,
     3719                'content_ids'  => $content_ids,
    34373720                'notification_frequency' => sanitize_text_field($_POST['notification_frequency']),
    34383721                'status' => sanitize_text_field($_POST['status'])
    34393722            );
    3440 
    3441             $plan_id = isset($_POST['plan_id']) ? intval($_POST['plan_id']) : 0;
    34423723
    34433724            if ($plan_id > 0) {
  • subscription-system/tags/1.0.18/admin/partials/content-plans-display.php

    r3375846 r3598150  
    223223            data.append('content_type', contentType);
    224224            data.append('nonce', '<?php echo wp_create_nonce('get_content_options'); ?>');
     225            data.append('plan_id', '<?php echo intval($editing_plan ? $editing_plan->id : 0); ?>');
    225226            <?php if ($editing_plan): ?>
    226227            data.append('selected_ids', '<?php echo wp_json_encode($editing_plan->content_ids); ?>');
    … …  
    278279                            <th scope="col"><?php esc_html_e('Duration', 'subscription-system'); ?></th>
    279280                            <th scope="col"><?php esc_html_e('Status', 'subscription-system'); ?></th>
     281                            <th scope="col"><?php esc_html_e('Stripe', 'subscription-system'); ?></th>
    280282                            <th scope="col"><?php esc_html_e('Subscribers', 'subscription-system'); ?></th>
    281283                            <th scope="col"><?php esc_html_e('Actions', 'subscription-system'); ?></th>
    … …  
    285287                        <?php foreach ($content_plans as $plan): ?>
    286288                            <?php
    287                             // Get subscriber count
    288289                            global $wpdb;
    289290                            $subscriber_count = $wpdb->get_var($wpdb->prepare(
    … …  
    291292                                $plan->id
    292293                            ));
     294                            $stripe_product_id = $plan->stripe_product_id ?? '';
     295                            $stripe_price_id   = $plan->stripe_price_id ?? '';
     296                            $is_connected      = ! empty($stripe_product_id) && ! empty($stripe_price_id);
     297                            $is_mock           = $is_connected && strpos($stripe_product_id, 'mock_') === 0;
     298                            $sync_nonce        = wp_create_nonce('sync_content_plan_with_stripe');
    293299                            ?>
    294300                            <tr>
    … …  
    313319                                    </span>
    314320                                </td>
     321                                <td class="ss-stripe-cell">
     322                                    <?php if ($is_connected): ?>
     323                                        <span class="stripe-status <?php echo $is_mock ? 'mock-connected' : 'connected'; ?>">
     324                                            <span class="dashicons dashicons-<?php echo $is_mock ? 'warning' : 'yes-alt'; ?>"></span>
     325                                            <?php echo $is_mock ? esc_html__('Mock', 'subscription-system') : esc_html__('Connected', 'subscription-system'); ?>
     326                                        </span>
     327                                        <?php if (! $is_mock): ?>
     328                                            <br><small title="<?php echo esc_attr($stripe_product_id); ?>"><?php echo esc_html(substr($stripe_product_id, 0, 12) . '…'); ?></small>
     329                                        <?php endif; ?>
     330                                    <?php else: ?>
     331                                        <span class="stripe-status not-connected">
     332                                            <span class="dashicons dashicons-no"></span>
     333                                            <?php esc_html_e('None', 'subscription-system'); ?>
     334                                        </span>
     335                                    <?php endif; ?>
     336                                    <br>
     337                                    <a href="#"
     338                                       class="sync-content-plan-stripe"
     339                                       data-plan-id="<?php echo esc_attr($plan->id); ?>"
     340                                       data-nonce="<?php echo esc_attr($sync_nonce); ?>"
     341                                       style="font-size:11px;">
     342                                        <?php esc_html_e('Sync with Stripe', 'subscription-system'); ?>
     343                                    </a>
     344                                </td>
    315345                                <td><?php echo esc_html($subscriber_count); ?></td>
    316346                                <td>
    … …  
    379409    background: #fafafa;
    380410}
     411
     412.content-checkboxes label {
     413    display: flex;
     414    align-items: center;
     415    gap: 6px;
     416    padding: 3px 0;
     417    line-height: 1.4;
     418}
     419
     420.ss-claimed-item {
     421    opacity: 0.55;
     422    cursor: not-allowed;
     423}
     424
     425.ss-claimed-item input[type="checkbox"] {
     426    cursor: not-allowed;
     427}
     428
     429.ss-claimed-badge {
     430    display: inline-block;
     431    font-size: 11px;
     432    font-weight: 600;
     433    color: #8a2020;
     434    background: #fce8e8;
     435    border: 1px solid #f5c6cb;
     436    border-radius: 3px;
     437    padding: 1px 6px;
     438    margin-left: 4px;
     439    white-space: nowrap;
     440}
     441
     442.ss-post-count {
     443    color: #8c8f94;
     444    font-size: 12px;
     445}
     446
     447/* ── Stripe status column ─── */
     448.stripe-status {
     449    display: inline-flex;
     450    align-items: center;
     451    gap: 4px;
     452    font-size: 12px;
     453    font-weight: 600;
     454    border-radius: 3px;
     455    padding: 2px 6px;
     456}
     457.stripe-status.connected {
     458    background: #d1fae5;
     459    color: #065f46;
     460}
     461.stripe-status.mock-connected {
     462    background: #fef3c7;
     463    color: #92400e;
     464}
     465.stripe-status.not-connected {
     466    background: #f3f4f6;
     467    color: #6b7280;
     468}
     469.stripe-status .dashicons {
     470    font-size: 14px;
     471    width: 14px;
     472    height: 14px;
     473    line-height: 1;
     474}
     475.ss-stripe-cell {
     476    min-width: 120px;
     477}
    381478</style>
     479
     480<script>
     481jQuery(document).ready(function ($) {
     482    $(document).on('click', '.sync-content-plan-stripe', function (e) {
     483        e.preventDefault();
     484
     485        var $link     = $(this);
     486        var $cell     = $link.closest('td');
     487        var planId    = $link.data('plan-id');
     488        var nonce     = $link.data('nonce');
     489        var origHtml  = $cell.html();
     490
     491        $cell.html('<span class="spinner is-active" style="float:none;margin:0;"></span> <?php esc_html_e('Syncing…', 'subscription-system'); ?>');
     492
     493        $.ajax({
     494            url:  (typeof ajaxurl !== 'undefined') ? ajaxurl : '<?php echo esc_js(admin_url('admin-ajax.php')); ?>',
     495            type: 'POST',
     496            data: {
     497                action:  'sync_content_plan_with_stripe',
     498                plan_id: planId,
     499                nonce:   nonce
     500            },
     501            success: function (response) {
     502                if (response.success) {
     503                    var isMock   = response.data.mock_mode;
     504                    var icon     = isMock ? 'warning' : 'yes-alt';
     505                    var label    = isMock ? '<?php esc_html_e('Mock', 'subscription-system'); ?>'
     506                                         : '<?php esc_html_e('Connected', 'subscription-system'); ?>';
     507                    var cssClass = isMock ? 'mock-connected' : 'connected';
     508
     509                    var html = '<span class="stripe-status ' + cssClass + '">'
     510                             + '<span class="dashicons dashicons-' + icon + '"></span> ' + label
     511                             + '</span>';
     512
     513                    if (!isMock && response.data.product_id && response.data.product_id !== 'free') {
     514                        html += '<br><small title="' + response.data.product_id + '">'
     515                              + response.data.product_id.substring(0, 12) + '…</small>';
     516                    }
     517                    html += '<br><a href="#" class="sync-content-plan-stripe"'
     518                          + ' data-plan-id="' + planId + '" data-nonce="' + nonce + '"'
     519                          + ' style="font-size:11px;"><?php esc_html_e('Sync with Stripe', 'subscription-system'); ?></a>';
     520
     521                    $cell.html(html);
     522
     523                    var noticeClass = isMock ? 'notice-warning' : 'notice-success';
     524                    $('h1.wp-heading-inline').after(
     525                        '<div class="notice ' + noticeClass + ' is-dismissible" style="margin-top:12px;">'
     526                        + '<p>' + response.data.message + '</p>'
     527                        + '<button type="button" class="notice-dismiss"><span class="screen-reader-text">Dismiss</span></button>'
     528                        + '</div>'
     529                    );
     530                    $(document).on('click', '.notice-dismiss', function () {
     531                        $(this).closest('.notice').fadeOut(200, function () { $(this).remove(); });
     532                    });
     533                } else {
     534                    $cell.html(origHtml);
     535                    alert('<?php esc_html_e('Error', 'subscription-system'); ?>: ' + (response.data ? response.data.message : '<?php esc_html_e('Unknown error.', 'subscription-system'); ?>'));
     536                }
     537            },
     538            error: function () {
     539                $cell.html(origHtml);
     540                alert('<?php esc_html_e('A network error occurred while syncing with Stripe.', 'subscription-system'); ?>');
     541            }
     542        });
     543    });
     544});
     545</script>
  • subscription-system/tags/1.0.18/admin/partials/help-support-display.php

    r3356551 r3598150  
    149149            <h3>📋 Table of Contents</h3>
    150150            <ul>
     151                <li><a href="#integration-steps"><strong>⚡ Step-by-Step Integration Guide</strong></a></li>
    151152                <li><a href="#getting-started">Getting Started</a></li>
    152153                <li><a href="#subscription-management">Subscription Management & Renewal Flow</a></li>
    153154                <li><a href="#content-subscriptions">Content Subscriptions</a></li>
     155                <li><a href="#interest-subscribe-modal"><strong>🔔 Subscribe Button & Modal (Category/Tag Subscriptions)</strong></a></li>
    154156                <li><a href="#email-notifications">Email Notifications</a></li>
    155157                <li><a href="#shortcodes">Available Shortcodes</a></li>
    … …  
    159161                <li><a href="#troubleshooting">Troubleshooting</a></li>
    160162            </ul>
     163        </div>
     164
     165        <!-- Step-by-Step Integration Guide -->
     166        <div class="help-section" id="integration-steps" style="border-left: 4px solid #1a8917;">
     167            <h2>⚡ Step-by-Step Integration Guide</h2>
     168            <p>Follow these steps in order to fully integrate the Subscription System plugin on your WordPress site.</p>
     169
     170            <!-- Step 1 -->
     171            <div style="background:#f0faf0;border:1px solid #b2dfb2;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     172                <h3 style="margin-top:0;color:#1a8917;">Step 1 — Install &amp; Activate the Plugin</h3>
     173                <ol>
     174                    <li>Upload the <strong>subscription_system</strong> plugin folder to <code>/wp-content/plugins/</code>, or install it via <strong>Plugins → Add New</strong>.</li>
     175                    <li>Go to <strong>Plugins → Installed Plugins</strong> and click <strong>Activate</strong> next to "Subscription System".</li>
     176                    <li>After activation a new <strong>Subscriptions</strong> menu will appear in the WordPress admin sidebar.</li>
     177                </ol>
     178            </div>
     179
     180            <!-- Step 2 -->
     181            <div style="background:#f0faf0;border:1px solid #b2dfb2;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     182                <h3 style="margin-top:0;color:#1a8917;">Step 2 — Configure Stripe Payment Settings</h3>
     183                <ol>
     184                    <li>Go to <strong>Subscriptions → Payment Settings</strong>.</li>
     185                    <li>Tick <strong>Enable Stripe</strong>.</li>
     186                    <li>Leave <strong>Test Mode</strong> enabled while you are testing; disable it when you go live.</li>
     187                    <li>Enter your <strong>Publishable Key</strong> and <strong>Secret Key</strong> from your <a href="https://dashboard.stripe.com/apikeys" target="_blank" rel="noopener">Stripe Dashboard → Developers → API keys</a>.
     188                        <ul>
     189                            <li>Test keys start with <code>pk_test_</code> / <code>sk_test_</code></li>
     190                            <li>Live keys start with <code>pk_live_</code> / <code>sk_live_</code></li>
     191                        </ul>
     192                    </li>
     193                    <li>Leave the <strong>Webhook Secret</strong> field empty for now — you will fill it in Step 3.</li>
     194                    <li>Click <strong>Save Settings</strong>.</li>
     195                    <li>Click <strong>Test Connection</strong> to confirm the API key works before proceeding.</li>
     196                </ol>
     197                <p style="margin-bottom:0;"><a href="<?php echo esc_url( admin_url('admin.php?page=subscription-payment-settings') ); ?>" class="button button-primary">Go to Payment Settings →</a></p>
     198            </div>
     199
     200            <!-- Step 3 -->
     201            <div style="background:#f0faf0;border:1px solid #b2dfb2;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     202                <h3 style="margin-top:0;color:#1a8917;">Step 3 — Set Up the Stripe Webhook</h3>
     203                <p>Webhooks are how Stripe notifies your site about payments, renewals, and cancellations. This step is <strong>required</strong> for automatic subscription management.</p>
     204                <ol>
     205                    <li>Open your <a href="https://dashboard.stripe.com/webhooks" target="_blank" rel="noopener">Stripe Dashboard → Developers → Webhooks</a> and click <strong>Add endpoint</strong>.</li>
     206                    <li>Set the <strong>Endpoint URL</strong> to:
     207                        <div class="shortcode-box" style="margin:8px 0;"><?php echo esc_url( site_url('/wp-json/subscription-system/v1/webhook') ); ?><button class="copy-button" onclick="copyToClipboard('<?php echo esc_js( site_url('/wp-json/subscription-system/v1/webhook') ); ?>')">Copy</button></div>
     208                    </li>
     209                    <li>Under <strong>Select events to listen to</strong>, add these four events:
     210                        <ul>
     211                            <li><code>customer.subscription.created</code></li>
     212                            <li><code>customer.subscription.updated</code></li>
     213                            <li><code>customer.subscription.deleted</code></li>
     214                            <li><code>invoice.payment_succeeded</code></li>
     215                            <li><code>invoice.payment_failed</code></li>
     216                        </ul>
     217                    </li>
     218                    <li>Click <strong>Add endpoint</strong>. Stripe will display a <strong>Signing secret</strong> — copy it.</li>
     219                    <li>Back in WordPress go to <strong>Subscriptions → Payment Settings</strong>, paste the signing secret into the <strong>Webhook Secret</strong> field, and click <strong>Save Settings</strong>.</li>
     220                </ol>
     221            </div>
     222
     223            <!-- Step 4 -->
     224            <div style="background:#f0faf0;border:1px solid #b2dfb2;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     225                <h3 style="margin-top:0;color:#1a8917;">Step 4 — Create the Required Pages</h3>
     226                <p>Go to <strong>Pages → Add New</strong> and create one page for each of the following. Add only the shortcode shown — no other content is needed.</p>
     227                <table style="width:100%;border-collapse:collapse;">
     228                    <thead>
     229                        <tr style="background:#e8f5e9;">
     230                            <th style="padding:8px 10px;border:1px solid #c8e6c9;text-align:left;">Page Name</th>
     231                            <th style="padding:8px 10px;border:1px solid #c8e6c9;text-align:left;">Shortcode to add</th>
     232                        </tr>
     233                    </thead>
     234                    <tbody>
     235                        <tr><td style="padding:8px 10px;border:1px solid #c8e6c9;">Login</td><td style="padding:8px 10px;border:1px solid #c8e6c9;font-family:monospace;">[subscriptionsystem_login]</td></tr>
     236                        <tr><td style="padding:8px 10px;border:1px solid #c8e6c9;">Register</td><td style="padding:8px 10px;border:1px solid #c8e6c9;font-family:monospace;">[subscriptionsystem_register]</td></tr>
     237                        <tr><td style="padding:8px 10px;border:1px solid #c8e6c9;">My Account</td><td style="padding:8px 10px;border:1px solid #c8e6c9;font-family:monospace;">[subscriptionsystem_account]</td></tr>
     238                        <tr><td style="padding:8px 10px;border:1px solid #c8e6c9;">Pricing / Plans</td><td style="padding:8px 10px;border:1px solid #c8e6c9;font-family:monospace;">[subscriptionsystem_plans layout="1"]</td></tr>
     239                        <tr><td style="padding:8px 10px;border:1px solid #c8e6c9;">Subscription Success</td><td style="padding:8px 10px;border:1px solid #c8e6c9;font-family:monospace;">[subscriptionsystem_success]</td></tr>
     240                        <tr><td style="padding:8px 10px;border:1px solid #c8e6c9;">Subscription Cancelled</td><td style="padding:8px 10px;border:1px solid #c8e6c9;font-family:monospace;">[checkout_failed]</td></tr>
     241                        <tr><td style="padding:8px 10px;border:1px solid #c8e6c9;">Forgot Password</td><td style="padding:8px 10px;border:1px solid #c8e6c9;font-family:monospace;">[subscriptionsystem_forgot_password]</td></tr>
     242                        <tr><td style="padding:8px 10px;border:1px solid #c8e6c9;">Edit Profile</td><td style="padding:8px 10px;border:1px solid #c8e6c9;font-family:monospace;">[edit_profile]</td></tr>
     243                        <tr><td style="padding:8px 10px;border:1px solid #c8e6c9;">Content Subscription Success <em>(optional)</em></td><td style="padding:8px 10px;border:1px solid #c8e6c9;font-family:monospace;">[content_subscription_success]</td></tr>
     244                    </tbody>
     245                </table>
     246            </div>
     247
     248            <!-- Step 5 -->
     249            <div style="background:#f0faf0;border:1px solid #b2dfb2;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     250                <h3 style="margin-top:0;color:#1a8917;">Step 5 — Assign Pages in Plugin Settings</h3>
     251                <ol>
     252                    <li>Go to <strong>Subscriptions → Settings → Pages</strong>.</li>
     253                    <li>Use each dropdown to select the page you created in Step 4 for the matching function.</li>
     254                    <li>Click <strong>Save Changes</strong>.</li>
     255                </ol>
     256                <p style="margin-bottom:0;">These assignments allow the plugin to generate correct redirect URLs in emails, checkout flows, and admin links.</p>
     257                <p style="margin:10px 0 0;"><a href="<?php echo esc_url( admin_url('admin.php?page=subscription-settings&tab=pages') ); ?>" class="button">Go to Pages Settings →</a></p>
     258            </div>
     259
     260            <!-- Step 6 -->
     261            <div style="background:#f0faf0;border:1px solid #b2dfb2;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     262                <h3 style="margin-top:0;color:#1a8917;">Step 6 — Configure Email &amp; SMTP</h3>
     263                <ol>
     264                    <li>Go to <strong>Subscriptions → Settings → Communication</strong>.</li>
     265                    <li>Tick <strong>Enable Email Notifications</strong>.</li>
     266                    <li>Set a <strong>From Email Address</strong> and <strong>From Name</strong>.</li>
     267                    <li>Enable <strong>SMTP</strong> for reliable delivery and fill in your SMTP host, port, username and password.
     268                        <ul>
     269                            <li><strong>SendGrid:</strong> Host <code>smtp.sendgrid.net</code>, Port <code>587</code>, Username <code>apikey</code>, Password = your API key</li>
     270                            <li><strong>Gmail:</strong> Host <code>smtp.gmail.com</code>, Port <code>587</code>, Password = app-specific password</li>
     271                        </ul>
     272                    </li>
     273                    <li>Use the built-in <strong>Send Test Email</strong> button to confirm delivery before going live.</li>
     274                    <li>Click <strong>Save Changes</strong>.</li>
     275                </ol>
     276                <p style="margin-bottom:0;"><a href="<?php echo esc_url( admin_url('admin.php?page=subscription-settings&tab=communication') ); ?>" class="button">Go to Communication Settings →</a></p>
     277            </div>
     278
     279            <!-- Step 7 -->
     280            <div style="background:#f0faf0;border:1px solid #b2dfb2;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     281                <h3 style="margin-top:0;color:#1a8917;">Step 7 — Create Plan Categories</h3>
     282                <ol>
     283                    <li>Go to <strong>Subscriptions → Plan Categories</strong>.</li>
     284                    <li>Click <strong>Add New Category</strong> and give it a name (e.g. "Monthly Plans", "Annual Plans").</li>
     285                    <li>Save the category — you will need it when creating plans in the next step.</li>
     286                </ol>
     287                <p style="margin-bottom:0;"><a href="<?php echo esc_url( admin_url('admin.php?page=subscription-plan-categories') ); ?>" class="button">Go to Plan Categories →</a></p>
     288            </div>
     289
     290            <!-- Step 8 -->
     291            <div style="background:#f0faf0;border:1px solid #b2dfb2;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     292                <h3 style="margin-top:0;color:#1a8917;">Step 8 — Create Subscription Plans</h3>
     293                <ol>
     294                    <li>Go to <strong>Subscriptions → Plans → Add New Plan</strong>.</li>
     295                    <li>Fill in the plan name, description, price, billing period (monthly / annual), and select a category from Step 7.</li>
     296                    <li>Save the plan, then click <strong>Sync with Stripe</strong> to create the corresponding Stripe product and price automatically.</li>
     297                    <li>Repeat for each plan you want to offer.</li>
     298                    <li>The plans will now appear on your Pricing page via the <code>[subscriptionsystem_plans]</code> shortcode.</li>
     299                </ol>
     300                <p style="margin-bottom:0;"><a href="<?php echo esc_url( admin_url('admin.php?page=subscription-plans') ); ?>" class="button">Go to Plans →</a></p>
     301            </div>
     302
     303            <!-- Step 9 (optional) -->
     304            <div style="background:#f0faf0;border:1px solid #b2dfb2;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     305                <h3 style="margin-top:0;color:#1a8917;">Step 9 — Set Up Content Subscription Plans <em>(Optional)</em></h3>
     306                <p>Content plans let users subscribe to email notifications for specific WordPress categories, tags, or post types.</p>
     307                <ol>
     308                    <li>Go to <strong>Subscriptions → Content Plans → Add New</strong>.</li>
     309                    <li>Choose the subscription type: <strong>Category</strong>, <strong>Tag</strong>, <strong>Post Type</strong>, or <strong>Specific Post</strong>.</li>
     310                    <li>Select the target content and set a price (or make it free).</li>
     311                    <li>Save the plan — the pop-up will automatically appear for visitors browsing matching content.</li>
     312                </ol>
     313                <p style="margin-bottom:0;"><a href="<?php echo esc_url( admin_url('admin.php?page=subscription-content-plans') ); ?>" class="button">Go to Content Plans →</a></p>
     314            </div>
     315
     316            <!-- Step 9b -->
     317            <div style="background:#f0f4ff;border:1px solid #c7d2fe;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     318                <h3 style="margin-top:0;color:#4338ca;">Step 9b — Set Up the Subscribe Button & Modal <em>(Interest Subscriptions)</em></h3>
     319                <p>This is the newer, more flexible category/tag subscription system. Users choose their own interests from a popup modal. Three trigger methods are available: auto-open, floating button, and shortcode.</p>
     320                <ol>
     321                    <li>Ensure you have at least one active plan in <strong>Subscriptions → Plans</strong> (free or paid).</li>
     322                    <li>Go to <strong>Subscriptions → Settings → Subscribe Button & Modal</strong>.</li>
     323                    <li>Under <strong>Trigger Options</strong>: enable the <strong>Floating Button</strong> and/or <strong>Auto-Open Modal</strong>.</li>
     324                    <li>Under <strong>Display Settings</strong>: choose where the triggers appear (all posts, single posts, specific pages, etc.).</li>
     325                    <li>Enable <strong>Auto-Select Current Post's Categories/Tags</strong> for context-aware pre-selection.</li>
     326                    <li>Enable <strong>Hide Name & Email Fields for Logged-In Users</strong> for a seamless experience.</li>
     327                    <li>Click <strong>Save Settings</strong>.</li>
     328                    <li>Optionally, place <code>[subscriptionsystem_subscribe]</code> in any post or page for an inline button.</li>
     329                </ol>
     330                <p>For full configuration details, see the <a href="#interest-subscribe-modal">Subscribe Button & Modal</a> section below.</p>
     331                <p style="margin-bottom:0;"><a href="<?php echo esc_url( admin_url('admin.php?page=subscription-settings&tab=subscribe_modal') ); ?>" class="button button-primary">Open Subscribe Modal Settings →</a></p>
     332            </div>
     333
     334            <!-- Step 10 -->
     335            <div style="background:#f0faf0;border:1px solid #b2dfb2;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     336                <h3 style="margin-top:0;color:#1a8917;">Step 10 — Test the Full Integration</h3>
     337                <ol>
     338                    <li>Ensure <strong>Stripe Test Mode</strong> is still active in Payment Settings.</li>
     339                    <li>Visit your front-end <strong>Pricing / Plans</strong> page and click a plan to start a checkout.</li>
     340                    <li>Complete payment using Stripe's test card numbers:
     341                        <ul>
     342                            <li><strong>4242 4242 4242 4242</strong> — successful payment</li>
     343                            <li><strong>4000 0000 0000 0002</strong> — declined payment</li>
     344                            <li><strong>4000 0025 0000 3155</strong> — requires 3D Secure</li>
     345                        </ul>
     346                        Use any future expiry date, any 3-digit CVC, and any postcode.
     347                    </li>
     348                    <li>Check that you are redirected to the <strong>Subscription Success</strong> page and receive a confirmation email.</li>
     349                    <li>In the <a href="https://dashboard.stripe.com/test/webhooks" target="_blank" rel="noopener">Stripe Dashboard → Webhooks</a>, verify the webhook events were delivered with a <strong>200</strong> status.</li>
     350                    <li>Check <strong>Subscriptions → Dashboard</strong> to confirm the new subscription appears there.</li>
     351                    <li>When everything works, go to <strong>Payment Settings</strong>, disable Test Mode, enter your <strong>live</strong> API keys and live webhook secret, and save.</li>
     352                </ol>
     353                <div style="background:#fff3cd;border:1px solid #ffeaa7;border-radius:4px;padding:10px;margin-top:10px;">
     354                    <strong>⚠️ Before going live:</strong> Make sure your site uses HTTPS — Stripe requires a secure connection for live payments.
     355                </div>
     356            </div>
     357
    161358        </div>
    162359
    … …  
    285482        </div>
    286483
     484        <!-- Subscribe Button & Modal -->
     485        <div class="help-section" id="interest-subscribe-modal" style="border-left: 4px solid #667eea;">
     486            <h2>🔔 Subscribe Button & Modal (Category / Tag Subscriptions)</h2>
     487            <p>This feature lets visitors subscribe to <strong>specific WordPress categories and tags</strong> — instead of the whole site. When new content matching their interests is published, they receive a targeted email notification.</p>
     488            <p>The subscription modal can be triggered in three ways and is configurable from <strong>Subscriptions → Settings → Subscribe Button & Modal</strong>.</p>
     489
     490            <!-- What It Is -->
     491            <h3>What Is This Feature?</h3>
     492            <div class="help-grid">
     493                <div class="help-card">
     494                    <h4>🎯 Interest-Based Subscriptions</h4>
     495                    <p>Instead of subscribing to everything, users pick the <strong>categories</strong> and <strong>tags</strong> they care about — for example: Category "Sports" + Tag "Hockey". They only receive emails when content matching those interests is published.</p>
     496                </div>
     497                <div class="help-card">
     498                    <h4>📋 Subscription Plans</h4>
     499                    <p>Plans are pulled from <strong>Subscriptions → Plans</strong>. Free plans activate immediately with configurable category/tag limits. Paid plans redirect the user to checkout. Multiple plans can be offered (e.g. "Basic – 2 categories" and "Pro – unlimited").</p>
     500                </div>
     501                <div class="help-card">
     502                    <h4>🧠 Context-Aware Modal</h4>
     503                    <p>When a visitor reads a post in the "Sports" category tagged "Hockey", the modal <strong>automatically pre-selects</strong> those categories and tags — reducing friction and increasing conversions.</p>
     504                </div>
     505                <div class="help-card">
     506                    <h4>👤 Logged-In User Experience</h4>
     507                    <p>Logged-in users are recognized automatically. The modal skips the name/email fields entirely and greets them by name. They proceed directly to choosing their interests and plan.</p>
     508                </div>
     509            </div>
     510
     511            <!-- How It Works -->
     512            <h3>How It Works — End-to-End Flow</h3>
     513            <ol>
     514                <li>A visitor reads a post or page where the modal trigger is active (floating button, auto-open, or shortcode button).</li>
     515                <li>The subscription modal opens showing all available categories, tags, and plans.</li>
     516                <li>If context-awareness is enabled and the user is on a post, the post's categories/tags are pre-checked.</li>
     517                <li>The visitor selects their interests, chooses a plan, and clicks <strong>Subscribe</strong>.</li>
     518                <li>For <strong>free plans</strong>: the subscription is activated immediately. For <strong>paid plans</strong>: the user is redirected to checkout.</li>
     519                <li>When new content is published, the system matches it against all active subscriptions and sends targeted email notifications.</li>
     520            </ol>
     521
     522            <!-- Configuration Steps -->
     523            <h3>⚙️ Configuration — Step by Step</h3>
     524
     525            <!-- Step A -->
     526            <div style="background:#f0f4ff;border:1px solid #c7d2fe;border-radius:6px;padding:15px 20px;margin-bottom:12px;">
     527                <h4 style="margin-top:0;color:#4338ca;">Step A — Create Subscription Plans</h4>
     528                <p>The modal reads plans from <strong>Subscriptions → Plans</strong>. At minimum you need one active plan.</p>
     529                <ol>
     530                    <li>Go to <strong>Subscriptions → Plans → Add New Plan</strong>.</li>
     531                    <li>Set the plan name (e.g. <em>"Free Newsletter"</em> or <em>"Pro Access"</em>).</li>
     532                    <li>Set the price to <strong>$0</strong> for a free plan, or a positive amount for paid.</li>
     533                    <li>In the <strong>Features</strong> or <strong>Description</strong> field you can optionally add a JSON object to control per-plan limits:
     534                        <div class="shortcode-box" style="font-size:13px;">{"max_categories": 2, "max_tags": 3}</div>
     535                        <em>A value of <code>0</code> means unlimited. Omit the field for unlimited by default on paid plans.</em>
     536                    </li>
     537                    <li>Set Status to <strong>Active</strong> and save. The plan now appears in the modal automatically.</li>
     538                </ol>
     539                <p style="margin-bottom:0;"><a href="<?php echo esc_url( admin_url('admin.php?page=subscription-plans') ); ?>" class="button">Go to Plans →</a></p>
     540            </div>
     541
     542            <!-- Step B -->
     543            <div style="background:#f0f4ff;border:1px solid #c7d2fe;border-radius:6px;padding:15px 20px;margin-bottom:12px;">
     544                <h4 style="margin-top:0;color:#4338ca;">Step B — Open Subscribe Button & Modal Settings</h4>
     545                <p>Go to <strong>Subscriptions → Settings → Subscribe Button & Modal</strong>.</p>
     546                <p style="margin-bottom:0;"><a href="<?php echo esc_url( admin_url('admin.php?page=subscription-settings&tab=subscribe_modal') ); ?>" class="button button-primary">Open Settings Tab →</a></p>
     547            </div>
     548
     549            <!-- Step C -->
     550            <div style="background:#f0f4ff;border:1px solid #c7d2fe;border-radius:6px;padding:15px 20px;margin-bottom:12px;">
     551                <h4 style="margin-top:0;color:#4338ca;">Step C — Choose Your Trigger Method</h4>
     552                <p>Under <strong>Trigger Options</strong>, pick one or more ways to present the modal:</p>
     553                <table style="width:100%;border-collapse:collapse;margin-top:8px;">
     554                    <thead>
     555                        <tr style="background:#e0e7ff;">
     556                            <th style="padding:8px 10px;border:1px solid #c7d2fe;text-align:left;">Method</th>
     557                            <th style="padding:8px 10px;border:1px solid #c7d2fe;text-align:left;">What It Does</th>
     558                            <th style="padding:8px 10px;border:1px solid #c7d2fe;text-align:left;">Settings</th>
     559                        </tr>
     560                    </thead>
     561                    <tbody>
     562                        <tr>
     563                            <td style="padding:8px 10px;border:1px solid #c7d2fe;"><strong>Auto-Open on Page Load</strong></td>
     564                            <td style="padding:8px 10px;border:1px solid #c7d2fe;">The modal opens automatically after the page loads, with a configurable delay.</td>
     565                            <td style="padding:8px 10px;border:1px solid #c7d2fe;">Enable checkbox + set delay in seconds (0 = immediate, 3 = recommended).</td>
     566                        </tr>
     567                        <tr>
     568                            <td style="padding:8px 10px;border:1px solid #c7d2fe;"><strong>Floating Button</strong></td>
     569                            <td style="padding:8px 10px;border:1px solid #c7d2fe;">A fixed button stays visible at the corner/bottom of every qualifying page. Clicking it opens the modal.</td>
     570                            <td style="padding:8px 10px;border:1px solid #c7d2fe;">Enable checkbox, set button text and position (Bottom Right, Left, or Center).</td>
     571                        </tr>
     572                        <tr>
     573                            <td style="padding:8px 10px;border:1px solid #c7d2fe;"><strong>Shortcode Button</strong></td>
     574                            <td style="padding:8px 10px;border:1px solid #c7d2fe;">Place a button anywhere in a post or page. Clicking it opens the modal.</td>
     575                            <td style="padding:8px 10px;border:1px solid #c7d2fe;">Add <code>[subscriptionsystem_subscribe]</code> to any page or post — no admin setting needed.</td>
     576                        </tr>
     577                    </tbody>
     578                </table>
     579                <p style="margin-top:10px;"><strong>Note:</strong> You can enable all three simultaneously — they use the same modal and share the same subscription logic.</p>
     580            </div>
     581
     582            <!-- Step D -->
     583            <div style="background:#f0f4ff;border:1px solid #c7d2fe;border-radius:6px;padding:15px 20px;margin-bottom:12px;">
     584                <h4 style="margin-top:0;color:#4338ca;">Step D — Configure Display Settings (Where Triggers Appear)</h4>
     585                <p>Under <strong>Display Settings</strong>, choose on which pages the floating button and auto-open modal are active. Options:</p>
     586                <ul>
     587                    <li><strong>Show on All Posts</strong> — any singular post of any post type (excluding pages).</li>
     588                    <li><strong>Show on All Pages</strong> — all WordPress pages.</li>
     589                    <li><strong>Show on Single Posts Only</strong> — only the standard blog post single view.</li>
     590                    <li><strong>Show on Single Pages Only</strong> — only single-page views.</li>
     591                    <li><strong>Show on Custom Post Types</strong> — tick one or more CPTs from the list (e.g. "Product", "Event").</li>
     592                    <li><strong>Show on Specific Pages</strong> — hand-pick individual pages from a scrollable checklist.</li>
     593                </ul>
     594                <p><strong>Tip:</strong> The shortcode <code>[subscriptionsystem_subscribe]</code> always renders wherever it is placed regardless of these display settings — they only control the floating button and auto-open behavior.</p>
     595            </div>
     596
     597            <!-- Step E -->
     598            <div style="background:#f0f4ff;border:1px solid #c7d2fe;border-radius:6px;padding:15px 20px;margin-bottom:12px;">
     599                <h4 style="margin-top:0;color:#4338ca;">Step E — Enable Context-Aware Pre-Selection</h4>
     600                <p>Under <strong>Context-Aware Modal</strong>:</p>
     601                <ul>
     602                    <li>Tick <strong>Auto-Select Current Post's Categories</strong> — when a visitor is on a post, its categories are pre-checked in the modal.</li>
     603                    <li>Tick <strong>Auto-Select Current Post's Tags</strong> — same for tags.</li>
     604                </ul>
     605                <p><em>Example: A visitor reads a post in "Sports" tagged "Hockey". Both checkboxes are pre-ticked when the modal opens, with a <strong>current</strong> label — they can confirm or adjust before subscribing.</em></p>
     606                <p>This works for both the shortcode button and the floating button / auto-open modal.</p>
     607            </div>
     608
     609            <!-- Step F -->
     610            <div style="background:#f0f4ff;border:1px solid #c7d2fe;border-radius:6px;padding:15px 20px;margin-bottom:12px;">
     611                <h4 style="margin-top:0;color:#4338ca;">Step F — Logged-In User Experience</h4>
     612                <p>Under <strong>Logged-In User Experience</strong>:</p>
     613                <ul>
     614                    <li>Tick <strong>Hide Name & Email Fields for Logged-In Users</strong> — when a logged-in user opens the modal, their name and email are passed silently in hidden fields. The modal opens directly on the category/tag/plan step with a personalized greeting (e.g. <em>"Hi John! Choose what you'd like to follow below."</em>).</li>
     615                </ul>
     616                <p>This setting is enabled by default. Disable it only if you want logged-in users to confirm their details each time.</p>
     617            </div>
     618
     619            <!-- Step G -->
     620            <div style="background:#f0f4ff;border:1px solid #c7d2fe;border-radius:6px;padding:15px 20px;margin-bottom:12px;">
     621                <h4 style="margin-top:0;color:#4338ca;">Step G — Save Settings & Verify</h4>
     622                <ol>
     623                    <li>Click <strong>Save Settings</strong> on the Subscribe Button & Modal settings tab.</li>
     624                    <li>Visit any post or page that matches your Display Settings in a new browser tab.</li>
     625                    <li>Confirm the floating button appears and/or the modal auto-opens after the configured delay.</li>
     626                    <li>Open the modal and verify the categories/tags are pre-selected if context-awareness is on.</li>
     627                    <li>Submit a test subscription (use a real or test email) and confirm the row appears in <strong>Subscriptions → Interest Subscribers</strong>.</li>
     628                </ol>
     629                <p style="margin-bottom:0;"><a href="<?php echo esc_url( admin_url('admin.php?page=subscription-interest-subscribers') ); ?>" class="button">View Interest Subscribers →</a></p>
     630            </div>
     631
     632            <!-- Shortcode Reference -->
     633            <h3>Shortcode Reference</h3>
     634            <h4>[subscriptionsystem_subscribe]</h4>
     635            <div class="shortcode-box">
     636                [subscriptionsystem_subscribe]
     637                <button class="copy-button" onclick="copyToClipboard('[subscriptionsystem_subscribe]')">Copy</button>
     638            </div>
     639            <p>Renders an inline subscribe button that opens the interest-subscription modal when clicked. Place it anywhere in a post, page, or widget.</p>
     640
     641            <h4>Optional attributes</h4>
     642            <div class="shortcode-box">
     643                [subscriptionsystem_subscribe button_text="Join Now" button_class="my-custom-class"]
     644                <button class="copy-button" onclick="copyToClipboard('[subscriptionsystem_subscribe button_text=&quot;Join Now&quot; button_class=&quot;my-custom-class&quot;]')">Copy</button>
     645            </div>
     646            <table style="width:100%;border-collapse:collapse;margin-top:8px;">
     647                <thead>
     648                    <tr style="background:#e0e7ff;">
     649                        <th style="padding:8px 10px;border:1px solid #c7d2fe;text-align:left;">Attribute</th>
     650                        <th style="padding:8px 10px;border:1px solid #c7d2fe;text-align:left;">Default</th>
     651                        <th style="padding:8px 10px;border:1px solid #c7d2fe;text-align:left;">Description</th>
     652                    </tr>
     653                </thead>
     654                <tbody>
     655                    <tr>
     656                        <td style="padding:8px 10px;border:1px solid #c7d2fe;font-family:monospace;">button_text</td>
     657                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Subscribe</td>
     658                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">The visible label of the button.</td>
     659                    </tr>
     660                    <tr>
     661                        <td style="padding:8px 10px;border:1px solid #c7d2fe;font-family:monospace;">button_class</td>
     662                        <td style="padding:8px 10px;border:1px solid #c7d2fe;"><em>(empty)</em></td>
     663                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Extra CSS class(es) added to the button element for custom styling.</td>
     664                    </tr>
     665                </tbody>
     666            </table>
     667
     668            <!-- Notification Flow -->
     669            <h3>Email Notification Flow (When New Content Is Published)</h3>
     670            <ol>
     671                <li>Admin publishes a new post.</li>
     672                <li>The system reads the post's categories and tags.</li>
     673                <li>It queries the <strong>interest subscribers</strong> table for active subscriptions that overlap with those categories or tags.</li>
     674                <li>A matching email is sent to each qualifying subscriber — subject line includes the site name and post title; body includes an excerpt and a "Read more" link.</li>
     675                <li>Unmatched subscribers receive no email for that post.</li>
     676            </ol>
     677            <p><strong>From name / From email:</strong> Controlled by <strong>Subscriptions → Settings → Communication</strong> (same as all other notification emails).</p>
     678
     679            <!-- Troubleshooting -->
     680            <h3>Troubleshooting</h3>
     681            <table style="width:100%;border-collapse:collapse;">
     682                <thead>
     683                    <tr style="background:#e0e7ff;">
     684                        <th style="padding:8px 10px;border:1px solid #c7d2fe;text-align:left;">Problem</th>
     685                        <th style="padding:8px 10px;border:1px solid #c7d2fe;text-align:left;">Likely Cause &amp; Fix</th>
     686                    </tr>
     687                </thead>
     688                <tbody>
     689                    <tr>
     690                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Modal does not appear</td>
     691                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Check that the current page type matches the <strong>Display Settings</strong>. Also verify that at least one active plan exists in <strong>Subscriptions → Plans</strong>.</td>
     692                    </tr>
     693                    <tr>
     694                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">No plans shown in modal</td>
     695                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Go to <strong>Subscriptions → Plans</strong> and ensure at least one plan has <strong>Status = Active</strong>.</td>
     696                    </tr>
     697                    <tr>
     698                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Floating button overlaps content</td>
     699                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Change the <strong>Floating Button Position</strong> to Bottom Left or Bottom Center, or add custom CSS to reposition it.</td>
     700                    </tr>
     701                    <tr>
     702                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Categories/tags not pre-selected</td>
     703                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Context-aware pre-selection only works on <strong>single post</strong> views. Archive, home, and page views do not have post-level taxonomy data.</td>
     704                    </tr>
     705                    <tr>
     706                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Logged-in user still sees name/email fields</td>
     707                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Make sure <strong>"Hide Name & Email Fields for Logged-In Users"</strong> is ticked in the Subscribe Button & Modal settings.</td>
     708                    </tr>
     709                    <tr>
     710                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Subscribers not receiving emails</td>
     711                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Verify <strong>Email Notifications</strong> are enabled in <strong>Communication Settings</strong> and your SMTP is configured correctly. Check the <strong>Email Logs</strong> page for bounce or error details.</td>
     712                    </tr>
     713                </tbody>
     714            </table>
     715
     716        </div>
     717
    287718        <!-- Email Notifications -->
    288719        <div class="help-section" id="email-notifications">
    … …  
    388819            </div>
    389820            <p>Displays the content subscription success page with plan details and next steps.</p>
     821
     822            <h3>Interest / Category-Tag Subscriptions</h3>
     823
     824            <h4>[subscriptionsystem_subscribe]</h4>
     825            <div class="shortcode-box">
     826                [subscriptionsystem_subscribe]
     827                <button class="copy-button" onclick="copyToClipboard('[subscriptionsystem_subscribe]')">Copy</button>
     828            </div>
     829            <p>Renders an inline button that opens the interest-subscription popup modal. When clicked, users can pick which categories and tags they want to follow, choose a plan, and subscribe — all without leaving the page.</p>
     830
     831            <div class="shortcode-box">
     832                [subscriptionsystem_subscribe button_text="Follow This Topic" button_class="my-btn"]
     833                <button class="copy-button" onclick="copyToClipboard('[subscriptionsystem_subscribe button_text=&quot;Follow This Topic&quot; button_class=&quot;my-btn&quot;]')">Copy</button>
     834            </div>
     835            <p><strong>Attributes:</strong> <code>button_text</code> (default: "Subscribe") — label shown on the button. <code>button_class</code> (default: empty) — additional CSS classes on the button element.</p>
     836            <p>For floating button and auto-open trigger configuration, see <a href="#interest-subscribe-modal">Subscribe Button & Modal Settings</a>.</p>
    390837        </div>
    391838
    … …  
    460907                <li><strong>Page Settings:</strong> Assign pages for different functions</li>
    461908                <li><strong>Communication Settings:</strong> Control email notifications</li>
     909                <li><strong>Subscribe Button & Modal:</strong> Configure triggers (floating button, auto-open, shortcode), display rules, context-awareness, and logged-in UX for the interest-based subscription modal</li>
    462910            </ul>
    463911        </div>
  • subscription-system/tags/1.0.18/admin/partials/plans-display.php

    r3438196 r3598150  
    9494                                <tr>
    9595                                    <th scope="row"><label for="price"><?php echo esc_html__('Price', 'subscription-system'); ?></label></th>
    96                                     <td><input type="number" name="price" id="price" class="regular-text" step="0.01" min="0" required></td>
     96                                    <td><input type="number" name="price" id="price" class="regular-text" step="0.01" min="0" required>
     97                                        <p class="description"><?php esc_html_e('Set to 0 for a free plan.', 'subscription-system'); ?></p>
     98                                    </td>
    9799                                </tr>
    98100                                <tr>
    … …  
    109111                                    </td>
    110112                                </tr>
     113                                <tr>
     114                                    <th scope="row"><label for="status"><?php echo esc_html__('Status', 'subscription-system'); ?></label></th>
     115                                    <td>
     116                                        <select name="status" id="status" class="regular-text">
     117                                            <option value="active"><?php esc_html_e('Active', 'subscription-system'); ?></option>
     118                                            <option value="inactive"><?php esc_html_e('Inactive', 'subscription-system'); ?></option>
     119                                        </select>
     120                                        <p class="description"><?php esc_html_e('Only active plans appear in the subscription modal and pricing pages.', 'subscription-system'); ?></p>
     121                                    </td>
     122                                </tr>
    111123                            </table>
     124
     125                            <h4 style="margin:20px 0 4px;color:#4338ca;">🔔 <?php esc_html_e('Interest Subscription Limits', 'subscription-system'); ?></h4>
     126                            <p class="description" style="margin-bottom:10px;"><?php esc_html_e('These limits control how many categories and tags a subscriber can pick when using the Subscribe Button & Modal. Set to 0 for unlimited.', 'subscription-system'); ?></p>
     127                            <table class="form-table" role="presentation" style="margin-top:0;">
     128                                <tr>
     129                                    <th scope="row"><label for="max_categories"><?php esc_html_e('Max Categories', 'subscription-system'); ?></label></th>
     130                                    <td>
     131                                        <input type="number" name="max_categories" id="max_categories" class="small-text" min="0" value="0">
     132                                        <p class="description"><?php esc_html_e('0 = unlimited. For free plans, a limit of 2 is recommended.', 'subscription-system'); ?></p>
     133                                    </td>
     134                                </tr>
     135                                <tr>
     136                                    <th scope="row"><label for="max_tags"><?php esc_html_e('Max Tags', 'subscription-system'); ?></label></th>
     137                                    <td>
     138                                        <input type="number" name="max_tags" id="max_tags" class="small-text" min="0" value="0">
     139                                        <p class="description"><?php esc_html_e('0 = unlimited. For free plans, a limit of 3 is recommended.', 'subscription-system'); ?></p>
     140                                    </td>
     141                                </tr>
     142                            </table>
     143                            <input type="hidden" name="plan_features_json" id="plan_features_json" value="">
     144
    112145                            <p class="submit">
    113146                                <input type="submit" name="add_plan" class="button button-primary" value="<?php echo esc_attr__('Save Plan', 'subscription-system'); ?>">
    … …  
    178211                                <tr>
    179212                                    <th scope="row"><label for="edit_price"><?php echo esc_html__('Price', 'subscription-system'); ?></label></th>
    180                                     <td><input type="number" name="price" id="edit_price" class="regular-text" step="0.01" min="0" required></td>
     213                                    <td><input type="number" name="price" id="edit_price" class="regular-text" step="0.01" min="0" required>
     214                                        <p class="description"><?php esc_html_e('Set to 0 for a free plan.', 'subscription-system'); ?></p>
     215                                    </td>
    181216                                </tr>
    182217                                <tr>
    … …  
    193228                                    </td>
    194229                                </tr>
     230                                <tr>
     231                                    <th scope="row"><label for="edit_status"><?php esc_html_e('Status', 'subscription-system'); ?></label></th>
     232                                    <td>
     233                                        <select name="status" id="edit_status" class="regular-text">
     234                                            <option value="active"><?php esc_html_e('Active', 'subscription-system'); ?></option>
     235                                            <option value="inactive"><?php esc_html_e('Inactive', 'subscription-system'); ?></option>
     236                                        </select>
     237                                        <p class="description"><?php esc_html_e('Only active plans appear in the subscription modal and pricing pages.', 'subscription-system'); ?></p>
     238                                    </td>
     239                                </tr>
    195240                            </table>
     241
     242                            <h4 style="margin:20px 0 4px;color:#4338ca;">🔔 <?php esc_html_e('Interest Subscription Limits', 'subscription-system'); ?></h4>
     243                            <p class="description" style="margin-bottom:10px;"><?php esc_html_e('These limits control how many categories and tags a subscriber can pick in the Subscribe Button & Modal. Set to 0 for unlimited.', 'subscription-system'); ?></p>
     244                            <table class="form-table" role="presentation" style="margin-top:0;">
     245                                <tr>
     246                                    <th scope="row"><label for="edit_max_categories"><?php esc_html_e('Max Categories', 'subscription-system'); ?></label></th>
     247                                    <td>
     248                                        <input type="number" name="max_categories" id="edit_max_categories" class="small-text" min="0" value="0">
     249                                        <p class="description"><?php esc_html_e('0 = unlimited. For free plans, 2 is recommended.', 'subscription-system'); ?></p>
     250                                    </td>
     251                                </tr>
     252                                <tr>
     253                                    <th scope="row"><label for="edit_max_tags"><?php esc_html_e('Max Tags', 'subscription-system'); ?></label></th>
     254                                    <td>
     255                                        <input type="number" name="max_tags" id="edit_max_tags" class="small-text" min="0" value="0">
     256                                        <p class="description"><?php esc_html_e('0 = unlimited. For free plans, 3 is recommended.', 'subscription-system'); ?></p>
     257                                    </td>
     258                                </tr>
     259                            </table>
     260                            <input type="hidden" name="plan_features_json" id="edit_plan_features_json" value="">
     261
    196262                            <p class="submit">
    197263                                <input type="submit" name="edit_plan" class="button button-primary" value="<?php echo esc_attr__('Update Plan', 'subscription-system'); ?>">
    … …  
    225291                <tr>
    226292                    <th scope="col" class="manage-column column-name"><?php echo esc_html__('Plan Name', 'subscription-system'); ?></th>
    227                     <th scope="col" class="manage-column column-description"><?php echo esc_html__('Description', 'subscription-system'); ?></th>
     293                    <th scope="col" class="manage-column column-description"><?php echo esc_html__('Features', 'subscription-system'); ?></th>
    228294                    <th scope="col" class="manage-column column-price"><?php echo esc_html__('Price', 'subscription-system'); ?></th>
    229                     <th scope="col" class="manage-column column-duration"><?php echo esc_html__('Duration (days)', 'subscription-system'); ?></th>
    230                     <th scope="col" class="manage-column column-category"><?php echo esc_html__('Category', 'subscription-system'); ?></th>
    231                     <th scope="col"><?php echo esc_html__('Stripe Status', 'subscription-system'); ?></th>
     295                    <th scope="col" class="manage-column column-duration"><?php echo esc_html__('Duration', 'subscription-system'); ?></th>
     296                    <th scope="col"><?php echo esc_html__('Interest Limits', 'subscription-system'); ?></th>
     297                    <th scope="col"><?php echo esc_html__('Status', 'subscription-system'); ?></th>
     298                    <th scope="col"><?php echo esc_html__('Stripe', 'subscription-system'); ?></th>
    232299                    <th scope="col" class="manage-column column-actions"><?php echo esc_html__('Actions', 'subscription-system'); ?></th>
    233300                </tr>
    … …  
    248315                            }
    249316                        }
     317
     318                        // Parse limits from features JSON
     319                        $plan_max_cats = 0;
     320                        $plan_max_tags = 0;
     321                        if ( ! empty( $plan->features ) ) {
     322                            $feat_data = json_decode( $plan->features, true );
     323                            if ( is_array( $feat_data ) ) {
     324                                $plan_max_cats = isset( $feat_data['max_categories'] ) ? (int) $feat_data['max_categories'] : 0;
     325                                $plan_max_tags = isset( $feat_data['max_tags'] )       ? (int) $feat_data['max_tags']       : 0;
     326                            }
     327                        }
     328
     329                        $plan_status = ! empty( $plan->status ) ? $plan->status : 'active';
    250330                    ?>
    251331                        <tr>
    252                             <td class="column-name"><?php echo esc_html($plan->plan_name); ?></td>
     332                            <td class="column-name">
     333                                <strong><?php echo esc_html($plan->plan_name); ?></strong>
     334                                <?php if ( $category_name ) : ?>
     335                                    <br><small style="color:#888;"><?php echo esc_html( $category_name ); ?></small>
     336                                <?php endif; ?>
     337                            </td>
    253338                            <td class="column-description">
    254339                                <?php
    255340                                $desc = $plan->description;
    256                                 // Check if description is JSON
    257341                                if (substr($desc, 0, 1) === '[' && substr($desc, -1) === ']') {
    258                                     try {
    259                                         // Handle escaped JSON strings
    260                                         $desc = stripslashes($desc);
    261                                         $features = json_decode($desc, true);
    262                                        
    263                                         if (is_array($features) && !empty($features)) {
    264                                             echo '<ul style="margin: 0; padding-left: 20px;">';
    265                                             foreach ($features as $feature) {
    266                                                 echo '<li>✓ ' . esc_html($feature) . '</li>';
    267                                             }
    268                                             echo '</ul>';
    269                                         } else {
    270                                             echo esc_html($desc);
     342                                    $desc = stripslashes($desc);
     343                                    $features_arr = json_decode($desc, true);
     344                                    if (is_array($features_arr) && !empty($features_arr)) {
     345                                        echo '<ul style="margin: 0; padding-left: 20px;">';
     346                                        foreach ($features_arr as $feature) {
     347                                            echo '<li>✓ ' . esc_html($feature) . '</li>';
    271348                                        }
    272                                     } catch (Exception $e) {
     349                                        echo '</ul>';
     350                                    } else {
    273351                                        echo esc_html($desc);
    274352                                    }
    … …  
    278356                                ?>
    279357                            </td>
    280                             <td class="column-price"><?php echo esc_html(number_format($plan->price, 2)); ?></td>
    281                             <td class="column-duration"><?php echo esc_html($plan->duration); ?></td>
    282                             <td class="column-category"><?php echo esc_html($category_name); ?></td>
     358                            <td class="column-price">
     359                                <?php if ( (float) $plan->price <= 0 ) : ?>
     360                                    <span style="color:#059669;font-weight:600;"><?php esc_html_e( 'Free', 'subscription-system' ); ?></span>
     361                                <?php else : ?>
     362                                    $<?php echo esc_html( number_format( $plan->price, 2 ) ); ?>
     363                                <?php endif; ?>
     364                            </td>
     365                            <td class="column-duration">
     366                                <?php
     367                                $dur_map = array( 1 => 'Daily', 7 => 'Weekly', 30 => 'Monthly', 90 => 'Quarterly', 180 => '6 Months', 365 => 'Yearly' );
     368                                echo esc_html( isset( $dur_map[ (int) $plan->duration ] ) ? $dur_map[ (int) $plan->duration ] : $plan->duration . ' days' );
     369                                ?>
     370                            </td>
     371                            <td>
     372                                <small>
     373                                    <?php
     374                                    $cats_label = $plan_max_cats > 0 ? $plan_max_cats : '∞';
     375                                    $tags_label = $plan_max_tags > 0 ? $plan_max_tags : '∞';
     376                                    printf( 'Cat: <strong>%s</strong> &nbsp; Tags: <strong>%s</strong>', esc_html( $cats_label ), esc_html( $tags_label ) );
     377                                    ?>
     378                                </small>
     379                            </td>
     380                            <td>
     381                                <?php if ( $plan_status === 'active' ) : ?>
     382                                    <span style="background:#d1fae5;color:#065f46;padding:2px 8px;border-radius:12px;font-size:12px;font-weight:600;"><?php esc_html_e('Active','subscription-system'); ?></span>
     383                                <?php else : ?>
     384                                    <span style="background:#fee2e2;color:#991b1b;padding:2px 8px;border-radius:12px;font-size:12px;font-weight:600;"><?php esc_html_e('Inactive','subscription-system'); ?></span>
     385                                <?php endif; ?>
     386                            </td>
    283387                            <td>
    284388                                <?php
    285389                                $stripe_product_id = $plan->stripe_product_id;
    286390                                $stripe_price_id = $plan->stripe_price_id;
    287                                
    288                                 // Check if the plan is connected to Stripe
    289391                                $is_connected = !empty($stripe_product_id) && !empty($stripe_price_id);
    290392                                $is_mock = $is_connected && (strpos($stripe_product_id, 'mock_') === 0);
    291                                
    292393                                if ($is_connected) {
    293394                                    echo '<span class="stripe-status ' . ($is_mock ? 'mock-connected' : 'connected') . '">' .
    294395                                        '<span class="dashicons dashicons-' . ($is_mock ? 'warning' : 'yes-alt') . '"></span> ' .
    295                                         ($is_mock ? 'Mock Connected' : 'Connected') . '</span>';
    296                                    
    297                                     if ($is_mock) {
    298                                         echo '<br><small>Using test data - not connected to Stripe</small>';
    299                                     } else {
    300                                         echo '<br><small>Product ID: ' . substr($stripe_product_id, 0, 8) . '...</small>';
    301                                         echo '<br><small>Price ID: ' . substr($stripe_price_id, 0, 8) . '...</small>';
    302                                     }
     396                                        ($is_mock ? 'Mock' : 'Connected') . '</span>';
    303397                                } else {
    304                                     echo '<span class="stripe-status not-connected"><span class="dashicons dashicons-no"></span> Not Connected</span>';
     398                                    echo '<span class="stripe-status not-connected"><span class="dashicons dashicons-no"></span> None</span>';
    305399                                }
    306400                                ?>
    307                                 <br>
    308                                 <a href="#" class="sync-with-stripe" data-plan-id="<?php echo $plan->id; ?>" data-nonce="<?php echo wp_create_nonce('sync_plan_with_stripe'); ?>">Sync with Stripe</a>
     401                                <br><a href="#" class="sync-with-stripe" data-plan-id="<?php echo $plan->id; ?>" data-nonce="<?php echo wp_create_nonce('sync_plan_with_stripe'); ?>" style="font-size:11px;">Sync</a>
    309402                            </td>
    310403                            <td class="column-actions">
    … …  
    315408                                        data-price="<?php echo esc_attr($plan->price); ?>"
    316409                                        data-duration="<?php echo esc_attr($plan->duration); ?>"
    317                                         data-category="<?php echo esc_attr($plan->category_id); ?>">
     410                                        data-category="<?php echo esc_attr($plan->category_id); ?>"
     411                                        data-status="<?php echo esc_attr($plan_status); ?>"
     412                                        data-max-categories="<?php echo esc_attr($plan_max_cats); ?>"
     413                                        data-max-tags="<?php echo esc_attr($plan_max_tags); ?>">
    318414                                    <?php echo esc_html__('Edit', 'subscription-system'); ?>
    319415                                </button>
    … …  
    354450        $('#add-plan-form').hide();
    355451       
    356         var id = $(this).data('id');
    357         var name = $(this).data('name');
    358         var description = $(this).data('description');
    359         var price = $(this).data('price');
    360         var duration = $(this).data('duration');
    361         var category = $(this).data('category');
     452        var id           = $(this).data('id');
     453        var name         = $(this).data('name');
     454        var description  = $(this).data('description');
     455        var price        = $(this).data('price');
     456        var duration     = $(this).data('duration');
     457        var category     = $(this).data('category');
     458        var status       = $(this).data('status') || 'active';
     459        var maxCats      = parseInt($(this).data('max-categories'), 10) || 0;
     460        var maxTags      = parseInt($(this).data('max-tags'), 10) || 0;
    362461       
    363462        $('#edit_plan_id').val(id);
    … …  
    366465        $('#edit_duration').val(duration);
    367466        $('#edit_category_id').val(category);
     467        $('#edit_status').val(status);
     468        $('#edit_max_categories').val(maxCats);
     469        $('#edit_max_tags').val(maxTags);
    368470       
    369471        // Clear existing feature inputs
    … …  
    372474        // Try to parse description as JSON (for features)
    373475        try {
    374             // First try to handle escaped JSON (with backslashes)
    375476            var cleanDescription = description.replace(/\\\\/g, '\\').replace(/\\"/g, '"');
    376477            var features = JSON.parse(cleanDescription);
    377            
    378478            if (Array.isArray(features)) {
    379                 // Add feature inputs for each feature
    380479                features.forEach(function(feature) {
    381480                    addEditFeature(feature);
    382481                });
    383482            } else {
    384                 // If not an array, add as a single feature
    385483                addEditFeature(cleanDescription);
    386484            }
    387485        } catch (e) {
    388             // If not valid JSON, add as a single feature
    389486            if (description) {
    390487                addEditFeature(description);
    … …  
    621718    });
    622719   
     720    // ── Build features JSON before submitting Add form ──────────────────────
     721    $('#add-plan-form form').on('submit', function() {
     722        var limits = {
     723            max_categories: parseInt($('#max_categories').val(), 10) || 0,
     724            max_tags:       parseInt($('#max_tags').val(), 10) || 0
     725        };
     726        $('#plan_features_json').val(JSON.stringify(limits));
     727    });
     728
     729    // ── Build features JSON before submitting Edit form ──────────────────────
     730    $('#edit-plan-form form').on('submit', function() {
     731        var limits = {
     732            max_categories: parseInt($('#edit_max_categories').val(), 10) || 0,
     733            max_tags:       parseInt($('#edit_max_tags').val(), 10) || 0
     734        };
     735        $('#edit_plan_features_json').val(JSON.stringify(limits));
     736    });
     737
    623738    // Fix for the category column display
    624739    $('.sync-with-stripe').on('click', function(e) {
  • subscription-system/tags/1.0.18/admin/partials/settings-display.php

    r3311076 r3598150  
    1717// Define tabs
    1818$tabs = array(
    19     'general' => __('General', 'subscription-system'),
    20     'payment' => __('Payment', 'subscription-system'),
    21     'communication' => __('Communication', 'subscription-system'),
    22     'pages' => __('Pages Settings', 'subscription-system')
     19    'general'        => __('General', 'subscription-system'),
     20    'payment'        => __('Payment', 'subscription-system'),
     21    'communication'  => __('Communication', 'subscription-system'),
     22    'pages'          => __('Pages Settings', 'subscription-system'),
     23    'subscribe_modal' => __('Subscribe Button & Modal', 'subscription-system'),
    2324);
    2425
    … …  
    7879                        case 'pages':
    7980                            include 'pages-settings-display.php';
     81                            break;
     82                        case 'subscribe_modal':
     83                            include 'subscribe-modal-settings-display.php';
    8084                            break;
    8185                    }
  • subscription-system/tags/1.0.18/includes/class-subs-sys-ajax.php

    r3435450 r3598150  
    7171                add_action( 'wp_ajax_unsubscribe_from_content_plan', array( $this, 'unsubscribe_from_content_plan' ) );
    7272                add_action( 'wp_ajax_test_content_ajax', array( $this, 'test_content_ajax' ) );
     73                add_action( 'wp_ajax_submit_interest_subscription', array( $this, 'submit_interest_subscription' ) );
     74                add_action( 'wp_ajax_nopriv_submit_interest_subscription', array( $this, 'submit_interest_subscription' ) );
    7375        }
    7476
    … …  
    16291631         */
    16301632        public function get_content_options() {
    1631                 // Debug logging
    1632                 error_log('AJAX get_content_options called');
    1633                 error_log('POST data: ' . print_r($_POST, true));
    1634 
    1635                 // Verify nonce
    16361633                if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'get_content_options' ) ) {
    1637                         error_log('Nonce verification failed');
    16381634                        wp_send_json_error( array( 'message' => __( 'Security check failed.', 'subscription-system' ) ) );
    16391635                        return;
    16401636                }
    16411637
    1642                 // Check user capabilities
    16431638                if ( ! current_user_can( 'manage_options' ) ) {
    1644                         error_log('User does not have manage_options capability');
    16451639                        wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'subscription-system' ) ) );
    16461640                        return;
    16471641                }
    16481642
    1649                 $content_type = isset( $_POST['content_type'] ) ? sanitize_text_field( $_POST['content_type'] ) : '';
    1650                 $selected_ids = isset( $_POST['selected_ids'] ) ? json_decode( stripslashes( $_POST['selected_ids'] ), true ) : array();
    1651                
    1652                 // Ensure selected_ids is an array
    1653                 if (!is_array($selected_ids)) {
     1643                $content_type    = isset( $_POST['content_type'] ) ? sanitize_text_field( wp_unslash( $_POST['content_type'] ) ) : '';
     1644                $selected_ids    = isset( $_POST['selected_ids'] ) ? json_decode( stripslashes( $_POST['selected_ids'] ), true ) : array();
     1645                $editing_plan_id = isset( $_POST['plan_id'] ) ? absint( $_POST['plan_id'] ) : 0;
     1646
     1647                if ( ! is_array( $selected_ids ) ) {
    16541648                        $selected_ids = array();
    16551649                }
    16561650
    1657                 error_log('Content type: ' . $content_type);
    1658                 error_log('Selected IDs: ' . print_r($selected_ids, true));
    1659 
    16601651                if ( empty( $content_type ) ) {
    1661                         error_log('Content type is empty');
    16621652                        wp_send_json_error( array( 'message' => __( 'Content type is required.', 'subscription-system' ) ) );
    16631653                        return;
     1654                }
     1655
     1656                // Build a map of term/item ID → plan name for already-claimed IDs (category & tag only).
     1657                $claimed = array(); // [ id => plan_name ]
     1658                if ( in_array( $content_type, array( 'category', 'tag' ), true ) && Subs_Sys_DB_Updater::content_tables_exist() ) {
     1659                        global $wpdb;
     1660                        $table  = $wpdb->prefix . 'subscription_content_plans';
     1661                        $query  = $wpdb->prepare(
     1662                                "SELECT id, plan_name, content_ids FROM $table WHERE content_type = %s AND status = 'active'",
     1663                                $content_type
     1664                        );
     1665                        if ( $editing_plan_id ) {
     1666                                $query = $wpdb->prepare(
     1667                                        "SELECT id, plan_name, content_ids FROM $table WHERE content_type = %s AND status = 'active' AND id != %d",
     1668                                        $content_type,
     1669                                        $editing_plan_id
     1670                                );
     1671                        }
     1672                        $other_plans = $wpdb->get_results( $query );
     1673                        foreach ( $other_plans as $op ) {
     1674                                $ids = json_decode( $op->content_ids, true ) ?: array();
     1675                                foreach ( $ids as $id ) {
     1676                                        $claimed[ (int) $id ] = $op->plan_name;
     1677                                }
     1678                        }
    16641679                }
    16651680
    … …  
    16681683                switch ( $content_type ) {
    16691684                        case 'category':
    1670                                 $categories = get_categories( array( 'hide_empty' => false ) );
     1685                                $categories = get_categories( array( 'hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC' ) );
    16711686                                $html = '<div class="content-checkboxes">';
    16721687                                foreach ( $categories as $category ) {
    1673                                         $checked = in_array( $category->term_id, $selected_ids ) || in_array( (string)$category->term_id, $selected_ids ) ? 'checked' : '';
    1674                                         $html .= sprintf(
    1675                                                 '<label><input type="checkbox" name="content_ids[]" value="%d" %s> %s (%d posts)</label><br>',
    1676                                                 $category->term_id,
    1677                                                 $checked,
    1678                                                 esc_html( $category->name ),
    1679                                                 $category->count
    1680                                         );
     1688                                        $tid        = (int) $category->term_id;
     1689                                        $is_claimed = isset( $claimed[ $tid ] );
     1690                                        $checked    = ! $is_claimed && ( in_array( $tid, $selected_ids, true ) || in_array( (string) $tid, $selected_ids, true ) ) ? 'checked' : '';
     1691                                        if ( $is_claimed ) {
     1692                                                $html .= sprintf(
     1693                                                        '<label class="ss-claimed-item" title="%s"><input type="checkbox" name="content_ids[]" value="%d" disabled> %s <span class="ss-claimed-badge">%s</span></label><br>',
     1694                                                        esc_attr( sprintf( __( 'Already used by plan: %s', 'subscription-system' ), $claimed[ $tid ] ) ),
     1695                                                        $tid,
     1696                                                        esc_html( $category->name ),
     1697                                                        esc_html( sprintf( __( 'Used by: %s', 'subscription-system' ), $claimed[ $tid ] ) )
     1698                                                );
     1699                                        } else {
     1700                                                $html .= sprintf(
     1701                                                        '<label><input type="checkbox" name="content_ids[]" value="%d" %s> %s <span class="ss-post-count">(%d)</span></label><br>',
     1702                                                        $tid,
     1703                                                        $checked,
     1704                                                        esc_html( $category->name ),
     1705                                                        (int) $category->count
     1706                                                );
     1707                                        }
    16811708                                }
    16821709                                $html .= '</div>';
    … …  
    16841711
    16851712                        case 'tag':
    1686                                 $tags = get_tags( array( 'hide_empty' => false ) );
     1713                                $tags = get_tags( array( 'hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC' ) );
    16871714                                $html = '<div class="content-checkboxes">';
    16881715                                foreach ( $tags as $tag ) {
    1689                                         $checked = in_array( $tag->term_id, $selected_ids ) || in_array( (string)$tag->term_id, $selected_ids ) ? 'checked' : '';
    1690                                         $html .= sprintf(
    1691                                                 '<label><input type="checkbox" name="content_ids[]" value="%d" %s> %s (%d posts)</label><br>',
    1692                                                 $tag->term_id,
    1693                                                 $checked,
    1694                                                 esc_html( $tag->name ),
    1695                                                 $tag->count
    1696                                         );
     1716                                        $tid        = (int) $tag->term_id;
     1717                                        $is_claimed = isset( $claimed[ $tid ] );
     1718                                        $checked    = ! $is_claimed && ( in_array( $tid, $selected_ids, true ) || in_array( (string) $tid, $selected_ids, true ) ) ? 'checked' : '';
     1719                                        if ( $is_claimed ) {
     1720                                                $html .= sprintf(
     1721                                                        '<label class="ss-claimed-item" title="%s"><input type="checkbox" name="content_ids[]" value="%d" disabled> %s <span class="ss-claimed-badge">%s</span></label><br>',
     1722                                                        esc_attr( sprintf( __( 'Already used by plan: %s', 'subscription-system' ), $claimed[ $tid ] ) ),
     1723                                                        $tid,
     1724                                                        esc_html( $tag->name ),
     1725                                                        esc_html( sprintf( __( 'Used by: %s', 'subscription-system' ), $claimed[ $tid ] ) )
     1726                                                );
     1727                                        } else {
     1728                                                $html .= sprintf(
     1729                                                        '<label><input type="checkbox" name="content_ids[]" value="%d" %s> %s <span class="ss-post-count">(%d)</span></label><br>',
     1730                                                        $tid,
     1731                                                        $checked,
     1732                                                        esc_html( $tag->name ),
     1733                                                        (int) $tag->count
     1734                                                );
     1735                                        }
    16971736                                }
    16981737                                $html .= '</div>';
    … …  
    17041743                                        'post_status' => 'publish',
    17051744                                        'orderby'     => 'title',
    1706                                         'order'       => 'ASC'
     1745                                        'order'       => 'ASC',
    17071746                                ) );
    17081747                                $html = '<div class="content-checkboxes">';
    17091748                                foreach ( $posts as $post ) {
    1710                                         $checked = in_array( $post->ID, $selected_ids ) || in_array( (string)$post->ID, $selected_ids ) ? 'checked' : '';
     1749                                        $checked = in_array( $post->ID, $selected_ids, true ) || in_array( (string) $post->ID, $selected_ids, true ) ? 'checked' : '';
    17111750                                        $html .= sprintf(
    17121751                                                '<label><input type="checkbox" name="content_ids[]" value="%d" %s> %s</label><br>',
    … …  
    17231762                                $html = '<div class="content-checkboxes">';
    17241763                                foreach ( $post_types as $post_type ) {
    1725                                         $checked = in_array( $post_type->name, $selected_ids ) ? 'checked' : '';
     1764                                        $checked = in_array( $post_type->name, $selected_ids, true ) ? 'checked' : '';
    17261765                                        $html .= sprintf(
    17271766                                                '<label><input type="checkbox" name="content_ids[]" value="%s" %s> %s</label><br>',
    1728                                                 $post_type->name,
     1767                                                esc_attr( $post_type->name ),
    17291768                                                $checked,
    17301769                                                esc_html( $post_type->label )
    … …  
    17391778                }
    17401779
    1741                 error_log('Generated HTML length: ' . strlen($html));
    1742                 error_log('Generated HTML: ' . substr($html, 0, 500) . '...');
    17431780                wp_send_json_success( $html );
    17441781        }
    … …  
    20092046                ) );
    20102047        }
    2011 }
     2048
     2049        /**
     2050         * Handle interest subscription form submission from the popup modal.
     2051         */
     2052        public function submit_interest_subscription() {
     2053                if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'interest_subscription_nonce' ) ) {
     2054                        wp_send_json_error( array( 'message' => __( 'Security check failed.', 'subscription-system' ) ) );
     2055                }
     2056
     2057                if ( ! Subs_Sys_DB_Updater::interest_table_exists() ) {
     2058                        $db_updater = new Subs_Sys_DB_Updater();
     2059                        $db_updater->add_interest_subscription_table();
     2060                }
     2061
     2062                $category_ids = isset( $_POST['category_ids'] ) ? array_map( 'absint', (array) wp_unslash( $_POST['category_ids'] ) ) : array();
     2063                $tag_ids      = isset( $_POST['tag_ids'] ) ? array_map( 'absint', (array) wp_unslash( $_POST['tag_ids'] ) ) : array();
     2064                $plan_id      = isset( $_POST['plan_id'] ) ? absint( $_POST['plan_id'] ) : 0;
     2065                $plan_source  = isset( $_POST['plan_source'] ) && 'content' === sanitize_text_field( wp_unslash( $_POST['plan_source'] ) ) ? 'content' : 'interest';
     2066
     2067                $interest = new Subs_Sys_Interest_Subscription();
     2068                $result   = $interest->save_subscription(
     2069                        array(
     2070                                'name'         => isset( $_POST['subscriber_name'] ) ? wp_unslash( $_POST['subscriber_name'] ) : '',
     2071                                'email'        => isset( $_POST['subscriber_email'] ) ? wp_unslash( $_POST['subscriber_email'] ) : '',
     2072                                'category_ids' => $category_ids,
     2073                                'tag_ids'      => $tag_ids,
     2074                                'plan_id'      => $plan_id,
     2075                                'plan_source'  => $plan_source,
     2076                        )
     2077                );
     2078
     2079                if ( is_wp_error( $result ) ) {
     2080                        wp_send_json_error( array( 'message' => $result->get_error_message() ) );
     2081                }
     2082
     2083                // Look up plan price from the appropriate table.
     2084                if ( 'content' === $plan_source ) {
     2085                        $plan = Subs_Sys_Interest_Subscription::get_content_plan_by_id( $plan_id );
     2086                } else {
     2087                        $plan = Subs_Sys_Interest_Subscription::get_plan_by_id( $plan_id );
     2088                }
     2089                $is_free = $plan && (float) $plan->price <= 0;
     2090
     2091                if ( ! $is_free ) {
     2092                        $checkout_url = add_query_arg(
     2093                                array(
     2094                                        'action'                   => 'interest_checkout',
     2095                                        'interest_subscription_id' => $result,
     2096                                ),
     2097                                home_url( '/' )
     2098                        );
     2099
     2100                        wp_send_json_success(
     2101                                array(
     2102                                        'message'  => __( 'Subscription saved. Redirecting to payment...', 'subscription-system' ),
     2103                                        'redirect' => $checkout_url,
     2104                                        'plan_id'  => $plan_id,
     2105                                        'price'    => $plan ? (float) $plan->price : 0,
     2106                                )
     2107                        );
     2108                }
     2109
     2110                wp_send_json_success(
     2111                        array(
     2112                                'message' => __( 'You are subscribed! You will receive updates for your selected categories and tags.', 'subscription-system' ),
     2113                                'plan_id' => $plan_id,
     2114                        )
     2115                );
     2116        }
     2117}
  • subscription-system/tags/1.0.18/includes/class-subs-sys-db-updater.php

    r3387836 r3598150  
    3131         * Current database version
    3232         */
    33         const DB_VERSION = '1.0.9';
     33        const DB_VERSION = '1.1.1';
    3434
    3535        /**
    … …  
    7575                // Check if content tables exist
    7676                $content_tables_missing = ! self::content_tables_exist();
     77
     78                $interest_table_missing = ! self::interest_table_exists();
    7779               
    7880                // Perform update if needed
    79                 if ( $version_update_needed || $email_table_missing || $content_tables_missing ) {
     81                if ( $version_update_needed || $email_table_missing || $content_tables_missing || $interest_table_missing ) {
    8082                       
    8183                        // Log what's triggering the update
    … …  
    8890                        if ( $content_tables_missing ) {
    8991                                error_log( "Subs_Sys: Content subscription tables are missing, will create them now" );
     92                        }
     93                        if ( $interest_table_missing ) {
     94                                error_log( 'Subs_Sys: Interest subscription table is missing, will create it now' );
    9095                        }
    9196                       
    … …  
    151156                        error_log( "Subs_Sys: Ensuring email logs table exists..." );
    152157                        $this->add_email_logs_table();
     158                }
     159
     160                if ( version_compare( $old_version, '1.1.0', '<' ) || ! self::interest_table_exists() ) {
     161                        error_log( 'Subs_Sys: Ensuring interest subscription table exists...' );
     162                        $this->add_interest_subscription_table();
     163                }
     164
     165                if ( version_compare( $old_version, '1.1.1', '<' ) ) {
     166                        $this->update_interest_subscription_table();
    153167                }
    154168
    … …  
    310324
    311325        /**
     326         * Add interest subscription table (version 1.1.0)
     327         */
     328        public function add_interest_subscription_table() {
     329                global $wpdb;
     330                $charset_collate = $wpdb->get_charset_collate();
     331
     332                $table = $wpdb->prefix . 'subscription_interest_subscriptions';
     333                $sql   = "CREATE TABLE IF NOT EXISTS $table (
     334                        id mediumint(9) NOT NULL AUTO_INCREMENT,
     335                        user_id bigint(20) DEFAULT NULL,
     336                        plan_id mediumint(9) DEFAULT NULL,
     337                        subscriber_name varchar(100) NOT NULL,
     338                        subscriber_email varchar(100) NOT NULL,
     339                        category_ids text NOT NULL,
     340                        tag_ids text NOT NULL,
     341                        plan_type varchar(20) NOT NULL DEFAULT 'free',
     342                        status varchar(20) NOT NULL DEFAULT 'active',
     343                        payment_id varchar(100) DEFAULT NULL,
     344                        created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
     345                        updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
     346                        PRIMARY KEY  (id),
     347                        KEY user_id (user_id),
     348                        KEY plan_id (plan_id),
     349                        KEY subscriber_email (subscriber_email),
     350                        KEY status (status),
     351                        KEY plan_type (plan_type)
     352                ) $charset_collate;";
     353
     354                require_once ABSPATH . 'wp-admin/includes/upgrade.php';
     355                dbDelta( $sql );
     356
     357                error_log( 'Subs_Sys: Interest subscription table created successfully' );
     358                set_transient( 'subs_sys_interest_table_created', true, 60 );
     359        }
     360
     361        /**
     362         * Add plan_id column to interest subscriptions (version 1.1.1).
     363         */
     364        public function update_interest_subscription_table() {
     365                global $wpdb;
     366
     367                $table = $wpdb->prefix . 'subscription_interest_subscriptions';
     368                if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) {
     369                        return;
     370                }
     371
     372                $column = $wpdb->get_results( "SHOW COLUMNS FROM $table LIKE 'plan_id'" );
     373                if ( empty( $column ) ) {
     374                        $wpdb->query( "ALTER TABLE $table ADD COLUMN plan_id mediumint(9) DEFAULT NULL AFTER user_id" );
     375                        $wpdb->query( "ALTER TABLE $table ADD KEY plan_id (plan_id)" );
     376                }
     377        }
     378
     379        /**
     380         * Check if interest subscription table exists.
     381         *
     382         * @return bool
     383         */
     384        public static function interest_table_exists() {
     385                global $wpdb;
     386                $table = $wpdb->prefix . 'subscription_interest_subscriptions';
     387                return $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) === $table;
     388        }
     389
     390        /**
    312391         * Check if email logs table exists
    313392         *
  • subscription-system/tags/1.0.18/includes/class-subs-sys-shortcodes.php

    r3375846 r3598150  
    5656        add_shortcode('content_subscription_checkout', array($this, 'render_content_subscription_checkout'));
    5757        add_shortcode('content_subscription_success', array($this, 'render_content_subscription_success'));
     58        add_shortcode('subscriptionsystem_subscribe', array($this, 'render_interest_subscribe_button'));
    5859       
    5960        // Add AJAX handlers for password reset
    … …  
    22382239        return ob_get_clean();
    22392240    }
    2240 }
     2241
     2242    /**
     2243     * Render subscribe button with category/tag interest popup modal.
     2244     *
     2245     * Usage: [subscriptionsystem_subscribe] or [subscriptionsystem_subscribe button_text="Subscribe"]
     2246     *
     2247     * @param array $atts Shortcode attributes.
     2248     * @return string
     2249     */
     2250    public function render_interest_subscribe_button( $atts ) {
     2251        $atts = shortcode_atts(
     2252            array(
     2253                'button_text' => __( 'Subscribe', 'subscription-system' ),
     2254                'button_class' => '',
     2255            ),
     2256            $atts,
     2257            'subscriptionsystem_subscribe'
     2258        );
     2259
     2260        $this->enqueue_interest_subscribe_assets();
     2261
     2262        // Gather context (categories/tags) from the current post if applicable.
     2263        $context_categories = array();
     2264        $context_tags       = array();
     2265        if ( is_singular() && get_the_ID() ) {
     2266            if ( get_option( 'subs_modal_context_categories', 1 ) ) {
     2267                $cats = wp_get_post_categories( get_the_ID(), array( 'fields' => 'ids' ) );
     2268                $context_categories = array_map( 'intval', is_array( $cats ) ? $cats : array() );
     2269            }
     2270            if ( get_option( 'subs_modal_context_tags', 1 ) ) {
     2271                $tags_list = wp_get_post_tags( get_the_ID(), array( 'fields' => 'ids' ) );
     2272                $context_tags = array_map( 'intval', is_array( $tags_list ) ? $tags_list : array() );
     2273            }
     2274        }
     2275
     2276        // Only show categories/tags actually on this post; fall back to all on non-singular pages.
     2277        $display_categories = ! empty( $context_categories )
     2278            ? Subs_Sys_Interest_Subscription::get_categories_by_ids( $context_categories )
     2279            : Subs_Sys_Interest_Subscription::get_available_categories();
     2280
     2281        $display_tags = ! empty( $context_tags )
     2282            ? Subs_Sys_Interest_Subscription::get_tags_by_ids( $context_tags )
     2283            : Subs_Sys_Interest_Subscription::get_available_tags();
     2284
     2285        $modal_id = 'ss-interest-modal-' . wp_unique_id();
     2286        $args = array(
     2287            'modal_id'           => $modal_id,
     2288            'categories'         => $display_categories,
     2289            'tags'               => $display_tags,
     2290            'plans'              => Subs_Sys_Interest_Subscription::get_content_modal_plans( $context_categories, $context_tags ),
     2291            'plan_source'        => 'content',
     2292            'context_categories' => $context_categories,
     2293            'context_tags'       => $context_tags,
     2294        );
     2295
     2296        ob_start();
     2297        ?>
     2298        <div class="ss-interest-subscribe-wrap">
     2299            <button type="button"
     2300                class="ss-interest-subscribe-btn <?php echo esc_attr( $atts['button_class'] ); ?>"
     2301                data-ss-open-interest-modal="#<?php echo esc_attr( $modal_id ); ?>">
     2302                <?php echo esc_html( $atts['button_text'] ); ?>
     2303            </button>
     2304        </div>
     2305        <?php
     2306        include SUBS_SYS_PLUGIN_DIR . 'public/partials/interest-subscribe-modal.php';
     2307        return ob_get_clean();
     2308    }
     2309
     2310    /**
     2311     * Enqueue assets for the interest subscribe modal.
     2312     */
     2313    private function enqueue_interest_subscribe_assets() {
     2314        subs_sys_enqueue_interest_modal_assets( $this->plugin_name, $this->version );
     2315    }
     2316}
  • subscription-system/tags/1.0.18/includes/class-subs-sys.php

    r3587496 r3598150  
    7676                        $this->version = SUBS_SYS_VERSION;
    7777                } else {
    78                         $this->version = '1.0.17';
     78                        $this->version = '1.0.18';
    7979                }
    8080                $this->plugin_name = 'subs-sys';
    … …  
    176176                 */
    177177                require_once __DIR__ . '/class-subs-sys-content-manager.php';
     178
     179                /**
     180                 * The class responsible for category/tag interest subscriptions
     181                 */
     182                require_once __DIR__ . '/class-subs-sys-interest-subscription.php';
    178183
    179184        /**
    … …  
    277282                // Initialize content subscription manager
    278283                $content_manager = new Subs_Sys_Content_Manager();
     284
     285                // Initialize interest subscription manager
     286                $interest_subscription = new Subs_Sys_Interest_Subscription();
    279287
    280288                // Initialize database updater (safe updates)
  • subscription-system/tags/1.0.18/public/class-subs-sys-public.php

    r3356551 r3598150  
    5252        add_action('wp_head', array($this, 'display_smart_subscription_bar'));
    5353        add_action('template_redirect', array($this, 'handle_content_checkout_redirect'));
     54        add_action('template_redirect', array($this, 'handle_interest_checkout_redirect'));
     55        add_action('template_redirect', array($this, 'handle_interest_checkout_success'));
    5456        add_action('template_redirect', array($this, 'handle_content_subscription_success'));
    5557        add_action('wp_head', array($this, 'display_subscription_success_message'));
     58
     59        // Floating button + auto-open modal
     60        add_action('wp_footer', array($this, 'render_interest_modal_triggers'));
    5661    }
    5762
    … …  
    24542459
    24552460    /**
     2461     * Handle interest subscription paid checkout redirect (phase 1 placeholder).
     2462     */
     2463    public function handle_interest_checkout_redirect() {
     2464        if ( ! isset( $_GET['action'] ) || 'interest_checkout' !== $_GET['action'] ) {
     2465            return;
     2466        }
     2467
     2468        $subscription_id = isset( $_GET['interest_subscription_id'] ) ? absint( $_GET['interest_subscription_id'] ) : 0;
     2469        if ( ! $subscription_id ) {
     2470            wp_die( esc_html__( 'Invalid subscription.', 'subscription-system' ) );
     2471        }
     2472
     2473        $interest     = new Subs_Sys_Interest_Subscription();
     2474        $subscription = $interest->get_subscription( $subscription_id );
     2475
     2476        if ( ! $subscription || 'paid' !== $subscription->plan_type ) {
     2477            wp_die( esc_html__( 'Subscription not found.', 'subscription-system' ) );
     2478        }
     2479
     2480        // Resolve plan — try content plans first, fall back to regular plans.
     2481        $plan      = null;
     2482        $plan_name = __( 'Paid Subscription', 'subscription-system' );
     2483        $price     = 0.0;
     2484        $duration  = 30;
     2485        $stripe_price_id = '';
     2486
     2487        if ( ! empty( $subscription->plan_id ) ) {
     2488            $cp = Subs_Sys_Interest_Subscription::get_content_plan_by_id( $subscription->plan_id );
     2489            if ( $cp ) {
     2490                $plan            = $cp;
     2491                $plan_name       = $cp->plan_name;
     2492                $price           = (float) $cp->price;
     2493                $duration        = (int) ( $cp->duration ?? 30 );
     2494                $stripe_price_id = $cp->stripe_price_id ?? '';
     2495            } else {
     2496                $rp = Subs_Sys_Interest_Subscription::get_plan_by_id( $subscription->plan_id );
     2497                if ( $rp ) {
     2498                    $plan            = $rp;
     2499                    $plan_name       = $rp->plan_name;
     2500                    $price           = (float) $rp->price;
     2501                    $duration        = (int) ( $rp->duration ?? 30 );
     2502                    $stripe_price_id = $rp->stripe_price_id ?? '';
     2503                }
     2504            }
     2505        }
     2506
     2507        $price = $price > 0 ? $price : Subs_Sys_Interest_Subscription::get_paid_price();
     2508
     2509        // Resolve billing interval from duration.
     2510        $interval       = 'month';
     2511        $interval_count = 1;
     2512        if ( $duration <= 1 )        { $interval = 'day';   $interval_count = 1; }
     2513        elseif ( $duration <= 7 )    { $interval = 'week';  $interval_count = 1; }
     2514        elseif ( $duration <= 31 )   { $interval = 'month'; $interval_count = 1; }
     2515        elseif ( $duration <= 93 )   { $interval = 'month'; $interval_count = 3; }
     2516        elseif ( $duration <= 186 )  { $interval = 'month'; $interval_count = 6; }
     2517        else                         { $interval = 'year';  $interval_count = 1; }
     2518
     2519        // Build success / cancel URLs.
     2520        $success_url = add_query_arg( array(
     2521            'action'                   => 'interest_checkout_success',
     2522            'interest_subscription_id' => $subscription_id,
     2523            'session_id'               => '{CHECKOUT_SESSION_ID}',
     2524        ), home_url( '/' ) );
     2525
     2526        $cancel_url = add_query_arg( array(
     2527            'action'                   => 'interest_checkout',
     2528            'interest_subscription_id' => $subscription_id,
     2529            'cancelled'                => '1',
     2530        ), home_url( '/' ) );
     2531
     2532        // ── Try Stripe Checkout ────────────────────────────────────────────
     2533        $stripe_enabled = get_option( 'subscription_stripe_enabled', 0 );
     2534        $test_mode      = get_option( 'subscription_stripe_test_mode', 1 );
     2535        $api_key        = $test_mode
     2536            ? get_option( 'subscription_stripe_test_secret_key', '' )
     2537            : get_option( 'subscription_stripe_live_secret_key', '' );
     2538        if ( empty( $api_key ) ) {
     2539            $api_key = get_option( 'subscription_stripe_secret_key', '' );
     2540        }
     2541
     2542        if ( $stripe_enabled && ! empty( $api_key ) ) {
     2543            if ( ! class_exists( '\Stripe\Stripe' ) ) {
     2544                $admin = new Subs_Sys_Admin( $this->plugin_name, $this->version );
     2545                if ( method_exists( $admin, 'ensure_stripe_sdk_loaded' ) ) {
     2546                    $admin->ensure_stripe_sdk_loaded();
     2547                }
     2548            }
     2549
     2550            if ( class_exists( '\Stripe\Stripe' ) ) {
     2551                try {
     2552                    \Stripe\Stripe::setApiKey( $api_key );
     2553
     2554                    // Use the synced price ID if it exists and is not a mock/free placeholder.
     2555                    $use_existing_price = ! empty( $stripe_price_id )
     2556                        && strpos( $stripe_price_id, 'mock_' ) === false
     2557                        && 'free' !== $stripe_price_id;
     2558
     2559                    $line_item = $use_existing_price
     2560                        ? array( 'price' => $stripe_price_id, 'quantity' => 1 )
     2561                        : array(
     2562                            'quantity'   => 1,
     2563                            'price_data' => array(
     2564                                'currency'     => strtolower( get_option( 'subscription_currency', 'usd' ) ),
     2565                                'product_data' => array( 'name' => $plan_name ),
     2566                                'unit_amount'  => (int) ( $price * 100 ),
     2567                                'recurring'    => array(
     2568                                    'interval'       => $interval,
     2569                                    'interval_count' => $interval_count,
     2570                                ),
     2571                            ),
     2572                        );
     2573
     2574                    $session = \Stripe\Checkout\Session::create( array(
     2575                        'payment_method_types' => array( 'card' ),
     2576                        'mode'                 => 'subscription',
     2577                        'line_items'           => array( $line_item ),
     2578                        'customer_email'       => $subscription->subscriber_email,
     2579                        'success_url'          => $success_url,
     2580                        'cancel_url'           => $cancel_url,
     2581                        'metadata'             => array(
     2582                            'interest_subscription_id' => $subscription_id,
     2583                            'subscriber_name'          => $subscription->subscriber_name,
     2584                        ),
     2585                    ) );
     2586
     2587                    // Redirect to Stripe-hosted checkout page.
     2588                    wp_redirect( $session->url );
     2589                    exit;
     2590
     2591                } catch ( \Exception $e ) {
     2592                    // Fall through to the info page with error notice.
     2593                    $stripe_error = $e->getMessage();
     2594                }
     2595            }
     2596        }
     2597
     2598        // ── Fallback: show info page (Stripe not configured) ──────────────
     2599        $category_names = array();
     2600        $tag_names      = array();
     2601        foreach ( (array) $subscription->category_ids as $cat_id ) {
     2602            $term = get_term( $cat_id, 'category' );
     2603            if ( $term && ! is_wp_error( $term ) ) {
     2604                $category_names[] = $term->name;
     2605            }
     2606        }
     2607        foreach ( (array) $subscription->tag_ids as $tag_id ) {
     2608            $term = get_term( $tag_id, 'post_tag' );
     2609            if ( $term && ! is_wp_error( $term ) ) {
     2610                $tag_names[] = $term->name;
     2611            }
     2612        }
     2613
     2614        $cancelled     = isset( $_GET['cancelled'] );
     2615        $stripe_setup  = admin_url( 'admin.php?page=subscription-settings&tab=payment' );
     2616        $price_display = '$' . number_format( $price, 2 ) . ' / ' . $interval_count . ' ' . $interval . ( $interval_count > 1 ? 's' : '' );
     2617
     2618        status_header( 200 );
     2619        nocache_headers();
     2620        ?>
     2621        <!DOCTYPE html>
     2622        <html <?php language_attributes(); ?>>
     2623        <head>
     2624            <meta charset="<?php bloginfo( 'charset' ); ?>">
     2625            <meta name="viewport" content="width=device-width, initial-scale=1">
     2626            <title><?php esc_html_e( 'Complete Your Subscription', 'subscription-system' ); ?></title>
     2627            <?php wp_head(); ?>
     2628            <style>
     2629                *{box-sizing:border-box}
     2630                body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;margin:0;padding:20px;background:#f3f4f6;color:#111827}
     2631                .cc{max-width:580px;margin:40px auto;background:#fff;border-radius:14px;box-shadow:0 4px 24px rgba(0,0,0,.09);overflow:hidden}
     2632                .ch{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;padding:32px;text-align:center}
     2633                .ch h1{margin:0 0 6px;font-size:22px}
     2634                .ch p{margin:0;opacity:.9;font-size:15px}
     2635                .cb{padding:28px 32px}
     2636                .row{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:20px}
     2637                .pill{background:#eef2ff;color:#4f46e5;border-radius:20px;padding:5px 14px;font-size:13px;font-weight:500}
     2638                .notice{padding:16px;border-radius:8px;font-size:14px;margin-bottom:18px;line-height:1.5}
     2639                .notice.info{background:#eef2ff;color:#3730a3;border-left:4px solid #667eea}
     2640                .notice.warn{background:#fff7ed;color:#9a3412;border-left:4px solid #f97316}
     2641                .notice.error{background:#fef2f2;color:#991b1b;border-left:4px solid #ef4444}
     2642                .btn{display:inline-block;padding:12px 28px;border-radius:8px;font-size:15px;font-weight:600;text-decoration:none;margin-top:6px}
     2643                .btn-primary{background:#667eea;color:#fff}
     2644                .btn-ghost{color:#667eea;border:1px solid #c7d2fe}
     2645                .label{font-size:12px;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px}
     2646            </style>
     2647        </head>
     2648        <body>
     2649        <div class="cc">
     2650            <div class="ch">
     2651                <h1><?php echo esc_html( $plan_name ); ?></h1>
     2652                <p><?php echo esc_html( $price_display ); ?></p>
     2653            </div>
     2654            <div class="cb">
     2655                <p><strong><?php esc_html_e( 'Subscriber:', 'subscription-system' ); ?></strong>
     2656                   <?php echo esc_html( $subscription->subscriber_name ); ?>
     2657                   &lt;<?php echo esc_html( $subscription->subscriber_email ); ?>&gt;</p>
     2658
     2659                <?php if ( ! empty( $category_names ) || ! empty( $tag_names ) ) : ?>
     2660                <p class="label"><?php esc_html_e( 'Subscribing to', 'subscription-system' ); ?></p>
     2661                <div class="row">
     2662                    <?php foreach ( $category_names as $n ) : ?>
     2663                        <span class="pill">📁 <?php echo esc_html( $n ); ?></span>
     2664                    <?php endforeach; ?>
     2665                    <?php foreach ( $tag_names as $n ) : ?>
     2666                        <span class="pill">🏷️ <?php echo esc_html( $n ); ?></span>
     2667                    <?php endforeach; ?>
     2668                </div>
     2669                <?php endif; ?>
     2670
     2671                <?php if ( $cancelled ) : ?>
     2672                <div class="notice warn">
     2673                    <?php esc_html_e( 'Payment was cancelled. Your subscription has not been activated. You can try again below.', 'subscription-system' ); ?>
     2674                </div>
     2675                <?php endif; ?>
     2676
     2677                <?php if ( ! empty( $stripe_error ) ) : ?>
     2678                <div class="notice error">
     2679                    <strong><?php esc_html_e( 'Stripe error:', 'subscription-system' ); ?></strong>
     2680                    <?php echo esc_html( $stripe_error ); ?>
     2681                </div>
     2682                <?php endif; ?>
     2683
     2684                <?php if ( ! $stripe_enabled || empty( $api_key ) ) : ?>
     2685                <div class="notice info">
     2686                    <strong><?php esc_html_e( 'Action required:', 'subscription-system' ); ?></strong>
     2687                    <?php esc_html_e( 'Stripe is not configured yet. To accept payments, go to your admin panel and add your Stripe API keys.', 'subscription-system' ); ?>
     2688                    <?php if ( current_user_can( 'manage_options' ) ) : ?>
     2689                        <br><br>
     2690                        <a class="btn btn-primary" href="<?php echo esc_url( $stripe_setup ); ?>">
     2691                            <?php esc_html_e( '⚙ Configure Stripe →', 'subscription-system' ); ?>
     2692                        </a>
     2693                    <?php endif; ?>
     2694                </div>
     2695                <?php else : ?>
     2696                <div class="notice info">
     2697                    <?php esc_html_e( 'Your subscription preferences are saved. There was a problem launching Stripe. Please contact the site administrator.', 'subscription-system' ); ?>
     2698                </div>
     2699                <?php endif; ?>
     2700
     2701                <p style="margin-top:24px">
     2702                    <a class="btn btn-ghost" href="<?php echo esc_url( home_url( '/' ) ); ?>">
     2703                        &larr; <?php esc_html_e( 'Back to Home', 'subscription-system' ); ?>
     2704                    </a>
     2705                </p>
     2706            </div>
     2707        </div>
     2708        <?php wp_footer(); ?>
     2709        </body>
     2710        </html>
     2711        <?php
     2712        exit;
     2713    }
     2714
     2715    /**
     2716     * Handle Stripe success redirect after interest subscription payment.
     2717     */
     2718    public function handle_interest_checkout_success() {
     2719        if ( ! isset( $_GET['action'] ) || 'interest_checkout_success' !== $_GET['action'] ) {
     2720            return;
     2721        }
     2722
     2723        $subscription_id = isset( $_GET['interest_subscription_id'] ) ? absint( $_GET['interest_subscription_id'] ) : 0;
     2724        $session_id      = isset( $_GET['session_id'] ) ? sanitize_text_field( wp_unslash( $_GET['session_id'] ) ) : '';
     2725
     2726        if ( ! $subscription_id ) {
     2727            wp_die( esc_html__( 'Invalid subscription.', 'subscription-system' ) );
     2728        }
     2729
     2730        $interest     = new Subs_Sys_Interest_Subscription();
     2731        $subscription = $interest->get_subscription( $subscription_id );
     2732
     2733        if ( ! $subscription ) {
     2734            wp_die( esc_html__( 'Subscription not found.', 'subscription-system' ) );
     2735        }
     2736
     2737        // Verify with Stripe if session ID is provided.
     2738        $verified = false;
     2739        if ( ! empty( $session_id ) ) {
     2740            $test_mode = get_option( 'subscription_stripe_test_mode', 1 );
     2741            $api_key   = $test_mode
     2742                ? get_option( 'subscription_stripe_test_secret_key', '' )
     2743                : get_option( 'subscription_stripe_live_secret_key', '' );
     2744            if ( empty( $api_key ) ) {
     2745                $api_key = get_option( 'subscription_stripe_secret_key', '' );
     2746            }
     2747
     2748            if ( ! empty( $api_key ) && class_exists( '\Stripe\Stripe' ) ) {
     2749                try {
     2750                    \Stripe\Stripe::setApiKey( $api_key );
     2751                    $session  = \Stripe\Checkout\Session::retrieve( $session_id );
     2752                    $verified = in_array( $session->payment_status, array( 'paid', 'no_payment_required' ), true );
     2753                } catch ( \Exception $e ) {
     2754                    // Verification failed — still activate (webhook will handle edge cases).
     2755                    $verified = false;
     2756                }
     2757            }
     2758        }
     2759
     2760        // Activate the subscription (Stripe verified, or session_id was absent — trust the redirect).
     2761        if ( $verified || empty( $session_id ) ) {
     2762            $interest->activate_paid_subscription( $subscription_id, $session_id );
     2763        }
     2764
     2765        // Build a clean success page.
     2766        $category_names = array();
     2767        $tag_names      = array();
     2768        foreach ( (array) $subscription->category_ids as $cat_id ) {
     2769            $term = get_term( $cat_id, 'category' );
     2770            if ( $term && ! is_wp_error( $term ) ) { $category_names[] = $term->name; }
     2771        }
     2772        foreach ( (array) $subscription->tag_ids as $tag_id ) {
     2773            $term = get_term( $tag_id, 'post_tag' );
     2774            if ( $term && ! is_wp_error( $term ) ) { $tag_names[] = $term->name; }
     2775        }
     2776
     2777        status_header( 200 );
     2778        nocache_headers();
     2779        ?>
     2780        <!DOCTYPE html>
     2781        <html <?php language_attributes(); ?>>
     2782        <head>
     2783            <meta charset="<?php bloginfo( 'charset' ); ?>">
     2784            <meta name="viewport" content="width=device-width, initial-scale=1">
     2785            <title><?php esc_html_e( 'Subscription Activated', 'subscription-system' ); ?></title>
     2786            <?php wp_head(); ?>
     2787            <style>
     2788                *{box-sizing:border-box}
     2789                body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;margin:0;padding:20px;background:#f3f4f6;color:#111827}
     2790                .cc{max-width:520px;margin:60px auto;background:#fff;border-radius:14px;box-shadow:0 4px 24px rgba(0,0,0,.09);padding:40px;text-align:center}
     2791                .icon{font-size:56px;margin-bottom:12px}
     2792                h1{margin:0 0 10px;font-size:24px;color:#111827}
     2793                p{color:#6b7280;font-size:15px;margin:0 0 20px;line-height:1.6}
     2794                .row{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin:16px 0 28px}
     2795                .pill{background:#d1fae5;color:#065f46;border-radius:20px;padding:6px 16px;font-size:13px;font-weight:500}
     2796                .btn{display:inline-block;padding:13px 30px;border-radius:8px;font-size:15px;font-weight:600;text-decoration:none;background:#667eea;color:#fff}
     2797            </style>
     2798        </head>
     2799        <body>
     2800        <div class="cc">
     2801            <div class="icon">🎉</div>
     2802            <h1><?php esc_html_e( 'You\'re subscribed!', 'subscription-system' ); ?></h1>
     2803            <p><?php esc_html_e( 'Your subscription is now active. You\'ll receive email notifications when new content is published in:', 'subscription-system' ); ?></p>
     2804            <div class="row">
     2805                <?php foreach ( $category_names as $n ) : ?>
     2806                    <span class="pill">📁 <?php echo esc_html( $n ); ?></span>
     2807                <?php endforeach; ?>
     2808                <?php foreach ( $tag_names as $n ) : ?>
     2809                    <span class="pill">🏷️ <?php echo esc_html( $n ); ?></span>
     2810                <?php endforeach; ?>
     2811            </div>
     2812            <a class="btn" href="<?php echo esc_url( home_url( '/' ) ); ?>">
     2813                <?php esc_html_e( 'Back to Home', 'subscription-system' ); ?>
     2814            </a>
     2815        </div>
     2816        <?php wp_footer(); ?>
     2817        </body>
     2818        </html>
     2819        <?php
     2820        exit;
     2821    }
     2822
     2823    /**
    24562824     * Handle content subscription success redirect
    24572825     */
    … …  
    27323100        return $applicable_plans;
    27333101    }
     3102
     3103    // ──────────────────────────────────────────────────────────────────────────
     3104    // Subscribe Button & Modal Trigger helpers
     3105    // ──────────────────────────────────────────────────────────────────────────
     3106
     3107    /**
     3108     * Determine whether the modal triggers should render on the current page,
     3109     * based on the admin Display Settings.
     3110     *
     3111     * @return bool
     3112     */
     3113    private function should_render_modal_triggers() {
     3114        // All posts (any singular post type except page).
     3115        if ( get_option( 'subs_modal_display_all_posts', 0 ) && is_singular() && ! is_page() ) {
     3116            return true;
     3117        }
     3118
     3119        // All pages.
     3120        if ( get_option( 'subs_modal_display_all_pages', 0 ) && is_page() ) {
     3121            return true;
     3122        }
     3123
     3124        // Single posts only.
     3125        if ( get_option( 'subs_modal_display_single_posts', 0 ) && is_single() ) {
     3126            return true;
     3127        }
     3128
     3129        // Single pages only.
     3130        if ( get_option( 'subs_modal_display_single_pages', 0 ) && is_page() && is_singular() ) {
     3131            return true;
     3132        }
     3133
     3134        // Custom post types.
     3135        $selected_cpts = (array) get_option( 'subs_modal_display_post_types', array() );
     3136        if ( ! empty( $selected_cpts ) && is_singular( $selected_cpts ) ) {
     3137            return true;
     3138        }
     3139
     3140        // Specific pages.
     3141        $specific_pages = array_map( 'absint', (array) get_option( 'subs_modal_display_specific_pages', array() ) );
     3142        if ( ! empty( $specific_pages ) && is_page( $specific_pages ) ) {
     3143            return true;
     3144        }
     3145
     3146        return false;
     3147    }
     3148
     3149    /**
     3150     * Collect context data (category/tag IDs) for the current post.
     3151     *
     3152     * @return array{category_ids: int[], tag_ids: int[]}
     3153     */
     3154    private function get_current_post_context() {
     3155        $context = array( 'category_ids' => array(), 'tag_ids' => array() );
     3156
     3157        if ( ! is_singular() ) {
     3158            return $context;
     3159        }
     3160
     3161        $post_id = get_the_ID();
     3162        if ( ! $post_id ) {
     3163            return $context;
     3164        }
     3165
     3166        if ( get_option( 'subs_modal_context_categories', 1 ) ) {
     3167            $cats = wp_get_post_categories( $post_id, array( 'fields' => 'ids' ) );
     3168            $context['category_ids'] = array_map( 'intval', is_array( $cats ) ? $cats : array() );
     3169        }
     3170
     3171        if ( get_option( 'subs_modal_context_tags', 1 ) ) {
     3172            $tags = wp_get_post_tags( $post_id, array( 'fields' => 'ids' ) );
     3173            $context['tag_ids'] = array_map( 'intval', is_array( $tags ) ? $tags : array() );
     3174        }
     3175
     3176        return $context;
     3177    }
     3178
     3179    /**
     3180     * Output the floating button and/or auto-open modal in wp_footer,
     3181     * respecting the configured display settings.
     3182     */
     3183    public function render_interest_modal_triggers() {
     3184        $show_floating = (bool) get_option( 'subs_modal_floating_btn', 0 );
     3185        $auto_open     = (bool) get_option( 'subs_modal_auto_open', 0 );
     3186
     3187        if ( ! $show_floating && ! $auto_open ) {
     3188            return;
     3189        }
     3190
     3191        if ( ! $this->should_render_modal_triggers() ) {
     3192            return;
     3193        }
     3194
     3195        // Enqueue the modal assets (shared helper, safe to call multiple times).
     3196        subs_sys_enqueue_interest_modal_assets( $this->plugin_name, $this->version );
     3197
     3198        $modal_id = 'ss-interest-modal-footer';
     3199        $btn_text = get_option( 'subs_modal_floating_btn_text', __( 'Subscribe', 'subscription-system' ) );
     3200        $btn_pos  = sanitize_key( get_option( 'subs_modal_floating_btn_position', 'bottom-right' ) );
     3201        $context  = $this->get_current_post_context();
     3202
     3203        // Only show the categories/tags actually assigned to the current post.
     3204        // Falls back to all available on non-singular pages (archives, home, etc.).
     3205        if ( ! empty( $context['category_ids'] ) ) {
     3206            $categories = Subs_Sys_Interest_Subscription::get_categories_by_ids( $context['category_ids'] );
     3207        } else {
     3208            $categories = Subs_Sys_Interest_Subscription::get_available_categories();
     3209        }
     3210
     3211        if ( ! empty( $context['tag_ids'] ) ) {
     3212            $tags = Subs_Sys_Interest_Subscription::get_tags_by_ids( $context['tag_ids'] );
     3213        } else {
     3214            $tags = Subs_Sys_Interest_Subscription::get_available_tags();
     3215        }
     3216
     3217        $plans = Subs_Sys_Interest_Subscription::get_content_modal_plans( $context['category_ids'], $context['tag_ids'] );
     3218
     3219        // Output floating button.
     3220        if ( $show_floating ) {
     3221            printf(
     3222                '<button type="button" class="ss-interest-subscribe-btn ss-interest-floating-btn ss-floating--%s" data-ss-open-interest-modal="#%s" aria-label="%s">%s</button>',
     3223                esc_attr( $btn_pos ),
     3224                esc_attr( $modal_id ),
     3225                esc_attr( $btn_text ),
     3226                esc_html( $btn_text )
     3227            );
     3228        }
     3229
     3230        // Output the modal (with context data as data attributes for JS to pick up).
     3231        $args = array(
     3232            'modal_id'           => $modal_id,
     3233            'categories'         => $categories,
     3234            'tags'               => $tags,
     3235            'plans'              => $plans,
     3236            'plan_source'        => 'content',
     3237            'context_categories' => $context['category_ids'],
     3238            'context_tags'       => $context['tag_ids'],
     3239        );
     3240
     3241        // Pass auto-open data attribute so JS knows to open this specific modal.
     3242        $data_auto_open = $auto_open ? ' data-ss-auto-open="1"' : '';
     3243        echo '<div' . $data_auto_open . '>';
     3244        include SUBS_SYS_PLUGIN_DIR . 'public/partials/interest-subscribe-modal.php';
     3245        echo '</div>';
     3246    }
    27343247}
  • subscription-system/tags/1.0.18/subscription-system.php

    r3587496 r3598150  
    1717 * Plugin URI:        https://mediahubster.in/subscription-system
    1818 * Description:       A comprehensive subscription management system for WordPress.
    19  * Version:           1.0.17
     19 * Version:           1.0.18
    2020 * Author:            Joe Youngblood
    2121 * Author URI:        https://mediahubster.in
    … …  
    9797
    9898// Define plugin constants
    99 define('SUBS_SYS_VERSION', '1.0.17');
     99define('SUBS_SYS_VERSION', '1.0.18');
    100100define('SUBS_SYS_PLUGIN_DIR', plugin_dir_path(__FILE__));
    101101define('SUBS_SYS_PLUGIN_URL', plugin_dir_url(__FILE__));
    102102define('SUBS_SYS_PLUGIN_BASENAME', plugin_basename(__FILE__));
     103
     104/**
     105 * Shared helper: enqueue interest-subscribe modal assets + localize script vars.
     106 * Called by both the shortcode handler and the floating-button/auto-open renderer
     107 * so the JS object is always fully populated regardless of which trigger is used.
     108 *
     109 * @param string $plugin_name Plugin slug.
     110 * @param string $version     Plugin version string.
     111 */
     112function subs_sys_enqueue_interest_modal_assets( $plugin_name, $version ) {
     113    static $done = false;
     114    if ( $done ) {
     115        return;
     116    }
     117    $done = true;
     118
     119    wp_enqueue_style(
     120        $plugin_name . '-interest-subscribe',
     121        SUBS_SYS_PLUGIN_URL . 'public/css/interest-subscribe.css',
     122        array(),
     123        $version
     124    );
     125
     126    wp_enqueue_script(
     127        $plugin_name . '-interest-subscribe',
     128        SUBS_SYS_PLUGIN_URL . 'public/js/interest-subscribe.js',
     129        array( 'jquery' ),
     130        $version,
     131        true
     132    );
     133
     134    $current_user = wp_get_current_user();
     135    $is_logged_in = $current_user->exists();
     136
     137    wp_localize_script(
     138        $plugin_name . '-interest-subscribe',
     139        'ssInterestVars',
     140        array(
     141            'ajax_url'              => admin_url( 'admin-ajax.php' ),
     142            'nonce'                 => wp_create_nonce( 'interest_subscription_nonce' ),
     143            'auto_open'             => (int) get_option( 'subs_modal_auto_open', 0 ),
     144            'auto_open_delay'       => absint( get_option( 'subs_modal_auto_open_delay', 3 ) ),
     145            'hide_fields_logged_in' => (int) get_option( 'subs_modal_hide_fields_logged_in', 1 ),
     146            'is_logged_in'          => $is_logged_in ? 1 : 0,
     147            'i18n'                  => array(
     148                'processing'  => __( 'Processing...', 'subscription-system' ),
     149                'submit'      => __( 'Subscribe', 'subscription-system' ),
     150                'error'       => __( 'Something went wrong. Please try again.', 'subscription-system' ),
     151                'select_plan' => __( 'Please select a subscription plan.', 'subscription-system' ),
     152            ),
     153        )
     154    );
     155}
    103156
    104157// Load Composer autoloader if it exists
    … …  
    312365        KEY status (status)
    313366    ) $charset_collate;";
     367
     368    // Create interest subscriptions table
     369    $interest_table = $wpdb->prefix . 'subscription_interest_subscriptions';
     370    $sql_interest = "CREATE TABLE IF NOT EXISTS $interest_table (
     371        id mediumint(9) NOT NULL AUTO_INCREMENT,
     372        user_id bigint(20) DEFAULT NULL,
     373        plan_id mediumint(9) DEFAULT NULL,
     374        subscriber_name varchar(100) NOT NULL,
     375        subscriber_email varchar(100) NOT NULL,
     376        category_ids text NOT NULL,
     377        tag_ids text NOT NULL,
     378        plan_type varchar(20) NOT NULL DEFAULT 'free',
     379        status varchar(20) NOT NULL DEFAULT 'active',
     380        payment_id varchar(100) DEFAULT NULL,
     381        created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
     382        updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
     383        PRIMARY KEY  (id),
     384        KEY user_id (user_id),
     385        KEY plan_id (plan_id),
     386        KEY subscriber_email (subscriber_email),
     387        KEY status (status),
     388        KEY plan_type (plan_type)
     389    ) $charset_collate;";
    314390   
    315391    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    … …  
    324400    dbDelta($sql_content_subscriptions);
    325401    dbDelta($sql_content_notifications);
     402    dbDelta($sql_interest);
    326403   
    327404    // Add version to options
    … …  
    364441    $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}subscription_content_subscriptions");
    365442    $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}subscription_content_notifications");
     443    $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}subscription_interest_subscriptions");
    366444
    367445    // Delete options
  • subscription-system/trunk/README.txt

    r3587496 r3598150  
    66Tested up to: 7.0
    77Requires PHP: 8.0
    8 Stable tag: 1.0.17
     8Stable tag: 1.0.18
    99License: GPLv2 or later
    1010License URI: http://www.gnu.org/licenses/gpl-2.0.html
    … …  
    8585
    8686== Changelog ==
     87
     88= 1.0.18 =
     89**Subscribe Button & Modal — Category / Tag Interest Subscriptions**
     90* Added new "Subscribe Button & Modal" admin settings tab with full trigger, display, and context configuration
     91* Floating subscribe button with configurable position (bottom-right, bottom-left, bottom-center)
     92* Auto-open modal on page load with configurable delay
     93* Shortcode `[subscriptionsystem_subscribe]` to place a subscribe button anywhere
     94* Context-aware modal: automatically detects and pre-selects the current post's categories and tags
     95* Logged-in user experience: hides name/email fields, greets user by name, and auto-fills their details
     96* Guest (not-logged-in) modal now shows a "Log in / Not now" panel instead of the full form
     97* After login, modal automatically reopens on the same page via `?ss_subscribe=1` URL parameter
     98* Session parameter is silently removed from the URL via `history.replaceState` after modal opens
     99
     100**Content Plans (Subscribe to Updates)**
     101* Modal now sources plans exclusively from **Subscription → Content Plans** instead of generic plans
     102* Strict plan filtering: only shows plans whose categories/tags exactly match the current post
     103* Each plan card now displays which categories or tags it covers (e.g. "Category: Technology")
     104* Conflict prevention: categories and tags already assigned to one active plan are shown as disabled (greyed out with "Used by: [Plan Name]" badge) when creating/editing another plan
     105* Backend validation blocks saving a plan with duplicate categories/tags — clear error message lists every conflict
     106* Content Plans admin list now has a **Stripe** column showing connection status (Connected / Mock / None)
     107* "Sync with Stripe" link on each Content Plan — creates/updates Stripe Product and Price, saves IDs back to DB
     108* Free content plans skip Stripe price creation (product only)
     109
     110**Checkout & Payment**
     111* Interest subscription checkout now creates a real **Stripe Checkout Session** and redirects users to Stripe's hosted payment page
     112* Plan lookup fixed to read from `subscription_content_plans` (or falls back to `subscription_plans` for legacy records)
     113* Uses pre-synced `stripe_price_id` when available; falls back to inline `price_data` for unsynced plans
     114* New `/?action=interest_checkout_success` handler: verifies Stripe session, activates the subscription, and shows a styled success page
     115* Cancelled Stripe sessions return to the checkout info page with a clear "Payment was cancelled" notice
     116* Fallback info page (when Stripe is not configured) now shows an admin-only "Configure Stripe →" button
     117
     118**Subscriber Experience Improvements**
     119* Modal categories/tags list filtered to show only terms assigned to the current post (not all site terms)
     120* Section labels update contextually: "Categories in this post" / "Tags in this post" vs. "Select Categories"
     121* All post-specific categories and tags are pre-checked by default
     122* Intro text adapts: "Subscribe to get notified when new content is published in these topics."
     123* Empty categories or tags sections are hidden rather than showing a placeholder
     124
     125**Plan Management (Subscription Plans)**
     126* Add/Edit plan forms include Status (Active/Inactive), Max Categories, and Max Tags fields
     127* Plans list table shows Interest Limits ("Cat: X, Tags: Y") and Status badge
     128* Backend correctly saves `features` JSON with `max_categories`, `max_tags`, and `status` on both create and update
    87129
    88130= 1.0.17 =
    … …  
    215257== Upgrade Notice ==
    216258
     259= 1.0.18 =
     260Major feature update. Introduces the Subscribe Button & Modal system with floating buttons, auto-open, and context-aware category/tag subscriptions. Content Plans now power the modal with strict per-post plan filtering, Stripe sync, and a full Stripe Checkout payment flow. Guest users see a "Log in / Not now" prompt. Includes plan conflict prevention, improved plan cards, and a polished post-payment success page.
     261
    217262= 1.0.17 =
    218263Recommended update for all users. Improves compatibility with the latest WordPress versions (including WordPress 7.0), enhances codebase structure, and includes general stability and performance improvements.
  • subscription-system/trunk/admin/class-subs-sys-admin.php

    r3463757 r3598150  
    9797        // 6. Content Subscribers (content-specific subscribers)
    9898        $this->add_content_subscribers_submenu();
     99
     100        // 6b. Interest Subscribers (category/tag popup subscriptions)
     101        $this->add_interest_subscribers_submenu();
    99102       
    100103        // 7. Channels
    … …  
    991994
    992995    /**
     996     * Add Interest Subscribers submenu.
     997     */
     998    public function add_interest_subscribers_submenu() {
     999        add_submenu_page(
     1000            'subscription-dashboard',
     1001            esc_html__( 'Interest Subscribers', 'subscription-system' ),
     1002            esc_html__( 'Interest Subscribers', 'subscription-system' ),
     1003            'manage_options',
     1004            'interest-subscribers',
     1005            array( $this, 'render_interest_subscribers_page' )
     1006        );
     1007    }
     1008
     1009    /**
     1010     * Render Interest Subscribers admin page.
     1011     */
     1012    public function render_interest_subscribers_page() {
     1013        include plugin_dir_path( dirname( __FILE__ ) ) . 'admin/partials/interest-subscribers-display.php';
     1014    }
     1015
     1016    /**
    9931017     * Add Email Logs submenu to the Subscriptions menu
    9941018     */
    … …  
    10871111            )
    10881112        );
     1113
     1114        // Subscribe Button & Modal settings
     1115        register_setting( 'subscription_modal_settings', 'subs_modal_auto_open', array( 'sanitize_callback' => 'absint', 'default' => 0 ) );
     1116        register_setting( 'subscription_modal_settings', 'subs_modal_auto_open_delay', array( 'sanitize_callback' => 'absint', 'default' => 3 ) );
     1117        register_setting( 'subscription_modal_settings', 'subs_modal_floating_btn', array( 'sanitize_callback' => 'absint', 'default' => 0 ) );
     1118        register_setting( 'subscription_modal_settings', 'subs_modal_floating_btn_text', array( 'sanitize_callback' => 'sanitize_text_field', 'default' => 'Subscribe' ) );
     1119        register_setting( 'subscription_modal_settings', 'subs_modal_floating_btn_position', array( 'sanitize_callback' => 'sanitize_text_field', 'default' => 'bottom-right' ) );
     1120        register_setting( 'subscription_modal_settings', 'subs_modal_display_all_posts', array( 'sanitize_callback' => 'absint', 'default' => 0 ) );
     1121        register_setting( 'subscription_modal_settings', 'subs_modal_display_all_pages', array( 'sanitize_callback' => 'absint', 'default' => 0 ) );
     1122        register_setting( 'subscription_modal_settings', 'subs_modal_display_single_posts', array( 'sanitize_callback' => 'absint', 'default' => 0 ) );
     1123        register_setting( 'subscription_modal_settings', 'subs_modal_display_single_pages', array( 'sanitize_callback' => 'absint', 'default' => 0 ) );
     1124        register_setting( 'subscription_modal_settings', 'subs_modal_display_post_types', array(
     1125            'sanitize_callback' => function( $val ) {
     1126                return is_array( $val ) ? array_map( 'sanitize_key', $val ) : array();
     1127            },
     1128            'default' => array(),
     1129        ) );
     1130        register_setting( 'subscription_modal_settings', 'subs_modal_display_specific_pages', array(
     1131            'sanitize_callback' => function( $val ) {
     1132                return is_array( $val ) ? array_map( 'absint', $val ) : array();
     1133            },
     1134            'default' => array(),
     1135        ) );
     1136        register_setting( 'subscription_modal_settings', 'subs_modal_context_categories', array( 'sanitize_callback' => 'absint', 'default' => 1 ) );
     1137        register_setting( 'subscription_modal_settings', 'subs_modal_context_tags', array( 'sanitize_callback' => 'absint', 'default' => 1 ) );
     1138        register_setting( 'subscription_modal_settings', 'subs_modal_hide_fields_logged_in', array( 'sanitize_callback' => 'absint', 'default' => 1 ) );
    10891139
    10901140        // Pause subscription setting
    … …  
    13661416    public function register_ajax_handlers() {
    13671417        add_action('wp_ajax_sync_plan_with_stripe', array($this, 'ajax_sync_plan_with_stripe'));
     1418        add_action('wp_ajax_sync_content_plan_with_stripe', array($this, 'ajax_sync_content_plan_with_stripe'));
    13681419        add_action('wp_ajax_test_stripe_connection', array($this, 'ajax_test_stripe_connection'));
    13691420        add_action('wp_ajax_install_stripe_sdk', array($this, 'ajax_install_stripe_sdk'));
    … …  
    13911442           
    13921443            // Get form data
     1444            $raw_features = isset( $_POST['plan_features_json'] ) ? sanitize_text_field( wp_unslash( $_POST['plan_features_json'] ) ) : '';
     1445            $features_arr = json_decode( $raw_features, true );
     1446            if ( ! is_array( $features_arr ) ) {
     1447                $features_arr = array( 'max_categories' => 0, 'max_tags' => 0 );
     1448            }
     1449            $features_arr['max_categories'] = max( 0, (int) ( $features_arr['max_categories'] ?? 0 ) );
     1450            $features_arr['max_tags']       = max( 0, (int) ( $features_arr['max_tags']       ?? 0 ) );
     1451
    13931452            $plan_data = array(
    1394                 'plan_name' => isset($_POST['plan_name']) ? sanitize_text_field($_POST['plan_name']) : '',
     1453                'plan_name'   => isset($_POST['plan_name']) ? sanitize_text_field($_POST['plan_name']) : '',
    13951454                'description' => isset($_POST['description']) ? wp_kses_post($_POST['description']) : '',
    1396                 'price' => isset($_POST['price']) ? floatval($_POST['price']) : 0,
    1397                 'duration' => isset($_POST['duration']) ? intval($_POST['duration']) : 30,
     1455                'price'       => isset($_POST['price']) ? floatval($_POST['price']) : 0,
     1456                'duration'    => isset($_POST['duration']) ? intval($_POST['duration']) : 30,
    13981457                'category_id' => isset($_POST['category_id']) ? intval($_POST['category_id']) : 0,
    1399                 'status' => isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'active'
     1458                'status'      => isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'active',
     1459                'features'    => wp_json_encode( $features_arr ),
    14001460            );
    14011461           
    … …  
    14401500            }
    14411501           
     1502            $raw_features_edit = isset( $_POST['plan_features_json'] ) ? sanitize_text_field( wp_unslash( $_POST['plan_features_json'] ) ) : '';
     1503            $features_arr_edit = json_decode( $raw_features_edit, true );
     1504            if ( ! is_array( $features_arr_edit ) ) {
     1505                $features_arr_edit = array( 'max_categories' => 0, 'max_tags' => 0 );
     1506            }
     1507            $features_arr_edit['max_categories'] = max( 0, (int) ( $features_arr_edit['max_categories'] ?? 0 ) );
     1508            $features_arr_edit['max_tags']       = max( 0, (int) ( $features_arr_edit['max_tags']       ?? 0 ) );
     1509
    14421510            $plan_data = array(
    1443                 'plan_name' => isset($_POST['plan_name']) ? sanitize_text_field($_POST['plan_name']) : '',
     1511                'plan_name'   => isset($_POST['plan_name']) ? sanitize_text_field($_POST['plan_name']) : '',
    14441512                'description' => isset($_POST['description']) ? wp_kses_post($_POST['description']) : '',
    1445                 'price' => isset($_POST['price']) ? floatval($_POST['price']) : 0,
    1446                 'duration' => isset($_POST['duration']) ? intval($_POST['duration']) : 30,
     1513                'price'       => isset($_POST['price']) ? floatval($_POST['price']) : 0,
     1514                'duration'    => isset($_POST['duration']) ? intval($_POST['duration']) : 30,
    14471515                'category_id' => isset($_POST['category_id']) ? intval($_POST['category_id']) : 0,
    1448                 'status' => isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'active',
    1449                 'updated_at' => current_time('mysql')
     1516                'status'      => isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'active',
     1517                'features'    => wp_json_encode( $features_arr_edit ),
     1518                'updated_at'  => current_time('mysql'),
    14501519            );
    14511520           
    … …  
    22412310        // Prepare the data
    22422311        $data = array(
    2243             'plan_name' => sanitize_text_field($plan_data['plan_name']),
     2312            'plan_name'   => sanitize_text_field($plan_data['plan_name']),
    22442313            'description' => isset($plan_data['description']) ? wp_kses_post($plan_data['description']) : '',
    2245             'price' => floatval($plan_data['price']),
    2246             'duration' => isset($plan_data['duration']) ? intval($plan_data['duration']) : 30,
     2314            'price'       => floatval($plan_data['price']),
     2315            'duration'    => isset($plan_data['duration']) ? intval($plan_data['duration']) : 30,
    22472316            'category_id' => isset($plan_data['category_id']) ? intval($plan_data['category_id']) : 0,
    2248             'status' => isset($plan_data['status']) ? sanitize_text_field($plan_data['status']) : 'active',
    2249             'created_at' => current_time('mysql'),
    2250             'updated_at' => current_time('mysql')
     2317            'status'      => isset($plan_data['status']) ? sanitize_text_field($plan_data['status']) : 'active',
     2318            'features'    => isset($plan_data['features']) ? $plan_data['features'] : wp_json_encode( array( 'max_categories' => 0, 'max_tags' => 0 ) ),
     2319            'created_at'  => current_time('mysql'),
     2320            'updated_at'  => current_time('mysql'),
    22512321        );
    22522322       
    … …  
    22572327            $table_name,
    22582328            $data,
    2259             array('%s', '%s', '%f', '%d', '%d', '%s', '%s', '%s')
     2329            array('%s', '%s', '%f', '%d', '%d', '%s', '%s', '%s', '%s')
    22602330        );
    22612331       
    … …  
    26142684
    26152685    /**
     2686     * AJAX handler: sync a content plan with Stripe.
     2687     * Mirrors ajax_sync_plan_with_stripe but operates on subscription_content_plans.
     2688     */
     2689    public function ajax_sync_content_plan_with_stripe() {
     2690        // Nonce check.
     2691        $nonce_ok = ( isset( $_POST['nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'sync_content_plan_with_stripe' ) );
     2692        if ( ! $nonce_ok ) {
     2693            wp_send_json_error( array( 'message' => 'Security check failed.', 'mock_mode' => true ) );
     2694            return;
     2695        }
     2696
     2697        if ( ! current_user_can( 'manage_options' ) ) {
     2698            wp_send_json_error( array( 'message' => 'Insufficient permissions.', 'mock_mode' => true ) );
     2699            return;
     2700        }
     2701
     2702        $plan_id = isset( $_POST['plan_id'] ) ? absint( $_POST['plan_id'] ) : 0;
     2703        if ( ! $plan_id ) {
     2704            wp_send_json_error( array( 'message' => 'Invalid plan ID.', 'mock_mode' => true ) );
     2705            return;
     2706        }
     2707
     2708        global $wpdb;
     2709        $table = $wpdb->prefix . 'subscription_content_plans';
     2710        $plan  = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table WHERE id = %d", $plan_id ) );
     2711
     2712        if ( ! $plan ) {
     2713            wp_send_json_error( array( 'message' => 'Content plan not found.', 'mock_mode' => true ) );
     2714            return;
     2715        }
     2716
     2717        // Helper: save IDs back to DB and return success.
     2718        $save_and_respond = function( $product_id, $price_id, $mock_mode, $message, $extra = array() ) use ( $wpdb, $table, $plan_id ) {
     2719            $wpdb->update(
     2720                $table,
     2721                array( 'stripe_product_id' => $product_id, 'stripe_price_id' => $price_id ),
     2722                array( 'id' => $plan_id ),
     2723                array( '%s', '%s' ),
     2724                array( '%d' )
     2725            );
     2726            wp_send_json_success( array_merge( array(
     2727                'product_id' => $product_id,
     2728                'price_id'   => $price_id,
     2729                'mock_mode'  => $mock_mode,
     2730                'message'    => $message,
     2731            ), $extra ) );
     2732        };
     2733
     2734        // Determine billing interval from duration.
     2735        $duration       = (int) ( $plan->duration ?? 30 );
     2736        $interval       = 'month';
     2737        $interval_count = 1;
     2738        if ( $duration <= 31 )       { $interval = 'month'; $interval_count = 1; }
     2739        elseif ( $duration <= 93 )   { $interval = 'month'; $interval_count = 3; }
     2740        elseif ( $duration <= 186 )  { $interval = 'month'; $interval_count = 6; }
     2741        else                         { $interval = 'year';  $interval_count = 1; }
     2742
     2743        // Check Stripe is enabled.
     2744        $stripe_enabled = get_option( 'subscription_stripe_enabled', 0 );
     2745
     2746        if ( ! $stripe_enabled ) {
     2747            $mock_prod  = 'mock_prod_'  . substr( md5( 'cpln_' . $plan_id ), 0, 10 );
     2748            $mock_price = 'mock_price_' . substr( md5( 'cpln_' . $plan_id . '_price' ), 0, 10 );
     2749            $save_and_respond( $mock_prod, $mock_price, true, 'Content plan synced with mock Stripe. Enable Stripe in Payment Settings for real integration.' );
     2750            return;
     2751        }
     2752
     2753        // Resolve API key.
     2754        $test_mode = get_option( 'subscription_stripe_test_mode', 1 );
     2755        $api_key   = $test_mode
     2756            ? get_option( 'subscription_stripe_test_secret_key', '' )
     2757            : get_option( 'subscription_stripe_live_secret_key', '' );
     2758        if ( empty( $api_key ) ) {
     2759            $api_key = get_option( 'subscription_stripe_secret_key', '' );
     2760        }
     2761
     2762        if ( empty( $api_key ) ) {
     2763            wp_send_json_error( array( 'message' => 'Stripe API key is not set. Please configure it in Payment Settings.', 'mock_mode' => true ) );
     2764            return;
     2765        }
     2766
     2767        // Load Stripe SDK.
     2768        if ( ! class_exists( '\Stripe\Stripe' ) ) {
     2769            $this->ensure_stripe_sdk_loaded();
     2770        }
     2771
     2772        if ( ! class_exists( '\Stripe\Stripe' ) ) {
     2773            $mock_prod  = 'mock_prod_'  . substr( md5( 'cpln_' . $plan_id ), 0, 10 );
     2774            $mock_price = 'mock_price_' . substr( md5( 'cpln_' . $plan_id . '_price' ), 0, 10 );
     2775            $save_and_respond( $mock_prod, $mock_price, true, 'Content plan synced with mock Stripe. SDK could not be loaded.' );
     2776            return;
     2777        }
     2778
     2779        try {
     2780            \Stripe\Stripe::setApiKey( $api_key );
     2781
     2782            $product_id = 'prod_' . substr( md5( 'cpln_' . $plan_id ), 0, 10 );
     2783
     2784            // Create or update the Stripe product.
     2785            try {
     2786                $product       = \Stripe\Product::retrieve( $product_id );
     2787                $product->name = $plan->plan_name;
     2788                if ( ! empty( $plan->description ) ) {
     2789                    $product->description = wp_strip_all_tags( $plan->description );
     2790                }
     2791                $product->save();
     2792            } catch ( \Exception $e ) {
     2793                $product = \Stripe\Product::create( array(
     2794                    'id'          => $product_id,
     2795                    'name'        => $plan->plan_name,
     2796                    'description' => wp_strip_all_tags( $plan->description ?? '' ),
     2797                    'metadata'    => array(
     2798                        'content_plan_id' => $plan_id,
     2799                        'content_type'    => $plan->content_type,
     2800                    ),
     2801                ) );
     2802            }
     2803
     2804            // Free plans: no Stripe price needed.
     2805            if ( (float) $plan->price <= 0 ) {
     2806                $save_and_respond( $product->id, 'free', false, 'Content plan (free) synced with Stripe as a product.' );
     2807                return;
     2808            }
     2809
     2810            $price_id = 'price_' . substr( md5( 'cpln_' . $plan_id . '_' . $plan->price ), 0, 10 );
     2811
     2812            // Create or reuse the Stripe price.
     2813            try {
     2814                $price = \Stripe\Price::retrieve( $price_id );
     2815                // If amount changed, create a new price.
     2816                if ( $price->unit_amount !== (int) ( $plan->price * 100 ) ) {
     2817                    $price = \Stripe\Price::create( array(
     2818                        'product'   => $product->id,
     2819                        'unit_amount' => (int) ( $plan->price * 100 ),
     2820                        'currency'  => strtolower( get_option( 'subscription_currency', 'usd' ) ),
     2821                        'recurring' => array( 'interval' => $interval, 'interval_count' => $interval_count ),
     2822                        'metadata'  => array( 'content_plan_id' => $plan_id ),
     2823                    ) );
     2824                }
     2825            } catch ( \Exception $e ) {
     2826                $price = \Stripe\Price::create( array(
     2827                    'product'   => $product->id,
     2828                    'unit_amount' => (int) ( $plan->price * 100 ),
     2829                    'currency'  => strtolower( get_option( 'subscription_currency', 'usd' ) ),
     2830                    'recurring' => array( 'interval' => $interval, 'interval_count' => $interval_count ),
     2831                    'metadata'  => array( 'content_plan_id' => $plan_id ),
     2832                ) );
     2833            }
     2834
     2835            $save_and_respond( $product->id, $price->id, false, 'Content plan successfully synced with Stripe.' );
     2836
     2837        } catch ( \Exception $e ) {
     2838            $mock_prod  = 'mock_prod_'  . substr( md5( 'cpln_' . $plan_id ), 0, 10 );
     2839            $mock_price = 'mock_price_' . substr( md5( 'cpln_' . $plan_id . '_price' ), 0, 10 );
     2840            $save_and_respond( $mock_prod, $mock_price, true,
     2841                'Content plan synced with mock Stripe. Error: ' . $e->getMessage(),
     2842                array( 'error' => $e->getMessage() )
     2843            );
     2844        }
     2845    }
     2846
     2847    /**
    26162848     * AJAX handler for checking the Stripe API key
    26172849     */
    … …  
    34273659        // Handle add/edit content plan
    34283660        if (isset($_POST['submit_content_plan']) && wp_verify_nonce($_POST['content_plan_nonce'], 'content_plan_action')) {
     3661            $content_type = sanitize_text_field($_POST['content_type']);
     3662            $content_ids  = isset($_POST['content_ids']) ? array_map('sanitize_text_field', $_POST['content_ids']) : array();
     3663            $plan_id      = isset($_POST['plan_id']) ? intval($_POST['plan_id']) : 0;
     3664
     3665            // Conflict check: category and tag IDs must be unique across all active plans.
     3666            if ( in_array( $content_type, array( 'category', 'tag' ), true ) && ! empty( $content_ids ) ) {
     3667                global $wpdb;
     3668                $cp_table    = $wpdb->prefix . 'subscription_content_plans';
     3669                $other_plans = $wpdb->get_results( $wpdb->prepare(
     3670                    "SELECT id, plan_name, content_ids FROM $cp_table WHERE content_type = %s AND status = 'active'" . ( $plan_id ? ' AND id != ' . intval( $plan_id ) : '' ),
     3671                    $content_type
     3672                ) );
     3673
     3674                $conflicts = array();
     3675                foreach ( $other_plans as $op ) {
     3676                    $op_ids = json_decode( $op->content_ids, true ) ?: array();
     3677                    $overlapping = array_intersect( array_map( 'intval', $content_ids ), array_map( 'intval', $op_ids ) );
     3678                    if ( ! empty( $overlapping ) ) {
     3679                        // Resolve names for display.
     3680                        $type_fn    = $content_type === 'category' ? 'get_category' : 'get_tag';
     3681                        $term_names = array();
     3682                        foreach ( $overlapping as $tid ) {
     3683                            $term = $type_fn( (int) $tid );
     3684                            $term_names[] = $term ? $term->name : "#$tid";
     3685                        }
     3686                        $conflicts[] = sprintf(
     3687                            /* translators: 1: term names list, 2: conflicting plan name */
     3688                            __( '%1$s (already used by "%2$s")', 'subscription-system' ),
     3689                            implode( ', ', $term_names ),
     3690                            $op->plan_name
     3691                        );
     3692                    }
     3693                }
     3694
     3695                if ( ! empty( $conflicts ) ) {
     3696                    $label = $content_type === 'category' ? __( 'categories', 'subscription-system' ) : __( 'tags', 'subscription-system' );
     3697                    add_settings_error(
     3698                        'subscription_messages',
     3699                        'content_plan_conflict',
     3700                        sprintf(
     3701                            /* translators: 1: content type label, 2: conflict details */
     3702                            __( 'The following %1$s are already assigned to another plan and cannot be reused: %2$s', 'subscription-system' ),
     3703                            $label,
     3704                            implode( '; ', $conflicts )
     3705                        ),
     3706                        'error'
     3707                    );
     3708                    return;
     3709                }
     3710            }
     3711
    34293712            $plan_data = array(
    34303713                'plan_name' => sanitize_text_field($_POST['plan_name']),
    … …  
    34333716                'duration' => intval($_POST['duration']),
    34343717                'duration_unit' => sanitize_text_field($_POST['duration_unit']),
    3435                 'content_type' => sanitize_text_field($_POST['content_type']),
    3436                 'content_ids' => isset($_POST['content_ids']) ? array_map('sanitize_text_field', $_POST['content_ids']) : array(),
     3718                'content_type' => $content_type,
     3719                'content_ids'  => $content_ids,
    34373720                'notification_frequency' => sanitize_text_field($_POST['notification_frequency']),
    34383721                'status' => sanitize_text_field($_POST['status'])
    34393722            );
    3440 
    3441             $plan_id = isset($_POST['plan_id']) ? intval($_POST['plan_id']) : 0;
    34423723
    34433724            if ($plan_id > 0) {
  • subscription-system/trunk/admin/partials/content-plans-display.php

    r3375846 r3598150  
    223223            data.append('content_type', contentType);
    224224            data.append('nonce', '<?php echo wp_create_nonce('get_content_options'); ?>');
     225            data.append('plan_id', '<?php echo intval($editing_plan ? $editing_plan->id : 0); ?>');
    225226            <?php if ($editing_plan): ?>
    226227            data.append('selected_ids', '<?php echo wp_json_encode($editing_plan->content_ids); ?>');
    … …  
    278279                            <th scope="col"><?php esc_html_e('Duration', 'subscription-system'); ?></th>
    279280                            <th scope="col"><?php esc_html_e('Status', 'subscription-system'); ?></th>
     281                            <th scope="col"><?php esc_html_e('Stripe', 'subscription-system'); ?></th>
    280282                            <th scope="col"><?php esc_html_e('Subscribers', 'subscription-system'); ?></th>
    281283                            <th scope="col"><?php esc_html_e('Actions', 'subscription-system'); ?></th>
    … …  
    285287                        <?php foreach ($content_plans as $plan): ?>
    286288                            <?php
    287                             // Get subscriber count
    288289                            global $wpdb;
    289290                            $subscriber_count = $wpdb->get_var($wpdb->prepare(
    … …  
    291292                                $plan->id
    292293                            ));
     294                            $stripe_product_id = $plan->stripe_product_id ?? '';
     295                            $stripe_price_id   = $plan->stripe_price_id ?? '';
     296                            $is_connected      = ! empty($stripe_product_id) && ! empty($stripe_price_id);
     297                            $is_mock           = $is_connected && strpos($stripe_product_id, 'mock_') === 0;
     298                            $sync_nonce        = wp_create_nonce('sync_content_plan_with_stripe');
    293299                            ?>
    294300                            <tr>
    … …  
    313319                                    </span>
    314320                                </td>
     321                                <td class="ss-stripe-cell">
     322                                    <?php if ($is_connected): ?>
     323                                        <span class="stripe-status <?php echo $is_mock ? 'mock-connected' : 'connected'; ?>">
     324                                            <span class="dashicons dashicons-<?php echo $is_mock ? 'warning' : 'yes-alt'; ?>"></span>
     325                                            <?php echo $is_mock ? esc_html__('Mock', 'subscription-system') : esc_html__('Connected', 'subscription-system'); ?>
     326                                        </span>
     327                                        <?php if (! $is_mock): ?>
     328                                            <br><small title="<?php echo esc_attr($stripe_product_id); ?>"><?php echo esc_html(substr($stripe_product_id, 0, 12) . '…'); ?></small>
     329                                        <?php endif; ?>
     330                                    <?php else: ?>
     331                                        <span class="stripe-status not-connected">
     332                                            <span class="dashicons dashicons-no"></span>
     333                                            <?php esc_html_e('None', 'subscription-system'); ?>
     334                                        </span>
     335                                    <?php endif; ?>
     336                                    <br>
     337                                    <a href="#"
     338                                       class="sync-content-plan-stripe"
     339                                       data-plan-id="<?php echo esc_attr($plan->id); ?>"
     340                                       data-nonce="<?php echo esc_attr($sync_nonce); ?>"
     341                                       style="font-size:11px;">
     342                                        <?php esc_html_e('Sync with Stripe', 'subscription-system'); ?>
     343                                    </a>
     344                                </td>
    315345                                <td><?php echo esc_html($subscriber_count); ?></td>
    316346                                <td>
    … …  
    379409    background: #fafafa;
    380410}
     411
     412.content-checkboxes label {
     413    display: flex;
     414    align-items: center;
     415    gap: 6px;
     416    padding: 3px 0;
     417    line-height: 1.4;
     418}
     419
     420.ss-claimed-item {
     421    opacity: 0.55;
     422    cursor: not-allowed;
     423}
     424
     425.ss-claimed-item input[type="checkbox"] {
     426    cursor: not-allowed;
     427}
     428
     429.ss-claimed-badge {
     430    display: inline-block;
     431    font-size: 11px;
     432    font-weight: 600;
     433    color: #8a2020;
     434    background: #fce8e8;
     435    border: 1px solid #f5c6cb;
     436    border-radius: 3px;
     437    padding: 1px 6px;
     438    margin-left: 4px;
     439    white-space: nowrap;
     440}
     441
     442.ss-post-count {
     443    color: #8c8f94;
     444    font-size: 12px;
     445}
     446
     447/* ── Stripe status column ─── */
     448.stripe-status {
     449    display: inline-flex;
     450    align-items: center;
     451    gap: 4px;
     452    font-size: 12px;
     453    font-weight: 600;
     454    border-radius: 3px;
     455    padding: 2px 6px;
     456}
     457.stripe-status.connected {
     458    background: #d1fae5;
     459    color: #065f46;
     460}
     461.stripe-status.mock-connected {
     462    background: #fef3c7;
     463    color: #92400e;
     464}
     465.stripe-status.not-connected {
     466    background: #f3f4f6;
     467    color: #6b7280;
     468}
     469.stripe-status .dashicons {
     470    font-size: 14px;
     471    width: 14px;
     472    height: 14px;
     473    line-height: 1;
     474}
     475.ss-stripe-cell {
     476    min-width: 120px;
     477}
    381478</style>
     479
     480<script>
     481jQuery(document).ready(function ($) {
     482    $(document).on('click', '.sync-content-plan-stripe', function (e) {
     483        e.preventDefault();
     484
     485        var $link     = $(this);
     486        var $cell     = $link.closest('td');
     487        var planId    = $link.data('plan-id');
     488        var nonce     = $link.data('nonce');
     489        var origHtml  = $cell.html();
     490
     491        $cell.html('<span class="spinner is-active" style="float:none;margin:0;"></span> <?php esc_html_e('Syncing…', 'subscription-system'); ?>');
     492
     493        $.ajax({
     494            url:  (typeof ajaxurl !== 'undefined') ? ajaxurl : '<?php echo esc_js(admin_url('admin-ajax.php')); ?>',
     495            type: 'POST',
     496            data: {
     497                action:  'sync_content_plan_with_stripe',
     498                plan_id: planId,
     499                nonce:   nonce
     500            },
     501            success: function (response) {
     502                if (response.success) {
     503                    var isMock   = response.data.mock_mode;
     504                    var icon     = isMock ? 'warning' : 'yes-alt';
     505                    var label    = isMock ? '<?php esc_html_e('Mock', 'subscription-system'); ?>'
     506                                         : '<?php esc_html_e('Connected', 'subscription-system'); ?>';
     507                    var cssClass = isMock ? 'mock-connected' : 'connected';
     508
     509                    var html = '<span class="stripe-status ' + cssClass + '">'
     510                             + '<span class="dashicons dashicons-' + icon + '"></span> ' + label
     511                             + '</span>';
     512
     513                    if (!isMock && response.data.product_id && response.data.product_id !== 'free') {
     514                        html += '<br><small title="' + response.data.product_id + '">'
     515                              + response.data.product_id.substring(0, 12) + '…</small>';
     516                    }
     517                    html += '<br><a href="#" class="sync-content-plan-stripe"'
     518                          + ' data-plan-id="' + planId + '" data-nonce="' + nonce + '"'
     519                          + ' style="font-size:11px;"><?php esc_html_e('Sync with Stripe', 'subscription-system'); ?></a>';
     520
     521                    $cell.html(html);
     522
     523                    var noticeClass = isMock ? 'notice-warning' : 'notice-success';
     524                    $('h1.wp-heading-inline').after(
     525                        '<div class="notice ' + noticeClass + ' is-dismissible" style="margin-top:12px;">'
     526                        + '<p>' + response.data.message + '</p>'
     527                        + '<button type="button" class="notice-dismiss"><span class="screen-reader-text">Dismiss</span></button>'
     528                        + '</div>'
     529                    );
     530                    $(document).on('click', '.notice-dismiss', function () {
     531                        $(this).closest('.notice').fadeOut(200, function () { $(this).remove(); });
     532                    });
     533                } else {
     534                    $cell.html(origHtml);
     535                    alert('<?php esc_html_e('Error', 'subscription-system'); ?>: ' + (response.data ? response.data.message : '<?php esc_html_e('Unknown error.', 'subscription-system'); ?>'));
     536                }
     537            },
     538            error: function () {
     539                $cell.html(origHtml);
     540                alert('<?php esc_html_e('A network error occurred while syncing with Stripe.', 'subscription-system'); ?>');
     541            }
     542        });
     543    });
     544});
     545</script>
  • subscription-system/trunk/admin/partials/help-support-display.php

    r3356551 r3598150  
    149149            <h3>📋 Table of Contents</h3>
    150150            <ul>
     151                <li><a href="#integration-steps"><strong>⚡ Step-by-Step Integration Guide</strong></a></li>
    151152                <li><a href="#getting-started">Getting Started</a></li>
    152153                <li><a href="#subscription-management">Subscription Management & Renewal Flow</a></li>
    153154                <li><a href="#content-subscriptions">Content Subscriptions</a></li>
     155                <li><a href="#interest-subscribe-modal"><strong>🔔 Subscribe Button & Modal (Category/Tag Subscriptions)</strong></a></li>
    154156                <li><a href="#email-notifications">Email Notifications</a></li>
    155157                <li><a href="#shortcodes">Available Shortcodes</a></li>
    … …  
    159161                <li><a href="#troubleshooting">Troubleshooting</a></li>
    160162            </ul>
     163        </div>
     164
     165        <!-- Step-by-Step Integration Guide -->
     166        <div class="help-section" id="integration-steps" style="border-left: 4px solid #1a8917;">
     167            <h2>⚡ Step-by-Step Integration Guide</h2>
     168            <p>Follow these steps in order to fully integrate the Subscription System plugin on your WordPress site.</p>
     169
     170            <!-- Step 1 -->
     171            <div style="background:#f0faf0;border:1px solid #b2dfb2;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     172                <h3 style="margin-top:0;color:#1a8917;">Step 1 — Install &amp; Activate the Plugin</h3>
     173                <ol>
     174                    <li>Upload the <strong>subscription_system</strong> plugin folder to <code>/wp-content/plugins/</code>, or install it via <strong>Plugins → Add New</strong>.</li>
     175                    <li>Go to <strong>Plugins → Installed Plugins</strong> and click <strong>Activate</strong> next to "Subscription System".</li>
     176                    <li>After activation a new <strong>Subscriptions</strong> menu will appear in the WordPress admin sidebar.</li>
     177                </ol>
     178            </div>
     179
     180            <!-- Step 2 -->
     181            <div style="background:#f0faf0;border:1px solid #b2dfb2;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     182                <h3 style="margin-top:0;color:#1a8917;">Step 2 — Configure Stripe Payment Settings</h3>
     183                <ol>
     184                    <li>Go to <strong>Subscriptions → Payment Settings</strong>.</li>
     185                    <li>Tick <strong>Enable Stripe</strong>.</li>
     186                    <li>Leave <strong>Test Mode</strong> enabled while you are testing; disable it when you go live.</li>
     187                    <li>Enter your <strong>Publishable Key</strong> and <strong>Secret Key</strong> from your <a href="https://dashboard.stripe.com/apikeys" target="_blank" rel="noopener">Stripe Dashboard → Developers → API keys</a>.
     188                        <ul>
     189                            <li>Test keys start with <code>pk_test_</code> / <code>sk_test_</code></li>
     190                            <li>Live keys start with <code>pk_live_</code> / <code>sk_live_</code></li>
     191                        </ul>
     192                    </li>
     193                    <li>Leave the <strong>Webhook Secret</strong> field empty for now — you will fill it in Step 3.</li>
     194                    <li>Click <strong>Save Settings</strong>.</li>
     195                    <li>Click <strong>Test Connection</strong> to confirm the API key works before proceeding.</li>
     196                </ol>
     197                <p style="margin-bottom:0;"><a href="<?php echo esc_url( admin_url('admin.php?page=subscription-payment-settings') ); ?>" class="button button-primary">Go to Payment Settings →</a></p>
     198            </div>
     199
     200            <!-- Step 3 -->
     201            <div style="background:#f0faf0;border:1px solid #b2dfb2;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     202                <h3 style="margin-top:0;color:#1a8917;">Step 3 — Set Up the Stripe Webhook</h3>
     203                <p>Webhooks are how Stripe notifies your site about payments, renewals, and cancellations. This step is <strong>required</strong> for automatic subscription management.</p>
     204                <ol>
     205                    <li>Open your <a href="https://dashboard.stripe.com/webhooks" target="_blank" rel="noopener">Stripe Dashboard → Developers → Webhooks</a> and click <strong>Add endpoint</strong>.</li>
     206                    <li>Set the <strong>Endpoint URL</strong> to:
     207                        <div class="shortcode-box" style="margin:8px 0;"><?php echo esc_url( site_url('/wp-json/subscription-system/v1/webhook') ); ?><button class="copy-button" onclick="copyToClipboard('<?php echo esc_js( site_url('/wp-json/subscription-system/v1/webhook') ); ?>')">Copy</button></div>
     208                    </li>
     209                    <li>Under <strong>Select events to listen to</strong>, add these four events:
     210                        <ul>
     211                            <li><code>customer.subscription.created</code></li>
     212                            <li><code>customer.subscription.updated</code></li>
     213                            <li><code>customer.subscription.deleted</code></li>
     214                            <li><code>invoice.payment_succeeded</code></li>
     215                            <li><code>invoice.payment_failed</code></li>
     216                        </ul>
     217                    </li>
     218                    <li>Click <strong>Add endpoint</strong>. Stripe will display a <strong>Signing secret</strong> — copy it.</li>
     219                    <li>Back in WordPress go to <strong>Subscriptions → Payment Settings</strong>, paste the signing secret into the <strong>Webhook Secret</strong> field, and click <strong>Save Settings</strong>.</li>
     220                </ol>
     221            </div>
     222
     223            <!-- Step 4 -->
     224            <div style="background:#f0faf0;border:1px solid #b2dfb2;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     225                <h3 style="margin-top:0;color:#1a8917;">Step 4 — Create the Required Pages</h3>
     226                <p>Go to <strong>Pages → Add New</strong> and create one page for each of the following. Add only the shortcode shown — no other content is needed.</p>
     227                <table style="width:100%;border-collapse:collapse;">
     228                    <thead>
     229                        <tr style="background:#e8f5e9;">
     230                            <th style="padding:8px 10px;border:1px solid #c8e6c9;text-align:left;">Page Name</th>
     231                            <th style="padding:8px 10px;border:1px solid #c8e6c9;text-align:left;">Shortcode to add</th>
     232                        </tr>
     233                    </thead>
     234                    <tbody>
     235                        <tr><td style="padding:8px 10px;border:1px solid #c8e6c9;">Login</td><td style="padding:8px 10px;border:1px solid #c8e6c9;font-family:monospace;">[subscriptionsystem_login]</td></tr>
     236                        <tr><td style="padding:8px 10px;border:1px solid #c8e6c9;">Register</td><td style="padding:8px 10px;border:1px solid #c8e6c9;font-family:monospace;">[subscriptionsystem_register]</td></tr>
     237                        <tr><td style="padding:8px 10px;border:1px solid #c8e6c9;">My Account</td><td style="padding:8px 10px;border:1px solid #c8e6c9;font-family:monospace;">[subscriptionsystem_account]</td></tr>
     238                        <tr><td style="padding:8px 10px;border:1px solid #c8e6c9;">Pricing / Plans</td><td style="padding:8px 10px;border:1px solid #c8e6c9;font-family:monospace;">[subscriptionsystem_plans layout="1"]</td></tr>
     239                        <tr><td style="padding:8px 10px;border:1px solid #c8e6c9;">Subscription Success</td><td style="padding:8px 10px;border:1px solid #c8e6c9;font-family:monospace;">[subscriptionsystem_success]</td></tr>
     240                        <tr><td style="padding:8px 10px;border:1px solid #c8e6c9;">Subscription Cancelled</td><td style="padding:8px 10px;border:1px solid #c8e6c9;font-family:monospace;">[checkout_failed]</td></tr>
     241                        <tr><td style="padding:8px 10px;border:1px solid #c8e6c9;">Forgot Password</td><td style="padding:8px 10px;border:1px solid #c8e6c9;font-family:monospace;">[subscriptionsystem_forgot_password]</td></tr>
     242                        <tr><td style="padding:8px 10px;border:1px solid #c8e6c9;">Edit Profile</td><td style="padding:8px 10px;border:1px solid #c8e6c9;font-family:monospace;">[edit_profile]</td></tr>
     243                        <tr><td style="padding:8px 10px;border:1px solid #c8e6c9;">Content Subscription Success <em>(optional)</em></td><td style="padding:8px 10px;border:1px solid #c8e6c9;font-family:monospace;">[content_subscription_success]</td></tr>
     244                    </tbody>
     245                </table>
     246            </div>
     247
     248            <!-- Step 5 -->
     249            <div style="background:#f0faf0;border:1px solid #b2dfb2;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     250                <h3 style="margin-top:0;color:#1a8917;">Step 5 — Assign Pages in Plugin Settings</h3>
     251                <ol>
     252                    <li>Go to <strong>Subscriptions → Settings → Pages</strong>.</li>
     253                    <li>Use each dropdown to select the page you created in Step 4 for the matching function.</li>
     254                    <li>Click <strong>Save Changes</strong>.</li>
     255                </ol>
     256                <p style="margin-bottom:0;">These assignments allow the plugin to generate correct redirect URLs in emails, checkout flows, and admin links.</p>
     257                <p style="margin:10px 0 0;"><a href="<?php echo esc_url( admin_url('admin.php?page=subscription-settings&tab=pages') ); ?>" class="button">Go to Pages Settings →</a></p>
     258            </div>
     259
     260            <!-- Step 6 -->
     261            <div style="background:#f0faf0;border:1px solid #b2dfb2;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     262                <h3 style="margin-top:0;color:#1a8917;">Step 6 — Configure Email &amp; SMTP</h3>
     263                <ol>
     264                    <li>Go to <strong>Subscriptions → Settings → Communication</strong>.</li>
     265                    <li>Tick <strong>Enable Email Notifications</strong>.</li>
     266                    <li>Set a <strong>From Email Address</strong> and <strong>From Name</strong>.</li>
     267                    <li>Enable <strong>SMTP</strong> for reliable delivery and fill in your SMTP host, port, username and password.
     268                        <ul>
     269                            <li><strong>SendGrid:</strong> Host <code>smtp.sendgrid.net</code>, Port <code>587</code>, Username <code>apikey</code>, Password = your API key</li>
     270                            <li><strong>Gmail:</strong> Host <code>smtp.gmail.com</code>, Port <code>587</code>, Password = app-specific password</li>
     271                        </ul>
     272                    </li>
     273                    <li>Use the built-in <strong>Send Test Email</strong> button to confirm delivery before going live.</li>
     274                    <li>Click <strong>Save Changes</strong>.</li>
     275                </ol>
     276                <p style="margin-bottom:0;"><a href="<?php echo esc_url( admin_url('admin.php?page=subscription-settings&tab=communication') ); ?>" class="button">Go to Communication Settings →</a></p>
     277            </div>
     278
     279            <!-- Step 7 -->
     280            <div style="background:#f0faf0;border:1px solid #b2dfb2;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     281                <h3 style="margin-top:0;color:#1a8917;">Step 7 — Create Plan Categories</h3>
     282                <ol>
     283                    <li>Go to <strong>Subscriptions → Plan Categories</strong>.</li>
     284                    <li>Click <strong>Add New Category</strong> and give it a name (e.g. "Monthly Plans", "Annual Plans").</li>
     285                    <li>Save the category — you will need it when creating plans in the next step.</li>
     286                </ol>
     287                <p style="margin-bottom:0;"><a href="<?php echo esc_url( admin_url('admin.php?page=subscription-plan-categories') ); ?>" class="button">Go to Plan Categories →</a></p>
     288            </div>
     289
     290            <!-- Step 8 -->
     291            <div style="background:#f0faf0;border:1px solid #b2dfb2;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     292                <h3 style="margin-top:0;color:#1a8917;">Step 8 — Create Subscription Plans</h3>
     293                <ol>
     294                    <li>Go to <strong>Subscriptions → Plans → Add New Plan</strong>.</li>
     295                    <li>Fill in the plan name, description, price, billing period (monthly / annual), and select a category from Step 7.</li>
     296                    <li>Save the plan, then click <strong>Sync with Stripe</strong> to create the corresponding Stripe product and price automatically.</li>
     297                    <li>Repeat for each plan you want to offer.</li>
     298                    <li>The plans will now appear on your Pricing page via the <code>[subscriptionsystem_plans]</code> shortcode.</li>
     299                </ol>
     300                <p style="margin-bottom:0;"><a href="<?php echo esc_url( admin_url('admin.php?page=subscription-plans') ); ?>" class="button">Go to Plans →</a></p>
     301            </div>
     302
     303            <!-- Step 9 (optional) -->
     304            <div style="background:#f0faf0;border:1px solid #b2dfb2;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     305                <h3 style="margin-top:0;color:#1a8917;">Step 9 — Set Up Content Subscription Plans <em>(Optional)</em></h3>
     306                <p>Content plans let users subscribe to email notifications for specific WordPress categories, tags, or post types.</p>
     307                <ol>
     308                    <li>Go to <strong>Subscriptions → Content Plans → Add New</strong>.</li>
     309                    <li>Choose the subscription type: <strong>Category</strong>, <strong>Tag</strong>, <strong>Post Type</strong>, or <strong>Specific Post</strong>.</li>
     310                    <li>Select the target content and set a price (or make it free).</li>
     311                    <li>Save the plan — the pop-up will automatically appear for visitors browsing matching content.</li>
     312                </ol>
     313                <p style="margin-bottom:0;"><a href="<?php echo esc_url( admin_url('admin.php?page=subscription-content-plans') ); ?>" class="button">Go to Content Plans →</a></p>
     314            </div>
     315
     316            <!-- Step 9b -->
     317            <div style="background:#f0f4ff;border:1px solid #c7d2fe;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     318                <h3 style="margin-top:0;color:#4338ca;">Step 9b — Set Up the Subscribe Button & Modal <em>(Interest Subscriptions)</em></h3>
     319                <p>This is the newer, more flexible category/tag subscription system. Users choose their own interests from a popup modal. Three trigger methods are available: auto-open, floating button, and shortcode.</p>
     320                <ol>
     321                    <li>Ensure you have at least one active plan in <strong>Subscriptions → Plans</strong> (free or paid).</li>
     322                    <li>Go to <strong>Subscriptions → Settings → Subscribe Button & Modal</strong>.</li>
     323                    <li>Under <strong>Trigger Options</strong>: enable the <strong>Floating Button</strong> and/or <strong>Auto-Open Modal</strong>.</li>
     324                    <li>Under <strong>Display Settings</strong>: choose where the triggers appear (all posts, single posts, specific pages, etc.).</li>
     325                    <li>Enable <strong>Auto-Select Current Post's Categories/Tags</strong> for context-aware pre-selection.</li>
     326                    <li>Enable <strong>Hide Name & Email Fields for Logged-In Users</strong> for a seamless experience.</li>
     327                    <li>Click <strong>Save Settings</strong>.</li>
     328                    <li>Optionally, place <code>[subscriptionsystem_subscribe]</code> in any post or page for an inline button.</li>
     329                </ol>
     330                <p>For full configuration details, see the <a href="#interest-subscribe-modal">Subscribe Button & Modal</a> section below.</p>
     331                <p style="margin-bottom:0;"><a href="<?php echo esc_url( admin_url('admin.php?page=subscription-settings&tab=subscribe_modal') ); ?>" class="button button-primary">Open Subscribe Modal Settings →</a></p>
     332            </div>
     333
     334            <!-- Step 10 -->
     335            <div style="background:#f0faf0;border:1px solid #b2dfb2;border-radius:6px;padding:15px 20px;margin-bottom:15px;">
     336                <h3 style="margin-top:0;color:#1a8917;">Step 10 — Test the Full Integration</h3>
     337                <ol>
     338                    <li>Ensure <strong>Stripe Test Mode</strong> is still active in Payment Settings.</li>
     339                    <li>Visit your front-end <strong>Pricing / Plans</strong> page and click a plan to start a checkout.</li>
     340                    <li>Complete payment using Stripe's test card numbers:
     341                        <ul>
     342                            <li><strong>4242 4242 4242 4242</strong> — successful payment</li>
     343                            <li><strong>4000 0000 0000 0002</strong> — declined payment</li>
     344                            <li><strong>4000 0025 0000 3155</strong> — requires 3D Secure</li>
     345                        </ul>
     346                        Use any future expiry date, any 3-digit CVC, and any postcode.
     347                    </li>
     348                    <li>Check that you are redirected to the <strong>Subscription Success</strong> page and receive a confirmation email.</li>
     349                    <li>In the <a href="https://dashboard.stripe.com/test/webhooks" target="_blank" rel="noopener">Stripe Dashboard → Webhooks</a>, verify the webhook events were delivered with a <strong>200</strong> status.</li>
     350                    <li>Check <strong>Subscriptions → Dashboard</strong> to confirm the new subscription appears there.</li>
     351                    <li>When everything works, go to <strong>Payment Settings</strong>, disable Test Mode, enter your <strong>live</strong> API keys and live webhook secret, and save.</li>
     352                </ol>
     353                <div style="background:#fff3cd;border:1px solid #ffeaa7;border-radius:4px;padding:10px;margin-top:10px;">
     354                    <strong>⚠️ Before going live:</strong> Make sure your site uses HTTPS — Stripe requires a secure connection for live payments.
     355                </div>
     356            </div>
     357
    161358        </div>
    162359
    … …  
    285482        </div>
    286483
     484        <!-- Subscribe Button & Modal -->
     485        <div class="help-section" id="interest-subscribe-modal" style="border-left: 4px solid #667eea;">
     486            <h2>🔔 Subscribe Button & Modal (Category / Tag Subscriptions)</h2>
     487            <p>This feature lets visitors subscribe to <strong>specific WordPress categories and tags</strong> — instead of the whole site. When new content matching their interests is published, they receive a targeted email notification.</p>
     488            <p>The subscription modal can be triggered in three ways and is configurable from <strong>Subscriptions → Settings → Subscribe Button & Modal</strong>.</p>
     489
     490            <!-- What It Is -->
     491            <h3>What Is This Feature?</h3>
     492            <div class="help-grid">
     493                <div class="help-card">
     494                    <h4>🎯 Interest-Based Subscriptions</h4>
     495                    <p>Instead of subscribing to everything, users pick the <strong>categories</strong> and <strong>tags</strong> they care about — for example: Category "Sports" + Tag "Hockey". They only receive emails when content matching those interests is published.</p>
     496                </div>
     497                <div class="help-card">
     498                    <h4>📋 Subscription Plans</h4>
     499                    <p>Plans are pulled from <strong>Subscriptions → Plans</strong>. Free plans activate immediately with configurable category/tag limits. Paid plans redirect the user to checkout. Multiple plans can be offered (e.g. "Basic – 2 categories" and "Pro – unlimited").</p>
     500                </div>
     501                <div class="help-card">
     502                    <h4>🧠 Context-Aware Modal</h4>
     503                    <p>When a visitor reads a post in the "Sports" category tagged "Hockey", the modal <strong>automatically pre-selects</strong> those categories and tags — reducing friction and increasing conversions.</p>
     504                </div>
     505                <div class="help-card">
     506                    <h4>👤 Logged-In User Experience</h4>
     507                    <p>Logged-in users are recognized automatically. The modal skips the name/email fields entirely and greets them by name. They proceed directly to choosing their interests and plan.</p>
     508                </div>
     509            </div>
     510
     511            <!-- How It Works -->
     512            <h3>How It Works — End-to-End Flow</h3>
     513            <ol>
     514                <li>A visitor reads a post or page where the modal trigger is active (floating button, auto-open, or shortcode button).</li>
     515                <li>The subscription modal opens showing all available categories, tags, and plans.</li>
     516                <li>If context-awareness is enabled and the user is on a post, the post's categories/tags are pre-checked.</li>
     517                <li>The visitor selects their interests, chooses a plan, and clicks <strong>Subscribe</strong>.</li>
     518                <li>For <strong>free plans</strong>: the subscription is activated immediately. For <strong>paid plans</strong>: the user is redirected to checkout.</li>
     519                <li>When new content is published, the system matches it against all active subscriptions and sends targeted email notifications.</li>
     520            </ol>
     521
     522            <!-- Configuration Steps -->
     523            <h3>⚙️ Configuration — Step by Step</h3>
     524
     525            <!-- Step A -->
     526            <div style="background:#f0f4ff;border:1px solid #c7d2fe;border-radius:6px;padding:15px 20px;margin-bottom:12px;">
     527                <h4 style="margin-top:0;color:#4338ca;">Step A — Create Subscription Plans</h4>
     528                <p>The modal reads plans from <strong>Subscriptions → Plans</strong>. At minimum you need one active plan.</p>
     529                <ol>
     530                    <li>Go to <strong>Subscriptions → Plans → Add New Plan</strong>.</li>
     531                    <li>Set the plan name (e.g. <em>"Free Newsletter"</em> or <em>"Pro Access"</em>).</li>
     532                    <li>Set the price to <strong>$0</strong> for a free plan, or a positive amount for paid.</li>
     533                    <li>In the <strong>Features</strong> or <strong>Description</strong> field you can optionally add a JSON object to control per-plan limits:
     534                        <div class="shortcode-box" style="font-size:13px;">{"max_categories": 2, "max_tags": 3}</div>
     535                        <em>A value of <code>0</code> means unlimited. Omit the field for unlimited by default on paid plans.</em>
     536                    </li>
     537                    <li>Set Status to <strong>Active</strong> and save. The plan now appears in the modal automatically.</li>
     538                </ol>
     539                <p style="margin-bottom:0;"><a href="<?php echo esc_url( admin_url('admin.php?page=subscription-plans') ); ?>" class="button">Go to Plans →</a></p>
     540            </div>
     541
     542            <!-- Step B -->
     543            <div style="background:#f0f4ff;border:1px solid #c7d2fe;border-radius:6px;padding:15px 20px;margin-bottom:12px;">
     544                <h4 style="margin-top:0;color:#4338ca;">Step B — Open Subscribe Button & Modal Settings</h4>
     545                <p>Go to <strong>Subscriptions → Settings → Subscribe Button & Modal</strong>.</p>
     546                <p style="margin-bottom:0;"><a href="<?php echo esc_url( admin_url('admin.php?page=subscription-settings&tab=subscribe_modal') ); ?>" class="button button-primary">Open Settings Tab →</a></p>
     547            </div>
     548
     549            <!-- Step C -->
     550            <div style="background:#f0f4ff;border:1px solid #c7d2fe;border-radius:6px;padding:15px 20px;margin-bottom:12px;">
     551                <h4 style="margin-top:0;color:#4338ca;">Step C — Choose Your Trigger Method</h4>
     552                <p>Under <strong>Trigger Options</strong>, pick one or more ways to present the modal:</p>
     553                <table style="width:100%;border-collapse:collapse;margin-top:8px;">
     554                    <thead>
     555                        <tr style="background:#e0e7ff;">
     556                            <th style="padding:8px 10px;border:1px solid #c7d2fe;text-align:left;">Method</th>
     557                            <th style="padding:8px 10px;border:1px solid #c7d2fe;text-align:left;">What It Does</th>
     558                            <th style="padding:8px 10px;border:1px solid #c7d2fe;text-align:left;">Settings</th>
     559                        </tr>
     560                    </thead>
     561                    <tbody>
     562                        <tr>
     563                            <td style="padding:8px 10px;border:1px solid #c7d2fe;"><strong>Auto-Open on Page Load</strong></td>
     564                            <td style="padding:8px 10px;border:1px solid #c7d2fe;">The modal opens automatically after the page loads, with a configurable delay.</td>
     565                            <td style="padding:8px 10px;border:1px solid #c7d2fe;">Enable checkbox + set delay in seconds (0 = immediate, 3 = recommended).</td>
     566                        </tr>
     567                        <tr>
     568                            <td style="padding:8px 10px;border:1px solid #c7d2fe;"><strong>Floating Button</strong></td>
     569                            <td style="padding:8px 10px;border:1px solid #c7d2fe;">A fixed button stays visible at the corner/bottom of every qualifying page. Clicking it opens the modal.</td>
     570                            <td style="padding:8px 10px;border:1px solid #c7d2fe;">Enable checkbox, set button text and position (Bottom Right, Left, or Center).</td>
     571                        </tr>
     572                        <tr>
     573                            <td style="padding:8px 10px;border:1px solid #c7d2fe;"><strong>Shortcode Button</strong></td>
     574                            <td style="padding:8px 10px;border:1px solid #c7d2fe;">Place a button anywhere in a post or page. Clicking it opens the modal.</td>
     575                            <td style="padding:8px 10px;border:1px solid #c7d2fe;">Add <code>[subscriptionsystem_subscribe]</code> to any page or post — no admin setting needed.</td>
     576                        </tr>
     577                    </tbody>
     578                </table>
     579                <p style="margin-top:10px;"><strong>Note:</strong> You can enable all three simultaneously — they use the same modal and share the same subscription logic.</p>
     580            </div>
     581
     582            <!-- Step D -->
     583            <div style="background:#f0f4ff;border:1px solid #c7d2fe;border-radius:6px;padding:15px 20px;margin-bottom:12px;">
     584                <h4 style="margin-top:0;color:#4338ca;">Step D — Configure Display Settings (Where Triggers Appear)</h4>
     585                <p>Under <strong>Display Settings</strong>, choose on which pages the floating button and auto-open modal are active. Options:</p>
     586                <ul>
     587                    <li><strong>Show on All Posts</strong> — any singular post of any post type (excluding pages).</li>
     588                    <li><strong>Show on All Pages</strong> — all WordPress pages.</li>
     589                    <li><strong>Show on Single Posts Only</strong> — only the standard blog post single view.</li>
     590                    <li><strong>Show on Single Pages Only</strong> — only single-page views.</li>
     591                    <li><strong>Show on Custom Post Types</strong> — tick one or more CPTs from the list (e.g. "Product", "Event").</li>
     592                    <li><strong>Show on Specific Pages</strong> — hand-pick individual pages from a scrollable checklist.</li>
     593                </ul>
     594                <p><strong>Tip:</strong> The shortcode <code>[subscriptionsystem_subscribe]</code> always renders wherever it is placed regardless of these display settings — they only control the floating button and auto-open behavior.</p>
     595            </div>
     596
     597            <!-- Step E -->
     598            <div style="background:#f0f4ff;border:1px solid #c7d2fe;border-radius:6px;padding:15px 20px;margin-bottom:12px;">
     599                <h4 style="margin-top:0;color:#4338ca;">Step E — Enable Context-Aware Pre-Selection</h4>
     600                <p>Under <strong>Context-Aware Modal</strong>:</p>
     601                <ul>
     602                    <li>Tick <strong>Auto-Select Current Post's Categories</strong> — when a visitor is on a post, its categories are pre-checked in the modal.</li>
     603                    <li>Tick <strong>Auto-Select Current Post's Tags</strong> — same for tags.</li>
     604                </ul>
     605                <p><em>Example: A visitor reads a post in "Sports" tagged "Hockey". Both checkboxes are pre-ticked when the modal opens, with a <strong>current</strong> label — they can confirm or adjust before subscribing.</em></p>
     606                <p>This works for both the shortcode button and the floating button / auto-open modal.</p>
     607            </div>
     608
     609            <!-- Step F -->
     610            <div style="background:#f0f4ff;border:1px solid #c7d2fe;border-radius:6px;padding:15px 20px;margin-bottom:12px;">
     611                <h4 style="margin-top:0;color:#4338ca;">Step F — Logged-In User Experience</h4>
     612                <p>Under <strong>Logged-In User Experience</strong>:</p>
     613                <ul>
     614                    <li>Tick <strong>Hide Name & Email Fields for Logged-In Users</strong> — when a logged-in user opens the modal, their name and email are passed silently in hidden fields. The modal opens directly on the category/tag/plan step with a personalized greeting (e.g. <em>"Hi John! Choose what you'd like to follow below."</em>).</li>
     615                </ul>
     616                <p>This setting is enabled by default. Disable it only if you want logged-in users to confirm their details each time.</p>
     617            </div>
     618
     619            <!-- Step G -->
     620            <div style="background:#f0f4ff;border:1px solid #c7d2fe;border-radius:6px;padding:15px 20px;margin-bottom:12px;">
     621                <h4 style="margin-top:0;color:#4338ca;">Step G — Save Settings & Verify</h4>
     622                <ol>
     623                    <li>Click <strong>Save Settings</strong> on the Subscribe Button & Modal settings tab.</li>
     624                    <li>Visit any post or page that matches your Display Settings in a new browser tab.</li>
     625                    <li>Confirm the floating button appears and/or the modal auto-opens after the configured delay.</li>
     626                    <li>Open the modal and verify the categories/tags are pre-selected if context-awareness is on.</li>
     627                    <li>Submit a test subscription (use a real or test email) and confirm the row appears in <strong>Subscriptions → Interest Subscribers</strong>.</li>
     628                </ol>
     629                <p style="margin-bottom:0;"><a href="<?php echo esc_url( admin_url('admin.php?page=subscription-interest-subscribers') ); ?>" class="button">View Interest Subscribers →</a></p>
     630            </div>
     631
     632            <!-- Shortcode Reference -->
     633            <h3>Shortcode Reference</h3>
     634            <h4>[subscriptionsystem_subscribe]</h4>
     635            <div class="shortcode-box">
     636                [subscriptionsystem_subscribe]
     637                <button class="copy-button" onclick="copyToClipboard('[subscriptionsystem_subscribe]')">Copy</button>
     638            </div>
     639            <p>Renders an inline subscribe button that opens the interest-subscription modal when clicked. Place it anywhere in a post, page, or widget.</p>
     640
     641            <h4>Optional attributes</h4>
     642            <div class="shortcode-box">
     643                [subscriptionsystem_subscribe button_text="Join Now" button_class="my-custom-class"]
     644                <button class="copy-button" onclick="copyToClipboard('[subscriptionsystem_subscribe button_text=&quot;Join Now&quot; button_class=&quot;my-custom-class&quot;]')">Copy</button>
     645            </div>
     646            <table style="width:100%;border-collapse:collapse;margin-top:8px;">
     647                <thead>
     648                    <tr style="background:#e0e7ff;">
     649                        <th style="padding:8px 10px;border:1px solid #c7d2fe;text-align:left;">Attribute</th>
     650                        <th style="padding:8px 10px;border:1px solid #c7d2fe;text-align:left;">Default</th>
     651                        <th style="padding:8px 10px;border:1px solid #c7d2fe;text-align:left;">Description</th>
     652                    </tr>
     653                </thead>
     654                <tbody>
     655                    <tr>
     656                        <td style="padding:8px 10px;border:1px solid #c7d2fe;font-family:monospace;">button_text</td>
     657                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Subscribe</td>
     658                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">The visible label of the button.</td>
     659                    </tr>
     660                    <tr>
     661                        <td style="padding:8px 10px;border:1px solid #c7d2fe;font-family:monospace;">button_class</td>
     662                        <td style="padding:8px 10px;border:1px solid #c7d2fe;"><em>(empty)</em></td>
     663                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Extra CSS class(es) added to the button element for custom styling.</td>
     664                    </tr>
     665                </tbody>
     666            </table>
     667
     668            <!-- Notification Flow -->
     669            <h3>Email Notification Flow (When New Content Is Published)</h3>
     670            <ol>
     671                <li>Admin publishes a new post.</li>
     672                <li>The system reads the post's categories and tags.</li>
     673                <li>It queries the <strong>interest subscribers</strong> table for active subscriptions that overlap with those categories or tags.</li>
     674                <li>A matching email is sent to each qualifying subscriber — subject line includes the site name and post title; body includes an excerpt and a "Read more" link.</li>
     675                <li>Unmatched subscribers receive no email for that post.</li>
     676            </ol>
     677            <p><strong>From name / From email:</strong> Controlled by <strong>Subscriptions → Settings → Communication</strong> (same as all other notification emails).</p>
     678
     679            <!-- Troubleshooting -->
     680            <h3>Troubleshooting</h3>
     681            <table style="width:100%;border-collapse:collapse;">
     682                <thead>
     683                    <tr style="background:#e0e7ff;">
     684                        <th style="padding:8px 10px;border:1px solid #c7d2fe;text-align:left;">Problem</th>
     685                        <th style="padding:8px 10px;border:1px solid #c7d2fe;text-align:left;">Likely Cause &amp; Fix</th>
     686                    </tr>
     687                </thead>
     688                <tbody>
     689                    <tr>
     690                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Modal does not appear</td>
     691                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Check that the current page type matches the <strong>Display Settings</strong>. Also verify that at least one active plan exists in <strong>Subscriptions → Plans</strong>.</td>
     692                    </tr>
     693                    <tr>
     694                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">No plans shown in modal</td>
     695                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Go to <strong>Subscriptions → Plans</strong> and ensure at least one plan has <strong>Status = Active</strong>.</td>
     696                    </tr>
     697                    <tr>
     698                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Floating button overlaps content</td>
     699                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Change the <strong>Floating Button Position</strong> to Bottom Left or Bottom Center, or add custom CSS to reposition it.</td>
     700                    </tr>
     701                    <tr>
     702                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Categories/tags not pre-selected</td>
     703                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Context-aware pre-selection only works on <strong>single post</strong> views. Archive, home, and page views do not have post-level taxonomy data.</td>
     704                    </tr>
     705                    <tr>
     706                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Logged-in user still sees name/email fields</td>
     707                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Make sure <strong>"Hide Name & Email Fields for Logged-In Users"</strong> is ticked in the Subscribe Button & Modal settings.</td>
     708                    </tr>
     709                    <tr>
     710                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Subscribers not receiving emails</td>
     711                        <td style="padding:8px 10px;border:1px solid #c7d2fe;">Verify <strong>Email Notifications</strong> are enabled in <strong>Communication Settings</strong> and your SMTP is configured correctly. Check the <strong>Email Logs</strong> page for bounce or error details.</td>
     712                    </tr>
     713                </tbody>
     714            </table>
     715
     716        </div>
     717
    287718        <!-- Email Notifications -->
    288719        <div class="help-section" id="email-notifications">
    … …  
    388819            </div>
    389820            <p>Displays the content subscription success page with plan details and next steps.</p>
     821
     822            <h3>Interest / Category-Tag Subscriptions</h3>
     823
     824            <h4>[subscriptionsystem_subscribe]</h4>
     825            <div class="shortcode-box">
     826                [subscriptionsystem_subscribe]
     827                <button class="copy-button" onclick="copyToClipboard('[subscriptionsystem_subscribe]')">Copy</button>
     828            </div>
     829            <p>Renders an inline button that opens the interest-subscription popup modal. When clicked, users can pick which categories and tags they want to follow, choose a plan, and subscribe — all without leaving the page.</p>
     830
     831            <div class="shortcode-box">
     832                [subscriptionsystem_subscribe button_text="Follow This Topic" button_class="my-btn"]
     833                <button class="copy-button" onclick="copyToClipboard('[subscriptionsystem_subscribe button_text=&quot;Follow This Topic&quot; button_class=&quot;my-btn&quot;]')">Copy</button>
     834            </div>
     835            <p><strong>Attributes:</strong> <code>button_text</code> (default: "Subscribe") — label shown on the button. <code>button_class</code> (default: empty) — additional CSS classes on the button element.</p>
     836            <p>For floating button and auto-open trigger configuration, see <a href="#interest-subscribe-modal">Subscribe Button & Modal Settings</a>.</p>
    390837        </div>
    391838
    … …  
    460907                <li><strong>Page Settings:</strong> Assign pages for different functions</li>
    461908                <li><strong>Communication Settings:</strong> Control email notifications</li>
     909                <li><strong>Subscribe Button & Modal:</strong> Configure triggers (floating button, auto-open, shortcode), display rules, context-awareness, and logged-in UX for the interest-based subscription modal</li>
    462910            </ul>
    463911        </div>
  • subscription-system/trunk/admin/partials/plans-display.php

    r3438196 r3598150  
    9494                                <tr>
    9595                                    <th scope="row"><label for="price"><?php echo esc_html__('Price', 'subscription-system'); ?></label></th>
    96                                     <td><input type="number" name="price" id="price" class="regular-text" step="0.01" min="0" required></td>
     96                                    <td><input type="number" name="price" id="price" class="regular-text" step="0.01" min="0" required>
     97                                        <p class="description"><?php esc_html_e('Set to 0 for a free plan.', 'subscription-system'); ?></p>
     98                                    </td>
    9799                                </tr>
    98100                                <tr>
    … …  
    109111                                    </td>
    110112                                </tr>
     113                                <tr>
     114                                    <th scope="row"><label for="status"><?php echo esc_html__('Status', 'subscription-system'); ?></label></th>
     115                                    <td>
     116                                        <select name="status" id="status" class="regular-text">
     117                                            <option value="active"><?php esc_html_e('Active', 'subscription-system'); ?></option>
     118                                            <option value="inactive"><?php esc_html_e('Inactive', 'subscription-system'); ?></option>
     119                                        </select>
     120                                        <p class="description"><?php esc_html_e('Only active plans appear in the subscription modal and pricing pages.', 'subscription-system'); ?></p>
     121                                    </td>
     122                                </tr>
    111123                            </table>
     124
     125                            <h4 style="margin:20px 0 4px;color:#4338ca;">🔔 <?php esc_html_e('Interest Subscription Limits', 'subscription-system'); ?></h4>
     126                            <p class="description" style="margin-bottom:10px;"><?php esc_html_e('These limits control how many categories and tags a subscriber can pick when using the Subscribe Button & Modal. Set to 0 for unlimited.', 'subscription-system'); ?></p>
     127                            <table class="form-table" role="presentation" style="margin-top:0;">
     128                                <tr>
     129                                    <th scope="row"><label for="max_categories"><?php esc_html_e('Max Categories', 'subscription-system'); ?></label></th>
     130                                    <td>
     131                                        <input type="number" name="max_categories" id="max_categories" class="small-text" min="0" value="0">
     132                                        <p class="description"><?php esc_html_e('0 = unlimited. For free plans, a limit of 2 is recommended.', 'subscription-system'); ?></p>
     133                                    </td>
     134                                </tr>
     135                                <tr>
     136                                    <th scope="row"><label for="max_tags"><?php esc_html_e('Max Tags', 'subscription-system'); ?></label></th>
     137                                    <td>
     138                                        <input type="number" name="max_tags" id="max_tags" class="small-text" min="0" value="0">
     139                                        <p class="description"><?php esc_html_e('0 = unlimited. For free plans, a limit of 3 is recommended.', 'subscription-system'); ?></p>
     140                                    </td>
     141                                </tr>
     142                            </table>
     143                            <input type="hidden" name="plan_features_json" id="plan_features_json" value="">
     144
    112145                            <p class="submit">
    113146                                <input type="submit" name="add_plan" class="button button-primary" value="<?php echo esc_attr__('Save Plan', 'subscription-system'); ?>">
    … …  
    178211                                <tr>
    179212                                    <th scope="row"><label for="edit_price"><?php echo esc_html__('Price', 'subscription-system'); ?></label></th>
    180                                     <td><input type="number" name="price" id="edit_price" class="regular-text" step="0.01" min="0" required></td>
     213                                    <td><input type="number" name="price" id="edit_price" class="regular-text" step="0.01" min="0" required>
     214                                        <p class="description"><?php esc_html_e('Set to 0 for a free plan.', 'subscription-system'); ?></p>
     215                                    </td>
    181216                                </tr>
    182217                                <tr>
    … …  
    193228                                    </td>
    194229                                </tr>
     230                                <tr>
     231                                    <th scope="row"><label for="edit_status"><?php esc_html_e('Status', 'subscription-system'); ?></label></th>
     232                                    <td>
     233                                        <select name="status" id="edit_status" class="regular-text">
     234                                            <option value="active"><?php esc_html_e('Active', 'subscription-system'); ?></option>
     235                                            <option value="inactive"><?php esc_html_e('Inactive', 'subscription-system'); ?></option>
     236                                        </select>
     237                                        <p class="description"><?php esc_html_e('Only active plans appear in the subscription modal and pricing pages.', 'subscription-system'); ?></p>
     238                                    </td>
     239                                </tr>
    195240                            </table>
     241
     242                            <h4 style="margin:20px 0 4px;color:#4338ca;">🔔 <?php esc_html_e('Interest Subscription Limits', 'subscription-system'); ?></h4>
     243                            <p class="description" style="margin-bottom:10px;"><?php esc_html_e('These limits control how many categories and tags a subscriber can pick in the Subscribe Button & Modal. Set to 0 for unlimited.', 'subscription-system'); ?></p>
     244                            <table class="form-table" role="presentation" style="margin-top:0;">
     245                                <tr>
     246                                    <th scope="row"><label for="edit_max_categories"><?php esc_html_e('Max Categories', 'subscription-system'); ?></label></th>
     247                                    <td>
     248                                        <input type="number" name="max_categories" id="edit_max_categories" class="small-text" min="0" value="0">
     249                                        <p class="description"><?php esc_html_e('0 = unlimited. For free plans, 2 is recommended.', 'subscription-system'); ?></p>
     250                                    </td>
     251                                </tr>
     252                                <tr>
     253                                    <th scope="row"><label for="edit_max_tags"><?php esc_html_e('Max Tags', 'subscription-system'); ?></label></th>
     254                                    <td>
     255                                        <input type="number" name="max_tags" id="edit_max_tags" class="small-text" min="0" value="0">
     256                                        <p class="description"><?php esc_html_e('0 = unlimited. For free plans, 3 is recommended.', 'subscription-system'); ?></p>
     257                                    </td>
     258                                </tr>
     259                            </table>
     260                            <input type="hidden" name="plan_features_json" id="edit_plan_features_json" value="">
     261
    196262                            <p class="submit">
    197263                                <input type="submit" name="edit_plan" class="button button-primary" value="<?php echo esc_attr__('Update Plan', 'subscription-system'); ?>">
    … …  
    225291                <tr>
    226292                    <th scope="col" class="manage-column column-name"><?php echo esc_html__('Plan Name', 'subscription-system'); ?></th>
    227                     <th scope="col" class="manage-column column-description"><?php echo esc_html__('Description', 'subscription-system'); ?></th>
     293                    <th scope="col" class="manage-column column-description"><?php echo esc_html__('Features', 'subscription-system'); ?></th>
    228294                    <th scope="col" class="manage-column column-price"><?php echo esc_html__('Price', 'subscription-system'); ?></th>
    229                     <th scope="col" class="manage-column column-duration"><?php echo esc_html__('Duration (days)', 'subscription-system'); ?></th>
    230                     <th scope="col" class="manage-column column-category"><?php echo esc_html__('Category', 'subscription-system'); ?></th>
    231                     <th scope="col"><?php echo esc_html__('Stripe Status', 'subscription-system'); ?></th>
     295                    <th scope="col" class="manage-column column-duration"><?php echo esc_html__('Duration', 'subscription-system'); ?></th>
     296                    <th scope="col"><?php echo esc_html__('Interest Limits', 'subscription-system'); ?></th>
     297                    <th scope="col"><?php echo esc_html__('Status', 'subscription-system'); ?></th>
     298                    <th scope="col"><?php echo esc_html__('Stripe', 'subscription-system'); ?></th>
    232299                    <th scope="col" class="manage-column column-actions"><?php echo esc_html__('Actions', 'subscription-system'); ?></th>
    233300                </tr>
    … …  
    248315                            }
    249316                        }
     317
     318                        // Parse limits from features JSON
     319                        $plan_max_cats = 0;
     320                        $plan_max_tags = 0;
     321                        if ( ! empty( $plan->features ) ) {
     322                            $feat_data = json_decode( $plan->features, true );
     323                            if ( is_array( $feat_data ) ) {
     324                                $plan_max_cats = isset( $feat_data['max_categories'] ) ? (int) $feat_data['max_categories'] : 0;
     325                                $plan_max_tags = isset( $feat_data['max_tags'] )       ? (int) $feat_data['max_tags']       : 0;
     326                            }
     327                        }
     328
     329                        $plan_status = ! empty( $plan->status ) ? $plan->status : 'active';
    250330                    ?>
    251331                        <tr>
    252                             <td class="column-name"><?php echo esc_html($plan->plan_name); ?></td>
     332                            <td class="column-name">
     333                                <strong><?php echo esc_html($plan->plan_name); ?></strong>
     334                                <?php if ( $category_name ) : ?>
     335                                    <br><small style="color:#888;"><?php echo esc_html( $category_name ); ?></small>
     336                                <?php endif; ?>
     337                            </td>
    253338                            <td class="column-description">
    254339                                <?php
    255340                                $desc = $plan->description;
    256                                 // Check if description is JSON
    257341                                if (substr($desc, 0, 1) === '[' && substr($desc, -1) === ']') {
    258                                     try {
    259                                         // Handle escaped JSON strings
    260                                         $desc = stripslashes($desc);
    261                                         $features = json_decode($desc, true);
    262                                        
    263                                         if (is_array($features) && !empty($features)) {
    264                                             echo '<ul style="margin: 0; padding-left: 20px;">';
    265                                             foreach ($features as $feature) {
    266                                                 echo '<li>✓ ' . esc_html($feature) . '</li>';
    267                                             }
    268                                             echo '</ul>';
    269                                         } else {
    270                                             echo esc_html($desc);
     342                                    $desc = stripslashes($desc);
     343                                    $features_arr = json_decode($desc, true);
     344                                    if (is_array($features_arr) && !empty($features_arr)) {
     345                                        echo '<ul style="margin: 0; padding-left: 20px;">';
     346                                        foreach ($features_arr as $feature) {
     347                                            echo '<li>✓ ' . esc_html($feature) . '</li>';
    271348                                        }
    272                                     } catch (Exception $e) {
     349                                        echo '</ul>';
     350                                    } else {
    273351                                        echo esc_html($desc);
    274352                                    }
    … …  
    278356                                ?>
    279357                            </td>
    280                             <td class="column-price"><?php echo esc_html(number_format($plan->price, 2)); ?></td>
    281                             <td class="column-duration"><?php echo esc_html($plan->duration); ?></td>
    282                             <td class="column-category"><?php echo esc_html($category_name); ?></td>
     358                            <td class="column-price">
     359                                <?php if ( (float) $plan->price <= 0 ) : ?>
     360                                    <span style="color:#059669;font-weight:600;"><?php esc_html_e( 'Free', 'subscription-system' ); ?></span>
     361                                <?php else : ?>
     362                                    $<?php echo esc_html( number_format( $plan->price, 2 ) ); ?>
     363                                <?php endif; ?>
     364                            </td>
     365                            <td class="column-duration">
     366                                <?php
     367                                $dur_map = array( 1 => 'Daily', 7 => 'Weekly', 30 => 'Monthly', 90 => 'Quarterly', 180 => '6 Months', 365 => 'Yearly' );
     368                                echo esc_html( isset( $dur_map[ (int) $plan->duration ] ) ? $dur_map[ (int) $plan->duration ] : $plan->duration . ' days' );
     369                                ?>
     370                            </td>
     371                            <td>
     372                                <small>
     373                                    <?php
     374                                    $cats_label = $plan_max_cats > 0 ? $plan_max_cats : '∞';
     375                                    $tags_label = $plan_max_tags > 0 ? $plan_max_tags : '∞';
     376                                    printf( 'Cat: <strong>%s</strong> &nbsp; Tags: <strong>%s</strong>', esc_html( $cats_label ), esc_html( $tags_label ) );
     377                                    ?>
     378                                </small>
     379                            </td>
     380                            <td>
     381                                <?php if ( $plan_status === 'active' ) : ?>
     382                                    <span style="background:#d1fae5;color:#065f46;padding:2px 8px;border-radius:12px;font-size:12px;font-weight:600;"><?php esc_html_e('Active','subscription-system'); ?></span>
     383                                <?php else : ?>
     384                                    <span style="background:#fee2e2;color:#991b1b;padding:2px 8px;border-radius:12px;font-size:12px;font-weight:600;"><?php esc_html_e('Inactive','subscription-system'); ?></span>
     385                                <?php endif; ?>
     386                            </td>
    283387                            <td>
    284388                                <?php
    285389                                $stripe_product_id = $plan->stripe_product_id;
    286390                                $stripe_price_id = $plan->stripe_price_id;
    287                                
    288                                 // Check if the plan is connected to Stripe
    289391                                $is_connected = !empty($stripe_product_id) && !empty($stripe_price_id);
    290392                                $is_mock = $is_connected && (strpos($stripe_product_id, 'mock_') === 0);
    291                                
    292393                                if ($is_connected) {
    293394                                    echo '<span class="stripe-status ' . ($is_mock ? 'mock-connected' : 'connected') . '">' .
    294395                                        '<span class="dashicons dashicons-' . ($is_mock ? 'warning' : 'yes-alt') . '"></span> ' .
    295                                         ($is_mock ? 'Mock Connected' : 'Connected') . '</span>';
    296                                    
    297                                     if ($is_mock) {
    298                                         echo '<br><small>Using test data - not connected to Stripe</small>';
    299                                     } else {
    300                                         echo '<br><small>Product ID: ' . substr($stripe_product_id, 0, 8) . '...</small>';
    301                                         echo '<br><small>Price ID: ' . substr($stripe_price_id, 0, 8) . '...</small>';
    302                                     }
     396                                        ($is_mock ? 'Mock' : 'Connected') . '</span>';
    303397                                } else {
    304                                     echo '<span class="stripe-status not-connected"><span class="dashicons dashicons-no"></span> Not Connected</span>';
     398                                    echo '<span class="stripe-status not-connected"><span class="dashicons dashicons-no"></span> None</span>';
    305399                                }
    306400                                ?>
    307                                 <br>
    308                                 <a href="#" class="sync-with-stripe" data-plan-id="<?php echo $plan->id; ?>" data-nonce="<?php echo wp_create_nonce('sync_plan_with_stripe'); ?>">Sync with Stripe</a>
     401                                <br><a href="#" class="sync-with-stripe" data-plan-id="<?php echo $plan->id; ?>" data-nonce="<?php echo wp_create_nonce('sync_plan_with_stripe'); ?>" style="font-size:11px;">Sync</a>
    309402                            </td>
    310403                            <td class="column-actions">
    … …  
    315408                                        data-price="<?php echo esc_attr($plan->price); ?>"
    316409                                        data-duration="<?php echo esc_attr($plan->duration); ?>"
    317                                         data-category="<?php echo esc_attr($plan->category_id); ?>">
     410                                        data-category="<?php echo esc_attr($plan->category_id); ?>"
     411                                        data-status="<?php echo esc_attr($plan_status); ?>"
     412                                        data-max-categories="<?php echo esc_attr($plan_max_cats); ?>"
     413                                        data-max-tags="<?php echo esc_attr($plan_max_tags); ?>">
    318414                                    <?php echo esc_html__('Edit', 'subscription-system'); ?>
    319415                                </button>
    … …  
    354450        $('#add-plan-form').hide();
    355451       
    356         var id = $(this).data('id');
    357         var name = $(this).data('name');
    358         var description = $(this).data('description');
    359         var price = $(this).data('price');
    360         var duration = $(this).data('duration');
    361         var category = $(this).data('category');
     452        var id           = $(this).data('id');
     453        var name         = $(this).data('name');
     454        var description  = $(this).data('description');
     455        var price        = $(this).data('price');
     456        var duration     = $(this).data('duration');
     457        var category     = $(this).data('category');
     458        var status       = $(this).data('status') || 'active';
     459        var maxCats      = parseInt($(this).data('max-categories'), 10) || 0;
     460        var maxTags      = parseInt($(this).data('max-tags'), 10) || 0;
    362461       
    363462        $('#edit_plan_id').val(id);
    … …  
    366465        $('#edit_duration').val(duration);
    367466        $('#edit_category_id').val(category);
     467        $('#edit_status').val(status);
     468        $('#edit_max_categories').val(maxCats);
     469        $('#edit_max_tags').val(maxTags);
    368470       
    369471        // Clear existing feature inputs
    … …  
    372474        // Try to parse description as JSON (for features)
    373475        try {
    374             // First try to handle escaped JSON (with backslashes)
    375476            var cleanDescription = description.replace(/\\\\/g, '\\').replace(/\\"/g, '"');
    376477            var features = JSON.parse(cleanDescription);
    377            
    378478            if (Array.isArray(features)) {
    379                 // Add feature inputs for each feature
    380479                features.forEach(function(feature) {
    381480                    addEditFeature(feature);
    382481                });
    383482            } else {
    384                 // If not an array, add as a single feature
    385483                addEditFeature(cleanDescription);
    386484            }
    387485        } catch (e) {
    388             // If not valid JSON, add as a single feature
    389486            if (description) {
    390487                addEditFeature(description);
    … …  
    621718    });
    622719   
     720    // ── Build features JSON before submitting Add form ──────────────────────
     721    $('#add-plan-form form').on('submit', function() {
     722        var limits = {
     723            max_categories: parseInt($('#max_categories').val(), 10) || 0,
     724            max_tags:       parseInt($('#max_tags').val(), 10) || 0
     725        };
     726        $('#plan_features_json').val(JSON.stringify(limits));
     727    });
     728
     729    // ── Build features JSON before submitting Edit form ──────────────────────
     730    $('#edit-plan-form form').on('submit', function() {
     731        var limits = {
     732            max_categories: parseInt($('#edit_max_categories').val(), 10) || 0,
     733            max_tags:       parseInt($('#edit_max_tags').val(), 10) || 0
     734        };
     735        $('#edit_plan_features_json').val(JSON.stringify(limits));
     736    });
     737
    623738    // Fix for the category column display
    624739    $('.sync-with-stripe').on('click', function(e) {
  • subscription-system/trunk/admin/partials/settings-display.php

    r3311076 r3598150  
    1717// Define tabs
    1818$tabs = array(
    19     'general' => __('General', 'subscription-system'),
    20     'payment' => __('Payment', 'subscription-system'),
    21     'communication' => __('Communication', 'subscription-system'),
    22     'pages' => __('Pages Settings', 'subscription-system')
     19    'general'        => __('General', 'subscription-system'),
     20    'payment'        => __('Payment', 'subscription-system'),
     21    'communication'  => __('Communication', 'subscription-system'),
     22    'pages'          => __('Pages Settings', 'subscription-system'),
     23    'subscribe_modal' => __('Subscribe Button & Modal', 'subscription-system'),
    2324);
    2425
    … …  
    7879                        case 'pages':
    7980                            include 'pages-settings-display.php';
     81                            break;
     82                        case 'subscribe_modal':
     83                            include 'subscribe-modal-settings-display.php';
    8084                            break;
    8185                    }
  • subscription-system/trunk/includes/class-subs-sys-ajax.php

    r3435450 r3598150  
    7171                add_action( 'wp_ajax_unsubscribe_from_content_plan', array( $this, 'unsubscribe_from_content_plan' ) );
    7272                add_action( 'wp_ajax_test_content_ajax', array( $this, 'test_content_ajax' ) );
     73                add_action( 'wp_ajax_submit_interest_subscription', array( $this, 'submit_interest_subscription' ) );
     74                add_action( 'wp_ajax_nopriv_submit_interest_subscription', array( $this, 'submit_interest_subscription' ) );
    7375        }
    7476
    … …  
    16291631         */
    16301632        public function get_content_options() {
    1631                 // Debug logging
    1632                 error_log('AJAX get_content_options called');
    1633                 error_log('POST data: ' . print_r($_POST, true));
    1634 
    1635                 // Verify nonce
    16361633                if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'get_content_options' ) ) {
    1637                         error_log('Nonce verification failed');
    16381634                        wp_send_json_error( array( 'message' => __( 'Security check failed.', 'subscription-system' ) ) );
    16391635                        return;
    16401636                }
    16411637
    1642                 // Check user capabilities
    16431638                if ( ! current_user_can( 'manage_options' ) ) {
    1644                         error_log('User does not have manage_options capability');
    16451639                        wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'subscription-system' ) ) );
    16461640                        return;
    16471641                }
    16481642
    1649                 $content_type = isset( $_POST['content_type'] ) ? sanitize_text_field( $_POST['content_type'] ) : '';
    1650                 $selected_ids = isset( $_POST['selected_ids'] ) ? json_decode( stripslashes( $_POST['selected_ids'] ), true ) : array();
    1651                
    1652                 // Ensure selected_ids is an array
    1653                 if (!is_array($selected_ids)) {
     1643                $content_type    = isset( $_POST['content_type'] ) ? sanitize_text_field( wp_unslash( $_POST['content_type'] ) ) : '';
     1644                $selected_ids    = isset( $_POST['selected_ids'] ) ? json_decode( stripslashes( $_POST['selected_ids'] ), true ) : array();
     1645                $editing_plan_id = isset( $_POST['plan_id'] ) ? absint( $_POST['plan_id'] ) : 0;
     1646
     1647                if ( ! is_array( $selected_ids ) ) {
    16541648                        $selected_ids = array();
    16551649                }
    16561650
    1657                 error_log('Content type: ' . $content_type);
    1658                 error_log('Selected IDs: ' . print_r($selected_ids, true));
    1659 
    16601651                if ( empty( $content_type ) ) {
    1661                         error_log('Content type is empty');
    16621652                        wp_send_json_error( array( 'message' => __( 'Content type is required.', 'subscription-system' ) ) );
    16631653                        return;
     1654                }
     1655
     1656                // Build a map of term/item ID → plan name for already-claimed IDs (category & tag only).
     1657                $claimed = array(); // [ id => plan_name ]
     1658                if ( in_array( $content_type, array( 'category', 'tag' ), true ) && Subs_Sys_DB_Updater::content_tables_exist() ) {
     1659                        global $wpdb;
     1660                        $table  = $wpdb->prefix . 'subscription_content_plans';
     1661                        $query  = $wpdb->prepare(
     1662                                "SELECT id, plan_name, content_ids FROM $table WHERE content_type = %s AND status = 'active'",
     1663                                $content_type
     1664                        );
     1665                        if ( $editing_plan_id ) {
     1666                                $query = $wpdb->prepare(
     1667                                        "SELECT id, plan_name, content_ids FROM $table WHERE content_type = %s AND status = 'active' AND id != %d",
     1668                                        $content_type,
     1669                                        $editing_plan_id
     1670                                );
     1671                        }
     1672                        $other_plans = $wpdb->get_results( $query );
     1673                        foreach ( $other_plans as $op ) {
     1674                                $ids = json_decode( $op->content_ids, true ) ?: array();
     1675                                foreach ( $ids as $id ) {
     1676                                        $claimed[ (int) $id ] = $op->plan_name;
     1677                                }
     1678                        }
    16641679                }
    16651680
    … …  
    16681683                switch ( $content_type ) {
    16691684                        case 'category':
    1670                                 $categories = get_categories( array( 'hide_empty' => false ) );
     1685                                $categories = get_categories( array( 'hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC' ) );
    16711686                                $html = '<div class="content-checkboxes">';
    16721687                                foreach ( $categories as $category ) {
    1673                                         $checked = in_array( $category->term_id, $selected_ids ) || in_array( (string)$category->term_id, $selected_ids ) ? 'checked' : '';
    1674                                         $html .= sprintf(
    1675                                                 '<label><input type="checkbox" name="content_ids[]" value="%d" %s> %s (%d posts)</label><br>',
    1676                                                 $category->term_id,
    1677                                                 $checked,
    1678                                                 esc_html( $category->name ),
    1679                                                 $category->count
    1680                                         );
     1688                                        $tid        = (int) $category->term_id;
     1689                                        $is_claimed = isset( $claimed[ $tid ] );
     1690                                        $checked    = ! $is_claimed && ( in_array( $tid, $selected_ids, true ) || in_array( (string) $tid, $selected_ids, true ) ) ? 'checked' : '';
     1691                                        if ( $is_claimed ) {
     1692                                                $html .= sprintf(
     1693                                                        '<label class="ss-claimed-item" title="%s"><input type="checkbox" name="content_ids[]" value="%d" disabled> %s <span class="ss-claimed-badge">%s</span></label><br>',
     1694                                                        esc_attr( sprintf( __( 'Already used by plan: %s', 'subscription-system' ), $claimed[ $tid ] ) ),
     1695                                                        $tid,
     1696                                                        esc_html( $category->name ),
     1697                                                        esc_html( sprintf( __( 'Used by: %s', 'subscription-system' ), $claimed[ $tid ] ) )
     1698                                                );
     1699                                        } else {
     1700                                                $html .= sprintf(
     1701                                                        '<label><input type="checkbox" name="content_ids[]" value="%d" %s> %s <span class="ss-post-count">(%d)</span></label><br>',
     1702                                                        $tid,
     1703                                                        $checked,
     1704                                                        esc_html( $category->name ),
     1705                                                        (int) $category->count
     1706                                                );
     1707                                        }
    16811708                                }
    16821709                                $html .= '</div>';
    … …  
    16841711
    16851712                        case 'tag':
    1686                                 $tags = get_tags( array( 'hide_empty' => false ) );
     1713                                $tags = get_tags( array( 'hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC' ) );
    16871714                                $html = '<div class="content-checkboxes">';
    16881715                                foreach ( $tags as $tag ) {
    1689                                         $checked = in_array( $tag->term_id, $selected_ids ) || in_array( (string)$tag->term_id, $selected_ids ) ? 'checked' : '';
    1690                                         $html .= sprintf(
    1691                                                 '<label><input type="checkbox" name="content_ids[]" value="%d" %s> %s (%d posts)</label><br>',
    1692                                                 $tag->term_id,
    1693                                                 $checked,
    1694                                                 esc_html( $tag->name ),
    1695                                                 $tag->count
    1696                                         );
     1716                                        $tid        = (int) $tag->term_id;
     1717                                        $is_claimed = isset( $claimed[ $tid ] );
     1718                                        $checked    = ! $is_claimed && ( in_array( $tid, $selected_ids, true ) || in_array( (string) $tid, $selected_ids, true ) ) ? 'checked' : '';
     1719                                        if ( $is_claimed ) {
     1720                                                $html .= sprintf(
     1721                                                        '<label class="ss-claimed-item" title="%s"><input type="checkbox" name="content_ids[]" value="%d" disabled> %s <span class="ss-claimed-badge">%s</span></label><br>',
     1722                                                        esc_attr( sprintf( __( 'Already used by plan: %s', 'subscription-system' ), $claimed[ $tid ] ) ),
     1723                                                        $tid,
     1724                                                        esc_html( $tag->name ),
     1725                                                        esc_html( sprintf( __( 'Used by: %s', 'subscription-system' ), $claimed[ $tid ] ) )
     1726                                                );
     1727                                        } else {
     1728                                                $html .= sprintf(
     1729                                                        '<label><input type="checkbox" name="content_ids[]" value="%d" %s> %s <span class="ss-post-count">(%d)</span></label><br>',
     1730                                                        $tid,
     1731                                                        $checked,
     1732                                                        esc_html( $tag->name ),
     1733                                                        (int) $tag->count
     1734                                                );
     1735                                        }
    16971736                                }
    16981737                                $html .= '</div>';
    … …  
    17041743                                        'post_status' => 'publish',
    17051744                                        'orderby'     => 'title',
    1706                                         'order'       => 'ASC'
     1745                                        'order'       => 'ASC',
    17071746                                ) );
    17081747                                $html = '<div class="content-checkboxes">';
    17091748                                foreach ( $posts as $post ) {
    1710                                         $checked = in_array( $post->ID, $selected_ids ) || in_array( (string)$post->ID, $selected_ids ) ? 'checked' : '';
     1749                                        $checked = in_array( $post->ID, $selected_ids, true ) || in_array( (string) $post->ID, $selected_ids, true ) ? 'checked' : '';
    17111750                                        $html .= sprintf(
    17121751                                                '<label><input type="checkbox" name="content_ids[]" value="%d" %s> %s</label><br>',
    … …  
    17231762                                $html = '<div class="content-checkboxes">';
    17241763                                foreach ( $post_types as $post_type ) {
    1725                                         $checked = in_array( $post_type->name, $selected_ids ) ? 'checked' : '';
     1764                                        $checked = in_array( $post_type->name, $selected_ids, true ) ? 'checked' : '';
    17261765                                        $html .= sprintf(
    17271766                                                '<label><input type="checkbox" name="content_ids[]" value="%s" %s> %s</label><br>',
    1728                                                 $post_type->name,
     1767                                                esc_attr( $post_type->name ),
    17291768                                                $checked,
    17301769                                                esc_html( $post_type->label )
    … …  
    17391778                }
    17401779
    1741                 error_log('Generated HTML length: ' . strlen($html));
    1742                 error_log('Generated HTML: ' . substr($html, 0, 500) . '...');
    17431780                wp_send_json_success( $html );
    17441781        }
    … …  
    20092046                ) );
    20102047        }
    2011 }
     2048
     2049        /**
     2050         * Handle interest subscription form submission from the popup modal.
     2051         */
     2052        public function submit_interest_subscription() {
     2053                if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'interest_subscription_nonce' ) ) {
     2054                        wp_send_json_error( array( 'message' => __( 'Security check failed.', 'subscription-system' ) ) );
     2055                }
     2056
     2057                if ( ! Subs_Sys_DB_Updater::interest_table_exists() ) {
     2058                        $db_updater = new Subs_Sys_DB_Updater();
     2059                        $db_updater->add_interest_subscription_table();
     2060                }
     2061
     2062                $category_ids = isset( $_POST['category_ids'] ) ? array_map( 'absint', (array) wp_unslash( $_POST['category_ids'] ) ) : array();
     2063                $tag_ids      = isset( $_POST['tag_ids'] ) ? array_map( 'absint', (array) wp_unslash( $_POST['tag_ids'] ) ) : array();
     2064                $plan_id      = isset( $_POST['plan_id'] ) ? absint( $_POST['plan_id'] ) : 0;
     2065                $plan_source  = isset( $_POST['plan_source'] ) && 'content' === sanitize_text_field( wp_unslash( $_POST['plan_source'] ) ) ? 'content' : 'interest';
     2066
     2067                $interest = new Subs_Sys_Interest_Subscription();
     2068                $result   = $interest->save_subscription(
     2069                        array(
     2070                                'name'         => isset( $_POST['subscriber_name'] ) ? wp_unslash( $_POST['subscriber_name'] ) : '',
     2071                                'email'        => isset( $_POST['subscriber_email'] ) ? wp_unslash( $_POST['subscriber_email'] ) : '',
     2072                                'category_ids' => $category_ids,
     2073                                'tag_ids'      => $tag_ids,
     2074                                'plan_id'      => $plan_id,
     2075                                'plan_source'  => $plan_source,
     2076                        )
     2077                );
     2078
     2079                if ( is_wp_error( $result ) ) {
     2080                        wp_send_json_error( array( 'message' => $result->get_error_message() ) );
     2081                }
     2082
     2083                // Look up plan price from the appropriate table.
     2084                if ( 'content' === $plan_source ) {
     2085                        $plan = Subs_Sys_Interest_Subscription::get_content_plan_by_id( $plan_id );
     2086                } else {
     2087                        $plan = Subs_Sys_Interest_Subscription::get_plan_by_id( $plan_id );
     2088                }
     2089                $is_free = $plan && (float) $plan->price <= 0;
     2090
     2091                if ( ! $is_free ) {
     2092                        $checkout_url = add_query_arg(
     2093                                array(
     2094                                        'action'                   => 'interest_checkout',
     2095                                        'interest_subscription_id' => $result,
     2096                                ),
     2097                                home_url( '/' )
     2098                        );
     2099
     2100                        wp_send_json_success(
     2101                                array(
     2102                                        'message'  => __( 'Subscription saved. Redirecting to payment...', 'subscription-system' ),
     2103                                        'redirect' => $checkout_url,
     2104                                        'plan_id'  => $plan_id,
     2105                                        'price'    => $plan ? (float) $plan->price : 0,
     2106                                )
     2107                        );
     2108                }
     2109
     2110                wp_send_json_success(
     2111                        array(
     2112                                'message' => __( 'You are subscribed! You will receive updates for your selected categories and tags.', 'subscription-system' ),
     2113                                'plan_id' => $plan_id,
     2114                        )
     2115                );
     2116        }
     2117}
  • subscription-system/trunk/includes/class-subs-sys-db-updater.php

    r3387836 r3598150  
    3131         * Current database version
    3232         */
    33         const DB_VERSION = '1.0.9';
     33        const DB_VERSION = '1.1.1';
    3434
    3535        /**
    … …  
    7575                // Check if content tables exist
    7676                $content_tables_missing = ! self::content_tables_exist();
     77
     78                $interest_table_missing = ! self::interest_table_exists();
    7779               
    7880                // Perform update if needed
    79                 if ( $version_update_needed || $email_table_missing || $content_tables_missing ) {
     81                if ( $version_update_needed || $email_table_missing || $content_tables_missing || $interest_table_missing ) {
    8082                       
    8183                        // Log what's triggering the update
    … …  
    8890                        if ( $content_tables_missing ) {
    8991                                error_log( "Subs_Sys: Content subscription tables are missing, will create them now" );
     92                        }
     93                        if ( $interest_table_missing ) {
     94                                error_log( 'Subs_Sys: Interest subscription table is missing, will create it now' );
    9095                        }
    9196                       
    … …  
    151156                        error_log( "Subs_Sys: Ensuring email logs table exists..." );
    152157                        $this->add_email_logs_table();
     158                }
     159
     160                if ( version_compare( $old_version, '1.1.0', '<' ) || ! self::interest_table_exists() ) {
     161                        error_log( 'Subs_Sys: Ensuring interest subscription table exists...' );
     162                        $this->add_interest_subscription_table();
     163                }
     164
     165                if ( version_compare( $old_version, '1.1.1', '<' ) ) {
     166                        $this->update_interest_subscription_table();
    153167                }
    154168
    … …  
    310324
    311325        /**
     326         * Add interest subscription table (version 1.1.0)
     327         */
     328        public function add_interest_subscription_table() {
     329                global $wpdb;
     330                $charset_collate = $wpdb->get_charset_collate();
     331
     332                $table = $wpdb->prefix . 'subscription_interest_subscriptions';
     333                $sql   = "CREATE TABLE IF NOT EXISTS $table (
     334                        id mediumint(9) NOT NULL AUTO_INCREMENT,
     335                        user_id bigint(20) DEFAULT NULL,
     336                        plan_id mediumint(9) DEFAULT NULL,
     337                        subscriber_name varchar(100) NOT NULL,
     338                        subscriber_email varchar(100) NOT NULL,
     339                        category_ids text NOT NULL,
     340                        tag_ids text NOT NULL,
     341                        plan_type varchar(20) NOT NULL DEFAULT 'free',
     342                        status varchar(20) NOT NULL DEFAULT 'active',
     343                        payment_id varchar(100) DEFAULT NULL,
     344                        created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
     345                        updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
     346                        PRIMARY KEY  (id),
     347                        KEY user_id (user_id),
     348                        KEY plan_id (plan_id),
     349                        KEY subscriber_email (subscriber_email),
     350                        KEY status (status),
     351                        KEY plan_type (plan_type)
     352                ) $charset_collate;";
     353
     354                require_once ABSPATH . 'wp-admin/includes/upgrade.php';
     355                dbDelta( $sql );
     356
     357                error_log( 'Subs_Sys: Interest subscription table created successfully' );
     358                set_transient( 'subs_sys_interest_table_created', true, 60 );
     359        }
     360
     361        /**
     362         * Add plan_id column to interest subscriptions (version 1.1.1).
     363         */
     364        public function update_interest_subscription_table() {
     365                global $wpdb;
     366
     367                $table = $wpdb->prefix . 'subscription_interest_subscriptions';
     368                if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) {
     369                        return;
     370                }
     371
     372                $column = $wpdb->get_results( "SHOW COLUMNS FROM $table LIKE 'plan_id'" );
     373                if ( empty( $column ) ) {
     374                        $wpdb->query( "ALTER TABLE $table ADD COLUMN plan_id mediumint(9) DEFAULT NULL AFTER user_id" );
     375                        $wpdb->query( "ALTER TABLE $table ADD KEY plan_id (plan_id)" );
     376                }
     377        }
     378
     379        /**
     380         * Check if interest subscription table exists.
     381         *
     382         * @return bool
     383         */
     384        public static function interest_table_exists() {
     385                global $wpdb;
     386                $table = $wpdb->prefix . 'subscription_interest_subscriptions';
     387                return $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) === $table;
     388        }
     389
     390        /**
    312391         * Check if email logs table exists
    313392         *
  • subscription-system/trunk/includes/class-subs-sys-shortcodes.php

    r3375846 r3598150  
    5656        add_shortcode('content_subscription_checkout', array($this, 'render_content_subscription_checkout'));
    5757        add_shortcode('content_subscription_success', array($this, 'render_content_subscription_success'));
     58        add_shortcode('subscriptionsystem_subscribe', array($this, 'render_interest_subscribe_button'));
    5859       
    5960        // Add AJAX handlers for password reset
    … …  
    22382239        return ob_get_clean();
    22392240    }
    2240 }
     2241
     2242    /**
     2243     * Render subscribe button with category/tag interest popup modal.
     2244     *
     2245     * Usage: [subscriptionsystem_subscribe] or [subscriptionsystem_subscribe button_text="Subscribe"]
     2246     *
     2247     * @param array $atts Shortcode attributes.
     2248     * @return string
     2249     */
     2250    public function render_interest_subscribe_button( $atts ) {
     2251        $atts = shortcode_atts(
     2252            array(
     2253                'button_text' => __( 'Subscribe', 'subscription-system' ),
     2254                'button_class' => '',
     2255            ),
     2256            $atts,
     2257            'subscriptionsystem_subscribe'
     2258        );
     2259
     2260        $this->enqueue_interest_subscribe_assets();
     2261
     2262        // Gather context (categories/tags) from the current post if applicable.
     2263        $context_categories = array();
     2264        $context_tags       = array();
     2265        if ( is_singular() && get_the_ID() ) {
     2266            if ( get_option( 'subs_modal_context_categories', 1 ) ) {
     2267                $cats = wp_get_post_categories( get_the_ID(), array( 'fields' => 'ids' ) );
     2268                $context_categories = array_map( 'intval', is_array( $cats ) ? $cats : array() );
     2269            }
     2270            if ( get_option( 'subs_modal_context_tags', 1 ) ) {
     2271                $tags_list = wp_get_post_tags( get_the_ID(), array( 'fields' => 'ids' ) );
     2272                $context_tags = array_map( 'intval', is_array( $tags_list ) ? $tags_list : array() );
     2273            }
     2274        }
     2275
     2276        // Only show categories/tags actually on this post; fall back to all on non-singular pages.
     2277        $display_categories = ! empty( $context_categories )
     2278            ? Subs_Sys_Interest_Subscription::get_categories_by_ids( $context_categories )
     2279            : Subs_Sys_Interest_Subscription::get_available_categories();
     2280
     2281        $display_tags = ! empty( $context_tags )
     2282            ? Subs_Sys_Interest_Subscription::get_tags_by_ids( $context_tags )
     2283            : Subs_Sys_Interest_Subscription::get_available_tags();
     2284
     2285        $modal_id = 'ss-interest-modal-' . wp_unique_id();
     2286        $args = array(
     2287            'modal_id'           => $modal_id,
     2288            'categories'         => $display_categories,
     2289            'tags'               => $display_tags,
     2290            'plans'              => Subs_Sys_Interest_Subscription::get_content_modal_plans( $context_categories, $context_tags ),
     2291            'plan_source'        => 'content',
     2292            'context_categories' => $context_categories,
     2293            'context_tags'       => $context_tags,
     2294        );
     2295
     2296        ob_start();
     2297        ?>
     2298        <div class="ss-interest-subscribe-wrap">
     2299            <button type="button"
     2300                class="ss-interest-subscribe-btn <?php echo esc_attr( $atts['button_class'] ); ?>"
     2301                data-ss-open-interest-modal="#<?php echo esc_attr( $modal_id ); ?>">
     2302                <?php echo esc_html( $atts['button_text'] ); ?>
     2303            </button>
     2304        </div>
     2305        <?php
     2306        include SUBS_SYS_PLUGIN_DIR . 'public/partials/interest-subscribe-modal.php';
     2307        return ob_get_clean();
     2308    }
     2309
     2310    /**
     2311     * Enqueue assets for the interest subscribe modal.
     2312     */
     2313    private function enqueue_interest_subscribe_assets() {
     2314        subs_sys_enqueue_interest_modal_assets( $this->plugin_name, $this->version );
     2315    }
     2316}
  • subscription-system/trunk/includes/class-subs-sys.php

    r3587496 r3598150  
    7676                        $this->version = SUBS_SYS_VERSION;
    7777                } else {
    78                         $this->version = '1.0.17';
     78                        $this->version = '1.0.18';
    7979                }
    8080                $this->plugin_name = 'subs-sys';
    … …  
    176176                 */
    177177                require_once __DIR__ . '/class-subs-sys-content-manager.php';
     178
     179                /**
     180                 * The class responsible for category/tag interest subscriptions
     181                 */
     182                require_once __DIR__ . '/class-subs-sys-interest-subscription.php';
    178183
    179184        /**
    … …  
    277282                // Initialize content subscription manager
    278283                $content_manager = new Subs_Sys_Content_Manager();
     284
     285                // Initialize interest subscription manager
     286                $interest_subscription = new Subs_Sys_Interest_Subscription();
    279287
    280288                // Initialize database updater (safe updates)
  • subscription-system/trunk/public/class-subs-sys-public.php

    r3356551 r3598150  
    5252        add_action('wp_head', array($this, 'display_smart_subscription_bar'));
    5353        add_action('template_redirect', array($this, 'handle_content_checkout_redirect'));
     54        add_action('template_redirect', array($this, 'handle_interest_checkout_redirect'));
     55        add_action('template_redirect', array($this, 'handle_interest_checkout_success'));
    5456        add_action('template_redirect', array($this, 'handle_content_subscription_success'));
    5557        add_action('wp_head', array($this, 'display_subscription_success_message'));
     58
     59        // Floating button + auto-open modal
     60        add_action('wp_footer', array($this, 'render_interest_modal_triggers'));
    5661    }
    5762
    … …  
    24542459
    24552460    /**
     2461     * Handle interest subscription paid checkout redirect (phase 1 placeholder).
     2462     */
     2463    public function handle_interest_checkout_redirect() {
     2464        if ( ! isset( $_GET['action'] ) || 'interest_checkout' !== $_GET['action'] ) {
     2465            return;
     2466        }
     2467
     2468        $subscription_id = isset( $_GET['interest_subscription_id'] ) ? absint( $_GET['interest_subscription_id'] ) : 0;
     2469        if ( ! $subscription_id ) {
     2470            wp_die( esc_html__( 'Invalid subscription.', 'subscription-system' ) );
     2471        }
     2472
     2473        $interest     = new Subs_Sys_Interest_Subscription();
     2474        $subscription = $interest->get_subscription( $subscription_id );
     2475
     2476        if ( ! $subscription || 'paid' !== $subscription->plan_type ) {
     2477            wp_die( esc_html__( 'Subscription not found.', 'subscription-system' ) );
     2478        }
     2479
     2480        // Resolve plan — try content plans first, fall back to regular plans.
     2481        $plan      = null;
     2482        $plan_name = __( 'Paid Subscription', 'subscription-system' );
     2483        $price     = 0.0;
     2484        $duration  = 30;
     2485        $stripe_price_id = '';
     2486
     2487        if ( ! empty( $subscription->plan_id ) ) {
     2488            $cp = Subs_Sys_Interest_Subscription::get_content_plan_by_id( $subscription->plan_id );
     2489            if ( $cp ) {
     2490                $plan            = $cp;
     2491                $plan_name       = $cp->plan_name;
     2492                $price           = (float) $cp->price;
     2493                $duration        = (int) ( $cp->duration ?? 30 );
     2494                $stripe_price_id = $cp->stripe_price_id ?? '';
     2495            } else {
     2496                $rp = Subs_Sys_Interest_Subscription::get_plan_by_id( $subscription->plan_id );
     2497                if ( $rp ) {
     2498                    $plan            = $rp;
     2499                    $plan_name       = $rp->plan_name;
     2500                    $price           = (float) $rp->price;
     2501                    $duration        = (int) ( $rp->duration ?? 30 );
     2502                    $stripe_price_id = $rp->stripe_price_id ?? '';
     2503                }
     2504            }
     2505        }
     2506
     2507        $price = $price > 0 ? $price : Subs_Sys_Interest_Subscription::get_paid_price();
     2508
     2509        // Resolve billing interval from duration.
     2510        $interval       = 'month';
     2511        $interval_count = 1;
     2512        if ( $duration <= 1 )        { $interval = 'day';   $interval_count = 1; }
     2513        elseif ( $duration <= 7 )    { $interval = 'week';  $interval_count = 1; }
     2514        elseif ( $duration <= 31 )   { $interval = 'month'; $interval_count = 1; }
     2515        elseif ( $duration <= 93 )   { $interval = 'month'; $interval_count = 3; }
     2516        elseif ( $duration <= 186 )  { $interval = 'month'; $interval_count = 6; }
     2517        else                         { $interval = 'year';  $interval_count = 1; }
     2518
     2519        // Build success / cancel URLs.
     2520        $success_url = add_query_arg( array(
     2521            'action'                   => 'interest_checkout_success',
     2522            'interest_subscription_id' => $subscription_id,
     2523            'session_id'               => '{CHECKOUT_SESSION_ID}',
     2524        ), home_url( '/' ) );
     2525
     2526        $cancel_url = add_query_arg( array(
     2527            'action'                   => 'interest_checkout',
     2528            'interest_subscription_id' => $subscription_id,
     2529            'cancelled'                => '1',
     2530        ), home_url( '/' ) );
     2531
     2532        // ── Try Stripe Checkout ────────────────────────────────────────────
     2533        $stripe_enabled = get_option( 'subscription_stripe_enabled', 0 );
     2534        $test_mode      = get_option( 'subscription_stripe_test_mode', 1 );
     2535        $api_key        = $test_mode
     2536            ? get_option( 'subscription_stripe_test_secret_key', '' )
     2537            : get_option( 'subscription_stripe_live_secret_key', '' );
     2538        if ( empty( $api_key ) ) {
     2539            $api_key = get_option( 'subscription_stripe_secret_key', '' );
     2540        }
     2541
     2542        if ( $stripe_enabled && ! empty( $api_key ) ) {
     2543            if ( ! class_exists( '\Stripe\Stripe' ) ) {
     2544                $admin = new Subs_Sys_Admin( $this->plugin_name, $this->version );
     2545                if ( method_exists( $admin, 'ensure_stripe_sdk_loaded' ) ) {
     2546                    $admin->ensure_stripe_sdk_loaded();
     2547                }
     2548            }
     2549
     2550            if ( class_exists( '\Stripe\Stripe' ) ) {
     2551                try {
     2552                    \Stripe\Stripe::setApiKey( $api_key );
     2553
     2554                    // Use the synced price ID if it exists and is not a mock/free placeholder.
     2555                    $use_existing_price = ! empty( $stripe_price_id )
     2556                        && strpos( $stripe_price_id, 'mock_' ) === false
     2557                        && 'free' !== $stripe_price_id;
     2558
     2559                    $line_item = $use_existing_price
     2560                        ? array( 'price' => $stripe_price_id, 'quantity' => 1 )
     2561                        : array(
     2562                            'quantity'   => 1,
     2563                            'price_data' => array(
     2564                                'currency'     => strtolower( get_option( 'subscription_currency', 'usd' ) ),
     2565                                'product_data' => array( 'name' => $plan_name ),
     2566                                'unit_amount'  => (int) ( $price * 100 ),
     2567                                'recurring'    => array(
     2568                                    'interval'       => $interval,
     2569                                    'interval_count' => $interval_count,
     2570                                ),
     2571                            ),
     2572                        );
     2573
     2574                    $session = \Stripe\Checkout\Session::create( array(
     2575                        'payment_method_types' => array( 'card' ),
     2576                        'mode'                 => 'subscription',
     2577                        'line_items'           => array( $line_item ),
     2578                        'customer_email'       => $subscription->subscriber_email,
     2579                        'success_url'          => $success_url,
     2580                        'cancel_url'           => $cancel_url,
     2581                        'metadata'             => array(
     2582                            'interest_subscription_id' => $subscription_id,
     2583                            'subscriber_name'          => $subscription->subscriber_name,
     2584                        ),
     2585                    ) );
     2586
     2587                    // Redirect to Stripe-hosted checkout page.
     2588                    wp_redirect( $session->url );
     2589                    exit;
     2590
     2591                } catch ( \Exception $e ) {
     2592                    // Fall through to the info page with error notice.
     2593                    $stripe_error = $e->getMessage();
     2594                }
     2595            }
     2596        }
     2597
     2598        // ── Fallback: show info page (Stripe not configured) ──────────────
     2599        $category_names = array();
     2600        $tag_names      = array();
     2601        foreach ( (array) $subscription->category_ids as $cat_id ) {
     2602            $term = get_term( $cat_id, 'category' );
     2603            if ( $term && ! is_wp_error( $term ) ) {
     2604                $category_names[] = $term->name;
     2605            }
     2606        }
     2607        foreach ( (array) $subscription->tag_ids as $tag_id ) {
     2608            $term = get_term( $tag_id, 'post_tag' );
     2609            if ( $term && ! is_wp_error( $term ) ) {
     2610                $tag_names[] = $term->name;
     2611            }
     2612        }
     2613
     2614        $cancelled     = isset( $_GET['cancelled'] );
     2615        $stripe_setup  = admin_url( 'admin.php?page=subscription-settings&tab=payment' );
     2616        $price_display = '$' . number_format( $price, 2 ) . ' / ' . $interval_count . ' ' . $interval . ( $interval_count > 1 ? 's' : '' );
     2617
     2618        status_header( 200 );
     2619        nocache_headers();
     2620        ?>
     2621        <!DOCTYPE html>
     2622        <html <?php language_attributes(); ?>>
     2623        <head>
     2624            <meta charset="<?php bloginfo( 'charset' ); ?>">
     2625            <meta name="viewport" content="width=device-width, initial-scale=1">
     2626            <title><?php esc_html_e( 'Complete Your Subscription', 'subscription-system' ); ?></title>
     2627            <?php wp_head(); ?>
     2628            <style>
     2629                *{box-sizing:border-box}
     2630                body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;margin:0;padding:20px;background:#f3f4f6;color:#111827}
     2631                .cc{max-width:580px;margin:40px auto;background:#fff;border-radius:14px;box-shadow:0 4px 24px rgba(0,0,0,.09);overflow:hidden}
     2632                .ch{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;padding:32px;text-align:center}
     2633                .ch h1{margin:0 0 6px;font-size:22px}
     2634                .ch p{margin:0;opacity:.9;font-size:15px}
     2635                .cb{padding:28px 32px}
     2636                .row{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:20px}
     2637                .pill{background:#eef2ff;color:#4f46e5;border-radius:20px;padding:5px 14px;font-size:13px;font-weight:500}
     2638                .notice{padding:16px;border-radius:8px;font-size:14px;margin-bottom:18px;line-height:1.5}
     2639                .notice.info{background:#eef2ff;color:#3730a3;border-left:4px solid #667eea}
     2640                .notice.warn{background:#fff7ed;color:#9a3412;border-left:4px solid #f97316}
     2641                .notice.error{background:#fef2f2;color:#991b1b;border-left:4px solid #ef4444}
     2642                .btn{display:inline-block;padding:12px 28px;border-radius:8px;font-size:15px;font-weight:600;text-decoration:none;margin-top:6px}
     2643                .btn-primary{background:#667eea;color:#fff}
     2644                .btn-ghost{color:#667eea;border:1px solid #c7d2fe}
     2645                .label{font-size:12px;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px}
     2646            </style>
     2647        </head>
     2648        <body>
     2649        <div class="cc">
     2650            <div class="ch">
     2651                <h1><?php echo esc_html( $plan_name ); ?></h1>
     2652                <p><?php echo esc_html( $price_display ); ?></p>
     2653            </div>
     2654            <div class="cb">
     2655                <p><strong><?php esc_html_e( 'Subscriber:', 'subscription-system' ); ?></strong>
     2656                   <?php echo esc_html( $subscription->subscriber_name ); ?>
     2657                   &lt;<?php echo esc_html( $subscription->subscriber_email ); ?>&gt;</p>
     2658
     2659                <?php if ( ! empty( $category_names ) || ! empty( $tag_names ) ) : ?>
     2660                <p class="label"><?php esc_html_e( 'Subscribing to', 'subscription-system' ); ?></p>
     2661                <div class="row">
     2662                    <?php foreach ( $category_names as $n ) : ?>
     2663                        <span class="pill">📁 <?php echo esc_html( $n ); ?></span>
     2664                    <?php endforeach; ?>
     2665                    <?php foreach ( $tag_names as $n ) : ?>
     2666                        <span class="pill">🏷️ <?php echo esc_html( $n ); ?></span>
     2667                    <?php endforeach; ?>
     2668                </div>
     2669                <?php endif; ?>
     2670
     2671                <?php if ( $cancelled ) : ?>
     2672                <div class="notice warn">
     2673                    <?php esc_html_e( 'Payment was cancelled. Your subscription has not been activated. You can try again below.', 'subscription-system' ); ?>
     2674                </div>
     2675                <?php endif; ?>
     2676
     2677                <?php if ( ! empty( $stripe_error ) ) : ?>
     2678                <div class="notice error">
     2679                    <strong><?php esc_html_e( 'Stripe error:', 'subscription-system' ); ?></strong>
     2680                    <?php echo esc_html( $stripe_error ); ?>
     2681                </div>
     2682                <?php endif; ?>
     2683
     2684                <?php if ( ! $stripe_enabled || empty( $api_key ) ) : ?>
     2685                <div class="notice info">
     2686                    <strong><?php esc_html_e( 'Action required:', 'subscription-system' ); ?></strong>
     2687                    <?php esc_html_e( 'Stripe is not configured yet. To accept payments, go to your admin panel and add your Stripe API keys.', 'subscription-system' ); ?>
     2688                    <?php if ( current_user_can( 'manage_options' ) ) : ?>
     2689                        <br><br>
     2690                        <a class="btn btn-primary" href="<?php echo esc_url( $stripe_setup ); ?>">
     2691                            <?php esc_html_e( '⚙ Configure Stripe →', 'subscription-system' ); ?>
     2692                        </a>
     2693                    <?php endif; ?>
     2694                </div>
     2695                <?php else : ?>
     2696                <div class="notice info">
     2697                    <?php esc_html_e( 'Your subscription preferences are saved. There was a problem launching Stripe. Please contact the site administrator.', 'subscription-system' ); ?>
     2698                </div>
     2699                <?php endif; ?>
     2700
     2701                <p style="margin-top:24px">
     2702                    <a class="btn btn-ghost" href="<?php echo esc_url( home_url( '/' ) ); ?>">
     2703                        &larr; <?php esc_html_e( 'Back to Home', 'subscription-system' ); ?>
     2704                    </a>
     2705                </p>
     2706            </div>
     2707        </div>
     2708        <?php wp_footer(); ?>
     2709        </body>
     2710        </html>
     2711        <?php
     2712        exit;
     2713    }
     2714
     2715    /**
     2716     * Handle Stripe success redirect after interest subscription payment.
     2717     */
     2718    public function handle_interest_checkout_success() {
     2719        if ( ! isset( $_GET['action'] ) || 'interest_checkout_success' !== $_GET['action'] ) {
     2720            return;
     2721        }
     2722
     2723        $subscription_id = isset( $_GET['interest_subscription_id'] ) ? absint( $_GET['interest_subscription_id'] ) : 0;
     2724        $session_id      = isset( $_GET['session_id'] ) ? sanitize_text_field( wp_unslash( $_GET['session_id'] ) ) : '';
     2725
     2726        if ( ! $subscription_id ) {
     2727            wp_die( esc_html__( 'Invalid subscription.', 'subscription-system' ) );
     2728        }
     2729
     2730        $interest     = new Subs_Sys_Interest_Subscription();
     2731        $subscription = $interest->get_subscription( $subscription_id );
     2732
     2733        if ( ! $subscription ) {
     2734            wp_die( esc_html__( 'Subscription not found.', 'subscription-system' ) );
     2735        }
     2736
     2737        // Verify with Stripe if session ID is provided.
     2738        $verified = false;
     2739        if ( ! empty( $session_id ) ) {
     2740            $test_mode = get_option( 'subscription_stripe_test_mode', 1 );
     2741            $api_key   = $test_mode
     2742                ? get_option( 'subscription_stripe_test_secret_key', '' )
     2743                : get_option( 'subscription_stripe_live_secret_key', '' );
     2744            if ( empty( $api_key ) ) {
     2745                $api_key = get_option( 'subscription_stripe_secret_key', '' );
     2746            }
     2747
     2748            if ( ! empty( $api_key ) && class_exists( '\Stripe\Stripe' ) ) {
     2749                try {
     2750                    \Stripe\Stripe::setApiKey( $api_key );
     2751                    $session  = \Stripe\Checkout\Session::retrieve( $session_id );
     2752                    $verified = in_array( $session->payment_status, array( 'paid', 'no_payment_required' ), true );
     2753                } catch ( \Exception $e ) {
     2754                    // Verification failed — still activate (webhook will handle edge cases).
     2755                    $verified = false;
     2756                }
     2757            }
     2758        }
     2759
     2760        // Activate the subscription (Stripe verified, or session_id was absent — trust the redirect).
     2761        if ( $verified || empty( $session_id ) ) {
     2762            $interest->activate_paid_subscription( $subscription_id, $session_id );
     2763        }
     2764
     2765        // Build a clean success page.
     2766        $category_names = array();
     2767        $tag_names      = array();
     2768        foreach ( (array) $subscription->category_ids as $cat_id ) {
     2769            $term = get_term( $cat_id, 'category' );
     2770            if ( $term && ! is_wp_error( $term ) ) { $category_names[] = $term->name; }
     2771        }
     2772        foreach ( (array) $subscription->tag_ids as $tag_id ) {
     2773            $term = get_term( $tag_id, 'post_tag' );
     2774            if ( $term && ! is_wp_error( $term ) ) { $tag_names[] = $term->name; }
     2775        }
     2776
     2777        status_header( 200 );
     2778        nocache_headers();
     2779        ?>
     2780        <!DOCTYPE html>
     2781        <html <?php language_attributes(); ?>>
     2782        <head>
     2783            <meta charset="<?php bloginfo( 'charset' ); ?>">
     2784            <meta name="viewport" content="width=device-width, initial-scale=1">
     2785            <title><?php esc_html_e( 'Subscription Activated', 'subscription-system' ); ?></title>
     2786            <?php wp_head(); ?>
     2787            <style>
     2788                *{box-sizing:border-box}
     2789                body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;margin:0;padding:20px;background:#f3f4f6;color:#111827}
     2790                .cc{max-width:520px;margin:60px auto;background:#fff;border-radius:14px;box-shadow:0 4px 24px rgba(0,0,0,.09);padding:40px;text-align:center}
     2791                .icon{font-size:56px;margin-bottom:12px}
     2792                h1{margin:0 0 10px;font-size:24px;color:#111827}
     2793                p{color:#6b7280;font-size:15px;margin:0 0 20px;line-height:1.6}
     2794                .row{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin:16px 0 28px}
     2795                .pill{background:#d1fae5;color:#065f46;border-radius:20px;padding:6px 16px;font-size:13px;font-weight:500}
     2796                .btn{display:inline-block;padding:13px 30px;border-radius:8px;font-size:15px;font-weight:600;text-decoration:none;background:#667eea;color:#fff}
     2797            </style>
     2798        </head>
     2799        <body>
     2800        <div class="cc">
     2801            <div class="icon">🎉</div>
     2802            <h1><?php esc_html_e( 'You\'re subscribed!', 'subscription-system' ); ?></h1>
     2803            <p><?php esc_html_e( 'Your subscription is now active. You\'ll receive email notifications when new content is published in:', 'subscription-system' ); ?></p>
     2804            <div class="row">
     2805                <?php foreach ( $category_names as $n ) : ?>
     2806                    <span class="pill">📁 <?php echo esc_html( $n ); ?></span>
     2807                <?php endforeach; ?>
     2808                <?php foreach ( $tag_names as $n ) : ?>
     2809                    <span class="pill">🏷️ <?php echo esc_html( $n ); ?></span>
     2810                <?php endforeach; ?>
     2811            </div>
     2812            <a class="btn" href="<?php echo esc_url( home_url( '/' ) ); ?>">
     2813                <?php esc_html_e( 'Back to Home', 'subscription-system' ); ?>
     2814            </a>
     2815        </div>
     2816        <?php wp_footer(); ?>
     2817        </body>
     2818        </html>
     2819        <?php
     2820        exit;
     2821    }
     2822
     2823    /**
    24562824     * Handle content subscription success redirect
    24572825     */
    … …  
    27323100        return $applicable_plans;
    27333101    }
     3102
     3103    // ──────────────────────────────────────────────────────────────────────────
     3104    // Subscribe Button & Modal Trigger helpers
     3105    // ──────────────────────────────────────────────────────────────────────────
     3106
     3107    /**
     3108     * Determine whether the modal triggers should render on the current page,
     3109     * based on the admin Display Settings.
     3110     *
     3111     * @return bool
     3112     */
     3113    private function should_render_modal_triggers() {
     3114        // All posts (any singular post type except page).
     3115        if ( get_option( 'subs_modal_display_all_posts', 0 ) && is_singular() && ! is_page() ) {
     3116            return true;
     3117        }
     3118
     3119        // All pages.
     3120        if ( get_option( 'subs_modal_display_all_pages', 0 ) && is_page() ) {
     3121            return true;
     3122        }
     3123
     3124        // Single posts only.
     3125        if ( get_option( 'subs_modal_display_single_posts', 0 ) && is_single() ) {
     3126            return true;
     3127        }
     3128
     3129        // Single pages only.
     3130        if ( get_option( 'subs_modal_display_single_pages', 0 ) && is_page() && is_singular() ) {
     3131            return true;
     3132        }
     3133
     3134        // Custom post types.
     3135        $selected_cpts = (array) get_option( 'subs_modal_display_post_types', array() );
     3136        if ( ! empty( $selected_cpts ) && is_singular( $selected_cpts ) ) {
     3137            return true;
     3138        }
     3139
     3140        // Specific pages.
     3141        $specific_pages = array_map( 'absint', (array) get_option( 'subs_modal_display_specific_pages', array() ) );
     3142        if ( ! empty( $specific_pages ) && is_page( $specific_pages ) ) {
     3143            return true;
     3144        }
     3145
     3146        return false;
     3147    }
     3148
     3149    /**
     3150     * Collect context data (category/tag IDs) for the current post.
     3151     *
     3152     * @return array{category_ids: int[], tag_ids: int[]}
     3153     */
     3154    private function get_current_post_context() {
     3155        $context = array( 'category_ids' => array(), 'tag_ids' => array() );
     3156
     3157        if ( ! is_singular() ) {
     3158            return $context;
     3159        }
     3160
     3161        $post_id = get_the_ID();
     3162        if ( ! $post_id ) {
     3163            return $context;
     3164        }
     3165
     3166        if ( get_option( 'subs_modal_context_categories', 1 ) ) {
     3167            $cats = wp_get_post_categories( $post_id, array( 'fields' => 'ids' ) );
     3168            $context['category_ids'] = array_map( 'intval', is_array( $cats ) ? $cats : array() );
     3169        }
     3170
     3171        if ( get_option( 'subs_modal_context_tags', 1 ) ) {
     3172            $tags = wp_get_post_tags( $post_id, array( 'fields' => 'ids' ) );
     3173            $context['tag_ids'] = array_map( 'intval', is_array( $tags ) ? $tags : array() );
     3174        }
     3175
     3176        return $context;
     3177    }
     3178
     3179    /**
     3180     * Output the floating button and/or auto-open modal in wp_footer,
     3181     * respecting the configured display settings.
     3182     */
     3183    public function render_interest_modal_triggers() {
     3184        $show_floating = (bool) get_option( 'subs_modal_floating_btn', 0 );
     3185        $auto_open     = (bool) get_option( 'subs_modal_auto_open', 0 );
     3186
     3187        if ( ! $show_floating && ! $auto_open ) {
     3188            return;
     3189        }
     3190
     3191        if ( ! $this->should_render_modal_triggers() ) {
     3192            return;
     3193        }
     3194
     3195        // Enqueue the modal assets (shared helper, safe to call multiple times).
     3196        subs_sys_enqueue_interest_modal_assets( $this->plugin_name, $this->version );
     3197
     3198        $modal_id = 'ss-interest-modal-footer';
     3199        $btn_text = get_option( 'subs_modal_floating_btn_text', __( 'Subscribe', 'subscription-system' ) );
     3200        $btn_pos  = sanitize_key( get_option( 'subs_modal_floating_btn_position', 'bottom-right' ) );
     3201        $context  = $this->get_current_post_context();
     3202
     3203        // Only show the categories/tags actually assigned to the current post.
     3204        // Falls back to all available on non-singular pages (archives, home, etc.).
     3205        if ( ! empty( $context['category_ids'] ) ) {
     3206            $categories = Subs_Sys_Interest_Subscription::get_categories_by_ids( $context['category_ids'] );
     3207        } else {
     3208            $categories = Subs_Sys_Interest_Subscription::get_available_categories();
     3209        }
     3210
     3211        if ( ! empty( $context['tag_ids'] ) ) {
     3212            $tags = Subs_Sys_Interest_Subscription::get_tags_by_ids( $context['tag_ids'] );
     3213        } else {
     3214            $tags = Subs_Sys_Interest_Subscription::get_available_tags();
     3215        }
     3216
     3217        $plans = Subs_Sys_Interest_Subscription::get_content_modal_plans( $context['category_ids'], $context['tag_ids'] );
     3218
     3219        // Output floating button.
     3220        if ( $show_floating ) {
     3221            printf(
     3222                '<button type="button" class="ss-interest-subscribe-btn ss-interest-floating-btn ss-floating--%s" data-ss-open-interest-modal="#%s" aria-label="%s">%s</button>',
     3223                esc_attr( $btn_pos ),
     3224                esc_attr( $modal_id ),
     3225                esc_attr( $btn_text ),
     3226                esc_html( $btn_text )
     3227            );
     3228        }
     3229
     3230        // Output the modal (with context data as data attributes for JS to pick up).
     3231        $args = array(
     3232            'modal_id'           => $modal_id,
     3233            'categories'         => $categories,
     3234            'tags'               => $tags,
     3235            'plans'              => $plans,
     3236            'plan_source'        => 'content',
     3237            'context_categories' => $context['category_ids'],
     3238            'context_tags'       => $context['tag_ids'],
     3239        );
     3240
     3241        // Pass auto-open data attribute so JS knows to open this specific modal.
     3242        $data_auto_open = $auto_open ? ' data-ss-auto-open="1"' : '';
     3243        echo '<div' . $data_auto_open . '>';
     3244        include SUBS_SYS_PLUGIN_DIR . 'public/partials/interest-subscribe-modal.php';
     3245        echo '</div>';
     3246    }
    27343247}
  • subscription-system/trunk/subscription-system.php

    r3587496 r3598150  
    1717 * Plugin URI:        https://mediahubster.in/subscription-system
    1818 * Description:       A comprehensive subscription management system for WordPress.
    19  * Version:           1.0.17
     19 * Version:           1.0.18
    2020 * Author:            Joe Youngblood
    2121 * Author URI:        https://mediahubster.in
    … …  
    9797
    9898// Define plugin constants
    99 define('SUBS_SYS_VERSION', '1.0.17');
     99define('SUBS_SYS_VERSION', '1.0.18');
    100100define('SUBS_SYS_PLUGIN_DIR', plugin_dir_path(__FILE__));
    101101define('SUBS_SYS_PLUGIN_URL', plugin_dir_url(__FILE__));
    102102define('SUBS_SYS_PLUGIN_BASENAME', plugin_basename(__FILE__));
     103
     104/**
     105 * Shared helper: enqueue interest-subscribe modal assets + localize script vars.
     106 * Called by both the shortcode handler and the floating-button/auto-open renderer
     107 * so the JS object is always fully populated regardless of which trigger is used.
     108 *
     109 * @param string $plugin_name Plugin slug.
     110 * @param string $version     Plugin version string.
     111 */
     112function subs_sys_enqueue_interest_modal_assets( $plugin_name, $version ) {
     113    static $done = false;
     114    if ( $done ) {
     115        return;
     116    }
     117    $done = true;
     118
     119    wp_enqueue_style(
     120        $plugin_name . '-interest-subscribe',
     121        SUBS_SYS_PLUGIN_URL . 'public/css/interest-subscribe.css',
     122        array(),
     123        $version
     124    );
     125
     126    wp_enqueue_script(
     127        $plugin_name . '-interest-subscribe',
     128        SUBS_SYS_PLUGIN_URL . 'public/js/interest-subscribe.js',
     129        array( 'jquery' ),
     130        $version,
     131        true
     132    );
     133
     134    $current_user = wp_get_current_user();
     135    $is_logged_in = $current_user->exists();
     136
     137    wp_localize_script(
     138        $plugin_name . '-interest-subscribe',
     139        'ssInterestVars',
     140        array(
     141            'ajax_url'              => admin_url( 'admin-ajax.php' ),
     142            'nonce'                 => wp_create_nonce( 'interest_subscription_nonce' ),
     143            'auto_open'             => (int) get_option( 'subs_modal_auto_open', 0 ),
     144            'auto_open_delay'       => absint( get_option( 'subs_modal_auto_open_delay', 3 ) ),
     145            'hide_fields_logged_in' => (int) get_option( 'subs_modal_hide_fields_logged_in', 1 ),
     146            'is_logged_in'          => $is_logged_in ? 1 : 0,
     147            'i18n'                  => array(
     148                'processing'  => __( 'Processing...', 'subscription-system' ),
     149                'submit'      => __( 'Subscribe', 'subscription-system' ),
     150                'error'       => __( 'Something went wrong. Please try again.', 'subscription-system' ),
     151                'select_plan' => __( 'Please select a subscription plan.', 'subscription-system' ),
     152            ),
     153        )
     154    );
     155}
    103156
    104157// Load Composer autoloader if it exists
    … …  
    312365        KEY status (status)
    313366    ) $charset_collate;";
     367
     368    // Create interest subscriptions table
     369    $interest_table = $wpdb->prefix . 'subscription_interest_subscriptions';
     370    $sql_interest = "CREATE TABLE IF NOT EXISTS $interest_table (
     371        id mediumint(9) NOT NULL AUTO_INCREMENT,
     372        user_id bigint(20) DEFAULT NULL,
     373        plan_id mediumint(9) DEFAULT NULL,
     374        subscriber_name varchar(100) NOT NULL,
     375        subscriber_email varchar(100) NOT NULL,
     376        category_ids text NOT NULL,
     377        tag_ids text NOT NULL,
     378        plan_type varchar(20) NOT NULL DEFAULT 'free',
     379        status varchar(20) NOT NULL DEFAULT 'active',
     380        payment_id varchar(100) DEFAULT NULL,
     381        created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
     382        updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
     383        PRIMARY KEY  (id),
     384        KEY user_id (user_id),
     385        KEY plan_id (plan_id),
     386        KEY subscriber_email (subscriber_email),
     387        KEY status (status),
     388        KEY plan_type (plan_type)
     389    ) $charset_collate;";
    314390   
    315391    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    … …  
    324400    dbDelta($sql_content_subscriptions);
    325401    dbDelta($sql_content_notifications);
     402    dbDelta($sql_interest);
    326403   
    327404    // Add version to options
    … …  
    364441    $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}subscription_content_subscriptions");
    365442    $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}subscription_content_notifications");
     443    $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}subscription_interest_subscriptions");
    366444
    367445    // Delete options
Note: See TracChangeset for help on using the changeset viewer.