fix: ajustar para configuração de plano ficam só no mongondb.
All checks were successful
Deploy QR Rapido / test (push) Successful in 41s
Deploy QR Rapido / build-and-push (push) Successful in 14m6s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m55s

This commit is contained in:
Ricardo Carneiro 2025-10-19 21:48:45 -03:00
parent 59e04fedc7
commit 2edb4e1196
12 changed files with 834 additions and 203 deletions

View File

@ -29,7 +29,9 @@
"Bash(nc:*)", "Bash(nc:*)",
"Bash(ssh:*)", "Bash(ssh:*)",
"Read(//mnt/c/vscode/**)", "Read(//mnt/c/vscode/**)",
"Read(//mnt/c/**)" "Read(//mnt/c/**)",
"Bash(chmod +x /mnt/c/vscode/qrrapido/Scripts/update-plans.sh)",
"Bash(netstat -tln)"
], ],
"deny": [] "deny": []
} }

View File

@ -0,0 +1,142 @@
using Microsoft.AspNetCore.Mvc;
using QRRapidoApp.Data;
using QRRapidoApp.Models;
using MongoDB.Driver;
namespace QRRapidoApp.Controllers
{
/// <summary>
/// Admin controller - ONLY accessible from localhost for security
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class AdminController : ControllerBase
{
private readonly MongoDbContext _context;
private readonly ILogger<AdminController> _logger;
public AdminController(MongoDbContext context, ILogger<AdminController> logger)
{
_context = context;
_logger = logger;
}
/// <summary>
/// Seed/Update MongoDB Plans collection
/// Only accessible from localhost (127.0.0.1 or ::1)
/// </summary>
[HttpPost("SeedPlans")]
public async Task<IActionResult> SeedPlans([FromBody] List<Plan> plans)
{
// SECURITY: Only allow from localhost
var remoteIp = HttpContext.Connection.RemoteIpAddress;
var isLocalhost = remoteIp != null &&
(remoteIp.ToString() == "127.0.0.1" ||
remoteIp.ToString() == "::1" ||
remoteIp.ToString() == "localhost");
if (!isLocalhost)
{
_logger.LogWarning($"Unauthorized admin access attempt from {remoteIp}");
return Forbid("This endpoint is only accessible from localhost");
}
try
{
_logger.LogInformation($"SeedPlans called from localhost - Upserting {plans.Count} plans");
foreach (var plan in plans)
{
// Upsert based on interval (month/year)
var filter = Builders<Plan>.Filter.Eq(p => p.Interval, plan.Interval);
var options = new ReplaceOptions { IsUpsert = true };
await _context.Plans.ReplaceOneAsync(filter, plan, options);
_logger.LogInformation($"Upserted plan: {plan.Interval}");
}
return Ok(new {
success = true,
message = $"{plans.Count} plans seeded successfully",
plans = plans.Select(p => new {
interval = p.Interval,
priceIds = p.PricesByCountry.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.StripePriceId
)
})
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error seeding plans");
return StatusCode(500, new { success = false, error = ex.Message });
}
}
/// <summary>
/// Get all plans from MongoDB
/// Only accessible from localhost
/// </summary>
[HttpGet("Plans")]
public async Task<IActionResult> GetPlans()
{
// SECURITY: Only allow from localhost
var remoteIp = HttpContext.Connection.RemoteIpAddress;
var isLocalhost = remoteIp != null &&
(remoteIp.ToString() == "127.0.0.1" ||
remoteIp.ToString() == "::1" ||
remoteIp.ToString() == "localhost");
if (!isLocalhost)
{
_logger.LogWarning($"Unauthorized admin access attempt from {remoteIp}");
return Forbid("This endpoint is only accessible from localhost");
}
try
{
var plans = await _context.Plans.Find(_ => true).ToListAsync();
return Ok(new { success = true, count = plans.Count, plans });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving plans");
return StatusCode(500, new { success = false, error = ex.Message });
}
}
/// <summary>
/// Delete all plans from MongoDB
/// Only accessible from localhost
/// </summary>
[HttpDelete("Plans")]
public async Task<IActionResult> DeleteAllPlans()
{
// SECURITY: Only allow from localhost
var remoteIp = HttpContext.Connection.RemoteIpAddress;
var isLocalhost = remoteIp != null &&
(remoteIp.ToString() == "127.0.0.1" ||
remoteIp.ToString() == "::1" ||
remoteIp.ToString() == "localhost");
if (!isLocalhost)
{
_logger.LogWarning($"Unauthorized admin access attempt from {remoteIp}");
return Forbid("This endpoint is only accessible from localhost");
}
try
{
var result = await _context.Plans.DeleteManyAsync(_ => true);
_logger.LogInformation($"Deleted {result.DeletedCount} plans");
return Ok(new { success = true, deletedCount = result.DeletedCount });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting plans");
return StatusCode(500, new { success = false, error = ex.Message });
}
}
}
}

View File

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

272
Scripts/README-ADMIN-API.md Normal file
View File

@ -0,0 +1,272 @@
# 🔧 Admin API - Gerenciamento de Planos
Este diretório contém ferramentas para gerenciar os planos do Stripe no MongoDB via API local.
## 🔒 Segurança
**IMPORTANTE**: O endpoint `/api/Admin/*` **só funciona quando acessado de localhost** (127.0.0.1). Qualquer acesso externo retornará `403 Forbidden`.
---
## 📋 Arquivos
- **`plans.json`** - Configuração dos planos com Price IDs do Stripe
- **`update-plans.sh`** - Script bash para atualizar planos via curl
- **`seed-mongodb-plans.js`** - Script MongoDB direto (método alternativo)
---
## 🚀 Método 1: API Endpoint (Recomendado)
### Vantagens
- ✅ Não precisa de credenciais do MongoDB
- ✅ Validação automática
- ✅ Logs no sistema
- ✅ Funciona com app rodando
### Como Usar
#### 1⃣ Edite o `plans.json` com seus Price IDs
```json
{
"pricesByCountry": {
"BR": {
"stripePriceId": "price_SEU_ID_AQUI"
}
}
}
```
#### 2⃣ Inicie a aplicação
```bash
dotnet run
# ou
dotnet watch run
```
#### 3⃣ Execute o script de atualização
```bash
# Linux/Mac/WSL
cd Scripts
./update-plans.sh
# Especificar porta diferente
./update-plans.sh 5000
```
#### 4⃣ Ou use curl diretamente
```bash
# Atualizar planos
curl -k -X POST \
-H "Content-Type: application/json" \
-d @Scripts/plans.json \
https://localhost:52428/api/Admin/SeedPlans
# Listar planos
curl -k https://localhost:52428/api/Admin/Plans
# Deletar todos os planos
curl -k -X DELETE https://localhost:52428/api/Admin/Plans
```
---
## 🗄️ Método 2: Script MongoDB Direto
### Vantagens
- ✅ Funciona sem a app rodando
- ✅ Acesso direto ao MongoDB
### Desvantagens
- ❌ Precisa de credenciais do MongoDB
- ❌ Sem validação automática
### Como Usar
```bash
# Localhost
mongosh "mongodb://localhost:27017/QrRapido" < Scripts/seed-mongodb-plans.js
# Produção
mongosh "mongodb://user:pass@host:27017/QrRapido?replicaSet=rs0&authSource=admin" < Scripts/seed-mongodb-plans.js
```
---
## 📝 Endpoints Disponíveis
### POST `/api/Admin/SeedPlans`
Cria ou atualiza planos no MongoDB
**Request Body:**
```json
[
{
"interval": "month",
"stripePriceId": "price_...",
"pricesByCountry": { ... }
}
]
```
**Response:**
```json
{
"success": true,
"message": "2 plans seeded successfully",
"plans": [ ... ]
}
```
---
### GET `/api/Admin/Plans`
Lista todos os planos do MongoDB
**Response:**
```json
{
"success": true,
"count": 2,
"plans": [ ... ]
}
```
---
### DELETE `/api/Admin/Plans`
Remove todos os planos do MongoDB
**Response:**
```json
{
"success": true,
"deletedCount": 2
}
```
---
## 🛡️ Proteção de Segurança
O código verifica o IP de origem:
```csharp
var remoteIp = HttpContext.Connection.RemoteIpAddress;
var isLocalhost = remoteIp != null &&
(remoteIp.ToString() == "127.0.0.1" ||
remoteIp.ToString() == "::1" ||
remoteIp.ToString() == "localhost");
if (!isLocalhost) {
return Forbid("This endpoint is only accessible from localhost");
}
```
**Tentativas de acesso externo são logadas:**
```
[Warning] Unauthorized admin access attempt from 192.168.1.100
```
---
## 🧪 Testando
```bash
# 1. Listar planos atuais
curl -k https://localhost:52428/api/Admin/Plans | jq '.'
# 2. Deletar planos
curl -k -X DELETE https://localhost:52428/api/Admin/Plans
# 3. Criar novos planos
curl -k -X POST \
-H "Content-Type: application/json" \
-d @Scripts/plans.json \
https://localhost:52428/api/Admin/SeedPlans | jq '.'
# 4. Verificar se foram criados
curl -k https://localhost:52428/api/Admin/Plans | jq '.plans[] | {interval, priceIds: .pricesByCountry}'
```
---
## ⚠️ Troubleshooting
### Erro: "Connection refused"
- ✅ Certifique-se que a app está rodando: `dotnet run`
### Erro: "Forbidden" mesmo em localhost
- ✅ Verifique se está usando `https://localhost` (não `http://`)
- ✅ Verifique se está usando `localhost` ou `127.0.0.1` (não o IP da máquina)
### Erro: "SSL certificate problem"
- ✅ Use o flag `-k` no curl para aceitar certificados auto-assinados
---
## 📊 Estrutura do plans.json
```json
[
{
"name": {
"pt-BR": "Nome em português",
"es-PY": "Nombre en español",
"en": "Name in english"
},
"description": { ... },
"features": { ... },
"interval": "month" ou "year",
"stripePriceId": "price_default",
"pricesByCountry": {
"BR": {
"amount": 9.90,
"currency": "BRL",
"stripePriceId": "price_BR_ID"
},
"PY": {
"amount": 35000,
"currency": "PYG",
"stripePriceId": "price_PY_ID"
}
},
"isActive": true,
"displayOrder": 1,
"badge": { ... } // Opcional
}
]
```
---
## 🎯 Fluxo Recomendado
1. **Desenvolvimento (localhost)**:
```bash
# Editar plans.json com Price IDs de teste
./update-plans.sh
```
2. **Staging**:
```bash
# SSH no servidor staging
ssh user@staging-server
cd /app/qrrapido
./Scripts/update-plans.sh 5000
```
3. **Produção**:
```bash
# Opção 1: Via API (se tiver acesso SSH)
ssh user@prod-server
cd /app/qrrapido
./Scripts/update-plans.sh 5001
# Opção 2: Via MongoDB direto
mongosh "connection_string" < Scripts/seed-mongodb-plans.js
```

134
Scripts/plans.json Normal file
View File

@ -0,0 +1,134 @@
[
{
"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",
"Customização avançada",
"Suporte para logos",
"Histórico e downloads",
"Suporte prioritário",
"Cancele a qualquer momento"
],
"es-PY": [
"Códigos QR ilimitados",
"Sin anuncios",
"Personalización avanzada",
"Soporte para logos",
"Historial y descargas",
"Soporte prioritario",
"Cancela cuando quieras"
],
"en": [
"Unlimited QR Codes",
"No ads",
"Advanced customization",
"Logo support",
"History and downloads",
"Priority support",
"Cancel anytime"
]
},
"interval": "month",
"stripePriceId": "price_1SJwebB6bFjHQirAloEqXWd6",
"pricesByCountry": {
"BR": {
"amount": 9.90,
"currency": "BRL",
"stripePriceId": "price_1SJwebB6bFjHQirAloEqXWd6"
},
"PY": {
"amount": 35000,
"currency": "PYG",
"stripePriceId": "price_1SK4Y0B6bFjHQirAaxNHxILi"
},
"US": {
"amount": 1.99,
"currency": "USD",
"stripePriceId": "price_XXXXX_monthly_us"
}
},
"isActive": true,
"displayOrder": 1
},
{
"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",
"Customização avançada",
"Suporte para logos",
"Histórico e downloads",
"Suporte prioritário",
"💰 Economia de 20%",
"Cobrança anual única"
],
"es-PY": [
"Códigos QR ilimitados",
"Sin anuncios",
"Personalización avanzada",
"Soporte para logos",
"Historial y descargas",
"Soporte prioritario",
"💰 Ahorro del 20%",
"Facturación anual única"
],
"en": [
"Unlimited QR Codes",
"No ads",
"Advanced customization",
"Logo support",
"History and downloads",
"Priority support",
"💰 Save 20%",
"Billed annually"
]
},
"interval": "year",
"stripePriceId": "price_1SK4X7B6bFjHQirAdMtviPw4",
"pricesByCountry": {
"BR": {
"amount": 95.04,
"currency": "BRL",
"stripePriceId": "price_1SK4X7B6bFjHQirAdMtviPw4"
},
"PY": {
"amount": 336000,
"currency": "PYG",
"stripePriceId": "price_1SK4Y0B6bFjHQirAaxNHxILi"
},
"US": {
"amount": 19.10,
"currency": "USD",
"stripePriceId": "price_XXXXX_yearly_us"
}
},
"isActive": true,
"displayOrder": 2,
"badge": {
"pt-BR": "MELHOR VALOR",
"es-PY": "MEJOR VALOR",
"en": "BEST VALUE"
}
}
]

