feat: ajustes do stripe
Some checks failed
Deploy QR Rapido / test (push) Successful in 3m44s
Deploy QR Rapido / build-and-push (push) Failing after 6s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped

This commit is contained in:
Ricardo Carneiro 2025-08-03 19:52:32 -03:00
parent 0c176a2abf
commit 3fa95aefd8
14 changed files with 479 additions and 47 deletions

View File

@ -112,14 +112,22 @@ namespace QRRapidoApp.Controllers
_logger.LogInformation("QR code generated successfully - GenerationTime: {GenerationTimeMs}ms, FromCache: {FromCache}, Size: {Size}px",
generationStopwatch.ElapsedMilliseconds, result.FromCache, request.Size);
// Update counter for free users
if (!request.IsPremium && userId != null)
// Update counter for all logged users
if (userId != null)
{
var remaining = await _userService.DecrementDailyQRCountAsync(userId);
if (request.IsPremium)
{
result.RemainingQRs = int.MaxValue; // Premium users have unlimited
// Still increment the count for statistics
await _userService.IncrementDailyQRCountAsync(userId);
}
else
{
var remaining = await _userService.IncrementDailyQRCountAsync(userId);
result.RemainingQRs = remaining;
_logger.LogDebug("Updated QR count for free user - Remaining: {RemainingQRs}", remaining);
}
}
// Save to history if user is logged in (fire and forget)
if (userId != null)
@ -467,11 +475,48 @@ namespace QRRapidoApp.Controllers
}
}
[HttpGet("GetUserStats")]
public async Task<IActionResult> GetUserStats()
{
try
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return Unauthorized();
}
var user = await _userService.GetUserAsync(userId);
var isPremium = user?.IsPremium ?? false;
// For logged users (premium or not), return -1 to indicate unlimited
// For consistency with the frontend logic
var remainingCount = -1; // Unlimited for all logged users
return Ok(new
{
remainingCount = remainingCount,
isPremium = isPremium,
isUnlimited = true
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting user stats");
return StatusCode(500);
}
}
private async Task<bool> CheckRateLimitAsync(string? userId, Models.User? user)
{
// Premium users have unlimited QR codes
if (user?.IsPremium == true) return true;
var dailyLimit = userId != null ? 50 : 10; // Logged in: 50/day, Anonymous: 10/day
// Logged users (non-premium) have unlimited QR codes
if (userId != null) return true;
// Anonymous users have 3 QR codes per day
var dailyLimit = 3;
var currentCount = await _userService.GetDailyQRCountAsync(userId);
return currentCount < dailyLimit;

View File

@ -67,7 +67,8 @@ namespace QRRapidoApp.Middleware
"api/", "health", "_framework/", "lib/", "css/", "js/", "images/",
"favicon.ico", "robots.txt", "sitemap.xml",
"signin-microsoft", "signin-google", "signout-callback-oidc",
"Account/ExternalLoginCallback", "Account/Logout"
"Account/ExternalLoginCallback", "Account/Logout", "Pagamento/CreateCheckout",
"Pagamento/StripeWebhook"
};
return specialRoutes.Any(route => path.StartsWith(route, StringComparison.OrdinalIgnoreCase));

View File

@ -21,7 +21,7 @@
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.1-dev-00953" />
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
<PackageReference Include="Stripe.net" Version="43.15.0" />
<PackageReference Include="Stripe.net" Version="48.4.0" />
<PackageReference Include="StackExchange.Redis" Version="2.7.4" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.0" />
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />

View File

