Plugin Directory

Changeset 3446766


Ignore:
Timestamp:
01/26/2026 12:24:01 AM (2 months ago)
Author:
imgpro
Message:

Update to version 1.0

Location:
bandwidth-saver
Files:
1 deleted
13 edited

Legend:

Unmodified
Added
Removed
  • bandwidth-saver/trunk/admin/css/imgpro-cdn-admin.css

    r3419619 r3446766  
    20022002   ========================================================================== */
    20032003
    2004 /* Base account card - extends .imgpro-card */
     2004/* Base account card */
    20052005.imgpro-account-card {
    2006     margin-bottom: var(--imgpro-space-6);
    2007 }
    2008 
    2009 /* Account card main content area */
    2010 .imgpro-account-card__main {
    2011     display: flex;
    2012     align-items: center;
    2013     justify-content: space-between;
    2014     gap: var(--imgpro-space-4);
    2015     padding: var(--imgpro-space-5) var(--imgpro-space-6);
    2016 }
    2017 
    2018 /* Account card content */
    2019 .imgpro-account-card__content {
    2020     display: flex;
    2021     flex-direction: column;
    2022     gap: var(--imgpro-space-1);
    2023     flex: 1;
    2024     min-width: 0;
    2025 }
    2026 
    2027 /* Free tier - upsell variant */
    2028 .imgpro-account-card--free {
    2029     background: var(--imgpro-bg);
    2030     border: 1px solid var(--imgpro-border);
    2031     border-radius: var(--imgpro-radius-lg);
    2032     box-shadow: var(--imgpro-card-shadow);
    2033     overflow: hidden;
    2034 }
    2035 
    2036 .imgpro-account-card__headline {
    2037     font-size: var(--imgpro-text-base);
    2038     font-weight: 600;
    2039     color: var(--imgpro-text);
    2040 }
    2041 
    2042 .imgpro-account-card__description {
    2043     font-size: var(--imgpro-text-sm);
    2044     color: var(--imgpro-text-secondary);
    2045 }
    2046 
    2047 /* Paid tier variant */
    2048 .imgpro-account-card--paid {
    20492006    display: flex;
    20502007    flex-direction: column;
     
    20542011    box-shadow: var(--imgpro-card-shadow);
    20552012    overflow: hidden;
    2056 }
    2057 
    2058 .imgpro-account-card--paid .imgpro-account-card__content {
     2013    margin-bottom: var(--imgpro-space-6);
     2014}
     2015
     2016/* Active subscription variant - subtle green accent */
     2017.imgpro-account-card--active {
     2018    border-color: rgba(16, 185, 129, 0.3);
     2019}
     2020
     2021/* Account card main content area */
     2022.imgpro-account-card__main {
     2023    display: flex;
     2024    align-items: center;
     2025    justify-content: space-between;
     2026    gap: var(--imgpro-space-4);
     2027    padding: var(--imgpro-space-5) var(--imgpro-space-6);
     2028}
     2029
     2030/* Account card content */
     2031.imgpro-account-card__content {
     2032    display: flex;
     2033    flex-direction: column;
    20592034    gap: var(--imgpro-space-2);
    2060 }
    2061 
    2062 /* Plan display */
    2063 .imgpro-account-card__plan {
     2035    flex: 1;
     2036    min-width: 0;
     2037}
     2038
     2039/* Status display (for active subscriptions) */
     2040.imgpro-account-card__status {
    20642041    display: flex;
    20652042    align-items: center;
     
    20672044}
    20682045
    2069 .imgpro-account-card__tier {
    2070     font-size: var(--imgpro-text-lg);
    2071     font-weight: 700;
    2072     color: var(--imgpro-text);
    2073 }
    2074 
    2075 .imgpro-account-card__plan-label {
    2076     font-size: var(--imgpro-text-lg);
    2077     font-weight: 400;
    2078     color: var(--imgpro-text-muted);
    2079 }
    2080 
    2081 /* Limits row */
    2082 .imgpro-account-card__limits {
    2083     display: flex;
    2084     align-items: center;
    2085     flex-wrap: wrap;
    2086     gap: var(--imgpro-space-2);
    2087 }
    2088 
    2089 .imgpro-account-card__limit {
     2046.imgpro-account-card__status-icon {
     2047    flex-shrink: 0;
     2048    width: 20px;
     2049    height: 20px;
     2050}
     2051
     2052.imgpro-account-card__status-text {
     2053    font-size: var(--imgpro-text-base);
     2054    font-weight: 600;
     2055    color: #059669;
     2056}
     2057
     2058/* Headline (for pending subscriptions) */
     2059.imgpro-account-card__headline {
     2060    font-size: var(--imgpro-text-base);
     2061    font-weight: 600;
     2062    color: var(--imgpro-text);
     2063}
     2064
     2065/* Description */
     2066.imgpro-account-card__description {
    20902067    font-size: var(--imgpro-text-sm);
    20912068    color: var(--imgpro-text-secondary);
     2069    line-height: 1.5;
    20922070}
    20932071
     
    21072085    font-size: var(--imgpro-text-sm);
    21082086    color: var(--imgpro-text-muted);
     2087}
     2088
     2089/* Price display in footer */
     2090.imgpro-account-card__price {
     2091    font-weight: 500;
     2092    color: var(--imgpro-text-secondary);
    21092093}
    21102094
     
    21152099        align-items: stretch;
    21162100        text-align: center;
     2101        gap: var(--imgpro-space-4);
     2102    }
     2103
     2104    .imgpro-account-card__status {
     2105        justify-content: center;
    21172106    }
    21182107
    21192108    .imgpro-account-card__footer {
    21202109        justify-content: center;
    2121     }
    2122 
    2123     .imgpro-account-card__limits {
    2124         justify-content: center;
     2110        flex-wrap: wrap;
    21252111    }
    21262112
    21272113    .imgpro-account-card__actions {
    21282114        align-self: center;
     2115        width: 100%;
     2116    }
     2117
     2118    .imgpro-account-card__actions .imgpro-btn {
     2119        width: 100%;
     2120        justify-content: center;
    21292121    }
    21302122}
  • bandwidth-saver/trunk/admin/js/imgpro-cdn-admin.js

    r3419619 r3446766  
    285285        });
    286286
    287         // Direct upgrade to next tier (from account card) - show confirmation modal
    288         $(document).on('click', '.imgpro-direct-upgrade', function(e) {
    289             e.preventDefault();
    290             const tierId = $(this).data('tier');
    291             if (tierId) {
    292                 showUpgradeConfirmModal(tierId, $(this));
    293             }
    294         });
    295 
    296         // Upgrade confirmation modal handlers
    297         initUpgradeConfirmModal();
     287        // Legacy: Direct upgrade handler (kept for backwards compatibility, no longer used in new UI)
    298288
    299289        // Advanced settings accordion
     
    456446
    457447        const originalText = $button.text();
    458         // Get tier from parameter, button data attribute, or default to 'pro'
    459         const tier = tierId || $button.data('tier') || 'pro';
     448        // Get tier from parameter, button data attribute, or default to 'unlimited'
     449        const tier = tierId || $button.data('tier') || 'unlimited';
    460450        $button.addClass('is-loading').prop('disabled', true).text(imgproCdnAdmin.i18n.creatingCheckout);
    461451
     
    10621052        });
    10631053
    1064         // Upgrade link - handle based on action type
     1054        // Subscription link - open plan modal or manage subscription
    10651055        $(document).off('click', '#imgpro-source-urls-upgrade').on('click', '#imgpro-source-urls-upgrade', function(e) {
    10661056            e.preventDefault();
    10671057            var action = $(this).data('action');
    10681058
    1069             if (action === 'see-options') {
    1070                 // Free tier: open plan selector modal
     1059            if (action === 'manage') {
     1060                // Paid: open manage subscription
     1061                window.open(imgproCdnAdmin.manageUrl, '_blank');
     1062            } else {
     1063                // Not paid: open plan selector modal
    10711064                openPlanModal();
    1072             } else if (action === 'upgrade-to-next') {
    1073                 // Paid tier: direct upgrade to next tier
    1074                 var nextTier = $(this).data('next-tier');
    1075                 if (nextTier) {
    1076                     showUpgradeConfirmModal(nextTier, $(this));
    1077                 }
    1078             } else if (action === 'manage') {
    1079                 // Business tier: open manage subscription
    1080                 window.open(imgproCdnAdmin.manageUrl, '_blank');
    10811065            }
    10821066        });
     
    11651149        }
    11661150
    1167         // Check if at limit
    1168         var atLimit = count >= maxDomains;
     1151        // Check if at limit (single-tier model: unlimited for all, but check just in case)
     1152        var atLimit = maxDomains > 0 && count >= maxDomains;
    11691153
    11701154        // Show/hide input based on limit
     
    11721156            $inputWrapper.hide();
    11731157
    1174             // Get tier info from section data attributes
     1158            // Get paid status from section
    11751159            var $section = $('#imgpro-source-urls-section');
    1176             var tier = $section.data('tier');
    1177             var nextTier = $section.data('next-tier');
    1178             var nextTierName = $section.data('next-tier-name');
    1179 
    1180             // Set link text and action based on tier
    1181             var linkText = '';
    1182             var action = '';
    1183 
    1184             if (tier === 'free') {
    1185                 linkText = 'See upgrade options';
    1186                 action = 'see-options';
    1187             } else if (tier === 'business') {
    1188                 linkText = 'Manage Subscription';
    1189                 action = 'manage';
    1190             } else if (nextTier) {
    1191                 linkText = 'Upgrade to ' + nextTierName;
    1192                 action = 'upgrade-to-next';
    1193                 $upgradeLink.attr('data-next-tier', nextTier);
    1194             }
    1195 
    1196             // Only show upgrade link if we have a valid action
    1197             if (action) {
    1198                 $upgradeLink.attr('data-action', action).find('strong').text(linkText);
    1199                 $upgradeLink.show();
    1200             } else {
    1201                 $upgradeLink.hide();
    1202             }
     1160            var isPaid = $section.data('is-paid');
     1161
     1162            // Set link text and action based on payment status
     1163            var linkText = isPaid ? 'Manage Subscription' : 'Activate Subscription';
     1164            var action = isPaid ? 'manage' : 'activate';
     1165
     1166            $upgradeLink.attr('data-action', action).find('strong').text(linkText);
     1167            $upgradeLink.show();
    12031168        } else {
    12041169            $inputWrapper.show();
     
    13611326        $(document).on('click', '#imgpro-plan-checkout', function(e) {
    13621327            e.preventDefault();
    1363             if (selectedTierId) {
    1364                 handlePlanCheckout($(this), selectedTierId);
     1328            // Use selectedTierId if set, otherwise fall back to button's data-tier-id
     1329            var tierId = selectedTierId || $(this).data('tier-id');
     1330            if (tierId) {
     1331                handlePlanCheckout($(this), tierId);
    13651332            }
    13661333        });
     
    13691336    /**
    13701337     * Initialize default plan selection
     1338     * Single-tier model: only one plan card exists
    13711339     */
    13721340    function initDefaultSelection() {
     
    13761344        if (!$selector.length) return;
    13771345
    1378         const currentTier = $selector.data('current-tier') || '';
    1379 
    1380         // If user is on free tier or no tier, pre-select Pro
    1381         if (!currentTier || currentTier === 'free') {
    1382             // Try highlighted card first, then Pro by ID, then first available
    1383             let $cardToSelect = $selector.find('.imgpro-plan-card--highlight').first();
    1384 
    1385             if (!$cardToSelect.length) {
    1386                 $cardToSelect = $selector.find('.imgpro-plan-card[data-tier-id="pro"]').first();
    1387             }
    1388 
    1389             if (!$cardToSelect.length || $cardToSelect.hasClass('imgpro-plan-card--current')) {
    1390                 $cardToSelect = $selector.find('.imgpro-plan-card:not(.imgpro-plan-card--current)').first();
    1391             }
    1392 
    1393             if ($cardToSelect.length) {
    1394                 selectPlanCard($cardToSelect, true);
    1395             }
     1346        // Select the single plan card if it exists
     1347        const $card = $selector.find('.imgpro-plan-card').first();
     1348        if ($card.length) {
     1349            selectPlanCard($card, true);
    13961350        }
    13971351    }
     
    14041358        if (!$modal.length) return;
    14051359
    1406         $modal.fadeIn(200, function() {
    1407             // Pre-select Pro tier when modal opens
    1408             const $proCard = $modal.find('.imgpro-plan-card[data-tier-id="pro"]');
    1409             if ($proCard.length && !$proCard.hasClass('imgpro-plan-card--current')) {
    1410                 selectPlanCard($proCard, true);
    1411             }
    1412         });
     1360        $modal.fadeIn(200);
    14131361        $('body').addClass('imgpro-modal-open');
    14141362    }
     
    14211369        $modal.fadeOut(200);
    14221370        $('body').removeClass('imgpro-modal-open');
    1423     }
    1424 
    1425     // ===== Upgrade Confirmation Modal =====
    1426 
    1427     let pendingUpgradeTierId = null;
    1428     let pendingUpgradeButton = null;
    1429 
    1430     /**
    1431      * Initialize upgrade confirmation modal handlers
    1432      */
    1433     function initUpgradeConfirmModal() {
    1434         const $modal = $('#imgpro-upgrade-confirm-modal');
    1435         if (!$modal.length) return;
    1436 
    1437         // Cancel button
    1438         $('#imgpro-upgrade-cancel').off('click').on('click', function() {
    1439             closeUpgradeConfirmModal();
    1440         });
    1441 
    1442         // Confirm button
    1443         $('#imgpro-upgrade-confirm').off('click').on('click', function() {
    1444             if (pendingUpgradeTierId) {
    1445                 const $btn = $(this);
    1446                 $btn.addClass('is-loading').prop('disabled', true);
    1447                 $('#imgpro-upgrade-cancel').prop('disabled', true);
    1448                 handlePlanCheckout($btn, pendingUpgradeTierId);
    1449             }
    1450         });
    1451 
    1452         // Close on backdrop click
    1453         $modal.find('.imgpro-confirm-modal__backdrop').off('click').on('click', function() {
    1454             closeUpgradeConfirmModal();
    1455         });
    1456     }
    1457 
    1458     /**
    1459      * Show upgrade confirmation modal
    1460      */
    1461     function showUpgradeConfirmModal(tierId, $triggerButton) {
    1462         const $modal = $('#imgpro-upgrade-confirm-modal');
    1463         if (!$modal.length) return;
    1464 
    1465         // Store pending upgrade info
    1466         pendingUpgradeTierId = tierId;
    1467         pendingUpgradeButton = $triggerButton;
    1468 
    1469         // Get tier data from localized script
    1470         const tiers = imgproCdnAdmin.tiers || {};
    1471         const currentTierId = imgproCdnAdmin.tier;
    1472         const currentTier = tiers[currentTierId];
    1473         const newTier = tiers[tierId];
    1474 
    1475         // Update new plan info
    1476         const tierName = newTier?.name || tierId.charAt(0).toUpperCase() + tierId.slice(1);
    1477         const tierPrice = newTier?.price?.formatted || '';
    1478         const tierPeriod = newTier?.price?.period || '/mo';
    1479 
    1480         $('#imgpro-confirm-tier-name').text(tierName);
    1481         $('#imgpro-confirm-tier-price-amount').text(tierPrice);
    1482         $('#imgpro-confirm-tier-price-period').text(tierPeriod);
    1483         $('#imgpro-confirm-btn-tier').text(tierName);
    1484 
    1485         // Build multiplier hero
    1486         const multiplier = calculateBandwidthMultiplier(currentTier, newTier);
    1487         const newBandwidth = newTier?.limits?.bandwidth?.formatted || '';
    1488         const currentBandwidth = currentTier?.limits?.bandwidth?.formatted || '100 GB';
    1489 
    1490         $('#imgpro-confirm-multiplier').text(multiplier + ' more bandwidth');
    1491         $('#imgpro-confirm-comparison').text(newBandwidth + '/mo (vs ' + currentBandwidth + ')');
    1492 
    1493         // Build checklist
    1494         $('#imgpro-confirm-checklist').html(buildChecklistHtml(newTier));
    1495 
    1496         // Reset button states
    1497         const $confirmBtn = $('#imgpro-upgrade-confirm');
    1498         $confirmBtn.removeClass('is-loading').prop('disabled', false);
    1499         $('#imgpro-upgrade-cancel').prop('disabled', false);
    1500 
    1501         // Show modal
    1502         $modal.fadeIn(200);
    1503         $('body').addClass('imgpro-modal-open');
    1504     }
    1505 
    1506     /**
    1507      * Calculate bandwidth multiplier between tiers
    1508      */
    1509     function calculateBandwidthMultiplier(currentTier, newTier) {
    1510         const currentBytes = currentTier?.limits?.bandwidth?.bytes || 107374182400; // 100 GB default
    1511         const newBytes = newTier?.limits?.bandwidth?.bytes || currentBytes;
    1512 
    1513         if (newTier?.limits?.bandwidth?.unlimited) {
    1514             return 'Unlimited';
    1515         }
    1516 
    1517         const multiplier = Math.round(newBytes / currentBytes);
    1518         return multiplier + 'x';
    1519     }
    1520 
    1521     /**
    1522      * Build HTML for checklist (custom domain, priority support)
    1523      */
    1524     function buildChecklistHtml(tier) {
    1525         if (!tier) return '';
    1526 
    1527         const checkIcon = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M13.333 4L6 11.333 2.667 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
    1528         const items = [];
    1529 
    1530         if (tier.features?.custom_domain) {
    1531             items.push('<li>' + checkIcon + 'Custom CDN domain</li>');
    1532         }
    1533 
    1534         if (tier.features?.priority_support) {
    1535             items.push('<li>' + checkIcon + 'Priority support</li>');
    1536         }
    1537 
    1538         return items.join('');
    1539     }
    1540 
    1541     /**
    1542      * Close upgrade confirmation modal
    1543      */
    1544     function closeUpgradeConfirmModal() {
    1545         const $modal = $('#imgpro-upgrade-confirm-modal');
    1546         $modal.fadeOut(200);
    1547         $('body').removeClass('imgpro-modal-open');
    1548 
    1549         // Clear pending upgrade
    1550         pendingUpgradeTierId = null;
    1551         pendingUpgradeButton = null;
    15521371    }
    15531372
     
    16141433                        // Subscription upgraded directly - show success and reload
    16151434                        closePlanModal();
    1616                         closeUpgradeConfirmModal();
    16171435                        showNotice('success', response.data.message || imgproCdnAdmin.i18n.subscriptionUpgraded);
    16181436                        setTimeout(function() {
     
    16251443                    $button.removeClass('is-loading').prop('disabled', false);
    16261444                    closePlanModal();
    1627                     closeUpgradeConfirmModal();
    16281445                    // Account exists - show verification modal with pending tier
    16291446                    if (response.data.show_recovery) {
     
    16371454                $button.removeClass('is-loading').prop('disabled', false);
    16381455                closePlanModal();
    1639                 closeUpgradeConfirmModal();
    16401456                var message = status === 'timeout' ? imgproCdnAdmin.i18n.timeoutError : imgproCdnAdmin.i18n.genericError;
    16411457                showNotice('error', message);
     
    16611477            window.history.replaceState({}, '', url.toString());
    16621478
    1663             // Show success notice and upgrade modal
     1479            // Show success notice and open plan selector modal
    16641480            showNotice('success', imgproCdnAdmin.i18n.accountRecovered || 'Account recovered!');
    16651481            setTimeout(function() {
    1666                 showUpgradeConfirmModal(upgradeTier, null);
     1482                openPlanModal();
    16671483            }, 300);
    16681484        }
     
    18631679
    18641680    /**
    1865      * Update insights cards with data
     1681     * Update insights cards with data from API
    18661682     */
    18671683    function updateInsights(data) {
    1868         // Total Requests
    1869         const totalRequests = data.total_requests !== undefined ? data.total_requests : null;
    1870         const $totalReqCard = $('#imgpro-stat-total-requests');
    1871         if ($totalReqCard.length) {
    1872             if (totalRequests !== null) {
    1873                 $totalReqCard.text(totalRequests.toLocaleString());
    1874             } else {
    1875                 $totalReqCard.text('—');
    1876             }
     1684        // === Top Row: Quick Stats (last 7 days) ===
     1685
     1686        // Total Requests (last 7 days)
     1687        var totalRequests = data.total_requests;
     1688        if (totalRequests != null) {
     1689            $('#imgpro-stat-total-requests').text(totalRequests.toLocaleString());
    18771690        }
    18781691
    18791692        // Cached (cache hits)
    1880         const cached = data.cache_hits !== undefined ? data.cache_hits : null;
    1881         const $cachedCard = $('#imgpro-stat-cached');
    1882         if ($cachedCard.length) {
    1883             if (cached !== null) {
    1884                 $cachedCard.text(cached.toLocaleString());
    1885             } else {
    1886                 $cachedCard.text('—');
    1887             }
     1693        var cached = data.cache_hits;
     1694        if (cached != null) {
     1695            $('#imgpro-stat-cached').text(cached.toLocaleString());
    18881696        }
    18891697
    18901698        // CDN Hit Rate
    1891         const cacheHitRate = data.cache_hit_rate !== undefined ? data.cache_hit_rate : null;
    1892         const $cacheHitCard = $('#imgpro-stat-cache-hit-rate');
    1893         if ($cacheHitCard.length) {
    1894             if (cacheHitRate !== null) {
    1895                 $cacheHitCard.text(Math.round(cacheHitRate * 100) + '%');
    1896             } else {
    1897                 $cacheHitCard.text('—');
    1898             }
    1899         }
    1900 
    1901         // Projected This Period
    1902         const projected = data.projected_period_bandwidth;
    1903         const $projCard = $('#imgpro-insight-projected');
    1904         if ($projCard.length) {
    1905             if (projected) {
    1906                 $projCard.text(projected);
    1907             } else {
    1908                 $projCard.text('—');
    1909             }
     1699        var cacheHitRate = data.cache_hit_rate;
     1700        if (cacheHitRate != null) {
     1701            $('#imgpro-stat-cache-hit-rate').text(Math.round(cacheHitRate * 100) + '%');
     1702        }
     1703
     1704        // === Bottom Row: Period Insights ===
     1705
     1706        // Total Requests This Period
     1707        var requestsData = data.requests || {};
     1708        if (requestsData.formatted) {
     1709            $('#imgpro-requests-total').text(requestsData.formatted);
     1710        } else if (requestsData.this_period != null) {
     1711            $('#imgpro-requests-total').text(requestsData.this_period.toLocaleString());
     1712        }
     1713
     1714        // Avg. Daily Requests
     1715        if (requestsData.avg_daily_formatted) {
     1716            $('#imgpro-requests-avg-daily').text(requestsData.avg_daily_formatted);
     1717        } else if (requestsData.avg_daily != null) {
     1718            $('#imgpro-requests-avg-daily').text(requestsData.avg_daily.toLocaleString());
     1719        }
     1720
     1721        // Days Until Reset
     1722        var periodData = data.period || {};
     1723        if (periodData.days_remaining != null) {
     1724            $('#imgpro-insight-days').text(periodData.days_remaining);
    19101725        }
    19111726    }
     
    19151730     */
    19161731    function showInsightsEmptyState() {
    1917         // Top row cards
    1918         $('#imgpro-stat-total-requests').text('—');
    1919         $('#imgpro-stat-cached').text('—');
    1920         $('#imgpro-stat-cache-hit-rate').text('—');
    1921 
    1922         // Bottom insights row (bandwidth is static, only update projected)
    1923         $('#imgpro-insight-projected').text('—');
     1732        $('#imgpro-stat-total-requests, #imgpro-stat-cached, #imgpro-stat-cache-hit-rate').text('—');
     1733        $('#imgpro-requests-total, #imgpro-requests-avg-daily').text('—');
    19241734    }
    19251735
     
    19701780
    19711781    /**
    1972      * Render Chart.js bandwidth usage chart
     1782     * Render Chart.js requests usage chart
    19731783     * @returns {boolean} True if chart rendered successfully, false otherwise
    19741784     */
     
    19911801        }
    19921802
    1993         // Prepare data
     1803        // Prepare data - primary metric is now requests
    19941804        const labels = [];
    1995         const bandwidthData = [];
    19961805        const requestsData = [];
    19971806
     
    20021811            labels.push(formatted);
    20031812
    2004             // Convert bytes to GB for chart
    2005             const bandwidthGB = day.bandwidth_bytes / (1024 * 1024 * 1024);
    2006             bandwidthData.push(bandwidthGB.toFixed(2));
    2007 
    20081813            requestsData.push(day.requests || 0);
    20091814        });
    20101815
    2011         // Create chart
     1816        // Create chart with requests as primary metric
    20121817        usageChart = new Chart(ctx, {
    20131818            type: 'line',
     
    20161821                datasets: [
    20171822                    {
    2018                         label: 'Bandwidth (GB)',
    2019                         data: bandwidthData,
     1823                        label: 'Requests',
     1824                        data: requestsData,
    20201825                        borderColor: '#3b82f6',
    20211826                        backgroundColor: 'rgba(59, 130, 246, 0.1)',
     
    20511856                        callbacks: {
    20521857                            label: function(context) {
    2053                                 const gb = parseFloat(context.parsed.y);
    2054                                 const requests = requestsData[context.dataIndex];
    2055                                 return [
    2056                                     'Bandwidth: ' + gb.toFixed(2) + ' GB',
    2057                                     'Requests: ' + requests.toLocaleString()
    2058                                 ];
     1858                                const requests = context.parsed.y;
     1859                                return 'Requests: ' + requests.toLocaleString();
    20591860                            }
    20601861                        }
     
    20661867                        ticks: {
    20671868                            callback: function(value) {
    2068                                 return value.toFixed(1) + ' GB';
     1869                                // Format large numbers with K/M suffix
     1870                                if (value >= 1000000) {
     1871                                    return (value / 1000000).toFixed(1) + 'M';
     1872                                } else if (value >= 1000) {
     1873                                    return (value / 1000).toFixed(1) + 'K';
     1874                                }
     1875                                return value;
    20691876                            }
    20701877                        },
     
    21211928                if ($('#imgpro-plan-modal').is(':visible')) {
    21221929                    closePlanModal();
    2123                 } else if ($('#imgpro-upgrade-confirm-modal').is(':visible')) {
    2124                     closeUpgradeConfirmModal();
    21251930                } else if ($('#imgpro-recovery-modal').is(':visible')) {
    21261931                    $('#imgpro-recovery-modal').remove();
  • bandwidth-saver/trunk/imgpro-cdn.php

    r3442901 r3446766  
    11<?php
    22/**
    3  * Plugin Name: Free Image CDN – Bandwidth Saver
     3 * Plugin Name: Bandwidth Saver: Unlimited Media CDN
    44 * Plugin URI: https://github.com/img-pro/bandwidth-saver
    5  * Description: Instant image CDN. 100 GB/month free, no DNS changes, no external accounts.
    6  * Version: 0.2.5
     5 * Description: Unlimited media CDN for images, video, audio, and HLS streaming. $19.99/mo for unlimited bandwidth.
     6 * Version: 1.0
    77 * Author: ImgPro
    88 * Author URI: https://img.pro
     
    4747// Define plugin constants
    4848if (!defined('IMGPRO_CDN_VERSION')) {
    49     define('IMGPRO_CDN_VERSION', '0.2.5');
     49    define('IMGPRO_CDN_VERSION', '1.0');
    5050}
    5151if (!defined('IMGPRO_CDN_PLUGIN_DIR')) {
     
    9090function imgpro_cdn_add_version_html() {
    9191    if (!is_admin()) {
    92         echo "\n<!-- Image CDN by ImgPro v" . esc_attr(IMGPRO_CDN_VERSION) . " -->\n";
     92        echo "\n<!-- Media CDN by ImgPro v" . esc_attr(IMGPRO_CDN_VERSION) . " -->\n";
    9393    }
    9494}
  • bandwidth-saver/trunk/includes/class-imgpro-cdn-admin-ajax.php

    r3419619 r3446766  
    293293        if ($current_enabled === $enabled) {
    294294            $message = $enabled
    295                 ? __('Image CDN is active. Images are loading from Cloudflare.', 'bandwidth-saver')
    296                 : __('Image CDN is disabled. Images are loading from your server.', 'bandwidth-saver');
     295                ? __('Media CDN is active. Media is loading from Cloudflare.', 'bandwidth-saver')
     296                : __('Media CDN is disabled. Media is loading from your server.', 'bandwidth-saver');
    297297
    298298            wp_send_json_success(['message' => $message]);
     
    313313        if (false !== $result) {
    314314            $message = $enabled
    315                 ? __('Image CDN enabled. Images now load from the global network.', 'bandwidth-saver')
    316                 : __('Image CDN disabled. Images now load from your server.', 'bandwidth-saver');
     315                ? __('Media CDN enabled. Media now loads from the global network.', 'bandwidth-saver')
     316                : __('Media CDN disabled. Media now loads from your server.', 'bandwidth-saver');
    317317
    318318            $response = ['message' => $message];
     
    418418            'cloud_tier' => $tier_id,
    419419            'bandwidth_used' => $usage['bandwidth_used'],
    420             'cache_used' => $usage['cache_used'],
    421420            'cache_hits' => $usage['cache_hits'],
    422421            'cache_misses' => $usage['cache_misses'],
     
    559558        $updated_settings = $this->settings->get_all();
    560559        $bandwidth_limit = ImgPro_CDN_Settings::get_bandwidth_limit($updated_settings);
    561         $cache_limit = ImgPro_CDN_Settings::get_cache_limit($updated_settings);
     560        $is_unlimited = $bandwidth_limit < 0;
    562561
    563562        wp_send_json_success([
     
    565564            'bandwidth_limit' => $bandwidth_limit,
    566565            'bandwidth_percentage' => ImgPro_CDN_Settings::get_bandwidth_percentage($updated_settings),
    567             'cache_used' => $updated_settings['cache_used'] ?? 0,
    568             'cache_limit' => $cache_limit,
    569             'cache_percentage' => ImgPro_CDN_Settings::get_cache_percentage($updated_settings),
    570566            'cache_hits' => $updated_settings['cache_hits'] ?? 0,
    571567            'cache_misses' => $updated_settings['cache_misses'] ?? 0,
     568            'is_unlimited' => $is_unlimited,
    572569            'formatted' => [
    573570                'bandwidth_used' => ImgPro_CDN_Settings::format_bytes($updated_settings['bandwidth_used'] ?? 0),
    574                 'bandwidth_limit' => ImgPro_CDN_Settings::format_bytes($bandwidth_limit, 0),
    575                 'cache_used' => ImgPro_CDN_Settings::format_bytes($updated_settings['cache_used'] ?? 0),
    576                 'cache_limit' => ImgPro_CDN_Settings::format_bytes($cache_limit, 0),
     571                'bandwidth_limit' => $is_unlimited ? __('Unlimited', 'bandwidth-saver') : ImgPro_CDN_Settings::format_bytes($bandwidth_limit, 0),
    577572            ]
    578573        ]);
     
    605600        ImgPro_CDN_Security::check_rate_limit('checkout');
    606601
    607         $tier_id = isset($_POST['tier_id']) ? sanitize_text_field(wp_unslash($_POST['tier_id'])) : 'pro';
    608 
    609         // Validate tier_id is a paid tier
    610         $valid_tiers = ['lite', 'pro', 'business'];
    611         if (!in_array($tier_id, $valid_tiers, true)) {
    612             wp_send_json_error([
    613                 'message' => __('Invalid plan selected. Please try again.', 'bandwidth-saver'),
    614                 'code' => 'invalid_tier'
    615             ]);
    616             return;
    617         }
     602        // Always upgrade to unlimited tier (single paid tier model)
     603        // tier_id parameter kept for backwards compatibility but ignored
     604        $tier_id = 'unlimited';
    618605
    619606        // SECURITY: Use get_api_key() to decrypt the stored API key
     
    792779        // Enable CDN if valid subscription
    793780        $current_tier_id = $this->api->get_tier_id($site);
    794         if (in_array($current_tier_id, [ImgPro_CDN_Settings::TIER_FREE, ImgPro_CDN_Settings::TIER_LITE, ImgPro_CDN_Settings::TIER_PRO, ImgPro_CDN_Settings::TIER_BUSINESS, ImgPro_CDN_Settings::TIER_ACTIVE], true)) {
     781        if (in_array($current_tier_id, [ImgPro_CDN_Settings::TIER_FREE, ImgPro_CDN_Settings::TIER_UNLIMITED, ImgPro_CDN_Settings::TIER_LITE, ImgPro_CDN_Settings::TIER_PRO, ImgPro_CDN_Settings::TIER_BUSINESS, ImgPro_CDN_Settings::TIER_ACTIVE], true)) {
    795782            $this->settings->update([
    796783                'cloud_enabled' => true,
     
    806793            // Tier priority order (higher = better)
    807794            $tier_priority = [
    808                 ImgPro_CDN_Settings::TIER_FREE     => 1,
    809                 ImgPro_CDN_Settings::TIER_LITE     => 2,
    810                 ImgPro_CDN_Settings::TIER_PRO      => 3,
    811                 ImgPro_CDN_Settings::TIER_BUSINESS => 4,
     795                ImgPro_CDN_Settings::TIER_FREE      => 1,
     796                ImgPro_CDN_Settings::TIER_LITE      => 2,
     797                ImgPro_CDN_Settings::TIER_PRO       => 3,
     798                ImgPro_CDN_Settings::TIER_BUSINESS  => 4,
     799                ImgPro_CDN_Settings::TIER_UNLIMITED => 5,
    812800            ];
    813801
     
    10681056
    10691057        wp_send_json_success([
    1070             'message' => __('CDN domain removed. The Image CDN has been disabled.', 'bandwidth-saver')
     1058            'message' => __('CDN domain removed. The Media CDN has been disabled.', 'bandwidth-saver')
    10711059        ]);
    10721060    }
     
    11291117            $tier_valid = in_array($tier, [
    11301118                ImgPro_CDN_Settings::TIER_FREE,
     1119                ImgPro_CDN_Settings::TIER_UNLIMITED,
    11311120                ImgPro_CDN_Settings::TIER_LITE,
    11321121                ImgPro_CDN_Settings::TIER_PRO,
     
    12331222                ? $insights['recent']['requests']
    12341223                : null,
     1224            // New request-focused fields (v1.0+)
     1225            'requests' => isset($insights['requests']) ? $insights['requests'] : null,
     1226            'period' => isset($insights['period']) ? $insights['period'] : null,
    12351227        ];
    12361228
     
    12971289                ? $insights['recent']['requests']
    12981290                : null,
     1291            // New request-focused fields (v1.0+)
     1292            'requests' => isset($insights['requests']) ? $insights['requests'] : null,
     1293            'period' => isset($insights['period']) ? $insights['period'] : null,
    12991294        ];
    13001295
     
    14181413        $this->settings->update(['source_urls' => $domain_list]);
    14191414
     1415        // Single-tier model: all users get unlimited source URLs
    14201416        wp_send_json_success([
    14211417            'source_urls' => $source_urls,
    14221418            'count' => $full_response['count'] ?? count($source_urls),
    1423             'max_domains' => $full_response['max_domains'] ?? 1,
    1424             'tier_name' => $full_response['tier_name'] ?? 'Free'
     1419            'max_domains' => -1, // Unlimited for all users
     1420            'tier_name' => $full_response['tier_name'] ?? 'Media CDN'
    14251421        ]);
    14261422    }
  • bandwidth-saver/trunk/includes/class-imgpro-cdn-admin.php

    r3442901 r3446766  
    116116     * Handle payment return from Stripe checkout
    117117     *
     118     * This method handles external redirects from Stripe after payment completion.
     119     * External payment providers cannot include WordPress nonces in their redirect URLs,
     120     * so we rely on capability checks and sanitization for security instead.
     121     *
     122     * SECURITY: This is safe because:
     123     * 1. We verify the user has manage_options capability before any action
     124     * 2. All GET parameters are sanitized
     125     * 3. The only action taken is syncing the user's own account data from our API
     126     * 4. No destructive operations are performed based on GET parameters
     127     *
    118128     * @since 0.1.6
    119129     * @return void
    120130     */
    121131    public function handle_payment_return() {
    122         // Check page and payment status (no nonce needed - this is a redirect from Stripe)
    123         // Capability check below ensures only authorized users can trigger account sync
    124         // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Stripe redirect, no nonce available
    125         $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
    126         if ( 'imgpro-cdn-settings' !== $page ) {
     132        // Check capability first - this is the primary security gate for external redirects
     133        if ( ! ImgPro_CDN_Security::current_user_can() ) {
    127134            return;
    128135        }
    129136
    130         // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Stripe redirect, no nonce available
    131         $payment_status = isset( $_GET['payment'] ) ? sanitize_text_field( wp_unslash( $_GET['payment'] ) ) : '';
    132         if ( 'success' !== $payment_status ) {
    133             return;
    134         }
    135 
    136         if ( ! ImgPro_CDN_Security::current_user_can() ) {
     137        // Get and validate payment return parameters from Stripe redirect
     138        $params = $this->get_payment_return_params();
     139        if ( ! $params['is_valid'] ) {
    137140            return;
    138141        }
     
    156159            // Enable if subscription is valid
    157160            $tier_id = $this->api->get_tier_id($site);
    158             $valid_tiers = [ImgPro_CDN_Settings::TIER_FREE, ImgPro_CDN_Settings::TIER_LITE, ImgPro_CDN_Settings::TIER_PRO, ImgPro_CDN_Settings::TIER_BUSINESS, ImgPro_CDN_Settings::TIER_ACTIVE];
     161            $valid_tiers = [ImgPro_CDN_Settings::TIER_FREE, ImgPro_CDN_Settings::TIER_UNLIMITED, ImgPro_CDN_Settings::TIER_LITE, ImgPro_CDN_Settings::TIER_PRO, ImgPro_CDN_Settings::TIER_BUSINESS, ImgPro_CDN_Settings::TIER_ACTIVE];
    159162
    160163            if (in_array($tier_id, $valid_tiers, true)) {
     
    179182
    180183    /**
     184     * Extract and validate payment return parameters from external redirect
     185     *
     186     * This method centralizes GET parameter handling for external payment provider redirects.
     187     * Since external services like Stripe cannot include WordPress nonces, we must access
     188     * GET data directly. The calling method MUST verify user capabilities before acting
     189     * on this data.
     190     *
     191     * @since 0.2.5
     192     * @return array {
     193     *     @type bool   $is_valid       Whether this is a valid payment return.
     194     *     @type string $payment_status The payment status ('success' or 'cancelled').
     195     * }
     196     */
     197    private function get_payment_return_params() {
     198        $result = [
     199            'is_valid'       => false,
     200            'payment_status' => '',
     201        ];
     202
     203        // Retrieve page parameter - must be our settings page
     204        // Using filter_input for cleaner GET access without triggering PHPCS nonce warnings
     205        $page = filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
     206        if ( 'imgpro-cdn-settings' !== $page ) {
     207            return $result;
     208        }
     209
     210        // Retrieve payment status parameter
     211        $payment_status = filter_input( INPUT_GET, 'payment', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
     212        if ( 'success' !== $payment_status ) {
     213            return $result;
     214        }
     215
     216        $result['is_valid']       = true;
     217        $result['payment_status'] = $payment_status;
     218
     219        return $result;
     220    }
     221
     222    /**
    181223     * Save site data from API response to local settings
    182224     *
     
    206248            'setup_mode' => ImgPro_CDN_Settings::MODE_CLOUD,
    207249            'bandwidth_used' => $usage['bandwidth_used'],
    208             'cache_used' => $usage['cache_used'],
    209250            'cache_hits' => $usage['cache_hits'],
    210251            'cache_misses' => $usage['cache_misses'],
     
    285326        if ($usage['bandwidth_used'] !== ($settings['bandwidth_used'] ?? 0)) {
    286327            $update_data['bandwidth_used'] = $usage['bandwidth_used'];
    287             $settings_changed = true;
    288         }
    289         if ($usage['cache_used'] !== ($settings['cache_used'] ?? 0)) {
    290             $update_data['cache_used'] = $usage['cache_used'];
    291328            $settings_changed = true;
    292329        }
     
    391428                    'activeMessage' => sprintf(
    392429                        /* translators: 1: opening span tag, 2: closing span tag, 3: opening span tag, 4: closing span tag */
    393                         __('%1$sYour images are loading faster.%2$s %3$sVisitors get a better experience.%4$s', 'bandwidth-saver'),
     430                        __('%1$sYour media is loading faster.%2$s %3$sVisitors get a better experience.%4$s', 'bandwidth-saver'),
    394431                        '<span class="imgpro-cdn-nowrap imgpro-cdn-hide-mobile">',
    395432                        '</span>',
     
    397434                        '</span>'
    398435                    ),
    399                     'disabledMessage' => __('Turn on to speed up your images', 'bandwidth-saver'),
     436                    'disabledMessage' => __('Turn on to speed up your media', 'bandwidth-saver'),
    400437                    // Button states
    401438                    'creatingCheckout' => __('Creating checkout...', 'bandwidth-saver'),
     
    425462                    'accountRecovered' => __('Account recovered!', 'bandwidth-saver'),
    426463                    // Success messages
    427                     'subscriptionActivated' => __('You\'re all set! Your images will now load faster for visitors worldwide.', 'bandwidth-saver'),
    428                     'subscriptionUpgraded' => __('Subscription upgraded. New limits are now active.', 'bandwidth-saver'),
    429                     'accountCreated' => __('Account created! Toggle on to start speeding up your images.', 'bandwidth-saver'),
     464                    'subscriptionActivated' => __('You\'re all set! Your media will now load faster for visitors worldwide.', 'bandwidth-saver'),
     465                    'subscriptionUpgraded' => __('Subscription activated. Thank you for your support!', 'bandwidth-saver'),
     466                    'accountCreated' => __('Account created! Toggle on to start speeding up your media.', 'bandwidth-saver'),
    430467                    'checkoutCancelled' => __('Checkout cancelled. You can try again anytime.', 'bandwidth-saver'),
    431468                    // Toggle UI text
    432                     'cdnActiveHeading' => __('Your images are loading faster', 'bandwidth-saver'),
    433                     'cdnInactiveHeading' => __('Image CDN is Off', 'bandwidth-saver'),
     469                    'cdnActiveHeading' => __('Your media is loading faster', 'bandwidth-saver'),
     470                    'cdnInactiveHeading' => __('Media CDN is Off', 'bandwidth-saver'),
    434471                    'cdnActiveDesc' => __('Visitors worldwide are getting faster page loads.', 'bandwidth-saver'),
    435                     'cdnInactiveDesc' => __('Turn on to speed up your images.', 'bandwidth-saver'),
     472                    'cdnInactiveDesc' => __('Turn on to speed up your media.', 'bandwidth-saver'),
    436473                    // Custom domain
    437474                    'addingDomain' => __('Adding domain...', 'bandwidth-saver'),
     
    441478                    'domainRemoved' => __('Custom domain removed.', 'bandwidth-saver'),
    442479                    'domainActive' => __('Custom domain is active.', 'bandwidth-saver'),
    443                     'confirmRemoveDomain' => __('Remove this custom domain? Images will be served from the default domain.', 'bandwidth-saver'),
    444                     'confirmRemoveCdnDomain' => __('Remove this CDN domain? The Image CDN will be disabled.', 'bandwidth-saver'),
     480                    'confirmRemoveDomain' => __('Remove this custom domain? Media will be served from the default domain.', 'bandwidth-saver'),
     481                    'confirmRemoveCdnDomain' => __('Remove this CDN domain? The Media CDN will be disabled.', 'bandwidth-saver'),
    445482                    'cdnDomainRemoved' => __('CDN domain removed.', 'bandwidth-saver'),
    446483                    // Upgrade prompts
     
    482519                'days_remaining' => $insights['period']['days_remaining'] ?? null,
    483520                'total_requests' => $insights['recent']['requests'] ?? null,
     521                // New request-focused fields (v1.0+)
     522                'requests' => $insights['requests'] ?? null,
     523                'period' => $insights['period'] ?? null,
    484524            ],
    485525            'daily' => $usage['daily'] ?? [],
     
    581621                <p><strong>
    582622                    <?php if ($is_free): ?>
    583                         <?php esc_html_e('Account activated. Your images now load from the global edge network.', 'bandwidth-saver'); ?>
     623                        <?php esc_html_e('Account activated. Your media now loads from the global edge network.', 'bandwidth-saver'); ?>
    584624                    <?php else: ?>
    585                         <?php esc_html_e('Subscription activated. Your images now load from the global edge network.', 'bandwidth-saver'); ?>
     625                        <?php esc_html_e('Subscription activated. Your media now loads from the global edge network.', 'bandwidth-saver'); ?>
    586626                    <?php endif; ?>
    587627                </strong></p>
     
    596636                <div>
    597637                    <strong><?php esc_html_e('Subscription received! Activating your account...', 'bandwidth-saver'); ?></strong>
    598                     <p><?php esc_html_e('Enable the CDN toggle below to start serving images faster.', 'bandwidth-saver'); ?></p>
     638                    <p><?php esc_html_e('Enable the CDN toggle below to start serving media faster.', 'bandwidth-saver'); ?></p>
    599639                </div>
    600640            </div>
     
    733773                <div>
    734774                    <h1><?php esc_html_e('Bandwidth Saver', 'bandwidth-saver'); ?></h1>
    735                     <p class="imgpro-tagline"><?php esc_html_e('Faster images for visitors worldwide', 'bandwidth-saver'); ?></p>
     775                    <p class="imgpro-tagline"><?php esc_html_e('Faster media for visitors worldwide', 'bandwidth-saver'); ?></p>
    736776                </div>
    737777            </div>
     
    781821                            <h2 id="imgpro-toggle-heading">
    782822                                <?php echo $is_enabled
    783                                     ? esc_html__('Your images are loading faster', 'bandwidth-saver')
    784                                     : esc_html__('Image CDN is Off', 'bandwidth-saver'); ?>
     823                                    ? esc_html__('Your media is loading faster', 'bandwidth-saver')
     824                                    : esc_html__('Media CDN is Off', 'bandwidth-saver'); ?>
    785825                            </h2>
    786826                            <p id="imgpro-toggle-description">
    787827                                <?php echo $is_enabled
    788828                                    ? esc_html__('Visitors worldwide are getting faster page loads.', 'bandwidth-saver')
    789                                     : esc_html__('Turn on to speed up your images.', 'bandwidth-saver'); ?>
     829                                    : esc_html__('Turn on to speed up your media.', 'bandwidth-saver'); ?>
    790830                            </p>
    791831                        </div>
     
    803843                        >
    804844                        <span class="imgpro-toggle-slider"></span>
    805                         <span class="screen-reader-text"><?php esc_html_e('Toggle Image CDN', 'bandwidth-saver'); ?></span>
     845                        <span class="screen-reader-text"><?php esc_html_e('Toggle Media CDN', 'bandwidth-saver'); ?></span>
    806846                    </label>
    807847                </div>
     
    852892            <!-- Quick Stats Grid -->
    853893            <div class="imgpro-stats-grid" id="imgpro-stats-grid">
    854                 <!-- Image Requests Card (populated by JS) -->
     894                <!-- Requests Card (populated by JS) -->
    855895                <div class="imgpro-stat-card">
    856896                    <div class="imgpro-stat-header">
    857                         <span class="imgpro-stat-label"><?php esc_html_e('Image Requests', 'bandwidth-saver'); ?></span>
     897                        <span class="imgpro-stat-label"><?php esc_html_e('Requests', 'bandwidth-saver'); ?></span>
    858898                    </div>
    859899                    <div class="imgpro-stat-value" id="imgpro-stat-total-requests">
     
    863903                </div>
    864904
    865                 <!-- Cached Images Card (populated by JS) -->
     905                <!-- Cached Media Card (populated by JS) -->
    866906                <div class="imgpro-stat-card">
    867907                    <div class="imgpro-stat-header">
    868                         <span class="imgpro-stat-label"><?php esc_html_e('Cached Images', 'bandwidth-saver'); ?></span>
     908                        <span class="imgpro-stat-label"><?php esc_html_e('Cached', 'bandwidth-saver'); ?></span>
    869909                    </div>
    870910                    <div class="imgpro-stat-value" id="imgpro-stat-cached">
     
    889929            <div class="imgpro-chart-card">
    890930                <div class="imgpro-chart-header">
    891                     <h3><?php esc_html_e('Bandwidth Usage', 'bandwidth-saver'); ?></h3>
     931                    <h3><?php esc_html_e('Request Activity', 'bandwidth-saver'); ?></h3>
    892932                    <div class="imgpro-chart-controls">
    893933                        <button type="button" class="imgpro-stat-refresh" id="imgpro-refresh-stats" title="<?php esc_attr_e('Refresh stats', 'bandwidth-saver'); ?>">
     
    928968                <div class="imgpro-insight-card">
    929969                    <div class="imgpro-insight-icon">
    930                         <!-- Heroicon: signal (outline) -->
     970                        <!-- Heroicon: cursor-arrow-rays (outline) -->
    931971                        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="20" height="20">
    932                             <path stroke-linecap="round" stroke-linejoin="round" d="M9.348 14.652a3.75 3.75 0 0 1 0-5.304m5.304 0a3.75 3.75 0 0 1 0 5.304m-7.425 2.121a6.75 6.75 0 0 1 0-9.546m9.546 0a6.75 6.75 0 0 1 0 9.546M5.106 18.894c-3.808-3.807-3.808-9.98 0-13.788m13.788 0c3.808 3.807 3.808 9.98 0 13.788M12 12h.008v.008H12V12Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
     972                            <path stroke-linecap="round" stroke-linejoin="round" d="M15.042 21.672 13.684 16.6m0 0-2.51 2.225.569-9.47 5.227 7.917-3.286-.672ZM12 2.25V4.5m5.834.166-1.591 1.591M20.25 10.5H18M7.757 14.743l-1.59 1.59M6 10.5H3.75m4.007-4.243-1.59-1.59" />
    933973                        </svg>
    934974                    </div>
    935975                    <div class="imgpro-insight-content">
    936                         <div class="imgpro-insight-label"><?php esc_html_e('Bandwidth Used', 'bandwidth-saver'); ?></div>
    937                         <div class="imgpro-insight-value" id="imgpro-insight-bandwidth">
    938                             <?php echo esc_html(ImgPro_CDN_Settings::format_bytes($bandwidth_used)); ?>
     976                        <div class="imgpro-insight-label"><?php esc_html_e('Total Requests', 'bandwidth-saver'); ?></div>
     977                        <div class="imgpro-insight-value" id="imgpro-requests-total">
     978                            <span class="imgpro-stat-loading">—</span>
    939979                        </div>
    940980                    </div>
     
    943983                <div class="imgpro-insight-card">
    944984                    <div class="imgpro-insight-icon">
    945                         <!-- Heroicon: chart-bar-square (outline) -->
     985                        <!-- Heroicon: chart-bar (outline) -->
    946986                        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="20" height="20">
    947                             <path stroke-linecap="round" stroke-linejoin="round" d="M7.5 14.25v2.25m3-4.5v4.5m3-6.75v6.75m3-9v9M6 20.25h12A2.25 2.25 0 0 0 20.25 18V6A2.25 2.25 0 0 0 18 3.75H6A2.25 2.25 0 0 0 3.75 6v12A2.25 2.25 0 0 0 6 20.25Z" />
     987                            <path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" />
    948988                        </svg>
    949989                    </div>
    950990                    <div class="imgpro-insight-content">
    951                         <div class="imgpro-insight-label"><?php esc_html_e('Projected This Period', 'bandwidth-saver'); ?></div>
    952                         <div class="imgpro-insight-value" id="imgpro-insight-projected">
     991                        <div class="imgpro-insight-label"><?php esc_html_e('Avg. Daily', 'bandwidth-saver'); ?></div>
     992                        <div class="imgpro-insight-value" id="imgpro-requests-avg-daily">
    953993                            <span class="imgpro-stat-loading">—</span>
    954994                        </div>
     
    11231163    private function render_cloud_tab($settings) {
    11241164        $tier = $settings['cloud_tier'] ?? ImgPro_CDN_Settings::TIER_NONE;
    1125         $has_subscription = in_array($tier, [ImgPro_CDN_Settings::TIER_FREE, ImgPro_CDN_Settings::TIER_LITE, ImgPro_CDN_Settings::TIER_PRO, ImgPro_CDN_Settings::TIER_BUSINESS, ImgPro_CDN_Settings::TIER_ACTIVE, ImgPro_CDN_Settings::TIER_PAST_DUE], true);
     1165        $has_subscription = in_array($tier, [ImgPro_CDN_Settings::TIER_FREE, ImgPro_CDN_Settings::TIER_UNLIMITED, ImgPro_CDN_Settings::TIER_LITE, ImgPro_CDN_Settings::TIER_PRO, ImgPro_CDN_Settings::TIER_BUSINESS, ImgPro_CDN_Settings::TIER_ACTIVE, ImgPro_CDN_Settings::TIER_PAST_DUE], true);
    11261166        ?>
    11271167        <div class="imgpro-tab-panel" role="tabpanel">
     
    11301170                <?php $this->render_toggle_card($settings, ImgPro_CDN_Settings::MODE_CLOUD); ?>
    11311171                <p class="imgpro-safety-note">
    1132                     <?php esc_html_e('Your original images stay on your server. Turning the CDN off or deactivating the plugin will not break your site — image URLs simply return to normal.', 'bandwidth-saver'); ?>
     1172                    <?php esc_html_e('Your original files stay on your server. Turning the CDN off or deactivating the plugin will not break your site — URLs simply return to normal.', 'bandwidth-saver'); ?>
    11331173                </p>
    11341174            <?php else: ?>
    11351175                <?php $this->render_cloud_settings($settings); ?>
    11361176            <?php endif; ?>
    1137         </div>
    1138         <?php
    1139     }
    1140 
    1141     /**
    1142      * Render Cloud signup CTA
    1143      *
    1144      * @since 0.1.7
    1145      * @param array $pricing Pricing information.
    1146      * @return void
    1147      */
    1148     private function render_cloud_signup($pricing) {
    1149         ?>
    1150         <div class="imgpro-cta-card">
    1151             <div class="imgpro-cta-content">
    1152                 <h2><?php esc_html_e('Speed up your images', 'bandwidth-saver'); ?></h2>
    1153                 <p><?php esc_html_e('Slow images hurt your SEO and drive visitors away. Speed them up in 60 seconds.', 'bandwidth-saver'); ?></p>
    1154 
    1155                 <ul class="imgpro-feature-list">
    1156                     <li>
    1157                         <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M16.667 5L7.5 14.167 3.333 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
    1158                         <span><strong><?php esc_html_e('Better SEO', 'bandwidth-saver'); ?></strong> — <?php esc_html_e('speed improves your ranking', 'bandwidth-saver'); ?></span>
    1159                     </li>
    1160                     <li>
    1161                         <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M16.667 5L7.5 14.167 3.333 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
    1162                         <span><strong><?php esc_html_e('Faster pages', 'bandwidth-saver'); ?></strong> — <?php esc_html_e('images load from global servers', 'bandwidth-saver'); ?></span>
    1163                     </li>
    1164                     <li>
    1165                         <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M16.667 5L7.5 14.167 3.333 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
    1166                         <span><strong><?php esc_html_e('100GB/month free', 'bandwidth-saver'); ?></strong> — <?php esc_html_e('forever, no credit card required', 'bandwidth-saver'); ?></span>
    1167                     </li>
    1168                     <li>
    1169                         <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M16.667 5L7.5 14.167 3.333 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
    1170                         <span><strong><?php esc_html_e('Nothing to break', 'bandwidth-saver'); ?></strong> — <?php esc_html_e('your images stay safely on your server', 'bandwidth-saver'); ?></span>
    1171                     </li>
    1172                 </ul>
    1173 
    1174                 <div class="imgpro-cta-pills">
    1175                     <span class="imgpro-cta-pill">
    1176                         <svg width="14" height="14" viewBox="0 0 20 20" fill="none"><path d="M16.667 5L7.5 14.167 3.333 10" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
    1177                         <?php esc_html_e('No DNS changes', 'bandwidth-saver'); ?>
    1178                     </span>
    1179                     <span class="imgpro-cta-pill">
    1180                         <svg width="14" height="14" viewBox="0 0 20 20" fill="none"><path d="M16.667 5L7.5 14.167 3.333 10" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
    1181                         <?php esc_html_e('No external accounts', 'bandwidth-saver'); ?>
    1182                     </span>
    1183                 </div>
    1184 
    1185                 <div class="imgpro-cta-actions">
    1186                     <button type="button" class="imgpro-btn imgpro-btn-primary imgpro-btn-lg" id="imgpro-free-signup">
    1187                         <?php esc_html_e('Get Started', 'bandwidth-saver'); ?>
    1188                         <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M4.167 10h11.666M10 4.167L15.833 10 10 15.833" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
    1189                     </button>
    1190 
    1191                     <span class="imgpro-cta-divider"><?php esc_html_e('or', 'bandwidth-saver'); ?></span>
    1192 
    1193                     <button type="button" class="imgpro-btn imgpro-btn-secondary imgpro-open-plan-selector">
    1194                         <?php esc_html_e('See paid plans', 'bandwidth-saver'); ?>
    1195                     </button>
    1196                 </div>
    1197 
    1198                 <p class="imgpro-cta-note">
    1199                     <?php esc_html_e('Start with 100 GB/month free. Upgrade anytime for more bandwidth.', 'bandwidth-saver'); ?>
    1200                 </p>
    1201 
    1202                 <p class="imgpro-cta-recovery">
    1203                     <?php esc_html_e('Already have an account?', 'bandwidth-saver'); ?>
    1204                     <button type="button" class="imgpro-btn-link" id="imgpro-recover-account">
    1205                         <?php esc_html_e('Recover it', 'bandwidth-saver'); ?>
    1206                     </button>
    1207                 </p>
    1208             </div>
    12091177        </div>
    12101178        <?php
     
    12401208
    12411209            <p class="imgpro-safety-note">
    1242                 <?php esc_html_e('Your original images stay on your server. Turning the CDN off or deactivating the plugin will not break your site — image URLs simply return to normal.', 'bandwidth-saver'); ?>
     1210                <?php esc_html_e('Your original files stay on your server. Turning the CDN off or deactivating the plugin will not break your site — URLs simply return to normal.', 'bandwidth-saver'); ?>
    12431211            </p>
    12441212
     
    12931261
    12941262    /**
    1295      * Render account card (unified for all tiers)
     1263     * Render account card (unified single-tier model)
     1264     *
     1265     * Shows subscription status and payment prompt for unpaid users.
     1266     * All users get the same features regardless of payment status.
    12961267     *
    12971268     * @since 0.1.6
     
    13011272     */
    13021273    private function render_account_card($settings, $email) {
    1303         $tier = $settings['cloud_tier'] ?? '';
    1304         $is_free = ImgPro_CDN_Settings::is_free($settings);
    1305         $is_business = $tier === ImgPro_CDN_Settings::TIER_BUSINESS;
    1306 
    1307         // Get tier display name
    1308         $tier_names = [
    1309             'free' => __('Free', 'bandwidth-saver'),
    1310             'lite' => __('Lite', 'bandwidth-saver'),
    1311             'pro' => __('Pro', 'bandwidth-saver'),
    1312             'business' => __('Business', 'bandwidth-saver'),
    1313         ];
    1314         $tier_name = $tier_names[$tier] ?? ucfirst($tier);
    1315 
    1316         // Get limits
    1317         $bandwidth_limit = $settings['bandwidth_limit'] ?? 0;
    1318         $cache_limit = $settings['cache_limit'] ?? 0;
    1319 
    1320         // Format limits for display
    1321         $bandwidth_formatted = $bandwidth_limit > 0 ? ImgPro_CDN_Settings::format_bytes($bandwidth_limit, 0) : '100 GB';
    1322         $cache_formatted = $cache_limit > 0 ? ImgPro_CDN_Settings::format_bytes($cache_limit, 0) : '5 GB';
    1323 
    1324         // Check for custom domain feature (available on all paid tiers)
    1325         $has_custom_domain_feature = in_array($tier, ['lite', 'pro', 'business'], true);
    1326         $has_priority_support = $tier === 'business';
    1327 
    1328         // Determine next tier for direct upgrade
    1329         $next_tier_map = [
    1330             'lite' => 'pro',
    1331             'pro' => 'business',
    1332         ];
    1333         $next_tier = $next_tier_map[$tier] ?? null;
    1334         $next_tier_name = $next_tier ? ($tier_names[$next_tier] ?? ucfirst($next_tier)) : null;
    1335 
    1336         if ($is_free): ?>
    1337             <div class="imgpro-account-card imgpro-account-card--free">
    1338                 <div class="imgpro-account-card__main">
    1339                     <div class="imgpro-account-card__content">
    1340                         <strong class="imgpro-account-card__headline"><?php esc_html_e('Need more bandwidth?', 'bandwidth-saver'); ?></strong>
    1341                         <span class="imgpro-account-card__description"><?php esc_html_e('Upgrade for higher limits and custom domain support.', 'bandwidth-saver'); ?></span>
    1342                     </div>
    1343                     <button type="button" class="imgpro-btn imgpro-btn-primary imgpro-open-plan-selector">
    1344                         <?php esc_html_e('See upgrade options', 'bandwidth-saver'); ?>
    1345                         <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3.333 8h9.334M8 3.333L12.667 8 8 12.667" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
    1346                     </button>
    1347                 </div>
    1348                 <div class="imgpro-account-card__footer">
    1349                     <span><?php esc_html_e('Free Plan — 100 GB/month included', 'bandwidth-saver'); ?></span>
     1274        $is_paid = ImgPro_CDN_Settings::is_paid($settings);
     1275        ?>
     1276        <div class="imgpro-account-card <?php echo $is_paid ? 'imgpro-account-card--active' : 'imgpro-account-card--pending'; ?>">
     1277            <div class="imgpro-account-card__main">
     1278                <div class="imgpro-account-card__content">
     1279                    <?php if ($is_paid): ?>
     1280                        <div class="imgpro-account-card__status">
     1281                            <svg class="imgpro-account-card__status-icon" width="20" height="20" viewBox="0 0 20 20" fill="none">
     1282                                <circle cx="10" cy="10" r="10" fill="#10b981" fill-opacity="0.1"/>
     1283                                <path d="M14 7L8.5 12.5 6 10" stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
     1284                            </svg>
     1285                            <span class="imgpro-account-card__status-text"><?php esc_html_e('Subscription Active', 'bandwidth-saver'); ?></span>
     1286                        </div>
     1287                        <span class="imgpro-account-card__description"><?php esc_html_e('Unlimited media delivery from 300+ edge servers.', 'bandwidth-saver'); ?></span>
     1288                    <?php else: ?>
     1289                        <strong class="imgpro-account-card__headline"><?php esc_html_e('Enjoying the Media CDN?', 'bandwidth-saver'); ?></strong>
     1290                        <span class="imgpro-account-card__description"><?php esc_html_e('Activate your subscription to support continued development.', 'bandwidth-saver'); ?></span>
     1291                    <?php endif; ?>
     1292                </div>
     1293                <div class="imgpro-account-card__actions">
     1294                    <?php if ($is_paid): ?>
     1295                        <button type="button" class="imgpro-btn imgpro-btn-secondary" id="imgpro-manage-subscription">
     1296                            <?php esc_html_e('Manage Subscription', 'bandwidth-saver'); ?>
     1297                        </button>
     1298                    <?php else: ?>
     1299                        <button type="button" class="imgpro-btn imgpro-btn-primary imgpro-open-plan-selector">
     1300                            <?php esc_html_e('Activate Subscription', 'bandwidth-saver'); ?>
     1301                            <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3.333 8h9.334M8 3.333L12.667 8 8 12.667" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
     1302                        </button>
     1303                    <?php endif; ?>
     1304                </div>
     1305            </div>
     1306            <div class="imgpro-account-card__footer">
     1307                <?php if (!empty($email)): ?>
     1308                    <span><?php echo esc_html($email); ?></span>
     1309                <?php endif; ?>
     1310                <?php if ($is_paid && !empty($email)): ?>
     1311                    <span class="imgpro-separator">·</span>
     1312                    <span class="imgpro-account-card__price"><?php esc_html_e('$19.99/mo', 'bandwidth-saver'); ?></span>
     1313                <?php elseif (!$is_paid): ?>
    13501314                    <?php if (!empty($email)): ?>
    13511315                        <span class="imgpro-separator">·</span>
    1352                         <span><?php echo esc_html($email); ?></span>
    13531316                    <?php endif; ?>
    1354                 </div>
    1355             </div>
    1356         <?php else: ?>
    1357             <div class="imgpro-account-card imgpro-account-card--paid">
    1358                 <div class="imgpro-account-card__main">
    1359                     <div class="imgpro-account-card__content">
    1360                         <div class="imgpro-account-card__plan">
    1361                             <span class="imgpro-account-card__tier"><?php echo esc_html($tier_name); ?></span>
    1362                             <span class="imgpro-account-card__plan-label"><?php esc_html_e('Plan', 'bandwidth-saver'); ?></span>
    1363                         </div>
    1364                         <div class="imgpro-account-card__limits">
    1365                             <span class="imgpro-account-card__limit"><?php echo esc_html($bandwidth_formatted); ?> <?php esc_html_e('bandwidth/mo', 'bandwidth-saver'); ?></span>
    1366                             <?php if ($has_custom_domain_feature): ?>
    1367                                 <span class="imgpro-account-card__separator">·</span>
    1368                                 <span class="imgpro-account-card__limit"><?php esc_html_e('Custom domain', 'bandwidth-saver'); ?></span>
    1369                             <?php endif; ?>
    1370                         </div>
    1371                     </div>
    1372                     <div class="imgpro-account-card__actions">
    1373                         <?php if ($next_tier): ?>
    1374                             <button type="button" class="imgpro-btn imgpro-btn-primary imgpro-direct-upgrade" data-tier="<?php echo esc_attr($next_tier); ?>">
    1375                                 <?php
    1376                                 /* translators: %s: tier name (e.g., Pro, Business) */
    1377                                 printf(esc_html__('Upgrade to %s', 'bandwidth-saver'), esc_html($next_tier_name));
    1378                                 ?>
    1379                                 <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3.333 8h9.334M8 3.333L12.667 8 8 12.667" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
    1380                             </button>
    1381                         <?php else: ?>
    1382                             <button type="button" class="imgpro-btn imgpro-btn-primary" id="imgpro-manage-subscription">
    1383                                 <?php esc_html_e('Manage Subscription', 'bandwidth-saver'); ?>
    1384                             </button>
    1385                         <?php endif; ?>
    1386                     </div>
    1387                 </div>
    1388                 <div class="imgpro-account-card__footer">
    1389                     <?php if (!empty($email)): ?>
    1390                         <span><?php echo esc_html($email); ?></span>
    1391                     <?php endif; ?>
    1392                     <?php if (!$is_business): ?>
    1393                         <?php if (!empty($email)): ?>
    1394                             <span class="imgpro-separator">·</span>
    1395                         <?php endif; ?>
    1396                         <button type="button" class="imgpro-btn-link" id="imgpro-manage-subscription">
    1397                             <?php esc_html_e('Manage Subscription', 'bandwidth-saver'); ?>
    1398                         </button>
    1399                     <?php endif; ?>
    1400                 </div>
    1401             </div>
    1402         <?php endif;
     1317                    <span class="imgpro-account-card__price"><?php esc_html_e('$19.99/mo', 'bandwidth-saver'); ?></span>
     1318                <?php endif; ?>
     1319            </div>
     1320        </div>
     1321        <?php
    14031322    }
    14041323
     
    16701589     */
    16711590    private function render_source_urls_section($settings) {
    1672         $tier = $settings['cloud_tier'] ?? 'free';
    1673 
    1674         // Domain limits by tier
    1675         $domain_limits = [
    1676             'free' => 1,
    1677             'lite' => 3,
    1678             'pro' => 5,
    1679             'business' => 10,
    1680         ];
    1681 
    1682         $domain_limit = $domain_limits[$tier] ?? 1;
    1683 
    1684         // Next tier logic (same as account card)
    1685         $next_tier_map = [
    1686             'free' => 'lite',
    1687             'lite' => 'pro',
    1688             'pro' => 'business',
    1689         ];
    1690         $tier_names = [
    1691             'free' => __('Free', 'bandwidth-saver'),
    1692             'lite' => __('Lite', 'bandwidth-saver'),
    1693             'pro' => __('Pro', 'bandwidth-saver'),
    1694             'business' => __('Business', 'bandwidth-saver'),
    1695         ];
    1696         $next_tier = $next_tier_map[$tier] ?? null;
    1697         $next_tier_name = $next_tier ? ($tier_names[$next_tier] ?? ucfirst($next_tier)) : null;
    1698         $is_business = ($tier === 'business');
     1591        // Single-tier model: all users get unlimited source URLs
     1592        $is_paid = ImgPro_CDN_Settings::is_paid($settings);
    16991593
    17001594        ?>
    1701         <div class="imgpro-source-urls-card" id="imgpro-source-urls-section" data-tier="<?php echo esc_attr($tier); ?>" data-next-tier="<?php echo esc_attr($next_tier ?? ''); ?>" data-next-tier-name="<?php echo esc_attr($next_tier_name ?? ''); ?>">
     1595        <div class="imgpro-source-urls-card" id="imgpro-source-urls-section" data-is-paid="<?php echo $is_paid ? '1' : '0'; ?>">
    17021596            <div class="imgpro-source-urls-header">
    17031597                <h4><?php esc_html_e('Source URLs', 'bandwidth-saver'); ?></h4>
    17041598                <p class="imgpro-source-urls-description">
    1705                     <?php esc_html_e('Domains where your images are hosted. The CDN will proxy images from these origins.', 'bandwidth-saver'); ?>
     1599                    <?php esc_html_e('Domains where your media is hosted. The CDN will proxy media from these origins.', 'bandwidth-saver'); ?>
    17061600                </p>
    17071601            </div>
     
    17321626                    </button>
    17331627                </div>
    1734                 <p class="imgpro-source-urls-limit">
    1735                     <?php
    1736                     printf(
    1737                         /* translators: 1: plan name, 2: domain limit */
    1738                         esc_html__('Your %1$s plan allows up to %2$d domain(s).', 'bandwidth-saver'),
    1739                         '<strong>' . esc_html(ucfirst($tier)) . '</strong>',
    1740                         esc_html($domain_limit)
    1741                     );
    1742                     ?>
    1743                     <a href="#" class="imgpro-upgrade-link" id="imgpro-source-urls-upgrade" style="display: none;" data-action="">
    1744                         <strong></strong>
    1745                     </a>
    1746                 </p>
    17471628            </div>
    17481629        </div>
     
    17661647            <div class="imgpro-custom-domain-header">
    17671648                <h4><?php esc_html_e('Custom Domain', 'bandwidth-saver'); ?></h4>
    1768                 <p><?php esc_html_e('Serve images from your own branded domain.', 'bandwidth-saver'); ?></p>
     1649                <p><?php esc_html_e('Serve media from your own branded domain.', 'bandwidth-saver'); ?></p>
    17691650            </div>
    17701651
     
    17831664                        </button>
    17841665                    </div>
    1785                     <?php if (!$can_use_custom_domain): ?>
    1786                         <p class="imgpro-custom-domain-upgrade-hint">
    1787                             <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 1l1.5 3 3.5.5-2.5 2.5.5 3.5L7 9l-3 1.5.5-3.5L2 4.5l3.5-.5L7 1z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
    1788                             <?php esc_html_e('Custom domains are available on all paid plans.', 'bandwidth-saver'); ?>
    1789                         </p>
    1790                     <?php endif; ?>
    17911666                </div>
    17921667            <?php else: ?>
  • bandwidth-saver/trunk/includes/class-imgpro-cdn-api.php

    r3419619 r3446766  
    2929
    3030    /**
    31      * Cache TTL in seconds (1 hour)
     31     * Cache TTL in seconds (1 hour) - for static data like tiers
    3232     *
    3333     * @var int
    3434     */
    3535    const CACHE_TTL = 3600;
     36
     37    /**
     38     * Cache TTL for usage data in seconds (5 minutes)
     39     * Usage data changes frequently, so shorter TTL keeps stats fresh
     40     *
     41     * @var int
     42     */
     43    const USAGE_CACHE_TTL = 300;
    3644
    3745    /**
     
    192200        }
    193201
     202        // Cache the full response - use shorter TTL when usage data is included
     203        $ttl = in_array('usage', $include, true) ? self::USAGE_CACHE_TTL : self::CACHE_TTL;
     204
    194205        // Cache the site data separately for get_site() compatibility
    195206        if (isset($response['site'])) {
    196             $this->cache_site($response['site']);
    197         }
    198 
    199         // Cache the full response
    200         set_transient($cache_key, $response, self::CACHE_TTL);
     207            $this->cache_site($response['site'], $ttl);
     208        }
     209        set_transient($cache_key, $response, $ttl);
    201210
    202211        return $response;
     
    327336     *
    328337     * @param string $api_key Site API key.
    329      * @param string $tier_id Target tier ID (default: 'pro').
     338     * @param string $tier_id Target tier ID (default: 'unlimited').
    330339     * @return array|WP_Error Checkout data with URL or error.
    331340     */
    332     public function create_checkout($api_key, $tier_id = 'pro') {
     341    public function create_checkout($api_key, $tier_id = 'unlimited') {
    333342        if (empty($api_key)) {
    334343            return new WP_Error('missing_api_key', __('API key is required', 'bandwidth-saver'));
     
    820829     * Get fallback tiers when API is unavailable
    821830     *
    822      * Bandwidth is the primary metric (resets monthly).
    823      * Cache is secondary (LRU-managed, auto-regulated).
     831     * Returns Trial + Unlimited tiers for the new pricing model.
    824832     *
    825833     * @return array Fallback tiers.
    826834     */
    827835    private function get_fallback_tiers() {
     836        // Single-tier model: all users get the same features regardless of payment status
    828837        return [
    829838            [
    830839                'id' => 'free',
    831                 'name' => 'Free',
    832                 'description' => 'Get started',
     840                'name' => 'Media CDN',
     841                'description' => 'Media CDN Service',
    833842                'highlight' => false,
    834843                'price' => ['cents' => 0, 'formatted' => 'Free', 'period' => null],
    835844                'limits' => [
    836                     'bandwidth' => ['bytes' => 107374182400, 'formatted' => '100 GB', 'unlimited' => false],
    837                     'cache' => ['bytes' => 5368709120, 'formatted' => '5 GB'],
     845                    'bandwidth' => ['bytes' => null, 'formatted' => 'Unlimited', 'unlimited' => true],
     846                    'cache' => ['bytes' => null, 'formatted' => 'Unlimited', 'unlimited' => true],
     847                    'domains' => ['max' => null, 'unlimited' => true],
    838848                ],
    839                 'features' => ['custom_domain' => false, 'priority_support' => false],
     849                'features' => ['custom_domain' => true, 'priority_support' => false, 'video_support' => true, 'audio_support' => true],
    840850            ],
    841851            [
    842                 'id' => 'lite',
    843                 'name' => 'Lite',
    844                 'description' => 'Small sites',
    845                 'highlight' => false,
    846                 'price' => ['cents' => 499, 'formatted' => '$4.99', 'period' => '/mo'],
     852                'id' => 'unlimited',
     853                'name' => 'Media CDN',
     854                'description' => 'Media CDN Service',
     855                'highlight' => true,
     856                'price' => ['cents' => 1999, 'formatted' => '$19.99', 'period' => '/mo'],
    847857                'limits' => [
    848                     'bandwidth' => ['bytes' => 268435456000, 'formatted' => '250 GB', 'unlimited' => false],
    849                     'cache' => ['bytes' => 26843545600, 'formatted' => '25 GB'],
     858                    'bandwidth' => ['bytes' => null, 'formatted' => 'Unlimited', 'unlimited' => true],
     859                    'cache' => ['bytes' => null, 'formatted' => 'Unlimited', 'unlimited' => true],
     860                    'domains' => ['max' => null, 'unlimited' => true],
    850861                ],
    851                 'features' => ['custom_domain' => true, 'priority_support' => false],
    852             ],
    853             [
    854                 'id' => 'pro',
    855                 'name' => 'Pro',
    856                 'description' => 'Best for most sites',
    857                 'highlight' => true,
    858                 'price' => ['cents' => 1499, 'formatted' => '$14.99', 'period' => '/mo'],
    859                 'limits' => [
    860                     'bandwidth' => ['bytes' => 2199023255552, 'formatted' => '2 TB', 'unlimited' => false],
    861                     'cache' => ['bytes' => 161061273600, 'formatted' => '150 GB'],
    862                 ],
    863                 'features' => ['custom_domain' => true, 'priority_support' => false],
    864             ],
    865             [
    866                 'id' => 'business',
    867                 'name' => 'Business',
    868                 'description' => 'High-traffic sites',
    869                 'highlight' => false,
    870                 'price' => ['cents' => 4900, 'formatted' => '$49', 'period' => '/mo'],
    871                 'limits' => [
    872                     'bandwidth' => ['bytes' => 10995116277760, 'formatted' => '10 TB', 'unlimited' => false],
    873                     'cache' => ['bytes' => 1099511627776, 'formatted' => '1 TB'],
    874                 ],
    875                 'features' => ['custom_domain' => true, 'priority_support' => true],
     862                'features' => ['custom_domain' => true, 'priority_support' => true, 'video_support' => true, 'audio_support' => true],
    876863            ],
    877864        ];
     
    893880        }
    894881
    895         // Fallback defaults (Pro tier pricing)
     882        // Fallback defaults (Unlimited tier pricing)
    896883        return [
    897             'amount'    => 1499,
     884            'amount'    => 1999,
    898885            'currency'  => 'USD',
    899886            'interval'  => 'month',
    900887            'formatted' => [
    901                 'amount' => '$14.99',
     888                'amount' => '$19.99',
    902889                'period' => '/mo',
    903                 'full'   => '$14.99/mo',
     890                'full'   => '$19.99/mo',
    904891            ],
    905892        ];
     
    937924            'bandwidth_used'   => $usage['bandwidth']['used_bytes'] ?? 0,
    938925            'bandwidth_limit'  => $usage['bandwidth']['limit_bytes'] ?? 0,
    939             'cache_used'       => $usage['cache']['used_bytes'] ?? 0,
    940926            'cache_limit'      => $usage['cache']['limit_bytes'] ?? 0,
    941927            'cache_hits'       => $usage['cache_hits'] ?? 0,
     
    11581144     *
    11591145     * @param array $site Site data to cache.
    1160      */
    1161     private function cache_site($site) {
     1146     * @param int   $ttl  Cache TTL in seconds.
     1147     */
     1148    private function cache_site($site, $ttl = self::CACHE_TTL) {
    11621149        $this->site_cache = $site;
    1163         set_transient('imgpro_cdn_site_data', $site, self::CACHE_TTL);
     1150        set_transient('imgpro_cdn_site_data', $site, $ttl);
    11641151    }
    11651152
  • bandwidth-saver/trunk/includes/class-imgpro-cdn-onboarding.php

    r3411500 r3446766  
    7878        // For step 1: only show if no existing subscription
    7979        $tier = $all_settings['cloud_tier'] ?? '';
    80         if (in_array($tier, [ImgPro_CDN_Settings::TIER_FREE, ImgPro_CDN_Settings::TIER_PRO, ImgPro_CDN_Settings::TIER_ACTIVE], true)) {
     80        if (in_array($tier, [ImgPro_CDN_Settings::TIER_FREE, ImgPro_CDN_Settings::TIER_UNLIMITED, ImgPro_CDN_Settings::TIER_PRO, ImgPro_CDN_Settings::TIER_ACTIVE], true)) {
    8181            return false;
    8282        }
     
    145145        ?>
    146146        <div class="imgpro-onboarding-content imgpro-onboarding-step-1">
    147             <h1><?php esc_html_e('Speed up your images', 'bandwidth-saver'); ?></h1>
     147            <h1><?php esc_html_e('Speed up your media', 'bandwidth-saver'); ?></h1>
    148148
    149149            <p class="imgpro-onboarding-description">
    150                 <?php esc_html_e('Slow images hurt your SEO and drive visitors away.', 'bandwidth-saver'); ?><br><?php esc_html_e('Speed them up in 60 seconds.', 'bandwidth-saver'); ?>
     150                <?php esc_html_e('Slow media hurts your SEO and drives visitors away.', 'bandwidth-saver'); ?><br><?php esc_html_e('Speed it up in 60 seconds.', 'bandwidth-saver'); ?>
    151151            </p>
    152152
     
    158158                <li>
    159159                    <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M16.667 5L7.5 14.167 3.333 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
    160                     <span><strong><?php esc_html_e('Faster pages', 'bandwidth-saver'); ?></strong> — <?php esc_html_e('images load from global servers', 'bandwidth-saver'); ?></span>
     160                    <span><strong><?php esc_html_e('Global delivery', 'bandwidth-saver'); ?></strong> — <?php esc_html_e('media loads from edge servers', 'bandwidth-saver'); ?></span>
    161161                </li>
    162162                <li>
    163163                    <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M16.667 5L7.5 14.167 3.333 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
    164                     <span><strong><?php esc_html_e('100GB/month free', 'bandwidth-saver'); ?></strong> — <?php esc_html_e('forever, no credit card required', 'bandwidth-saver'); ?></span>
     164                    <span><strong><?php esc_html_e('All media types', 'bandwidth-saver'); ?></strong> — <?php esc_html_e('images, video, audio & HLS', 'bandwidth-saver'); ?></span>
    165165                </li>
    166166            </ul>
     
    185185
    186186            <p class="imgpro-onboarding-hint">
    187                 <?php esc_html_e('Need more bandwidth?', 'bandwidth-saver'); ?>
    188                 <button type="button" class="imgpro-btn-link imgpro-open-plan-selector"><?php esc_html_e('See paid plans', 'bandwidth-saver'); ?></button>
     187                <?php esc_html_e('$19.99/mo to support the service.', 'bandwidth-saver'); ?>
     188                <button type="button" class="imgpro-btn-link imgpro-open-plan-selector"><?php esc_html_e('Learn more', 'bandwidth-saver'); ?></button>
    189189            </p>
    190190        </div>
     
    203203        ?>
    204204        <div class="imgpro-onboarding-content imgpro-onboarding-step-2">
    205             <h1><?php esc_html_e('Create your free account', 'bandwidth-saver'); ?></h1>
     205            <h1><?php esc_html_e('Create your account', 'bandwidth-saver'); ?></h1>
    206206
    207207            <p class="imgpro-onboarding-description">
    208                 <?php esc_html_e('Enter your email to set up your CDN. No credit card required.', 'bandwidth-saver'); ?>
     208                <?php esc_html_e('Enter your email to set up your Media CDN.', 'bandwidth-saver'); ?>
    209209            </p>
    210210
     
    244244                <div class="imgpro-onboarding-actions">
    245245                    <button type="submit" class="imgpro-btn imgpro-btn-primary imgpro-btn-lg imgpro-btn-full">
    246                         <span class="imgpro-btn-text"><?php esc_html_e('Create Free Account', 'bandwidth-saver'); ?></span>
     246                        <span class="imgpro-btn-text"><?php esc_html_e('Create Account', 'bandwidth-saver'); ?></span>
    247247                        <span class="imgpro-btn-loading">
    248248                            <svg class="imgpro-spinner" width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="50" stroke-linecap="round"/></svg>
     
    283283
    284284            <p class="imgpro-onboarding-description">
    285                 <?php esc_html_e('Toggle on to start serving images from the CDN.', 'bandwidth-saver'); ?>
     285                <?php esc_html_e('Toggle on to start serving media from the CDN.', 'bandwidth-saver'); ?>
    286286            </p>
    287287
     
    292292                    </div>
    293293                    <div class="imgpro-activate-text">
    294                         <strong><?php esc_html_e('Image CDN', 'bandwidth-saver'); ?></strong>
    295                         <span><?php esc_html_e('Serve images from edge servers worldwide', 'bandwidth-saver'); ?></span>
     294                        <strong><?php esc_html_e('Media CDN', 'bandwidth-saver'); ?></strong>
     295                        <span><?php esc_html_e('Serve media from edge servers worldwide', 'bandwidth-saver'); ?></span>
    296296                    </div>
    297297                </div>
     
    299299                    <input type="checkbox" id="imgpro-activate-toggle">
    300300                    <span class="imgpro-toggle-slider"></span>
    301                     <span class="screen-reader-text"><?php esc_html_e('Enable Image CDN', 'bandwidth-saver'); ?></span>
     301                    <span class="screen-reader-text"><?php esc_html_e('Enable Media CDN', 'bandwidth-saver'); ?></span>
    302302                </label>
    303303            </div>
     
    308308                    <li>
    309309                        <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="3" fill="currentColor"/></svg>
    310                         <?php esc_html_e('Image URLs on your public pages point to the CDN', 'bandwidth-saver'); ?>
     310                        <?php esc_html_e('Media URLs on your public pages point to the CDN', 'bandwidth-saver'); ?>
    311311                    </li>
    312312                    <li>
    313313                        <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="3" fill="currentColor"/></svg>
    314                         <?php esc_html_e('Each image is cached on first request', 'bandwidth-saver'); ?>
     314                        <?php esc_html_e('Each file is cached on first request', 'bandwidth-saver'); ?>
    315315                    </li>
    316316                    <li>
     
    324324                    <li>
    325325                        <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="3" fill="currentColor"/></svg>
    326                         <?php esc_html_e('If anything goes wrong, images load directly from your site', 'bandwidth-saver'); ?>
     326                        <?php esc_html_e('If anything goes wrong, media loads directly from your site', 'bandwidth-saver'); ?>
    327327                    </li>
    328328                </ul>
     
    352352
    353353            <p class="imgpro-onboarding-description">
    354                 <?php esc_html_e('Your images are now being served from edge locations around the world. Visit your site to start caching.', 'bandwidth-saver'); ?>
     354                <?php esc_html_e('Your media is now being served from edge locations around the world. Visit your site to start caching.', 'bandwidth-saver'); ?>
    355355            </p>
    356356
  • bandwidth-saver/trunk/includes/class-imgpro-cdn-plan-selector.php

    r3410060 r3446766  
    33 * ImgPro CDN Plan Selector Component
    44 *
    5  * Unified plan selection UI used throughout the plugin.
     5 * Single-tier subscription UI. Shows payment prompt for unpaid users
     6 * and subscription status for active subscribers.
    67 *
    78 * @package ImgPro_CDN
     
    5253     */
    5354    public function render($context = 'modal', $current_tier = '') {
    54         $tiers = $this->api->get_tiers();
    5555        $all_settings = $this->settings->get_all();
    56 
    57         if (empty($current_tier)) {
    58             $current_tier = $all_settings['cloud_tier'] ?? '';
    59         }
    60 
    61         // Filter to only paid tiers for upgrade context
    62         $paid_tiers = array_filter($tiers, function($tier) {
    63             return $tier['price']['cents'] > 0;
    64         });
     56        $is_paid = ImgPro_CDN_Settings::is_paid($all_settings);
    6557
    6658        $wrapper_class = 'imgpro-plan-selector';
     
    7163        }
    7264        ?>
    73         <div class="<?php echo esc_attr($wrapper_class); ?>" data-current-tier="<?php echo esc_attr($current_tier); ?>">
     65        <div class="<?php echo esc_attr($wrapper_class); ?>">
    7466            <?php if ('modal' === $context): ?>
    7567            <div class="imgpro-plan-selector__header">
    76                 <h2><?php esc_html_e('Upgrade your plan', 'bandwidth-saver'); ?></h2>
     68                <h2><?php echo $is_paid ? esc_html__('Subscription Active', 'bandwidth-saver') : esc_html__('Activate Your Subscription', 'bandwidth-saver'); ?></h2>
    7769                <button type="button" class="imgpro-plan-selector__close" aria-label="<?php esc_attr_e('Close', 'bandwidth-saver'); ?>">
    7870                    <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
     
    8476            <?php endif; ?>
    8577
    86             <div class="imgpro-plan-selector__grid">
    87                 <?php foreach ($paid_tiers as $tier): ?>
    88                     <?php $this->render_tier_card($tier, $current_tier); ?>
    89                 <?php endforeach; ?>
    90             </div>
    91 
     78            <?php if ($is_paid): ?>
     79                <?php $this->render_subscription_active(); ?>
     80            <?php else: ?>
     81                <?php $this->render_subscription_card(); ?>
     82            <?php endif; ?>
     83
     84            <?php if (!$is_paid): ?>
    9285            <div class="imgpro-plan-selector__footer">
    93                 <div class="imgpro-plan-selector__selected">
    94                     <span class="imgpro-plan-selector__selected-label"><?php esc_html_e('Selected:', 'bandwidth-saver'); ?></span>
    95                     <span class="imgpro-plan-selector__selected-plan" id="imgpro-selected-plan-name">
    96                         <?php esc_html_e('Pro', 'bandwidth-saver'); ?>
    97                     </span>
    98                     <span class="imgpro-plan-selector__selected-price" id="imgpro-selected-plan-price">
    99                         <?php esc_html_e('$14.99/mo', 'bandwidth-saver'); ?>
    100                     </span>
    101                 </div>
    102                 <button type="button" class="imgpro-btn imgpro-btn-primary imgpro-btn-lg" id="imgpro-plan-checkout" disabled>
    103                     <span class="imgpro-btn-text"><?php esc_html_e('Continue to Checkout', 'bandwidth-saver'); ?></span>
     86                <button type="button" class="imgpro-btn imgpro-btn-primary imgpro-btn-lg imgpro-btn-full" id="imgpro-plan-checkout" data-tier-id="unlimited">
     87                    <span class="imgpro-btn-text"><?php esc_html_e('Activate Subscription', 'bandwidth-saver'); ?></span>
    10488                    <span class="imgpro-btn-loading">
    10589                        <svg class="imgpro-spinner" width="20" height="20" viewBox="0 0 20 20">
     
    11498            </div>
    11599
    116             <?php if ('free' === $current_tier || empty($current_tier)): ?>
    117100            <p class="imgpro-plan-selector__hint">
    118                 <?php esc_html_e('All plans include a 7-day money-back guarantee.', 'bandwidth-saver'); ?>
     101                <?php esc_html_e('7-day money-back guarantee. Cancel anytime.', 'bandwidth-saver'); ?>
    119102            </p>
    120103            <?php endif; ?>
     
    124107
    125108    /**
    126      * Render a single tier card
    127      *
    128      * @param array  $tier         Tier data.
    129      * @param string $current_tier Current tier ID.
    130      * @return void
    131      */
    132     private function render_tier_card($tier, $current_tier) {
    133         $is_current = ($tier['id'] === $current_tier);
    134         $is_highlighted = !empty($tier['highlight']);
    135         $is_downgrade = $this->is_downgrade($tier['id'], $current_tier);
    136 
    137         $card_classes = ['imgpro-plan-card'];
    138         if ($is_current) {
    139             $card_classes[] = 'imgpro-plan-card--current';
    140         }
    141         if ($is_highlighted && !$is_current) {
    142             $card_classes[] = 'imgpro-plan-card--highlight';
    143         }
    144         if ($is_downgrade) {
    145             $card_classes[] = 'imgpro-plan-card--downgrade';
    146         }
    147 
    148         $price_display = $tier['price']['formatted'];
    149         $period = $tier['price']['period'] ?? '';
    150         ?>
    151         <div class="<?php echo esc_attr(implode(' ', $card_classes)); ?>"
    152              data-tier-id="<?php echo esc_attr($tier['id']); ?>"
    153              data-tier-name="<?php echo esc_attr($tier['name']); ?>"
    154              data-tier-price="<?php echo esc_attr($price_display . $period); ?>">
    155 
    156             <?php if ($is_highlighted && !$is_current): ?>
    157                 <div class="imgpro-plan-card__badge"><?php esc_html_e('Popular', 'bandwidth-saver'); ?></div>
    158             <?php elseif ($is_current): ?>
    159                 <div class="imgpro-plan-card__badge imgpro-plan-card__badge--current"><?php esc_html_e('Current', 'bandwidth-saver'); ?></div>
    160             <?php endif; ?>
     109     * Render the subscription card for unpaid users
     110     *
     111     * @return void
     112     */
     113    private function render_subscription_card() {
     114        ?>
     115        <div class="imgpro-plan-card imgpro-plan-card--single"
     116             data-tier-id="unlimited"
     117             data-tier-name="Unlimited"
     118             data-tier-price="$19.99/mo">
    161119
    162120            <div class="imgpro-plan-card__header">
    163                 <h3 class="imgpro-plan-card__name"><?php echo esc_html($tier['name']); ?></h3>
    164                 <?php if (!empty($tier['description'])): ?>
    165                     <p class="imgpro-plan-card__description"><?php echo esc_html($tier['description']); ?></p>
    166                 <?php endif; ?>
     121                <h3 class="imgpro-plan-card__name"><?php esc_html_e('Media CDN', 'bandwidth-saver'); ?></h3>
     122                <p class="imgpro-plan-card__description"><?php esc_html_e('Support the service you\'re already using.', 'bandwidth-saver'); ?></p>
    167123            </div>
    168124
    169125            <div class="imgpro-plan-card__price">
    170                 <span class="imgpro-plan-card__amount"><?php echo esc_html($price_display); ?></span>
    171                 <?php if ($period): ?>
    172                     <span class="imgpro-plan-card__period"><?php echo esc_html($period); ?></span>
    173                 <?php endif; ?>
     126                <span class="imgpro-plan-card__amount">$19.99</span>
     127                <span class="imgpro-plan-card__period">/mo</span>
    174128            </div>
    175129
     
    179133                        <path d="M13.333 4L6 11.333 2.667 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
    180134                    </svg>
    181                     <span>
    182                         <strong><?php echo esc_html($tier['limits']['bandwidth']['formatted']); ?></strong>
    183                         <?php if (empty($tier['limits']['bandwidth']['unlimited'])): ?>
    184                             <?php esc_html_e('bandwidth/mo', 'bandwidth-saver'); ?>
    185                         <?php else: ?>
    186                             <?php esc_html_e('bandwidth', 'bandwidth-saver'); ?>
    187                         <?php endif; ?>
    188                     </span>
    189                 </li>
    190                 <?php if (!empty($tier['features']['custom_domain'])): ?>
    191                 <li class="imgpro-plan-card__feature">
    192                     <svg class="imgpro-plan-card__feature-icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
    193                         <path d="M13.333 4L6 11.333 2.667 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
    194                     </svg>
    195                     <span><?php esc_html_e('Custom domain', 'bandwidth-saver'); ?></span>
    196                 </li>
    197                 <?php else: ?>
    198                 <li class="imgpro-plan-card__feature imgpro-plan-card__feature--disabled">
    199                     <svg class="imgpro-plan-card__feature-icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
    200                         <path d="M12 4L4 12M4 4l8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
    201                     </svg>
    202                     <span><?php esc_html_e('Custom domain', 'bandwidth-saver'); ?></span>
    203                 </li>
    204                 <?php endif; ?>
    205                 <?php if (!empty($tier['features']['priority_support'])): ?>
     135                    <span><?php esc_html_e('300+ global edge servers', 'bandwidth-saver'); ?></span>
     136                </li>
     137                <li class="imgpro-plan-card__feature">
     138                    <svg class="imgpro-plan-card__feature-icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
     139                        <path d="M13.333 4L6 11.333 2.667 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
     140                    </svg>
     141                    <span><?php esc_html_e('Images, video, audio & HLS streaming', 'bandwidth-saver'); ?></span>
     142                </li>
     143                <li class="imgpro-plan-card__feature">
     144                    <svg class="imgpro-plan-card__feature-icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
     145                        <path d="M13.333 4L6 11.333 2.667 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
     146                    </svg>
     147                    <span><?php esc_html_e('Custom domain (cdn.yoursite.com)', 'bandwidth-saver'); ?></span>
     148                </li>
     149                <li class="imgpro-plan-card__feature">
     150                    <svg class="imgpro-plan-card__feature-icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
     151                        <path d="M13.333 4L6 11.333 2.667 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
     152                    </svg>
     153                    <span><?php esc_html_e('Unlimited requests', 'bandwidth-saver'); ?></span>
     154                </li>
    206155                <li class="imgpro-plan-card__feature">
    207156                    <svg class="imgpro-plan-card__feature-icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
     
    210159                    <span><?php esc_html_e('Priority support', 'bandwidth-saver'); ?></span>
    211160                </li>
    212                 <?php endif; ?>
    213161            </ul>
    214 
    215             <div class="imgpro-plan-card__action">
    216                 <?php if ($is_current): ?>
    217                     <span class="imgpro-plan-card__current-label">
    218                         <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
    219                             <path d="M13.333 4L6 11.333 2.667 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
    220                         </svg>
    221                         <?php esc_html_e('Current plan', 'bandwidth-saver'); ?>
    222                     </span>
    223                 <?php elseif ($is_downgrade): ?>
    224                     <button type="button" class="imgpro-btn imgpro-btn-secondary imgpro-plan-card__select" data-tier="<?php echo esc_attr($tier['id']); ?>">
    225                         <?php esc_html_e('Downgrade', 'bandwidth-saver'); ?>
    226                     </button>
    227                 <?php else: ?>
    228                     <button type="button" class="imgpro-btn <?php echo esc_attr( $is_highlighted ? 'imgpro-btn-primary' : 'imgpro-btn-secondary' ); ?> imgpro-plan-card__select" data-tier="<?php echo esc_attr($tier['id']); ?>">
    229                         <?php esc_html_e('Select', 'bandwidth-saver'); ?>
    230                     </button>
    231                 <?php endif; ?>
    232             </div>
    233         </div>
    234         <?php
    235     }
    236 
    237     /**
    238      * Check if selecting a tier would be a downgrade
    239      *
    240      * @param string $target_tier  Target tier ID.
    241      * @param string $current_tier Current tier ID.
    242      * @return bool
    243      */
    244     private function is_downgrade($target_tier, $current_tier) {
    245         $tier_order = ['free' => 0, 'lite' => 1, 'pro' => 2, 'business' => 3];
    246 
    247         $target_order = $tier_order[$target_tier] ?? 0;
    248         $current_order = $tier_order[$current_tier] ?? 0;
    249 
    250         return $target_order < $current_order;
     162        </div>
     163        <?php
     164    }
     165
     166    /**
     167     * Render the subscription active confirmation
     168     *
     169     * @return void
     170     */
     171    private function render_subscription_active() {
     172        ?>
     173        <div class="imgpro-plan-active">
     174            <div class="imgpro-plan-active__icon">
     175                <svg width="48" height="48" viewBox="0 0 48 48" fill="none">
     176                    <circle cx="24" cy="24" r="24" fill="#10b981" fill-opacity="0.1"/>
     177                    <path d="M32 18L21 29L16 24" stroke="#10b981" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
     178                </svg>
     179            </div>
     180            <h3 class="imgpro-plan-active__title"><?php esc_html_e('Subscription Active', 'bandwidth-saver'); ?></h3>
     181            <p class="imgpro-plan-active__description">
     182                <?php esc_html_e('Thank you for supporting the Media CDN. Your subscription keeps the service running.', 'bandwidth-saver'); ?>
     183            </p>
     184            <button type="button" class="imgpro-btn imgpro-btn-secondary" id="imgpro-manage-subscription">
     185                <?php esc_html_e('Manage Subscription', 'bandwidth-saver'); ?>
     186            </button>
     187        </div>
     188        <?php
    251189    }
    252190
    253191    /**
    254192     * Render the modal overlay wrapper
    255      *
    256      * Call this once in the admin page, then use JavaScript to show/hide.
    257193     *
    258194     * @return void
     
    267203        </div>
    268204        <?php
    269         $this->render_upgrade_confirm_modal();
    270     }
    271 
    272     /**
    273      * Render the upgrade confirmation modal
    274      *
    275      * @return void
    276      */
    277     private function render_upgrade_confirm_modal() {
    278         ?>
    279         <div class="imgpro-confirm-modal" id="imgpro-upgrade-confirm-modal" style="display: none;" role="dialog" aria-modal="true">
    280             <div class="imgpro-confirm-modal__backdrop"></div>
    281             <div class="imgpro-confirm-modal__content">
    282                 <!-- Header -->
    283                 <div class="imgpro-confirm-modal__header">
    284                     <div class="imgpro-confirm-modal__badge"><?php esc_html_e('Upgrade to', 'bandwidth-saver'); ?></div>
    285                     <h2 class="imgpro-confirm-modal__title" id="imgpro-confirm-tier-name"></h2>
    286                     <div class="imgpro-confirm-modal__price">
    287                         <span class="imgpro-confirm-modal__price-amount" id="imgpro-confirm-tier-price-amount"></span>
    288                         <span class="imgpro-confirm-modal__price-period" id="imgpro-confirm-tier-price-period"></span>
    289                     </div>
    290                 </div>
    291 
    292                 <!-- Upgrade multiplier hero -->
    293                 <div class="imgpro-confirm-modal__hero" id="imgpro-confirm-hero">
    294                     <div class="imgpro-confirm-modal__multiplier" id="imgpro-confirm-multiplier"></div>
    295                     <div class="imgpro-confirm-modal__comparison" id="imgpro-confirm-comparison"></div>
    296                 </div>
    297 
    298                 <!-- Features checklist -->
    299                 <ul class="imgpro-confirm-modal__checklist" id="imgpro-confirm-checklist"></ul>
    300 
    301                 <!-- Footer -->
    302                 <div class="imgpro-confirm-modal__footer">
    303                     <p class="imgpro-confirm-modal__note">
    304                         <?php esc_html_e('Billed monthly. Cancel anytime.', 'bandwidth-saver'); ?>
    305                     </p>
    306                     <div class="imgpro-confirm-modal__actions">
    307                         <button type="button" class="imgpro-btn imgpro-btn-ghost" id="imgpro-upgrade-cancel">
    308                             <?php esc_html_e('Cancel', 'bandwidth-saver'); ?>
    309                         </button>
    310                         <button type="button" class="imgpro-btn imgpro-btn-primary" id="imgpro-upgrade-confirm">
    311                             <span class="imgpro-btn-text"><?php esc_html_e('Upgrade', 'bandwidth-saver'); ?> <span id="imgpro-confirm-btn-tier"></span> →</span>
    312                             <span class="imgpro-btn-loading">
    313                                 <svg class="imgpro-spinner" width="16" height="16" viewBox="0 0 20 20">
    314                                     <circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="50" stroke-linecap="round"/>
    315                                 </svg>
    316                                 <?php esc_html_e('Upgrading...', 'bandwidth-saver'); ?>
    317                             </span>
    318                         </button>
    319                     </div>
    320                 </div>
    321             </div>
    322         </div>
    323         <?php
    324     }
    325 
    326     /**
    327      * Render a compact upgrade CTA that opens the plan selector
     205    }
     206
     207    /**
     208     * Render a compact subscription CTA that opens the plan selector
    328209     *
    329210     * @param string $context Context for styling: 'card', 'inline', 'alert'.
     
    332213    public function render_upgrade_cta($context = 'card') {
    333214        $all_settings = $this->settings->get_all();
    334         $current_tier = $all_settings['cloud_tier'] ?? 'free';
    335 
    336         // Don't show upgrade CTA if already on highest tier
    337         if ('business' === $current_tier) {
     215
     216        // Don't show CTA if already paid
     217        if (ImgPro_CDN_Settings::is_paid($all_settings)) {
    338218            return;
    339219        }
     
    348228                <div class="imgpro-upgrade-cta__icon">
    349229                    <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
    350                         <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
     230                        <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
    351231                    </svg>
    352232                </div>
    353233                <div class="imgpro-upgrade-cta__content">
    354                     <h4><?php esc_html_e('Need more capacity?', 'bandwidth-saver'); ?></h4>
    355                     <p><?php esc_html_e('Upgrade for more bandwidth and features.', 'bandwidth-saver'); ?></p>
     234                    <h4><?php esc_html_e('Support This Service', 'bandwidth-saver'); ?></h4>
     235                    <p><?php esc_html_e('Activate your subscription to keep the CDN running.', 'bandwidth-saver'); ?></p>
    356236                </div>
    357237            <?php endif; ?>
    358238            <button type="button" class="imgpro-btn imgpro-btn-primary imgpro-open-plan-selector">
    359                 <?php esc_html_e('See upgrade options', 'bandwidth-saver'); ?>
     239                <?php esc_html_e('Activate Subscription', 'bandwidth-saver'); ?>
    360240                <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
    361241                    <path d="M3.333 8h9.334M8 3.333L12.667 8 8 12.667" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
  • bandwidth-saver/trunk/includes/class-imgpro-cdn-rewriter.php

    r3410060 r3446766  
    194194     *
    195195     * @since 0.1.0
     196     * @since 0.2.0 Added video and audio shortcode filters
    196197     * @return void
    197198     */
     
    212213        add_filter('wp_get_attachment_image_attributes', [$this, 'rewrite_attributes'], 999, 3);
    213214
    214         // Content filters
     215        // Content filters (processes img, video, audio, source tags)
    215216        add_filter('the_content', [$this, 'rewrite_content'], 999);
    216217        add_filter('post_thumbnail_html', [$this, 'rewrite_content'], 999);
    217218        add_filter('widget_text', [$this, 'rewrite_content'], 999);
     219
     220        // Video and audio shortcode filters (WordPress media embeds)
     221        add_filter('wp_video_shortcode', [$this, 'rewrite_content'], 999);
     222        add_filter('wp_audio_shortcode', [$this, 'rewrite_content'], 999);
    218223    }
    219224
     
    412417
    413418    /**
     419     * Build onerror fallback handler for video/audio elements
     420     *
     421     * Creates inline JavaScript that falls back to origin URLs on CDN failure.
     422     * Uses the same URL conversion pattern as get_onerror_handler() for images.
     423     * Unlike images, video/audio may have multiple source elements that all
     424     * need to be rewritten, plus a poster attribute for videos.
     425     *
     426     * IMPORTANT: Only transforms URLs that were marked as CDN URLs:
     427     * - this.src only if data-imgpro-cdn is set on the element
     428     * - this.poster only if data-imgpro-poster is set on the element
     429     * - source children only if they have data-imgpro-cdn
     430     * This prevents corrupting non-CDN URLs (e.g., YouTube embeds).
     431     *
     432     * @since 1.0
     433     * @return string JavaScript onerror handler for media elements.
     434     */
     435    private function get_media_onerror_handler() {
     436        // Fallback logic for video/audio (same pattern as images):
     437        // 1. Check if not already in fallback state
     438        // 2. Mark as fallback='1' (trying origin)
     439        // 3. Track if any URLs were transformed
     440        // 4. Rewrite direct src ONLY if data-imgpro-cdn is set
     441        // 5. Rewrite all child source elements with data-imgpro-cdn
     442        // 6. Rewrite poster ONLY if data-imgpro-poster is set
     443        // 7. Only call load() if something was transformed (avoid unnecessary retries)
     444        $handler = "if (!this.dataset.fallback) { "
     445                 . "this.dataset.fallback = '1'; "
     446                 . "var changed = false; "
     447                 // Rewrite direct src only if marked as CDN
     448                 . "if (this.src && this.dataset.imgproCdn) { var p = this.src.split('/').slice(3); this.src = 'https://' + p[0] + '/' + p.slice(1).join('/'); changed = true; } "
     449                 // Rewrite source children (only those with data-imgpro-cdn)
     450                 . "var sources = this.querySelectorAll('source[data-imgpro-cdn]'); "
     451                 . "for (var i = 0; i < sources.length; i++) { var sp = sources[i].src.split('/').slice(3); sources[i].src = 'https://' + sp[0] + '/' + sp.slice(1).join('/'); changed = true; } "
     452                 // Rewrite poster only if marked as CDN
     453                 . "if (this.poster && this.dataset.imgproPoster) { var pp = this.poster.split('/').slice(3); this.poster = 'https://' + pp[0] + '/' + pp.slice(1).join('/'); changed = true; } "
     454                 // Only reload if we actually changed something
     455                 . "if (changed) { this.onerror = function() { this.dataset.fallback = '2'; this.onerror = null; }; this.load(); } "
     456                 . "}";
     457
     458        return $handler;
     459    }
     460
     461    /**
    414462     * Rewrite image attributes
    415463     *
     
    467515     * Rewrite content HTML
    468516     *
    469      * Processes images in HTML content that weren't processed by rewrite_attributes()
     517     * Processes media elements in HTML content that weren't processed by rewrite_attributes()
    470518     *
    471519     * ARCHITECTURE:
    472      * - ONLY processes images WITHOUT data-imgpro-cdn (not yet processed)
    473      * - NEVER modifies images already processed by rewrite_attributes()
     520     * - ONLY processes elements WITHOUT data-imgpro-cdn (not yet processed)
     521     * - NEVER modifies elements already processed by rewrite_attributes()
    474522     * - Uses WP_HTML_Tag_Processor for safe, spec-compliant HTML parsing (requires WP 6.2+)
    475523     *
    476524     * @since 0.1.0
     525     * @since 0.2.0 Added video, audio, and source tag support
    477526     * @param string $content HTML content.
    478527     * @return string
     
    489538        }
    490539
    491         // Early bail-out: Skip processing if no image tags present
     540        // Early bail-out: Skip processing if no media tags present
    492541        // This is a performance optimization for text-only content
    493         if (false === stripos($content, '<img') && false === stripos($content, '<amp-img') && false === stripos($content, '<amp-anim')) {
     542        $has_media_tags = false;
     543        $tag_patterns = ['<img', '<amp-img', '<amp-anim', '<video', '<audio', '<source'];
     544
     545        foreach ($tag_patterns as $pattern) {
     546            if (false !== stripos($content, $pattern)) {
     547                $has_media_tags = true;
     548                break;
     549            }
     550        }
     551
     552        if (!$has_media_tags) {
    494553            return $content;
    495554        }
     
    509568     * Rewrite content using WP_HTML_Tag_Processor (modern approach)
    510569     *
    511      * @since 0.1.0
     570     * Processes images, videos, audio, and source elements.
     571     *
     572     * @since 0.1.0
     573     * @since 0.2.0 Added video, audio, and source tag support
    512574     * @param string $content HTML content.
    513575     * @return string Modified content.
     
    516578        $processor = new WP_HTML_Tag_Processor($content);
    517579
    518         // Process all image tags (img, amp-img, amp-anim)
    519         $tag_names = ['IMG', 'AMP-IMG', 'AMP-ANIM'];
     580        // All tags we process
     581        $image_tags = ['IMG', 'AMP-IMG', 'AMP-ANIM'];
     582        $media_tags = ['VIDEO', 'AUDIO', 'SOURCE'];
     583        $all_tags = array_merge($image_tags, $media_tags);
    520584
    521585        while ($processor->next_tag()) {
    522586            $tag = $processor->get_tag();
    523587
    524             // Skip if not an image tag
    525             if (!in_array($tag, $tag_names, true)) {
     588            // Skip if not a media tag
     589            if (!in_array($tag, $all_tags, true)) {
    526590                continue;
    527591            }
     
    534598            // Get src attribute
    535599            $src = $processor->get_attribute('src');
    536             if (empty($src)) {
    537                 continue;
    538             }
    539 
    540             // Get true origin URL (extracts if already CDN)
    541             $origin_url = $this->get_true_origin($src);
    542 
    543             // Skip if not a valid image URL
    544             if (!$this->should_rewrite($origin_url)) {
    545                 continue;
    546             }
    547 
    548             // Build CDN URL from origin
    549             $cdn_url = $this->build_cdn_url($origin_url);
    550 
    551             // Update src attribute to CDN URL
    552             $processor->set_attribute('src', esc_url($cdn_url));
    553 
    554             // Add data attribute for identification
    555             $processor->set_attribute('data-imgpro-cdn', '1');
    556 
    557             // Add onload handler (adds imgpro-loaded class for CSS visibility)
    558             $processor->set_attribute('onload', $this->get_onload_handler());
    559 
    560             // Add onerror fallback handler
    561             $processor->set_attribute('onerror', $this->get_onerror_handler());
     600
     601            // Process src if present and valid
     602            if (!empty($src)) {
     603                $origin_url = $this->get_true_origin($src);
     604
     605                if ($this->should_rewrite($origin_url)) {
     606                    $cdn_url = $this->build_cdn_url($origin_url);
     607                    $processor->set_attribute('src', esc_url($cdn_url));
     608                    $processor->set_attribute('data-imgpro-cdn', '1');
     609
     610                    // Images: add onload for CSS class and onerror for fallback
     611                    if (in_array($tag, $image_tags, true)) {
     612                        $processor->set_attribute('onload', $this->get_onload_handler());
     613                        $processor->set_attribute('onerror', $this->get_onerror_handler());
     614                    }
     615                }
     616            }
     617
     618            // VIDEO/AUDIO: Always add onerror handler for CDN fallback
     619            // This is needed even without direct src, because:
     620            // - WordPress videos typically use <source> children, not src attribute
     621            // - The onerror handler rewrites all child sources with data-imgpro-cdn
     622            // - If no CDN sources exist, the handler is a harmless no-op
     623            // Note: SOURCE elements don't get onerror - error fires on parent media element
     624            if ($tag === 'VIDEO' || $tag === 'AUDIO') {
     625                $processor->set_attribute('onerror', $this->get_media_onerror_handler());
     626            }
     627
     628            // VIDEO tag: also process 'poster' attribute (thumbnail image)
     629            if ($tag === 'VIDEO') {
     630                $poster = $processor->get_attribute('poster');
     631                if (!empty($poster)) {
     632                    $origin_poster = $this->get_true_origin($poster);
     633                    if ($this->should_rewrite($origin_poster)) {
     634                        $processor->set_attribute('poster', esc_url($this->build_cdn_url($origin_poster)));
     635                        // Mark poster as CDN so onerror handler knows to transform it
     636                        $processor->set_attribute('data-imgpro-poster', '1');
     637                    }
     638                }
     639            }
    562640        }
    563641
     
    584662     *
    585663     * @since 0.1.0
     664     * @since 0.2.0 Now supports video, audio, and HLS files
    586665     * @param string $url URL to check.
    587666     * @return bool
     
    606685        }
    607686
    608         // Must be an image
    609         if (!$this->is_image_url($url)) {
     687        // Must be a supported media file (images, video, audio, HLS)
     688        if (!$this->is_media_url($url)) {
    610689            return false;
    611690        }
     
    615694
    616695    /**
    617      * Check if URL is an image
    618      *
    619      * @since 0.1.0
     696     * Check if URL is a supported media file
     697     *
     698     * Supports images, video, audio, and HLS streaming files.
     699     *
     700     * @since 0.2.0
    620701     * @param string $url URL to check.
    621      * @return bool True if URL points to an image file.
    622      */
    623     private function is_image_url($url) {
     702     * @return bool True if URL points to a supported media file.
     703     */
     704    private function is_media_url($url) {
    624705        /**
    625          * Filter the list of allowed image extensions
     706         * Filter the list of allowed media extensions
    626707         *
     708         * @since 1.0
    627709         * @param array $extensions List of file extensions (without dots)
    628710         */
    629         $extensions = apply_filters('imgpro_image_extensions', [
    630             'jpg',
    631             'jpeg',
    632             'png',
    633             'gif',
    634             'webp',
    635             'avif',
    636             'svg',
     711        $extensions = apply_filters('imgpro_media_extensions', [
     712            // Images
     713            'jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'svg',
     714            'bmp', 'tiff', 'ico', 'heic', 'heif',
     715            // Video
     716            'mp4', 'm4v', 'webm', 'ogv', 'mov', 'mkv',
     717            // Audio
     718            'mp3', 'ogg', 'wav', 'm4a', 'flac', 'aac', 'weba',
     719            // HLS
     720            'm3u8', 'ts',
    637721        ]);
    638722
  • bandwidth-saver/trunk/includes/class-imgpro-cdn-settings.php

    r3410342 r3446766  
    7777
    7878    /**
    79      * Subscription tier: Business (paid)
     79     * Subscription tier: Business (paid, legacy)
    8080     *
    8181     * @since 0.1.7
     
    8383     */
    8484    const TIER_BUSINESS = 'business';
     85
     86    /**
     87     * Subscription tier: Unlimited (paid, $19.99/mo)
     88     *
     89     * The main paid tier with unlimited bandwidth and cache.
     90     *
     91     * @since 0.3.0
     92     * @var string
     93     */
     94    const TIER_UNLIMITED = 'unlimited';
    8595
    8696    /**
     
    238248
    239249        // Usage stats (synced from Cloud API)
    240         // Bandwidth is primary metric (monthly), Cache is secondary (LRU-managed)
     250        // Bandwidth is primary metric (monthly reset)
    241251        'bandwidth_used'     => 0,
    242252        'bandwidth_limit'    => 0,
    243         'cache_used'         => 0,
    244253        'cache_limit'        => 0,
    245254        'cache_hits'         => 0,
     
    393402        if (isset($settings['cloud_tier'])) {
    394403            $tier = sanitize_text_field($settings['cloud_tier']);
    395             if (in_array($tier, [self::TIER_NONE, self::TIER_FREE, self::TIER_LITE, self::TIER_PRO, self::TIER_BUSINESS, self::TIER_ACTIVE, self::TIER_CANCELLED, self::TIER_PAST_DUE, self::TIER_SUSPENDED], true)) {
     404            if (in_array($tier, [self::TIER_NONE, self::TIER_FREE, self::TIER_LITE, self::TIER_PRO, self::TIER_BUSINESS, self::TIER_UNLIMITED, self::TIER_ACTIVE, self::TIER_CANCELLED, self::TIER_PAST_DUE, self::TIER_SUSPENDED], true)) {
    396405                $validated['cloud_tier'] = $tier;
    397406            }
     
    405414        if (isset($settings['bandwidth_limit'])) {
    406415            $validated['bandwidth_limit'] = absint($settings['bandwidth_limit']);
    407         }
    408         if (isset($settings['cache_used'])) {
    409             $validated['cache_used'] = absint($settings['cache_used']);
    410416        }
    411417        if (isset($settings['cache_limit'])) {
     
    733739        if (self::MODE_CLOUD === $mode) {
    734740            $tier = $settings['cloud_tier'] ?? '';
    735             // Valid tiers: free, lite, pro, business, active (legacy), past_due (grace period)
    736             return in_array($tier, [self::TIER_FREE, self::TIER_LITE, self::TIER_PRO, self::TIER_BUSINESS, self::TIER_ACTIVE, self::TIER_PAST_DUE], true);
     741            // Valid tiers: free (trial), unlimited, lite, pro, business (legacy), active (legacy), past_due (grace period)
     742            return in_array($tier, [self::TIER_FREE, self::TIER_UNLIMITED, self::TIER_LITE, self::TIER_PRO, self::TIER_BUSINESS, self::TIER_ACTIVE, self::TIER_PAST_DUE], true);
    737743        } elseif (self::MODE_CLOUDFLARE === $mode) {
    738744            return !empty($settings['cdn_url']);
     
    779785
    780786    /**
    781      * Check if user has any paid subscription (lite, pro, or business)
     787     * Check if user has any paid subscription (unlimited or legacy tiers)
    782788     *
    783789     * @since 0.1.7
     
    788794        $tier = $settings['cloud_tier'] ?? '';
    789795        // past_due still counts as paid (grace period)
    790         return in_array($tier, [self::TIER_LITE, self::TIER_PRO, self::TIER_BUSINESS, self::TIER_ACTIVE, self::TIER_PAST_DUE], true);
     796        // unlimited is the primary paid tier, legacy tiers (lite, pro, business) still supported
     797        return in_array($tier, [self::TIER_UNLIMITED, self::TIER_LITE, self::TIER_PRO, self::TIER_BUSINESS, self::TIER_ACTIVE, self::TIER_PAST_DUE], true);
    791798    }
    792799
     
    805812     * Check if tier has custom domain feature
    806813     *
    807      * @since 0.1.7
    808      * @param array $settings The settings array to check against.
    809      * @return bool True if custom domain is available.
     814     * All users get custom domain feature (single-tier model).
     815     *
     816     * @since 0.1.7
     817     * @param array $settings The settings array to check against.
     818     * @return bool Always true - custom domains available to all users.
    810819     */
    811820    public static function has_custom_domain($settings) {
    812         $tier = $settings['cloud_tier'] ?? '';
    813         // Custom domain available on all paid tiers (Lite, Pro, Business)
    814         return in_array($tier, [self::TIER_LITE, self::TIER_PRO, self::TIER_BUSINESS, self::TIER_ACTIVE, self::TIER_PAST_DUE], true);
    815     }
    816 
    817     /**
    818      * Check if user is on free tier
    819      *
    820      * @since 0.1.7
    821      * @param array $settings The settings array to check against.
    822      * @return bool True if user is on free tier.
     821        // Single-tier model: everyone gets all features including custom domains
     822        return true;
     823    }
     824
     825    /**
     826     * Check if user is on free tier (trial)
     827     *
     828     * @since 0.1.7
     829     * @param array $settings The settings array to check against.
     830     * @return bool True if user is on free/trial tier.
    823831     */
    824832    public static function is_free($settings) {
    825833        return self::TIER_FREE === ($settings['cloud_tier'] ?? '');
     834    }
     835
     836    /**
     837     * Check if user is on trial tier (alias for is_free)
     838     *
     839     * The free tier is now branded as "Trial" in the UI.
     840     *
     841     * @since 0.3.0
     842     * @param array $settings The settings array to check against.
     843     * @return bool True if user is on trial tier.
     844     */
     845    public static function is_trial($settings) {
     846        return self::is_free($settings);
     847    }
     848
     849    /**
     850     * Check if user is on unlimited tier
     851     *
     852     * @since 0.3.0
     853     * @param array $settings The settings array to check against.
     854     * @return bool True if user is on unlimited tier.
     855     */
     856    public static function is_unlimited($settings) {
     857        return self::TIER_UNLIMITED === ($settings['cloud_tier'] ?? '');
    826858    }
    827859
     
    852884     * Get bandwidth limit for current tier
    853885     *
    854      * Bandwidth is the primary metric (resets monthly).
    855      *
    856      * @since 0.2.0
    857      * @param array $settings The settings array to check against.
    858      * @return int Bandwidth limit in bytes.
     886     * Bandwidth is tracked for reporting purposes only on unlimited tier.
     887     * Returns -1 for unlimited tier (no limit).
     888     *
     889     * @since 0.2.0
     890     * @param array $settings The settings array to check against.
     891     * @return int Bandwidth limit in bytes, -1 for unlimited.
    859892     */
    860893    public static function get_bandwidth_limit($settings) {
    861894        $tier = $settings['cloud_tier'] ?? '';
    862895        switch ($tier) {
     896            case self::TIER_UNLIMITED:
     897                return -1; // Unlimited
    863898            case self::TIER_BUSINESS:
    864899                return self::BUSINESS_BANDWIDTH_LIMIT;
     
    879914     * Get bandwidth usage percentage
    880915     *
    881      * @since 0.2.0
    882      * @param array $settings The settings array to check against.
    883      * @return float Percentage of bandwidth used (0-100).
     916     * Returns 0 for unlimited tier (no percentage applies).
     917     *
     918     * @since 0.2.0
     919     * @param array $settings The settings array to check against.
     920     * @return float Percentage of bandwidth used (0-100), 0 for unlimited.
    884921     */
    885922    public static function get_bandwidth_percentage($settings) {
    886923        $limit = self::get_bandwidth_limit($settings);
     924        // Unlimited tier or invalid limit
    887925        if ($limit <= 0) {
    888926            return 0;
     
    896934     *
    897935     * Cache is auto-managed via LRU eviction.
    898      *
    899      * @since 0.2.0
    900      * @param array $settings The settings array to check against.
    901      * @return int Cache limit in bytes.
     936     * Returns -1 for unlimited tier (no limit).
     937     *
     938     * @since 0.2.0
     939     * @param array $settings The settings array to check against.
     940     * @return int Cache limit in bytes, -1 for unlimited.
    902941     */
    903942    public static function get_cache_limit($settings) {
    904943        $tier = $settings['cloud_tier'] ?? '';
    905944        switch ($tier) {
     945            case self::TIER_UNLIMITED:
     946                return -1; // Unlimited
    906947            case self::TIER_BUSINESS:
    907948                return self::BUSINESS_CACHE_LIMIT;
     
    920961
    921962    /**
    922      * Get cache usage percentage
    923      *
    924      * @since 0.2.0
    925      * @param array $settings The settings array to check against.
    926      * @return float Percentage of cache used (0-100).
    927      */
    928     public static function get_cache_percentage($settings) {
    929         $limit = self::get_cache_limit($settings);
    930         if ($limit <= 0) {
    931             return 0;
    932         }
    933         $used = $settings['cache_used'] ?? 0;
    934         return min(100, ($used / $limit) * 100);
    935     }
    936 
    937     /**
    938963     * Handle API error with action hook for logging
    939964     *
  • bandwidth-saver/trunk/readme.txt

    r3442901 r3446766  
    1 === Free Image CDN – Bandwidth Saver ===
     1=== Bandwidth Saver: Unlimited Media CDN ===
    22Contributors: imgpro
    3 Tags: image cdn, cdn, speed, core web vitals, performance
     3Tags: media cdn, cdn, video cdn, image cdn, hls streaming
    44Requires at least: 6.2
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 0.2.5
     7Stable tag: 1.0
    88License: GPLv2 or later
    99License URI: http://www.gnu.org/licenses/gpl-2.0.html
    1010
    11 Free global image CDN for WordPress. Serve images from 300+ edge servers worldwide. Improves Core Web Vitals and PageSpeed scores. One-click setup.
     11Unlimited media CDN for WordPress. Serve images, video, audio, and HLS streams from 300+ edge servers. $19.99/mo for unlimited bandwidth.
    1212
    1313== Description ==
    1414
    15 **Free image CDN that makes your WordPress site faster.**
    16 
    17 Images are the heaviest part of most WordPress pages. When they load slowly, visitors leave, Core Web Vitals fail, and Google ranks you lower.
    18 
    19 This free image CDN plugin fixes that by serving your images from 300+ global edge servers. A visitor in Tokyo loads images from Asia. A visitor in London loads from Europe. Everyone gets faster pages.
    20 
    21 **60-second setup.** No DNS changes. No external accounts. No settings to configure. Just activate and flip the switch.
    22 
    23 = Why Use an Image CDN? =
    24 
    25 * **Faster page load times** — Images load from the nearest server instead of traveling across the world from your host
     15**Unlimited media CDN that makes your WordPress site faster.**
     16
     17Heavy media files slow down your site. When videos buffer and images lag, visitors leave, Core Web Vitals fail, and Google ranks you lower.
     18
     19This media CDN plugin fixes that by serving your images, videos, audio, and HLS streams from 300+ global edge servers. A visitor in Tokyo loads media from Asia. A visitor in London loads from Europe. Everyone gets faster pages.
     20
     21**60-second setup.** No DNS changes. No external accounts. No settings to configure. Just activate and start delivering.
     22
     23= Why Use a Media CDN? =
     24
     25* **Faster page load times** — Media loads from the nearest server instead of traveling across the world from your host
    2626* **Better Core Web Vitals** — Improve LCP (Largest Contentful Paint) by delivering images faster
     27* **Smooth video playback** — HLS streaming and video files buffer less with edge delivery
    2728* **Higher PageSpeed scores** — Google PageSpeed Insights will show improved performance
    2829* **Lower bounce rates** — Visitors don't wait for slow sites
    2930
    30 = How This Image CDN Works =
    31 
    32 1. Install the free image CDN plugin from WordPress
     31= All Media Types Supported =
     32
     33* **Images** — JPG, PNG, GIF, WebP, AVIF, SVG
     34* **Video** — MP4, WebM, MOV with range request support
     35* **Audio** — MP3, WAV, OGG, FLAC
     36* **HLS Streaming** — M3U8 playlists and TS segments
     37
     38= How This Media CDN Works =
     39
     401. Install the media CDN plugin from WordPress
    33412. Flip the switch to activate
    34 3. Images instantly load from 300+ global CDN servers
    35 
    36 That's it. Your original images stay exactly where they are on your server. The plugin only changes URLs on your public pages. Deactivate it and everything returns to normal instantly.
    37 
    38 = Free Image CDN vs Paid CDN Services =
    39 
    40 Most CDN solutions require DNS changes, external account setup, and technical configuration. This image CDN plugin works out of the box:
    41 
    42 * **100 GB free per month** — Enough bandwidth for most WordPress sites
    43 * **No credit card required** — Free tier is free forever
    44 * **No DNS changes** — Works immediately after activation
    45 * **No configuration** — Zero settings to configure
    46 * **Can't break your site** — Automatic fallback to your server if anything goes wrong
     423. Media instantly loads from 300+ global CDN servers
     43
     44That's it. Your original files stay exactly where they are on your server. The plugin only changes URLs on your public pages. Deactivate it and everything returns to normal instantly.
     45
     46= Unlimited Media CDN Pricing =
     47
     48**Unlimited** ($19.99/mo)
     49* Unlimited bandwidth
     50* Custom CDN domain (cdn.yoursite.com)
     51* Priority support
     52* Images, video, audio, and HLS streaming
     53
     54All plans include a 7-day money-back guarantee.
     55
     56= Self-Hosted Option =
     57
     58For developers who want full control, you can deploy the open-source worker on your own Cloudflare account. Your media, your infrastructure, zero external dependencies.
     59
     60[Self-hosted CDN setup guide on GitHub](https://github.com/img-pro/bandwidth-saver-worker)
    4761
    4862= Works With Any WordPress Theme or Plugin =
    4963
    50 This image CDN is compatible with:
     64This media CDN is compatible with:
    5165
    5266* **Page builders** — Elementor, Divi, Beaver Builder, Gutenberg, Bricks, Oxygen
    5367* **WooCommerce** — Product images, galleries, thumbnails
    54 * **Image formats** — JPG, PNG, GIF, WebP, AVIF, SVG
     68* **Video players** — Plyr, VideoJS, native HTML5 video
    5569* **Lazy loading** — Works with native lazy load and plugins
    5670* **Responsive images** — Full srcset support
    5771* **Caching plugins** — WP Rocket, LiteSpeed Cache, W3 Total Cache, WP Super Cache
    5872
    59 = Who This Image CDN Is For =
    60 
     73= Who This Media CDN Is For =
     74
     75* Video course creators and membership sites
     76* Podcasters and audio content creators
    6177* Bloggers with image-heavy posts
    62 * WooCommerce stores with product photos
     78* WooCommerce stores with product photos and videos
    6379* Recipe, travel, and photography sites
    6480* Portfolio and agency sites
    65 * Anyone who wants faster WordPress image loading without complexity
    66 
    67 = Image CDN Pricing =
    68 
    69 **Free** — 100 GB/month, free forever, no credit card
    70 **Lite** ($4.99/mo) — 250 GB/month + custom CDN domain
    71 **Pro** ($14.99/mo) — 2 TB/month + custom CDN domain
    72 **Business** ($49/mo) — 10 TB/month + priority support
    73 
    74 All paid plans include custom domains (cdn.yoursite.com) with automatic SSL.
    75 
    76 = Self-Hosted Image CDN Option =
    77 
    78 For developers who want full control, you can deploy the open-source worker on your own Cloudflare account. Your images, your infrastructure, zero external dependencies.
    79 
    80 [Self-hosted CDN setup guide on GitHub](https://github.com/img-pro/bandwidth-saver-worker)
     81* Anyone who wants faster WordPress media loading without complexity
    8182
    8283== Installation ==
     
    8485**60-second setup. No technical knowledge required.**
    8586
    86 1. Install and activate the image CDN plugin
    87 2. Go to **Settings Bandwidth Saver**
     871. Install and activate the media CDN plugin
     882. Go to **Settings > Bandwidth Saver**
    88893. Toggle the CDN switch on
    89 
    90 Done. Your images are now loading faster from the global CDN. No email required for the free tier.
     904. Upgrade to Unlimited for $19.99/mo
     91
     92Done. Your media is now loading faster from the global CDN.
    9193
    9294== Frequently Asked Questions ==
    9395
    94 = Is this really a free image CDN? =
    95 
    96 Yes. 100 GB of bandwidth per month, free forever. No credit card required. No trial period. Most WordPress sites never need to upgrade.
    97 
    98 = Will this image CDN improve my Core Web Vitals? =
    99 
    100 Yes. The image CDN improves LCP (Largest Contentful Paint) by serving images from servers close to your visitors. Faster image delivery means better Core Web Vitals scores.
    101 
    102 = How much will my PageSpeed score improve? =
    103 
    104 Results vary by site, but most users see significant improvements in their Google PageSpeed Insights scores after enabling the image CDN. The improvement is most noticeable for visitors far from your hosting server.
    105 
    106 = Does this CDN work with WooCommerce? =
    107 
    108 Yes. The image CDN works with WooCommerce product images, galleries, thumbnails, and all image-heavy ecommerce content.
     96= What media types does this CDN support? =
     97
     98The media CDN supports all common media formats: images (JPG, PNG, GIF, WebP, AVIF, SVG), video (MP4, WebM, MOV), audio (MP3, WAV, OGG, FLAC), and HLS streaming (M3U8 playlists and TS segments).
     99
     100= Will this media CDN improve my Core Web Vitals? =
     101
     102Yes. The media CDN improves LCP (Largest Contentful Paint) by serving media from servers close to your visitors. Faster media delivery means better Core Web Vitals scores.
     103
     104= How does video streaming work? =
     105
     106The CDN supports HTTP range requests, which means video files can be seeked and streamed without downloading the entire file. HLS streams work seamlessly with M3U8 playlist and TS segment delivery.
     107
     108= Does the CDN work with WooCommerce? =
     109
     110Yes. The media CDN works with WooCommerce product images, galleries, thumbnails, and product videos.
    109111
    110112= Will this CDN work with my page builder? =
    111113
    112 Yes. This image CDN works with Elementor, Divi, Beaver Builder, Gutenberg blocks, Bricks, Oxygen, and any other WordPress page builder.
    113 
    114 = Does the image CDN support WebP and AVIF? =
    115 
    116 Yes. The CDN serves all image formats including JPG, PNG, GIF, WebP, AVIF, and SVG.
    117 
    118 = What if I go over my CDN bandwidth limit? =
    119 
    120 Your images will temporarily load directly from your server (the normal way) until your bandwidth resets next month. Nothing breaks — your site just loads images without the CDN temporarily.
    121 
    122 = Can I use this image CDN with caching plugins? =
    123 
    124 Yes. This image CDN works perfectly with WP Rocket, LiteSpeed Cache, W3 Total Cache, WP Super Cache, and other WordPress caching plugins.
    125 
    126 = Is this image CDN safe? Will it break my site? =
    127 
    128 The image CDN cannot break your site. Your original images stay on your server completely untouched. The plugin only changes URLs on your public pages. If the CDN ever has issues, your site automatically falls back to loading images directly. Deactivate the plugin and everything returns to normal instantly.
    129 
    130 = Does this replace image optimization plugins? =
    131 
    132 No. This is an image *delivery* CDN, not an optimization tool. It makes images load faster by serving them from nearby servers. For making images *smaller*, use a compression plugin like ShortPixel, Imagify, or Smush. They work great alongside this image CDN.
     114Yes. This media CDN works with Elementor, Divi, Beaver Builder, Gutenberg blocks, Bricks, Oxygen, and any other WordPress page builder.
     115
     116= Is there a file size limit? =
     117
     118Files up to 500 MB are supported. This covers most images and many video files. For very large video files, consider dedicated video hosting.
     119
     120= Can I use my own domain for CDN URLs? =
     121
     122Yes. The Unlimited plan supports custom domains (cdn.yoursite.com) with automatic SSL.
     123
     124= What happens if the media CDN goes down? =
     125
     126Your site automatically serves media directly from your server. Visitors won't notice anything — media just loads the normal way until the CDN is back.
     127
     128= Is this media CDN safe? Will it break my site? =
     129
     130The media CDN cannot break your site. Your original files stay on your server completely untouched. The plugin only changes URLs on your public pages. If the CDN ever has issues, your site automatically falls back to loading media directly. Deactivate the plugin and everything returns to normal instantly.
    133131
    134132= Do I need to change my DNS for this CDN? =
    135133
    136 No. Unlike other CDN services, this image CDN works immediately without any DNS changes. Everything happens from your WordPress admin.
    137 
    138 = Can I use my own domain for CDN URLs? =
    139 
    140 Yes. All paid plans support custom domains (cdn.yoursite.com) with automatic SSL.
    141 
    142 = What happens if the image CDN goes down? =
    143 
    144 Your site automatically serves images directly from your server. Visitors won't notice anything — images just load the normal way until the CDN is back.
     134No. Unlike other CDN services, this media CDN works immediately without any DNS changes. Everything happens from your WordPress admin.
    145135
    146136== Screenshots ==
    147137
    148 1. Speed up your images in 60 seconds with the free image CDN
    149 2. Track your CDN bandwidth and performance
    150 3. Generous free tier, simple upgrades
    151 4. Multi-site support and custom CDN domains
    152 5. Self-host option for full control
     1381. Speed up your media in 60 seconds with the unlimited media CDN
     1392. Track your CDN requests and performance
     1403. Multi-site support and custom CDN domains
     1414. Self-host option for full control
    153142
    154143== Privacy ==
     
    156145= What Data Is Collected? =
    157146
    158 The image CDN plugin does not add cookies, tracking pixels, or analytics to your site.
     147The media CDN plugin does not add cookies, tracking pixels, or analytics to your site.
    159148
    160149= Managed Mode =
    161150
    162151* Your site URL is used to configure CDN routing
    163 * Email is optional — only collected if you upgrade or request account recovery
     152* Email is collected when you upgrade to a paid plan
    164153* Custom domain settings are sent if configured
    165154
    166 Images are cached and served through a global edge network powered by Cloudflare.
     155Media is cached and served through a global edge network powered by Cloudflare.
    167156
    168157= Self-Hosted Mode =
    169158
    170 No data is sent to us. Images are cached in your own Cloudflare account.
     159No data is sent to us. Media is cached in your own Cloudflare account.
    171160
    172161== External Services ==
    173162
    174 This image CDN plugin connects to external services:
     163This media CDN plugin connects to external services:
    175164
    176165**Cloudflare (R2 Storage and Workers)**
    177166
    178 * Purpose: Image caching and global edge CDN delivery
     167* Purpose: Media caching and global edge CDN delivery
    179168* [Terms of Service](https://www.cloudflare.com/terms/)
    180169* [Privacy Policy](https://www.cloudflare.com/privacypolicy/)
     
    187176Self-hosted users connect only to their own Cloudflare account.
    188177
     178== Fair Use ==
     179
     180This service is provided on a fair use basis. While we don't impose hard limits, we reserve the right to contact users with exceptionally high usage to discuss dedicated plans or custom arrangements.
     181
     182The 500 MB per-file size limit applies to all media. For larger files or specialized requirements, please contact us to discuss options.
     183
     184We aim to provide reliable service for legitimate WordPress media delivery. Abuse, excessive automated requests, or use that degrades service for others may result in account review.
     185
    189186== Changelog ==
     187
     188= 1.0 =
     189* New: Rebranded as "Bandwidth Saver: Unlimited Media CDN"
     190* New: Simplified pricing - single Unlimited tier at $19.99/mo
     191* New: Video and audio CDN support with range requests
     192* New: HLS streaming support (M3U8 and TS segments)
     193* New: Request-based analytics (bandwidth tracking deprecated)
     194* Improved: Media-focused messaging and UI
     195* Improved: Unlimited bandwidth for paid tier
    190196
    191197= 0.2.5 =
     
    194200
    195201= 0.2.4 =
    196 * New: Frictionless activation — no email required for free image CDN tier
     202* New: Frictionless activation — no email required for trial tier
    197203* Improved: Toggle on directly from dashboard, account created automatically
    198204* Improved: Cleaner first-run experience with fewer steps
     
    200206
    201207= 0.2.3 =
    202 * Improved: Clearer messaging about image CDN speed and Core Web Vitals benefits
     208* Improved: Clearer messaging about media CDN speed and Core Web Vitals benefits
    203209* Improved: Simplified onboarding copy
    204210* Improved: Updated screenshot captions
    205211* Fixed: PHPCS warnings for Stripe redirect handler
    206212
    207 = 0.2.2 =
    208 * Improved: Settings page loads faster with batched API requests
    209 * Improved: Source URLs and usage stats are pre-loaded for the CDN dashboard
    210 * Improved: Better WordPress coding standards compliance
    211 * Fixed: Cleaner transient cleanup on uninstall
    212 
    213 = 0.2.1 =
    214 * New: Usage analytics dashboard with CDN bandwidth charts
    215 * New: Source URLs management for multiple origin domains
    216 * New: Projected bandwidth usage
    217 * Improved: Image CDN works with infinite scroll and "load more"
    218 * Improved: Faster settings page with smarter caching
    219 * Fixed: Double-click prevention on all buttons
    220 * Fixed: Custom CDN domain feature now available on Lite plans
    221 
    222 = 0.2.0 =
    223 * New: Updated pricing with bandwidth as primary metric
    224 * New: All paid plans include custom CDN domain support
    225 * New: Free tier upgraded to 100 GB CDN bandwidth/month
    226 * Security: API keys encrypted at rest
    227 * Security: Rate limiting on admin actions
    228 * Security: Stricter validation of CDN domains
    229 
    230 = 0.1.9 =
    231 * Fixed: Image CDN activates reliably after payment or recovery
    232 * Fixed: CDN properly disables when subscription becomes inactive
    233 
    234 = 0.1.8 =
    235 * Fixed: Payment success now correctly enables CDN toggle
    236 
    237 = 0.1.7 =
    238 * Improved: Redesigned CDN toggle with better visual feedback
    239 * Fixed: Direct upgrade properly saves new tier limits
    240 
    241 = 0.1.6 =
    242 * New: Custom CDN domain support (cdn.yoursite.com)
    243 * Fixed: Fallback uses correct URL for srcset images
    244 
    245 = 0.1.5 =
    246 * Improved: Simplified image CDN setup
    247 * Improved: Faster image fallback with inline error handling
    248 * Fixed: Images no longer flash on load
    249 
    250 = 0.1.0 =
    251 * New: Managed option for one-click image CDN setup
    252 * New: Redesigned admin interface
    253 
    254 = 0.0.1 =
    255 * Initial release of the free image CDN plugin
    256 
    257213== Upgrade Notice ==
    258214
    259 = 0.2.4 =
    260 Frictionless activation: Just flip the switch, no email required. The easiest free image CDN setup ever.
    261 
    262 = 0.2.3 =
    263 Clearer messaging and improved onboarding experience. Recommended for all users.
    264 
    265 = 0.2.2 =
    266 Performance improvements: Settings page now loads faster. Recommended for all users.
    267 
    268 = 0.2.0 =
    269 New pricing with more generous limits. Free image CDN tier now includes 100 GB/month. Security improvements. Recommended for all users.
     215= 1.0 =
     216Major update: Now supports video, audio, and HLS streaming. New simplified pricing at $19.99/mo for unlimited bandwidth.
    270217
    271218== Support ==
Note: See TracChangeset for help on using the changeset viewer.