Changeset 3446766
- Timestamp:
- 01/26/2026 12:24:01 AM (2 months ago)
- Location:
- bandwidth-saver
- Files:
-
- 1 deleted
- 13 edited
-
assets/screenshot-3.png (modified) (previous)
-
assets/screenshot-4.png (modified) (previous)
-
assets/screenshot-5.png (deleted)
-
trunk/admin/css/imgpro-cdn-admin.css (modified) (5 diffs)
-
trunk/admin/js/imgpro-cdn-admin.js (modified) (23 diffs)
-
trunk/imgpro-cdn.php (modified) (3 diffs)
-
trunk/includes/class-imgpro-cdn-admin-ajax.php (modified) (13 diffs)
-
trunk/includes/class-imgpro-cdn-admin.php (modified) (29 diffs)
-
trunk/includes/class-imgpro-cdn-api.php (modified) (7 diffs)
-
trunk/includes/class-imgpro-cdn-onboarding.php (modified) (12 diffs)
-
trunk/includes/class-imgpro-cdn-plan-selector.php (modified) (11 diffs)
-
trunk/includes/class-imgpro-cdn-rewriter.php (modified) (11 diffs)
-
trunk/includes/class-imgpro-cdn-settings.php (modified) (13 diffs)
-
trunk/readme.txt (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
-
bandwidth-saver/trunk/admin/css/imgpro-cdn-admin.css
r3419619 r3446766 2002 2002 ========================================================================== */ 2003 2003 2004 /* Base account card - extends .imgpro-card*/2004 /* Base account card */ 2005 2005 .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 {2049 2006 display: flex; 2050 2007 flex-direction: column; … … 2054 2011 box-shadow: var(--imgpro-card-shadow); 2055 2012 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; 2059 2034 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 { 2064 2041 display: flex; 2065 2042 align-items: center; … … 2067 2044 } 2068 2045 2069 .imgpro-account-card__ tier{2070 f ont-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 { 2090 2067 font-size: var(--imgpro-text-sm); 2091 2068 color: var(--imgpro-text-secondary); 2069 line-height: 1.5; 2092 2070 } 2093 2071 … … 2107 2085 font-size: var(--imgpro-text-sm); 2108 2086 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); 2109 2093 } 2110 2094 … … 2115 2099 align-items: stretch; 2116 2100 text-align: center; 2101 gap: var(--imgpro-space-4); 2102 } 2103 2104 .imgpro-account-card__status { 2105 justify-content: center; 2117 2106 } 2118 2107 2119 2108 .imgpro-account-card__footer { 2120 2109 justify-content: center; 2121 } 2122 2123 .imgpro-account-card__limits { 2124 justify-content: center; 2110 flex-wrap: wrap; 2125 2111 } 2126 2112 2127 2113 .imgpro-account-card__actions { 2128 2114 align-self: center; 2115 width: 100%; 2116 } 2117 2118 .imgpro-account-card__actions .imgpro-btn { 2119 width: 100%; 2120 justify-content: center; 2129 2121 } 2130 2122 } -
bandwidth-saver/trunk/admin/js/imgpro-cdn-admin.js
r3419619 r3446766 285 285 }); 286 286 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) 298 288 299 289 // Advanced settings accordion … … 456 446 457 447 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'; 460 450 $button.addClass('is-loading').prop('disabled', true).text(imgproCdnAdmin.i18n.creatingCheckout); 461 451 … … 1062 1052 }); 1063 1053 1064 // Upgrade link - handle based on action type1054 // Subscription link - open plan modal or manage subscription 1065 1055 $(document).off('click', '#imgpro-source-urls-upgrade').on('click', '#imgpro-source-urls-upgrade', function(e) { 1066 1056 e.preventDefault(); 1067 1057 var action = $(this).data('action'); 1068 1058 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 1071 1064 openPlanModal(); 1072 } else if (action === 'upgrade-to-next') {1073 // Paid tier: direct upgrade to next tier1074 var nextTier = $(this).data('next-tier');1075 if (nextTier) {1076 showUpgradeConfirmModal(nextTier, $(this));1077 }1078 } else if (action === 'manage') {1079 // Business tier: open manage subscription1080 window.open(imgproCdnAdmin.manageUrl, '_blank');1081 1065 } 1082 1066 }); … … 1165 1149 } 1166 1150 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; 1169 1153 1170 1154 // Show/hide input based on limit … … 1172 1156 $inputWrapper.hide(); 1173 1157 1174 // Get tier info from section data attributes1158 // Get paid status from section 1175 1159 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(); 1203 1168 } else { 1204 1169 $inputWrapper.show(); … … 1361 1326 $(document).on('click', '#imgpro-plan-checkout', function(e) { 1362 1327 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); 1365 1332 } 1366 1333 }); … … 1369 1336 /** 1370 1337 * Initialize default plan selection 1338 * Single-tier model: only one plan card exists 1371 1339 */ 1372 1340 function initDefaultSelection() { … … 1376 1344 if (!$selector.length) return; 1377 1345 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); 1396 1350 } 1397 1351 } … … 1404 1358 if (!$modal.length) return; 1405 1359 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); 1413 1361 $('body').addClass('imgpro-modal-open'); 1414 1362 } … … 1421 1369 $modal.fadeOut(200); 1422 1370 $('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 handlers1432 */1433 function initUpgradeConfirmModal() {1434 const $modal = $('#imgpro-upgrade-confirm-modal');1435 if (!$modal.length) return;1436 1437 // Cancel button1438 $('#imgpro-upgrade-cancel').off('click').on('click', function() {1439 closeUpgradeConfirmModal();1440 });1441 1442 // Confirm button1443 $('#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 click1453 $modal.find('.imgpro-confirm-modal__backdrop').off('click').on('click', function() {1454 closeUpgradeConfirmModal();1455 });1456 }1457 1458 /**1459 * Show upgrade confirmation modal1460 */1461 function showUpgradeConfirmModal(tierId, $triggerButton) {1462 const $modal = $('#imgpro-upgrade-confirm-modal');1463 if (!$modal.length) return;1464 1465 // Store pending upgrade info1466 pendingUpgradeTierId = tierId;1467 pendingUpgradeButton = $triggerButton;1468 1469 // Get tier data from localized script1470 const tiers = imgproCdnAdmin.tiers || {};1471 const currentTierId = imgproCdnAdmin.tier;1472 const currentTier = tiers[currentTierId];1473 const newTier = tiers[tierId];1474 1475 // Update new plan info1476 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 hero1486 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 checklist1494 $('#imgpro-confirm-checklist').html(buildChecklistHtml(newTier));1495 1496 // Reset button states1497 const $confirmBtn = $('#imgpro-upgrade-confirm');1498 $confirmBtn.removeClass('is-loading').prop('disabled', false);1499 $('#imgpro-upgrade-cancel').prop('disabled', false);1500 1501 // Show modal1502 $modal.fadeIn(200);1503 $('body').addClass('imgpro-modal-open');1504 }1505 1506 /**1507 * Calculate bandwidth multiplier between tiers1508 */1509 function calculateBandwidthMultiplier(currentTier, newTier) {1510 const currentBytes = currentTier?.limits?.bandwidth?.bytes || 107374182400; // 100 GB default1511 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 modal1543 */1544 function closeUpgradeConfirmModal() {1545 const $modal = $('#imgpro-upgrade-confirm-modal');1546 $modal.fadeOut(200);1547 $('body').removeClass('imgpro-modal-open');1548 1549 // Clear pending upgrade1550 pendingUpgradeTierId = null;1551 pendingUpgradeButton = null;1552 1371 } 1553 1372 … … 1614 1433 // Subscription upgraded directly - show success and reload 1615 1434 closePlanModal(); 1616 closeUpgradeConfirmModal();1617 1435 showNotice('success', response.data.message || imgproCdnAdmin.i18n.subscriptionUpgraded); 1618 1436 setTimeout(function() { … … 1625 1443 $button.removeClass('is-loading').prop('disabled', false); 1626 1444 closePlanModal(); 1627 closeUpgradeConfirmModal();1628 1445 // Account exists - show verification modal with pending tier 1629 1446 if (response.data.show_recovery) { … … 1637 1454 $button.removeClass('is-loading').prop('disabled', false); 1638 1455 closePlanModal(); 1639 closeUpgradeConfirmModal();1640 1456 var message = status === 'timeout' ? imgproCdnAdmin.i18n.timeoutError : imgproCdnAdmin.i18n.genericError; 1641 1457 showNotice('error', message); … … 1661 1477 window.history.replaceState({}, '', url.toString()); 1662 1478 1663 // Show success notice and upgrademodal1479 // Show success notice and open plan selector modal 1664 1480 showNotice('success', imgproCdnAdmin.i18n.accountRecovered || 'Account recovered!'); 1665 1481 setTimeout(function() { 1666 showUpgradeConfirmModal(upgradeTier, null);1482 openPlanModal(); 1667 1483 }, 300); 1668 1484 } … … 1863 1679 1864 1680 /** 1865 * Update insights cards with data 1681 * Update insights cards with data from API 1866 1682 */ 1867 1683 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()); 1877 1690 } 1878 1691 1879 1692 // 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()); 1888 1696 } 1889 1697 1890 1698 // 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); 1910 1725 } 1911 1726 } … … 1915 1730 */ 1916 1731 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('—'); 1924 1734 } 1925 1735 … … 1970 1780 1971 1781 /** 1972 * Render Chart.js bandwidthusage chart1782 * Render Chart.js requests usage chart 1973 1783 * @returns {boolean} True if chart rendered successfully, false otherwise 1974 1784 */ … … 1991 1801 } 1992 1802 1993 // Prepare data 1803 // Prepare data - primary metric is now requests 1994 1804 const labels = []; 1995 const bandwidthData = [];1996 1805 const requestsData = []; 1997 1806 … … 2002 1811 labels.push(formatted); 2003 1812 2004 // Convert bytes to GB for chart2005 const bandwidthGB = day.bandwidth_bytes / (1024 * 1024 * 1024);2006 bandwidthData.push(bandwidthGB.toFixed(2));2007 2008 1813 requestsData.push(day.requests || 0); 2009 1814 }); 2010 1815 2011 // Create chart 1816 // Create chart with requests as primary metric 2012 1817 usageChart = new Chart(ctx, { 2013 1818 type: 'line', … … 2016 1821 datasets: [ 2017 1822 { 2018 label: ' Bandwidth (GB)',2019 data: bandwidthData,1823 label: 'Requests', 1824 data: requestsData, 2020 1825 borderColor: '#3b82f6', 2021 1826 backgroundColor: 'rgba(59, 130, 246, 0.1)', … … 2051 1856 callbacks: { 2052 1857 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(); 2059 1860 } 2060 1861 } … … 2066 1867 ticks: { 2067 1868 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; 2069 1876 } 2070 1877 }, … … 2121 1928 if ($('#imgpro-plan-modal').is(':visible')) { 2122 1929 closePlanModal(); 2123 } else if ($('#imgpro-upgrade-confirm-modal').is(':visible')) {2124 closeUpgradeConfirmModal();2125 1930 } else if ($('#imgpro-recovery-modal').is(':visible')) { 2126 1931 $('#imgpro-recovery-modal').remove(); -
bandwidth-saver/trunk/imgpro-cdn.php
r3442901 r3446766 1 1 <?php 2 2 /** 3 * Plugin Name: Free Image CDN – Bandwidth Saver3 * Plugin Name: Bandwidth Saver: Unlimited Media CDN 4 4 * 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.55 * Description: Unlimited media CDN for images, video, audio, and HLS streaming. $19.99/mo for unlimited bandwidth. 6 * Version: 1.0 7 7 * Author: ImgPro 8 8 * Author URI: https://img.pro … … 47 47 // Define plugin constants 48 48 if (!defined('IMGPRO_CDN_VERSION')) { 49 define('IMGPRO_CDN_VERSION', ' 0.2.5');49 define('IMGPRO_CDN_VERSION', '1.0'); 50 50 } 51 51 if (!defined('IMGPRO_CDN_PLUGIN_DIR')) { … … 90 90 function imgpro_cdn_add_version_html() { 91 91 if (!is_admin()) { 92 echo "\n<!-- ImageCDN by ImgPro v" . esc_attr(IMGPRO_CDN_VERSION) . " -->\n";92 echo "\n<!-- Media CDN by ImgPro v" . esc_attr(IMGPRO_CDN_VERSION) . " -->\n"; 93 93 } 94 94 } -
bandwidth-saver/trunk/includes/class-imgpro-cdn-admin-ajax.php
r3419619 r3446766 293 293 if ($current_enabled === $enabled) { 294 294 $message = $enabled 295 ? __(' Image CDN is active. Images areloading from Cloudflare.', 'bandwidth-saver')296 : __(' Image CDN is disabled. Images areloading 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'); 297 297 298 298 wp_send_json_success(['message' => $message]); … … 313 313 if (false !== $result) { 314 314 $message = $enabled 315 ? __(' Image CDN enabled. Images now loadfrom the global network.', 'bandwidth-saver')316 : __(' Image CDN disabled. Images now loadfrom 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'); 317 317 318 318 $response = ['message' => $message]; … … 418 418 'cloud_tier' => $tier_id, 419 419 'bandwidth_used' => $usage['bandwidth_used'], 420 'cache_used' => $usage['cache_used'],421 420 'cache_hits' => $usage['cache_hits'], 422 421 'cache_misses' => $usage['cache_misses'], … … 559 558 $updated_settings = $this->settings->get_all(); 560 559 $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; 562 561 563 562 wp_send_json_success([ … … 565 564 'bandwidth_limit' => $bandwidth_limit, 566 565 '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),570 566 'cache_hits' => $updated_settings['cache_hits'] ?? 0, 571 567 'cache_misses' => $updated_settings['cache_misses'] ?? 0, 568 'is_unlimited' => $is_unlimited, 572 569 'formatted' => [ 573 570 '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), 577 572 ] 578 573 ]); … … 605 600 ImgPro_CDN_Security::check_rate_limit('checkout'); 606 601 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'; 618 605 619 606 // SECURITY: Use get_api_key() to decrypt the stored API key … … 792 779 // Enable CDN if valid subscription 793 780 $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)) { 795 782 $this->settings->update([ 796 783 'cloud_enabled' => true, … … 806 793 // Tier priority order (higher = better) 807 794 $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, 812 800 ]; 813 801 … … 1068 1056 1069 1057 wp_send_json_success([ 1070 'message' => __('CDN domain removed. The ImageCDN has been disabled.', 'bandwidth-saver')1058 'message' => __('CDN domain removed. The Media CDN has been disabled.', 'bandwidth-saver') 1071 1059 ]); 1072 1060 } … … 1129 1117 $tier_valid = in_array($tier, [ 1130 1118 ImgPro_CDN_Settings::TIER_FREE, 1119 ImgPro_CDN_Settings::TIER_UNLIMITED, 1131 1120 ImgPro_CDN_Settings::TIER_LITE, 1132 1121 ImgPro_CDN_Settings::TIER_PRO, … … 1233 1222 ? $insights['recent']['requests'] 1234 1223 : null, 1224 // New request-focused fields (v1.0+) 1225 'requests' => isset($insights['requests']) ? $insights['requests'] : null, 1226 'period' => isset($insights['period']) ? $insights['period'] : null, 1235 1227 ]; 1236 1228 … … 1297 1289 ? $insights['recent']['requests'] 1298 1290 : null, 1291 // New request-focused fields (v1.0+) 1292 'requests' => isset($insights['requests']) ? $insights['requests'] : null, 1293 'period' => isset($insights['period']) ? $insights['period'] : null, 1299 1294 ]; 1300 1295 … … 1418 1413 $this->settings->update(['source_urls' => $domain_list]); 1419 1414 1415 // Single-tier model: all users get unlimited source URLs 1420 1416 wp_send_json_success([ 1421 1417 'source_urls' => $source_urls, 1422 1418 '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' 1425 1421 ]); 1426 1422 } -
bandwidth-saver/trunk/includes/class-imgpro-cdn-admin.php
r3442901 r3446766 116 116 * Handle payment return from Stripe checkout 117 117 * 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 * 118 128 * @since 0.1.6 119 129 * @return void 120 130 */ 121 131 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() ) { 127 134 return; 128 135 } 129 136 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'] ) { 137 140 return; 138 141 } … … 156 159 // Enable if subscription is valid 157 160 $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]; 159 162 160 163 if (in_array($tier_id, $valid_tiers, true)) { … … 179 182 180 183 /** 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 /** 181 223 * Save site data from API response to local settings 182 224 * … … 206 248 'setup_mode' => ImgPro_CDN_Settings::MODE_CLOUD, 207 249 'bandwidth_used' => $usage['bandwidth_used'], 208 'cache_used' => $usage['cache_used'],209 250 'cache_hits' => $usage['cache_hits'], 210 251 'cache_misses' => $usage['cache_misses'], … … 285 326 if ($usage['bandwidth_used'] !== ($settings['bandwidth_used'] ?? 0)) { 286 327 $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'];291 328 $settings_changed = true; 292 329 } … … 391 428 'activeMessage' => sprintf( 392 429 /* translators: 1: opening span tag, 2: closing span tag, 3: opening span tag, 4: closing span tag */ 393 __('%1$sYour images areloading 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'), 394 431 '<span class="imgpro-cdn-nowrap imgpro-cdn-hide-mobile">', 395 432 '</span>', … … 397 434 '</span>' 398 435 ), 399 'disabledMessage' => __('Turn on to speed up your images', 'bandwidth-saver'),436 'disabledMessage' => __('Turn on to speed up your media', 'bandwidth-saver'), 400 437 // Button states 401 438 'creatingCheckout' => __('Creating checkout...', 'bandwidth-saver'), … … 425 462 'accountRecovered' => __('Account recovered!', 'bandwidth-saver'), 426 463 // Success messages 427 'subscriptionActivated' => __('You\'re all set! Your imageswill 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'), 430 467 'checkoutCancelled' => __('Checkout cancelled. You can try again anytime.', 'bandwidth-saver'), 431 468 // Toggle UI text 432 'cdnActiveHeading' => __('Your images areloading faster', 'bandwidth-saver'),433 'cdnInactiveHeading' => __(' ImageCDN is Off', 'bandwidth-saver'),469 'cdnActiveHeading' => __('Your media is loading faster', 'bandwidth-saver'), 470 'cdnInactiveHeading' => __('Media CDN is Off', 'bandwidth-saver'), 434 471 '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'), 436 473 // Custom domain 437 474 'addingDomain' => __('Adding domain...', 'bandwidth-saver'), … … 441 478 'domainRemoved' => __('Custom domain removed.', 'bandwidth-saver'), 442 479 'domainActive' => __('Custom domain is active.', 'bandwidth-saver'), 443 'confirmRemoveDomain' => __('Remove this custom domain? Imageswill be served from the default domain.', 'bandwidth-saver'),444 'confirmRemoveCdnDomain' => __('Remove this CDN domain? The ImageCDN 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'), 445 482 'cdnDomainRemoved' => __('CDN domain removed.', 'bandwidth-saver'), 446 483 // Upgrade prompts … … 482 519 'days_remaining' => $insights['period']['days_remaining'] ?? null, 483 520 'total_requests' => $insights['recent']['requests'] ?? null, 521 // New request-focused fields (v1.0+) 522 'requests' => $insights['requests'] ?? null, 523 'period' => $insights['period'] ?? null, 484 524 ], 485 525 'daily' => $usage['daily'] ?? [], … … 581 621 <p><strong> 582 622 <?php if ($is_free): ?> 583 <?php esc_html_e('Account activated. Your images now loadfrom the global edge network.', 'bandwidth-saver'); ?>623 <?php esc_html_e('Account activated. Your media now loads from the global edge network.', 'bandwidth-saver'); ?> 584 624 <?php else: ?> 585 <?php esc_html_e('Subscription activated. Your images now loadfrom the global edge network.', 'bandwidth-saver'); ?>625 <?php esc_html_e('Subscription activated. Your media now loads from the global edge network.', 'bandwidth-saver'); ?> 586 626 <?php endif; ?> 587 627 </strong></p> … … 596 636 <div> 597 637 <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 imagesfaster.', 'bandwidth-saver'); ?></p>638 <p><?php esc_html_e('Enable the CDN toggle below to start serving media faster.', 'bandwidth-saver'); ?></p> 599 639 </div> 600 640 </div> … … 733 773 <div> 734 774 <h1><?php esc_html_e('Bandwidth Saver', 'bandwidth-saver'); ?></h1> 735 <p class="imgpro-tagline"><?php esc_html_e('Faster imagesfor visitors worldwide', 'bandwidth-saver'); ?></p>775 <p class="imgpro-tagline"><?php esc_html_e('Faster media for visitors worldwide', 'bandwidth-saver'); ?></p> 736 776 </div> 737 777 </div> … … 781 821 <h2 id="imgpro-toggle-heading"> 782 822 <?php echo $is_enabled 783 ? esc_html__('Your images areloading faster', 'bandwidth-saver')784 : esc_html__(' ImageCDN is Off', 'bandwidth-saver'); ?>823 ? esc_html__('Your media is loading faster', 'bandwidth-saver') 824 : esc_html__('Media CDN is Off', 'bandwidth-saver'); ?> 785 825 </h2> 786 826 <p id="imgpro-toggle-description"> 787 827 <?php echo $is_enabled 788 828 ? 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'); ?> 790 830 </p> 791 831 </div> … … 803 843 > 804 844 <span class="imgpro-toggle-slider"></span> 805 <span class="screen-reader-text"><?php esc_html_e('Toggle ImageCDN', 'bandwidth-saver'); ?></span>845 <span class="screen-reader-text"><?php esc_html_e('Toggle Media CDN', 'bandwidth-saver'); ?></span> 806 846 </label> 807 847 </div> … … 852 892 <!-- Quick Stats Grid --> 853 893 <div class="imgpro-stats-grid" id="imgpro-stats-grid"> 854 <!-- ImageRequests Card (populated by JS) -->894 <!-- Requests Card (populated by JS) --> 855 895 <div class="imgpro-stat-card"> 856 896 <div class="imgpro-stat-header"> 857 <span class="imgpro-stat-label"><?php esc_html_e(' ImageRequests', 'bandwidth-saver'); ?></span>897 <span class="imgpro-stat-label"><?php esc_html_e('Requests', 'bandwidth-saver'); ?></span> 858 898 </div> 859 899 <div class="imgpro-stat-value" id="imgpro-stat-total-requests"> … … 863 903 </div> 864 904 865 <!-- Cached ImagesCard (populated by JS) -->905 <!-- Cached Media Card (populated by JS) --> 866 906 <div class="imgpro-stat-card"> 867 907 <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> 869 909 </div> 870 910 <div class="imgpro-stat-value" id="imgpro-stat-cached"> … … 889 929 <div class="imgpro-chart-card"> 890 930 <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> 892 932 <div class="imgpro-chart-controls"> 893 933 <button type="button" class="imgpro-stat-refresh" id="imgpro-refresh-stats" title="<?php esc_attr_e('Refresh stats', 'bandwidth-saver'); ?>"> … … 928 968 <div class="imgpro-insight-card"> 929 969 <div class="imgpro-insight-icon"> 930 <!-- Heroicon: signal(outline) -->970 <!-- Heroicon: cursor-arrow-rays (outline) --> 931 971 <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="M 9.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" /> 933 973 </svg> 934 974 </div> 935 975 <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> 939 979 </div> 940 980 </div> … … 943 983 <div class="imgpro-insight-card"> 944 984 <div class="imgpro-insight-icon"> 945 <!-- Heroicon: chart-bar -square(outline) -->985 <!-- Heroicon: chart-bar (outline) --> 946 986 <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="M 7.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" /> 948 988 </svg> 949 989 </div> 950 990 <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"> 953 993 <span class="imgpro-stat-loading">—</span> 954 994 </div> … … 1123 1163 private function render_cloud_tab($settings) { 1124 1164 $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); 1126 1166 ?> 1127 1167 <div class="imgpro-tab-panel" role="tabpanel"> … … 1130 1170 <?php $this->render_toggle_card($settings, ImgPro_CDN_Settings::MODE_CLOUD); ?> 1131 1171 <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 — imageURLs 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'); ?> 1133 1173 </p> 1134 1174 <?php else: ?> 1135 1175 <?php $this->render_cloud_settings($settings); ?> 1136 1176 <?php endif; ?> 1137 </div>1138 <?php1139 }1140 1141 /**1142 * Render Cloud signup CTA1143 *1144 * @since 0.1.71145 * @param array $pricing Pricing information.1146 * @return void1147 */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>1209 1177 </div> 1210 1178 <?php … … 1240 1208 1241 1209 <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 — imageURLs 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'); ?> 1243 1211 </p> 1244 1212 … … 1293 1261 1294 1262 /** 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. 1296 1267 * 1297 1268 * @since 0.1.6 … … 1301 1272 */ 1302 1273 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): ?> 1350 1314 <?php if (!empty($email)): ?> 1351 1315 <span class="imgpro-separator">·</span> 1352 <span><?php echo esc_html($email); ?></span>1353 1316 <?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 1403 1322 } 1404 1323 … … 1670 1589 */ 1671 1590 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); 1699 1593 1700 1594 ?> 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'; ?>"> 1702 1596 <div class="imgpro-source-urls-header"> 1703 1597 <h4><?php esc_html_e('Source URLs', 'bandwidth-saver'); ?></h4> 1704 1598 <p class="imgpro-source-urls-description"> 1705 <?php esc_html_e('Domains where your images are hosted. The CDN will proxy imagesfrom 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'); ?> 1706 1600 </p> 1707 1601 </div> … … 1732 1626 </button> 1733 1627 </div> 1734 <p class="imgpro-source-urls-limit">1735 <?php1736 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>1747 1628 </div> 1748 1629 </div> … … 1766 1647 <div class="imgpro-custom-domain-header"> 1767 1648 <h4><?php esc_html_e('Custom Domain', 'bandwidth-saver'); ?></h4> 1768 <p><?php esc_html_e('Serve imagesfrom your own branded domain.', 'bandwidth-saver'); ?></p>1649 <p><?php esc_html_e('Serve media from your own branded domain.', 'bandwidth-saver'); ?></p> 1769 1650 </div> 1770 1651 … … 1783 1664 </button> 1784 1665 </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; ?>1791 1666 </div> 1792 1667 <?php else: ?> -
bandwidth-saver/trunk/includes/class-imgpro-cdn-api.php
r3419619 r3446766 29 29 30 30 /** 31 * Cache TTL in seconds (1 hour) 31 * Cache TTL in seconds (1 hour) - for static data like tiers 32 32 * 33 33 * @var int 34 34 */ 35 35 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; 36 44 37 45 /** … … 192 200 } 193 201 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 194 205 // Cache the site data separately for get_site() compatibility 195 206 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); 201 210 202 211 return $response; … … 327 336 * 328 337 * @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'). 330 339 * @return array|WP_Error Checkout data with URL or error. 331 340 */ 332 public function create_checkout($api_key, $tier_id = ' pro') {341 public function create_checkout($api_key, $tier_id = 'unlimited') { 333 342 if (empty($api_key)) { 334 343 return new WP_Error('missing_api_key', __('API key is required', 'bandwidth-saver')); … … 820 829 * Get fallback tiers when API is unavailable 821 830 * 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. 824 832 * 825 833 * @return array Fallback tiers. 826 834 */ 827 835 private function get_fallback_tiers() { 836 // Single-tier model: all users get the same features regardless of payment status 828 837 return [ 829 838 [ 830 839 'id' => 'free', 831 'name' => ' Free',832 'description' => ' Get started',840 'name' => 'Media CDN', 841 'description' => 'Media CDN Service', 833 842 'highlight' => false, 834 843 'price' => ['cents' => 0, 'formatted' => 'Free', 'period' => null], 835 844 '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], 838 848 ], 839 'features' => ['custom_domain' => false, 'priority_support' => false],849 'features' => ['custom_domain' => true, 'priority_support' => false, 'video_support' => true, 'audio_support' => true], 840 850 ], 841 851 [ 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'], 847 857 '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], 850 861 ], 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], 876 863 ], 877 864 ]; … … 893 880 } 894 881 895 // Fallback defaults ( Protier pricing)882 // Fallback defaults (Unlimited tier pricing) 896 883 return [ 897 'amount' => 1 499,884 'amount' => 1999, 898 885 'currency' => 'USD', 899 886 'interval' => 'month', 900 887 'formatted' => [ 901 'amount' => '$1 4.99',888 'amount' => '$19.99', 902 889 'period' => '/mo', 903 'full' => '$1 4.99/mo',890 'full' => '$19.99/mo', 904 891 ], 905 892 ]; … … 937 924 'bandwidth_used' => $usage['bandwidth']['used_bytes'] ?? 0, 938 925 'bandwidth_limit' => $usage['bandwidth']['limit_bytes'] ?? 0, 939 'cache_used' => $usage['cache']['used_bytes'] ?? 0,940 926 'cache_limit' => $usage['cache']['limit_bytes'] ?? 0, 941 927 'cache_hits' => $usage['cache_hits'] ?? 0, … … 1158 1144 * 1159 1145 * @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) { 1162 1149 $this->site_cache = $site; 1163 set_transient('imgpro_cdn_site_data', $site, self::CACHE_TTL);1150 set_transient('imgpro_cdn_site_data', $site, $ttl); 1164 1151 } 1165 1152 -
bandwidth-saver/trunk/includes/class-imgpro-cdn-onboarding.php
r3411500 r3446766 78 78 // For step 1: only show if no existing subscription 79 79 $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)) { 81 81 return false; 82 82 } … … 145 145 ?> 146 146 <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> 148 148 149 149 <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 themup 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'); ?> 151 151 </p> 152 152 … … 158 158 <li> 159 159 <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 globalservers', '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> 161 161 </li> 162 162 <li> 163 163 <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> 165 165 </li> 166 166 </ul> … … 185 185 186 186 <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> 189 189 </p> 190 190 </div> … … 203 203 ?> 204 204 <div class="imgpro-onboarding-content imgpro-onboarding-step-2"> 205 <h1><?php esc_html_e('Create your freeaccount', 'bandwidth-saver'); ?></h1>205 <h1><?php esc_html_e('Create your account', 'bandwidth-saver'); ?></h1> 206 206 207 207 <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'); ?> 209 209 </p> 210 210 … … 244 244 <div class="imgpro-onboarding-actions"> 245 245 <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 FreeAccount', 'bandwidth-saver'); ?></span>246 <span class="imgpro-btn-text"><?php esc_html_e('Create Account', 'bandwidth-saver'); ?></span> 247 247 <span class="imgpro-btn-loading"> 248 248 <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> … … 283 283 284 284 <p class="imgpro-onboarding-description"> 285 <?php esc_html_e('Toggle on to start serving imagesfrom the CDN.', 'bandwidth-saver'); ?>285 <?php esc_html_e('Toggle on to start serving media from the CDN.', 'bandwidth-saver'); ?> 286 286 </p> 287 287 … … 292 292 </div> 293 293 <div class="imgpro-activate-text"> 294 <strong><?php esc_html_e(' ImageCDN', 'bandwidth-saver'); ?></strong>295 <span><?php esc_html_e('Serve imagesfrom 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> 296 296 </div> 297 297 </div> … … 299 299 <input type="checkbox" id="imgpro-activate-toggle"> 300 300 <span class="imgpro-toggle-slider"></span> 301 <span class="screen-reader-text"><?php esc_html_e('Enable ImageCDN', 'bandwidth-saver'); ?></span>301 <span class="screen-reader-text"><?php esc_html_e('Enable Media CDN', 'bandwidth-saver'); ?></span> 302 302 </label> 303 303 </div> … … 308 308 <li> 309 309 <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(' ImageURLs 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'); ?> 311 311 </li> 312 312 <li> 313 313 <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'); ?> 315 315 </li> 316 316 <li> … … 324 324 <li> 325 325 <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 loaddirectly from your site', 'bandwidth-saver'); ?>326 <?php esc_html_e('If anything goes wrong, media loads directly from your site', 'bandwidth-saver'); ?> 327 327 </li> 328 328 </ul> … … 352 352 353 353 <p class="imgpro-onboarding-description"> 354 <?php esc_html_e('Your images arenow 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'); ?> 355 355 </p> 356 356 -
bandwidth-saver/trunk/includes/class-imgpro-cdn-plan-selector.php
r3410060 r3446766 3 3 * ImgPro CDN Plan Selector Component 4 4 * 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. 6 7 * 7 8 * @package ImgPro_CDN … … 52 53 */ 53 54 public function render($context = 'modal', $current_tier = '') { 54 $tiers = $this->api->get_tiers();55 55 $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); 65 57 66 58 $wrapper_class = 'imgpro-plan-selector'; … … 71 63 } 72 64 ?> 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); ?>"> 74 66 <?php if ('modal' === $context): ?> 75 67 <div class="imgpro-plan-selector__header"> 76 <h2><?php e sc_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> 77 69 <button type="button" class="imgpro-plan-selector__close" aria-label="<?php esc_attr_e('Close', 'bandwidth-saver'); ?>"> 78 70 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> … … 84 76 <?php endif; ?> 85 77 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): ?> 92 85 <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> 104 88 <span class="imgpro-btn-loading"> 105 89 <svg class="imgpro-spinner" width="20" height="20" viewBox="0 0 20 20"> … … 114 98 </div> 115 99 116 <?php if ('free' === $current_tier || empty($current_tier)): ?>117 100 <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'); ?> 119 102 </p> 120 103 <?php endif; ?> … … 124 107 125 108 /** 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"> 161 119 162 120 <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> 167 123 </div> 168 124 169 125 <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> 174 128 </div> 175 129 … … 179 133 <path d="M13.333 4L6 11.333 2.667 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> 180 134 </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> 206 155 <li class="imgpro-plan-card__feature"> 207 156 <svg class="imgpro-plan-card__feature-icon" width="16" height="16" viewBox="0 0 16 16" fill="none"> … … 210 159 <span><?php esc_html_e('Priority support', 'bandwidth-saver'); ?></span> 211 160 </li> 212 <?php endif; ?>213 161 </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 251 189 } 252 190 253 191 /** 254 192 * Render the modal overlay wrapper 255 *256 * Call this once in the admin page, then use JavaScript to show/hide.257 193 * 258 194 * @return void … … 267 203 </div> 268 204 <?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 328 209 * 329 210 * @param string $context Context for styling: 'card', 'inline', 'alert'. … … 332 213 public function render_upgrade_cta($context = 'card') { 333 214 $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)) { 338 218 return; 339 219 } … … 348 228 <div class="imgpro-upgrade-cta__icon"> 349 229 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 350 <path d="M1 3 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"/> 351 231 </svg> 352 232 </div> 353 233 <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> 356 236 </div> 357 237 <?php endif; ?> 358 238 <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'); ?> 360 240 <svg width="16" height="16" viewBox="0 0 16 16" fill="none"> 361 241 <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 194 194 * 195 195 * @since 0.1.0 196 * @since 0.2.0 Added video and audio shortcode filters 196 197 * @return void 197 198 */ … … 212 213 add_filter('wp_get_attachment_image_attributes', [$this, 'rewrite_attributes'], 999, 3); 213 214 214 // Content filters 215 // Content filters (processes img, video, audio, source tags) 215 216 add_filter('the_content', [$this, 'rewrite_content'], 999); 216 217 add_filter('post_thumbnail_html', [$this, 'rewrite_content'], 999); 217 218 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); 218 223 } 219 224 … … 412 417 413 418 /** 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 /** 414 462 * Rewrite image attributes 415 463 * … … 467 515 * Rewrite content HTML 468 516 * 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() 470 518 * 471 519 * 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() 474 522 * - Uses WP_HTML_Tag_Processor for safe, spec-compliant HTML parsing (requires WP 6.2+) 475 523 * 476 524 * @since 0.1.0 525 * @since 0.2.0 Added video, audio, and source tag support 477 526 * @param string $content HTML content. 478 527 * @return string … … 489 538 } 490 539 491 // Early bail-out: Skip processing if no imagetags present540 // Early bail-out: Skip processing if no media tags present 492 541 // 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) { 494 553 return $content; 495 554 } … … 509 568 * Rewrite content using WP_HTML_Tag_Processor (modern approach) 510 569 * 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 512 574 * @param string $content HTML content. 513 575 * @return string Modified content. … … 516 578 $processor = new WP_HTML_Tag_Processor($content); 517 579 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); 520 584 521 585 while ($processor->next_tag()) { 522 586 $tag = $processor->get_tag(); 523 587 524 // Skip if not a n imagetag525 if (!in_array($tag, $ tag_names, true)) {588 // Skip if not a media tag 589 if (!in_array($tag, $all_tags, true)) { 526 590 continue; 527 591 } … … 534 598 // Get src attribute 535 599 $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 } 562 640 } 563 641 … … 584 662 * 585 663 * @since 0.1.0 664 * @since 0.2.0 Now supports video, audio, and HLS files 586 665 * @param string $url URL to check. 587 666 * @return bool … … 606 685 } 607 686 608 // Must be a n image609 if (!$this->is_ image_url($url)) {687 // Must be a supported media file (images, video, audio, HLS) 688 if (!$this->is_media_url($url)) { 610 689 return false; 611 690 } … … 615 694 616 695 /** 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 620 701 * @param string $url URL to check. 621 * @return bool True if URL points to a n imagefile.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) { 624 705 /** 625 * Filter the list of allowed imageextensions706 * Filter the list of allowed media extensions 626 707 * 708 * @since 1.0 627 709 * @param array $extensions List of file extensions (without dots) 628 710 */ 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', 637 721 ]); 638 722 -
bandwidth-saver/trunk/includes/class-imgpro-cdn-settings.php
r3410342 r3446766 77 77 78 78 /** 79 * Subscription tier: Business (paid )79 * Subscription tier: Business (paid, legacy) 80 80 * 81 81 * @since 0.1.7 … … 83 83 */ 84 84 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'; 85 95 86 96 /** … … 238 248 239 249 // Usage stats (synced from Cloud API) 240 // Bandwidth is primary metric (monthly ), Cache is secondary (LRU-managed)250 // Bandwidth is primary metric (monthly reset) 241 251 'bandwidth_used' => 0, 242 252 'bandwidth_limit' => 0, 243 'cache_used' => 0,244 253 'cache_limit' => 0, 245 254 'cache_hits' => 0, … … 393 402 if (isset($settings['cloud_tier'])) { 394 403 $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)) { 396 405 $validated['cloud_tier'] = $tier; 397 406 } … … 405 414 if (isset($settings['bandwidth_limit'])) { 406 415 $validated['bandwidth_limit'] = absint($settings['bandwidth_limit']); 407 }408 if (isset($settings['cache_used'])) {409 $validated['cache_used'] = absint($settings['cache_used']);410 416 } 411 417 if (isset($settings['cache_limit'])) { … … 733 739 if (self::MODE_CLOUD === $mode) { 734 740 $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); 737 743 } elseif (self::MODE_CLOUDFLARE === $mode) { 738 744 return !empty($settings['cdn_url']); … … 779 785 780 786 /** 781 * Check if user has any paid subscription ( lite, pro, or business)787 * Check if user has any paid subscription (unlimited or legacy tiers) 782 788 * 783 789 * @since 0.1.7 … … 788 794 $tier = $settings['cloud_tier'] ?? ''; 789 795 // 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); 791 798 } 792 799 … … 805 812 * Check if tier has custom domain feature 806 813 * 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. 810 819 */ 811 820 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. 823 831 */ 824 832 public static function is_free($settings) { 825 833 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'] ?? ''); 826 858 } 827 859 … … 852 884 * Get bandwidth limit for current tier 853 885 * 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. 859 892 */ 860 893 public static function get_bandwidth_limit($settings) { 861 894 $tier = $settings['cloud_tier'] ?? ''; 862 895 switch ($tier) { 896 case self::TIER_UNLIMITED: 897 return -1; // Unlimited 863 898 case self::TIER_BUSINESS: 864 899 return self::BUSINESS_BANDWIDTH_LIMIT; … … 879 914 * Get bandwidth usage percentage 880 915 * 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. 884 921 */ 885 922 public static function get_bandwidth_percentage($settings) { 886 923 $limit = self::get_bandwidth_limit($settings); 924 // Unlimited tier or invalid limit 887 925 if ($limit <= 0) { 888 926 return 0; … … 896 934 * 897 935 * 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. 902 941 */ 903 942 public static function get_cache_limit($settings) { 904 943 $tier = $settings['cloud_tier'] ?? ''; 905 944 switch ($tier) { 945 case self::TIER_UNLIMITED: 946 return -1; // Unlimited 906 947 case self::TIER_BUSINESS: 907 948 return self::BUSINESS_CACHE_LIMIT; … … 920 961 921 962 /** 922 * Get cache usage percentage923 *924 * @since 0.2.0925 * @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 /**938 963 * Handle API error with action hook for logging 939 964 * -
bandwidth-saver/trunk/readme.txt
r3442901 r3446766 1 === Free Image CDN – Bandwidth Saver===1 === Bandwidth Saver: Unlimited Media CDN === 2 2 Contributors: imgpro 3 Tags: image cdn, cdn, speed, core web vitals, performance3 Tags: media cdn, cdn, video cdn, image cdn, hls streaming 4 4 Requires at least: 6.2 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 0.2.57 Stable tag: 1.0 8 8 License: GPLv2 or later 9 9 License URI: http://www.gnu.org/licenses/gpl-2.0.html 10 10 11 Free global image CDN for WordPress. Serve images from 300+ edge servers worldwide. Improves Core Web Vitals and PageSpeed scores. One-click setup.11 Unlimited media CDN for WordPress. Serve images, video, audio, and HLS streams from 300+ edge servers. $19.99/mo for unlimited bandwidth. 12 12 13 13 == Description == 14 14 15 ** Free imageCDN 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 imagesfrom 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 a n ImageCDN? =24 25 * **Faster page load times** — Images loadfrom the nearest server instead of traveling across the world from your host15 **Unlimited media CDN that makes your WordPress site faster.** 16 17 Heavy media files slow down your site. When videos buffer and images lag, visitors leave, Core Web Vitals fail, and Google ranks you lower. 18 19 This 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 26 26 * **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 27 28 * **Higher PageSpeed scores** — Google PageSpeed Insights will show improved performance 28 29 * **Lower bounce rates** — Visitors don't wait for slow sites 29 30 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 40 1. Install the media CDN plugin from WordPress 33 41 2. 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 42 3. Media instantly loads from 300+ global CDN servers 43 44 That'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 54 All plans include a 7-day money-back guarantee. 55 56 = Self-Hosted Option = 57 58 For 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) 47 61 48 62 = Works With Any WordPress Theme or Plugin = 49 63 50 This imageCDN is compatible with:64 This media CDN is compatible with: 51 65 52 66 * **Page builders** — Elementor, Divi, Beaver Builder, Gutenberg, Bricks, Oxygen 53 67 * **WooCommerce** — Product images, galleries, thumbnails 54 * ** Image formats** — JPG, PNG, GIF, WebP, AVIF, SVG68 * **Video players** — Plyr, VideoJS, native HTML5 video 55 69 * **Lazy loading** — Works with native lazy load and plugins 56 70 * **Responsive images** — Full srcset support 57 71 * **Caching plugins** — WP Rocket, LiteSpeed Cache, W3 Total Cache, WP Super Cache 58 72 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 61 77 * Bloggers with image-heavy posts 62 * WooCommerce stores with product photos 78 * WooCommerce stores with product photos and videos 63 79 * Recipe, travel, and photography sites 64 80 * 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 81 82 82 83 == Installation == … … 84 85 **60-second setup. No technical knowledge required.** 85 86 86 1. Install and activate the imageCDN plugin87 2. Go to **Settings →Bandwidth Saver**87 1. Install and activate the media CDN plugin 88 2. Go to **Settings > Bandwidth Saver** 88 89 3. 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. 90 4. Upgrade to Unlimited for $19.99/mo 91 92 Done. Your media is now loading faster from the global CDN. 91 93 92 94 == Frequently Asked Questions == 93 95 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 imageCDN 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 imagedelivery 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 th isCDN 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 98 The 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 102 Yes. 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 106 The 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 110 Yes. The media CDN works with WooCommerce product images, galleries, thumbnails, and product videos. 109 111 110 112 = Will this CDN work with my page builder? = 111 113 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. 114 Yes. 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 118 Files 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 122 Yes. The Unlimited plan supports custom domains (cdn.yoursite.com) with automatic SSL. 123 124 = What happens if the media CDN goes down? = 125 126 Your 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 130 The 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. 133 131 134 132 = Do I need to change my DNS for this CDN? = 135 133 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. 134 No. Unlike other CDN services, this media CDN works immediately without any DNS changes. Everything happens from your WordPress admin. 145 135 146 136 == Screenshots == 147 137 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 138 1. Speed up your media in 60 seconds with the unlimited media CDN 139 2. Track your CDN requests and performance 140 3. Multi-site support and custom CDN domains 141 4. Self-host option for full control 153 142 154 143 == Privacy == … … 156 145 = What Data Is Collected? = 157 146 158 The imageCDN plugin does not add cookies, tracking pixels, or analytics to your site.147 The media CDN plugin does not add cookies, tracking pixels, or analytics to your site. 159 148 160 149 = Managed Mode = 161 150 162 151 * Your site URL is used to configure CDN routing 163 * Email is optional — only collected if you upgrade or request account recovery152 * Email is collected when you upgrade to a paid plan 164 153 * Custom domain settings are sent if configured 165 154 166 Images arecached and served through a global edge network powered by Cloudflare.155 Media is cached and served through a global edge network powered by Cloudflare. 167 156 168 157 = Self-Hosted Mode = 169 158 170 No data is sent to us. Images arecached in your own Cloudflare account.159 No data is sent to us. Media is cached in your own Cloudflare account. 171 160 172 161 == External Services == 173 162 174 This imageCDN plugin connects to external services:163 This media CDN plugin connects to external services: 175 164 176 165 **Cloudflare (R2 Storage and Workers)** 177 166 178 * Purpose: Imagecaching and global edge CDN delivery167 * Purpose: Media caching and global edge CDN delivery 179 168 * [Terms of Service](https://www.cloudflare.com/terms/) 180 169 * [Privacy Policy](https://www.cloudflare.com/privacypolicy/) … … 187 176 Self-hosted users connect only to their own Cloudflare account. 188 177 178 == Fair Use == 179 180 This 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 182 The 500 MB per-file size limit applies to all media. For larger files or specialized requirements, please contact us to discuss options. 183 184 We 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 189 186 == 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 190 196 191 197 = 0.2.5 = … … 194 200 195 201 = 0.2.4 = 196 * New: Frictionless activation — no email required for free image CDNtier202 * New: Frictionless activation — no email required for trial tier 197 203 * Improved: Toggle on directly from dashboard, account created automatically 198 204 * Improved: Cleaner first-run experience with fewer steps … … 200 206 201 207 = 0.2.3 = 202 * Improved: Clearer messaging about imageCDN speed and Core Web Vitals benefits208 * Improved: Clearer messaging about media CDN speed and Core Web Vitals benefits 203 209 * Improved: Simplified onboarding copy 204 210 * Improved: Updated screenshot captions 205 211 * Fixed: PHPCS warnings for Stripe redirect handler 206 212 207 = 0.2.2 =208 * Improved: Settings page loads faster with batched API requests209 * Improved: Source URLs and usage stats are pre-loaded for the CDN dashboard210 * Improved: Better WordPress coding standards compliance211 * Fixed: Cleaner transient cleanup on uninstall212 213 = 0.2.1 =214 * New: Usage analytics dashboard with CDN bandwidth charts215 * New: Source URLs management for multiple origin domains216 * New: Projected bandwidth usage217 * Improved: Image CDN works with infinite scroll and "load more"218 * Improved: Faster settings page with smarter caching219 * Fixed: Double-click prevention on all buttons220 * Fixed: Custom CDN domain feature now available on Lite plans221 222 = 0.2.0 =223 * New: Updated pricing with bandwidth as primary metric224 * New: All paid plans include custom CDN domain support225 * New: Free tier upgraded to 100 GB CDN bandwidth/month226 * Security: API keys encrypted at rest227 * Security: Rate limiting on admin actions228 * Security: Stricter validation of CDN domains229 230 = 0.1.9 =231 * Fixed: Image CDN activates reliably after payment or recovery232 * Fixed: CDN properly disables when subscription becomes inactive233 234 = 0.1.8 =235 * Fixed: Payment success now correctly enables CDN toggle236 237 = 0.1.7 =238 * Improved: Redesigned CDN toggle with better visual feedback239 * Fixed: Direct upgrade properly saves new tier limits240 241 = 0.1.6 =242 * New: Custom CDN domain support (cdn.yoursite.com)243 * Fixed: Fallback uses correct URL for srcset images244 245 = 0.1.5 =246 * Improved: Simplified image CDN setup247 * Improved: Faster image fallback with inline error handling248 * Fixed: Images no longer flash on load249 250 = 0.1.0 =251 * New: Managed option for one-click image CDN setup252 * New: Redesigned admin interface253 254 = 0.0.1 =255 * Initial release of the free image CDN plugin256 257 213 == Upgrade Notice == 258 214 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 = 216 Major update: Now supports video, audio, and HLS streaming. New simplified pricing at $19.99/mo for unlimited bandwidth. 270 217 271 218 == Support ==
Note: See TracChangeset
for help on using the changeset viewer.