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:
idevakk
2025-12-06 02:01:17 -08:00
parent ebb041c0cc
commit 5ee5c5b8dc
5 changed files with 447 additions and 5 deletions

View 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');
}
}

View File

@@ -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) {