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 @@
{{ __('Profile') }} + {{ __('Billing') }} {{ __('Password') }} {{ __('Appearance') }} diff --git a/resources/views/livewire/settings/billing.blade.php b/resources/views/livewire/settings/billing.blade.php new file mode 100644 index 0000000..da5ad12 --- /dev/null +++ b/resources/views/livewire/settings/billing.blade.php @@ -0,0 +1,265 @@ +
+ @include('partials.settings-heading') + + + + + + +
+ {{ __('Current Subscription') }} + + @php + $latestActiveSubscription = $subscriptions->first(function($sub) { + return $sub->isActive(); + }); + + $latestSubscription = $subscriptions->first(); + $isWithin30Minutes = $latestSubscription && + $latestSubscription->created_at->diffInMinutes(now()) <= 30; + @endphp + + @if($latestActiveSubscription) +
+
+
+
+ {{ $latestActiveSubscription->plan->name }} + + {{ ucfirst($latestActiveSubscription->status) }} + + + {{ $latestActiveSubscription->getProviderDisplayName() }} + +
+ + + {{ $latestActiveSubscription->plan->description ?? 'Subscription plan' }} + + + @if($latestActiveSubscription->isActive()) + + @if($latestActiveSubscription->ends_at) + {{ __('Renews on :date', ['date' => $latestActiveSubscription->ends_at->format('M j, Y')]) }} + @else + {{ __('Active subscription') }} + @endif + + @endif + + @if($latestActiveSubscription->provider === 'activation_key' && $latestActiveSubscription->status === 'pending_activation') +
+ + {{ __('Activation Key:') }} + + {{ $latestActiveSubscription->getProviderData('activation_key') }} + + +
+ @endif +
+ +
+ @if($latestActiveSubscription->provider === 'polar' && $latestActiveSubscription->isActive()) + + {{ __('Manage in Polar') }} + + @endif + + @if(in_array($latestActiveSubscription->provider, ['polar']) && $latestActiveSubscription->isActive()) + + {{ __('Sync') }} + {{ __('Syncing...') }} + + @endif + + @if($latestActiveSubscription->isActive() && $latestActiveSubscription->provider !== 'activation_key') + + {{ __('Cancel') }} + + @endif +
+
+
+ @else +
+ + {{ __('You have no active subscriptions.') }} + + + {{ __('Choose a plan') }} + +
+ @endif +
+ + +
+ {{ __('Transaction History') }} + + @if($subscriptions->count() > 0) +
+ @foreach($subscriptions as $subscription) +
+
+
+
+ {{ $subscription->plan->name }} + + {{ ucfirst($subscription->status) }} + + + {{ $subscription->getProviderDisplayName() }} + +
+ +
+
{{ __('Created:') }} {{ $subscription->created_at->diffForHumans() }}
+ @if($subscription->starts_at) +
{{ __('Started:') }} {{ $subscription->starts_at->diffForHumans() }}
+ @endif + @if($subscription->ends_at) +
{{ __('Ends:') }} {{ $subscription->ends_at->diffForHumans() }}
+ @endif + @if($subscription->cancelled_at) +
{{ __('Cancelled:') }} {{ $subscription->cancelled_at->diffForHumans() }}
+ @endif +
+ + @if($subscription->provider === 'activation_key' && $subscription->status === 'pending_activation') +
+ + {{ __('Activation Key:') }} + + {{ $subscription->getProviderData('activation_key') }} + + +
+ @endif +
+ +
+ + ${{ number_format($subscription->plan->price, 2) }} + + + @if($isWithin30Minutes && $subscription->id === $latestSubscription->id) +
+ + {{ __('Recheck Status') }} + {{ __('Checking...') }} + +
+ @endif +
+
+
+ @endforeach +
+ @else +
+ + {{ __('No subscription history found.') }} + +
+ @endif +
+ + + +
+ {{ __('Cancel Subscription') }} + + + {{ __('Are you sure you want to cancel your subscription? This action cannot be undone.') }} + + + @if($subscriptionToCancel) +
+ {{ $subscriptionToCancel->plan->name }} + + {{ $subscriptionToCancel->getProviderDisplayName() }} + +
+ @endif + + + +
+ + {{ __('Nevermind') }} + + + {{ __('Cancel Subscription') }} + {{ __('Cancelling...') }} + +
+
+
+
+
\ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 06e6c70..9752c92 100644 --- a/routes/web.php +++ b/routes/web.php @@ -39,6 +39,7 @@ use App\Livewire\Home; use App\Livewire\ListBlog; use App\Livewire\Page; use App\Livewire\Settings\Appearance; +use App\Livewire\Settings\Billing; use App\Livewire\Settings\Password; use App\Livewire\Settings\Profile; use App\Models\Email; @@ -174,6 +175,7 @@ Route::middleware(['auth'])->group(function (): void { Route::redirect('settings', 'settings/profile'); Route::get('settings/profile', Profile::class)->name('settings.profile'); + Route::get('settings/billing', Billing::class)->name('settings.billing'); Route::get('settings/password', Password::class)->name('settings.password'); Route::get('settings/appearance', Appearance::class)->name('settings.appearance'); });