feat: complete email system redesign with corporate templates

- Redesigned all email templates with professional corporate style
- Created base layout with dark header, status cards, and footer
- Updated: subscription-cancelled, account-activation, welcome, welcome-new-user, due-payments-alert
- Removed emojis and gradients for cleaner look
- Added multi-language support (ES, PT-BR, EN)
- Fixed email delivery (sync instead of queue)
- Fixed PayPal already-cancelled subscription handling
- Cleaned orphan subscriptions from deleted users
This commit is contained in:
marcoitaloesp-ai 2025-12-18 00:44:37 +00:00 committed by GitHub
parent 984855e36c
commit 6292b62315
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 3741 additions and 975 deletions

View File

@ -102,7 +102,11 @@ private function createProduct(PayPalService $paypal, Plan $plan): array
? 'https://api-m.sandbox.paypal.com'
: 'https://api-m.paypal.com';
$response = \Illuminate\Support\Facades\Http::withToken($paypal->getAccessToken())
// Get fresh token for this request
\Illuminate\Support\Facades\Cache::forget('paypal_access_token');
$token = $paypal->getAccessToken();
$response = \Illuminate\Support\Facades\Http::withToken($token)
->post("{$baseUrl}/v1/catalogs/products", [
'name' => "WEBMoney - {$plan->name}",
'description' => $plan->description ?? "Subscription plan for WEBMoney",

View File

@ -4,17 +4,21 @@
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\EmailVerificationToken;
use App\Services\UserSetupService;
use App\Mail\AccountActivationMail;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
class AuthController extends Controller
{
/**
* Register a new user
* Register a new user (without auto-login - requires PayPal payment and email activation)
*/
public function register(Request $request): JsonResponse
{
@ -23,6 +27,7 @@ public function register(Request $request): JsonResponse
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed',
'plan_id' => 'nullable|exists:plans,id',
], [
'name.required' => 'El nombre es obligatorio',
'email.required' => 'El email es obligatorio',
@ -41,32 +46,40 @@ public function register(Request $request): JsonResponse
], 422);
}
// Create user WITHOUT email verification (will be verified after PayPal payment)
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
'email_verified_at' => null, // NOT verified yet
]);
// Criar categorias e dados padrão para o novo usuário
$setupService = new UserSetupService();
$setupService->setupNewUser($user->id);
// DISABLED: Create default categories and data for the new user
// TODO: Re-enable when category templates are ready
// $setupService = new UserSetupService();
// $setupService->setupNewUser($user->id);
$token = $user->createToken('auth-token')->plainTextToken;
// Create a temporary token for PayPal flow (expires in 1 hour)
$tempToken = $user->createToken('registration-flow', ['registration'])->plainTextToken;
return response()->json([
'success' => true,
'message' => 'Usuario registrado exitosamente',
'message' => 'Usuario registrado. Procede al pago para activar tu cuenta.',
'data' => [
'user' => [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'email_verified' => false,
],
'token' => $token,
'token' => $tempToken,
'requires_payment' => true,
'requires_activation' => true,
]
], 201);
} catch (\Exception $e) {
Log::error('Registration error: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Error al registrar usuario',
@ -98,14 +111,42 @@ public function login(Request $request): JsonResponse
], 422);
}
if (!Auth::attempt($request->only('email', 'password'))) {
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
return response()->json([
'success' => false,
'message' => 'Credenciales incorrectas'
], 401);
}
$user = User::where('email', $request->email)->first();
// Check if email is verified (account activated)
if (!$user->email_verified_at) {
return response()->json([
'success' => false,
'message' => 'Tu cuenta aún no está activada. Revisa tu email para activarla.',
'error' => 'email_not_verified',
'data' => [
'email_verified' => false,
'can_resend' => true,
]
], 403);
}
// Check if user has an active subscription
$hasActiveSubscription = $user->subscriptions()->active()->exists();
if (!$hasActiveSubscription) {
return response()->json([
'success' => false,
'message' => 'No tienes una suscripción activa. Por favor, completa el pago.',
'error' => 'no_subscription',
'data' => [
'email_verified' => true,
'has_subscription' => false,
]
], 403);
}
$token = $user->createToken('auth-token')->plainTextToken;
return response()->json([
@ -116,12 +157,14 @@ public function login(Request $request): JsonResponse
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'email_verified' => true,
],
'token' => $token,
]
], 200);
} catch (\Exception $e) {
Log::error('Login error: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Error al iniciar sesión',
@ -130,6 +173,175 @@ public function login(Request $request): JsonResponse
}
}
/**
* Activate account via email token
*/
public function activateAccount(Request $request): JsonResponse
{
try {
$validator = Validator::make($request->all(), [
'token' => 'required|string|size:64',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Token inválido',
], 422);
}
$verificationToken = EmailVerificationToken::findValid($request->token);
if (!$verificationToken) {
return response()->json([
'success' => false,
'message' => 'El enlace de activación es inválido o ha expirado.',
'error' => 'invalid_token',
], 400);
}
$user = $verificationToken->user;
// Activate the user
$user->update(['email_verified_at' => now()]);
$verificationToken->markAsUsed();
// Create auth token for immediate login
$authToken = $user->createToken('auth-token')->plainTextToken;
return response()->json([
'success' => true,
'message' => '¡Tu cuenta ha sido activada! Ya puedes acceder al sistema.',
'data' => [
'user' => [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'email_verified' => true,
],
'token' => $authToken,
]
]);
} catch (\Exception $e) {
Log::error('Activation error: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Error al activar la cuenta',
], 500);
}
}
/**
* Resend activation email
*/
public function resendActivation(Request $request): JsonResponse
{
try {
$validator = Validator::make($request->all(), [
'email' => 'required|email|exists:users,email',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Email no encontrado',
], 404);
}
$user = User::where('email', $request->email)->first();
if ($user->email_verified_at) {
return response()->json([
'success' => false,
'message' => 'Esta cuenta ya está activada',
], 400);
}
// Check if user has completed payment
$subscription = $user->subscriptions()->active()->first();
if (!$subscription) {
return response()->json([
'success' => false,
'message' => 'Primero debes completar el pago de tu suscripción',
], 400);
}
// Create new verification token and send email
$verificationToken = EmailVerificationToken::createForUser($user);
$frontendUrl = config('app.frontend_url', 'https://webmoney.cnxifly.com');
$activationUrl = "{$frontendUrl}/activate?token={$verificationToken->token}";
Mail::to($user->email)->send(new AccountActivationMail(
$user,
$activationUrl,
$subscription->plan->name
));
return response()->json([
'success' => true,
'message' => 'Email de activación reenviado. Revisa tu bandeja de entrada.',
]);
} catch (\Exception $e) {
Log::error('Resend activation error: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Error al reenviar el email',
], 500);
}
}
/**
* Cancel registration - delete unactivated user account
* Used when PayPal payment is canceled or fails
*/
public function cancelRegistration(Request $request): JsonResponse
{
try {
$validator = Validator::make($request->all(), [
'email' => 'required|email|exists:users,email',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Usuario no encontrado',
], 404);
}
$user = User::where('email', $request->email)->first();
// Only allow deletion if account is NOT activated
if ($user->email_verified_at) {
return response()->json([
'success' => false,
'message' => 'Esta cuenta ya está activada y no puede ser eliminada',
], 400);
}
// Delete associated data
$user->tokens()->delete(); // Delete all tokens
EmailVerificationToken::where('user_id', $user->id)->delete();
$user->subscriptions()->delete();
$user->delete();
Log::info("Unactivated user account deleted: {$request->email}");
return response()->json([
'success' => true,
'message' => 'Registro cancelado. Puedes intentar nuevamente.',
]);
} catch (\Exception $e) {
Log::error('Cancel registration error: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Error al cancelar el registro',
], 500);
}
}
/**
* Logout user (revoke token)
*/

View File

@ -62,6 +62,36 @@ public function store(Request $request): JsonResponse
'keywords.*' => 'string|max:100',
]);
$user = Auth::user();
$plan = $user->currentPlan();
// Check subcategory limit if parent_id is provided
if (!empty($validated['parent_id']) && $plan) {
$limits = $plan->limits ?? [];
$subcategoryLimit = $limits['subcategories'] ?? null;
if ($subcategoryLimit !== null) {
$currentSubcategories = Category::where('user_id', $user->id)
->whereNotNull('parent_id')
->count();
if ($currentSubcategories >= $subcategoryLimit) {
return response()->json([
'success' => false,
'message' => "Has alcanzado el límite de {$subcategoryLimit} subcategorías de tu plan. Actualiza a Pro para subcategorías ilimitadas.",
'error' => 'plan_limit_exceeded',
'data' => [
'resource' => 'subcategories',
'current' => $currentSubcategories,
'limit' => $subcategoryLimit,
'plan' => $plan->name,
'upgrade_url' => '/pricing',
],
], 403);
}
}
}
// Verificar se parent_id pertence ao usuário
if (!empty($validated['parent_id'])) {
$parent = Category::where('user_id', Auth::id())

View File

@ -10,16 +10,25 @@
class PlanController extends Controller
{
/**
* List all active plans for pricing page
* List all plans for pricing page (including coming soon)
*/
public function index(): JsonResponse
{
$plans = Plan::active()->ordered()->get();
// Get active plans
$activePlans = Plan::active()->ordered()->get();
// Get coming soon plans (inactive but should be displayed)
$comingSoonPlans = Plan::where('is_active', false)
->whereIn('slug', ['business'])
->ordered()
->get();
$allPlans = $activePlans->merge($comingSoonPlans);
return response()->json([
'success' => true,
'data' => [
'plans' => $plans->map(function ($plan) {
'plans' => $allPlans->map(function ($plan) {
return [
'id' => $plan->id,
'slug' => $plan->slug,
@ -35,6 +44,8 @@ public function index(): JsonResponse
'limits' => $plan->limits,
'is_free' => $plan->is_free,
'is_featured' => $plan->is_featured,
'is_active' => $plan->is_active,
'coming_soon' => !$plan->is_active,
'has_trial' => $plan->has_trial,
'savings_percent' => $plan->savings_percent,
];

View File

@ -9,6 +9,10 @@
use App\Services\PayPalService;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Models\EmailVerificationToken;
use App\Mail\AccountActivationMail;
use App\Mail\SubscriptionCancelledMail;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
@ -50,18 +54,46 @@ public function status(Request $request): JsonResponse
}
}
// Calculate guarantee period info (7 days from subscription creation)
$withinGuaranteePeriod = false;
$guaranteeDaysRemaining = 0;
$guaranteeEndsAt = null;
if ($subscription && $subscription->created_at) {
$guaranteeEndsAt = $subscription->created_at->copy()->addDays(7);
$withinGuaranteePeriod = now()->lt($guaranteeEndsAt);
$guaranteeDaysRemaining = $withinGuaranteePeriod
? (int) ceil(now()->diffInHours($guaranteeEndsAt) / 24)
: 0;
}
return response()->json([
'success' => true,
'data' => [
'has_subscription' => $subscription !== null,
'subscription' => $subscription ? [
'id' => $subscription->id,
'status' => $subscription->status,
'trial_ends_at' => $subscription->trial_ends_at,
'current_period_start' => $subscription->current_period_start,
'current_period_end' => $subscription->current_period_end,
'canceled_at' => $subscription->canceled_at,
'ends_at' => $subscription->ends_at,
'on_trial' => $subscription->isOnTrial(),
'on_grace_period' => $subscription->onGracePeriod(),
] : null,
'on_trial' => $subscription?->isOnTrial() ?? false,
'trial_ends_at' => $subscription?->trial_ends_at,
'days_until_trial_ends' => $subscription?->days_until_trial_ends,
'current_period_start' => $subscription?->current_period_start,
'current_period_end' => $subscription?->current_period_end,
'status' => $subscription?->status,
'status_label' => $subscription?->status_label,
'canceled_at' => $subscription?->canceled_at,
'on_grace_period' => $subscription?->onGracePeriod() ?? false,
'within_guarantee_period' => $withinGuaranteePeriod,
'guarantee_days_remaining' => $guaranteeDaysRemaining,
'guarantee_ends_at' => $guaranteeEndsAt?->toIso8601String(),
'plan' => $currentPlan ? [
'id' => $currentPlan->id,
'slug' => $currentPlan->slug,
@ -166,6 +198,116 @@ public function subscribe(Request $request): JsonResponse
]);
}
/**
* Start subscription for newly registered user (public - no auth required)
* Used immediately after registration, before user is logged in
*/
public function startSubscription(Request $request): JsonResponse
{
$request->validate([
'plan_id' => 'required|integer|exists:plans,id',
'user_email' => 'required|email|exists:users,email',
]);
$user = \App\Models\User::where('email', $request->user_email)->first();
if (!$user) {
return response()->json([
'success' => false,
'message' => 'User not found',
], 404);
}
// Verify user hasn't already verified email (prevent abuse)
if ($user->email_verified_at) {
return response()->json([
'success' => false,
'message' => 'User already activated. Please login.',
], 400);
}
$plan = Plan::where('id', $request->plan_id)->where('is_active', true)->first();
if (!$plan) {
return response()->json([
'success' => false,
'message' => 'Plan not found or inactive',
], 404);
}
// All plans are paid now - no free subscriptions during registration
if ($plan->is_free || $plan->price <= 0) {
return response()->json([
'success' => false,
'message' => 'All plans require payment',
], 400);
}
// Check if PayPal is configured
if (!$this->paypal->isConfigured()) {
return response()->json([
'success' => false,
'message' => 'Payment gateway not configured',
], 500);
}
// Check if plan has PayPal plan ID
if (!$plan->paypal_plan_id) {
return response()->json([
'success' => false,
'message' => 'Plan not configured for payments yet',
], 500);
}
// Create PayPal subscription
$frontendUrl = config('app.frontend_url', 'https://webmoney.cnxifly.com');
$returnUrl = "{$frontendUrl}/payment-success?user_email={$user->email}&plan={$plan->slug}";
$cancelUrl = "{$frontendUrl}/register?payment_canceled=true";
$paypalSubscription = $this->paypal->createSubscription($plan, $returnUrl, $cancelUrl);
if (!$paypalSubscription) {
return response()->json([
'success' => false,
'message' => 'Failed to create subscription',
], 500);
}
// Find approve link
$approveUrl = collect($paypalSubscription['links'] ?? [])
->firstWhere('rel', 'approve')['href'] ?? null;
if (!$approveUrl) {
return response()->json([
'success' => false,
'message' => 'No approval URL received',
], 500);
}
// Create pending subscription in our DB
$subscription = Subscription::create([
'user_id' => $user->id,
'plan_id' => $plan->id,
'status' => Subscription::STATUS_PENDING,
'paypal_subscription_id' => $paypalSubscription['id'],
'paypal_status' => $paypalSubscription['status'],
'paypal_data' => $paypalSubscription,
'price_paid' => $plan->price,
'currency' => $plan->currency,
]);
\Illuminate\Support\Facades\Log::info("Started subscription for user {$user->email}, PayPal ID: {$paypalSubscription['id']}");
return response()->json([
'success' => true,
'data' => [
'subscription_id' => $subscription->id,
'paypal_subscription_id' => $paypalSubscription['id'],
'approve_url' => $approveUrl,
],
]);
}
/**
* Subscribe to free plan
*/
@ -246,13 +388,139 @@ public function confirm(Request $request): JsonResponse
)->markAsPaid($paypalData['id'] ?? null);
}
// Send activation email if subscription is active and user not verified yet
$activationSent = false;
if ($subscription->isActive() && !$user->email_verified_at) {
try {
$verificationToken = EmailVerificationToken::createForUser($user);
$frontendUrl = config('app.frontend_url', 'https://webmoney.cnxifly.com');
$activationUrl = "{$frontendUrl}/activate?token={$verificationToken->token}";
Mail::to($user->email)->send(new AccountActivationMail(
$user,
$activationUrl,
$subscription->plan->name
));
$activationSent = true;
Log::info("Activation email sent to {$user->email}");
} catch (\Exception $e) {
Log::error("Failed to send activation email: " . $e->getMessage());
}
}
return response()->json([
'success' => true,
'message' => 'Subscription confirmed',
'message' => $activationSent
? 'Suscripción confirmada. Revisa tu email para activar tu cuenta.'
: 'Subscription confirmed',
'data' => [
'status' => $subscription->status,
'status_label' => $subscription->status_label,
'plan' => $subscription->plan->name,
'activation_email_sent' => $activationSent,
'email_verified' => $user->email_verified_at !== null,
],
]);
}
/**
* Confirm subscription after PayPal approval (public - no auth required)
* Used for new user registration flow
*/
public function confirmPublic(Request $request): JsonResponse
{
$request->validate([
'subscription_id' => 'required|string',
'user_email' => 'required|email',
]);
$user = \App\Models\User::where('email', $request->user_email)->first();
if (!$user) {
return response()->json([
'success' => false,
'message' => 'User not found',
], 404);
}
$subscription = Subscription::where('paypal_subscription_id', $request->subscription_id)
->where('user_id', $user->id)
->first();
if (!$subscription) {
return response()->json([
'success' => false,
'message' => 'Subscription not found',
], 404);
}
// Get subscription details from PayPal
$paypalData = $this->paypal->getSubscription($request->subscription_id);
if (!$paypalData) {
return response()->json([
'success' => false,
'message' => 'Failed to verify subscription with PayPal',
], 500);
}
\Illuminate\Support\Facades\Log::info("PayPal confirmation for {$user->email}: " . json_encode($paypalData));
// Update subscription based on PayPal status
$this->updateSubscriptionFromPayPal($subscription, $paypalData);
// Cancel other active subscriptions
$user->subscriptions()
->where('id', '!=', $subscription->id)
->active()
->update([
'status' => Subscription::STATUS_CANCELED,
'canceled_at' => now(),
'ends_at' => now(),
'cancel_reason' => 'Replaced by new subscription',
]);
// Create invoice for the subscription
if ($subscription->isActive() && !$subscription->plan->is_free) {
Invoice::createForSubscription(
$subscription,
Invoice::REASON_SUBSCRIPTION_CREATE,
"{$subscription->plan->name} - Nueva suscripción"
)->markAsPaid($paypalData['id'] ?? null);
}
// Send activation email if subscription is active and user not verified yet
$activationSent = false;
if ($subscription->isActive() && !$user->email_verified_at) {
try {
$verificationToken = EmailVerificationToken::createForUser($user);
$frontendUrl = config('app.frontend_url', 'https://webmoney.cnxifly.com');
$activationUrl = "{$frontendUrl}/activate?token={$verificationToken->token}";
Mail::to($user->email)->send(new AccountActivationMail(
$user,
$activationUrl,
$subscription->plan->name
));
$activationSent = true;
\Illuminate\Support\Facades\Log::info("Activation email sent to {$user->email}");
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error("Failed to send activation email: " . $e->getMessage());
}
}
return response()->json([
'success' => true,
'message' => $activationSent
? 'Pagamento confirmado! Verifique seu email para ativar sua conta.'
: 'Payment confirmed',
'data' => [
'status' => $subscription->status,
'status_label' => $subscription->status_label,
'plan' => $subscription->plan->name,
'activation_email_sent' => $activationSent,
],
]);
}
@ -265,6 +533,7 @@ public function cancel(Request $request): JsonResponse
$request->validate([
'reason' => 'nullable|string|max:500',
'immediately' => 'nullable|boolean',
'request_refund' => 'nullable|boolean',
]);
$user = $request->user();
@ -277,35 +546,108 @@ public function cancel(Request $request): JsonResponse
], 404);
}
// If it's a paid plan, cancel on PayPal
if ($subscription->paypal_subscription_id && !$subscription->plan->is_free) {
$canceled = $this->paypal->cancelSubscription(
$subscription->paypal_subscription_id,
$request->reason ?? 'User requested cancellation'
);
$refundResult = null;
$isWithinGuaranteePeriod = false;
if (!$canceled) {
return response()->json([
'success' => false,
'message' => 'Failed to cancel subscription on PayPal',
], 500);
// Check if within 7-day guarantee period (from subscription creation date)
if ($subscription->created_at) {
$daysSinceCreation = now()->diffInDays($subscription->created_at);
$isWithinGuaranteePeriod = $daysSinceCreation <= 7;
}
// If it's a paid plan with PayPal subscription
if ($subscription->paypal_subscription_id && !$subscription->plan->is_free) {
// If user requests refund and is within guarantee period, cancel and refund
if ($request->boolean('request_refund') && $isWithinGuaranteePeriod) {
$refundResult = $this->paypal->cancelAndRefund(
$subscription->paypal_subscription_id,
$request->reason ?? 'Refund within 7-day guarantee period'
);
if (!$refundResult['canceled']) {
return response()->json([
'success' => false,
'message' => 'Failed to cancel subscription on PayPal',
], 500);
}
Log::info('Subscription canceled with refund', [
'user_id' => $user->id,
'subscription_id' => $subscription->id,
'refund_result' => $refundResult,
]);
} else {
// Just cancel without refund
$canceled = $this->paypal->cancelSubscription(
$subscription->paypal_subscription_id,
$request->reason ?? 'User requested cancellation'
);
if (!$canceled) {
return response()->json([
'success' => false,
'message' => 'Failed to cancel subscription on PayPal',
], 500);
}
}
}
// Cancel in our DB
// Cancel in our DB - always immediately when refunded
$cancelImmediately = $request->boolean('request_refund') || $request->boolean('immediately', false);
$subscription->cancel(
$request->reason,
$request->boolean('immediately', false)
$cancelImmediately
);
// Send cancellation confirmation email
try {
$wasRefunded = $refundResult && $refundResult['refunded'];
$refundAmount = $wasRefunded && isset($refundResult['refund_amount'])
? number_format($refundResult['refund_amount'], 2) . ' ' . ($subscription->plan->currency ?? 'EUR')
: null;
Mail::to($user->email)->send(new SubscriptionCancelledMail(
$user,
$subscription->plan->name,
$wasRefunded,
$refundAmount
));
Log::info('Cancellation email sent', [
'user_id' => $user->id,
'email' => $user->email,
'refunded' => $wasRefunded,
]);
} catch (\Exception $e) {
Log::error('Failed to send cancellation email', [
'user_id' => $user->id,
'error' => $e->getMessage(),
]);
// Don't fail the cancellation just because email failed
}
// Build response message
$message = 'Subscription canceled';
if ($refundResult && $refundResult['refunded']) {
$message = 'Subscription canceled and refund processed';
} elseif ($refundResult && !$refundResult['refunded']) {
$message = 'Subscription canceled. Refund could not be processed automatically - please contact support.';
} elseif ($cancelImmediately) {
$message = 'Subscription canceled immediately';
} else {
$message = 'Subscription will be canceled at period end';
}
return response()->json([
'success' => true,
'message' => $request->boolean('immediately')
? 'Subscription canceled immediately'
: 'Subscription will be canceled at period end',
'message' => $message,
'data' => [
'status' => $subscription->status,
'ends_at' => $subscription->ends_at,
'refunded' => $refundResult['refunded'] ?? false,
'refund_id' => $refundResult['refund_id'] ?? null,
'within_guarantee_period' => $isWithinGuaranteePeriod,
],
]);
}
@ -547,11 +889,39 @@ private function updateSubscriptionFromPayPal(Subscription $subscription, array
switch ($status) {
case 'ACTIVE':
$subscription->status = Subscription::STATUS_ACTIVE;
if (isset($paypalData['billing_info']['next_billing_time'])) {
$subscription->current_period_end = Carbon::parse($paypalData['billing_info']['next_billing_time']);
// Always set current_period_start to now() on activation if not set
if (!$subscription->current_period_start) {
$subscription->current_period_start = now();
}
// Calculate period end based on plan interval
$plan = $subscription->plan;
if ($plan) {
$periodEnd = now();
if ($plan->interval === 'year') {
$periodEnd = now()->addYear();
} else {
$periodEnd = now()->addMonth();
}
$subscription->current_period_end = $periodEnd;
}
// Only use PayPal dates if they make sense (within reasonable range)
if (isset($paypalData['billing_info']['last_payment']['time'])) {
$subscription->current_period_start = Carbon::parse($paypalData['billing_info']['last_payment']['time']);
$lastPayment = Carbon::parse($paypalData['billing_info']['last_payment']['time']);
// Accept if within last 30 days
if ($lastPayment->gte(now()->subDays(30)) && $lastPayment->lte(now()->addDay())) {
$subscription->current_period_start = $lastPayment;
}
}
if (isset($paypalData['billing_info']['next_billing_time'])) {
$nextBilling = Carbon::parse($paypalData['billing_info']['next_billing_time']);
// Accept if within next 13 months (reasonable for monthly/yearly plans)
if ($nextBilling->gt(now()) && $nextBilling->lt(now()->addMonths(13))) {
$subscription->current_period_end = $nextBilling;
}
}
break;

View File

@ -14,6 +14,7 @@ class CheckPlanLimits
protected array $resourceLimits = [
'accounts' => 'accounts',
'categories' => 'categories',
'subcategories' => 'subcategories',
'budgets' => 'budgets',
'transactions' => 'transactions',
'goals' => 'goals',
@ -80,7 +81,8 @@ protected function getCurrentCount($user, string $resource): int
{
return match ($resource) {
'accounts' => $user->accounts()->count(),
'categories' => $user->categories()->count(),
'categories' => $user->categories()->whereNull('parent_id')->count(),
'subcategories' => $user->categories()->whereNotNull('parent_id')->count(),
'budgets' => $user->budgets()->count(),
'transactions' => $user->transactions()->count(),
'goals' => $user->goals()->count(),
@ -96,6 +98,7 @@ protected function getLimitMessage(string $resource, int $limit): string
$messages = [
'accounts' => "Has alcanzado el límite de {$limit} cuenta(s) de tu plan. Actualiza a Pro para cuentas ilimitadas.",
'categories' => "Has alcanzado el límite de {$limit} categorías de tu plan. Actualiza a Pro para categorías ilimitadas.",
'subcategories' => "Has alcanzado el límite de {$limit} subcategorías de tu plan. Actualiza a Pro para subcategorías ilimitadas.",
'budgets' => "Has alcanzado el límite de {$limit} presupuesto(s) de tu plan. Actualiza a Pro para presupuestos ilimitados.",
'transactions' => "Has alcanzado el límite de {$limit} transacciones de tu plan. Actualiza a Pro para transacciones ilimitadas.",
'goals' => "Has alcanzado el límite de {$limit} meta(s) de tu plan. Actualiza a Pro para metas ilimitadas.",

View File

@ -0,0 +1,35 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class AccountActivationMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(
public User $user,
public string $activationUrl,
public string $planName
) {}
public function envelope(): Envelope
{
return new Envelope(
subject: '🎉 Ativa a tua conta WEBMoney - Pagamento confirmado!',
);
}
public function content(): Content
{
return new Content(
view: 'emails.account-activation',
);
}
}

View File

@ -0,0 +1,122 @@
<?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\Mail\Mailables\Address;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App;
class SubscriptionCancelledMail extends Mailable
{
use Queueable, SerializesModels;
public User $user;
public string $planName;
public bool $wasRefunded;
public ?string $refundAmount;
public string $userLocale;
/**
* Create a new message instance.
*/
public function __construct(
User $user,
string $planName,
bool $wasRefunded = false,
?string $refundAmount = null
) {
$this->user = $user;
$this->planName = $planName;
$this->wasRefunded = $wasRefunded;
$this->refundAmount = $refundAmount;
$this->userLocale = $user->locale ?? $user->language ?? 'es';
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
// Set locale for translations
App::setLocale($this->userLocale);
$subject = $this->getSubject();
return new Envelope(
from: new Address('no-reply@cnxifly.com', 'WEBMoney - ConneXiFly'),
replyTo: [
new Address('support@cnxifly.com', $this->getSupportName()),
],
subject: $subject,
tags: ['subscription', 'cancellation', $this->wasRefunded ? 'refund' : 'no-refund'],
metadata: [
'user_id' => $this->user->id,
'user_email' => $this->user->email,
'plan' => $this->planName,
'refunded' => $this->wasRefunded,
],
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
// Set locale for translations
App::setLocale($this->userLocale);
return new Content(
view: 'emails.subscription-cancelled',
text: 'emails.subscription-cancelled-text',
with: [
'userName' => $this->user->name,
'userEmail' => $this->user->email,
'planName' => $this->planName,
'wasRefunded' => $this->wasRefunded,
'refundAmount' => $this->refundAmount,
'locale' => $this->userLocale,
],
);
}
/**
* Get the subject based on locale
*/
private function getSubject(): string
{
$subjects = [
'es' => $this->wasRefunded
? 'Confirmación de cancelación y reembolso - WEBMoney'
: 'Confirmación de cancelación de suscripción - WEBMoney',
'pt-BR' => $this->wasRefunded
? 'Confirmação de cancelamento e reembolso - WEBMoney'
: 'Confirmação de cancelamento de assinatura - WEBMoney',
'en' => $this->wasRefunded
? 'Cancellation and Refund Confirmation - WEBMoney'
: 'Subscription Cancellation Confirmation - WEBMoney',
];
return $subjects[$this->userLocale] ?? $subjects['es'];
}
/**
* Get support name based on locale
*/
private function getSupportName(): string
{
$names = [
'es' => 'Soporte WEBMoney',
'pt-BR' => 'Suporte WEBMoney',
'en' => 'WEBMoney Support',
];
return $names[$this->userLocale] ?? $names['es'];
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
class EmailVerificationToken extends Model
{
protected $fillable = [
'user_id',
'token',
'expires_at',
'used_at',
];
protected $casts = [
'expires_at' => 'datetime',
'used_at' => 'datetime',
];
/**
* Relationship: User
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Create a new verification token for user
*/
public static function createForUser(User $user, int $expiresInHours = 24): self
{
// Invalidate existing tokens
self::where('user_id', $user->id)->whereNull('used_at')->delete();
return self::create([
'user_id' => $user->id,
'token' => Str::random(64),
'expires_at' => now()->addHours($expiresInHours),
]);
}
/**
* Find valid token
*/
public static function findValid(string $token): ?self
{
return self::where('token', $token)
->where('expires_at', '>', now())
->whereNull('used_at')
->first();
}
/**
* Check if token is valid
*/
public function isValid(): bool
{
return $this->expires_at > now() && $this->used_at === null;
}
/**
* Mark token as used
*/
public function markAsUsed(): void
{
$this->update(['used_at' => now()]);
}
}

View File

@ -12,6 +12,7 @@ class Subscription extends Model
{
use HasFactory;
const STATUS_PENDING = 'pending';
const STATUS_TRIALING = 'trialing';
const STATUS_ACTIVE = 'active';
const STATUS_PAST_DUE = 'past_due';

View File

@ -252,7 +252,15 @@ public function cancelSubscription(string $subscriptionId, string $reason = 'Use
return true;
}
Log::error('PayPal cancel subscription failed', ['response' => $response->json()]);
// Check if subscription is already cancelled - treat as success
$responseData = $response->json();
if (isset($responseData['details'][0]['issue']) &&
$responseData['details'][0]['issue'] === 'SUBSCRIPTION_STATUS_INVALID') {
Log::info('PayPal subscription already cancelled', ['subscription_id' => $subscriptionId]);
return true;
}
Log::error('PayPal cancel subscription failed', ['response' => $responseData]);
return false;
} catch (\Exception $e) {
Log::error('PayPal cancel subscription exception', ['error' => $e->getMessage()]);
@ -343,6 +351,124 @@ public function getSubscriptionTransactions(string $subscriptionId, string $star
}
}
/**
* Refund a capture (payment)
*/
public function refundCapture(string $captureId, ?float $amount = null, ?string $currency = 'EUR', string $note = 'Refund within 7-day guarantee period'): ?array
{
$token = $this->getAccessToken();
if (!$token) return null;
try {
$body = [
'note_to_payer' => $note,
];
// If amount specified, do partial refund; otherwise full refund
if ($amount !== null) {
$body['amount'] = [
'value' => number_format($amount, 2, '.', ''),
'currency_code' => $currency,
];
}
$response = Http::withToken($token)
->post("{$this->baseUrl}/v2/payments/captures/{$captureId}/refund", $body);
if ($response->successful()) {
Log::info('PayPal refund successful', ['capture_id' => $captureId, 'response' => $response->json()]);
return $response->json();
}
Log::error('PayPal refund failed', ['capture_id' => $captureId, 'response' => $response->json()]);
return null;
} catch (\Exception $e) {
Log::error('PayPal refund exception', ['error' => $e->getMessage()]);
return null;
}
}
/**
* Get the last transaction/capture for a subscription to refund
*/
public function getLastSubscriptionCapture(string $subscriptionId): ?string
{
$token = $this->getAccessToken();
if (!$token) return null;
try {
// Get transactions from last 30 days
$startTime = now()->subDays(30)->toIso8601String();
$endTime = now()->toIso8601String();
$response = Http::withToken($token)
->get("{$this->baseUrl}/v1/billing/subscriptions/{$subscriptionId}/transactions", [
'start_time' => $startTime,
'end_time' => $endTime,
]);
if ($response->successful()) {
$transactions = $response->json('transactions') ?? [];
// Find the most recent COMPLETED transaction
foreach ($transactions as $transaction) {
if (($transaction['status'] ?? '') === 'COMPLETED' && !empty($transaction['id'])) {
Log::info('Found capture for refund', ['subscription_id' => $subscriptionId, 'capture_id' => $transaction['id']]);
return $transaction['id'];
}
}
}
Log::warning('No capture found for subscription', ['subscription_id' => $subscriptionId]);
return null;
} catch (\Exception $e) {
Log::error('PayPal get capture exception', ['error' => $e->getMessage()]);
return null;
}
}
/**
* Cancel subscription and refund if within guarantee period
*/
public function cancelAndRefund(string $subscriptionId, string $reason = 'Refund within 7-day guarantee'): array
{
$result = [
'canceled' => false,
'refunded' => false,
'refund_id' => null,
'refund_amount' => null,
'error' => null,
];
// First, get capture ID for refund
$captureId = $this->getLastSubscriptionCapture($subscriptionId);
// Cancel the subscription
$canceled = $this->cancelSubscription($subscriptionId, $reason);
$result['canceled'] = $canceled;
if (!$canceled) {
$result['error'] = 'Failed to cancel subscription';
return $result;
}
// Process refund if we have a capture
if ($captureId) {
$refund = $this->refundCapture($captureId, null, 'EUR', $reason);
if ($refund) {
$result['refunded'] = true;
$result['refund_id'] = $refund['id'] ?? null;
$result['refund_amount'] = $refund['amount']['value'] ?? null;
} else {
$result['error'] = 'Subscription canceled but refund failed';
}
} else {
$result['error'] = 'Subscription canceled but no payment found to refund';
}
return $result;
}
/**
* Check if PayPal is configured
*/

View File

@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('email_verification_tokens', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('token', 64)->unique();
$table->timestamp('expires_at');
$table->timestamp('used_at')->nullable();
$table->timestamps();
$table->index(['token', 'expires_at']);
});
}
public function down(): void
{
Schema::dropIfExists('email_verification_tokens');
}
};

View File

@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Add 'pending' to status enum
DB::statement("ALTER TABLE subscriptions MODIFY COLUMN status ENUM('pending', 'trialing', 'active', 'past_due', 'canceled', 'expired') NOT NULL DEFAULT 'trialing'");
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Remove 'pending' from status enum
DB::statement("ALTER TABLE subscriptions MODIFY COLUMN status ENUM('trialing', 'active', 'past_due', 'canceled', 'expired') NOT NULL DEFAULT 'trialing'");
}
};

View File

@ -0,0 +1,166 @@
@extends('emails.layouts.base')
@php $locale = $locale ?? 'pt-BR'; @endphp
@section('title')
@if($locale === 'pt-BR')
Ativação de Conta
@elseif($locale === 'en')
Account Activation
@else
Activación de Cuenta
@endif
@endsection
@section('content')
@if($locale === 'pt-BR')
{{-- Portuguese (Brazil) --}}
<p class="greeting">Olá, {{ $user->name }}</p>
<div class="content">
<p>O seu pagamento foi processado com sucesso e a sua subscrição do plano <strong>{{ $planName }}</strong> está ativa.</p>
<div class="status-card success">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="52" valign="top">
<div style="width: 40px; height: 40px; background-color: #c6f6d5; border-radius: 50%; text-align: center; line-height: 40px; font-size: 18px;"></div>
</td>
<td valign="top">
<p class="status-title">Pagamento Confirmado</p>
</td>
</tr>
</table>
<p style="margin-top: 16px;">A sua subscrição está pronta para ser ativada.</p>
</div>
<div class="status-card info">
<p class="status-title" style="color: #2d3748; margin-bottom: 16px;">Detalhes da Subscrição</p>
<ul class="info-list">
<li><strong>Plano:</strong> {{ $planName }}</li>
<li><strong>Email:</strong> {{ $user->email }}</li>
</ul>
</div>
<p>Para começar a utilizar o WEBMoney, ative a sua conta clicando no botão abaixo:</p>
<div class="btn-container">
<a href="{{ $activationUrl }}" class="btn btn-primary">ATIVAR CONTA</a>
</div>
<div class="status-card warning">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="52" valign="top">
<div style="width: 40px; height: 40px; background-color: #feebc8; border-radius: 50%; text-align: center; line-height: 40px; font-size: 18px;">!</div>
</td>
<td valign="top">
<p class="status-title">Validade do Link</p>
</td>
</tr>
</table>
<p style="margin-top: 12px;">Este link é válido por 24 horas. Após este período, será necessário solicitar um novo link de ativação.</p>
</div>
</div>
@elseif($locale === 'en')
{{-- English --}}
<p class="greeting">Hello, {{ $user->name }}</p>
<div class="content">
<p>Your payment has been successfully processed and your <strong>{{ $planName }}</strong> subscription is active.</p>
<div class="status-card success">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="52" valign="top">
<div style="width: 40px; height: 40px; background-color: #c6f6d5; border-radius: 50%; text-align: center; line-height: 40px; font-size: 18px;"></div>
</td>
<td valign="top">
<p class="status-title">Payment Confirmed</p>
</td>
</tr>
</table>
<p style="margin-top: 16px;">Your subscription is ready to be activated.</p>
</div>
<div class="status-card info">
<p class="status-title" style="color: #2d3748; margin-bottom: 16px;">Subscription Details</p>
<ul class="info-list">
<li><strong>Plan:</strong> {{ $planName }}</li>
<li><strong>Email:</strong> {{ $user->email }}</li>
</ul>
</div>
<p>To start using WEBMoney, activate your account by clicking the button below:</p>
<div class="btn-container">
<a href="{{ $activationUrl }}" class="btn btn-primary">ACTIVATE ACCOUNT</a>
</div>
<div class="status-card warning">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="52" valign="top">
<div style="width: 40px; height: 40px; background-color: #feebc8; border-radius: 50%; text-align: center; line-height: 40px; font-size: 18px;">!</div>
</td>
<td valign="top">
<p class="status-title">Link Validity</p>
</td>
</tr>
</table>
<p style="margin-top: 12px;">This link is valid for 24 hours. After this period, you will need to request a new activation link.</p>
</div>
</div>
@else
{{-- Spanish (default) --}}
<p class="greeting">Hola, {{ $user->name }}</p>
<div class="content">
<p>Tu pago ha sido procesado con éxito y tu suscripción al plan <strong>{{ $planName }}</strong> está activa.</p>
<div class="status-card success">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="52" valign="top">
<div style="width: 40px; height: 40px; background-color: #c6f6d5; border-radius: 50%; text-align: center; line-height: 40px; font-size: 18px;"></div>
</td>
<td valign="top">
<p class="status-title">Pago Confirmado</p>
</td>
</tr>
</table>
<p style="margin-top: 16px;">Tu suscripción está lista para ser activada.</p>
</div>
<div class="status-card info">
<p class="status-title" style="color: #2d3748; margin-bottom: 16px;">Detalles de la Suscripción</p>
<ul class="info-list">
<li><strong>Plan:</strong> {{ $planName }}</li>
<li><strong>Email:</strong> {{ $user->email }}</li>
</ul>
</div>
<p>Para comenzar a usar WEBMoney, activa tu cuenta haciendo clic en el botón:</p>
<div class="btn-container">
<a href="{{ $activationUrl }}" class="btn btn-primary">ACTIVAR CUENTA</a>
</div>
<div class="status-card warning">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="52" valign="top">
<div style="width: 40px; height: 40px; background-color: #feebc8; border-radius: 50%; text-align: center; line-height: 40px; font-size: 18px;">!</div>
</td>
<td valign="top">
<p class="status-title">Validez del Enlace</p>
</td>
</tr>
</table>
<p style="margin-top: 12px;">Este enlace es válido por 24 horas. Después de este período, deberás solicitar un nuevo enlace de activación.</p>
</div>
</div>
@endif
@endsection

View File

@ -1,388 +1,461 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="pt-BR">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no" />
<title>WEBMoney - Alerta de Pagamentos</title>
<!--[if mso]>
<style type="text/css">
body, table, td {font-family: Arial, Helvetica, sans-serif !important;}
</style>
<![endif]-->
<style type="text/css">
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: #ffffff;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header {
text-align: center;
border-bottom: 2px solid #0f172a;
padding-bottom: 20px;
margin-bottom: 20px;
}
.header h1 {
color: #0f172a;
margin: 0;
font-size: 24px;
}
.summary-box {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
color: white;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.summary-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.summary-row:last-child {
border-bottom: none;
}
.summary-label {
color: #94a3b8;
}
.summary-value {
font-weight: bold;
}
.shortage {
background-color: #dc2626;
color: white;
padding: 15px;
border-radius: 8px;
text-align: center;
margin: 20px 0;
}
.shortage h3 {
margin: 0 0 5px 0;
}
.shortage .amount {
font-size: 28px;
font-weight: bold;
}
.section {
margin: 25px 0;
}
.section-title {
font-size: 18px;
color: #0f172a;
border-bottom: 2px solid #e2e8f0;
padding-bottom: 10px;
margin-bottom: 15px;
}
.item {
background-color: #f8fafc;
border-left: 4px solid #64748b;
padding: 12px 15px;
margin: 10px 0;
border-radius: 0 4px 4px 0;
}
.item.overdue {
border-left-color: #dc2626;
background-color: #fef2f2;
}
.item.tomorrow {
border-left-color: #f59e0b;
background-color: #fffbeb;
}
.item.payable {
border-left-color: #22c55e;
background-color: #f0fdf4;
}
.item.unpayable {
border-left-color: #dc2626;
background-color: #fef2f2;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.item-description {
font-weight: 600;
color: #1e293b;
}
.item-amount {
font-weight: bold;
color: #dc2626;
}
.item-details {
font-size: 13px;
color: #64748b;
margin-top: 5px;
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
}
.badge-overdue {
background-color: #dc2626;
color: white;
}
.badge-tomorrow {
background-color: #f59e0b;
color: white;
}
.badge-ok {
background-color: #22c55e;
color: white;
}
.account-balance {
display: flex;
justify-content: space-between;
padding: 10px 15px;
background-color: #f8fafc;
margin: 5px 0;
border-radius: 4px;
}
.account-name {
font-weight: 500;
}
.balance-positive {
color: #22c55e;
font-weight: bold;
}
.balance-negative {
color: #dc2626;
font-weight: bold;
}
.transfer-suggestion {
background-color: #eff6ff;
border: 1px solid #3b82f6;
border-radius: 8px;
padding: 15px;
margin: 10px 0;
}
.transfer-arrow {
text-align: center;
font-size: 20px;
color: #3b82f6;
}
.footer {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e2e8f0;
color: #64748b;
font-size: 13px;
}
.cta-button {
display: inline-block;
background-color: #3b82f6;
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 6px;
font-weight: bold;
margin: 20px 0;
}
.cta-button:hover {
background-color: #2563eb;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>💰 WEBMoney - Alerta de Pagamentos</h1>
<p>Olá, {{ $userName }}!</p>
</div>
@extends('emails.layouts.base')
<!-- Summary Box -->
<div class="summary-box">
<div class="summary-row">
<span class="summary-label">💳 Saldo Total Disponível</span>
<span class="summary-value">{{ number_format($totalAvailable, 2, ',', '.') }} {{ $currency }}</span>
</div>
<div class="summary-row">
<span class="summary-label">📋 Total a Pagar</span>
<span class="summary-value">{{ number_format($totalDue, 2, ',', '.') }} {{ $currency }}</span>
@php $locale = $locale ?? 'pt-BR'; @endphp
@section('title')
@if($locale === 'pt-BR')
Alerta de Pagamentos
@elseif($locale === 'en')
Payment Alert
@else
Alerta de Pagos
@endif
@endsection
@section('content')
@if($locale === 'pt-BR')
{{-- Portuguese (Brazil) --}}
<p class="greeting">Olá, {{ $user->name }}</p>
<div class="content">
<p>Este é o seu resumo de pagamentos para os próximos dias.</p>
{{-- Summary Box --}}
<div class="status-card info" style="background-color: #1a1a2e; border-color: #4361ee;">
<p class="status-title" style="color: #ffffff; margin-bottom: 16px;">Resumo Financeiro</p>
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="color: #ffffff;">
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.1);">
<span style="color: #94a3b8;">Saldo Disponível</span>
</td>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.1); text-align: right; font-weight: 600;">
{{ $currency }} {{ number_format($totalBalance, 2, ',', '.') }}
</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.1);">
<span style="color: #94a3b8;">Total a Pagar</span>
</td>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.1); text-align: right; font-weight: 600; color: #f87171;">
{{ $currency }} {{ number_format($totalDue, 2, ',', '.') }}
</td>
</tr>
<tr>
<td style="padding: 8px 0;">
<span style="color: #94a3b8;">Pagamentos Pendentes</span>
</td>
<td style="padding: 8px 0; text-align: right; font-weight: 600;">
{{ $totalPayments }}
</td>
</tr>
</table>
</div>
{{-- Shortage Alert --}}
@if($shortage > 0)
<div class="summary-row" style="color: #fca5a5;">
<span class="summary-label">⚠️ Falta</span>
<span class="summary-value">{{ number_format($shortage, 2, ',', '.') }} {{ $currency }}</span>
</div>
@else
<div class="summary-row" style="color: #86efac;">
<span class="summary-label"> Situação</span>
<span class="summary-value">Saldo suficiente!</span>
</div>
<div class="status-card warning" style="background-color: #fef2f2; border-color: #dc2626;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="52" valign="top">
<div style="width: 40px; height: 40px; background-color: #fecaca; border-radius: 50%; text-align: center; line-height: 40px; font-size: 18px; color: #dc2626;">!</div>
</td>
<td valign="top">
<p class="status-title" style="color: #dc2626;">Saldo Insuficiente</p>
</td>
</tr>
</table>
<p style="margin-top: 12px; color: #991b1b; text-align: center;">
<span style="font-size: 28px; font-weight: 700; display: block;">{{ $currency }} {{ number_format($shortage, 2, ',', '.') }}</span>
<span style="font-size: 13px;">em falta para cobrir todos os pagamentos</span>
</p>
</div>
@endif
</div>
@if($shortage > 0)
<div class="shortage">
<h3>⚠️ SALDO INSUFICIENTE</h3>
<div class="amount">-{{ number_format($shortage, 2, ',', '.') }} {{ $currency }}</div>
<p style="margin: 10px 0 0 0; font-size: 14px;">Você não tem saldo suficiente para cobrir todos os pagamentos.</p>
</div>
@endif
{{-- Overdue Payments --}}
@if(count($overduePayments) > 0)
<div style="margin-top: 24px;">
<p style="font-size: 14px; font-weight: 600; color: #dc2626; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 2px solid #fecaca;">
Pagamentos em Atraso
</p>
@foreach($overduePayments as $payment)
<div style="background-color: #fef2f2; border-left: 4px solid #dc2626; padding: 12px 16px; margin: 8px 0; border-radius: 0 4px 4px 0;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td>
<span style="font-weight: 600; color: #1e293b;">{{ $payment['description'] }}</span>
</td>
<td style="text-align: right;">
<span style="font-weight: 700; color: #dc2626;">{{ $currency }} {{ number_format($payment['amount'], 2, ',', '.') }}</span>
</td>
</tr>
<tr>
<td colspan="2" style="padding-top: 4px;">
<span style="font-size: 12px; color: #64748b;">Venceu em {{ \Carbon\Carbon::parse($payment['due_date'])->format('d/m/Y') }}</span>
<span style="display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 10px; font-weight: 700; text-transform: uppercase; background-color: #dc2626; color: white; margin-left: 8px;">ATRASADO</span>
</td>
</tr>
</table>
</div>
@endforeach
</div>
@endif
<!-- Account Balances -->
<div class="section">
<h2 class="section-title">💳 Saldo das Contas</h2>
@foreach($accountBalances as $account)
<div class="account-balance">
<span class="account-name">{{ $account['name'] }}</span>
<span class="{{ $account['balance'] >= 0 ? 'balance-positive' : 'balance-negative' }}">
{{ number_format($account['balance'], 2, ',', '.') }} {{ $account['currency'] }}
</span>
{{-- Tomorrow Payments --}}
@if(count($tomorrowPayments) > 0)
<div style="margin-top: 24px;">
<p style="font-size: 14px; font-weight: 600; color: #f59e0b; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 2px solid #fde68a;">
Vencem Amanhã
</p>
@foreach($tomorrowPayments as $payment)
<div style="background-color: #fffbeb; border-left: 4px solid #f59e0b; padding: 12px 16px; margin: 8px 0; border-radius: 0 4px 4px 0;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td>
<span style="font-weight: 600; color: #1e293b;">{{ $payment['description'] }}</span>
</td>
<td style="text-align: right;">
<span style="font-weight: 700; color: #d97706;">{{ $currency }} {{ number_format($payment['amount'], 2, ',', '.') }}</span>
</td>
</tr>
<tr>
<td colspan="2" style="padding-top: 4px;">
<span style="font-size: 12px; color: #64748b;">{{ \Carbon\Carbon::parse($payment['due_date'])->format('d/m/Y') }}</span>
<span style="display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 10px; font-weight: 700; text-transform: uppercase; background-color: #f59e0b; color: white; margin-left: 8px;">AMANHÃ</span>
</td>
</tr>
</table>
</div>
@endforeach
</div>
@endif
{{-- Upcoming Payments --}}
@if(count($upcomingPayments) > 0)
<div style="margin-top: 24px;">
<p style="font-size: 14px; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 2px solid #e2e8f0;">
Próximos Pagamentos
</p>
@foreach($upcomingPayments as $payment)
<div style="background-color: #f8fafc; border-left: 4px solid #64748b; padding: 12px 16px; margin: 8px 0; border-radius: 0 4px 4px 0;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td>
<span style="font-weight: 600; color: #1e293b;">{{ $payment['description'] }}</span>
</td>
<td style="text-align: right;">
<span style="font-weight: 700; color: #475569;">{{ $currency }} {{ number_format($payment['amount'], 2, ',', '.') }}</span>
</td>
</tr>
<tr>
<td colspan="2" style="padding-top: 4px;">
<span style="font-size: 12px; color: #64748b;">{{ \Carbon\Carbon::parse($payment['due_date'])->format('d/m/Y') }}</span>
</td>
</tr>
</table>
</div>
@endforeach
</div>
@endif
<div class="divider"></div>
<div class="btn-container">
<a href="https://webmoney.cnxifly.com/dashboard" class="btn btn-primary">VER DASHBOARD</a>
</div>
@endforeach
</div>
<!-- Overdue Items -->
@if(count($overdueItems) > 0)
<div class="section">
<h2 class="section-title">🔴 Pagamentos Vencidos ({{ count($overdueItems) }})</h2>
@foreach($overdueItems as $item)
<div class="item overdue">
<div class="item-header">
<span class="item-description">{{ $item['description'] }}</span>
<span class="item-amount">{{ number_format($item['amount'], 2, ',', '.') }} {{ $item['currency'] }}</span>
</div>
<div class="item-details">
<span class="badge badge-overdue">{{ $item['days_overdue'] }} dias de atraso</span>
Venceu em {{ \Carbon\Carbon::parse($item['due_date'])->format('d/m/Y') }}
@if($item['account_name'])
Conta: {{ $item['account_name'] }}
@endif
</div>
@elseif($locale === 'en')
{{-- English --}}
<p class="greeting">Hello, {{ $user->name }}</p>
<div class="content">
<p>Here is your payment summary for the coming days.</p>
{{-- Summary Box --}}
<div class="status-card info" style="background-color: #1a1a2e; border-color: #4361ee;">
<p class="status-title" style="color: #ffffff; margin-bottom: 16px;">Financial Summary</p>
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="color: #ffffff;">
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.1);">
<span style="color: #94a3b8;">Available Balance</span>
</td>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.1); text-align: right; font-weight: 600;">
{{ $currency }} {{ number_format($totalBalance, 2, '.', ',') }}
</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.1);">
<span style="color: #94a3b8;">Total Due</span>
</td>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.1); text-align: right; font-weight: 600; color: #f87171;">
{{ $currency }} {{ number_format($totalDue, 2, '.', ',') }}
</td>
</tr>
<tr>
<td style="padding: 8px 0;">
<span style="color: #94a3b8;">Pending Payments</span>
</td>
<td style="padding: 8px 0; text-align: right; font-weight: 600;">
{{ $totalPayments }}
</td>
</tr>
</table>
</div>
@endforeach
</div>
@endif
<!-- Tomorrow Items -->
@if(count($tomorrowItems) > 0)
<div class="section">
<h2 class="section-title">🟡 Vencem Amanhã ({{ count($tomorrowItems) }})</h2>
@foreach($tomorrowItems as $item)
<div class="item tomorrow">
<div class="item-header">
<span class="item-description">{{ $item['description'] }}</span>
<span class="item-amount">{{ number_format($item['amount'], 2, ',', '.') }} {{ $item['currency'] }}</span>
@if($shortage > 0)
<div class="status-card warning" style="background-color: #fef2f2; border-color: #dc2626;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="52" valign="top">
<div style="width: 40px; height: 40px; background-color: #fecaca; border-radius: 50%; text-align: center; line-height: 40px; font-size: 18px; color: #dc2626;">!</div>
</td>
<td valign="top">
<p class="status-title" style="color: #dc2626;">Insufficient Balance</p>
</td>
</tr>
</table>
<p style="margin-top: 12px; color: #991b1b; text-align: center;">
<span style="font-size: 28px; font-weight: 700; display: block;">{{ $currency }} {{ number_format($shortage, 2, '.', ',') }}</span>
<span style="font-size: 13px;">short to cover all payments</span>
</p>
</div>
<div class="item-details">
<span class="badge badge-tomorrow">Amanhã</span>
{{ \Carbon\Carbon::parse($item['due_date'])->format('d/m/Y') }}
@if($item['account_name'])
Conta: {{ $item['account_name'] }}
@endif
@endif
@if(count($overduePayments) > 0)
<div style="margin-top: 24px;">
<p style="font-size: 14px; font-weight: 600; color: #dc2626; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 2px solid #fecaca;">
Overdue Payments
</p>
@foreach($overduePayments as $payment)
<div style="background-color: #fef2f2; border-left: 4px solid #dc2626; padding: 12px 16px; margin: 8px 0; border-radius: 0 4px 4px 0;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td>
<span style="font-weight: 600; color: #1e293b;">{{ $payment['description'] }}</span>
</td>
<td style="text-align: right;">
<span style="font-weight: 700; color: #dc2626;">{{ $currency }} {{ number_format($payment['amount'], 2, '.', ',') }}</span>
</td>
</tr>
<tr>
<td colspan="2" style="padding-top: 4px;">
<span style="font-size: 12px; color: #64748b;">Due {{ \Carbon\Carbon::parse($payment['due_date'])->format('M d, Y') }}</span>
<span style="display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 10px; font-weight: 700; text-transform: uppercase; background-color: #dc2626; color: white; margin-left: 8px;">OVERDUE</span>
</td>
</tr>
</table>
</div>
@endforeach
</div>
@endif
@if(count($tomorrowPayments) > 0)
<div style="margin-top: 24px;">
<p style="font-size: 14px; font-weight: 600; color: #f59e0b; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 2px solid #fde68a;">
Due Tomorrow
</p>
@foreach($tomorrowPayments as $payment)
<div style="background-color: #fffbeb; border-left: 4px solid #f59e0b; padding: 12px 16px; margin: 8px 0; border-radius: 0 4px 4px 0;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td>
<span style="font-weight: 600; color: #1e293b;">{{ $payment['description'] }}</span>
</td>
<td style="text-align: right;">
<span style="font-weight: 700; color: #d97706;">{{ $currency }} {{ number_format($payment['amount'], 2, '.', ',') }}</span>
</td>
</tr>
<tr>
<td colspan="2" style="padding-top: 4px;">
<span style="font-size: 12px; color: #64748b;">{{ \Carbon\Carbon::parse($payment['due_date'])->format('M d, Y') }}</span>
<span style="display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 10px; font-weight: 700; text-transform: uppercase; background-color: #f59e0b; color: white; margin-left: 8px;">TOMORROW</span>
</td>
</tr>
</table>
</div>
@endforeach
</div>
@endif
@if(count($upcomingPayments) > 0)
<div style="margin-top: 24px;">
<p style="font-size: 14px; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 2px solid #e2e8f0;">
Upcoming Payments
</p>
@foreach($upcomingPayments as $payment)
<div style="background-color: #f8fafc; border-left: 4px solid #64748b; padding: 12px 16px; margin: 8px 0; border-radius: 0 4px 4px 0;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td>
<span style="font-weight: 600; color: #1e293b;">{{ $payment['description'] }}</span>
</td>
<td style="text-align: right;">
<span style="font-weight: 700; color: #475569;">{{ $currency }} {{ number_format($payment['amount'], 2, '.', ',') }}</span>
</td>
</tr>
<tr>
<td colspan="2" style="padding-top: 4px;">
<span style="font-size: 12px; color: #64748b;">{{ \Carbon\Carbon::parse($payment['due_date'])->format('M d, Y') }}</span>
</td>
</tr>
</table>
</div>
@endforeach
</div>
@endif
<div class="divider"></div>
<div class="btn-container">
<a href="https://webmoney.cnxifly.com/dashboard" class="btn btn-primary">VIEW DASHBOARD</a>
</div>
@endforeach
</div>
@endif
<!-- Payable Items -->
@if(count($payableItems) > 0)
<div class="section">
<h2 class="section-title"> Pagamentos Possíveis ({{ count($payableItems) }})</h2>
<p style="color: #64748b; font-size: 14px;">Com base no saldo atual, você consegue pagar:</p>
@foreach($payableItems as $item)
<div class="item payable">
<div class="item-header">
<span class="item-description">{{ $item['description'] }}</span>
<span class="item-amount" style="color: #22c55e;">{{ number_format($item['amount'], 2, ',', '.') }} {{ $item['currency'] }}</span>
</div>
<div class="item-details">
<span class="badge badge-ok"> Pode pagar</span>
@if($item['account_name'])
Conta: {{ $item['account_name'] }}
@endif
</div>
@else
{{-- Spanish (default) --}}
<p class="greeting">Hola, {{ $user->name }}</p>
<div class="content">
<p>Este es tu resumen de pagos para los próximos días.</p>
{{-- Summary Box --}}
<div class="status-card info" style="background-color: #1a1a2e; border-color: #4361ee;">
<p class="status-title" style="color: #ffffff; margin-bottom: 16px;">Resumen Financiero</p>
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="color: #ffffff;">
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.1);">
<span style="color: #94a3b8;">Saldo Disponible</span>
</td>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.1); text-align: right; font-weight: 600;">
{{ $currency }} {{ number_format($totalBalance, 2, ',', '.') }}
</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.1);">
<span style="color: #94a3b8;">Total a Pagar</span>
</td>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.1); text-align: right; font-weight: 600; color: #f87171;">
{{ $currency }} {{ number_format($totalDue, 2, ',', '.') }}
</td>
</tr>
<tr>
<td style="padding: 8px 0;">
<span style="color: #94a3b8;">Pagos Pendientes</span>
</td>
<td style="padding: 8px 0; text-align: right; font-weight: 600;">
{{ $totalPayments }}
</td>
</tr>
</table>
</div>
@endforeach
</div>
@endif
<!-- Unpayable Items -->
@if(count($unpayableItems) > 0)
<div class="section">
<h2 class="section-title"> Sem Saldo Suficiente ({{ count($unpayableItems) }})</h2>
<p style="color: #64748b; font-size: 14px;">Não saldo disponível para estes pagamentos:</p>
@foreach($unpayableItems as $item)
<div class="item unpayable">
<div class="item-header">
<span class="item-description">{{ $item['description'] }}</span>
<span class="item-amount">{{ number_format($item['amount'], 2, ',', '.') }} {{ $item['currency'] }}</span>
@if($shortage > 0)
<div class="status-card warning" style="background-color: #fef2f2; border-color: #dc2626;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="52" valign="top">
<div style="width: 40px; height: 40px; background-color: #fecaca; border-radius: 50%; text-align: center; line-height: 40px; font-size: 18px; color: #dc2626;">!</div>
</td>
<td valign="top">
<p class="status-title" style="color: #dc2626;">Saldo Insuficiente</p>
</td>
</tr>
</table>
<p style="margin-top: 12px; color: #991b1b; text-align: center;">
<span style="font-size: 28px; font-weight: 700; display: block;">{{ $currency }} {{ number_format($shortage, 2, ',', '.') }}</span>
<span style="font-size: 13px;">faltan para cubrir todos los pagos</span>
</p>
</div>
<div class="item-details">
<span class="badge badge-overdue"> Sem saldo</span>
@if($item['account_name'])
Conta: {{ $item['account_name'] }}
@endif
@endif
@if(count($overduePayments) > 0)
<div style="margin-top: 24px;">
<p style="font-size: 14px; font-weight: 600; color: #dc2626; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 2px solid #fecaca;">
Pagos Vencidos
</p>
@foreach($overduePayments as $payment)
<div style="background-color: #fef2f2; border-left: 4px solid #dc2626; padding: 12px 16px; margin: 8px 0; border-radius: 0 4px 4px 0;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td>
<span style="font-weight: 600; color: #1e293b;">{{ $payment['description'] }}</span>
</td>
<td style="text-align: right;">
<span style="font-weight: 700; color: #dc2626;">{{ $currency }} {{ number_format($payment['amount'], 2, ',', '.') }}</span>
</td>
</tr>
<tr>
<td colspan="2" style="padding-top: 4px;">
<span style="font-size: 12px; color: #64748b;">Venció el {{ \Carbon\Carbon::parse($payment['due_date'])->format('d/m/Y') }}</span>
<span style="display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 10px; font-weight: 700; text-transform: uppercase; background-color: #dc2626; color: white; margin-left: 8px;">VENCIDO</span>
</td>
</tr>
</table>
</div>
@endforeach
</div>
@endif
@if(count($tomorrowPayments) > 0)
<div style="margin-top: 24px;">
<p style="font-size: 14px; font-weight: 600; color: #f59e0b; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 2px solid #fde68a;">
Vencen Mañana
</p>
@foreach($tomorrowPayments as $payment)
<div style="background-color: #fffbeb; border-left: 4px solid #f59e0b; padding: 12px 16px; margin: 8px 0; border-radius: 0 4px 4px 0;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td>
<span style="font-weight: 600; color: #1e293b;">{{ $payment['description'] }}</span>
</td>
<td style="text-align: right;">
<span style="font-weight: 700; color: #d97706;">{{ $currency }} {{ number_format($payment['amount'], 2, ',', '.') }}</span>
</td>
</tr>
<tr>
<td colspan="2" style="padding-top: 4px;">
<span style="font-size: 12px; color: #64748b;">{{ \Carbon\Carbon::parse($payment['due_date'])->format('d/m/Y') }}</span>
<span style="display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 10px; font-weight: 700; text-transform: uppercase; background-color: #f59e0b; color: white; margin-left: 8px;">MAÑANA</span>
</td>
</tr>
</table>
</div>
@endforeach
</div>
@endif
@if(count($upcomingPayments) > 0)
<div style="margin-top: 24px;">
<p style="font-size: 14px; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 2px solid #e2e8f0;">
Próximos Pagos
</p>
@foreach($upcomingPayments as $payment)
<div style="background-color: #f8fafc; border-left: 4px solid #64748b; padding: 12px 16px; margin: 8px 0; border-radius: 0 4px 4px 0;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td>
<span style="font-weight: 600; color: #1e293b;">{{ $payment['description'] }}</span>
</td>
<td style="text-align: right;">
<span style="font-weight: 700; color: #475569;">{{ $currency }} {{ number_format($payment['amount'], 2, ',', '.') }}</span>
</td>
</tr>
<tr>
<td colspan="2" style="padding-top: 4px;">
<span style="font-size: 12px; color: #64748b;">{{ \Carbon\Carbon::parse($payment['due_date'])->format('d/m/Y') }}</span>
</td>
</tr>
</table>
</div>
@endforeach
</div>
@endif
<div class="divider"></div>
<div class="btn-container">
<a href="https://webmoney.cnxifly.com/dashboard" class="btn btn-primary">VER DASHBOARD</a>
</div>
@endforeach
</div>
@endif
<!-- Transfer Suggestions -->
@if(count($transferSuggestions) > 0)
<div class="section">
<h2 class="section-title">💱 Sugestões de Transferência</h2>
<p style="color: #64748b; font-size: 14px;">Para cobrir os pagamentos, considere transferir entre suas contas:</p>
@foreach($transferSuggestions as $transfer)
<div class="transfer-suggestion">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<strong>{{ $transfer['from_account'] }}</strong>
<div style="font-size: 12px; color: #64748b;">Origem</div>
</div>
<div class="transfer-arrow"></div>
<div style="text-align: right;">
<strong>{{ $transfer['to_account'] }}</strong>
<div style="font-size: 12px; color: #64748b;">Destino</div>
</div>
</div>
<div style="text-align: center; margin-top: 10px; font-size: 20px; font-weight: bold; color: #3b82f6;">
{{ number_format($transfer['amount'], 2, ',', '.') }} {{ $currency }}
</div>
<div style="text-align: center; font-size: 12px; color: #64748b;">{{ $transfer['reason'] }}</div>
</div>
@endforeach
</div>
@endif
<div style="text-align: center;">
<a href="https://webmoney.cnxifly.com/transactions" class="cta-button">
Acessar WEBMoney
</a>
</div>
<div class="footer">
<p>Este email foi enviado automaticamente pelo sistema WEBMoney para {{ $userName }}.</p>
<p>Você recebe esta mensagem porque ativou as notificações de pagamentos.</p>
<p>Para desativar estas notificações, acesse <a href="https://webmoney.cnxifly.com/preferences" style="color: #3b82f6;">Preferências</a>.</p>
<p style="margin-top: 15px; font-size: 11px; color: #94a3b8;">
WEBMoney - ConneXiFly<br />
Serviço de gestão financeira pessoal<br />
Madrid, Espanha
</p>
<p style="font-size: 11px; color: #94a3b8;">&copy; {{ date('Y') }} WEBMoney - Todos os direitos reservados</p>
</div>
</div>
</body>
</html>
@endif
@endsection

