feature - 18 - Add billing page
This commit is contained in:
parent
bcbc1ce8e7
commit
fc6fd87c4b
7 changed files with 122 additions and 2 deletions
|
|
@ -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();
|
||||||
|
|
|
||||||
19
app/Http/Middleware/RequireSaasMode.php
Normal file
19
app/Http/Middleware/RequireSaasMode.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
49
resources/views/billing/index.blade.php
Normal file
49
resources/views/billing/index.blade.php
Normal 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>
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue