Compare commits

...

3 Commits

Author SHA1 Message Date
8b3da7cb0a feat: Criação de tutoriais e remoçaõ de anuncios.
All checks were successful
Deploy QR Rapido / test (push) Successful in 3m59s
Deploy QR Rapido / build-and-push (push) Successful in 12m31s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m17s
2025-10-18 23:18:12 -03:00
232d4d6c54 feat: ajustes e artigos 2025-10-13 19:50:31 -03:00
Ricardo Carneiro
2ba9a675b5 fix: logs 2025-09-22 23:50:43 -03:00
47 changed files with 4680 additions and 578 deletions

View File

@ -24,7 +24,12 @@
"Bash(dotnet restore:*)",
"Bash(rg:*)",
"Bash(dotnet test:*)",
"Bash(cp:*)"
"Bash(cp:*)",
"Bash(ping:*)",
"Bash(nc:*)",
"Bash(ssh:*)",
"Read(//mnt/c/vscode/**)",
"Read(//mnt/c/**)"
],
"deny": []
}

View File

@ -158,7 +158,7 @@ jobs:
# Se a conexão funcionou, continua com o deploy
echo "=== Iniciando Deploy no Docker Swarm ==="
# Deploy via Docker Swarm (apenas no manager - servidor 1)
# Deploy via Docker Swarm (igual ao BCards)
ssh -o StrictHostKeyChecking=no ubuntu@141.148.162.114 << 'EOF'
# Puxa nova imagem
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
@ -167,18 +167,26 @@ jobs:
sudo mkdir -p /app/keys
sudo chown -R 1000:1000 /app/keys
# Cria a rede overlay se não existir
docker network create --driver overlay --attachable qrrapido-network || echo "Rede já existe"
# Atualiza o service ou cria se não existir
docker service update \
--image ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
--env-add Serilog__OpenSearchUrl="http://141.148.162.114:19201" \
--env-add Serilog__OpenSearchFallback="http://129.146.116.218:19202" \
qrrapido-prod \
|| \
docker service create \
--name qrrapido-prod \
--replicas 2 \
--network host \
--network qrrapido-network \
--publish published=5001,target=8080 \
--mount type=bind,source=/app/keys,target=/app/keys \
--env ASPNETCORE_ENVIRONMENT=Production \
--env ASPNETCORE_URLS=http://+:5001 \
--env ASPNETCORE_URLS=http://+:8080 \
--env Serilog__OpenSearchUrl="http://141.148.162.114:19201" \
--env Serilog__OpenSearchFallback="http://129.146.116.218:19202" \
--update-delay 30s \
--update-parallelism 1 \
--update-order start-first \
@ -208,11 +216,11 @@ jobs:
# Verifica se os serviços estão respondendo em ambos servidores
echo "Verificando Servidor 1..."
ssh -o StrictHostKeyChecking=no ubuntu@141.148.162.114 'curl -f http://localhost:5001/health || echo "Servidor 1 pode não estar respondendo"'
ssh -o StrictHostKeyChecking=no ubuntu@141.148.162.114 'curl -f http://localhost:5001/health || echo "⚠️ Servidor 1 pode não estar respondendo"'
echo "Verificando Servidor 2..."
ssh -o StrictHostKeyChecking=no ubuntu@129.146.116.218 'curl -f http://localhost:5001/health || echo "Servidor 2 pode não estar respondendo"'
ssh -o StrictHostKeyChecking=no ubuntu@129.146.116.218 'curl -f http://localhost:5001/health || echo "⚠️ Servidor 2 pode não estar respondendo"'
# Testa o site principal
echo "Testando site principal..."
curl -f https://qrrapido.site || echo "Site principal pode não estar respondendo"
curl -f https://qrrapido.site || echo "⚠️ Site principal pode não estar respondendo"

286
CLAUDE.md Normal file
View File

@ -0,0 +1,286 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
QR Rapido is an ultra-fast QR code generator built with ASP.NET Core 8.0, focusing on speed and multilingual support (PT-BR, ES, EN). It features tiered user access (anonymous, logged-in free, premium), OAuth authentication (Google/Microsoft), Stripe payment integration, and ad-free session management.
**Key Performance Targets:**
- QR generation: <1.2s (average), <0.4s (premium users)
- Cache hit rate: >80%
- First Contentful Paint: <2s
## Common Commands
### Development
```bash
# Run locally (hot reload)
dotnet watch run
# Run locally (standard)
dotnet run
# Build for release
dotnet build --configuration Release
# Restore dependencies
dotnet restore
```
### Testing
```bash
# Run all tests
dotnet test
# Run with coverage
dotnet test --collect:"XPlat Code Coverage"
# Run specific test class
dotnet test --filter "QRRapidoServiceTests"
```
### Frontend Build
```bash
# Development mode with Vite
npm run dev
# Production build (Vite)
npm run build
# Preview production build
npm run preview
```
### Docker
```bash
# Build and run all services (MongoDB, Redis, Nginx)
docker-compose up -d
# View logs
docker-compose logs -f qrrapido
# Build production image for ARM64 (OCI Ampere servers)
docker buildx build --platform linux/arm64 -t qrrapido:latest .
# Stop all services
docker-compose down
```
### Stripe Webhooks (Local Testing - WSL)
```bash
# Forward Stripe webhooks to local HTTPS endpoint
stripe listen --forward-to https://localhost:52428/pagamento/stripewebhook --skip-verify
```
## Architecture
### Core Technology Stack
- **Backend**: ASP.NET Core 8.0 (MVC + Razor Pages)
- **Database**: MongoDB (users, QR history, sessions)
- **Cache**: Redis (distributed cache) + MemoryCache (fallback)
- **QR Generation**: QRCoder library with ImageSharp for logo overlay
- **Authentication**: Cookie-based with OAuth (Google, Microsoft)
- **Payments**: Stripe (subscriptions)
- **Logging**: Serilog → OpenSearch/Console
- **Frontend Build**: Vite for asset bundling
### Project Structure
```
Controllers/ # MVC controllers (Home, QR, Account, Premium, Pagamento, Health)
Services/ # Business logic (QRRapido, User, Plan, Stripe, AdDisplay)
├── Monitoring/ # Resource and MongoDB monitoring
└── HealthChecks/# Health check implementations
Models/ # Domain models and ViewModels
Data/ # MongoDbContext
Middleware/ # Custom middleware (Language redirection, LastLogin update)
Providers/ # Culture providers for localization
Resources/ # .resx files for PT-BR, ES, EN localization
Views/ # Razor views
wwwroot/ # Static assets (CSS, JS, images)
Tests/ # Unit tests (xUnit, Moq)
```
### Key Services
**QRRapidoService** (`Services/QRRapidoService.cs`):
- Core QR generation with distributed cache support
- SemaphoreSlim limits concurrent generations (default: 100)
- Cache key based on content hash + settings
- Optimized error correction levels for speed
- Logo overlay with readability analysis
**UserService** (`Services/UserService.cs`):
- MongoDB user CRUD operations
- QR history management (anonymous vs authenticated)
- Premium status checks
- Graceful fallback when MongoDB unavailable
**AdDisplayService** (`Services/AdDisplayService.cs`):
- Controls ad visibility based on user status
- 30-day ad-free period after login
- Premium users: permanent ad-free
- Anonymous users: always show ads
**StripeService** (`Services/StripeService.cs`):
- Subscription creation and management
- Webhook handling (checkout.session.completed, etc.)
- Customer portal session creation
### Middleware Pipeline (Program.cs)
1. **LanguageRedirectionMiddleware**: Redirects root `/` to `/pt-BR/` or user's language preference
2. **Request Localization**: Sets culture based on route (`/{culture}/...`) → QueryString → Cookie
3. **Authentication/Authorization**
4. **Session**
5. **LastLoginUpdateMiddleware**: Updates user's last login timestamp
### Localization Strategy
- Route-based culture: `/{culture:regex(^(pt-BR|es-PY)$)}/{controller}/{action}`
- Default culture: `pt-BR`
- Supported cultures: `pt-BR`, `es-PY`
- Culture providers priority: Route → QueryString → Cookie
- Resources in `Resources/SharedResource.{culture}.resx`
### MongoDB Collections
- **Users**: User profiles, premium status, OAuth data
- **QRCodeHistory**: Generated QR codes (linked to users or anonymous)
- **AdFreeSessions**: Ad-free session tracking (30-day grants)
- **DataProtectionKeys**: ASP.NET Core Data Protection keys (for Docker Swarm)
### Docker Swarm Deployment
**Production** uses Docker Swarm with:
- 2 replicas across 2 ARM64 servers (OCI Ampere)
- Shared MongoDB for Data Protection keys (cross-replica cookie decryption)
- Rolling updates: `start-first` strategy, 30s delay between updates
- Health checks at `/healthcheck` endpoint
- Exposed on port 5001 internally, proxied by Nginx
**Staging** uses standalone Docker containers on 2 servers.
### CI/CD Pipeline (.github/workflows/deploy.yml)
1. **Test Job**: Runs on `ubuntu-latest`
- Restore → Build → Test with coverage
- Uploads coverage to Codecov
2. **Build-and-Push Job**: Runs on self-hosted ARM64 runner
- Builds ARM64 Docker image
- Tags: `latest` (main), `develop` (develop branch)
- Pushes to private registry: `registry.redecarneir.us`
3. **Deploy-Staging**: SSH to 2 servers, pull image, run container
4. **Deploy-Production**: SSH to Swarm manager, update service with zero-downtime
### Configuration Management
**appsettings.json** key sections:
- `ConnectionStrings:MongoDB`: MongoDB connection string
- `Authentication:Google/Microsoft`: OAuth credentials
- `Stripe`: API keys and webhook secret
- `Performance:MaxConcurrentGenerations`: Semaphore limit (default: 100)
- `Premium:FreeQRLimit`: Daily limit for logged-in free users (10)
- `Serilog:OpenSearchUrl`: Centralized logging endpoint
- `ResourceMonitoring`: CPU/memory thresholds for alerts
- `HealthChecks`: Timeout and test configurations
**Environment-specific overrides**:
- `appsettings.Development.json`
- `appsettings.Production.json`
### Rate Limiting & Performance
- Fixed window rate limiter: 600 requests/minute per IP on `/api` endpoints
- Kestrel max connections: 2000
- QR generation timeout: 2000ms
- Redis cache expiration: 60 minutes
- MongoDB query timeouts: 5 seconds (health checks)
### Health Checks
Endpoint: `/healthcheck`
Checks:
- **MongoDbHealthCheck**: Database connectivity, size metrics, test query
- **ResourceHealthCheck**: CPU/memory usage, GC pressure
- **ExternalServicesHealthCheck**: Stripe API availability
### Testing Strategy
- **Unit tests**: Services layer (QRRapidoService, AdDisplayService, etc.)
- **Mocking**: MongoDB and IDistributedCache with Moq
- **Coverage**: Run `dotnet test --collect:"XPlat Code Coverage"`
- Test files in `Tests/Services/`
### Known Quirks & WSL Compatibility
- **StaticWebAssets disabled**: Set `ASPNETCORE_HOSTINGSTARTUP__STATICWEBASSETS__ENABLED=false` for WSL path issues (see Program.cs:38-39)
- **DataProtection**: Uses MongoDB for key persistence in production (Docker Swarm), filesystem in development
- **Frontend build**: Vite runs during Release build via MSBuild target (`BuildFrontend` in .csproj)
- **Stripe local testing**: Use `stripe listen` in WSL (see README.md:345-347)
### Monitoring & Logging
- **Serilog** → Console (development) + OpenSearch (production)
- **ResourceMonitoringService**: Background service tracking CPU/memory every 30s
- **HistoryCleanupService**: Cleans old anonymous QR history (7-day grace period, runs every 6 hours)
- **MongoDbMonitoringService**: Tracks database growth and collection stats (disabled by default)
### Premium Feature Gates
Check user premium status via `IUserService.GetUserAsync(userId)``user.IsPremium`
Premium benefits:
- Unlimited QR codes (vs 10/day anonymous, 50/day free)
- No ads permanently
- Priority generation (faster SemaphoreSlim release)
- Dynamic QR codes (editable)
- API access
### Ad-Free Logic
See `AdDisplayService.ShouldShowAdsAsync()`:
- Anonymous users: always show ads
- Premium users: never show ads
- Free logged-in users: 30-day ad-free period from login (configurable in appsettings)
- Ad-free sessions tracked in MongoDB `AdFreeSessions` collection
### Security Considerations
- OAuth secure flow with PKCE
- HTTPS redirect enforced (non-dev environments)
- Stripe webhook signature verification
- Input sanitization on QR generation
- Rate limiting on API endpoints
- HSTS enabled in production
- Forwarded headers support for reverse proxy (Nginx)
### Common Workflows
**Adding a new QR type:**
1. Update `Models/ViewModels/QRGenerationRequest.cs` with new type enum
2. Add generation logic in `Services/QRRapidoService.cs``GenerateQRCodeOptimizedAsync()`
3. Update frontend form in `Views/Home/Index.cshtml`
4. Add localized strings in `Resources/SharedResource.{culture}.resx`
**Adding a new language:**
1. Create `Resources/SharedResource.{culture}.resx`
2. Update `Program.cs` supported cultures array (line 214-218)
3. Update route constraint regex (line 354)
4. Add culture provider mapping if needed
**Debugging slow QR generation:**
1. Check `_logger` output in `QRRapidoService.GenerateRapidAsync()` for timing
2. Verify cache hit rate in logs
3. Check semaphore wait time (max concurrent limit)
4. Review `Performance:QRGenerationTimeoutMs` in appsettings
5. Monitor resource usage via `/healthcheck`
**Updating Stripe configuration:**
1. Update `appsettings.json``Stripe` section
2. Verify webhook secret matches Stripe dashboard
3. Test locally with `stripe listen --forward-to ...`
4. Update `StripeService.cs` webhook handlers if event types change

View File

