feat(billing): implement Polar customer portal integration
- Add comprehensive billing page with current subscription display and transaction history - Integrate Polar.sh customer portal for subscription management - Fix Polar API endpoint from /customer-portal to /customer-sessions - Use Polar's direct customer_portal_url response for seamless redirect - Add responsive button layout with cursor-pointer styling - Implement human-readable timestamps using diffForHumans() - Add subscription sync functionality with 30-minute recheck window - Include subscription cancellation with modal confirmation - Support activation key provider with pending activation display - Add proper error handling and user feedback messages
This commit is contained in:
156
app/Livewire/Settings/Billing.php
Normal file
156
app/Livewire/Settings/Billing.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Settings;
|
||||
|
||||
use App\Models\Subscription;
|
||||
use App\Services\Payments\PaymentOrchestrator;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Layout('components.layouts.dashboard')]
|
||||
class Billing extends Component
|
||||
{
|
||||
public $subscriptions;
|
||||
|
||||
public $showCancelModal = false;
|
||||
|
||||
public $subscriptionToCancel = null;
|
||||
|
||||
public $cancellationReason = '';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->loadBillingData();
|
||||
}
|
||||
|
||||
public function loadBillingData()
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
// Get user's subscriptions
|
||||
$this->subscriptions = $user->subscriptions()
|
||||
->with('plan')
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function managePolarSubscription()
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
try {
|
||||
// Check if user has polar_cust_id
|
||||
if (! $user->polar_cust_id) {
|
||||
$this->dispatch('error', 'No Polar customer account found. Please create a subscription first.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$paymentOrchestrator = app(PaymentOrchestrator::class);
|
||||
$activeProviders = $paymentOrchestrator->getActiveProviders();
|
||||
$polarProvider = $activeProviders->first(function ($provider) {
|
||||
return $provider->getName() === 'polar';
|
||||
});
|
||||
|
||||
if (! $polarProvider || ! $polarProvider->isActive()) {
|
||||
$this->dispatch('error', 'Polar payment provider is not available.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Create customer portal session using user's polar_cust_id
|
||||
$portalSession = $polarProvider->createCustomerPortalSession($user);
|
||||
|
||||
if (isset($portalSession['portal_url'])) {
|
||||
return redirect()->away($portalSession['portal_url']);
|
||||
}
|
||||
|
||||
$this->dispatch('error', 'Unable to access Polar billing portal.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to create Polar portal session', [
|
||||
'user_id' => $user->id,
|
||||
'polar_cust_id' => $user->polar_cust_id ?? 'none',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
$this->dispatch('error', 'Failed to access Polar billing portal. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
public function confirmCancelSubscription(Subscription $subscription)
|
||||
{
|
||||
// Verify ownership
|
||||
if ($subscription->user_id !== Auth::id()) {
|
||||
abort(403, 'Unauthorized');
|
||||
}
|
||||
|
||||
$this->subscriptionToCancel = $subscription;
|
||||
$this->showCancelModal = true;
|
||||
}
|
||||
|
||||
public function cancelSubscription()
|
||||
{
|
||||
if (! $this->subscriptionToCancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$reason = $this->cancellationReason ?: 'User requested cancellation via billing portal';
|
||||
$success = $this->subscriptionToCancel->cancel($reason);
|
||||
|
||||
if ($success) {
|
||||
$this->dispatch('success', 'Subscription cancelled successfully.');
|
||||
$this->loadBillingData(); // Refresh data
|
||||
} else {
|
||||
$this->dispatch('error', 'Failed to cancel subscription. Please try again.');
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to cancel subscription', [
|
||||
'subscription_id' => $this->subscriptionToCancel->id,
|
||||
'user_id' => Auth::id(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
$this->dispatch('error', 'Failed to cancel subscription. Please try again.');
|
||||
}
|
||||
|
||||
$this->reset(['showCancelModal', 'subscriptionToCancel', 'cancellationReason']);
|
||||
}
|
||||
|
||||
public function syncSubscription(Subscription $subscription)
|
||||
{
|
||||
// Verify ownership
|
||||
if ($subscription->user_id !== Auth::id()) {
|
||||
abort(403, 'Unauthorized');
|
||||
}
|
||||
|
||||
try {
|
||||
$success = $subscription->syncWithProvider();
|
||||
|
||||
if ($success) {
|
||||
$this->dispatch('success', 'Subscription synced successfully.');
|
||||
$this->loadBillingData(); // Refresh data
|
||||
} else {
|
||||
$this->dispatch('error', 'Failed to sync subscription. Please try again.');
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to sync subscription', [
|
||||
'subscription_id' => $subscription->id,
|
||||
'user_id' => Auth::id(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
$this->dispatch('error', 'Failed to sync subscription. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.settings.billing');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user