View File

@ -0,0 +1,412 @@
<!DOCTYPE html>
<html lang="{{ $locale ?? 'es' }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>@yield('title') - WEBMoney</title>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<style>
/* Reset */
body, table, td, p, a, li, blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
}
/* Base styles */
body {
margin: 0 !important;
padding: 0 !important;
background-color: #f8f9fa;
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', Arial, sans-serif;
}
/* Container */
.email-wrapper {
width: 100%;
background-color: #f8f9fa;
padding: 48px 16px;
}
.email-container {
max-width: 580px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
border: 1px solid #e9ecef;
}
/* Header */
.email-header {
background-color: #1a1a2e;
padding: 32px 40px;
text-align: center;
border-bottom: 3px solid #4361ee;
}
.logo {
font-size: 24px;
font-weight: 700;
color: #ffffff;
letter-spacing: -0.3px;
margin: 0;
text-transform: uppercase;
}
.logo-accent {
color: #4361ee;
}
/* Body */
.email-body {
padding: 40px;
}
.greeting {
font-size: 20px;
font-weight: 600;
color: #1a1a2e;
margin: 0 0 24px 0;
line-height: 1.4;
}
.content p {
font-size: 15px;
line-height: 1.7;
color: #495057;
margin: 0 0 16px 0;
}
.content strong {
color: #1a1a2e;
font-weight: 600;
}
/* Status Card */
.status-card {
border-radius: 8px;
padding: 24px;
margin: 24px 0;
border: 1px solid;
}
.status-card.success {
background-color: #f8fff8;
border-color: #c6f6d5;
}
.status-card.info {
background-color: #f7fafc;
border-color: #e2e8f0;
}
.status-card.warning {
background-color: #fffaf0;
border-color: #feebc8;
}
.status-header {
display: flex;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(0,0,0,0.06);
}
.status-icon {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
margin-right: 12px;
}
.status-card.success .status-icon {
background-color: #c6f6d5;
}
.status-card.info .status-icon {
background-color: #e2e8f0;
}
.status-card.warning .status-icon {
background-color: #feebc8;
}
.status-title {
font-size: 15px;
font-weight: 600;
margin: 0;
}
.status-card.success .status-title {
color: #22543d;
}
.status-card.info .status-title {
color: #2d3748;
}
.status-card.warning .status-title {
color: #744210;
}
.status-card p {
font-size: 14px;
margin: 0;
line-height: 1.6;
}
.status-card.success p {
color: #276749;
}
.status-card.info p {
color: #4a5568;
}
.status-card.warning p {
color: #975a16;
}
/* Amount */
.amount-value {
font-size: 32px;
font-weight: 700;
color: #22543d;
display: block;
margin: 16px 0;
letter-spacing: -0.5px;
}
/* List */
.info-list {
list-style: none;
padding: 0;
margin: 0;
}
.info-list li {
position: relative;
padding: 8px 0 8px 24px;
font-size: 14px;
color: #4a5568;
line-height: 1.5;
}
.info-list li::before {
content: "";
position: absolute;
left: 0;
top: 14px;
width: 6px;
height: 6px;
background-color: #4361ee;
border-radius: 50%;
}
/* Buttons */
.btn-container {
text-align: center;
margin: 32px 0;
}
.btn {
display: inline-block;
padding: 14px 32px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
border-radius: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-primary {
background-color: #4361ee;
color: #ffffff !important;
}
/* Divider */
.divider {
height: 1px;
background-color: #e9ecef;
margin: 32px 0;
}
/* Footer */
.email-footer {
background-color: #f8f9fa;
padding: 32px 40px;
border-top: 1px solid #e9ecef;
}
.footer-brand {
text-align: center;
margin-bottom: 20px;
}
.footer-brand-name {
font-size: 14px;
font-weight: 700;
color: #1a1a2e;
text-transform: uppercase;
letter-spacing: 1px;
}
.footer-tagline {
font-size: 12px;
color: #6c757d;
margin-top: 4px;
}
.footer-links {
text-align: center;
margin: 16px 0;
}
.footer-links a {
color: #4361ee;
text-decoration: none;
font-size: 12px;
margin: 0 12px;
font-weight: 500;
}
.footer-legal {
text-align: center;
font-size: 11px;
color: #adb5bd;
line-height: 1.6;
margin-top: 20px;
}
.footer-legal a {
color: #6c757d;
text-decoration: underline;
}
/* Responsive */
@media only screen and (max-width: 600px) {
.email-wrapper {
padding: 24px 12px;
}
.email-header {
padding: 24px 20px;
}
.email-body {
padding: 28px 20px;
}
.email-footer {
padding: 24px 20px;
}
.logo {
font-size: 20px;
}
.greeting {
font-size: 18px;
}
.btn {
padding: 12px 28px;
font-size: 13px;
}
.amount-value {
font-size: 28px;
}
.status-card {
padding: 20px;
}
}
</style>
</head>
<body>
<div class="email-wrapper">
<div class="email-container">
<!-- Header -->
<div class="email-header">
<h1 class="logo">WEB<span class="logo-accent">Money</span></h1>
</div>
<!-- Body -->
<div class="email-body">
@yield('content')
</div>
<!-- Footer -->
<div class="email-footer">
<div class="footer-brand">
<div class="footer-brand-name">WEBMoney</div>
<div class="footer-tagline">
@if(($locale ?? 'es') === 'pt-BR')
Gestão Financeira Pessoal
@elseif(($locale ?? 'es') === 'en')
Personal Finance Management
@else
Gestión Financiera Personal
@endif
</div>
</div>
<div class="footer-links">
<a href="{{ config('app.frontend_url', 'https://webmoney.cnxifly.com') }}">
@if(($locale ?? 'es') === 'pt-BR')
Acessar Conta
@elseif(($locale ?? 'es') === 'en')
Access Account
@else
Acceder a Cuenta
@endif
</a>
<a href="{{ config('app.frontend_url', 'https://webmoney.cnxifly.com') }}/support">
@if(($locale ?? 'es') === 'pt-BR')
Central de Ajuda
@elseif(($locale ?? 'es') === 'en')
Help Center
@else
Centro de Ayuda
@endif
</a>
</div>
<div class="footer-legal">
@if(($locale ?? 'es') === 'pt-BR')
Este email foi enviado por WEBMoney, um serviço de ConneXiFly.<br>
© {{ date('Y') }} ConneXiFly. Todos os direitos reservados.
@elseif(($locale ?? 'es') === 'en')
This email was sent by WEBMoney, a ConneXiFly service.<br>
© {{ date('Y') }} ConneXiFly. All rights reserved.
@else
Este correo fue enviado por WEBMoney, un servicio de ConneXiFly.<br>
© {{ date('Y') }} ConneXiFly. Todos los derechos reservados.
@endif
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,96 @@
@if($locale === 'pt-BR')
CANCELAMENTO DE ASSINATURA - WEBMONEY
=====================================
Olá, {{ $userName }}!
Confirmamos o cancelamento da sua assinatura do plano {{ $planName }}.
@if($wasRefunded)
REEMBOLSO PROCESSADO
-----------------------
Processamos um reembolso total no valor de: {{ $refundAmount }}
O reembolso será creditado na sua forma de pagamento original em até 5-10 dias úteis, dependendo do seu banco.
@endif
O QUE ACONTECE AGORA?
---------------------
Seu acesso premium foi encerrado imediatamente
Você ainda pode acessar sua conta com funcionalidades básicas
Seus dados foram preservados caso queira voltar
Sentimos muito -lo partir! Se tiver qualquer dúvida ou feedback, não hesite em nos contatar.
Se mudar de ideia, você sempre pode assinar novamente:
{{ config('app.frontend_url', 'https://webmoney.cnxifly.com') }}/pricing
---
Este email foi enviado para {{ $userEmail }}
© {{ date('Y') }} WEBMoney - ConneXiFly
Precisa de ajuda? Responda este email.
@elseif($locale === 'en')
SUBSCRIPTION CANCELLATION - WEBMONEY
====================================
Hello, {{ $userName }}!
We confirm the cancellation of your {{ $planName }} subscription.
@if($wasRefunded)
REFUND PROCESSED
-------------------
We have processed a full refund of: {{ $refundAmount }}
The refund will be credited to your original payment method within 5-10 business days, depending on your bank.
@endif
WHAT HAPPENS NOW?
-----------------
Your premium access has ended immediately
You can still access your account with basic features
Your data has been preserved in case you want to return
We're sorry to see you go! If you have any questions or feedback, please don't hesitate to contact us.
If you change your mind, you can always subscribe again:
{{ config('app.frontend_url', 'https://webmoney.cnxifly.com') }}/pricing
---
This email was sent to {{ $userEmail }}
© {{ date('Y') }} WEBMoney - ConneXiFly
Need help? Reply to this email.
@else
CANCELACIÓN DE SUSCRIPCIÓN - WEBMONEY
=====================================
¡Hola, {{ $userName }}!
Confirmamos la cancelación de tu suscripción al plan {{ $planName }}.
@if($wasRefunded)
REEMBOLSO PROCESADO
----------------------
Hemos procesado un reembolso total por el valor de: {{ $refundAmount }}
El reembolso se acreditará en tu método de pago original en un plazo de 5-10 días hábiles, dependiendo de tu banco.
@endif
¿QUÉ SUCEDE AHORA?
------------------
Tu acceso premium ha finalizado inmediatamente
Puedes seguir accediendo a tu cuenta con funciones básicas
Tus datos han sido preservados por si deseas volver
¡Sentimos mucho verte partir! Si tienes alguna pregunta o comentario, no dudes en contactarnos.
Si cambias de opinión, siempre puedes volver a suscribirte:
{{ config('app.frontend_url', 'https://webmoney.cnxifly.com') }}/pricing
---
Este correo fue enviado a {{ $userEmail }}
© {{ date('Y') }} WEBMoney - ConneXiFly
¿Necesitas ayuda? Responde a este correo.
@endif

View File

@ -0,0 +1,164 @@
@extends('emails.layouts.base')
@section('title')
@if($locale === 'pt-BR')
Cancelamento de Assinatura
@elseif($locale === 'en')
Subscription Cancellation
@else
Cancelación de Suscripción
@endif
@endsection
@section('content')
@if($locale === 'pt-BR')
{{-- Portuguese (Brazil) --}}
<p class="greeting">Olá, {{ $userName }}</p>
<div class="content">
<p>Confirmamos o cancelamento da sua assinatura do plano <strong>{{ $planName }}</strong>.</p>
@if($wasRefunded)
<div class="status-card success">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="52" valign="top">
<div style="width: 40px; height: 40px; background-color: #c6f6d5; border-radius: 50%; text-align: center; line-height: 40px; font-size: 18px;"></div>
</td>
<td valign="top">
<p class="status-title">Reembolso Processado</p>
</td>
</tr>
</table>
<p style="margin-top: 16px;">Valor reembolsado conforme garantia de 7 dias:</p>
<span class="amount-value">{{ $refundAmount }}</span>
<p style="font-size: 13px; color: #48bb78; margin-top: 8px;">
Prazo: 5-10 dias úteis para crédito na forma de pagamento original.
</p>
</div>
@endif
<div class="status-card info">
<p class="status-title" style="color: #2d3748; margin-bottom: 16px;">Informações Importantes</p>
<ul class="info-list">
<li>Acesso premium encerrado</li>
<li>Conta disponível com funcionalidades básicas</li>
<li>Seus dados foram preservados</li>
<li>Reativação disponível a qualquer momento</li>
</ul>
</div>
<div class="divider"></div>
<p style="text-align: center; color: #6c757d; font-size: 14px;">
Agradecemos por ter sido nosso cliente.
</p>
<div class="btn-container">
<a href="{{ config('app.frontend_url', 'https://webmoney.cnxifly.com') }}/pricing" class="btn btn-primary">
VER PLANOS
</a>
</div>
</div>
@elseif($locale === 'en')
{{-- English --}}
<p class="greeting">Hello, {{ $userName }}</p>
<div class="content">
<p>We confirm the cancellation of your <strong>{{ $planName }}</strong> subscription.</p>
@if($wasRefunded)
<div class="status-card success">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="52" valign="top">
<div style="width: 40px; height: 40px; background-color: #c6f6d5; border-radius: 50%; text-align: center; line-height: 40px; font-size: 18px;"></div>
</td>
<td valign="top">
<p class="status-title">Refund Processed</p>
</td>
</tr>
</table>
<p style="margin-top: 16px;">Amount refunded per our 7-day guarantee:</p>
<span class="amount-value">{{ $refundAmount }}</span>
<p style="font-size: 13px; color: #48bb78; margin-top: 8px;">
Timeline: 5-10 business days to credit your original payment method.
</p>
</div>
@endif
<div class="status-card info">
<p class="status-title" style="color: #2d3748; margin-bottom: 16px;">Important Information</p>
<ul class="info-list">
<li>Premium access ended</li>
<li>Account available with basic features</li>
<li>Your data has been preserved</li>
<li>Reactivation available anytime</li>
</ul>
</div>
<div class="divider"></div>
<p style="text-align: center; color: #6c757d; font-size: 14px;">
Thank you for being our customer.
</p>
<div class="btn-container">
<a href="{{ config('app.frontend_url', 'https://webmoney.cnxifly.com') }}/pricing" class="btn btn-primary">
VIEW PLANS
</a>
</div>
</div>
@else
{{-- Spanish (default) --}}
<p class="greeting">Hola, {{ $userName }}</p>
<div class="content">
<p>Confirmamos la cancelación de tu suscripción al plan <strong>{{ $planName }}</strong>.</p>
@if($wasRefunded)
<div class="status-card success">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="52" valign="top">
<div style="width: 40px; height: 40px; background-color: #c6f6d5; border-radius: 50%; text-align: center; line-height: 40px; font-size: 18px;"></div>
</td>
<td valign="top">
<p class="status-title">Reembolso Procesado</p>
</td>
</tr>
</table>
<p style="margin-top: 16px;">Monto reembolsado según garantía de 7 días:</p>
<span class="amount-value">{{ $refundAmount }}</span>
<p style="font-size: 13px; color: #48bb78; margin-top: 8px;">
Plazo: 5-10 días hábiles para acreditar en tu método de pago original.
</p>
</div>
@endif
<div class="status-card info">
<p class="status-title" style="color: #2d3748; margin-bottom: 16px;">Información Importante</p>
<ul class="info-list">
<li>Acceso premium finalizado</li>
<li>Cuenta disponible con funciones básicas</li>
<li>Tus datos han sido preservados</li>
<li>Reactivación disponible en cualquier momento</li>
</ul>
</div>
<div class="divider"></div>
<p style="text-align: center; color: #6c757d; font-size: 14px;">
Gracias por haber sido nuestro cliente.
</p>
<div class="btn-container">
<a href="{{ config('app.frontend_url', 'https://webmoney.cnxifly.com') }}/pricing" class="btn btn-primary">
VER PLANES
</a>
</div>
</div>
@endif
@endsection

View File

@ -1,294 +1,181 @@
<!DOCTYPE html>
<html lang="{{ $language }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
@if($language === 'pt-BR')
Bem-vindo ao WebMoney
@elseif($language === 'en')
Welcome to WebMoney
@else
Bienvenido a WebMoney
@endif
</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: #ffffff;
border-radius: 12px;
padding: 40px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
margin-bottom: 30px;
}
.logo {
font-size: 32px;
font-weight: bold;
color: #3b82f6;
margin-bottom: 10px;
}
.logo span {
color: #22c55e;
}
h1 {
color: #1e293b;
font-size: 24px;
margin-bottom: 20px;
}
.credentials-box {
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
border-radius: 8px;
padding: 25px;
margin: 25px 0;
color: #fff;
}
.credentials-box h3 {
color: #22c55e;
margin: 0 0 15px 0;
font-size: 16px;
text-transform: uppercase;
letter-spacing: 1px;
}
.credential-item {
margin: 12px 0;
padding: 10px;
background: rgba(255, 255, 255, 0.1);
border-radius: 6px;
}
.credential-label {
color: #94a3b8;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.credential-value {
color: #fff;
font-size: 18px;
font-weight: 600;
font-family: monospace;
word-break: break-all;
}
.warning-box {
background-color: #fef3c7;
border-left: 4px solid #f59e0b;
padding: 15px;
margin: 20px 0;
border-radius: 0 8px 8px 0;
}
.warning-box p {
margin: 0;
color: #92400e;
font-size: 14px;
}
.button {
display: inline-block;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: #ffffff !important;
text-decoration: none;
padding: 14px 32px;
border-radius: 8px;
font-weight: 600;
font-size: 16px;
margin: 20px 0;
text-align: center;
}
.button:hover {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
}
.features {
background-color: #f8fafc;
border-radius: 8px;
padding: 20px;
margin: 25px 0;
}
.features h3 {
color: #1e293b;
margin: 0 0 15px 0;
font-size: 16px;
}
.features ul {
margin: 0;
padding-left: 20px;
color: #64748b;
}
.features li {
margin: 8px 0;
}
.footer {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e2e8f0;
color: #64748b;
font-size: 12px;
}
.footer a {
color: #3b82f6;
text-decoration: none;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">Web<span>Money</span></div>
@extends('emails.layouts.base')
@php $locale = $language ?? 'es'; @endphp
@section('title')
@if($locale === 'pt-BR')
Bem-vindo ao WEBMoney
@elseif($locale === 'en')
Welcome to WEBMoney
@else
Bienvenido a WEBMoney
@endif
@endsection
@section('content')
@if($locale === 'pt-BR')
{{-- Portuguese (Brazil) --}}
<p class="greeting">Olá, {{ $user->name }}</p>
<div class="content">
<p>Bem-vindo ao WEBMoney! A sua conta foi criada com sucesso.</p>
<div class="status-card info" style="background-color: #1a1a2e; border-color: #4361ee;">
<p class="status-title" style="color: #ffffff; margin-bottom: 16px;">Credenciais de Acesso</p>
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-top: 12px;">
<tr>
<td style="padding: 12px; background: rgba(255,255,255,0.1); border-radius: 4px; margin-bottom: 8px;">
<span style="color: #94a3b8; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px;">Email</span><br>
<span style="color: #ffffff; font-size: 16px; font-weight: 600; font-family: monospace;">{{ $user->email }}</span>
</td>
</tr>
<tr><td style="height: 8px;"></td></tr>
<tr>
<td style="padding: 12px; background: rgba(255,255,255,0.1); border-radius: 4px;">
<span style="color: #94a3b8; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px;">Palavra-passe</span><br>
<span style="color: #ffffff; font-size: 16px; font-weight: 600; font-family: monospace;">{{ $password }}</span>
</td>
</tr>
</table>
</div>
<div class="status-card warning">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="52" valign="top">
<div style="width: 40px; height: 40px; background-color: #feebc8; border-radius: 50%; text-align: center; line-height: 40px; font-size: 18px;">!</div>
</td>
<td valign="top">
<p class="status-title">Segurança</p>
</td>
</tr>
</table>
<p style="margin-top: 12px;">Recomendamos que altere a sua palavra-passe após o primeiro login. Guarde estas credenciais em local seguro.</p>
</div>
<div class="status-card info">
<p class="status-title" style="color: #2d3748; margin-bottom: 16px;">O que pode fazer</p>
<ul class="info-list">
<li>Registar receitas e despesas</li>
<li>Visualizar relatórios e gráficos</li>
<li>Gerir o seu orçamento mensal</li>
<li>Exportar dados em múltiplos formatos</li>
</ul>
</div>
<div class="btn-container">
<a href="https://webmoney.cnxifly.com/login" class="btn btn-primary">INICIAR SESSÃO</a>
</div>
</div>
@if($language === 'pt-BR')
{{-- PORTUGUÊS --}}
<h1>Olá, {{ $user->name }}! 👋</h1>
@elseif($locale === 'en')
{{-- English --}}
<p class="greeting">Hello, {{ $user->name }}</p>
<p>Sua conta WebMoney foi criada com sucesso. Estamos muito felizes em -lo conosco!</p>
<div class="content">
<p>Welcome to WEBMoney! Your account has been successfully created.</p>
<p>Abaixo estão suas credenciais de acesso:</p>
<div class="credentials-box">
<h3>🔐 Suas Credenciais</h3>
<div class="credential-item">
<div class="credential-label">Email</div>
<div class="credential-value">{{ $user->email }}</div>
</div>
<div class="credential-item">
<div class="credential-label">Senha Temporária</div>
<div class="credential-value">{{ $temporaryPassword }}</div>
</div>
<div class="status-card info" style="background-color: #1a1a2e; border-color: #4361ee;">
<p class="status-title" style="color: #ffffff; margin-bottom: 16px;">Access Credentials</p>
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-top: 12px;">
<tr>
<td style="padding: 12px; background: rgba(255,255,255,0.1); border-radius: 4px; margin-bottom: 8px;">
<span style="color: #94a3b8; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px;">Email</span><br>
<span style="color: #ffffff; font-size: 16px; font-weight: 600; font-family: monospace;">{{ $user->email }}</span>
</td>
</tr>
<tr><td style="height: 8px;"></td></tr>
<tr>
<td style="padding: 12px; background: rgba(255,255,255,0.1); border-radius: 4px;">
<span style="color: #94a3b8; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px;">Password</span><br>
<span style="color: #ffffff; font-size: 16px; font-weight: 600; font-family: monospace;">{{ $password }}</span>
</td>
</tr>
</table>
</div>
<div class="warning-box">
<p>⚠️ <strong>Importante:</strong> Recomendamos que você altere sua senha após o primeiro login por motivos de segurança.</p>
<div class="status-card warning">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="52" valign="top">
<div style="width: 40px; height: 40px; background-color: #feebc8; border-radius: 50%; text-align: center; line-height: 40px; font-size: 18px;">!</div>
</td>
<td valign="top">
<p class="status-title">Security</p>
</td>
</tr>
</table>
<p style="margin-top: 12px;">We recommend changing your password after your first login. Keep these credentials in a safe place.</p>
</div>
<div style="text-align: center;">
<a href="{{ $loginUrl }}" class="button">Acessar Minha Conta</a>
</div>
<div class="features">
<h3>🚀 O que você pode fazer com o WebMoney:</h3>
<ul>
<li>Gerenciar todas suas contas bancárias em um lugar</li>
<li>Categorizar receitas e despesas automaticamente</li>
<li>Criar orçamentos e acompanhar seus gastos</li>
<li>Visualizar relatórios e gráficos detalhados</li>
<li>Definir metas financeiras e alcançá-las</li>
<div class="status-card info">
<p class="status-title" style="color: #2d3748; margin-bottom: 16px;">What you can do</p>
<ul class="info-list">
<li>Record income and expenses</li>
<li>View reports and charts</li>
<li>Manage your monthly budget</li>
<li>Export data in multiple formats</li>
</ul>
</div>
<p>Se você tiver alguma dúvida ou precisar de ajuda, não hesite em nos contatar.</p>
<p>Atenciosamente,<br><strong>Equipe WebMoney</strong></p>
@elseif($language === 'en')
{{-- ENGLISH --}}
<h1>Hello, {{ $user->name }}! 👋</h1>
<p>Your WebMoney account has been successfully created. We're thrilled to have you with us!</p>
<p>Below are your login credentials:</p>
<div class="credentials-box">
<h3>🔐 Your Credentials</h3>
<div class="credential-item">
<div class="credential-label">Email</div>
<div class="credential-value">{{ $user->email }}</div>
</div>
<div class="credential-item">
<div class="credential-label">Temporary Password</div>
<div class="credential-value">{{ $temporaryPassword }}</div>
</div>
<div class="btn-container">
<a href="https://webmoney.cnxifly.com/login" class="btn btn-primary">SIGN IN</a>
</div>
<div class="warning-box">
<p>⚠️ <strong>Important:</strong> We recommend changing your password after your first login for security purposes.</p>
</div>
<div style="text-align: center;">
<a href="{{ $loginUrl }}" class="button">Access My Account</a>
</div>
<div class="features">
<h3>🚀 What you can do with WebMoney:</h3>
<ul>
<li>Manage all your bank accounts in one place</li>
<li>Automatically categorize income and expenses</li>
<li>Create budgets and track your spending</li>
<li>View detailed reports and charts</li>
<li>Set financial goals and achieve them</li>
</ul>
</div>
<p>If you have any questions or need help, don't hesitate to contact us.</p>
<p>Best regards,<br><strong>The WebMoney Team</strong></p>
@else
{{-- ESPAÑOL (default) --}}
<h1>¡Hola, {{ $user->name }}! 👋</h1>
<p>Tu cuenta de WebMoney ha sido creada exitosamente. ¡Estamos muy contentos de tenerte con nosotros!</p>
<p>A continuación encontrarás tus credenciales de acceso:</p>
<div class="credentials-box">
<h3>🔐 Tus Credenciales</h3>
<div class="credential-item">
<div class="credential-label">Email</div>
<div class="credential-value">{{ $user->email }}</div>
</div>
<div class="credential-item">
<div class="credential-label">Contraseña Temporal</div>
<div class="credential-value">{{ $temporaryPassword }}</div>
</div>
</div>
<div class="warning-box">
<p>⚠️ <strong>Importante:</strong> Te recomendamos cambiar tu contraseña después de tu primer inicio de sesión por motivos de seguridad.</p>
</div>
<div style="text-align: center;">
<a href="{{ $loginUrl }}" class="button">Acceder a Mi Cuenta</a>
</div>
<div class="features">
<h3>🚀 Lo que puedes hacer con WebMoney:</h3>
<ul>
<li>Gestionar todas tus cuentas bancarias en un solo lugar</li>
<li>Categorizar ingresos y gastos automáticamente</li>
<li>Crear presupuestos y hacer seguimiento de tus gastos</li>
<li>Ver informes y gráficos detallados</li>
<li>Establecer metas financieras y alcanzarlas</li>
</ul>
</div>
<p>Si tienes alguna pregunta o necesitas ayuda, no dudes en contactarnos.</p>
<p>Saludos cordiales,<br><strong>El Equipo de WebMoney</strong></p>
@endif
<div class="footer">
<p>© {{ date('Y') }} WebMoney.
@if($language === 'pt-BR')
Todos os direitos reservados.
@elseif($language === 'en')
All rights reserved.
@else
Todos los derechos reservados.
@endif
</p>
<p>
<a href="{{ $loginUrl }}">{{ $loginUrl }}</a>
</p>
</div>
</div>
</body>
</html>
@else
{{-- Spanish (default) --}}
<p class="greeting">Hola, {{ $user->name }}</p>
<div class="content">
<p>Bienvenido a WEBMoney. Tu cuenta ha sido creada exitosamente.</p>
<div class="status-card info" style="background-color: #1a1a2e; border-color: #4361ee;">
<p class="status-title" style="color: #ffffff; margin-bottom: 16px;">Credenciales de Acceso</p>
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-top: 12px;">
<tr>
<td style="padding: 12px; background: rgba(255,255,255,0.1); border-radius: 4px; margin-bottom: 8px;">
<span style="color: #94a3b8; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px;">Email</span><br>
<span style="color: #ffffff; font-size: 16px; font-weight: 600; font-family: monospace;">{{ $user->email }}</span>
</td>
</tr>
<tr><td style="height: 8px;"></td></tr>
<tr>
<td style="padding: 12px; background: rgba(255,255,255,0.1); border-radius: 4px;">
<span style="color: #94a3b8; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px;">Contraseña</span><br>
<span style="color: #ffffff; font-size: 16px; font-weight: 600; font-family: monospace;">{{ $password }}</span>
</td>
</tr>
</table>
</div>
<div class="status-card warning">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="52" valign="top">
<div style="width: 40px; height: 40px; background-color: #feebc8; border-radius: 50%; text-align: center; line-height: 40px; font-size: 18px;">!</div>
</td>
<td valign="top">
<p class="status-title">Seguridad</p>
</td>
</tr>
</table>
<p style="margin-top: 12px;">Recomendamos cambiar tu contraseña después del primer inicio de sesión. Guarda estas credenciales en un lugar seguro.</p>
</div>
<div class="status-card info">
<p class="status-title" style="color: #2d3748; margin-bottom: 16px;">Qué puedes hacer</p>
<ul class="info-list">
<li>Registrar ingresos y gastos</li>
<li>Visualizar reportes y gráficos</li>
<li>Gestionar tu presupuesto mensual</li>
<li>Exportar datos en múltiples formatos</li>
</ul>
</div>
<div class="btn-container">
<a href="https://webmoney.cnxifly.com/login" class="btn btn-primary">INICIAR SESIÓN</a>
</div>
</div>
@endif
@endsection

View File

@ -1,113 +1,142 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Bienvenido a WEBMoney</title>
<!--[if mso]>
<style type="text/css">
body, table, td {font-family: Arial, Helvetica, sans-serif !important;}
</style>
<![endif]-->
</head>
<body style="margin: 0; padding: 0; background-color: #f4f4f4; font-family: Arial, Helvetica, sans-serif;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #f4f4f4;">
<tr>
<td style="padding: 20px 0;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<!-- Header -->
@extends('emails.layouts.base')
@php $locale = $locale ?? 'es'; @endphp
@section('title')
@if($locale === 'pt-BR')
Bem-vindo ao WEBMoney
@elseif($locale === 'en')
Welcome to WEBMoney
@else
Bienvenido a WEBMoney
@endif
@endsection
@section('content')
@if($locale === 'pt-BR')
{{-- Portuguese (Brazil) --}}
<p class="greeting">Olá, {{ $userName }}</p>
<div class="content">
<p>A sua conta no WEBMoney foi criada com sucesso. pode começar a gerir as suas finanças pessoais de forma fácil e segura.</p>
<div class="status-card success">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px 30px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: 700;">
🎉 ¡Bienvenido a WEBMoney!
</h1>
<td width="52" valign="top">
<div style="width: 40px; height: 40px; background-color: #c6f6d5; border-radius: 50%; text-align: center; line-height: 40px; font-size: 18px;"></div>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 40px 30px;">
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.6;">
Hola <strong>{{ $userName }}</strong>,
</p>
<p style="margin: 0 0 20px; color: #555555; font-size: 16px; line-height: 1.6;">
¡Gracias por registrarte en <strong>WEBMoney</strong>! Tu cuenta ha sido creada exitosamente y ya puedes comenzar a gestionar tus finanzas personales de manera fácil y segura.
</p>
<div style="background-color: #f8f9fa; border-left: 4px solid #667eea; padding: 20px; margin: 30px 0; border-radius: 4px;">
<p style="margin: 0 0 10px; color: #333333; font-size: 14px; font-weight: 600;">
📧 Cuenta registrada:
</p>
<p style="margin: 0; color: #667eea; font-size: 16px; font-weight: 700;">
{{ $userEmail }}
</p>
</div>
<h3 style="margin: 30px 0 15px; color: #333333; font-size: 18px; font-weight: 600;">
¿Qué puedes hacer ahora?
</h3>
<ul style="margin: 0 0 30px; padding-left: 20px; color: #555555; font-size: 15px; line-height: 1.8;">
<li>📊 Registrar tus ingresos y gastos</li>
<li>📈 Visualizar reportes y gráficos</li>
<li>💰 Gestionar tu presupuesto mensual</li>
<li>📄 Exportar datos en múltiples formatos</li>
</ul>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: 30px 0;">
<tr>
<td style="text-align: center;">
<a href="https://webmoney.cnxifly.com/login" style="display: inline-block; padding: 15px 40px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff; text-decoration: none; border-radius: 6px; font-size: 16px; font-weight: 600;">
Iniciar Sesión Ahora
</a>
</td>
</tr>
</table>
<p style="margin: 30px 0 10px; color: #666666; font-size: 14px; line-height: 1.6;">
Si tienes alguna pregunta o necesitas ayuda, no dudes en contactarnos respondiendo a este email.
</p>
<p style="margin: 0; color: #666666; font-size: 14px; line-height: 1.6;">
¡Gracias por confiar en nosotros!
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #f8f9fa; padding: 30px; text-align: center; border-radius: 0 0 8px 8px;">
<p style="margin: 0 0 10px; color: #666666; font-size: 14px;">
<strong>WEBMoney</strong> - Tu gestor financiero personal
</p>
<p style="margin: 0 0 15px; color: #999999; font-size: 12px;">
ConneXiFly · webmoney.cnxifly.com
</p>
<p style="margin: 0; color: #999999; font-size: 11px;">
Este es un correo automático. Por favor, no respondas directamente a este mensaje.
<br>
Para soporte, escríbenos a: <a href="mailto:support@cnxifly.com" style="color: #667eea; text-decoration: none;">support@cnxifly.com</a>
</p>
<td valign="top">
<p class="status-title">Conta Registada</p>
</td>
</tr>
</table>
<p style="margin-top: 16px;"><strong>Email:</strong> {{ $userEmail }}</p>
</div>
<!-- Email Footer (outside box) -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" style="margin: 20px auto 0;">
<div class="status-card info">
<p class="status-title" style="color: #2d3748; margin-bottom: 16px;">Funcionalidades Disponíveis</p>
<ul class="info-list">
<li>Registar receitas e despesas</li>
<li>Visualizar relatórios e gráficos</li>
<li>Gerir o seu orçamento mensal</li>
<li>Exportar dados em múltiplos formatos</li>
</ul>
</div>
<div class="btn-container">
<a href="https://webmoney.cnxifly.com/login" class="btn btn-primary">INICIAR SESSÃO</a>
</div>
<div class="divider"></div>
<p style="text-align: center; color: #6c757d; font-size: 14px;">
Obrigado por confiar em nós.
</p>
</div>
@elseif($locale === 'en')
{{-- English --}}
<p class="greeting">Hello, {{ $userName }}</p>
<div class="content">
<p>Your WEBMoney account has been successfully created. You can now start managing your personal finances easily and securely.</p>
<div class="status-card success">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="text-align: center; padding: 20px;">
<p style="margin: 0; color: #999999; font-size: 11px; line-height: 1.6;">
© 2025 ConneXiFly. Todos los derechos reservados.
<br>
Si no solicitaste esta cuenta, ignora este correo.
</p>
<td width="52" valign="top">
<div style="width: 40px; height: 40px; background-color: #c6f6d5; border-radius: 50%; text-align: center; line-height: 40px; font-size: 18px;"></div>
</td>
<td valign="top">
<p class="status-title">Account Registered</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
<p style="margin-top: 16px;"><strong>Email:</strong> {{ $userEmail }}</p>
</div>
<div class="status-card info">
<p class="status-title" style="color: #2d3748; margin-bottom: 16px;">Available Features</p>
<ul class="info-list">
<li>Record income and expenses</li>
<li>View reports and charts</li>
<li>Manage your monthly budget</li>
<li>Export data in multiple formats</li>
</ul>
</div>
<div class="btn-container">
<a href="https://webmoney.cnxifly.com/login" class="btn btn-primary">SIGN IN</a>
</div>
<div class="divider"></div>
<p style="text-align: center; color: #6c757d; font-size: 14px;">
Thank you for trusting us.
</p>
</div>
@else
{{-- Spanish (default) --}}
<p class="greeting">Hola, {{ $userName }}</p>
<div class="content">
<p>Tu cuenta en WEBMoney ha sido creada exitosamente. Ya puedes comenzar a gestionar tus finanzas personales de manera fácil y segura.</p>
<div class="status-card success">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="52" valign="top">
<div style="width: 40px; height: 40px; background-color: #c6f6d5; border-radius: 50%; text-align: center; line-height: 40px; font-size: 18px;"></div>
</td>
<td valign="top">
<p class="status-title">Cuenta Registrada</p>
</td>
</tr>
</table>
<p style="margin-top: 16px;"><strong>Email:</strong> {{ $userEmail }}</p>
</div>
<div class="status-card info">
<p class="status-title" style="color: #2d3748; margin-bottom: 16px;">Funcionalidades Disponibles</p>
<ul class="info-list">
<li>Registrar ingresos y gastos</li>
<li>Visualizar reportes y gráficos</li>
<li>Gestionar tu presupuesto mensual</li>
<li>Exportar datos en múltiples formatos</li>
</ul>
</div>
<div class="btn-container">
<a href="https://webmoney.cnxifly.com/login" class="btn btn-primary">INICIAR SESIÓN</a>
</div>
<div class="divider"></div>
<p style="text-align: center; color: #6c757d; font-size: 14px;">
Gracias por confiar en nosotros.
</p>
</div>
@endif
@endsection

View File

@ -31,6 +31,11 @@
Route::post('/register', [AuthController::class, 'register'])->middleware('throttle:register');
Route::post('/login', [AuthController::class, 'login'])->middleware('throttle:login');
// Account activation (public)
Route::post('/activate', [AuthController::class, 'activateAccount']);
Route::post('/resend-activation', [AuthController::class, 'resendActivation']);
Route::post('/cancel-registration', [AuthController::class, 'cancelRegistration']);
// Plans (public - for pricing page)
Route::get('/plans', [PlanController::class, 'index']);
Route::get('/plans/{slug}', [PlanController::class, 'show']);
@ -41,6 +46,12 @@
// PayPal webhook (public - called by PayPal)
Route::post('/paypal/webhook', [SubscriptionController::class, 'webhook']);
// Subscription start for new users (public - used after registration)
Route::post('/subscription/start', [SubscriptionController::class, 'startSubscription']);
// Subscription confirm for new users (public - called after PayPal redirect)
Route::post('/subscription/confirm-public', [SubscriptionController::class, 'confirmPublic']);
// Email testing routes (should be protected in production)
Route::post('/email/send-test', [EmailTestController::class, 'sendTest']);
Route::get('/email/anti-spam-info', [EmailTestController::class, 'getAntiSpamInfo']);

View File

@ -29,6 +29,8 @@ import Billing from './pages/Billing';
import Users from './pages/Users';
import SiteSettings from './pages/SiteSettings';
import Register from './pages/Register';
import ActivateAccount from './pages/ActivateAccount';
import PaymentSuccess from './pages/PaymentSuccess';
function App() {
return (
@ -38,6 +40,8 @@ function App() {
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/activate" element={<ActivateAccount />} />
<Route path="/payment-success" element={<PaymentSuccess />} />
<Route
path="/dashboard"
element={

View File

@ -5,6 +5,9 @@ import { useAuth } from '../context/AuthContext';
const ProtectedRoute = ({ children }) => {
const { isAuthenticated, loading } = useAuth();
// Verificar também o token no localStorage (para casos de navegação imediata após registro)
const hasToken = !!localStorage.getItem('token');
if (loading) {
return (
<div className="d-flex justify-content-center align-items-center min-vh-100">
@ -15,7 +18,7 @@ const ProtectedRoute = ({ children }) => {
);
}
return isAuthenticated ? children : <Navigate to="/login" />;
return (isAuthenticated || hasToken) ? children : <Navigate to="/login" />;
};
export default ProtectedRoute;

View File

@ -25,10 +25,9 @@ export const AuthProvider = ({ children }) => {
};
const register = async (userData) => {
// Register but don't set user - user needs PayPal payment + email activation first
const response = await authService.register(userData);
if (response.success) {
setUser(response.data.user);
}
// Don't set user here - wait for activation
return response;
};

View File

@ -77,7 +77,56 @@
"loginSuccess": "Successfully logged in",
"loginError": "Login error",
"logoutSuccess": "Successfully logged out",
"invalidCredentials": "Invalid credentials"
"invalidCredentials": "Invalid credentials",
"createAccount": "Create your account",
"backToLogin": "Back to login",
"goToLogin": "Go to login"
},
"login": {
"noAccount": "Don't have an account?",
"createAccount": "Create one here",
"noSubscription": "You don't have an active subscription. Please complete the payment."
},
"errors": {
"connection": "Connection error. Please try again.",
"resendFailed": "Error resending email",
"subscriptionFailed": "Error creating subscription. Please try again."
},
"register": {
"selectPlan": "Select a plan",
"repeatPassword": "Repeat your password",
"continueToPayment": "Continue to payment",
"createAccount": "Create Account",
"alreadyHaveAccount": "Already have an account?",
"loginHere": "Login here",
"paymentCanceled": "Payment was canceled. You can try again."
},
"activate": {
"activating": "Activating your account...",
"pleaseWait": "Please wait while we activate your account.",
"successTitle": "Account Activated!",
"success": "Your account has been successfully activated. You can now use WEBMoney!",
"errorTitle": "Activation Error",
"error": "Could not activate your account. The link may have expired.",
"invalidLink": "Invalid activation link.",
"redirecting": "Redirecting in {{seconds}} seconds...",
"goToDashboard": "Go to Dashboard",
"checkEmail": "Check your email",
"checkEmailMessage": "We sent an activation email to {{email}}. Click the link to activate your account.",
"didntReceive": "Didn't receive the email?",
"resend": "Resend email",
"resendSuccess": "Email resent successfully!"
},
"payment": {
"confirming": "Confirming payment...",
"pleaseWait": "Please wait while we process your payment.",
"successTitle": "Payment Confirmed!",
"successMessage": "Your subscription has been successfully confirmed.",
"checkYourEmail": "Check your email!",
"activationSent": "We sent an activation email to {{email}}. Click the link to activate your account and start using WEBMoney.",
"errorTitle": "Payment Error",
"error": "Error confirming payment",
"noSubscriptionId": "Subscription ID not found"
},
"nav": {
"dashboard": "Dashboard",
@ -2019,6 +2068,7 @@
"lastNamePlaceholder": "Your last name",
"lastNameRequired": "Last name is required",
"name": "Name",
"namePlaceholder": "Your name",
"email": "Email",
"phone": "Phone",
"phoneRequired": "Phone number is required",
@ -2053,6 +2103,7 @@
"billedAnnually": "Billed annually €{{price}}",
"save": "Save {{percent}}%",
"trialDays": "{{days}}-day free trial",
"trial": "trial",
"mostPopular": "Most Popular",
"currentPlan": "Current Plan",
"startFree": "Start Free",
@ -2065,6 +2116,16 @@
"securePayment": "Secure payment",
"cancelAnytime": "Cancel anytime",
"paypalSecure": "Secure payment with PayPal",
"comingSoon": "Coming Soon",
"forPymes": "Tools for SMEs",
"features": {
"multiUsers": "Multiple users",
"integratedBilling": "Integrated billing",
"advancedReports": "Advanced reports",
"apiAccess": "API access",
"prioritySupport": "Priority support",
"dedicatedManager": "Dedicated account manager"
},
"faq": {
"title": "Frequently Asked Questions",
"q1": "Can I change plans at any time?",
@ -2102,7 +2163,8 @@
"trialing": "Trial",
"canceled": "Canceled",
"expired": "Expired",
"past_due": "Past Due"
"past_due": "Past Due",
"pending": "Pending"
},
"invoiceStatus": {
"paid": "Paid",
@ -2116,13 +2178,20 @@
"subscriptionConfirmed": "Subscription confirmed successfully!",
"confirmError": "Error confirming subscription",
"subscriptionCanceled": "Subscription canceled",
"subscriptionCanceledRefunded": "Subscription canceled and refund processed",
"cancelError": "Error canceling subscription",
"cancelConfirmTitle": "Cancel subscription?",
"cancelConfirmMessage": "Are you sure you want to cancel your subscription?",
"cancelNote1": "You'll keep access until the end of the current period",
"cancelNote2": "Your data will not be deleted",
"cancelNote3": "You can reactivate your subscription at any time",
"confirmCancel": "Yes, Cancel"
"confirmCancel": "Yes, Cancel",
"guaranteePeriod": "Guarantee Period",
"guaranteeMessage": "You have {{days}} day(s) remaining in the 7-day guarantee period. You can cancel and receive a full refund.",
"guaranteeBadge": "Guarantee: {{days}} days",
"requestRefund": "Request full refund",
"refundNote": "The refund will be processed by PayPal within 5-10 business days.",
"cancelAndRefund": "Cancel and Refund"
},
"landing": {
"nav": {
@ -2133,41 +2202,38 @@
"register": "Start Now"
},
"hero": {
"title": "Take Control of Your",
"titleHighlight": "Finances",
"title": "Take Control of Your Finances",
"subtitle": "Intelligent financial management for individuals and businesses. Track income, expenses, and achieve your financial goals.",
"cta": "Start Free",
"ctaSecondary": "View Plans",
"previewBalance": "Total Balance",
"previewIncome": "Income",
"previewExpense": "Expenses"
"learnMore": "Learn More",
"secure": "100% Secure"
},
"features": {
"title": "Everything You Need",
"subtitle": "Powerful tools to manage your money",
"item1": {
"accounts": {
"title": "Multiple Accounts",
"description": "Manage bank accounts, cards, and cash in one place"
},
"item2": {
"title": "Smart Categories",
"description": "Automatic categorization with keywords and subcategories"
},
"item3": {
"title": "Bank Import",
"description": "Import statements from Excel, CSV, OFX, and PDF"
},
"item4": {
"analytics": {
"title": "Detailed Reports",
"description": "Charts and analysis to understand your spending"
},
"item5": {
"title": "Goals & Budgets",
"description": "Define goals and control monthly spending"
"categories": {
"title": "Smart Categories",
"description": "Automatic categorization with keywords and subcategories"
},
"item6": {
"title": "Multi-currency",
"description": "Manage finances in different currencies"
"import": {
"title": "Bank Import",
"description": "Import statements from Excel, CSV, OFX, and PDF"
},
"recurring": {
"title": "Recurring Transactions",
"description": "Automate bills, subscriptions and recurring income"
},
"security": {
"title": "Total Security",
"description": "Bank-level encryption to protect your data"
}
},
"pricing": {
@ -2176,11 +2242,34 @@
"monthly": "Monthly",
"annual": "Annual",
"popular": "Most Popular",
"perMonth": "/month",
"perYear": "/year",
"month": "month",
"year": "year",
"free": "Free",
"startFree": "Start Free",
"subscribe": "Subscribe Now",
"billedAnnually": "Billed annually"
"billedAnnually": "Billed annually €{{price}}",
"comingSoon": "Coming Soon",
"forPymes": "Tools for SMEs",
"features": {
"oneAccount": "1 bank account",
"tenCategories": "10 categories",
"hundredSubcategories": "100 subcategories",
"thousandTransactions": "1,000 transactions",
"unlimitedAccounts": "Unlimited accounts",
"unlimitedCategories": "Unlimited categories",
"unlimitedTransactions": "Unlimited transactions",
"multiUsers": "Multiple users",
"integratedBilling": "Integrated billing",
"advancedReports": "Advanced reports",
"cashFlow": "Cash flow management",
"budgetControl": "Budget control by project",
"businessModule": "Business module",
"prioritySupport": "Priority support"
},
"goldTeaser": {
"title": "GOLD Plan Coming Soon",
"description": "Direct online synchronization with your bank. Connect your accounts and see your transactions automatically updated in real-time."
}
},
"faq": {
"title": "Frequently Asked Questions",
@ -2190,8 +2279,8 @@
"a2": "Yes, you can cancel your subscription at any time without fees. You'll keep access until the end of the period you already paid.",
"q3": "Which banks are compatible?",
"a3": "You can import statements from any bank that exports to Excel, CSV, OFX, or PDF. We have predefined mappings for major banks.",
"q4": "How does the free trial work?",
"a4": "You get full access to all features during the trial period. No credit card required to start. At the end, choose the plan that fits your needs."
"q4": "How does the 7-day guarantee work?",
"a4": "You pay via PayPal and get immediate full access to all features. If you're not satisfied, cancel within 7 days and receive a full refund, no questions asked."
},
"cta": {
"title": "Ready to Transform Your Finances?",
@ -2199,17 +2288,10 @@
"button": "Create Free Account"
},
"footer": {
"description": "Smart Financial Management for individuals and businesses.",
"product": "Product",
"company": "Company",
"legal": "Legal",
"features": "Features",
"pricing": "Pricing",
"about": "About Us",
"contact": "Contact",
"rights": "All rights reserved.",
"privacy": "Privacy Policy",
"terms": "Terms of Use",
"rights": "All rights reserved."
"contact": "Contact"
}
}
}

View File

@ -78,7 +78,56 @@
"loginSuccess": "Sesión iniciada correctamente",
"loginError": "Error al iniciar sesión",
"logoutSuccess": "Sesión cerrada correctamente",
"invalidCredentials": "Credenciales inválidas"
"invalidCredentials": "Credenciales inválidas",
"createAccount": "Crea tu cuenta",
"backToLogin": "Volver al login",
"goToLogin": "Ir al login"
},
"login": {
"noAccount": "¿No tienes cuenta?",
"createAccount": "Crea una aquí",
"noSubscription": "No tienes una suscripción activa. Por favor, completa el pago."
},
"errors": {
"connection": "Error de conexión. Intenta de nuevo.",
"resendFailed": "Error al reenviar email",
"subscriptionFailed": "Error al crear suscripción. Intenta de nuevo."
},
"register": {
"selectPlan": "Selecciona un plan",
"repeatPassword": "Repite tu contraseña",
"continueToPayment": "Continuar al pago",
"createAccount": "Crear Cuenta",
"alreadyHaveAccount": "¿Ya tienes cuenta?",
"loginHere": "Inicia sesión aquí",
"paymentCanceled": "El pago fue cancelado. Puedes intentarlo de nuevo."
},
"activate": {
"activating": "Activando tu cuenta...",
"pleaseWait": "Por favor, espera mientras activamos tu cuenta.",
"successTitle": "¡Cuenta Activada!",
"success": "Tu cuenta ha sido activada exitosamente. ¡Ya puedes usar WEBMoney!",
"errorTitle": "Error de Activación",
"error": "No se pudo activar tu cuenta. El enlace puede haber expirado.",
"invalidLink": "Enlace de activación inválido.",
"redirecting": "Redirigiendo en {{seconds}} segundos...",
"goToDashboard": "Ir al Panel",
"checkEmail": "Revisa tu email",
"checkEmailMessage": "Hemos enviado un email de activación a {{email}}. Haz clic en el enlace para activar tu cuenta.",
"didntReceive": "¿No recibiste el email?",
"resend": "Reenviar email",
"resendSuccess": "¡Email reenviado exitosamente!"
},
"payment": {
"confirming": "Confirmando pago...",
"pleaseWait": "Por favor, espera mientras procesamos tu pago.",
"successTitle": "¡Pago Confirmado!",
"successMessage": "Tu suscripción ha sido confirmada exitosamente.",
"checkYourEmail": "¡Revisa tu email!",
"activationSent": "Hemos enviado un email de activación a {{email}}. Haz clic en el enlace para activar tu cuenta y comenzar a usar WEBMoney.",
"errorTitle": "Error de Pago",
"error": "Error al confirmar el pago",
"noSubscriptionId": "ID de suscripción no encontrado"
},
"nav": {
"dashboard": "Panel",
@ -2011,6 +2060,7 @@
"lastNamePlaceholder": "Tus apellidos",
"lastNameRequired": "Los apellidos son obligatorios",
"name": "Nombre",
"namePlaceholder": "Tu nombre",
"email": "Correo electrónico",
"phone": "Teléfono",
"phoneRequired": "El teléfono es obligatorio",
@ -2045,6 +2095,7 @@
"billedAnnually": "Facturado anualmente €{{price}}",
"save": "Ahorra {{percent}}%",
"trialDays": "{{days}} días de prueba gratis",
"trial": "de prueba",
"mostPopular": "Más Popular",
"currentPlan": "Plan Actual",
"startFree": "Comenzar Gratis",
@ -2057,6 +2108,16 @@
"securePayment": "Pago seguro",
"cancelAnytime": "Cancela cuando quieras",
"paypalSecure": "Pago seguro con PayPal",
"comingSoon": "Próximamente",
"forPymes": "Herramientas para PyMEs",
"features": {
"multiUsers": "Múltiples usuarios",
"integratedBilling": "Facturación integrada",
"advancedReports": "Reportes avanzados",
"apiAccess": "Acceso a API",
"prioritySupport": "Soporte prioritario",
"dedicatedManager": "Gestor de cuenta dedicado"
},
"faq": {
"title": "Preguntas Frecuentes",
"q1": "¿Puedo cambiar de plan en cualquier momento?",
@ -2104,7 +2165,8 @@
"trialing": "En Prueba",
"canceled": "Cancelada",
"expired": "Expirada",
"past_due": "Pago Pendiente"
"past_due": "Pago Pendiente",
"pending": "Pendiente"
},
"invoiceStatus": {
"paid": "Pagada",
@ -2118,13 +2180,20 @@
"subscriptionConfirmed": "¡Suscripción confirmada con éxito!",
"confirmError": "Error al confirmar la suscripción",
"subscriptionCanceled": "Suscripción cancelada",
"subscriptionCanceledRefunded": "Suscripción cancelada y reembolso procesado",
"cancelError": "Error al cancelar la suscripción",
"cancelConfirmTitle": "¿Cancelar suscripción?",
"cancelConfirmMessage": "¿Estás seguro de que deseas cancelar tu suscripción?",
"cancelNote1": "Mantendrás acceso hasta el final del período actual",
"cancelNote2": "Tus datos no se eliminarán",
"cancelNote3": "Puedes reactivar tu suscripción en cualquier momento",
"confirmCancel": "Sí, Cancelar"
"confirmCancel": "Sí, Cancelar",
"guaranteePeriod": "Período de Garantía",
"guaranteeMessage": "Te quedan {{days}} día(s) en el período de garantía de 7 días. Puedes cancelar y recibir un reembolso total.",
"guaranteeBadge": "Garantía: {{days}} días",
"requestRefund": "Solicitar reembolso total",
"refundNote": "El reembolso será procesado por PayPal en 5-10 días hábiles.",
"cancelAndRefund": "Cancelar y Reembolsar"
},
"landing": {
"nav": {
@ -2135,41 +2204,38 @@
"register": "Empezar Ahora"
},
"hero": {
"title": "Toma el Control de tus",
"titleHighlight": "Finanzas",
"title": "Toma el Control de tus Finanzas",
"subtitle": "Gestión financiera inteligente para personas y empresas. Controla ingresos, gastos y alcanza tus metas financieras.",
"cta": "Comenzar Gratis",
"ctaSecondary": "Ver Planes",
"previewBalance": "Saldo Total",
"previewIncome": "Ingresos",
"previewExpense": "Gastos"
"learnMore": "Saber Más",
"secure": "100% Seguro"
},
"features": {
"title": "Todo lo que Necesitas",
"subtitle": "Herramientas potentes para gestionar tu dinero",
"item1": {
"accounts": {
"title": "Múltiples Cuentas",
"description": "Gestiona cuentas bancarias, tarjetas y efectivo en un solo lugar"
},
"item2": {
"title": "Categorías Inteligentes",
"description": "Categorización automática con palabras clave y subcategorías"
},
"item3": {
"title": "Importación Bancaria",
"description": "Importa extractos de Excel, CSV, OFX y PDF"
},
"item4": {
"analytics": {
"title": "Reportes Detallados",
"description": "Gráficos y análisis para entender tus gastos"
},
"item5": {
"title": "Metas y Presupuestos",
"description": "Define objetivos y controla gastos mensuales"
"categories": {
"title": "Categorías Inteligentes",
"description": "Categorización automática con palabras clave y subcategorías"
},
"item6": {
"title": "Multi-moneda",
"description": "Gestiona finanzas en diferentes monedas"
"import": {
"title": "Importación Bancaria",
"description": "Importa extractos de Excel, CSV, OFX y PDF"
},
"recurring": {
"title": "Transacciones Recurrentes",
"description": "Automatiza facturas, suscripciones e ingresos recurrentes"
},
"security": {
"title": "Seguridad Total",
"description": "Cifrado de nivel bancario para proteger tus datos"
}
},
"pricing": {
@ -2178,11 +2244,34 @@
"monthly": "Mensual",
"annual": "Anual",
"popular": "Más Popular",
"perMonth": "/mes",
"perYear": "/año",
"month": "mes",
"year": "año",
"free": "Gratis",
"startFree": "Comenzar Gratis",
"subscribe": "Suscribirse Ahora",
"billedAnnually": "Facturado anualmente"
"billedAnnually": "Facturado anualmente €{{price}}",
"comingSoon": "Próximamente",
"forPymes": "Herramientas para PyMEs",
"features": {
"oneAccount": "1 cuenta bancaria",
"tenCategories": "10 categorías",
"hundredSubcategories": "100 subcategorías",
"thousandTransactions": "1.000 transacciones",
"unlimitedAccounts": "Cuentas ilimitadas",
"unlimitedCategories": "Categorías ilimitadas",
"unlimitedTransactions": "Transacciones ilimitadas",
"multiUsers": "Múltiples usuarios",
"integratedBilling": "Facturación integrada",
"advancedReports": "Reportes avanzados",
"cashFlow": "Gestión de flujo de caja",
"budgetControl": "Control de presupuesto por proyecto",
"businessModule": "Módulo de negocios",
"prioritySupport": "Soporte prioritario"
},
"goldTeaser": {
"title": "Plan GOLD Próximamente",
"description": "Sincronización online directa con tu banco. Conecta tus cuentas y mira tus transacciones actualizadas automáticamente en tiempo real."
}
},
"faq": {
"title": "Preguntas Frecuentes",
@ -2192,8 +2281,8 @@
"a2": "Sí, puedes cancelar tu suscripción en cualquier momento sin cargos. Mantendrás el acceso hasta el final del período que ya pagaste.",
"q3": "¿Qué bancos son compatibles?",
"a3": "Puedes importar extractos de cualquier banco que exporte a Excel, CSV, OFX o PDF. Tenemos mapeos predefinidos para los principales bancos.",
"q4": "¿Cómo funciona la prueba gratuita?",
"a4": "Tienes acceso completo a todas las funcionalidades durante el período de prueba. No se requiere tarjeta de crédito para empezar. Al final, elige el plan que se adapte a tus necesidades."
"q4": "¿Cómo funciona la garantía de 7 días?",
"a4": "Pagas con PayPal y obtienes acceso completo inmediato a todas las funcionalidades. Si no estás satisfecho, cancela en los primeros 7 días y recibirás un reembolso total, sin preguntas."
},
"cta": {
"title": "¿Listo para Transformar tus Finanzas?",
@ -2201,17 +2290,10 @@
"button": "Crear Cuenta Gratis"
},
"footer": {
"description": "Gestión Financiera Inteligente para personas y empresas.",
"product": "Producto",
"company": "Empresa",
"legal": "Legal",
"features": "Funcionalidades",
"pricing": "Precios",
"about": "Sobre Nosotros",
"contact": "Contacto",
"rights": "Todos los derechos reservados.",
"privacy": "Política de Privacidad",
"terms": "Términos de Uso",
"rights": "Todos los derechos reservados."
"contact": "Contacto"
}
}
}

View File

@ -79,7 +79,56 @@
"loginSuccess": "Login realizado com sucesso",
"loginError": "Erro ao fazer login",
"logoutSuccess": "Logout realizado com sucesso",
"invalidCredentials": "Credenciais inválidas"
"invalidCredentials": "Credenciais inválidas",
"createAccount": "Crie sua conta",
"backToLogin": "Voltar para login",
"goToLogin": "Ir para login"
},
"login": {
"noAccount": "Não tem uma conta?",
"createAccount": "Crie uma aqui",
"noSubscription": "Você não possui uma assinatura ativa. Por favor, complete o pagamento."
},
"errors": {
"connection": "Erro de conexão. Tente novamente.",
"resendFailed": "Erro ao reenviar email",
"subscriptionFailed": "Erro ao criar assinatura. Tente novamente."
},
"register": {
"selectPlan": "Selecione um plano",
"repeatPassword": "Repita sua senha",
"continueToPayment": "Continuar para pagamento",
"createAccount": "Criar Conta",
"alreadyHaveAccount": "Já tem uma conta?",
"loginHere": "Entre aqui",
"paymentCanceled": "O pagamento foi cancelado. Você pode tentar novamente."
},
"activate": {
"activating": "Ativando sua conta...",
"pleaseWait": "Por favor, aguarde enquanto ativamos sua conta.",
"successTitle": "Conta Ativada!",
"success": "Sua conta foi ativada com sucesso. Agora você pode usar o WEBMoney!",
"errorTitle": "Erro na Ativação",
"error": "Não foi possível ativar sua conta. O link pode ter expirado.",
"invalidLink": "Link de ativação inválido.",
"redirecting": "Redirecionando em {{seconds}} segundos...",
"goToDashboard": "Ir para o Painel",
"checkEmail": "Verifique seu email",
"checkEmailMessage": "Enviamos um email de ativação para {{email}}. Clique no link para ativar sua conta.",
"didntReceive": "Não recebeu o email?",
"resend": "Reenviar email",
"resendSuccess": "Email reenviado com sucesso!"
},
"payment": {
"confirming": "Confirmando pagamento...",
"pleaseWait": "Por favor, aguarde enquanto processamos seu pagamento.",
"successTitle": "Pagamento Confirmado!",
"successMessage": "Sua assinatura foi confirmada com sucesso.",
"checkYourEmail": "Verifique seu email!",
"activationSent": "Enviamos um email de ativação para {{email}}. Clique no link para ativar sua conta e começar a usar o WEBMoney.",
"errorTitle": "Erro no Pagamento",
"error": "Erro ao confirmar pagamento",
"noSubscriptionId": "ID da assinatura não encontrado"
},
"nav": {
"dashboard": "Painel",
@ -2029,6 +2078,7 @@
"lastNamePlaceholder": "Seu sobrenome",
"lastNameRequired": "O sobrenome é obrigatório",
"name": "Nome",
"namePlaceholder": "Seu nome",
"email": "E-mail",
"phone": "Telefone",
"phoneRequired": "O telefone é obrigatório",
@ -2063,6 +2113,7 @@
"billedAnnually": "Cobrado anualmente €{{price}}",
"save": "Economize {{percent}}%",
"trialDays": "{{days}} dias de teste grátis",
"trial": "de teste",
"mostPopular": "Mais Popular",
"currentPlan": "Plano Atual",
"startFree": "Começar Grátis",
@ -2075,6 +2126,16 @@
"securePayment": "Pagamento seguro",
"cancelAnytime": "Cancele quando quiser",
"paypalSecure": "Pagamento seguro com PayPal",
"comingSoon": "Em Breve",
"forPymes": "Ferramentas para PMEs",
"features": {
"multiUsers": "Múltiplos usuários",
"integratedBilling": "Faturamento integrado",
"advancedReports": "Relatórios avançados",
"apiAccess": "Acesso à API",
"prioritySupport": "Suporte prioritário",
"dedicatedManager": "Gerente de conta dedicado"
},
"faq": {
"title": "Perguntas Frequentes",
"q1": "Posso mudar de plano a qualquer momento?",
@ -2122,7 +2183,8 @@
"trialing": "Em Teste",
"canceled": "Cancelada",
"expired": "Expirada",
"past_due": "Pagamento Pendente"
"past_due": "Pagamento Pendente",
"pending": "Pendente"
},
"invoiceStatus": {
"paid": "Paga",
@ -2136,13 +2198,20 @@
"subscriptionConfirmed": "Assinatura confirmada com sucesso!",
"confirmError": "Erro ao confirmar assinatura",
"subscriptionCanceled": "Assinatura cancelada",
"subscriptionCanceledRefunded": "Assinatura cancelada e reembolso processado",
"cancelError": "Erro ao cancelar assinatura",
"cancelConfirmTitle": "Cancelar assinatura?",
"cancelConfirmMessage": "Tem certeza que deseja cancelar sua assinatura?",
"cancelNote1": "Você manterá acesso até o final do período atual",
"cancelNote2": "Seus dados não serão excluídos",
"cancelNote3": "Você pode reativar sua assinatura a qualquer momento",
"confirmCancel": "Sim, Cancelar"
"confirmCancel": "Sim, Cancelar",
"guaranteePeriod": "Período de Garantia",
"guaranteeMessage": "Você ainda tem {{days}} dia(s) restantes no período de garantia de 7 dias. Você pode cancelar e receber um reembolso total.",
"guaranteeBadge": "Garantia: {{days}} dias",
"requestRefund": "Solicitar reembolso total",
"refundNote": "O reembolso será processado pelo PayPal em 5-10 dias úteis.",
"cancelAndRefund": "Cancelar e Reembolsar"
},
"landing": {
"nav": {
@ -2153,41 +2222,38 @@
"register": "Começar Agora"
},
"hero": {
"title": "Assuma o Controle das suas",
"titleHighlight": "Finanças",
"title": "Assuma o Controle das suas Finanças",
"subtitle": "Gestão financeira inteligente para pessoas e empresas. Acompanhe receitas, despesas e alcance seus objetivos financeiros.",
"cta": "Começar Grátis",
"ctaSecondary": "Ver Planos",
"previewBalance": "Saldo Total",
"previewIncome": "Receitas",
"previewExpense": "Despesas"
"learnMore": "Saiba Mais",
"secure": "100% Seguro"
},
"features": {
"title": "Tudo que Você Precisa",
"subtitle": "Ferramentas poderosas para gerenciar seu dinheiro",
"item1": {
"accounts": {
"title": "Múltiplas Contas",
"description": "Gerencie contas bancárias, cartões e dinheiro em um só lugar"
},
"item2": {
"title": "Categorias Inteligentes",
"description": "Categorização automática com palavras-chave e subcategorias"
},
"item3": {
"title": "Importação Bancária",
"description": "Importe extratos de Excel, CSV, OFX e PDF"
},
"item4": {
"analytics": {
"title": "Relatórios Detalhados",
"description": "Gráficos e análises para entender seus gastos"
},
"item5": {
"title": "Metas e Orçamentos",
"description": "Defina objetivos e controle gastos mensais"
"categories": {
"title": "Categorias Inteligentes",
"description": "Categorização automática com palavras-chave e subcategorias"
},
"item6": {
"title": "Multi-moeda",
"description": "Gerencie finanças em diferentes moedas"
"import": {
"title": "Importação Bancária",
"description": "Importe extratos de Excel, CSV, OFX e PDF"
},
"recurring": {
"title": "Transações Recorrentes",
"description": "Automatize contas, assinaturas e receitas recorrentes"
},
"security": {
"title": "Segurança Total",
"description": "Criptografia de nível bancário para proteger seus dados"
}
},
"pricing": {
@ -2196,11 +2262,34 @@
"monthly": "Mensal",
"annual": "Anual",
"popular": "Mais Popular",
"perMonth": "/mês",
"perYear": "/ano",
"month": "mês",
"year": "ano",
"free": "Grátis",
"startFree": "Começar Grátis",
"subscribe": "Assinar Agora",
"billedAnnually": "Cobrado anualmente"
"billedAnnually": "Cobrado anualmente €{{price}}",
"comingSoon": "Em Breve",
"forPymes": "Ferramentas para PMEs",
"features": {
"oneAccount": "1 conta bancária",
"tenCategories": "10 categorias",
"hundredSubcategories": "100 subcategorias",
"thousandTransactions": "1.000 transações",
"unlimitedAccounts": "Contas ilimitadas",
"unlimitedCategories": "Categorias ilimitadas",
"unlimitedTransactions": "Transações ilimitadas",
"multiUsers": "Múltiplos usuários",
"integratedBilling": "Faturamento integrado",
"advancedReports": "Relatórios avançados",
"cashFlow": "Gestão de fluxo de caixa",
"budgetControl": "Controle de orçamento por projeto",
"businessModule": "Módulo de negócios",
"prioritySupport": "Suporte prioritário"
},
"goldTeaser": {
"title": "Plano GOLD Em Breve",
"description": "Sincronização online direta com seu banco. Conecte suas contas e veja suas transações atualizadas automaticamente em tempo real."
}
},
"faq": {
"title": "Perguntas Frequentes",
@ -2210,8 +2299,8 @@
"a2": "Sim, você pode cancelar sua assinatura a qualquer momento sem taxas. Você manterá o acesso até o final do período que já pagou.",
"q3": "Quais bancos são compatíveis?",
"a3": "Você pode importar extratos de qualquer banco que exporte para Excel, CSV, OFX ou PDF. Temos mapeamentos predefinidos para os principais bancos.",
"q4": "Como funciona o período de teste?",
"a4": "Você tem acesso completo a todos os recursos durante o período de teste. Não é necessário cartão de crédito para começar. No final, escolha o plano que atende às suas necessidades."
"q4": "Como funciona a garantia de 7 dias?",
"a4": "Você paga via PayPal e obtém acesso completo imediato a todos os recursos. Se não ficar satisfeito, cancele em até 7 dias e receba reembolso total, sem perguntas."
},
"cta": {
"title": "Pronto para Transformar suas Finanças?",
@ -2219,17 +2308,10 @@
"button": "Criar Conta Grátis"
},
"footer": {
"description": "Gestão Financeira Inteligente para pessoas e empresas.",
"product": "Produto",
"company": "Empresa",
"legal": "Legal",
"features": "Recursos",
"pricing": "Preços",
"about": "Sobre Nós",
"contact": "Contato",
"rights": "Todos os direitos reservados.",
"privacy": "Política de Privacidade",
"terms": "Termos de Uso",
"rights": "Todos os direitos reservados."
"contact": "Contato"
}
}
}

View File

@ -0,0 +1,125 @@
import React, { useState, useEffect } from 'react';
import { useSearchParams, useNavigate, Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import api from '../services/api';
import logo from '../assets/logo-white.png';
const ActivateAccount = () => {
const { t } = useTranslation();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [status, setStatus] = useState('loading'); // loading, success, error
const [message, setMessage] = useState('');
const [countdown, setCountdown] = useState(5);
useEffect(() => {
const token = searchParams.get('token');
if (!token) {
setStatus('error');
setMessage(t('activate.invalidLink'));
return;
}
activateAccount(token);
}, [searchParams]);
useEffect(() => {
if (status === 'success' && countdown > 0) {
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
return () => clearTimeout(timer);
} else if (status === 'success' && countdown === 0) {
navigate('/dashboard');
}
}, [status, countdown, navigate]);
const activateAccount = async (token) => {
try {
const response = await api.post('/activate', { token });
if (response.data.success) {
// Save token and user to localStorage
localStorage.setItem('token', response.data.data.token);
localStorage.setItem('user', JSON.stringify(response.data.data.user));
setStatus('success');
setMessage(t('activate.success'));
} else {
setStatus('error');
setMessage(response.data.message || t('activate.error'));
}
} catch (error) {
setStatus('error');
setMessage(error.response?.data?.message || t('activate.error'));
}
};
return (
<div className="min-vh-100 d-flex align-items-center justify-content-center"
style={{ background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)' }}>
<div className="container">
<div className="row justify-content-center">
<div className="col-md-6 col-lg-5">
<div className="card shadow-lg border-0">
<div className="card-body p-5 text-center">
<Link to="/">
<img src={logo} alt="WEBMoney" className="mb-4" style={{ height: '80px', width: 'auto' }} />
</Link>
{status === 'loading' && (
<>
<div className="spinner-border text-primary mb-4" role="status">
<span className="visually-hidden">{t('common.loading')}</span>
</div>
<h4 className="mb-3">{t('activate.activating')}</h4>
<p className="text-muted">{t('activate.pleaseWait')}</p>
</>
)}
{status === 'success' && (
<>
<div className="mb-4">
<i className="bi bi-check-circle-fill text-success" style={{ fontSize: '80px' }}></i>
</div>
<h4 className="mb-3 text-success">{t('activate.successTitle')}</h4>
<p className="text-muted mb-4">{message}</p>
<div className="alert alert-info">
<i className="bi bi-clock me-2"></i>
{t('activate.redirecting', { seconds: countdown })}
</div>
<Link to="/dashboard" className="btn btn-primary btn-lg w-100">
<i className="bi bi-speedometer2 me-2"></i>
{t('activate.goToDashboard')}
</Link>
</>
)}
{status === 'error' && (
<>
<div className="mb-4">
<i className="bi bi-x-circle-fill text-danger" style={{ fontSize: '80px' }}></i>
</div>
<h4 className="mb-3 text-danger">{t('activate.errorTitle')}</h4>
<p className="text-muted mb-4">{message}</p>
<div className="d-grid gap-2">
<Link to="/login" className="btn btn-primary btn-lg">
<i className="bi bi-box-arrow-in-right me-2"></i>
{t('auth.login')}
</Link>
<Link to="/" className="btn btn-outline-secondary">
<i className="bi bi-house me-2"></i>
{t('common.back')}
</Link>
</div>
</>
)}
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default ActivateAccount;

View File

@ -16,6 +16,9 @@ export default function Billing() {
const [loading, setLoading] = useState(true);
const [canceling, setCanceling] = useState(false);
const [showCancelModal, setShowCancelModal] = useState(false);
const [requestRefund, setRequestRefund] = useState(false);
const [withinGuaranteePeriod, setWithinGuaranteePeriod] = useState(false);
const [guaranteeDaysRemaining, setGuaranteeDaysRemaining] = useState(0);
useEffect(() => {
// Handle subscription confirmation from PayPal return
@ -59,6 +62,8 @@ export default function Billing() {
if (statusResponse.data.success) {
setSubscription(statusResponse.data.data.subscription);
setPlan(statusResponse.data.data.plan);
setWithinGuaranteePeriod(statusResponse.data.data.within_guarantee_period || false);
setGuaranteeDaysRemaining(statusResponse.data.data.guarantee_days_remaining || 0);
}
// Load invoices
@ -79,11 +84,17 @@ export default function Billing() {
const handleCancelSubscription = async () => {
try {
setCanceling(true);
const response = await api.post('/subscription/cancel');
const response = await api.post('/subscription/cancel', {
request_refund: requestRefund && withinGuaranteePeriod,
});
if (response.data.success) {
showToast(t('billing.subscriptionCanceled'), 'success');
const message = response.data.data?.refunded
? t('billing.subscriptionCanceledRefunded', 'Assinatura cancelada e reembolso processado')
: t('billing.subscriptionCanceled');
showToast(message, 'success');
setShowCancelModal(false);
setRequestRefund(false);
loadData();
}
} catch (error) {
@ -162,6 +173,13 @@ export default function Billing() {
{t(`billing.status.${subscription.status}`)}
</span>
{withinGuaranteePeriod && (
<span className="badge bg-info me-2">
<i className="bi bi-shield-check me-1"></i>
{t('billing.guaranteeBadge', { days: guaranteeDaysRemaining, defaultValue: `Garantia: ${guaranteeDaysRemaining} dias` })}
</span>
)}
{subscription.status === 'trialing' && subscription.trial_ends_at && (
<small className="text-muted">
{t('billing.trialEnds', { date: formatDate(subscription.trial_ends_at) })}
@ -242,7 +260,7 @@ export default function Billing() {
{plan.features?.slice(0, Math.ceil(plan.features.length / 2)).map((feature, idx) => (
<li key={idx} className="mb-2">
<i className="bi bi-check-circle-fill text-success me-2"></i>
{feature}
{t(feature, feature)}
</li>
))}
</ul>
@ -252,7 +270,7 @@ export default function Billing() {
{plan.features?.slice(Math.ceil(plan.features.length / 2)).map((feature, idx) => (
<li key={idx} className="mb-2">
<i className="bi bi-check-circle-fill text-success me-2"></i>
{feature}
{t(feature, feature)}
</li>
))}
</ul>
@ -318,7 +336,7 @@ export default function Billing() {
<th>{t('billing.date')}</th>
<th>{t('billing.description')}</th>
<th className="text-end">{t('billing.amount')}</th>
<th>{t('billing.status')}</th>
<th>{t('common.status')}</th>
<th></th>
</tr>
</thead>
@ -326,12 +344,12 @@ export default function Billing() {
{invoices.map((invoice) => (
<tr key={invoice.id}>
<td>
<code>{invoice.invoice_number}</code>
<code>{invoice.number || '-'}</code>
</td>
<td>{formatDate(invoice.invoice_date)}</td>
<td>{formatDate(invoice.paid_at || invoice.created_at)}</td>
<td>{invoice.description || '-'}</td>
<td className="text-end">
{formatCurrency(invoice.total_amount, invoice.currency)}
{invoice.formatted_total || formatCurrency(invoice.total, invoice.currency)}
</td>
<td>
<span className={`badge ${getInvoiceStatusBadge(invoice.status)}`}>
@ -369,28 +387,60 @@ export default function Billing() {
<button
type="button"
className="btn-close"
onClick={() => setShowCancelModal(false)}
onClick={() => { setShowCancelModal(false); setRequestRefund(false); }}
></button>
</div>
<div className="modal-body">
<p>{t('billing.cancelConfirmMessage')}</p>
<ul className="text-muted">
{withinGuaranteePeriod ? (
<>
<div className="alert alert-info">
<i className="bi bi-info-circle me-2"></i>
<strong>{t('billing.guaranteePeriod', 'Período de garantia')}</strong>
<p className="mb-0 mt-1">
{t('billing.guaranteeMessage', {
days: guaranteeDaysRemaining,
defaultValue: `Você ainda tem ${guaranteeDaysRemaining} dia(s) restantes no período de garantia de 7 dias. Você pode cancelar e receber um reembolso total.`
})}
</p>
</div>
<div className="form-check mb-3">
<input
className="form-check-input"
type="checkbox"
id="requestRefund"
checked={requestRefund}
onChange={(e) => setRequestRefund(e.target.checked)}
/>
<label className="form-check-label" htmlFor="requestRefund">
<strong>{t('billing.requestRefund', 'Solicitar reembolso total')}</strong>
<small className="d-block text-muted">
{t('billing.refundNote', 'O reembolso será processado pela PayPal em até 5-10 dias úteis.')}
</small>
</label>
</div>
</>
) : (
<p>{t('billing.cancelConfirmMessage')}</p>
)}
<ul className="text-muted small">
<li>{t('billing.cancelNote1')}</li>
<li>{t('billing.cancelNote2')}</li>
<li>{t('billing.cancelNote3')}</li>
{!requestRefund && <li>{t('billing.cancelNote3')}</li>}
</ul>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
onClick={() => setShowCancelModal(false)}
onClick={() => { setShowCancelModal(false); setRequestRefund(false); }}
>
{t('common.cancel')}
</button>
<button
type="button"
className="btn btn-danger"
className={`btn ${requestRefund ? 'btn-warning' : 'btn-danger'}`}
onClick={handleCancelSubscription}
disabled={canceling}
>
@ -399,6 +449,8 @@ export default function Billing() {
<span className="spinner-border spinner-border-sm me-2"></span>
{t('common.processing')}
</>
) : requestRefund ? (
t('billing.cancelAndRefund', 'Cancelar e Reembolsar')
) : (
t('billing.confirmCancel')
)}

View File

@ -209,6 +209,47 @@
font-weight: 600;
}
.coming-soon-badge {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: linear-gradient(135deg, #6366f1, #4f46e5);
color: white;
padding: 6px 20px;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 600;
white-space: nowrap;
}
.pricing-card.coming-soon {
opacity: 0.9;
border: 2px dashed rgba(99, 102, 241, 0.5);
}
.pricing-card.coming-soon .coming-soon-text {
font-size: 3rem;
color: #6366f1;
}
.pricing-card.coming-soon .pricing-features li {
opacity: 0.7;
}
/* Gold Plan Teaser */
.gold-teaser {
background: linear-gradient(135deg, rgba(234, 179, 8, 0.1) 0%, rgba(161, 98, 7, 0.2) 100%);
border: 2px solid rgba(234, 179, 8, 0.3);
backdrop-filter: blur(10px);
}
.gold-teaser h4 {
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
}
.pricing-header {
text-align: center;
margin-bottom: 24px;

View File

@ -253,20 +253,30 @@ export default function Landing() {
) : (
<div className="row justify-content-center g-4">
{plans.map((plan) => (
<div key={plan.id} className="col-lg-4 col-md-6">
<div className={`pricing-card h-100 ${plan.is_featured ? 'featured' : ''}`}>
<div key={plan.id} className="col-lg-3 col-md-6">
<div className={`pricing-card h-100 ${plan.is_featured ? 'featured' : ''} ${plan.coming_soon ? 'coming-soon' : ''}`}>
{plan.is_featured && (
<div className="featured-badge">
<i className="bi bi-star-fill me-1"></i>
{t('landing.pricing.popular')}
</div>
)}
{plan.coming_soon && (
<div className="coming-soon-badge">
<i className="bi bi-clock me-1"></i>
{t('landing.pricing.comingSoon')}
</div>
)}
<div className="pricing-header">
<h3>{plan.name}</h3>
<div className="price">
{plan.is_free ? (
<span className="amount">{t('landing.pricing.free')}</span>
) : plan.coming_soon ? (
<span className="amount coming-soon-text">
<i className="bi bi-rocket-takeoff"></i>
</span>
) : (
<>
<span className="currency"></span>
@ -275,35 +285,63 @@ export default function Landing() {
</>
)}
</div>
{plan.billing_period === 'annual' && !plan.is_free && (
{plan.billing_period === 'annual' && !plan.is_free && !plan.coming_soon && (
<p className="billing-note">
{t('landing.pricing.billedAnnually', { price: plan.price })}
</p>
)}
{plan.coming_soon && (
<p className="billing-note">
{t('landing.pricing.forPymes')}
</p>
)}
</div>
<ul className="pricing-features">
{(plan.features || []).map((feature, idx) => (
<li key={idx}>
<i className="bi bi-check-circle-fill text-success me-2"></i>
{feature}
{feature.startsWith('landing.pricing.features.') ? t(feature) : feature}
</li>
))}
</ul>
<div className="pricing-footer">
<Link
to={`/register?plan=${plan.slug}`}
className={`btn w-100 ${plan.is_featured ? 'btn-primary' : 'btn-outline-primary'}`}
>
{plan.is_free ? t('landing.pricing.startFree') : t('landing.pricing.subscribe')}
</Link>
{plan.coming_soon ? (
<button className="btn btn-secondary w-100" disabled>
<i className="bi bi-bell me-2"></i>
{t('landing.pricing.comingSoon')}
</button>
) : (
<Link
to={`/register?plan=${plan.slug}`}
className={`btn w-100 ${plan.is_featured ? 'btn-primary' : 'btn-outline-primary'}`}
>
{plan.is_free ? t('landing.pricing.startFree') : t('landing.pricing.subscribe')}
</Link>
)}
</div>
</div>
</div>
))}
</div>
)}
{/* Gold Plan Teaser */}
<div className="row justify-content-center mt-5">
<div className="col-lg-8">
<div className="gold-teaser text-center p-4 rounded-4">
<div className="d-flex align-items-center justify-content-center gap-3 mb-3">
<i className="bi bi-stars text-warning fs-2"></i>
<h4 className="mb-0 text-warning">{t('landing.pricing.goldTeaser.title')}</h4>
<i className="bi bi-stars text-warning fs-2"></i>
</div>
<p className="mb-0 text-light">
{t('landing.pricing.goldTeaser.description')}
</p>
</div>
</div>
</div>
</div>
</section>

View File

@ -1,10 +1,13 @@
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../context/AuthContext';
import Footer from '../components/Footer';
import api from '../services/api';
import logo from '../assets/logo-white.png';
const Login = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { login } = useAuth();
const [formData, setFormData] = useState({
@ -13,6 +16,9 @@ const Login = () => {
});
const [errors, setErrors] = useState({});
const [loading, setLoading] = useState(false);
const [needsActivation, setNeedsActivation] = useState(false);
const [resendingEmail, setResendingEmail] = useState(false);
const [resendSuccess, setResendSuccess] = useState(false);
const handleChange = (e) => {
setFormData({
@ -23,12 +29,19 @@ const Login = () => {
if (errors[e.target.name]) {
setErrors({ ...errors, [e.target.name]: null });
}
// Reset activation state when email changes
if (e.target.name === 'email') {
setNeedsActivation(false);
setResendSuccess(false);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setErrors({});
setNeedsActivation(false);
setResendSuccess(false);
try {
const response = await login(formData);
@ -36,18 +49,42 @@ const Login = () => {
navigate('/dashboard');
}
} catch (error) {
if (error.response?.data?.errors) {
setErrors(error.response.data.errors);
} else if (error.response?.data?.message) {
setErrors({ general: error.response.data.message });
const errorData = error.response?.data;
// Check if it's an activation error
if (errorData?.error === 'email_not_verified') {
setNeedsActivation(true);
setErrors({ general: errorData.message });
} else if (errorData?.error === 'no_subscription') {
setErrors({ general: t('login.noSubscription', 'Você não possui uma assinatura ativa. Por favor, complete o pagamento.') });
} else if (errorData?.errors) {
setErrors(errorData.errors);
} else if (errorData?.message) {
setErrors({ general: errorData.message });
} else {
setErrors({ general: 'Error de conexión. Intenta nuevamente.' });
setErrors({ general: t('errors.connection', 'Erro de conexão. Tente novamente.') });
}
} finally {
setLoading(false);
}
};
const handleResendActivation = async () => {
setResendingEmail(true);
setResendSuccess(false);
try {
const response = await api.post('/resend-activation', { email: formData.email });
if (response.data.success) {
setResendSuccess(true);
}
} catch (error) {
const message = error.response?.data?.message || t('errors.resendFailed', 'Erro ao reenviar email');
setErrors({ general: message });
} finally {
setResendingEmail(false);
}
};
return (
<div className="container">
<div className="row justify-content-center align-items-center min-vh-100">
@ -55,22 +92,49 @@ const Login = () => {
<div className="card shadow-lg border-0">
<div className="card-body p-5">
<div className="text-center mb-4">
<img src={logo} alt="WebMoney" className="mb-3" style={{ height: '80px', width: 'auto' }} />
<Link to="/">
<img src={logo} alt="WebMoney" className="mb-3" style={{ height: '80px', width: 'auto' }} />
</Link>
<h2 className="fw-bold text-primary">WebMoney</h2>
<p className="text-muted">Gestión Financiera Inteligente</p>
<p className="text-muted">{t('landing.hero.subtitle', 'Gestión Financiera Inteligente')}</p>
</div>
{errors.general && (
<div className="alert alert-danger" role="alert">
<i className="bi bi-exclamation-circle me-2"></i>
<div className={`alert ${needsActivation ? 'alert-warning' : 'alert-danger'}`} role="alert">
<i className={`bi ${needsActivation ? 'bi-envelope-exclamation' : 'bi-exclamation-circle'} me-2`}></i>
{errors.general}
</div>
)}
{needsActivation && (
<div className="mb-3">
{resendSuccess ? (
<div className="alert alert-success">
<i className="bi bi-check-circle me-2"></i>
{t('activate.resendSuccess', 'Email de ativação reenviado! Verifique sua caixa de entrada.')}
</div>
) : (
<button
type="button"
className="btn btn-outline-warning w-100"
onClick={handleResendActivation}
disabled={resendingEmail || !formData.email}
>
{resendingEmail ? (
<span className="spinner-border spinner-border-sm me-2"></span>
) : (
<i className="bi bi-envelope me-2"></i>
)}
{t('activate.resend', 'Reenviar email de ativação')}
</button>
)}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label htmlFor="email" className="form-label">
Email
{t('auth.email', 'Email')}
</label>
<input
type="email"
@ -90,7 +154,7 @@ const Login = () => {
<div className="mb-3">
<label htmlFor="password" className="form-label">
Contraseña
{t('auth.password', 'Contraseña')}
</label>
<input
type="password"
@ -116,13 +180,22 @@ const Login = () => {
{loading ? (
<>
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
Iniciando sesión...
{t('common.processing', 'Procesando...')}
</>
) : (
'Iniciar Sesión'
t('auth.login', 'Iniciar Sesión')
)}
</button>
</form>
<div className="text-center mt-4">
<p className="mb-0">
{t('login.noAccount', '¿No tienes cuenta?')}{' '}
<Link to="/register" className="text-decoration-none fw-semibold">
{t('login.createAccount', 'Crea una aquí')}
</Link>
</p>
</div>
</div>
</div>

View File

@ -0,0 +1,160 @@
import React, { useState, useEffect } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import api from '../services/api';
import logo from '../assets/logo-white.png';
const PaymentSuccess = () => {
const { t } = useTranslation();
const [searchParams] = useSearchParams();
const [status, setStatus] = useState('loading'); // loading, success, error
const [message, setMessage] = useState('');
const [userEmail, setUserEmail] = useState('');
useEffect(() => {
const confirmPayment = async () => {
const subscriptionId = searchParams.get('subscription_id');
const email = searchParams.get('user_email') || sessionStorage.getItem('pendingActivationEmail');
if (email) {
setUserEmail(email);
}
if (!subscriptionId) {
setStatus('error');
setMessage(t('payment.noSubscriptionId', 'ID da assinatura não encontrado'));
return;
}
try {
// Confirm the subscription with PayPal
const response = await api.post('/subscription/confirm-public', {
subscription_id: subscriptionId,
user_email: email,
});
if (response.data.success) {
setStatus('success');
setMessage(response.data.message || t('payment.success', 'Pagamento confirmado!'));
// Clean up session storage
sessionStorage.removeItem('pendingActivationEmail');
} else {
setStatus('error');
setMessage(response.data.message || t('payment.error', 'Erro ao confirmar pagamento'));
}
} catch (error) {
console.error('Payment confirmation error:', error);
setStatus('error');
setMessage(error.response?.data?.message || t('payment.error', 'Erro ao confirmar pagamento'));
}
};
confirmPayment();
}, [searchParams, t]);
const renderContent = () => {
switch (status) {
case 'loading':
return (
<>
<div className="mb-4">
<div className="spinner-border text-primary" style={{ width: '3rem', height: '3rem' }} role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
<h3 className="fw-bold mb-3">
{t('payment.confirming', 'Confirmando pagamento...')}
</h3>
<p className="text-muted">
{t('payment.pleaseWait', 'Por favor, aguarde enquanto processamos seu pagamento.')}
</p>
</>
);
case 'success':
return (
<>
<div className="mb-4">
<div className="bg-success bg-opacity-10 rounded-circle d-inline-flex p-3">
<i className="bi bi-check-circle-fill text-success" style={{ fontSize: '3rem' }}></i>
</div>
</div>
<h3 className="fw-bold text-success mb-3">
{t('payment.successTitle', 'Pagamento Confirmado!')}
</h3>
<p className="text-muted mb-4">
{t('payment.successMessage', 'Sua assinatura foi confirmada com sucesso.')}
</p>
<div className="alert alert-info text-start">
<i className="bi bi-envelope-check me-2"></i>
<strong>{t('payment.checkYourEmail', 'Verifique seu email!')}</strong>
<p className="mb-0 mt-2">
{t('payment.activationSent', 'Enviamos um email de ativação para {{email}}. Clique no link para ativar sua conta e começar a usar o WEBMoney.', { email: userEmail || 'seu email' })}
</p>
</div>
<div className="mt-4">
<Link to="/login" className="btn btn-primary">
<i className="bi bi-box-arrow-in-right me-2"></i>
{t('auth.goToLogin', 'Ir para Login')}
</Link>
</div>
</>
);
case 'error':
return (
<>
<div className="mb-4">
<div className="bg-danger bg-opacity-10 rounded-circle d-inline-flex p-3">
<i className="bi bi-exclamation-circle-fill text-danger" style={{ fontSize: '3rem' }}></i>
</div>
</div>
<h3 className="fw-bold text-danger mb-3">
{t('payment.errorTitle', 'Erro no Pagamento')}
</h3>
<p className="text-muted mb-4">
{message}
</p>
<div className="d-flex gap-2 justify-content-center">
<Link to="/register" className="btn btn-outline-primary">
<i className="bi bi-arrow-left me-2"></i>
{t('common.tryAgain', 'Tentar novamente')}
</Link>
<Link to="/" className="btn btn-outline-secondary">
<i className="bi bi-house me-2"></i>
{t('common.backToHome', 'Voltar ao início')}
</Link>
</div>
</>
);
default:
return null;
}
};
return (
<div className="container">
<div className="row justify-content-center align-items-center min-vh-100">
<div className="col-md-6">
<div className="card shadow-lg border-0">
<div className="card-body p-5 text-center">
<div className="mb-4">
<Link to="/">
<img src={logo} alt="WebMoney" style={{ height: '60px', width: 'auto' }} />
</Link>
</div>
{renderContent()}
</div>
</div>
</div>
</div>
</div>
);
};
export default PaymentSuccess;

View File

@ -1,16 +1,13 @@
import React, { useState, useEffect } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { Link, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../context/AuthContext';
import Footer from '../components/Footer';
import api from '../services/api';
import api, { authService } from '../services/api';
import logo from '../assets/logo-white.png';
const Register = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { register } = useAuth();
const [searchParams, setSearchParams] = useSearchParams();
const [plans, setPlans] = useState([]);
const [selectedPlan, setSelectedPlan] = useState(null);
const [formData, setFormData] = useState({
@ -22,22 +19,56 @@ const Register = () => {
const [errors, setErrors] = useState({});
const [loading, setLoading] = useState(false);
const [loadingPlans, setLoadingPlans] = useState(true);
const [registrationComplete, setRegistrationComplete] = useState(false);
const [registeredEmail, setRegisteredEmail] = useState('');
const [resendingEmail, setResendingEmail] = useState(false);
const [resendSuccess, setResendSuccess] = useState(false);
const [paymentCanceled, setPaymentCanceled] = useState(false);
// Check if payment was canceled
useEffect(() => {
const canceled = searchParams.get('payment_canceled');
const email = sessionStorage.getItem('pendingActivationEmail');
if (canceled === 'true' && email) {
setPaymentCanceled(true);
// Clean up the unactivated account
api.post('/cancel-registration', { email })
.then(() => {
sessionStorage.removeItem('pendingActivationEmail');
// Clean URL
setSearchParams({});
})
.catch((err) => {
console.error('Error canceling registration:', err);
});
}
}, [searchParams, setSearchParams]);
// Carregar planos
useEffect(() => {
const fetchPlans = async () => {
try {
const response = await api.get('/plans');
setPlans(response.data.data || response.data);
// API returns { success: true, data: { plans: [...] } }
const allPlans = response.data.data?.plans || response.data.plans || response.data.data || [];
// Filtrar apenas planos ativos
const activePlans = Array.isArray(allPlans) ? allPlans.filter(p => p.is_active) : [];
setPlans(activePlans);
// Verificar se há plano na URL
const planSlug = searchParams.get('plan');
if (planSlug && response.data.data) {
const plan = response.data.data.find(p => p.slug === planSlug);
if (planSlug && activePlans.length > 0) {
const plan = activePlans.find(p => p.slug === planSlug);
if (plan) {
setSelectedPlan(plan);
}
}
// Se só houver um plano, selecioná-lo automaticamente
if (activePlans.length === 1) {
setSelectedPlan(activePlans[0]);
}
} catch (error) {
console.error('Error loading plans:', error);
} finally {
@ -60,31 +91,43 @@ const Register = () => {
const handleSubmit = async (e) => {
e.preventDefault();
if (!selectedPlan) {
setErrors({ general: t('register.selectPlan', 'Selecione um plano') });
return;
}
setLoading(true);
setErrors({});
try {
const response = await register({
// Step 1: Register user (won't login - needs activation)
const response = await api.post('/register', {
...formData,
plan_id: selectedPlan?.id,
plan_id: selectedPlan.id,
});
if (response.success) {
// Se for plano pago, redirecionar para pagamento
if (selectedPlan && selectedPlan.price > 0) {
try {
const subscriptionResponse = await api.post('/subscription/subscribe', {
plan_id: selectedPlan.id,
});
if (subscriptionResponse.data.approve_url) {
window.location.href = subscriptionResponse.data.approve_url;
return;
}
} catch (subError) {
console.error('Subscription error:', subError);
// Continuar para o dashboard mesmo sem subscrição
if (response.data.success) {
// Step 2: Create PayPal subscription via public endpoint
try {
const subscriptionResponse = await api.post('/subscription/start', {
plan_id: selectedPlan.id,
user_email: formData.email,
});
if (subscriptionResponse.data.data?.approve_url) {
// Save email for later reference when user returns
sessionStorage.setItem('pendingActivationEmail', formData.email);
// Redirect to PayPal
window.location.href = subscriptionResponse.data.data.approve_url;
return;
}
} catch (subError) {
console.error('Subscription error:', subError);
setErrors({
general: t('errors.subscriptionFailed', 'Erro ao criar assinatura. Tente novamente.')
});
}
navigate('/dashboard');
}
} catch (error) {
if (error.response?.data?.errors) {
@ -92,13 +135,93 @@ const Register = () => {
} else if (error.response?.data?.message) {
setErrors({ general: error.response.data.message });
} else {
setErrors({ general: 'Error de conexión. Intenta nuevamente.' });
setErrors({ general: t('errors.connection', 'Erro de conexão. Tente novamente.') });
}
} finally {
setLoading(false);
}
};
const handleResendEmail = async () => {
setResendingEmail(true);
setResendSuccess(false);
try {
await authService.resendActivation(registeredEmail);
setResendSuccess(true);
setTimeout(() => setResendSuccess(false), 5000);
} catch (error) {
console.error('Error resending email:', error);
} finally {
setResendingEmail(false);
}
};
// Show activation pending screen
if (registrationComplete) {
return (
<div className="container">
<div className="row justify-content-center align-items-center min-vh-100">
<div className="col-md-6">
<div className="card shadow-lg border-0">
<div className="card-body p-5 text-center">
<div className="mb-4">
<Link to="/">
<img src={logo} alt="WebMoney" style={{ height: '60px', width: 'auto' }} />
</Link>
</div>
<div className="mb-4">
<div className="bg-success bg-opacity-10 rounded-circle d-inline-flex p-3 mb-3">
<i className="bi bi-envelope-check text-success" style={{ fontSize: '3rem' }}></i>
</div>
<h3 className="fw-bold text-success">
{t('activate.checkEmail', 'Verifique seu email')}
</h3>
</div>
<p className="text-muted mb-4">
{t('activate.checkEmailMessage', 'Enviamos um email de ativação para {{email}}. Clique no link para ativar sua conta.', { email: registeredEmail })}
</p>
<div className="alert alert-info">
<i className="bi bi-info-circle me-2"></i>
{t('activate.didntReceive', 'Não recebeu o email?')}
</div>
<button
className="btn btn-outline-primary"
onClick={handleResendEmail}
disabled={resendingEmail}
>
{resendingEmail ? (
<span className="spinner-border spinner-border-sm me-2"></span>
) : (
<i className="bi bi-arrow-repeat me-2"></i>
)}
{t('activate.resend', 'Reenviar email')}
</button>
{resendSuccess && (
<div className="alert alert-success mt-3">
<i className="bi bi-check-circle me-2"></i>
{t('activate.resendSuccess', 'Email reenviado com sucesso!')}
</div>
)}
<hr className="my-4" />
<Link to="/login" className="text-decoration-none">
<i className="bi bi-arrow-left me-1"></i>
{t('auth.backToLogin', 'Voltar para login')}
</Link>
</div>
</div>
</div>
</div>
</div>
);
}
return (
<div className="container">
<div className="row justify-content-center align-items-center min-vh-100">
@ -153,6 +276,13 @@ const Register = () => {
</div>
)}
{paymentCanceled && (
<div className="alert alert-warning" role="alert">
<i className="bi bi-exclamation-triangle me-2"></i>
{t('register.paymentCanceled', 'O pagamento foi cancelado. Você pode tentar novamente.')}
</div>
)}
{errors.general && (
<div className="alert alert-danger" role="alert">
<i className="bi bi-exclamation-circle me-2"></i>

View File

@ -41,6 +41,7 @@ api.interceptors.response.use(
// Auth Services
export const authService = {
register: async (userData) => {
// Register user but DON'T save token - user needs to pay and activate via email first
const response = await api.post('/register', userData);
return response.data;
},
@ -54,6 +55,22 @@ export const authService = {
return response.data;
},
// Activate account from email link
activateAccount: async (token) => {
const response = await api.post('/activate', { token });
if (response.data.success) {
localStorage.setItem('token', response.data.data.token);
localStorage.setItem('user', JSON.stringify(response.data.data.user));
}
return response.data;
},
// Resend activation email
resendActivation: async (email) => {
const response = await api.post('/resend-activation', { email });
return response.data;
},
logout: async () => {
try {
await api.post('/logout');