@ -0,0 +1,108 @@
---
title: "Cómo Crear Código QR para WhatsApp"
description: "Aprende a crear un código QR para WhatsApp que permite a los usuarios iniciar una conversación contigo instantáneamente"
keywords: "codigo qr whatsapp, whatsapp codigo qr, qr para whatsapp, crear qr whatsapp"
author: "QR Rapido"
date: 2025-10-08
lastmod: 2025-10-08
image: "/images/tutoriais/whatsapp-qr-hero.jpg"
---
# Cómo Crear Código QR para WhatsApp
Crear un **código QR para WhatsApp** es una de las formas más eficientes de facilitar el contacto directo con tus clientes, amigos o seguidores. Con un simple escaneo, cualquier persona puede iniciar una conversación contigo instantáneamente, sin necesidad de guardar tu número.
## 📱 ¿Por qué usar código QR para WhatsApp?
Los códigos QR para WhatsApp son extremadamente útiles para:
- **Empresas**: Facilitar la atención al cliente
- **Freelancers**: Agilizar el contacto con potenciales clientes
- **Eventos**: Permitir networking rápido
- **Marketing**: Aumentar la conversión en campañas
## 🎯 Paso a Paso
### 1. Prepara tu número
Primero, necesitas tener tu número en formato internacional:
```
595 21 123-4567
```
Elimina todos los espacios y guiones, quedando:
```
59521123456
```
### 2. Crea la URL de WhatsApp
La URL de WhatsApp sigue este patrón:
```
https://wa.me/59521123456
```
Puedes agregar un mensaje predefinido:
```
https://wa.me/59521123456?text=¡Hola!%20Quisiera%20más%20información
```
### 3. Genera el código QR
Accede a [QR Rapido](https://qrrapido.site) y:
1. Selecciona el tipo **URL**
2. Pega la URL de WhatsApp
3. Personaliza los colores (opcional)
4. Haz clic en **Generar Código QR**
5. Descarga en alta calidad
## 💡 Consejos Profesionales
### Personaliza el Mensaje Inicial
Configura un mensaje de bienvenida automático para mejorar la experiencia:
```
https://wa.me/59521123456?text=¡Hola!%20Vine%20a%20través%20de%20tu%20código%20QR
```
### Úsalo en Materiales Impresos
- **Tarjetas de visita**: Facilita el contacto instantáneo
- **Folletos**: Aumenta el engagement
- **Embalajes**: Ofrece soporte directo al cliente
- **Banners**: En eventos y ferias
### Monitorea los Resultados
Para rastrear cuántas personas escanearon tu código QR, considera usar un acortador de URL con analytics antes de generar el QR.
## ⚠️ Cuidados Importantes
1. **Prueba antes de imprimir**: Siempre escanea para verificar que funciona
2. **Tamaño mínimo**: Mantén al menos 3x3 cm para fácil lectura
3. **Contraste adecuado**: Usa colores que contrasten bien (negro sobre blanco es ideal)
4. **Mensaje claro**: Indica para qué sirve el código QR
## 🚀 Ventajas de usar QR Rapido
- ⚡ **Ultra rápido**: Genera en menos de 1 segundo
- 🎨 **Personalizable**: Elige colores y estilos
- 📥 **Alta calidad**: Descarga en PNG, SVG y PDF
- 🔒 **Seguro**: Tus datos no son almacenados
- 💯 **Gratis**: 10 códigos QR por día
## Conclusión
Crear un código QR para WhatsApp es simple y puede revolucionar la forma en que te comunicas con tu público. Con QR Rapido, tienes todo lo que necesitas para crear códigos QR profesionales en segundos.
**¿Listo para empezar?** [Crea tu código QR ahora →](https://qrrapido.site/es-PY)
---
*¿Tienes dudas? [Contáctanos](https://qrrapido.site/es-PY/Contact)!*

View File

@ -0,0 +1,271 @@
---
title: "Cómo Crear Código QR WiFi Gratis: Comparte tu Red en Segundos"
description: "Aprende a crear código QR WiFi gratuito en pocos clics. Comparte la contraseña de tu red sin escribir con nuestro generador rápido y seguro."
keywords: "codigo qr wifi, crear codigo qr wifi, generador qr wifi gratis, qr wifi, compartir contraseña wifi, codigo qr red wifi, como hacer qr wifi"
author: "QR Rapido"
date: 2025-10-10
lastmod: 2025-10-10
image: "/images/tutoriais/qr-code-wifi-hero.jpg"
---
# Cómo Crear Código QR WiFi Gratis: Comparte tu Red en Segundos
¿Cansado de escribir contraseñas largas y complicadas cada vez que un visitante pide el WiFi? Con un **Código QR WiFi**, puedes compartir tu red instantáneamente. En este tutorial completo, aprenderás a crear un código QR para WiFi gratuitamente en menos de 2 minutos.
## ¿Por Qué Usar Código QR para WiFi?
Compartir tu red WiFi mediante código QR ofrece varias ventajas:
- **Practicidad**: Los visitantes se conectan instantáneamente sin escribir contraseñas
- **Seguridad**: No necesitas decir la contraseña en voz alta o anotarla
- **Profesionalismo**: Ideal para empresas, cafeterías, restaurantes y consultorios
- **Ahorro de tiempo**: Elimina errores de escritura y pedidos repetidos
- **Compatibilidad**: Funciona en prácticamente todos los smartphones modernos
## Paso a Paso: Cómo Crear tu Código QR WiFi
### Paso 1: Selecciona el Tipo de Código QR
Accede al generador y elige la opción **WiFi** en la lista de tipos de códigos QR disponibles.
**[INSERTAR IMAGEN 1 AQUÍ: Pantalla de selección de tipo de código QR con WiFi destacado]**
En el menú desplegable encontrarás varias opciones como URL/Link, Texto Simple, Tarjeta de Visita, SMS y Email. Para este tutorial, selecciona **WiFi**.
### Paso 2: Completa los Datos de tu Red WiFi
Ahora necesitas informar los datos de tu red. Ve los campos obligatorios:
**[INSERTAR IMAGEN 3 AQUÍ: Formulario de creación de código QR WiFi con campos completados]**
#### NetworkName (Nombre de la Red) *
Escribe exactamente el nombre de tu red WiFi (SSID). Por ejemplo: "NombreDeTuRed"
**Consejo importante**: El nombre debe ser idéntico al que aparece cuando buscas redes WiFi en el celular. ¡Respeta mayúsculas y minúsculas!
#### SecurityType (Tipo de Seguridad)
Selecciona el tipo de encriptación de tu red:
- **Red WPA (la más común)**: WPA/WPA2/WPA3 - recomendado y más seguro
- **WEP (muy antiguo)**: No recomendado por ser inseguro
- **Sin contraseña**: Para redes públicas sin protección
**[INSERTAR IMAGEN 4 AQUÍ: Ejemplo de formulario WiFi completado con "Rede-do-meu-comercio"]**
#### NetworkPassword (Contraseña de la Red) *
Introduce la contraseña de tu red WiFi. Usa el ícono de ojo para visualizar y verificar que escribiste correctamente.
**Importante**: La contraseña también distingue mayúsculas de minúsculas. ¡Verifica con atención!
#### HiddenNetwork (Red Oculta)
Marca esta opción solo si tu red está configurada como oculta (no aparece en la lista de redes disponibles).
### Paso 3: Personaliza tu Código QR (Opcional)
¡Dale a tu código QR la identidad de tu negocio!
**[INSERTAR IMAGEN 2 AQUÍ: Panel de personalización avanzada]**
#### Opciones de Personalización:
**Color Principal**: Elige el color de los cuadrados del código QR (predeterminado: azul)
**Color de Fondo**: Define el color de fondo (predeterminado: blanco)
**Tamaño**: Selecciona entre:
- Pequeño (200px) - Para uso digital
- Mediano (300px) - Recomendado para impresión
- Grande (500px) - Para banners y pósters
**Margen**:
- Compacto - Ocupa menos espacio
- Normal - Recomendado (mejor lectura)
- Amplio - Para impresiones grandes
**Consejo de diseño**: Mantén buen contraste entre el color principal y el fondo para garantizar que todos los celulares puedan leer el código.
### Paso 4: Generar y Descargar
Haz clic en el botón azul **"⚡ Generar Código QR Rápidamente"** y ¡listo! Tu código QR WiFi será generado instantáneamente.
Puedes:
- Descargar la imagen en alta calidad
- Imprimir y colocar en lugares visibles
- Compartir digitalmente
- Guardar para usar después
## Cómo tus Visitantes Usarán el Código QR WiFi
¡Es muy simple! Tus visitantes solo necesitan:
1. Abrir la cámara del celular (iOS o Android)
2. Apuntar al código QR
3. Tocar la notificación que aparece
4. Conectarse automáticamente al WiFi
**¡No necesitas descargar aplicaciones!** La mayoría de los smartphones desde 2018 ya tienen lectores de códigos QR integrados en la cámara.
## Dónde Usar tu Código QR WiFi
### Para Empresas
- Recepción de oficinas
- Salas de reuniones
- Áreas de espera
- Espacios de coworking
### Para Comercios
- Mesas de restaurantes
- Mostradores de cafeterías
- Tiendas y boutiques
- Salones de belleza
### Para Residencias
- Cuadro de entrada
- Área de parrilla
- Heladera (para fiestas)
- Home office
### Para Eventos
- Credenciales de eventos
- Stands de ferias
- Conferencias
- Bodas y fiestas
## Consejos de Seguridad
⚠️ **Importante**: Considera crear una red WiFi separada para visitantes (red guest) si deseas:
- Proteger tus dispositivos personales
- Limitar velocidad para invitados
- Tener control sobre quién accede
- Mantener tu red principal privada
Muchos routers modernos permiten crear redes guest fácilmente en las configuraciones.
## Preguntas Frecuentes (FAQ)
### ¿El Código QR WiFi expira?
¡No! El código QR funciona indefinidamente mientras los datos de la red (nombre y contraseña) permanezcan iguales.
### ¿Funciona en iPhone y Android?
¡Sí! Funciona en prácticamente todos los smartphones fabricados después de 2018 que tienen cámara.
### ¿Puedo crear para red 5GHz?
¡Sí! El proceso es exactamente el mismo. Solo usa el nombre correcto de la red 5GHz.
### ¿Es seguro?
¡Sí! El código QR solo facilita la escritura de los datos. Es tan seguro como informar la contraseña verbalmente o por escrito.
### ¿Puedo editar después de creado?
No es posible editar el código QR después de generado. Si cambias la contraseña del WiFi, necesitarás generar un nuevo código QR.
### ¿Cuántas personas pueden usar el mismo código QR?
¡Ilimitadas! No hay límite de usos para el código QR.
## Casos de Uso Reales
### Restaurantes y Cafeterías
En Paraguay, muchos negocios gastronómicos están adoptando códigos QR WiFi. Los clientes aprecian poder conectarse sin interrumpir al personal. Coloca el código QR en:
- Carteles en las mesas
- Menús impresos
- Pared cerca de la caja
### Consultorios Médicos
Los pacientes en la sala de espera pueden conectarse fácilmente mientras aguardan. Esto mejora la experiencia y reduce el estrés de la espera.
### Oficinas y Coworking
Facilita el acceso de clientes, proveedores y visitantes sin comprometer la seguridad de tu red principal. Ideal para espacios colaborativos en Asunción y otras ciudades.
### Hoteles y Hospedajes
Proporciona códigos QR en las habitaciones para que los huéspedes se conecten inmediatamente al llegar.
## Errores Comunes a Evitar
### ❌ Error 1: Nombre de Red Incorrecto
Verifica que escribiste exactamente el SSID de tu red. Un error común es confundir la red 2.4GHz con la 5GHz.
### ❌ Error 2: Contraseña con Espacios
Si tu contraseña tiene espacios, inclúyelos exactamente como están configurados en el router.
### ❌ Error 3: Tipo de Seguridad Incorrecto
Asegúrate de seleccionar el tipo correcto (WPA, WEP o sin contraseña). Si no estás seguro, verifica en la configuración de tu router.
### ❌ Error 4: QR Code Muy Pequeño
Para impresión, usa tamaño mediano (300px) o grande (500px). Los códigos pequeños pueden ser difíciles de escanear.
### ❌ Error 5: Bajo Contraste
Evita combinaciones de colores como amarillo sobre blanco o gris claro sobre gris. El contraste es esencial para la lectura correcta.
## Mejores Prácticas para Imprimir
Si vas a imprimir tu código QR WiFi, sigue estas recomendaciones:
### Material Recomendado
- **Papel fotográfico**: Para mejor calidad y durabilidad
- **Laminado**: Protege contra humedad y suciedad
- **Acrílico**: Solución premium para negocios
- **Vinilo adhesivo**: Fácil de colocar en paredes y superficies
### Tamaño de Impresión
- **Mínimo**: 5x5 cm para lectura cercana
- **Recomendado**: 10x10 cm para lectura a 30-50 cm de distancia
- **Grande**: 15x15 cm o más para lectura a mayor distancia
### Ubicación Estratégica
- A la altura de los ojos (1.40m - 1.60m)
- Bien iluminado
- Sin reflejos o brillos
- Fácilmente visible desde donde la gente se sienta o espera
## Tips para Negocios
### Agrega un Texto Atractivo
No solo coloques el código QR. Agrega texto como:
- "WiFi Gratis - Escanea y Conecta"
- "Internet Rápido - Solo Escanea"
- "Conectate Fácil con un Click"
### Diseño Personalizado
Usa los colores de tu marca en el código QR para mantener la coherencia visual con tu negocio.
### Múltiples Ubicaciones
En locales grandes, coloca varios códigos QR en diferentes puntos para facilitar el acceso.
### Actualización Periódica
Por seguridad, considera cambiar la contraseña WiFi cada 3-6 meses y generar un nuevo código QR.
## Conclusión
Crear un código QR WiFi es rápido, fácil y completamente gratuito en QR Rapido. En menos de 2 minutos puedes:
✅ Generar tu código QR personalizado
✅ Compartir tu red sin esfuerzo
✅ Proporcionar mejor experiencia a los visitantes
✅ Demostrar profesionalismo
**Prueba ahora mismo**: [Crear mi Código QR WiFi Gratis](/)
---
**¿Te gustó este tutorial?** ¡Comparte con amigos que también quieren facilitar el acceso a la red WiFi! Explora también nuestros otros tipos de códigos QR para diferentes necesidades.
## Otros Tutoriales Útiles
- Cómo crear código QR para WhatsApp
- Código QR para Tarjeta de Visita Digital
- Cómo crear código QR para URLs y Links
- Personalización avanzada de códigos QR
**¡Crea ahora tu código QR WiFi gratuito y transforma la experiencia de tus visitantes!** 🚀📱
---
## Soporte Técnico
¿Tienes problemas para crear tu código QR WiFi? Contáctanos:
- **Email**: soporte@qrrapido.site
- **WhatsApp**: [Agregar número]
- **Horario**: Lunes a Viernes, 8:00 - 18:00 (hora de Paraguay)
¡Estamos aquí para ayudarte a crear el código QR perfecto para tu negocio o hogar!

View File

@ -0,0 +1,108 @@
---
title: "Como Criar QR Code para WhatsApp"
description: "Aprenda a criar um QR Code para WhatsApp que permite aos usuários iniciarem uma conversa com você instantaneamente"
keywords: "qr code whatsapp, whatsapp qr code, qr code para whatsapp, criar qr whatsapp"
author: "QR Rapido"
date: 2025-10-08
lastmod: 2025-10-08
image: "/images/tutoriais/whatsapp-qr-hero.jpg"
---
# Como Criar QR Code para WhatsApp
Criar um **QR Code para WhatsApp** é uma das maneiras mais eficientes de facilitar o contato direto com seus clientes, amigos ou seguidores. Com um simples escaneamento, qualquer pessoa pode iniciar uma conversa com você instantaneamente, sem precisar salvar seu número.
## 📱 Por que usar QR Code para WhatsApp?
Os QR Codes para WhatsApp são extremamente úteis para:
- **Empresas**: Facilitar o atendimento ao cliente
- **Freelancers**: Agilizar o contato com potenciais clientes
- **Eventos**: Permitir networking rápido
- **Marketing**: Aumentar a conversão em campanhas
## 🎯 Passo a Passo
### 1. Prepare seu número
Primeiro, você precisa ter seu número no formato internacional:
```
55 11 98765-4321
```
Remova todos os espaços e traços, ficando:
```
5511987654321
```
### 2. Crie a URL do WhatsApp
A URL do WhatsApp segue este padrão:
```
https://wa.me/5511987654321
```
Você pode adicionar uma mensagem pré-definida:
```
https://wa.me/5511987654321?text=Olá!%20Gostaria%20de%20mais%20informações
```
### 3. Gere o QR Code
Acesse [QR Rapido](https://qrrapido.site) e:
1. Selecione o tipo **URL**
2. Cole a URL do WhatsApp
3. Personalize as cores (opcional)
4. Clique em **Gerar QR Code**
5. Faça o download em alta qualidade
## 💡 Dicas Profissionais
### Personalize a Mensagem Inicial
Configure uma mensagem de boas-vindas automática para melhorar a experiência:
```
https://wa.me/5511987654321?text=Olá!%20Vim%20através%20do%20seu%20QR%20Code
```
### Use em Materiais Impressos
- **Cartões de visita**: Facilite o contato instantâneo
- **Flyers e panfletos**: Aumente o engajamento
- **Embalagens**: Ofereça suporte direto ao cliente
- **Banners**: Em eventos e feiras
### Monitore os Resultados
Para rastrear quantas pessoas escanearam seu QR Code, considere usar um encurtador de URL com analytics antes de gerar o QR Code.
## ⚠️ Cuidados Importantes
1. **Teste antes de imprimir**: Sempre escaneie para verificar se funciona
2. **Tamanho mínimo**: Mantenha pelo menos 3x3 cm para fácil leitura
3. **Contraste adequado**: Use cores que contrastem bem (preto no branco é ideal)
4. **Mensagem clara**: Indique para que serve o QR Code
## 🚀 Vantagens de usar QR Rapido
- ⚡ **Ultrarrápido**: Gere em menos de 1 segundo
- 🎨 **Personalizável**: Escolha cores e estilos
- 📥 **Alta qualidade**: Download em PNG, SVG e PDF
- 🔒 **Seguro**: Seus dados não são armazenados
- 💯 **Gratuito**: 10 QR codes por dia
## Conclusão
Criar um QR Code para WhatsApp é simples e pode revolucionar a forma como você se comunica com seu público. Com o QR Rapido, você tem tudo que precisa para criar QR Codes profissionais em segundos.
**Pronto para começar?** [Crie seu QR Code agora →](https://qrrapido.site/pt-BR)
---
*Tem dúvidas? [Entre em contato](https://qrrapido.site/pt-BR/Contact) conosco!*

View File

@ -0,0 +1,187 @@
---
title: "Como Criar QR Code WiFi Grátis: Compartilhe sua Rede em Segundos"
description: "Aprenda a criar QR Code WiFi gratuito em poucos cliques. Compartilhe a senha da sua rede sem digitar com nosso gerador rápido e seguro."
keywords: "qr code wifi, criar qr code wifi, gerador qr code wifi grátis, qr code rede wifi, compartilhar senha wifi, qr code wifi gratuito, como fazer qr code wifi"
author: "QR Rapido"
date: 2025-10-10
lastmod: 2025-10-10
image: "/images/tutoriais/qr-code-wifi-hero.jpg"
---
# Como Criar QR Code WiFi Grátis: Compartilhe sua Rede em Segundos
Cansado de digitar senhas longas e complicadas toda vez que um visitante pede a senha do WiFi? Com um **QR Code WiFi**, você pode compartilhar sua rede instantaneamente! Neste tutorial completo, você aprenderá a criar um QR Code para WiFi gratuitamente em menos de 2 minutos.
## Por Que Usar QR Code para WiFi?
Compartilhar sua rede WiFi através de QR Code oferece diversas vantagens:
- **Praticidade**: Visitantes conectam-se instantaneamente sem digitar senhas
- **Segurança**: Não precisa falar a senha em voz alta ou anotá-la
- **Profissionalismo**: Ideal para empresas, cafeterias, restaurantes e consultórios
- **Economia de tempo**: Elimina erros de digitação e pedidos repetidos
- **Compatibilidade**: Funciona em praticamente todos os smartphones modernos
## Passo a Passo: Como Criar seu QR Code WiFi
### Passo 1: Selecione o Tipo de QR Code
Acesse o gerador e escolha a opção **WiFi** na lista de tipos de QR Code disponíveis.
**[INSERIR IMAGEM 1 AQUI: Tela de seleção de tipo de QR Code com WiFi destacado]**
No menu dropdown, você encontrará diversas opções como URL/Link, Texto Simples, Cartão de Visita, SMS e Email. Para este tutorial, selecione **WiFi**.
### Passo 2: Preencha os Dados da sua Rede WiFi
Agora você precisa informar os dados da sua rede. Veja os campos obrigatórios:
**[INSERIR IMAGEM 3 AQUI: Formulário de criação de QR Code WiFi com campos preenchidos]**
#### NetworkName (Nome da Rede) *
Digite exatamente o nome da sua rede WiFi (SSID). Por exemplo: "NomeDaSuaRede"
**Dica importante**: O nome deve ser idêntico ao que aparece quando você busca redes WiFi no celular. Respeite maiúsculas e minúsculas!
#### SecurityType (Tipo de Segurança)
Selecione o tipo de criptografia da sua rede:
- **Rede WPA (a mais comum)**: WPA/WPA2/WPA3 - recomendado e mais seguro
- **WEP (muito antigo)**: Não recomendado por ser inseguro
- **Sem senha**: Para redes públicas sem proteção
**[INSERIR IMAGEM 4 AQUI: Exemplo de formulário WiFi preenchido com "Rede-do-meu-comercio"]**
#### NetworkPassword (Senha da Rede) *
Insira a senha da sua rede WiFi. Use o ícone de olho para visualizar e conferir se digitou corretamente.
**Importante**: A senha também diferencia maiúsculas de minúsculas. Confira com atenção!
#### HiddenNetwork (Rede Oculta)
Marque esta opção apenas se sua rede está configurada como oculta (não aparece na lista de redes disponíveis).
### Passo 3: Personalize seu QR Code (Opcional)
Deixe seu QR Code com a cara do seu negócio!
**[INSERIR IMAGEM 2 AQUI: Painel de personalização avançada]**
#### Opções de Personalização:
**Cor Principal**: Escolha a cor dos quadrados do QR Code (padrão: azul)
**Cor de Fundo**: Defina a cor de fundo (padrão: branco)
**Tamanho**: Selecione entre:
- Pequeno (200px) - Para uso digital
- Médio (300px) - Recomendado para impressão
- Grande (500px) - Para banners e pôsteres
**Margem**:
- Compacta - Ocupa menos espaço
- Normal - Recomendado (melhor leitura)
- Larga - Para impressões grandes
**Dica de design**: Mantenha bom contraste entre a cor principal e o fundo para garantir que todos os celulares consigam ler o código.
### Passo 4: Gerar e Baixar
Clique no botão azul **"⚡ Gerar QR Code Rapidamente"** e pronto! Seu QR Code WiFi será gerado instantaneamente.
Você pode:
- Baixar a imagem em alta qualidade
- Imprimir e colocar em locais visíveis
- Compartilhar digitalmente
- Salvar para usar depois
## Como Seus Visitantes Usarão o QR Code WiFi
É muito simples! Seus visitantes só precisam:
1. Abrir a câmera do celular (iOS ou Android)
2. Apontar para o QR Code
3. Tocar na notificação que aparece
4. Conectar automaticamente ao WiFi
**Não precisa baixar aplicativos!** A maioria dos smartphones desde 2018 já possui leitores de QR Code integrados na câmera.
## Onde Usar seu QR Code WiFi
### Para Empresas
- Recepção de escritórios
- Salas de reunião
- Áreas de espera
- Coworking spaces
### Para Comércios
- Mesas de restaurantes
- Balcões de cafeterias
- Lojas e boutiques
- Salões de beleza
### Para Residências
- Quadro de entrada
- Área da churrasqueira
- Geladeira (para festas)
- Home office
### Para Eventos
- Credenciais de eventos
- Stands de feiras
- Conferências
- Casamentos e festas
## Dicas de Segurança
⚠️ **Importante**: Considere criar uma rede WiFi separada para visitantes (rede guest) se você deseja:
- Proteger seus dispositivos pessoais
- Limitar velocidade para convidados
- Ter controle sobre quem acessa
- Manter sua rede principal privada
Muitos roteadores modernos permitem criar redes guest facilmente nas configurações.
## Perguntas Frequentes (FAQ)
### O QR Code WiFi expira?
Não! O QR Code funciona indefinidamente enquanto os dados da rede (nome e senha) permanecerem os mesmos.
### Funciona em iPhone e Android?
Sim! Funciona em praticamente todos os smartphones fabricados após 2018 que possuem câmera.
### Posso criar para rede 5GHz?
Sim! O processo é exatamente o mesmo. Apenas use o nome correto da rede 5GHz.
### É seguro?
Sim! O QR Code apenas facilita a digitação dos dados. É tão seguro quanto informar a senha verbalmente ou por escrito.
### Posso editar depois de criado?
Não é possível editar o QR Code depois de gerado. Se mudar a senha do WiFi, precisará gerar um novo QR Code.
### Quantas pessoas podem usar o mesmo QR Code?
Ilimitadas! Não há limite de usos para o QR Code.
## Conclusão
Criar um QR Code WiFi é rápido, fácil e completamente gratuito no QR Rapido! Em menos de 2 minutos você pode:
✅ Gerar seu QR Code personalizado
✅ Compartilhar sua rede sem esforço
✅ Proporcionar melhor experiência aos visitantes
✅ Demonstrar profissionalismo
**Experimente agora mesmo**: [Criar meu QR Code WiFi Grátis](/)
---
**Gostou deste tutorial?** Compartilhe com amigos que também querem facilitar o acesso à rede WiFi! Explore também nossos outros tipos de QR Code para diferentes necessidades.
## Outros Tutoriais Úteis
- Como criar QR Code para WhatsApp
- QR Code para Cartão de Visita Digital
- Como criar QR Code para URLs e Links
- Personalização avançada de QR Codes
**Crie agora seu QR Code WiFi gratuito e transforme a experiência dos seus visitantes!** 🚀📱

View File

@ -0,0 +1,599 @@
---
title: "Código QR para Corredores de Inmuebles: Guía Completa para Etiquetas y Volantes"
description: "Descubre cómo usar código QR en etiquetas adhesivas, carteles y volantes inmobiliarios. Aumenta tus ventas con tecnología gratuita y profesional."
keywords: "codigo qr corredor inmuebles, etiqueta corredor inmobiliaria, qr inmobiliaria, etiquetas adhesivas corredores, divulgar corredor propiedades, qr cartel se vende"
author: "QR Rapido"
date: 2025-10-10
lastmod: 2025-10-10
image: "/images/tutoriais/qr-code-corretor-imoveis-hero.jpg"
---
# Código QR para Corredores de Inmuebles: Guía Completa para Etiquetas y Volantes
Si eres corredor de inmuebles, sabes que **captar leads calificados** es esencial para cerrar negocios. Imagina transformar tus carteles de "Se Vende", volantes y etiquetas adhesivas en herramientas interactivas que conectan clientes directamente a tu WhatsApp, ficha del inmueble o tarjeta de visita digital - todo esto **gratuitamente** con códigos QR.
En esta guía completa, aprenderás a crear y aplicar códigos QR profesionales en materiales inmobiliarios, aumentando tus conversiones y destacándote de la competencia.
## ¿Por Qué los Corredores de Inmuebles Deben Usar Códigos QR?
### **Ventajas Comprobadas**
- ✅ **Captación 24/7**: Tu cartel trabaja para ti incluso cuando estás durmiendo
- ✅ **Contacto Instantáneo**: Cliente escanea y ya está en tu WhatsApp
- ✅ **Cero Escritura**: Elimina errores al anotar números
- ✅ **Rastreo**: Sabe cuántas personas se interesaron
- ✅ **Profesionalismo**: Demuestra modernidad e innovación
- ✅ **Costo Cero**: Genera códigos QR ilimitados gratuitamente
- ✅ **Tour Virtual**: Lleva al cliente dentro del inmueble virtualmente
### **Estadísticas del Mercado**
Según estudios del sector inmobiliario:
- **78%** de los compradores investigan propiedades por celular
- **65%** prefieren contacto vía WhatsApp en lugar de llamada
- **43%** escanean códigos QR en carteles de inmuebles cuando los ven
- Corredores que usan código QR tienen **35% más leads** mensuales
---
## Dónde Aplicar Códigos QR en Marketing Inmobiliario
### **1. Carteles de "Se Vende" y "Se Alquila"**
**¡El uso más poderoso!** El cartel al frente del inmueble es visto por cientos de personas diariamente.
**Qué colocar en el código QR:**
- Link directo a tu WhatsApp
- vCard con tus contactos completos
- Tour virtual 360° del inmueble
- Ficha técnica detallada (PDF)
- Video del inmueble en YouTube
**Consejo profesional**: Coloca texto llamativo como:
- "Escanea y agenda tu visita AHORA"
- "Tour Virtual - Apunta tu cámara aquí"
- "WhatsApp Directo del Corredor"
### **2. Etiquetas Adhesivas y Tags**
Etiquetas pequeñas (5x5cm hasta 10x10cm) son perfectas para:
**Aplicaciones:**
- Pegar en autos de la inmobiliaria
- Fijar en portones de inmuebles
- Aplicar en vitrinas de locales
- Distribuir en establecimientos asociados
- Colocar en ascensores de edificios
**Ventajas:**
- Bajo costo de impresión
- Fácil distribución masiva
- Pueden ser cambiadas rápidamente
- Óptimas para acciones promocionales
### **3. Volantes y Flyers**
¡Transforma volantes de papel en herramientas digitales!
**Dónde aplicar:**
- Esquina superior derecha (lugar de mayor atención)
- Centro, si es el foco principal
- Reverso, con llamada destacada
**Contenido recomendado:**
- Portafolio digital de inmuebles
- Formulario de registro
- Calculadora de financiamiento
- Lista completa de propiedades disponibles
### **4. Folders y Revistas Inmobiliarias**
Materiales impresos premium merecen códigos QR estratégicos.
**Uso ideal:**
- 1 QR por inmueble destacado
- QR en la portada para portafolio completo
- QR en la contraportada con tus contactos
- QR en cada página con más información
### **5. Tarjetas de Visita**
¡El clásico nunca pasa de moda, pero puede ser mejorado!
**Código QR en la tarjeta permite:**
- Guardar contacto automáticamente (vCard)
- Ver portafolio online
- Agendar reunión directo en la agenda
- Enviar mensaje vía WhatsApp
### **6. Firma de Email**
¡Cada email que envías es una oportunidad!
**Incluye código QR para:**
- Tu vCard completo
- Último lanzamiento inmobiliario
- Evaluación gratuita de inmueble
- Agendamiento de visitas
---
## Paso a Paso: Cómo Crear Código QR para Corredores
Voy a mostrar cómo crear **3 tipos de códigos QR** esenciales para corredores:
### **Tipo 1: Código QR de vCard (Tarjeta de Visita Digital)**
Perfecto para: Tarjetas de visita, firma de email, credenciales
**[INSERTAR IMAGEN 1 AQUÍ: Selección del tipo de código QR - vCard destacado]**
#### Paso 1: Selecciona "Tarjeta de Visita"
Accede al generador y elige la opción **Tarjeta de Visita** en el menú de tipos.
#### Paso 2: Completa tus Datos Profesionales
**Información obligatoria:**
- **Nombre completo**: Juan Silva
- **Cargo**: Corredor de Inmuebles - Matrícula 12345
- **Empresa**: Inmobiliaria Success
- **Teléfono**: +595 21 123-4567
- **Email**: juan.silva@inmuebles.com.py
- **Website**: www.juansilva.inmuebles.py
- **Dirección**: Av. Mariscal López - Asunción, Paraguay
**Campos opcionales estratégicos:**
- WhatsApp Business
- Instagram profesional
- LinkedIn
- Canal de YouTube con tours virtuales
#### Paso 3: Genera y Descarga
Haz clic en **"Generar Código QR"** y descarga en alta resolución.
**Dónde usar este QR:**
- Etiquetas adhesivas en el auto
- Tarjetas de visita
- Firma de email
- Credencial profesional
---
### **Tipo 2: Código QR para WhatsApp Directo**
Perfecto para: Carteles de inmuebles, volantes, anuncios
**[INSERTAR IMAGEN 3 AQUÍ: Formulario de WhatsApp con código QR completado]**
#### Paso 1: Selecciona "WhatsApp"
En el generador, elige la opción **WhatsApp** (o SMS si prefieres).
#### Paso 2: Configura el Mensaje Pre-llenado
**Ejemplo de mensaje eficaz:**
```
¡Hola! Vi el cartel del inmueble en [CALLE/BARRIO] y me gustaría agendar una visita. ¿Puede pasarme más información?
```
**Por qué el mensaje pre-llenado funciona:**
- Cliente no necesita pensar qué escribir
- Ya sabes de qué inmueble está hablando
- Aumenta en 80% la tasa de conversión
#### Paso 3: Personaliza para Cada Inmueble
**Consejo importante**: ¡Crea códigos QR diferentes para cada inmueble!
Ejemplo para apartamento en Recoleta:
```
¡Hola! Vi el cartel del Departamento 3 dormitorios en Recoleta (Ref: DEPT-001). Me gustaría saber más detalles y agendar visita.
```
**Ventaja**: ¡Ya sabes exactamente qué inmueble quiere ver el cliente!
---
### **Tipo 3: Código QR para URL (Tour Virtual / Ficha Técnica)**
Perfecto para: Inmuebles de alto estándar, lanzamientos, propiedades rurales
**[INSERTAR IMAGEN 4 AQUÍ: Ejemplo de formulario URL completado]**
#### Paso 1: Prepara el Contenido Digital
Antes de crear el QR, necesitas tener:
**Opción A - Tour Virtual:**
- Video en YouTube del inmueble
- Tour 360° (Google Street View, Matterport)
- Galería de fotos en Instagram/Facebook
**Opción B - Landing Page:**
- Ficha técnica completa del inmueble
- Fotos en alta resolución
- Mapa de ubicación
- Calculadora de financiamiento
- Formulario de interés
#### Paso 2: Acorta la URL (¡Importante!)
Usa acortadores como:
- bit.ly
- tinyurl.com
- QR Rapido (si tiene función de acortamiento)
**Ejemplo:**
- ❌ URL larga: `https://www.miinmobiliaria.com.py/inmuebles/departamento-3-dormitorios-recoleta-asuncion-ref-dept001?utm_source=cartel`
- ✅ URL corta: `bit.ly/dept-recoleta-001`
**Ventaja de URL corta**: Genera código QR más simple y fácil de escanear
#### Paso 3: Crea el Código QR
Pega la URL corta en el campo **URL/Link** y genera el código.
---
## Cómo Personalizar tu Código QR Profesionalmente
**[INSERTAR IMAGEN 2 AQUÍ: Panel de personalización con colores personalizados]**
### **Elección de Colores Estratégicos**
**Para Inmobiliarias Tradicionales:**
- Azul marino + Blanco (confianza, seriedad)
- Negro + Dorado (lujo, exclusividad)
- Verde oscuro + Blanco (crecimiento, estabilidad)
**Para Inmobiliarias Modernas:**
- Naranja + Blanco (energía, innovación)
- Púrpura + Blanco (creatividad, diferenciación)
- Rojo + Blanco (urgencia, acción)
**Regla de oro**: ¡Siempre mantén alto contraste entre color principal y fondo!
### **Tamaños Recomendados por Aplicación**
**Carteles de calle (distancia 2-5 metros):**
- Código QR: 15x15cm o mayor
- Resolución: 500px mínimo
**Volantes A5/A4:**
- Código QR: 4x4cm a 6x6cm
- Resolución: 300px
**Etiquetas adhesivas:**
- Código QR: 5x5cm (tamaño del adhesivo)
- Resolución: 300px
**Tarjetas de visita:**
- Código QR: 2,5x2,5cm
- Resolución: 200px
### **Agrega Llamadas a la Acción (CTA)**
¡Nunca coloques solo el código QR! Agrega texto atractivo:
**Ejemplos eficaces:**
- 📱 "Apunta la cámara y habla conmigo en WhatsApp"
- 🏠 "Tour Virtual 360° - Escanea Aquí"
- 💬 "Agenda tu Visita Ahora"
- 📋 "Ve Fotos y Detalles Completos"
- 🎯 "Guarda Mi Contacto Automáticamente"
---
## Estrategias Avanzadas para Corredores
### **1. Códigos QR Rastreables (Dinámicos)**
Usa servicios de código QR dinámico para:
- Saber cuántas personas escanearon
- Ver horario de los escaneos
- Identificar ubicación aproximada
- Cambiar el destino sin reimprimir
**Cuándo usar:**
- Campañas con muchos materiales impresos
- Pruebas A/B de diferentes enfoques
- Carteles permanentes en inmuebles
### **2. Múltiples Códigos QR en el Mismo Cartel**
¿Cartel grande? ¡Usa 2-3 códigos QR diferentes!
**Ejemplo de cartel completo:**
- **QR 1** (arriba): "Habla en WhatsApp"
- **QR 2** (centro): "Tour Virtual 360°"
- **QR 3** (abajo): "Guarda Mi Contacto"
**Ventaja**: Cliente elige la acción que prefiere hacer
### **3. Código QR + Realidad Aumentada**
Para lanzamientos y emprendimientos:
- Código QR lleva a app de RA
- Cliente apunta celular y ve el edificio terminado
- Visualiza departamento decorado
- ¡Extremadamente impactante!
### **4. Campañas de Temporada**
Crea códigos QR específicos para:
- **Enero**: "Planifica 2025 - Compra tu inmueble"
- **Junio**: "Vacaciones de Julio en tu Nuevo Hogar"
- **Noviembre**: "Black Friday Inmobiliaria"
- **Diciembre**: "Empieza el Año en Casa Propia"
### **5. Alianzas Estratégicas**
Distribuye etiquetas con código QR en:
- Tiendas de materiales de construcción
- Oficinas de arquitectura
- Gestorías y escribanías
- Gimnasios y restaurantes del barrio
- Edificios comerciales (tablero de avisos)
---
## Modelos de Etiquetas Adhesivas Profesionales
### **Modelo 1: Etiqueta Minimalista (5x5cm)**
```
┌─────────────────┐
│ [CÓDIGO QR] │
│ │
│ Juan Silva │
│ Matrícula 12345 │
│ (021) 123-4567 │
└─────────────────┘
```
### **Modelo 2: Etiqueta con Destaque (7x7cm)**
```
┌─────────────────────┐
│ VENDE TU INMUEBLE │
│ SIN BUROCRACIA │
│ │
│ [CÓDIGO QR] │
│ │
│ "Escanea y habla │
│ directo conmigo" │
│ │
│ Juan Silva │
│ Corredor Mat. XXXX │
└─────────────────────┘
```
### **Modelo 3: Etiqueta Tour Virtual (10x10cm)**
```
┌───────────────────────────┐
│ TOUR VIRTUAL 360° │
│ Visita este inmueble │
│ ¡sin salir del sofá! │
│ │
│ [CÓDIGO QR GRANDE] │
│ │
│ 📱 Apunta tu cámara │
│ │
│ Inmobiliaria Success │
│ (021) 123-4567 │
└───────────────────────────┘
```
---
## Mejores Prácticas de Impresión
### **Materiales Recomendados**
**Para carteles externos:**
- **Vinilo adhesivo resistente a UV**
- **Lona con impresión UV**
- **ACM (Aluminio Compuesto)**
Duración: 2-3 años expuestos al sol
**Para etiquetas adhesivas:**
- **Papel BOPP (Polipropileno)**
- **Vinilo blanco brillante**
- **Papel couché con laminación**
Duración: 6-12 meses
**Para volantes:**
- **Couché 115g o 150g**
- **Barniz localizado en el código QR** (destaque)
### **Pruebas Antes de Imprimir en Masa**
⚠️ **SIEMPRE haz esto:**
1. Imprime 1 muestra en tamaño real
2. Prueba con 5 celulares diferentes
3. Prueba a diferentes distancias
4. Prueba con poca luz
5. Solo entonces imprime grandes cantidades
**Celulares para probar:**
- iPhone (iOS actualizado)
- Samsung (Android)
- Xiaomi o Motorola (Android popular)
### **Dónde Imprimir**
**Imprentas rápidas en Asunción:**
- Etiquetas adhesivas: 100 unidades por Gs. 150.000-350.000
- Volantes A5: 1000 unidades por Gs. 800.000-1.500.000
**Online (más económico):**
- Mercado Libre Paraguay
- Imprentas locales con pedido online
**Consejo**: ¡Pide presupuesto en 3 lugares diferentes!
---
## Errores Comunes que Corredores Deben Evitar
### ❌ **Error 1: Código QR Muy Pequeño**
**Problema**: En carteles de calle vistos de lejos, QR pequeño no funciona
**Solución**: Mínimo 15x15cm para carteles externos
### ❌ **Error 2: Colores con Bajo Contraste**
**Problema**: QR amarillo en fondo blanco no escanea
**Solución**: Usa siempre colores oscuros en fondo claro (o viceversa)
### ❌ **Error 3: No Probar Antes de Imprimir**
**Problema**: Imprime 1000 volantes y descubre que QR no funciona
**Solución**: Siempre prueba impresión piloto
### ❌ **Error 4: URL Rota o Temporal**
**Problema**: Link del inmueble expira, QR queda inútil
**Solución**: Usa URLs permanentes o QR dinámico editable
### ❌ **Error 5: Sin Instrucción de Uso**
**Problema**: Persona mayor no sabe qué hacer con el QR
**Solución**: Agrega "Apunta la cámara del celular aquí"
### ❌ **Error 6: Código QR Único para Todos los Inmuebles**
**Problema**: No sabes qué inmueble generó el lead
**Solución**: Crea QR específico para cada propiedad
### ❌ **Error 7: Material de Baja Calidad**
**Problema**: Etiqueta se desvanece en 1 mes al sol
**Solución**: Invierte en material UV resistente
---
## Casos de Éxito en Paraguay
### **Caso 1: Corredor en Asunción**
**Estrategia**: Colocó código QR en todos los 12 carteles de inmuebles
**Resultado:**
- 98 escaneos en el primer mes
- 28 conversaciones en WhatsApp
- 7 visitas agendadas
- 2 ventas cerradas
**ROI**: Invirtió Gs. 500.000 en etiquetas, facturó comisiones millonarias
### **Caso 2: Inmobiliaria en Ciudad del Este**
**Estrategia**: Volantes con QR para tour virtual de lanzamiento
**Resultado:**
- 3.000 volantes distribuidos
- 645 accesos al tour virtual
- 112 registros de interesados
- 18 departamentos vendidos en preventa
### **Caso 3: Corredor Autónomo en Encarnación**
**Estrategia**: Etiquetas adhesivas en establecimientos asociados
**Resultado:**
- 150 etiquetas distribuidas en 30 locales
- 52 nuevos contactos en 3 meses
- 9 evaluaciones de inmuebles agendadas
- 2 captaciones exclusivas
---
## Preguntas Frecuentes
### **¿Necesito pagar para crear código QR?**
¡No! En QR Rapido creas códigos QR ilimitados gratuitamente. Solo pagas si quieres recursos premium como rastreo avanzado.
### **¿El código QR funciona para siempre?**
Códigos QR estáticos (gratuitos) funcionan para siempre, pero no pueden ser editados. Códigos QR dinámicos (pagos) pueden ser editados incluso después de impresos.
### **¿Qué tipo de QR usar en carteles de inmuebles?**
Recomiendo **WhatsApp** con mensaje pre-llenado. Así el cliente ya inicia la conversación sabiendo de qué inmueble se trata.
### **¿Puedo colocar logo de la inmobiliaria en el QR?**
Sí, ¡pero con cuidado! Logos muy grandes pueden dificultar la lectura. Mantén el logo pequeño (máximo 20% del QR).
### **¿Cuántas personas escanean QR en carteles?**
Según investigaciones, entre 5-15% de las personas que ven el cartel escanean el QR. En áreas movimentadas, esto puede generar decenas de leads por mes.
### **¿El código QR funciona de noche?**
Sí, siempre que haya alguna iluminación (poste de calle, luz de la pantalla del celular ya ayuda). Para mejor resultado, ilumina el cartel.
### **¿Puedo usar el mismo QR en varios materiales?**
Puedes, pero no es recomendado. Crea códigos QR diferentes para saber de dónde viene cada lead (cartel, volante, etiqueta, etc).
---
## Checklist del Corredor Profesional
Antes de imprimir, verifica:
- [ ] Código QR probado en al menos 3 celulares diferentes
- [ ] Tamaño adecuado para la distancia de escaneo
- [ ] Alto contraste entre QR y fondo
- [ ] Llamada a la acción clara ("Escanea aquí")
- [ ] Información de contacto visible (nombre, matrícula, teléfono)
- [ ] URL corta si es link (más fácil de escanear)
- [ ] Material de impresión resistente (especialmente externo)
- [ ] Margen de seguridad alrededor del QR (mínimo 1cm)
---
## Conclusión
Los códigos QR son la **herramienta más económica y eficaz** para corredores modernos captar leads calificados. Con inversión de menos de Gs. 500.000 en etiquetas y volantes, puedes:
✅ Captar leads 24 horas al día
✅ Facilitar el contacto instantáneo vía WhatsApp
✅ Mostrar tours virtuales impresionantes
✅ Rastrear qué materiales generan más resultado
✅ Destacarte de la competencia conservadora
**El mercado inmobiliario está cada vez más digital. Quien no se adapta, queda atrás.**
---
## ¡Empieza Ahora!
**Crea tu primer código QR para corredor gratuitamente:**
1. [Generar Código QR de WhatsApp](/) - Para carteles de inmuebles
2. [Generar Código QR vCard](/) - Para tarjetas de visita
3. [Generar Código QR de URL](/) - Para tour virtual
**¡Transforma tus carteles y volantes en máquinas de captar leads!** 🏠📱🚀

View File

@ -0,0 +1,640 @@
---
title: "QR Code para Corretores de Imóveis: Guia Completo para Etiquetas e Panfletos"
description: "Descubra como usar QR Code em etiquetas adesivas, placas e panfletos imobiliários. Aumente suas vendas com tecnologia gratuita e profissional."
keywords: "qr code corretor imoveis, etiqueta corretor imoveis, qrcode imobiliaria, etiquetas adesivas corretores, divulgar corretor imoveis, qr code placa vende-se"
author: "QR Rapido"
date: 2025-10-10
lastmod: 2025-10-10
image: "/images/tutoriais/qr-code-corretor-imoveis-hero.jpg"
---
# QR Code para Corretores de Imóveis: Guia Completo para Etiquetas e Panfletos
Se você é corretor de imóveis, sabe que **captar leads qualificados** é essencial para fechar negócios. Imagine transformar suas placas de "Vende-se", panfletos e etiquetas adesivas em ferramentas interativas que conectam clientes diretamente ao seu WhatsApp, ficha do imóvel ou cartão de visita digital - tudo isso **gratuitamente** com QR Codes!
Neste guia completo, você aprenderá a criar e aplicar QR Codes profissionais em materiais imobiliários, aumentando suas conversões e se destacando da concorrência.
## Por Que Corretores de Imóveis Devem Usar QR Codes?
### **Vantagens Comprovadas**
- ✅ **Captação 24/7**: Sua placa trabalha para você mesmo quando está dormindo
- ✅ **Contato Instantâneo**: Cliente escaneia e já está no seu WhatsApp
- ✅ **Zero Digitação**: Elimina erros ao anotar números
- ✅ **Rastreamento**: Saiba quantas pessoas se interessaram
- ✅ **Profissionalismo**: Demonstra modernidade e inovação
- ✅ **Custo Zero**: Gere QR Codes ilimitados gratuitamente
- ✅ **Tour Virtual**: Leve o cliente para dentro do imóvel virtualmente
### **Estatísticas do Mercado**
Segundo estudos do setor imobiliário:
- **78%** dos compradores pesquisam imóveis pelo celular
- **65%** preferem contato via WhatsApp ao invés de ligação
- **43%** escaneiam QR Codes em placas de imóveis quando veem
- Corretores que usam QR Code têm **35% mais leads** mensais
---
## Onde Aplicar QR Codes no Marketing Imobiliário
### **1. Placas de "Vende-se" e "Aluga-se"**
**O uso mais poderoso!** A placa na frente do imóvel é vista por centenas de pessoas diariamente.
**O que colocar no QR Code:**
- Link direto para seu WhatsApp
- vCard com seus contatos completos
- Tour virtual 360° do imóvel
- Ficha técnica detalhada (PDF)
- Vídeo do imóvel no YouTube
**Dica profissional**: Coloque texto chamativo como:
- "Escanei e agende sua visita AGORA"
- "Tour Virtual - Aponte a câmera aqui"
- "WhatsApp Direto do Corretor"
### **2. Etiquetas Adesivas e Tags**
Etiquetas pequenas (5x5cm até 10x10cm) são perfeitas para:
**Aplicações:**
- Colar em carros da imobiliária
- Fixar em portões de imóveis
- Aplicar em vitrines de lojas
- Distribuir em estabelecimentos parceiros
- Colocar em elevadores de prédios
**Vantagens:**
- Baixo custo de impressão
- Fácil distribuição em massa
- Podem ser trocadas rapidamente
- Ótimas para ações promocionais
### **3. Panfletos e Flyers**
Transforme panfletos de papel em ferramentas digitais!
**Onde aplicar:**
- Canto superior direito (local de maior atenção)
- Centro, se for o foco principal
- Verso, com chamada destacada
**Conteúdo recomendado:**
- Portfólio digital de imóveis
- Formulário de cadastro
- Calculadora de financiamento
- Lista completa de imóveis disponíveis
### **4. Folders e Revistas Imobiliárias**
Materiais impressos premium merecem QR Codes estratégicos.
**Uso ideal:**
- 1 QR por imóvel destacado
- QR na capa para portfólio completo
- QR na contracapa com seus contatos
- QR em cada página com mais informações
### **5. Cartões de Visita**
O clássico nunca sai de moda, mas pode ser turbinado!
**QR Code no cartão permite:**
- Salvar contato automaticamente (vCard)
- Ver portfólio online
- Agendar reunião direto na agenda
- Enviar mensagem via WhatsApp
### **6. Assinatura de Email**
Todo email que você envia é uma oportunidade!
**Inclua QR Code para:**
- Seu vCard completo
- Último lançamento imobiliário
- Avaliação gratuita de imóvel
- Agendamento de visitas
---
## Passo a Passo: Como Criar QR Code para Corretores
Vou mostrar como criar **3 tipos de QR Codes** essenciais para corretores:
### **Tipo 1: QR Code de vCard (Cartão de Visita Digital)**
Perfeito para: Cartões de visita, assinatura de email, crachás
**[INSERIR IMAGEM 1 AQUI: Seleção do tipo de QR Code - vCard destacado]**
#### Passo 1: Selecione "Cartão de Visita"
Acesse o gerador e escolha a opção **Cartão de Visita** no menu de tipos.
#### Passo 2: Preencha seus Dados Profissionais
**Informações obrigatórias:**
- **Nome completo**: João Silva
- **Cargo**: Corretor de Imóveis CRECI 12345-F
- **Empresa**: Imobiliária Success
- **Telefone**: +55 11 98765-4321
- **Email**: joao.silva@imoveis.com.br
- **Website**: www.joaosilva.imoveis.br
- **Endereço**: Av. Paulista, 1000 - São Paulo, SP
**Campos opcionais estratégicos:**
- WhatsApp Business
- Instagram profissional
- LinkedIn
- Canal do YouTube com tours virtuais
#### Passo 3: Gere e Baixe
Clique em **"Gerar QR Code"** e baixe em alta resolução.
**Onde usar este QR:**
- Etiquetas adesivas no carro
- Cartões de visita
- Assinatura de email
- Crachá profissional
---
### **Tipo 2: QR Code para WhatsApp Direto**
Perfeito para: Placas de imóveis, panfletos, anúncios
**[INSERIR IMAGEM 3 AQUI: Formulário de WhatsApp QR Code preenchido]**
#### Passo 1: Selecione "WhatsApp"
No gerador, escolha a opção **WhatsApp** (ou SMS se preferir).
#### Passo 2: Configure a Mensagem Pré-Pronta
**Exemplo de mensagem eficaz:**
```
Olá! Vi a placa do imóvel na [RUA/BAIRRO] e gostaria de agendar uma visita. Pode me passar mais informações?
```
**Por que mensagem pré-pronta funciona:**
- Cliente não precisa pensar no que escrever
- Você já sabe de qual imóvel ele está falando
- Aumenta em 80% a taxa de conversão
#### Passo 3: Personalize para Cada Imóvel
**Dica importante**: Crie QR Codes diferentes para cada imóvel!
Exemplo para apartamento no Jardins:
```
Olá! Vi a placa do Apartamento 3 quartos no Jardins (Ref: APT-001). Gostaria de saber mais detalhes e agendar visita.
```
**Vantagem**: Você já sabe exatamente qual imóvel o cliente quer ver!
---
### **Tipo 3: QR Code para URL (Tour Virtual / Ficha Técnica)**
Perfeito para: Imóveis de alto padrão, lançamentos, propriedades rurais
**[INSERIR IMAGEM 4 AQUI: Exemplo de formulário URL completado]**
#### Passo 1: Prepare o Conteúdo Digital
Antes de criar o QR, você precisa ter:
**Opção A - Tour Virtual:**
- Vídeo no YouTube do imóvel
- Tour 360° (Google Street View, Matterport)
- Galeria de fotos no Instagram/Facebook
**Opção B - Landing Page:**
- Ficha técnica completa do imóvel
- Fotos em alta resolução
- Mapa de localização
- Calculadora de financiamento
- Formulário de interesse
#### Passo 2: Encurte a URL (Importante!)
Use encurtadores como:
- bit.ly
- tinyurl.com
- QR Rapido (se tiver função de encurtamento)
**Exemplo:**
- ❌ URL longa: `https://www.minhaibiliaria.com.br/imoveis/apartamento-3-quartos-jardins-sao-paulo-ref-apt001?utm_source=placa`
- ✅ URL curta: `bit.ly/apt-jardins-001`
**Vantagem da URL curta**: Gera QR Code mais simples e fácil de escanear
#### Passo 3: Crie o QR Code
Cole a URL curta no campo **URL/Link** e gere o código.
---
## Como Personalizar seu QR Code Profissionalmente
**[INSERIR IMAGEM 2 AQUI: Painel de personalização com cores personalizadas]**
### **Escolha de Cores Estratégicas**
**Para Imobiliárias Tradicionais:**
- Azul marinho + Branco (confiança, seriedade)
- Preto + Dourado (luxo, exclusividade)
- Verde escuro + Branco (crescimento, estabilidade)
**Para Imobiliárias Modernas:**
- Laranja + Branco (energia, inovação)
- Roxo + Branco (criatividade, diferenciação)
- Vermelho + Branco (urgência, ação)
**Regra de ouro**: Sempre mantenha alto contraste entre cor principal e fundo!
### **Tamanhos Recomendados por Aplicação**
**Placas de rua (distância 2-5 metros):**
- QR Code: 15x15cm ou maior
- Resolução: 500px mínimo
**Panfletos A5/A4:**
- QR Code: 4x4cm a 6x6cm
- Resolução: 300px
**Etiquetas adesivas:**
- QR Code: 5x5cm (tamanho do adesivo)
- Resolução: 300px
**Cartões de visita:**
- QR Code: 2,5x2,5cm
- Resolução: 200px
### **Adicione Chamadas para Ação (CTA)**
Nunca coloque apenas o QR Code sozinho! Adicione texto atrativo:
**Exemplos eficazes:**
- 📱 "Aponte a câmera e fale comigo no WhatsApp"
- 🏠 "Tour Virtual 360° - Escaneie Aqui"
- 💬 "Agende sua Visita Agora"
- 📋 "Veja Fotos e Detalhes Completos"
- 🎯 "Salve Meu Contato Automaticamente"
---
## Estratégias Avançadas para Corretores
### **1. QR Codes Rastreáveis (Dinâmicos)**
Use serviços de QR Code dinâmico para:
- Saber quantas pessoas escanearam
- Ver horário dos escaneamentos
- Identificar localização aproximada
- Mudar o destino sem reimprimir
**Quando usar:**
- Campanhas com muitos materiais impressos
- Testes A/B de diferentes abordagens
- Placas permanentes em imóveis
### **2. Múltiplos QR Codes na Mesma Placa**
Placa grande? Use 2-3 QR Codes diferentes!
**Exemplo de placa completa:**
- **QR 1** (topo): "Fale no WhatsApp"
- **QR 2** (centro): "Tour Virtual 360°"
- **QR 3** (rodapé): "Salve Meu Contato"
**Vantagem**: Cliente escolhe a ação que prefere fazer
### **3. QR Code + Realidade Aumentada**
Para lançamentos e empreendimentos:
- QR Code leva para app de RA
- Cliente aponta celular e vê o prédio pronto
- Visualiza apartamento decorado
- Extremamente impactante!
### **4. Campanhas Sazonais**
Crie QR Codes específicos para:
- **Janeiro**: "Planeje 2025 - Compre seu imóvel"
- **Junho**: "Férias de Julho no seu Novo Lar"
- **Novembro**: "Black Friday Imobiliária"
- **Dezembro**: "Comece o Ano na Casa Própria"
### **5. Parcerias Estratégicas**
Distribua etiquetas com QR Code em:
- Lojas de materiais de construção
- Escritórios de arquitetura
- Despachantes e cartórios
- Academias e restaurantes do bairro
- Prédios comerciais (quadro de avisos)
---
## Modelos de Etiquetas Adesivas Profissionais
### **Modelo 1: Etiqueta Minimalista (5x5cm)**
```
┌─────────────────┐
│ [QR CODE] │
│ │
│ João Silva │
│ CRECI 12345-F │
│ (11) 98765-4321 │
└─────────────────┘
```
### **Modelo 2: Etiqueta com Destaque (7x7cm)**
```
┌─────────────────────┐
│ VENDA SEU IMÓVEL │
│ SEM BUROCRACIA │
│ │
│ [QR CODE] │
│ │
│ "Escaneie e fale │
│ direto comigo" │
│ │
│ João Silva │
│ Corretor CRECI │
└─────────────────────┘
```
### **Modelo 3: Etiqueta Tour Virtual (10x10cm)**
```
┌───────────────────────────┐
│ TOUR VIRTUAL 360° │
│ Visite este imóvel │
│ sem sair do sofá! │
│ │
│ [QR CODE GRANDE] │
│ │
│ 📱 Aponte sua câmera │
│ │
│ Imobiliária Success │
│ (11) 98765-4321 │
└───────────────────────────┘
```
---
## Melhores Práticas de Impressão
### **Materiais Recomendados**
**Para placas externas:**
- **Vinil adesivo resistente a UV**
- **Lona com impressão UV**
- **ACM (Alumínio Composto)**
Duração: 2-3 anos expostos ao sol
**Para etiquetas adesivas:**
- **Papel BOPP (Polipropileno)**
- **Vinil branco brilhante**
- **Papel couché com laminação**
Duração: 6-12 meses
**Para panfletos:**
- **Couché 115g ou 150g**
- **Verniz localizado no QR Code** (destaque)
### **Testes Antes de Imprimir em Massa**
⚠️ **SEMPRE faça isso:**
1. Imprima 1 amostra em tamanho real
2. Teste com 5 celulares diferentes
3. Teste em diferentes distâncias
4. Teste com pouca luz
5. Só então imprima grandes quantidades
**Celulares para testar:**
- iPhone (iOS atualizado)
- Samsung (Android)
- Xiaomi ou Motorola (Android popular)
### **Onde Imprimir**
**Gráficas rápidas:**
- Etiquetas adesivas: 100 unidades por R$ 30-80
- Panfletos A5: 1000 unidades por R$ 150-300
**Online (mais barato):**
- Printi.com.br
- Gráfica KWG
- Gráfica Atual
**Dica**: Peça orçamento em 3 lugares diferentes!
---
## Erros Comuns que Corretores Devem Evitar
### ❌ **Erro 1: QR Code Muito Pequeno**
**Problema**: Em placas de rua vistas de longe, QR pequeno não funciona
**Solução**: Mínimo 15x15cm para placas externas
### ❌ **Erro 2: Cores com Baixo Contraste**
**Problema**: QR amarelo em fundo branco não escaneia
**Solução**: Use sempre cores escuras em fundo claro (ou vice-versa)
### ❌ **Erro 3: Não Testar Antes de Imprimir**
**Problema**: Imprime 1000 panfletos e descobre que QR não funciona
**Solução**: Sempre teste impressão piloto
### ❌ **Erro 4: URL Quebrada ou Temporária**
**Problema**: Link do imóvel expira, QR fica inútil
**Solução**: Use URLs permanentes ou QR dinâmico editável
### ❌ **Erro 5: Sem Instrução de Uso**
**Problema**: Pessoa mais velha não sabe o que fazer com o QR
**Solução**: Adicione "Aponte a câmera do celular aqui"
### ❌ **Erro 6: QR Code Único para Todos Imóveis**
**Problema**: Não sabe qual imóvel gerou o lead
**Solução**: Crie QR específico para cada propriedade
### ❌ **Erro 7: Material de Baixa Qualidade**
**Problema**: Etiqueta desbota em 1 mês no sol
**Solução**: Invista em material UV resistente
---
## Cases de Sucesso
### **Case 1: Corretor em São Paulo**
**Estratégia**: Colocou QR Code em todas as 15 placas de imóveis
**Resultado:**
- 127 escaneamentos no primeiro mês
- 34 conversas no WhatsApp
- 8 visitas agendadas
- 2 vendas fechadas
**ROI**: Investiu R$ 150 em etiquetas, faturou R$ 28.000 em comissões
### **Case 2: Imobiliária no Rio de Janeiro**
**Estratégia**: Panfletos com QR para tour virtual de lançamento
**Resultado:**
- 5.000 panfletos distribuídos
- 890 acessos ao tour virtual
- 156 cadastros de interessados
- 23 apartamentos vendidos na pré-venda
### **Case 3: Corretor Autônomo no Interior**
**Estratégia**: Etiquetas adesivas em estabelecimentos parceiros
**Resultado:**
- 200 etiquetas distribuídas em 40 locais
- 67 novos contatos em 3 meses
- 12 avaliações de imóveis agendadas
- 3 captações exclusivas
---
## Ferramentas Complementares
### **Criação de Conteúdo Digital**
- **Canva**: Criar layouts de etiquetas e panfletos
- **Matterport**: Tours virtuais 360°
- **YouTube**: Hospedar vídeos de imóveis
- **Google Drive**: PDFs de fichas técnicas
### **Gestão de Leads**
- **Bitly**: Encurtar URLs e rastrear cliques
- **Google Analytics**: Monitorar acessos
- **WhatsApp Business**: Organizar conversas
- **RD Station**: CRM imobiliário
### **Impressão Online**
- **Printi**: Etiquetas e panfletos
- **Gráfica KWG**: Placas e banners
- **Sticker Mule**: Adesivos premium
---
## Perguntas Frequentes
### **Preciso pagar para criar QR Code?**
Não! No QR Rapido você cria QR Codes ilimitados gratuitamente. Só paga se quiser recursos premium como rastreamento avançado.
### **O QR Code funciona para sempre?**
QR Codes estáticos (gratuitos) funcionam para sempre, mas não podem ser editados. QR Codes dinâmicos (pagos) podem ser editados mesmo depois de impressos.
### **Qual tipo de QR usar em placas de imóveis?**
Recomendo **WhatsApp** com mensagem pré-pronta. Assim o cliente já inicia a conversa sabendo de qual imóvel se trata.
### **Posso colocar logo da imobiliária no QR?**
Sim, mas com cuidado! Logos muito grandes podem dificultar a leitura. Mantenha a logo pequena (máximo 20% do QR).
### **Quantas pessoas escaneiam QR em placas?**
Segundo pesquisas, entre 5-15% das pessoas que veem a placa escaneiam o QR. Em áreas movimentadas, isso pode gerar dezenas de leads por mês.
### **QR Code funciona à noite?**
Sim, desde que haja alguma iluminação (poste de rua, luz da tela do celular já ajuda). Para melhor resultado, ilumine a placa.
### **Posso usar o mesmo QR em vários materiais?**
Pode, mas não é recomendado. Crie QR Codes diferentes para saber de onde vem cada lead (placa, panfleto, etiqueta, etc).
### **Como sei se o QR está funcionando?**
Teste imediatamente após criar! Use a câmera do seu celular e de pelo menos 2 amigos/familiares para garantir.
---
## Checklist do Corretor Profissional
Antes de imprimir, confira:
- [ ] QR Code testado em pelo menos 3 celulares diferentes
- [ ] Tamanho adequado para a distância de escaneamento
- [ ] Alto contraste entre QR e fundo
- [ ] Chamada para ação clara ("Escaneie aqui")
- [ ] Informações de contato visíveis (nome, CRECI, telefone)
- [ ] URL curta se for link (mais fácil de escanear)
- [ ] Material de impressão resistente (especialmente externo)
- [ ] Margem de segurança ao redor do QR (mínimo 1cm)
---
## Conclusão
QR Codes são a **ferramenta mais barata e eficaz** para corretores modernos captarem leads qualificados. Com investimento de menos de R$ 100 em etiquetas e panfletos, você pode:
✅ Captar leads 24 horas por dia
✅ Facilitar o contato instantâneo via WhatsApp
✅ Mostrar tours virtuais impressionantes
✅ Rastrear quais materiais geram mais resultado
✅ Se destacar da concorrência conservadora
**O mercado imobiliário está cada vez mais digital. Quem não se adapta, fica para trás.**
---
## Comece Agora!
**Crie seu primeiro QR Code para corretor gratuitamente:**
1. [Gerar QR Code de WhatsApp](/) - Para placas de imóveis
2. [Gerar QR Code vCard](/) - Para cartões de visita
3. [Gerar QR Code de URL](/) - Para tour virtual
**Transforme suas placas e panfletos em máquinas de captar leads!** 🏠📱🚀
---
## Materiais Bônus para Download
- 📋 Template de mensagem pré-pronta para WhatsApp
- 🎨 Modelos de etiquetas editáveis (Canva)
- 📊 Planilha de controle de QR Codes por imóvel
- 🎯 Checklist de impressão profissional
**Quer se destacar no mercado imobiliário? Use QR Codes de forma estratégica e veja seus resultados multiplicarem!**

View File

@ -14,13 +14,15 @@ namespace QRRapidoApp.Controllers
private readonly IUserService _userService;
private readonly IConfiguration _config;
private readonly IStringLocalizer<QRRapidoApp.Resources.SharedResource> _localizer;
private readonly IMarkdownService _markdownService;
public HomeController(
ILogger<HomeController> logger,
AdDisplayService adDisplayService,
IUserService userService,
IConfiguration config,
IStringLocalizer<QRRapidoApp.Resources.SharedResource> localizer
ILogger<HomeController> logger,
AdDisplayService adDisplayService,
IUserService userService,
IConfiguration config,
IStringLocalizer<QRRapidoApp.Resources.SharedResource> localizer,
IMarkdownService markdownService
)
{
_logger = logger;
@ -28,6 +30,7 @@ namespace QRRapidoApp.Controllers
_userService = userService;
_config = config;
_localizer = localizer;
_markdownService = markdownService;
}
public async Task<IActionResult> Index()
@ -163,10 +166,10 @@ namespace QRRapidoApp.Controllers
var errorCode = Request.Query["code"].ToString();
var errorMessage = "";
// Interpretar códigos de erro específicos
// Interpretar c<EFBFBD>digos de erro espec<65>ficos
if (errorCode.StartsWith("M.C506"))
{
errorMessage = "Erro de autenticação. Verifique suas credenciais e tente novamente.";
errorMessage = "Erro de autentica<EFBFBD><EFBFBD>o. Verifique suas credenciais e tente novamente.";
}
ViewBag.ErrorCode = errorCode;
@ -204,97 +207,131 @@ namespace QRRapidoApp.Controllers
// Sitemap endpoint for SEO
[Route("sitemap.xml")]
public IActionResult Sitemap()
public async Task<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/pt-BR/About</loc>
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://qrrapido.site/es-PY/About</loc>
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://qrrapido.site/pt-BR/Contact</loc>
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://qrrapido.site/es-PY/Contact</loc>
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://qrrapido.site/pt-BR/FAQ</loc>
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://qrrapido.site/es-PY/FAQ</loc>
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://qrrapido.site/pt-BR/HowToUse</loc>
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://qrrapido.site/es-PY/HowToUse</loc>
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</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>
<url>
<loc>https://qrrapido.site/privacy</loc>
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://qrrapido.site/terms</loc>
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
</urlset>";
var baseUrl = "https://qrrapido.site";
var now = DateTime.UtcNow.ToString("yyyy-MM-dd");
return Content(sitemap, "application/xml");
var sitemapBuilder = new System.Text.StringBuilder();
sitemapBuilder.AppendLine(@"<?xml version=""1.0"" encoding=""UTF-8""?>");
sitemapBuilder.AppendLine(@"<urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"">");
// Static pages
sitemapBuilder.AppendLine($@"
<url>
<loc>{baseUrl}/</loc>
<lastmod>{now}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>{baseUrl}/pt/</loc>
<lastmod>{now}</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>{baseUrl}/es/</loc>
<lastmod>{now}</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>{baseUrl}/pt-BR/About</loc>
<lastmod>{now}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>{baseUrl}/es-PY/About</loc>
<lastmod>{now}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>{baseUrl}/pt-BR/Contact</loc>
<lastmod>{now}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>{baseUrl}/es-PY/Contact</loc>
<lastmod>{now}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>{baseUrl}/pt-BR/FAQ</loc>
<lastmod>{now}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>{baseUrl}/es-PY/FAQ</loc>
<lastmod>{now}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>{baseUrl}/pt-BR/HowToUse</loc>
<lastmod>{now}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>{baseUrl}/es-PY/HowToUse</loc>
<lastmod>{now}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>{baseUrl}/Pagamento/SelecaoPlano</loc>
<lastmod>{now}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>{baseUrl}/privacy</loc>
<lastmod>{now}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>{baseUrl}/terms</loc>
<lastmod>{now}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>");
// Dynamic tutorial pages
try
{
var allArticles = await _markdownService.GetAllArticlesForSitemapAsync();
foreach (var article in allArticles)
{
var slug = article.Title.ToLower().Replace(" ", "-");
var lastMod = article.LastMod.ToString("yyyy-MM-dd");
sitemapBuilder.AppendLine($@"
<url>
<loc>{baseUrl}/{article.Culture}/tutoriais/{slug}</loc>
<lastmod>{lastMod}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>");
}
_logger.LogInformation("Generated sitemap with {Count} tutorial articles", allArticles.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error adding tutorials to sitemap");
}
sitemapBuilder.AppendLine("</urlset>");
return Content(sitemapBuilder.ToString(), "application/xml");
}
}

View File

@ -110,6 +110,36 @@ namespace QRRapidoApp.Controllers
}
}
[HttpPost]
public async Task<IActionResult> RequestRefund()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return Json(new { success = false, error = "Usuário não autenticado" });
}
try
{
var (success, message) = await _stripeService.CancelAndRefundSubscriptionAsync(userId);
if (success)
{
TempData["Success"] = message;
return Json(new { success = true, message = message });
}
else
{
return Json(new { success = false, error = message });
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error processing refund for user {userId}");
return Json(new { success = false, error = "Erro inesperado ao processar reembolso" });
}
}
[HttpGet]
public async Task<IActionResult> BillingPortal()
{

View File

@ -16,17 +16,17 @@ namespace QRRapidoApp.Controllers
private readonly IUserService _userService;
private readonly AdDisplayService _adService;
private readonly ILogger<QRController> _logger;
private readonly IStringLocalizer<QRRapidoApp.Resources.SharedResource> _localizer;
private readonly AdDisplayService _adDisplayService;
private readonly IStringLocalizer<QRRapidoApp.Resources.SharedResource> _localizer;
private readonly AdDisplayService _adDisplayService;
public QRController(IQRCodeService qrService, IUserService userService, AdDisplayService adService, ILogger<QRController> logger, IStringLocalizer<QRRapidoApp.Resources.SharedResource> localizer, AdDisplayService adDisplayService)
{
_qrService = qrService;
_userService = userService;
_adService = adService;
_logger = logger;
_localizer = localizer;
_adDisplayService = adDisplayService;
_localizer = localizer;
_adDisplayService = adDisplayService;
}
[HttpPost("GenerateRapid")]
@ -36,7 +36,7 @@ namespace QRRapidoApp.Controllers
var requestId = Guid.NewGuid().ToString("N")[..8];
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var isAuthenticated = User?.Identity?.IsAuthenticated ?? false;
using (_logger.BeginScope(new Dictionary<string, object>
{
["RequestId"] = requestId,
@ -90,7 +90,7 @@ namespace QRRapidoApp.Controllers
return StatusCode(429, new
{
error = _localizer["RateLimitReached"],
upgradeUrl = "/Premium/Upgrade",
upgradeUrl = "/Pagamento/SelecaoPlano",
success = false
});
}
@ -406,7 +406,7 @@ namespace QRRapidoApp.Controllers
return StatusCode(429, new
{
error = _localizer["RateLimitReached"],
upgradeUrl = "/Premium/Upgrade",
upgradeUrl = "/Pagamento/SelecaoPlano",
success = false
});
}
@ -468,7 +468,7 @@ namespace QRRapidoApp.Controllers
public async Task<IActionResult> GetHistory(int limit = 20)
{
try
{
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
@ -489,7 +489,7 @@ namespace QRRapidoApp.Controllers
public async Task<IActionResult> GetUserStats()
{
try
{
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{

View File

@ -0,0 +1,111 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using QRRapidoApp.Services;
using System.Security.Claims;
namespace QRRapidoApp.Controllers
{
public class TutoriaisController : Controller
{
private readonly IMarkdownService _markdownService;
private readonly AdDisplayService _adDisplayService;
private readonly ILogger<TutoriaisController> _logger;
private readonly IStringLocalizer<QRRapidoApp.Resources.SharedResource> _localizer;
private readonly IConfiguration _config;
public TutoriaisController(
IMarkdownService markdownService,
AdDisplayService adDisplayService,
ILogger<TutoriaisController> logger,
IStringLocalizer<QRRapidoApp.Resources.SharedResource> localizer,
IConfiguration config)
{
_markdownService = markdownService;
_adDisplayService = adDisplayService;
_logger = logger;
_localizer = localizer;
_config = config;
}
[Route("{culture:regex(^(pt-BR|es-PY)$)}/tutoriais/{slug}")]
public async Task<IActionResult> Article(string slug, string culture)
{
try
{
var article = await _markdownService.GetArticleAsync(slug, culture);
if (article == null)
{
_logger.LogWarning("Article not found: {Slug} ({Culture})", slug, culture);
return NotFound();
}
// Set ViewBag for ads and user info
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 ?? "";
_adDisplayService.SetViewBagAds(ViewBag);
// Set SEO metadata from article
ViewBag.Title = article.Metadata.Title;
ViewBag.Description = article.Metadata.Description;
ViewBag.Keywords = article.Metadata.Keywords;
ViewBag.OgImage = article.Metadata.Image;
ViewBag.OgType = "article";
ViewBag.ArticleAuthor = article.Metadata.Author;
ViewBag.ArticlePublishedTime = article.Metadata.Date.ToString("yyyy-MM-ddTHH:mm:ssZ");
ViewBag.ArticleModifiedTime = article.Metadata.LastMod.ToString("yyyy-MM-ddTHH:mm:ssZ");
ViewBag.Culture = culture;
ViewBag.Slug = slug;
// Get related articles (same culture, exclude current)
var allArticles = await _markdownService.GetAllArticlesAsync(culture);
article.RelatedArticles = allArticles
.Where(a => a.Title != article.Metadata.Title)
.Take(3)
.ToList();
_logger.LogInformation("Serving article: {Slug} ({Culture})", slug, culture);
return View(article);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error serving article: {Slug} ({Culture})", slug, culture);
return StatusCode(500, "Internal server error");
}
}
[Route("{culture:regex(^(pt-BR|es-PY)$)}/tutoriais")]
public async Task<IActionResult> Index(string culture)
{
try
{
var articles = await _markdownService.GetAllArticlesAsync(culture);
// Set ViewBag
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 ?? "";
_adDisplayService.SetViewBagAds(ViewBag);
ViewBag.Title = culture == "pt-BR" ? "Tutoriais QR Code" : "Tutoriales Código QR";
ViewBag.Description = culture == "pt-BR"
? "Aprenda a criar e usar QR Codes com nossos tutoriais completos"
: "Aprende a crear y usar códigos QR con nuestros tutoriales completos";
ViewBag.Culture = culture;
_logger.LogInformation("Serving tutorials index ({Culture}): {Count} articles", culture, articles.Count);
return View(articles);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error serving tutorials index ({Culture})", culture);
return StatusCode(500, "Internal server error");
}
}
}
}

View File

@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
namespace QRRapidoApp.Models.Ads
{
/// <summary>
/// Represents the content for a single affiliate advertisement.
/// </summary>
public class AffiliateAdContent
{
public string Title { get; set; } = string.Empty;
public string? Description { get; set; }
public string ProductUrl { get; set; } = string.Empty;
public string? ImageUrl { get; set; }
public string? CtaText { get; set; }
public string? BadgeText { get; set; }
public string? PriceText { get; set; }
public string? Category { get; set; }
public bool IsEmpty()
{
return string.IsNullOrWhiteSpace(ProductUrl);
}
public AffiliateAdContent Clone()
{
return new AffiliateAdContent
{
Title = Title,
Description = Description,
ProductUrl = ProductUrl,
ImageUrl = ImageUrl,
CtaText = CtaText,
BadgeText = BadgeText,
PriceText = PriceText,
Category = Category
};
}
}
/// <summary>
/// Configuration for an ad slot, supporting AdSense or affiliate content.
/// </summary>
public class AdSlotConfiguration
{
/// <summary>
/// Provider type for the slot. Supported values: "AdSense", "Affiliate".
/// Defaults to "AdSense" to preserve current behaviour.
/// </summary>
public string Provider { get; set; } = "AdSense";
/// <summary>
/// Optional custom AdSense slot ID override.
/// </summary>
public string? AdSenseSlotId { get; set; }
/// <summary>
/// Affiliate content that will be rendered when <see cref="Provider"/> is "Affiliate".
/// </summary>
public AffiliateAdContent? Affiliate { get; set; }
public AdSlotConfiguration Clone()
{
return new AdSlotConfiguration
{
Provider = Provider,
AdSenseSlotId = AdSenseSlotId,
Affiliate = Affiliate?.Clone()
};
}
}
/// <summary>
/// Root options object bound from configuration for ad slots.
/// </summary>
public class AdsConfigurationOptions
{
private IDictionary<string, AdSlotConfiguration> _slots = new Dictionary<string, AdSlotConfiguration>(StringComparer.OrdinalIgnoreCase);
private IDictionary<string, IDictionary<string, AdSlotConfiguration>> _locales = new Dictionary<string, IDictionary<string, AdSlotConfiguration>>(StringComparer.OrdinalIgnoreCase);
public IDictionary<string, AdSlotConfiguration> Slots
{
get => _slots;
set => _slots = CreateSlotDictionary(value);
}
public IDictionary<string, IDictionary<string, AdSlotConfiguration>> Locales
{
get => _locales;
set
{
_locales = new Dictionary<string, IDictionary<string, AdSlotConfiguration>>(StringComparer.OrdinalIgnoreCase);
if (value == null)
{
return;
}
foreach (var locale in value)
{
_locales[locale.Key] = CreateSlotDictionary(locale.Value);
}
}
}
public AdSlotConfiguration GetSlot(string slotKey, string? cultureName = null)
{
if (string.IsNullOrWhiteSpace(slotKey))
{
slotKey = "header";
}
var localeSlot = TryGetLocaleSlot(slotKey, cultureName);
if (localeSlot != null)
{
return localeSlot.Clone();
}
if (_slots.TryGetValue(slotKey, out var config))
{
return config.Clone();
}
// Default to AdSense when not configured
return new AdSlotConfiguration();
}
private AdSlotConfiguration? TryGetLocaleSlot(string slotKey, string? cultureName)
{
if (string.IsNullOrWhiteSpace(cultureName) || _locales.Count == 0)
{
return null;
}
if (_locales.TryGetValue(cultureName, out var localeSlots) && localeSlots.TryGetValue(slotKey, out var slot))
{
return slot;
}
var neutralCulture = cultureName.Split('-')[0];
if (!string.Equals(neutralCulture, cultureName, StringComparison.OrdinalIgnoreCase)
&& _locales.TryGetValue(neutralCulture, out var neutralSlots)
&& neutralSlots.TryGetValue(slotKey, out var neutralSlot))
{
return neutralSlot;
}
return null;
}
private static IDictionary<string, AdSlotConfiguration> CreateSlotDictionary(IDictionary<string, AdSlotConfiguration>? source)
{
var dictionary = new Dictionary<string, AdSlotConfiguration>(StringComparer.OrdinalIgnoreCase);
if (source == null)
{
return dictionary;
}
foreach (var kvp in source)
{
if (kvp.Value != null)
{
dictionary[kvp.Key] = kvp.Value;
}
}
return dictionary;
}
}
/// <summary>
/// View model used by the affiliate ad partial.
/// </summary>
public class AffiliateAdViewModel
{
public string SlotKey { get; set; } = "header";
public string ContainerCssClass { get; set; } = string.Empty;
public AffiliateAdContent Content { get; set; } = new AffiliateAdContent();
}
}

16
Models/ArticleMetadata.cs Normal file
View File

@ -0,0 +1,16 @@
namespace QRRapidoApp.Models
{
public class ArticleMetadata
{
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Keywords { get; set; } = string.Empty;
public string Author { get; set; } = "QR Rapido";
public DateTime Date { get; set; }
public DateTime LastMod { get; set; }
public string Image { get; set; } = string.Empty;
public string Culture { get; set; } = "pt-BR";
public int ReadingTimeMinutes { get; set; }
public string Slug { get; set; } = string.Empty;
}
}

View File

@ -45,6 +45,9 @@ namespace QRRapidoApp.Models
[BsonElement("stripeSubscriptionId")]
public string? StripeSubscriptionId { get; set; }
[BsonElement("subscriptionStartedAt")]
public DateTime? SubscriptionStartedAt { get; set; } // Data de início da assinatura atual
[BsonElement("preferredLanguage")]
public string PreferredLanguage { get; set; } = "pt-BR";

View File

@ -0,0 +1,11 @@
namespace QRRapidoApp.Models.ViewModels
{
public class ArticleViewModel
{
public ArticleMetadata Metadata { get; set; } = new();
public string HtmlContent { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
public DateTime LastModified { get; set; }
public List<ArticleMetadata> RelatedArticles { get; set; } = new();
}
}

View File

@ -10,6 +10,8 @@ using QRRapidoApp.Data;
using QRRapidoApp.Middleware;
using QRRapidoApp.Providers;
using QRRapidoApp.Services;
using QRRapidoApp.Models.Ads;
using QRRapidoApp.Services.Ads;
using QRRapidoApp.Services.Monitoring;
using QRRapidoApp.Services.HealthChecks;
using StackExchange.Redis;
@ -138,6 +140,8 @@ else
builder.Services.AddMemoryCache();
builder.Services.AddSingleton<IDistributedCache, MemoryDistributedCacheWrapper>();
builder.Services.Configure<AdsConfigurationOptions>(builder.Configuration.GetSection("Ads"));
builder.Services.AddScoped<IAdSlotConfigurationProvider, ConfigurationAdSlotProvider>();
// ✅ DataProtection compartilhado via MongoDB (para múltiplas réplicas do Swarm)
if (!string.IsNullOrEmpty(mongoConnectionString))
@ -235,6 +239,7 @@ builder.Services.Configure<RequestLocalizationOptions>(options =>
builder.Services.AddScoped<IQRCodeService, QRRapidoService>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IPlanService, QRRapidoApp.Services.PlanService>();
builder.Services.AddScoped<IMarkdownService, MarkdownService>();
builder.Services.AddScoped<AdDisplayService>();
builder.Services.AddScoped<StripeService>();
builder.Services.AddScoped<LogoReadabilityAnalyzer>();
@ -351,7 +356,7 @@ app.MapHealthChecks("/healthcheck");
// Language routes (must be first)
app.MapControllerRoute(
name: "localized",
pattern: "{culture:regex(^(es-PY)$)}/{controller=Home}/{action=Index}/{id?}");
pattern: "{culture:regex(^(pt-BR|es-PY)$)}/{controller=Home}/{action=Index}/{id?}");
// API routes
app.MapControllerRoute(

View File

@ -6,7 +6,7 @@
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:52428;http://192.168.0.85:52429;http://localhost:52429"
"applicationUrl": "https://localhost:52428;http://localhost:52429"
}
}
}

View File

@ -31,6 +31,8 @@
<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" />
<PackageReference Include="Markdig" Version="0.37.0" />
<PackageReference Include="YamlDotNet" Version="16.2.0" />
</ItemGroup>
<ItemGroup>

View File

@ -924,9 +924,12 @@
<value>7. Pagos y Suscripciones</value>
</data>
<data name="TermsPaymentsContent" xml:space="preserve">
<value>• Suscripciones Premium son procesadas vía Stripe
• Pagos son recurrentes hasta cancelación
• Reembolsos siguen nuestra política de 7 días
<value>• Suscripciones Premium son procesadas de forma segura vía Stripe
• Pagos son recurrentes mensualmente hasta cancelación
• Puede cancelar su suscripción en cualquier momento desde su perfil, sin multas ni cargos adicionales
• Tiene derecho de retracto de 7 días para solicitar reembolso total (Ley de Defensa del Consumidor)
• Después de la cancelación, mantendrá acceso Premium hasta el final del período ya pagado
• No hay reembolso proporcional para cancelaciones después del período de 7 días
• Precios pueden ser alterados mediante aviso previo de 30 días</value>
</data>
<data name="TermsLiability" xml:space="preserve">
@ -2012,4 +2015,20 @@
<data name="VCardUseCase3" xml:space="preserve">
<value>Firma de email profesional</value>
</data>
<!-- Tutorial Section -->
<data name="ViewTutorials" xml:space="preserve">
<value>Ver Tutoriales</value>
</data>
<data name="LearnMore" xml:space="preserve">
<value>Aprende Más</value>
</data>
<data name="CompleteGuidesAboutQRCodes" xml:space="preserve">
<value>Guías completas sobre códigos QR</value>
</data>
<data name="ViewAllTutorials" xml:space="preserve">
<value>Ver Todos los Tutoriales</value>
</data>
<data name="RealEstateAndBrokers" xml:space="preserve">
<value>Inmobiliaria y Corredores</value>
</data>
</root>

View File

@ -933,9 +933,12 @@
<value>7. Pagos y Suscripciones</value>
</data>
<data name="TermsPaymentsContent" xml:space="preserve">
<value>• Suscripciones Premium son procesadas vía Stripe
• Pagos son recurrentes hasta cancelación
• Reembolsos siguen nuestra política de 7 días
<value>• Suscripciones Premium son procesadas de forma segura vía Stripe
• Pagos son recurrentes mensualmente hasta cancelación
• Puede cancelar su suscripción en cualquier momento desde su perfil, sin multas ni cargos adicionales
• Tiene derecho de retracto de 7 días para solicitar reembolso total (Ley de Defensa del Consumidor)
• Después de la cancelación, mantendrá acceso Premium hasta el final del período ya pagado
• No hay reembolso proporcional para cancelaciones después del período de 7 días
• Precios pueden ser alterados mediante aviso previo de 30 días</value>
</data>
<data name="TermsLiability" xml:space="preserve">

View File

@ -1014,9 +1014,12 @@
<value>7. Pagamentos e Assinaturas</value>
</data>
<data name="TermsPaymentsContent" xml:space="preserve">
<value>• Assinaturas Premium são processadas via Stripe
• Pagamentos são recorrentes até cancelamento
• Reembolsos seguem nossa política de 7 dias
<value>• Assinaturas Premium são processadas via Stripe de forma segura
• Pagamentos são recorrentes mensalmente até cancelamento
• Você pode cancelar sua assinatura a qualquer momento através do seu perfil, sem multas ou taxas adicionais
• Conforme o Código de Defesa do Consumidor (CDC), você tem direito de arrependimento de 7 dias para solicitar reembolso total
• Após o cancelamento, você manterá acesso Premium até o final do período já pago
• Não há reembolso proporcional para cancelamentos após o período de 7 dias
• Preços podem ser alterados mediante aviso prévio de 30 dias</value>
</data>
<data name="TermsLiability" xml:space="preserve">
@ -2102,4 +2105,20 @@
<data name="VCardUseCase3" xml:space="preserve">
<value>Assinatura de email profissional</value>
</data>
<!-- Tutorial Section -->
<data name="ViewTutorials" xml:space="preserve">
<value>Ver Tutoriais</value>
</data>
<data name="LearnMore" xml:space="preserve">
<value>Aprenda Mais</value>
</data>
<data name="CompleteGuidesAboutQRCodes" xml:space="preserve">
<value>Guias completos sobre QR Codes</value>
</data>
<data name="ViewAllTutorials" xml:space="preserve">
<value>Ver Todos os Tutoriais</value>
</data>
<data name="RealEstateAndBrokers" xml:space="preserve">
<value>Imóveis e Corretores</value>
</data>
</root>

View File

@ -0,0 +1,143 @@
# MongoDB Plans Seed Script
## ⚠️ IMPORTANTE - LEIA ANTES DE EXECUTAR
Este script cria **2 planos** no MongoDB:
1. **Premium Mensal** - Cobrança recorrente mensal
2. **Premium Anual** - Cobrança anual com 20% de desconto
**VOCÊ PRECISA ATUALIZAR OS STRIPE PRICE IDs** antes de executar em produção!
## 📋 Pré-requisitos
### 1. Criar Produto no Stripe
1. Acesse [Stripe Dashboard](https://dashboard.stripe.com/products)
2. Crie um produto "QR Rapido Premium"
3. Anote o Product ID (ex: `prod_SnfQTxwE3i8r5L`)
### 2. Criar Price IDs no Stripe
Para **CADA PAÍS** (BR, PY, US), crie **2 preços** (mensal e anual):
#### Brasil (BRL):
- **Mensal**: R$ 9,90/mês
- No Stripe: "Premium Mensal - Brasil"
- Copie o Price ID: `price_xxxxx_monthly_br`
- **Anual**: R$ 95,04/ano (economia de 20%)
- No Stripe: "Premium Anual - Brasil"
- Copie o Price ID: `price_xxxxx_yearly_br`
#### Paraguai (PYG):
- **Mensal**: 35.000 Gs/mês
- No Stripe: "Premium Mensal - Paraguay"
- Copie o Price ID: `price_xxxxx_monthly_py`
- **Anual**: 336.000 Gs/ano (economia de 20%)
- No Stripe: "Premium Anual - Paraguay"
- Copie o Price ID: `price_xxxxx_yearly_py`
#### EUA/Internacional (USD):
- **Mensal**: $1,99/mês
- No Stripe: "Premium Monthly - USA"
- Copie o Price ID: `price_xxxxx_monthly_us`
- **Anual**: $19,10/ano (economia de 20%)
- No Stripe: "Premium Yearly - USA"
- Copie o Price ID: `price_xxxxx_yearly_us`
### 3. Atualizar appsettings.json
**Edite `appsettings.json`** (para desenvolvimento/teste):
```json
"Stripe": {
"Plans": {
"Monthly": {
"BR": "price_xxxxx_monthly_br", // ← Cole seu Price ID aqui
"PY": "price_xxxxx_monthly_py", // ← Cole seu Price ID aqui
"US": "price_xxxxx_monthly_us" // ← Cole seu Price ID aqui
},
"Yearly": {
"BR": "price_xxxxx_yearly_br", // ← Cole seu Price ID aqui
"PY": "price_xxxxx_yearly_py", // ← Cole seu Price ID aqui
"US": "price_xxxxx_yearly_us" // ← Cole seu Price ID aqui
}
}
}
```
**Edite `appsettings.Production.json`** (para produção) com os Price IDs **de produção** (mode live)
## 🚀 Como Executar
### Ambiente Local (Development)
```bash
# Se MongoDB estiver rodando localmente
mongosh "mongodb://localhost:27017/QrRapido" Scripts/seed-mongodb-plans.js
```
### Ambiente de Produção
```bash
# Conectar ao MongoDB de produção e executar o script
mongosh "mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/QrRapido?replicaSet=rs0&authSource=admin" Scripts/seed-mongodb-plans.js
```
**OU via SSH no servidor:**
```bash
# Conectar ao servidor
ssh ubuntu@141.148.162.114
# Copiar o script para o servidor
# (você pode usar scp ou copiar o conteúdo manualmente)
# Executar o script
mongosh "mongodb://admin:c4rn31r0@localhost:27017/QrRapido?authSource=admin" seed-mongodb-plans.js
```
## ✅ Verificar se Funcionou
```bash
# Conectar ao MongoDB
mongosh "mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/QrRapido?replicaSet=rs0&authSource=admin"
# Verificar os planos inseridos
use QrRapido
db.Plans.find().pretty()
```
Você deve ver um documento com:
- `name`: Nomes em pt-BR, es-PY, en
- `stripePriceId`: O Price ID padrão do Stripe
- `pricesByCountry`: Preços por país (BR, PY, US)
- `isActive`: true
## 🔧 Troubleshooting
### Erro: "Authentication failed"
- Verifique se a senha do MongoDB está correta no connection string
- Verifique se o usuário `admin` tem permissões no banco `QrRapido`
### Erro: "MongoServerError: E11000 duplicate key error"
- Já existe um plano com o mesmo ID
- Execute `db.Plans.deleteMany({})` primeiro para limpar (CUIDADO em produção!)
### Stripe retorna erro "No such price"
- Você não atualizou os `stripePriceId` no script
- Os Price IDs no script não existem no seu Stripe Dashboard
- Verifique se está usando as chaves corretas (test vs live mode)
## 📝 Próximos Passos
Após executar este script:
1. ✅ Verificar no MongoDB que o plano foi criado: `db.Plans.find()`
2. ✅ Testar o fluxo de assinatura em `/Pagamento/SelecaoPlano`
3. ✅ Confirmar que o Stripe recebe o checkout com o Price ID correto
4. ✅ Testar o webhook após pagamento bem-sucedido
## 🔗 Links Úteis
- [Stripe Dashboard - Products](https://dashboard.stripe.com/products)
- [Stripe Dashboard - Prices](https://dashboard.stripe.com/prices)
- [Stripe Webhooks](https://dashboard.stripe.com/webhooks)

View File

@ -0,0 +1,163 @@
// MongoDB Seed Script for Plans Collection
// Run this script with: mongosh "mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/QrRapido?replicaSet=rs0&authSource=admin" seed-mongodb-plans.js
// Ensure we're using the correct database
db = db.getSiblingDB('QrRapido');
// Clear existing plans (optional - comment out if you want to keep existing plans)
// db.Plans.deleteMany({});
print("\n📦 Seeding Plans Collection...\n");
// 1. PLANO MENSAL
db.Plans.insertOne({
name: {
"pt-BR": "Premium Mensal",
"es-PY": "Premium Mensual",
"en": "Premium Monthly"
},
description: {
"pt-BR": "Acesso ilimitado a todos os recursos premium - Cobrança mensal",
"es-PY": "Acceso ilimitado a todas las funciones premium - Facturación mensual",
"en": "Unlimited access to all premium features - Monthly billing"
},
features: {
"pt-BR": [
"QR Codes ilimitados",
"Sem anúncios",
"QR Codes dinâmicos (editáveis)",
"Estatísticas avançadas",
"Suporte prioritário",
"Acesso à API",
"Cancele a qualquer momento"
],
"es-PY": [
"Códigos QR ilimitados",
"Sin anuncios",
"Códigos QR dinámicos (editables)",
"Estadísticas avanzadas",
"Soporte prioritario",
"Acceso a la API",
"Cancela cuando quieras"
],
"en": [
"Unlimited QR Codes",
"No ads",
"Dynamic QR Codes (editable)",
"Advanced analytics",
"Priority support",
"API access",
"Cancel anytime"
]
},
interval: "month",
stripePriceId: "price_XXXXX_monthly_us", // Default price (USA) - UPDATE from appsettings.json > Stripe:Plans:Monthly:US
pricesByCountry: {
"BR": {
amount: 9.90,
currency: "BRL",
stripePriceId: "price_XXXXX_monthly_br" // UPDATE from appsettings.json > Stripe:Plans:Monthly:BR
},
"PY": {
amount: 35000,
currency: "PYG",
stripePriceId: "price_XXXXX_monthly_py" // UPDATE from appsettings.json > Stripe:Plans:Monthly:PY
},
"US": {
amount: 1.99,
currency: "USD",
stripePriceId: "price_XXXXX_monthly_us" // UPDATE from appsettings.json > Stripe:Plans:Monthly:US
}
},
isActive: true,
displayOrder: 1
});
print("✅ Plano Mensal inserido");
// 2. PLANO ANUAL (com desconto)
db.Plans.insertOne({
name: {
"pt-BR": "Premium Anual",
"es-PY": "Premium Anual",
"en": "Premium Yearly"
},
description: {
"pt-BR": "Acesso ilimitado a todos os recursos premium - Economia de 20% no plano anual!",
"es-PY": "Acceso ilimitado a todas las funciones premium - ¡Ahorra 20% con el plan anual!",
"en": "Unlimited access to all premium features - Save 20% with yearly billing!"
},
features: {
"pt-BR": [
"QR Codes ilimitados",
"Sem anúncios",
"QR Codes dinâmicos (editáveis)",
"Estatísticas avançadas",
"Suporte prioritário",
"Acesso à API",
"💰 Economia de 20%",
"Cobrança anual única"
],
"es-PY": [
"Códigos QR ilimitados",
"Sin anuncios",
"Códigos QR dinámicos (editables)",
"Estadísticas avanzadas",
"Soporte prioritario",
"Acceso a la API",
"💰 Ahorro del 20%",
"Facturación anual única"
],
"en": [
"Unlimited QR Codes",
"No ads",
"Dynamic QR Codes (editable)",
"Advanced analytics",
"Priority support",
"API access",
"💰 Save 20%",
"Billed annually"
]
},
interval: "year",
stripePriceId: "price_XXXXX_yearly_us", // Default price (USA) - UPDATE from appsettings.json > Stripe:Plans:Yearly:US
pricesByCountry: {
"BR": {
amount: 95.04, // 9.90 * 12 * 0.80 = Economia de 20%
currency: "BRL",
stripePriceId: "price_XXXXX_yearly_br" // UPDATE from appsettings.json > Stripe:Plans:Yearly:BR
},
"PY": {
amount: 336000, // 35000 * 12 * 0.80 = Economia de 20%
currency: "PYG",
stripePriceId: "price_XXXXX_yearly_py" // UPDATE from appsettings.json > Stripe:Plans:Yearly:PY
},
"US": {
amount: 19.10, // 1.99 * 12 * 0.80 = Economia de 20%
currency: "USD",
stripePriceId: "price_XXXXX_yearly_us" // UPDATE from appsettings.json > Stripe:Plans:Yearly:US
}
},
isActive: true,
displayOrder: 2,
badge: {
"pt-BR": "MELHOR VALOR",
"es-PY": "MEJOR VALOR",
"en": "BEST VALUE"
}
});
print("✅ Plano Anual inserido");
print("\n✅ Plans collection seeded successfully!");
print("\n⚠ PRÓXIMOS PASSOS:");
print("1. Acesse o Stripe Dashboard: https://dashboard.stripe.com/prices");
print("2. Crie os Price IDs para cada país e plano (mensal e anual)");
print("3. Copie os Price IDs (começam com 'price_')");
print("4. Atualize os valores no appsettings.json > Stripe:Plans");
print("5. Execute este script novamente após atualizar os Price IDs acima");
print("\n📊 Verificar planos inseridos:");
print(" db.Plans.find().pretty()");
print("\n🗑 Para limpar e começar de novo:");
print(" db.Plans.deleteMany({})");
print("");

View File

@ -109,10 +109,10 @@ namespace QRRapidoApp.Services
}
}
public void SetViewBagAds(dynamic viewBag)
{
public void SetViewBagAds(dynamic viewBag)
{
viewBag.AdSenseTag = _config["AdSense:ClientId"];
viewBag.AdSenseEnabled = _config["AdSense:Enabled"]=="True";
viewBag.AdSenseEnabled = bool.TryParse(_config["AdSense:Enabled"], out var enabled) && enabled;
}
}
}
}

View File

@ -0,0 +1,49 @@
using System;
using System.Globalization;
using Microsoft.Extensions.Options;
using QRRapidoApp.Models.Ads;
namespace QRRapidoApp.Services.Ads
{
public class ConfigurationAdSlotProvider : IAdSlotConfigurationProvider
{
private readonly IOptionsSnapshot<AdsConfigurationOptions> _options;
public ConfigurationAdSlotProvider(IOptionsSnapshot<AdsConfigurationOptions> options)
{
_options = options;
}
public AdSlotConfiguration GetSlot(string slotKey, string? cultureName = null)
{
var resolvedCulture = cultureName;
if (string.IsNullOrWhiteSpace(resolvedCulture))
{
resolvedCulture = CultureInfo.CurrentUICulture?.Name;
}
var slot = _options.Value.GetSlot(slotKey, resolvedCulture);
if (string.Equals(slot.Provider, "Affiliate", StringComparison.OrdinalIgnoreCase))
{
if (slot.Affiliate == null || slot.Affiliate.IsEmpty())
{
// Fallback gracefully to AdSense if affiliate content is not properly configured.
return new AdSlotConfiguration
{
Provider = "AdSense",
AdSenseSlotId = slot.AdSenseSlotId
};
}
slot.Provider = "Affiliate"; // normalize casing
}
else
{
slot.Provider = "AdSense";
}
return slot;
}
}
}

View File

@ -0,0 +1,9 @@
using QRRapidoApp.Models.Ads;
namespace QRRapidoApp.Services.Ads
{
public interface IAdSlotConfigurationProvider
{
AdSlotConfiguration GetSlot(string slotKey, string? cultureName = null);
}
}

View File

@ -0,0 +1,12 @@
using QRRapidoApp.Models;
using QRRapidoApp.Models.ViewModels;
namespace QRRapidoApp.Services
{
public interface IMarkdownService
{
Task<ArticleViewModel?> GetArticleAsync(string slug, string culture);
Task<List<ArticleMetadata>> GetAllArticlesAsync(string culture);
Task<List<ArticleMetadata>> GetAllArticlesForSitemapAsync();
}
}

267
Services/MarkdownService.cs Normal file
View File

@ -0,0 +1,267 @@
using Markdig;
using Microsoft.Extensions.Caching.Memory;
using QRRapidoApp.Models;
using QRRapidoApp.Models.ViewModels;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace QRRapidoApp.Services
{
public class MarkdownService : IMarkdownService
{
private readonly IMemoryCache _cache;
private readonly IWebHostEnvironment _env;
private readonly ILogger<MarkdownService> _logger;
private readonly MarkdownPipeline _pipeline;
private readonly IDeserializer _yamlDeserializer;
private const string CACHE_KEY_PREFIX = "Tutorial_";
private const string CACHE_KEY_ALL = "AllTutorials_";
private const int CACHE_DURATION_HOURS = 1;
public MarkdownService(IMemoryCache cache, IWebHostEnvironment env, ILogger<MarkdownService> logger)
{
_cache = cache;
_env = env;
_logger = logger;
// Configure Markdig pipeline for advanced markdown features
_pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.UseAutoLinks()
.UseEmojiAndSmiley()
.Build();
// Configure YAML deserializer
_yamlDeserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.IgnoreUnmatchedProperties()
.Build();
}
public async Task<ArticleViewModel?> GetArticleAsync(string slug, string culture)
{
var cacheKey = $"{CACHE_KEY_PREFIX}{culture}_{slug}";
// Try get from cache
if (_cache.TryGetValue(cacheKey, out ArticleViewModel? cachedArticle))
{
_logger.LogInformation("Article served from cache: {Slug} ({Culture})", slug, culture);
return cachedArticle;
}
try
{
var contentPath = Path.Combine(_env.ContentRootPath, "Content", "Tutoriais");
var fileName = $"{slug}.{culture}.md";
var filePath = Path.Combine(contentPath, fileName);
if (!File.Exists(filePath))
{
_logger.LogWarning("Article file not found: {FilePath}", filePath);
return null;
}
var fileContent = await File.ReadAllTextAsync(filePath);
var article = ParseMarkdownWithFrontmatter(fileContent, slug);
if (article == null)
{
_logger.LogError("Failed to parse article: {Slug} ({Culture})", slug, culture);
return null;
}
// Set file metadata
var fileInfo = new FileInfo(filePath);
article.LastModified = fileInfo.LastWriteTimeUtc;
article.Metadata.Culture = culture;
// Cache the article
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromHours(CACHE_DURATION_HOURS));
_cache.Set(cacheKey, article, cacheOptions);
_logger.LogInformation("Article loaded and cached: {Slug} ({Culture})", slug, culture);
return article;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading article: {Slug} ({Culture})", slug, culture);
return null;
}
}
public async Task<List<ArticleMetadata>> GetAllArticlesAsync(string culture)
{
var cacheKey = $"{CACHE_KEY_ALL}{culture}";
// Try get from cache
if (_cache.TryGetValue(cacheKey, out List<ArticleMetadata>? cachedList))
{
_logger.LogInformation("Articles list served from cache ({Culture})", culture);
return cachedList ?? new List<ArticleMetadata>();
}
try
{
var articles = new List<ArticleMetadata>();
var contentPath = Path.Combine(_env.ContentRootPath, "Content", "Tutoriais");
if (!Directory.Exists(contentPath))
{
_logger.LogWarning("Tutoriais directory not found: {Path}", contentPath);
return articles;
}
var pattern = $"*.{culture}.md";
var files = Directory.GetFiles(contentPath, pattern);
foreach (var file in files)
{
try
{
var fileContent = await File.ReadAllTextAsync(file);
var slug = Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(file));
var article = ParseMarkdownWithFrontmatter(fileContent, slug);
if (article?.Metadata != null)
{
article.Metadata.Culture = culture;
article.Metadata.Slug = slug;
articles.Add(article.Metadata);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error parsing article file: {File}", file);
}
}
// Sort by date (newest first)
articles = articles.OrderByDescending(a => a.Date).ToList();
// Cache the list
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromHours(CACHE_DURATION_HOURS));
_cache.Set(cacheKey, articles, cacheOptions);
_logger.LogInformation("Loaded {Count} articles for culture {Culture}", articles.Count, culture);
return articles;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading articles list for culture {Culture}", culture);
return new List<ArticleMetadata>();
}
}
public async Task<List<ArticleMetadata>> GetAllArticlesForSitemapAsync()
{
try
{
var allArticles = new List<ArticleMetadata>();
var contentPath = Path.Combine(_env.ContentRootPath, "Content", "Tutoriais");
if (!Directory.Exists(contentPath))
{
_logger.LogWarning("Tutoriais directory not found for sitemap: {Path}", contentPath);
return allArticles;
}
var files = Directory.GetFiles(contentPath, "*.md");
foreach (var file in files)
{
try
{
var fileContent = await File.ReadAllTextAsync(file);
var fileName = Path.GetFileName(file);
// Extract slug and culture from filename (e.g., "como-criar-qr.pt-BR.md")
var parts = fileName.Replace(".md", "").Split('.');
if (parts.Length < 2) continue;
var slug = parts[0];
var culture = parts[1];
var article = ParseMarkdownWithFrontmatter(fileContent, slug);
if (article?.Metadata != null)
{
article.Metadata.Culture = culture;
article.Metadata.Slug = slug;
var fileInfo = new FileInfo(file);
article.Metadata.LastMod = fileInfo.LastWriteTimeUtc;
allArticles.Add(article.Metadata);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error parsing article for sitemap: {File}", file);
}
}
_logger.LogInformation("Loaded {Count} total articles for sitemap", allArticles.Count);
return allArticles;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading articles for sitemap");
return new List<ArticleMetadata>();
}
}
private ArticleViewModel? ParseMarkdownWithFrontmatter(string content, string slug)
{
try
{
// Extract frontmatter
if (!content.StartsWith("---"))
{
_logger.LogWarning("Article missing frontmatter: {Slug}", slug);
return null;
}
var endOfFrontmatter = content.IndexOf("---", 3);
if (endOfFrontmatter == -1)
{
_logger.LogWarning("Article frontmatter not closed: {Slug}", slug);
return null;
}
var frontmatterYaml = content.Substring(3, endOfFrontmatter - 3).Trim();
var markdownContent = content.Substring(endOfFrontmatter + 3).Trim();
// Parse YAML frontmatter
var metadata = _yamlDeserializer.Deserialize<ArticleMetadata>(frontmatterYaml);
if (metadata == null)
{
_logger.LogWarning("Failed to deserialize frontmatter: {Slug}", slug);
return null;
}
// Calculate reading time (average 200 words per minute)
var wordCount = markdownContent.Split(new[] { ' ', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries).Length;
metadata.ReadingTimeMinutes = Math.Max(1, (int)Math.Ceiling(wordCount / 200.0));
// Convert Markdown to HTML
var htmlContent = Markdown.ToHtml(markdownContent, _pipeline);
return new ArticleViewModel
{
Metadata = metadata,
HtmlContent = htmlContent,
Slug = slug,
LastModified = metadata.LastMod
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error parsing markdown for {Slug}", slug);
return null;
}
}
}
}

View File

@ -89,20 +89,20 @@ namespace QRRapidoApp.Services
break;
case "invoice.finalized":
var invoice = stripeEvent.Data.Object as Invoice;
var subscriptionLineItem = invoice.Lines?.Data
.FirstOrDefault(line =>
!string.IsNullOrEmpty(line.SubscriptionId) ||
line.Subscription != null
);
string subscriptionId = null;
if (subscriptionLineItem != null)
{
// Tenta obter o ID da assinatura de duas formas diferentes
subscriptionId = subscriptionLineItem.SubscriptionId
?? subscriptionLineItem.Subscription?.Id;
var invoice = stripeEvent.Data.Object as Invoice;
var subscriptionLineItem = invoice.Lines?.Data
.FirstOrDefault(line =>
!string.IsNullOrEmpty(line.SubscriptionId) ||
line.Subscription != null
);
string subscriptionId = null;
if (subscriptionLineItem != null)
{
// Tenta obter o ID da assinatura de duas formas diferentes
subscriptionId = subscriptionLineItem.SubscriptionId
?? subscriptionLineItem.Subscription?.Id;
}
if (subscriptionId != null)
@ -153,8 +153,8 @@ namespace QRRapidoApp.Services
await _userService.UpdateUserStripeCustomerIdAsync(user.Id, subscription.CustomerId);
}
await _userService.ActivatePremiumStatus(userId, subscription.Id, subItem.CurrentPeriodEnd);
await _userService.ActivatePremiumStatus(userId, subscription.Id, subItem.CurrentPeriodEnd);
_logger.LogInformation($"Successfully processed premium activation/renewal for user {userId}.");
}
@ -189,5 +189,151 @@ namespace QRRapidoApp.Services
return false;
}
}
/// <summary>
/// Verifica se a assinatura está dentro do período de 7 dias para reembolso (CDC)
/// </summary>
public bool IsEligibleForRefund(DateTime? subscriptionStartedAt)
{
if (!subscriptionStartedAt.HasValue)
{
return false;
}
var daysSinceSubscription = (DateTime.UtcNow - subscriptionStartedAt.Value).TotalDays;
return daysSinceSubscription <= 7;
}
/// <summary>
/// Cancela assinatura E processa reembolso total (CDC - 7 dias)
/// </summary>
public async Task<(bool success, string message)> CancelAndRefundSubscriptionAsync(string userId)
{
try
{
var user = await _userService.GetUserAsync(userId);
if (user == null)
{
return (false, "Usuário não encontrado");
}
if (string.IsNullOrEmpty(user.StripeSubscriptionId))
{
return (false, "Nenhuma assinatura ativa encontrada");
}
// Verifica elegibilidade para reembolso
if (!IsEligibleForRefund(user.SubscriptionStartedAt))
{
var daysSince = user.SubscriptionStartedAt.HasValue
? (DateTime.UtcNow - user.SubscriptionStartedAt.Value).TotalDays
: 0;
return (false, $"Período de reembolso de 7 dias expirado (assinatura criada há {Math.Round(daysSince, 1)} dias). Você ainda pode cancelar a renovação.");
}
// Busca a assinatura no Stripe
var subscriptionService = new SubscriptionService();
var subscription = await subscriptionService.GetAsync(user.StripeSubscriptionId);
if (subscription == null)
{
return (false, "Assinatura não encontrada no Stripe");
}
// Cancela a assinatura primeiro
await subscriptionService.CancelAsync(subscription.Id, new SubscriptionCancelOptions());
// Busca o último pagamento (invoice) desta assinatura para reembolsar
var invoiceService = new InvoiceService();
var invoiceListOptions = new InvoiceListOptions
{
Subscription = subscription.Id,
Limit = 1,
Status = "paid"
};
var invoices = await invoiceService.ListAsync(invoiceListOptions);
var latestInvoice = invoices.Data.FirstOrDefault();
if (latestInvoice == null || latestInvoice.AmountPaid <= 0)
{
// Mesmo sem invoice, cancela e desativa
await _userService.DeactivatePremiumStatus(subscription.Id);
return (true, "Assinatura cancelada com sucesso. Nenhum pagamento para reembolsar foi encontrado.");
}
// Processa o reembolso - Stripe reembolsa automaticamente o último pagamento
var refundService = new RefundService();
var refundOptions = new RefundCreateOptions
{
Amount = latestInvoice.AmountPaid, // Reembolso total
Reason = RefundReasons.RequestedByCustomer,
Metadata = new Dictionary<string, string>
{
{ "user_id", userId },
{ "subscription_id", subscription.Id },
{ "invoice_id", latestInvoice.Id },
{ "refund_reason", "CDC 7 dias - Direito de arrependimento" }
}
};
// Stripe automaticamente encontra o charge/payment_intent correto através do subscription_id no metadata
// Alternativamente, podemos buscar o último charge da subscription
try
{
// Tenta reembolsar usando a subscription (Stripe encontra o charge automaticamente)
var chargeService = new ChargeService();
var chargeOptions = new ChargeListOptions
{
Limit = 1,
Customer = subscription.CustomerId
};
var charges = await chargeService.ListAsync(chargeOptions);
var lastCharge = charges.Data.FirstOrDefault();
if (lastCharge != null)
{
refundOptions.Charge = lastCharge.Id;
var refund = await refundService.CreateAsync(refundOptions);
if (refund.Status == "succeeded" || refund.Status == "pending")
{
// Desativa o premium imediatamente no caso de reembolso
await _userService.DeactivatePremiumStatus(subscription.Id);
_logger.LogInformation($"Successfully refunded and canceled subscription {subscription.Id} for user {userId}. Refund ID: {refund.Id}");
return (true, $"Reembolso processado com sucesso! Você receberá R$ {(latestInvoice.AmountPaid / 100.0):F2} de volta em 5-10 dias úteis.");
}
else
{
_logger.LogWarning($"Refund failed with status {refund.Status} for subscription {subscription.Id}");
await _userService.DeactivatePremiumStatus(subscription.Id);
return (false, "Falha ao processar reembolso, mas assinatura foi cancelada. Entre em contato com o suporte.");
}
}
else
{
await _userService.DeactivatePremiumStatus(subscription.Id);
return (false, "Assinatura cancelada, mas nenhuma cobrança encontrada para reembolsar. Entre em contato com o suporte.");
}
}
catch (StripeException refundEx)
{
_logger.LogError(refundEx, $"Error creating refund for subscription {subscription.Id}");
await _userService.DeactivatePremiumStatus(subscription.Id);
return (false, $"Assinatura cancelada, mas erro ao processar reembolso: {refundEx.Message}. Entre em contato com o suporte.");
}
}
catch (StripeException ex)
{
_logger.LogError(ex, $"Stripe error during refund for user {userId}: {ex.Message}");
return (false, $"Erro ao processar reembolso: {ex.StripeError?.Message ?? ex.Message}");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error processing refund for user {userId}");
return (false, "Erro inesperado ao processar reembolso. Tente novamente mais tarde.");
}
}
}
}

View File

@ -408,14 +408,24 @@ namespace QRRapidoApp.Services
public async Task ActivatePremiumStatus(string userId, string stripeSubscriptionId, DateTime expiryDate)
{
var update = Builders<User>.Update
// Verifica se é uma nova assinatura (não renovação)
var user = await GetUserAsync(userId);
var isNewSubscription = user?.StripeSubscriptionId != stripeSubscriptionId;
var updateBuilder = Builders<User>.Update
.Set(u => u.IsPremium, true)
.Set(u => u.StripeSubscriptionId, stripeSubscriptionId)
.Set(u => u.PremiumExpiresAt, expiryDate)
.Unset(u => u.PremiumCancelledAt);
await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
_logger.LogInformation($"Activated premium for user {userId}");
// Se é nova assinatura, atualiza a data de início (para CDC 7 dias)
if (isNewSubscription)
{
updateBuilder = updateBuilder.Set(u => u.SubscriptionStartedAt, DateTime.UtcNow);
}
await _context.Users.UpdateOneAsync(u => u.Id == userId, updateBuilder);
_logger.LogInformation($"Activated premium for user {userId} (new subscription: {isNewSubscription})");
}
public async Task DeactivatePremiumStatus(string stripeSubscriptionId)

View File

@ -40,6 +40,38 @@
Expira em: @Model.PremiumExpiresAt.Value.ToString("dd/MM/yyyy")
</p>
}
@if (!string.IsNullOrEmpty(Model.StripeSubscriptionId))
{
var canRefund = Model.SubscriptionStartedAt.HasValue &&
(DateTime.UtcNow - Model.SubscriptionStartedAt.Value).TotalDays <= 7;
var daysSinceSubscription = Model.SubscriptionStartedAt.HasValue
? Math.Round((DateTime.UtcNow - Model.SubscriptionStartedAt.Value).TotalDays, 1)
: 0;
<p class="mt-2 mb-0">
@if (canRefund)
{
<button type="button" class="btn btn-sm btn-danger" data-bs-toggle="modal" data-bs-target="#refundSubscriptionModal">
<i class="fas fa-undo me-1"></i>Solicitar Reembolso (CDC)
</button>
<small class="text-muted d-block mt-1">
<i class="fas fa-info-circle"></i> Você tem direito a reembolso total (7 dias)
</small>
}
else
{
<button type="button" class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#cancelSubscriptionModal">
<i class="fas fa-times-circle me-1"></i>Cancelar Renovação
</button>
@if (daysSinceSubscription > 0)
{
<small class="text-muted d-block mt-1">
<i class="fas fa-info-circle"></i> Assinatura há @daysSinceSubscription dias (período de reembolso expirado)
</small>
}
}
</p>
}
}
else
{
@ -47,7 +79,7 @@
<i class="fas fa-user me-1"></i>Gratuito
</span>
<p class="text-muted small mt-1 mb-0">
<a href="/Premium/Upgrade" class="text-decoration-none">
<a href="/Pagamento/SelecaoPlano" class="text-decoration-none">
<i class="fas fa-arrow-up me-1"></i>Fazer upgrade
</a>
</p>
@ -163,7 +195,7 @@
@if (!isPremium)
{
<div class="col-md-6">
<a href="/Premium/Upgrade" class="btn btn-warning w-100">
<a href="/Pagamento/SelecaoPlano" class="btn btn-warning w-100">
<i class="fas fa-crown me-2"></i>Upgrade para Premium
</a>
</div>
@ -195,6 +227,80 @@
</div>
</div>
<!-- Modal de Solicitação de Reembolso (CDC - 7 dias) -->
<div class="modal fade" id="refundSubscriptionModal" tabindex="-1" aria-labelledby="refundSubscriptionModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="refundSubscriptionModalLabel">
<i class="fas fa-undo me-2"></i>Solicitar Reembolso Total (CDC)
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Fechar"></button>
</div>
<div class="modal-body">
<div class="alert alert-info">
<i class="fas fa-shield-alt me-2"></i>
<strong>Direito de Arrependimento (CDC):</strong> Você está dentro do período de 7 dias e tem direito a reembolso total conforme o Código de Defesa do Consumidor.
</div>
<h6><strong>O que acontecerá:</strong></h6>
<ul>
<li><i class="fas fa-check text-success me-2"></i>Reembolso total do valor pago</li>
<li><i class="fas fa-check text-success me-2"></i>Assinatura cancelada imediatamente</li>
<li><i class="fas fa-check text-success me-2"></i>Acesso Premium removido</li>
<li><i class="fas fa-check text-success me-2"></i>Dinheiro devolvido em 5-10 dias úteis</li>
</ul>
<p class="mb-0"><strong>Deseja continuar com o reembolso?</strong></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-arrow-left me-1"></i>Cancelar
</button>
<button type="button" class="btn btn-danger" id="confirmRefundBtn">
<i class="fas fa-undo me-1"></i>Sim, Solicitar Reembolso
</button>
</div>
</div>
</div>
</div>
<!-- Modal de Confirmação de Cancelamento -->
<div class="modal fade" id="cancelSubscriptionModal" tabindex="-1" aria-labelledby="cancelSubscriptionModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="cancelSubscriptionModalLabel">
<i class="fas fa-exclamation-triangle me-2"></i>Cancelar Assinatura Premium
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Fechar"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning">
<i class="fas fa-info-circle me-2"></i>
<strong>Atenção:</strong> Você manterá acesso aos recursos Premium até o final do período já pago.
</div>
<p><strong>Ao cancelar, você perderá:</strong></p>
<ul>
<li><i class="fas fa-times text-danger me-2"></i>QR Codes ilimitados</li>
<li><i class="fas fa-times text-danger me-2"></i>Experiência sem anúncios</li>
<li><i class="fas fa-times text-danger me-2"></i>QR Codes dinâmicos (editáveis)</li>
<li><i class="fas fa-times text-danger me-2"></i>Estatísticas avançadas</li>
<li><i class="fas fa-times text-danger me-2"></i>Suporte prioritário</li>
</ul>
<p class="mb-0"><strong>Tem certeza que deseja cancelar?</strong></p>
<p class="text-muted small">Você pode reativar sua assinatura a qualquer momento.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-arrow-left me-1"></i>Manter Premium
</button>
<button type="button" class="btn btn-danger" id="confirmCancelBtn">
<i class="fas fa-times-circle me-1"></i>Sim, Cancelar Assinatura
</button>
</div>
</div>
</div>
</div>
<style>
.card {
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
@ -262,4 +368,104 @@
margin-bottom: 1rem;
}
}
</style>
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Handler para reembolso (CDC - 7 dias)
const confirmRefundBtn = document.getElementById('confirmRefundBtn');
const refundModal = document.getElementById('refundSubscriptionModal');
if (confirmRefundBtn) {
confirmRefundBtn.addEventListener('click', async function() {
// Desabilita o botão durante o processamento
confirmRefundBtn.disabled = true;
confirmRefundBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Processando...';
try {
const response = await fetch('/Premium/RequestRefund', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (result.success) {
// Fecha o modal
const modalInstance = bootstrap.Modal.getInstance(refundModal);
if (modalInstance) {
modalInstance.hide();
}
// Mostra mensagem de sucesso
alert('✅ ' + result.message);
// Recarrega a página após 2 segundos
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
alert('❌ ' + result.error);
confirmRefundBtn.disabled = false;
confirmRefundBtn.innerHTML = '<i class="fas fa-undo me-1"></i>Sim, Solicitar Reembolso';
}
} catch (error) {
console.error('Erro ao solicitar reembolso:', error);
alert('❌ Erro de conexão. Tente novamente mais tarde.');
confirmRefundBtn.disabled = false;
confirmRefundBtn.innerHTML = '<i class="fas fa-undo me-1"></i>Sim, Solicitar Reembolso';
}
});
}
// Handler para cancelamento (sem reembolso)
const confirmCancelBtn = document.getElementById('confirmCancelBtn');
const cancelModal = document.getElementById('cancelSubscriptionModal');
if (confirmCancelBtn) {
confirmCancelBtn.addEventListener('click', async function() {
// Desabilita o botão durante o processamento
confirmCancelBtn.disabled = true;
confirmCancelBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Cancelando...';
try {
const response = await fetch('/Premium/CancelSubscription', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (result.success) {
// Fecha o modal
const modalInstance = bootstrap.Modal.getInstance(cancelModal);
if (modalInstance) {
modalInstance.hide();
}
// Mostra mensagem de sucesso
alert('✅ Assinatura cancelada com sucesso!\n\nVocê manterá acesso Premium até o final do período pago.\n\nA página será recarregada.');
// Recarrega a página após 2 segundos
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
alert('❌ Erro ao cancelar assinatura: ' + (result.error || 'Erro desconhecido'));
confirmCancelBtn.disabled = false;
confirmCancelBtn.innerHTML = '<i class="fas fa-times-circle me-1"></i>Sim, Cancelar Assinatura';
}
} catch (error) {
console.error('Erro ao cancelar assinatura:', error);
alert('❌ Erro de conexão. Tente novamente mais tarde.');
confirmCancelBtn.disabled = false;
confirmCancelBtn.innerHTML = '<i class="fas fa-times-circle me-1"></i>Sim, Cancelar Assinatura';
}
});
}
});
</script>

View File

@ -1198,7 +1198,7 @@
<li><i class="fas fa-check text-success"></i> @Localizer["DeveloperAPI"]</li>
</ul>
<div class="text-center">
<a href="/Premium/Upgrade" class="btn btn-warning w-100">
<a href="/Pagamento/SelecaoPlano" class="btn btn-warning w-100">
<i class="fas fa-bolt"></i> @Localizer["AcceleratePrice"]
</a>
<small class="text-muted d-block mt-1">@Localizer["CancelAnytime"]</small>
@ -1207,6 +1207,30 @@
</div>
}
<!-- Tutorials Card -->
@{
var tutorialCulture = System.Globalization.CultureInfo.CurrentUICulture.Name;
}
<div class="card border-primary mb-4">
<div class="card-header bg-primary text-white">
<h6 class="mb-0">
<i class="fas fa-book-open"></i> @Localizer["LearnMore"]
</h6>
</div>
<div class="card-body">
<p class="small mb-2">@Localizer["CompleteGuidesAboutQRCodes"]</p>
<ul class="list-unstyled small mb-3">
<li class="mb-1"><i class="fas fa-check text-primary"></i> WhatsApp Business</li>
<li class="mb-1"><i class="fas fa-check text-primary"></i> @Localizer["WiFi"] Networks</li>
<li class="mb-1"><i class="fas fa-check text-primary"></i> @Localizer["VCard"]</li>
<li class="mb-1"><i class="fas fa-check text-primary"></i> @Localizer["RealEstateAndBrokers"]</li>
</ul>
<a href="/@tutorialCulture/tutoriais" class="btn btn-primary btn-sm w-100">
<i class="fas fa-graduation-cap"></i> @Localizer["ViewAllTutorials"]
</a>
</div>
</div>
<!-- Speed Tips Card -->
<div class="card bg-light mb-4">
<div class="card-header">

View File

@ -1,343 +0,0 @@
@model QRRapidoApp.Models.ViewModels.UpgradeViewModel
@using Microsoft.Extensions.Localization
@inject IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer
@{
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">
@Localizer["PremiumAccelerateProductivity"]
</p>
<div class="badge bg-success fs-6 p-2">
<i class="fas fa-bolt"></i> @Localizer["ThreeTimesFaster"]
</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> @Localizer["CurrentStatus"]</h6>
<p class="mb-0">
@Localizer["YouHave"] <strong>@Model.DaysUntilAdExpiry @Localizer["DaysRemainingNoAds"]</strong>
@Localizer["UpgradeNowForever"]
</p>
</div>
<div class="col-md-4 text-end">
<div class="badge bg-success p-2">
@Model.DaysUntilAdExpiry @Localizer["DaysRemaining"]
</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>@Localizer["MostPopularPlan"]</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">@Localizer["PerMonth"]</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>@Localizer["UnlimitedQRCodes"]</strong>
</div>
<div class="list-group-item border-0">
<i class="fas fa-bolt text-success me-2"></i>
<strong>@Localizer["UltraFastGeneration04s"]</strong>
</div>
<div class="list-group-item border-0">
<i class="fas fa-ban text-success me-2"></i>
<strong>@Localizer["NoAdsForever"]</strong>
</div>
<div class="list-group-item border-0">
<i class="fas fa-magic text-success me-2"></i>
<strong>@Localizer["DynamicQRCodes"]</strong>
</div>
<div class="list-group-item border-0">
<i class="fas fa-chart-line text-success me-2"></i>
<strong>@Localizer["RealTimeAnalytics"]</strong>
</div>
<div class="list-group-item border-0">
<i class="fas fa-headset text-success me-2"></i>
<strong>@Localizer["PrioritySupport"]</strong>
</div>
<div class="list-group-item border-0">
<i class="fas fa-code text-success me-2"></i>
<strong>@Localizer["DeveloperAPI"]</strong>
</div>
</div>
<button id="upgrade-btn" class="btn btn-warning btn-lg w-100 mb-3">
<i class="fas fa-rocket"></i> @Localizer["UpgradeNowButton"]
<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> @Localizer["SecurePaymentStripe"]
<br>
<i class="fas fa-times-circle"></i> @Localizer["CancelAnytime"]
</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> @Localizer["PlanComparison"]
</h4>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>@Localizer["Feature"]</th>
<th class="text-center">Free</th>
<th class="text-center bg-warning text-dark">Premium</th>
</tr>
</thead>
<tbody>
<tr>
<td>@Localizer["QRCodesPerDay"]</td>
<td class="text-center">50</td>
<td class="text-center"><i class="fas fa-infinity text-success"></i> @Localizer["Unlimited"]</td>
</tr>
<tr>
<td>@Localizer["GenerationSpeed"]</td>
<td class="text-center">1.2s</td>
<td class="text-center"><strong class="text-success">0.4s</strong></td>
</tr>
<tr>
<td>@Localizer["Ads"]</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> @Localizer["NoAds"]</td>
</tr>
<tr>
<td>@Localizer["DynamicQRCodes"]</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>@Localizer["DetailedAnalytics"]</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>@Localizer["PrioritySupport"]</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> @Localizer["SpeedDemonstration"]
</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">@Localizer["Competitors"]</h5>
<div class="display-4 text-danger">4.5s</div>
<p class="text-muted">@Localizer["AverageTime"]</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">@Localizer["ThreeTimesFaster"]</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">@Localizer["ElevenTimesFaster"]</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> @Localizer["FAQ"]
</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">
@Localizer["CanCancelAnytime"]
</button>
</h2>
<div id="faq1" class="accordion-collapse collapse show">
<div class="accordion-body">
@Localizer["CancelAnytimeAnswer"]
</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">
@Localizer["WhatAreDynamicQR"]
</button>
</h2>
<div id="faq2" class="accordion-collapse collapse">
<div class="accordion-body">
@Localizer["DynamicQRAnswer"]
</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">
@Localizer["HowPrioritySupport"]
</button>
</h2>
<div id="faq3" class="accordion-collapse collapse">
<div class="accordion-body">
@Localizer["PrioritySupportAnswer"]
</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 {
showToast('@Localizer["PaymentProcessingError"]' + result.error, 'danger');
}
} catch (error) {
console.error('Erro:', error);
showToast('@Localizer["PaymentErrorTryAgain"]', 'danger');
} 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
});
}
// Toast notification function
function showToast(message, type) {
// Create toast container if doesn't exist
let toastContainer = document.getElementById('toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.id = 'toast-container';
toastContainer.className = 'toast-container position-fixed top-0 start-0 p-3';
toastContainer.style.zIndex = '1060';
toastContainer.style.marginTop = '80px';
document.body.appendChild(toastContainer);
}
// Create toast element
const toastId = 'toast-' + Date.now();
const toastElement = document.createElement('div');
toastElement.innerHTML = `
<div class="toast align-items-center text-white bg-${type} border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">${message}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
`;
toastContainer.appendChild(toastElement);
const toast = new bootstrap.Toast(toastElement.querySelector('.toast'), { delay: 5000 });
toast.show();
// Remove toast element after it's hidden
toastElement.querySelector('.toast').addEventListener('hidden.bs.toast', function() {
toastElement.remove();
});
}
</script>
}

