From b189ea7275c92cd6cb3902c2279adc04087c506d Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Tue, 29 Jul 2025 19:11:47 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Historico=20e=20localiza=C3=A7=C3=A3o!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 3 +- Controllers/AccountController.cs | 15 +- Controllers/PagamentoController.cs | 120 +++ Controllers/PremiumController.cs | 106 +-- Data/MongoDbContext.cs | 5 +- Middleware/LanguageRedirectionMiddleware.cs | 111 +++ Models/Extensions/PlanExtensions.cs | 58 ++ Models/Plan.cs | 47 ++ Models/ViewModels/SelecaoPlanoViewModel.cs | 12 + Program.cs | 69 +- QRRapidoApp.csproj | 26 +- Resources/SharedResource.Designer.cs | 693 ++++++++++++++++++ ...iews.pt-BR.resx => SharedResource.en.resx} | 84 +-- .../{Views.es.resx => SharedResource.es.resx} | 106 ++- Resources/SharedResource.pt-BR.resx | 271 +++++++ Resources/SharedResource.resx | 271 +++++++ Services/IPlanService.cs | 14 + Services/IUserService.cs | 4 + Services/PlanService.cs | 35 + Services/RouteDataRequestCultureProvider.cs | 27 + Services/StripeService.cs | 344 +++------ Services/UserService.cs | 58 +- Views/Account/History.cshtml | 158 ++++ Views/Home/Index.cshtml | 56 +- Views/Pagamento/Cancelar.cshtml | 12 + Views/Pagamento/SelecaoPlano.cshtml | 116 +++ Views/Pagamento/Sucesso.cshtml | 12 + Views/Shared/_Layout.cshtml | 120 +-- appsettings.json | 8 +- wwwroot/js/language-switcher.js | 144 ++++ 30 files changed, 2540 insertions(+), 565 deletions(-) create mode 100644 Controllers/PagamentoController.cs create mode 100644 Middleware/LanguageRedirectionMiddleware.cs create mode 100644 Models/Extensions/PlanExtensions.cs create mode 100644 Models/Plan.cs create mode 100644 Models/ViewModels/SelecaoPlanoViewModel.cs create mode 100644 Resources/SharedResource.Designer.cs rename Resources/{Views.pt-BR.resx => SharedResource.en.resx} (77%) rename Resources/{Views.es.resx => SharedResource.es.resx} (68%) create mode 100644 Resources/SharedResource.pt-BR.resx create mode 100644 Resources/SharedResource.resx create mode 100644 Services/IPlanService.cs create mode 100644 Services/PlanService.cs create mode 100644 Services/RouteDataRequestCultureProvider.cs create mode 100644 Views/Account/History.cshtml create mode 100644 Views/Pagamento/Cancelar.cshtml create mode 100644 Views/Pagamento/SelecaoPlano.cshtml create mode 100644 Views/Pagamento/Sucesso.cshtml create mode 100644 wwwroot/js/language-switcher.js diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 088fcf9..16797be 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -4,7 +4,8 @@ "Bash(dotnet new:*)", "Bash(find:*)", "Bash(dotnet build:*)", - "Bash(timeout:*)" + "Bash(timeout:*)", + "Bash(rm:*)" ], "deny": [] } diff --git a/Controllers/AccountController.cs b/Controllers/AccountController.cs index c890574..d988058 100644 --- a/Controllers/AccountController.cs +++ b/Controllers/AccountController.cs @@ -15,12 +15,15 @@ namespace QRRapidoApp.Controllers private readonly IUserService _userService; private readonly AdDisplayService _adDisplayService; private readonly ILogger _logger; + private readonly IConfiguration _configuration; - public AccountController(IUserService userService, AdDisplayService adDisplayService, ILogger logger) + public AccountController(IUserService userService, AdDisplayService adDisplayService, + ILogger logger, IConfiguration configuration) { _userService = userService; _adDisplayService = adDisplayService; _logger = logger; + _configuration = configuration; } [HttpGet] @@ -33,9 +36,10 @@ namespace QRRapidoApp.Controllers [HttpGet] public IActionResult LoginGoogle(string returnUrl = "/") { + var baseUrl = _configuration.GetSection("App:BaseUrl").Value; var properties = new AuthenticationProperties { - RedirectUri = Url.Action("GoogleCallback"), + RedirectUri = $"{baseUrl}{Url.Action("GoogleCallback")}", Items = { { "returnUrl", returnUrl } } }; return Challenge(properties, GoogleDefaults.AuthenticationScheme); @@ -44,11 +48,8 @@ namespace QRRapidoApp.Controllers [HttpGet] public IActionResult LoginMicrosoft(string returnUrl = "/") { - var properties = new AuthenticationProperties - { - RedirectUri = Url.Action("MicrosoftCallback"), - Items = { { "returnUrl", returnUrl } } - }; + var redirectUrl = Url.Action("MicrosoftCallback", "Account", new { returnUrl }); + var properties = new AuthenticationProperties { RedirectUri = redirectUrl }; return Challenge(properties, MicrosoftAccountDefaults.AuthenticationScheme); } diff --git a/Controllers/PagamentoController.cs b/Controllers/PagamentoController.cs new file mode 100644 index 0000000..81b6ef4 --- /dev/null +++ b/Controllers/PagamentoController.cs @@ -0,0 +1,120 @@ + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using QRRapidoApp.Services; +using System.Security.Claims; +using System.Threading.Tasks; +using QRRapidoApp.Models.ViewModels; +using System.Linq; + +namespace QRRapidoApp.Controllers +{ + [Authorize] + public class PagamentoController : Controller + { + private readonly IPlanService _planService; + private readonly IUserService _userService; + private readonly StripeService _stripeService; + private readonly ILogger _logger; + + public PagamentoController(IPlanService planService, IUserService userService, StripeService stripeService, ILogger logger) + { + _planService = planService; + _userService = userService; + _stripeService = stripeService; + _logger = logger; + } + + [HttpGet] + public async Task SelecaoPlano() + { + var plans = await _planService.GetActivePlansAsync(); + var countryCode = GetUserCountryCode(); // Implement this method based on your needs + + var model = new SelecaoPlanoViewModel + { + Plans = plans, + CountryCode = countryCode + }; + + return View(model); + } + + [HttpPost] + public async Task CreateCheckout(string planId) + { + var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (string.IsNullOrEmpty(userId)) + { + return Json(new { success = false, error = "User not authenticated" }); + } + + var plan = await _planService.GetPlanByIdAsync(planId); + if (plan == null) + { + return Json(new { success = false, error = "Plan not found" }); + } + + var countryCode = GetUserCountryCode(); + var priceId = plan.PricesByCountry.ContainsKey(countryCode) + ? plan.PricesByCountry[countryCode].StripePriceId + : plan.StripePriceId; + + try + { + var checkoutUrl = await _stripeService.CreateCheckoutSessionAsync(userId, priceId); + return Json(new { success = true, url = checkoutUrl }); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error creating checkout session for user {userId} and plan {planId}"); + return Json(new { success = false, error = ex.Message }); + } + } + + [HttpGet] + public IActionResult Sucesso() + { + ViewBag.SuccessMessage = "Pagamento concluído com sucesso! Bem-vindo ao Premium."; + return View(); + } + + [HttpGet] + public IActionResult Cancelar() + { + ViewBag.CancelMessage = "O pagamento foi cancelado. Você pode tentar novamente a qualquer momento."; + return View("SelecaoPlano"); + } + + [HttpPost] + [AllowAnonymous] + public async Task StripeWebhook() + { + try + { + using var reader = new StreamReader(HttpContext.Request.Body); + var json = await reader.ReadToEndAsync(); + var signature = Request.Headers["Stripe-Signature"].FirstOrDefault(); + + if (string.IsNullOrEmpty(signature)) + { + return BadRequest("Missing Stripe signature"); + } + + await _stripeService.HandleWebhookAsync(json, signature); + return Ok(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing Stripe webhook"); + return BadRequest(ex.Message); + } + } + + private string GetUserCountryCode() + { + // Prioritize Cloudflare header, fallback to a default or other methods + return HttpContext.Request.Headers["CF-IPCountry"].FirstOrDefault() ?? "BR"; + } + } +} diff --git a/Controllers/PremiumController.cs b/Controllers/PremiumController.cs index bec1641..7bf826d 100644 --- a/Controllers/PremiumController.cs +++ b/Controllers/PremiumController.cs @@ -25,111 +25,9 @@ namespace QRRapidoApp.Controllers } [HttpGet] - public async Task Upgrade() + public IActionResult Upgrade() { - var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; - if (string.IsNullOrEmpty(userId)) - { - return RedirectToAction("Login", "Account"); - } - - var user = await _userService.GetUserAsync(userId); - if (user?.IsPremium == true) - { - return RedirectToAction("Dashboard"); - } - - var model = new UpgradeViewModel - { - CurrentPlan = "Free", - PremiumPrice = _config.GetValue("Premium:PremiumPrice"), - Features = _config.GetSection("Premium:Features").Get>() ?? new(), - RemainingQRs = await _userService.GetDailyQRCountAsync(userId), - IsAdFreeActive = !await _adDisplayService.ShouldShowAds(userId), - DaysUntilAdExpiry = (int)((await _adDisplayService.GetAdFreeExpiryDate(userId) - DateTime.UtcNow)?.TotalDays ?? 0) - }; - - return View(model); - } - - [HttpPost] - public async Task CreateCheckout() - { - var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; - if (string.IsNullOrEmpty(userId)) - { - return Json(new { success = false, error = "User not authenticated" }); - } - - var priceId = _config["Stripe:PriceId"]; - if (string.IsNullOrEmpty(priceId)) - { - return Json(new { success = false, error = "Stripe not configured" }); - } - - try - { - var checkoutUrl = await _stripeService.CreateCheckoutSessionAsync(userId, priceId); - return Json(new { success = true, url = checkoutUrl }); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error creating checkout session for user {userId}"); - return Json(new { success = false, error = ex.Message }); - } - } - - [HttpGet] - public async Task Success(string session_id) - { - if (string.IsNullOrEmpty(session_id)) - { - return RedirectToAction("Upgrade"); - } - - try - { - ViewBag.Success = true; - ViewBag.SessionId = session_id; - return View(); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error processing successful payment for session {session_id}"); - return RedirectToAction("Upgrade"); - } - } - - [HttpGet] - public IActionResult Cancel() - { - ViewBag.Cancelled = true; - return View("Upgrade"); - } - - [HttpPost] - [AllowAnonymous] - public async Task StripeWebhook() - { - try - { - using var reader = new StreamReader(HttpContext.Request.Body); - var json = await reader.ReadToEndAsync(); - var signature = Request.Headers["Stripe-Signature"].FirstOrDefault(); - - if (string.IsNullOrEmpty(signature)) - { - return BadRequest("Missing Stripe signature"); - } - - await _stripeService.HandleWebhookAsync(json, signature); - return Ok(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error processing Stripe webhook"); - return BadRequest(ex.Message); - } + return RedirectToAction("SelecaoPlano", "Pagamento"); } [HttpGet] diff --git a/Data/MongoDbContext.cs b/Data/MongoDbContext.cs index feabf83..e4c7d70 100644 --- a/Data/MongoDbContext.cs +++ b/Data/MongoDbContext.cs @@ -30,8 +30,9 @@ namespace QRRapidoApp.Data } } - public IMongoCollection? Users => _isConnected ? _database?.GetCollection("users") : null; - public IMongoCollection? QRCodeHistory => _isConnected ? _database?.GetCollection("qr_codes") : null; + public IMongoCollection Users => _database.GetCollection("users"); + public IMongoCollection QRCodeHistory => _database.GetCollection("qrCodeHistory"); + public IMongoCollection Plans => _database.GetCollection("plans"); public IMongoCollection? AdFreeSessions => _isConnected ? _database?.GetCollection("ad_free_sessions") : null; public IMongoDatabase? Database => _isConnected ? _database : null; diff --git a/Middleware/LanguageRedirectionMiddleware.cs b/Middleware/LanguageRedirectionMiddleware.cs new file mode 100644 index 0000000..733fc2a --- /dev/null +++ b/Middleware/LanguageRedirectionMiddleware.cs @@ -0,0 +1,111 @@ +using System.Globalization; + +namespace QRRapidoApp.Middleware +{ + public class LanguageRedirectionMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly string[] _supportedCultures = { "pt-BR", "es", "en" }; + private const string DefaultCulture = "pt-BR"; + + public LanguageRedirectionMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + var path = context.Request.Path.Value?.TrimStart('/') ?? ""; + + // Skip if already has culture in path, or if it's an API, static file, or special route + if (HasCultureInPath(path) || IsSpecialRoute(path)) + { + await _next(context); + return; + } + + // Detect browser language + var detectedCulture = DetectBrowserLanguage(context); + + // Build redirect URL with culture + var redirectUrl = $"/{detectedCulture}"; + if (!string.IsNullOrEmpty(path)) + { + redirectUrl += $"/{path}"; + } + + // Add query string if present + if (context.Request.QueryString.HasValue) + { + redirectUrl += context.Request.QueryString.Value; + } + + _logger.LogInformation("Redirecting to localized URL: {RedirectUrl} (detected culture: {Culture})", + redirectUrl, detectedCulture); + + context.Response.Redirect(redirectUrl, permanent: false); + } + + private bool HasCultureInPath(string path) + { + if (string.IsNullOrEmpty(path)) + return false; + + var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries); + if (segments.Length == 0) + return false; + + return _supportedCultures.Contains(segments[0]); + } + + private bool IsSpecialRoute(string path) + { + var specialRoutes = new[] + { + "api/", "health", "_framework/", "lib/", "css/", "js/", "images/", + "favicon.ico", "robots.txt", "sitemap.xml" + }; + + return specialRoutes.Any(route => path.StartsWith(route, StringComparison.OrdinalIgnoreCase)); + } + + private string DetectBrowserLanguage(HttpContext context) + { + var acceptLanguage = context.Request.GetTypedHeaders().AcceptLanguage; + + if (acceptLanguage != null && acceptLanguage.Any()) + { + // Check for exact matches first + foreach (var lang in acceptLanguage.OrderByDescending(x => x.Quality ?? 1.0)) + { + var langCode = lang.Value.Value; + + // Special case: es-PY should redirect to pt-BR + if (string.Equals(langCode, "es-PY", StringComparison.OrdinalIgnoreCase)) + { + return DefaultCulture; + } + + // Check exact match + if (_supportedCultures.Contains(langCode)) + { + return langCode; + } + + // Check language part only (e.g., 'es' from 'es-AR') + var languagePart = langCode.Split('-')[0]; + var matchingCulture = _supportedCultures.FirstOrDefault(c => c.StartsWith(languagePart + "-") || c == languagePart); + if (matchingCulture != null) + { + return matchingCulture; + } + } + } + + // Default fallback + return DefaultCulture; + } + } +} \ No newline at end of file diff --git a/Models/Extensions/PlanExtensions.cs b/Models/Extensions/PlanExtensions.cs new file mode 100644 index 0000000..49b5657 --- /dev/null +++ b/Models/Extensions/PlanExtensions.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Linq; + +namespace QRRapidoApp.Models.Extensions +{ + public static class PlanExtensions + { + public static string GetLocalizedName(this Plan plan, string languageCode) + { + if (plan.Name.TryGetValue(languageCode, out var name)) + return name; + + // Fallback to Portuguese if language not found + if (plan.Name.TryGetValue("pt-BR", out var ptName)) + return ptName; + + // Final fallback to first available language + return plan.Name.Values.FirstOrDefault() ?? "Plan"; + } + + public static string GetLocalizedDescription(this Plan plan, string languageCode) + { + if (plan.Description.TryGetValue(languageCode, out var description)) + return description; + + // Fallback to Portuguese if language not found + if (plan.Description.TryGetValue("pt-BR", out var ptDescription)) + return ptDescription; + + // Final fallback to first available language + return plan.Description.Values.FirstOrDefault() ?? "Premium plan description"; + } + + public static List GetLocalizedFeatures(this Plan plan, string languageCode) + { + if (plan.Features.TryGetValue(languageCode, out var features)) + return features; + + // Fallback to Portuguese if language not found + if (plan.Features.TryGetValue("pt-BR", out var ptFeatures)) + return ptFeatures; + + // Final fallback to first available language + return plan.Features.Values.FirstOrDefault() ?? new List(); + } + + public static string GetLanguageCode(string culture) + { + return culture switch + { + "pt-BR" or "pt" => "pt-BR", + "es" or "es-ES" => "es", + "en" or "en-US" => "en", + _ => "pt-BR" // Default to Portuguese + }; + } + } +} \ No newline at end of file diff --git a/Models/Plan.cs b/Models/Plan.cs new file mode 100644 index 0000000..30c48a6 --- /dev/null +++ b/Models/Plan.cs @@ -0,0 +1,47 @@ + +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Collections.Generic; + +namespace QRRapidoApp.Models +{ + public class Plan + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } = string.Empty; + + [BsonElement("name")] + public Dictionary Name { get; set; } = new(); // e.g., {"pt": "Premium Mensal", "en": "Premium Monthly", "es": "Premium Mensual"} + + [BsonElement("description")] + public Dictionary Description { get; set; } = new(); // Multilingual descriptions + + [BsonElement("features")] + public Dictionary> Features { get; set; } = new(); // Multilingual feature lists + + [BsonElement("interval")] + public string Interval { get; set; } = string.Empty; // e.g., "month", "year" + + [BsonElement("stripePriceId")] + public string StripePriceId { get; set; } = string.Empty; // Default Price ID + + [BsonElement("pricesByCountry")] + public Dictionary PricesByCountry { get; set; } = new(); + + [BsonElement("isActive")] + public bool IsActive { get; set; } = true; + } + + public class PriceInfo + { + [BsonElement("amount")] + public decimal Amount { get; set; } + + [BsonElement("currency")] + public string Currency { get; set; } = string.Empty; + + [BsonElement("stripePriceId")] + public string StripePriceId { get; set; } = string.Empty; + } +} diff --git a/Models/ViewModels/SelecaoPlanoViewModel.cs b/Models/ViewModels/SelecaoPlanoViewModel.cs new file mode 100644 index 0000000..d6d81c8 --- /dev/null +++ b/Models/ViewModels/SelecaoPlanoViewModel.cs @@ -0,0 +1,12 @@ + +using QRRapidoApp.Models; +using System.Collections.Generic; + +namespace QRRapidoApp.Models.ViewModels +{ + public class SelecaoPlanoViewModel + { + public List Plans { get; set; } = new(); + public string CountryCode { get; set; } = "BR"; + } +} diff --git a/Program.cs b/Program.cs index 5ed87a3..85eeb44 100644 --- a/Program.cs +++ b/Program.cs @@ -17,6 +17,7 @@ using System.Globalization; using Serilog; using Serilog.Events; using Serilog.Sinks.SystemConsole.Themes; +using Microsoft.AspNetCore.Mvc.Razor; var builder = WebApplication.CreateBuilder(args); @@ -28,33 +29,28 @@ Log.Logger = new LoggerConfiguration() .Enrich.WithProcessId() .Enrich.WithThreadId() .Enrich.WithProperty("ApplicationName", builder.Configuration["ApplicationName"] ?? "QRRapido") + .Enrich.WithProperty("Environment", "Dev") .WriteTo.Async(a => a.Console( outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}", theme: AnsiConsoleTheme.Code)) - .WriteTo.Async(a => { - var seqUrl = builder.Configuration["Serilog:SeqUrl"]; - var apiKey = builder.Configuration["Serilog:ApiKey"]; - - if (!string.IsNullOrEmpty(seqUrl)) - { - try - { - // Temporarily skip Seq until packages are installed - Console.WriteLine($"Seq configured for: {seqUrl}"); - } - catch (Exception ex) - { - Console.WriteLine($"Failed to configure Seq sink: {ex.Message}"); - // Continue without Seq - will still log to console - } - } - }) + .WriteTo.Async(a => a.Seq(builder.Configuration["Serilog:SeqUrl"], + apiKey: builder.Configuration["Serilog:ApiKey"]=="" ? null : builder.Configuration["Serilog:ApiKey"])) .CreateLogger(); builder.Host.UseSerilog(); // Add services to the container -builder.Services.AddControllersWithViews(); +builder.Services.AddControllersWithViews() + .AddViewLocalization(Microsoft.AspNetCore.Mvc.Razor.LanguageViewLocationExpanderFormat.Suffix) + .AddDataAnnotationsLocalization(); + +builder.Services.AddDistributedMemoryCache(); // Armazena os dados da sess�o na mem�ria +builder.Services.AddSession(options => +{ + options.Cookie.HttpOnly = true; + options.Cookie.IsEssential = true; + options.IdleTimeout = TimeSpan.FromMinutes(30); // Tempo de expira��o +}); // Add HttpClient for health checks builder.Services.AddHttpClient(); @@ -132,7 +128,8 @@ builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationSc StripeConfiguration.ApiKey = builder.Configuration["Stripe:SecretKey"]; // Localization -builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); +builder.Services.AddLocalization(options => options.ResourcesPath = ""); +//builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); builder.Services.Configure(options => { var supportedCultures = new[] @@ -142,17 +139,24 @@ builder.Services.Configure(options => new CultureInfo("en") }; - options.DefaultRequestCulture = new RequestCulture("pt-BR"); + options.DefaultRequestCulture = new RequestCulture("pt-BR", "pt-BR"); options.SupportedCultures = supportedCultures; options.SupportedUICultures = supportedCultures; - - options.RequestCultureProviders.Insert(0, new QueryStringRequestCultureProvider()); - options.RequestCultureProviders.Insert(1, new CookieRequestCultureProvider()); + + options.FallBackToParentCultures = true; + options.FallBackToParentUICultures = true; + + // Clear default providers and add custom ones in priority order + options.RequestCultureProviders.Clear(); + options.RequestCultureProviders.Add(new RouteDataRequestCultureProvider()); + options.RequestCultureProviders.Add(new QueryStringRequestCultureProvider()); + options.RequestCultureProviders.Add(new CookieRequestCultureProvider()); }); // Custom Services builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -206,6 +210,9 @@ app.UseHttpsRedirection(); app.UseStaticFiles(); +// Language redirection middleware (before routing) +app.UseMiddleware(); + app.UseRouting(); app.UseCors("AllowSpecificOrigins"); @@ -216,26 +223,28 @@ app.UseRequestLocalization(); app.UseAuthentication(); app.UseAuthorization(); +app.UseSession(); + // Custom middleware app.UseMiddleware(); // Health check endpoint app.MapHealthChecks("/health"); -// Controller routes +// Language routes (must be first) app.MapControllerRoute( - name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}"); + name: "localized", + pattern: "{culture:regex(^(pt-BR|es|en)$)}/{controller=Home}/{action=Index}/{id?}"); // API routes app.MapControllerRoute( name: "api", pattern: "api/{controller}/{action=Index}/{id?}"); -// Language routes +// Default fallback route (for development/testing without culture) app.MapControllerRoute( - name: "localized", - pattern: "{culture:regex(^(pt-BR|es|en)$)}/{controller=Home}/{action=Index}/{id?}"); + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); try { diff --git a/QRRapidoApp.csproj b/QRRapidoApp.csproj index d974de7..9676638 100644 --- a/QRRapidoApp.csproj +++ b/QRRapidoApp.csproj @@ -20,6 +20,7 @@ + @@ -34,7 +35,6 @@ - @@ -42,4 +42,28 @@ + + + True + True + SharedResource.resx + + + + + + PublicResXFileCodeGenerator + + + PublicResXFileCodeGenerator + + + PublicResXFileCodeGenerator + + + PublicResXFileCodeGenerator + SharedResource.Designer.cs + + + \ No newline at end of file diff --git a/Resources/SharedResource.Designer.cs b/Resources/SharedResource.Designer.cs new file mode 100644 index 0000000..197f181 --- /dev/null +++ b/Resources/SharedResource.Designer.cs @@ -0,0 +1,693 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace QRRapidoApp.Resources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class SharedResource { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal SharedResource() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("QRRapidoApp.Resources.SharedResource", typeof(SharedResource).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Unlock all premium features!. + /// + public static string AdFreeOffer { + get { + return ResourceManager.GetString("AdFreeOffer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Advanced Customization. + /// + public static string AdvancedCustomization { + get { + return ResourceManager.GetString("AdvancedCustomization", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Background Color. + /// + public static string BackgroundColor { + get { + return ResourceManager.GetString("BackgroundColor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Back to generator. + /// + public static string BackToGenerator { + get { + return ResourceManager.GetString("BackToGenerator", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Classic. + /// + public static string Classic { + get { + return ResourceManager.GetString("Classic", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Classic. + /// + public static string ClassicStyle { + get { + return ResourceManager.GetString("ClassicStyle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Colorful. + /// + public static string Colorful { + get { + return ResourceManager.GetString("Colorful", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Colorful. + /// + public static string ColorfulStyle { + get { + return ResourceManager.GetString("ColorfulStyle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Content. + /// + public static string Content { + get { + return ResourceManager.GetString("Content", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Content hints. + /// + public static string ContentHints { + get { + return ResourceManager.GetString("ContentHints", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enter your QR code content here.... + /// + public static string ContentPlaceholder { + get { + return ResourceManager.GetString("ContentPlaceholder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Corner Style. + /// + public static string CornerStyle { + get { + return ResourceManager.GetString("CornerStyle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Create QR Code Quickly. + /// + public static string CreateQRCodeQuickly { + get { + return ResourceManager.GetString("CreateQRCodeQuickly", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Download PDF. + /// + public static string DownloadPDF { + get { + return ResourceManager.GetString("DownloadPDF", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Download PNG. + /// + public static string DownloadPNG { + get { + return ResourceManager.GetString("DownloadPNG", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Download SVG (Vector). + /// + public static string DownloadSVG { + get { + return ResourceManager.GetString("DownloadSVG", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Dynamic QR (Premium). + /// + public static string DynamicQRPremium { + get { + return ResourceManager.GetString("DynamicQRPremium", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Dynamic QR (Premium). + /// + public static string DynamicType { + get { + return ResourceManager.GetString("DynamicType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Email. + /// + public static string EmailType { + get { + return ResourceManager.GetString("EmailType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enter your QR code content here.... + /// + public static string EnterQRCodeContent { + get { + return ResourceManager.GetString("EnterQRCodeContent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generation error. Try again.. + /// + public static string Error { + get { + return ResourceManager.GetString("Error", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fast generation!. + /// + public static string Fast { + get { + return ResourceManager.GetString("Fast", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generated in. + /// + public static string GeneratedIn { + get { + return ResourceManager.GetString("GeneratedIn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generate QR Code. + /// + public static string GenerateQR { + get { + return ResourceManager.GetString("GenerateQR", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generate QR Code Rapidly. + /// + public static string GenerateRapidly { + get { + return ResourceManager.GetString("GenerateRapidly", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Google. + /// + public static string Google { + get { + return ResourceManager.GetString("Google", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Login. + /// + public static string Login { + get { + return ResourceManager.GetString("Login", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove ads, access detailed analytics and much more for only $12.90/month or $129.00/year. Login and subscribe now!. + /// + public static string LoginBenefits { + get { + return ResourceManager.GetString("LoginBenefits", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Login to save to history. + /// + public static string LoginToSave { + get { + return ResourceManager.GetString("LoginToSave", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Login with. + /// + public static string LoginWith { + get { + return ResourceManager.GetString("LoginWith", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logo/Icon. + /// + public static string Logo { + get { + return ResourceManager.GetString("Logo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Margin. + /// + public static string Margin { + get { + return ResourceManager.GetString("Margin", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Microsoft. + /// + public static string Microsoft { + get { + return ResourceManager.GetString("Microsoft", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Modern. + /// + public static string Modern { + get { + return ResourceManager.GetString("Modern", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Modern. + /// + public static string ModernStyle { + get { + return ResourceManager.GetString("ModernStyle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No Ads • History • Unlimited QR. + /// + public static string NoAdsHistoryUnlimitedQR { + get { + return ResourceManager.GetString("NoAdsHistoryUnlimitedQR", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Normal generation. + /// + public static string Normal { + get { + return ResourceManager.GetString("Normal", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to QR Rapido Premium. + /// + public static string PremiumTitle { + get { + return ResourceManager.GetString("PremiumTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Premium User Active. + /// + public static string PremiumUserActive { + get { + return ResourceManager.GetString("PremiumUserActive", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Preview. + /// + public static string Preview { + get { + return ResourceManager.GetString("Preview", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your QR code will appear here in seconds. + /// + public static string PreviewPlaceholder { + get { + return ResourceManager.GetString("PreviewPlaceholder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Primary Color. + /// + public static string PrimaryColor { + get { + return ResourceManager.GetString("PrimaryColor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Privacy Policy. + /// + public static string Privacy { + get { + return ResourceManager.GetString("Privacy", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to QR codes remaining. + /// + public static string QRCodesRemaining { + get { + return ResourceManager.GetString("QRCodesRemaining", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to QR Code Type. + /// + public static string QRCodeType { + get { + return ResourceManager.GetString("QRCodeType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to QR Code Type. + /// + public static string QRType { + get { + return ResourceManager.GetString("QRType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Quick Style. + /// + public static string QuickStyle { + get { + return ResourceManager.GetString("QuickStyle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Save to History. + /// + public static string SaveToHistory { + get { + return ResourceManager.GetString("SaveToHistory", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to s. + /// + public static string Seconds { + get { + return ResourceManager.GetString("Seconds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Select type. + /// + public static string SelectType { + get { + return ResourceManager.GetString("SelectType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Simple Text. + /// + public static string SimpleText { + get { + return ResourceManager.GetString("SimpleText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Size. + /// + public static string Size { + get { + return ResourceManager.GetString("Size", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SMS. + /// + public static string SMSType { + get { + return ResourceManager.GetString("SMSType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Premium Offer!. + /// + public static string SpecialOffer { + get { + return ResourceManager.GetString("SpecialOffer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Short URLs generate faster. + /// + public static string SpeedTip1 { + get { + return ResourceManager.GetString("SpeedTip1", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Less text = higher speed. + /// + public static string SpeedTip2 { + get { + return ResourceManager.GetString("SpeedTip2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Solid colors optimize the process. + /// + public static string SpeedTip3 { + get { + return ResourceManager.GetString("SpeedTip3", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Smaller sizes speed up downloads. + /// + public static string SpeedTip4 { + get { + return ResourceManager.GetString("SpeedTip4", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tips for Faster QR. + /// + public static string SpeedTipsTitle { + get { + return ResourceManager.GetString("SpeedTipsTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to QR Code saved to history!. + /// + public static string Success { + get { + return ResourceManager.GetString("Success", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generate QR codes in seconds!. + /// + public static string Tagline { + get { + return ResourceManager.GetString("Tagline", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Plain Text. + /// + public static string TextType { + get { + return ResourceManager.GetString("TextType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Ultra fast generation!. + /// + public static string UltraFast { + get { + return ResourceManager.GetString("UltraFast", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Ultra-fast generation guaranteed. + /// + public static string UltraFastGeneration { + get { + return ResourceManager.GetString("UltraFastGeneration", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unlimited today. + /// + public static string UnlimitedToday { + get { + return ResourceManager.GetString("UnlimitedToday", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to URL/Link. + /// + public static string URLLink { + get { + return ResourceManager.GetString("URLLink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to URL/Link. + /// + public static string URLType { + get { + return ResourceManager.GetString("URLType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Business Card. + /// + public static string VCard { + get { + return ResourceManager.GetString("VCard", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Business Card. + /// + public static string VCardType { + get { + return ResourceManager.GetString("VCardType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to WiFi. + /// + public static string WiFiType { + get { + return ResourceManager.GetString("WiFiType", resourceCulture); + } + } + } +} diff --git a/Resources/Views.pt-BR.resx b/Resources/SharedResource.en.resx similarity index 77% rename from Resources/Views.pt-BR.resx rename to Resources/SharedResource.en.resx index d5299bb..c8e1a4c 100644 --- a/Resources/Views.pt-BR.resx +++ b/Resources/SharedResource.en.resx @@ -59,28 +59,28 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Gere QR codes em segundos! + Generate QR codes in seconds! - Gerar QR Code + Generate QR Code - Tipo de QR Code + QR Code Type - Conteúdo + Content URL/Link - Texto Simples + Plain Text WiFi - Cartão de Visita + Business Card SMS @@ -89,94 +89,94 @@ Email - QR Dinâmico (Premium) + Dynamic QR (Premium) - Estilo Rápido + Quick Style - Clássico + Classic - Moderno + Modern - Colorido + Colorful - Digite o conteúdo do seu QR code aqui... + Enter your QR code content here... - Personalização Avançada + Advanced Customization - Cor Principal + Primary Color - Cor de Fundo + Background Color - Tamanho + Size - Margem + Margin - Logo/Ícone + Logo/Icon - Estilo das Bordas + Corner Style - Gerar QR Code Rapidamente + Generate QR Code Rapidly Preview - Seu QR code aparecerá aqui em segundos + Your QR code will appear here in seconds - Geração ultra-rápida garantida + Ultra-fast generation guaranteed Download PNG - Download SVG (Vetorial) + Download SVG (Vector) Download PDF - Salvar no Histórico + Save to History - Faça login para salvar no histórico + Login to save to history QR Rapido Premium - Dicas para QR Mais Rápidos + Tips for Faster QR - URLs curtas geram mais rápido + Short URLs generate faster - Menos texto = maior velocidade + Less text = higher speed - Cores sólidas otimizam o processo + Solid colors optimize the process - Tamanhos menores aceleram o download + Smaller sizes speed up downloads Login - Entrar com + Login with Google @@ -185,39 +185,39 @@ Microsoft - Login = 30 dias sem anúncios! + Unlock all premium features! - Oferta Especial! + Premium Offer! - Ao fazer login, você ganha automaticamente 30 dias sem anúncios e pode gerar até 50 QR codes por dia gratuitamente. + Remove ads, access detailed analytics and much more for only $12.90/month or $129.00/year. Login and subscribe now! - Política de Privacidade + Privacy Policy - Voltar ao gerador + Back to generator - Gerado em + Generated in s - Geração ultra rápida! + Ultra fast generation! - Geração rápida! + Fast generation! - Geração normal + Normal generation - Erro na geração. Tente novamente. + Generation error. Try again. - QR Code salvo no histórico! + QR Code saved to history! \ No newline at end of file diff --git a/Resources/Views.es.resx b/Resources/SharedResource.es.resx similarity index 68% rename from Resources/Views.es.resx rename to Resources/SharedResource.es.resx index 01cb9fc..982471b 100644 --- a/Resources/Views.es.resx +++ b/Resources/SharedResource.es.resx @@ -59,13 +59,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - ¡Genera códigos QR en segundos! + Genera codigos QR en segundos! - Generar Código QR + Generar Codigo QR - Tipo de Código QR + Tipo de Codigo QR Contenido @@ -89,13 +89,13 @@ Email - QR Dinámico (Premium) + QR Dinamico (Premium) - Estilo Rápido + Estilo Rapido - Clásico + Clasico Moderno @@ -104,10 +104,10 @@ Colorido - Escribe el contenido de tu código QR aquí... + Ingrese el contenido de su codigo QR aqui... - Personalización Avanzada + Personalizacion Avanzada Color Principal @@ -116,7 +116,7 @@ Color de Fondo - Tamaño + Tamano Margen @@ -125,19 +125,19 @@ Logo/Icono - Estilo de Bordes + Estilo de Esquinas - Generar Código QR Rápidamente + Generar Codigo QR Rapidamente Vista Previa - Tu código QR aparecerá aquí en segundos + Su codigo QR aparecera aqui en segundos - Generación ultra-rápida garantizada + Generacion ultra-rapida garantizada Descargar PNG @@ -152,31 +152,31 @@ Guardar en Historial - Inicia sesión para guardar en el historial + Inicie sesion para guardar en el historial QR Rapido Premium - Consejos para QR Más Rápidos + Consejos para QR Mas Rapidos - URLs cortas se generan más rápido + URLs cortas se generan mas rapido Menos texto = mayor velocidad - Colores sólidos optimizan el proceso + Colores solidos optimizan el proceso - Tamaños menores aceleran la descarga + Tamanos menores aceleran la descarga - Iniciar Sesión + Iniciar Sesion - Iniciar sesión con + Iniciar con Google @@ -185,16 +185,16 @@ Microsoft - ¡Login = 30 días sin anuncios! + Desbloquea todas las funciones premium! - ¡Oferta Especial! + Oferta Premium! - Al iniciar sesión, ganas automáticamente 30 días sin anuncios y puedes generar hasta 50 códigos QR por día gratis. + Elimina anuncios, accede a analisis detallados y mucho mas por solo $12.90/mes o $129.00/ano. Inicia sesion y suscribete ahora! - Política de Privacidad + Politica de Privacidad Volver al generador @@ -206,18 +206,66 @@ s - ¡Generación ultra rápida! + Generacion ultra rapida! - ¡Generación rápida! + Generacion rapida! - Generación normal + Generacion normal - Error en la generación. Inténtalo de nuevo. + Error en la generacion. Intentelo de nuevo. - ¡Código QR guardado en el historial! + Codigo QR guardado en el historial! + + + Crear Codigo QR Rapidamente + + + Usuario Premium Activo + + + Sin Anuncios • Historial • QR Ilimitado + + + Ilimitado hoy + + + Codigos QR restantes + + + Tipo de Codigo QR + + + Seleccionar tipo + + + URL/Enlace + + + Texto Simple + + + Tarjeta de Visita + + + QR Dinamico (Premium) + + + Ingrese el contenido de su codigo QR aqui... + + + Sugerencias de contenido + + + Clasico + + + Moderno + + + Colorido \ No newline at end of file diff --git a/Resources/SharedResource.pt-BR.resx b/Resources/SharedResource.pt-BR.resx new file mode 100644 index 0000000..564aac3 --- /dev/null +++ b/Resources/SharedResource.pt-BR.resx @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Gere QR codes em segundos! + + + Gerar QR Code + + + Tipo de QR Code + + + Conteudo + + + URL/Link + + + Texto Simples + + + WiFi + + + Cartao de Visita + + + SMS + + + Email + + + QR Dinamico (Premium) + + + Estilo Rapido + + + Classico + + + Moderno + + + Colorido + + + Digite o conteudo do seu QR code aqui... + + + Personalizacao Avancada + + + Cor Principal + + + Cor de Fundo + + + Tamanho + + + Margem + + + Logo/Icone + + + Estilo das Bordas + + + Gerar QR Code Rapidamente + + + Preview + + + Seu QR code aparecera aqui em segundos + + + Geracao ultra-rapida garantida + + + Download PNG + + + Download SVG (Vetorial) + + + Download PDF + + + Salvar no Historico + + + Faca login para salvar no historico + + + QR Rapido Premium + + + Dicas para QR Mais Rapidos + + + URLs curtas geram mais rapido + + + Menos texto = maior velocidade + + + Cores solidas otimizam o processo + + + Tamanhos menores aceleram o download + + + Login + + + Entrar com + + + Google + + + Microsoft + + + Desbloqueie todos os recursos premium! + + + Oferta Premium! + + + Remova anuncios, acesse analytics detalhados e muito mais por apenas R$ 12,90/mes ou R$ 129,00/ano. Faca login e assine agora! + + + Politica de Privacidade + + + Voltar ao gerador + + + Gerado em + + + s + + + Geracao ultra rapida! + + + Geracao rapida! + + + Geracao normal + + + Erro na geracao. Tente novamente. + + + QR Code salvo no historico! + + + Criar QR Code Rapidamente + + + Usuario Premium Ativo + + + Sem Anuncios • Historico • QR Ilimitado + + + Ilimitado hoje + + + QR codes restantes + + + Tipo de QR Code + + + Selecionar tipo + + + URL/Link + + + Texto Simples + + + Cartao de Visita + + + QR Dinamico (Premium) + + + Digite o conteudo do seu QR code aqui... + + + Dicas de conteudo + + + Classico + + + Moderno + + + Colorido + + \ No newline at end of file diff --git a/Resources/SharedResource.resx b/Resources/SharedResource.resx new file mode 100644 index 0000000..d5fd987 --- /dev/null +++ b/Resources/SharedResource.resx @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Generate QR codes in seconds! + + + Generate QR Code + + + QR Code Type + + + Content + + + URL/Link + + + Plain Text + + + WiFi + + + Business Card + + + SMS + + + Email + + + Dynamic QR (Premium) + + + Quick Style + + + Classic + + + Modern + + + Colorful + + + Enter your QR code content here... + + + Advanced Customization + + + Primary Color + + + Background Color + + + Size + + + Margin + + + Logo/Icon + + + Corner Style + + + Generate QR Code Rapidly + + + Preview + + + Your QR code will appear here in seconds + + + Ultra-fast generation guaranteed + + + Download PNG + + + Download SVG (Vector) + + + Download PDF + + + Save to History + + + Login to save to history + + + QR Rapido Premium + + + Tips for Faster QR + + + Short URLs generate faster + + + Less text = higher speed + + + Solid colors optimize the process + + + Smaller sizes speed up downloads + + + Login + + + Login with + + + Google + + + Microsoft + + + Unlock all premium features! + + + Premium Offer! + + + Remove ads, access detailed analytics and much more for only $12.90/month or $129.00/year. Login and subscribe now! + + + Privacy Policy + + + Back to generator + + + Generated in + + + s + + + Ultra fast generation! + + + Fast generation! + + + Normal generation + + + Generation error. Try again. + + + QR Code saved to history! + + + Create QR Code Quickly + + + Premium User Active + + + No Ads • History • Unlimited QR + + + Unlimited today + + + QR codes remaining + + + QR Code Type + + + Select type + + + URL/Link + + + Simple Text + + + Business Card + + + Dynamic QR (Premium) + + + Enter your QR code content here... + + + Content hints + + + Classic + + + Modern + + + Colorful + + \ No newline at end of file diff --git a/Services/IPlanService.cs b/Services/IPlanService.cs new file mode 100644 index 0000000..b265599 --- /dev/null +++ b/Services/IPlanService.cs @@ -0,0 +1,14 @@ + +using QRRapidoApp.Models; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace QRRapidoApp.Services +{ + public interface IPlanService + { + Task> GetActivePlansAsync(); + Task> GetPlansByLanguageAsync(string languageCode, string countryCode = "BRL"); + Task GetPlanByIdAsync(string id); + } +} diff --git a/Services/IUserService.cs b/Services/IUserService.cs index 821a147..c553a69 100644 --- a/Services/IUserService.cs +++ b/Services/IUserService.cs @@ -10,6 +10,10 @@ namespace QRRapidoApp.Services Task GetUserByProviderAsync(string provider, string providerId); Task CreateUserAsync(string email, string name, string provider, string providerId); Task UpdateLastLoginAsync(string userId); + Task ActivatePremiumStatus(string userId, string stripeSubscriptionId, DateTime expiryDate); + Task DeactivatePremiumStatus(string stripeSubscriptionId); + Task UpdateUserStripeCustomerIdAsync(string userId, string stripeCustomerId); + Task GetUserByStripeCustomerIdAsync(string customerId); Task UpdateUserAsync(User user); Task GetDailyQRCountAsync(string? userId); Task DecrementDailyQRCountAsync(string userId); diff --git a/Services/PlanService.cs b/Services/PlanService.cs new file mode 100644 index 0000000..6c4ebc8 --- /dev/null +++ b/Services/PlanService.cs @@ -0,0 +1,35 @@ + +using MongoDB.Driver; +using QRRapidoApp.Data; +using QRRapidoApp.Models; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace QRRapidoApp.Services +{ + public class PlanService : IPlanService + { + private readonly IMongoCollection _plans; + + public PlanService(MongoDbContext context) + { + _plans = context.Plans; + } + + public async Task> GetActivePlansAsync() + { + return await _plans.Find(p => p.IsActive).SortBy(p => p.PricesByCountry["BRL"].Amount).ToListAsync(); + } + + public async Task> GetPlansByLanguageAsync(string languageCode, string countryCode = "BRL") + { + var plans = await _plans.Find(p => p.IsActive).SortBy(p => p.PricesByCountry[countryCode].Amount).ToListAsync(); + return plans; + } + + public async Task GetPlanByIdAsync(string id) + { + return await _plans.Find(p => p.Id == id).FirstOrDefaultAsync(); + } + } +} diff --git a/Services/RouteDataRequestCultureProvider.cs b/Services/RouteDataRequestCultureProvider.cs new file mode 100644 index 0000000..628d75f --- /dev/null +++ b/Services/RouteDataRequestCultureProvider.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Localization; + +namespace QRRapidoApp.Services +{ + public class RouteDataRequestCultureProvider : RequestCultureProvider + { + public override Task DetermineProviderCultureResult(HttpContext httpContext) + { + if (httpContext?.Request?.RouteValues == null) + return Task.FromResult(null); + + var routeValues = httpContext.Request.RouteValues; + if (!routeValues.ContainsKey("culture")) + return Task.FromResult(null); + + var culture = routeValues["culture"]?.ToString(); + if (string.IsNullOrEmpty(culture)) + return Task.FromResult(null); + + var supportedCultures = new[] { "pt-BR", "es", "en" }; + if (!supportedCultures.Contains(culture)) + return Task.FromResult(null); + + return Task.FromResult(new ProviderCultureResult(culture)); + } + } +} \ No newline at end of file diff --git a/Services/StripeService.cs b/Services/StripeService.cs index 34fc6a8..2a86d61 100644 --- a/Services/StripeService.cs +++ b/Services/StripeService.cs @@ -1,6 +1,12 @@ + using Stripe; using Stripe.Checkout; using QRRapidoApp.Models; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System; namespace QRRapidoApp.Services { @@ -15,253 +21,128 @@ namespace QRRapidoApp.Services _config = config; _userService = userService; _logger = logger; + StripeConfiguration.ApiKey = _config["Stripe:SecretKey"]; } public async Task CreateCheckoutSessionAsync(string userId, string priceId) { - try + var user = await _userService.GetUserAsync(userId); + if (user == null) { - var options = new SessionCreateOptions - { - PaymentMethodTypes = new List { "card" }, - Mode = "subscription", - LineItems = new List - { - new SessionLineItemOptions - { - Price = priceId, - Quantity = 1 - } - }, - ClientReferenceId = userId, - SuccessUrl = $"{_config["App:BaseUrl"]}/Premium/Success?session_id={{CHECKOUT_SESSION_ID}}", - CancelUrl = $"{_config["App:BaseUrl"]}/Premium/Cancel", - CustomerEmail = await _userService.GetUserEmailAsync(userId), - AllowPromotionCodes = true, - BillingAddressCollection = "auto", - Metadata = new Dictionary - { - { "userId", userId }, - { "product", "QR Rapido Premium" } - } - }; + throw new Exception("User not found"); + } - var service = new SessionService(); - var session = await service.CreateAsync(options); - - _logger.LogInformation($"Created Stripe checkout session for user {userId}: {session.Id}"); - - return session.Url; - } - catch (Exception ex) + var customerId = user.StripeCustomerId; + if (string.IsNullOrEmpty(customerId)) { - _logger.LogError(ex, $"Error creating Stripe checkout session for user {userId}: {ex.Message}"); - throw; + var customerOptions = new CustomerCreateOptions + { + Email = user.Email, + Name = user.Name, + Metadata = new Dictionary { { "app_user_id", user.Id } } + }; + var customerService = new CustomerService(); + var customer = await customerService.CreateAsync(customerOptions); + customerId = customer.Id; + await _userService.UpdateUserStripeCustomerIdAsync(userId, customerId); } + + var options = new SessionCreateOptions + { + PaymentMethodTypes = new List { "card" }, + Mode = "subscription", + LineItems = new List + { + new SessionLineItemOptions { Price = priceId, Quantity = 1 } + }, + Customer = customerId, + ClientReferenceId = userId, + SuccessUrl = $"{_config["App:BaseUrl"]}/Pagamento/Sucesso", + CancelUrl = $"{_config["App:BaseUrl"]}/Pagamento/Cancelar", + AllowPromotionCodes = true, + Metadata = new Dictionary { { "user_id", userId } } + }; + + var service = new SessionService(); + var session = await service.CreateAsync(options); + _logger.LogInformation($"Created Stripe checkout session {session.Id} for user {userId}"); + return session.Url; } public async Task HandleWebhookAsync(string json, string signature) { var webhookSecret = _config["Stripe:WebhookSecret"]; + var stripeEvent = EventUtility.ConstructEvent(json, signature, webhookSecret); + + _logger.LogInformation($"Processing Stripe webhook: {stripeEvent.Type}"); + + switch (stripeEvent.Type) + { + case Events.CheckoutSessionCompleted: + var session = stripeEvent.Data.Object as Session; + if (session?.SubscriptionId != null) + { + var subscriptionService = new SubscriptionService(); + var subscription = await subscriptionService.GetAsync(session.SubscriptionId); + await ProcessSubscriptionActivation(session.ClientReferenceId, subscription); + } + break; + + case Events.InvoicePaymentSucceeded: + var invoice = stripeEvent.Data.Object as Invoice; + if (invoice?.SubscriptionId != null) + { + var subscriptionService = new SubscriptionService(); + var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId); + var user = await _userService.GetUserByStripeCustomerIdAsync(subscription.CustomerId); + if (user != null) + { + await ProcessSubscriptionActivation(user.Id, subscription); + } + } + break; + + case Events.CustomerSubscriptionDeleted: + var deletedSubscription = stripeEvent.Data.Object as Subscription; + if (deletedSubscription != null) + { + await _userService.DeactivatePremiumStatus(deletedSubscription.Id); + } + break; + + default: + _logger.LogWarning($"Unhandled Stripe webhook event type: {stripeEvent.Type}"); + break; + } + } + + private async Task ProcessSubscriptionActivation(string userId, Subscription subscription) + { + if (string.IsNullOrEmpty(userId) || subscription == null) + { + _logger.LogWarning("Could not process subscription activation due to missing userId or subscription data."); + return; + } - try + var user = await _userService.GetUserAsync(userId); + if (user == null) { - var stripeEvent = EventUtility.ConstructEvent(json, signature, webhookSecret); - - _logger.LogInformation($"Processing Stripe webhook: {stripeEvent.Type}"); - - switch (stripeEvent.Type) - { - case "checkout.session.completed": - var session = stripeEvent.Data.Object as Session; - if (session != null) - { - await ActivatePremiumAsync(session.ClientReferenceId, session.CustomerId, session.SubscriptionId); - } - break; - - case "invoice.payment_succeeded": - var invoice = stripeEvent.Data.Object as Invoice; - if (invoice != null && invoice.SubscriptionId != null) - { - await RenewPremiumSubscriptionAsync(invoice.SubscriptionId); - } - break; - - case "invoice.payment_failed": - var failedInvoice = stripeEvent.Data.Object as Invoice; - if (failedInvoice != null && failedInvoice.SubscriptionId != null) - { - await HandleFailedPaymentAsync(failedInvoice.SubscriptionId); - } - break; - - case "customer.subscription.deleted": - var deletedSubscription = stripeEvent.Data.Object as Subscription; - if (deletedSubscription != null) - { - await DeactivatePremiumAsync(deletedSubscription); - } - break; - - case "customer.subscription.updated": - var updatedSubscription = stripeEvent.Data.Object as Subscription; - if (updatedSubscription != null) - { - await UpdateSubscriptionAsync(updatedSubscription); - } - break; - - default: - _logger.LogWarning($"Unhandled Stripe webhook event type: {stripeEvent.Type}"); - break; - } + _logger.LogWarning($"User not found for premium activation: {userId}"); + return; } - catch (StripeException ex) + + if (string.IsNullOrEmpty(user.StripeCustomerId)) { - _logger.LogError(ex, $"Stripe webhook error: {ex.Message}"); - throw; + await _userService.UpdateUserStripeCustomerIdAsync(user.Id, subscription.CustomerId); } - catch (Exception ex) - { - _logger.LogError(ex, $"Error processing Stripe webhook: {ex.Message}"); - throw; - } - } - private async Task ActivatePremiumAsync(string? userId, string? customerId, string? subscriptionId) - { - if (string.IsNullOrEmpty(userId)) return; - - try - { - var user = await _userService.GetUserAsync(userId); - if (user == null) - { - _logger.LogWarning($"User not found for premium activation: {userId}"); - return; - } - - user.IsPremium = true; - user.StripeCustomerId = customerId; - user.StripeSubscriptionId = subscriptionId; - user.PremiumExpiresAt = DateTime.UtcNow.AddDays(32); // Buffer for billing cycles - - await _userService.UpdateUserAsync(user); - - _logger.LogInformation($"Activated premium for user {userId}"); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error activating premium for user {userId}: {ex.Message}"); - } - } - - private async Task RenewPremiumSubscriptionAsync(string subscriptionId) - { - try - { - // Find user by subscription ID - var user = await FindUserBySubscriptionIdAsync(subscriptionId); - if (user == null) return; - - // Extend premium expiry - user.PremiumExpiresAt = DateTime.UtcNow.AddDays(32); - await _userService.UpdateUserAsync(user); - - _logger.LogInformation($"Renewed premium subscription for user {user.Id}"); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error renewing premium subscription {subscriptionId}: {ex.Message}"); - } - } - - private async Task HandleFailedPaymentAsync(string subscriptionId) - { - try - { - var user = await FindUserBySubscriptionIdAsync(subscriptionId); - if (user == null) return; - - // Don't immediately deactivate - Stripe will retry - _logger.LogWarning($"Payment failed for user {user.Id}, subscription {subscriptionId}"); - - // Could send notification email here - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error handling failed payment for subscription {subscriptionId}: {ex.Message}"); - } - } - - private async Task DeactivatePremiumAsync(Subscription subscription) - { - try - { - var user = await FindUserBySubscriptionIdAsync(subscription.Id); - if (user == null) return; - - // ADICIONAR: marcar data de cancelamento - await _userService.MarkPremiumCancelledAsync(user.Id, DateTime.UtcNow); - - _logger.LogInformation($"Deactivated premium for user {user.Id}"); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error deactivating premium for subscription {subscription.Id}: {ex.Message}"); - } - } - - private async Task UpdateSubscriptionAsync(Subscription subscription) - { - try - { - var user = await FindUserBySubscriptionIdAsync(subscription.Id); - if (user == null) return; - - // Update based on subscription status - if (subscription.Status == "active") - { - user.IsPremium = true; - user.PremiumExpiresAt = subscription.CurrentPeriodEnd.AddDays(2); // Small buffer - } - else if (subscription.Status == "canceled" || subscription.Status == "unpaid") - { - user.IsPremium = false; - user.PremiumExpiresAt = DateTime.UtcNow; - } - - await _userService.UpdateUserAsync(user); - - _logger.LogInformation($"Updated subscription for user {user.Id}: {subscription.Status}"); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error updating subscription {subscription.Id}: {ex.Message}"); - } - } - - private async Task FindUserBySubscriptionIdAsync(string subscriptionId) - { - try - { - // This would require implementing a method in UserService to find by subscription ID - // For now, we'll leave this as a placeholder - return null; - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error finding user by subscription ID {subscriptionId}: {ex.Message}"); - return null; - } + await _userService.ActivatePremiumStatus(userId, subscription.Id, subscription.CurrentPeriodEnd); + _logger.LogInformation($"Successfully processed premium activation/renewal for user {userId}."); } public async Task GetSubscriptionStatusAsync(string? subscriptionId) { - if (string.IsNullOrEmpty(subscriptionId)) - return "None"; - + if (string.IsNullOrEmpty(subscriptionId)) return "None"; try { var service = new SubscriptionService(); @@ -270,7 +151,7 @@ namespace QRRapidoApp.Services } catch (Exception ex) { - _logger.LogError(ex, $"Error getting subscription status for {subscriptionId}: {ex.Message}"); + _logger.LogError(ex, $"Error getting subscription status for {subscriptionId}"); return "Unknown"; } } @@ -280,20 +161,15 @@ namespace QRRapidoApp.Services try { var service = new SubscriptionService(); - var subscription = await service.CancelAsync(subscriptionId, new SubscriptionCancelOptions - { - InvoiceNow = false, - Prorate = false - }); - - _logger.LogInformation($"Canceled subscription {subscriptionId}"); + await service.CancelAsync(subscriptionId, new SubscriptionCancelOptions()); + _logger.LogInformation($"Canceled subscription {subscriptionId} via API."); return true; } catch (Exception ex) { - _logger.LogError(ex, $"Error canceling subscription {subscriptionId}: {ex.Message}"); + _logger.LogError(ex, $"Error canceling subscription {subscriptionId}"); return false; } } } -} \ No newline at end of file +} diff --git a/Services/UserService.cs b/Services/UserService.cs index e0ade97..aa62c1c 100644 --- a/Services/UserService.cs +++ b/Services/UserService.cs @@ -83,9 +83,6 @@ namespace QRRapidoApp.Services await _context.Users.InsertOneAsync(user); _logger.LogInformation($"Created new user: {email} via {provider}"); - // Create initial ad-free session for new users - await CreateAdFreeSessionAsync(user.Id, "Login"); - return user; } @@ -97,9 +94,6 @@ namespace QRRapidoApp.Services .Set(u => u.LastLoginAt, DateTime.UtcNow); await _context.Users.UpdateOneAsync(u => u.Id == userId, update); - - // Create new ad-free session if needed - await CreateAdFreeSessionAsync(userId, "Login"); } catch (Exception ex) { @@ -352,31 +346,37 @@ namespace QRRapidoApp.Services } } - private async Task CreateAdFreeSessionAsync(string userId, string sessionType, int? customMinutes = null) + public async Task ActivatePremiumStatus(string userId, string stripeSubscriptionId, DateTime expiryDate) { - try - { - var durationMinutes = customMinutes ?? _config.GetValue("AdFree:LoginMinutes", 43200); - - var session = new AdFreeSession - { - UserId = userId, - StartedAt = DateTime.UtcNow, - ExpiresAt = DateTime.UtcNow.AddMinutes(durationMinutes), - IsActive = true, - SessionType = sessionType, - DurationMinutes = durationMinutes, - CreatedAt = DateTime.UtcNow - }; + var update = Builders.Update + .Set(u => u.IsPremium, true) + .Set(u => u.StripeSubscriptionId, stripeSubscriptionId) + .Set(u => u.PremiumExpiresAt, expiryDate) + .Unset(u => u.PremiumCancelledAt); - await _context.AdFreeSessions.InsertOneAsync(session); - - _logger.LogInformation($"Created {sessionType} ad-free session for user {userId} - {durationMinutes} minutes"); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error creating ad-free session for user {userId}: {ex.Message}"); - } + await _context.Users.UpdateOneAsync(u => u.Id == userId, update); + _logger.LogInformation($"Activated premium for user {userId}"); + } + + public async Task DeactivatePremiumStatus(string stripeSubscriptionId) + { + var update = Builders.Update + .Set(u => u.IsPremium, false) + .Set(u => u.PremiumCancelledAt, DateTime.UtcNow); + + await _context.Users.UpdateOneAsync(u => u.StripeSubscriptionId == stripeSubscriptionId, update); + _logger.LogInformation($"Deactivated premium for subscription {stripeSubscriptionId}"); + } + + public async Task GetUserByStripeCustomerIdAsync(string customerId) + { + return await _context.Users.Find(u => u.StripeCustomerId == customerId).FirstOrDefaultAsync(); + } + + public async Task UpdateUserStripeCustomerIdAsync(string userId, string stripeCustomerId) + { + var update = Builders.Update.Set(u => u.StripeCustomerId, stripeCustomerId); + await _context.Users.UpdateOneAsync(u => u.Id == userId, update); } } } \ No newline at end of file diff --git a/Views/Account/History.cshtml b/Views/Account/History.cshtml new file mode 100644 index 0000000..163eee7 --- /dev/null +++ b/Views/Account/History.cshtml @@ -0,0 +1,158 @@ +@model List +@using Microsoft.Extensions.Localization +@inject IStringLocalizer Localizer + +@{ + ViewData["Title"] = "Histórico de QR Codes"; + Layout = "~/Views/Shared/_Layout.cshtml"; + var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; +} + +
+
+
+
+
+

Histórico de QR Codes

+

Seus QR codes gerados ficam salvos aqui para download futuro

+
+ +
+ + @if (Model != null && Model.Any()) + { +
+ @foreach (var qr in Model) + { +
+
+
+
+ QR Code +
+ +
+ Tipo: + @qr.Type +
+ +
+ Conteúdo: +

+ @if (qr.Content.Length > 50) + { + @(qr.Content.Substring(0, 50) + "...") + } + else + { + @qr.Content + } +

+
+ +
+ Criado em: +
+ @qr.CreatedAt.ToString("dd/MM/yyyy HH:mm") +
+
+ + +
+
+ } +
+ + @if (Model.Count == 50) + { +
+ + Mostrando os 50 QR codes mais recentes. Os mais antigos são removidos automaticamente. +
+ } + } + else + { +
+ +

Nenhum QR Code encontrado

+

+ Quando você gerar QR codes estando logado, eles aparecerão aqui para download futuro. +

+ + Gerar Primeiro QRCode + +
+ } +
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/Views/Home/Index.cshtml b/Views/Home/Index.cshtml index 41ee696..1d84930 100644 --- a/Views/Home/Index.cshtml +++ b/Views/Home/Index.cshtml @@ -1,11 +1,15 @@ @using QRRapidoApp.Services +@using Microsoft.Extensions.Localization @inject AdDisplayService AdService +@inject IStringLocalizer Localizer + @{ ViewData["Title"] = "Home"; var userId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; Layout = "~/Views/Shared/_Layout.cshtml"; } +
@@ -13,7 +17,7 @@

- Criar QR Code Rapidamente + @Localizer["CreateQRCodeQuickly"]

@@ -25,8 +29,8 @@ {
- Usuário Premium ativo! - Sem anúncios + Histórico + QR ilimitados + @Localizer["PremiumUserActive"] + @Localizer["NoAdsHistoryUnlimitedQR"]
} } @@ -42,7 +46,7 @@
- Geração ultra rápida! + @Localizer["UltraFastGeneration"]
@@ -51,13 +55,13 @@ @if (User.Identity.IsAuthenticated) { - Ilimitado hoje + @Localizer["UnlimitedToday"] } else { - 10 QR codes restantes + @Localizer["QRCodesRemaining"] }
@@ -66,50 +70,50 @@
- + - + - +
- Dicas aparecerão aqui baseadas no tipo selecionado + @Localizer["ContentHints"]
@@ -118,22 +122,22 @@

- +
- +
- +