feat: Sistema completo de cancelamento de assinatura v1.60.0

 Novo Sistema de Cancelamento com Retenção Inteligente
- Wizard de 5 etapas com mensagens emocionais personalizadas
- Oferta de 3 meses grátis para cancelamentos por preço (elegível se >= 3 meses pagos)
- Cumprimento legal: período de garantia 7 dias com reembolso total
- Rastreamento de ofertas em DB (impede reuso)
- Analytics de cancelamentos (motivo, feedback, within_guarantee)

🎨 Frontend
- CancellationWizard.jsx (1050+ linhas) com 5 steps
- Profile.jsx com nova seção de assinatura
- i18n completo em 3 idiomas (pt-BR, ES, EN)
- 40+ traduções específicas

🔧 Backend
- SubscriptionController: 4 novos métodos (eligibility, process, offer, execute)
- PayPalService: suspensão temporária de assinatura
- 2 novas tabelas: retention_offers, cancellations
- Email de retenção emotivo em Blade

⚖️ Legal
- 7 dias: cancelamento = reembolso total + acesso imediato termina
- Após 7 dias: cancelamento = acesso até fim do período, sem reembolso
- Grace period apropriado conforme período de garantia
This commit is contained in:
marco 2025-12-19 17:00:54 +01:00
parent f5d2c8df16
commit 2c883be168
14 changed files with 1595 additions and 4 deletions

View File

@ -5,6 +5,79 @@ O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/).
Este projeto adota [Versionamento Semântico](https://semver.org/pt-BR/). Este projeto adota [Versionamento Semântico](https://semver.org/pt-BR/).
## [1.60.0] - 2025-12-19
### Added
- 🚨 **Sistema Completo de Cancelamento de Assinatura** - Solução legal e com retenção inteligente:
- **Wizard de 5 Etapas** com interface emocional e estratégica
- **Passo 1**: Seleção do motivo (preço, não usando, faltam recursos, mudando serviço, outro)
- **Passo 2**: Mensagem emocional personalizada por motivo com lista de benefícios
- **Passo 3**: Oferta de retenção (3 meses grátis) - SOMENTE se motivo = preço
- **Passo 4**: Feedback opcional do usuário (campo de texto livre)
- **Passo 5**: Confirmação final (digitar "CANCELAR" em maiúsculas)
- 🎁 **Oferta de Retenção Inteligente**:
- 3 meses GRÁTIS se motivo = preço
- Elegível apenas se: `monthsPaid >= 3` E nunca usou oferta antes
- Suspende assinatura PayPal por 3 meses (não cancela)
- Uma única vez por usuário (rastreado em DB)
- Card visual destacado com gradiente verde e emojis
- ⚖️ **Cumprimento Legal**:
- Período de garantia de 7 dias: cancelamento = reembolso total imediato
- Após 7 dias: acesso até final do período, sem reembolso
- Integração completa com PayPal para cancelamento/suspensão
- Rastreamento de todos os cancelamentos para analytics
- 📧 **Email de Retenção**:
- Template HTML emotivo enviado ao aceitar oferta
- Design com gradiente verde, emojis, lista de benefícios
- Mensagem sobre importância do usuário
- 🎨 **Integração em Profile**:
- Nova seção "Sua Assinatura" mostrando plano ativo
- Preço, data de renovação, link PayPal
- Botão "Cancelar Assinatura" destacado
- Badge de período de garantia (primeiros 7 dias)
### Backend
- `SubscriptionController` expandido com 4 novos métodos:
- `cancellationEligibility()` - Verifica elegibilidade para oferta 3 meses
- `processCancellation()` - Processa cancelamento ou aplica oferta
- `applyRetentionOffer()` - Aplica 3 meses grátis e suspende PayPal
- `executeCancellation()` - Cancela com grace period apropriado
- `PayPalService::suspendSubscription()` - Suspende por X meses
- `CancellationRetentionOfferMail` - Email emotivo de retenção
- Template Blade `cancellation-retention-offer.blade.php`
- 2 novas tabelas:
- `subscription_retention_offers` - Rastreia ofertas (impede reuso)
- `subscription_cancellations` - Analytics de cancelamentos
### Frontend
- `CancellationWizard.jsx` (1050+ linhas):
- 5 steps com navegação sequencial
- Mensagens emocionais específicas por razão
- Validações (confirmação, elegibilidade)
- Integração completa com i18n (pt-BR, ES, EN)
- `Profile.jsx` - Nova seção de assinatura
- `api.js` - `subscriptionService` com métodos de cancelamento
- 40+ traduções em 3 idiomas (pt-BR, ES, EN)
### Database
- Migration `create_subscription_retention_offers_table`:
- Campos: user_id, subscription_id, offer_type, status, reason
- Índices para performance
- Migration `create_subscription_cancellations_table`:
- Campos: reason, feedback, within_guarantee_period, refund_amount
- Analytics de churn
### UX/UI
- Cards de razão com ícones coloridos e hover effects
- Mensagens emocionais com emojis estratégicos 💙🌟🚀❤️🤝
- Lista visual de benefícios que serão perdidos
- Card de oferta com destaque visual (gradiente verde)
- Confirmação final com campo de texto (fricção intencional)
## [1.59.0] - 2025-12-19 ## [1.59.0] - 2025-12-19
### Added ### Added

View File

@ -1 +1 @@
1.59.0 1.60.0

View File

@ -12,8 +12,11 @@
use App\Models\EmailVerificationToken; use App\Models\EmailVerificationToken;
use App\Mail\AccountActivationMail; use App\Mail\AccountActivationMail;
use App\Mail\SubscriptionCancelledMail; use App\Mail\SubscriptionCancelledMail;
use App\Mail\CancellationRetentionOfferMail;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Carbon\Carbon; use Carbon\Carbon;
class SubscriptionController extends Controller class SubscriptionController extends Controller
@ -954,4 +957,307 @@ private function updateSubscriptionFromPayPal(Subscription $subscription, array
$subscription->save(); $subscription->save();
} }
// ==================== CANCELLATION WIZARD ====================
/**
* Get cancellation eligibility and options
*/
public function cancellationEligibility(Request $request): JsonResponse
{
$user = $request->user();
$subscription = $user->subscriptions()->active()->with('plan')->first();
if (!$subscription) {
return response()->json([
'success' => false,
'message' => 'No active subscription found',
], 404);
}
// Calculate subscription age (months paid)
$subscriptionAge = $subscription->created_at->diffInMonths(now());
$monthsPaid = max(0, $subscriptionAge);
// Check if eligible for 3-month free offer
$eligibleForFreeMonths = $monthsPaid >= 3;
// Check if already used the 3-month offer
$hasUsedFreeMonthsOffer = DB::table('subscription_retention_offers')
->where('user_id', $user->id)
->where('offer_type', 'free_3_months')
->where('status', 'accepted')
->exists();
// Calculate refund eligibility (7 days guarantee)
$withinGuaranteePeriod = $subscription->created_at->diffInDays(now()) <= 7;
$guaranteeDaysRemaining = max(0, 7 - $subscription->created_at->diffInDays(now()));
return response()->json([
'success' => true,
'data' => [
'subscription' => [
'id' => $subscription->id,
'plan_name' => $subscription->plan->name,
'price' => $subscription->plan->price,
'formatted_price' => $subscription->plan->formatted_price,
'billing_period' => $subscription->plan->billing_period,
'status' => $subscription->status,
'created_at' => $subscription->created_at->toIso8601String(),
'current_period_end' => $subscription->current_period_end?->toIso8601String(),
],
'subscription_age' => [
'months' => $monthsPaid,
'days' => $subscription->created_at->diffInDays(now()),
],
'retention_offer' => [
'eligible_for_free_months' => $eligibleForFreeMonths && !$hasUsedFreeMonthsOffer,
'has_used_offer' => $hasUsedFreeMonthsOffer,
'months_offered' => 3,
'reason_required' => 'price',
],
'guarantee' => [
'within_period' => $withinGuaranteePeriod,
'days_remaining' => $guaranteeDaysRemaining,
'refund_eligible' => $withinGuaranteePeriod,
],
],
]);
}
/**
* Process cancellation with retention attempt
*/
public function processCancellation(Request $request): JsonResponse
{
$request->validate([
'reason' => 'required|string|in:price,features,not_using,switching,other',
'feedback' => 'nullable|string|max:500',
'accept_retention_offer' => 'nullable|boolean',
]);
$user = $request->user();
$subscription = $user->subscriptions()->active()->with('plan')->first();
if (!$subscription) {
return response()->json([
'success' => false,
'message' => 'No active subscription found',
], 404);
}
$reason = $request->reason;
$acceptOffer = $request->boolean('accept_retention_offer', false);
// Check if user accepted retention offer (3 free months)
if ($acceptOffer && $reason === 'price') {
return $this->applyRetentionOffer($user, $subscription, $request->feedback);
}
// Process cancellation
return $this->executeCancellation($user, $subscription, $reason, $request->feedback);
}
/**
* Apply retention offer (3 free months)
*/
private function applyRetentionOffer($user, Subscription $subscription, $feedback): JsonResponse
{
$monthsPaid = $subscription->created_at->diffInMonths(now());
// Validate eligibility
if ($monthsPaid < 3) {
return response()->json([
'success' => false,
'message' => 'Not eligible for this offer. Need at least 3 months paid.',
], 400);
}
// Check if already used
$hasUsed = DB::table('subscription_retention_offers')
->where('user_id', $user->id)
->where('offer_type', 'free_3_months')
->where('status', 'accepted')
->exists();
if ($hasUsed) {
return response()->json([
'success' => false,
'message' => 'This offer can only be used once.',
], 400);
}
DB::beginTransaction();
try {
// Record the offer acceptance
DB::table('subscription_retention_offers')->insert([
'user_id' => $user->id,
'subscription_id' => $subscription->id,
'offer_type' => 'free_3_months',
'status' => 'accepted',
'original_cancel_reason' => 'price',
'user_feedback' => $feedback,
'offer_start_date' => now(),
'offer_end_date' => now()->addMonths(3),
'created_at' => now(),
'updated_at' => now(),
]);
// Extend subscription period by 3 months
$subscription->current_period_end = $subscription->current_period_end
? $subscription->current_period_end->addMonths(3)
: now()->addMonths(3);
$subscription->save();
// Cancel PayPal subscription for 3 months (pause)
if ($subscription->paypal_subscription_id) {
try {
$this->paypal->suspendSubscription($subscription->paypal_subscription_id);
} catch (\Exception $e) {
Log::warning('Failed to suspend PayPal subscription', [
'subscription_id' => $subscription->id,
'error' => $e->getMessage(),
]);
}
}
// Send confirmation email
try {
Mail::to($user->email)->send(new CancellationRetentionOfferMail(
$user,
$subscription,
3, // months
now()->addMonths(3)
));
} catch (\Exception $e) {
Log::warning('Failed to send retention offer email', [
'user_id' => $user->id,
'error' => $e->getMessage(),
]);
}
DB::commit();
return response()->json([
'success' => true,
'message' => '3 free months applied successfully!',
'data' => [
'subscription' => [
'id' => $subscription->id,
'status' => $subscription->status,
'current_period_end' => $subscription->current_period_end->toIso8601String(),
],
'offer' => [
'type' => 'free_3_months',
'end_date' => now()->addMonths(3)->toIso8601String(),
],
],
]);
} catch (\Exception $e) {
DB::rollBack();
Log::error('Failed to apply retention offer', [
'user_id' => $user->id,
'error' => $e->getMessage(),
]);
return response()->json([
'success' => false,
'message' => 'Failed to apply offer',
'error' => $e->getMessage(),
], 500);
}
}
/**
* Execute subscription cancellation
*/
private function executeCancellation($user, Subscription $subscription, $reason, $feedback): JsonResponse
{
DB::beginTransaction();
try {
$withinGuaranteePeriod = $subscription->created_at->diffInDays(now()) <= 7;
// Cancel on PayPal if exists
if ($subscription->paypal_subscription_id) {
try {
$this->paypal->cancelSubscription($subscription->paypal_subscription_id);
} catch (\Exception $e) {
Log::warning('Failed to cancel PayPal subscription', [
'subscription_id' => $subscription->id,
'error' => $e->getMessage(),
]);
}
}
// Update subscription
$subscription->status = Subscription::STATUS_CANCELED;
$subscription->canceled_at = now();
$subscription->ends_at = $withinGuaranteePeriod ? now() : $subscription->current_period_end;
$subscription->cancel_reason = $reason;
$subscription->save();
// Record cancellation
DB::table('subscription_cancellations')->insert([
'user_id' => $user->id,
'subscription_id' => $subscription->id,
'reason' => $reason,
'feedback' => $feedback,
'canceled_at' => now(),
'within_guarantee_period' => $withinGuaranteePeriod,
'refund_issued' => false, // Manual process
'created_at' => now(),
'updated_at' => now(),
]);
// Send cancellation email
try {
Mail::to($user->email)->send(new SubscriptionCancelledMail(
$user,
$subscription->plan,
$subscription->ends_at,
$withinGuaranteePeriod
));
} catch (\Exception $e) {
Log::warning('Failed to send cancellation email', [
'user_id' => $user->id,
'error' => $e->getMessage(),
]);
}
DB::commit();
return response()->json([
'success' => true,
'message' => 'Subscription cancelled successfully',
'data' => [
'subscription' => [
'id' => $subscription->id,
'status' => $subscription->status,
'canceled_at' => $subscription->canceled_at->toIso8601String(),
'ends_at' => $subscription->ends_at?->toIso8601String(),
'access_until' => $subscription->ends_at?->toIso8601String(),
],
'guarantee' => [
'within_period' => $withinGuaranteePeriod,
'refund_eligible' => $withinGuaranteePeriod,
'refund_message' => $withinGuaranteePeriod
? 'Your refund will be processed within 5-7 business days'
: null,
],
],
]);
} catch (\Exception $e) {
DB::rollBack();
Log::error('Failed to cancel subscription', [
'user_id' => $user->id,
'error' => $e->getMessage(),
]);
return response()->json([
'success' => false,
'message' => 'Failed to cancel subscription',
'error' => $e->getMessage(),
], 500);
}
}
} }

