Changeset 3730384
- Timestamp:
- 10/06/2026 09:22:05 AM (3 weeks ago)
- Location:
- salespulse
- Files:
-
- 61 added
- 13 edited
-
tags/1.1.0 (added)
-
tags/1.1.0/admin (added)
-
tags/1.1.0/admin/class-admin.php (added)
-
tags/1.1.0/admin/css (added)
-
tags/1.1.0/admin/css/admin.css (added)
-
tags/1.1.0/admin/js (added)
-
tags/1.1.0/admin/js/admin.js (added)
-
tags/1.1.0/admin/views (added)
-
tags/1.1.0/admin/views/admin-page.php (added)
-
tags/1.1.0/assets (added)
-
tags/1.1.0/assets/fonts (added)
-
tags/1.1.0/assets/fonts/fonts.css (added)
-
tags/1.1.0/assets/fonts/inter-400.ttf (added)
-
tags/1.1.0/assets/fonts/inter-500.ttf (added)
-
tags/1.1.0/assets/fonts/inter-600.ttf (added)
-
tags/1.1.0/assets/fonts/inter-700.ttf (added)
-
tags/1.1.0/assets/fonts/material-symbols-outlined.ttf (added)
-
tags/1.1.0/blocks (added)
-
tags/1.1.0/blocks/css (added)
-
tags/1.1.0/blocks/css/block-editor.css (added)
-
tags/1.1.0/blocks/js (added)
-
tags/1.1.0/blocks/js/block-editor.js (added)
-
tags/1.1.0/demo (added)
-
tags/1.1.0/demo/build-demo.ps1 (added)
-
tags/1.1.0/demo/demo-template.html (added)
-
tags/1.1.0/demo/demo.html (added)
-
tags/1.1.0/demo/icon-animation.html (added)
-
tags/1.1.0/demo/images (added)
-
tags/1.1.0/demo/images/backpack.png (added)
-
tags/1.1.0/demo/images/bag.png (added)
-
tags/1.1.0/demo/images/headphones.png (added)
-
tags/1.1.0/demo/images/phone.png (added)
-
tags/1.1.0/demo/images/shoes.png (added)
-
tags/1.1.0/demo/images/sunglasses.png (added)
-
tags/1.1.0/demo/images/watch.png (added)
-
tags/1.1.0/frontend (added)
-
tags/1.1.0/frontend/class-frontend.php (added)
-
tags/1.1.0/frontend/css (added)
-
tags/1.1.0/frontend/css/salespulse.css (added)
-
tags/1.1.0/frontend/js (added)
-
tags/1.1.0/frontend/js/salespulse.js (added)
-
tags/1.1.0/includes (added)
-
tags/1.1.0/includes/class-activator.php (added)
-
tags/1.1.0/includes/class-analytics.php (added)
-
tags/1.1.0/includes/class-block.php (added)
-
tags/1.1.0/includes/class-cookie-consent.php (added)
-
tags/1.1.0/includes/class-data-collector.php (added)
-
tags/1.1.0/includes/class-deactivator.php (added)
-
tags/1.1.0/includes/class-merge-tags.php (added)
-
tags/1.1.0/includes/class-notification-bar.php (added)
-
tags/1.1.0/includes/class-notification-engine.php (added)
-
tags/1.1.0/includes/class-salespulse.php (added)
-
tags/1.1.0/includes/class-shortcode.php (added)
-
tags/1.1.0/includes/integrations (added)
-
tags/1.1.0/includes/integrations/class-woocommerce.php (added)
-
tags/1.1.0/languages (added)
-
tags/1.1.0/languages/salespulse.pot (added)
-
tags/1.1.0/license.txt (added)
-
tags/1.1.0/readme.txt (added)
-
tags/1.1.0/salespulse.php (added)
-
tags/1.1.0/uninstall.php (added)
-
trunk/admin/class-admin.php (modified) (2 diffs)
-
trunk/admin/css/admin.css (modified) (1 diff)
-
trunk/admin/js/admin.js (modified) (2 diffs)
-
trunk/admin/views/admin-page.php (modified) (1 diff)
-
trunk/frontend/class-frontend.php (modified) (1 diff)
-
trunk/frontend/js/salespulse.js (modified) (7 diffs)
-
trunk/includes/class-analytics.php (modified) (1 diff)
-
trunk/includes/class-data-collector.php (modified) (2 diffs)
-
trunk/includes/class-merge-tags.php (modified) (2 diffs)
-
trunk/includes/class-notification-engine.php (modified) (3 diffs)
-
trunk/includes/class-salespulse.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (3 diffs)
-
trunk/salespulse.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
salespulse/trunk/admin/class-admin.php
r3480727 r3730384 28 28 add_action('admin_menu', array($this, 'register_menu')); 29 29 add_action('admin_enqueue_scripts', array($this, 'enqueue_assets')); 30 add_action('wp_ajax_salespulse_dismiss_review', array($this, 'ajax_dismiss_review')); 30 31 } 31 32 … … 175 176 return apply_filters('salespulse_template_list', $templates); 176 177 } 178 179 /** 180 * Check whether the review request notice should be displayed. 181 * 182 * Conditions: 183 * 1. User has not permanently dismissed it. 184 * 2. User has not snoozed it (or snooze time expired). 185 * 3. Either installed >= 3 days ago OR total recorded impressions >= 15. 186 * 187 * @return bool 188 */ 189 public static function should_show_review_notice() 190 { 191 if ('yes' === get_option('salespulse_review_dismissed')) { 192 return false; 193 } 194 195 $snooze_until = (int) get_option('salespulse_review_snooze_until', 0); 196 if ($snooze_until && time() < $snooze_until) { 197 return false; 198 } 199 200 $install_time = (int) get_option('salespulse_install_time', 0); 201 if (!$install_time) { 202 // For existing installs, backdate by 3 days so active users see the prompt. 203 $install_time = time() - (3 * DAY_IN_SECONDS); 204 update_option('salespulse_install_time', $install_time); 205 } 206 207 if ((time() - $install_time) >= (3 * DAY_IN_SECONDS)) { 208 return true; 209 } 210 211 // Or show if they have active impressions. 212 global $wpdb; 213 $table = $wpdb->prefix . 'salespulse_stats'; 214 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter 215 $has_impressions = (int) $wpdb->get_var("SELECT COUNT(id) FROM {$table} WHERE event_type = 'impression'"); 216 if ($has_impressions >= 15) { 217 return true; 218 } 219 220 return false; 221 } 222 223 /** 224 * AJAX handler for dismissing or snoozing the review notice. 225 */ 226 public function ajax_dismiss_review() 227 { 228 check_ajax_referer('salespulse_admin', '_ajax_nonce'); 229 if (!current_user_can('manage_options')) { 230 wp_send_json_error(array('message' => 'Unauthorized'), 403); 231 } 232 233 $dismiss_type = sanitize_key($_POST['dismiss_type'] ?? 'permanent'); // phpcs:ignore WordPress.Security.NonceVerification.Missing 234 235 if ('snooze' === $dismiss_type) { 236 update_option('salespulse_review_snooze_until', time() + (7 * DAY_IN_SECONDS)); 237 } else { 238 update_option('salespulse_review_dismissed', 'yes'); 239 } 240 241 wp_send_json_success(); 242 } 177 243 } -
salespulse/trunk/admin/css/admin.css
r3486898 r3730384 4978 4978 font-size: 18px; 4979 4979 } 4980 4981 /* ── Review Request Banner ─────────────────────────────── */ 4982 .sp-review-banner { 4983 display: flex; 4984 align-items: center; 4985 gap: 16px; 4986 margin: 16px 24px 0; 4987 padding: 14px 20px; 4988 background: linear-gradient(135deg, #f0f7f2 0%, #e8f2eb 100%); 4989 border: 1px solid rgba(96, 125, 102, 0.3); 4990 border-radius: var(--sp-radius-sm); 4991 box-shadow: var(--sp-shadow); 4992 transition: opacity 0.3s ease, transform 0.3s ease; 4993 } 4994 4995 .sp-review-banner-badge { 4996 width: 40px; 4997 height: 40px; 4998 border-radius: 10px; 4999 background: var(--sp-primary); 5000 display: flex; 5001 align-items: center; 5002 justify-content: center; 5003 flex-shrink: 0; 5004 } 5005 5006 .sp-review-banner-badge .material-symbols-outlined { 5007 color: #fff; 5008 font-size: 22px; 5009 } 5010 5011 .sp-review-banner-text { 5012 flex: 1; 5013 min-width: 0; 5014 } 5015 5016 .sp-review-banner-text strong { 5017 display: block; 5018 font-size: 14px; 5019 font-weight: 600; 5020 color: var(--sp-dark); 5021 margin-bottom: 2px; 5022 } 5023 5024 .sp-review-banner-text p { 5025 margin: 0; 5026 font-size: 13px; 5027 color: var(--sp-text-light); 5028 line-height: 1.4; 5029 } 5030 5031 .sp-review-banner-actions { 5032 display: flex; 5033 align-items: center; 5034 gap: 8px; 5035 flex-shrink: 0; 5036 } 5037 5038 .sp-review-btn-primary { 5039 display: inline-flex; 5040 align-items: center; 5041 gap: 6px; 5042 padding: 8px 16px; 5043 background: var(--sp-primary); 5044 color: #fff !important; 5045 text-decoration: none; 5046 border-radius: var(--sp-radius-sm); 5047 font-size: 13px; 5048 font-weight: 600; 5049 border: none; 5050 cursor: pointer; 5051 transition: background var(--sp-transition); 5052 } 5053 5054 .sp-review-btn-primary:hover { 5055 background: var(--sp-primary-hover); 5056 } 5057 5058 .sp-review-btn-primary .material-symbols-outlined { 5059 font-size: 18px; 5060 } 5061 5062 .sp-review-btn-secondary { 5063 display: inline-flex; 5064 align-items: center; 5065 padding: 8px 14px; 5066 background: transparent; 5067 color: var(--sp-text-light); 5068 border: 1px solid var(--sp-border); 5069 border-radius: var(--sp-radius-sm); 5070 font-size: 13px; 5071 font-weight: 500; 5072 cursor: pointer; 5073 transition: all var(--sp-transition); 5074 } 5075 5076 .sp-review-btn-secondary:hover { 5077 background: #fff; 5078 color: var(--sp-dark); 5079 border-color: #cbd5e1; 5080 } 5081 5082 .sp-review-btn-dismiss { 5083 display: inline-flex; 5084 align-items: center; 5085 justify-content: center; 5086 width: 32px; 5087 height: 32px; 5088 background: transparent; 5089 border: none; 5090 border-radius: var(--sp-radius-sm); 5091 color: var(--sp-text-light); 5092 cursor: pointer; 5093 transition: all var(--sp-transition); 5094 } 5095 5096 .sp-review-btn-dismiss:hover { 5097 background: rgba(0, 0, 0, 0.06); 5098 color: var(--sp-dark); 5099 } 5100 5101 .sp-review-btn-dismiss .material-symbols-outlined { 5102 font-size: 18px; 5103 } -
salespulse/trunk/admin/js/admin.js
r3486898 r3730384 1869 1869 } 1870 1870 1871 // ── Review Request Banner ──────────────────────────────── 1872 function initReviewBanner() { 1873 const banner = $("#sp-review-banner"); 1874 if (!banner) return; 1875 1876 banner.querySelectorAll(".sp-btn-review-action").forEach((btn) => { 1877 btn.addEventListener("click", () => { 1878 const action = btn.dataset.action; // 'review', 'snooze', or 'permanent' 1879 1880 // Smoothly fade out and remove banner 1881 banner.style.opacity = "0"; 1882 setTimeout(() => banner.remove(), 300); 1883 1884 // Send AJAX preference to backend 1885 const formData = new FormData(); 1886 formData.append("action", "salespulse_dismiss_review"); 1887 formData.append("_ajax_nonce", config.ajaxNonce); 1888 formData.append("dismiss_type", action === "snooze" ? "snooze" : "permanent"); 1889 1890 fetch(config.ajaxUrl, { 1891 method: "POST", 1892 body: formData, 1893 }).catch((err) => console.error("Review dismiss error:", err)); 1894 }); 1895 }); 1896 } 1897 1871 1898 // ── Init ───────────────────────────────────────────────── 1872 1899 document.addEventListener("DOMContentLoaded", () => { … … 1876 1903 initWizard(); 1877 1904 initQuickActions(); 1905 initReviewBanner(); 1878 1906 loadSettings(); 1879 1907 loadNotifications(); -
salespulse/trunk/admin/views/admin-page.php
r3486898 r3730384 78 78 <!-- Toast notification --> 79 79 <div id="salespulse-toast" class="salespulse-toast" aria-live="polite"></div> 80 81 <?php if ( \SalesPulse\Admin::should_show_review_notice() ) : ?> 82 <!-- Review Prompt Banner --> 83 <div class="sp-review-banner" id="sp-review-banner"> 84 <div class="sp-review-banner-badge"> 85 <span class="material-symbols-outlined">star</span> 86 </div> 87 <div class="sp-review-banner-text"> 88 <strong><?php esc_html_e( 'Loving SalesPulse? Support our free plugin!', 'salespulse' ); ?></strong> 89 <p><?php esc_html_e( 'If SalesPulse is helping your store build social proof and sales, please take 30 seconds to rate us 5 stars on WordPress.org. It helps independent WordPress developers keep building great free tools!', 'salespulse' ); ?></p> 90 </div> 91 <div class="sp-review-banner-actions"> 92 <a href="https://wordpress.org/support/plugin/salespulse/reviews/#new-post" target="_blank" rel="noopener noreferrer" class="sp-review-btn-primary sp-btn-review-action" data-action="review"> 93 <span class="material-symbols-outlined">rate_review</span> 94 <?php esc_html_e( 'Leave a 5-Star Review', 'salespulse' ); ?> 95 </a> 96 <button type="button" class="sp-review-btn-secondary sp-btn-review-action" data-action="snooze"> 97 <?php esc_html_e( 'Maybe Later', 'salespulse' ); ?> 98 </button> 99 <button type="button" class="sp-review-btn-dismiss sp-btn-review-action" data-action="permanent" title="<?php esc_attr_e( 'Dismiss permanently', 'salespulse' ); ?>"> 100 <span class="material-symbols-outlined">close</span> 101 </button> 102 </div> 103 </div> 104 <?php endif; ?> 80 105 81 106 <!-- ================================================================ -
salespulse/trunk/frontend/class-frontend.php
r3486898 r3730384 197 197 'trackUrl' => esc_url_raw(rest_url('salespulse/v1/track')), 198 198 'captureUrl' => esc_url_raw(rest_url('salespulse/v1/capture')), 199 'queueUrl' => esc_url_raw(rest_url('salespulse/v1/queue')), 199 200 'nonce' => wp_create_nonce('wp_rest'), 200 201 'pageId' => $page_id, -
salespulse/trunk/frontend/js/salespulse.js
r3486898 r3730384 61 61 } 62 62 63 // Skip popup logic if no queue.64 if ( ! queue.length ) return;65 66 63 container = document.getElementById( 'salespulse-popup-container' ); 67 64 if ( ! container ) return; … … 77 74 initSmartCapture(); 78 75 79 // Start displaying after initial delay. 80 setTimeout( showNext, settings.initialDelay || 3000 ); 76 if ( queue.length ) { 77 // Start displaying from pre-rendered queue. 78 setTimeout( showNext, settings.initialDelay || 3000 ); 79 } else if ( data.queueUrl ) { 80 // Page-cache fallback: Fetch fresh queue asynchronously 81 fetch( data.queueUrl + '?page_id=' + encodeURIComponent( data.pageId || 'global' ) ) 82 .then( function ( r ) { return r.json(); } ) 83 .then( function ( res ) { 84 if ( res && res.success && Array.isArray( res.queue ) && res.queue.length ) { 85 res.queue.forEach( function ( item ) { 86 if ( item.type === 'notification_bar' ) { 87 initIndividualBar( item ); 88 } else { 89 queue.push( item ); 90 } 91 } ); 92 if ( queue.length ) { 93 setTimeout( showNext, settings.initialDelay || 3000 ); 94 } 95 } 96 } ) 97 .catch( function () {} ); 98 } 81 99 } 82 100 … … 198 216 popup.addEventListener( 'click', function ( e ) { 199 217 if ( e.target.classList.contains( 'sp-notification-close' ) ) return; 200 trackEvent( notification.id, 'click', notification.type );218 trackEvent( notification.id, 'click', notification.type, notification.ab_variant, notification.ab_test_id ); 201 219 window.open( notification.link, '_blank', 'noopener' ); 202 220 } ); … … 275 293 276 294 // Track impression. 277 trackEvent( notification.id, 'impression', notification.type );295 trackEvent( notification.id, 'impression', notification.type, notification.ab_variant, notification.ab_test_id ); 278 296 279 297 // Initialize any countdown timer inside this popup. … … 366 384 367 385 // ── Analytics Tracking ─────────────────────────────────── 368 function trackEvent( notificationId, eventType, notificationType ) {386 function trackEvent( notificationId, eventType, notificationType, abVariant, abTestId ) { 369 387 if ( ! data.trackUrl || ! notificationId ) return; 370 388 … … 379 397 event: eventType, 380 398 page_id: data.pageId || 'global', 399 ab_variant: abVariant || '', 400 ab_test_id: abTestId || 0, 381 401 } ), 382 402 } ).catch( () => {} ); // Silently fail. … … 388 408 notification_type: notificationType || 'unknown', 389 409 page_id: data.pageId || 'global', 410 ab_variant: abVariant || 'default', 390 411 } ); 391 412 } -
salespulse/trunk/includes/class-analytics.php
r3479316 r3730384 142 142 ); 143 143 144 // Allow extensions (e.g. SalesPulse Pro A/B testing) to process tracking events. 145 do_action('salespulse_track_event', $notification_id, $event, $page_id, $request); 146 144 147 return new \WP_REST_Response(array('success' => true), 200); 145 148 } -
salespulse/trunk/includes/class-data-collector.php
r3486898 r3730384 117 117 public static function get_recent_comments( $limit = 10 ) { 118 118 $comments = get_comments( array( 119 'status' => 'approve', 120 'number' => $limit, 121 'orderby' => 'comment_date', 122 'order' => 'DESC', 119 'status' => 'approve', 120 'post_type' => 'post', 121 'number' => $limit, 122 'orderby' => 'comment_date', 123 'order' => 'DESC', 123 124 ) ); 125 126 // Fallback if no blog post comments exist. 127 if ( empty( $comments ) ) { 128 $comments = get_comments( array( 129 'status' => 'approve', 130 'number' => $limit, 131 'orderby' => 'comment_date', 132 'order' => 'DESC', 133 ) ); 134 } 124 135 125 136 $comment_data = array(); … … 137 148 138 149 return $comment_data; 150 } 151 152 /** 153 * Get recent WooCommerce product reviews. 154 * 155 * Queries approved reviews attached to WooCommerce products. 156 * 157 * @param int $limit Max reviews to return. 158 * @return array Array of review data arrays. 159 */ 160 public static function get_recent_reviews( $limit = 10 ) { 161 $args = array( 162 'status' => 'approve', 163 'post_type' => 'product', 164 'number' => $limit, 165 'orderby' => 'comment_date', 166 'order' => 'DESC', 167 ); 168 169 if ( class_exists( 'WooCommerce' ) ) { 170 $args['type'] = 'review'; 171 } 172 173 $reviews = get_comments( $args ); 174 175 // Fallback without comment_type if none found. 176 if ( empty( $reviews ) && ! empty( $args['type'] ) ) { 177 unset( $args['type'] ); 178 $reviews = get_comments( $args ); 179 } 180 181 if ( empty( $reviews ) ) { 182 return array(); 183 } 184 185 $review_data = array(); 186 187 foreach ( $reviews as $review ) { 188 $product_id = (int) $review->comment_post_ID; 189 $rating = (int) get_comment_meta( $review->comment_ID, 'rating', true ); 190 if ( ! $rating || $rating < 1 || $rating > 5 ) { 191 $rating = 5; 192 } 193 194 $product_image = get_the_post_thumbnail_url( $product_id, 'thumbnail' ); 195 if ( ! $product_image && function_exists( 'wc_placeholder_img_src' ) ) { 196 $product_image = wc_placeholder_img_src( 'thumbnail' ); 197 } 198 199 $product_title = get_the_title( $product_id ); 200 $product_url = get_permalink( $product_id ); 201 202 $review_data[] = array( 203 'customer_name' => $review->comment_author ?: esc_html__( 'Someone', 'salespulse' ), 204 'comment_text' => wp_trim_words( $review->comment_content, 12, '...' ), 205 'review_text' => wp_trim_words( $review->comment_content, 12, '...' ), 206 'product_name' => $product_title ?: esc_html__( 'Product', 'salespulse' ), 207 'post_title' => $product_title, 208 'product_url' => $product_url, 209 'post_url' => $product_url, 210 'product_image' => $product_image ?: '', 211 'rating' => $rating, 212 'rating_stars' => str_repeat( '★', $rating ), 213 'time_ago' => self::human_time_diff( strtotime( $review->comment_date ) ), 214 'timestamp' => strtotime( $review->comment_date ), 215 ); 216 } 217 218 return $review_data; 139 219 } 140 220 -
salespulse/trunk/includes/class-merge-tags.php
r3486898 r3730384 35 35 '{post_title}' => esc_html__( 'Post/page title', 'salespulse' ), 36 36 '{comment_text}' => esc_html__( 'Comment excerpt (10 words)', 'salespulse' ), 37 '{review_text}' => esc_html__( 'Review excerpt (12 words)', 'salespulse' ), 38 '{rating}' => esc_html__( 'Rating number (1-5)', 'salespulse' ), 39 '{rating_stars}' => esc_html__( 'Rating stars (e.g. ★★★★★)', 'salespulse' ), 37 40 '{on_fire_count}' => esc_html__( 'Aggregate activity count (On Fire)', 'salespulse' ), 38 41 '{on_fire_window}' => esc_html__( 'Time window label (e.g., "24 hours")', 'salespulse' ), … … 61 64 '{visitor_count}' => absint( $data['visitor_count'] ?? Data_Collector::get_visitor_count() ), 62 65 '{post_title}' => esc_html( $data['post_title'] ?? '' ), 63 '{comment_text}' => esc_html( $data['comment_text'] ?? '' ), 66 '{comment_text}' => esc_html( $data['comment_text'] ?? $data['review_text'] ?? '' ), 67 '{review_text}' => esc_html( $data['review_text'] ?? $data['comment_text'] ?? '' ), 68 '{rating}' => absint( $data['rating'] ?? 5 ), 69 '{rating_stars}' => esc_html( $data['rating_stars'] ?? '★★★★★' ), 64 70 '{on_fire_count}' => absint( $data['on_fire_count'] ?? 0 ), 65 71 '{on_fire_window}' => esc_html( $data['on_fire_window'] ?? '' ), -
salespulse/trunk/includes/class-notification-engine.php
r3486898 r3730384 44 44 $queue = array(); 45 45 46 // Fetch visitor geo data once for all notifications. 47 $visitor_geo = Data_Collector::get_visitor_geo(); 46 // Fetch visitor geo data lazily only if geo-filtering is hooked or geo merge tags are used. 47 $needs_geo = has_filter( 'salespulse_geo_filter_notification' ); 48 if ( ! $needs_geo ) { 49 foreach ( $notifications as $notif_item ) { 50 $cfg_str = is_string( $notif_item['config'] ?? '' ) ? $notif_item['config'] : wp_json_encode( $notif_item['config'] ?? '' ); 51 if ( false !== strpos( $cfg_str, 'visitor_city' ) || false !== strpos( $cfg_str, 'visitor_country' ) ) { 52 $needs_geo = true; 53 break; 54 } 55 } 56 } 57 58 $visitor_geo = $needs_geo ? Data_Collector::get_visitor_geo() : array(); 48 59 49 60 foreach ( $notifications as $notification ) { … … 154 165 155 166 case 'review': 156 $comments = Data_Collector::get_recent_comments( 1 ); 157 return $comments; 167 return Data_Collector::get_recent_reviews( 1 ); 158 168 159 169 case 'comment': … … 288 298 } 289 299 300 if ( 'review' === $type ) { 301 $item['product_name'] = esc_html__( 'Featured Product', 'salespulse' ); 302 $item['post_title'] = $item['product_name']; 303 $item['comment_text'] = esc_html__( 'Amazing quality, exceeded my expectations!', 'salespulse' ); 304 $item['review_text'] = $item['comment_text']; 305 $item['rating'] = 5; 306 $item['rating_stars'] = '★★★★★'; 307 $item['product_url'] = home_url(); 308 $item['post_url'] = home_url(); 309 $item['product_image'] = ''; 310 } 311 290 312 if ( 'comment' === $type ) { 291 313 $post_titles = array( 'Getting Started Guide', 'Top 10 Tips', 'Product Review', 'How-To Tutorial', 'Latest Updates' ); -
salespulse/trunk/includes/class-salespulse.php
r3486898 r3730384 213 213 'callback' => array($this, 'rest_smart_capture'), 214 214 'permission_callback' => '__return_true', 215 ), 216 )); 217 218 // Public endpoint: fetch fresh notification queue for page cache compatibility. 219 register_rest_route('salespulse/v1', '/queue', array( 220 array( 221 'methods' => 'GET', 222 'callback' => array($this, 'rest_get_queue'), 223 'permission_callback' => '__return_true', 224 'args' => array( 225 'page_id' => array( 226 'default' => 'global', 227 'sanitize_callback' => 'sanitize_text_field', 228 ), 229 ), 215 230 ), 216 231 )); … … 532 547 Cookie_Consent::save_settings($data); 533 548 return rest_ensure_response(array('success' => true)); 549 } 550 551 /** 552 * REST: Get fresh notification queue. 553 * 554 * Ensures notifications work dynamically on cached pages (LiteSpeed, WP Rocket, Cloudflare). 555 * 556 * @param \WP_REST_Request $request Request. 557 * @return \WP_REST_Response 558 */ 559 public function rest_get_queue($request) 560 { 561 $page_id = sanitize_text_field($request->get_param('page_id') ?: 'global'); 562 $queue = Notification_Engine::build_queue($page_id); 563 return rest_ensure_response(array( 564 'success' => true, 565 'queue' => $queue, 566 )); 534 567 } 535 568 -
salespulse/trunk/readme.txt
r3486898 r3730384 1 === SalesPulse - Social Proof & FOMO Notifications===1 === SalesPulse – Recent Sales Notification, Live Social Proof & FOMO Popups for WooCommerce === 2 2 Contributors: wpmatcha 3 3 Donate link: https://wpmatcha.com/ 4 Tags: social proof, fomo, notifications, woocommerce, conversion4 Tags: woocommerce, sales notification, recent sales, social proof, fomo popup 5 5 Requires at least: 5.8 6 6 Tested up to: 6.9 7 7 Requires PHP: 7.4 8 Stable tag: 1. 0.28 Stable tag: 1.1.0 9 9 License: GPLv2 or later 10 10 License URI: https://www.gnu.org/licenses/gpl-2.0.html 11 11 12 Boost conversions with real-time social proof & FOMO popups. Show purchases, signups, reviews, visitor counts & announcement bars.12 Recent sales popups, live social proof, purchase notifications & FOMO alerts for WooCommerce. Boost sales, trust, and conversions in minutes. 13 13 14 14 == Description == … … 142 142 == Changelog == 143 143 144 = 1.1.0 = 145 * Feature: Full page cache compatibility (LiteSpeed, WP Rocket, Cloudflare) via dynamic REST queue fallback. 146 * Fix: Resolved issue where product reviews fell back to general blog comments instead of WooCommerce product reviews. 147 * Improvement: Added {rating}, {rating_stars}, and {review_text} merge tags with automatic product link and image resolution for reviews. 148 * Improvement: Optimized SEO title and keywords for WooCommerce store discoverability. 149 * Improvement: Added extension hooks for Pro A/B testing and click-through attribution. 150 * Fix: Prevented stale notifications from displaying on cached product pages. 151 144 152 = 1.0.2 = 145 153 * Bug Fixes … … 177 185 == Upgrade Notice == 178 186 187 = 1.1.0 = 188 Recommended update: Adds full page caching compatibility for WooCommerce stores, performance optimizations, and bug fixes. 189 179 190 = 1.0.0 = 180 191 First release of SalesPulse 192 193 == External Services == 194 195 This plugin can optionally query an external service to resolve geo-location for merge tags: 196 * **ip-api.com** (https://ip-api.com/) 197 - **Purpose:** Resolves visitor IP address to approximate city and country for localized merge tags ({visitor_city}, {visitor_country}). 198 - **Invocation:** Only invoked if notifications explicitly include visitor geo merge tags. Otherwise, no external requests are made. 199 - **Data Transmitted:** Visitor IP address. No personal information, names, or order details are ever sent. 200 - **Terms & Privacy:** https://ip-api.com/docs/legal -
salespulse/trunk/salespulse.php
r3486898 r3730384 1 1 <?php 2 2 /** 3 * Plugin Name: SalesPulse - Social Proof & FOMO Notifications3 * Plugin Name: SalesPulse – Recent Sales Notification, Live Social Proof & FOMO Popups for WooCommerce 4 4 * Description: Boost conversions with real-time social proof notifications. Show recent purchases, signups, reviews & visitor activity to build trust and create urgency. 5 * Version: 1. 0.25 * Version: 1.1.0 6 6 * Author: WPMatcha 7 7 * Author URI: https://wpmatcha.com/ … … 38 38 * Plugin constants. 39 39 */ 40 define( 'SALESPULSE_VERSION', '1. 0.2' );40 define( 'SALESPULSE_VERSION', '1.1.0' ); 41 41 define( 'SALESPULSE_PLUGIN_FILE', __FILE__ ); 42 42 define( 'SALESPULSE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
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)