Plugin Directory

Changeset 3707420


Ignore:
Timestamp:
09/22/2026 01:18:48 PM (5 weeks ago)
Author:
DReardon
Message:

Update to 4.1.8 to support category tracking for new price optimization feature. See Vendor portal for the metrics.

Location:
2waymicropay
Files:
12 edited

Legend:

Unmodified
Added
Removed
  • 2waymicropay/tags/4.1.7/readme.txt

    r3645800 r3707420  
    33Tags: micropayments, paywall, monetization, authors, comments
    44Requires at least: 5.0
    5 Tested up to: 7.1
     5Tested up to: 7.0
    66Requires PHP: 7.4
    77Stable tag: 4.1.7
  • 2waymicropay/trunk/admin/class-qixit-admin.php

    r3621852 r3707420  
    755755                    <?php wp_nonce_field('qixit_resync_all', 'qixit_resync_nonce'); ?>
    756756                    <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?');">
    758758                        🔄 <?php esc_html_e('Resync All Products & Payouts', '2waymicropay'); ?>
    759759                    </button>
    … …  
    761761                </div>
    762762                <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'); ?>
    764764                </p>
    765765                <div id="qixit-test-results" style="display:none; margin-top:10px;"></div>
    … …  
    828828                    <tr>
    829829                        <th scope="row">
    830                             <label for="qixit_default_commercial_category"><?php esc_html_e('Default Commercial Category', '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>
    831831                        </th>
    832832                        <td>
    … …  
    842842                            </select>
    843843                            <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                                ?>
    845856                            </p>
    846857                            <?php if (empty($default_category)): ?>
    847858                            <p class="description" style="color: #d63638; font-weight: bold;">
    848                                 ⚠️ <?php esc_html_e('You must select a default commercial category before creating premium content.', '2waymicropay'); ?>
     859                                ⚠️ <?php esc_html_e('You must select a default commercial code before creating premium content.', '2waymicropay'); ?>
    849860                            </p>
    850861                            <?php endif; ?>
    … …  
    23802391        }
    23812392       
     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
    23822404        // 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');
    23842406        $message = sprintf(
    23852407            $resync_message_template,
    … …  
    25572579                        <li>View transaction reports and payouts</li>
    25582580                        <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>
    25592582                    </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>
    25602589                </div>
    25612590               
  • 2waymicropay/trunk/admin/class-qixit-metabox.php

    r3621852 r3707420  
    485485               
    486486                <p>
    487                     <label for="qixit_commercial_category"><strong><?php esc_html_e('Commercial Category:', '2waymicropay'); ?></strong></label>
     487                    <label for="qixit_commercial_category"><strong><?php esc_html_e('Commercial Code:', '2waymicropay'); ?></strong></label>
    488488                    <select id="qixit_commercial_category" name="qixit_commercial_category" class="widefat">
    489489                        <?php foreach ($commercial_categories as $value => $label): ?>
    … …  
    505505                        ?>
    506506                    </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>
    507508                </p>
    508509               
    … …  
    936937       
    937938        // Category change action
    938         $bulk_actions['qixit_change_category'] = __('2WayMicropay: Change Commercial Category', '2waymicropay');
     939        $bulk_actions['qixit_change_category'] = __('2WayMicropay: Change Commercial Code', '2waymicropay');
    939940       
    940941        // Paid comments actions
    … …  
    13151316    public function add_qixit_column($columns) {
    13161317        $columns['qixit_status'] = __('2WayMicropay', '2waymicropay');
    1317         $columns['qixit_category'] = __('Category', '2waymicropay');
     1318        $columns['qixit_category'] = __('Commercial Code', '2waymicropay');
    13181319        return $columns;
    13191320    }
  • 2waymicropay/trunk/includes/class-qixit-api.php

    r3621852 r3707420  
    183183     * @return array Response with product data (cost, description, accessHours, etc.)
    184184     */
    185     public function get_public_product($product_guid) {
     185    public function get_public_product($product_guid, $session = null) {
    186186        // Request-level cache: the same product GUID is often fetched twice per
    187187        // page (once in has_user_purchased, once in render_paywall). Avoid the
    188188        // 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;
    189192        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;
    195202        return $result;
    196203    }
    … …  
    423430            'deliveryUrl' => isset($product_data['deliveryUrl']) ? $product_data['deliveryUrl'] : '',
    424431            '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,
    425434            'affiliateUserId' => isset($product_data['affiliateUserId']) ? $product_data['affiliateUserId'] : '',
    426435            'affiliatePercentage' => isset($product_data['affiliatePercentage']) ? intval($product_data['affiliatePercentage']) : 0,
    … …  
    436445            'accessHours' => isset($product_data['accessHours']) ? intval($product_data['accessHours']) : -1
    437446        );
     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        }
    438454       
    439455        return $this->request('api/products', 'POST', $data);
    … …  
    454470            'deliveryUrl' => isset($product_data['deliveryUrl']) ? $product_data['deliveryUrl'] : '',
    455471            '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,
    456474            'affiliateUserId' => isset($product_data['affiliateUserId']) ? $product_data['affiliateUserId'] : '',
    457475            'affiliatePercentage' => isset($product_data['affiliatePercentage']) ? intval($product_data['affiliatePercentage']) : 0,
    … …  
    467485            'accessHours' => isset($product_data['accessHours']) ? intval($product_data['accessHours']) : -1
    468486        );
     487        if ($data['sectionKey'] === null || $data['sectionKey'] === '') {
     488            unset($data['sectionKey']);
     489        }
     490        if ($data['sectionName'] === null || $data['sectionName'] === '') {
     491            unset($data['sectionName']);
     492        }
    469493       
    470494        return $this->request('api/products/' . $product_guid, 'PUT', $data);
  • 2waymicropay/trunk/includes/class-qixit-author-commerce.php

    r3621852 r3707420  
    102102        if (empty($category)) {
    103103            $result['success'] = false;
    104             $result['errors'][] = __('Default Commercial Category is 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');
    105105        }
    106106
    … …  
    662662            'newBrowser' => 0,
    663663        );
     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        }
    664667        if (function_exists('qixit_apply_forwarder_to_api_data')) {
    665668            $api_data = qixit_apply_forwarder_to_api_data($api_data);
    … …  
    759762            'newBrowser' => 0,
    760763        );
     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        }
    761767        if (function_exists('qixit_apply_forwarder_to_api_data')) {
    762768            $api_data = qixit_apply_forwarder_to_api_data($api_data);
    … …  
    848854            'newBrowser' => 0,
    849855        );
     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        }
    850859        if (function_exists('qixit_apply_forwarder_to_api_data')) {
    851860            $api_data = qixit_apply_forwarder_to_api_data($api_data);
    … …  
    20572066            'newBrowser' => 0,
    20582067        );
     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        }
    20592071        if (function_exists('qixit_apply_forwarder_to_api_data')) {
    20602072            $api_data = qixit_apply_forwarder_to_api_data($api_data);
  • 2waymicropay/trunk/includes/class-qixit-product.php

    r3621852 r3707420  
    790790            'reportUrl' => $report_url  // Server-to-server callback for purchase tracking
    791791        );
     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        }
    792795        if (!empty($author_split)) {
    793796            $api_data = array_merge($api_data, $author_split);
    … …  
    950953            'reportUrl' => $report_url  // Server-to-server callback for purchase tracking
    951954        );
     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        }
    952958        if ($use_author_comment_share && !empty($author_comment_split)) {
    953959            $api_data = array_merge($api_data, $author_comment_split);
    … …  
    11891195            'newBrowser' => 0  // Redirect in same window to unlock content
    11901196        );
     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        }
    11911200
    11921201        // Keep author split payload consistent on save/update operations.
  • 2waymicropay/trunk/includes/qixit-functions.php

    r3621814 r3707420  
    125125    $api_data['forwardPercentage'] = qixit_get_forwarder_percent();
    126126    $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 */
     137function 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 */
     164function 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 */
     205function 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    }
    127218    return $api_data;
    128219}
    … …  
    826917
    827918/**
    828  * Idle seconds before a visitor purchase/payout session expires (default 8 hours).
     919 * Idle seconds before a visitor purchase/payout session expires (default 12 hours).
    829920 *
    830921 * @return int
    … …  
    833924    $hours = defined('QIXIT_VISITOR_SESSION_IDLE_HOURS')
    834925        ? (int) QIXIT_VISITOR_SESSION_IDLE_HOURS
    835         : 8;
     926        : 12;
    836927    return max(1, $hours) * 3600;
    837928}
  • 2waymicropay/trunk/js/frontend-script.js

    r3621814 r3707420  
    5151    }
    5252
     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
    53116    /**
    54117     * Append SourcePage so the products portal can store the full publisher page path.
    … …  
    60123        }
    61124        if (/[?&]SourcePage=/i.test(targetUrl)) {
    62             return targetUrl;
     125            return appendPriceSessionParam(targetUrl);
    63126        }
    64127        var pageUrl = window.location.href;
    … …  
    68131        }
    69132        var sep = targetUrl.indexOf('?') >= 0 ? '&' : '?';
    70         return targetUrl + sep + 'SourcePage=' + encodeURIComponent(pageUrl);
     133        return appendPriceSessionParam(targetUrl + sep + 'SourcePage=' + encodeURIComponent(pageUrl));
    71134    }
    72135
    … …  
    172235    var VISITOR_IDLE_HOURS = (window.qixitFrontend && window.qixitFrontend.visitorSessionIdleHours)
    173236        ? parseInt(window.qixitFrontend.visitorSessionIdleHours, 10)
    174         : 8;
     237        : 12;
    175238    if (isNaN(VISITOR_IDLE_HOURS) || VISITOR_IDLE_HOURS < 1) {
    176         VISITOR_IDLE_HOURS = 8;
     239        VISITOR_IDLE_HOURS = 12;
    177240    }
    178241
    … …  
    240303        /**
    241304         * 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).
    243307         * Token is preferred but optional — paywall unlock only needs a valid visitor session.
    244308         */
    … …  
    246310            if (!userId && !username && !token) return;
    247311            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 */ }
    248325        },
    249326
    … …  
    339416
    340417        /**
    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.
    342420         */
    343421        _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            }
    347444        },
    348445
    … …  
    411508            this.enhancePurchaseLinks();
    412509            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            });
    413576        },
    414577       
    … …  
    573736            self._productPriceInflight[productGuid] = [callback];
    574737
    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)) : '');
    576741            var finish = function(price, data) {
    577742                self._productPriceCache[productGuid] = { price: price, data: data };
    … …  
    599764                        }
    600765                        finish(priceDisplay, data);
     766                        beaconPrice(productGuid, 'purchase', 'display', true);
    601767                    } else {
    602768                        finish(null, data || null);
    … …  
    680846                        vendorName: data.vendorName || ''
    681847                    });
     848                    beaconPrice(paymentGuid, 'payout', 'display', true);
    682849                },
    683850                error: function(xhr) {
    … …  
    9611128            var self = this;
    9621129           
     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
    9631137            // Micro-frame trigger click (Read More links, Buy buttons)
    9641138            $(document).on('click', '.qixit-microframe-trigger', function(e) {
    … …  
    10291203            var containerId = $button.data('container-id');
    10301204            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            }
    10311213           
    10321214            if (!purchaseUrl) {
    10331215                console.error('Qixit PayPerPlay: No purchase URL provided');
    10341216                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);
    10351223            }
    10361224           
    … …  
    13231511            var containerId = $trigger.data('container-id');
    13241512            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            }
    13251523            var forceFreshLogin = $trigger.data('force-fresh-login') ? true : false;
    13261524            var isCommentCheckout = $trigger.data('comment-checkout') === true
    … …  
    13711569
    13721570            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;
    13771578
    13781579            // Author/force-login checkout: center in the viewport so the login
    … …  
    13851586
    13861587            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.
    13891588                this._openIframeOverlay(purchaseUrl, cachedAuth, clickX, clickY, {
    13901589                    forceFreshLogin: forceFreshLogin,
    … …  
    13921591                });
    13931592            } else {
    1394                 // No cached auth on a regular purchase — need a first-party
    1395                 // popup 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.
    13961595                this._openPopup(purchaseUrl, clickX, clickY, productGuid);
    13971596            }
  • 2waymicropay/trunk/public/class-qixit-frontend.php

    r3621814 r3707420  
    202202            'visitorSessionIdleHours' => defined('QIXIT_VISITOR_SESSION_IDLE_HOURS')
    203203                ? (int) QIXIT_VISITOR_SESSION_IDLE_HOURS
    204                 : 8,
     204                : 12,
    205205            'strings' => array(
    206206                'loading' => __('Loading...', '2waymicropay'),
  • 2waymicropay/trunk/public/qixit-shortcodes.php

    r3621852 r3707420  
    280280 * - height: Container height (default: 400px)
    281281 */
     282/**
     283 * Format a dollar amount for pay-per-play / overlay labels.
     284 *
     285 * @param float $price Amount in dollars.
     286 * @return string
     287 */
     288function 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 */
     306function 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 */
     327function 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
    282339function qixit_payperplay_shortcode($atts, $content = null) {
    283340    qixit_shortcode_require_assets();
    … …  
    304361        return '<p class="qixit-error">⚠️ ' . __('Pay-Per-Play: Missing placeholder image (image attribute)', '2waymicropay') . '</p>';
    305362    }
    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        );
    314387    }
    315388   
    … …  
    328401    <div id="<?php echo esc_attr($container_id); ?>"
    329402         class="qixit-payperplay-container"
     403         data-product-guid="<?php echo esc_attr($atts['pid']); ?>"
    330404         style="width: <?php echo esc_attr($atts['width']); ?>; height: <?php echo esc_attr($atts['height']); ?>; position: relative;">
    331405       
    … …  
    349423                       data-purchase-url="<?php echo esc_url($purchase_url); ?>"
    350424                       data-container-id="<?php echo esc_attr($container_id); ?>"
     425                       data-product-guid="<?php echo esc_attr($atts['pid']); ?>"
    351426                       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); ?>"
    353428                       data-button-again-text="<?php echo esc_attr($atts['button_again_text']); ?>"
    354429                       style="background: linear-gradient(135deg, var(--qixit-brand-start, #52ac5b) 0%, var(--qixit-brand-end, #306817) 100%);
    … …  
    357432                              gap: 10px; box-shadow: 0 4px 15px rgba(48, 104, 23, 0.35);
    358433                              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>
    360435                </button>
    361436               
    … …  
    12491324
    12501325/**
    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).
    12521329 */
    12531330function 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'));
    12561334    $cached_price = get_transient($cache_key);
    1257    
     1335
    12581336    if ($cached_price !== false) {
    12591337        return floatval($cached_price);
    12601338    }
    1261    
     1339
    12621340    // Fetch from public API (no authentication required)
    12631341    try {
    12641342        $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
    12671345        if ($result['success'] && isset($result['data']['cost'])) {
    12681346            $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);
    12711350            return $price;
    12721351        }
    … …  
    12771356        }
    12781357    }
    1279    
     1358
    12801359    return 0;
    12811360}
  • 2waymicropay/trunk/readme.txt

    r3645794 r3707420  
    33Tags: micropayments, paywall, monetization, authors, comments
    44Requires at least: 5.0
    5 Tested up to: 7.1
     5Tested up to: 7.1.1
    66Requires PHP: 7.4
    7 Stable tag: 4.1.7
     7Stable tag: 4.1.8
    88License: GPL-2.0-or-later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    530530
    531531== 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).
    532538
    533539= 4.1.7 =
  • 2waymicropay/trunk/wp-qixit-modern.php

    r3621852 r3707420  
    44Plugin URI: https://2waymicropay.com/plugin/
    55Description: Monetize your content with secure 2WayMicropay credits, in price points as low as one penny.
    6 Version: 4.1.7
     6Version: 4.1.8
    77Author: 2WayMicropay Team
    88Author URI: https://qixit.com/
    … …  
    5252
    5353// Plugin version
    54 define('QIXIT_VERSION', '4.1.7');
     54define('QIXIT_VERSION', '4.1.8');
    5555define('QIXIT_TEXT_DOMAIN', '2waymicropay');
    5656
    … …  
    131131
    132132if (!defined('QIXIT_VISITOR_SESSION_IDLE_HOURS')) {
    133     define('QIXIT_VISITOR_SESSION_IDLE_HOURS', 8);
     133    define('QIXIT_VISITOR_SESSION_IDLE_HOURS', 12);
    134134}
    135135
Note: See TracChangeset for help on using the changeset viewer.