Skip to content

Commit 2f99e6a

Browse files
simonhampclaude
andcommitted
Improve plugin edit UX: tier validation, de-listed indicator, submit summary
Add server-side validation for pricing tier when saving paid draft plugins with inline error and red card border. Show gray dot for de-listed plugins and remove Status column from plugin index. Move GitHub banner into Details tab. Add plugin summary card to Submit for Review tab. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0cdb170 commit 2f99e6a

4 files changed

Lines changed: 114 additions & 31 deletions

File tree

app/Livewire/Customer/Plugins/Show.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,16 @@ function (string $attribute, mixed $value, \Closure $fail) {
170170

171171
if ($this->plugin->isDraft()) {
172172
$rules['notes'] = ['nullable', 'string', 'max:5000'];
173+
$rules['pluginType'] = ['required', 'string', 'in:free,paid'];
174+
175+
if ($this->pluginType === 'paid') {
176+
$rules['tier'] = ['required', 'string', 'in:bronze,silver,gold'];
177+
}
173178
}
174179

175-
$this->validate($rules);
180+
$this->validate($rules, [
181+
'tier.required' => 'Please select a pricing tier for your paid plugin.',
182+
]);
176183

177184
$data = [
178185
'display_name' => $this->displayName ?: null,

resources/views/livewire/customer/plugins/index.blade.php

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@
7777
<flux:table class="mt-4">
7878
<flux:table.columns>
7979
<flux:table.column>Plugin</flux:table.column>
80-
<flux:table.column>Status</flux:table.column>
8180
<flux:table.column></flux:table.column>
8281
</flux:table.columns>
8382

@@ -91,8 +90,10 @@
9190
<div class="size-3 rounded-full bg-gray-400"></div>
9291
@elseif ($plugin->isPending())
9392
<div class="size-3 animate-pulse rounded-full bg-yellow-400"></div>
94-
@elseif ($plugin->isApproved())
93+
@elseif ($plugin->isApproved() && $plugin->is_active)
9594
<div class="size-3 rounded-full bg-green-400"></div>
95+
@elseif ($plugin->isApproved() && ! $plugin->is_active)
96+
<div class="size-3 rounded-full bg-gray-400"></div>
9697
@else
9798
<div class="size-3 rounded-full bg-red-400"></div>
9899
@endif
@@ -109,10 +110,6 @@
109110
</div>
110111
</flux:table.cell>
111112

112-
<flux:table.cell>
113-
<x-customer.status-badge :status="$plugin->status->label()" />
114-
</flux:table.cell>
115-
116113
<flux:table.cell class="text-right">
117114
<flux:button size="xs" variant="ghost" href="{{ route('customer.plugins.show', $plugin->routeParams()) }}">
118115
@if ($plugin->isDraft())

resources/views/livewire/customer/plugins/show.blade.php

Lines changed: 98 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -78,21 +78,6 @@
7878
</flux:card>
7979
@endif
8080

81-
{{-- Plugin Status (hidden for Pending/Rejected — details card covers this) --}}
82-
@if ($plugin->isDraft() || $plugin->isApproved())
83-
<a href="{{ $plugin->repository_url }}" target="_blank" rel="noopener noreferrer" class="block">
84-
<flux:card class="mb-6 transition hover:bg-gray-50 dark:hover:bg-gray-700/50">
85-
<div class="flex items-center justify-between">
86-
<div class="flex items-center gap-3">
87-
<x-icons.github class="size-5 text-gray-400 dark:text-gray-500" />
88-
<span class="font-mono text-sm font-medium text-gray-900 dark:text-white">{{ $plugin->name }}</span>
89-
</div>
90-
<x-heroicon-o-arrow-top-right-on-square class="size-4 text-gray-400 dark:text-gray-500" />
91-
</div>
92-
</flux:card>
93-
</a>
94-
@endif
95-
9681
{{-- Review Checks (show for Pending, Rejected, Approved — not Draft) --}}
9782
@if (! $plugin->isDraft() && $plugin->review_checks)
9883
<flux:card class="mb-6">
@@ -202,6 +187,19 @@
202187
</flux:tabs>
203188

204189
<flux:tab.panel name="details">
190+
{{-- GitHub Repo --}}
191+
<a href="{{ $plugin->repository_url }}" target="_blank" rel="noopener noreferrer" class="block">
192+
<flux:card class="mb-6 transition hover:bg-gray-50 dark:hover:bg-gray-700/50">
193+
<div class="flex items-center justify-between">
194+
<div class="flex items-center gap-3">
195+
<x-icons.github class="size-5 text-gray-400 dark:text-gray-500" />
196+
<span class="font-mono text-sm font-medium text-gray-900 dark:text-white">{{ $plugin->name }}</span>
197+
</div>
198+
<x-heroicon-o-arrow-top-right-on-square class="size-4 text-gray-400 dark:text-gray-500" />
199+
</div>
200+
</flux:card>
201+
</a>
202+
205203
<form wire:submit="save" class="space-y-6">
206204
{{-- Plugin Type --}}
207205
@feature(App\Features\AllowPaidPlugins::class)
@@ -232,7 +230,7 @@
232230

233231
{{-- Pricing Tier (only when paid) --}}
234232
@if ($pluginType === 'paid')
235-
<flux:card>
233+
<flux:card :class="$errors->has('tier') ? '!border-red-500 dark:!border-red-400' : ''">
236234
<flux:heading size="lg">Pricing Tier</flux:heading>
237235
<flux:text class="mt-1">Choose a pricing tier for your plugin.</flux:text>
238236

@@ -254,6 +252,10 @@
254252
@endforeach
255253
</div>
256254

255+
@error('tier')
256+
<flux:text class="mt-4 text-sm text-red-600 dark:text-red-400">{{ $message }}</flux:text>
257+
@enderror
258+
257259
<flux:text class="mt-4 text-xs text-gray-500 dark:text-gray-400">
258260
Actual sale price may vary due to discounts and offers. You keep 70% of the sale price. If a NativePHP Ultra subscriber purchases your plugin, you receive 100% of the sale price. Additional payment processing fees may apply.
259261
</flux:text>
@@ -410,6 +412,73 @@ class="block text-sm text-gray-500 file:mr-4 file:rounded-md file:border-0 file:
410412

411413
<flux:tab.panel name="submit">
412414
<div class="space-y-6">
415+
{{-- Plugin Summary --}}
416+
<flux:card>
417+
<div class="flex items-start justify-between">
418+
<div class="flex items-start gap-4">
419+
@if ($plugin->hasLogo())
420+
<img src="{{ $plugin->getLogoUrl() }}" alt="{{ $plugin->name }} logo" class="size-16 shrink-0 rounded-lg object-cover shadow-sm" />
421+
@elseif ($plugin->hasGradientIcon())
422+
<div class="grid size-16 shrink-0 place-items-center rounded-lg bg-gradient-to-br {{ $plugin->getGradientClasses() }} text-white shadow-sm">
423+
<x-dynamic-component :component="'heroicon-o-' . $plugin->icon_name" class="size-8" />
424+
</div>
425+
@else
426+
<div class="grid size-16 shrink-0 place-items-center rounded-lg bg-gradient-to-br from-indigo-500 to-purple-600 text-white shadow-sm">
427+
<x-vaadin-plug class="size-8" />
428+
</div>
429+
@endif
430+
<div>
431+
<flux:heading size="lg">{{ $plugin->display_name ?? $plugin->name }}</flux:heading>
432+
@if ($plugin->display_name)
433+
<flux:text class="font-mono text-xs">{{ $plugin->name }}</flux:text>
434+
@endif
435+
@if ($plugin->description)
436+
<flux:text class="mt-2">{{ $plugin->description }}</flux:text>
437+
@else
438+
<flux:text class="mt-2 text-gray-400 dark:text-gray-500">No description provided</flux:text>
439+
@endif
440+
</div>
441+
</div>
442+
@if ($plugin->isPaid() && $plugin->tier)
443+
@php
444+
$regularPrice = $plugin->tier->getPrices()[\App\Enums\PriceTier::Regular->value] / 100;
445+
@endphp
446+
<span class="inline-flex shrink-0 items-center text-center rounded-full bg-emerald-100 px-2.5 py-0.5 text-xs font-medium text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-400">
447+
{{ $plugin->tier->label() }}&nbsp;&mdash;&nbsp;${{ number_format($regularPrice) }}
448+
</span>
449+
@elseif ($plugin->isPaid())
450+
<span class="inline-flex shrink-0 items-center text-center rounded-full bg-emerald-100 px-2.5 py-0.5 text-xs font-medium text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-400">
451+
Paid
452+
</span>
453+
@else
454+
<span class="inline-flex shrink-0 items-center text-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800 dark:bg-gray-700 dark:text-gray-300">
455+
Free
456+
</span>
457+
@endif
458+
</div>
459+
460+
<flux:separator class="my-4" />
461+
462+
<div class="space-y-3">
463+
<div>
464+
<flux:heading size="sm">Support Channel</flux:heading>
465+
@if ($plugin->support_channel)
466+
<flux:text class="mt-1">{{ $plugin->support_channel }}</flux:text>
467+
@else
468+
<flux:text class="mt-1 text-gray-400 dark:text-gray-500">No support channel set</flux:text>
469+
@endif
470+
</div>
471+
472+
<div>
473+
<flux:heading size="sm">Repository</flux:heading>
474+
<a href="{{ $plugin->repository_url }}" target="_blank" rel="noopener noreferrer" class="mt-1 inline-flex items-center gap-1 text-sm text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300">
475+
{{ $plugin->repository_url }}
476+
<x-heroicon-o-arrow-top-right-on-square class="size-3.5" />
477+
</a>
478+
</div>
479+
</div>
480+
</flux:card>
481+
413482
{{-- Notes --}}
414483
<flux:card>
415484
<flux:heading size="lg">Notes</flux:heading>
@@ -434,6 +503,19 @@ class="block text-sm text-gray-500 file:mr-4 file:rounded-md file:border-0 file:
434503
@elseif ($plugin->isApproved())
435504
{{-- Editable fields for Approved plugins (no tabs) --}}
436505

506+
{{-- GitHub Repo --}}
507+
<a href="{{ $plugin->repository_url }}" target="_blank" rel="noopener noreferrer" class="block">
508+
<flux:card class="mb-6 transition hover:bg-gray-50 dark:hover:bg-gray-700/50">
509+
<div class="flex items-center justify-between">
510+
<div class="flex items-center gap-3">
511+
<x-icons.github class="size-5 text-gray-400 dark:text-gray-500" />
512+
<span class="font-mono text-sm font-medium text-gray-900 dark:text-white">{{ $plugin->name }}</span>
513+
</div>
514+
<x-heroicon-o-arrow-top-right-on-square class="size-4 text-gray-400 dark:text-gray-500" />
515+
</div>
516+
</flux:card>
517+
</a>
518+
437519
{{-- Read-only Type & Tier --}}
438520
<flux:card class="mb-6">
439521
<div class="flex items-center justify-between">

tests/Feature/Livewire/Customer/PluginStatusTransitionsTest.php

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ public function test_submit_free_plugin_does_not_require_tier(): void
369369
$this->assertEquals(PluginStatus::Pending, $plugin->status);
370370
}
371371

372-
public function test_submit_paid_plugin_requires_tier(): void
372+
public function test_save_paid_plugin_requires_tier(): void
373373
{
374374
Feature::define(AllowPaidPlugins::class, true);
375375

@@ -378,16 +378,13 @@ public function test_submit_paid_plugin_requires_tier(): void
378378

379379
$this->mountShowComponent($user, $plugin)
380380
->set('description', 'A test plugin')
381+
->set('supportChannel', 'support@example.com')
381382
->set('pluginType', 'paid')
382-
->call('save');
383-
384-
$plugin->refresh();
385-
386-
$this->mountShowComponent($user, $plugin)
387-
->call('submitForReview');
383+
->call('save')
384+
->assertHasErrors(['tier']);
388385

389386
$plugin->refresh();
390-
$this->assertEquals(PluginStatus::Draft, $plugin->status);
387+
$this->assertNull($plugin->tier);
391388
}
392389

393390
public function test_submit_paid_plugin_with_tier_saves_type_and_tier(): void

0 commit comments

Comments
 (0)