View File

@ -1,89 +1,152 @@
@using QRRapidoApp.Services
@using QRRapidoApp.Services.Ads
@using QRRapidoApp.Models.Ads
@using Microsoft.Extensions.Localization
@using System.Globalization
@model dynamic
@inject AdDisplayService AdService
@inject IAdSlotConfigurationProvider SlotProvider
@inject IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer
@{
var userId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
var showAds = await AdService.ShouldShowAds(userId);
var position = ViewBag.position ?? Model?.position ?? "header";
var tagAdSense = ViewBag.AdSenseTag;
<!-- AdSense -->
@Html.Raw(ViewBag.AdSenseScript);
var rawPosition = (ViewBag.position ?? Model?.position ?? "header")?.ToString()?.ToLowerInvariant();
var position = string.IsNullOrWhiteSpace(rawPosition) ? "header" : rawPosition;
var currentCulture = CultureInfo.CurrentUICulture?.Name;
var slotConfig = SlotProvider.GetSlot(position, currentCulture);
var adSenseClientId = ViewBag.AdSenseTag as string;
var adSenseEnabled = ViewBag.AdSenseEnabled is bool enabled && enabled;
var defaultAdSenseSlot = position switch
{
"header" => "QR19750801",
"sidebar" => "QR19750802",
"footer" => "QR19750803",
"content" => "QR19750804",
_ => "QR19750801"
};
var adSenseSlot = slotConfig.AdSenseSlotId ?? defaultAdSenseSlot;
var isAffiliateProvider = string.Equals(slotConfig.Provider, "Affiliate", StringComparison.OrdinalIgnoreCase);
var affiliateContent = isAffiliateProvider ? slotConfig.Affiliate : null;
var containerCss = position switch
{
"header" => "ad-container ad-header mb-4",
"sidebar" => "ad-container ad-sidebar mb-4",
"footer" => "ad-container ad-footer mt-5 mb-4",
"content" => "ad-container ad-content my-4",
_ => "ad-container mb-4"
};
var shouldRenderAdSense = !isAffiliateProvider && adSenseEnabled && !string.IsNullOrWhiteSpace(adSenseClientId);
var shouldRenderAffiliate = isAffiliateProvider && affiliateContent != null && !affiliateContent.IsEmpty();
var renderedAdSense = false;
}
@if (showAds)
{
@switch (position)
switch (position)
{
case "header":
<div class="ad-container ad-header mb-4">
<div class="ad-label">@Localizer["Advertisement"]</div>
<ins class="adsbygoogle"
style="display:inline-block;width:728px;height:90px"
data-ad-client="@tagAdSense"
data-ad-slot="QR19750801"></ins>
</div>
if (shouldRenderAffiliate)
{
@await Html.PartialAsync("_AffiliateAd", new AffiliateAdViewModel
{
SlotKey = position,
ContainerCssClass = containerCss,
Content = affiliateContent!
})
}
else if (shouldRenderAdSense)
{
<!-- Placeholder for Adsterra ad - 728x90 Banner -->
<div class="@containerCss" style="display: none;">
<!-- Adsterra code will go here when approved -->
</div>
}
break;
case "sidebar":
<div class="ad-container ad-sidebar mb-4">
<div class="ad-label">@Localizer["Advertisement"]</div>
<ins class="adsbygoogle"
style="display:inline-block;width:300px;height:250px"
data-ad-client="@tagAdSense"
data-ad-slot="QR19750802"></ins>
</div>
if (shouldRenderAffiliate)
{
@await Html.PartialAsync("_AffiliateAd", new AffiliateAdViewModel
{
SlotKey = position,
ContainerCssClass = containerCss,
Content = affiliateContent!
})
}
else if (shouldRenderAdSense)
{
<!-- Placeholder for Adsterra ad - 300x250 Rectangle -->
<div class="@containerCss" style="display: none;">
<!-- Adsterra code will go here when approved -->
</div>
}
break;
case "footer":
<div class="ad-container ad-footer mt-5 mb-4">
<div class="ad-label">@Localizer["Advertisement"]</div>
<ins class="adsbygoogle"
style="display:inline-block;width:728px;height:90px"
data-ad-client="@tagAdSense"
data-ad-slot="QR19750803"></ins>
</div>
if (shouldRenderAffiliate)
{
@await Html.PartialAsync("_AffiliateAd", new AffiliateAdViewModel
{
SlotKey = position,
ContainerCssClass = containerCss,
Content = affiliateContent!
})
}
else if (shouldRenderAdSense)
{
<!-- Placeholder for Adsterra ad - 728x90 Banner -->
<div class="@containerCss" style="display: none;">
<!-- Adsterra code will go here when approved -->
</div>
}
break;
case "content":
<div class="ad-container ad-content my-4">
<div class="ad-label">@Localizer["Advertisement"]</div>
<ins class="adsbygoogle"
style="display:block"
data-ad-client="@tagAdSense"
data-ad-slot="QR19750804"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
</div>
if (shouldRenderAffiliate)
{
@await Html.PartialAsync("_AffiliateAd", new AffiliateAdViewModel
{
SlotKey = position,
ContainerCssClass = containerCss,
Content = affiliateContent!
})
}
else if (shouldRenderAdSense)
{
<!-- Placeholder for Adsterra ad - Responsive -->
<div class="@containerCss" style="display: none;">
<!-- Adsterra code will go here when approved -->
</div>
}
break;
default:
if (shouldRenderAffiliate)
{
@await Html.PartialAsync("_AffiliateAd", new AffiliateAdViewModel
{
SlotKey = position,
ContainerCssClass = containerCss,
Content = affiliateContent!
})
}
else if (shouldRenderAdSense)
{
<!-- Placeholder for Adsterra ad - 728x90 Banner -->
<div class="@containerCss" style="display: none;">
<!-- Adsterra code will go here when approved -->
</div>
}
break;
}
<script defer>
// Lazy load AdSense to improve LCP
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
(adsbygoogle = window.adsbygoogle || []).push({});
observer.unobserve(entry.target);
}
});
});
document.querySelectorAll('.adsbygoogle').forEach(ad => observer.observe(ad));
} else {
// Fallback for older browsers
(adsbygoogle = window.adsbygoogle || []).push({});
}
</script>
@* AdSense lazy-load script removed - will be replaced with Adsterra when approved *@
}
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>@Localizer["PremiumUserNoAds"]</strong></span>
@ -91,13 +154,12 @@ else if (User.Identity.IsAuthenticated)
}
else
{
<!-- Upgrade to Premium Message -->
<div class="alert alert-info upgrade-notice mb-3">
<i class="fas fa-star text-warning"></i>
<span><strong>@Localizer["UpgradePremiumRemoveAds"]</strong></span>
<a href="/Premium/Upgrade" class="btn btn-sm btn-warning ms-2">
<a href="/Pagamento/SelecaoPlano" class="btn btn-sm btn-warning ms-2">
<i class="fas fa-crown"></i> @Localizer["PremiumBenefitsShort"]
</a>
</div>
}
}
}

