Changeset 3707420
- Timestamp:
- 09/22/2026 01:18:48 PM (5 weeks ago)
- Location:
- 2waymicropay
- Files:
-
- 12 edited
-
tags/4.1.7/readme.txt (modified) (1 diff)
-
trunk/admin/class-qixit-admin.php (modified) (6 diffs)
-
trunk/admin/class-qixit-metabox.php (modified) (4 diffs)
-
trunk/includes/class-qixit-api.php (modified) (5 diffs)
-
trunk/includes/class-qixit-author-commerce.php (modified) (5 diffs)
-
trunk/includes/class-qixit-product.php (modified) (3 diffs)
-
trunk/includes/qixit-functions.php (modified) (3 diffs)
-
trunk/js/frontend-script.js (modified) (17 diffs)
-
trunk/public/class-qixit-frontend.php (modified) (1 diff)
-
trunk/public/qixit-shortcodes.php (modified) (7 diffs)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/wp-qixit-modern.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
2waymicropay/tags/4.1.7/readme.txt
r3645800 r3707420 3 3 Tags: micropayments, paywall, monetization, authors, comments 4 4 Requires at least: 5.0 5 Tested up to: 7. 15 Tested up to: 7.0 6 6 Requires PHP: 7.4 7 7 Stable tag: 4.1.7 -
2waymicropay/trunk/admin/class-qixit-admin.php
r3621852 r3707420 755 755 <?php wp_nonce_field('qixit_resync_all', 'qixit_resync_nonce'); ?> 756 756 <button type="submit" name="qixit_resync_all" class="button button-primary" 757 onclick="return confirm('This will resync ALL products and paid comments to the Products Portal API . This may take a while for sites with many products. Continue?');">757 onclick="return confirm('This will resync ALL products and paid comments to the Products Portal API (including Price Optimization Categories). This may take a while for sites with many products. Continue?');"> 758 758 🔄 <?php esc_html_e('Resync All Products & Payouts', '2waymicropay'); ?> 759 759 </button> … … 761 761 </div> 762 762 <p class="description" style="margin-top:8px;"> 763 <?php esc_html_e('Use Test API for troubleshooting. Use Resync to refresh Report URLs and purchase tracking links.', '2waymicropay'); ?>763 <?php esc_html_e('Use Test API for troubleshooting. Use Resync to refresh Report URLs, purchase tracking links, and Price Optimization Categories (WordPress categories or WP Post/WP Page / Paid Comments / Author Rights defaults). Metrics and A/B tests remain in the Vendor portal.', '2waymicropay'); ?> 764 764 </p> 765 765 <div id="qixit-test-results" style="display:none; margin-top:10px;"></div> … … 828 828 <tr> 829 829 <th scope="row"> 830 <label for="qixit_default_commercial_category"><?php esc_html_e('Default Commercial C ategory', '2waymicropay'); ?> <span style="color:red;">*</span></label>830 <label for="qixit_default_commercial_category"><?php esc_html_e('Default Commercial Code', '2waymicropay'); ?> <span style="color:red;">*</span></label> 831 831 </th> 832 832 <td> … … 842 842 </select> 843 843 <p class="description"> 844 <?php esc_html_e('This category will be used as the default for all new premium content. Individual posts can override this setting.', '2waymicropay'); ?> 844 <?php esc_html_e('Postage / commercial classification for new premium content (not the Vendor portal Price Optimization Category). Individual posts can override this. Price Optimization Categories follow WordPress categories on posts/pages, or defaults such as Paid Comments and Author Rights.', '2waymicropay'); ?> 845 </p> 846 <p class="description"> 847 <?php 848 echo wp_kses_post( 849 sprintf( 850 /* translators: %s: products portal pricing URL */ 851 __('View priced-render metrics, demand curves, and A/B price tests in the <a href="%s" target="_blank" rel="noopener noreferrer">Vendor portal (Pricing)</a>. This plugin does not show those metrics.', '2waymicropay'), 852 esc_url('https://products.qixit.com/pricing') 853 ) 854 ); 855 ?> 845 856 </p> 846 857 <?php if (empty($default_category)): ?> 847 858 <p class="description" style="color: #d63638; font-weight: bold;"> 848 ⚠️ <?php esc_html_e('You must select a default commercial c ategorybefore creating premium content.', '2waymicropay'); ?>859 ⚠️ <?php esc_html_e('You must select a default commercial code before creating premium content.', '2waymicropay'); ?> 849 860 </p> 850 861 <?php endif; ?> … … 2380 2391 } 2381 2392 2393 // Re-provision author-rights / publication / revision products so Categories update too. 2394 if (class_exists('Qixit_Author_Commerce')) { 2395 $logger->log('Resync All: Ensuring author commerce products (incl. Price Optimization Categories)'); 2396 try { 2397 Qixit_Author_Commerce::get_instance()->ensure_required_products(true); 2398 } catch (Exception $e) { 2399 $errors++; 2400 $logger->log('Resync All: Author commerce ensure failed: ' . $e->getMessage(), 'error'); 2401 } 2402 } 2403 2382 2404 // translators: 1: synced products count, 2: synced comments count, 3: error count 2383 $resync_message_template = __('Resync complete! Products: %1$d synced. Paid Comments: %2$d synced. Errors: %3$d. ', '2waymicropay');2405 $resync_message_template = __('Resync complete! Products: %1$d synced. Paid Comments: %2$d synced. Errors: %3$d. Author-rights products refreshed when configured.', '2waymicropay'); 2384 2406 $message = sprintf( 2385 2407 $resync_message_template, … … 2557 2579 <li>View transaction reports and payouts</li> 2558 2580 <li>Manage your account settings</li> 2581 <li><strong>Price Optimization</strong> — priced-render metrics, demand curves, and A/B price tests at <a href="https://products.qixit.com/pricing" target="_blank" rel="noopener noreferrer">products.qixit.com/pricing</a> (not shown in this plugin)</li> 2559 2582 </ul> 2583 <p> 2584 <strong>Commercial Code</strong> (postage) is set in plugin Settings / the post metabox. 2585 <strong>Price Optimization Category</strong> follows each post or page’s WordPress categories 2586 (or defaults to <em>WP Post</em> / <em>WP Page</em> when none). Paid Comments and Author Rights 2587 use those named Categories. Use <strong>Resync All</strong> to push Categories to existing products. 2588 </p> 2560 2589 </div> 2561 2590 -
2waymicropay/trunk/admin/class-qixit-metabox.php
r3621852 r3707420 485 485 486 486 <p> 487 <label for="qixit_commercial_category"><strong><?php esc_html_e('Commercial C ategory:', '2waymicropay'); ?></strong></label>487 <label for="qixit_commercial_category"><strong><?php esc_html_e('Commercial Code:', '2waymicropay'); ?></strong></label> 488 488 <select id="qixit_commercial_category" name="qixit_commercial_category" class="widefat"> 489 489 <?php foreach ($commercial_categories as $value => $label): ?> … … 505 505 ?> 506 506 </span> 507 <br><span class="description"><?php esc_html_e('Postage classification only. Price Optimization Category follows this post/page’s WordPress categories (or defaults to WP Post / WP Page if none). View metrics and A/B tests in the Vendor portal.', '2waymicropay'); ?></span> 507 508 </p> 508 509 … … 936 937 937 938 // Category change action 938 $bulk_actions['qixit_change_category'] = __('2WayMicropay: Change Commercial C ategory', '2waymicropay');939 $bulk_actions['qixit_change_category'] = __('2WayMicropay: Change Commercial Code', '2waymicropay'); 939 940 940 941 // Paid comments actions … … 1315 1316 public function add_qixit_column($columns) { 1316 1317 $columns['qixit_status'] = __('2WayMicropay', '2waymicropay'); 1317 $columns['qixit_category'] = __('C ategory', '2waymicropay');1318 $columns['qixit_category'] = __('Commercial Code', '2waymicropay'); 1318 1319 return $columns; 1319 1320 } -
2waymicropay/trunk/includes/class-qixit-api.php
r3621852 r3707420 183 183 * @return array Response with product data (cost, description, accessHours, etc.) 184 184 */ 185 public function get_public_product($product_guid ) {185 public function get_public_product($product_guid, $session = null) { 186 186 // Request-level cache: the same product GUID is often fetched twice per 187 187 // page (once in has_user_purchased, once in render_paywall). Avoid the 188 188 // duplicate HTTP round-trip by caching within this PHP execution. 189 // Include session so A/B assignment is not collapsed across viewers. 190 $session_key = is_string($session) ? trim($session) : ''; 191 $cache_key = $product_guid . '|' . $session_key; 189 192 static $cache = array(); 190 if (isset($cache[$product_guid])) { 191 return $cache[$product_guid]; 192 } 193 $result = $this->request_public('api/products/public/' . $product_guid); 194 $cache[$product_guid] = $result; 193 if (isset($cache[$cache_key])) { 194 return $cache[$cache_key]; 195 } 196 $endpoint = 'api/products/public/' . $product_guid; 197 if ($session_key !== '') { 198 $endpoint .= '?session=' . rawurlencode($session_key); 199 } 200 $result = $this->request_public($endpoint); 201 $cache[$cache_key] = $result; 195 202 return $result; 196 203 } … … 423 430 'deliveryUrl' => isset($product_data['deliveryUrl']) ? $product_data['deliveryUrl'] : '', 424 431 'commercialCode' => isset($product_data['commercialCode']) ? $product_data['commercialCode'] : 'CONTENT', 432 'sectionKey' => isset($product_data['sectionKey']) ? $product_data['sectionKey'] : null, 433 'sectionName' => isset($product_data['sectionName']) ? $product_data['sectionName'] : null, 425 434 'affiliateUserId' => isset($product_data['affiliateUserId']) ? $product_data['affiliateUserId'] : '', 426 435 'affiliatePercentage' => isset($product_data['affiliatePercentage']) ? intval($product_data['affiliatePercentage']) : 0, … … 436 445 'accessHours' => isset($product_data['accessHours']) ? intval($product_data['accessHours']) : -1 437 446 ); 447 // Drop null section fields so API defaulting still applies when unset. 448 if ($data['sectionKey'] === null || $data['sectionKey'] === '') { 449 unset($data['sectionKey']); 450 } 451 if ($data['sectionName'] === null || $data['sectionName'] === '') { 452 unset($data['sectionName']); 453 } 438 454 439 455 return $this->request('api/products', 'POST', $data); … … 454 470 'deliveryUrl' => isset($product_data['deliveryUrl']) ? $product_data['deliveryUrl'] : '', 455 471 'commercialCode' => isset($product_data['commercialCode']) ? $product_data['commercialCode'] : 'CONTENT', 472 'sectionKey' => isset($product_data['sectionKey']) ? $product_data['sectionKey'] : null, 473 'sectionName' => isset($product_data['sectionName']) ? $product_data['sectionName'] : null, 456 474 'affiliateUserId' => isset($product_data['affiliateUserId']) ? $product_data['affiliateUserId'] : '', 457 475 'affiliatePercentage' => isset($product_data['affiliatePercentage']) ? intval($product_data['affiliatePercentage']) : 0, … … 467 485 'accessHours' => isset($product_data['accessHours']) ? intval($product_data['accessHours']) : -1 468 486 ); 487 if ($data['sectionKey'] === null || $data['sectionKey'] === '') { 488 unset($data['sectionKey']); 489 } 490 if ($data['sectionName'] === null || $data['sectionName'] === '') { 491 unset($data['sectionName']); 492 } 469 493 470 494 return $this->request('api/products/' . $product_guid, 'PUT', $data); -
2waymicropay/trunk/includes/class-qixit-author-commerce.php
r3621852 r3707420 102 102 if (empty($category)) { 103 103 $result['success'] = false; 104 $result['errors'][] = __('Default Commercial C ategoryis missing. Select one in Settings before provisioning products.', '2waymicropay');104 $result['errors'][] = __('Default Commercial Code is missing. Select one in Settings before provisioning products.', '2waymicropay'); 105 105 } 106 106 … … 662 662 'newBrowser' => 0, 663 663 ); 664 if (function_exists('qixit_price_opt_section_fixed') && function_exists('qixit_apply_price_opt_section_to_api_data')) { 665 $api_data = qixit_apply_price_opt_section_to_api_data($api_data, qixit_price_opt_section_fixed('author_rights')); 666 } 664 667 if (function_exists('qixit_apply_forwarder_to_api_data')) { 665 668 $api_data = qixit_apply_forwarder_to_api_data($api_data); … … 759 762 'newBrowser' => 0, 760 763 ); 764 if (function_exists('qixit_price_opt_section_fixed') && function_exists('qixit_apply_price_opt_section_to_api_data')) { 765 $api_data = qixit_apply_price_opt_section_to_api_data($api_data, qixit_price_opt_section_fixed('author_rights')); 766 } 761 767 if (function_exists('qixit_apply_forwarder_to_api_data')) { 762 768 $api_data = qixit_apply_forwarder_to_api_data($api_data); … … 848 854 'newBrowser' => 0, 849 855 ); 856 if (function_exists('qixit_price_opt_section_fixed') && function_exists('qixit_apply_price_opt_section_to_api_data')) { 857 $api_data = qixit_apply_price_opt_section_to_api_data($api_data, qixit_price_opt_section_fixed('author_rights')); 858 } 850 859 if (function_exists('qixit_apply_forwarder_to_api_data')) { 851 860 $api_data = qixit_apply_forwarder_to_api_data($api_data); … … 2057 2066 'newBrowser' => 0, 2058 2067 ); 2068 if (function_exists('qixit_price_opt_section_fixed') && function_exists('qixit_apply_price_opt_section_to_api_data')) { 2069 $api_data = qixit_apply_price_opt_section_to_api_data($api_data, qixit_price_opt_section_fixed('author_rights')); 2070 } 2059 2071 if (function_exists('qixit_apply_forwarder_to_api_data')) { 2060 2072 $api_data = qixit_apply_forwarder_to_api_data($api_data); -
2waymicropay/trunk/includes/class-qixit-product.php
r3621852 r3707420 790 790 'reportUrl' => $report_url // Server-to-server callback for purchase tracking 791 791 ); 792 if (function_exists('qixit_price_opt_section_for_post') && function_exists('qixit_apply_price_opt_section_to_api_data')) { 793 $api_data = qixit_apply_price_opt_section_to_api_data($api_data, qixit_price_opt_section_for_post($post_id)); 794 } 792 795 if (!empty($author_split)) { 793 796 $api_data = array_merge($api_data, $author_split); … … 950 953 'reportUrl' => $report_url // Server-to-server callback for purchase tracking 951 954 ); 955 if (function_exists('qixit_price_opt_section_fixed') && function_exists('qixit_apply_price_opt_section_to_api_data')) { 956 $api_data = qixit_apply_price_opt_section_to_api_data($api_data, qixit_price_opt_section_fixed('paid_comments')); 957 } 952 958 if ($use_author_comment_share && !empty($author_comment_split)) { 953 959 $api_data = array_merge($api_data, $author_comment_split); … … 1189 1195 'newBrowser' => 0 // Redirect in same window to unlock content 1190 1196 ); 1197 if (function_exists('qixit_price_opt_section_for_post') && function_exists('qixit_apply_price_opt_section_to_api_data')) { 1198 $api_data = qixit_apply_price_opt_section_to_api_data($api_data, qixit_price_opt_section_for_post($post_id)); 1199 } 1191 1200 1192 1201 // Keep author split payload consistent on save/update operations. -
2waymicropay/trunk/includes/qixit-functions.php
r3621814 r3707420 125 125 $api_data['forwardPercentage'] = qixit_get_forwarder_percent(); 126 126 $api_data['forwardUserId'] = ''; 127 return $api_data; 128 } 129 130 /** 131 * Fixed Price Optimization Category for non-post products (Vendor portal sections). 132 * Not the same as Commercial Code (postage). 133 * 134 * @param string $kind One of: paid_comments, author_rights 135 * @return array{sectionKey:string,sectionName:string} 136 */ 137 function qixit_price_opt_section_fixed($kind) { 138 $kind = sanitize_key((string) $kind); 139 if ($kind === 'paid_comments') { 140 return array( 141 'sectionKey' => 'paid-comments', 142 'sectionName' => 'Paid Comments', 143 ); 144 } 145 if ($kind === 'author_rights') { 146 return array( 147 'sectionKey' => 'author-rights', 148 'sectionName' => 'Author Rights', 149 ); 150 } 151 return array( 152 'sectionKey' => 'uncategorized', 153 'sectionName' => 'Uncategorized', 154 ); 155 } 156 157 /** 158 * Resolve Price Optimization Category for a paywalled post/page from WP categories. 159 * First assigned category wins; if none, WP Post / WP Page by post_type. 160 * 161 * @param int $post_id Post ID 162 * @return array{sectionKey:string,sectionName:string} 163 */ 164 function qixit_price_opt_section_for_post($post_id) { 165 $post_id = intval($post_id); 166 $post_type = $post_id > 0 ? get_post_type($post_id) : ''; 167 if ($post_type === 'page') { 168 $fallback = array('sectionKey' => 'wp-page', 'sectionName' => 'WP Page'); 169 } else { 170 $fallback = array('sectionKey' => 'wp-post', 'sectionName' => 'WP Post'); 171 } 172 173 if ($post_id <= 0) { 174 return $fallback; 175 } 176 177 $term_ids = wp_get_post_categories($post_id, array('fields' => 'ids')); 178 if (empty($term_ids) || !is_array($term_ids)) { 179 return $fallback; 180 } 181 182 $term = get_term(intval($term_ids[0]), 'category'); 183 if (!$term || is_wp_error($term) || empty($term->name)) { 184 return $fallback; 185 } 186 187 $slug = !empty($term->slug) ? sanitize_title($term->slug) : sanitize_title($term->name); 188 if ($slug === '') { 189 $slug = 'wp-category-' . intval($term->term_id); 190 } 191 192 return array( 193 'sectionKey' => $slug, 194 'sectionName' => $term->name, 195 ); 196 } 197 198 /** 199 * Merge Price Optimization Category fields into a Products Portal API payload. 200 * 201 * @param array $api_data Existing payload 202 * @param array $section Keys sectionKey, sectionName 203 * @return array 204 */ 205 function qixit_apply_price_opt_section_to_api_data($api_data, $section) { 206 if (!is_array($api_data)) { 207 $api_data = array(); 208 } 209 if (!is_array($section)) { 210 return $api_data; 211 } 212 if (!empty($section['sectionKey'])) { 213 $api_data['sectionKey'] = (string) $section['sectionKey']; 214 } 215 if (!empty($section['sectionName'])) { 216 $api_data['sectionName'] = (string) $section['sectionName']; 217 } 127 218 return $api_data; 128 219 } … … 826 917 827 918 /** 828 * Idle seconds before a visitor purchase/payout session expires (default 8hours).919 * Idle seconds before a visitor purchase/payout session expires (default 12 hours). 829 920 * 830 921 * @return int … … 833 924 $hours = defined('QIXIT_VISITOR_SESSION_IDLE_HOURS') 834 925 ? (int) QIXIT_VISITOR_SESSION_IDLE_HOURS 835 : 8;926 : 12; 836 927 return max(1, $hours) * 3600; 837 928 } -
2waymicropay/trunk/js/frontend-script.js
r3621814 r3707420 51 51 } 52 52 53 function mirrorPriceSessionCookie(id) { 54 if (!id || typeof document === 'undefined') { 55 return; 56 } 57 try { 58 var maxAge = 60 * 60 * 24 * 365; 59 document.cookie = 'qixit_price_session=' + encodeURIComponent(id) 60 + '; path=/; max-age=' + maxAge + '; SameSite=Lax'; 61 } catch (e) {} 62 } 63 64 function priceSessionId() { 65 try { 66 var existing = window.localStorage.getItem('qixitPriceSession'); 67 if (existing) { 68 mirrorPriceSessionCookie(existing); 69 return existing; 70 } 71 var id = (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : String(Date.now()); 72 window.localStorage.setItem('qixitPriceSession', id); 73 mirrorPriceSessionCookie(id); 74 return id; 75 } catch (e) { 76 return ''; 77 } 78 } 79 80 function appendPriceSessionParam(targetUrl) { 81 if (!targetUrl || /[?&]psid=/i.test(targetUrl)) { 82 return targetUrl; 83 } 84 var sid = priceSessionId(); 85 if (!sid) { 86 return targetUrl; 87 } 88 var sep = targetUrl.indexOf('?') >= 0 ? '&' : '?'; 89 return targetUrl + sep + 'psid=' + encodeURIComponent(sid); 90 } 91 92 function beaconPrice(guid, kind, eventType, priceVisible) { 93 var base = (typeof qixitFrontend !== 'undefined' && qixitFrontend.productsApiUrl) 94 ? String(qixitFrontend.productsApiUrl).replace(/\/$/, '') 95 : ''; 96 if (!base || !guid) { 97 return; 98 } 99 try { 100 fetch(base + '/api/pricemetrics/beacon', { 101 method: 'POST', 102 headers: { 'Content-Type': 'application/json' }, 103 keepalive: true, 104 body: JSON.stringify({ 105 linkKind: kind, 106 resourceId: guid, 107 eventType: eventType, 108 sessionId: priceSessionId(), 109 sourcePage: window.location.href.split('?')[0], 110 priceVisible: !!priceVisible 111 }) 112 }).catch(function () {}); 113 } catch (e) {} 114 } 115 53 116 /** 54 117 * Append SourcePage so the products portal can store the full publisher page path. … … 60 123 } 61 124 if (/[?&]SourcePage=/i.test(targetUrl)) { 62 return targetUrl;125 return appendPriceSessionParam(targetUrl); 63 126 } 64 127 var pageUrl = window.location.href; … … 68 131 } 69 132 var sep = targetUrl.indexOf('?') >= 0 ? '&' : '?'; 70 return targetUrl + sep + 'SourcePage=' + encodeURIComponent(pageUrl);133 return appendPriceSessionParam(targetUrl + sep + 'SourcePage=' + encodeURIComponent(pageUrl)); 71 134 } 72 135 … … 172 235 var VISITOR_IDLE_HOURS = (window.qixitFrontend && window.qixitFrontend.visitorSessionIdleHours) 173 236 ? parseInt(window.qixitFrontend.visitorSessionIdleHours, 10) 174 : 8;237 : 12; 175 238 if (isNaN(VISITOR_IDLE_HOURS) || VISITOR_IDLE_HOURS < 1) { 176 VISITOR_IDLE_HOURS = 8;239 VISITOR_IDLE_HOURS = 12; 177 240 } 178 241 … … 240 303 /** 241 304 * Cache auth on this publisher site after checkout confirms login/purchase. 242 * Central Qixit session lives on .qixit.com; local cookies are per-site cache only. 305 * Central Qixit session lives on .qixit.com; localStorage cache is only used to 306 * authRelay into third-party iframes (pay-per-play/view overlays). 243 307 * Token is preferred but optional — paywall unlock only needs a valid visitor session. 244 308 */ … … 246 310 if (!userId && !username && !token) return; 247 311 touchVisitorAuthCookies(username || userId || ''); 312 if (!userId || !token) return; 313 try { 314 var now = Date.now(); 315 var endOfDay = new Date(now); 316 endOfDay.setHours(23, 59, 59, 999); 317 localStorage.setItem(AUTH_CACHE_KEY, JSON.stringify({ 318 userId: userId, 319 username: username || '', 320 token: token, 321 ts: now, 322 expiresAt: Math.min(now + (VISITOR_IDLE_HOURS * 3600 * 1000), endOfDay.getTime()) 323 })); 324 } catch (e) { /* localStorage unavailable */ } 248 325 }, 249 326 … … 339 416 340 417 /** 341 * Retrieve cached auth, or null if absent/expired (24 h TTL). 418 * Retrieve cached auth for iframe authRelay, or null if absent/expired. 419 * Matches global SSO window: 12h idle or end of local calendar day. 342 420 */ 343 421 _getCachedAuth: function() { 344 // Intentionally disable per-site auth cache to avoid stale login 345 // state when users switch aliases or logout on another website. 346 return null; 422 try { 423 var raw = localStorage.getItem(AUTH_CACHE_KEY); 424 if (!raw) return null; 425 var data = JSON.parse(raw); 426 if (!data || !data.userId || !data.token) { 427 localStorage.removeItem(AUTH_CACHE_KEY); 428 return null; 429 } 430 var expiresAt = data.expiresAt; 431 if (!expiresAt && data.ts) { 432 var endOfDay = new Date(data.ts); 433 endOfDay.setHours(23, 59, 59, 999); 434 expiresAt = Math.min(data.ts + (VISITOR_IDLE_HOURS * 3600 * 1000), endOfDay.getTime()); 435 } 436 if (!expiresAt || Date.now() > expiresAt) { 437 localStorage.removeItem(AUTH_CACHE_KEY); 438 return null; 439 } 440 return data; 441 } catch (e) { 442 return null; 443 } 347 444 }, 348 445 … … 411 508 this.enhancePurchaseLinks(); 412 509 this.enhanceExistingImageLinks(); 510 this.enhancePayPerPlayPrices(); 511 }, 512 513 /** 514 * Replace a baked-in amount inside a pay-per-play button label. 515 */ 516 replaceAmountInLabel: function(text, priceDisplay) { 517 if (!text || !priceDisplay) { 518 return text; 519 } 520 var next = String(text).replace( 521 /(\$?\d+(?:\.\d+)?\s*(?:¢|cents?|cent)|\$\d+\.\d{2}|\d+\s*¢)/iu, 522 priceDisplay 523 ); 524 return next; 525 }, 526 527 /** 528 * Refresh [qixit_payperplay] labels from the live public product price (A/B aware). 529 */ 530 enhancePayPerPlayPrices: function() { 531 var self = this; 532 $('.qixit-payperplay-button').each(function() { 533 var $button = $(this); 534 var purchaseUrl = ($button.attr('data-purchase-url') || $button.data('purchase-url') || '').toString(); 535 var guid = ($button.attr('data-product-guid') || $button.data('product-guid') || '').toString(); 536 if (!guid) { 537 var pidMatch = purchaseUrl.match(/[?&]PID=([a-f0-9\-]+)/i); 538 guid = pidMatch ? pidMatch[1] : ''; 539 } 540 if (!guid) { 541 return; 542 } 543 544 $button.attr('data-product-guid', guid); 545 $button.closest('.qixit-payperplay-container').attr('data-product-guid', guid); 546 547 var withSession = preparePurchaseUrl(purchaseUrl); 548 if (withSession && withSession !== purchaseUrl) { 549 $button.attr('data-purchase-url', withSession); 550 } 551 552 self.fetchProductPrice(guid, function(price) { 553 if (!price) { 554 return; 555 } 556 // JS returns "3¢" / "$1.00"; also keep a "3 cents" form for the price span. 557 var centsMatch = String(price).match(/^(\d+)¢$/); 558 var priceWords = centsMatch 559 ? (centsMatch[1] + ' cent' + (centsMatch[1] === '1' ? '' : 's')) 560 : price; 561 562 $button.attr('data-price', priceWords); 563 var $container = $button.closest('.qixit-payperplay-container'); 564 $container.find('.qixit-payperplay-price').text(priceWords); 565 566 var label = ($button.attr('data-button-text') || $button.find('.qixit-payperplay-button-text').text() || '').toString(); 567 var updated = self.replaceAmountInLabel(label, price); 568 if (updated && updated !== label) { 569 $button.attr('data-button-text', updated); 570 $button.find('.qixit-payperplay-button-text').text(updated); 571 } else if (!/(\d+\s*¢|\$\d|\d+\s*cent)/i.test(label)) { 572 // Label has no amount — leave button text; price span carries the amount. 573 } 574 }); 575 }); 413 576 }, 414 577 … … 573 736 self._productPriceInflight[productGuid] = [callback]; 574 737 575 var apiUrl = PUBLIC_API_BASE + '/api/products/public/' + productGuid; 738 var sid = priceSessionId(); 739 var apiUrl = PUBLIC_API_BASE + '/api/products/public/' + productGuid 740 + (sid ? ('?session=' + encodeURIComponent(sid)) : ''); 576 741 var finish = function(price, data) { 577 742 self._productPriceCache[productGuid] = { price: price, data: data }; … … 599 764 } 600 765 finish(priceDisplay, data); 766 beaconPrice(productGuid, 'purchase', 'display', true); 601 767 } else { 602 768 finish(null, data || null); … … 680 846 vendorName: data.vendorName || '' 681 847 }); 848 beaconPrice(paymentGuid, 'payout', 'display', true); 682 849 }, 683 850 error: function(xhr) { … … 961 1128 var self = this; 962 1129 1130 $(document).on('click', 'a.qixit-payment-link', function() { 1131 var guid = ($(this).data('payment-guid') || '').toString(); 1132 if (guid) { 1133 beaconPrice(guid, 'payout', 'click', true); 1134 } 1135 }); 1136 963 1137 // Micro-frame trigger click (Read More links, Buy buttons) 964 1138 $(document).on('click', '.qixit-microframe-trigger', function(e) { … … 1029 1203 var containerId = $button.data('container-id'); 1030 1204 var price = $button.data('price'); 1205 var productGuid = ($button.attr('data-product-guid') || $button.data('product-guid') || '').toString(); 1206 if (!productGuid && purchaseUrl) { 1207 var pidMatch = String(purchaseUrl).match(/[?&]PID=([a-f0-9\-]+)/i); 1208 productGuid = pidMatch ? pidMatch[1] : ''; 1209 if (productGuid) { 1210 $button.attr('data-product-guid', productGuid); 1211 } 1212 } 1031 1213 1032 1214 if (!purchaseUrl) { 1033 1215 console.error('Qixit PayPerPlay: No purchase URL provided'); 1034 1216 return; 1217 } 1218 1219 // Ensure sticky A/B session reaches PurchaseFast. 1220 var withSession = preparePurchaseUrl(purchaseUrl); 1221 if (withSession) { 1222 $button.attr('data-purchase-url', withSession); 1035 1223 } 1036 1224 … … 1323 1511 var containerId = $trigger.data('container-id'); 1324 1512 var productGuid = $trigger.data('product-guid') || ''; 1513 if (!productGuid && purchaseUrl) { 1514 var pidFromUrl = String(purchaseUrl).match(/[?&]PID=([a-f0-9\-]+)/i); 1515 productGuid = pidFromUrl ? pidFromUrl[1] : ''; 1516 } 1517 var paymentGuid = $trigger.data('payment-guid') || ''; 1518 if (productGuid) { 1519 beaconPrice(productGuid, 'purchase', 'click', true); 1520 } else if (paymentGuid) { 1521 beaconPrice(paymentGuid, 'payout', 'click', true); 1522 } 1325 1523 var forceFreshLogin = $trigger.data('force-fresh-login') ? true : false; 1326 1524 var isCommentCheckout = $trigger.data('comment-checkout') === true … … 1371 1569 1372 1570 var cachedAuth = forceFreshLogin ? null : this._getCachedAuth(); 1373 var useOverlay = effectivePayPerPlay 1374 || isPayPerViewTrigger 1375 || !!cachedAuth 1376 || forceFreshLogin; 1571 // Iframe overlays cannot read the first-party .qixit.com SSO cookie 1572 // (Chrome partitions third-party storage). Only use an overlay when: 1573 // - we can authRelay a publisher-cached token, or 1574 // - author/force-login must show an inline taller login form. 1575 // Otherwise open a first-party popup so PaymentFast/prior logins persist 1576 // across pages and sites for the documented 8h / end-of-day window. 1577 var useOverlay = forceFreshLogin || !!cachedAuth; 1377 1578 1378 1579 // Author/force-login checkout: center in the viewport so the login … … 1385 1586 1386 1587 if (useOverlay) { 1387 // Pay-per-play and pay-per-view always stay inline on the page.1388 // For regular purchases, use the overlay when auth is cached.1389 1588 this._openIframeOverlay(purchaseUrl, cachedAuth, clickX, clickY, { 1390 1589 forceFreshLogin: forceFreshLogin, … … 1392 1591 }); 1393 1592 } else { 1394 // No cached auth on a regular purchase — need a first-party1395 // po pup so the login persists across sites.1593 // Includes pay-per-play/view without relayable cache — popup still 1594 // postMessages purchaseComplete so in-page reveal keeps working. 1396 1595 this._openPopup(purchaseUrl, clickX, clickY, productGuid); 1397 1596 } -
2waymicropay/trunk/public/class-qixit-frontend.php
r3621814 r3707420 202 202 'visitorSessionIdleHours' => defined('QIXIT_VISITOR_SESSION_IDLE_HOURS') 203 203 ? (int) QIXIT_VISITOR_SESSION_IDLE_HOURS 204 : 8,204 : 12, 205 205 'strings' => array( 206 206 'loading' => __('Loading...', '2waymicropay'), -
2waymicropay/trunk/public/qixit-shortcodes.php
r3621852 r3707420 280 280 * - height: Container height (default: 400px) 281 281 */ 282 /** 283 * Format a dollar amount for pay-per-play / overlay labels. 284 * 285 * @param float $price Amount in dollars. 286 * @return string 287 */ 288 function qixit_format_pay_amount_display($price) { 289 $price = floatval($price); 290 if ($price <= 0) { 291 return ''; 292 } 293 if ($price < 1.00) { 294 $cents = intval(round($price * 100)); 295 return $cents . ' cent' . ($cents != 1 ? 's' : ''); 296 } 297 return '$' . number_format($price, 2); 298 } 299 300 /** 301 * Parse a shortcode price= attribute ("0.02", "2 cents", etc.). 302 * 303 * @param string $raw Raw attribute value. 304 * @return float|null 305 */ 306 function qixit_parse_price_attr($raw) { 307 if ($raw === null || $raw === '') { 308 return null; 309 } 310 $raw = trim((string) $raw); 311 if (preg_match('/^\s*(\d+(?:\.\d+)?)\s*cents?\s*$/i', $raw, $m)) { 312 return floatval($m[1]) / 100.0; 313 } 314 if (preg_match('/^\s*\$?\s*(\d+(?:\.\d+)?)\s*$/', $raw, $m)) { 315 $v = floatval($m[1]); 316 return $v > 0 ? $v : null; 317 } 318 $v = floatval($raw); 319 return $v > 0 ? $v : null; 320 } 321 322 /** 323 * Sticky A/B session id from the first-party cookie (mirrored by frontend JS). 324 * 325 * @return string 326 */ 327 function qixit_price_session_id() { 328 if (empty($_COOKIE['qixit_price_session'])) { 329 return ''; 330 } 331 $sid = sanitize_text_field(wp_unslash($_COOKIE['qixit_price_session'])); 332 // UUID or opaque token; keep conservative length. 333 if ($sid === '' || strlen($sid) > 128 || !preg_match('/^[A-Za-z0-9._\-]+$/', $sid)) { 334 return ''; 335 } 336 return $sid; 337 } 338 282 339 function qixit_payperplay_shortcode($atts, $content = null) { 283 340 qixit_shortcode_require_assets(); … … 304 361 return '<p class="qixit-error">⚠️ ' . __('Pay-Per-Play: Missing placeholder image (image attribute)', '2waymicropay') . '</p>'; 305 362 } 306 307 // Format price for display 308 $price = floatval($atts['price']); 309 if ($price < 0.10) { 310 $cents = intval($price * 100); 311 $price_display = $cents . ' cent' . ($cents != 1 ? 's' : ''); 312 } else { 313 $price_display = '$' . number_format($price, 2); 363 364 // Prefer live catalog / A/B price; shortcode price= is fallback only. 365 $live_price = function_exists('qixit_get_product_price') 366 ? floatval(qixit_get_product_price($atts['pid'])) 367 : 0.0; 368 $fallback_price = qixit_parse_price_attr($atts['price']); 369 $price = $live_price > 0 ? $live_price : ($fallback_price !== null ? $fallback_price : 0.01); 370 $price_display = qixit_format_pay_amount_display($price); 371 if ($price_display === '') { 372 $price_display = qixit_format_pay_amount_display(0.01); 373 } 374 375 // If button_text baked in an old amount (e.g. "Listen Now. 2¢"), refresh it. 376 $button_text = $atts['button_text']; 377 if ($price_display !== '' && preg_match('/(\$?\d+(?:\.\d+)?\s*(?:¢|cents?|cent)|\$\d+\.\d{2}|\d+\s*¢)/iu', $button_text)) { 378 $compact = ($price < 1.00) 379 ? (intval(round($price * 100)) . '¢') 380 : ('$' . number_format($price, 2)); 381 $button_text = preg_replace( 382 '/(\$?\d+(?:\.\d+)?\s*(?:¢|cents?|cent)|\$\d+\.\d{2}|\d+\s*¢)/iu', 383 $compact, 384 $button_text, 385 1 386 ); 314 387 } 315 388 … … 328 401 <div id="<?php echo esc_attr($container_id); ?>" 329 402 class="qixit-payperplay-container" 403 data-product-guid="<?php echo esc_attr($atts['pid']); ?>" 330 404 style="width: <?php echo esc_attr($atts['width']); ?>; height: <?php echo esc_attr($atts['height']); ?>; position: relative;"> 331 405 … … 349 423 data-purchase-url="<?php echo esc_url($purchase_url); ?>" 350 424 data-container-id="<?php echo esc_attr($container_id); ?>" 425 data-product-guid="<?php echo esc_attr($atts['pid']); ?>" 351 426 data-price="<?php echo esc_attr($price_display); ?>" 352 data-button-text="<?php echo esc_attr($ atts['button_text']); ?>"427 data-button-text="<?php echo esc_attr($button_text); ?>" 353 428 data-button-again-text="<?php echo esc_attr($atts['button_again_text']); ?>" 354 429 style="background: linear-gradient(135deg, var(--qixit-brand-start, #52ac5b) 0%, var(--qixit-brand-end, #306817) 100%); … … 357 432 gap: 10px; box-shadow: 0 4px 15px rgba(48, 104, 23, 0.35); 358 433 transition: transform 0.2s, box-shadow 0.2s;"> 359 <span class="qixit-payperplay-button-text"><?php echo esc_html($ atts['button_text']); ?></span>434 <span class="qixit-payperplay-button-text"><?php echo esc_html($button_text); ?></span> 360 435 </button> 361 436 … … 1249 1324 1250 1325 /** 1251 * Helper function to get product price from API (uses public endpoint, no auth required) 1326 * Helper function to get product price from API (uses public endpoint, no auth required). 1327 * Passes sticky A/B session when the qixit_price_session cookie is present so SSR 1328 * matches the JS overlay / checkout arm (avoids catalog-era flash). 1252 1329 */ 1253 1330 function qixit_get_product_price($product_guid) { 1254 // Try to get cached price first 1255 $cache_key = 'qixit_product_price_' . $product_guid; 1331 $session = function_exists('qixit_price_session_id') ? qixit_price_session_id() : ''; 1332 // v2 prefix retires pre-A/B catalog transients (e.g. stale $0.02). 1333 $cache_key = 'qixit_pp_v2_' . md5($product_guid . '|' . ($session !== '' ? $session : 'anon')); 1256 1334 $cached_price = get_transient($cache_key); 1257 1335 1258 1336 if ($cached_price !== false) { 1259 1337 return floatval($cached_price); 1260 1338 } 1261 1339 1262 1340 // Fetch from public API (no authentication required) 1263 1341 try { 1264 1342 $api = Qixit_API::get_instance(); 1265 $result = $api->get_public_product($product_guid );1266 1343 $result = $api->get_public_product($product_guid, $session !== '' ? $session : null); 1344 1267 1345 if ($result['success'] && isset($result['data']['cost'])) { 1268 1346 $price = floatval($result['data']['cost']); 1269 // Cache for 5 minutes 1270 set_transient($cache_key, $price, 5 * MINUTE_IN_SECONDS); 1347 // Short TTL: A/B / Apply can change cost; session arms are sticky via session id. 1348 $ttl = $session !== '' ? MINUTE_IN_SECONDS : 60; 1349 set_transient($cache_key, $price, $ttl); 1271 1350 return $price; 1272 1351 } … … 1277 1356 } 1278 1357 } 1279 1358 1280 1359 return 0; 1281 1360 } -
2waymicropay/trunk/readme.txt
r3645794 r3707420 3 3 Tags: micropayments, paywall, monetization, authors, comments 4 4 Requires at least: 5.0 5 Tested up to: 7.1 5 Tested up to: 7.1.1 6 6 Requires PHP: 7.4 7 Stable tag: 4.1. 77 Stable tag: 4.1.8 8 8 License: GPL-2.0-or-later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 530 530 531 531 == Changelog == 532 533 = 4.1.8 = 534 535 - Syncs Price Optimization Categories to the Vendor portal: WordPress categories on posts/pages (or WP Post / WP Page when none), Paid Comments, and Author Rights. 536 - Clarifies Commercial Code (postage) vs Price Optimization Category; Resync All refreshes Categories on existing products. 537 - Points admins to the Vendor portal Pricing page for metrics and A/B tests (not shown in the plugin). 532 538 533 539 = 4.1.7 = -
2waymicropay/trunk/wp-qixit-modern.php
r3621852 r3707420 4 4 Plugin URI: https://2waymicropay.com/plugin/ 5 5 Description: Monetize your content with secure 2WayMicropay credits, in price points as low as one penny. 6 Version: 4.1. 76 Version: 4.1.8 7 7 Author: 2WayMicropay Team 8 8 Author URI: https://qixit.com/ … … 52 52 53 53 // Plugin version 54 define('QIXIT_VERSION', '4.1. 7');54 define('QIXIT_VERSION', '4.1.8'); 55 55 define('QIXIT_TEXT_DOMAIN', '2waymicropay'); 56 56 … … 131 131 132 132 if (!defined('QIXIT_VISITOR_SESSION_IDLE_HOURS')) { 133 define('QIXIT_VISITOR_SESSION_IDLE_HOURS', 8);133 define('QIXIT_VISITOR_SESSION_IDLE_HOURS', 12); 134 134 } 135 135
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)