View File

@ -51,22 +51,22 @@ db.Plans.insertOne({
] ]
}, },
interval: "month", interval: "month",
stripePriceId: "price_XXXXX_monthly_us", // Default price (USA) - UPDATE from appsettings.json > Stripe:Plans:Monthly:US stripePriceId: "price_1SJwebB6bFjHQirAloEqXWd6", // Default price (BR)
pricesByCountry: { pricesByCountry: {
"BR": { "BR": {
amount: 9.90, amount: 9.90,
currency: "BRL", currency: "BRL",
stripePriceId: "price_XXXXX_monthly_br" // UPDATE from appsettings.json > Stripe:Plans:Monthly:BR stripePriceId: "price_1SJwebB6bFjHQirAloEqXWd6"
}, },
"PY": { "PY": {
amount: 35000, amount: 35000,
currency: "PYG", currency: "PYG",
stripePriceId: "price_XXXXX_monthly_py" // UPDATE from appsettings.json > Stripe:Plans:Monthly:PY stripePriceId: "price_1SK4Y0B6bFjHQirAaxNHxILi"
}, },
"US": { "US": {
amount: 1.99, amount: 1.99,
currency: "USD", currency: "USD",
stripePriceId: "price_XXXXX_monthly_us" // UPDATE from appsettings.json > Stripe:Plans:Monthly:US stripePriceId: "price_XXXXX_monthly_us" // TODO: Update with real Stripe Price ID
} }
}, },
isActive: true, isActive: true,
@ -120,22 +120,22 @@ db.Plans.insertOne({
] ]
}, },
interval: "year", interval: "year",
stripePriceId: "price_XXXXX_yearly_us", // Default price (USA) - UPDATE from appsettings.json > Stripe:Plans:Yearly:US stripePriceId: "price_1SK4X7B6bFjHQirAdMtviPw4", // Default price (BR)
pricesByCountry: { pricesByCountry: {
"BR": { "BR": {
amount: 95.04, // 9.90 * 12 * 0.80 = Economia de 20% amount: 95.04, // 9.90 * 12 * 0.80 = Economia de 20%
currency: "BRL", currency: "BRL",
stripePriceId: "price_XXXXX_yearly_br" // UPDATE from appsettings.json > Stripe:Plans:Yearly:BR stripePriceId: "price_1SK4X7B6bFjHQirAdMtviPw4"
}, },
"PY": { "PY": {
amount: 336000, // 35000 * 12 * 0.80 = Economia de 20% amount: 336000, // 35000 * 12 * 0.80 = Economia de 20%
currency: "PYG", currency: "PYG",
stripePriceId: "price_XXXXX_yearly_py" // UPDATE from appsettings.json > Stripe:Plans:Yearly:PY stripePriceId: "price_1SK4Y0B6bFjHQirAaxNHxILi"
}, },
"US": { "US": {
amount: 19.10, // 1.99 * 12 * 0.80 = Economia de 20% amount: 19.10, // 1.99 * 12 * 0.80 = Economia de 20%
currency: "USD", currency: "USD",
stripePriceId: "price_XXXXX_yearly_us" // UPDATE from appsettings.json > Stripe:Plans:Yearly:US stripePriceId: "price_XXXXX_yearly_us" // TODO: Update with real Stripe Price ID
} }
}, },
isActive: true, isActive: true,

