Changeset 3598150
- Timestamp:
- 07/06/2026 05:21:45 PM (4 months ago)
- Location:
- subscription-system
- Files:
-
- 12 added
- 21 edited
- 5 copied
-
tags/1.0.18 (copied) (copied from subscription-system/trunk )
-
tags/1.0.18/README.txt (copied) (copied from subscription-system/trunk/README.txt ) (3 diffs)
-
tags/1.0.18/admin/class-subs-sys-admin.php (modified) (11 diffs)
-
tags/1.0.18/admin/partials/content-plans-display.php (modified) (6 diffs)
-
tags/1.0.18/admin/partials/help-support-display.php (modified) (5 diffs)
-
tags/1.0.18/admin/partials/interest-subscribers-display.php (added)
-
tags/1.0.18/admin/partials/plans-display.php (modified) (12 diffs)
-
tags/1.0.18/admin/partials/settings-display.php (modified) (2 diffs)
-
tags/1.0.18/admin/partials/subscribe-modal-settings-display.php (added)
-
tags/1.0.18/includes/class-subs-sys-ajax.php (modified) (8 diffs)
-
tags/1.0.18/includes/class-subs-sys-db-updater.php (modified) (5 diffs)
-
tags/1.0.18/includes/class-subs-sys-interest-subscription.php (added)
-
tags/1.0.18/includes/class-subs-sys-shortcodes.php (modified) (2 diffs)
-
tags/1.0.18/includes/class-subs-sys.php (copied) (copied from subscription-system/trunk/includes/class-subs-sys.php ) (3 diffs)
-
tags/1.0.18/public/class-subs-sys-public.php (modified) (3 diffs)
-
tags/1.0.18/public/css/interest-subscribe.css (added)
-
tags/1.0.18/public/js/interest-subscribe.js (added)
-
tags/1.0.18/public/partials/interest-subscribe-modal.php (added)
-
tags/1.0.18/subscription-system.php (copied) (copied from subscription-system/trunk/subscription-system.php ) (5 diffs)
-
tags/1.0.18/vendor/composer/autoload_psr4.php (copied) (copied from subscription-system/trunk/vendor/composer/autoload_psr4.php )
-
trunk/README.txt (modified) (3 diffs)
-
trunk/admin/class-subs-sys-admin.php (modified) (11 diffs)
-
trunk/admin/partials/content-plans-display.php (modified) (6 diffs)
-
trunk/admin/partials/help-support-display.php (modified) (5 diffs)
-
trunk/admin/partials/interest-subscribers-display.php (added)
-
trunk/admin/partials/plans-display.php (modified) (12 diffs)
-
trunk/admin/partials/settings-display.php (modified) (2 diffs)
-
trunk/admin/partials/subscribe-modal-settings-display.php (added)
-
trunk/includes/class-subs-sys-ajax.php (modified) (8 diffs)
-
trunk/includes/class-subs-sys-db-updater.php (modified) (5 diffs)
-
trunk/includes/class-subs-sys-interest-subscription.php (added)
-
trunk/includes/class-subs-sys-shortcodes.php (modified) (2 diffs)
-
trunk/includes/class-subs-sys.php (modified) (3 diffs)
-
trunk/public/class-subs-sys-public.php (modified) (3 diffs)
-
trunk/public/css/interest-subscribe.css (added)
-
trunk/public/js/interest-subscribe.js (added)
-
trunk/public/partials/interest-subscribe-modal.php (added)
-
trunk/subscription-system.php (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
subscription-system/tags/1.0.18/README.txt
r3587496 r3598150 6 6 Tested up to: 7.0 7 7 Requires PHP: 8.0 8 Stable tag: 1.0.1 78 Stable tag: 1.0.18 9 9 License: GPLv2 or later 10 10 License URI: http://www.gnu.org/licenses/gpl-2.0.html … … 85 85 86 86 == 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 87 129 88 130 = 1.0.17 = … … 215 257 == Upgrade Notice == 216 258 259 = 1.0.18 = 260 Major 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 217 262 = 1.0.17 = 218 263 Recommended 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 97 97 // 6. Content Subscribers (content-specific subscribers) 98 98 $this->add_content_subscribers_submenu(); 99 100 // 6b. Interest Subscribers (category/tag popup subscriptions) 101 $this->add_interest_subscribers_submenu(); 99 102 100 103 // 7. Channels … … 991 994 992 995 /** 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 /** 993 1017 * Add Email Logs submenu to the Subscriptions menu 994 1018 */ … … 1087 1111 ) 1088 1112 ); 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 ) ); 1089 1139 1090 1140 // Pause subscription setting … … 1366 1416 public function register_ajax_handlers() { 1367 1417 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')); 1368 1419 add_action('wp_ajax_test_stripe_connection', array($this, 'ajax_test_stripe_connection')); 1369 1420 add_action('wp_ajax_install_stripe_sdk', array($this, 'ajax_install_stripe_sdk')); … … 1391 1442 1392 1443 // 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 1393 1452 $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']) : '', 1395 1454 '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, 1398 1457 '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 ), 1400 1460 ); 1401 1461 … … 1440 1500 } 1441 1501 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 1442 1510 $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']) : '', 1444 1512 '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, 1447 1515 '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'), 1450 1519 ); 1451 1520 … … 2241 2310 // Prepare the data 2242 2311 $data = array( 2243 'plan_name' => sanitize_text_field($plan_data['plan_name']),2312 'plan_name' => sanitize_text_field($plan_data['plan_name']), 2244 2313 '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, 2247 2316 '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'), 2251 2321 ); 2252 2322 … … 2257 2327 $table_name, 2258 2328 $data, 2259 array('%s', '%s', '%f', '%d', '%d', '%s', '%s', '%s' )2329 array('%s', '%s', '%f', '%d', '%d', '%s', '%s', '%s', '%s') 2260 2330 ); 2261 2331 … … 2614 2684 2615 2685 /** 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 /** 2616 2848 * AJAX handler for checking the Stripe API key 2617 2849 */ … … 3427 3659 // Handle add/edit content plan 3428 3660 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 3429 3712 $plan_data = array( 3430 3713 'plan_name' => sanitize_text_field($_POST['plan_name']), … … 3433 3716 'duration' => intval($_POST['duration']), 3434 3717 '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, 3437 3720 'notification_frequency' => sanitize_text_field($_POST['notification_frequency']), 3438 3721 'status' => sanitize_text_field($_POST['status']) 3439 3722 ); 3440 3441 $plan_id = isset($_POST['plan_id']) ? intval($_POST['plan_id']) : 0;3442 3723 3443 3724 if ($plan_id > 0) { -
subscription-system/tags/1.0.18/admin/partials/content-plans-display.php
r3375846 r3598150 223 223 data.append('content_type', contentType); 224 224 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); ?>'); 225 226 <?php if ($editing_plan): ?> 226 227 data.append('selected_ids', '<?php echo wp_json_encode($editing_plan->content_ids); ?>'); … … 278 279 <th scope="col"><?php esc_html_e('Duration', 'subscription-system'); ?></th> 279 280 <th scope="col"><?php esc_html_e('Status', 'subscription-system'); ?></th> 281 <th scope="col"><?php esc_html_e('Stripe', 'subscription-system'); ?></th> 280 282 <th scope="col"><?php esc_html_e('Subscribers', 'subscription-system'); ?></th> 281 283 <th scope="col"><?php esc_html_e('Actions', 'subscription-system'); ?></th> … … 285 287 <?php foreach ($content_plans as $plan): ?> 286 288 <?php 287 // Get subscriber count288 289 global $wpdb; 289 290 $subscriber_count = $wpdb->get_var($wpdb->prepare( … … 291 292 $plan->id 292 293 )); 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'); 293 299 ?> 294 300 <tr> … … 313 319 </span> 314 320 </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> 315 345 <td><?php echo esc_html($subscriber_count); ?></td> 316 346 <td> … … 379 409 background: #fafafa; 380 410 } 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 } 381 478 </style> 479 480 <script> 481 jQuery(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 149 149 <h3>📋 Table of Contents</h3> 150 150 <ul> 151 <li><a href="#integration-steps"><strong>⚡ Step-by-Step Integration Guide</strong></a></li> 151 152 <li><a href="#getting-started">Getting Started</a></li> 152 153 <li><a href="#subscription-management">Subscription Management & Renewal Flow</a></li> 153 154 <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> 154 156 <li><a href="#email-notifications">Email Notifications</a></li> 155 157 <li><a href="#shortcodes">Available Shortcodes</a></li> … … 159 161 <li><a href="#troubleshooting">Troubleshooting</a></li> 160 162 </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 & 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 & 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 161 358 </div> 162 359 … … 285 482 </div> 286 483 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="Join Now" button_class="my-custom-class"]')">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 & 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 287 718 <!-- Email Notifications --> 288 719 <div class="help-section" id="email-notifications"> … … 388 819 </div> 389 820 <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="Follow This Topic" button_class="my-btn"]')">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> 390 837 </div> 391 838 … … 460 907 <li><strong>Page Settings:</strong> Assign pages for different functions</li> 461 908 <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> 462 910 </ul> 463 911 </div> -
subscription-system/tags/1.0.18/admin/partials/plans-display.php
r3438196 r3598150 94 94 <tr> 95 95 <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> 97 99 </tr> 98 100 <tr> … … 109 111 </td> 110 112 </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> 111 123 </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 112 145 <p class="submit"> 113 146 <input type="submit" name="add_plan" class="button button-primary" value="<?php echo esc_attr__('Save Plan', 'subscription-system'); ?>"> … … 178 211 <tr> 179 212 <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> 181 216 </tr> 182 217 <tr> … … 193 228 </td> 194 229 </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> 195 240 </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 196 262 <p class="submit"> 197 263 <input type="submit" name="edit_plan" class="button button-primary" value="<?php echo esc_attr__('Update Plan', 'subscription-system'); ?>"> … … 225 291 <tr> 226 292 <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> 228 294 <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> 232 299 <th scope="col" class="manage-column column-actions"><?php echo esc_html__('Actions', 'subscription-system'); ?></th> 233 300 </tr> … … 248 315 } 249 316 } 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'; 250 330 ?> 251 331 <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> 253 338 <td class="column-description"> 254 339 <?php 255 340 $desc = $plan->description; 256 // Check if description is JSON257 341 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>'; 271 348 } 272 } catch (Exception $e) { 349 echo '</ul>'; 350 } else { 273 351 echo esc_html($desc); 274 352 } … … 278 356 ?> 279 357 </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> 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> 283 387 <td> 284 388 <?php 285 389 $stripe_product_id = $plan->stripe_product_id; 286 390 $stripe_price_id = $plan->stripe_price_id; 287 288 // Check if the plan is connected to Stripe289 391 $is_connected = !empty($stripe_product_id) && !empty($stripe_price_id); 290 392 $is_mock = $is_connected && (strpos($stripe_product_id, 'mock_') === 0); 291 292 393 if ($is_connected) { 293 394 echo '<span class="stripe-status ' . ($is_mock ? 'mock-connected' : 'connected') . '">' . 294 395 '<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>'; 303 397 } else { 304 echo '<span class="stripe-status not-connected"><span class="dashicons dashicons-no"></span> No t Connected</span>';398 echo '<span class="stripe-status not-connected"><span class="dashicons dashicons-no"></span> None</span>'; 305 399 } 306 400 ?> 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> 309 402 </td> 310 403 <td class="column-actions"> … … 315 408 data-price="<?php echo esc_attr($plan->price); ?>" 316 409 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); ?>"> 318 414 <?php echo esc_html__('Edit', 'subscription-system'); ?> 319 415 </button> … … 354 450 $('#add-plan-form').hide(); 355 451 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; 362 461 363 462 $('#edit_plan_id').val(id); … … 366 465 $('#edit_duration').val(duration); 367 466 $('#edit_category_id').val(category); 467 $('#edit_status').val(status); 468 $('#edit_max_categories').val(maxCats); 469 $('#edit_max_tags').val(maxTags); 368 470 369 471 // Clear existing feature inputs … … 372 474 // Try to parse description as JSON (for features) 373 475 try { 374 // First try to handle escaped JSON (with backslashes)375 476 var cleanDescription = description.replace(/\\\\/g, '\\').replace(/\\"/g, '"'); 376 477 var features = JSON.parse(cleanDescription); 377 378 478 if (Array.isArray(features)) { 379 // Add feature inputs for each feature380 479 features.forEach(function(feature) { 381 480 addEditFeature(feature); 382 481 }); 383 482 } else { 384 // If not an array, add as a single feature385 483 addEditFeature(cleanDescription); 386 484 } 387 485 } catch (e) { 388 // If not valid JSON, add as a single feature389 486 if (description) { 390 487 addEditFeature(description); … … 621 718 }); 622 719 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 623 738 // Fix for the category column display 624 739 $('.sync-with-stripe').on('click', function(e) { -
subscription-system/tags/1.0.18/admin/partials/settings-display.php
r3311076 r3598150 17 17 // Define tabs 18 18 $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'), 23 24 ); 24 25 … … 78 79 case 'pages': 79 80 include 'pages-settings-display.php'; 81 break; 82 case 'subscribe_modal': 83 include 'subscribe-modal-settings-display.php'; 80 84 break; 81 85 } -
subscription-system/tags/1.0.18/includes/class-subs-sys-ajax.php
r3435450 r3598150 71 71 add_action( 'wp_ajax_unsubscribe_from_content_plan', array( $this, 'unsubscribe_from_content_plan' ) ); 72 72 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' ) ); 73 75 } 74 76 … … 1629 1631 */ 1630 1632 public function get_content_options() { 1631 // Debug logging1632 error_log('AJAX get_content_options called');1633 error_log('POST data: ' . print_r($_POST, true));1634 1635 // Verify nonce1636 1633 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'get_content_options' ) ) { 1637 error_log('Nonce verification failed');1638 1634 wp_send_json_error( array( 'message' => __( 'Security check failed.', 'subscription-system' ) ) ); 1639 1635 return; 1640 1636 } 1641 1637 1642 // Check user capabilities1643 1638 if ( ! current_user_can( 'manage_options' ) ) { 1644 error_log('User does not have manage_options capability');1645 1639 wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'subscription-system' ) ) ); 1646 1640 return; 1647 1641 } 1648 1642 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 ) ) { 1654 1648 $selected_ids = array(); 1655 1649 } 1656 1650 1657 error_log('Content type: ' . $content_type);1658 error_log('Selected IDs: ' . print_r($selected_ids, true));1659 1660 1651 if ( empty( $content_type ) ) { 1661 error_log('Content type is empty');1662 1652 wp_send_json_error( array( 'message' => __( 'Content type is required.', 'subscription-system' ) ) ); 1663 1653 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 } 1664 1679 } 1665 1680 … … 1668 1683 switch ( $content_type ) { 1669 1684 case 'category': 1670 $categories = get_categories( array( 'hide_empty' => false ) );1685 $categories = get_categories( array( 'hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC' ) ); 1671 1686 $html = '<div class="content-checkboxes">'; 1672 1687 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 } 1681 1708 } 1682 1709 $html .= '</div>'; … … 1684 1711 1685 1712 case 'tag': 1686 $tags = get_tags( array( 'hide_empty' => false ) );1713 $tags = get_tags( array( 'hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC' ) ); 1687 1714 $html = '<div class="content-checkboxes">'; 1688 1715 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 } 1697 1736 } 1698 1737 $html .= '</div>'; … … 1704 1743 'post_status' => 'publish', 1705 1744 'orderby' => 'title', 1706 'order' => 'ASC' 1745 'order' => 'ASC', 1707 1746 ) ); 1708 1747 $html = '<div class="content-checkboxes">'; 1709 1748 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' : ''; 1711 1750 $html .= sprintf( 1712 1751 '<label><input type="checkbox" name="content_ids[]" value="%d" %s> %s</label><br>', … … 1723 1762 $html = '<div class="content-checkboxes">'; 1724 1763 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' : ''; 1726 1765 $html .= sprintf( 1727 1766 '<label><input type="checkbox" name="content_ids[]" value="%s" %s> %s</label><br>', 1728 $post_type->name,1767 esc_attr( $post_type->name ), 1729 1768 $checked, 1730 1769 esc_html( $post_type->label ) … … 1739 1778 } 1740 1779 1741 error_log('Generated HTML length: ' . strlen($html));1742 error_log('Generated HTML: ' . substr($html, 0, 500) . '...');1743 1780 wp_send_json_success( $html ); 1744 1781 } … … 2009 2046 ) ); 2010 2047 } 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 31 31 * Current database version 32 32 */ 33 const DB_VERSION = '1. 0.9';33 const DB_VERSION = '1.1.1'; 34 34 35 35 /** … … 75 75 // Check if content tables exist 76 76 $content_tables_missing = ! self::content_tables_exist(); 77 78 $interest_table_missing = ! self::interest_table_exists(); 77 79 78 80 // 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 ) { 80 82 81 83 // Log what's triggering the update … … 88 90 if ( $content_tables_missing ) { 89 91 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' ); 90 95 } 91 96 … … 151 156 error_log( "Subs_Sys: Ensuring email logs table exists..." ); 152 157 $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(); 153 167 } 154 168 … … 310 324 311 325 /** 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 /** 312 391 * Check if email logs table exists 313 392 * -
subscription-system/tags/1.0.18/includes/class-subs-sys-shortcodes.php
r3375846 r3598150 56 56 add_shortcode('content_subscription_checkout', array($this, 'render_content_subscription_checkout')); 57 57 add_shortcode('content_subscription_success', array($this, 'render_content_subscription_success')); 58 add_shortcode('subscriptionsystem_subscribe', array($this, 'render_interest_subscribe_button')); 58 59 59 60 // Add AJAX handlers for password reset … … 2238 2239 return ob_get_clean(); 2239 2240 } 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 76 76 $this->version = SUBS_SYS_VERSION; 77 77 } else { 78 $this->version = '1.0.1 7';78 $this->version = '1.0.18'; 79 79 } 80 80 $this->plugin_name = 'subs-sys'; … … 176 176 */ 177 177 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'; 178 183 179 184 /** … … 277 282 // Initialize content subscription manager 278 283 $content_manager = new Subs_Sys_Content_Manager(); 284 285 // Initialize interest subscription manager 286 $interest_subscription = new Subs_Sys_Interest_Subscription(); 279 287 280 288 // Initialize database updater (safe updates) -
subscription-system/tags/1.0.18/public/class-subs-sys-public.php
r3356551 r3598150 52 52 add_action('wp_head', array($this, 'display_smart_subscription_bar')); 53 53 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')); 54 56 add_action('template_redirect', array($this, 'handle_content_subscription_success')); 55 57 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')); 56 61 } 57 62 … … 2454 2459 2455 2460 /** 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 <<?php echo esc_html( $subscription->subscriber_email ); ?>></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 ← <?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 /** 2456 2824 * Handle content subscription success redirect 2457 2825 */ … … 2732 3100 return $applicable_plans; 2733 3101 } 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 } 2734 3247 } -
subscription-system/tags/1.0.18/subscription-system.php
r3587496 r3598150 17 17 * Plugin URI: https://mediahubster.in/subscription-system 18 18 * Description: A comprehensive subscription management system for WordPress. 19 * Version: 1.0.1 719 * Version: 1.0.18 20 20 * Author: Joe Youngblood 21 21 * Author URI: https://mediahubster.in … … 97 97 98 98 // Define plugin constants 99 define('SUBS_SYS_VERSION', '1.0.1 7');99 define('SUBS_SYS_VERSION', '1.0.18'); 100 100 define('SUBS_SYS_PLUGIN_DIR', plugin_dir_path(__FILE__)); 101 101 define('SUBS_SYS_PLUGIN_URL', plugin_dir_url(__FILE__)); 102 102 define('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 */ 112 function 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 } 103 156 104 157 // Load Composer autoloader if it exists … … 312 365 KEY status (status) 313 366 ) $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;"; 314 390 315 391 require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); … … 324 400 dbDelta($sql_content_subscriptions); 325 401 dbDelta($sql_content_notifications); 402 dbDelta($sql_interest); 326 403 327 404 // Add version to options … … 364 441 $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}subscription_content_subscriptions"); 365 442 $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}subscription_content_notifications"); 443 $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}subscription_interest_subscriptions"); 366 444 367 445 // Delete options -
subscription-system/trunk/README.txt
r3587496 r3598150 6 6 Tested up to: 7.0 7 7 Requires PHP: 8.0 8 Stable tag: 1.0.1 78 Stable tag: 1.0.18 9 9 License: GPLv2 or later 10 10 License URI: http://www.gnu.org/licenses/gpl-2.0.html … … 85 85 86 86 == 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 87 129 88 130 = 1.0.17 = … … 215 257 == Upgrade Notice == 216 258 259 = 1.0.18 = 260 Major 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 217 262 = 1.0.17 = 218 263 Recommended 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 97 97 // 6. Content Subscribers (content-specific subscribers) 98 98 $this->add_content_subscribers_submenu(); 99 100 // 6b. Interest Subscribers (category/tag popup subscriptions) 101 $this->add_interest_subscribers_submenu(); 99 102 100 103 // 7. Channels … … 991 994 992 995 /** 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 /** 993 1017 * Add Email Logs submenu to the Subscriptions menu 994 1018 */ … … 1087 1111 ) 1088 1112 ); 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 ) ); 1089 1139 1090 1140 // Pause subscription setting … … 1366 1416 public function register_ajax_handlers() { 1367 1417 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')); 1368 1419 add_action('wp_ajax_test_stripe_connection', array($this, 'ajax_test_stripe_connection')); 1369 1420 add_action('wp_ajax_install_stripe_sdk', array($this, 'ajax_install_stripe_sdk')); … … 1391 1442 1392 1443 // 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 1393 1452 $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']) : '', 1395 1454 '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, 1398 1457 '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 ), 1400 1460 ); 1401 1461 … … 1440 1500 } 1441 1501 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 1442 1510 $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']) : '', 1444 1512 '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, 1447 1515 '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'), 1450 1519 ); 1451 1520 … … 2241 2310 // Prepare the data 2242 2311 $data = array( 2243 'plan_name' => sanitize_text_field($plan_data['plan_name']),2312 'plan_name' => sanitize_text_field($plan_data['plan_name']), 2244 2313 '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, 2247 2316 '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'), 2251 2321 ); 2252 2322 … … 2257 2327 $table_name, 2258 2328 $data, 2259 array('%s', '%s', '%f', '%d', '%d', '%s', '%s', '%s' )2329 array('%s', '%s', '%f', '%d', '%d', '%s', '%s', '%s', '%s') 2260 2330 ); 2261 2331 … … 2614 2684 2615 2685 /** 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 /** 2616 2848 * AJAX handler for checking the Stripe API key 2617 2849 */ … … 3427 3659 // Handle add/edit content plan 3428 3660 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 3429 3712 $plan_data = array( 3430 3713 'plan_name' => sanitize_text_field($_POST['plan_name']), … … 3433 3716 'duration' => intval($_POST['duration']), 3434 3717 '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, 3437 3720 'notification_frequency' => sanitize_text_field($_POST['notification_frequency']), 3438 3721 'status' => sanitize_text_field($_POST['status']) 3439 3722 ); 3440 3441 $plan_id = isset($_POST['plan_id']) ? intval($_POST['plan_id']) : 0;3442 3723 3443 3724 if ($plan_id > 0) { -
subscription-system/trunk/admin/partials/content-plans-display.php
r3375846 r3598150 223 223 data.append('content_type', contentType); 224 224 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); ?>'); 225 226 <?php if ($editing_plan): ?> 226 227 data.append('selected_ids', '<?php echo wp_json_encode($editing_plan->content_ids); ?>'); … … 278 279 <th scope="col"><?php esc_html_e('Duration', 'subscription-system'); ?></th> 279 280 <th scope="col"><?php esc_html_e('Status', 'subscription-system'); ?></th> 281 <th scope="col"><?php esc_html_e('Stripe', 'subscription-system'); ?></th> 280 282 <th scope="col"><?php esc_html_e('Subscribers', 'subscription-system'); ?></th> 281 283 <th scope="col"><?php esc_html_e('Actions', 'subscription-system'); ?></th> … … 285 287 <?php foreach ($content_plans as $plan): ?> 286 288 <?php 287 // Get subscriber count288 289 global $wpdb; 289 290 $subscriber_count = $wpdb->get_var($wpdb->prepare( … … 291 292 $plan->id 292 293 )); 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'); 293 299 ?> 294 300 <tr> … … 313 319 </span> 314 320 </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> 315 345 <td><?php echo esc_html($subscriber_count); ?></td> 316 346 <td> … … 379 409 background: #fafafa; 380 410 } 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 } 381 478 </style> 479 480 <script> 481 jQuery(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 149 149 <h3>📋 Table of Contents</h3> 150 150 <ul> 151 <li><a href="#integration-steps"><strong>⚡ Step-by-Step Integration Guide</strong></a></li> 151 152 <li><a href="#getting-started">Getting Started</a></li> 152 153 <li><a href="#subscription-management">Subscription Management & Renewal Flow</a></li> 153 154 <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> 154 156 <li><a href="#email-notifications">Email Notifications</a></li> 155 157 <li><a href="#shortcodes">Available Shortcodes</a></li> … … 159 161 <li><a href="#troubleshooting">Troubleshooting</a></li> 160 162 </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 & 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 & 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 161 358 </div> 162 359 … … 285 482 </div> 286 483 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="Join Now" button_class="my-custom-class"]')">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 & 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 287 718 <!-- Email Notifications --> 288 719 <div class="help-section" id="email-notifications"> … … 388 819 </div> 389 820 <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="Follow This Topic" button_class="my-btn"]')">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> 390 837 </div> 391 838 … … 460 907 <li><strong>Page Settings:</strong> Assign pages for different functions</li> 461 908 <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> 462 910 </ul> 463 911 </div> -
subscription-system/trunk/admin/partials/plans-display.php
r3438196 r3598150 94 94 <tr> 95 95 <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> 97 99 </tr> 98 100 <tr> … … 109 111 </td> 110 112 </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> 111 123 </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 112 145 <p class="submit"> 113 146 <input type="submit" name="add_plan" class="button button-primary" value="<?php echo esc_attr__('Save Plan', 'subscription-system'); ?>"> … … 178 211 <tr> 179 212 <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> 181 216 </tr> 182 217 <tr> … … 193 228 </td> 194 229 </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> 195 240 </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 196 262 <p class="submit"> 197 263 <input type="submit" name="edit_plan" class="button button-primary" value="<?php echo esc_attr__('Update Plan', 'subscription-system'); ?>"> … … 225 291 <tr> 226 292 <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> 228 294 <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> 232 299 <th scope="col" class="manage-column column-actions"><?php echo esc_html__('Actions', 'subscription-system'); ?></th> 233 300 </tr> … … 248 315 } 249 316 } 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'; 250 330 ?> 251 331 <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> 253 338 <td class="column-description"> 254 339 <?php 255 340 $desc = $plan->description; 256 // Check if description is JSON257 341 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>'; 271 348 } 272 } catch (Exception $e) { 349 echo '</ul>'; 350 } else { 273 351 echo esc_html($desc); 274 352 } … … 278 356 ?> 279 357 </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> 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> 283 387 <td> 284 388 <?php 285 389 $stripe_product_id = $plan->stripe_product_id; 286 390 $stripe_price_id = $plan->stripe_price_id; 287 288 // Check if the plan is connected to Stripe289 391 $is_connected = !empty($stripe_product_id) && !empty($stripe_price_id); 290 392 $is_mock = $is_connected && (strpos($stripe_product_id, 'mock_') === 0); 291 292 393 if ($is_connected) { 293 394 echo '<span class="stripe-status ' . ($is_mock ? 'mock-connected' : 'connected') . '">' . 294 395 '<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>'; 303 397 } else { 304 echo '<span class="stripe-status not-connected"><span class="dashicons dashicons-no"></span> No t Connected</span>';398 echo '<span class="stripe-status not-connected"><span class="dashicons dashicons-no"></span> None</span>'; 305 399 } 306 400 ?> 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> 309 402 </td> 310 403 <td class="column-actions"> … … 315 408 data-price="<?php echo esc_attr($plan->price); ?>" 316 409 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); ?>"> 318 414 <?php echo esc_html__('Edit', 'subscription-system'); ?> 319 415 </button> … … 354 450 $('#add-plan-form').hide(); 355 451 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; 362 461 363 462 $('#edit_plan_id').val(id); … … 366 465 $('#edit_duration').val(duration); 367 466 $('#edit_category_id').val(category); 467 $('#edit_status').val(status); 468 $('#edit_max_categories').val(maxCats); 469 $('#edit_max_tags').val(maxTags); 368 470 369 471 // Clear existing feature inputs … … 372 474 // Try to parse description as JSON (for features) 373 475 try { 374 // First try to handle escaped JSON (with backslashes)375 476 var cleanDescription = description.replace(/\\\\/g, '\\').replace(/\\"/g, '"'); 376 477 var features = JSON.parse(cleanDescription); 377 378 478 if (Array.isArray(features)) { 379 // Add feature inputs for each feature380 479 features.forEach(function(feature) { 381 480 addEditFeature(feature); 382 481 }); 383 482 } else { 384 // If not an array, add as a single feature385 483 addEditFeature(cleanDescription); 386 484 } 387 485 } catch (e) { 388 // If not valid JSON, add as a single feature389 486 if (description) { 390 487 addEditFeature(description); … … 621 718 }); 622 719 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 623 738 // Fix for the category column display 624 739 $('.sync-with-stripe').on('click', function(e) { -
subscription-system/trunk/admin/partials/settings-display.php
r3311076 r3598150 17 17 // Define tabs 18 18 $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'), 23 24 ); 24 25 … … 78 79 case 'pages': 79 80 include 'pages-settings-display.php'; 81 break; 82 case 'subscribe_modal': 83 include 'subscribe-modal-settings-display.php'; 80 84 break; 81 85 } -
subscription-system/trunk/includes/class-subs-sys-ajax.php
r3435450 r3598150 71 71 add_action( 'wp_ajax_unsubscribe_from_content_plan', array( $this, 'unsubscribe_from_content_plan' ) ); 72 72 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' ) ); 73 75 } 74 76 … … 1629 1631 */ 1630 1632 public function get_content_options() { 1631 // Debug logging1632 error_log('AJAX get_content_options called');1633 error_log('POST data: ' . print_r($_POST, true));1634 1635 // Verify nonce1636 1633 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'get_content_options' ) ) { 1637 error_log('Nonce verification failed');1638 1634 wp_send_json_error( array( 'message' => __( 'Security check failed.', 'subscription-system' ) ) ); 1639 1635 return; 1640 1636 } 1641 1637 1642 // Check user capabilities1643 1638 if ( ! current_user_can( 'manage_options' ) ) { 1644 error_log('User does not have manage_options capability');1645 1639 wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'subscription-system' ) ) ); 1646 1640 return; 1647 1641 } 1648 1642 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 ) ) { 1654 1648 $selected_ids = array(); 1655 1649 } 1656 1650 1657 error_log('Content type: ' . $content_type);1658 error_log('Selected IDs: ' . print_r($selected_ids, true));1659 1660 1651 if ( empty( $content_type ) ) { 1661 error_log('Content type is empty');1662 1652 wp_send_json_error( array( 'message' => __( 'Content type is required.', 'subscription-system' ) ) ); 1663 1653 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 } 1664 1679 } 1665 1680 … … 1668 1683 switch ( $content_type ) { 1669 1684 case 'category': 1670 $categories = get_categories( array( 'hide_empty' => false ) );1685 $categories = get_categories( array( 'hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC' ) ); 1671 1686 $html = '<div class="content-checkboxes">'; 1672 1687 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 } 1681 1708 } 1682 1709 $html .= '</div>'; … … 1684 1711 1685 1712 case 'tag': 1686 $tags = get_tags( array( 'hide_empty' => false ) );1713 $tags = get_tags( array( 'hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC' ) ); 1687 1714 $html = '<div class="content-checkboxes">'; 1688 1715 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 } 1697 1736 } 1698 1737 $html .= '</div>'; … … 1704 1743 'post_status' => 'publish', 1705 1744 'orderby' => 'title', 1706 'order' => 'ASC' 1745 'order' => 'ASC', 1707 1746 ) ); 1708 1747 $html = '<div class="content-checkboxes">'; 1709 1748 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' : ''; 1711 1750 $html .= sprintf( 1712 1751 '<label><input type="checkbox" name="content_ids[]" value="%d" %s> %s</label><br>', … … 1723 1762 $html = '<div class="content-checkboxes">'; 1724 1763 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' : ''; 1726 1765 $html .= sprintf( 1727 1766 '<label><input type="checkbox" name="content_ids[]" value="%s" %s> %s</label><br>', 1728 $post_type->name,1767 esc_attr( $post_type->name ), 1729 1768 $checked, 1730 1769 esc_html( $post_type->label ) … … 1739 1778 } 1740 1779 1741 error_log('Generated HTML length: ' . strlen($html));1742 error_log('Generated HTML: ' . substr($html, 0, 500) . '...');1743 1780 wp_send_json_success( $html ); 1744 1781 } … … 2009 2046 ) ); 2010 2047 } 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 31 31 * Current database version 32 32 */ 33 const DB_VERSION = '1. 0.9';33 const DB_VERSION = '1.1.1'; 34 34 35 35 /** … … 75 75 // Check if content tables exist 76 76 $content_tables_missing = ! self::content_tables_exist(); 77 78 $interest_table_missing = ! self::interest_table_exists(); 77 79 78 80 // 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 ) { 80 82 81 83 // Log what's triggering the update … … 88 90 if ( $content_tables_missing ) { 89 91 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' ); 90 95 } 91 96 … … 151 156 error_log( "Subs_Sys: Ensuring email logs table exists..." ); 152 157 $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(); 153 167 } 154 168 … … 310 324 311 325 /** 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 /** 312 391 * Check if email logs table exists 313 392 * -
subscription-system/trunk/includes/class-subs-sys-shortcodes.php
r3375846 r3598150 56 56 add_shortcode('content_subscription_checkout', array($this, 'render_content_subscription_checkout')); 57 57 add_shortcode('content_subscription_success', array($this, 'render_content_subscription_success')); 58 add_shortcode('subscriptionsystem_subscribe', array($this, 'render_interest_subscribe_button')); 58 59 59 60 // Add AJAX handlers for password reset … … 2238 2239 return ob_get_clean(); 2239 2240 } 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 76 76 $this->version = SUBS_SYS_VERSION; 77 77 } else { 78 $this->version = '1.0.1 7';78 $this->version = '1.0.18'; 79 79 } 80 80 $this->plugin_name = 'subs-sys'; … … 176 176 */ 177 177 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'; 178 183 179 184 /** … … 277 282 // Initialize content subscription manager 278 283 $content_manager = new Subs_Sys_Content_Manager(); 284 285 // Initialize interest subscription manager 286 $interest_subscription = new Subs_Sys_Interest_Subscription(); 279 287 280 288 // Initialize database updater (safe updates) -
subscription-system/trunk/public/class-subs-sys-public.php
r3356551 r3598150 52 52 add_action('wp_head', array($this, 'display_smart_subscription_bar')); 53 53 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')); 54 56 add_action('template_redirect', array($this, 'handle_content_subscription_success')); 55 57 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')); 56 61 } 57 62 … … 2454 2459 2455 2460 /** 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 <<?php echo esc_html( $subscription->subscriber_email ); ?>></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 ← <?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 /** 2456 2824 * Handle content subscription success redirect 2457 2825 */ … … 2732 3100 return $applicable_plans; 2733 3101 } 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 } 2734 3247 } -
subscription-system/trunk/subscription-system.php
r3587496 r3598150 17 17 * Plugin URI: https://mediahubster.in/subscription-system 18 18 * Description: A comprehensive subscription management system for WordPress. 19 * Version: 1.0.1 719 * Version: 1.0.18 20 20 * Author: Joe Youngblood 21 21 * Author URI: https://mediahubster.in … … 97 97 98 98 // Define plugin constants 99 define('SUBS_SYS_VERSION', '1.0.1 7');99 define('SUBS_SYS_VERSION', '1.0.18'); 100 100 define('SUBS_SYS_PLUGIN_DIR', plugin_dir_path(__FILE__)); 101 101 define('SUBS_SYS_PLUGIN_URL', plugin_dir_url(__FILE__)); 102 102 define('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 */ 112 function 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 } 103 156 104 157 // Load Composer autoloader if it exists … … 312 365 KEY status (status) 313 366 ) $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;"; 314 390 315 391 require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); … … 324 400 dbDelta($sql_content_subscriptions); 325 401 dbDelta($sql_content_notifications); 402 dbDelta($sql_interest); 326 403 327 404 // Add version to options … … 364 441 $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}subscription_content_subscriptions"); 365 442 $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}subscription_content_notifications"); 443 $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}subscription_interest_subscriptions"); 366 444 367 445 // Delete options
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)