206 lines
9.8 KiB
PHP
206 lines
9.8 KiB
PHP
<?php
|
|
|
|
use App\Enums\SubscriptionPlan;
|
|
use App\Enums\SubscriptionStatus;
|
|
use App\Services\Billing\SubscriptionManager;
|
|
use Flux\Flux;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Validation\Rule;
|
|
use Livewire\Attributes\Computed;
|
|
use Livewire\Attributes\Title;
|
|
use Livewire\Component;
|
|
|
|
new #[Title('Billing')] class extends Component
|
|
{
|
|
public function choosePlan(string $plan): void
|
|
{
|
|
$validated = validator(['plan' => $plan], [
|
|
'plan' => ['required', Rule::enum(SubscriptionPlan::class)],
|
|
])->validate();
|
|
|
|
$subscription = app(SubscriptionManager::class)->changePlan(
|
|
Auth::user(),
|
|
SubscriptionPlan::from($validated['plan']),
|
|
);
|
|
|
|
unset($this->subscription, $this->invoices);
|
|
|
|
$unpaidInvoice = $subscription->invoices()
|
|
->whereNotIn('state', ['paid', 'uncollectible'])
|
|
->latest('id')
|
|
->first();
|
|
|
|
if ($unpaidInvoice?->payment_url) {
|
|
Flux::toast(variant: 'success', text: __('Plan updated — complete the payment to keep your new limit.'));
|
|
$this->redirect($unpaidInvoice->payment_url, navigate: false);
|
|
|
|
return;
|
|
}
|
|
|
|
Flux::toast(variant: 'success', text: __('Plan updated.'));
|
|
}
|
|
|
|
public function cancel(): void
|
|
{
|
|
app(SubscriptionManager::class)->cancel(Auth::user());
|
|
|
|
unset($this->subscription);
|
|
|
|
Flux::toast(variant: 'success', text: __("Your subscription won't renew. You'll keep access until the end of the current period."));
|
|
}
|
|
|
|
public function resume(): void
|
|
{
|
|
app(SubscriptionManager::class)->resume(Auth::user());
|
|
|
|
unset($this->subscription);
|
|
|
|
Flux::toast(variant: 'success', text: __('Cancellation undone — your subscription will renew again.'));
|
|
}
|
|
|
|
#[Computed]
|
|
public function subscription()
|
|
{
|
|
return Auth::user()->subscription ?? Auth::user()->subscription()->create();
|
|
}
|
|
|
|
#[Computed]
|
|
public function usageCount(): int
|
|
{
|
|
return Auth::user()->certificates()->count();
|
|
}
|
|
|
|
#[Computed]
|
|
public function invoices()
|
|
{
|
|
return $this->subscription->invoices()->latest('id')->get();
|
|
}
|
|
|
|
/**
|
|
* @return array<int, SubscriptionPlan>
|
|
*/
|
|
#[Computed]
|
|
public function plans(): array
|
|
{
|
|
return SubscriptionPlan::cases();
|
|
}
|
|
}; ?>
|
|
|
|
<section class="w-full">
|
|
@include('partials.settings-heading')
|
|
|
|
<x-pages::settings.layout :heading="__('Billing')" :subheading="__('Manage your Certified subscription and invoices')">
|
|
<div class="my-6 w-full max-w-none space-y-8">
|
|
{{-- Current status --}}
|
|
<div class="rounded-2xl border border-slate-200 p-6 dark:border-slate-800">
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
<div>
|
|
<div class="flex items-center gap-2">
|
|
<flux:heading size="lg">{{ $this->subscription->plan->label() }}</flux:heading>
|
|
<flux:badge size="sm" :color="$this->subscription->status->color()">{{ $this->subscription->status->label() }}</flux:badge>
|
|
</div>
|
|
@php $limit = $this->subscription->plan->certificateLimit(); @endphp
|
|
@if ($limit !== null)
|
|
<flux:text class="mt-1">
|
|
{{ __(':used of :limit certificates used', ['used' => $this->usageCount, 'limit' => $limit]) }}
|
|
</flux:text>
|
|
@endif
|
|
</div>
|
|
|
|
@if ($this->subscription->plan->isPaid() && $this->subscription->current_period_ends_at)
|
|
<div class="text-right">
|
|
@if ($this->subscription->cancel_at_period_end)
|
|
<flux:text class="text-amber-600 dark:text-amber-400">
|
|
{{ __('Ends on :date', ['date' => $this->subscription->current_period_ends_at->toFormattedDateString()]) }}
|
|
</flux:text>
|
|
<flux:button size="sm" variant="ghost" wire:click="resume">{{ __('Undo cancellation') }}</flux:button>
|
|
@else
|
|
<flux:text>{{ __('Renews on :date', ['date' => $this->subscription->current_period_ends_at->toFormattedDateString()]) }}</flux:text>
|
|
<flux:button size="sm" variant="ghost" wire:click="cancel" wire:confirm="{{ __('Are you sure you want to cancel? Your subscription stays active until the end of the current period.') }}">
|
|
{{ __('Cancel subscription') }}
|
|
</flux:button>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
@if ($this->subscription->status === SubscriptionStatus::PastDue && $this->subscription->grace_ends_at)
|
|
<flux:callout icon="exclamation-triangle" variant="danger" class="mt-4">
|
|
{{ __("We haven't received payment yet. Without payment before :date we'll automatically switch you back to the Free plan.", ['date' => $this->subscription->grace_ends_at->toFormattedDateString()]) }}
|
|
</flux:callout>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- Plan switcher --}}
|
|
<div>
|
|
<flux:heading size="lg">{{ __('Change plan') }}</flux:heading>
|
|
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-3">
|
|
@foreach ($this->plans as $plan)
|
|
@php $isCurrent = $this->subscription->plan === $plan; @endphp
|
|
<div @class([
|
|
'flex flex-col gap-2 rounded-2xl border p-5',
|
|
'border-emerald-500 ring-1 ring-emerald-500' => $isCurrent,
|
|
'border-slate-200 dark:border-slate-800' => ! $isCurrent,
|
|
])>
|
|
<flux:heading>{{ $plan->label() }}</flux:heading>
|
|
<flux:text>
|
|
@php $planLimit = $plan->certificateLimit(); @endphp
|
|
{{ $planLimit === null ? __('Unlimited certificates') : __('Up to :count certificates', ['count' => $planLimit]) }}
|
|
</flux:text>
|
|
<flux:text class="text-2xl font-bold text-slate-900 dark:text-white">
|
|
€{{ number_format($plan->priceInclVatCents() / 100, 2, ',', '.') }}<span class="text-sm font-normal text-slate-500">/{{ __('year') }}</span>
|
|
</flux:text>
|
|
@if ($plan->isPaid())
|
|
<flux:text class="text-xs text-slate-400">
|
|
€{{ number_format($plan->priceInCents() / 100, 2, ',', '.') }} {{ __('excl. VAT') }}
|
|
</flux:text>
|
|
@endif
|
|
@if ($isCurrent)
|
|
<flux:button disabled class="mt-2">{{ __('Current plan') }}</flux:button>
|
|
@else
|
|
<flux:button
|
|
class="mt-2"
|
|
variant="{{ $plan->priceInCents() > $this->subscription->plan->priceInCents() ? 'primary' : 'ghost' }}"
|
|
wire:click="choosePlan('{{ $plan->value }}')"
|
|
>
|
|
{{ $plan->priceInCents() > $this->subscription->plan->priceInCents() ? __('Upgrade') : __('Switch') }}
|
|
</flux:button>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Invoice history --}}
|
|
@if ($this->invoices->isNotEmpty())
|
|
<div>
|
|
<flux:heading size="lg">{{ __('Invoices') }}</flux:heading>
|
|
<flux:table class="mt-4">
|
|
<flux:table.columns>
|
|
<flux:table.column>{{ __('Date') }}</flux:table.column>
|
|
<flux:table.column>{{ __('Plan') }}</flux:table.column>
|
|
<flux:table.column>{{ __('Amount') }}</flux:table.column>
|
|
<flux:table.column>{{ __('Status') }}</flux:table.column>
|
|
<flux:table.column></flux:table.column>
|
|
</flux:table.columns>
|
|
<flux:table.rows>
|
|
@foreach ($this->invoices as $invoice)
|
|
<flux:table.row>
|
|
<flux:table.cell>{{ $invoice->invoice_date?->toFormattedDateString() }}</flux:table.cell>
|
|
<flux:table.cell>{{ $invoice->plan->label() }}</flux:table.cell>
|
|
<flux:table.cell>€{{ number_format($invoice->amount_cents / 100, 2, ',', '.') }}</flux:table.cell>
|
|
<flux:table.cell>{{ $invoice->state }}</flux:table.cell>
|
|
<flux:table.cell>
|
|
@if ($invoice->state !== 'paid' && $invoice->payment_url)
|
|
<flux:link :href="$invoice->payment_url" target="_blank">{{ __('Pay') }}</flux:link>
|
|
@endif
|
|
</flux:table.cell>
|
|
</flux:table.row>
|
|
@endforeach
|
|
</flux:table.rows>
|
|
</flux:table>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</x-pages::settings.layout>
|
|
</section>
|