From 2239d2480cebba044ec2e8e563f5c7442e67137a Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Mon, 26 Jan 2026 12:01:53 -0300 Subject: [PATCH] feat: translate billing page and fix flash messages - Move billing routes from /billing to /settings/billing - Add billing translations (en/pt-br) - Update billing/Index.vue to use $t() for all strings - Fix all controllers using route('dashboard') to use route('calendar') or route('accounts') - Replace ->with('error', ...) pattern with session()->flash('flash.banner', ...) - Add self-hosted mode support for hasActiveSubscription() and workspace methods - Add flash translations for account connection errors Co-Authored-By: Claude Opus 4.5 --- .../Controllers/Auth/FacebookController.php | 17 ++-- .../Controllers/Auth/InstagramController.php | 5 +- .../Controllers/Auth/LinkedInController.php | 5 +- .../Auth/LinkedInPageController.php | 17 ++-- .../Controllers/Auth/PinterestController.php | 5 +- .../Controllers/Auth/ThreadsController.php | 5 +- .../Controllers/Auth/TikTokController.php | 5 +- app/Http/Controllers/Auth/XController.php | 5 +- .../Controllers/Auth/YouTubeController.php | 23 ++++-- app/Http/Controllers/BillingController.php | 4 +- app/Models/Traits/HasWorkspace.php | 20 +++++ app/Models/User.php | 5 ++ lang/en/accounts.php | 5 ++ lang/en/billing.php | 37 +++++++++ lang/pt-br/accounts.php | 5 ++ lang/pt-br/billing.php | 37 +++++++++ resources/js/pages/billing/Index.vue | 50 +++++------- routes/web.php | 6 +- .../Social/LinkedInPageControllerTest.php | 3 +- tests/Feature/WorkspaceControllerTest.php | 20 ++++- tests/Unit/Traits/HasWorkspaceTest.php | 81 +++++++++++++++++-- 21 files changed, 291 insertions(+), 69 deletions(-) create mode 100644 lang/en/billing.php create mode 100644 lang/pt-br/billing.php diff --git a/app/Http/Controllers/Auth/FacebookController.php b/app/Http/Controllers/Auth/FacebookController.php index f0fb65fb..e6dcf895 100644 --- a/app/Http/Controllers/Auth/FacebookController.php +++ b/app/Http/Controllers/Auth/FacebookController.php @@ -45,7 +45,10 @@ public function connect(Request $request): Response|RedirectResponse ->first(); if ($existingAccount && ! $existingAccount->isDisconnected()) { - return back()->with('error', 'This platform is already connected.'); + session()->flash('flash.banner', __('accounts.flash.already_connected')); + session()->flash('flash.bannerStyle', 'danger'); + + return back(); } session([ @@ -174,15 +177,19 @@ public function selectPage(Request $request) $workspaceId = session('social_connect_workspace'); if (! $oauthData || ! $workspaceId) { - return redirect()->route('dashboard') - ->with('error', 'Session expired. Please try again.'); + session()->flash('flash.banner', __('accounts.flash.session_expired')); + session()->flash('flash.bannerStyle', 'danger'); + + return redirect()->route('accounts'); } $workspace = Workspace::find($workspaceId); if (! $workspace) { - return redirect()->route('dashboard') - ->with('error', 'Workspace not found.'); + session()->flash('flash.banner', __('accounts.flash.workspace_not_found')); + session()->flash('flash.bannerStyle', 'danger'); + + return redirect()->route('accounts'); } return Inertia::render('accounts/FacebookPageSelect', [ diff --git a/app/Http/Controllers/Auth/InstagramController.php b/app/Http/Controllers/Auth/InstagramController.php index e761b332..d26473ca 100644 --- a/app/Http/Controllers/Auth/InstagramController.php +++ b/app/Http/Controllers/Auth/InstagramController.php @@ -41,7 +41,10 @@ public function connect(Request $request): Response|RedirectResponse ->first(); if ($existingAccount && ! $existingAccount->isDisconnected()) { - return back()->with('error', 'This platform is already connected.'); + session()->flash('flash.banner', __('accounts.flash.already_connected')); + session()->flash('flash.bannerStyle', 'danger'); + + return back(); } session([ diff --git a/app/Http/Controllers/Auth/LinkedInController.php b/app/Http/Controllers/Auth/LinkedInController.php index 9a8e2c99..89413d4b 100644 --- a/app/Http/Controllers/Auth/LinkedInController.php +++ b/app/Http/Controllers/Auth/LinkedInController.php @@ -44,7 +44,10 @@ public function connect(Request $request): Response|RedirectResponse ->first(); if ($existingAccount && ! $existingAccount->isDisconnected()) { - return back()->with('error', 'This platform is already connected.'); + session()->flash('flash.banner', __('accounts.flash.already_connected')); + session()->flash('flash.bannerStyle', 'danger'); + + return back(); } return $this->redirectToProvider($request, $this->driver, $this->scopes); diff --git a/app/Http/Controllers/Auth/LinkedInPageController.php b/app/Http/Controllers/Auth/LinkedInPageController.php index c49d911d..6b151cea 100644 --- a/app/Http/Controllers/Auth/LinkedInPageController.php +++ b/app/Http/Controllers/Auth/LinkedInPageController.php @@ -49,7 +49,10 @@ public function connect(Request $request): SymfonyResponse|RedirectResponse ->first(); if ($existingAccount && ! $existingAccount->isDisconnected()) { - return back()->with('error', 'This platform is already connected.'); + session()->flash('flash.banner', __('accounts.flash.already_connected')); + session()->flash('flash.bannerStyle', 'danger'); + + return back(); } session([ @@ -128,15 +131,19 @@ public function selectPage(Request $request): Response|RedirectResponse $pendingData = session('linkedin_page_pending'); if (! $pendingData) { - return redirect()->route('dashboard') - ->with('error', 'Session expired. Please try again.'); + session()->flash('flash.banner', __('accounts.flash.session_expired')); + session()->flash('flash.bannerStyle', 'danger'); + + return redirect()->route('accounts'); } $workspace = Workspace::find($pendingData['workspace_id']); if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) { - return redirect()->route('dashboard') - ->with('error', 'Workspace not found.'); + session()->flash('flash.banner', __('accounts.flash.workspace_not_found')); + session()->flash('flash.bannerStyle', 'danger'); + + return redirect()->route('accounts'); } return Inertia::render('accounts/LinkedInPageSelect', [ diff --git a/app/Http/Controllers/Auth/PinterestController.php b/app/Http/Controllers/Auth/PinterestController.php index e5b66036..4f335947 100644 --- a/app/Http/Controllers/Auth/PinterestController.php +++ b/app/Http/Controllers/Auth/PinterestController.php @@ -43,7 +43,10 @@ public function connect(Request $request): Response|RedirectResponse ->first(); if ($existingAccount && ! $existingAccount->isDisconnected()) { - return back()->with('error', 'This platform is already connected.'); + session()->flash('flash.banner', __('accounts.flash.already_connected')); + session()->flash('flash.bannerStyle', 'danger'); + + return back(); } return $this->redirectToProvider($request, $this->driver, $this->scopes); diff --git a/app/Http/Controllers/Auth/ThreadsController.php b/app/Http/Controllers/Auth/ThreadsController.php index bccdab11..90ea35b5 100644 --- a/app/Http/Controllers/Auth/ThreadsController.php +++ b/app/Http/Controllers/Auth/ThreadsController.php @@ -41,7 +41,10 @@ public function connect(Request $request): Response|RedirectResponse ->first(); if ($existingAccount && ! $existingAccount->isDisconnected()) { - return back()->with('error', 'This platform is already connected.'); + session()->flash('flash.banner', __('accounts.flash.already_connected')); + session()->flash('flash.bannerStyle', 'danger'); + + return back(); } session([ diff --git a/app/Http/Controllers/Auth/TikTokController.php b/app/Http/Controllers/Auth/TikTokController.php index 4487ec8d..597a6cf5 100644 --- a/app/Http/Controllers/Auth/TikTokController.php +++ b/app/Http/Controllers/Auth/TikTokController.php @@ -41,7 +41,10 @@ public function connect(Request $request): Response|RedirectResponse ->first(); if ($existingAccount && ! $existingAccount->isDisconnected()) { - return back()->with('error', 'This platform is already connected.'); + session()->flash('flash.banner', __('accounts.flash.already_connected')); + session()->flash('flash.bannerStyle', 'danger'); + + return back(); } session(['social_reconnect_id' => $existingAccount?->id]); diff --git a/app/Http/Controllers/Auth/XController.php b/app/Http/Controllers/Auth/XController.php index 2b57cc6b..481dc998 100644 --- a/app/Http/Controllers/Auth/XController.php +++ b/app/Http/Controllers/Auth/XController.php @@ -39,7 +39,10 @@ public function connect(Request $request): Response|RedirectResponse ->first(); if ($existingAccount && ! $existingAccount->isDisconnected()) { - return back()->with('error', 'This platform is already connected.'); + session()->flash('flash.banner', __('accounts.flash.already_connected')); + session()->flash('flash.bannerStyle', 'danger'); + + return back(); } return $this->redirectToProvider($request, $this->driver, $this->scopes); diff --git a/app/Http/Controllers/Auth/YouTubeController.php b/app/Http/Controllers/Auth/YouTubeController.php index fd2c47e7..27790385 100644 --- a/app/Http/Controllers/Auth/YouTubeController.php +++ b/app/Http/Controllers/Auth/YouTubeController.php @@ -41,7 +41,10 @@ public function connect(Request $request): Response|RedirectResponse ->first(); if ($existingAccount && ! $existingAccount->isDisconnected()) { - return back()->with('error', 'This platform is already connected.'); + session()->flash('flash.banner', __('accounts.flash.already_connected')); + session()->flash('flash.bannerStyle', 'danger'); + + return back(); } session([ @@ -164,15 +167,19 @@ public function selectChannel(Request $request) $workspaceId = session('social_connect_workspace'); if (! $oauthData || ! $workspaceId) { - return redirect()->route('dashboard') - ->with('error', 'Session expired. Please try again.'); + session()->flash('flash.banner', __('accounts.flash.session_expired')); + session()->flash('flash.bannerStyle', 'danger'); + + return redirect()->route('accounts'); } $workspace = Workspace::find($workspaceId); if (! $workspace) { - return redirect()->route('dashboard') - ->with('error', 'Workspace not found.'); + session()->flash('flash.banner', __('accounts.flash.workspace_not_found')); + session()->flash('flash.bannerStyle', 'danger'); + + return redirect()->route('accounts'); } // Fetch YouTube channels @@ -183,8 +190,10 @@ public function selectChannel(Request $request) $this->forgetSocialConnectSession(); session()->forget('youtube_oauth'); - return redirect()->route($redirectRoute) - ->with('error', 'No YouTube channels found. Please create a channel first.'); + session()->flash('flash.banner', __('accounts.flash.no_youtube_channels')); + session()->flash('flash.bannerStyle', 'danger'); + + return redirect()->route($redirectRoute); } return inertia('accounts/YouTubeChannelSelect', [ diff --git a/app/Http/Controllers/BillingController.php b/app/Http/Controllers/BillingController.php index 6ecdffb5..9cd5e470 100644 --- a/app/Http/Controllers/BillingController.php +++ b/app/Http/Controllers/BillingController.php @@ -89,9 +89,9 @@ public function processing(Request $request): Response|RedirectResponse $user = $request->user(); $status = $request->query('status', 'processing'); - // If already subscribed, redirect to dashboard + // If already subscribed, redirect to calendar if ($user->subscribed('default')) { - return redirect()->route('dashboard'); + return redirect()->route('calendar'); } // Validate status diff --git a/app/Models/Traits/HasWorkspace.php b/app/Models/Traits/HasWorkspace.php index d48c194c..8998b117 100644 --- a/app/Models/Traits/HasWorkspace.php +++ b/app/Models/Traits/HasWorkspace.php @@ -52,9 +52,14 @@ public function ownedWorkspacesCount(): int /** * Check if user can create more workspaces based on subscription. + * In self-hosted mode, users can create unlimited workspaces. */ public function canCreateWorkspace(): bool { + if (config('trypost.self_hosted')) { + return true; + } + if (! $this->hasActiveSubscription()) { return $this->ownedWorkspacesCount() === 0; } @@ -66,9 +71,14 @@ public function canCreateWorkspace(): bool /** * Increment workspace quantity on subscription. + * Skipped in self-hosted mode (no Stripe subscription). */ public function incrementWorkspaceQuantity(): void { + if (config('trypost.self_hosted')) { + return; + } + if ($this->hasActiveSubscription()) { $this->subscription('default')->incrementQuantity(); } @@ -76,9 +86,14 @@ public function incrementWorkspaceQuantity(): void /** * Decrement workspace quantity on subscription. + * Skipped in self-hosted mode (no Stripe subscription). */ public function decrementWorkspaceQuantity(): void { + if (config('trypost.self_hosted')) { + return; + } + if ($this->hasActiveSubscription()) { $subscription = $this->subscription('default'); @@ -90,9 +105,14 @@ public function decrementWorkspaceQuantity(): void /** * Sync subscription quantity with actual workspace count. + * Skipped in self-hosted mode (no Stripe subscription). */ public function syncWorkspaceQuantity(): void { + if (config('trypost.self_hosted')) { + return; + } + if ($this->hasActiveSubscription()) { $count = $this->ownedWorkspacesCount(); diff --git a/app/Models/User.php b/app/Models/User.php index 3d4bcf09..0fbd5e72 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -88,9 +88,14 @@ public function language(): BelongsTo /** * Check if user has an active subscription. + * In self-hosted mode, always returns true (no subscription required). */ public function hasActiveSubscription(): bool { + if (config('trypost.self_hosted')) { + return true; + } + return $this->subscribed('default'); } diff --git a/lang/en/accounts.php b/lang/en/accounts.php index 6a3ea502..f7c3e30d 100644 --- a/lang/en/accounts.php +++ b/lang/en/accounts.php @@ -59,5 +59,10 @@ 'flash' => [ 'disconnected' => 'Account disconnected successfully!', + 'connected' => 'Account connected successfully!', + 'session_expired' => 'Session expired. Please try again.', + 'workspace_not_found' => 'Workspace not found.', + 'already_connected' => 'This platform is already connected.', + 'no_youtube_channels' => 'No YouTube channels found. Please create a channel first.', ], ]; diff --git a/lang/en/billing.php b/lang/en/billing.php new file mode 100644 index 00000000..33448842 --- /dev/null +++ b/lang/en/billing.php @@ -0,0 +1,37 @@ + 'Subscription', + 'description' => 'Manage your subscription and payment method', + + 'trial' => [ + 'title' => 'Trial period active', + 'description' => 'Your trial ends on :date. After that, your subscription will be charged automatically.', + ], + + 'subscription' => [ + 'title' => 'Your Subscription', + 'workspaces' => 'Workspaces', + 'quantity' => 'Subscription quantity', + 'expires' => 'Expires :date', + 'canceled_on' => 'Your subscription will be canceled on :date', + 'manage' => 'Manage on Stripe', + ], + + 'invoices' => [ + 'title' => 'Invoices', + 'description' => 'Payment history', + 'empty' => 'No invoices found', + 'paid' => 'Paid', + ], + + 'status' => [ + 'active' => 'Active', + 'canceled' => 'Canceled', + 'incomplete' => 'Incomplete', + 'incomplete_expired' => 'Expired', + 'past_due' => 'Past due', + 'trialing' => 'Trial', + 'unpaid' => 'Unpaid', + ], +]; diff --git a/lang/pt-br/accounts.php b/lang/pt-br/accounts.php index 7a49bece..65667854 100644 --- a/lang/pt-br/accounts.php +++ b/lang/pt-br/accounts.php @@ -59,5 +59,10 @@ 'flash' => [ 'disconnected' => 'Conta desconectada com sucesso!', + 'connected' => 'Conta conectada com sucesso!', + 'session_expired' => 'Sessão expirada. Por favor, tente novamente.', + 'workspace_not_found' => 'Workspace não encontrado.', + 'already_connected' => 'Esta plataforma já está conectada.', + 'no_youtube_channels' => 'Nenhum canal do YouTube encontrado. Por favor, crie um canal primeiro.', ], ]; diff --git a/lang/pt-br/billing.php b/lang/pt-br/billing.php new file mode 100644 index 00000000..943b13f9 --- /dev/null +++ b/lang/pt-br/billing.php @@ -0,0 +1,37 @@ + 'Assinatura', + 'description' => 'Gerencie sua assinatura e método de pagamento', + + 'trial' => [ + 'title' => 'Período de teste ativo', + 'description' => 'Seu período de teste termina em :date. Após isso, sua assinatura será cobrada automaticamente.', + ], + + 'subscription' => [ + 'title' => 'Sua Assinatura', + 'workspaces' => 'Workspaces', + 'quantity' => 'Quantidade da assinatura', + 'expires' => 'Expira em :date', + 'canceled_on' => 'Sua assinatura será cancelada em :date', + 'manage' => 'Gerenciar no Stripe', + ], + + 'invoices' => [ + 'title' => 'Faturas', + 'description' => 'Histórico de pagamentos', + 'empty' => 'Nenhuma fatura encontrada', + 'paid' => 'Pago', + ], + + 'status' => [ + 'active' => 'Ativo', + 'canceled' => 'Cancelado', + 'incomplete' => 'Incompleto', + 'incomplete_expired' => 'Expirado', + 'past_due' => 'Vencido', + 'trialing' => 'Teste', + 'unpaid' => 'Não pago', + ], +]; diff --git a/resources/js/pages/billing/Index.vue b/resources/js/pages/billing/Index.vue index 6e5ce76a..4441a1bc 100644 --- a/resources/js/pages/billing/Index.vue +++ b/resources/js/pages/billing/Index.vue @@ -1,5 +1,6 @@