fix: ajustes de rodape e visualização
This commit is contained in:
parent
116134b87a
commit
aa2c864689
@ -215,10 +215,19 @@ public class StripeWebhookController : ControllerBase
|
|||||||
// This would be configured based on your actual Stripe price IDs
|
// This would be configured based on your actual Stripe price IDs
|
||||||
return priceId switch
|
return priceId switch
|
||||||
{
|
{
|
||||||
var id when id.Contains("basic") => "basic",
|
"price_1RjUskBMIadsOxJVgLwlVo1y" => "Basic",
|
||||||
var id when id.Contains("professional") => "professional",
|
"price_1RjUv9BMIadsOxJVORqlM4E9" => "Professional",
|
||||||
var id when id.Contains("premium") => "premium",
|
"price_1RjUw0BMIadsOxJVmdouNV1g" => "Premium",
|
||||||
_ => "trial"
|
"price_basic_yearly_placeholder" => "BasicYearly",
|
||||||
|
"price_professional_yearly_placeholder" => "ProfessionalYearly",
|
||||||
|
"price_premium_yearly_placeholder" => "PremiumYearly",
|
||||||
|
var id when id.Contains("basic") && id.Contains("yearly") => "BasicYearly",
|
||||||
|
var id when id.Contains("professional") && id.Contains("yearly") => "ProfessionalYearly",
|
||||||
|
var id when id.Contains("premium") && id.Contains("yearly") => "PremiumYearly",
|
||||||
|
var id when id.Contains("basic") => "Basic",
|
||||||
|
var id when id.Contains("professional") => "Professional",
|
||||||
|
var id when id.Contains("premium") => "Premium",
|
||||||
|
_ => "Trial"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
155
src/BCards.Web/Controllers/SubscriptionController.cs
Normal file
155
src/BCards.Web/Controllers/SubscriptionController.cs
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using BCards.Web.Services;
|
||||||
|
using BCards.Web.ViewModels;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace BCards.Web.Controllers;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[Route("subscription")]
|
||||||
|
public class SubscriptionController : Controller
|
||||||
|
{
|
||||||
|
private readonly IPaymentService _paymentService;
|
||||||
|
private readonly ILogger<SubscriptionController> _logger;
|
||||||
|
|
||||||
|
public SubscriptionController(
|
||||||
|
IPaymentService paymentService,
|
||||||
|
ILogger<SubscriptionController> logger)
|
||||||
|
{
|
||||||
|
_paymentService = paymentService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("cancel")]
|
||||||
|
public async Task<IActionResult> Cancel()
|
||||||
|
{
|
||||||
|
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
return RedirectToAction("Login", "Auth");
|
||||||
|
|
||||||
|
var subscription = await _paymentService.GetSubscriptionDetailsAsync(userId);
|
||||||
|
if (subscription == null)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Nenhuma assinatura ativa encontrada.";
|
||||||
|
return RedirectToAction("ManageSubscription", "Payment");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcular opções de reembolso
|
||||||
|
var (canRefundFull, canRefundPartial, refundAmount) = await _paymentService.CalculateRefundAsync(subscription.Id);
|
||||||
|
|
||||||
|
// Obter datas via SubscriptionItem
|
||||||
|
var subscriptionItemService = new Stripe.SubscriptionItemService();
|
||||||
|
var subItem = await subscriptionItemService.GetAsync(subscription.Items.Data[0].Id);
|
||||||
|
|
||||||
|
var viewModel = new CancelSubscriptionViewModel
|
||||||
|
{
|
||||||
|
SubscriptionId = subscription.Id,
|
||||||
|
PlanName = subscription.Items.Data.FirstOrDefault()?.Price.Nickname ?? "Plano Atual",
|
||||||
|
CurrentPeriodEnd = subItem.CurrentPeriodEnd,
|
||||||
|
CanRefundFull = canRefundFull,
|
||||||
|
CanRefundPartial = canRefundPartial,
|
||||||
|
RefundAmount = refundAmount,
|
||||||
|
DaysRemaining = (subItem.CurrentPeriodEnd - DateTime.UtcNow).Days
|
||||||
|
};
|
||||||
|
|
||||||
|
return View(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("cancel")]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> ProcessCancel(CancelSubscriptionRequest request)
|
||||||
|
{
|
||||||
|
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
return RedirectToAction("Login", "Auth");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool success = false;
|
||||||
|
string message = "";
|
||||||
|
|
||||||
|
switch (request.CancelType)
|
||||||
|
{
|
||||||
|
case "immediate_with_refund":
|
||||||
|
success = await _paymentService.CancelSubscriptionImmediatelyAsync(request.SubscriptionId, true);
|
||||||
|
message = success ? "Assinatura cancelada. Reembolso será processado manualmente em até 10 dias úteis." : "Erro ao processar cancelamento.";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "immediate_no_refund":
|
||||||
|
success = await _paymentService.CancelSubscriptionImmediatelyAsync(request.SubscriptionId, false);
|
||||||
|
message = success ? "Assinatura cancelada imediatamente." : "Erro ao cancelar assinatura.";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "partial_refund":
|
||||||
|
success = await _paymentService.CancelSubscriptionImmediatelyAsync(request.SubscriptionId, false);
|
||||||
|
var (_, canRefundPartial, refundAmount) = await _paymentService.CalculateRefundAsync(request.SubscriptionId);
|
||||||
|
if (success && canRefundPartial && refundAmount > 0)
|
||||||
|
{
|
||||||
|
message = $"Assinatura cancelada. Reembolso parcial de R$ {refundAmount:F2} será processado manualmente em até 10 dias úteis.";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
message = success ? "Assinatura cancelada. Reembolso parcial não disponível." : "Erro ao cancelar assinatura.";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "at_period_end":
|
||||||
|
default:
|
||||||
|
success = await _paymentService.CancelSubscriptionAtPeriodEndAsync(request.SubscriptionId);
|
||||||
|
message = success ? "Assinatura será cancelada no final do período atual." : "Erro ao agendar cancelamento.";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
TempData["Success"] = message;
|
||||||
|
_logger.LogInformation($"User {userId} cancelled subscription {request.SubscriptionId} with type {request.CancelType}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TempData["Error"] = message;
|
||||||
|
_logger.LogError($"Failed to cancel subscription {request.SubscriptionId} for user {userId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Ocorreu um erro ao processar o cancelamento. Tente novamente.";
|
||||||
|
_logger.LogError(ex, $"Error cancelling subscription {request.SubscriptionId} for user {userId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction("ManageSubscription", "Payment");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("reactivate")]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> Reactivate(string subscriptionId)
|
||||||
|
{
|
||||||
|
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
return RedirectToAction("Login", "Auth");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Remover agendamento de cancelamento
|
||||||
|
var success = await _paymentService.CancelSubscriptionAtPeriodEndAsync(subscriptionId);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
TempData["Success"] = "Assinatura reativada com sucesso!";
|
||||||
|
_logger.LogInformation($"User {userId} reactivated subscription {subscriptionId}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Erro ao reativar assinatura.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Ocorreu um erro ao reativar a assinatura.";
|
||||||
|
_logger.LogError(ex, $"Error reactivating subscription {subscriptionId} for user {userId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction("ManageSubscription", "Payment");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,4 +17,10 @@ public interface IPaymentService
|
|||||||
Task<Stripe.Subscription?> GetSubscriptionDetailsAsync(string userId);
|
Task<Stripe.Subscription?> GetSubscriptionDetailsAsync(string userId);
|
||||||
Task<List<Invoice>> GetPaymentHistoryAsync(string userId);
|
Task<List<Invoice>> GetPaymentHistoryAsync(string userId);
|
||||||
Task<string> CreatePortalSessionAsync(string customerId, string returnUrl);
|
Task<string> CreatePortalSessionAsync(string customerId, string returnUrl);
|
||||||
|
|
||||||
|
// Métodos para cancelamento com diferentes políticas
|
||||||
|
Task<bool> CancelSubscriptionImmediatelyAsync(string subscriptionId, bool refund = false);
|
||||||
|
Task<bool> CancelSubscriptionAtPeriodEndAsync(string subscriptionId);
|
||||||
|
Task<Refund> CreatePartialRefundAsync(string subscriptionId, decimal refundAmount);
|
||||||
|
Task<(bool CanRefundFull, bool CanRefundPartial, decimal RefundAmount)> CalculateRefundAsync(string subscriptionId);
|
||||||
}
|
}
|
||||||
@ -396,6 +396,145 @@ public class PaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Métodos de cancelamento com diferentes políticas
|
||||||
|
public async Task<bool> CancelSubscriptionImmediatelyAsync(string subscriptionId, bool refund = false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var service = new SubscriptionService();
|
||||||
|
|
||||||
|
if (refund)
|
||||||
|
{
|
||||||
|
// Para reembolso completo, apenas cancela - reembolso deve ser feito manualmente via Stripe Dashboard
|
||||||
|
await service.CancelAsync(subscriptionId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await service.CancelAsync(subscriptionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atualizar subscription local
|
||||||
|
var localSubscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId);
|
||||||
|
if (localSubscription != null)
|
||||||
|
{
|
||||||
|
localSubscription.Status = "cancelled";
|
||||||
|
localSubscription.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _subscriptionRepository.UpdateAsync(localSubscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (StripeException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CancelSubscriptionAtPeriodEndAsync(string subscriptionId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var service = new SubscriptionService();
|
||||||
|
var options = new SubscriptionUpdateOptions
|
||||||
|
{
|
||||||
|
CancelAtPeriodEnd = true
|
||||||
|
};
|
||||||
|
|
||||||
|
await service.UpdateAsync(subscriptionId, options);
|
||||||
|
|
||||||
|
// Atualizar subscription local
|
||||||
|
var localSubscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId);
|
||||||
|
if (localSubscription != null)
|
||||||
|
{
|
||||||
|
localSubscription.CancelAtPeriodEnd = true;
|
||||||
|
localSubscription.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _subscriptionRepository.UpdateAsync(localSubscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (StripeException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Refund> CreatePartialRefundAsync(string subscriptionId, decimal refundAmount)
|
||||||
|
{
|
||||||
|
// NOTA: Para planos de assinatura, reembolsos devem ser processados manualmente via Stripe Dashboard
|
||||||
|
// Este método retorna null para indicar que o reembolso precisa ser feito manualmente
|
||||||
|
await Task.CompletedTask;
|
||||||
|
throw new InvalidOperationException("Reembolsos parciais para assinaturas devem ser processados manualmente via Stripe Dashboard ou Portal de Cobrança");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(bool CanRefundFull, bool CanRefundPartial, decimal RefundAmount)> CalculateRefundAsync(string subscriptionId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var service = new SubscriptionService();
|
||||||
|
var subscription = await service.GetAsync(subscriptionId);
|
||||||
|
|
||||||
|
// Obter datas via SubscriptionItem
|
||||||
|
var subscriptionItemService = new SubscriptionItemService();
|
||||||
|
var subItem = await subscriptionItemService.GetAsync(subscription.Items.Data[0].Id);
|
||||||
|
|
||||||
|
var daysSinceStart = (DateTime.UtcNow - subItem.CurrentPeriodStart).TotalDays;
|
||||||
|
var totalDays = (subItem.CurrentPeriodEnd - subItem.CurrentPeriodStart).TotalDays;
|
||||||
|
var daysRemaining = totalDays - daysSinceStart;
|
||||||
|
|
||||||
|
// Direito de arrependimento (7 dias)
|
||||||
|
bool canRefundFull = daysSinceStart <= 7;
|
||||||
|
|
||||||
|
// Reembolso proporcional para planos anuais
|
||||||
|
bool canRefundPartial = false;
|
||||||
|
decimal refundAmount = 0;
|
||||||
|
|
||||||
|
if (!canRefundFull && daysRemaining > 0)
|
||||||
|
{
|
||||||
|
// Verificar se é plano anual (pela duração do período)
|
||||||
|
bool isYearlyPlan = totalDays > 300; // Aproximadamente 1 ano
|
||||||
|
|
||||||
|
if (isYearlyPlan)
|
||||||
|
{
|
||||||
|
canRefundPartial = true;
|
||||||
|
|
||||||
|
// Buscar valor pago na última fatura
|
||||||
|
var invoiceService = new InvoiceService();
|
||||||
|
var invoices = await invoiceService.ListAsync(new InvoiceListOptions
|
||||||
|
{
|
||||||
|
Subscription = subscriptionId,
|
||||||
|
Status = "paid",
|
||||||
|
Limit = 1
|
||||||
|
});
|
||||||
|
|
||||||
|
if (invoices.Data.Any())
|
||||||
|
{
|
||||||
|
var latestInvoice = invoices.Data.First();
|
||||||
|
var totalPaid = (decimal)latestInvoice.AmountPaid / 100; // Converter de centavos
|
||||||
|
|
||||||
|
// Calcular proporção do tempo restante
|
||||||
|
var proportionRemaining = daysRemaining / totalDays;
|
||||||
|
refundAmount = totalPaid * (decimal)proportionRemaining;
|
||||||
|
refundAmount = Math.Round(refundAmount, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (canRefundFull, canRefundPartial, refundAmount);
|
||||||
|
}
|
||||||
|
catch (StripeException)
|
||||||
|
{
|
||||||
|
return (false, false, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Refund?> CreateFullRefundAsync(string? chargeId)
|
||||||
|
{
|
||||||
|
// NOTA: Para planos de assinatura, reembolsos devem ser processados manualmente
|
||||||
|
await Task.CompletedTask;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private string GetSubscriptionId(Invoice? invoice)
|
private string GetSubscriptionId(Invoice? invoice)
|
||||||
{
|
{
|
||||||
if (invoice!=null)
|
if (invoice!=null)
|
||||||
|
|||||||
18
src/BCards.Web/ViewModels/CancelSubscriptionViewModel.cs
Normal file
18
src/BCards.Web/ViewModels/CancelSubscriptionViewModel.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
namespace BCards.Web.ViewModels;
|
||||||
|
|
||||||
|
public class CancelSubscriptionViewModel
|
||||||
|
{
|
||||||
|
public string SubscriptionId { get; set; } = string.Empty;
|
||||||
|
public string PlanName { get; set; } = string.Empty;
|
||||||
|
public DateTime CurrentPeriodEnd { get; set; }
|
||||||
|
public bool CanRefundFull { get; set; }
|
||||||
|
public bool CanRefundPartial { get; set; }
|
||||||
|
public decimal RefundAmount { get; set; }
|
||||||
|
public int DaysRemaining { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CancelSubscriptionRequest
|
||||||
|
{
|
||||||
|
public string SubscriptionId { get; set; } = string.Empty;
|
||||||
|
public string CancelType { get; set; } = "at_period_end"; // immediate_with_refund, immediate_no_refund, partial_refund, at_period_end
|
||||||
|
}
|
||||||
@ -18,6 +18,7 @@ public class ManageSubscriptionViewModel
|
|||||||
public bool CanUpgrade => HasActiveSubscription && User.CurrentPlan != "premium";
|
public bool CanUpgrade => HasActiveSubscription && User.CurrentPlan != "premium";
|
||||||
public bool CanDowngrade => HasActiveSubscription && User.CurrentPlan != "basic";
|
public bool CanDowngrade => HasActiveSubscription && User.CurrentPlan != "basic";
|
||||||
public bool WillCancelAtPeriodEnd => StripeSubscription?.CancelAtPeriodEnd == true;
|
public bool WillCancelAtPeriodEnd => StripeSubscription?.CancelAtPeriodEnd == true;
|
||||||
|
public string? StripeSubscriptionId => StripeSubscription?.Id;
|
||||||
|
|
||||||
public DateTime? CurrentPeriodEnd { get; set; }
|
public DateTime? CurrentPeriodEnd { get; set; }
|
||||||
public DateTime? NextBillingDate => !WillCancelAtPeriodEnd ? CurrentPeriodEnd : null;
|
public DateTime? NextBillingDate => !WillCancelAtPeriodEnd ? CurrentPeriodEnd : null;
|
||||||
|
|||||||
@ -9,6 +9,20 @@
|
|||||||
<div class="text-center mb-5">
|
<div class="text-center mb-5">
|
||||||
<h1 class="display-5 fw-bold mb-3">Escolha o plano ideal para você</h1>
|
<h1 class="display-5 fw-bold mb-3">Escolha o plano ideal para você</h1>
|
||||||
<p class="lead text-muted">Comece grátis e faça upgrade quando precisar de mais recursos</p>
|
<p class="lead text-muted">Comece grátis e faça upgrade quando precisar de mais recursos</p>
|
||||||
|
|
||||||
|
<!-- Toggle Mensal/Anual -->
|
||||||
|
<div class="d-flex justify-content-center mb-4">
|
||||||
|
<div class="btn-group" role="group" aria-label="Período de cobrança">
|
||||||
|
<input type="radio" class="btn-check" name="billingPeriod" id="monthly" autocomplete="off" checked>
|
||||||
|
<label class="btn btn-outline-primary" for="monthly">Mensal</label>
|
||||||
|
|
||||||
|
<input type="radio" class="btn-check" name="billingPeriod" id="yearly" autocomplete="off">
|
||||||
|
<label class="btn btn-outline-primary" for="yearly">
|
||||||
|
Anual
|
||||||
|
<span class="badge bg-success ms-1">2 meses grátis</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-4 justify-content-center">
|
<div class="row g-4 justify-content-center">
|
||||||
@ -65,8 +79,16 @@
|
|||||||
<div class="card-header bg-light text-center py-4">
|
<div class="card-header bg-light text-center py-4">
|
||||||
<h4 class="mb-0">Básico</h4>
|
<h4 class="mb-0">Básico</h4>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<span class="display-4 fw-bold text-primary">R$ 9,90</span>
|
<div class="pricing-monthly">
|
||||||
<span class="text-muted">/mês</span>
|
<span class="display-4 fw-bold text-primary">R$ 9,90</span>
|
||||||
|
<span class="text-muted">/mês</span>
|
||||||
|
</div>
|
||||||
|
<div class="pricing-yearly d-none">
|
||||||
|
<span class="display-4 fw-bold text-primary">R$ 99,00</span>
|
||||||
|
<span class="text-muted">/ano</span>
|
||||||
|
<br>
|
||||||
|
<small class="text-success">Economize R$ 19,80 (2 meses grátis)</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
@ -100,10 +122,18 @@
|
|||||||
<div class="card-footer bg-transparent p-4">
|
<div class="card-footer bg-transparent p-4">
|
||||||
@if (User.Identity?.IsAuthenticated == true)
|
@if (User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
<form asp-controller="Payment" asp-action="CreateCheckoutSession" method="post">
|
<div class="pricing-monthly">
|
||||||
<input type="hidden" name="planType" value="Basic" />
|
<form asp-controller="Payment" asp-action="CreateCheckoutSession" method="post">
|
||||||
<button type="submit" class="btn btn-outline-primary w-100">Escolher Básico</button>
|
<input type="hidden" name="planType" value="Basic" />
|
||||||
</form>
|
<button type="submit" class="btn btn-outline-primary w-100">Escolher Básico</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="pricing-yearly d-none">
|
||||||
|
<form asp-controller="Payment" asp-action="CreateCheckoutSession" method="post">
|
||||||
|
<input type="hidden" name="planType" value="BasicYearly" />
|
||||||
|
<button type="submit" class="btn btn-outline-primary w-100">Escolher Básico Anual</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -119,8 +149,16 @@
|
|||||||
<div class="card-header bg-warning bg-opacity-10 text-center py-4">
|
<div class="card-header bg-warning bg-opacity-10 text-center py-4">
|
||||||
<h4 class="mb-0">Profissional</h4>
|
<h4 class="mb-0">Profissional</h4>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<span class="display-4 fw-bold text-warning">R$ 24,90</span>
|
<div class="pricing-monthly">
|
||||||
<span class="text-muted">/mês</span>
|
<span class="display-4 fw-bold text-warning">R$ 24,90</span>
|
||||||
|
<span class="text-muted">/mês</span>
|
||||||
|
</div>
|
||||||
|
<div class="pricing-yearly d-none">
|
||||||
|
<span class="display-4 fw-bold text-warning">R$ 249,00</span>
|
||||||
|
<span class="text-muted">/ano</span>
|
||||||
|
<br>
|
||||||
|
<small class="text-success">Economize R$ 49,80 (2 meses grátis)</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
@ -139,7 +177,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-success me-2">✓</i>
|
<i class="text-success me-2">✓</i>
|
||||||
Domínio personalizado
|
Suporte por email
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-muted me-2">✗</i>
|
<i class="text-muted me-2">✗</i>
|
||||||
@ -154,10 +192,18 @@
|
|||||||
<div class="card-footer bg-transparent p-4">
|
<div class="card-footer bg-transparent p-4">
|
||||||
@if (User.Identity?.IsAuthenticated == true)
|
@if (User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
<form asp-controller="Payment" asp-action="CreateCheckoutSession" method="post">
|
<div class="pricing-monthly">
|
||||||
<input type="hidden" name="planType" value="Professional" />
|
<form asp-controller="Payment" asp-action="CreateCheckoutSession" method="post">
|
||||||
<button type="submit" class="btn btn-warning w-100">Escolher Profissional</button>
|
<input type="hidden" name="planType" value="Professional" />
|
||||||
</form>
|
<button type="submit" class="btn btn-warning w-100">Escolher Profissional</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="pricing-yearly d-none">
|
||||||
|
<form asp-controller="Payment" asp-action="CreateCheckoutSession" method="post">
|
||||||
|
<input type="hidden" name="planType" value="ProfessionalYearly" />
|
||||||
|
<button type="submit" class="btn btn-warning w-100">Escolher Profissional Anual</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -176,8 +222,16 @@
|
|||||||
<div class="card-header bg-primary text-white text-center py-4">
|
<div class="card-header bg-primary text-white text-center py-4">
|
||||||
<h4 class="mb-0">Premium</h4>
|
<h4 class="mb-0">Premium</h4>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<span class="display-4 fw-bold">R$ 29,90</span>
|
<div class="pricing-monthly">
|
||||||
<span class="opacity-75">/mês</span>
|
<span class="display-4 fw-bold">R$ 29,90</span>
|
||||||
|
<span class="opacity-75">/mês</span>
|
||||||
|
</div>
|
||||||
|
<div class="pricing-yearly d-none">
|
||||||
|
<span class="display-4 fw-bold">R$ 299,00</span>
|
||||||
|
<span class="opacity-75">/ano</span>
|
||||||
|
<br>
|
||||||
|
<small class="text-light">Economize R$ 59,80 (2 meses grátis)</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<small class="opacity-75">Melhor custo-benefício!</small>
|
<small class="opacity-75">Melhor custo-benefício!</small>
|
||||||
</div>
|
</div>
|
||||||
@ -197,7 +251,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-success me-2">✓</i>
|
<i class="text-success me-2">✓</i>
|
||||||
Múltiplos domínios
|
Backup automático
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-success me-2">✓</i>
|
<i class="text-success me-2">✓</i>
|
||||||
@ -212,10 +266,18 @@
|
|||||||
<div class="card-footer bg-transparent p-4">
|
<div class="card-footer bg-transparent p-4">
|
||||||
@if (User.Identity?.IsAuthenticated == true)
|
@if (User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
<form asp-controller="Payment" asp-action="CreateCheckoutSession" method="post">
|
<div class="pricing-monthly">
|
||||||
<input type="hidden" name="planType" value="Premium" />
|
<form asp-controller="Payment" asp-action="CreateCheckoutSession" method="post">
|
||||||
<button type="submit" class="btn btn-primary w-100 fw-bold">Escolher Premium</button>
|
<input type="hidden" name="planType" value="Premium" />
|
||||||
</form>
|
<button type="submit" class="btn btn-primary w-100 fw-bold">Escolher Premium</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="pricing-yearly d-none">
|
||||||
|
<form asp-controller="Payment" asp-action="CreateCheckoutSession" method="post">
|
||||||
|
<input type="hidden" name="planType" value="PremiumYearly" />
|
||||||
|
<button type="submit" class="btn btn-primary w-100 fw-bold">Escolher Premium Anual</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -270,14 +332,14 @@
|
|||||||
<td class="text-center"><strong>Completo</strong></td>
|
<td class="text-center"><strong>Completo</strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Domínio personalizado</td>
|
<td>Suporte por email</td>
|
||||||
<td class="text-center">❌</td>
|
<td class="text-center">❌</td>
|
||||||
<td class="text-center">❌</td>
|
<td class="text-center">❌</td>
|
||||||
<td class="text-center">✅</td>
|
<td class="text-center">✅</td>
|
||||||
<td class="text-center">✅</td>
|
<td class="text-center">✅</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Múltiplos domínios</td>
|
<td>Backup automático</td>
|
||||||
<td class="text-center">❌</td>
|
<td class="text-center">❌</td>
|
||||||
<td class="text-center">❌</td>
|
<td class="text-center">❌</td>
|
||||||
<td class="text-center">❌</td>
|
<td class="text-center">❌</td>
|
||||||
@ -317,12 +379,12 @@
|
|||||||
<div class="accordion-item">
|
<div class="accordion-item">
|
||||||
<h3 class="accordion-header" id="faq2">
|
<h3 class="accordion-header" id="faq2">
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse2">
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse2">
|
||||||
Como funciona o domínio personalizado?
|
Como funcionam os planos anuais?
|
||||||
</button>
|
</button>
|
||||||
</h3>
|
</h3>
|
||||||
<div id="collapse2" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
|
<div id="collapse2" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
Com os planos Profissional e Premium, você pode conectar seu próprio domínio (ex: meusite.com) à sua página BCards.
|
Nos planos anuais você economiza 2 meses! Pague 10 meses e use por 12 meses. O desconto é aplicado automaticamente na cobrança anual.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -392,3 +454,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const monthlyRadio = document.getElementById('monthly');
|
||||||
|
const yearlyRadio = document.getElementById('yearly');
|
||||||
|
const monthlyElements = document.querySelectorAll('.pricing-monthly');
|
||||||
|
const yearlyElements = document.querySelectorAll('.pricing-yearly');
|
||||||
|
|
||||||
|
function togglePricing() {
|
||||||
|
if (yearlyRadio.checked) {
|
||||||
|
monthlyElements.forEach(el => el.classList.add('d-none'));
|
||||||
|
yearlyElements.forEach(el => el.classList.remove('d-none'));
|
||||||
|
} else {
|
||||||
|
monthlyElements.forEach(el => el.classList.remove('d-none'));
|
||||||
|
yearlyElements.forEach(el => el.classList.add('d-none'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
monthlyRadio.addEventListener('change', togglePricing);
|
||||||
|
yearlyRadio.addEventListener('change', togglePricing);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -103,10 +103,20 @@
|
|||||||
<div class="d-flex gap-2 flex-wrap">
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
@if (!Model.WillCancelAtPeriodEnd)
|
@if (!Model.WillCancelAtPeriodEnd)
|
||||||
{
|
{
|
||||||
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#cancelModal">
|
<a asp-controller="Subscription" asp-action="Cancel" class="btn btn-danger">
|
||||||
<i class="fas fa-times me-1"></i>
|
<i class="fas fa-times me-1"></i>
|
||||||
Cancelar Assinatura
|
Cancelar Assinatura
|
||||||
</button>
|
</a>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<form asp-controller="Subscription" asp-action="Reactivate" method="post" class="d-inline">
|
||||||
|
<input type="hidden" name="subscriptionId" value="@Model.StripeSubscriptionId" />
|
||||||
|
<button type="submit" class="btn btn-success" onclick="return confirm('Deseja reativar sua assinatura?')">
|
||||||
|
<i class="fas fa-undo me-1"></i>
|
||||||
|
Reativar Assinatura
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
}
|
}
|
||||||
|
|
||||||
<form method="post" action="@Url.Action("OpenStripePortal")" class="d-inline">
|
<form method="post" action="@Url.Action("OpenStripePortal")" class="d-inline">
|
||||||
|
|||||||
167
src/BCards.Web/Views/Subscription/Cancel.cshtml
Normal file
167
src/BCards.Web/Views/Subscription/Cancel.cshtml
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
@model BCards.Web.ViewModels.CancelSubscriptionViewModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Cancelar Assinatura";
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-warning text-dark">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||||
|
Cancelar Assinatura
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h5>Informações da Assinatura</h5>
|
||||||
|
<p><strong>Plano:</strong> @Model.PlanName</p>
|
||||||
|
<p><strong>Válido até:</strong> @Model.CurrentPeriodEnd.ToString("dd/MM/yyyy")</p>
|
||||||
|
<p><strong>Dias restantes:</strong> @Model.DaysRemaining dias</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mb-3">Escolha uma opção de cancelamento:</h5>
|
||||||
|
|
||||||
|
<form asp-action="ProcessCancel" method="post">
|
||||||
|
<input type="hidden" name="SubscriptionId" value="@Model.SubscriptionId" />
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<!-- Reembolso Total (7 dias) -->
|
||||||
|
@if (Model.CanRefundFull)
|
||||||
|
{
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-success">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="CancelType" value="immediate_with_refund" id="refundFull">
|
||||||
|
<label class="form-check-label" for="refundFull">
|
||||||
|
<strong class="text-success">Cancelar com Reembolso Total</strong>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">
|
||||||
|
Direito de arrependimento (7 dias). Cancela imediatamente e processa reembolso total.
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Reembolso Parcial (Planos Anuais) -->
|
||||||
|
@if (Model.CanRefundPartial && Model.RefundAmount > 0)
|
||||||
|
{
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-primary">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="CancelType" value="partial_refund" id="refundPartial">
|
||||||
|
<label class="form-check-label" for="refundPartial">
|
||||||
|
<strong class="text-primary">Cancelar com Reembolso Parcial</strong>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">
|
||||||
|
Cancela imediatamente e reembolsa R$ @Model.RefundAmount.ToString("F2") (proporcional ao tempo restante).
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Cancelar no Final do Período -->
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-warning">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="CancelType" value="at_period_end" id="cancelAtEnd" checked>
|
||||||
|
<label class="form-check-label" for="cancelAtEnd">
|
||||||
|
<strong class="text-warning">Cancelar no Final do Período</strong>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">
|
||||||
|
Mantém acesso até @Model.CurrentPeriodEnd.ToString("dd/MM/yyyy"). Sem reembolso.
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cancelar Imediatamente (sem reembolso) -->
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-danger">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="CancelType" value="immediate_no_refund" id="cancelImmediate">
|
||||||
|
<label class="form-check-label" for="cancelImmediate">
|
||||||
|
<strong class="text-danger">Cancelar Imediatamente</strong>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">
|
||||||
|
Perde acesso imediato aos recursos premium. Sem reembolso.
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<h6><i class="fas fa-info-circle me-2"></i>Importante:</h6>
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>Reembolsos são processados em até 10 dias úteis</li>
|
||||||
|
<li>O valor retorna para o mesmo cartão usado na compra</li>
|
||||||
|
<li>Após o cancelamento, suas páginas podem ser desativadas conforme o plano escolhido</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mt-4">
|
||||||
|
<a asp-controller="Payment" asp-action="ManageSubscription" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>Voltar
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-danger" onclick="return confirm('Tem certeza que deseja cancelar sua assinatura?')">
|
||||||
|
<i class="fas fa-times me-2"></i>Confirmar Cancelamento
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const radioButtons = document.querySelectorAll('input[name="CancelType"]');
|
||||||
|
const submitButton = document.querySelector('button[type="submit"]');
|
||||||
|
|
||||||
|
radioButtons.forEach(radio => {
|
||||||
|
radio.addEventListener('change', function() {
|
||||||
|
const selectedOption = this.value;
|
||||||
|
let confirmMessage = 'Tem certeza que deseja cancelar sua assinatura?';
|
||||||
|
|
||||||
|
switch(selectedOption) {
|
||||||
|
case 'immediate_with_refund':
|
||||||
|
confirmMessage = 'Confirma o cancelamento com reembolso total?';
|
||||||
|
break;
|
||||||
|
case 'partial_refund':
|
||||||
|
confirmMessage = 'Confirma o cancelamento com reembolso parcial de R$ @Model.RefundAmount.ToString("F2")?';
|
||||||
|
break;
|
||||||
|
case 'at_period_end':
|
||||||
|
confirmMessage = 'Confirma o agendamento de cancelamento para @Model.CurrentPeriodEnd.ToString("dd/MM/yyyy")?';
|
||||||
|
break;
|
||||||
|
case 'immediate_no_refund':
|
||||||
|
confirmMessage = 'Confirma o cancelamento imediato? Você perderá acesso aos recursos premium agora.';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitButton.onclick = function() {
|
||||||
|
return confirm(confirmMessage);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -42,12 +42,150 @@
|
|||||||
</style>
|
</style>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (!isPreview)
|
||||||
|
{
|
||||||
|
<style>
|
||||||
|
/* Layout normal sem preview - corrigir centralização */
|
||||||
|
.user-page {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
min-height: 100vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-page .container {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 1140px !important;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-page .row {
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-page .col-lg-6,
|
||||||
|
.user-page .col-md-8 {
|
||||||
|
display: block !important;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
float: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-card {
|
||||||
|
margin: 0 auto !important;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
}
|
||||||
|
|
||||||
@if (isPreview)
|
@if (isPreview)
|
||||||
{
|
{
|
||||||
<style>
|
<style>
|
||||||
/* Compensar espaço da barra de preview */
|
/* Compensar espaço da barra de preview */
|
||||||
body {
|
body {
|
||||||
padding-top: 60px !important;
|
padding-top: 60px !important;
|
||||||
|
display: block !important; /* Override flexbox for user pages */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Corrigir centralização do conteúdo da página */
|
||||||
|
.user-page {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
min-height: calc(100vh - 60px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-page .container {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 1140px !important;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-page .row {
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-page .col-lg-6,
|
||||||
|
.user-page .col-md-8 {
|
||||||
|
display: block !important;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
float: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-card {
|
||||||
|
margin: 0 auto !important;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Garantir que a barra de preview funcione corretamente */
|
||||||
|
.position-fixed.top-0 {
|
||||||
|
display: block !important;
|
||||||
|
flex: none !important;
|
||||||
|
height: 60px !important;
|
||||||
|
line-height: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-fixed.top-0 .container-fluid {
|
||||||
|
display: block !important;
|
||||||
|
flex: none !important;
|
||||||
|
padding: 0.75rem 1rem !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-fixed.top-0 .container-fluid .row {
|
||||||
|
display: flex !important;
|
||||||
|
flex-wrap: nowrap !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
min-height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-fixed.top-0 .container-fluid .row .col-auto {
|
||||||
|
flex: 0 0 auto !important;
|
||||||
|
padding: 0 0.25rem !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-fixed.top-0 .container-fluid .row .col {
|
||||||
|
flex: 1 1 auto !important;
|
||||||
|
padding: 0 0.5rem !important;
|
||||||
|
text-align: center !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Garantir que os botões fiquem na mesma linha */
|
||||||
|
.position-fixed.top-0 .btn {
|
||||||
|
margin: 0 0.25rem !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
padding: 0.25rem 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forçar layout horizontal para a barra toda */
|
||||||
|
.position-fixed.top-0 .container-fluid .row {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Para telas grandes (Full HD+) */
|
||||||
|
@@media (min-width: 1200px) {
|
||||||
|
.position-fixed.top-0 .container-fluid {
|
||||||
|
max-width: 100% !important;
|
||||||
|
padding: 0.5rem 2rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-fixed.top-0 .container-fluid .row .col-auto:last-child {
|
||||||
|
flex-shrink: 0 !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-fixed.top-0 .btn {
|
||||||
|
display: inline-block !important;
|
||||||
|
margin: 0 0.125rem !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsivo para mobile */
|
/* Responsivo para mobile */
|
||||||
|
|||||||
@ -61,13 +61,34 @@
|
|||||||
"PriceId": "price_1RjUv9BMIadsOxJVORqlM4E9",
|
"PriceId": "price_1RjUv9BMIadsOxJVORqlM4E9",
|
||||||
"Price": 24.90,
|
"Price": 24.90,
|
||||||
"MaxLinks": 15,
|
"MaxLinks": 15,
|
||||||
"Features": [ "all_themes", "advanced_analytics", "custom_domain" ]
|
"Features": [ "all_themes", "advanced_analytics" ]
|
||||||
},
|
},
|
||||||
"Premium": {
|
"Premium": {
|
||||||
"PriceId": "price_1RjUw0BMIadsOxJVmdouNV1g",
|
"PriceId": "price_1RjUw0BMIadsOxJVmdouNV1g",
|
||||||
"Price": 29.90,
|
"Price": 29.90,
|
||||||
"MaxLinks": -1,
|
"MaxLinks": -1,
|
||||||
"Features": [ "custom_themes", "full_analytics", "multiple_domains", "priority_support" ]
|
"Features": [ "custom_themes", "full_analytics", "priority_support" ]
|
||||||
|
},
|
||||||
|
"BasicYearly": {
|
||||||
|
"PriceId": "price_basic_yearly_placeholder",
|
||||||
|
"Price": 99.00,
|
||||||
|
"MaxLinks": 5,
|
||||||
|
"Features": [ "basic_themes", "simple_analytics" ],
|
||||||
|
"Interval": "year"
|
||||||
|
},
|
||||||
|
"ProfessionalYearly": {
|
||||||
|
"PriceId": "price_professional_yearly_placeholder",
|
||||||
|
"Price": 249.00,
|
||||||
|
"MaxLinks": 15,
|
||||||
|
"Features": [ "all_themes", "advanced_analytics" ],
|
||||||
|
"Interval": "year"
|
||||||
|
},
|
||||||
|
"PremiumYearly": {
|
||||||
|
"PriceId": "price_premium_yearly_placeholder",
|
||||||
|
"Price": 299.00,
|
||||||
|
"MaxLinks": -1,
|
||||||
|
"Features": [ "custom_themes", "full_analytics", "priority_support" ],
|
||||||
|
"Interval": "year"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Moderation": {
|
"Moderation": {
|
||||||
|
|||||||
@ -36,13 +36,34 @@
|
|||||||
"PriceId": "price_1RjUv9BMIadsOxJVORqlM4E9",
|
"PriceId": "price_1RjUv9BMIadsOxJVORqlM4E9",
|
||||||
"Price": 24.90,
|
"Price": 24.90,
|
||||||
"MaxLinks": 15,
|
"MaxLinks": 15,
|
||||||
"Features": [ "all_themes", "advanced_analytics", "custom_domain" ]
|
"Features": [ "all_themes", "advanced_analytics" ]
|
||||||
},
|
},
|
||||||
"Premium": {
|
"Premium": {
|
||||||
"PriceId": "price_1RjUw0BMIadsOxJVmdouNV1g",
|
"PriceId": "price_1RjUw0BMIadsOxJVmdouNV1g",
|
||||||
"Price": 29.90,
|
"Price": 29.90,
|
||||||
"MaxLinks": -1,
|
"MaxLinks": -1,
|
||||||
"Features": [ "custom_themes", "full_analytics", "multiple_domains", "priority_support" ]
|
"Features": [ "custom_themes", "full_analytics", "priority_support" ]
|
||||||
|
},
|
||||||
|
"BasicYearly": {
|
||||||
|
"PriceId": "price_basic_yearly_placeholder",
|
||||||
|
"Price": 99.00,
|
||||||
|
"MaxLinks": 5,
|
||||||
|
"Features": [ "basic_themes", "simple_analytics" ],
|
||||||
|
"Interval": "year"
|
||||||
|
},
|
||||||
|
"ProfessionalYearly": {
|
||||||
|
"PriceId": "price_professional_yearly_placeholder",
|
||||||
|
"Price": 249.00,
|
||||||
|
"MaxLinks": 15,
|
||||||
|
"Features": [ "all_themes", "advanced_analytics" ],
|
||||||
|
"Interval": "year"
|
||||||
|
},
|
||||||
|
"PremiumYearly": {
|
||||||
|
"PriceId": "price_premium_yearly_placeholder",
|
||||||
|
"Price": 299.00,
|
||||||
|
"MaxLinks": -1,
|
||||||
|
"Features": [ "custom_themes", "full_analytics", "priority_support" ],
|
||||||
|
"Interval": "year"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Moderation": {
|
"Moderation": {
|
||||||
|
|||||||
@ -13,21 +13,80 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
position: relative;
|
height: 100%;
|
||||||
min-height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin-bottom: 60px;
|
min-height: 100vh;
|
||||||
padding-top: 70px; /* Altura da navbar fixa */
|
padding-top: 70px; /* Altura da navbar fixa */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Garantir que a navbar não seja afetada pelo flexbox */
|
||||||
|
.navbar {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
z-index: 1030 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container principal com flexbox apenas para o conteúdo */
|
||||||
|
body > .container-fluid {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: calc(100vh - 70px); /* Altura total menos navbar */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Exceções para containers que não devem usar flexbox */
|
||||||
|
.position-fixed .container-fluid,
|
||||||
|
.navbar .container-fluid,
|
||||||
|
.user-page .container {
|
||||||
|
display: block !important;
|
||||||
|
flex: none !important;
|
||||||
|
min-height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Correção específica para a tarja de preview */
|
||||||
|
.position-fixed .container-fluid .row {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-fixed .container-fluid .row .col,
|
||||||
|
.position-fixed .container-fluid .row .col-auto {
|
||||||
|
display: block !important;
|
||||||
|
flex: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Garantir que páginas de usuário não sejam afetadas pelo flexbox global */
|
||||||
|
.user-page {
|
||||||
|
flex: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reset específico para user pages - deixar que o CSS inline controle */
|
||||||
|
.user-page .container,
|
||||||
|
.user-page .row,
|
||||||
|
.user-page .col,
|
||||||
|
.user-page .col-lg-6,
|
||||||
|
.user-page .col-md-8 {
|
||||||
|
/* CSS inline controlará o layout */
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
white-space: nowrap;
|
margin-top: auto;
|
||||||
line-height: 60px;
|
background-color: #f8f9fa;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
padding: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom Styles */
|
/* Custom Styles */
|
||||||
@ -146,6 +205,27 @@ body {
|
|||||||
.navbar {
|
.navbar {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
display: flex !important;
|
||||||
|
flex-wrap: nowrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Corrigir problemas de layout da navbar */
|
||||||
|
.navbar .container-fluid {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: row !important;
|
||||||
|
min-height: auto !important;
|
||||||
|
flex-wrap: nowrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: row !important;
|
||||||
|
flex-wrap: nowrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-collapse {
|
||||||
|
display: flex !important;
|
||||||
|
flex-basis: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link {
|
.nav-link {
|
||||||
@ -243,6 +323,27 @@ body {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
flex-direction: column !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav {
|
||||||
|
flex-direction: column !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-collapse {
|
||||||
|
flex-direction: column !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Para telas médias e grandes, manter horizontal */
|
||||||
|
@media (min-width: 577px) {
|
||||||
|
.navbar-nav {
|
||||||
|
flex-direction: row !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-collapse {
|
||||||
|
flex-direction: row !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user