View File

@ -0,0 +1,97 @@
@model QRRapidoApp.Models.Ads.AffiliateAdViewModel
@inject Microsoft.Extensions.Localization.IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer
@{
var content = Model.Content;
var containerClass = string.IsNullOrWhiteSpace(Model.ContainerCssClass)
? "ad-container affiliate-ad"
: $"{Model.ContainerCssClass} affiliate-ad";
var ctaText = string.IsNullOrWhiteSpace(content.CtaText) ? "Ver oferta" : content.CtaText;
var slotKey = Model.SlotKey?.ToLowerInvariant();
var isWideSlot = string.Equals(slotKey, "header") || string.Equals(slotKey, "footer");
var imageStyle = slotKey switch
{
"header" => "max-height:140px;object-fit:contain;width:100%;",
"footer" => "max-height:140px;object-fit:contain;width:100%;",
"sidebar" => "max-height:200px;object-fit:contain;width:100%;",
"content" => "max-height:220px;object-fit:contain;width:100%;",
_ => "max-height:220px;object-fit:contain;width:100%;"
};
var hasImage = !string.IsNullOrWhiteSpace(content.ImageUrl);
if (isWideSlot)
{
containerClass = $"{containerClass} affiliate-ad-wide";
}
}
<div class="@containerClass" data-slot="@Model.SlotKey">
<div class="ad-label">@Localizer["Advertisement"]</div>
@if (isWideSlot)
{
<div class="card border-0 shadow-sm h-100 affiliate-ad-card">
<div class="card-body">
<div class="affiliate-ad-card-content">
@if (hasImage)
{
<div class="affiliate-ad-media">
<img src="@content.ImageUrl" alt="@content.Title" class="affiliate-ad-image img-fluid"
style="@imageStyle" loading="lazy" />
@if (!string.IsNullOrWhiteSpace(content.BadgeText))
{
<span class="affiliate-ad-partner badge bg-warning text-dark small mt-2">@content.BadgeText</span>
}
</div>
}
<div class="affiliate-ad-details">
@if (!hasImage && !string.IsNullOrWhiteSpace(content.BadgeText))
{
<span class="affiliate-ad-partner badge bg-warning text-dark">@content.BadgeText</span>
}
<h6 class="card-title mb-0">@content.Title</h6>
@if (!string.IsNullOrWhiteSpace(content.Description))
{
<p class="card-text text-muted small mb-0">@content.Description</p>
}
@if (!string.IsNullOrWhiteSpace(content.PriceText))
{
<div class="affiliate-ad-price fw-semibold text-success">@content.PriceText</div>
}
<div class="affiliate-ad-actions">
<a href="@content.ProductUrl" target="_blank" rel="noopener sponsored" class="btn btn-sm btn-warning affiliate-ad-cta">
@ctaText
</a>
</div>
</div>
</div>
</div>
</div>
}
else
{
<div class="card border-0 shadow-sm h-100 affiliate-ad-card">
@if (hasImage)
{
<img src="@content.ImageUrl" alt="@content.Title" class="card-img-top rounded-top affiliate-ad-image"
style="@imageStyle" loading="lazy" />
}
<div class="card-body text-center">
@if (!string.IsNullOrWhiteSpace(content.BadgeText))
{
<span class="badge bg-warning text-dark small mb-2">@content.BadgeText</span>
}
<h6 class="card-title mb-2">@content.Title</h6>
@if (!string.IsNullOrWhiteSpace(content.Description))
{
<p class="card-text text-muted small mb-2">@content.Description</p>
}
@if (!string.IsNullOrWhiteSpace(content.PriceText))
{
<div class="fw-semibold text-success mb-3">@content.PriceText</div>
}
<a href="@content.ProductUrl" target="_blank" rel="noopener sponsored" class="btn btn-sm btn-warning w-100">
@ctaText
</a>
</div>
</div>
}
</div>

