Certified/resources/views/pages/settings/⚡billing.blade.php
2026-08-09 16:33:53 +02:00

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">
&euro;{{ 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">
&euro;{{ 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>&euro;{{ 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>