63
Scripts/update-plans.sh Normal file
View File

@ -0,0 +1,63 @@
#!/bin/bash
# Script to update MongoDB Plans via localhost-only API endpoint
# Usage: ./update-plans.sh [port]
PORT=${1:-52428}
API_URL="https://localhost:$PORT/api/Admin/SeedPlans"
PLANS_FILE="$(dirname "$0")/plans.json"
echo "🔧 QR Rapido - Update Plans Script"
echo "=================================="
echo ""
echo "📋 Plans file: $PLANS_FILE"
echo "🌐 API URL: $API_URL"
echo ""
# Check if plans.json exists
if [ ! -f "$PLANS_FILE" ]; then
echo "❌ Error: plans.json not found at $PLANS_FILE"
echo ""
echo "Please create plans.json with your Stripe Price IDs"
exit 1
fi
# Check if the app is running
echo "🔍 Checking if app is running on port $PORT..."
if ! curl -k -s "https://localhost:$PORT/health" > /dev/null 2>&1; then
echo "❌ Error: App not responding on https://localhost:$PORT"
echo ""
echo "Please start the app first:"
echo " dotnet run"
exit 1
fi
echo "✅ App is running"
echo ""
# Send request
echo "📤 Sending plans to API..."
response=$(curl -k -s -w "\n%{http_code}" \
-X POST \
-H "Content-Type: application/json" \
-d @"$PLANS_FILE" \
"$API_URL")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')
echo ""
if [ "$http_code" -eq 200 ]; then
echo "✅ Success! Plans updated in MongoDB"
echo ""
echo "$body" | jq '.' 2>/dev/null || echo "$body"
else
echo "❌ Error! HTTP Status: $http_code"
echo ""
echo "$body"
exit 1
fi
echo ""
echo "🎉 Done!"

