first commit
This commit is contained in:
parent
da57bb8dd8
commit
2ccd35bb7d
10
.claude/settings.local.json
Normal file
10
.claude/settings.local.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(dotnet new:*)",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"Bash(dotnet build:*)"
|
||||||
|
],
|
||||||
|
"deny": []
|
||||||
|
}
|
||||||
|
}
|
||||||
121
.github/workflows/deploy.yml
vendored
Normal file
121
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
name: Deploy QR Rapido
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: 8.0.x
|
||||||
|
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.nuget/packages
|
||||||
|
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-nuget-
|
||||||
|
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: dotnet restore
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: dotnet build --no-restore --configuration Release
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: dotnet test --no-build --configuration Release --verbosity normal --collect:"XPlat Code Coverage"
|
||||||
|
|
||||||
|
- name: Upload coverage
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
files: coverage.cobertura.xml
|
||||||
|
|
||||||
|
build-and-push:
|
||||||
|
needs: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Login to Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=sha,prefix={{branch}}-
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
deploy-staging:
|
||||||
|
needs: build-and-push
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.ref == 'refs/heads/develop'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Deploy to Staging
|
||||||
|
uses: azure/webapps-deploy@v2
|
||||||
|
with:
|
||||||
|
app-name: 'qrrapido-staging'
|
||||||
|
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE_STAGING }}
|
||||||
|
images: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop'
|
||||||
|
|
||||||
|
deploy-production:
|
||||||
|
needs: build-and-push
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
environment: production
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Deploy to Production
|
||||||
|
uses: azure/webapps-deploy@v2
|
||||||
|
with:
|
||||||
|
app-name: 'qrrapido-production'
|
||||||
|
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE_PROD }}
|
||||||
|
images: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest'
|
||||||
|
|
||||||
|
- name: Notify Slack
|
||||||
|
uses: 8398a7/action-slack@v3
|
||||||
|
with:
|
||||||
|
status: ${{ job.status }}
|
||||||
|
channel: '#deployments'
|
||||||
|
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
|
||||||
|
text: "QR Rapido deployed to production! 🚀"
|
||||||
382
.gitignore
vendored
Normal file
382
.gitignore
vendored
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
##
|
||||||
|
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.rsuser
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
# Mono auto generated files
|
||||||
|
mono_crash.*
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
[Ww][Ii][Nn]32/
|
||||||
|
[Aa][Rr][Mm]/
|
||||||
|
[Aa][Rr][Mm]64/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Oo]ut/
|
||||||
|
[Ll]og/
|
||||||
|
[Ll]ogs/
|
||||||
|
|
||||||
|
# Visual Studio 2015/2017 cache/options directory
|
||||||
|
.vs/
|
||||||
|
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||||
|
#wwwroot/
|
||||||
|
|
||||||
|
# Visual Studio 2017 auto generated files
|
||||||
|
Generated\ Files/
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
# NUnit
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
nunit-*.xml
|
||||||
|
|
||||||
|
# Build Results of an ATL Project
|
||||||
|
[Dd]ebugPS/
|
||||||
|
[Rr]eleasePS/
|
||||||
|
dlldata.c
|
||||||
|
|
||||||
|
# Benchmark Results
|
||||||
|
BenchmarkDotNet.Artifacts/
|
||||||
|
|
||||||
|
# .NET Core
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
# ASP.NET Scaffolding
|
||||||
|
ScaffoldingReadMe.txt
|
||||||
|
|
||||||
|
# StyleCop
|
||||||
|
StyleCopReport.xml
|
||||||
|
|
||||||
|
# Files built by Visual Studio
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_h.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.iobj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.ipdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*_wpftmp.csproj
|
||||||
|
*.log
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
|
||||||
|
# Chutzpah Test files
|
||||||
|
_Chutzpah*
|
||||||
|
|
||||||
|
# Visual C++ cache files
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opendb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
*.VC.db
|
||||||
|
*.VC.VC.opendb
|
||||||
|
|
||||||
|
# Visual Studio profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
*.sap
|
||||||
|
|
||||||
|
# Visual Studio Trace Files
|
||||||
|
*.e2e
|
||||||
|
|
||||||
|
# TFS 2012 Local Workspace
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# ReSharper is a .NET coding add-in
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# TeamCity is a build add-in
|
||||||
|
_TeamCity*
|
||||||
|
|
||||||
|
# DotCover is a Code Coverage Tool
|
||||||
|
*.dotCover
|
||||||
|
|
||||||
|
# AxoCover is a Code Coverage Tool
|
||||||
|
.axoCover/*
|
||||||
|
!.axoCover/settings.json
|
||||||
|
|
||||||
|
# Coverlet is a free, cross platform Code Coverage Tool
|
||||||
|
coverage*.json
|
||||||
|
coverage*.xml
|
||||||
|
coverage*.info
|
||||||
|
|
||||||
|
# Visual Studio code coverage results
|
||||||
|
*.coverage
|
||||||
|
*.coveragexml
|
||||||
|
|
||||||
|
# NCrunch
|
||||||
|
_NCrunch_*
|
||||||
|
.*crunch*.local.xml
|
||||||
|
nCrunchTemp_*
|
||||||
|
|
||||||
|
# MightyMoose
|
||||||
|
*.mm.*
|
||||||
|
AutoTest.Net/
|
||||||
|
|
||||||
|
# Web workbench (sass)
|
||||||
|
.sass-cache/
|
||||||
|
|
||||||
|
# Installshield output folder
|
||||||
|
[Ee]xpress/
|
||||||
|
|
||||||
|
# DocProject is a documentation generator add-in
|
||||||
|
DocProject/buildhelp/
|
||||||
|
DocProject/Help/*.HxT
|
||||||
|
DocProject/Help/*.HxC
|
||||||
|
DocProject/Help/*.hhc
|
||||||
|
DocProject/Help/*.hhk
|
||||||
|
DocProject/Help/*.hhp
|
||||||
|
DocProject/Help/*.HxS
|
||||||
|
DocProject/Help/Html2
|
||||||
|
DocProject/Help/html
|
||||||
|
|
||||||
|
# Click-Once directory
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Publish Web Output
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
*.azurePubxml
|
||||||
|
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||||
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
|
*.pubxml
|
||||||
|
*.publishproj
|
||||||
|
|
||||||
|
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||||
|
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||||
|
# in these files may be disclosed. Comment the next line if you want to checkin
|
||||||
|
# your web deploy settings, but sensitive information contained in these files may
|
||||||
|
# be disclosed. Comment the next line if you want to checkin your web deploy settings,
|
||||||
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
|
*.azurePubxml
|
||||||
|
|
||||||
|
# Microsoft Azure Build Output
|
||||||
|
csx/
|
||||||
|
*.build.csdef
|
||||||
|
|
||||||
|
# Microsoft Azure Emulator
|
||||||
|
ecf/
|
||||||
|
rcf/
|
||||||
|
|
||||||
|
# Windows Store app package directories and files
|
||||||
|
AppPackages/
|
||||||
|
BundleArtifacts/
|
||||||
|
Package.StoreAssociation.xml
|
||||||
|
_pkginfo.txt
|
||||||
|
*.appx
|
||||||
|
*.appxbundle
|
||||||
|
*.appxupload
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!?*.[Cc]ache/
|
||||||
|
|
||||||
|
# Others
|
||||||
|
ClientBin/
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.dbmdl
|
||||||
|
*.dbproj.schemaview
|
||||||
|
*.jfm
|
||||||
|
*.pfx
|
||||||
|
*.publishsettings
|
||||||
|
orleans.codegen.cs
|
||||||
|
|
||||||
|
# Including strong name files can present a security risk
|
||||||
|
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||||
|
#*.snk
|
||||||
|
|
||||||
|
# Since there are multiple workflows, uncomment the next line to ignore bower_components
|
||||||
|
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||||
|
#bower_components/
|
||||||
|
|
||||||
|
# RIA/Silverlight projects
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# Backup & report files from converting an old project file
|
||||||
|
# to a newer Visual Studio version. Backup files are not needed,
|
||||||
|
# because we have git ;-)
|
||||||
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
CConvertLog*.txt
|
||||||
|
|
||||||
|
# SQL Server files
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
*.ndf
|
||||||
|
|
||||||
|
# Business Intelligence projects
|
||||||
|
*.rdl.data
|
||||||
|
*.bim.layout
|
||||||
|
*.bim_*.settings
|
||||||
|
*.rptproj.rsuser
|
||||||
|
*- [Bb]ackup.rdl
|
||||||
|
*- [Bb]ackup ([0-9]).rdl
|
||||||
|
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||||
|
|
||||||
|
# Microsoft Fakes
|
||||||
|
FakesAssemblies/
|
||||||
|
|
||||||
|
# GhostDoc plugin setting file
|
||||||
|
*.GhostDoc.xml
|
||||||
|
|
||||||
|
# Node.js Tools for Visual Studio
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Visual Studio 6 build log
|
||||||
|
*.plg
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace options file
|
||||||
|
*.opt
|
||||||
|
|
||||||
|
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||||
|
*.vbw
|
||||||
|
|
||||||
|
# Visual Studio LightSwitch build output
|
||||||
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
|
**/*.Server/GeneratedArtifacts
|
||||||
|
**/*.Server/ModelManifest.xml
|
||||||
|
_Pvt_Extensions
|
||||||
|
|
||||||
|
# Paket dependency manager
|
||||||
|
.paket/paket.exe
|
||||||
|
paket-files/
|
||||||
|
|
||||||
|
# FAKE - F# Make
|
||||||
|
.fake/
|
||||||
|
|
||||||
|
# CodeRush personal settings
|
||||||
|
.cr/personal
|
||||||
|
|
||||||
|
# Python Tools for Visual Studio (PTVS)
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Cake - Uncomment if you are using it
|
||||||
|
# tools/**
|
||||||
|
# !tools/packages.config
|
||||||
|
|
||||||
|
# Tabs Studio
|
||||||
|
*.tss
|
||||||
|
|
||||||
|
# Telerik's JustMock configuration file
|
||||||
|
*.jmconfig
|
||||||
|
|
||||||
|
# BizTalk build output
|
||||||
|
*.btp.cs
|
||||||
|
*.btm.cs
|
||||||
|
*.odx.cs
|
||||||
|
*.xsd.cs
|
||||||
|
|
||||||
|
# OpenCover UI analysis results
|
||||||
|
OpenCover/
|
||||||
|
|
||||||
|
# Azure Stream Analytics local run output
|
||||||
|
ASALocalRun/
|
||||||
|
|
||||||
|
# MSBuild Binary and Structured Log
|
||||||
|
*.binlog
|
||||||
|
|
||||||
|
# NVidia Nsight GPU debugger configuration file
|
||||||
|
*.nvuser
|
||||||
|
|
||||||
|
# MFractors (Xamarin productivity tool) working folder
|
||||||
|
.mfractor/
|
||||||
|
|
||||||
|
# Local History for Visual Studio
|
||||||
|
.localhistory/
|
||||||
|
|
||||||
|
# BeatPulse healthcheck temp database
|
||||||
|
healthchecksdb
|
||||||
|
|
||||||
|
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||||
|
MigrationBackup/
|
||||||
|
|
||||||
|
# Ionide (cross platform F# VS Code tools) working folder
|
||||||
|
.ionide/
|
||||||
|
|
||||||
|
# Fody - auto-generated XML schema
|
||||||
|
FodyWeavers.xsd
|
||||||
|
|
||||||
|
# Custom
|
||||||
|
appsettings.Local.json
|
||||||
|
appsettings.Production.json
|
||||||
|
*.Development.json
|
||||||
|
logs/
|
||||||
|
uploads/
|
||||||
|
temp/
|
||||||
|
*.env
|
||||||
|
.env.local
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Certificates
|
||||||
|
*.pfx
|
||||||
|
*.crt
|
||||||
|
*.key
|
||||||
242
Controllers/AccountController.cs
Normal file
242
Controllers/AccountController.cs
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Google;
|
||||||
|
using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using QRRapidoApp.Models.ViewModels;
|
||||||
|
using QRRapidoApp.Services;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Controllers
|
||||||
|
{
|
||||||
|
public class AccountController : Controller
|
||||||
|
{
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly AdDisplayService _adDisplayService;
|
||||||
|
private readonly ILogger<AccountController> _logger;
|
||||||
|
|
||||||
|
public AccountController(IUserService userService, AdDisplayService adDisplayService, ILogger<AccountController> logger)
|
||||||
|
{
|
||||||
|
_userService = userService;
|
||||||
|
_adDisplayService = adDisplayService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult Login(string returnUrl = "/")
|
||||||
|
{
|
||||||
|
ViewBag.ReturnUrl = returnUrl;
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult LoginGoogle(string returnUrl = "/")
|
||||||
|
{
|
||||||
|
var properties = new AuthenticationProperties
|
||||||
|
{
|
||||||
|
RedirectUri = Url.Action("GoogleCallback"),
|
||||||
|
Items = { { "returnUrl", returnUrl } }
|
||||||
|
};
|
||||||
|
return Challenge(properties, GoogleDefaults.AuthenticationScheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult LoginMicrosoft(string returnUrl = "/")
|
||||||
|
{
|
||||||
|
var properties = new AuthenticationProperties
|
||||||
|
{
|
||||||
|
RedirectUri = Url.Action("MicrosoftCallback"),
|
||||||
|
Items = { { "returnUrl", returnUrl } }
|
||||||
|
};
|
||||||
|
return Challenge(properties, MicrosoftAccountDefaults.AuthenticationScheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GoogleCallback()
|
||||||
|
{
|
||||||
|
return await HandleExternalLoginCallbackAsync(GoogleDefaults.AuthenticationScheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> MicrosoftCallback()
|
||||||
|
{
|
||||||
|
return await HandleExternalLoginCallbackAsync(MicrosoftAccountDefaults.AuthenticationScheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IActionResult> HandleExternalLoginCallbackAsync(string scheme)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await HttpContext.AuthenticateAsync(scheme);
|
||||||
|
if (!result.Succeeded)
|
||||||
|
{
|
||||||
|
_logger.LogWarning($"External authentication failed for scheme {scheme}");
|
||||||
|
return RedirectToAction("Login");
|
||||||
|
}
|
||||||
|
|
||||||
|
var email = result.Principal?.FindFirst(ClaimTypes.Email)?.Value;
|
||||||
|
var name = result.Principal?.FindFirst(ClaimTypes.Name)?.Value;
|
||||||
|
var providerId = result.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(providerId))
|
||||||
|
{
|
||||||
|
_logger.LogWarning($"Missing required claims from {scheme} authentication");
|
||||||
|
return RedirectToAction("Login");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or create user
|
||||||
|
var user = await _userService.GetUserByProviderAsync(scheme, providerId);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
user = await _userService.CreateUserAsync(email, name ?? email, scheme, providerId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _userService.UpdateLastLoginAsync(user.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create application claims
|
||||||
|
var claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, user.Id),
|
||||||
|
new Claim(ClaimTypes.Email, user.Email),
|
||||||
|
new Claim(ClaimTypes.Name, user.Name),
|
||||||
|
new Claim("Provider", user.Provider),
|
||||||
|
new Claim("IsPremium", user.IsPremium.ToString())
|
||||||
|
};
|
||||||
|
|
||||||
|
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
var authProperties = new AuthenticationProperties
|
||||||
|
{
|
||||||
|
IsPersistent = true,
|
||||||
|
ExpiresUtc = DateTimeOffset.UtcNow.AddDays(30)
|
||||||
|
};
|
||||||
|
|
||||||
|
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
|
||||||
|
new ClaimsPrincipal(claimsIdentity), authProperties);
|
||||||
|
|
||||||
|
var returnUrl = result.Properties?.Items["returnUrl"] ?? "/";
|
||||||
|
return Redirect(returnUrl);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error in external login callback for {scheme}");
|
||||||
|
return RedirectToAction("Login");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IActionResult> Logout()
|
||||||
|
{
|
||||||
|
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
return RedirectToAction("Index", "Home");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IActionResult> Profile()
|
||||||
|
{
|
||||||
|
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
return RedirectToAction("Login");
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await _userService.GetUserAsync(userId);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return RedirectToAction("Login");
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewBag.QRHistory = await _userService.GetUserQRHistoryAsync(userId, 10);
|
||||||
|
ViewBag.MonthlyQRCount = await _userService.GetQRCountThisMonthAsync(userId);
|
||||||
|
ViewBag.IsPremium = await _adDisplayService.HasValidPremiumSubscription(userId);
|
||||||
|
|
||||||
|
return View(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> AdFreeStatus()
|
||||||
|
{
|
||||||
|
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
return Json(new AdFreeStatusViewModel
|
||||||
|
{
|
||||||
|
IsAdFree = false,
|
||||||
|
TimeRemaining = 0,
|
||||||
|
IsPremium = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var shouldShowAds = await _adDisplayService.ShouldShowAds(userId);
|
||||||
|
var isPremium = await _adDisplayService.HasValidPremiumSubscription(userId);
|
||||||
|
var expiryDate = await _adDisplayService.GetAdFreeExpiryDate(userId);
|
||||||
|
var status = await _adDisplayService.GetAdFreeStatusAsync(userId);
|
||||||
|
|
||||||
|
return Json(new AdFreeStatusViewModel
|
||||||
|
{
|
||||||
|
IsAdFree = !shouldShowAds,
|
||||||
|
TimeRemaining = isPremium ? int.MaxValue : 0,
|
||||||
|
IsPremium = isPremium,
|
||||||
|
ExpiryDate = expiryDate,
|
||||||
|
SessionType = status
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IActionResult> ExtendAdFreeTime(int minutes)
|
||||||
|
{
|
||||||
|
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
|
||||||
|
// Método removido - sem extensão de tempo ad-free
|
||||||
|
return Json(new { success = false, message = "Feature not available" });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IActionResult> History()
|
||||||
|
{
|
||||||
|
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
return RedirectToAction("Login");
|
||||||
|
}
|
||||||
|
|
||||||
|
var history = await _userService.GetUserQRHistoryAsync(userId, 50);
|
||||||
|
return View(history);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IActionResult> UpdatePreferences(string language)
|
||||||
|
{
|
||||||
|
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
return Json(new { success = false });
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var user = await _userService.GetUserAsync(userId);
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
user.PreferredLanguage = language;
|
||||||
|
await _userService.UpdateUserAsync(user);
|
||||||
|
return Json(new { success = true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error updating preferences for user {userId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(new { success = false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
128
Controllers/HomeController.cs
Normal file
128
Controllers/HomeController.cs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using QRRapidoApp.Models;
|
||||||
|
using QRRapidoApp.Services;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Controllers
|
||||||
|
{
|
||||||
|
public class HomeController : Controller
|
||||||
|
{
|
||||||
|
private readonly ILogger<HomeController> _logger;
|
||||||
|
private readonly AdDisplayService _adDisplayService;
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
|
||||||
|
public HomeController(ILogger<HomeController> logger, AdDisplayService adDisplayService, IUserService userService, IConfiguration config)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_adDisplayService = adDisplayService;
|
||||||
|
_userService = userService;
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> Index()
|
||||||
|
{
|
||||||
|
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
|
||||||
|
ViewBag.ShowAds = await _adDisplayService.ShouldShowAds(userId);
|
||||||
|
ViewBag.IsPremium = await _adDisplayService.HasValidPremiumSubscription(userId ?? "");
|
||||||
|
ViewBag.IsAuthenticated = User.Identity?.IsAuthenticated ?? false;
|
||||||
|
ViewBag.UserName = User.Identity?.Name ?? "";
|
||||||
|
|
||||||
|
// SEO and Analytics data
|
||||||
|
ViewBag.Title = _config["App:TaglinePT"];
|
||||||
|
ViewBag.Keywords = _config["SEO:KeywordsPT"];
|
||||||
|
ViewBag.Description = "QR Rapido: Gere códigos QR em segundos! Gerador ultrarrápido em português e espanhol. Grátis, sem cadastro obrigatório. 30 dias sem anúncios após login.";
|
||||||
|
|
||||||
|
// User stats for logged in users
|
||||||
|
if (!string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
ViewBag.DailyQRCount = await _userService.GetDailyQRCountAsync(userId);
|
||||||
|
ViewBag.MonthlyQRCount = await _userService.GetQRCountThisMonthAsync(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult Privacy()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult Terms()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||||
|
public IActionResult Error()
|
||||||
|
{
|
||||||
|
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic QR redirect endpoint
|
||||||
|
[Route("d/{id}")]
|
||||||
|
public async Task<IActionResult> DynamicRedirect(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// This would lookup the dynamic QR content from cache/database
|
||||||
|
// For now, return a placeholder
|
||||||
|
return Redirect("https://qrrapido.site");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
[Route("health")]
|
||||||
|
public IActionResult Health()
|
||||||
|
{
|
||||||
|
return Ok(new { status = "healthy", timestamp = DateTime.UtcNow });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sitemap endpoint for SEO
|
||||||
|
[Route("sitemap.xml")]
|
||||||
|
public IActionResult Sitemap()
|
||||||
|
{
|
||||||
|
var sitemap = $@"<?xml version=""1.0"" encoding=""UTF-8""?>
|
||||||
|
<urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"">
|
||||||
|
<url>
|
||||||
|
<loc>https://qrrapido.site/</loc>
|
||||||
|
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://qrrapido.site/pt/</loc>
|
||||||
|
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<priority>0.9</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://qrrapido.site/es/</loc>
|
||||||
|
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<priority>0.9</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://qrrapido.site/Premium/Upgrade</loc>
|
||||||
|
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>";
|
||||||
|
|
||||||
|
return Content(sitemap, "application/xml");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ErrorViewModel
|
||||||
|
{
|
||||||
|
public string? RequestId { get; set; }
|
||||||
|
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
230
Controllers/PremiumController.cs
Normal file
230
Controllers/PremiumController.cs
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using QRRapidoApp.Models.ViewModels;
|
||||||
|
using QRRapidoApp.Services;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Controllers
|
||||||
|
{
|
||||||
|
[Authorize]
|
||||||
|
public class PremiumController : Controller
|
||||||
|
{
|
||||||
|
private readonly StripeService _stripeService;
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly AdDisplayService _adDisplayService;
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
private readonly ILogger<PremiumController> _logger;
|
||||||
|
|
||||||
|
public PremiumController(StripeService stripeService, IUserService userService, AdDisplayService adDisplayService, IConfiguration config, ILogger<PremiumController> logger)
|
||||||
|
{
|
||||||
|
_stripeService = stripeService;
|
||||||
|
_userService = userService;
|
||||||
|
_adDisplayService = adDisplayService;
|
||||||
|
_config = config;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<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<decimal>("Premium:PremiumPrice"),
|
||||||
|
Features = _config.GetSection("Premium:Features").Get<Dictionary<string, bool>>() ?? 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<IActionResult> 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<IActionResult> 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<IActionResult> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> Dashboard()
|
||||||
|
{
|
||||||
|
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("Upgrade");
|
||||||
|
}
|
||||||
|
|
||||||
|
var model = new PremiumDashboardViewModel
|
||||||
|
{
|
||||||
|
User = user,
|
||||||
|
QRCodesThisMonth = await _userService.GetQRCountThisMonthAsync(userId),
|
||||||
|
TotalQRCodes = user.QRHistoryIds?.Count ?? 0,
|
||||||
|
SubscriptionStatus = await _stripeService.GetSubscriptionStatusAsync(user.StripeSubscriptionId),
|
||||||
|
RecentQRCodes = await _userService.GetUserQRHistoryAsync(userId, 10)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate next billing date (simplified)
|
||||||
|
if (user.PremiumExpiresAt.HasValue)
|
||||||
|
{
|
||||||
|
model.NextBillingDate = user.PremiumExpiresAt.Value.AddDays(-2); // Approximate
|
||||||
|
}
|
||||||
|
|
||||||
|
// QR type statistics
|
||||||
|
model.QRTypeStats = model.RecentQRCodes
|
||||||
|
.GroupBy(q => q.Type)
|
||||||
|
.ToDictionary(g => g.Key, g => g.Count());
|
||||||
|
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> CancelSubscription()
|
||||||
|
{
|
||||||
|
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
return Json(new { success = false, error = "User not authenticated" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var user = await _userService.GetUserAsync(userId);
|
||||||
|
if (user?.StripeSubscriptionId == null)
|
||||||
|
{
|
||||||
|
return Json(new { success = false, error = "No active subscription found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
var success = await _stripeService.CancelSubscriptionAsync(user.StripeSubscriptionId);
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
TempData["Success"] = "Assinatura cancelada com sucesso. Você manterá o acesso premium até o final do período pago.";
|
||||||
|
return Json(new { success = true });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Json(new { success = false, error = "Failed to cancel subscription" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error canceling subscription for user {userId}");
|
||||||
|
return Json(new { success = false, error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> BillingPortal()
|
||||||
|
{
|
||||||
|
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
return RedirectToAction("Login", "Account");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// This would create a Stripe billing portal session
|
||||||
|
// For now, redirect to dashboard
|
||||||
|
return RedirectToAction("Dashboard");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error creating billing portal for user {userId}");
|
||||||
|
return RedirectToAction("Dashboard");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
216
Controllers/QRController.cs
Normal file
216
Controllers/QRController.cs
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using QRRapidoApp.Models.ViewModels;
|
||||||
|
using QRRapidoApp.Services;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class QRController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IQRCodeService _qrService;
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly AdDisplayService _adService;
|
||||||
|
private readonly ILogger<QRController> _logger;
|
||||||
|
|
||||||
|
public QRController(IQRCodeService qrService, IUserService userService, AdDisplayService adService, ILogger<QRController> logger)
|
||||||
|
{
|
||||||
|
_qrService = qrService;
|
||||||
|
_userService = userService;
|
||||||
|
_adService = adService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("GenerateRapid")]
|
||||||
|
public async Task<IActionResult> GenerateRapid([FromBody] QRGenerationRequest request)
|
||||||
|
{
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Quick validations
|
||||||
|
if (string.IsNullOrWhiteSpace(request.Content))
|
||||||
|
{
|
||||||
|
return BadRequest(new { error = "Conteúdo é obrigatório", success = false });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.Content.Length > 4000) // Limit to maintain speed
|
||||||
|
{
|
||||||
|
return BadRequest(new { error = "Conteúdo muito longo. Máximo 4000 caracteres.", success = false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user status
|
||||||
|
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
var user = await _userService.GetUserAsync(userId);
|
||||||
|
|
||||||
|
// Rate limiting for free users
|
||||||
|
if (!await CheckRateLimitAsync(userId, user))
|
||||||
|
{
|
||||||
|
return StatusCode(429, new
|
||||||
|
{
|
||||||
|
error = "Limite de QR codes atingido",
|
||||||
|
upgradeUrl = "/Premium/Upgrade",
|
||||||
|
success = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure optimizations based on user
|
||||||
|
request.IsPremium = user?.IsPremium == true;
|
||||||
|
request.OptimizeForSpeed = true;
|
||||||
|
|
||||||
|
// Generate QR code
|
||||||
|
var result = await _qrService.GenerateRapidAsync(request);
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { error = result.ErrorMessage, success = false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update counter for free users
|
||||||
|
if (!request.IsPremium && userId != null)
|
||||||
|
{
|
||||||
|
var remaining = await _userService.DecrementDailyQRCountAsync(userId);
|
||||||
|
result.RemainingQRs = remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to history if user is logged in (fire and forget)
|
||||||
|
if (userId != null)
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _userService.SaveQRToHistoryAsync(userId, result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error saving QR to history");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
// Performance logging
|
||||||
|
_logger.LogInformation($"QR Rapido generated in {stopwatch.ElapsedMilliseconds}ms " +
|
||||||
|
$"(service: {result.GenerationTimeMs}ms, " +
|
||||||
|
$"cache: {result.FromCache}, " +
|
||||||
|
$"user: {(request.IsPremium ? "premium" : "free")})");
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error in rapid QR code generation");
|
||||||
|
return StatusCode(500, new { error = "Erro interno do servidor", success = false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("Download/{qrId}")]
|
||||||
|
public async Task<IActionResult> Download(string qrId, string format = "png")
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var qrData = await _userService.GetQRDataAsync(qrId);
|
||||||
|
if (qrData == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentType = format.ToLower() switch
|
||||||
|
{
|
||||||
|
"svg" => "image/svg+xml",
|
||||||
|
"pdf" => "application/pdf",
|
||||||
|
_ => "image/png"
|
||||||
|
};
|
||||||
|
|
||||||
|
var fileName = $"qrrapido-{DateTime.Now:yyyyMMdd-HHmmss}.{format}";
|
||||||
|
|
||||||
|
if (format.ToLower() == "svg")
|
||||||
|
{
|
||||||
|
var svgContent = await _qrService.ConvertToSvgAsync(qrData.QRCodeBase64);
|
||||||
|
return File(svgContent, contentType, fileName);
|
||||||
|
}
|
||||||
|
else if (format.ToLower() == "pdf")
|
||||||
|
{
|
||||||
|
var pdfContent = await _qrService.ConvertToPdfAsync(qrData.QRCodeBase64, qrData.Size);
|
||||||
|
return File(pdfContent, contentType, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageBytes = Convert.FromBase64String(qrData.QRCodeBase64);
|
||||||
|
return File(imageBytes, contentType, fileName);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error downloading QR {qrId}");
|
||||||
|
return StatusCode(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("SaveToHistory")]
|
||||||
|
public async Task<IActionResult> SaveToHistory([FromBody] SaveToHistoryRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var qrData = await _userService.GetQRDataAsync(request.QrId);
|
||||||
|
if (qrData == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// QR is already saved when generated, just return success
|
||||||
|
return Ok(new { success = true, message = "QR Code salvo no histórico!" });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error saving QR to history");
|
||||||
|
return StatusCode(500, new { error = "Erro ao salvar no histórico." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("History")]
|
||||||
|
public async Task<IActionResult> GetHistory(int limit = 20)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var history = await _userService.GetUserQRHistoryAsync(userId, limit);
|
||||||
|
return Ok(history);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting QR history");
|
||||||
|
return StatusCode(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> CheckRateLimitAsync(string? userId, Models.User? user)
|
||||||
|
{
|
||||||
|
if (user?.IsPremium == true) return true;
|
||||||
|
|
||||||
|
var dailyLimit = userId != null ? 50 : 10; // Logged in: 50/day, Anonymous: 10/day
|
||||||
|
var currentCount = await _userService.GetDailyQRCountAsync(userId);
|
||||||
|
|
||||||
|
return currentCount < dailyLimit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SaveToHistoryRequest
|
||||||
|
{
|
||||||
|
public string QrId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
94
Data/MongoDbContext.cs
Normal file
94
Data/MongoDbContext.cs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
using MongoDB.Driver;
|
||||||
|
using QRRapidoApp.Models;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Data
|
||||||
|
{
|
||||||
|
public class MongoDbContext
|
||||||
|
{
|
||||||
|
private readonly IMongoDatabase _database;
|
||||||
|
private readonly bool _isConnected;
|
||||||
|
|
||||||
|
public MongoDbContext(IConfiguration configuration, IMongoClient mongoClient = null)
|
||||||
|
{
|
||||||
|
var connectionString = configuration.GetConnectionString("MongoDB");
|
||||||
|
if (mongoClient != null && !string.IsNullOrEmpty(connectionString))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var databaseName = MongoUrl.Create(connectionString).DatabaseName;
|
||||||
|
_database = mongoClient.GetDatabase(databaseName);
|
||||||
|
_isConnected = true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_isConnected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_isConnected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IMongoCollection<User> Users => _isConnected ? _database.GetCollection<User>("users") : null;
|
||||||
|
public IMongoCollection<QRCodeHistory> QRCodeHistory => _isConnected ? _database.GetCollection<QRCodeHistory>("qr_codes") : null;
|
||||||
|
public IMongoCollection<AdFreeSession> AdFreeSessions => _isConnected ? _database.GetCollection<AdFreeSession>("ad_free_sessions") : null;
|
||||||
|
|
||||||
|
public bool IsConnected => _isConnected;
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
if (_isConnected)
|
||||||
|
{
|
||||||
|
// Create indexes for better performance
|
||||||
|
await CreateIndexesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateIndexesAsync()
|
||||||
|
{
|
||||||
|
// User indexes
|
||||||
|
var userIndexKeys = Builders<User>.IndexKeys;
|
||||||
|
await Users.Indexes.CreateManyAsync(new[]
|
||||||
|
{
|
||||||
|
new CreateIndexModel<User>(userIndexKeys.Ascending(u => u.Email)),
|
||||||
|
new CreateIndexModel<User>(userIndexKeys.Ascending(u => u.ProviderId)),
|
||||||
|
new CreateIndexModel<User>(userIndexKeys.Ascending(u => u.Provider)),
|
||||||
|
new CreateIndexModel<User>(userIndexKeys.Ascending(u => u.LastLoginAt)),
|
||||||
|
new CreateIndexModel<User>(userIndexKeys.Ascending(u => u.IsPremium))
|
||||||
|
});
|
||||||
|
|
||||||
|
// QR Code History indexes
|
||||||
|
var qrIndexKeys = Builders<QRCodeHistory>.IndexKeys;
|
||||||
|
await QRCodeHistory.Indexes.CreateManyAsync(new[]
|
||||||
|
{
|
||||||
|
new CreateIndexModel<QRCodeHistory>(qrIndexKeys.Ascending(q => q.UserId)),
|
||||||
|
new CreateIndexModel<QRCodeHistory>(qrIndexKeys.Ascending(q => q.CreatedAt)),
|
||||||
|
new CreateIndexModel<QRCodeHistory>(qrIndexKeys.Ascending(q => q.Type)),
|
||||||
|
new CreateIndexModel<QRCodeHistory>(qrIndexKeys.Ascending(q => q.IsActive)),
|
||||||
|
new CreateIndexModel<QRCodeHistory>(
|
||||||
|
Builders<QRCodeHistory>.IndexKeys.Combine(
|
||||||
|
qrIndexKeys.Ascending(q => q.UserId),
|
||||||
|
qrIndexKeys.Descending(q => q.CreatedAt)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ad Free Session indexes
|
||||||
|
var adFreeIndexKeys = Builders<AdFreeSession>.IndexKeys;
|
||||||
|
await AdFreeSessions.Indexes.CreateManyAsync(new[]
|
||||||
|
{
|
||||||
|
new CreateIndexModel<AdFreeSession>(adFreeIndexKeys.Ascending(a => a.UserId)),
|
||||||
|
new CreateIndexModel<AdFreeSession>(adFreeIndexKeys.Ascending(a => a.ExpiresAt)),
|
||||||
|
new CreateIndexModel<AdFreeSession>(adFreeIndexKeys.Ascending(a => a.IsActive)),
|
||||||
|
new CreateIndexModel<AdFreeSession>(
|
||||||
|
Builders<AdFreeSession>.IndexKeys.Combine(
|
||||||
|
adFreeIndexKeys.Ascending(a => a.UserId),
|
||||||
|
adFreeIndexKeys.Ascending(a => a.IsActive),
|
||||||
|
adFreeIndexKeys.Descending(a => a.ExpiresAt)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
Dockerfile
Normal file
54
Dockerfile
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Copy csproj and restore dependencies (better caching)
|
||||||
|
COPY ["QRRapidoApp.csproj", "./"]
|
||||||
|
RUN dotnet restore "QRRapidoApp.csproj"
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build optimized for production
|
||||||
|
RUN dotnet build "QRRapidoApp.csproj" -c Release -o /app/build --no-restore
|
||||||
|
|
||||||
|
# Publish stage
|
||||||
|
FROM build AS publish
|
||||||
|
RUN dotnet publish "QRRapidoApp.csproj" -c Release -o /app/publish --no-restore --no-build \
|
||||||
|
/p:PublishReadyToRun=true \
|
||||||
|
/p:PublishSingleFile=false \
|
||||||
|
/p:PublishTrimmed=false
|
||||||
|
|
||||||
|
# Runtime stage
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies for QR code generation
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libgdiplus \
|
||||||
|
libc6-dev \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy application
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
|
||||||
|
# Configure production environment
|
||||||
|
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
ENV ASPNETCORE_URLS=http://+:80
|
||||||
|
ENV DOTNET_EnableDiagnostics=0
|
||||||
|
|
||||||
|
# Create non-root user for security
|
||||||
|
RUN addgroup --system --gid 1001 qrrapido
|
||||||
|
RUN adduser --system --uid 1001 qrrapido
|
||||||
|
|
||||||
|
# Set ownership
|
||||||
|
RUN chown -R qrrapido:qrrapido /app
|
||||||
|
USER qrrapido
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost/health || exit 1
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
ENTRYPOINT ["dotnet", "QRRapidoApp.dll"]
|
||||||
48
Middleware/LastLoginUpdateMiddleware.cs
Normal file
48
Middleware/LastLoginUpdateMiddleware.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
using QRRapidoApp.Services;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Middleware
|
||||||
|
{
|
||||||
|
public class LastLoginUpdateMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly ILogger<LastLoginUpdateMiddleware> _logger;
|
||||||
|
|
||||||
|
public LastLoginUpdateMiddleware(RequestDelegate next, IServiceScopeFactory scopeFactory, ILogger<LastLoginUpdateMiddleware> logger)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
// Only update login for authenticated users on first request of session
|
||||||
|
if (context.User.Identity?.IsAuthenticated == true)
|
||||||
|
{
|
||||||
|
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(userId) && !context.Session.GetString("LoginUpdated")?.Equals("true") == true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
|
||||||
|
|
||||||
|
await userService.UpdateLastLoginAsync(userId);
|
||||||
|
context.Session.SetString("LoginUpdated", "true");
|
||||||
|
|
||||||
|
_logger.LogInformation($"Updated last login for user {userId}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error updating last login for user {userId}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Models/AdFreeSession.cs
Normal file
33
Models/AdFreeSession.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Models
|
||||||
|
{
|
||||||
|
public class AdFreeSession
|
||||||
|
{
|
||||||
|
[BsonId]
|
||||||
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("userId")]
|
||||||
|
public string UserId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("startedAt")]
|
||||||
|
public DateTime StartedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
[BsonElement("expiresAt")]
|
||||||
|
public DateTime ExpiresAt { get; set; }
|
||||||
|
|
||||||
|
[BsonElement("isActive")]
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
|
||||||
|
[BsonElement("sessionType")]
|
||||||
|
public string SessionType { get; set; } = "Login"; // "Login", "Premium", "Trial", "Promotion"
|
||||||
|
|
||||||
|
[BsonElement("durationMinutes")]
|
||||||
|
public int DurationMinutes { get; set; } = 43200; // 30 days default
|
||||||
|
|
||||||
|
[BsonElement("createdAt")]
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
54
Models/QRCodeHistory.cs
Normal file
54
Models/QRCodeHistory.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Models
|
||||||
|
{
|
||||||
|
public class QRCodeHistory
|
||||||
|
{
|
||||||
|
[BsonId]
|
||||||
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("userId")]
|
||||||
|
public string? UserId { get; set; } // null for anonymous users
|
||||||
|
|
||||||
|
[BsonElement("type")]
|
||||||
|
public string Type { get; set; } = string.Empty; // URL, Text, WiFi, vCard, SMS, Email
|
||||||
|
|
||||||
|
[BsonElement("content")]
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("qrCodeBase64")]
|
||||||
|
public string QRCodeBase64 { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("customizationSettings")]
|
||||||
|
public string CustomizationSettings { get; set; } = string.Empty; // JSON
|
||||||
|
|
||||||
|
[BsonElement("createdAt")]
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
[BsonElement("language")]
|
||||||
|
public string Language { get; set; } = "pt-BR";
|
||||||
|
|
||||||
|
[BsonElement("scanCount")]
|
||||||
|
public int ScanCount { get; set; } = 0;
|
||||||
|
|
||||||
|
[BsonElement("isDynamic")]
|
||||||
|
public bool IsDynamic { get; set; } = false;
|
||||||
|
|
||||||
|
[BsonElement("size")]
|
||||||
|
public int Size { get; set; } = 300;
|
||||||
|
|
||||||
|
[BsonElement("generationTimeMs")]
|
||||||
|
public long GenerationTimeMs { get; set; } = 0;
|
||||||
|
|
||||||
|
[BsonElement("fromCache")]
|
||||||
|
public bool FromCache { get; set; } = false;
|
||||||
|
|
||||||
|
[BsonElement("isActive")]
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
|
||||||
|
[BsonElement("lastAccessedAt")]
|
||||||
|
public DateTime LastAccessedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
Models/User.cs
Normal file
60
Models/User.cs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Models
|
||||||
|
{
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
[BsonId]
|
||||||
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("email")]
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("name")]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("provider")]
|
||||||
|
public string Provider { get; set; } = string.Empty; // Google, Microsoft
|
||||||
|
|
||||||
|
[BsonElement("providerId")]
|
||||||
|
public string ProviderId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("isPremium")]
|
||||||
|
public bool IsPremium { get; set; } = false;
|
||||||
|
|
||||||
|
[BsonElement("createdAt")]
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
[BsonElement("premiumExpiresAt")]
|
||||||
|
public DateTime? PremiumExpiresAt { get; set; }
|
||||||
|
|
||||||
|
[BsonElement("premiumCancelledAt")]
|
||||||
|
public DateTime? PremiumCancelledAt { get; set; } // ADICIONADO: Data do cancelamento
|
||||||
|
|
||||||
|
[BsonElement("lastLoginAt")]
|
||||||
|
public DateTime LastLoginAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
[BsonElement("qrHistoryIds")]
|
||||||
|
public List<string> QRHistoryIds { get; set; } = new();
|
||||||
|
|
||||||
|
[BsonElement("stripeCustomerId")]
|
||||||
|
public string? StripeCustomerId { get; set; }
|
||||||
|
|
||||||
|
[BsonElement("stripeSubscriptionId")]
|
||||||
|
public string? StripeSubscriptionId { get; set; }
|
||||||
|
|
||||||
|
[BsonElement("preferredLanguage")]
|
||||||
|
public string PreferredLanguage { get; set; } = "pt-BR";
|
||||||
|
|
||||||
|
[BsonElement("dailyQRCount")]
|
||||||
|
public int DailyQRCount { get; set; } = 0;
|
||||||
|
|
||||||
|
[BsonElement("lastQRDate")]
|
||||||
|
public DateTime LastQRDate { get; set; } = DateTime.UtcNow.Date;
|
||||||
|
|
||||||
|
[BsonElement("totalQRGenerated")]
|
||||||
|
public int TotalQRGenerated { get; set; } = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
Models/ViewModels/PremiumViewModels.cs
Normal file
32
Models/ViewModels/PremiumViewModels.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
namespace QRRapidoApp.Models.ViewModels
|
||||||
|
{
|
||||||
|
public class UpgradeViewModel
|
||||||
|
{
|
||||||
|
public string CurrentPlan { get; set; } = "Free";
|
||||||
|
public decimal PremiumPrice { get; set; }
|
||||||
|
public Dictionary<string, bool> Features { get; set; } = new();
|
||||||
|
public int RemainingQRs { get; set; }
|
||||||
|
public int DaysUntilAdExpiry { get; set; }
|
||||||
|
public bool IsAdFreeActive { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PremiumDashboardViewModel
|
||||||
|
{
|
||||||
|
public User User { get; set; } = new();
|
||||||
|
public int QRCodesThisMonth { get; set; }
|
||||||
|
public int TotalQRCodes { get; set; }
|
||||||
|
public string SubscriptionStatus { get; set; } = string.Empty;
|
||||||
|
public DateTime? NextBillingDate { get; set; }
|
||||||
|
public List<QRCodeHistory> RecentQRCodes { get; set; } = new();
|
||||||
|
public Dictionary<string, int> QRTypeStats { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AdFreeStatusViewModel
|
||||||
|
{
|
||||||
|
public bool IsAdFree { get; set; }
|
||||||
|
public int TimeRemaining { get; set; } // minutes
|
||||||
|
public bool IsPremium { get; set; }
|
||||||
|
public DateTime? ExpiryDate { get; set; }
|
||||||
|
public string SessionType { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
Models/ViewModels/QRGenerationRequest.cs
Normal file
32
Models/ViewModels/QRGenerationRequest.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
namespace QRRapidoApp.Models.ViewModels
|
||||||
|
{
|
||||||
|
public class QRGenerationRequest
|
||||||
|
{
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
public string QuickStyle { get; set; } = "classic";
|
||||||
|
public string PrimaryColor { get; set; } = "#000000";
|
||||||
|
public string BackgroundColor { get; set; } = "#FFFFFF";
|
||||||
|
public int Size { get; set; } = 300;
|
||||||
|
public int Margin { get; set; } = 2;
|
||||||
|
public string CornerStyle { get; set; } = "square";
|
||||||
|
public bool OptimizeForSpeed { get; set; } = true;
|
||||||
|
public string Language { get; set; } = "pt-BR";
|
||||||
|
public bool IsPremium { get; set; } = false;
|
||||||
|
public bool HasLogo { get; set; } = false;
|
||||||
|
public byte[]? Logo { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QRGenerationResult
|
||||||
|
{
|
||||||
|
public string QRCodeBase64 { get; set; } = string.Empty;
|
||||||
|
public string QRId { get; set; } = string.Empty;
|
||||||
|
public long GenerationTimeMs { get; set; }
|
||||||
|
public bool FromCache { get; set; }
|
||||||
|
public int Size { get; set; }
|
||||||
|
public QRGenerationRequest? RequestSettings { get; set; }
|
||||||
|
public int? RemainingQRs { get; set; } // For free users
|
||||||
|
public bool Success { get; set; } = true;
|
||||||
|
public string? ErrorMessage { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
179
Program.cs
Normal file
179
Program.cs
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Google;
|
||||||
|
using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
|
||||||
|
using Microsoft.AspNetCore.Localization;
|
||||||
|
using Microsoft.AspNetCore.StaticFiles;
|
||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
using QRRapidoApp.Data;
|
||||||
|
using QRRapidoApp.Middleware;
|
||||||
|
using QRRapidoApp.Services;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using Stripe;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Add services to the container
|
||||||
|
builder.Services.AddControllersWithViews();
|
||||||
|
|
||||||
|
// MongoDB Configuration - optional for development
|
||||||
|
var mongoConnectionString = builder.Configuration.GetConnectionString("MongoDB");
|
||||||
|
if (!string.IsNullOrEmpty(mongoConnectionString))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
builder.Services.AddSingleton<IMongoClient>(serviceProvider =>
|
||||||
|
{
|
||||||
|
return new MongoClient(mongoConnectionString);
|
||||||
|
});
|
||||||
|
builder.Services.AddScoped<MongoDbContext>();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// MongoDB not available - services will handle gracefully
|
||||||
|
builder.Services.AddScoped<MongoDbContext>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Development mode without MongoDB
|
||||||
|
builder.Services.AddScoped<MongoDbContext>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache Configuration - use Redis if available, otherwise memory cache
|
||||||
|
var redisConnectionString = builder.Configuration.GetConnectionString("Redis");
|
||||||
|
if (!string.IsNullOrEmpty(redisConnectionString))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
builder.Services.AddStackExchangeRedisCache(options =>
|
||||||
|
{
|
||||||
|
options.Configuration = redisConnectionString;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Fallback to memory cache if Redis fails
|
||||||
|
builder.Services.AddMemoryCache();
|
||||||
|
builder.Services.AddSingleton<IDistributedCache, QRRapidoApp.Services.MemoryDistributedCacheWrapper>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Use memory cache when Redis is not configured
|
||||||
|
builder.Services.AddMemoryCache();
|
||||||
|
builder.Services.AddSingleton<IDistributedCache, MemoryDistributedCacheWrapper>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentication Configuration
|
||||||
|
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||||
|
.AddCookie(options =>
|
||||||
|
{
|
||||||
|
options.LoginPath = "/Account/Login";
|
||||||
|
options.LogoutPath = "/Account/Logout";
|
||||||
|
options.ExpireTimeSpan = TimeSpan.FromDays(30);
|
||||||
|
options.SlidingExpiration = true;
|
||||||
|
})
|
||||||
|
.AddGoogle(GoogleDefaults.AuthenticationScheme, options =>
|
||||||
|
{
|
||||||
|
options.ClientId = builder.Configuration["Authentication:Google:ClientId"];
|
||||||
|
options.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"];
|
||||||
|
})
|
||||||
|
.AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, options =>
|
||||||
|
{
|
||||||
|
options.ClientId = builder.Configuration["Authentication:Microsoft:ClientId"];
|
||||||
|
options.ClientSecret = builder.Configuration["Authentication:Microsoft:ClientSecret"];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stripe Configuration
|
||||||
|
StripeConfiguration.ApiKey = builder.Configuration["Stripe:SecretKey"];
|
||||||
|
|
||||||
|
// Localization
|
||||||
|
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
|
||||||
|
builder.Services.Configure<RequestLocalizationOptions>(options =>
|
||||||
|
{
|
||||||
|
var supportedCultures = new[]
|
||||||
|
{
|
||||||
|
new CultureInfo("pt-BR"),
|
||||||
|
new CultureInfo("es"),
|
||||||
|
new CultureInfo("en")
|
||||||
|
};
|
||||||
|
|
||||||
|
options.DefaultRequestCulture = new RequestCulture("pt-BR");
|
||||||
|
options.SupportedCultures = supportedCultures;
|
||||||
|
options.SupportedUICultures = supportedCultures;
|
||||||
|
|
||||||
|
options.RequestCultureProviders.Insert(0, new QueryStringRequestCultureProvider());
|
||||||
|
options.RequestCultureProviders.Insert(1, new CookieRequestCultureProvider());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom Services
|
||||||
|
builder.Services.AddScoped<IQRCodeService, QRRapidoService>();
|
||||||
|
builder.Services.AddScoped<IUserService, UserService>();
|
||||||
|
builder.Services.AddScoped<AdDisplayService>();
|
||||||
|
builder.Services.AddScoped<StripeService>();
|
||||||
|
|
||||||
|
// Background Services
|
||||||
|
builder.Services.AddHostedService<HistoryCleanupService>();
|
||||||
|
|
||||||
|
// CORS for API endpoints
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("AllowSpecificOrigins", policy =>
|
||||||
|
{
|
||||||
|
policy.WithOrigins("https://qrrapido.site", "https://www.qrrapido.site")
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Health checks (basic implementation without external dependencies)
|
||||||
|
builder.Services.AddHealthChecks();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Configure the HTTP request pipeline
|
||||||
|
if (!app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseExceptionHandler("/Home/Error");
|
||||||
|
app.UseHsts();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.UseStaticFiles();
|
||||||
|
|
||||||
|
app.UseRouting();
|
||||||
|
|
||||||
|
app.UseCors("AllowSpecificOrigins");
|
||||||
|
|
||||||
|
// Localization middleware
|
||||||
|
app.UseRequestLocalization();
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
// Custom middleware
|
||||||
|
app.UseMiddleware<LastLoginUpdateMiddleware>();
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
app.MapHealthChecks("/health");
|
||||||
|
|
||||||
|
// Controller routes
|
||||||
|
app.MapControllerRoute(
|
||||||
|
name: "default",
|
||||||
|
pattern: "{controller=Home}/{action=Index}/{id?}");
|
||||||
|
|
||||||
|
// API routes
|
||||||
|
app.MapControllerRoute(
|
||||||
|
name: "api",
|
||||||
|
pattern: "api/{controller}/{action=Index}/{id?}");
|
||||||
|
|
||||||
|
// Language routes
|
||||||
|
app.MapControllerRoute(
|
||||||
|
name: "localized",
|
||||||
|
pattern: "{culture:regex(^(pt-BR|es|en)$)}/{controller=Home}/{action=Index}/{id?}");
|
||||||
|
|
||||||
|
app.Run();
|
||||||
12
Properties/launchSettings.json
Normal file
12
Properties/launchSettings.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"QRRapidoApp": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"applicationUrl": "https://localhost:52428;https://192.168.0.85:52428;http://localhost:52429"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
QRRapidoApp.csproj
Normal file
39
QRRapidoApp.csproj
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<AssemblyName>QRRapidoApp</AssemblyName>
|
||||||
|
<RootNamespace>QRRapidoApp</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="8.0.0" />
|
||||||
|
<PackageReference Include="MongoDB.Driver" Version="2.22.0" />
|
||||||
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
|
<PackageReference Include="QRCoder" Version="1.4.3" />
|
||||||
|
<PackageReference Include="Stripe.net" Version="43.15.0" />
|
||||||
|
<PackageReference Include="StackExchange.Redis" Version="2.7.4" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.0" />
|
||||||
|
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Localization" Version="2.2.0" />
|
||||||
|
<PackageReference Include="xunit.assert" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.extensibility.core" Version="2.9.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="wwwroot\images\" />
|
||||||
|
<Folder Include="wwwroot\css\" />
|
||||||
|
<Folder Include="wwwroot\js\" />
|
||||||
|
<Folder Include="Resources\" />
|
||||||
|
<Folder Include="Data\" />
|
||||||
|
<Folder Include="Services\" />
|
||||||
|
<Folder Include="Models\" />
|
||||||
|
<Folder Include="Middleware\" />
|
||||||
|
<Folder Include="Tests\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
25
QRRapidoApp.sln
Normal file
25
QRRapidoApp.sln
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.13.35818.85 d17.13
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QRRapidoApp", "QRRapidoApp.csproj", "{8AF92774-40E8-830E-08B3-67F0A0B91DDE}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{8AF92774-40E8-830E-08B3-67F0A0B91DDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{8AF92774-40E8-830E-08B3-67F0A0B91DDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{8AF92774-40E8-830E-08B3-67F0A0B91DDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{8AF92774-40E8-830E-08B3-67F0A0B91DDE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {9E53D8E2-0957-4925-B347-404E3B14587B}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
223
Resources/Views.es.resx
Normal file
223
Resources/Views.es.resx
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="Tagline" xml:space="preserve">
|
||||||
|
<value>¡Genera códigos QR en segundos!</value>
|
||||||
|
</data>
|
||||||
|
<data name="GenerateQR" xml:space="preserve">
|
||||||
|
<value>Generar Código QR</value>
|
||||||
|
</data>
|
||||||
|
<data name="QRType" xml:space="preserve">
|
||||||
|
<value>Tipo de Código QR</value>
|
||||||
|
</data>
|
||||||
|
<data name="Content" xml:space="preserve">
|
||||||
|
<value>Contenido</value>
|
||||||
|
</data>
|
||||||
|
<data name="URLType" xml:space="preserve">
|
||||||
|
<value>URL/Enlace</value>
|
||||||
|
</data>
|
||||||
|
<data name="TextType" xml:space="preserve">
|
||||||
|
<value>Texto Simple</value>
|
||||||
|
</data>
|
||||||
|
<data name="WiFiType" xml:space="preserve">
|
||||||
|
<value>WiFi</value>
|
||||||
|
</data>
|
||||||
|
<data name="VCardType" xml:space="preserve">
|
||||||
|
<value>Tarjeta de Visita</value>
|
||||||
|
</data>
|
||||||
|
<data name="SMSType" xml:space="preserve">
|
||||||
|
<value>SMS</value>
|
||||||
|
</data>
|
||||||
|
<data name="EmailType" xml:space="preserve">
|
||||||
|
<value>Email</value>
|
||||||
|
</data>
|
||||||
|
<data name="DynamicType" xml:space="preserve">
|
||||||
|
<value>QR Dinámico (Premium)</value>
|
||||||
|
</data>
|
||||||
|
<data name="QuickStyle" xml:space="preserve">
|
||||||
|
<value>Estilo Rápido</value>
|
||||||
|
</data>
|
||||||
|
<data name="ClassicStyle" xml:space="preserve">
|
||||||
|
<value>Clásico</value>
|
||||||
|
</data>
|
||||||
|
<data name="ModernStyle" xml:space="preserve">
|
||||||
|
<value>Moderno</value>
|
||||||
|
</data>
|
||||||
|
<data name="ColorfulStyle" xml:space="preserve">
|
||||||
|
<value>Colorido</value>
|
||||||
|
</data>
|
||||||
|
<data name="ContentPlaceholder" xml:space="preserve">
|
||||||
|
<value>Escribe el contenido de tu código QR aquí...</value>
|
||||||
|
</data>
|
||||||
|
<data name="AdvancedCustomization" xml:space="preserve">
|
||||||
|
<value>Personalización Avanzada</value>
|
||||||
|
</data>
|
||||||
|
<data name="PrimaryColor" xml:space="preserve">
|
||||||
|
<value>Color Principal</value>
|
||||||
|
</data>
|
||||||
|
<data name="BackgroundColor" xml:space="preserve">
|
||||||
|
<value>Color de Fondo</value>
|
||||||
|
</data>
|
||||||
|
<data name="Size" xml:space="preserve">
|
||||||
|
<value>Tamaño</value>
|
||||||
|
</data>
|
||||||
|
<data name="Margin" xml:space="preserve">
|
||||||
|
<value>Margen</value>
|
||||||
|
</data>
|
||||||
|
<data name="Logo" xml:space="preserve">
|
||||||
|
<value>Logo/Icono</value>
|
||||||
|
</data>
|
||||||
|
<data name="CornerStyle" xml:space="preserve">
|
||||||
|
<value>Estilo de Bordes</value>
|
||||||
|
</data>
|
||||||
|
<data name="GenerateRapidly" xml:space="preserve">
|
||||||
|
<value>Generar Código QR Rápidamente</value>
|
||||||
|
</data>
|
||||||
|
<data name="Preview" xml:space="preserve">
|
||||||
|
<value>Vista Previa</value>
|
||||||
|
</data>
|
||||||
|
<data name="PreviewPlaceholder" xml:space="preserve">
|
||||||
|
<value>Tu código QR aparecerá aquí en segundos</value>
|
||||||
|
</data>
|
||||||
|
<data name="UltraFastGeneration" xml:space="preserve">
|
||||||
|
<value>Generación ultra-rápida garantizada</value>
|
||||||
|
</data>
|
||||||
|
<data name="DownloadPNG" xml:space="preserve">
|
||||||
|
<value>Descargar PNG</value>
|
||||||
|
</data>
|
||||||
|
<data name="DownloadSVG" xml:space="preserve">
|
||||||
|
<value>Descargar SVG (Vectorial)</value>
|
||||||
|
</data>
|
||||||
|
<data name="DownloadPDF" xml:space="preserve">
|
||||||
|
<value>Descargar PDF</value>
|
||||||
|
</data>
|
||||||
|
<data name="SaveToHistory" xml:space="preserve">
|
||||||
|
<value>Guardar en Historial</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoginToSave" xml:space="preserve">
|
||||||
|
<value>Inicia sesión para guardar en el historial</value>
|
||||||
|
</data>
|
||||||
|
<data name="PremiumTitle" xml:space="preserve">
|
||||||
|
<value>QR Rapido Premium</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpeedTipsTitle" xml:space="preserve">
|
||||||
|
<value>Consejos para QR Más Rápidos</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpeedTip1" xml:space="preserve">
|
||||||
|
<value>URLs cortas se generan más rápido</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpeedTip2" xml:space="preserve">
|
||||||
|
<value>Menos texto = mayor velocidad</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpeedTip3" xml:space="preserve">
|
||||||
|
<value>Colores sólidos optimizan el proceso</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpeedTip4" xml:space="preserve">
|
||||||
|
<value>Tamaños menores aceleran la descarga</value>
|
||||||
|
</data>
|
||||||
|
<data name="Login" xml:space="preserve">
|
||||||
|
<value>Iniciar Sesión</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoginWith" xml:space="preserve">
|
||||||
|
<value>Iniciar sesión con</value>
|
||||||
|
</data>
|
||||||
|
<data name="Google" xml:space="preserve">
|
||||||
|
<value>Google</value>
|
||||||
|
</data>
|
||||||
|
<data name="Microsoft" xml:space="preserve">
|
||||||
|
<value>Microsoft</value>
|
||||||
|
</data>
|
||||||
|
<data name="AdFreeOffer" xml:space="preserve">
|
||||||
|
<value>¡Login = 30 días sin anuncios!</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpecialOffer" xml:space="preserve">
|
||||||
|
<value>¡Oferta Especial!</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoginBenefits" xml:space="preserve">
|
||||||
|
<value>Al iniciar sesión, ganas automáticamente 30 días sin anuncios y puedes generar hasta 50 códigos QR por día gratis.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Privacy" xml:space="preserve">
|
||||||
|
<value>Política de Privacidad</value>
|
||||||
|
</data>
|
||||||
|
<data name="BackToGenerator" xml:space="preserve">
|
||||||
|
<value>Volver al generador</value>
|
||||||
|
</data>
|
||||||
|
<data name="GeneratedIn" xml:space="preserve">
|
||||||
|
<value>Generado en</value>
|
||||||
|
</data>
|
||||||
|
<data name="Seconds" xml:space="preserve">
|
||||||
|
<value>s</value>
|
||||||
|
</data>
|
||||||
|
<data name="UltraFast" xml:space="preserve">
|
||||||
|
<value>¡Generación ultra rápida!</value>
|
||||||
|
</data>
|
||||||
|
<data name="Fast" xml:space="preserve">
|
||||||
|
<value>¡Generación rápida!</value>
|
||||||
|
</data>
|
||||||
|
<data name="Normal" xml:space="preserve">
|
||||||
|
<value>Generación normal</value>
|
||||||
|
</data>
|
||||||
|
<data name="Error" xml:space="preserve">
|
||||||
|
<value>Error en la generación. Inténtalo de nuevo.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Success" xml:space="preserve">
|
||||||
|
<value>¡Código QR guardado en el historial!</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
223
Resources/Views.pt-BR.resx
Normal file
223
Resources/Views.pt-BR.resx
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="Tagline" xml:space="preserve">
|
||||||
|
<value>Gere QR codes em segundos!</value>
|
||||||
|
</data>
|
||||||
|
<data name="GenerateQR" xml:space="preserve">
|
||||||
|
<value>Gerar QR Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="QRType" xml:space="preserve">
|
||||||
|
<value>Tipo de QR Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="Content" xml:space="preserve">
|
||||||
|
<value>Conteúdo</value>
|
||||||
|
</data>
|
||||||
|
<data name="URLType" xml:space="preserve">
|
||||||
|
<value>URL/Link</value>
|
||||||
|
</data>
|
||||||
|
<data name="TextType" xml:space="preserve">
|
||||||
|
<value>Texto Simples</value>
|
||||||
|
</data>
|
||||||
|
<data name="WiFiType" xml:space="preserve">
|
||||||
|
<value>WiFi</value>
|
||||||
|
</data>
|
||||||
|
<data name="VCardType" xml:space="preserve">
|
||||||
|
<value>Cartão de Visita</value>
|
||||||
|
</data>
|
||||||
|
<data name="SMSType" xml:space="preserve">
|
||||||
|
<value>SMS</value>
|
||||||
|
</data>
|
||||||
|
<data name="EmailType" xml:space="preserve">
|
||||||
|
<value>Email</value>
|
||||||
|
</data>
|
||||||
|
<data name="DynamicType" xml:space="preserve">
|
||||||
|
<value>QR Dinâmico (Premium)</value>
|
||||||
|
</data>
|
||||||
|
<data name="QuickStyle" xml:space="preserve">
|
||||||
|
<value>Estilo Rápido</value>
|
||||||
|
</data>
|
||||||
|
<data name="ClassicStyle" xml:space="preserve">
|
||||||
|
<value>Clássico</value>
|
||||||
|
</data>
|
||||||
|
<data name="ModernStyle" xml:space="preserve">
|
||||||
|
<value>Moderno</value>
|
||||||
|
</data>
|
||||||
|
<data name="ColorfulStyle" xml:space="preserve">
|
||||||
|
<value>Colorido</value>
|
||||||
|
</data>
|
||||||
|
<data name="ContentPlaceholder" xml:space="preserve">
|
||||||
|
<value>Digite o conteúdo do seu QR code aqui...</value>
|
||||||
|
</data>
|
||||||
|
<data name="AdvancedCustomization" xml:space="preserve">
|
||||||
|
<value>Personalização Avançada</value>
|
||||||
|
</data>
|
||||||
|
<data name="PrimaryColor" xml:space="preserve">
|
||||||
|
<value>Cor Principal</value>
|
||||||
|
</data>
|
||||||
|
<data name="BackgroundColor" xml:space="preserve">
|
||||||
|
<value>Cor de Fundo</value>
|
||||||
|
</data>
|
||||||
|
<data name="Size" xml:space="preserve">
|
||||||
|
<value>Tamanho</value>
|
||||||
|
</data>
|
||||||
|
<data name="Margin" xml:space="preserve">
|
||||||
|
<value>Margem</value>
|
||||||
|
</data>
|
||||||
|
<data name="Logo" xml:space="preserve">
|
||||||
|
<value>Logo/Ícone</value>
|
||||||
|
</data>
|
||||||
|
<data name="CornerStyle" xml:space="preserve">
|
||||||
|
<value>Estilo das Bordas</value>
|
||||||
|
</data>
|
||||||
|
<data name="GenerateRapidly" xml:space="preserve">
|
||||||
|
<value>Gerar QR Code Rapidamente</value>
|
||||||
|
</data>
|
||||||
|
<data name="Preview" xml:space="preserve">
|
||||||
|
<value>Preview</value>
|
||||||
|
</data>
|
||||||
|
<data name="PreviewPlaceholder" xml:space="preserve">
|
||||||
|
<value>Seu QR code aparecerá aqui em segundos</value>
|
||||||
|
</data>
|
||||||
|
<data name="UltraFastGeneration" xml:space="preserve">
|
||||||
|
<value>Geração ultra-rápida garantida</value>
|
||||||
|
</data>
|
||||||
|
<data name="DownloadPNG" xml:space="preserve">
|
||||||
|
<value>Download PNG</value>
|
||||||
|
</data>
|
||||||
|
<data name="DownloadSVG" xml:space="preserve">
|
||||||
|
<value>Download SVG (Vetorial)</value>
|
||||||
|
</data>
|
||||||
|
<data name="DownloadPDF" xml:space="preserve">
|
||||||
|
<value>Download PDF</value>
|
||||||
|
</data>
|
||||||
|
<data name="SaveToHistory" xml:space="preserve">
|
||||||
|
<value>Salvar no Histórico</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoginToSave" xml:space="preserve">
|
||||||
|
<value>Faça login para salvar no histórico</value>
|
||||||
|
</data>
|
||||||
|
<data name="PremiumTitle" xml:space="preserve">
|
||||||
|
<value>QR Rapido Premium</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpeedTipsTitle" xml:space="preserve">
|
||||||
|
<value>Dicas para QR Mais Rápidos</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpeedTip1" xml:space="preserve">
|
||||||
|
<value>URLs curtas geram mais rápido</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpeedTip2" xml:space="preserve">
|
||||||
|
<value>Menos texto = maior velocidade</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpeedTip3" xml:space="preserve">
|
||||||
|
<value>Cores sólidas otimizam o processo</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpeedTip4" xml:space="preserve">
|
||||||
|
<value>Tamanhos menores aceleram o download</value>
|
||||||
|
</data>
|
||||||
|
<data name="Login" xml:space="preserve">
|
||||||
|
<value>Login</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoginWith" xml:space="preserve">
|
||||||
|
<value>Entrar com</value>
|
||||||
|
</data>
|
||||||
|
<data name="Google" xml:space="preserve">
|
||||||
|
<value>Google</value>
|
||||||
|
</data>
|
||||||
|
<data name="Microsoft" xml:space="preserve">
|
||||||
|
<value>Microsoft</value>
|
||||||
|
</data>
|
||||||
|
<data name="AdFreeOffer" xml:space="preserve">
|
||||||
|
<value>Login = 30 dias sem anúncios!</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpecialOffer" xml:space="preserve">
|
||||||
|
<value>Oferta Especial!</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoginBenefits" xml:space="preserve">
|
||||||
|
<value>Ao fazer login, você ganha automaticamente 30 dias sem anúncios e pode gerar até 50 QR codes por dia gratuitamente.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Privacy" xml:space="preserve">
|
||||||
|
<value>Política de Privacidade</value>
|
||||||
|
</data>
|
||||||
|
<data name="BackToGenerator" xml:space="preserve">
|
||||||
|
<value>Voltar ao gerador</value>
|
||||||
|
</data>
|
||||||
|
<data name="GeneratedIn" xml:space="preserve">
|
||||||
|
<value>Gerado em</value>
|
||||||
|
</data>
|
||||||
|
<data name="Seconds" xml:space="preserve">
|
||||||
|
<value>s</value>
|
||||||
|
</data>
|
||||||
|
<data name="UltraFast" xml:space="preserve">
|
||||||
|
<value>Geração ultra rápida!</value>
|
||||||
|
</data>
|
||||||
|
<data name="Fast" xml:space="preserve">
|
||||||
|
<value>Geração rápida!</value>
|
||||||
|
</data>
|
||||||
|
<data name="Normal" xml:space="preserve">
|
||||||
|
<value>Geração normal</value>
|
||||||
|
</data>
|
||||||
|
<data name="Error" xml:space="preserve">
|
||||||
|
<value>Erro na geração. Tente novamente.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Success" xml:space="preserve">
|
||||||
|
<value>QR Code salvo no histórico!</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
117
Services/AdDisplayService.cs
Normal file
117
Services/AdDisplayService.cs
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
using MongoDB.Driver;
|
||||||
|
using QRRapidoApp.Data;
|
||||||
|
using QRRapidoApp.Models;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Services
|
||||||
|
{
|
||||||
|
public class AdDisplayService
|
||||||
|
{
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly MongoDbContext _context;
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
private readonly ILogger<AdDisplayService> _logger;
|
||||||
|
|
||||||
|
public AdDisplayService(IUserService userService, MongoDbContext context, IConfiguration config, ILogger<AdDisplayService> logger)
|
||||||
|
{
|
||||||
|
_userService = userService;
|
||||||
|
_context = context;
|
||||||
|
_config = config;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ShouldShowAds(string? userId = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Usuários não logados: sempre mostrar anúncios
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var user = await _userService.GetUserAsync(userId);
|
||||||
|
if (user == null) return true;
|
||||||
|
|
||||||
|
// APENAS Premium users não veem anúncios
|
||||||
|
return !(user.IsPremium && user.PremiumExpiresAt > DateTime.UtcNow);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error checking ad display status for user {userId}: {ex.Message}");
|
||||||
|
return true; // Default to showing ads on error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MÉTODO REMOVIDO: GetAdFreeTimeRemaining - não é mais necessário
|
||||||
|
// MÉTODO REMOVIDO: GetActiveAdFreeSessionAsync - não é mais necessário
|
||||||
|
|
||||||
|
public async Task<bool> HasValidPremiumSubscription(string userId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var user = await _userService.GetUserAsync(userId);
|
||||||
|
return user?.IsPremium == true && user.PremiumExpiresAt > DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error checking premium subscription for user {userId}: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetAdFreeStatusAsync(string userId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (await HasValidPremiumSubscription(userId))
|
||||||
|
return "Premium";
|
||||||
|
|
||||||
|
return "None";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error getting ad-free status for user {userId}: {ex.Message}");
|
||||||
|
return "None";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DateTime?> GetAdFreeExpiryDate(string userId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var user = await _userService.GetUserAsync(userId);
|
||||||
|
if (user?.IsPremium == true && user.PremiumExpiresAt > DateTime.UtcNow)
|
||||||
|
return user.PremiumExpiresAt;
|
||||||
|
|
||||||
|
return null; // Sem sessões ad-free temporárias
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error getting ad-free expiry date for user {userId}: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeactivateExpiredSessionsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var filter = Builders<AdFreeSession>.Filter.And(
|
||||||
|
Builders<AdFreeSession>.Filter.Eq(s => s.IsActive, true),
|
||||||
|
Builders<AdFreeSession>.Filter.Lt(s => s.ExpiresAt, DateTime.UtcNow)
|
||||||
|
);
|
||||||
|
|
||||||
|
var update = Builders<AdFreeSession>.Update.Set(s => s.IsActive, false);
|
||||||
|
|
||||||
|
var result = await _context.AdFreeSessions.UpdateManyAsync(filter, update);
|
||||||
|
|
||||||
|
if (result.ModifiedCount > 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"Deactivated {result.ModifiedCount} expired ad-free sessions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error deactivating expired sessions: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
Services/HistoryCleanupService.cs
Normal file
70
Services/HistoryCleanupService.cs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
using QRRapidoApp.Data;
|
||||||
|
using QRRapidoApp.Models;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Services
|
||||||
|
{
|
||||||
|
public class HistoryCleanupService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly ILogger<HistoryCleanupService> _logger;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public HistoryCleanupService(IServiceScopeFactory scopeFactory, ILogger<HistoryCleanupService> logger, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
_logger = logger;
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
var gracePeriodDays = _configuration.GetValue<int>("HistoryCleanup:GracePeriodDays", 7);
|
||||||
|
var cleanupIntervalHours = _configuration.GetValue<int>("HistoryCleanup:CleanupIntervalHours", 6);
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var context = scope.ServiceProvider.GetRequiredService<MongoDbContext>();
|
||||||
|
|
||||||
|
if (context.IsConnected && context.Users != null && context.QRCodeHistory != null)
|
||||||
|
{
|
||||||
|
// Buscar usuários que cancelaram há mais de X dias
|
||||||
|
var cutoffDate = DateTime.UtcNow.AddDays(-gracePeriodDays);
|
||||||
|
|
||||||
|
var usersToCleanup = await context.Users
|
||||||
|
.Find(u => u.PremiumCancelledAt != null &&
|
||||||
|
u.PremiumCancelledAt < cutoffDate &&
|
||||||
|
u.QRHistoryIds.Count > 0)
|
||||||
|
.ToListAsync(stoppingToken);
|
||||||
|
|
||||||
|
foreach (var user in usersToCleanup)
|
||||||
|
{
|
||||||
|
// Remover histórico de QR codes
|
||||||
|
if (user.QRHistoryIds.Any())
|
||||||
|
{
|
||||||
|
await context.QRCodeHistory.DeleteManyAsync(
|
||||||
|
qr => user.QRHistoryIds.Contains(qr.Id), stoppingToken);
|
||||||
|
|
||||||
|
// Limpar lista de histórico do usuário
|
||||||
|
var update = Builders<User>.Update.Set(u => u.QRHistoryIds, new List<string>());
|
||||||
|
await context.Users.UpdateOneAsync(u => u.Id == user.Id, update, cancellationToken: stoppingToken);
|
||||||
|
|
||||||
|
_logger.LogInformation($"Histórico removido para usuário {user.Id} - inadimplente há {gracePeriodDays}+ dias");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro no cleanup do histórico");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executar a cada X horas
|
||||||
|
await Task.Delay(TimeSpan.FromHours(cleanupIntervalHours), stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Services/IQRCodeService.cs
Normal file
13
Services/IQRCodeService.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using QRRapidoApp.Models.ViewModels;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Services
|
||||||
|
{
|
||||||
|
public interface IQRCodeService
|
||||||
|
{
|
||||||
|
Task<QRGenerationResult> GenerateRapidAsync(QRGenerationRequest request);
|
||||||
|
Task<byte[]> ConvertToSvgAsync(string qrCodeBase64);
|
||||||
|
Task<byte[]> ConvertToPdfAsync(string qrCodeBase64, int size = 300);
|
||||||
|
Task<string> GenerateDynamicQRAsync(QRGenerationRequest request, string userId);
|
||||||
|
Task<bool> UpdateDynamicQRAsync(string qrId, string newContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Services/IUserService.cs
Normal file
26
Services/IUserService.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using QRRapidoApp.Models;
|
||||||
|
using QRRapidoApp.Models.ViewModels;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Services
|
||||||
|
{
|
||||||
|
public interface IUserService
|
||||||
|
{
|
||||||
|
Task<User?> GetUserAsync(string userId);
|
||||||
|
Task<User?> GetUserByEmailAsync(string email);
|
||||||
|
Task<User?> GetUserByProviderAsync(string provider, string providerId);
|
||||||
|
Task<User> CreateUserAsync(string email, string name, string provider, string providerId);
|
||||||
|
Task UpdateLastLoginAsync(string userId);
|
||||||
|
Task<bool> UpdateUserAsync(User user);
|
||||||
|
Task<int> GetDailyQRCountAsync(string? userId);
|
||||||
|
Task<int> DecrementDailyQRCountAsync(string userId);
|
||||||
|
Task<bool> CanGenerateQRAsync(string? userId, bool isPremium);
|
||||||
|
Task SaveQRToHistoryAsync(string? userId, QRGenerationResult qrResult);
|
||||||
|
Task<List<QRCodeHistory>> GetUserQRHistoryAsync(string userId, int limit = 50);
|
||||||
|
Task<QRCodeHistory?> GetQRDataAsync(string qrId);
|
||||||
|
Task<int> GetQRCountThisMonthAsync(string userId);
|
||||||
|
Task<string> GetUserEmailAsync(string userId);
|
||||||
|
Task MarkPremiumCancelledAsync(string userId, DateTime cancelledAt);
|
||||||
|
Task<List<User>> GetUsersForHistoryCleanupAsync(DateTime cutoffDate);
|
||||||
|
Task DeleteUserHistoryAsync(string userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
70
Services/MemoryDistributedCacheWrapper.cs
Normal file
70
Services/MemoryDistributedCacheWrapper.cs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Services
|
||||||
|
{
|
||||||
|
public class MemoryDistributedCacheWrapper : IDistributedCache
|
||||||
|
{
|
||||||
|
private readonly IMemoryCache _memoryCache;
|
||||||
|
|
||||||
|
public MemoryDistributedCacheWrapper(IMemoryCache memoryCache)
|
||||||
|
{
|
||||||
|
_memoryCache = memoryCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[]? Get(string key)
|
||||||
|
{
|
||||||
|
var value = _memoryCache.Get<string>(key);
|
||||||
|
return value != null ? Encoding.UTF8.GetBytes(value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<byte[]?> GetAsync(string key, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult(Get(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
|
||||||
|
{
|
||||||
|
var stringValue = Encoding.UTF8.GetString(value);
|
||||||
|
var memoryCacheOptions = new MemoryCacheEntryOptions();
|
||||||
|
|
||||||
|
if (options.AbsoluteExpiration.HasValue)
|
||||||
|
memoryCacheOptions.AbsoluteExpiration = options.AbsoluteExpiration;
|
||||||
|
else if (options.AbsoluteExpirationRelativeToNow.HasValue)
|
||||||
|
memoryCacheOptions.AbsoluteExpirationRelativeToNow = options.AbsoluteExpirationRelativeToNow;
|
||||||
|
|
||||||
|
if (options.SlidingExpiration.HasValue)
|
||||||
|
memoryCacheOptions.SlidingExpiration = options.SlidingExpiration;
|
||||||
|
|
||||||
|
_memoryCache.Set(key, stringValue, memoryCacheOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
Set(key, value, options);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Refresh(string key)
|
||||||
|
{
|
||||||
|
// Memory cache doesn't need refresh
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RefreshAsync(string key, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(string key)
|
||||||
|
{
|
||||||
|
_memoryCache.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RemoveAsync(string key, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
Remove(key);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
257
Services/QRRapidoService.cs
Normal file
257
Services/QRRapidoService.cs
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using QRCoder;
|
||||||
|
using QRRapidoApp.Models.ViewModels;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Services
|
||||||
|
{
|
||||||
|
public class QRRapidoService : IQRCodeService
|
||||||
|
{
|
||||||
|
private readonly IDistributedCache _cache;
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
private readonly ILogger<QRRapidoService> _logger;
|
||||||
|
private readonly SemaphoreSlim _semaphore;
|
||||||
|
|
||||||
|
public QRRapidoService(IDistributedCache cache, IConfiguration config, ILogger<QRRapidoService> logger)
|
||||||
|
{
|
||||||
|
_cache = cache;
|
||||||
|
_config = config;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
// Limit simultaneous generations to maintain performance
|
||||||
|
var maxConcurrent = _config.GetValue<int>("Performance:MaxConcurrentGenerations", 100);
|
||||||
|
_semaphore = new SemaphoreSlim(maxConcurrent, maxConcurrent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<QRGenerationResult> GenerateRapidAsync(QRGenerationRequest request)
|
||||||
|
{
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _semaphore.WaitAsync();
|
||||||
|
|
||||||
|
// Cache key based on content and settings
|
||||||
|
var cacheKey = GenerateCacheKey(request);
|
||||||
|
var cached = await _cache.GetStringAsync(cacheKey);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(cached))
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
_logger.LogInformation($"QR code served from cache in {stopwatch.ElapsedMilliseconds}ms");
|
||||||
|
|
||||||
|
var cachedResult = JsonSerializer.Deserialize<QRGenerationResult>(cached);
|
||||||
|
if (cachedResult != null)
|
||||||
|
{
|
||||||
|
cachedResult.GenerationTimeMs = stopwatch.ElapsedMilliseconds;
|
||||||
|
cachedResult.FromCache = true;
|
||||||
|
return cachedResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimized generation
|
||||||
|
var qrCode = await GenerateQRCodeOptimizedAsync(request);
|
||||||
|
var base64 = Convert.ToBase64String(qrCode);
|
||||||
|
|
||||||
|
var result = new QRGenerationResult
|
||||||
|
{
|
||||||
|
QRCodeBase64 = base64,
|
||||||
|
QRId = Guid.NewGuid().ToString(),
|
||||||
|
GenerationTimeMs = stopwatch.ElapsedMilliseconds,
|
||||||
|
FromCache = false,
|
||||||
|
Size = qrCode.Length,
|
||||||
|
RequestSettings = request,
|
||||||
|
Success = true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cache for configurable time
|
||||||
|
var cacheExpiration = TimeSpan.FromMinutes(_config.GetValue<int>("Performance:CacheExpirationMinutes", 60));
|
||||||
|
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(result), new DistributedCacheEntryOptions
|
||||||
|
{
|
||||||
|
AbsoluteExpirationRelativeToNow = cacheExpiration
|
||||||
|
});
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
|
_logger.LogInformation($"QR code generated in {stopwatch.ElapsedMilliseconds}ms for type {request.Type}");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error in rapid QR code generation: {ex.Message}");
|
||||||
|
return new QRGenerationResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
ErrorMessage = ex.Message,
|
||||||
|
GenerationTimeMs = stopwatch.ElapsedMilliseconds
|
||||||
|
};
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> GenerateQRCodeOptimizedAsync(QRGenerationRequest request)
|
||||||
|
{
|
||||||
|
return await Task.Run(() =>
|
||||||
|
{
|
||||||
|
using var qrGenerator = new QRCodeGenerator();
|
||||||
|
using var qrCodeData = qrGenerator.CreateQrCode(request.Content, GetErrorCorrectionLevel(request));
|
||||||
|
|
||||||
|
// Optimized settings for speed
|
||||||
|
using var qrCode = new PngByteQRCode(qrCodeData);
|
||||||
|
|
||||||
|
// Apply optimizations based on user type
|
||||||
|
var pixelsPerModule = request.IsPremium ?
|
||||||
|
GetOptimalPixelsPerModule(request.Size) :
|
||||||
|
Math.Max(8, request.Size / 40); // Lower quality for free users, but faster
|
||||||
|
|
||||||
|
var primaryColorBytes = ColorToBytes(ParseHtmlColor(request.PrimaryColor));
|
||||||
|
var backgroundColorBytes = ColorToBytes(ParseHtmlColor(request.BackgroundColor));
|
||||||
|
|
||||||
|
return qrCode.GetGraphic(pixelsPerModule, primaryColorBytes, backgroundColorBytes);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private QRCodeGenerator.ECCLevel GetErrorCorrectionLevel(QRGenerationRequest request)
|
||||||
|
{
|
||||||
|
// Lower error correction = faster generation
|
||||||
|
if (request.OptimizeForSpeed)
|
||||||
|
{
|
||||||
|
return QRCodeGenerator.ECCLevel.L; // ~7% correction
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.HasLogo ?
|
||||||
|
QRCodeGenerator.ECCLevel.H : // ~30% correction for logos
|
||||||
|
QRCodeGenerator.ECCLevel.M; // ~15% correction default
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetOptimalPixelsPerModule(int targetSize)
|
||||||
|
{
|
||||||
|
// Optimized algorithm for best quality/speed ratio
|
||||||
|
return targetSize switch
|
||||||
|
{
|
||||||
|
<= 200 => 8,
|
||||||
|
<= 300 => 12,
|
||||||
|
<= 500 => 16,
|
||||||
|
<= 800 => 20,
|
||||||
|
_ => 24
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GenerateCacheKey(QRGenerationRequest request)
|
||||||
|
{
|
||||||
|
var keyData = $"{request.Content}|{request.Type}|{request.Size}|{request.PrimaryColor}|{request.BackgroundColor}|{request.QuickStyle}|{request.CornerStyle}|{request.Margin}";
|
||||||
|
using var sha256 = SHA256.Create();
|
||||||
|
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(keyData));
|
||||||
|
return $"qr_rapid_{Convert.ToBase64String(hash)[..16]}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<byte[]> ConvertToSvgAsync(string qrCodeBase64)
|
||||||
|
{
|
||||||
|
return await Task.Run(() =>
|
||||||
|
{
|
||||||
|
// Convert PNG to SVG (simplified implementation)
|
||||||
|
var svgContent = $@"<?xml version=""1.0"" encoding=""UTF-8""?>
|
||||||
|
<svg xmlns=""http://www.w3.org/2000/svg"" viewBox=""0 0 300 300"">
|
||||||
|
<rect width=""300"" height=""300"" fill=""white""/>
|
||||||
|
<image href=""data:image/png;base64,{qrCodeBase64}"" width=""300"" height=""300""/>
|
||||||
|
</svg>";
|
||||||
|
return Encoding.UTF8.GetBytes(svgContent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<byte[]> ConvertToPdfAsync(string qrCodeBase64, int size = 300)
|
||||||
|
{
|
||||||
|
return await Task.Run(() =>
|
||||||
|
{
|
||||||
|
// Simplified PDF generation - in real implementation, use iTextSharp or similar
|
||||||
|
var pdfHeader = "%PDF-1.4\n";
|
||||||
|
var pdfBody = $"1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n" +
|
||||||
|
$"2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj\n" +
|
||||||
|
$"3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 {size} {size}]>>endobj\n" +
|
||||||
|
$"xref\n0 4\n0000000000 65535 f \n0000000010 00000 n \n0000000053 00000 n \n0000000125 00000 n \n";
|
||||||
|
|
||||||
|
var pdfContent = pdfBody + $"trailer<</Size 4/Root 1 0 R>>\nstartxref\n{pdfHeader.Length + pdfBody.Length}\n%%EOF";
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetBytes(pdfHeader + pdfContent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GenerateDynamicQRAsync(QRGenerationRequest request, string userId)
|
||||||
|
{
|
||||||
|
// For premium users only - dynamic QR codes that can be edited
|
||||||
|
var dynamicId = Guid.NewGuid().ToString();
|
||||||
|
var dynamicUrl = $"https://qrrapido.site/d/{dynamicId}";
|
||||||
|
|
||||||
|
// Store mapping in cache/database
|
||||||
|
var cacheKey = $"dynamic_qr_{dynamicId}";
|
||||||
|
await _cache.SetStringAsync(cacheKey, request.Content, new DistributedCacheEntryOptions
|
||||||
|
{
|
||||||
|
SlidingExpiration = TimeSpan.FromDays(365) // Long-lived for premium users
|
||||||
|
});
|
||||||
|
|
||||||
|
return dynamicId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateDynamicQRAsync(string qrId, string newContent)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cacheKey = $"dynamic_qr_{qrId}";
|
||||||
|
await _cache.SetStringAsync(cacheKey, newContent, new DistributedCacheEntryOptions
|
||||||
|
{
|
||||||
|
SlidingExpiration = TimeSpan.FromDays(365)
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Failed to update dynamic QR {qrId}: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private System.Drawing.Color ParseHtmlColor(string htmlColor)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(htmlColor) || !htmlColor.StartsWith("#"))
|
||||||
|
{
|
||||||
|
return System.Drawing.Color.Black;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (htmlColor.Length == 7) // #RRGGBB
|
||||||
|
{
|
||||||
|
var r = Convert.ToByte(htmlColor.Substring(1, 2), 16);
|
||||||
|
var g = Convert.ToByte(htmlColor.Substring(3, 2), 16);
|
||||||
|
var b = Convert.ToByte(htmlColor.Substring(5, 2), 16);
|
||||||
|
return System.Drawing.Color.FromArgb(r, g, b);
|
||||||
|
}
|
||||||
|
else if (htmlColor.Length == 4) // #RGB
|
||||||
|
{
|
||||||
|
var r = Convert.ToByte(htmlColor.Substring(1, 1) + htmlColor.Substring(1, 1), 16);
|
||||||
|
var g = Convert.ToByte(htmlColor.Substring(2, 1) + htmlColor.Substring(2, 1), 16);
|
||||||
|
var b = Convert.ToByte(htmlColor.Substring(3, 1) + htmlColor.Substring(3, 1), 16);
|
||||||
|
return System.Drawing.Color.FromArgb(r, g, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return System.Drawing.Color.Black;
|
||||||
|
}
|
||||||
|
|
||||||
|
return System.Drawing.Color.Black;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] ColorToBytes(System.Drawing.Color color)
|
||||||
|
{
|
||||||
|
return new byte[] { color.R, color.G, color.B };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
299
Services/StripeService.cs
Normal file
299
Services/StripeService.cs
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
using Stripe;
|
||||||
|
using Stripe.Checkout;
|
||||||
|
using QRRapidoApp.Models;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Services
|
||||||
|
{
|
||||||
|
public class StripeService
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly ILogger<StripeService> _logger;
|
||||||
|
|
||||||
|
public StripeService(IConfiguration config, IUserService userService, ILogger<StripeService> logger)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_userService = userService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> CreateCheckoutSessionAsync(string userId, string priceId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var options = new SessionCreateOptions
|
||||||
|
{
|
||||||
|
PaymentMethodTypes = new List<string> { "card" },
|
||||||
|
Mode = "subscription",
|
||||||
|
LineItems = new List<SessionLineItemOptions>
|
||||||
|
{
|
||||||
|
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<string, string>
|
||||||
|
{
|
||||||
|
{ "userId", userId },
|
||||||
|
{ "product", "QR Rapido Premium" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error creating Stripe checkout session for user {userId}: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HandleWebhookAsync(string json, string signature)
|
||||||
|
{
|
||||||
|
var webhookSecret = _config["Stripe:WebhookSecret"];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (StripeException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Stripe webhook error: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
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<User?> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetSubscriptionStatusAsync(string? subscriptionId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(subscriptionId))
|
||||||
|
return "None";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var service = new SubscriptionService();
|
||||||
|
var subscription = await service.GetAsync(subscriptionId);
|
||||||
|
return subscription.Status;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error getting subscription status for {subscriptionId}: {ex.Message}");
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CancelSubscriptionAsync(string subscriptionId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var service = new SubscriptionService();
|
||||||
|
var subscription = await service.CancelAsync(subscriptionId, new SubscriptionCancelOptions
|
||||||
|
{
|
||||||
|
InvoiceNow = false,
|
||||||
|
Prorate = false
|
||||||
|
});
|
||||||
|
|
||||||
|
_logger.LogInformation($"Canceled subscription {subscriptionId}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error canceling subscription {subscriptionId}: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
382
Services/UserService.cs
Normal file
382
Services/UserService.cs
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
using MongoDB.Driver;
|
||||||
|
using QRRapidoApp.Data;
|
||||||
|
using QRRapidoApp.Models;
|
||||||
|
using QRRapidoApp.Models.ViewModels;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Services
|
||||||
|
{
|
||||||
|
public class UserService : IUserService
|
||||||
|
{
|
||||||
|
private readonly MongoDbContext _context;
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
private readonly ILogger<UserService> _logger;
|
||||||
|
|
||||||
|
public UserService(MongoDbContext context, IConfiguration config, ILogger<UserService> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_config = config;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User?> GetUserAsync(string userId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_context.Users == null) return null; // Development mode without MongoDB
|
||||||
|
|
||||||
|
return await _context.Users.Find(u => u.Id == userId).FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error getting user {userId}: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User?> GetUserByEmailAsync(string email)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_context.Users == null) return null; // Development mode without MongoDB
|
||||||
|
|
||||||
|
return await _context.Users.Find(u => u.Email == email).FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error getting user by email {email}: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User?> GetUserByProviderAsync(string provider, string providerId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _context.Users
|
||||||
|
.Find(u => u.Provider == provider && u.ProviderId == providerId)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error getting user by provider {provider}:{providerId}: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User> CreateUserAsync(string email, string name, string provider, string providerId)
|
||||||
|
{
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
Email = email,
|
||||||
|
Name = name,
|
||||||
|
Provider = provider,
|
||||||
|
ProviderId = providerId,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
LastLoginAt = DateTime.UtcNow,
|
||||||
|
PreferredLanguage = "pt-BR",
|
||||||
|
DailyQRCount = 0,
|
||||||
|
LastQRDate = DateTime.UtcNow.Date,
|
||||||
|
TotalQRGenerated = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateLastLoginAsync(string userId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var update = Builders<User>.Update
|
||||||
|
.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)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error updating last login for user {userId}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateUserAsync(User user)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _context.Users.ReplaceOneAsync(u => u.Id == user.Id, user);
|
||||||
|
return result.ModifiedCount > 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error updating user {user.Id}: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetDailyQRCountAsync(string? userId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
return 0; // Anonymous users tracked separately
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var user = await GetUserAsync(userId);
|
||||||
|
if (user == null) return 0;
|
||||||
|
|
||||||
|
// Reset count if it's a new day
|
||||||
|
if (user.LastQRDate.Date < DateTime.UtcNow.Date)
|
||||||
|
{
|
||||||
|
user.DailyQRCount = 0;
|
||||||
|
user.LastQRDate = DateTime.UtcNow.Date;
|
||||||
|
await UpdateUserAsync(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.DailyQRCount;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error getting daily QR count for user {userId}: {ex.Message}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> DecrementDailyQRCountAsync(string userId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var user = await GetUserAsync(userId);
|
||||||
|
if (user == null) return 0;
|
||||||
|
|
||||||
|
// Reset count if it's a new day
|
||||||
|
if (user.LastQRDate.Date < DateTime.UtcNow.Date)
|
||||||
|
{
|
||||||
|
user.DailyQRCount = 1;
|
||||||
|
user.LastQRDate = DateTime.UtcNow.Date;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
user.DailyQRCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.TotalQRGenerated++;
|
||||||
|
await UpdateUserAsync(user);
|
||||||
|
|
||||||
|
// Calculate remaining QRs for free users
|
||||||
|
var dailyLimit = user.IsPremium ? int.MaxValue : _config.GetValue<int>("Premium:FreeQRLimit", 50);
|
||||||
|
return Math.Max(0, dailyLimit - user.DailyQRCount);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error decrementing daily QR count for user {userId}: {ex.Message}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanGenerateQRAsync(string? userId, bool isPremium)
|
||||||
|
{
|
||||||
|
if (isPremium) return true;
|
||||||
|
|
||||||
|
var dailyCount = await GetDailyQRCountAsync(userId);
|
||||||
|
var limit = string.IsNullOrEmpty(userId) ? 10 : _config.GetValue<int>("Premium:FreeQRLimit", 50);
|
||||||
|
|
||||||
|
return dailyCount < limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveQRToHistoryAsync(string? userId, QRGenerationResult qrResult)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var qrHistory = new QRCodeHistory
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
Type = qrResult.RequestSettings?.Type ?? "unknown",
|
||||||
|
Content = qrResult.RequestSettings?.Content ?? "",
|
||||||
|
QRCodeBase64 = qrResult.QRCodeBase64,
|
||||||
|
CustomizationSettings = JsonSerializer.Serialize(qrResult.RequestSettings),
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
Language = qrResult.RequestSettings?.Language ?? "pt-BR",
|
||||||
|
Size = qrResult.RequestSettings?.Size ?? 300,
|
||||||
|
GenerationTimeMs = qrResult.GenerationTimeMs,
|
||||||
|
FromCache = qrResult.FromCache,
|
||||||
|
IsActive = true,
|
||||||
|
LastAccessedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
await _context.QRCodeHistory.InsertOneAsync(qrHistory);
|
||||||
|
|
||||||
|
// Update user's QR history IDs if logged in
|
||||||
|
if (!string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
var update = Builders<User>.Update
|
||||||
|
.Push(u => u.QRHistoryIds, qrHistory.Id);
|
||||||
|
await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error saving QR to history: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<QRCodeHistory>> GetUserQRHistoryAsync(string userId, int limit = 50)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _context.QRCodeHistory
|
||||||
|
.Find(q => q.UserId == userId && q.IsActive)
|
||||||
|
.SortByDescending(q => q.CreatedAt)
|
||||||
|
.Limit(limit)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error getting QR history for user {userId}: {ex.Message}");
|
||||||
|
return new List<QRCodeHistory>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<QRCodeHistory?> GetQRDataAsync(string qrId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _context.QRCodeHistory
|
||||||
|
.Find(q => q.Id == qrId && q.IsActive)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error getting QR data {qrId}: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetQRCountThisMonthAsync(string userId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var startOfMonth = new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, 1);
|
||||||
|
var endOfMonth = startOfMonth.AddMonths(1);
|
||||||
|
|
||||||
|
var count = await _context.QRCodeHistory
|
||||||
|
.CountDocumentsAsync(q => q.UserId == userId &&
|
||||||
|
q.CreatedAt >= startOfMonth &&
|
||||||
|
q.CreatedAt < endOfMonth);
|
||||||
|
|
||||||
|
return (int)count;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error getting monthly QR count for user {userId}: {ex.Message}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MÉTODO REMOVIDO: ExtendAdFreeTimeAsync - não é mais necessário
|
||||||
|
|
||||||
|
public async Task<string> GetUserEmailAsync(string userId)
|
||||||
|
{
|
||||||
|
var user = await GetUserAsync(userId);
|
||||||
|
return user?.Email ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task MarkPremiumCancelledAsync(string userId, DateTime cancelledAt)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_context.Users == null) return; // Development mode without MongoDB
|
||||||
|
|
||||||
|
var update = Builders<User>.Update
|
||||||
|
.Set(u => u.IsPremium, false)
|
||||||
|
.Set(u => u.PremiumCancelledAt, cancelledAt)
|
||||||
|
.Set(u => u.PremiumExpiresAt, null);
|
||||||
|
|
||||||
|
await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
|
||||||
|
|
||||||
|
_logger.LogInformation($"Marked premium as cancelled for user {userId} at {cancelledAt}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error marking premium cancelled for user {userId}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<User>> GetUsersForHistoryCleanupAsync(DateTime cutoffDate)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_context.Users == null) return new List<User>(); // Development mode without MongoDB
|
||||||
|
|
||||||
|
return await _context.Users
|
||||||
|
.Find(u => u.PremiumCancelledAt != null &&
|
||||||
|
u.PremiumCancelledAt < cutoffDate &&
|
||||||
|
u.QRHistoryIds.Count > 0)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error getting users for history cleanup: {ex.Message}");
|
||||||
|
return new List<User>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteUserHistoryAsync(string userId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_context.Users == null || _context.QRCodeHistory == null) return; // Development mode without MongoDB
|
||||||
|
|
||||||
|
var user = await GetUserAsync(userId);
|
||||||
|
if (user?.QRHistoryIds?.Any() == true)
|
||||||
|
{
|
||||||
|
// Remover histórico de QR codes
|
||||||
|
await _context.QRCodeHistory.DeleteManyAsync(qr => user.QRHistoryIds.Contains(qr.Id));
|
||||||
|
|
||||||
|
// Limpar lista de histórico do usuário
|
||||||
|
var update = Builders<User>.Update.Set(u => u.QRHistoryIds, new List<string>());
|
||||||
|
await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
|
||||||
|
|
||||||
|
_logger.LogInformation($"Deleted history for user {userId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error deleting history for user {userId}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateAdFreeSessionAsync(string userId, string sessionType, int? customMinutes = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var durationMinutes = customMinutes ?? _config.GetValue<int>("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
|
||||||
|
};
|
||||||
|
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
Tests/QRRapidoApp.Tests.csproj
Normal file
28
Tests/QRRapidoApp.Tests.csproj
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.6.1" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Moq" Version="4.20.69" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||||
|
<PackageReference Include="MongoDB.Driver" Version="2.22.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../QRRapidoApp.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
266
Tests/Services/AdDisplayServiceTests.cs
Normal file
266
Tests/Services/AdDisplayServiceTests.cs
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
using Moq;
|
||||||
|
using QRRapidoApp.Data;
|
||||||
|
using QRRapidoApp.Models;
|
||||||
|
using QRRapidoApp.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Tests.Services
|
||||||
|
{
|
||||||
|
public class AdDisplayServiceTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IUserService> _userServiceMock;
|
||||||
|
private readonly Mock<MongoDbContext> _contextMock;
|
||||||
|
private readonly Mock<IConfiguration> _configMock;
|
||||||
|
private readonly Mock<ILogger<AdDisplayService>> _loggerMock;
|
||||||
|
private readonly Mock<IMongoCollection<AdFreeSession>> _sessionCollectionMock;
|
||||||
|
private readonly AdDisplayService _service;
|
||||||
|
|
||||||
|
public AdDisplayServiceTests()
|
||||||
|
{
|
||||||
|
_userServiceMock = new Mock<IUserService>();
|
||||||
|
_contextMock = new Mock<MongoDbContext>(Mock.Of<IMongoClient>(), Mock.Of<IConfiguration>());
|
||||||
|
_configMock = new Mock<IConfiguration>();
|
||||||
|
_loggerMock = new Mock<ILogger<AdDisplayService>>();
|
||||||
|
_sessionCollectionMock = new Mock<IMongoCollection<AdFreeSession>>();
|
||||||
|
|
||||||
|
_contextMock.Setup(c => c.AdFreeSessions).Returns(_sessionCollectionMock.Object);
|
||||||
|
|
||||||
|
_service = new AdDisplayService(
|
||||||
|
_userServiceMock.Object,
|
||||||
|
_contextMock.Object,
|
||||||
|
_configMock.Object,
|
||||||
|
_loggerMock.Object
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ShouldShowAds_WithAnonymousUser_ShouldReturnTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
string? userId = null;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.ShouldShowAds(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ShouldShowAds_WithPremiumUser_ShouldReturnFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userId = "test-user-id";
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
Id = userId,
|
||||||
|
IsPremium = true,
|
||||||
|
PremiumExpiresAt = DateTime.UtcNow.AddDays(30)
|
||||||
|
};
|
||||||
|
|
||||||
|
_userServiceMock.Setup(s => s.GetUserAsync(userId))
|
||||||
|
.ReturnsAsync(user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.ShouldShowAds(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ShouldShowAds_WithExpiredPremiumUser_ShouldReturnTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userId = "test-user-id";
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
Id = userId,
|
||||||
|
IsPremium = true,
|
||||||
|
PremiumExpiresAt = DateTime.UtcNow.AddDays(-1) // Expired
|
||||||
|
};
|
||||||
|
|
||||||
|
_userServiceMock.Setup(s => s.GetUserAsync(userId))
|
||||||
|
.ReturnsAsync(user);
|
||||||
|
|
||||||
|
var cursor = new Mock<IAsyncCursor<AdFreeSession>>();
|
||||||
|
cursor.Setup(_ => _.Current).Returns(new List<AdFreeSession>());
|
||||||
|
cursor.SetupSequence(_ => _.MoveNext(It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(false);
|
||||||
|
cursor.SetupSequence(_ => _.MoveNextAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(false);
|
||||||
|
|
||||||
|
_sessionCollectionMock.Setup(c => c.FindAsync(
|
||||||
|
It.IsAny<FilterDefinition<AdFreeSession>>(),
|
||||||
|
It.IsAny<FindOptions<AdFreeSession>>(),
|
||||||
|
It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(cursor.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.ShouldShowAds(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ShouldShowAds_WithActiveAdFreeSession_ShouldReturnFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userId = "test-user-id";
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
Id = userId,
|
||||||
|
IsPremium = false
|
||||||
|
};
|
||||||
|
|
||||||
|
var activeSession = new AdFreeSession
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
IsActive = true,
|
||||||
|
ExpiresAt = DateTime.UtcNow.AddDays(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
_userServiceMock.Setup(s => s.GetUserAsync(userId))
|
||||||
|
.ReturnsAsync(user);
|
||||||
|
|
||||||
|
var cursor = new Mock<IAsyncCursor<AdFreeSession>>();
|
||||||
|
cursor.Setup(_ => _.Current).Returns(new List<AdFreeSession> { activeSession });
|
||||||
|
cursor.SetupSequence(_ => _.MoveNext(It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(true)
|
||||||
|
.Returns(false);
|
||||||
|
cursor.SetupSequence(_ => _.MoveNextAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(true)
|
||||||
|
.ReturnsAsync(false);
|
||||||
|
|
||||||
|
_sessionCollectionMock.Setup(c => c.FindAsync(
|
||||||
|
It.IsAny<FilterDefinition<AdFreeSession>>(),
|
||||||
|
It.IsAny<FindOptions<AdFreeSession>>(),
|
||||||
|
It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(cursor.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.ShouldShowAds(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TESTE REMOVIDO: GetAdFreeTimeRemaining_WithPremiumUser_ShouldReturnMaxValue - método não existe mais
|
||||||
|
|
||||||
|
// TESTES REMOVIDOS: GetAdFreeTimeRemaining - método não existe mais
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HasValidPremiumSubscription_WithValidPremium_ShouldReturnTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userId = "test-user-id";
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
Id = userId,
|
||||||
|
IsPremium = true,
|
||||||
|
PremiumExpiresAt = DateTime.UtcNow.AddDays(30)
|
||||||
|
};
|
||||||
|
|
||||||
|
_userServiceMock.Setup(s => s.GetUserAsync(userId))
|
||||||
|
.ReturnsAsync(user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.HasValidPremiumSubscription(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HasValidPremiumSubscription_WithExpiredPremium_ShouldReturnFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userId = "test-user-id";
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
Id = userId,
|
||||||
|
IsPremium = true,
|
||||||
|
PremiumExpiresAt = DateTime.UtcNow.AddDays(-1) // Expired
|
||||||
|
};
|
||||||
|
|
||||||
|
_userServiceMock.Setup(s => s.GetUserAsync(userId))
|
||||||
|
.ReturnsAsync(user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.HasValidPremiumSubscription(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetAdFreeStatusAsync_WithPremiumUser_ShouldReturnPremium()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userId = "test-user-id";
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
Id = userId,
|
||||||
|
IsPremium = true,
|
||||||
|
PremiumExpiresAt = DateTime.UtcNow.AddDays(30)
|
||||||
|
};
|
||||||
|
|
||||||
|
_userServiceMock.Setup(s => s.GetUserAsync(userId))
|
||||||
|
.ReturnsAsync(user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.GetAdFreeStatusAsync(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Premium", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetAdFreeStatusAsync_WithActiveSession_ShouldReturnSessionType()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userId = "test-user-id";
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
Id = userId,
|
||||||
|
IsPremium = false
|
||||||
|
};
|
||||||
|
|
||||||
|
var activeSession = new AdFreeSession
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
IsActive = true,
|
||||||
|
ExpiresAt = DateTime.UtcNow.AddDays(1),
|
||||||
|
SessionType = "Login"
|
||||||
|
};
|
||||||
|
|
||||||
|
_userServiceMock.Setup(s => s.GetUserAsync(userId))
|
||||||
|
.ReturnsAsync(user);
|
||||||
|
|
||||||
|
var cursor = new Mock<IAsyncCursor<AdFreeSession>>();
|
||||||
|
cursor.Setup(_ => _.Current).Returns(new List<AdFreeSession> { activeSession });
|
||||||
|
cursor.SetupSequence(_ => _.MoveNext(It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(true)
|
||||||
|
.Returns(false);
|
||||||
|
cursor.SetupSequence(_ => _.MoveNextAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(true)
|
||||||
|
.ReturnsAsync(false);
|
||||||
|
|
||||||
|
_sessionCollectionMock.Setup(c => c.FindAsync(
|
||||||
|
It.IsAny<FilterDefinition<AdFreeSession>>(),
|
||||||
|
It.IsAny<FindOptions<AdFreeSession>>(),
|
||||||
|
It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(cursor.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.GetAdFreeStatusAsync(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Login", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
235
Tests/Services/QRRapidoServiceTests.cs
Normal file
235
Tests/Services/QRRapidoServiceTests.cs
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
using QRRapidoApp.Models.ViewModels;
|
||||||
|
using QRRapidoApp.Services;
|
||||||
|
using System.Text;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Tests.Services
|
||||||
|
{
|
||||||
|
public class QRRapidoServiceTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IDistributedCache> _cacheMock;
|
||||||
|
private readonly Mock<IConfiguration> _configMock;
|
||||||
|
private readonly Mock<ILogger<QRRapidoService>> _loggerMock;
|
||||||
|
private readonly QRRapidoService _service;
|
||||||
|
|
||||||
|
public QRRapidoServiceTests()
|
||||||
|
{
|
||||||
|
_cacheMock = new Mock<IDistributedCache>();
|
||||||
|
_configMock = new Mock<IConfiguration>();
|
||||||
|
_loggerMock = new Mock<ILogger<QRRapidoService>>();
|
||||||
|
|
||||||
|
SetupDefaultConfiguration();
|
||||||
|
_service = new QRRapidoService(_cacheMock.Object, _configMock.Object, _loggerMock.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupDefaultConfiguration()
|
||||||
|
{
|
||||||
|
_configMock.Setup(c => c.GetValue<int>("Performance:MaxConcurrentGenerations", 100))
|
||||||
|
.Returns(100);
|
||||||
|
_configMock.Setup(c => c.GetValue<int>("Performance:CacheExpirationMinutes", 60))
|
||||||
|
.Returns(60);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GenerateRapidAsync_WithValidRequest_ShouldReturnSuccessResult()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var request = new QRGenerationRequest
|
||||||
|
{
|
||||||
|
Type = "url",
|
||||||
|
Content = "https://example.com",
|
||||||
|
Size = 300,
|
||||||
|
PrimaryColor = "#000000",
|
||||||
|
BackgroundColor = "#FFFFFF",
|
||||||
|
OptimizeForSpeed = true
|
||||||
|
};
|
||||||
|
|
||||||
|
_cacheMock.Setup(c => c.GetStringAsync(It.IsAny<string>(), default))
|
||||||
|
.ReturnsAsync((string)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.GenerateRapidAsync(request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.True(result.Success);
|
||||||
|
Assert.False(string.IsNullOrEmpty(result.QRCodeBase64));
|
||||||
|
Assert.False(string.IsNullOrEmpty(result.QRId));
|
||||||
|
Assert.True(result.GenerationTimeMs > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GenerateRapidAsync_WithEmptyContent_ShouldReturnError()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var request = new QRGenerationRequest
|
||||||
|
{
|
||||||
|
Type = "url",
|
||||||
|
Content = "",
|
||||||
|
Size = 300
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.GenerateRapidAsync(request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.False(result.Success);
|
||||||
|
Assert.False(string.IsNullOrEmpty(result.ErrorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GenerateRapidAsync_WithCachedResult_ShouldReturnFromCache()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var request = new QRGenerationRequest
|
||||||
|
{
|
||||||
|
Type = "url",
|
||||||
|
Content = "https://example.com",
|
||||||
|
Size = 300
|
||||||
|
};
|
||||||
|
|
||||||
|
var cachedResult = new QRGenerationResult
|
||||||
|
{
|
||||||
|
QRCodeBase64 = "cached-base64",
|
||||||
|
QRId = "cached-id",
|
||||||
|
Success = true,
|
||||||
|
FromCache = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var cachedJson = System.Text.Json.JsonSerializer.Serialize(cachedResult);
|
||||||
|
_cacheMock.Setup(c => c.GetStringAsync(It.IsAny<string>(), default))
|
||||||
|
.ReturnsAsync(cachedJson);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.GenerateRapidAsync(request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.True(result.Success);
|
||||||
|
Assert.True(result.FromCache);
|
||||||
|
Assert.Equal("cached-base64", result.QRCodeBase64);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConvertToSvgAsync_WithValidBase64_ShouldReturnSvgContent()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.ConvertToSvgAsync(base64);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
var svgString = Encoding.UTF8.GetString(result);
|
||||||
|
Assert.Contains("<?xml", svgString);
|
||||||
|
Assert.Contains("<svg", svgString);
|
||||||
|
Assert.Contains("</svg>", svgString);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConvertToPdfAsync_WithValidBase64_ShouldReturnPdfContent()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.ConvertToPdfAsync(base64, 300);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
var pdfString = Encoding.UTF8.GetString(result);
|
||||||
|
Assert.Contains("%PDF", pdfString);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("url", "https://example.com")]
|
||||||
|
[InlineData("text", "Hello World")]
|
||||||
|
[InlineData("email", "test@example.com")]
|
||||||
|
public async Task GenerateRapidAsync_WithDifferentTypes_ShouldHandleAllTypes(string type, string content)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var request = new QRGenerationRequest
|
||||||
|
{
|
||||||
|
Type = type,
|
||||||
|
Content = content,
|
||||||
|
Size = 300,
|
||||||
|
OptimizeForSpeed = true
|
||||||
|
};
|
||||||
|
|
||||||
|
_cacheMock.Setup(c => c.GetStringAsync(It.IsAny<string>(), default))
|
||||||
|
.ReturnsAsync((string)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.GenerateRapidAsync(request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.True(result.Success);
|
||||||
|
Assert.False(string.IsNullOrEmpty(result.QRCodeBase64));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GenerateRapidAsync_WithPremiumUser_ShouldUseHigherQuality()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var request = new QRGenerationRequest
|
||||||
|
{
|
||||||
|
Type = "url",
|
||||||
|
Content = "https://example.com",
|
||||||
|
Size = 500,
|
||||||
|
IsPremium = true,
|
||||||
|
OptimizeForSpeed = true
|
||||||
|
};
|
||||||
|
|
||||||
|
_cacheMock.Setup(c => c.GetStringAsync(It.IsAny<string>(), default))
|
||||||
|
.ReturnsAsync((string)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.GenerateRapidAsync(request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.True(result.Success);
|
||||||
|
Assert.True(result.Size > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GenerateDynamicQRAsync_WithValidData_ShouldReturnDynamicId()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var request = new QRGenerationRequest
|
||||||
|
{
|
||||||
|
Type = "url",
|
||||||
|
Content = "https://example.com"
|
||||||
|
};
|
||||||
|
var userId = "test-user-id";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.GenerateDynamicQRAsync(request, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(string.IsNullOrEmpty(result));
|
||||||
|
Assert.True(Guid.TryParse(result, out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateDynamicQRAsync_WithValidData_ShouldReturnTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var qrId = Guid.NewGuid().ToString();
|
||||||
|
var newContent = "https://updated-example.com";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.UpdateDynamicQRAsync(qrId, newContent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
92
Views/Account/Login.cshtml
Normal file
92
Views/Account/Login.cshtml
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "Login";
|
||||||
|
var returnUrl = ViewBag.ReturnUrl ?? "/";
|
||||||
|
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-primary text-white text-center">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<i class="fas fa-sign-in-alt"></i> Entrar
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<p class="text-muted">Entre com sua conta e ganhe:</p>
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col-12 mb-2">
|
||||||
|
<div class="badge bg-success p-2 w-100">
|
||||||
|
<i class="fas fa-crown"></i> 30 dias sem anúncios
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mb-2">
|
||||||
|
<div class="badge bg-primary p-2 w-100">
|
||||||
|
<i class="fas fa-infinity"></i> 50 QR codes/dia
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mb-2">
|
||||||
|
<div class="badge bg-info p-2 w-100">
|
||||||
|
<i class="fas fa-history"></i> Histórico de QR codes
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-3">
|
||||||
|
<a href="/Account/LoginGoogle?returnUrl=@returnUrl" class="btn btn-danger btn-lg">
|
||||||
|
<i class="fab fa-google"></i> Entrar com Google
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/Account/LoginMicrosoft?returnUrl=@returnUrl" class="btn btn-primary btn-lg">
|
||||||
|
<i class="fab fa-microsoft"></i> Entrar com Microsoft
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<h6 class="text-success">
|
||||||
|
<i class="fas fa-gift"></i> Oferta Especial!
|
||||||
|
</h6>
|
||||||
|
<p class="small text-muted">
|
||||||
|
Ao fazer login, você ganha automaticamente <strong>30 dias sem anúncios</strong>
|
||||||
|
e pode gerar até <strong>50 QR codes por dia</strong> gratuitamente.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<small class="text-muted">
|
||||||
|
Não cadastramos você sem sua permissão. <br>
|
||||||
|
<a href="/Home/Privacy" class="text-primary">Política de Privacidade</a>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="/" class="text-muted">
|
||||||
|
<i class="fas fa-arrow-left"></i> Voltar ao gerador
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script>
|
||||||
|
// Track login attempts
|
||||||
|
document.querySelectorAll('[href*="Login"]').forEach(link => {
|
||||||
|
link.addEventListener('click', () => {
|
||||||
|
if (typeof gtag !== 'undefined') {
|
||||||
|
gtag('event', 'login_attempt', {
|
||||||
|
'event_category': 'Authentication',
|
||||||
|
'method': link.textContent.includes('Google') ? 'google' : 'microsoft'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
435
Views/Home/Index.cshtml
Normal file
435
Views/Home/Index.cshtml
Normal file
@ -0,0 +1,435 @@
|
|||||||
|
@using QRRapidoApp.Services
|
||||||
|
@inject AdDisplayService AdService
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Home";
|
||||||
|
var userId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<!-- QR Generator Form -->
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h3 class="h5 mb-0">
|
||||||
|
<i class="fas fa-qrcode"></i> Criar QR Code Rapidamente
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
@if (User.Identity.IsAuthenticated)
|
||||||
|
{
|
||||||
|
var isPremium = await AdService.HasValidPremiumSubscription(userId);
|
||||||
|
@if (isPremium)
|
||||||
|
{
|
||||||
|
<div class="alert alert-success border-0">
|
||||||
|
<i class="fas fa-crown text-warning"></i>
|
||||||
|
<strong>Usuário Premium ativo!</strong>
|
||||||
|
<span class="badge bg-success">Sem anúncios + Histórico + QR ilimitados</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<form id="qr-speed-form" class="needs-validation" novalidate>
|
||||||
|
<!-- Generation timer -->
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<div class="generation-timer d-none">
|
||||||
|
<i class="fas fa-stopwatch text-primary"></i>
|
||||||
|
<span class="fw-bold text-primary">0.0s</span>
|
||||||
|
</div>
|
||||||
|
<div class="speed-badge d-none">
|
||||||
|
<span class="badge bg-success">
|
||||||
|
<i class="fas fa-bolt"></i> Geração ultra rápida!
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 text-end">
|
||||||
|
@if (User.Identity.IsAuthenticated)
|
||||||
|
{
|
||||||
|
<small class="text-muted">
|
||||||
|
<span class="qr-counter">Ilimitado hoje</span>
|
||||||
|
</small>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<small class="text-muted">
|
||||||
|
<span class="qr-counter">10 QR codes restantes</span>
|
||||||
|
</small>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">
|
||||||
|
<i class="fas fa-list"></i> Tipo de QR Code
|
||||||
|
</label>
|
||||||
|
<select id="qr-type" class="form-select" required>
|
||||||
|
<option value="">Selecione o tipo...</option>
|
||||||
|
<option value="url">🌐 URL/Link</option>
|
||||||
|
<option value="text">📝 Texto Simples</option>
|
||||||
|
<option value="wifi">📶 WiFi</option>
|
||||||
|
<option value="vcard">👤 Cartão de Visita</option>
|
||||||
|
<option value="sms">💬 SMS</option>
|
||||||
|
<option value="email">📧 Email</option>
|
||||||
|
@if (User.Identity.IsAuthenticated)
|
||||||
|
{
|
||||||
|
<option value="dynamic">⚡ QR Dinâmico (Premium)</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">
|
||||||
|
<i class="fas fa-palette"></i> Estilo Rápido
|
||||||
|
</label>
|
||||||
|
<div class="btn-group w-100" role="group">
|
||||||
|
<input type="radio" class="btn-check" name="quick-style" id="style-classic" value="classic" checked>
|
||||||
|
<label class="btn btn-outline-secondary" for="style-classic">Clássico</label>
|
||||||
|
|
||||||
|
<input type="radio" class="btn-check" name="quick-style" id="style-modern" value="modern">
|
||||||
|
<label class="btn btn-outline-secondary" for="style-modern">Moderno</label>
|
||||||
|
|
||||||
|
<input type="radio" class="btn-check" name="quick-style" id="style-colorful" value="colorful">
|
||||||
|
<label class="btn btn-outline-secondary" for="style-colorful">Colorido</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">
|
||||||
|
<i class="fas fa-edit"></i> Conteúdo
|
||||||
|
</label>
|
||||||
|
<textarea id="qr-content"
|
||||||
|
class="form-control form-control-lg"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Digite o conteúdo do seu QR code aqui..."
|
||||||
|
required></textarea>
|
||||||
|
<div class="form-text">
|
||||||
|
<span id="content-hints">Dicas aparecerão aqui baseadas no tipo selecionado</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Advanced customization (collapsible) -->
|
||||||
|
<div class="accordion mb-3" id="customization-accordion">
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#customization-panel">
|
||||||
|
<i class="fas fa-sliders-h me-2"></i> Personalização Avançada
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="customization-panel" class="accordion-collapse collapse">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label class="form-label">Cor Principal</label>
|
||||||
|
<input type="color" id="primary-color" class="form-control form-control-color" value="#007BFF">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label class="form-label">Cor de Fundo</label>
|
||||||
|
<input type="color" id="bg-color" class="form-control form-control-color" value="#FFFFFF">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label class="form-label">Tamanho</label>
|
||||||
|
<select id="qr-size" class="form-select">
|
||||||
|
<option value="200">Pequeno (200px)</option>
|
||||||
|
<option value="300" selected>Médio (300px)</option>
|
||||||
|
<option value="500">Grande (500px)</option>
|
||||||
|
<option value="800">XL (800px)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label class="form-label">Margem</label>
|
||||||
|
<select id="qr-margin" class="form-select">
|
||||||
|
<option value="1">Mínima</option>
|
||||||
|
<option value="2" selected>Normal</option>
|
||||||
|
<option value="4">Grande</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (User.Identity.IsAuthenticated)
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Logo/Ícone</label>
|
||||||
|
<input type="file" id="logo-upload" class="form-control" accept="image/*">
|
||||||
|
<div class="form-text">PNG, JPG até 2MB</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Estilo das Bordas</label>
|
||||||
|
<select id="corner-style" class="form-select">
|
||||||
|
<option value="square">Quadrado</option>
|
||||||
|
<option value="rounded">Arredondado</option>
|
||||||
|
<option value="circle">Circular</option>
|
||||||
|
<option value="leaf">Folha</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg" id="generate-btn">
|
||||||
|
<i class="fas fa-bolt"></i> Gerar QR Code Rapidamente
|
||||||
|
<div class="spinner-border spinner-border-sm ms-2 d-none" role="status">
|
||||||
|
<span class="visually-hidden">Gerando...</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Speed statistics -->
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card text-center border-success">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="text-success">
|
||||||
|
<i class="fas fa-stopwatch"></i> 1.2s
|
||||||
|
</h5>
|
||||||
|
<small class="text-muted">Tempo médio</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card text-center border-primary">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="text-primary">
|
||||||
|
<i class="fas fa-chart-line"></i> 99.9%
|
||||||
|
</h5>
|
||||||
|
<small class="text-muted">Disponibilidade</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card text-center border-warning">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="text-warning">
|
||||||
|
<i class="fas fa-users"></i> <span id="total-qrs">10.5K</span>
|
||||||
|
</h5>
|
||||||
|
<small class="text-muted">QRs gerados hoje</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ad Space Between Content (conditional) -->
|
||||||
|
@await Html.PartialAsync("_AdSpace", new { position = "content" })
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar with preview and ads -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<!-- Preview with timer -->
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="fas fa-eye"></i> Preview
|
||||||
|
</h5>
|
||||||
|
<div class="generation-stats d-none">
|
||||||
|
<small class="text-success">
|
||||||
|
<i class="fas fa-check-circle"></i> Gerado em <span class="generation-time">0s</span>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div id="qr-preview" class="mb-3">
|
||||||
|
<div class="placeholder-qr p-5">
|
||||||
|
<i class="fas fa-qrcode fa-4x text-muted mb-3"></i>
|
||||||
|
<p class="text-muted">Seu QR code aparecerá aqui em segundos</p>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-bolt"></i> Geração ultra-rápida garantida
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="download-section" style="display: none;">
|
||||||
|
<div class="btn-group-vertical w-100 mb-3">
|
||||||
|
<button id="download-png" class="btn btn-success">
|
||||||
|
<i class="fas fa-download"></i> Download PNG
|
||||||
|
</button>
|
||||||
|
<button id="download-svg" class="btn btn-outline-success">
|
||||||
|
<i class="fas fa-vector-square"></i> Download SVG (Vetorial)
|
||||||
|
</button>
|
||||||
|
<button id="download-pdf" class="btn btn-outline-success">
|
||||||
|
<i class="fas fa-file-pdf"></i> Download PDF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Share Button with Dropdown -->
|
||||||
|
<div class="dropdown w-100 mb-3">
|
||||||
|
<button class="btn btn-primary dropdown-toggle w-100" type="button" id="share-qr-btn" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fas fa-share-alt"></i> Compartilhar QR Code
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu w-100" aria-labelledby="share-qr-btn" id="share-dropdown">
|
||||||
|
<!-- Native share option (mobile only) -->
|
||||||
|
<li class="d-none" id="native-share-option">
|
||||||
|
<a class="dropdown-item" href="#" id="native-share">
|
||||||
|
<i class="fas fa-mobile-alt text-primary"></i> Compartilhar (Sistema)
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<!-- WhatsApp -->
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="#" id="share-whatsapp">
|
||||||
|
<i class="fab fa-whatsapp text-success"></i> WhatsApp
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<!-- Telegram -->
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="#" id="share-telegram">
|
||||||
|
<i class="fab fa-telegram text-info"></i> Telegram
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<!-- Email -->
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="#" id="share-email">
|
||||||
|
<i class="fas fa-envelope text-warning"></i> Email
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<!-- Copy to clipboard -->
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="#" id="copy-qr-link">
|
||||||
|
<i class="fas fa-copy text-secondary"></i> Copiar Link
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<!-- Save to gallery (mobile only) -->
|
||||||
|
<li class="d-none" id="save-gallery-option">
|
||||||
|
<a class="dropdown-item" href="#" id="save-to-gallery">
|
||||||
|
<i class="fas fa-images text-purple"></i> Salvar na Galeria
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (User.Identity.IsAuthenticated)
|
||||||
|
{
|
||||||
|
<button id="save-to-history" class="btn btn-outline-primary w-100">
|
||||||
|
<i class="fas fa-save"></i> Salvar no Histórico
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="text-center">
|
||||||
|
<small class="text-muted">
|
||||||
|
<a href="/Account/Login" class="text-primary">Faça login</a>
|
||||||
|
para salvar no histórico
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Premium Card for non-premium users -->
|
||||||
|
@if (User.Identity.IsAuthenticated && await AdService.ShouldShowAds(userId))
|
||||||
|
{
|
||||||
|
<div class="card border-warning mb-4">
|
||||||
|
<div class="card-header bg-warning text-dark">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-rocket"></i> QR Rapido Premium
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="text-center mb-3">
|
||||||
|
<div class="badge bg-success mb-2">⚡ 3x Mais Rápido</div>
|
||||||
|
</div>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li><i class="fas fa-check text-success"></i> Sem anúncios para sempre</li>
|
||||||
|
<li><i class="fas fa-check text-success"></i> QR codes ilimitados</li>
|
||||||
|
<li><i class="fas fa-check text-success"></i> Geração prioritária (0.4s)</li>
|
||||||
|
<li><i class="fas fa-check text-success"></i> QR codes dinâmicos</li>
|
||||||
|
<li><i class="fas fa-check text-success"></i> Analytics em tempo real</li>
|
||||||
|
<li><i class="fas fa-check text-success"></i> API para desenvolvedores</li>
|
||||||
|
</ul>
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="/Premium/Upgrade" class="btn btn-warning w-100">
|
||||||
|
<i class="fas fa-bolt"></i> Acelerar por R$ 19,90/mês
|
||||||
|
</a>
|
||||||
|
<small class="text-muted d-block mt-1">Cancele quando quiser</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Speed Tips Card -->
|
||||||
|
<div class="card bg-light mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-lightbulb text-warning"></i> Dicas para QR Mais Rápidos
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="list-unstyled small">
|
||||||
|
<li><i class="fas fa-arrow-right text-primary"></i> URLs curtas geram mais rápido</li>
|
||||||
|
<li><i class="fas fa-arrow-right text-primary"></i> Menos texto = maior velocidade</li>
|
||||||
|
<li><i class="fas fa-arrow-right text-primary"></i> Cores sólidas otimizam o processo</li>
|
||||||
|
<li><i class="fas fa-arrow-right text-primary"></i> Tamanhos menores aceleram o download</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ad Space Sidebar (conditional) -->
|
||||||
|
@await Html.PartialAsync("_AdSpace", new { position = "sidebar" })
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Speed Comparison Section -->
|
||||||
|
<section class="mt-5 mb-4">
|
||||||
|
<div class="container">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<h3><i class="fas fa-tachometer-alt text-primary"></i> Por que QR Rapido é mais rápido?</h3>
|
||||||
|
<p class="text-muted">Comparação com outros geradores populares</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card h-100 border-success">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="text-success">QR Rapido</h5>
|
||||||
|
<div class="display-4 text-success fw-bold">1.2s</div>
|
||||||
|
<p class="text-muted">Otimizado para velocidade</p>
|
||||||
|
<i class="fas fa-crown text-warning"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="text-muted">Concorrente A</h5>
|
||||||
|
<div class="display-4 text-muted">3.5s</div>
|
||||||
|
<p class="text-muted">Gerador tradicional</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="text-muted">Concorrente B</h5>
|
||||||
|
<div class="display-4 text-muted">4.8s</div>
|
||||||
|
<p class="text-muted">Interface pesada</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="text-muted">Concorrente C</h5>
|
||||||
|
<div class="display-4 text-muted">6.2s</div>
|
||||||
|
<p class="text-muted">Muitos anúncios</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Ad Space Footer (conditional) -->
|
||||||
|
@await Html.PartialAsync("_AdSpace", new { position = "footer" })
|
||||||
309
Views/Premium/Upgrade.cshtml
Normal file
309
Views/Premium/Upgrade.cshtml
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
@model QRRapidoApp.Models.ViewModels.UpgradeViewModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "QR Rapido Premium";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<div class="text-center mb-5">
|
||||||
|
<h1 class="display-4 text-gradient">
|
||||||
|
<i class="fas fa-rocket"></i> QR Rapido Premium
|
||||||
|
</h1>
|
||||||
|
<p class="lead text-muted">
|
||||||
|
Acelere sua produtividade com o gerador de QR mais rápido do mundo
|
||||||
|
</p>
|
||||||
|
<div class="badge bg-success fs-6 p-2">
|
||||||
|
<i class="fas fa-bolt"></i> 3x mais rápido que a concorrência
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Current Status -->
|
||||||
|
@if (Model.IsAdFreeActive)
|
||||||
|
{
|
||||||
|
<div class="alert alert-info border-0 shadow-sm mb-4">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h6><i class="fas fa-info-circle"></i> Status Atual</h6>
|
||||||
|
<p class="mb-0">
|
||||||
|
Você tem <strong>@Model.DaysUntilAdExpiry dias</strong> restantes sem anúncios.
|
||||||
|
Upgrade agora e tenha acesso premium para sempre!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 text-end">
|
||||||
|
<div class="badge bg-success p-2">
|
||||||
|
@Model.DaysUntilAdExpiry dias restantes
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Pricing Card -->
|
||||||
|
<div class="row justify-content-center mb-5">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card shadow-lg border-warning">
|
||||||
|
<div class="card-header bg-warning text-dark text-center">
|
||||||
|
<h3 class="mb-0">
|
||||||
|
<i class="fas fa-crown"></i> QR Rapido Premium
|
||||||
|
</h3>
|
||||||
|
<small>O plano mais popular</small>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="display-3 text-warning fw-bold mb-2">
|
||||||
|
R$ @Model.PremiumPrice.ToString("0.00")
|
||||||
|
</div>
|
||||||
|
<p class="text-muted">por mês</p>
|
||||||
|
|
||||||
|
<div class="list-group list-group-flush mb-4">
|
||||||
|
<div class="list-group-item border-0">
|
||||||
|
<i class="fas fa-infinity text-success me-2"></i>
|
||||||
|
<strong>QR codes ilimitados</strong>
|
||||||
|
</div>
|
||||||
|
<div class="list-group-item border-0">
|
||||||
|
<i class="fas fa-bolt text-success me-2"></i>
|
||||||
|
<strong>Geração ultra-rápida (0.4s)</strong>
|
||||||
|
</div>
|
||||||
|
<div class="list-group-item border-0">
|
||||||
|
<i class="fas fa-ban text-success me-2"></i>
|
||||||
|
<strong>Sem anúncios para sempre</strong>
|
||||||
|
</div>
|
||||||
|
<div class="list-group-item border-0">
|
||||||
|
<i class="fas fa-magic text-success me-2"></i>
|
||||||
|
<strong>QR codes dinâmicos</strong>
|
||||||
|
</div>
|
||||||
|
<div class="list-group-item border-0">
|
||||||
|
<i class="fas fa-chart-line text-success me-2"></i>
|
||||||
|
<strong>Analytics em tempo real</strong>
|
||||||
|
</div>
|
||||||
|
<div class="list-group-item border-0">
|
||||||
|
<i class="fas fa-headset text-success me-2"></i>
|
||||||
|
<strong>Suporte prioritário</strong>
|
||||||
|
</div>
|
||||||
|
<div class="list-group-item border-0">
|
||||||
|
<i class="fas fa-code text-success me-2"></i>
|
||||||
|
<strong>API para desenvolvedores</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="upgrade-btn" class="btn btn-warning btn-lg w-100 mb-3">
|
||||||
|
<i class="fas fa-rocket"></i> Fazer Upgrade Agora
|
||||||
|
<div class="spinner-border spinner-border-sm ms-2 d-none" role="status"></div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-shield-alt"></i> Pagamento seguro via Stripe
|
||||||
|
<br>
|
||||||
|
<i class="fas fa-times-circle"></i> Cancele quando quiser
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Feature Comparison -->
|
||||||
|
<div class="card shadow-sm mb-5">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<i class="fas fa-balance-scale"></i> Comparação de Planos
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Recurso</th>
|
||||||
|
<th class="text-center">Free</th>
|
||||||
|
<th class="text-center bg-warning text-dark">Premium</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>QR codes por dia</td>
|
||||||
|
<td class="text-center">50</td>
|
||||||
|
<td class="text-center"><i class="fas fa-infinity text-success"></i> Ilimitado</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Velocidade de geração</td>
|
||||||
|
<td class="text-center">1.2s</td>
|
||||||
|
<td class="text-center"><strong class="text-success">0.4s</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Anúncios</td>
|
||||||
|
<td class="text-center"><i class="fas fa-times text-danger"></i></td>
|
||||||
|
<td class="text-center"><i class="fas fa-check text-success"></i> Sem anúncios</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>QR codes dinâmicos</td>
|
||||||
|
<td class="text-center"><i class="fas fa-times text-danger"></i></td>
|
||||||
|
<td class="text-center"><i class="fas fa-check text-success"></i></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Analytics detalhados</td>
|
||||||
|
<td class="text-center"><i class="fas fa-times text-danger"></i></td>
|
||||||
|
<td class="text-center"><i class="fas fa-check text-success"></i></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Suporte prioritário</td>
|
||||||
|
<td class="text-center"><i class="fas fa-times text-danger"></i></td>
|
||||||
|
<td class="text-center"><i class="fas fa-check text-success"></i></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>API access</td>
|
||||||
|
<td class="text-center"><i class="fas fa-times text-danger"></i></td>
|
||||||
|
<td class="text-center"><i class="fas fa-check text-success"></i></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Speed Demonstration -->
|
||||||
|
<div class="card shadow-sm mb-5">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<i class="fas fa-stopwatch"></i> Demonstração de Velocidade
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card border-danger">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="text-danger">Concorrentes</h5>
|
||||||
|
<div class="display-4 text-danger">4.5s</div>
|
||||||
|
<p class="text-muted">Tempo médio</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card border-primary">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="text-primary">QR Rapido Free</h5>
|
||||||
|
<div class="display-4 text-primary">1.2s</div>
|
||||||
|
<p class="text-muted">3x mais rápido</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card border-success">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="text-success">QR Rapido Premium</h5>
|
||||||
|
<div class="display-4 text-success">0.4s</div>
|
||||||
|
<p class="text-muted">11x mais rápido!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FAQ -->
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<i class="fas fa-question-circle"></i> Perguntas Frequentes
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="accordion" id="faqAccordion">
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#faq1">
|
||||||
|
Posso cancelar a qualquer momento?
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="faq1" class="accordion-collapse collapse show">
|
||||||
|
<div class="accordion-body">
|
||||||
|
Sim! Você pode cancelar sua assinatura a qualquer momento. Não há taxas de cancelamento
|
||||||
|
e você manterá o acesso premium até o final do período já pago.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq2">
|
||||||
|
O que são QR codes dinâmicos?
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="faq2" class="accordion-collapse collapse">
|
||||||
|
<div class="accordion-body">
|
||||||
|
QR codes dinâmicos permitem que você altere o conteúdo do QR após ele ter sido criado,
|
||||||
|
sem precisar gerar um novo código. Perfeito para campanhas de marketing e uso empresarial.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq3">
|
||||||
|
Como funciona o suporte prioritário?
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="faq3" class="accordion-collapse collapse">
|
||||||
|
<div class="accordion-body">
|
||||||
|
Usuários premium recebem resposta em até 2 horas úteis por email,
|
||||||
|
acesso ao chat direto e suporte técnico especializado.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script>
|
||||||
|
document.getElementById('upgrade-btn').addEventListener('click', async function() {
|
||||||
|
const btn = this;
|
||||||
|
const spinner = btn.querySelector('.spinner-border');
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
spinner.classList.remove('d-none');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/Premium/CreateCheckout', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Track conversion attempt
|
||||||
|
if (typeof gtag !== 'undefined') {
|
||||||
|
gtag('event', 'begin_checkout', {
|
||||||
|
'event_category': 'Premium',
|
||||||
|
'value': @Model.PremiumPrice,
|
||||||
|
'currency': 'BRL'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = result.url;
|
||||||
|
} else {
|
||||||
|
alert('Erro ao processar pagamento: ' + result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro:', error);
|
||||||
|
alert('Erro ao processar pagamento. Tente novamente.');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
spinner.classList.add('d-none');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track page view
|
||||||
|
if (typeof gtag !== 'undefined') {
|
||||||
|
gtag('event', 'page_view', {
|
||||||
|
'page_title': 'Premium Upgrade',
|
||||||
|
'page_location': window.location.href
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
}
|
||||||
83
Views/Shared/_AdSpace.cshtml
Normal file
83
Views/Shared/_AdSpace.cshtml
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
@using QRRapidoApp.Services
|
||||||
|
@model dynamic
|
||||||
|
@inject AdDisplayService AdService
|
||||||
|
@{
|
||||||
|
var userId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
var showAds = await AdService.ShouldShowAds(userId);
|
||||||
|
var position = ViewBag.position ?? Model?.position ?? "header";
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (showAds)
|
||||||
|
{
|
||||||
|
@switch (position)
|
||||||
|
{
|
||||||
|
case "header":
|
||||||
|
<div class="ad-container ad-header mb-4">
|
||||||
|
<div class="ad-label">Publicidade</div>
|
||||||
|
<ins class="adsbygoogle"
|
||||||
|
style="display:inline-block;width:728px;height:90px"
|
||||||
|
data-ad-client="ca-pub-XXXXXXXXXX"
|
||||||
|
data-ad-slot="XXXXXXXXXX"></ins>
|
||||||
|
</div>
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "sidebar":
|
||||||
|
<div class="ad-container ad-sidebar mb-4">
|
||||||
|
<div class="ad-label">Publicidade</div>
|
||||||
|
<ins class="adsbygoogle"
|
||||||
|
style="display:inline-block;width:300px;height:250px"
|
||||||
|
data-ad-client="ca-pub-XXXXXXXXXX"
|
||||||
|
data-ad-slot="YYYYYYYYYY"></ins>
|
||||||
|
</div>
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "footer":
|
||||||
|
<div class="ad-container ad-footer mt-5 mb-4">
|
||||||
|
<div class="ad-label">Publicidade</div>
|
||||||
|
<ins class="adsbygoogle"
|
||||||
|
style="display:inline-block;width:728px;height:90px"
|
||||||
|
data-ad-client="ca-pub-XXXXXXXXXX"
|
||||||
|
data-ad-slot="ZZZZZZZZZZ"></ins>
|
||||||
|
</div>
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "content":
|
||||||
|
<div class="ad-container ad-content my-4">
|
||||||
|
<div class="ad-label">Publicidade</div>
|
||||||
|
<ins class="adsbygoogle"
|
||||||
|
style="display:block"
|
||||||
|
data-ad-client="ca-pub-XXXXXXXXXX"
|
||||||
|
data-ad-slot="WWWWWWWWWW"
|
||||||
|
data-ad-format="auto"
|
||||||
|
data-full-width-responsive="true"></ins>
|
||||||
|
</div>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(adsbygoogle = window.adsbygoogle || []).push({});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
else if (User.Identity.IsAuthenticated)
|
||||||
|
{
|
||||||
|
var isPremium = await AdService.HasValidPremiumSubscription(userId);
|
||||||
|
if (isPremium)
|
||||||
|
{
|
||||||
|
<!-- Premium User Message -->
|
||||||
|
<div class="alert alert-success ad-free-notice mb-3">
|
||||||
|
<i class="fas fa-crown text-warning"></i>
|
||||||
|
<span><strong>✨ Usuário Premium - Sem anúncios!</strong></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<!-- Upgrade to Premium Message -->
|
||||||
|
<div class="alert alert-info upgrade-notice mb-3">
|
||||||
|
<i class="fas fa-star text-warning"></i>
|
||||||
|
<span><strong>Faça upgrade para Premium e remova os anúncios!</strong></span>
|
||||||
|
<a href="/Premium/Upgrade" class="btn btn-sm btn-warning ms-2">
|
||||||
|
<i class="fas fa-crown"></i> Premium: Sem anúncios + Histórico + QR ilimitados
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
293
Views/Shared/_Layout.cshtml
Normal file
293
Views/Shared/_Layout.cshtml
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
@using QRRapidoApp.Services
|
||||||
|
@using Microsoft.AspNetCore.Http.Extensions
|
||||||
|
@inject AdDisplayService AdService
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>@ViewData["Title"] - QR Rapido | Gerador QR Code Ultrarrápido</title>
|
||||||
|
|
||||||
|
<!-- SEO Meta Tags -->
|
||||||
|
<meta name="description" content="QR Rapido: Gere códigos QR em segundos! Gerador ultrarrápido em português e espanhol. Grátis, sem cadastro obrigatório. 30 dias sem anúncios após login.">
|
||||||
|
<meta name="keywords" content="qr rapido, gerador qr rapido, qr code rapido, codigo qr rapido, qr gratis rapido, generador qr rapido, qr ultrarapido">
|
||||||
|
<meta name="author" content="QR Rapido">
|
||||||
|
<meta name="robots" content="index, follow">
|
||||||
|
|
||||||
|
<!-- Canonical URL -->
|
||||||
|
<link rel="canonical" href="@Context.Request.GetDisplayUrl()">
|
||||||
|
|
||||||
|
<!-- Hreflang for multilingual -->
|
||||||
|
<link rel="alternate" hreflang="pt-BR" href="https://qrrapido.site/pt/">
|
||||||
|
<link rel="alternate" hreflang="es" href="https://qrrapido.site/es/">
|
||||||
|
<link rel="alternate" hreflang="en" href="https://qrrapido.site/en/">
|
||||||
|
<link rel="alternate" hreflang="x-default" href="https://qrrapido.site/">
|
||||||
|
|
||||||
|
<!-- Open Graph -->
|
||||||
|
<meta property="og:title" content="QR Rapido - Gerador QR Code Ultrarrápido">
|
||||||
|
<meta property="og:description" content="Gere códigos QR em segundos! Grátis, rápido e fácil. 30 dias sem anúncios após login.">
|
||||||
|
<meta property="og:image" content="https://qrrapido.site/images/qrrapido-og-image.png">
|
||||||
|
<meta property="og:url" content="@Context.Request.GetDisplayUrl()">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:site_name" content="QR Rapido">
|
||||||
|
<meta property="og:locale" content="pt_BR">
|
||||||
|
|
||||||
|
<!-- Twitter Cards -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="QR Rapido - Gerador QR Code Ultrarrápido">
|
||||||
|
<meta name="twitter:description" content="Gere códigos QR em segundos! Grátis, rápido e fácil.">
|
||||||
|
<meta name="twitter:image" content="https://qrrapido.site/images/qrrapido-twitter-card.png">
|
||||||
|
|
||||||
|
<!-- Structured Data Schema.org -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@@context": "https://schema.org",
|
||||||
|
"@@type": "WebApplication",
|
||||||
|
"name": "QR Rapido",
|
||||||
|
"description": "Gerador de QR Code ultrarrápido em português e espanhol",
|
||||||
|
"url": "https://qrrapido.site",
|
||||||
|
"applicationCategory": "UtilityApplication",
|
||||||
|
"operatingSystem": "Web",
|
||||||
|
"author": {
|
||||||
|
"@@type": "Organization",
|
||||||
|
"name": "QR Rapido"
|
||||||
|
},
|
||||||
|
"offers": {
|
||||||
|
"@@type": "Offer",
|
||||||
|
"price": "0",
|
||||||
|
"priceCurrency": "BRL",
|
||||||
|
"description": "Geração gratuita de QR codes"
|
||||||
|
},
|
||||||
|
"aggregateRating": {
|
||||||
|
"@@type": "AggregateRating",
|
||||||
|
"ratingValue": "4.8",
|
||||||
|
"reviewCount": "2547"
|
||||||
|
},
|
||||||
|
"featureList": [
|
||||||
|
"Geração em segundos",
|
||||||
|
"Suporte multilíngue",
|
||||||
|
"Sem cadastro obrigatório",
|
||||||
|
"30 dias sem anúncios",
|
||||||
|
"Download múltiplos formatos"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Google Analytics 4 -->
|
||||||
|
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
|
||||||
|
<script>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
gtag('config', 'GA_MEASUREMENT_ID', {
|
||||||
|
send_page_view: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom events for QR Rapido
|
||||||
|
window.trackQRGeneration = function(type, time, isPremium) {
|
||||||
|
gtag('event', 'qr_generated', {
|
||||||
|
'event_category': 'QR Generation',
|
||||||
|
'event_label': type,
|
||||||
|
'value': Math.round(parseFloat(time) * 1000),
|
||||||
|
'custom_parameters': {
|
||||||
|
'generation_time': parseFloat(time),
|
||||||
|
'user_type': isPremium ? 'premium' : 'free',
|
||||||
|
'speed_category': time < 1.0 ? 'ultra_fast' : time < 2.0 ? 'fast' : 'normal'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.trackSpeedComparison = function(ourTime, competitorAvg) {
|
||||||
|
gtag('event', 'speed_comparison', {
|
||||||
|
'event_category': 'Performance',
|
||||||
|
'our_time': parseFloat(ourTime),
|
||||||
|
'competitor_avg': parseFloat(competitorAvg),
|
||||||
|
'speed_advantage': parseFloat(competitorAvg) - parseFloat(ourTime)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.trackLanguageChange = function(from, to) {
|
||||||
|
gtag('event', 'language_change', {
|
||||||
|
'event_category': 'Localization',
|
||||||
|
'previous_language': from,
|
||||||
|
'new_language': to
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- AdSense -->
|
||||||
|
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXXXX"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<!-- Bootstrap 5 -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom CSS -->
|
||||||
|
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
||||||
|
<link rel="stylesheet" href="~/css/qrrapido-theme.css" asp-append-version="true" />
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/images/qrrapido-favicon.svg">
|
||||||
|
<link rel="icon" type="image/png" href="/images/qrrapido-favicon-32x32.png">
|
||||||
|
|
||||||
|
<!-- Web App Manifest -->
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
<meta name="theme-color" content="#007BFF">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header with QR Rapido branding -->
|
||||||
|
<header class="navbar navbar-expand-lg navbar-light bg-white border-bottom sticky-top">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand d-flex align-items-center" href="/">
|
||||||
|
<svg width="40" height="40" class="me-2" viewBox="0 0 100 100">
|
||||||
|
<!-- QR Rapido logo with speed effect -->
|
||||||
|
<rect x="10" y="10" width="80" height="80" fill="#007BFF" rx="8"/>
|
||||||
|
<rect x="20" y="20" width="15" height="15" fill="white"/>
|
||||||
|
<rect x="65" y="20" width="15" height="15" fill="white"/>
|
||||||
|
<rect x="20" y="65" width="15" height="15" fill="white"/>
|
||||||
|
<!-- Speed lines -->
|
||||||
|
<path d="M85 45 L95 45 M85 50 L92 50 M85 55 L89 55" stroke="#FF6B35" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<h1 class="h4 mb-0 text-primary fw-bold">QR Rapido</h1>
|
||||||
|
<small class="text-muted" id="tagline">Gere QR codes em segundos!</small>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="navbar-nav ms-auto d-flex flex-row align-items-center gap-3">
|
||||||
|
<!-- Language selector -->
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||||
|
<i class="fas fa-globe"></i> <span id="current-lang">PT</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="#" data-lang="pt-BR">🇧🇷 Português</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" data-lang="es">🇪🇸 Español</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" data-lang="en">🇺🇸 English</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Global speed timer -->
|
||||||
|
<div class="d-none d-md-block">
|
||||||
|
<small class="text-success fw-bold">
|
||||||
|
<i class="fas fa-stopwatch"></i>
|
||||||
|
<span id="avg-generation-time">1.2s</span> médio
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (User.Identity.IsAuthenticated)
|
||||||
|
{
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-outline-primary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||||
|
<i class="fas fa-user"></i> @User.Identity.Name
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="/Account/Profile">
|
||||||
|
<i class="fas fa-user-cog"></i> Perfil
|
||||||
|
</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/Account/History">
|
||||||
|
<i class="fas fa-history"></i> Histórico
|
||||||
|
</a></li>
|
||||||
|
@{
|
||||||
|
var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
var shouldShowAds = await AdService.ShouldShowAds(userId);
|
||||||
|
}
|
||||||
|
@if (!shouldShowAds)
|
||||||
|
{
|
||||||
|
<li><span class="dropdown-item text-success">
|
||||||
|
<i class="fas fa-crown"></i> Premium Ativo
|
||||||
|
</span></li>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<li><a class="dropdown-item text-warning" href="/Premium/Upgrade">
|
||||||
|
<i class="fas fa-rocket"></i> QR Rapido Premium
|
||||||
|
</a></li>
|
||||||
|
}
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li>
|
||||||
|
<form method="post" action="/Account/Logout" class="d-inline">
|
||||||
|
<button type="submit" class="dropdown-item">
|
||||||
|
<i class="fas fa-sign-out-alt"></i> Sair
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="/Account/Login" class="btn btn-primary btn-sm">
|
||||||
|
<i class="fas fa-sign-in-alt"></i> Login
|
||||||
|
</a>
|
||||||
|
<div class="d-none d-md-block">
|
||||||
|
<small class="text-success">
|
||||||
|
<i class="fas fa-gift"></i> Login = 30 dias sem anúncios!
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Hero Section for speed -->
|
||||||
|
<section class="bg-gradient-primary text-white py-4 mb-4">
|
||||||
|
<div class="container text-center">
|
||||||
|
<h2 class="h5 mb-2">
|
||||||
|
<i class="fas fa-bolt"></i> O gerador de QR mais rápido da web
|
||||||
|
</h2>
|
||||||
|
<p class="mb-0 opacity-75">
|
||||||
|
Média de <strong>1.2 segundos</strong> por QR code • Grátis • Sem cadastro obrigatório
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Ad Space Header (conditional) -->
|
||||||
|
@await Html.PartialAsync("_AdSpace", new { position = "header" })
|
||||||
|
|
||||||
|
<main role="main">
|
||||||
|
@RenderBody()
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="bg-dark text-light py-4 mt-5">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5>QR Rapido</h5>
|
||||||
|
<p class="small">O gerador de QR codes mais rápido da web. Grátis, seguro e confiável.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<h6>Links Úteis</h6>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li><a href="/Home/Privacy" class="text-light">Privacidade</a></li>
|
||||||
|
<li><a href="/Home/Terms" class="text-light">Termos de Uso</a></li>
|
||||||
|
<li><a href="/Premium/Upgrade" class="text-warning">Premium</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<h6>Suporte</h6>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li><a href="mailto:contato@qrrapido.site" class="text-light">Contato</a></li>
|
||||||
|
<li><a href="/Help" class="text-light">Ajuda</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="text-center">
|
||||||
|
<small>© 2024 QR Rapido. Todos os direitos reservados.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Bootstrap 5 JS -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Custom JS -->
|
||||||
|
<script src="~/js/test.js" asp-append-version="true"></script>
|
||||||
|
<script src="~/js/qr-speed-generator.js" asp-append-version="true"></script>
|
||||||
|
|
||||||
|
@await RenderSectionAsync("Scripts", required: false)
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
65
appsettings.json
Normal file
65
appsettings.json
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"App": {
|
||||||
|
"Name": "QR Rapido",
|
||||||
|
"BaseUrl": "https://qrrapido.site",
|
||||||
|
"TaglinePT": "Gere QR codes em segundos!",
|
||||||
|
"TaglineES": "¡Genera códigos QR en segundos!",
|
||||||
|
"TaglineEN": "Generate QR codes in seconds!",
|
||||||
|
"Version": "1.0.0"
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
},
|
||||||
|
"Authentication": {
|
||||||
|
"Google": {
|
||||||
|
"ClientId": "your-google-client-id",
|
||||||
|
"ClientSecret": "your-google-client-secret"
|
||||||
|
},
|
||||||
|
"Microsoft": {
|
||||||
|
"ClientId": "your-microsoft-client-id",
|
||||||
|
"ClientSecret": "your-microsoft-client-secret"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Stripe": {
|
||||||
|
"PublishableKey": "pk_test_xxxxx",
|
||||||
|
"SecretKey": "sk_test_xxxxx",
|
||||||
|
"WebhookSecret": "whsec_xxxxx",
|
||||||
|
"PriceId": "price_xxxxx"
|
||||||
|
},
|
||||||
|
"AdSense": {
|
||||||
|
"ClientId": "ca-pub-XXXXXXXXXX",
|
||||||
|
"Enabled": true
|
||||||
|
},
|
||||||
|
"Performance": {
|
||||||
|
"QRGenerationTimeoutMs": 2000,
|
||||||
|
"CacheExpirationMinutes": 60,
|
||||||
|
"MaxConcurrentGenerations": 100
|
||||||
|
},
|
||||||
|
"HistoryCleanup": {
|
||||||
|
"GracePeriodDays": 7,
|
||||||
|
"CleanupIntervalHours": 6
|
||||||
|
},
|
||||||
|
"Premium": {
|
||||||
|
"FreeQRLimit": 10,
|
||||||
|
"PremiumPrice": 19.90,
|
||||||
|
"Features": {
|
||||||
|
"UnlimitedQR": true,
|
||||||
|
"DynamicQR": true,
|
||||||
|
"NoAds": true,
|
||||||
|
"PrioritySupport": true,
|
||||||
|
"AdvancedAnalytics": true,
|
||||||
|
"SpeedBoost": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SEO": {
|
||||||
|
"KeywordsPT": "qr rapido, gerador qr rapido, qr code rapido, codigo qr rapido, qr gratis rapido",
|
||||||
|
"KeywordsES": "qr rapido, generador qr rapido, codigo qr rapido, qr gratis rapido",
|
||||||
|
"KeywordsEN": "fast qr, quick qr generator, rapid qr code, qr code generator"
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
83
docker-compose.yml
Normal file
83
docker-compose.yml
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
qrrapido:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "5000:80"
|
||||||
|
environment:
|
||||||
|
- ASPNETCORE_ENVIRONMENT=Development
|
||||||
|
- ConnectionStrings__MongoDB=mongodb://mongo:27017/qrrapido
|
||||||
|
- ConnectionStrings__Redis=redis:6379
|
||||||
|
- Authentication__Google__ClientId=${GOOGLE_CLIENT_ID}
|
||||||
|
- Authentication__Google__ClientSecret=${GOOGLE_CLIENT_SECRET}
|
||||||
|
- Authentication__Microsoft__ClientId=${MICROSOFT_CLIENT_ID}
|
||||||
|
- Authentication__Microsoft__ClientSecret=${MICROSOFT_CLIENT_SECRET}
|
||||||
|
- Stripe__PublishableKey=${STRIPE_PUBLISHABLE_KEY}
|
||||||
|
- Stripe__SecretKey=${STRIPE_SECRET_KEY}
|
||||||
|
- Stripe__WebhookSecret=${STRIPE_WEBHOOK_SECRET}
|
||||||
|
- Stripe__PriceId=${STRIPE_PRICE_ID}
|
||||||
|
depends_on:
|
||||||
|
- mongo
|
||||||
|
- redis
|
||||||
|
volumes:
|
||||||
|
- ./logs:/app/logs
|
||||||
|
networks:
|
||||||
|
- qrrapido-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
mongo:
|
||||||
|
image: mongo:7.0
|
||||||
|
container_name: qrrapido-mongo
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
environment:
|
||||||
|
- MONGO_INITDB_DATABASE=qrrapido
|
||||||
|
- MONGO_INITDB_ROOT_USERNAME=admin
|
||||||
|
- MONGO_INITDB_ROOT_PASSWORD=password123
|
||||||
|
volumes:
|
||||||
|
- mongo_data:/data/db
|
||||||
|
- ./scripts/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
|
||||||
|
networks:
|
||||||
|
- qrrapido-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7.2-alpine
|
||||||
|
container_name: qrrapido-redis
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru --appendonly yes
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
- qrrapido-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: qrrapido-nginx
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||||
|
- ./logs/nginx:/var/log/nginx
|
||||||
|
depends_on:
|
||||||
|
- qrrapido
|
||||||
|
networks:
|
||||||
|
- qrrapido-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo_data:
|
||||||
|
driver: local
|
||||||
|
redis_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
qrrapido-network:
|
||||||
|
driver: bridge
|
||||||
383
wwwroot/css/qrrapido-theme.css
Normal file
383
wwwroot/css/qrrapido-theme.css
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
/* QR Rapido Custom Theme */
|
||||||
|
:root {
|
||||||
|
--qr-primary: #007BFF;
|
||||||
|
--qr-secondary: #28A745;
|
||||||
|
--qr-accent: #FF6B35;
|
||||||
|
--qr-warning: #FFC107;
|
||||||
|
--qr-success: #28A745;
|
||||||
|
--qr-danger: #DC3545;
|
||||||
|
--qr-dark: #343A40;
|
||||||
|
--qr-light: #F8F9FA;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Global Styles */
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gradient-primary {
|
||||||
|
background: linear-gradient(135deg, var(--qr-primary) 0%, #0056B3 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generation Timer Styles */
|
||||||
|
.generation-timer {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 2px solid var(--qr-primary);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generation-timer.active {
|
||||||
|
background: var(--qr-primary);
|
||||||
|
color: white;
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Speed Badge Animation */
|
||||||
|
.speed-badge {
|
||||||
|
animation: slideInRight 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.05); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* QR Preview Placeholder */
|
||||||
|
.placeholder-qr {
|
||||||
|
border: 2px dashed #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-qr:hover {
|
||||||
|
border-color: var(--qr-primary);
|
||||||
|
background: #e3f2fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo and Branding */
|
||||||
|
.navbar-brand svg {
|
||||||
|
filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.1));
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand:hover svg {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* QR Preview Image */
|
||||||
|
#qr-preview img {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#qr-preview img:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Hover Effects */
|
||||||
|
.card {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button Styles */
|
||||||
|
.btn-check:checked + .btn {
|
||||||
|
background-color: var(--qr-primary);
|
||||||
|
border-color: var(--qr-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--qr-primary) 0%, #0056B3 100%);
|
||||||
|
border: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: linear-gradient(135deg, #0056B3 0%, var(--qr-primary) 100%);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Controls */
|
||||||
|
.form-control:focus,
|
||||||
|
.form-select:focus {
|
||||||
|
border-color: var(--qr-primary);
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control-lg {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Speed Statistics Cards */
|
||||||
|
.card.border-success {
|
||||||
|
border-color: var(--qr-success) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.border-primary {
|
||||||
|
border-color: var(--qr-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.border-warning {
|
||||||
|
border-color: var(--qr-warning) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generation Stats Animation */
|
||||||
|
.generation-stats {
|
||||||
|
animation: slideInRight 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ad Container Styles */
|
||||||
|
.ad-container {
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ad-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #6c757d;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ad-free-notice {
|
||||||
|
text-align: center;
|
||||||
|
border-left: 4px solid var(--qr-success);
|
||||||
|
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ad-free-notice .fas {
|
||||||
|
color: var(--qr-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Animations */
|
||||||
|
.spinner-border-sm {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress Bar for QR Generation */
|
||||||
|
.progress {
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
background: linear-gradient(90deg, var(--qr-primary), var(--qr-accent));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Speed Tips */
|
||||||
|
.list-unstyled li {
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-unstyled li:hover {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
color: var(--qr-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.generation-timer {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speed-badge .badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-qr {
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ad-container {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Premium Features Styling */
|
||||||
|
.premium-feature {
|
||||||
|
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
|
||||||
|
border-left: 4px solid var(--qr-warning);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.premium-badge {
|
||||||
|
background: linear-gradient(135deg, var(--qr-warning) 0%, #f39c12 100%);
|
||||||
|
color: var(--qr-dark);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Accordion Customization */
|
||||||
|
.accordion-button {
|
||||||
|
background: var(--qr-light);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-button:not(.collapsed) {
|
||||||
|
background: var(--qr-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-button:focus {
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer Styling */
|
||||||
|
footer {
|
||||||
|
background: linear-gradient(135deg, var(--qr-dark) 0%, #2c3e50 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
footer a {
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer a:hover {
|
||||||
|
color: var(--qr-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Language Selector */
|
||||||
|
.dropdown-toggle::after {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background: var(--qr-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--qr-primary);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility Classes */
|
||||||
|
.text-gradient {
|
||||||
|
background: linear-gradient(135deg, var(--qr-primary) 0%, var(--qr-accent) 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-custom {
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 123, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-gradient {
|
||||||
|
border: 2px solid;
|
||||||
|
border-image: linear-gradient(135deg, var(--qr-primary) 0%, var(--qr-accent) 100%) 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print Styles */
|
||||||
|
@media print {
|
||||||
|
.ad-container,
|
||||||
|
.btn,
|
||||||
|
.navbar,
|
||||||
|
footer {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#qr-preview img {
|
||||||
|
max-width: 300px;
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode Support (future enhancement) */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.card {
|
||||||
|
background-color: #2d3748;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-qr {
|
||||||
|
background: #4a5568;
|
||||||
|
border-color: #718096;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control,
|
||||||
|
.form-select {
|
||||||
|
background-color: #4a5568;
|
||||||
|
border-color: #718096;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
126
wwwroot/css/site.css
Normal file
126
wwwroot/css/site.css
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
/* Site test CSS */
|
||||||
|
body {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-style {
|
||||||
|
color: red;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure Bootstrap and FontAwesome work */
|
||||||
|
.btn {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Share Button Styles */
|
||||||
|
#share-qr-btn {
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#share-qr-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#share-dropdown {
|
||||||
|
min-width: 250px;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
#share-dropdown .dropdown-item {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-bottom: 1px solid #f1f3f4;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#share-dropdown .dropdown-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#share-dropdown .dropdown-item:hover {
|
||||||
|
background: linear-gradient(45deg, #f8f9fa, #e9ecef);
|
||||||
|
padding-left: 25px;
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#share-dropdown .dropdown-item i {
|
||||||
|
width: 20px;
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile optimizations for share button */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#share-dropdown {
|
||||||
|
min-width: 100%;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#share-dropdown .dropdown-item {
|
||||||
|
padding: 15px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#share-dropdown .dropdown-item i {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#share-qr-btn {
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Share button loading state */
|
||||||
|
#share-qr-btn.loading {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
#share-qr-btn.loading::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -8px;
|
||||||
|
margin-top: -8px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-top: 2px solid #ffffff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Special colors for social platforms */
|
||||||
|
.text-purple {
|
||||||
|
color: #6f42c1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Share success feedback */
|
||||||
|
.share-success {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1050;
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-success.show {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
874
wwwroot/js/qr-speed-generator.js
Normal file
874
wwwroot/js/qr-speed-generator.js
Normal file
@ -0,0 +1,874 @@
|
|||||||
|
// QR Rapido Speed Generator
|
||||||
|
class QRRapidoGenerator {
|
||||||
|
constructor() {
|
||||||
|
this.startTime = 0;
|
||||||
|
this.currentQR = null;
|
||||||
|
this.timerInterval = null;
|
||||||
|
this.languageStrings = {
|
||||||
|
'pt-BR': {
|
||||||
|
tagline: 'Gere QR codes em segundos!',
|
||||||
|
generating: 'Gerando...',
|
||||||
|
generated: 'Gerado em',
|
||||||
|
seconds: 's',
|
||||||
|
ultraFast: 'Geração ultra rápida!',
|
||||||
|
fast: 'Geração rápida!',
|
||||||
|
normal: 'Geração normal',
|
||||||
|
error: 'Erro na geração. Tente novamente.',
|
||||||
|
success: 'QR Code salvo no histórico!'
|
||||||
|
},
|
||||||
|
'es': {
|
||||||
|
tagline: '¡Genera códigos QR en segundos!',
|
||||||
|
generating: 'Generando...',
|
||||||
|
generated: 'Generado en',
|
||||||
|
seconds: 's',
|
||||||
|
ultraFast: '¡Generación ultra rápida!',
|
||||||
|
fast: '¡Generación rápida!',
|
||||||
|
normal: 'Generación normal',
|
||||||
|
error: 'Error en la generación. Inténtalo de nuevo.',
|
||||||
|
success: '¡Código QR guardado en el historial!'
|
||||||
|
},
|
||||||
|
'en': {
|
||||||
|
tagline: 'Generate QR codes in seconds!',
|
||||||
|
generating: 'Generating...',
|
||||||
|
generated: 'Generated in',
|
||||||
|
seconds: 's',
|
||||||
|
ultraFast: 'Ultra fast generation!',
|
||||||
|
fast: 'Fast generation!',
|
||||||
|
normal: 'Normal generation',
|
||||||
|
error: 'Generation error. Please try again.',
|
||||||
|
success: 'QR Code saved to history!'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.currentLang = localStorage.getItem('qrrapido-lang') || 'pt-BR';
|
||||||
|
|
||||||
|
this.initializeEvents();
|
||||||
|
this.checkAdFreeStatus();
|
||||||
|
this.updateLanguage();
|
||||||
|
this.updateStatsCounters();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeEvents() {
|
||||||
|
// Form submission with timer
|
||||||
|
const form = document.getElementById('qr-speed-form');
|
||||||
|
if (form) {
|
||||||
|
form.addEventListener('submit', this.generateQRWithTimer.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quick style selection
|
||||||
|
document.querySelectorAll('input[name="quick-style"]').forEach(radio => {
|
||||||
|
radio.addEventListener('change', this.applyQuickStyle.bind(this));
|
||||||
|
});
|
||||||
|
|
||||||
|
// QR type change with hints
|
||||||
|
const qrType = document.getElementById('qr-type');
|
||||||
|
if (qrType) {
|
||||||
|
qrType.addEventListener('change', this.updateContentHints.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Language selector
|
||||||
|
document.querySelectorAll('[data-lang]').forEach(link => {
|
||||||
|
link.addEventListener('click', this.changeLanguage.bind(this));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Real-time preview for premium users
|
||||||
|
if (this.isPremiumUser()) {
|
||||||
|
this.setupRealTimePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download buttons
|
||||||
|
this.setupDownloadButtons();
|
||||||
|
|
||||||
|
// Share functionality
|
||||||
|
this.setupShareButtons();
|
||||||
|
|
||||||
|
// Save to history
|
||||||
|
const saveBtn = document.getElementById('save-to-history');
|
||||||
|
if (saveBtn) {
|
||||||
|
saveBtn.addEventListener('click', this.saveToHistory.bind(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupDownloadButtons() {
|
||||||
|
const pngBtn = document.getElementById('download-png');
|
||||||
|
const svgBtn = document.getElementById('download-svg');
|
||||||
|
const pdfBtn = document.getElementById('download-pdf');
|
||||||
|
|
||||||
|
if (pngBtn) pngBtn.addEventListener('click', () => this.downloadQR('png'));
|
||||||
|
if (svgBtn) svgBtn.addEventListener('click', () => this.downloadQR('svg'));
|
||||||
|
if (pdfBtn) pdfBtn.addEventListener('click', () => this.downloadQR('pdf'));
|
||||||
|
}
|
||||||
|
|
||||||
|
setupShareButtons() {
|
||||||
|
// Check if Web Share API is supported and show/hide native share option
|
||||||
|
if (navigator.share && this.isMobileDevice()) {
|
||||||
|
const nativeShareOption = document.getElementById('native-share-option');
|
||||||
|
if (nativeShareOption) {
|
||||||
|
nativeShareOption.classList.remove('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show save to gallery option on mobile
|
||||||
|
if (this.isMobileDevice()) {
|
||||||
|
const saveGalleryOption = document.getElementById('save-gallery-option');
|
||||||
|
if (saveGalleryOption) {
|
||||||
|
saveGalleryOption.classList.remove('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners to share buttons
|
||||||
|
const shareButtons = {
|
||||||
|
'native-share': () => this.shareNative(),
|
||||||
|
'share-whatsapp': () => this.shareWhatsApp(),
|
||||||
|
'share-telegram': () => this.shareTelegram(),
|
||||||
|
'share-email': () => this.shareEmail(),
|
||||||
|
'copy-qr-link': () => this.copyToClipboard(),
|
||||||
|
'save-to-gallery': () => this.saveToGallery()
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(shareButtons).forEach(([id, handler]) => {
|
||||||
|
const button = document.getElementById(id);
|
||||||
|
if (button) {
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handler();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isMobileDevice() {
|
||||||
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
|
||||||
|
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform));
|
||||||
|
}
|
||||||
|
|
||||||
|
async shareNative() {
|
||||||
|
if (!this.currentQR || !navigator.share) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a blob from the base64 image
|
||||||
|
const base64Response = await fetch(`data:image/png;base64,${this.currentQR.base64}`);
|
||||||
|
const blob = await base64Response.blob();
|
||||||
|
const file = new File([blob], 'qrcode.png', { type: 'image/png' });
|
||||||
|
|
||||||
|
const shareData = {
|
||||||
|
title: 'QR Code - QR Rapido',
|
||||||
|
text: 'QR Code gerado com QR Rapido - o gerador mais rápido do Brasil!',
|
||||||
|
url: window.location.origin,
|
||||||
|
files: [file]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if files can be shared
|
||||||
|
if (navigator.canShare && navigator.canShare(shareData)) {
|
||||||
|
await navigator.share(shareData);
|
||||||
|
} else {
|
||||||
|
// Fallback without files
|
||||||
|
await navigator.share({
|
||||||
|
title: shareData.title,
|
||||||
|
text: shareData.text,
|
||||||
|
url: shareData.url
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.trackShareEvent('native');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sharing:', error);
|
||||||
|
if (error.name !== 'AbortError') {
|
||||||
|
this.showError('Erro ao compartilhar. Tente outro método.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shareWhatsApp() {
|
||||||
|
if (!this.currentQR) return;
|
||||||
|
|
||||||
|
const text = encodeURIComponent('QR Code gerado com QR Rapido - o gerador mais rápido do Brasil! ' + window.location.origin);
|
||||||
|
const url = `https://wa.me/?text=${text}`;
|
||||||
|
|
||||||
|
if (this.isMobileDevice()) {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
} else {
|
||||||
|
window.open(`https://web.whatsapp.com/send?text=${text}`, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.trackShareEvent('whatsapp');
|
||||||
|
}
|
||||||
|
|
||||||
|
shareTelegram() {
|
||||||
|
if (!this.currentQR) return;
|
||||||
|
|
||||||
|
const text = encodeURIComponent('QR Code gerado com QR Rapido - o gerador mais rápido do Brasil!');
|
||||||
|
const url = encodeURIComponent(window.location.origin);
|
||||||
|
const telegramUrl = `https://t.me/share/url?url=${url}&text=${text}`;
|
||||||
|
|
||||||
|
window.open(telegramUrl, '_blank');
|
||||||
|
this.trackShareEvent('telegram');
|
||||||
|
}
|
||||||
|
|
||||||
|
shareEmail() {
|
||||||
|
if (!this.currentQR) return;
|
||||||
|
|
||||||
|
const subject = encodeURIComponent('QR Code - QR Rapido');
|
||||||
|
const body = encodeURIComponent(`Olá!\n\nCompartilho com você este QR Code gerado no QR Rapido, o gerador mais rápido do Brasil!\n\nAcesse: ${window.location.origin}\n\nAbraços!`);
|
||||||
|
const mailtoUrl = `mailto:?subject=${subject}&body=${body}`;
|
||||||
|
|
||||||
|
window.location.href = mailtoUrl;
|
||||||
|
this.trackShareEvent('email');
|
||||||
|
}
|
||||||
|
|
||||||
|
async copyToClipboard() {
|
||||||
|
if (!this.currentQR) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const shareText = `QR Code gerado com QR Rapido - ${window.location.origin}`;
|
||||||
|
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
await navigator.clipboard.writeText(shareText);
|
||||||
|
} else {
|
||||||
|
// Fallback for older browsers
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = shareText;
|
||||||
|
textArea.style.position = 'fixed';
|
||||||
|
textArea.style.left = '-999999px';
|
||||||
|
textArea.style.top = '-999999px';
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.focus();
|
||||||
|
textArea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
textArea.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showSuccess('Link copiado para a área de transferência!');
|
||||||
|
this.trackShareEvent('copy');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error copying to clipboard:', error);
|
||||||
|
this.showError('Erro ao copiar link. Tente novamente.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveToGallery() {
|
||||||
|
if (!this.currentQR) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a blob from the base64 image
|
||||||
|
const base64Response = await fetch(`data:image/png;base64,${this.currentQR.base64}`);
|
||||||
|
const blob = await base64Response.blob();
|
||||||
|
|
||||||
|
// Create download link
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `qrrapido-${new Date().toISOString().slice(0,10)}-${Date.now()}.png`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
this.showSuccess('QR Code baixado! Verifique sua galeria/downloads.');
|
||||||
|
this.trackShareEvent('gallery');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving to gallery:', error);
|
||||||
|
this.showError('Erro ao salvar na galeria. Tente novamente.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trackShareEvent(method) {
|
||||||
|
// Google Analytics
|
||||||
|
if (typeof gtag !== 'undefined') {
|
||||||
|
gtag('event', 'qr_shared', {
|
||||||
|
'share_method': method,
|
||||||
|
'user_type': this.isPremiumUser() ? 'premium' : 'free',
|
||||||
|
'language': this.currentLang
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal tracking
|
||||||
|
console.log(`QR Code shared via ${method}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateQRWithTimer(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (!this.validateForm()) return;
|
||||||
|
|
||||||
|
// Start timer
|
||||||
|
this.startTime = performance.now();
|
||||||
|
this.showGenerationStarted();
|
||||||
|
|
||||||
|
const formData = this.collectFormData();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/QR/GenerateRapid', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 429) {
|
||||||
|
this.showUpgradeModal('Limite de QR codes atingido! Upgrade para QR Rapido Premium e gere códigos ilimitados.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error('Erro na geração');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Erro desconhecido');
|
||||||
|
}
|
||||||
|
|
||||||
|
const generationTime = ((performance.now() - this.startTime) / 1000).toFixed(1);
|
||||||
|
|
||||||
|
this.displayQRResult(result, generationTime);
|
||||||
|
this.updateSpeedStats(generationTime);
|
||||||
|
this.trackGenerationEvent(formData.type, generationTime);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao gerar QR:', error);
|
||||||
|
this.showError(this.languageStrings[this.currentLang].error);
|
||||||
|
} finally {
|
||||||
|
this.hideGenerationLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validateForm() {
|
||||||
|
const qrType = document.getElementById('qr-type').value;
|
||||||
|
const qrContent = document.getElementById('qr-content').value.trim();
|
||||||
|
|
||||||
|
if (!qrType) {
|
||||||
|
this.showError('Selecione o tipo de QR code');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!qrContent) {
|
||||||
|
this.showError('Digite o conteúdo do QR code');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qrContent.length > 4000) {
|
||||||
|
this.showError('Conteúdo muito longo. Máximo 4000 caracteres.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
collectFormData() {
|
||||||
|
const quickStyle = document.querySelector('input[name="quick-style"]:checked')?.value || 'classic';
|
||||||
|
const styleSettings = this.getStyleSettings(quickStyle);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: document.getElementById('qr-type').value,
|
||||||
|
content: document.getElementById('qr-content').value,
|
||||||
|
quickStyle: quickStyle,
|
||||||
|
primaryColor: document.getElementById('primary-color').value,
|
||||||
|
backgroundColor: document.getElementById('bg-color').value,
|
||||||
|
size: parseInt(document.getElementById('qr-size').value),
|
||||||
|
margin: parseInt(document.getElementById('qr-margin').value),
|
||||||
|
cornerStyle: document.getElementById('corner-style')?.value || 'square',
|
||||||
|
optimizeForSpeed: true,
|
||||||
|
language: this.currentLang,
|
||||||
|
...styleSettings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getStyleSettings(style) {
|
||||||
|
const styles = {
|
||||||
|
classic: { primaryColor: '#000000', backgroundColor: '#FFFFFF' },
|
||||||
|
modern: { primaryColor: '#007BFF', backgroundColor: '#F8F9FA' },
|
||||||
|
colorful: { primaryColor: '#FF6B35', backgroundColor: '#FFF3E0' }
|
||||||
|
};
|
||||||
|
return styles[style] || styles.classic;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayQRResult(result, generationTime) {
|
||||||
|
const previewDiv = document.getElementById('qr-preview');
|
||||||
|
if (!previewDiv) return;
|
||||||
|
|
||||||
|
previewDiv.innerHTML = `
|
||||||
|
<img src="data:image/png;base64,${result.qrCodeBase64}"
|
||||||
|
class="img-fluid border rounded shadow-sm"
|
||||||
|
alt="QR Code gerado em ${generationTime}s">
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Show generation statistics
|
||||||
|
this.showGenerationStats(generationTime);
|
||||||
|
|
||||||
|
// Show download buttons
|
||||||
|
const downloadSection = document.getElementById('download-section');
|
||||||
|
if (downloadSection) {
|
||||||
|
downloadSection.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save current data
|
||||||
|
this.currentQR = {
|
||||||
|
base64: result.qrCodeBase64,
|
||||||
|
id: result.qrId,
|
||||||
|
generationTime: generationTime
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update counter for free users
|
||||||
|
if (result.remainingQRs !== undefined) {
|
||||||
|
this.updateRemainingCounter(result.remainingQRs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showGenerationStarted() {
|
||||||
|
const button = document.getElementById('generate-btn');
|
||||||
|
const spinner = button?.querySelector('.spinner-border');
|
||||||
|
const timer = document.querySelector('.generation-timer');
|
||||||
|
|
||||||
|
if (button) button.disabled = true;
|
||||||
|
if (spinner) spinner.classList.remove('d-none');
|
||||||
|
if (timer) timer.classList.remove('d-none');
|
||||||
|
|
||||||
|
// Update timer in real time
|
||||||
|
this.timerInterval = setInterval(() => {
|
||||||
|
const elapsed = ((performance.now() - this.startTime) / 1000).toFixed(1);
|
||||||
|
const timerSpan = timer?.querySelector('span');
|
||||||
|
if (timerSpan) {
|
||||||
|
timerSpan.textContent = `${elapsed}s`;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Preview loading
|
||||||
|
const preview = document.getElementById('qr-preview');
|
||||||
|
if (preview) {
|
||||||
|
preview.innerHTML = `
|
||||||
|
<div class="text-center p-4">
|
||||||
|
<div class="spinner-border text-primary mb-3" role="status"></div>
|
||||||
|
<p class="text-muted">${this.languageStrings[this.currentLang].generating}</p>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||||
|
style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showGenerationStats(generationTime) {
|
||||||
|
const statsDiv = document.querySelector('.generation-stats');
|
||||||
|
const speedBadge = document.querySelector('.speed-badge');
|
||||||
|
|
||||||
|
if (statsDiv) {
|
||||||
|
statsDiv.classList.remove('d-none');
|
||||||
|
const timeSpan = statsDiv.querySelector('.generation-time');
|
||||||
|
if (timeSpan) {
|
||||||
|
timeSpan.textContent = `${generationTime}s`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show speed badge
|
||||||
|
if (speedBadge) {
|
||||||
|
const strings = this.languageStrings[this.currentLang];
|
||||||
|
let badgeText = strings.normal;
|
||||||
|
let badgeClass = 'bg-secondary';
|
||||||
|
|
||||||
|
if (generationTime < 1.0) {
|
||||||
|
badgeText = strings.ultraFast;
|
||||||
|
badgeClass = 'bg-success';
|
||||||
|
} else if (generationTime < 2.0) {
|
||||||
|
badgeText = strings.fast;
|
||||||
|
badgeClass = 'bg-primary';
|
||||||
|
}
|
||||||
|
|
||||||
|
speedBadge.innerHTML = `
|
||||||
|
<span class="badge ${badgeClass}">
|
||||||
|
<i class="fas fa-bolt"></i> ${badgeText}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
speedBadge.classList.remove('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideGenerationLoading() {
|
||||||
|
const button = document.getElementById('generate-btn');
|
||||||
|
const spinner = button?.querySelector('.spinner-border');
|
||||||
|
|
||||||
|
if (button) button.disabled = false;
|
||||||
|
if (spinner) spinner.classList.add('d-none');
|
||||||
|
|
||||||
|
if (this.timerInterval) {
|
||||||
|
clearInterval(this.timerInterval);
|
||||||
|
this.timerInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContentHints() {
|
||||||
|
const type = document.getElementById('qr-type')?.value;
|
||||||
|
const hintsElement = document.getElementById('content-hints');
|
||||||
|
if (!hintsElement || !type) return;
|
||||||
|
|
||||||
|
const hints = {
|
||||||
|
'pt-BR': {
|
||||||
|
'url': 'Ex: https://www.exemplo.com.br',
|
||||||
|
'text': 'Digite qualquer texto que desejar',
|
||||||
|
'wifi': 'Nome da rede;Senha;Tipo de segurança (WPA/WEP)',
|
||||||
|
'vcard': 'Nome;Telefone;Email;Empresa',
|
||||||
|
'sms': 'Número;Mensagem',
|
||||||
|
'email': 'email@exemplo.com;Assunto;Mensagem'
|
||||||
|
},
|
||||||
|
'es': {
|
||||||
|
'url': 'Ej: https://www.ejemplo.com',
|
||||||
|
'text': 'Escribe cualquier texto que desees',
|
||||||
|
'wifi': 'Nombre de red;Contraseña;Tipo de seguridad (WPA/WEP)',
|
||||||
|
'vcard': 'Nombre;Teléfono;Email;Empresa',
|
||||||
|
'sms': 'Número;Mensaje',
|
||||||
|
'email': 'email@ejemplo.com;Asunto;Mensaje'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const langHints = hints[this.currentLang] || hints['pt-BR'];
|
||||||
|
hintsElement.textContent = langHints[type] || 'Digite o conteúdo apropriado para o tipo selecionado';
|
||||||
|
}
|
||||||
|
|
||||||
|
changeLanguage(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.currentLang = e.target.dataset.lang;
|
||||||
|
this.updateLanguage();
|
||||||
|
this.updateContentHints();
|
||||||
|
|
||||||
|
// Save preference
|
||||||
|
localStorage.setItem('qrrapido-lang', this.currentLang);
|
||||||
|
|
||||||
|
// Track language change
|
||||||
|
window.trackLanguageChange && window.trackLanguageChange('pt-BR', this.currentLang);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLanguage() {
|
||||||
|
const strings = this.languageStrings[this.currentLang];
|
||||||
|
|
||||||
|
// Update tagline
|
||||||
|
const tagline = document.getElementById('tagline');
|
||||||
|
if (tagline) {
|
||||||
|
tagline.textContent = strings.tagline;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update language selector
|
||||||
|
const langMap = { 'pt-BR': 'PT', 'es': 'ES', 'en': 'EN' };
|
||||||
|
const currentLang = document.getElementById('current-lang');
|
||||||
|
if (currentLang) {
|
||||||
|
currentLang.textContent = langMap[this.currentLang];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update hints if type already selected
|
||||||
|
const qrType = document.getElementById('qr-type');
|
||||||
|
if (qrType?.value) {
|
||||||
|
this.updateContentHints();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyQuickStyle(e) {
|
||||||
|
const style = e.target.value;
|
||||||
|
const settings = this.getStyleSettings(style);
|
||||||
|
|
||||||
|
const primaryColor = document.getElementById('primary-color');
|
||||||
|
const bgColor = document.getElementById('bg-color');
|
||||||
|
|
||||||
|
if (primaryColor) primaryColor.value = settings.primaryColor;
|
||||||
|
if (bgColor) bgColor.value = settings.backgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatsCounters() {
|
||||||
|
// Simulate real-time counters
|
||||||
|
setInterval(() => {
|
||||||
|
const totalElement = document.getElementById('total-qrs');
|
||||||
|
if (totalElement) {
|
||||||
|
const current = parseFloat(totalElement.textContent.replace('K', '')) || 10.5;
|
||||||
|
const newValue = (current + Math.random() * 0.1).toFixed(1);
|
||||||
|
totalElement.textContent = `${newValue}K`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update average time based on real performance
|
||||||
|
const avgElement = document.getElementById('avg-generation-time');
|
||||||
|
if (avgElement && window.qrRapidoStats) {
|
||||||
|
const avg = window.qrRapidoStats.getAverageTime();
|
||||||
|
avgElement.textContent = `${avg}s`;
|
||||||
|
}
|
||||||
|
}, 30000); // Update every 30 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
trackGenerationEvent(type, time) {
|
||||||
|
// Google Analytics
|
||||||
|
if (typeof gtag !== 'undefined') {
|
||||||
|
gtag('event', 'qr_generated', {
|
||||||
|
'qr_type': type,
|
||||||
|
'generation_time': parseFloat(time),
|
||||||
|
'user_type': this.isPremiumUser() ? 'premium' : 'free',
|
||||||
|
'language': this.currentLang
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal statistics
|
||||||
|
if (!window.qrRapidoStats) {
|
||||||
|
window.qrRapidoStats = {
|
||||||
|
times: [],
|
||||||
|
getAverageTime: function() {
|
||||||
|
if (this.times.length === 0) return '1.2';
|
||||||
|
const avg = this.times.reduce((a, b) => a + b) / this.times.length;
|
||||||
|
return avg.toFixed(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
window.qrRapidoStats.times.push(parseFloat(time));
|
||||||
|
}
|
||||||
|
|
||||||
|
isPremiumUser() {
|
||||||
|
return document.querySelector('.text-success')?.textContent.includes('Premium Ativo') || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadQR(format) {
|
||||||
|
if (!this.currentQR) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/QR/Download/${this.currentQR.id}?format=${format}`);
|
||||||
|
if (!response.ok) throw new Error('Download failed');
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `qrrapido-${new Date().toISOString().slice(0,10)}.${format}`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Download error:', error);
|
||||||
|
this.showError('Erro ao fazer download. Tente novamente.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveToHistory() {
|
||||||
|
if (!this.currentQR) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/QR/SaveToHistory', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
qrId: this.currentQR.id
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
this.showSuccess(this.languageStrings[this.currentLang].success);
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to save');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Save error:', error);
|
||||||
|
this.showError('Erro ao salvar no histórico.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkAdFreeStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/Account/AdFreeStatus');
|
||||||
|
const status = await response.json();
|
||||||
|
|
||||||
|
if (status.isAdFree) {
|
||||||
|
this.hideAllAds();
|
||||||
|
this.showAdFreeMessage(status.timeRemaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking ad-free status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideAllAds() {
|
||||||
|
document.querySelectorAll('.ad-container').forEach(ad => {
|
||||||
|
ad.style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showAdFreeMessage(timeRemaining) {
|
||||||
|
if (timeRemaining <= 0) return;
|
||||||
|
|
||||||
|
const existing = document.querySelector('.ad-free-notice');
|
||||||
|
if (existing) return; // Already shown
|
||||||
|
|
||||||
|
const message = document.createElement('div');
|
||||||
|
message.className = 'alert alert-success text-center mb-3 ad-free-notice';
|
||||||
|
message.innerHTML = `
|
||||||
|
<i class="fas fa-crown text-warning"></i>
|
||||||
|
<strong>Sessão sem anúncios ativa!</strong>
|
||||||
|
Tempo restante: <span class="ad-free-countdown">${this.formatTime(timeRemaining)}</span>
|
||||||
|
<a href="/Premium/Upgrade" class="btn btn-sm btn-warning ms-2">Tornar Permanente</a>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const container = document.querySelector('.container');
|
||||||
|
const row = container?.querySelector('.row');
|
||||||
|
if (container && row) {
|
||||||
|
container.insertBefore(message, row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatTime(minutes) {
|
||||||
|
if (minutes === 0) return '0m';
|
||||||
|
|
||||||
|
const days = Math.floor(minutes / 1440);
|
||||||
|
const hours = Math.floor((minutes % 1440) / 60);
|
||||||
|
const mins = minutes % 60;
|
||||||
|
|
||||||
|
if (days > 0) return `${days}d ${hours}h ${mins}m`;
|
||||||
|
if (hours > 0) return `${hours}h ${mins}m`;
|
||||||
|
return `${mins}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
showUpgradeModal(message) {
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'modal fade';
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-warning">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="fas fa-crown"></i> Upgrade para Premium
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>${message}</p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Plano Atual (Free)</h6>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li>❌ Limite de 10 QR/dia</li>
|
||||||
|
<li>❌ Anúncios</li>
|
||||||
|
<li>✅ QR básicos</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Premium (R$ 19,90/mês)</h6>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li>✅ QR ilimitados</li>
|
||||||
|
<li>✅ Sem anúncios</li>
|
||||||
|
<li>✅ QR dinâmicos</li>
|
||||||
|
<li>✅ Analytics</li>
|
||||||
|
<li>✅ Suporte prioritário</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<a href="/Premium/Upgrade" class="btn btn-warning">
|
||||||
|
<i class="fas fa-crown"></i> Fazer Upgrade
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
const bsModal = new bootstrap.Modal(modal);
|
||||||
|
bsModal.show();
|
||||||
|
|
||||||
|
// Remove modal when closed
|
||||||
|
modal.addEventListener('hidden.bs.modal', () => {
|
||||||
|
document.body.removeChild(modal);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRemainingCounter(remaining) {
|
||||||
|
const counterElement = document.querySelector('.qr-counter');
|
||||||
|
if (counterElement) {
|
||||||
|
counterElement.textContent = `${remaining} QR codes restantes hoje`;
|
||||||
|
|
||||||
|
if (remaining <= 3) {
|
||||||
|
counterElement.className = 'badge bg-warning qr-counter';
|
||||||
|
}
|
||||||
|
if (remaining === 0) {
|
||||||
|
counterElement.className = 'badge bg-danger qr-counter';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(message) {
|
||||||
|
this.showAlert(message, 'danger');
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccess(message) {
|
||||||
|
this.showAlert(message, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
showAlert(message, type) {
|
||||||
|
const alert = document.createElement('div');
|
||||||
|
alert.className = `alert alert-${type} alert-dismissible fade show`;
|
||||||
|
alert.innerHTML = `
|
||||||
|
${message}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const container = document.querySelector('.container');
|
||||||
|
const row = container?.querySelector('.row');
|
||||||
|
if (container && row) {
|
||||||
|
container.insertBefore(alert, row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-remove after delay
|
||||||
|
setTimeout(() => {
|
||||||
|
if (alert.parentNode) {
|
||||||
|
alert.parentNode.removeChild(alert);
|
||||||
|
}
|
||||||
|
}, type === 'success' ? 3000 : 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupRealTimePreview() {
|
||||||
|
const contentField = document.getElementById('qr-content');
|
||||||
|
const typeField = document.getElementById('qr-type');
|
||||||
|
|
||||||
|
if (contentField && typeField) {
|
||||||
|
let previewTimeout;
|
||||||
|
const updatePreview = () => {
|
||||||
|
clearTimeout(previewTimeout);
|
||||||
|
previewTimeout = setTimeout(() => {
|
||||||
|
if (contentField.value.trim() && typeField.value) {
|
||||||
|
// Could implement real-time preview for premium users
|
||||||
|
console.log('Real-time preview update');
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
contentField.addEventListener('input', updatePreview);
|
||||||
|
typeField.addEventListener('change', updatePreview);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize when DOM loads
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
window.qrGenerator = new QRRapidoGenerator();
|
||||||
|
|
||||||
|
// Initialize AdSense if necessary
|
||||||
|
if (window.adsbygoogle && document.querySelector('.adsbygoogle')) {
|
||||||
|
(adsbygoogle = window.adsbygoogle || []).push({});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Global functions for ad control
|
||||||
|
window.QRApp = {
|
||||||
|
refreshAds: function() {
|
||||||
|
if (window.adsbygoogle) {
|
||||||
|
document.querySelectorAll('.adsbygoogle').forEach(ad => {
|
||||||
|
(adsbygoogle = window.adsbygoogle || []).push({});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hideAds: function() {
|
||||||
|
document.querySelectorAll('.ad-container').forEach(ad => {
|
||||||
|
ad.style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
13
wwwroot/js/test.js
Normal file
13
wwwroot/js/test.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// Test JavaScript file
|
||||||
|
console.log('JavaScript loaded successfully!');
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
console.log('DOM Content Loaded!');
|
||||||
|
|
||||||
|
// Add test class to body
|
||||||
|
document.body.classList.add('js-loaded');
|
||||||
|
|
||||||
|
// Test if elements exist
|
||||||
|
console.log('Bootstrap classes found:', document.querySelector('.container') !== null);
|
||||||
|
console.log('FontAwesome icons found:', document.querySelector('.fas') !== null);
|
||||||
|
});
|
||||||
40
wwwroot/manifest.json
Normal file
40
wwwroot/manifest.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "QR Rapido - Gerador QR Code Ultrarrápido",
|
||||||
|
"short_name": "QR Rapido",
|
||||||
|
"description": "Gere códigos QR em segundos! Grátis e ultrarrápido.",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#FFFFFF",
|
||||||
|
"theme_color": "#007BFF",
|
||||||
|
"orientation": "portrait-primary",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/images/qrrapido-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/images/qrrapido-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"categories": ["utilities", "productivity"],
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": "Gerar QR URL",
|
||||||
|
"short_name": "QR URL",
|
||||||
|
"description": "Criar QR code para URL",
|
||||||
|
"url": "/?type=url",
|
||||||
|
"icons": [{"src": "/images/shortcut-url.png", "sizes": "96x96"}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gerar QR Texto",
|
||||||
|
"short_name": "QR Texto",
|
||||||
|
"description": "Criar QR code para texto",
|
||||||
|
"url": "/?type=text",
|
||||||
|
"icons": [{"src": "/images/shortcut-text.png", "sizes": "96x96"}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user