feature - 18 - Add billing page

This commit is contained in:
myrmidex 2026-01-07 01:08:58 +01:00
parent bcbc1ce8e7
commit fc6fd87c4b
7 changed files with 122 additions and 2 deletions

View file

@ -19,8 +19,8 @@ public function checkout(Request $request)
$plan = $request->input('plan', 'monthly'); $plan = $request->input('plan', 'monthly');
$priceId = $plan === 'yearly' $priceId = $plan === 'yearly'
? env('STRIPE_PRICE_YEARLY') ? config('services.stripe.price_yearly')
: env('STRIPE_PRICE_MONTHLY'); : config('services.stripe.price_monthly');
return $planner->newSubscription('default', $priceId) return $planner->newSubscription('default', $priceId)
->checkout([ ->checkout([
@ -57,6 +57,41 @@ public function success(Request $request): RedirectResponse
return redirect()->route('dashboard')->with('success', 'Subscription activated!'); return redirect()->route('dashboard')->with('success', 'Subscription activated!');
} }
public function billing(Request $request)
{
$planner = $request->user();
$subscription = $planner->subscription();
if (! $subscription) {
return redirect()->route('subscription.index');
}
$planType = match ($subscription->stripe_price) {
config('services.stripe.price_yearly') => 'Yearly',
config('services.stripe.price_monthly') => 'Monthly',
default => 'Unknown',
};
$nextBillingDate = null;
if ($subscription->stripe_status === 'active') {
try {
$stripeSubscription = Cashier::stripe()->subscriptions->retrieve($subscription->stripe_id);
$nextBillingDate = $stripeSubscription->current_period_end
? now()->setTimestamp($stripeSubscription->current_period_end)
: null;
} catch (\Exception $e) {
// Stripe API error - continue without next billing date
}
}
return view('billing.index', [
'subscription' => $subscription,
'planner' => $planner,
'planType' => $planType,
'nextBillingDate' => $nextBillingDate,
]);
}
public function cancel(Request $request): RedirectResponse public function cancel(Request $request): RedirectResponse
{ {
$planner = $request->user(); $planner = $request->user();

View file

@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class RequireSaasMode
{
public function handle(Request $request, Closure $next): Response
{
if (! is_mode_saas()) {
abort(404);
}
return $next($request);
}
}

View file

@ -1,6 +1,7 @@
<?php <?php
use App\Http\Middleware\ForceJsonResponse; use App\Http\Middleware\ForceJsonResponse;
use App\Http\Middleware\RequireSaasMode;
use App\Http\Middleware\RequireSubscription; use App\Http\Middleware\RequireSubscription;
use App\Services\OutputService; use App\Services\OutputService;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
@ -29,6 +30,7 @@
$middleware->alias([ $middleware->alias([
'subscription' => RequireSubscription::class, 'subscription' => RequireSubscription::class,
'saas' => RequireSaasMode::class,
]); ]);
// Exclude Stripe webhook from CSRF verification // Exclude Stripe webhook from CSRF verification

View file

@ -41,6 +41,8 @@
'webhook' => [ 'webhook' => [
'secret' => env('STRIPE_WEBHOOK_SECRET'), 'secret' => env('STRIPE_WEBHOOK_SECRET'),
], ],
'price_monthly' => env('STRIPE_PRICE_MONTHLY'),
'price_yearly' => env('STRIPE_PRICE_YEARLY'),
], ],
]; ];

View file

@ -0,0 +1,49 @@
<x-layouts.app>
<div class="px-4 sm:px-6 lg:px-8">
<div class="max-w-2xl mx-auto">
<h1 class="text-2xl font-syncopate text-accent-blue mb-8">BILLING</h1>
<div class="border-2 border-secondary rounded-lg p-6">
<h3 class="text-xl font-bold text-primary mb-6">Subscription Details</h3>
<div class="space-y-4">
<div class="flex justify-between items-center border-b border-gray-600 pb-4">
<span class="text-gray-300">Plan</span>
<span class="text-white font-bold">{{ $planType }}</span>
</div>
<div class="flex justify-between items-center border-b border-gray-600 pb-4">
<span class="text-gray-300">Status</span>
<span class="font-bold {{ $subscription->stripe_status === 'active' ? 'text-success' : 'text-warning' }}">
{{ ucfirst($subscription->stripe_status) }}
</span>
</div>
@if($nextBillingDate)
<div class="flex justify-between items-center border-b border-gray-600 pb-4">
<span class="text-gray-300">Next billing date</span>
<span class="text-white">{{ $nextBillingDate->format('F j, Y') }}</span>
</div>
@endif
@if($subscription->ends_at)
<div class="flex justify-between items-center border-b border-gray-600 pb-4">
<span class="text-gray-300">Access until</span>
<span class="text-warning">{{ $subscription->ends_at->format('F j, Y') }}</span>
</div>
@endif
@if($planner->pm_last_four)
<div class="flex justify-between items-center pb-4">
<span class="text-gray-300">Payment method</span>
<span class="text-white">
<span class="text-gray-400">{{ ucfirst($planner->pm_type ?? 'Card') }}</span>
•••• {{ $planner->pm_last_four }}
</span>
</div>
@endif
</div>
</div>
</div>
</div>
</x-layouts.app>

View file

@ -65,6 +65,11 @@ class="inline-flex items-center px-3 py-2 text-sm font-medium {{ request()->rout
x-transition x-transition
class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-gray-700 ring-1 ring-secondary"> class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-gray-700 ring-1 ring-secondary">
<div class="py-1"> <div class="py-1">
@if(is_mode_saas())
<a href="{{ route('billing') }}" class="block px-4 py-2 text-sm text-gray-100 hover:bg-gray-600 hover:text-accent-blue">
Billing
</a>
@endif
<form method="POST" action="{{ route('logout') }}"> <form method="POST" action="{{ route('logout') }}">
@csrf @csrf
<button type="submit" class="block w-full text-left px-4 py-2 text-sm text-gray-100 hover:bg-gray-600 hover:text-accent-blue"> <button type="submit" class="block w-full text-left px-4 py-2 text-sm text-gray-100 hover:bg-gray-600 hover:text-accent-blue">
@ -140,6 +145,11 @@ class="block text-2xl font-medium {{ request()->routeIs('schedule.*') ? 'text-ac
<div class="mt-12 pt-6 border-t border-secondary text-center"> <div class="mt-12 pt-6 border-t border-secondary text-center">
<div class="text-gray-300 mb-4">{{ Auth::user()->name }}</div> <div class="text-gray-300 mb-4">{{ Auth::user()->name }}</div>
@if(is_mode_saas())
<a href="{{ route('billing') }}" class="block text-xl text-accent-blue mb-4">
Billing
</a>
@endif
<form method="POST" action="{{ route('logout') }}"> <form method="POST" action="{{ route('logout') }}">
@csrf @csrf
<button type="submit" class="text-xl text-danger"> <button type="submit" class="text-xl text-danger">

View file

@ -3,6 +3,7 @@
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Auth\LoginController; use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\RegisterController; use App\Http\Controllers\Auth\RegisterController;
use App\Http\Controllers\SubscriptionController;
Route::get('/', function () { Route::get('/', function () {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
@ -42,5 +43,7 @@
Route::get('/users', function () { Route::get('/users', function () {
return view('users.index'); return view('users.index');
})->name('users.index'); })->name('users.index');
Route::get('/billing', [SubscriptionController::class, 'billing'])->name('billing')->middleware('saas');
}); });
}); });