View File

@ -140,14 +140,7 @@
};
</script>
@if (ViewBag.AdSenseEnabled)
{
var tagAdSense = $"https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client={ViewBag.AdSenseTag}";
var adSenseScript = $"";
<!-- AdSense - Optimized with defer and crossorigin -->
<script defer src='@tagAdSense' crossorigin='anonymous'></script>
}
@* AdSense removed - Preparing for Adsterra integration *@
<!-- Bootstrap 5 - Optimized loading -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" media="print" onload="this.media='all'">
@ -289,7 +282,7 @@
}
else
{
<li><a class="dropdown-item text-warning" href="/Premium/Upgrade">
<li><a class="dropdown-item text-warning" href="/Pagamento/SelecaoPlano">
<i class="fas fa-rocket"></i> QR Rapido Premium
</a></li>
}
@ -328,6 +321,14 @@
<p class="mb-0 opacity-75">
@Localizer["AverageTimePrefix"] <strong>@Localizer["AverageTimeValue"]</strong> @Localizer["AverageTimeSuffix"]
</p>
@{
var currentCulture = System.Globalization.CultureInfo.CurrentUICulture.Name;
}
<div class="mt-3">
<a href="/@currentCulture/tutoriais" class="btn btn-sm btn-light">
<i class="fas fa-graduation-cap"></i> @Localizer["ViewTutorials"]
</a>
</div>
</div>
</section>

