From 3fa95aefd8da3e16b895003a135ac5d409597437 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Sun, 3 Aug 2025 19:52:32 -0300 Subject: [PATCH] feat: ajustes do stripe --- Controllers/QRController.cs | 59 +++- Middleware/LanguageRedirectionMiddleware.cs | 3 +- QRRapidoApp.csproj | 2 +- README.md | 6 +- Resources/SharedResource.en.resx | 9 + Resources/SharedResource.es.resx | 9 + Resources/SharedResource.pt-BR.resx | 9 + Services/IUserService.cs | 3 +- Services/StripeService.cs | 30 +- Services/UserService.cs | 36 ++- Views/Home/Index.cshtml | 32 +- Views/Pagamento/Sucesso.cshtml | 1 + appsettings.json | 10 +- wwwroot/js/qr-speed-generator.js | 317 +++++++++++++++++++- 14 files changed, 479 insertions(+), 47 deletions(-) diff --git a/Controllers/QRController.cs b/Controllers/QRController.cs index df5c726..11c177f 100644 --- a/Controllers/QRController.cs +++ b/Controllers/QRController.cs @@ -112,13 +112,21 @@ 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); - result.RemainingQRs = remaining; - - _logger.LogDebug("Updated QR count for free user - Remaining: {RemainingQRs}", remaining); + 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) @@ -467,11 +475,48 @@ namespace QRRapidoApp.Controllers } } + [HttpGet("GetUserStats")] + public async Task 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 CheckRateLimitAsync(string? userId, Models.User? user) { + // Premium users have unlimited QR codes if (user?.IsPremium == true) return true; + + // Logged users (non-premium) have unlimited QR codes + if (userId != null) return true; - var dailyLimit = userId != null ? 50 : 10; // Logged in: 50/day, Anonymous: 10/day + // Anonymous users have 3 QR codes per day + var dailyLimit = 3; var currentCount = await _userService.GetDailyQRCountAsync(userId); return currentCount < dailyLimit; diff --git a/Middleware/LanguageRedirectionMiddleware.cs b/Middleware/LanguageRedirectionMiddleware.cs index 5172bcc..da85db6 100644 --- a/Middleware/LanguageRedirectionMiddleware.cs +++ b/Middleware/LanguageRedirectionMiddleware.cs @@ -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)); diff --git a/QRRapidoApp.csproj b/QRRapidoApp.csproj index 9676638..36df240 100644 --- a/QRRapidoApp.csproj +++ b/QRRapidoApp.csproj @@ -21,7 +21,7 @@ - + diff --git a/README.md b/README.md index 4a2f1b8..7a5ae9c 100644 --- a/README.md +++ b/README.md @@ -342,4 +342,8 @@ 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 newline at end of file +*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 diff --git a/Resources/SharedResource.en.resx b/Resources/SharedResource.en.resx index 5e8b560..15dbffa 100644 --- a/Resources/SharedResource.en.resx +++ b/Resources/SharedResource.en.resx @@ -774,4 +774,13 @@ All rights reserved. + + QR codes remaining today + + + Daily limit reached! Login for unlimited access. + + + Anonymous users: 3 QR codes per day + \ No newline at end of file diff --git a/Resources/SharedResource.es.resx b/Resources/SharedResource.es.resx index 6bf68cb..81d282e 100644 --- a/Resources/SharedResource.es.resx +++ b/Resources/SharedResource.es.resx @@ -774,4 +774,13 @@ Todos los derechos reservados. + + Códigos QR restantes hoy + + + ¡Límite diario alcanzado! Inicia sesión para acceso ilimitado. + + + Usuarios anónimos: 3 códigos QR por día + \ No newline at end of file diff --git a/Resources/SharedResource.pt-BR.resx b/Resources/SharedResource.pt-BR.resx index 13a10e4..02d435f 100644 --- a/Resources/SharedResource.pt-BR.resx +++ b/Resources/SharedResource.pt-BR.resx @@ -774,4 +774,13 @@ Todos os direitos reservados. + + QR codes restantes hoje + + + Limite diário atingido! Faça login para acesso ilimitado. + + + Usuários anônimos: 3 QR codes por dia + \ No newline at end of file diff --git a/Services/IUserService.cs b/Services/IUserService.cs index c553a69..c46df8d 100644 --- a/Services/IUserService.cs +++ b/Services/IUserService.cs @@ -16,7 +16,8 @@ namespace QRRapidoApp.Services Task GetUserByStripeCustomerIdAsync(string customerId); Task UpdateUserAsync(User user); Task GetDailyQRCountAsync(string? userId); - Task DecrementDailyQRCountAsync(string userId); + Task IncrementDailyQRCountAsync(string userId); + Task GetRemainingQRCountAsync(string userId); Task CanGenerateQRAsync(string? userId, bool isPremium); Task SaveQRToHistoryAsync(string? userId, QRGenerationResult qrResult); Task> GetUserQRHistoryAsync(string userId, int limit = 50); diff --git a/Services/StripeService.cs b/Services/StripeService.cs index 2a86d61..d45824b 100644 --- a/Services/StripeService.cs +++ b/Services/StripeService.cs @@ -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}."); } diff --git a/Services/UserService.cs b/Services/UserService.cs index aa62c1c..5628336 100644 --- a/Services/UserService.cs +++ b/Services/UserService.cs @@ -142,7 +142,7 @@ namespace QRRapidoApp.Services } } - public async Task DecrementDailyQRCountAsync(string userId) + public async Task 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("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 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 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("Premium:FreeQRLimit", 50); + var limit = 3; return dailyCount < limit; } diff --git a/Views/Home/Index.cshtml b/Views/Home/Index.cshtml index 0a9dbf6..58506f1 100644 --- a/Views/Home/Index.cshtml +++ b/Views/Home/Index.cshtml @@ -12,7 +12,22 @@
- + @{ + var statusValue = "anonymous"; + if (User.Identity.IsAuthenticated) + { + var isPremium = await AdService.HasValidPremiumSubscription(userId); + statusValue = isPremium ? "premium" : "logged-in"; + } + } + + + +
+ @Localizer["RateLimitExceeded"] + @Localizer["QRCodesRemainingToday"] + @Localizer["UnlimitedToday"] +
@@ -55,18 +70,9 @@
- @if (User.Identity.IsAuthenticated) - { - - @Localizer["UnlimitedToday"] - - } - else - { - - @Localizer["QRCodesRemaining"] - - } + + Carregando... +
diff --git a/Views/Pagamento/Sucesso.cshtml b/Views/Pagamento/Sucesso.cshtml index 5eef1c6..1808362 100644 --- a/Views/Pagamento/Sucesso.cshtml +++ b/Views/Pagamento/Sucesso.cshtml @@ -1,6 +1,7 @@ @using Microsoft.Extensions.Localization @inject IStringLocalizer Localizer @{ + Layout = "~/Views/Shared/_Layout.cshtml"; ViewData["Title"] = "Sucesso"; } diff --git a/appsettings.json b/appsettings.json index 113df9a..ecf6bcc 100644 --- a/appsettings.json +++ b/appsettings.json @@ -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, diff --git a/wwwroot/js/qr-speed-generator.js b/wwwroot/js/qr-speed-generator.js index 5fb97eb..8dbeaab 100644 --- a/wwwroot/js/qr-speed-generator.js +++ b/wwwroot/js/qr-speed-generator.js @@ -1,5 +1,5 @@ -// QR Rapido Speed Generator -class QRRapidoGenerator { +// QR Rapido Speed Generator +class QRRapidoGenerator { constructor() { this.startTime = 0; this.currentQR = null; @@ -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; @@ -707,10 +712,18 @@ class QRRapidoGenerator { id: result.qrId, generationTime: generationTime }; + + // Increment rate limit counter after successful generation + this.incrementRateLimit(); - // Update counter for free users + // Update counter for logged users if (result.remainingQRs !== undefined) { - this.updateRemainingCounter(result.remainingQRs); + if (result.remainingQRs === -1 || result.remainingQRs === 2147483647 || result.remainingQRs >= 1000000) { + // Logged user - show unlimited + this.showUnlimitedCounter(); + } else { + this.updateRemainingCounter(result.remainingQRs); + } } } @@ -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