View File

@ -33,6 +33,24 @@ namespace QRRapidoApp.Services
} }
var customerId = user.StripeCustomerId; var customerId = user.StripeCustomerId;
var customerService = new CustomerService();
// Verify if customer exists in Stripe, create new if not
if (!string.IsNullOrEmpty(customerId))
{
try
{
// Try to retrieve the customer to verify it exists
await customerService.GetAsync(customerId);
_logger.LogInformation($"Using existing Stripe customer {customerId} for user {userId}");
}
catch (StripeException ex) when (ex.StripeError?.Code == "resource_missing")
{
_logger.LogWarning($"Stripe customer {customerId} not found, creating new one for user {userId}");
customerId = null; // Force creation of new customer
}
}
if (string.IsNullOrEmpty(customerId)) if (string.IsNullOrEmpty(customerId))
{ {
var customerOptions = new CustomerCreateOptions var customerOptions = new CustomerCreateOptions
@ -41,10 +59,10 @@ namespace QRRapidoApp.Services
Name = user.Name, Name = user.Name,
Metadata = new Dictionary<string, string> { { "app_user_id", user.Id } } Metadata = new Dictionary<string, string> { { "app_user_id", user.Id } }
}; };
var customerService = new CustomerService();
var customer = await customerService.CreateAsync(customerOptions); var customer = await customerService.CreateAsync(customerOptions);
customerId = customer.Id; customerId = customer.Id;
await _userService.UpdateUserStripeCustomerIdAsync(userId, customerId); await _userService.UpdateUserStripeCustomerIdAsync(userId, customerId);
_logger.LogInformation($"Created new Stripe customer {customerId} for user {userId}");
} }
var options = new SessionCreateOptions var options = new SessionCreateOptions