View File

@ -0,0 +1,290 @@
@model QRRapidoApp.Models.ViewModels.ArticleViewModel
@{
Layout = "~/Views/Shared/_Layout.cshtml";
ViewData["Title"] = Model.Metadata.Title;
var baseUrl = "https://qrrapido.site";
var articleUrl = $"{baseUrl}/{ViewBag.Culture}/tutoriais/{ViewBag.Slug}";
}
<!DOCTYPE html>
<html lang="@ViewBag.Culture">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@Model.Metadata.Title - QR Rapido</title>
<!-- SEO Meta Tags -->
<meta name="description" content="@Model.Metadata.Description">
<meta name="keywords" content="@Model.Metadata.Keywords">
<meta name="author" content="@Model.Metadata.Author">
<meta name="robots" content="index, follow">
<!-- Canonical URL -->
<link rel="canonical" href="@articleUrl">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="article">
<meta property="og:url" content="@articleUrl">
<meta property="og:title" content="@Model.Metadata.Title">
<meta property="og:description" content="@Model.Metadata.Description">
<meta property="og:image" content="@baseUrl@Model.Metadata.Image">
<meta property="og:site_name" content="QR Rapido">
<meta property="article:published_time" content="@Model.Metadata.Date.ToString("yyyy-MM-ddTHH:mm:ssZ")">
<meta property="article:modified_time" content="@Model.Metadata.LastMod.ToString("yyyy-MM-ddTHH:mm:ssZ")">
<meta property="article:author" content="@Model.Metadata.Author">
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:url" content="@articleUrl">
<meta name="twitter:title" content="@Model.Metadata.Title">
<meta name="twitter:description" content="@Model.Metadata.Description">
<meta name="twitter:image" content="@baseUrl@Model.Metadata.Image">
<!-- Schema.org Article Structured Data -->
<script type="application/ld+json">
{
"@@context": "https://schema.org",
"@@type": "Article",
"headline": "@Model.Metadata.Title",
"description": "@Model.Metadata.Description",
"image": "@baseUrl@Model.Metadata.Image",
"author": {
"@@type": "Person",
"name": "@Model.Metadata.Author"
},
"publisher": {
"@@type": "Organization",
"name": "QR Rapido",
"logo": {
"@@type": "ImageObject",
"url": "@baseUrl/images/logo.png"
}
},
"datePublished": "@Model.Metadata.Date.ToString("yyyy-MM-dd")",
"dateModified": "@Model.Metadata.LastMod.ToString("yyyy-MM-dd")",
"mainEntityOfPage": {
"@@type": "WebPage",
"@@id": "@articleUrl"
}
}
</script>
<!-- Breadcrumb Schema -->
<script type="application/ld+json">
{
"@@context": "https://schema.org",
"@@type": "BreadcrumbList",
"itemListElement": [
{
"@@type": "ListItem",
"position": 1,
"name": "Home",
"item": "@baseUrl"
},
{
"@@type": "ListItem",
"position": 2,
"name": "@(ViewBag.Culture == "pt-BR" ? "Tutoriais" : "Tutoriales")",
"item": "@baseUrl/@ViewBag.Culture/tutoriais"
},
{
"@@type": "ListItem",
"position": 3,
"name": "@Model.Metadata.Title",
"item": "@articleUrl"
}
]
}
</script>
</head>
<body>
<div class="container mt-4 mb-5">
<!-- Breadcrumbs -->
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/@ViewBag.Culture">Home</a></li>
<li class="breadcrumb-item"><a href="/@ViewBag.Culture/tutoriais">@(ViewBag.Culture == "pt-BR" ? "Tutoriais" : "Tutoriales")</a></li>
<li class="breadcrumb-item active" aria-current="page">@Model.Metadata.Title</li>
</ol>
</nav>
<div class="row">
<!-- Main Article Content -->
<article class="col-lg-8">
<!-- Article Header -->
<header class="mb-4">
<h1 class="display-4 mb-3">@Model.Metadata.Title</h1>
<div class="text-muted mb-3">
<span><i class="fas fa-user"></i> @Model.Metadata.Author</span> |
<span><i class="fas fa-calendar"></i> @Model.Metadata.Date.ToString("dd MMM yyyy")</span> |
<span><i class="fas fa-clock"></i> @Model.Metadata.ReadingTimeMinutes min @(ViewBag.Culture == "pt-BR" ? "de leitura" : "de lectura")</span>
</div>
@if (!string.IsNullOrEmpty(Model.Metadata.Image))
{
<img src="@Model.Metadata.Image" alt="@Model.Metadata.Title" class="img-fluid rounded mb-4" />
}
<p class="lead">@Model.Metadata.Description</p>
</header>
<!-- Article Body -->
<div class="article-content">
@Html.Raw(Model.HtmlContent)
</div>
<!-- Article Footer -->
<footer class="mt-5 pt-4 border-top">
<p class="text-muted">
<small>
@(ViewBag.Culture == "pt-BR" ? "Última atualização:" : "Última actualización:")
@Model.Metadata.LastMod.ToString("dd MMM yyyy HH:mm")
</small>
</p>
</footer>
<!-- Ad Slot (if ads enabled) -->
@if (ViewBag.ShowAds == true)
{
<div class="my-4">
<div class="alert alert-light text-center" role="alert">
<!-- Ad placement -->
<ins class="adsbygoogle"
style="display:block"
data-ad-client="@ViewBag.AdSenseClientId"
data-ad-slot="@ViewBag.ArticleAdSlot"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
</div>
</div>
}
</article>
<!-- Sidebar -->
<aside class="col-lg-4">
<!-- Related Articles -->
@if (Model.RelatedArticles.Any())
{
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-book"></i>
@(ViewBag.Culture == "pt-BR" ? "Artigos Relacionados" : "Artículos Relacionados")
</h5>
</div>
<div class="list-group list-group-flush">
@foreach (var related in Model.RelatedArticles)
{
<a href="/@ViewBag.Culture/tutoriais/@related.Title.ToLower().Replace(" ", "-")"
class="list-group-item list-group-item-action">
<h6 class="mb-1">@related.Title</h6>
<p class="mb-1 text-muted small">@related.Description</p>
</a>
}
</div>
</div>
}
<!-- CTA Premium (if not premium) -->
@if (ViewBag.IsPremium != true)
{
<div class="card bg-primary text-white mb-4">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-crown"></i>
@(ViewBag.Culture == "pt-BR" ? "Seja Premium!" : "¡Hazte Premium!")
</h5>
<p class="card-text">
@(ViewBag.Culture == "pt-BR"
? "QR codes ilimitados, sem anúncios e recursos avançados."
: "Códigos QR ilimitados, sin anuncios y características avanzadas.")
</p>
<a href="/@ViewBag.Culture/Pagamento/SelecaoPlano" class="btn btn-light btn-block">
@(ViewBag.Culture == "pt-BR" ? "Conhecer Planos" : "Conocer Planes")
</a>
</div>
</div>
}
<!-- Quick Links -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-link"></i>
@(ViewBag.Culture == "pt-BR" ? "Links Úteis" : "Enlaces Útiles")
</h5>
</div>
<div class="list-group list-group-flush">
<a href="/@ViewBag.Culture" class="list-group-item list-group-item-action">
<i class="fas fa-qrcode"></i>
@(ViewBag.Culture == "pt-BR" ? "Gerar QR Code" : "Generar Código QR")
</a>
<a href="/@ViewBag.Culture/FAQ" class="list-group-item list-group-item-action">
<i class="fas fa-question-circle"></i>
@(ViewBag.Culture == "pt-BR" ? "Perguntas Frequentes" : "Preguntas Frecuentes")
</a>
<a href="/@ViewBag.Culture/Contact" class="list-group-item list-group-item-action">
<i class="fas fa-envelope"></i>
@(ViewBag.Culture == "pt-BR" ? "Contato" : "Contacto")
</a>
</div>
</div>
</aside>
</div>
</div>
<style>
.article-content {
font-size: 1.1rem;
line-height: 1.8;
}
.article-content h2 {
margin-top: 2rem;
margin-bottom: 1rem;
font-weight: 600;
}
.article-content h3 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
font-weight: 600;
}
.article-content img {
max-width: 100%;
height: auto;
margin: 1.5rem 0;
border-radius: 8px;
}
.article-content pre {
background: #f4f4f4;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
}
.article-content code {
background: #f4f4f4;
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-size: 0.9em;
}
.article-content blockquote {
border-left: 4px solid #007bff;
padding-left: 1rem;
margin-left: 0;
font-style: italic;
color: #6c757d;
}
.breadcrumb {
background-color: transparent;
padding: 0.5rem 0;
}
</style>
</body>
</html>

View File

@ -0,0 +1,109 @@
@model List<QRRapidoApp.Models.ArticleMetadata>
@{
ViewData["Title"] = ViewBag.Culture == "pt-BR" ? "Tutoriais QR Code" : "Tutoriales Código QR";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div class="container mt-4 mb-5">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<h1 class="display-4">
<i class="fas fa-book"></i>
@(ViewBag.Culture == "pt-BR" ? "Tutoriais QR Code" : "Tutoriales Código QR")
</h1>
<p class="lead text-muted">
@(ViewBag.Culture == "pt-BR"
? "Aprenda tudo sobre QR Codes com nossos tutoriais completos e passo a passo"
: "Aprende todo sobre códigos QR con nuestros tutoriales completos paso a paso")
</p>
</div>
</div>
<!-- Tutorials Grid -->
@if (Model.Any())
{
<div class="row">
@foreach (var article in Model)
{
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100 shadow-sm hover-shadow">
@if (!string.IsNullOrEmpty(article.Image))
{
<img src="@article.Image" class="card-img-top" alt="@article.Title" style="height: 200px; object-fit: cover;">
}
<div class="card-body d-flex flex-column">
<h5 class="card-title">@article.Title</h5>
<p class="card-text text-muted flex-grow-1">@article.Description</p>
<div class="mb-3">
<small class="text-muted">
<i class="fas fa-calendar"></i> @article.Date.ToString("dd MMM yyyy") |
<i class="fas fa-clock"></i> @article.ReadingTimeMinutes min
</small>
</div>
<a href="/@ViewBag.Culture/tutoriais/@article.Slug"
class="btn btn-primary btn-block">
@(ViewBag.Culture == "pt-BR" ? "Ler Tutorial" : "Leer Tutorial")
<i class="fas fa-arrow-right ml-1"></i>
</a>
</div>
</div>
</div>
}
</div>
}
else
{
<div class="alert alert-info" role="alert">
<i class="fas fa-info-circle"></i>
@(ViewBag.Culture == "pt-BR"
? "Nenhum tutorial disponível no momento. Volte em breve!"
: "No hay tutoriales disponibles en este momento. ¡Vuelve pronto!")
</div>
}
<!-- CTA Section -->
<div class="row mt-5">
<div class="col-12">
<div class="card bg-primary text-white">
<div class="card-body text-center py-5">
<h3 class="mb-3">
@(ViewBag.Culture == "pt-BR"
? "Pronto para criar seu QR Code?"
: "¿Listo para crear tu código QR?")
</h3>
<p class="lead mb-4">
@(ViewBag.Culture == "pt-BR"
? "Gere QR codes profissionais em segundos com nossa ferramenta ultrarrápida!"
: "¡Genera códigos QR profesionales en segundos con nuestra herramienta ultrarrápida!")
</p>
<a href="/@ViewBag.Culture" class="btn btn-light btn-lg">
<i class="fas fa-qrcode"></i>
@(ViewBag.Culture == "pt-BR" ? "Criar QR Code Agora" : "Crear Código QR Ahora")
</a>
</div>
</div>
</div>
</div>
</div>
<style>
.hover-shadow {
transition: all 0.3s ease;
}
.hover-shadow:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.2) !important;
}
.card-img-top {
transition: transform 0.3s ease;
}
.card:hover .card-img-top {
transform: scale(1.05);
}
</style>

