diff --git a/CHANGELOG.md b/CHANGELOG.md index 663cfc3..1a48536 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/). +## [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 ### Added diff --git a/VERSION b/VERSION index bb120e8..4d5fde5 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.59.0 +1.60.0 diff --git a/backend/app/Http/Controllers/Api/SubscriptionController.php b/backend/app/Http/Controllers/Api/SubscriptionController.php index b99d025..39bf06a 100755 --- a/backend/app/Http/Controllers/Api/SubscriptionController.php +++ b/backend/app/Http/Controllers/Api/SubscriptionController.php @@ -12,8 +12,11 @@ use App\Models\EmailVerificationToken; use App\Mail\AccountActivationMail; use App\Mail\SubscriptionCancelledMail; +use App\Mail\CancellationRetentionOfferMail; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Cache; use Carbon\Carbon; class SubscriptionController extends Controller @@ -954,4 +957,307 @@ private function updateSubscriptionFromPayPal(Subscription $subscription, array $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); + } + } } diff --git a/backend/app/Mail/CancellationRetentionOfferMail.php b/backend/app/Mail/CancellationRetentionOfferMail.php new file mode 100644 index 0000000..a241025 --- /dev/null +++ b/backend/app/Mail/CancellationRetentionOfferMail.php @@ -0,0 +1,63 @@ +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 + */ + public function attachments(): array + { + return []; + } +} diff --git a/backend/database/migrations/2025_12_19_154839_create_subscription_retention_offers_table.php b/backend/database/migrations/2025_12_19_154839_create_subscription_retention_offers_table.php new file mode 100644 index 0000000..2fc98e9 --- /dev/null +++ b/backend/database/migrations/2025_12_19_154839_create_subscription_retention_offers_table.php @@ -0,0 +1,37 @@ +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'); + } +}; diff --git a/backend/database/migrations/2025_12_19_154844_create_subscription_cancellations_table.php b/backend/database/migrations/2025_12_19_154844_create_subscription_cancellations_table.php new file mode 100644 index 0000000..29f0017 --- /dev/null +++ b/backend/database/migrations/2025_12_19_154844_create_subscription_cancellations_table.php @@ -0,0 +1,38 @@ +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'); + } +}; diff --git a/backend/resources/views/emails/cancellation-retention-offer.blade.php b/backend/resources/views/emails/cancellation-retention-offer.blade.php new file mode 100644 index 0000000..924f4bd --- /dev/null +++ b/backend/resources/views/emails/cancellation-retention-offer.blade.php @@ -0,0 +1,209 @@ + + + + + + Oferta Especial de Retenção + + + +
+
+ 🎉 +

OFERTA EXCLUSIVA PARA VOCÊ!

+
+ +
+

+ Olá, {{ $user->name }} +

+ +

+ Entendemos que o preço pode ser uma preocupação, e queremos mantê-lo conosco! + Por isso, temos uma oferta especial só para você: +

+ +
+
✨ OFERTA ÚNICA ✨
+
{{ $freeMonths }}
+
MESES GRÁTIS
+
+ Sem custos, sem compromisso! +
+
+ +
+

💙 Você é importante para nós!

+

+ Sabemos que as coisas podem estar apertadas financeiramente. Esta oferta especial de + {{ $freeMonths }} meses grátis é nossa forma de dizer: + "Queremos você aqui com a gente!" +

+

+ Durante este período, você terá acesso completo a todos os recursos premium do WebMoney, + sem pagar nada. É tempo suficiente para as coisas melhorarem! 🌟 +

+
+ +
+ O que você continua tendo GRÁTIS: +
    +
  • 📊 Controle completo de todas as suas contas
  • +
  • 💳 Gestão ilimitada de cartões de crédito
  • +
  • 📈 Orçamentos e metas financeiras
  • +
  • 📱 Importação automática de extratos
  • +
  • 🎯 Categorização inteligente de transações
  • +
  • 📋 Relatórios detalhados e insights
  • +
  • ☁️ Backup automático na nuvem
  • +
+
+ +
+ +
+

+ ✅ Oferta Já Aplicada! +

+

+ Sua assinatura foi estendida automaticamente por {{ $freeMonths }} meses.
+ Continue aproveitando todos os recursos premium sem custo! +