View File

@ -145,15 +145,9 @@
else if (User.Identity.IsAuthenticated) else if (User.Identity.IsAuthenticated)
{ {
var isPremium = await AdService.HasValidPremiumSubscription(userId); var isPremium = await AdService.HasValidPremiumSubscription(userId);
if (isPremium) if (!isPremium)
{
<div class="alert alert-success ad-free-notice mb-3">
<i class="fas fa-crown text-warning"></i>
<span><strong>@Localizer["PremiumUserNoAds"]</strong></span>
</div>
}
else
{ {
@* Only show upgrade notice for non-premium users *@
<div class="alert alert-info upgrade-notice mb-3"> <div class="alert alert-info upgrade-notice mb-3">
<i class="fas fa-star text-warning"></i> <i class="fas fa-star text-warning"></i>
<span><strong>@Localizer["UpgradePremiumRemoveAds"]</strong></span> <span><strong>@Localizer["UpgradePremiumRemoveAds"]</strong></span>
@ -162,4 +156,5 @@ else if (User.Identity.IsAuthenticated)
</a> </a>
</div> </div>
} }
@* Premium users: render nothing (100% invisible) *@
} }

View File

@ -38,6 +38,24 @@
"GrowthRateWarningMBPerHour": 200, "GrowthRateWarningMBPerHour": 200,
"IncludeCollectionStats": true "IncludeCollectionStats": true
}, },
"Stripe": {
"PublishableKey": "pk_test_51Rs42SB6bFjHQirAJ6kzbFCbBuAobyNbmlgpULFsInl8KRzlpclUoqOZICqvp2S51kquw3Bc04CPO9bNUEgDLDgd00XbAHT7Fh",
"SecretKey": "sk_test_51Rs42SB6bFjHQirANOUg8jgzPALbNdVWULSVRMycFRBTzE0QUGA6pkpoQaTVsCIoV3XGRgoJ7E3CA6Y67vWlM76q00QBoKW0aH",
"WebhookSecret": "whsec_667402ff1d753b181f626636d556975f2749b5fec4d1231d44f040b057fb3009",
"ProductId": "prod_TGTbombliOUYmQ",
"Plans": {
"Monthly": {
"BR": "price_1SJwebB6bFjHQirAloEqXWd6",
"PY": "price_1SK4Y0B6bFjHQirAaxNHxILi",
"US": "price_XXXXX_monthly_us"
},
"Yearly": {
"BR": "price_1SK4X7B6bFjHQirAdMtviPw4",
"PY": "price_1SK4Y0B6bFjHQirAaxNHxILi",
"US": "price_XXXXX_yearly_us"
}
}
},
"HealthChecks": { "HealthChecks": {
"MongoDB": { "MongoDB": {
"TimeoutSeconds": 10, "TimeoutSeconds": 10,

View File

@ -21,22 +21,9 @@
} }
}, },
"Stripe": { "Stripe": {
"PublishableKey": "pk_test_51Rs42tBeR5IFYUsBooapyDwQTgh6CFuKbya5R3MVDTrdOUKmgiHQYipU0pgOdG5iKogH77RUYIKBJzbCt5BghUOY00xitV5KiN", "PublishableKey": "pk_test_51Rs42SB6bFjHQirAJ6kzbFCbBuAobyNbmlgpULFsInl8KRzlpclUoqOZICqvp2S51kquw3Bc04CPO9bNUEgDLDgd00XbAHT7Fh",
"SecretKey": "sk_test_51Rs42tBeR5IFYUsBtycRlJJcdwgoMbh8MfQIKIGelBPTQFwDcOn2iCCbw5uG6hnqlpgNAUuFgWRAUUMA8qkABKun00EIx4odDF", "SecretKey": "sk_test_51Rs42SB6bFjHQirANOUg8jgzPALbNdVWULSVRMycFRBTzE0QUGA6pkpoQaTVsCIoV3XGRgoJ7E3CA6Y67vWlM76q00QBoKW0aH",
"WebhookSecret": "whsec_2e828803ceb48e7865458b0cf332b68535fdff8753d26d69b1c88ea55cb0e482", "WebhookSecret": "whsec_667402ff1d753b181f626636d556975f2749b5fec4d1231d44f040b057fb3009"
"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": { "AdSense": {
"ClientId": "ca-pub-3475956393038764", "ClientId": "ca-pub-3475956393038764",