feat(payment): implement secure payment cancellation page with session tracking
- Create PaymentCancelController with authentication and subscription detection - Design responsive cancellation view with red/orange gradient theme - Add session token logging and recent subscription lookup functionality - Update payment cancel route to use new controller with auth middleware - Include security assurances and proper navigation to checkout/dashboard - Remove broken route references and ensure all buttons link to valid pages
This commit is contained in:
51
app/Http/Controllers/PaymentCancelController.php
Normal file
51
app/Http/Controllers/PaymentCancelController.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Subscription;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class PaymentCancelController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show the payment cancellation page
|
||||||
|
*/
|
||||||
|
public function show(Request $request)
|
||||||
|
{
|
||||||
|
// Get the session token from Polar if available
|
||||||
|
$sessionToken = $request->get('customer_session_token');
|
||||||
|
|
||||||
|
Log::info('PaymentCancelController: Cancellation page accessed', [
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'session_token' => $sessionToken ? substr($sessionToken, 0, 20) . '...' : 'none',
|
||||||
|
'ip_address' => $request->ip(),
|
||||||
|
'user_agent' => $request->userAgent(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Look for any recent subscriptions for this user
|
||||||
|
$recentSubscription = null;
|
||||||
|
if (auth()->check()) {
|
||||||
|
$recentMinutes = 15; // Look for subscriptions in last 15 minutes
|
||||||
|
$recentSubscription = Subscription::where('user_id', auth()->id())
|
||||||
|
->where('created_at', '>=', now()->subMinutes($recentMinutes))
|
||||||
|
->whereIn('status', ['pending_payment', 'incomplete', 'cancelled'])
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($recentSubscription) {
|
||||||
|
Log::info('PaymentCancelController: Found recent subscription', [
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'subscription_id' => $recentSubscription->id,
|
||||||
|
'status' => $recentSubscription->status,
|
||||||
|
'provider' => $recentSubscription->provider,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('payment.cancel', [
|
||||||
|
'sessionToken' => $sessionToken,
|
||||||
|
'recentSubscription' => $recentSubscription,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
154
resources/views/payment/cancel.blade.php
Normal file
154
resources/views/payment/cancel.blade.php
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Payment Cancelled - {{ config('app.name') }}</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script>
|
||||||
|
// Initialize dark mode detection
|
||||||
|
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
|
document.documentElement.classList.add('dark')
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen bg-gradient-to-br from-red-50 via-white to-orange-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
|
||||||
|
<!-- Navigation Header -->
|
||||||
|
<header class="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border-b border-gray-200 dark:border-gray-700 sticky top-0 z-50">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="flex items-center justify-between h-16">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-8 h-8 bg-gradient-to-r from-red-600 to-orange-600 rounded-lg flex items-center justify-center">
|
||||||
|
<span class="text-white font-bold text-sm">Z</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-xl font-bold bg-gradient-to-r from-red-600 to-orange-600 bg-clip-text text-transparent">
|
||||||
|
Zemailnator
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('dashboard') }}"
|
||||||
|
class="inline-flex items-center gap-2 px-4 py-2 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||||
|
</svg>
|
||||||
|
Back to Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="flex items-center justify-center min-h-[calc(100vh-4rem)] p-4">
|
||||||
|
<div class="w-full max-w-2xl">
|
||||||
|
<!-- Payment Status Card -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl overflow-hidden">
|
||||||
|
<!-- Cancellation Header -->
|
||||||
|
<div class="bg-gradient-to-r from-red-600 to-orange-600 p-8 text-center">
|
||||||
|
<div class="inline-flex items-center justify-center w-16 h-16 bg-white/20 rounded-full mb-4">
|
||||||
|
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl font-bold text-white mb-2">
|
||||||
|
Payment Cancelled
|
||||||
|
</h2>
|
||||||
|
<p class="text-red-100 text-lg">
|
||||||
|
Your payment session has been cancelled. No charges were made.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cancellation Details -->
|
||||||
|
<div class="p-8">
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<div class="inline-flex items-center justify-center w-20 h-20 bg-red-100 dark:bg-red-900/20 rounded-full mb-4">
|
||||||
|
<svg class="w-10 h-10 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 15.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-3">
|
||||||
|
No worries, your subscription wasn't created
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mb-6">
|
||||||
|
You can try subscribing again anytime. Your payment information is secure and no charges were processed.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Recent Subscription Info (if available) -->
|
||||||
|
@if ($recentSubscription)
|
||||||
|
<div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4 mb-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<svg class="w-5 h-5 text-yellow-600 dark:text-yellow-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
<p class="text-sm text-yellow-800 dark:text-yellow-200">
|
||||||
|
We found a recent subscription attempt that was cancelled. You can try again from the pricing page.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<a href="{{ route('dashboard') }}"
|
||||||
|
class="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 text-white font-medium rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"></path>
|
||||||
|
</svg>
|
||||||
|
Try Again
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ route('dashboard') }}"
|
||||||
|
class="inline-flex items-center gap-2 px-6 py-3 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 font-medium rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
|
||||||
|
</svg>
|
||||||
|
Go to Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Section -->
|
||||||
|
<div class="mt-6 text-center">
|
||||||
|
<div class="inline-flex items-center gap-2 px-4 py-2 bg-gray-100 dark:bg-gray-700 rounded-lg">
|
||||||
|
<svg class="w-4 h-4 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Session: {{ $sessionToken ? substr($sessionToken, 0, 20) . '...' : 'Not provided' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Security Assurance -->
|
||||||
|
<div class="mt-6 text-center">
|
||||||
|
<div class="inline-flex items-center gap-2 px-4 py-2 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||||
|
<svg class="w-4 h-4 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm text-green-800 dark:text-green-200 font-medium">
|
||||||
|
Your payment information is secure. No charges were made.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Custom Styles -->
|
||||||
|
<style>
|
||||||
|
@keyframes pulse-scale {
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.05); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse-scale {
|
||||||
|
animation: pulse-scale 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\PaymentCancelController;
|
||||||
use App\Http\Controllers\PaymentController;
|
use App\Http\Controllers\PaymentController;
|
||||||
use App\Http\Controllers\PaymentProviderController;
|
use App\Http\Controllers\PaymentProviderController;
|
||||||
use App\Http\Controllers\PaymentSuccessController;
|
use App\Http\Controllers\PaymentSuccessController;
|
||||||
@@ -16,7 +17,9 @@ Route::prefix('payment')->name('payment.')->group(function () {
|
|||||||
Route::get('/success', [PaymentSuccessController::class, 'show'])
|
Route::get('/success', [PaymentSuccessController::class, 'show'])
|
||||||
->middleware(['auth', 'verified'])
|
->middleware(['auth', 'verified'])
|
||||||
->name('success');
|
->name('success');
|
||||||
Route::get('/cancel', [PaymentController::class, 'cancel'])->name('cancel');
|
Route::get('/cancel', [PaymentCancelController::class, 'show'])
|
||||||
|
->middleware(['auth', 'verified'])
|
||||||
|
->name('cancel');
|
||||||
|
|
||||||
// UNIFIED: Payment processing endpoints (new unified payment system)
|
// UNIFIED: Payment processing endpoints (new unified payment system)
|
||||||
Route::post('/checkout', [PaymentController::class, 'createCheckout'])->name('checkout');
|
Route::post('/checkout', [PaymentController::class, 'createCheckout'])->name('checkout');
|
||||||
|
|||||||
Reference in New Issue
Block a user