Skip to content

Commit acc5e45

Browse files
lessevvclaude
andcommitted
feat(plugins): show permissions and version history on the public plugin page
Adds a "What This Plugin Can Access" panel, sourced from the latest visible version's manifest, and a version history/changelog panel rendering each release's notes. Both skip any version currently held back for admin review. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 08b15dd commit acc5e45

4 files changed

Lines changed: 261 additions & 0 deletions

File tree

‎app/Http/Controllers/PluginDirectoryController.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ public function show(string $vendor, string $package): View
7272
$bestPrice = $plugin->getBestPriceForUser($user);
7373
$regularPrice = $plugin->getRegularPrice();
7474

75+
$currentManifest = $plugin->versions()->visible()->latest('published_at')->first()?->manifest_permissions;
76+
$versionHistory = $plugin->versions()->visible()->orderByDesc('published_at')->limit(15)->get();
77+
7578
$this->setPluginSeo($plugin);
7679

7780
return view('plugin-show', [
@@ -81,6 +84,8 @@ public function show(string $vendor, string $package): View
8184
'regularPrice' => $regularPrice,
8285
'hasDiscount' => $bestPrice && $regularPrice && $bestPrice->id !== $regularPrice->id,
8386
'isAdminPreview' => (! $plugin->isApproved() || ! $plugin->is_active) && ($isAdmin || $isOwner),
87+
'currentManifest' => $currentManifest,
88+
'versionHistory' => $versionHistory,
8489
]);
8590
}
8691

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Support;
6+
7+
use App\Enums\AndroidPermission;
8+
use App\Enums\IosBackgroundMode;
9+
use Illuminate\Support\Str;
10+
11+
final class PluginPermissionLabels
12+
{
13+
public static function androidPermission(string $permission): string
14+
{
15+
return AndroidPermission::tryFrom($permission)?->label() ?? Str::of($permission)
16+
->after('android.permission.')
17+
->replace('_', ' ')
18+
->title()
19+
->toString();
20+
}
21+
22+
public static function iosBackgroundMode(string $mode): string
23+
{
24+
return IosBackgroundMode::tryFrom($mode)?->label() ?? Str::title(str_replace('-', ' ', $mode));
25+
}
26+
27+
public static function iosCapability(string $capability): string
28+
{
29+
return Str::title(str_replace('-', ' ', $capability));
30+
}
31+
32+
public static function iosEntitlement(string $entitlement): string
33+
{
34+
return Str::of($entitlement)->afterLast('.')->replace('-', ' ')->title()->toString();
35+
}
36+
}

‎resources/views/plugin-show.blade.php‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,90 @@ class="size-10 shrink-0 rounded-lg object-cover"
575575
</div>
576576
@endif
577577

578+
{{-- What this plugin can access --}}
579+
@if ($currentManifest && array_filter($currentManifest))
580+
<div class="mt-4 rounded-2xl border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-slate-800/50">
581+
<h2 class="flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
582+
<x-heroicon-o-shield-check class="size-4 shrink-0" aria-hidden="true" />
583+
What This Plugin Can Access
584+
</h2>
585+
<ul class="mt-3 space-y-1.5">
586+
@foreach ($currentManifest['android_permissions'] ?? [] as $permission)
587+
<li class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
588+
<span class="size-1.5 shrink-0 rounded-full bg-indigo-400"></span>
589+
{{ \App\Support\PluginPermissionLabels::androidPermission($permission) }}
590+
</li>
591+
@endforeach
592+
593+
@foreach ($currentManifest['ios_background_modes'] ?? [] as $mode)
594+
<li class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
595+
<span class="size-1.5 shrink-0 rounded-full bg-indigo-400"></span>
596+
{{ \App\Support\PluginPermissionLabels::iosBackgroundMode($mode) }}
597+
</li>
598+
@endforeach
599+
600+
@foreach ($currentManifest['ios_capabilities'] ?? [] as $capability)
601+
<li class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
602+
<span class="size-1.5 shrink-0 rounded-full bg-indigo-400"></span>
603+
{{ \App\Support\PluginPermissionLabels::iosCapability($capability) }}
604+
</li>
605+
@endforeach
606+
</ul>
607+
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
608+
Declared by the plugin author and applied to your app at build time.
609+
</p>
610+
</div>
611+
@endif
612+
613+
{{-- Version history --}}
614+
@if ($versionHistory->isNotEmpty())
615+
<div
616+
x-data="{ open: false }"
617+
class="mt-4 rounded-2xl border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-slate-800/50"
618+
>
619+
<button
620+
type="button"
621+
x-on:click="open = !open"
622+
:aria-expanded="open"
623+
class="flex w-full items-center justify-between gap-2 text-left"
624+
>
625+
<span class="text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
626+
Version History
627+
</span>
628+
<svg
629+
xmlns="http://www.w3.org/2000/svg"
630+
fill="none"
631+
viewBox="0 0 24 24"
632+
stroke-width="2"
633+
stroke="currentColor"
634+
class="size-4 shrink-0 text-gray-400 transition-transform duration-200"
635+
:class="{ 'rotate-180': open }"
636+
aria-hidden="true"
637+
>
638+
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
639+
</svg>
640+
</button>
641+
642+
<div x-show="open" x-collapse x-cloak class="mt-4 space-y-4">
643+
@foreach ($versionHistory as $version)
644+
<div class="border-l-2 border-gray-200 pl-3 dark:border-gray-700">
645+
<div class="flex items-baseline gap-2">
646+
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ $version->version }}</span>
647+
@if ($version->published_at)
648+
<span class="text-xs text-gray-500 dark:text-gray-400">{{ $version->published_at->format('M j, Y') }}</span>
649+
@endif
650+
</div>
651+
@if ($version->release_notes_html)
652+
<div class="prose prose-sm prose-gallery mt-1 max-w-none text-gray-600 dark:text-gray-400">
653+
{!! $version->release_notes_html !!}
654+
</div>
655+
@endif
656+
</div>
657+
@endforeach
658+
</div>
659+
</div>
660+
@endif
661+
578662
</aside>
579663
</div>
580664
</section>
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
3+
namespace Tests\Feature;
4+
5+
use App\Features\ShowPlugins;
6+
use App\Models\Plugin;
7+
use App\Models\PluginVersion;
8+
use Illuminate\Foundation\Testing\RefreshDatabase;
9+
use Laravel\Pennant\Feature;
10+
use Tests\TestCase;
11+
12+
class PluginPermissionAndChangelogDisplayTest extends TestCase
13+
{
14+
use RefreshDatabase;
15+
16+
protected function setUp(): void
17+
{
18+
parent::setUp();
19+
20+
Feature::define(ShowPlugins::class, true);
21+
}
22+
23+
public function test_it_shows_declared_permissions_from_the_latest_visible_version(): void
24+
{
25+
$plugin = Plugin::factory()->approved()->create();
26+
27+
PluginVersion::create([
28+
'plugin_id' => $plugin->id,
29+
'version' => '1.0.0',
30+
'tag_name' => 'v1.0.0',
31+
'github_release_id' => '1',
32+
'published_at' => now(),
33+
'manifest_permissions' => [
34+
'android_permissions' => ['android.permission.CAMERA'],
35+
'ios_background_modes' => ['fetch'],
36+
],
37+
]);
38+
39+
$response = $this->get(route('plugins.show', $plugin->routeParams()));
40+
41+
$response->assertStatus(200);
42+
$response->assertSee('What This Plugin Can Access');
43+
$response->assertSee('Camera');
44+
$response->assertSee('Background Fetch');
45+
}
46+
47+
public function test_it_does_not_show_the_permission_panel_when_nothing_is_declared(): void
48+
{
49+
$plugin = Plugin::factory()->approved()->create();
50+
51+
PluginVersion::create([
52+
'plugin_id' => $plugin->id,
53+
'version' => '1.0.0',
54+
'tag_name' => 'v1.0.0',
55+
'github_release_id' => '1',
56+
'published_at' => now(),
57+
'manifest_permissions' => ['android_permissions' => []],
58+
]);
59+
60+
$response = $this->get(route('plugins.show', $plugin->routeParams()));
61+
62+
$response->assertStatus(200);
63+
$response->assertDontSee('What This Plugin Can Access');
64+
}
65+
66+
public function test_a_version_pending_review_is_not_used_for_the_permission_panel(): void
67+
{
68+
$plugin = Plugin::factory()->approved()->create();
69+
70+
PluginVersion::create([
71+
'plugin_id' => $plugin->id,
72+
'version' => '1.0.0',
73+
'tag_name' => 'v1.0.0',
74+
'github_release_id' => '1',
75+
'published_at' => now(),
76+
'manifest_permissions' => ['android_permissions' => ['android.permission.CAMERA']],
77+
]);
78+
79+
PluginVersion::create([
80+
'plugin_id' => $plugin->id,
81+
'version' => '2.0.0',
82+
'tag_name' => 'v2.0.0',
83+
'github_release_id' => '2',
84+
'published_at' => now()->addDay(),
85+
'manifest_permissions' => ['android_permissions' => ['android.permission.CAMERA', 'android.permission.ACCESS_FINE_LOCATION']],
86+
'requires_review' => true,
87+
]);
88+
89+
$response = $this->get(route('plugins.show', $plugin->routeParams()));
90+
91+
$response->assertStatus(200);
92+
$response->assertSee('Camera');
93+
$response->assertDontSee('Precise Location');
94+
}
95+
96+
public function test_it_shows_version_history_with_rendered_release_notes(): void
97+
{
98+
$plugin = Plugin::factory()->approved()->create();
99+
100+
PluginVersion::create([
101+
'plugin_id' => $plugin->id,
102+
'version' => '1.0.0',
103+
'tag_name' => 'v1.0.0',
104+
'github_release_id' => '1',
105+
'published_at' => now(),
106+
'release_notes' => '**Fixed** a crash on launch.',
107+
'release_notes_html' => '<p><strong>Fixed</strong> a crash on launch.</p>',
108+
]);
109+
110+
$response = $this->get(route('plugins.show', $plugin->routeParams()));
111+
112+
$response->assertStatus(200);
113+
$response->assertSee('Version History');
114+
$response->assertSee('<strong>Fixed</strong> a crash on launch.', false);
115+
}
116+
117+
public function test_version_history_excludes_versions_pending_review(): void
118+
{
119+
$plugin = Plugin::factory()->approved()->create();
120+
121+
PluginVersion::create([
122+
'plugin_id' => $plugin->id,
123+
'version' => '2.0.0',
124+
'tag_name' => 'v2.0.0',
125+
'github_release_id' => '2',
126+
'published_at' => now(),
127+
'release_notes_html' => '<p>Secret unreleased notes.</p>',
128+
'requires_review' => true,
129+
]);
130+
131+
$response = $this->get(route('plugins.show', $plugin->routeParams()));
132+
133+
$response->assertStatus(200);
134+
$response->assertDontSee('Secret unreleased notes.');
135+
}
136+
}

0 commit comments

Comments
 (0)