- Fix bulk delete and individual delete actions using before() hook with halt() - Add daily/weekly billing cycle options to plan resource and Polar provider - Enhance payment confirmation with dynamic polling and loading states - Add graceful handling for deleted plans in subscription display - Update Polar provider to support dynamic billing cycles
269 lines
15 KiB
PHP
269 lines
15 KiB
PHP
<section class="w-full">
|
|
@include('partials.settings-heading')
|
|
|
|
<x-settings.layout :heading="__('Billing & Subscription')" :subheading="__('Manage your subscriptions and payment methods')">
|
|
<!-- Success/Error Messages -->
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
Livewire.on('success', function(message) {
|
|
// Show success message
|
|
const successDiv = document.createElement('div');
|
|
successDiv.className = 'mb-6 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-md';
|
|
successDiv.innerHTML = '<div class="text-green-800 dark:text-green-200 text-sm">' + message + '</div>';
|
|
// Insert at the beginning of the content area
|
|
const contentArea = document.querySelector('.max-w-lg').parentElement;
|
|
contentArea.insertBefore(successDiv, contentArea.firstChild);
|
|
|
|
// Remove after 5 seconds
|
|
setTimeout(() => successDiv.remove(), 5000);
|
|
});
|
|
|
|
Livewire.on('error', function(message) {
|
|
// Show error message
|
|
const errorDiv = document.createElement('div');
|
|
errorDiv.className = 'mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md';
|
|
errorDiv.innerHTML = '<div class="text-red-800 dark:text-red-200 text-sm">' + message + '</div>';
|
|
// Insert at the beginning of the content area
|
|
const contentArea = document.querySelector('.max-w-lg').parentElement;
|
|
contentArea.insertBefore(errorDiv, contentArea.firstChild);
|
|
|
|
// Remove after 5 seconds
|
|
setTimeout(() => errorDiv.remove(), 5000);
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<!-- Current Subscription -->
|
|
<div class="space-y-6">
|
|
<flux:heading level="2">{{ __('Current Subscription') }}</flux:heading>
|
|
|
|
@php
|
|
$latestActiveSubscription = $subscriptions->first(function($sub) {
|
|
return $sub->isActive();
|
|
});
|
|
|
|
$latestSubscription = $subscriptions->first();
|
|
$isWithin30Minutes = $latestSubscription &&
|
|
$latestSubscription->created_at->diffInMinutes(now()) <= 30;
|
|
@endphp
|
|
|
|
@if($latestActiveSubscription)
|
|
<div class="border rounded-lg p-6 dark:border-gray-700">
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-3">
|
|
<flux:heading level="3">{{ $latestActiveSubscription->getPlanDisplayName() }}</flux:heading>
|
|
<flux:badge variant="success">
|
|
{{ ucfirst($latestActiveSubscription->status) }}
|
|
</flux:badge>
|
|
<flux:badge variant="neutral">
|
|
{{ $latestActiveSubscription->getProviderDisplayName() }}
|
|
</flux:badge>
|
|
</div>
|
|
|
|
<flux:text class="mt-2 text-gray-600 dark:text-gray-400">
|
|
{{ $latestActiveSubscription->plan?->description ?? 'Subscription plan' }}
|
|
</flux:text>
|
|
|
|
@if($latestActiveSubscription->isActive())
|
|
<flux:text class="mt-1 text-sm text-gray-500 dark:text-gray-500">
|
|
@if($latestActiveSubscription->ends_at)
|
|
@if($latestActiveSubscription->cancelled_at)
|
|
{{ __('Expires on :date', ['date' => $latestActiveSubscription->ends_at->format('M j, Y')]) }}
|
|
@else
|
|
{{ __('Renews on :date', ['date' => $latestActiveSubscription->ends_at->format('M j, Y')]) }}
|
|
@endif
|
|
@else
|
|
{{ __('Active subscription') }}
|
|
@endif
|
|
</flux:text>
|
|
@endif
|
|
|
|
@if($latestActiveSubscription->provider === 'activation_key' && $latestActiveSubscription->status === 'pending_activation')
|
|
<div class="mt-3 p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-md">
|
|
<flux:text class="text-sm text-yellow-800 dark:text-yellow-200">
|
|
{{ __('Activation Key:') }}
|
|
<code class="bg-yellow-100 dark:bg-yellow-800 px-2 py-1 rounded text-xs">
|
|
{{ $latestActiveSubscription->getProviderData('activation_key') }}
|
|
</code>
|
|
</flux:text>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="mt-4 flex flex-col sm:flex-row sm:items-center sm:gap-2 space-y-2 sm:space-y-0">
|
|
@if($latestActiveSubscription->provider === 'polar' && $latestActiveSubscription->isActive())
|
|
<flux:button
|
|
wire:click="managePolarSubscription"
|
|
variant="outline"
|
|
size="sm"
|
|
class="w-full sm:w-auto cursor-pointer sm:mr-2"
|
|
>
|
|
{{ __('Manage in Polar') }}
|
|
</flux:button>
|
|
@endif
|
|
|
|
@if(in_array($latestActiveSubscription->provider, ['polar']) && $latestActiveSubscription->isActive())
|
|
<flux:button
|
|
wire:click="syncSubscription({{ $latestActiveSubscription->id }})"
|
|
variant="outline"
|
|
size="sm"
|
|
wire:loading.attr="disabled"
|
|
class="w-full sm:w-auto cursor-pointer"
|
|
>
|
|
<span wire:loading.remove>{{ __('Sync') }}</span>
|
|
<span wire:loading>{{ __('Syncing...') }}</span>
|
|
</flux:button>
|
|
@endif
|
|
|
|
@if($latestActiveSubscription->isActive() && in_array($latestActiveSubscription->provider, ['activation_key']))
|
|
<flux:button
|
|
wire:click="confirmCancelSubscription({{ $latestActiveSubscription->id }})"
|
|
variant="danger"
|
|
size="sm"
|
|
class="w-full sm:w-auto cursor-pointer"
|
|
>
|
|
{{ __('Cancel') }}
|
|
</flux:button>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@else
|
|
<div class="text-center py-12 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg">
|
|
<flux:text class="text-gray-500 dark:text-gray-400 mb-6">
|
|
{{ __('You have no active subscriptions.') }}
|
|
</flux:text>
|
|
<flux:button variant="primary" href="{{ route('dashboard') }}" class="cursor-pointer">
|
|
{{ __('Choose a plan') }}
|
|
</flux:button>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Transaction History -->
|
|
<div class="mt-12 space-y-6">
|
|
<flux:heading level="2">{{ __('Transaction History') }}</flux:heading>
|
|
|
|
@if($subscriptions->count() > 0)
|
|
<div class="space-y-4">
|
|
@foreach($subscriptions as $subscription)
|
|
<div class="border rounded-lg p-4 dark:border-gray-700">
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-3">
|
|
<flux:heading level="4">{{ $subscription->getPlanDisplayName() }}</flux:heading>
|
|
<flux:badge
|
|
variant="{{ $subscription->isActive() ? 'success' : ($subscription->isCancelled() ? 'danger' : 'warning') }}"
|
|
>
|
|
{{ ucfirst($subscription->status) }}
|
|
</flux:badge>
|
|
<flux:badge variant="neutral">
|
|
{{ $subscription->getProviderDisplayName() }}
|
|
</flux:badge>
|
|
</div>
|
|
|
|
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
<div>{{ __('Created:') }} {{ $subscription->created_at->diffForHumans() }}</div>
|
|
@if($subscription->starts_at)
|
|
<div>{{ __('Started:') }} {{ $subscription->starts_at->diffForHumans() }}</div>
|
|
@endif
|
|
@if($subscription->ends_at)
|
|
<div>{{ __('Ends:') }} {{ $subscription->ends_at->diffForHumans() }}</div>
|
|
@endif
|
|
@if($subscription->cancelled_at)
|
|
<div>{{ __('Cancelled:') }} {{ $subscription->cancelled_at->diffForHumans() }}</div>
|
|
@endif
|
|
</div>
|
|
|
|
@if($subscription->provider === 'activation_key' && $subscription->status === 'pending_activation')
|
|
<div class="mt-2 p-2 bg-yellow-50 dark:bg-yellow-900/20 rounded-md">
|
|
<flux:text class="text-xs text-yellow-800 dark:text-yellow-200">
|
|
{{ __('Activation Key:') }}
|
|
<code class="bg-yellow-100 dark:bg-yellow-800 px-1 py-0.5 rounded text-xs">
|
|
{{ $subscription->getProviderData('activation_key') }}
|
|
</code>
|
|
</flux:text>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="text-right">
|
|
<flux:text class="text-lg font-semibold">
|
|
${{ number_format($subscription->getPlanPrice(), 2) }}
|
|
</flux:text>
|
|
|
|
@if($isWithin30Minutes && $subscription->id === $latestSubscription->id)
|
|
<div class="mt-2">
|
|
<flux:button
|
|
wire:click="syncSubscription({{ $subscription->id }})"
|
|
variant="primary"
|
|
size="sm"
|
|
wire:loading.attr="disabled"
|
|
class="cursor-pointer"
|
|
>
|
|
<span wire:loading.remove>{{ __('Recheck Status') }}</span>
|
|
<span wire:loading>{{ __('Checking...') }}</span>
|
|
</flux:button>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<div class="text-center py-8 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg">
|
|
<flux:text class="text-gray-500 dark:text-gray-400">
|
|
{{ __('No subscription history found.') }}
|
|
</flux:text>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Cancel Subscription Modal -->
|
|
<flux:modal name="cancel-subscription" wire:model="showCancelModal">
|
|
<div class="space-y-4">
|
|
<flux:heading level="3">{{ __('Cancel Subscription') }}</flux:heading>
|
|
|
|
<flux:text>
|
|
{{ __('Are you sure you want to cancel your subscription? This action cannot be undone.') }}
|
|
</flux:text>
|
|
|
|
@if($subscriptionToCancel)
|
|
<div class="p-4 bg-gray-50 dark:bg-gray-800 rounded-md">
|
|
<flux:text class="font-medium">{{ $subscriptionToCancel->getPlanDisplayName() }}</flux:text>
|
|
<flux:text class="text-sm text-gray-600 dark:text-gray-400">
|
|
{{ $subscriptionToCancel->getProviderDisplayName() }}
|
|
</flux:text>
|
|
</div>
|
|
@endif
|
|
|
|
<flux:input
|
|
wire:model="cancellationReason"
|
|
label="{{ __('Reason (optional)') }}"
|
|
placeholder="{{ __('Why are you cancelling?') }}"
|
|
/>
|
|
|
|
<div class="flex gap-3">
|
|
<flux:button
|
|
variant="outline"
|
|
wire:click="$set('showCancelModal', false)"
|
|
class="flex-1 cursor-pointer"
|
|
>
|
|
{{ __('Nevermind') }}
|
|
</flux:button>
|
|
<flux:button
|
|
variant="danger"
|
|
wire:click="cancelSubscription"
|
|
wire:loading.attr="disabled"
|
|
class="flex-1 cursor-pointer"
|
|
>
|
|
<span wire:loading.remove>{{ __('Cancel Subscription') }}</span>
|
|
<span wire:loading>{{ __('Cancelling...') }}</span>
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
</flux:modal>
|
|
</x-settings.layout>
|
|
</section> |