+
+ Sua próxima cobrança será em:
+ {{ \Carbon\Carbon::parse($offerEndDate)->format('d/m/Y') }} +
+
+ +
+

+ 📌 Importante: 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. +

+
+ +

+ Muito obrigado por escolher o WebMoney! Estamos aqui para ajudá-lo a alcançar + seus objetivos financeiros. 💚 +

+ +

+ Com carinho,
+ Equipe WebMoney +

+
+ + +
+ + diff --git a/backend/routes/api.php b/backend/routes/api.php index d76d69c..4af3fdc 100755 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -77,6 +77,10 @@ Route::post('/subscription/cancel', [SubscriptionController::class, 'cancel']); 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 // ============================================ diff --git a/frontend/src/components/CancellationWizard.jsx b/frontend/src/components/CancellationWizard.jsx new file mode 100644 index 0000000..1705b6f --- /dev/null +++ b/frontend/src/components/CancellationWizard.jsx @@ -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 ( +
+
+
+
+
+

{t('common.loading') || 'Carregando...'}

+
+
+
+
+ ); + } + + return ( +
+
+
+ + {/* Header */} +
+
+ + {t('cancellation.title') || 'Cancelar Assinatura'} +
+ +
+ + {/* Body */} +
+ + {/* Step Indicator */} +
+ {[1, 2, 3, 4, 5].slice(0, reason === 'price' && eligibility?.retention_offer?.eligible_for_free_months ? 5 : 4).map((s) => ( +
+ {s < step ? '✓' : s} +
+ ))} +
+ + {/* Step 1: Motivo */} + {step === 1 && ( +
+
+ +
+ {t('cancellation.step1Title') || 'Por que você quer cancelar?'} +
+

+ {t('cancellation.step1Description') || + 'Sua resposta nos ajuda a melhorar nosso serviço'} +

+
+ +
+ {reasons.map((r) => ( +
+
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'; + } + }} + > +
+ +
{r.label}
+
+
+
+ ))} +
+
+ )} + + {/* Step 2: Apelo Emocional */} + {step === 2 && ( +
+ {(() => { + const emotional = getEmotionalMessage(); + return ( + <> +
+
{emotional.icon}
+
{emotional.title}
+
+ +
+
+

+ {emotional.message} +

+
+
+ +
+
+
+ + {t('cancellation.benefitsTitle') || 'O que você vai perder:'} +
+
    +
  • {t('cancellation.benefit1') || 'Controle completo das suas finanças'}
  • +
  • {t('cancellation.benefit2') || 'Importação automática de extratos'}
  • +
  • {t('cancellation.benefit3') || 'Orçamentos e metas inteligentes'}
  • +
  • {t('cancellation.benefit4') || 'Relatórios detalhados e insights'}
  • +
  • {t('cancellation.benefit5') || 'Suporte prioritário'}
  • +
  • {t('cancellation.benefit6') || 'Backup automático na nuvem'}
  • +
+
+
+ + ); + })()} +
+ )} + + {/* Step 3: Oferta de Retenção (apenas para motivo = preço) */} + {step === 3 && reason === 'price' && eligibility?.retention_offer?.eligible_for_free_months && ( +
+
+
🎉
+
+ {t('cancellation.offerTitle') || 'ESPERE! Temos uma Oferta Especial para Você!'} +
+
+ +
+
+
+ ✨ OFERTA EXCLUSIVA ✨ +
+
+ {eligibility.retention_offer.months_offered} +
+
+ {t('cancellation.freeMonths') || 'MESES GRÁTIS'} +
+
+ {t('cancellation.offerSubtitle') || 'Sem custos, sem compromisso!'} +
+
+
+ +
+
+ + {t('cancellation.offerExplanation') || 'Por que estamos fazendo isso?'} +
+

+ {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.'} +

+
+ +
+
+

+ + {t('cancellation.offerDetails') || 'Detalhes:'} 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. +

+
+
+
+ )} + + {/* Step 4: Feedback */} + {step === 4 && ( +
+
+ +
+ {t('cancellation.feedbackTitle') || 'Sua Opinião é Importante'} +
+

+ {t('cancellation.feedbackDescription') || + 'Por favor, compartilhe mais detalhes para nos ajudar a melhorar'} +

+
+ +
+
+ +