From 5ee5c5b8dc64b6358b59332844f3d649bd522ff8 Mon Sep 17 00:00:00 2001 From: idevakk <219866223+idevakk@users.noreply.github.com> Date: Sat, 6 Dec 2025 02:01:17 -0800 Subject: [PATCH] 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 --- app/Livewire/Settings/Billing.php | 156 +++++++++++ .../Payments/Providers/PolarProvider.php | 28 +- .../components/settings/layout.blade.php | 1 + .../views/livewire/settings/billing.blade.php | 265 ++++++++++++++++++ routes/web.php | 2 + 5 files changed, 447 insertions(+), 5 deletions(-) create mode 100644 app/Livewire/Settings/Billing.php create mode 100644 resources/views/livewire/settings/billing.blade.php diff --git a/app/Livewire/Settings/Billing.php b/app/Livewire/Settings/Billing.php new file mode 100644 index 0000000..b19d5f6 --- /dev/null +++ b/app/Livewire/Settings/Billing.php @@ -0,0 +1,156 @@ +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'); + } +} diff --git a/app/Services/Payments/Providers/PolarProvider.php b/app/Services/Payments/Providers/PolarProvider.php index f0a4e9d..116fd92 100644 --- a/app/Services/Payments/Providers/PolarProvider.php +++ b/app/Services/Payments/Providers/PolarProvider.php @@ -257,7 +257,7 @@ class PolarProvider implements PaymentProviderContract $subscription = Subscription::create([ 'user_id' => $user->id, 'plan_id' => $plan->id, - 'type' => 'recurring', + 'type' => 'default', 'stripe_id' => $checkout['id'], // Using stripe_id field for Polar checkout ID 'stripe_status' => 'pending', 'provider' => $this->getName(), @@ -529,20 +529,38 @@ class PolarProvider implements PaymentProviderContract try { $customer = $this->getOrCreateCustomer($user); - $response = $this->makeAuthenticatedRequest('POST', '/customer-portal', [ + // Create customer session using correct Polar API endpoint + $response = $this->makeAuthenticatedRequest('POST', '/customer-sessions', [ 'customer_id' => $customer['id'], 'return_url' => route('dashboard'), ]); if (! $response->successful()) { - Log::error('Polar customer portal creation failed: '.$response->body()); + Log::error('Polar customer session creation failed: '.$response->body()); + throw new \Exception('Failed to create customer session'); } - $portal = $response->json(); + $session = $response->json(); + + // Polar provides a direct customer_portal_url in the response + if (! isset($session['customer_portal_url'])) { + Log::error('Invalid Polar customer session response', [ + 'response' => $session, + ]); + throw new \Exception('Invalid customer session response - missing portal URL'); + } + + Log::info('Polar customer portal session created successfully', [ + 'user_id' => $user->id, + 'customer_id' => $customer['id'], + 'portal_url' => $session['customer_portal_url'], + ]); return [ - 'portal_url' => $portal['url'], + 'portal_url' => $session['customer_portal_url'], 'customer_id' => $customer['id'], + 'session_token' => $session['token'] ?? null, + 'expires_at' => $session['expires_at'] ?? null, ]; } catch (\Exception $e) { diff --git a/resources/views/components/settings/layout.blade.php b/resources/views/components/settings/layout.blade.php index 05c0637..51ce7a2 100644 --- a/resources/views/components/settings/layout.blade.php +++ b/resources/views/components/settings/layout.blade.php @@ -2,6 +2,7 @@
+ {{ $latestActiveSubscription->getProviderData('activation_key') }}
+
+
+ {{ $subscription->getProviderData('activation_key') }}
+
+