View File

@ -0,0 +1,63 @@
<?php
namespace App\Mail;
use App\Models\User;
use App\Models\Subscription;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
use Carbon\Carbon;
class CancellationRetentionOfferMail extends Mailable
{
use Queueable, SerializesModels;
public $user;
public $subscription;
public $freeMonths;
public $offerEndDate;
/**
* Create a new message instance.
*/
public function __construct(User $user, Subscription $subscription, int $freeMonths, Carbon $offerEndDate)
{
$this->user = $user;
$this->subscription = $subscription;
$this->freeMonths = $freeMonths;
$this->offerEndDate = $offerEndDate;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '🎉 Oferta Especial: ' . $this->freeMonths . ' Meses Grátis!',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
view: 'emails.cancellation-retention-offer',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('subscription_retention_offers', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('subscription_id')->constrained()->onDelete('cascade');
$table->string('offer_type'); // 'free_3_months', 'discount', etc
$table->string('status')->default('offered'); // 'offered', 'accepted', 'declined'
$table->string('original_cancel_reason')->nullable();
$table->text('user_feedback')->nullable();
$table->timestamp('offer_start_date')->nullable();
$table->timestamp('offer_end_date')->nullable();
$table->timestamps();
$table->index(['user_id', 'offer_type', 'status']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('subscription_retention_offers');
}
};

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('subscription_cancellations', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('subscription_id')->constrained()->onDelete('cascade');
$table->string('reason'); // 'price', 'features', 'not_using', 'switching', 'other'
$table->text('feedback')->nullable();
$table->timestamp('canceled_at');
$table->boolean('within_guarantee_period')->default(false);
$table->boolean('refund_issued')->default(false);
$table->timestamp('refund_processed_at')->nullable();
$table->timestamps();
$table->index(['user_id', 'reason']);
$table->index('canceled_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('subscription_cancellations');
}
};

View File

@ -0,0 +1,209 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Oferta Especial de Retenção</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 40px auto;
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.header {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
padding: 40px 30px;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 28px;
font-weight: bold;
}
.header .emoji {
font-size: 48px;
display: block;
margin-bottom: 10px;
}
.content {
padding: 40px 30px;
}
.highlight-box {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
border-radius: 12px;
padding: 30px;
text-align: center;
margin: 30px 0;
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}
.highlight-box .number {
font-size: 72px;
font-weight: bold;
line-height: 1;
margin: 10px 0;
}
.highlight-box .text {
font-size: 24px;
font-weight: bold;
margin-top: 10px;
}
.benefit-list {
background-color: #f0fdf4;
border-left: 4px solid #10b981;
padding: 20px;
margin: 25px 0;
border-radius: 4px;
}
.benefit-list ul {
margin: 10px 0;
padding-left: 25px;
}
.benefit-list li {
color: #065f46;
margin: 10px 0;
font-size: 15px;
}
.emotional-message {
background-color: #fef3c7;
border-left: 4px solid #f59e0b;
padding: 20px;
margin: 25px 0;
border-radius: 4px;
}
.emotional-message p {
color: #78350f;
margin: 0;
line-height: 1.6;
}
.cta-box {
text-align: center;
margin: 30px 0;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}
.cta-box .date {
font-size: 18px;
color: #059669;
font-weight: bold;
margin-top: 15px;
}
.footer {
background-color: #f8f9fa;
padding: 20px;
text-align: center;
color: #6c757d;
font-size: 12px;
}
.divider {
height: 1px;
background-color: #e5e7eb;
margin: 30px 0;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<span class="emoji">🎉</span>
<h1>OFERTA EXCLUSIVA PARA VOCÊ!</h1>
</div>
<div class="content">
<p style="font-size: 16px; color: #374151; line-height: 1.6;">
Olá, <strong>{{ $user->name }}</strong>
</p>
<p style="font-size: 16px; color: #374151; line-height: 1.6;">
Entendemos que o preço pode ser uma preocupação, e <strong>queremos mantê-lo conosco</strong>!
Por isso, temos uma oferta especial para você:
</p>
<div class="highlight-box">
<div style="font-size: 18px; margin-bottom: 10px;"> OFERTA ÚNICA </div>
<div class="number">{{ $freeMonths }}</div>
<div class="text">MESES GRÁTIS</div>
<div style="font-size: 16px; margin-top: 15px; opacity: 0.9;">
Sem custos, sem compromisso!
</div>
</div>
<div class="emotional-message">
<p style="font-weight: bold; margin-bottom: 10px;">💙 Você é importante para nós!</p>
<p>
Sabemos que as coisas podem estar apertadas financeiramente. Esta oferta especial de
<strong>{{ $freeMonths }} meses grátis</strong> é nossa forma de dizer:
<em>"Queremos você aqui com a gente!"</em>
</p>
<p style="margin-top: 10px;">
Durante este período, você terá acesso completo a todos os recursos premium do WebMoney,
sem pagar nada. É tempo suficiente para as coisas melhorarem! 🌟
</p>
</div>
<div class="benefit-list">
<strong style="color: #065f46; font-size: 16px;">O que você continua tendo GRÁTIS:</strong>
<ul>
<li>📊 Controle completo de todas as suas contas</li>
<li>💳 Gestão ilimitada de cartões de crédito</li>
<li>📈 Orçamentos e metas financeiras</li>
<li>📱 Importação automática de extratos</li>
<li>🎯 Categorização inteligente de transações</li>
<li>📋 Relatórios detalhados e insights</li>
<li>☁️ Backup automático na nuvem</li>
</ul>
</div>
<div class="divider"></div>
<div class="cta-box">
<p style="font-size: 18px; color: #374151; font-weight: bold; margin: 0 0 15px 0;">
Oferta Aplicada!
</p>
<p style="font-size: 15px; color: #6b7280; margin: 0;">
Sua assinatura foi estendida automaticamente por {{ $freeMonths }} meses.<br>
Continue aproveitando todos os recursos premium sem custo!
</p>
<div class="date">
Sua próxima cobrança será em:<br>
<strong>{{ \Carbon\Carbon::parse($offerEndDate)->format('d/m/Y') }}</strong>
</div>
</div>
<div style="background-color: #eff6ff; border-left: 4px solid #3b82f6; padding: 20px; border-radius: 4px; margin-top: 25px;">
<p style="color: #1e40af; margin: 0; font-size: 14px;">
<strong>📌 Importante:</strong> Esta oferta é exclusiva e pode ser usada apenas uma vez.
Após os {{ $freeMonths }} meses, sua assinatura voltará ao valor normal. Você pode cancelar
a qualquer momento antes disso, sem nenhum custo.
</p>
</div>
<p style="font-size: 16px; color: #374151; line-height: 1.6; margin-top: 30px;">
Muito obrigado por escolher o <strong>WebMoney</strong>! Estamos aqui para ajudá-lo a alcançar
seus objetivos financeiros. 💚
</p>
<p style="font-size: 14px; color: #6b7280; margin-top: 25px;">
Com carinho,<br>
<strong style="color: #374151;">Equipe WebMoney</strong>
</p>
</div>
<div class="footer">
<p>Este é um email automático do <strong>WebMoney</strong></p>
<p>Por favor, não responda a este email.</p>
</div>
</div>
</body>
</html>

View File

@ -77,6 +77,10 @@
Route::post('/subscription/cancel', [SubscriptionController::class, 'cancel']); Route::post('/subscription/cancel', [SubscriptionController::class, 'cancel']);
Route::get('/subscription/invoices', [SubscriptionController::class, 'invoices']); Route::get('/subscription/invoices', [SubscriptionController::class, 'invoices']);
// Subscription Cancellation Wizard
Route::get('/subscription/cancellation/eligibility', [SubscriptionController::class, 'cancellationEligibility']);
Route::post('/subscription/cancellation/process', [SubscriptionController::class, 'processCancellation']);
// ============================================ // ============================================
// Contas (Accounts) - Com limite de plano // Contas (Accounts) - Com limite de plano
// ============================================ // ============================================

View File

@ -0,0 +1,541 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { subscriptionService } from '../services/api';
import { useToast } from './Toast';
const CancellationWizard = ({ onClose, subscription }) => {
const { t } = useTranslation();
const toast = useToast();
const navigate = useNavigate();
// Estados do wizard
const [step, setStep] = useState(1); // 1-5: Motivo, Apelo, Oferta, Feedback, Confirmação
const [loading, setLoading] = useState(false);
const [eligibility, setEligibility] = useState(null);
// Dados do formulário
const [reason, setReason] = useState('');
const [feedback, setFeedback] = useState('');
const [acceptOffer, setAcceptOffer] = useState(false);
const [finalConfirmation, setFinalConfirmation] = useState('');
const reasons = [
{ value: 'price', icon: 'bi-currency-dollar', label: t('cancellation.reasonPrice') || 'Preço muito alto' },
{ value: 'not_using', icon: 'bi-clock', label: t('cancellation.reasonNotUsing') || 'Não estou usando' },
{ value: 'features', icon: 'bi-puzzle', label: t('cancellation.reasonFeatures') || 'Faltam recursos' },
{ value: 'switching', icon: 'bi-arrow-left-right', label: t('cancellation.reasonSwitching') || 'Mudando para outro serviço' },
{ value: 'other', icon: 'bi-three-dots', label: t('cancellation.reasonOther') || 'Outro motivo' },
];
useEffect(() => {
loadEligibility();
}, []);
const loadEligibility = async () => {
try {
const response = await subscriptionService.getCancellationEligibility();
if (response.success) {
setEligibility(response.data);
}
} catch (error) {
console.error('Error loading eligibility:', error);
toast.error(t('cancellation.loadError') || 'Erro ao carregar informações');
}
};
const handleSelectReason = (selectedReason) => {
setReason(selectedReason);
setStep(2); // Ir para apelo emocional
};
const handleContinue = () => {
// Se motivo é preço E elegível para oferta, mostrar oferta
if (reason === 'price' && eligibility?.retention_offer?.eligible_for_free_months) {
setStep(3); // Mostrar oferta
} else {
setStep(4); // Pular para feedback
}
};
const handleAcceptOffer = () => {
setAcceptOffer(true);
executeCancellation(true);
};
const handleDeclineOffer = () => {
setAcceptOffer(false);
setStep(4); // Ir para feedback
};
const handleProceedToCancellation = () => {
setStep(5); // Confirmação final
};
const executeCancellation = async (acceptingOffer = false) => {
setLoading(true);
try {
const response = await subscriptionService.processCancellation({
reason,
feedback,
accept_retention_offer: acceptingOffer,
});
if (response.success) {
if (acceptingOffer) {
toast.success(t('cancellation.offerAccepted') || '🎉 3 meses grátis aplicados!');
} else {
toast.success(t('cancellation.cancelled') || 'Assinatura cancelada');
}
setTimeout(() => {
onClose();
window.location.reload();
}, 2000);
}
} catch (error) {
console.error('Error processing cancellation:', error);
const errorMsg = error.response?.data?.message || t('cancellation.error') || 'Erro ao processar cancelamento';
toast.error(errorMsg);
} finally {
setLoading(false);
}
};
const handleFinalConfirm = () => {
if (finalConfirmation.toUpperCase() !== 'CANCELAR') {
toast.error(t('cancellation.confirmationError') || 'Digite CANCELAR para confirmar');
return;
}
executeCancellation(false);
};
// Função para obter mensagem emocional baseada no motivo
const getEmotionalMessage = () => {
const messages = {
price: {
icon: '💰',
title: t('cancellation.emotionalPriceTitle') || 'Entendemos suas preocupações financeiras',
message: t('cancellation.emotionalPriceMessage') ||
'Sabemos que o orçamento pode estar apertado. Antes de ir, queremos trabalhar com você para encontrar uma solução que funcione para seu bolso. Temos uma oferta especial esperando por você! 💙'
},
not_using: {
icon: '😔',
title: t('cancellation.emotionalNotUsingTitle') || 'Sentiremos sua falta!',
message: t('cancellation.emotionalNotUsingMessage') ||
'Às vezes a correria do dia a dia nos afasta das ferramentas que realmente podem nos ajudar. O WebMoney está aqui para simplificar sua vida financeira, não complicá-la. Que tal darmos uma segunda chance? Podemos ajudá-lo a tirar melhor proveito dos recursos! 🌟'
},
features: {
icon: '🎯',
title: t('cancellation.emotionalFeaturesTitle') || 'Nos conte o que está faltando!',
message: t('cancellation.emotionalFeaturesMessage') ||
'Sua opinião é fundamental para nós! Estamos constantemente melhorando o WebMoney com novos recursos. O que você gostaria de ver? Muitas funcionalidades que temos hoje vieram de sugestões de usuários como você. Vamos construir isso juntos? 🚀'
},
switching: {
icon: '💔',
title: t('cancellation.emotionalSwitchingTitle') || 'Por que nos deixar?',
message: t('cancellation.emotionalSwitchingMessage') ||
'Estamos tristes em ver você partir. O WebMoney foi construído com carinho pensando em cada detalhe da sua experiência. O que a outra ferramenta oferece que não temos? Adoraríamos saber para podermos melhorar! ❤️'
},
other: {
icon: '💬',
title: t('cancellation.emotionalOtherTitle') || 'Nos conte mais',
message: t('cancellation.emotionalOtherMessage') ||
'Cada usuário é único e especial para nós. Se houver algo que possamos fazer para melhorar sua experiência, por favor nos diga. Estamos aqui para ouvir e ajudar! 🤝'
}
};
return messages[reason] || messages.other;
};
if (!eligibility) {
return (
<div className="modal show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.9)' }}>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content border-0" style={{ background: '#1e293b' }}>
<div className="modal-body text-center py-5">
<div className="spinner-border text-primary mb-3" style={{ width: '3rem', height: '3rem' }}></div>
<p className="text-white">{t('common.loading') || 'Carregando...'}</p>
</div>
</div>
</div>
</div>
);
}
return (
<div className="modal show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.9)' }}>
<div className="modal-dialog modal-dialog-centered modal-lg">
<div className="modal-content border-0" style={{ background: '#1e293b' }}>
{/* Header */}
<div className="modal-header border-0 pb-2">
<h5 className="modal-title text-white d-flex align-items-center">
<i className="bi bi-heart-break me-2 text-warning"></i>
{t('cancellation.title') || 'Cancelar Assinatura'}
</h5>
<button
className="btn-close btn-close-white"
onClick={onClose}
disabled={loading}
></button>
</div>
{/* Body */}
<div className="modal-body pt-3" style={{ maxHeight: '70vh', overflowY: 'auto' }}>
{/* Step Indicator */}
<div className="d-flex justify-content-center mb-4">
{[1, 2, 3, 4, 5].slice(0, reason === 'price' && eligibility?.retention_offer?.eligible_for_free_months ? 5 : 4).map((s) => (
<div
key={s}
className={`rounded-circle d-flex align-items-center justify-content-center me-2 ${
s === step ? 'bg-warning text-dark' : s < step ? 'bg-success text-white' : 'bg-secondary text-white'
}`}
style={{ width: '35px', height: '35px', fontSize: '14px', fontWeight: 'bold' }}
>
{s < step ? '✓' : s}
</div>
))}
</div>
{/* Step 1: Motivo */}
{step === 1 && (
<div>
<div className="text-center mb-4">
<i className="bi bi-chat-left-dots display-1 text-warning"></i>
<h5 className="text-white mt-3 mb-2">
{t('cancellation.step1Title') || 'Por que você quer cancelar?'}
</h5>
<p className="text-slate-400" style={{ fontSize: '14px' }}>
{t('cancellation.step1Description') ||
'Sua resposta nos ajuda a melhorar nosso serviço'}
</p>
</div>
<div className="row g-3">
{reasons.map((r) => (
<div key={r.value} className="col-md-6">
<div
className="card h-100 border-0 cursor-pointer"
style={{
background: reason === r.value ? '#3b82f6' : '#0f172a',
cursor: 'pointer',
transition: 'all 0.2s'
}}
onClick={() => handleSelectReason(r.value)}
onMouseEnter={(e) => {
if (reason !== r.value) {
e.currentTarget.style.background = '#1e293b';
}
}}
onMouseLeave={(e) => {
if (reason !== r.value) {
e.currentTarget.style.background = '#0f172a';
}
}}
>
<div className="card-body text-center py-4">
<i className={`bi ${r.icon} display-4 text-primary mb-2`}></i>
<h6 className="text-white mb-0">{r.label}</h6>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Step 2: Apelo Emocional */}
{step === 2 && (
<div>
{(() => {
const emotional = getEmotionalMessage();
return (
<>
<div className="text-center mb-4">
<div style={{ fontSize: '72px', lineHeight: 1 }}>{emotional.icon}</div>
<h5 className="text-white mt-3 mb-2">{emotional.title}</h5>
</div>
<div className="card bg-warning bg-opacity-10 border-warning mb-4">
<div className="card-body">
<p className="text-white mb-0" style={{ lineHeight: '1.7' }}>
{emotional.message}
</p>
</div>
</div>
<div className="card bg-dark border-0 mb-3">
<div className="card-body">
<h6 className="text-white mb-3">
<i className="bi bi-star-fill text-warning me-2"></i>
{t('cancellation.benefitsTitle') || 'O que você vai perder:'}
</h6>
<ul className="text-slate-300 mb-0" style={{ fontSize: '14px' }}>
<li>{t('cancellation.benefit1') || 'Controle completo das suas finanças'}</li>
<li>{t('cancellation.benefit2') || 'Importação automática de extratos'}</li>
<li>{t('cancellation.benefit3') || 'Orçamentos e metas inteligentes'}</li>
<li>{t('cancellation.benefit4') || 'Relatórios detalhados e insights'}</li>
<li>{t('cancellation.benefit5') || 'Suporte prioritário'}</li>
<li>{t('cancellation.benefit6') || 'Backup automático na nuvem'}</li>
</ul>
</div>
</div>
</>
);
})()}
</div>
)}
{/* Step 3: Oferta de Retenção (apenas para motivo = preço) */}
{step === 3 && reason === 'price' && eligibility?.retention_offer?.eligible_for_free_months && (
<div>
<div className="text-center mb-4">
<div style={{ fontSize: '72px', lineHeight: 1 }}>🎉</div>
<h5 className="text-white mt-3 mb-2">
{t('cancellation.offerTitle') || 'ESPERE! Temos uma Oferta Especial para Você!'}
</h5>
</div>
<div
className="card border-0 mb-4"
style={{
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
boxShadow: '0 8px 20px rgba(16, 185, 129, 0.4)'
}}
>
<div className="card-body text-center py-5">
<div className="text-white" style={{ fontSize: '18px', fontWeight: 'bold' }}>
OFERTA EXCLUSIVA
</div>
<div className="text-white display-1 fw-bold my-3">
{eligibility.retention_offer.months_offered}
</div>
<div className="text-white" style={{ fontSize: '28px', fontWeight: 'bold' }}>
{t('cancellation.freeMonths') || 'MESES GRÁTIS'}
</div>
<div className="text-white mt-3" style={{ fontSize: '16px', opacity: 0.9 }}>
{t('cancellation.offerSubtitle') || 'Sem custos, sem compromisso!'}
</div>
</div>
</div>
<div className="alert alert-warning border-0 mb-4">
<h6 className="alert-heading">
<i className="bi bi-heart-fill me-2"></i>
{t('cancellation.offerExplanation') || 'Por que estamos fazendo isso?'}
</h6>
<p className="mb-0" style={{ fontSize: '14px' }}>
{t('cancellation.offerExplanationText') ||
'Você é importante para nós! Sabemos que as coisas podem estar difíceis financeiramente. Por isso, queremos dar a você 3 meses GRÁTIS para continuar aproveitando todos os recursos premium do WebMoney. É tempo suficiente para as coisas melhorarem! Esta oferta é exclusiva e pode ser usada apenas UMA VEZ.'}
</p>
</div>
<div className="card bg-info bg-opacity-10 border-info">
<div className="card-body">
<p className="text-white mb-0" style={{ fontSize: '14px' }}>
<i className="bi bi-info-circle me-2"></i>
<strong>{t('cancellation.offerDetails') || 'Detalhes:'}</strong> Sua assinatura será pausada por 3 meses. Após este período, voltará ao valor normal. Você pode cancelar a qualquer momento antes disso, sem custos.
</p>
</div>
</div>
</div>
)}
{/* Step 4: Feedback */}
{step === 4 && (
<div>
<div className="text-center mb-4">
<i className="bi bi-chat-quote display-1 text-info"></i>
<h5 className="text-white mt-3 mb-2">
{t('cancellation.feedbackTitle') || 'Sua Opinião é Importante'}
</h5>
<p className="text-slate-400" style={{ fontSize: '14px' }}>
{t('cancellation.feedbackDescription') ||
'Por favor, compartilhe mais detalhes para nos ajudar a melhorar'}
</p>
</div>
<div className="card bg-dark border-0 mb-3">
<div className="card-body">
<label className="form-label text-white">
{t('cancellation.feedbackLabel') || 'Como podemos melhorar?'}
</label>
<textarea
className="form-control"
rows="5"
placeholder={t('cancellation.feedbackPlaceholder') || 'Compartilhe seus pensamentos...'}
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
style={{
background: '#0f172a',
border: '1px solid #475569',
color: 'white'
}}
/>
<small className="text-slate-400 mt-2 d-block">
{t('cancellation.feedbackHelp') || 'Opcional, mas muito apreciado'}
</small>
</div>
</div>
<div className="alert alert-danger border-0">
<p className="mb-0">
<i className="bi bi-exclamation-triangle me-2"></i>
{eligibility?.guarantee?.within_period
? (t('cancellation.guaranteeWarning') || 'Você está dentro do período de garantia de 7 dias. Você receberá reembolso total.')
: (t('cancellation.noRefundWarning') || 'Você terá acesso até o final do período atual, mas não haverá reembolso.')}
</p>
</div>
</div>
)}
{/* Step 5: Confirmação Final */}
{step === 5 && (
<div>
<div className="text-center mb-4">
<i className="bi bi-exclamation-diamond display-1 text-danger"></i>
<h5 className="text-white mt-3 mb-2">
{t('cancellation.finalTitle') || 'Confirmação de Cancelamento'}
</h5>
<p className="text-slate-400" style={{ fontSize: '14px' }}>
{t('cancellation.finalDescription') ||
'Esta é sua última chance de mudar de ideia'}
</p>
</div>
<div className="alert alert-danger border-0 mb-3">
<h6 className="alert-heading">
{t('cancellation.finalWarning') || 'Tem certeza absoluta?'}
</h6>
<p className="mb-0">
{t('cancellation.finalWarningText') ||
'Ao cancelar, você perderá acesso a todos os recursos premium e seus dados não serão mais sincronizados automaticamente.'}
</p>
</div>
<div className="card bg-dark border-danger mb-3">
<div className="card-body">
<label className="form-label text-white">
{t('cancellation.typeToConfirm') || 'Digite CANCELAR para confirmar:'}
</label>
<input
type="text"
className="form-control form-control-lg"
placeholder="CANCELAR"
value={finalConfirmation}
onChange={(e) => setFinalConfirmation(e.target.value)}
style={{
background: '#0f172a',
border: '2px solid #dc3545',
color: 'white'
}}
/>
<small className="text-slate-400 mt-2 d-block">
{t('cancellation.confirmationHelp') || 'Digite exatamente: CANCELAR (em maiúsculas)'}
</small>
</div>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="modal-footer border-0">
{step > 1 && step !== 3 && (
<button
className="btn btn-outline-secondary px-4"
onClick={() => setStep(step - 1)}
disabled={loading}
>
<i className="bi bi-arrow-left me-2"></i>
{t('common.back') || 'Voltar'}
</button>
)}
<button
className="btn btn-outline-secondary px-4"
onClick={onClose}
disabled={loading}
>
{t('cancellation.keepSubscription') || 'Manter Assinatura'}
</button>
{step === 2 && (
<button
className="btn btn-warning px-4"
onClick={handleContinue}
>
{t('common.continue') || 'Continuar'}
<i className="bi bi-arrow-right ms-2"></i>
</button>
)}
{step === 3 && (
<>
<button
className="btn btn-outline-danger px-4"
onClick={handleDeclineOffer}
disabled={loading}
>
{t('cancellation.declineOffer') || 'Recusar Oferta'}
</button>
<button
className="btn btn-success px-4"
onClick={handleAcceptOffer}
disabled={loading}
>
{loading ? (
<>
<span className="spinner-border spinner-border-sm me-2"></span>
{t('common.processing') || 'Processando...'}
</>
) : (
<>
<i className="bi bi-check-circle me-2"></i>
{t('cancellation.acceptOffer') || 'Aceitar 3 Meses Grátis!'}
</>
)}
</button>
</>
)}
{step === 4 && (
<button
className="btn btn-danger px-4"
onClick={handleProceedToCancellation}
>
{t('cancellation.proceedCancellation') || 'Prosseguir com Cancelamento'}
<i className="bi bi-arrow-right ms-2"></i>
</button>
)}
{step === 5 && (
<button
className="btn btn-danger px-4"
onClick={handleFinalConfirm}
disabled={loading || finalConfirmation.toUpperCase() !== 'CANCELAR'}
>
{loading ? (
<>
<span className="spinner-border spinner-border-sm me-2"></span>
{t('common.processing') || 'Processando...'}
</>
) : (
<>
<i className="bi bi-x-circle me-2"></i>
{t('cancellation.confirmCancellation') || 'Confirmar Cancelamento'}
</>
)}
</button>
)}
</div>
</div>
</div>
</div>
);
};
export default CancellationWizard;

View File

@ -2194,7 +2194,13 @@
"factoryReset": "Factory Reset", "factoryReset": "Factory Reset",
"factoryResetDesc": "WARNING: Permanently deletes ALL your data. This action cannot be undone.", "factoryResetDesc": "WARNING: Permanently deletes ALL your data. This action cannot be undone.",
"factoryResetBtn": "Start Factory Reset", "factoryResetBtn": "Start Factory Reset",
"dataManagementWarning": "Before doing a Factory Reset, we recommend creating a backup of your data to recover it later if needed." "dataManagementWarning": "Before doing a Factory Reset, we recommend creating a backup of your data to recover it later if needed.",
"subscription": "Subscription",
"renewsOn": "Renews on",
"managePayPal": "Manage on PayPal",
"cancelSubscription": "Cancel Subscription",
"cancelAnytime": "You can cancel anytime",
"guaranteePeriod": "You are within the 7-day guarantee period"
}, },
"pricing": { "pricing": {
"title": "Plans & Pricing", "title": "Plans & Pricing",
@ -2460,5 +2466,61 @@
"error": "Error importing backup", "error": "Error importing backup",
"warning": "Warning: Import may take a few minutes depending on backup size.", "warning": "Warning: Import may take a few minutes depending on backup size.",
"import": "Import Backup" "import": "Import Backup"
},
"cancellation": {
"title": "Cancel Subscription",
"loadError": "Error loading information",
"step1Title": "Why do you want to cancel?",
"step1Description": "Your feedback helps us improve our service",
"reasonPrice": "Price too high",
"reasonNotUsing": "Not using it",
"reasonFeatures": "Missing features",
"reasonSwitching": "Switching to another service",
"reasonOther": "Other reason",
"emotionalPriceTitle": "We understand your financial concerns",
"emotionalPriceMessage": "We know budgets can be tight. Before you go, we want to work with you to find a solution that works for your wallet. We have a special offer waiting for you! 💙",
"emotionalNotUsingTitle": "We'll miss you!",
"emotionalNotUsingMessage": "Sometimes the daily hustle takes us away from the tools that can really help us. WebMoney is here to simplify your financial life, not complicate it. How about giving us a second chance? We can help you make better use of the features! 🌟",
"emotionalFeaturesTitle": "Tell us what's missing!",
"emotionalFeaturesMessage": "Your opinion is fundamental to us! We're constantly improving WebMoney with new features. What would you like to see? Many features we have today came from suggestions from users like you. Let's build this together? 🚀",
"emotionalSwitchingTitle": "Why leave us?",
"emotionalSwitchingMessage": "We're sad to see you go. WebMoney was built with care thinking about every detail of your experience. What does the other tool offer that we don't have? We'd love to know so we can improve! ❤️",
"emotionalOtherTitle": "Tell us more",
"emotionalOtherMessage": "Each user is unique and special to us. If there's anything we can do to improve your experience, please tell us. We're here to listen and help! 🤝",
"benefitsTitle": "What you'll lose:",
"benefit1": "Complete control of your finances",
"benefit2": "Automatic statement import",
"benefit3": "Smart budgets and goals",
"benefit4": "Detailed reports and insights",
"benefit5": "Priority support",
"benefit6": "Automatic cloud backup",
"offerTitle": "WAIT! We Have a Special Offer for You!",
"freeMonths": "FREE MONTHS",
"offerSubtitle": "No cost, no commitment!",
"offerExplanation": "Why are we doing this?",
"offerExplanationText": "You're important to us! We know things can be financially difficult. That's why we want to give you 3 FREE months to continue enjoying all WebMoney premium features. It's enough time for things to get better! This offer is exclusive and can only be used ONCE.",
"offerDetails": "Details:",
"acceptOffer": "Accept 3 Free Months!",
"declineOffer": "Decline Offer",
"offerAccepted": "🎉 3 free months applied!",
"feedbackTitle": "Your Opinion Matters",
"feedbackDescription": "Please share more details to help us improve",
"feedbackLabel": "How can we improve?",
"feedbackPlaceholder": "Share your thoughts...",
"feedbackHelp": "Optional, but much appreciated",
"guaranteeWarning": "You are within the 7-day guarantee period. You will receive a full refund.",
"noRefundWarning": "You will have access until the end of the current period, but there will be no refund.",
"finalTitle": "Cancellation Confirmation",
"finalDescription": "This is your last chance to change your mind",
"finalWarning": "Are you absolutely sure?",
"finalWarningText": "By canceling, you'll lose access to all premium features and your data will no longer sync automatically.",
"typeToConfirm": "Type CANCEL to confirm:",
"confirmationHelp": "Type exactly: CANCEL (in uppercase)",
"confirmationError": "Type CANCEL to confirm",
"keepSubscription": "Keep Subscription",
"proceedCancellation": "Proceed with Cancellation",
"confirmCancellation": "Confirm Cancellation",
"cancelled": "Subscription cancelled",
"error": "Error processing cancellation"
} }
} }

View File

@ -2186,7 +2186,13 @@
"factoryReset": "Factory Reset", "factoryReset": "Factory Reset",
"factoryResetDesc": "ATENCIÓN: Elimina permanentemente TODOS sus datos. Esta acción no se puede deshacer.", "factoryResetDesc": "ATENCIÓN: Elimina permanentemente TODOS sus datos. Esta acción no se puede deshacer.",
"factoryResetBtn": "Iniciar Factory Reset", "factoryResetBtn": "Iniciar Factory Reset",
"dataManagementWarning": "Antes de hacer Factory Reset, recomendamos crear una copia de seguridad de sus datos para recuperarlos posteriormente si es necesario." "dataManagementWarning": "Antes de hacer Factory Reset, recomendamos crear una copia de seguridad de sus datos para recuperarlos posteriormente si es necesario.",
"subscription": "Suscripción",
"renewsOn": "Se renueva el",
"managePayPal": "Gestionar en PayPal",
"cancelSubscription": "Cancelar Suscripción",
"cancelAnytime": "Puedes cancelar en cualquier momento",
"guaranteePeriod": "Estás dentro del período de garantía de 7 días"
}, },
"pricing": { "pricing": {
"title": "Planes y Precios", "title": "Planes y Precios",
@ -2462,5 +2468,61 @@
"error": "Error al importar copia de seguridad", "error": "Error al importar copia de seguridad",
"warning": "Atención: La importación puede tardar unos minutos dependiendo del tamaño de la copia de seguridad.", "warning": "Atención: La importación puede tardar unos minutos dependiendo del tamaño de la copia de seguridad.",
"import": "Importar Copia de Seguridad" "import": "Importar Copia de Seguridad"
},
"cancellation": {
"title": "Cancelar Suscripción",
"loadError": "Error al cargar información",
"step1Title": "¿Por qué quieres cancelar?",
"step1Description": "Tu respuesta nos ayuda a mejorar nuestro servicio",
"reasonPrice": "Precio muy alto",
"reasonNotUsing": "No lo estoy usando",
"reasonFeatures": "Faltan funciones",
"reasonSwitching": "Cambiando a otro servicio",
"reasonOther": "Otro motivo",
"emotionalPriceTitle": "Entendemos tus preocupaciones financieras",
"emotionalPriceMessage": "Sabemos que el presupuesto puede estar ajustado. Antes de irte, queremos trabajar contigo para encontrar una solución que funcione para tu bolsillo. ¡Tenemos una oferta especial esperándote! 💙",
"emotionalNotUsingTitle": "¡Te echaremos de menos!",
"emotionalNotUsingMessage": "A veces el ajetreo del día a día nos aleja de las herramientas que realmente pueden ayudarnos. WebMoney está aquí para simplificar tu vida financiera, no complicarla. ¿Qué tal si nos das una segunda oportunidad? ¡Podemos ayudarte a aprovechar mejor las funciones! 🌟",
"emotionalFeaturesTitle": "¡Cuéntanos qué te hace falta!",
"emotionalFeaturesMessage": "¡Tu opinión es fundamental para nosotros! Estamos mejorando constantemente WebMoney con nuevas funciones. ¿Qué te gustaría ver? Muchas de las funciones que tenemos hoy vinieron de sugerencias de usuarios como tú. ¿Lo construimos juntos? 🚀",
"emotionalSwitchingTitle": "¿Por qué dejarnos?",
"emotionalSwitchingMessage": "Estamos tristes de verte partir. WebMoney fue construido con cariño pensando en cada detalle de tu experiencia. ¿Qué ofrece la otra herramienta que no tenemos? ¡Nos encantaría saberlo para poder mejorar! ❤️",
"emotionalOtherTitle": "Cuéntanos más",
"emotionalOtherMessage": "Cada usuario es único y especial para nosotros. Si hay algo que podamos hacer para mejorar tu experiencia, por favor dínoslo. ¡Estamos aquí para escuchar y ayudar! 🤝",
"benefitsTitle": "Lo que perderás:",
"benefit1": "Control completo de tus finanzas",
"benefit2": "Importación automática de extractos",
"benefit3": "Presupuestos y metas inteligentes",
"benefit4": "Reportes detallados e insights",
"benefit5": "Soporte prioritario",
"benefit6": "Backup automático en la nube",
"offerTitle": "¡ESPERA! Tenemos una Oferta Especial para Ti!",
"freeMonths": "MESES GRATIS",
"offerSubtitle": "¡Sin costos, sin compromiso!",
"offerExplanation": "¿Por qué hacemos esto?",
"offerExplanationText": "¡Eres importante para nosotros! Sabemos que las cosas pueden estar difíciles financieramente. Por eso, queremos darte 3 meses GRATIS para que sigas disfrutando de todas las funciones premium de WebMoney. ¡Es tiempo suficiente para que las cosas mejoren! Esta oferta es exclusiva y puede usarse solo UNA VEZ.",
"offerDetails": "Detalles:",
"acceptOffer": "¡Aceptar 3 Meses Gratis!",
"declineOffer": "Rechazar Oferta",
"offerAccepted": "🎉 ¡3 meses gratis aplicados!",
"feedbackTitle": "Tu Opinión es Importante",
"feedbackDescription": "Por favor, comparte más detalles para ayudarnos a mejorar",
"feedbackLabel": "¿Cómo podemos mejorar?",
"feedbackPlaceholder": "Comparte tus pensamientos...",
"feedbackHelp": "Opcional, pero muy apreciado",
"guaranteeWarning": "Estás dentro del período de garantía de 7 días. Recibirás un reembolso total.",
"noRefundWarning": "Tendrás acceso hasta el final del período actual, pero no habrá reembolso.",
"finalTitle": "Confirmación de Cancelación",
"finalDescription": "Esta es tu última oportunidad de cambiar de opinión",
"finalWarning": "¿Estás absolutamente seguro?",
"finalWarningText": "Al cancelar, perderás acceso a todas las funciones premium y tus datos ya no se sincronizarán automáticamente.",
"typeToConfirm": "Escribe CANCELAR para confirmar:",
"confirmationHelp": "Escribe exactamente: CANCELAR (en mayúsculas)",
"confirmationError": "Escribe CANCELAR para confirmar",
"keepSubscription": "Mantener Suscripción",
"proceedCancellation": "Proceder con Cancelación",
"confirmCancellation": "Confirmar Cancelación",
"cancelled": "Suscripción cancelada",
"error": "Error al procesar cancelación"
} }
} }

View File

@ -2197,6 +2197,12 @@
"passwordError": "Erro ao alterar senha", "passwordError": "Erro ao alterar senha",
"passwordMismatch": "As senhas não coincidem", "passwordMismatch": "As senhas não coincidem",
"passwordTooShort": "A senha deve ter pelo menos 8 caracteres", "passwordTooShort": "A senha deve ter pelo menos 8 caracteres",
"subscription": "Minha Assinatura",
"renewsOn": "Renovação em",
"managePayPal": "Gerenciar no PayPal",
"cancelSubscription": "Cancelar Assinatura",
"cancelAnytime": "Cancele a qualquer momento",
"guaranteePeriod": "Período de Garantia",
"dataManagement": "Gerenciamento de Dados", "dataManagement": "Gerenciamento de Dados",
"importBackup": "Importar Backup", "importBackup": "Importar Backup",
"importBackupDesc": "Restaure dados de um backup anteriormente exportado. Os dados serão adicionados à sua conta.", "importBackupDesc": "Restaure dados de um backup anteriormente exportado. Os dados serão adicionados à sua conta.",
@ -2480,5 +2486,61 @@
"error": "Erro ao importar backup", "error": "Erro ao importar backup",
"warning": "Atenção: A importação pode levar alguns minutos dependendo do tamanho do backup.", "warning": "Atenção: A importação pode levar alguns minutos dependendo do tamanho do backup.",
"import": "Importar Backup" "import": "Importar Backup"
},
"cancellation": {
"title": "Cancelar Assinatura",
"loadError": "Erro ao carregar informações",
"step1Title": "Por que você quer cancelar?",
"step1Description": "Sua resposta nos ajuda a melhorar nosso serviço",
"reasonPrice": "Preço muito alto",
"reasonNotUsing": "Não estou usando",
"reasonFeatures": "Faltam recursos",
"reasonSwitching": "Mudando para outro serviço",
"reasonOther": "Outro motivo",
"emotionalPriceTitle": "Entendemos suas preocupações financeiras",
"emotionalPriceMessage": "Sabemos que o orçamento pode estar apertado. Antes de ir, queremos trabalhar com você para encontrar uma solução que funcione para seu bolso. Temos uma oferta especial esperando por você! 💙",
"emotionalNotUsingTitle": "Sentiremos sua falta!",
"emotionalNotUsingMessage": "Às vezes a correria do dia a dia nos afasta das ferramentas que realmente podem nos ajudar. O WebMoney está aqui para simplificar sua vida financeira, não complicá-la. Que tal darmos uma segunda chance? Podemos ajudá-lo a tirar melhor proveito dos recursos! 🌟",
"emotionalFeaturesTitle": "Nos conte o que está faltando!",
"emotionalFeaturesMessage": "Sua opinião é fundamental para nós! Estamos constantemente melhorando o WebMoney com novos recursos. O que você gostaria de ver? Muitas funcionalidades que temos hoje vieram de sugestões de usuários como você. Vamos construir isso juntos? 🚀",
"emotionalSwitchingTitle": "Por que nos deixar?",
"emotionalSwitchingMessage": "Estamos tristes em ver você partir. O WebMoney foi construído com carinho pensando em cada detalhe da sua experiência. O que a outra ferramenta oferece que não temos? Adoraríamos saber para podermos melhorar! ❤️",
"emotionalOtherTitle": "Nos conte mais",
"emotionalOtherMessage": "Cada usuário é único e especial para nós. Se houver algo que possamos fazer para melhorar sua experiência, por favor nos diga. Estamos aqui para ouvir e ajudar! 🤝",
"benefitsTitle": "O que você vai perder:",
"benefit1": "Controle completo das suas finanças",
"benefit2": "Importação automática de extratos",
"benefit3": "Orçamentos e metas inteligentes",
"benefit4": "Relatórios detalhados e insights",
"benefit5": "Suporte prioritário",
"benefit6": "Backup automático na nuvem",
"offerTitle": "ESPERE! Temos uma Oferta Especial para Você!",
"freeMonths": "MESES GRÁTIS",
"offerSubtitle": "Sem custos, sem compromisso!",
"offerExplanation": "Por que estamos fazendo isso?",
"offerExplanationText": "Você é importante para nós! Sabemos que as coisas podem estar difíceis financeiramente. Por isso, queremos dar a você 3 meses GRÁTIS para continuar aproveitando todos os recursos premium do WebMoney. É tempo suficiente para as coisas melhorarem! Esta oferta é exclusiva e pode ser usada apenas UMA VEZ.",
"offerDetails": "Detalhes:",
"acceptOffer": "Aceitar 3 Meses Grátis!",
"declineOffer": "Recusar Oferta",
"offerAccepted": "🎉 3 meses grátis aplicados!",
"feedbackTitle": "Sua Opinião é Importante",
"feedbackDescription": "Por favor, compartilhe mais detalhes para nos ajudar a melhorar",
"feedbackLabel": "Como podemos melhorar?",
"feedbackPlaceholder": "Compartilhe seus pensamentos...",
"feedbackHelp": "Opcional, mas muito apreciado",
"guaranteeWarning": "Você está dentro do período de garantia de 7 dias. Você receberá reembolso total.",
"noRefundWarning": "Você terá acesso até o final do período atual, mas não haverá reembolso.",
"finalTitle": "Confirmação de Cancelamento",
"finalDescription": "Esta é sua última chance de mudar de ideia",
"finalWarning": "Tem certeza absoluta?",
"finalWarningText": "Ao cancelar, você perderá acesso a todos os recursos premium e seus dados não serão mais sincronizados automaticamente.",
"typeToConfirm": "Digite CANCELAR para confirmar:",
"confirmationHelp": "Digite exatamente: CANCELAR (em maiúsculas)",
"confirmationError": "Digite CANCELAR para confirmar",
"keepSubscription": "Manter Assinatura",
"proceedCancellation": "Prosseguir com Cancelamento",
"confirmCancellation": "Confirmar Cancelamento",
"cancelled": "Assinatura cancelada",
"error": "Erro ao processar cancelamento"
} }
} }