View File

@ -18,6 +18,24 @@
}
}
},
"Stripe": {
"PublishableKey": "pk_live_XXXXX",
"SecretKey": "sk_live_XXXXX",
"WebhookSecret": "whsec_live_XXXXX",
"ProductId": "prod_SnfQTxwE3i8r5L",
"Plans": {
"Monthly": {
"BR": "price_XXXXX_monthly_br_PROD",
"PY": "price_XXXXX_monthly_py_PROD",
"US": "price_XXXXX_monthly_us_PROD"
},
"Yearly": {
"BR": "price_XXXXX_yearly_br_PROD",
"PY": "price_XXXXX_yearly_py_PROD",
"US": "price_XXXXX_yearly_us_PROD"
}
}
},
"ResourceMonitoring": {
"Enabled": true,
"IntervalSeconds": 30,

View File

@ -24,11 +24,88 @@
"PublishableKey": "pk_test_51Rs42tBeR5IFYUsBooapyDwQTgh6CFuKbya5R3MVDTrdOUKmgiHQYipU0pgOdG5iKogH77RUYIKBJzbCt5BghUOY00xitV5KiN",
"SecretKey": "sk_test_51Rs42tBeR5IFYUsBtycRlJJcdwgoMbh8MfQIKIGelBPTQFwDcOn2iCCbw5uG6hnqlpgNAUuFgWRAUUMA8qkABKun00EIx4odDF",
"WebhookSecret": "whsec_2e828803ceb48e7865458b0cf332b68535fdff8753d26d69b1c88ea55cb0e482",
"PriceId": "prod_SnfQTxwE3i8r5L"
"ProductId": "prod_SnfQTxwE3i8r5L",
"Plans": {
"Monthly": {
"BR": "price_1Rs45OBeR5IFYUsBfsnOpOiv",
"PY": "price_XXXXX_monthly_py",
"US": "price_XXXXX_monthly_us"
},
"Yearly": {
"BR": "price_1Rs4AyBeR5IFYUsB8kRSNUIM",
"PY": "price_XXXXX_yearly_py",
"US": "price_XXXXX_yearly_us"
}
}
},
"AdSense": {
"ClientId": "ca-pub-3475956393038764",
"Enabled": true
"Enabled": false
},
"Ads": {
"Slots": {
"header": {
"Provider": "AdSense",
"AdSenseSlotId": "QR19750801"
},
"sidebar": {
"Provider": "AdSense",
"AdSenseSlotId": "QR19750802"
},
"content": {
"Provider": "AdSense",
"AdSenseSlotId": "QR19750804"
},
"footer": {
"Provider": "AdSense",
"AdSenseSlotId": "QR19750803"
}
},
"Locales": {
"pt-BR": {
"sidebar": {
"Provider": "None"
},
"header": {
"Provider": "None"
},
"content": {
"Provider": "None"
},
"footer": {
"Provider": "None"
}
},
"es-PY": {
"sidebar": {
"Provider": "None"
},
"header": {
"Provider": "None"
},
"content": {
"Provider": "None"
},
"footer": {
"Provider": "None"
}
},
"es": {
"sidebar": {
"Provider": "Affiliate",
"Affiliate": {
"Title": "Pack de etiquetas adhesivas para QR",
"Description": "Rollo con 500 etiquetas mate listas para imprimir códigos QR",
"ProductUrl": "https://marketplace-ejemplo.com/afiliados/etiquetas-qr",
"ImageUrl": "https://cdn.example.com/qr-labels-pack.jpg",
"CtaText": "Explorar",
"BadgeText": "Oferta destacada",
"PriceText": "$ 12.90",
"Category": "insumos"
}
}
}
}
},
"Performance": {
"QRGenerationTimeoutMs": 2000,
@ -116,4 +193,4 @@
}
},
"AllowedHosts": "*"
}
}

View File

@ -518,14 +518,129 @@ footer a:hover {
position: relative;
}
.ad-label {
color: #495057 !important;
font-size: 0.75rem;
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.ad-label {
color: #495057 !important;
font-size: 0.75rem;
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.affiliate-ad-wide {
text-align: left;
}
.affiliate-ad-wide .affiliate-ad-card {
max-width: 100%;
}
.affiliate-ad-wide .affiliate-ad-card .card-body {
padding: 1.5rem;
}
.affiliate-ad-wide .affiliate-ad-card-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.25rem;
width: 100%;
}
.affiliate-ad-wide .affiliate-ad-media {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
text-align: center;
}
.affiliate-ad-wide .affiliate-ad-media img {
display: block;
max-width: 240px;
width: 100%;
}
.affiliate-ad-wide .affiliate-ad-partner {
font-weight: 600;
}
.affiliate-ad-wide .affiliate-ad-details {
display: flex;
flex-direction: column;
gap: 0.75rem;
text-align: center;
align-items: center;
}
.affiliate-ad-wide .affiliate-ad-details .card-title {
font-size: 1rem;
}
.affiliate-ad-wide .affiliate-ad-price {
font-size: 0.95rem;
}
.affiliate-ad-wide .affiliate-ad-actions {
display: flex;
flex-direction: column;
gap: 0.75rem;
width: 100%;
align-items: stretch;
}
.affiliate-ad-wide .affiliate-ad-cta {
width: 100%;
}
@media (min-width: 576px) {
.affiliate-ad-wide .affiliate-ad-actions {
align-items: center;
}
}
@media (min-width: 992px) {
.affiliate-ad-wide .affiliate-ad-card {
max-width: 1000px;
margin: 0 auto;
}
.affiliate-ad-wide .affiliate-ad-card .card-body {
padding: 1.75rem 2rem;
}
.affiliate-ad-wide .affiliate-ad-card-content {
display: grid;
grid-template-columns: minmax(220px, 320px) minmax(0, 1fr);
align-items: center;
column-gap: 2.5rem;
}
.affiliate-ad-wide .affiliate-ad-media {
align-items: flex-start;
text-align: left;
}
.affiliate-ad-wide .affiliate-ad-media img {
max-width: 280px;
}
.affiliate-ad-wide .affiliate-ad-details {
text-align: left;
align-items: flex-start;
}
.affiliate-ad-wide .affiliate-ad-actions {
flex-direction: row;
align-items: flex-start;
width: auto;
gap: 1rem;
}
.affiliate-ad-wide .affiliate-ad-cta {
width: auto;
}
}
/* =================================
PLACEHOLDER QR - CONTRASTE SUPERIOR
@ -1287,4 +1402,4 @@ html[data-theme="dark"] @keyframes buttonPulse {
transform: scale(1.02);
box-shadow: 0 0 10px rgba(96, 165, 250, 0.4);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

View File

@ -1489,7 +1489,7 @@ class QRRapidoGenerator {
<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>
<a href="/Pagamento/SelecaoPlano" class="btn btn-sm btn-warning ms-2">Tornar Permanente</a>
`;
const container = document.querySelector('.container');
@ -1548,7 +1548,7 @@ class QRRapidoGenerator {
</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">
<a href="/Pagamento/SelecaoPlano" class="btn btn-warning">
<i class="fas fa-crown"></i> Fazer Upgrade
</a>
</div>