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:
parent
984855e36c
commit
6292b62315
@ -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",
|
||||
|
||||
@ -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)
|
||||
*/
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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,
|
||||
];
|
||||
|
||||
@ -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,8 +546,39 @@ public function cancel(Request $request): JsonResponse
|
||||
], 404);
|
||||
}
|
||||
|
||||
// If it's a paid plan, cancel on PayPal
|
||||
$refundResult = null;
|
||||
$isWithinGuaranteePeriod = false;
|
||||
|
||||
// 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'
|
||||
@ -291,21 +591,63 @@ public function cancel(Request $request): JsonResponse
|
||||
], 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;
|
||||
|
||||
|
||||
@ -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.",
|
||||
|
||||
35
backend/app/Mail/AccountActivationMail.php
Normal file
35
backend/app/Mail/AccountActivationMail.php
Normal 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',
|
||||
);
|
||||
}
|
||||
}
|
||||
122
backend/app/Mail/SubscriptionCancelledMail.php
Normal file
122
backend/app/Mail/SubscriptionCancelledMail.php
Normal 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'];
|
||||
}
|
||||
}
|
||||
72
backend/app/Models/EmailVerificationToken.php
Normal file
72
backend/app/Models/EmailVerificationToken.php
Normal 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()]);
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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'");
|
||||
}
|
||||
};
|
||||
166
backend/resources/views/emails/account-activation.blade.php
Normal file
166
backend/resources/views/emails/account-activation.blade.php
Normal 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
|
||||
@ -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>
|
||||
</div>
|
||||
@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>
|
||||
@php $locale = $locale ?? 'pt-BR'; @endphp
|
||||
|
||||
@section('title')
|
||||
@if($locale === 'pt-BR')
|
||||
Alerta de Pagamentos
|
||||
@elseif($locale === 'en')
|
||||
Payment Alert
|
||||
@else
|
||||
<div class="summary-row" style="color: #86efac;">
|
||||
<span class="summary-label">✅ Situação</span>
|
||||
<span class="summary-value">Saldo suficiente!</span>
|
||||
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="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
|
||||
|
||||
{{-- 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
|
||||
|
||||
{{-- 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>
|
||||
</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>
|
||||
|
||||
@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
|
||||
|
||||
<!-- 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>
|
||||
</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>
|
||||
</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>
|
||||
</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
|
||||
</div>
|
||||
</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>
|
||||
</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 há 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>
|
||||
</div>
|
||||
<div class="item-details">
|
||||
<span class="badge badge-overdue">✗ Sem saldo</span>
|
||||
@if($item['account_name'])
|
||||
• Conta: {{ $item['account_name'] }}
|
||||
@endif
|
||||
</div>
|
||||
</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
|
||||
<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>
|
||||
<p style="font-size: 11px; color: #94a3b8;">© {{ date('Y') }} WEBMoney - Todos os direitos reservados</p>
|
||||
</div>
|
||||
@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>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@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>
|
||||
|
||||
@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>
|
||||
@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>
|
||||
</div>
|
||||
@endif
|
||||
@endsection
|
||||
|
||||
412
backend/resources/views/emails/layouts/base.blade.php
Normal file
412
backend/resources/views/emails/layouts/base.blade.php
Normal 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>
|
||||
@ -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 vê-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
|
||||
164
backend/resources/views/emails/subscription-cancelled.blade.php
Normal file
164
backend/resources/views/emails/subscription-cancelled.blade.php
Normal 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
|
||||
@ -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
|
||||
@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
|
||||
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>
|
||||
@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>
|
||||
|
||||
@if($language === 'pt-BR')
|
||||
{{-- PORTUGUÊS --}}
|
||||
<h1>Olá, {{ $user->name }}! 👋</h1>
|
||||
|
||||
<p>Sua conta WebMoney foi criada com sucesso. Estamos muito felizes em tê-lo conosco!</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 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="warning-box">
|
||||
<p>⚠️ <strong>Importante:</strong> Recomendamos que você altere sua senha após o primeiro login por motivos de segurança.</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 só 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;">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>
|
||||
|
||||
<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 class="btn-container">
|
||||
<a href="https://webmoney.cnxifly.com/login" class="btn btn-primary">INICIAR SESSÃO</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="warning-box">
|
||||
<p>⚠️ <strong>Important:</strong> We recommend changing your password after your first login for security purposes.</p>
|
||||
@elseif($locale === 'en')
|
||||
{{-- English --}}
|
||||
<p class="greeting">Hello, {{ $user->name }}</p>
|
||||
|
||||
<div class="content">
|
||||
<p>Welcome to WEBMoney! Your account has been successfully created.</p>
|
||||
|
||||
<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 style="text-align: center;">
|
||||
<a href="{{ $loginUrl }}" class="button">Access My Account</a>
|
||||
<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 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>
|
||||
<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>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>
|
||||
<div class="btn-container">
|
||||
<a href="https://webmoney.cnxifly.com/login" class="btn btn-primary">SIGN IN</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@else
|
||||
{{-- ESPAÑOL (default) --}}
|
||||
<h1>¡Hola, {{ $user->name }}! 👋</h1>
|
||||
{{-- Spanish (default) --}}
|
||||
<p class="greeting">Hola, {{ $user->name }}</p>
|
||||
|
||||
<p>Tu cuenta de WebMoney ha sido creada exitosamente. ¡Estamos muy contentos de tenerte con nosotros!</p>
|
||||
<div class="content">
|
||||
<p>Bienvenido a WEBMoney. Tu cuenta ha sido creada exitosamente.</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 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="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 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 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>
|
||||
<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>
|
||||
|
||||
<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 class="btn-container">
|
||||
<a href="https://webmoney.cnxifly.com/login" class="btn btn-primary">INICIAR SESIÓN</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@endif
|
||||
@endsection
|
||||
|
||||
@ -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;">
|
||||
@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. Já 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="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 -->
|
||||
<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>
|
||||
<td valign="top">
|
||||
<p class="status-title">Conta Registada</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p style="margin-top: 16px;"><strong>Email:</strong> {{ $userEmail }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<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>
|
||||
|
||||
<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 class="btn-container">
|
||||
<a href="https://webmoney.cnxifly.com/login" class="btn btn-primary">INICIAR SESSÃO</a>
|
||||
</div>
|
||||
|
||||
<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 }}
|
||||
<div class="divider"></div>
|
||||
|
||||
<p style="text-align: center; color: #6c757d; font-size: 14px;">
|
||||
Obrigado por confiar em nós.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3 style="margin: 30px 0 15px; color: #333333; font-size: 18px; font-weight: 600;">
|
||||
¿Qué puedes hacer ahora?
|
||||
</h3>
|
||||
@elseif($locale === 'en')
|
||||
{{-- English --}}
|
||||
<p class="greeting">Hello, {{ $userName }}</p>
|
||||
|
||||
<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>
|
||||
<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 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>
|
||||
<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>
|
||||
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: 30px 0;">
|
||||
<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 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 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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<p style="margin: 0; color: #666666; font-size: 14px; line-height: 1.6;">
|
||||
¡Gracias por confiar en nosotros!
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<div class="btn-container">
|
||||
<a href="https://webmoney.cnxifly.com/login" class="btn btn-primary">INICIAR SESIÓN</a>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- Email Footer (outside box) -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" style="margin: 20px auto 0;">
|
||||
<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 style="text-align: center; color: #6c757d; font-size: 14px;">
|
||||
Gracias por confiar en nosotros.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
</div>
|
||||
@endif
|
||||
@endsection
|
||||
|
||||
@ -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']);
|
||||
|
||||
@ -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={
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
125
frontend/src/pages/ActivateAccount.jsx
Normal file
125
frontend/src/pages/ActivateAccount.jsx
Normal 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;
|
||||
@ -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">
|
||||
{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">
|
||||
)}
|
||||
|
||||
<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')
|
||||
)}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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">
|
||||
{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>
|
||||
|
||||
|
||||
@ -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">
|
||||
<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>
|
||||
|
||||
|
||||
160
frontend/src/pages/PaymentSuccess.jsx
Normal file
160
frontend/src/pages/PaymentSuccess.jsx
Normal 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;
|
||||
@ -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,45 +91,137 @@ 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,
|
||||
});
|
||||
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;
|
||||
|
||||
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);
|
||||
// Continuar para o dashboard mesmo sem subscrição
|
||||
setErrors({
|
||||
general: t('errors.subscriptionFailed', 'Erro ao criar assinatura. Tente novamente.')
|
||||
});
|
||||
}
|
||||
}
|
||||
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 });
|
||||
} 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>
|
||||
|
||||
@ -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');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user