@ -343,3 +343,7 @@ Este projeto está licenciado sob a licença MIT - veja o arquivo [LICENSE](LICE
**QR Rapido** - Velocidade extrema meets experiência excepcional 🚀
*Desenvolvido com ❤️ para a comunidade de desenvolvedores*
No WSL - Para rodar o webhook do stripe local.
```bash
stripe listen --forward-to https://localhost:52428/pagamento/stripewebhook --skip-verify

View File

@ -774,4 +774,13 @@
<data name="AllRightsReserved" xml:space="preserve">
<value>All rights reserved.</value>
</data>
<data name="QRCodesRemainingToday" xml:space="preserve">
<value>QR codes remaining today</value>
</data>
<data name="RateLimitExceeded" xml:space="preserve">
<value>Daily limit reached! Login for unlimited access.</value>
</data>
<data name="AnonymousUserLimit" xml:space="preserve">
<value>Anonymous users: 3 QR codes per day</value>
</data>
</root>

View File

@ -774,4 +774,13 @@
<data name="AllRightsReserved" xml:space="preserve">
<value>Todos los derechos reservados.</value>
</data>
<data name="QRCodesRemainingToday" xml:space="preserve">
<value>Códigos QR restantes hoy</value>
</data>
<data name="RateLimitExceeded" xml:space="preserve">
<value>¡Límite diario alcanzado! Inicia sesión para acceso ilimitado.</value>
</data>
<data name="AnonymousUserLimit" xml:space="preserve">
<value>Usuarios anónimos: 3 códigos QR por día</value>
</data>
</root>

View File

@ -774,4 +774,13 @@
<data name="AllRightsReserved" xml:space="preserve">
<value>Todos os direitos reservados.</value>
</data>
<data name="QRCodesRemainingToday" xml:space="preserve">
<value>QR codes restantes hoje</value>
</data>
<data name="RateLimitExceeded" xml:space="preserve">
<value>Limite diário atingido! Faça login para acesso ilimitado.</value>
</data>
<data name="AnonymousUserLimit" xml:space="preserve">
<value>Usuários anônimos: 3 QR codes por dia</value>
</data>
</root>

View File

@ -16,7 +16,8 @@ namespace QRRapidoApp.Services
Task<User?> GetUserByStripeCustomerIdAsync(string customerId);
Task<bool> UpdateUserAsync(User user);
Task<int> GetDailyQRCountAsync(string? userId);
Task<int> DecrementDailyQRCountAsync(string userId);
Task<int> IncrementDailyQRCountAsync(string userId);
Task<int> GetRemainingQRCountAsync(string userId);
Task<bool> CanGenerateQRAsync(string? userId, bool isPremium);
Task SaveQRToHistoryAsync(string? userId, QRGenerationResult qrResult);
Task<List<QRCodeHistory>> GetUserQRHistoryAsync(string userId, int limit = 50);

View File

@ -78,7 +78,7 @@ namespace QRRapidoApp.Services
switch (stripeEvent.Type)
{
case Events.CheckoutSessionCompleted:
case "checkout.session.completed":
var session = stripeEvent.Data.Object as Session;
if (session?.SubscriptionId != null)
{
@ -88,12 +88,27 @@ namespace QRRapidoApp.Services
}
break;
case Events.InvoicePaymentSucceeded:
case "invoice.finalized":
var invoice = stripeEvent.Data.Object as Invoice;
if (invoice?.SubscriptionId != null)
var subscriptionLineItem = invoice.Lines?.Data
.FirstOrDefault(line =>
!string.IsNullOrEmpty(line.SubscriptionId) ||
line.Subscription != null
);
string subscriptionId = null;
if (subscriptionLineItem != null)
{
// Tenta obter o ID da assinatura de duas formas diferentes
subscriptionId = subscriptionLineItem.SubscriptionId
?? subscriptionLineItem.Subscription?.Id;
}
if (subscriptionId != null)
{
var subscriptionService = new SubscriptionService();
var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
var subscription = await subscriptionService.GetAsync(subscriptionId);
var user = await _userService.GetUserByStripeCustomerIdAsync(subscription.CustomerId);
if (user != null)
{
@ -102,7 +117,7 @@ namespace QRRapidoApp.Services
}
break;
case Events.CustomerSubscriptionDeleted:
case "customer.subscription.deleted":
var deletedSubscription = stripeEvent.Data.Object as Subscription;
if (deletedSubscription != null)
{
@ -118,6 +133,8 @@ namespace QRRapidoApp.Services
private async Task ProcessSubscriptionActivation(string userId, Subscription subscription)
{
var service = new SubscriptionItemService();
var subItem = service.Get(subscription.Items.Data[0].Id);
if (string.IsNullOrEmpty(userId) || subscription == null)
{
_logger.LogWarning("Could not process subscription activation due to missing userId or subscription data.");
@ -136,7 +153,8 @@ namespace QRRapidoApp.Services
await _userService.UpdateUserStripeCustomerIdAsync(user.Id, subscription.CustomerId);
}
await _userService.ActivatePremiumStatus(userId, subscription.Id, subscription.CurrentPeriodEnd);
await _userService.ActivatePremiumStatus(userId, subscription.Id, subItem.CurrentPeriodEnd);
_logger.LogInformation($"Successfully processed premium activation/renewal for user {userId}.");
}

View File

@ -142,7 +142,7 @@ namespace QRRapidoApp.Services
}
}
public async Task<int> DecrementDailyQRCountAsync(string userId)
public async Task<int> IncrementDailyQRCountAsync(string userId)
{
try
{
@ -163,23 +163,47 @@ namespace QRRapidoApp.Services
user.TotalQRGenerated++;
await UpdateUserAsync(user);
// Calculate remaining QRs for free users
var dailyLimit = user.IsPremium ? int.MaxValue : _config.GetValue<int>("Premium:FreeQRLimit", 50);
return Math.Max(0, dailyLimit - user.DailyQRCount);
// Premium and logged users have unlimited QR codes
return int.MaxValue;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error decrementing daily QR count for user {userId}: {ex.Message}");
_logger.LogError(ex, $"Error incrementing daily QR count for user {userId}: {ex.Message}");
return 0;
}
}
public async Task<int> GetRemainingQRCountAsync(string userId)
{
try
{
var user = await GetUserAsync(userId);
if (user == null) return 0;
// Premium users have unlimited
if (user.IsPremium) return int.MaxValue;
// Logged users (non-premium) have unlimited
return int.MaxValue;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting remaining QR count for user {userId}: {ex.Message}");
return 0;
}
}
public async Task<bool> CanGenerateQRAsync(string? userId, bool isPremium)
{
// Premium users have unlimited QR codes
if (isPremium) return true;
// Logged users (non-premium) have unlimited QR codes
if (!string.IsNullOrEmpty(userId)) return true;
// Anonymous users have 3 QR codes per day
var dailyCount = await GetDailyQRCountAsync(userId);
var limit = string.IsNullOrEmpty(userId) ? 10 : _config.GetValue<int>("Premium:FreeQRLimit", 50);
var limit = 3;
return dailyCount < limit;
}

View File

@ -12,7 +12,22 @@
<div class="container">
<!-- Hidden input for JavaScript premium status check -->
<input type="hidden" id="user-premium-status" value="@(ViewBag.IsPremium == true ? "true" : "false")" />
@{
var statusValue = "anonymous";
if (User.Identity.IsAuthenticated)
{
var isPremium = await AdService.HasValidPremiumSubscription(userId);
statusValue = isPremium ? "premium" : "logged-in";
}
}
<input type="hidden" id="user-premium-status" value="@statusValue" />
<!-- Hidden localized strings for rate limiting -->
<div style="display: none;">
<span data-localized="RateLimitExceeded">@Localizer["RateLimitExceeded"]</span>
<span data-localized="QRCodesRemainingToday">@Localizer["QRCodesRemainingToday"]</span>
<span data-localized="UnlimitedToday">@Localizer["UnlimitedToday"]</span>
</div>
<div class="row">
<!-- QR Generator Form -->
@ -55,18 +70,9 @@
</div>
</div>
<div class="col-md-4 text-end">
@if (User.Identity.IsAuthenticated)
{
<small class="text-muted">
<span class="qr-counter">@Localizer["UnlimitedToday"]</span>
<span class="qr-counter">Carregando...</span>
</small>
}
else
{
<small class="text-muted">
<span class="qr-counter">@Localizer["QRCodesRemaining"]</span>
</small>
}
</div>
</div>

View File

@ -1,6 +1,7 @@
@using Microsoft.Extensions.Localization
@inject IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer
@{
Layout = "~/Views/Shared/_Layout.cshtml";
ViewData["Title"] = "Sucesso";
}

View File

@ -21,10 +21,10 @@
}
},
"Stripe": {
"PublishableKey": "pk_test_xxxxx",
"SecretKey": "sk_test_xxxxx",
"WebhookSecret": "whsec_xxxxx",
"PriceId": "price_xxxxx"
"PublishableKey": "pk_test_51Rs42tBeR5IFYUsBooapyDwQTgh6CFuKbya5R3MVDTrdOUKmgiHQYipU0pgOdG5iKogH77RUYIKBJzbCt5BghUOY00xitV5KiN",
"SecretKey": "sk_test_51Rs42tBeR5IFYUsBtycRlJJcdwgoMbh8MfQIKIGelBPTQFwDcOn2iCCbw5uG6hnqlpgNAUuFgWRAUUMA8qkABKun00EIx4odDF",
"WebhookSecret": "whsec_2e828803ceb48e7865458b0cf332b68535fdff8753d26d69b1c88ea55cb0e482",
"PriceId": "prod_SnfQTxwE3i8r5L"
},
"AdSense": {
"ClientId": "ca-pub-XXXXXXXXXX",
@ -41,7 +41,7 @@
},
"Premium": {
"FreeQRLimit": 10,
"PremiumPrice": 19.90,
"PremiumPrice": 12.90,
"Features": {
"UnlimitedQR": true,
"DynamicQR": true,

View File

@ -1,4 +1,4 @@
// QR Rapido Speed Generator
// QR Rapido Speed Generator
class QRRapidoGenerator {
constructor() {
this.startTime = 0;
@ -49,7 +49,9 @@ class QRRapidoGenerator {
this.checkAdFreeStatus();
this.updateLanguage();
this.updateStatsCounters();
this.initializeUserCounter();
this.initializeProgressiveFlow();
this.initializeRateLimiting();
// Validar segurança dos dados após carregamento
setTimeout(() => {
@ -345,6 +347,9 @@ class QRRapidoGenerator {
async generateQRWithTimer(e) {
e.preventDefault();
// Check rate limit for anonymous users
if (!this.checkRateLimit()) return;
// Validation
if (!this.validateForm()) return;
@ -708,11 +713,19 @@ class QRRapidoGenerator {
generationTime: generationTime
};
// Update counter for free users
// Increment rate limit counter after successful generation
this.incrementRateLimit();
// Update counter for logged users
if (result.remainingQRs !== undefined) {
if (result.remainingQRs === -1 || result.remainingQRs === 2147483647 || result.remainingQRs >= 1000000) {
// Logged user - show unlimited
this.showUnlimitedCounter();
} else {
this.updateRemainingCounter(result.remainingQRs);
}
}
}
showGenerationStarted() {
const button = document.getElementById('generate-btn');
@ -965,6 +978,27 @@ class QRRapidoGenerator {
}, 30000); // Update every 30 seconds
}
async initializeUserCounter() {
try {
const response = await fetch('/api/QR/GetUserStats');
if (response.ok) {
const stats = await response.json();
console.log('User stats loaded:', stats);
console.log('remainingCount:', stats.remainingCount, 'type:', typeof stats.remainingCount);
console.log('isUnlimited:', stats.isUnlimited);
// For logged users, always show unlimited (they all have unlimited QR codes)
console.log('Calling showUnlimitedCounter directly for logged user');
this.showUnlimitedCounter();
} else {
console.log('GetUserStats response not ok:', response.status);
}
} catch (error) {
// If not authenticated or error, keep the default "Carregando..." text
console.debug('User not authenticated or error loading stats:', error);
}
}
trackGenerationEvent(type, time) {
// Google Analytics
if (typeof gtag !== 'undefined') {
@ -1332,8 +1366,15 @@ class QRRapidoGenerator {
updateRemainingCounter(remaining) {
const counterElement = document.querySelector('.qr-counter');
if (counterElement) {
counterElement.textContent = `${remaining} QR codes restantes hoje`;
if (counterElement && remaining !== null && remaining !== undefined) {
// If it's unlimited (any special value indicating unlimited)
if (remaining === -1 || remaining >= 2147483647 || remaining > 1000000) {
this.showUnlimitedCounter();
return;
}
const remainingText = this.getLocalizedString('QRCodesRemainingToday');
counterElement.textContent = `${remaining} ${remainingText}`;
if (remaining <= 3) {
counterElement.className = 'badge bg-warning qr-counter';
@ -1344,6 +1385,18 @@ class QRRapidoGenerator {
}
}
showUnlimitedCounter() {
console.log('showUnlimitedCounter called');
const counterElement = document.querySelector('.qr-counter');
if (counterElement) {
counterElement.textContent = 'QR Codes ilimitados';
counterElement.className = 'badge bg-success qr-counter';
console.log('Set counter to: QR Codes ilimitados');
} else {
console.log('Counter element not found');
}
}
showError(message) {
this.showAlert(message, 'danger');
}
@ -1996,6 +2049,258 @@ class QRRapidoGenerator {
console.groupEnd();
}
// Rate Limiting Methods
initializeRateLimiting() {
// Wait a bit for DOM to be fully ready
setTimeout(() => {
this.updateRateDisplayCounter();
}, 100);
}
checkRateLimit() {
// Check if user is logged in (unlimited access)
const userStatus = document.getElementById('user-premium-status');
console.log('🔍 Rate limit check - User status:', userStatus ? userStatus.value : 'not found');
if (userStatus && userStatus.value === 'logged-in') {
console.log('✅ User is logged in - unlimited access');
return true; // Unlimited for logged users
}
// For anonymous users, check daily limit
const today = new Date().toDateString();
const cookieName = 'qr_daily_count';
const rateLimitData = this.getCookie(cookieName);
console.log('📅 Today:', today);
console.log('🍪 Cookie data:', rateLimitData);
let currentData = { date: today, count: 0 };
if (rateLimitData) {
try {
currentData = JSON.parse(rateLimitData);
console.log('📊 Parsed data:', currentData);
// Reset count if it's a new day
if (currentData.date !== today) {
console.log('🔄 New day detected, resetting count');
currentData = { date: today, count: 0 };
}
} catch (e) {
console.log('❌ Error parsing cookie:', e);
currentData = { date: today, count: 0 };
}
} else {
console.log('🆕 No cookie found, starting fresh');
}
console.log('📈 Current count:', currentData.count);
// Check if limit exceeded (don't increment here)
if (currentData.count >= 3) {
console.log('🚫 Rate limit exceeded');
this.showRateLimitError();
return false;
}
console.log('✅ Rate limit check passed');
return true;
}
incrementRateLimit() {
// Only increment after successful QR generation
const userStatus = document.getElementById('user-premium-status');
if (userStatus && userStatus.value === 'logged-in') {
return; // No limits for logged users
}
const today = new Date().toDateString();
const cookieName = 'qr_daily_count';
const rateLimitData = this.getCookie(cookieName);
let currentData = { date: today, count: 0 };
if (rateLimitData) {
try {
currentData = JSON.parse(rateLimitData);
if (currentData.date !== today) {
currentData = { date: today, count: 0 };
}
} catch (e) {
currentData = { date: today, count: 0 };
}
}
// Increment count and save
currentData.count++;
this.setCookie(cookieName, JSON.stringify(currentData), 1);
// Update display counter
this.updateRateDisplayCounter();
}
showRateLimitError() {
const message = this.getLocalizedString('RateLimitExceeded') || 'Daily limit reached! Login for unlimited access.';
this.showError(message);
}
updateRateDisplayCounter() {
const counterElement = document.querySelector('.qr-counter');
if (!counterElement) return;
// Check user status
const userStatus = document.getElementById('user-premium-status');
if (userStatus && userStatus.value === 'premium') {
// Premium users have unlimited QRs
const unlimitedText = this.getLocalizedString('UnlimitedToday');
counterElement.textContent = unlimitedText;
counterElement.className = 'badge bg-success qr-counter';
return;
} else if (userStatus && userStatus.value === 'logged-in') {
// Free logged users - we need to get their actual remaining count
this.updateLoggedUserCounter();
return;
}
// For anonymous users, show remaining count
const today = new Date().toDateString();
const cookieName = 'qr_daily_count';
const rateLimitData = this.getCookie(cookieName);
let remaining = 3;
if (rateLimitData) {
try {
const currentData = JSON.parse(rateLimitData);
if (currentData.date === today) {
remaining = Math.max(0, 3 - currentData.count);
}
} catch (e) {
remaining = 3;
}
}
const remainingText = this.getLocalizedString('QRCodesRemainingToday');
counterElement.textContent = `${remaining} ${remainingText}`;
}
async updateLoggedUserCounter() {
const counterElement = document.querySelector('.qr-counter');
if (!counterElement) return;
try {
// Fetch user's remaining QR count from the server
const response = await fetch('/api/QR/GetUserStats', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
});
if (response.ok) {
const data = await response.json();
if (data.isPremium) {
// Premium user - show unlimited
const unlimitedText = this.getLocalizedString('UnlimitedToday');
counterElement.textContent = unlimitedText;
counterElement.className = 'badge bg-success qr-counter';
} else {
// Free user - show remaining count
const remaining = data.remainingCount || 0;
const remainingText = this.getLocalizedString('QRCodesRemainingToday');
if (remaining !== -1) {
counterElement.textContent = `${remaining} ${remainingText}`;
counterElement.className = 'badge bg-primary qr-counter';
if (remaining <= 3) {
counterElement.className = 'badge bg-warning qr-counter';
}
if (remaining === 0) {
counterElement.className = 'badge bg-danger qr-counter';
}
}
else {
const unlimitedText = this.getLocalizedString('UnlimitedToday');
counterElement.textContent = unlimitedText;
counterElement.className = 'badge bg-success qr-counter';
}
}
} else {
// Fallback to showing 50 remaining if API fails
const remainingText = this.getLocalizedString('QRCodesRemainingToday');
counterElement.textContent = `50 ${remainingText}`;
counterElement.className = 'badge bg-primary qr-counter';
}
} catch (error) {
console.warn('Failed to fetch user stats:', error);
// Fallback to showing 50 remaining if API fails
const remainingText = this.getLocalizedString('QRCodesRemainingToday');
counterElement.textContent = `50 ${remainingText}`;
counterElement.className = 'badge bg-primary qr-counter';
}
}
getLocalizedString(key) {
// Try to get from server-side localization first
const element = document.querySelector(`[data-localized="${key}"]`);
if (element) {
const text = element.textContent.trim() || element.value;
if (text) return text;
}
// Fallback to client-side strings
if (this.languageStrings[this.currentLang] && this.languageStrings[this.currentLang][key]) {
return this.languageStrings[this.currentLang][key];
}
// Default fallbacks based on key
const defaults = {
'UnlimitedToday': 'Ilimitado hoje',
'QRCodesRemainingToday': 'QR codes restantes hoje',
'RateLimitExceeded': 'Limite diário atingido! Faça login para acesso ilimitado.'
};
return defaults[key] || key;
}
setCookie(name, value, days) {
const expires = new Date();
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Strict`;
}
getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
// Debug/reset function - call from console if needed
resetRateLimit() {
// Multiple ways to clear the cookie
document.cookie = 'qr_daily_count=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
document.cookie = 'qr_daily_count=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=' + window.location.hostname + ';';
document.cookie = 'qr_daily_count=; max-age=0; path=/;';
// Also clear from storage if exists
if (typeof Storage !== "undefined") {
localStorage.removeItem('qr_daily_count');
sessionStorage.removeItem('qr_daily_count');
}
this.updateRateDisplayCounter();
console.log('🧹 Rate limit completely reset!');
console.log('🍪 All cookies:', document.cookie);
}
}
// Initialize when DOM loads