View File

@ -1,9 +1,10 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useToast } from '../components/Toast'; import { useToast } from '../components/Toast';
import { profileService } from '../services/api'; import { profileService, subscriptionService } from '../services/api';
import FactoryResetWizard from '../components/FactoryResetWizard'; import FactoryResetWizard from '../components/FactoryResetWizard';
import ImportBackupModal from '../components/ImportBackupModal'; import ImportBackupModal from '../components/ImportBackupModal';
import CancellationWizard from '../components/CancellationWizard';
// Lista de países com código de telefone (foco: ES, BR, US) // Lista de países com código de telefone (foco: ES, BR, US)
const COUNTRY_CODES = [ const COUNTRY_CODES = [
@ -98,9 +99,14 @@ export default function Profile() {
// Modais // Modais
const [showFactoryReset, setShowFactoryReset] = useState(false); const [showFactoryReset, setShowFactoryReset] = useState(false);
const [showImportBackup, setShowImportBackup] = useState(false); const [showImportBackup, setShowImportBackup] = useState(false);
const [showCancellation, setShowCancellation] = useState(false);
// Subscription data
const [subscriptionData, setSubscriptionData] = useState(null);
useEffect(() => { useEffect(() => {
loadProfile(); loadProfile();
loadSubscription();
}, []); }, []);
const loadProfile = async () => { const loadProfile = async () => {
@ -130,6 +136,17 @@ export default function Profile() {
} }
}; };
const loadSubscription = async () => {
try {
const response = await subscriptionService.getStatus();
if (response.success) {
setSubscriptionData(response.data);
}
} catch (error) {
console.error('Error loading subscription:', error);
}
};
const handleProfileChange = (field, value) => { const handleProfileChange = (field, value) => {
setProfile(prev => ({ setProfile(prev => ({
...prev, ...prev,
@ -574,6 +591,76 @@ export default function Profile() {
</div> </div>
</div> </div>
{/* Seção: Assinatura */}
{subscriptionData?.has_subscription && subscriptionData?.plan && !subscriptionData?.plan?.is_free && (
<div className="col-12 mt-4">
<div className="card shadow-sm">
<div className="card-header bg-primary text-white">
<h5 className="mb-0">
<i className="bi bi-credit-card me-2"></i>
{t('profile.subscription') || 'Minha Assinatura'}
</h5>
</div>
<div className="card-body">
<div className="row align-items-center">
<div className="col-md-8">
<div className="d-flex align-items-start mb-3">
<i className="bi bi-star-fill text-warning fs-3 me-3"></i>
<div>
<h6 className="fw-bold mb-1">
{subscriptionData.plan.name}
<span className="badge bg-success ms-2">{t('common.active') || 'Ativo'}</span>
</h6>
<p className="text-muted mb-2" style={{ fontSize: '14px' }}>
{subscriptionData.plan.formatted_price} / {subscriptionData.plan.billing_period === 'monthly' ? t('common.month') || 'mês' : t('common.year') || 'ano'}
</p>
{subscriptionData.subscription?.current_period_end && (
<p className="text-muted mb-0" style={{ fontSize: '13px' }}>
<i className="bi bi-calendar-event me-1"></i>
{t('profile.renewsOn') || 'Renovação em'}: {new Date(subscriptionData.subscription.current_period_end).toLocaleDateString()}
</p>
)}
</div>
</div>
<div className="d-flex gap-2 flex-wrap">
<button
className="btn btn-sm btn-outline-secondary"
onClick={() => window.open('https://www.paypal.com/myaccount/autopay/', '_blank')}
>
<i className="bi bi-paypal me-1"></i>
{t('profile.managePayPal') || 'Gerenciar no PayPal'}
</button>
</div>
</div>
<div className="col-md-4 text-md-end mt-3 mt-md-0">
<button
className="btn btn-outline-danger w-100"
onClick={() => setShowCancellation(true)}
>
<i className="bi bi-x-circle me-2"></i>
{t('profile.cancelSubscription') || 'Cancelar Assinatura'}
</button>
<small className="text-muted d-block mt-2">
{t('profile.cancelAnytime') || 'Cancele a qualquer momento'}
</small>
</div>
</div>
{subscriptionData?.guarantee?.within_period && (
<div className="alert alert-info mt-3 mb-0">
<small>
<i className="bi bi-shield-check me-2"></i>
<strong>{t('profile.guaranteePeriod') || 'Período de Garantia:'}</strong> Você tem {subscriptionData.guarantee.days_remaining} dia(s) restantes para cancelar com reembolso total (garantia de 7 dias).
</small>
</div>
)}
</div>
</div>
</div>
)}
{/* Seção: Backup e Factory Reset */} {/* Seção: Backup e Factory Reset */}
<div className="col-12 mt-4"> <div className="col-12 mt-4">
<div className="card shadow-sm"> <div className="card shadow-sm">
@ -657,6 +744,12 @@ export default function Profile() {
}} }}
/> />
)} )}
{showCancellation && subscriptionData?.subscription && (
<CancellationWizard
onClose={() => setShowCancellation(false)}
subscription={subscriptionData.subscription}
/>
)}
</div> </div>
); );
} }

View File

@ -1701,5 +1701,46 @@ export const accountDeletionService = {
}, },
}; };
// ============================================
// Subscription Service (Assinaturas)
// ============================================
export const subscriptionService = {
// Obter status da assinatura
getStatus: async () => {
const response = await api.get('/subscription/status');
return response.data;
},
// Obter elegibilidade para cancelamento
getCancellationEligibility: async () => {
const response = await api.get('/subscription/cancellation/eligibility');
return response.data;
},
// Processar cancelamento com retenção
processCancellation: async (data) => {
const response = await api.post('/subscription/cancellation/process', data);
return response.data;
},
// Assinar plano
subscribe: async (planSlug) => {
const response = await api.post('/subscription/subscribe', { plan_slug: planSlug });
return response.data;
},
// Confirmar assinatura
confirm: async (subscriptionId) => {
const response = await api.post('/subscription/confirm', { subscription_id: subscriptionId });
return response.data;
},
// Obter faturas
getInvoices: async () => {
const response = await api.get('/subscription/invoices');
return response.data;
},
};
export default api; export default api;