Skip to content

Commit 86abf25

Browse files
simonhampclaude
andcommitted
Add webhook self-certification, preflight checks, and plugin summary enhancements
- Let developers self-certify webhook installation when auto-registration fails - Add retry automatic webhook setup button - Verify webhook existence via GitHub API during preflight checks - Gate plugin submission on preflight checks (license, release, webhook) - Add description validation to submission flow - Enhance plugin summary card with author, version, license, min iOS/Android - Replace session flash messages with Flux toasts - Add scroll-to-first-error on validation failures - Always show webhook setup instructions when check fails - Always populate review_checks even when GitHub API calls fail - Add flux:toast to dashboard layout Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 85f461e commit 86abf25

9 files changed

Lines changed: 854 additions & 125 deletions

File tree

app/Jobs/ReviewPluginRepository.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,29 @@ public function handle(): array
2525
{
2626
$repo = $this->plugin->getRepositoryOwnerAndName();
2727

28+
$failedChecks = [
29+
'has_license_file' => false,
30+
'has_release_version' => false,
31+
'release_version' => null,
32+
'supports_ios' => false,
33+
'supports_android' => false,
34+
'supports_js' => false,
35+
'requires_mobile_sdk' => false,
36+
'mobile_sdk_constraint' => null,
37+
'has_ios_min_version' => false,
38+
'ios_min_version' => null,
39+
'has_android_min_version' => false,
40+
'android_min_version' => null,
41+
];
42+
2843
if (! $repo) {
2944
Log::warning('[ReviewPluginRepository] No valid repository URL', [
3045
'plugin_id' => $this->plugin->id,
3146
]);
3247

33-
return [];
48+
$this->plugin->update(['review_checks' => $failedChecks, 'reviewed_at' => now()]);
49+
50+
return $failedChecks;
3451
}
3552

3653
$token = $this->getGitHubToken();
@@ -44,7 +61,9 @@ public function handle(): array
4461
'plugin_id' => $this->plugin->id,
4562
]);
4663

47-
return [];
64+
$this->plugin->update(['review_checks' => $failedChecks, 'reviewed_at' => now()]);
65+
66+
return $failedChecks;
4867
}
4968

5069
$tree = $this->fetchRepoTree($owner, $repoName, $defaultBranch, $token);

app/Livewire/Customer/Plugins/Show.php

Lines changed: 136 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
use App\Models\Plugin;
99
use App\Notifications\PluginSubmitted;
1010
use App\Services\GitHubUserService;
11+
use Flux\Flux;
1112
use Illuminate\Support\Facades\Storage;
13+
use Illuminate\Validation\ValidationException;
1214
use Livewire\Attributes\Computed;
1315
use Livewire\Attributes\Layout;
1416
use Livewire\Attributes\Title;
@@ -74,45 +76,86 @@ public function mount(string $vendor, string $package): void
7476
$this->tier = $this->plugin->tier?->value;
7577
}
7678

79+
public function runPreflightChecks(): void
80+
{
81+
if (! $this->plugin->isDraft()) {
82+
return;
83+
}
84+
85+
$user = auth()->user();
86+
$repoInfo = $this->plugin->getRepositoryOwnerAndName();
87+
88+
// Ensure a webhook secret exists so we can always show setup instructions
89+
if (! $this->plugin->webhook_secret) {
90+
$this->plugin->generateWebhookSecret();
91+
}
92+
93+
// Verify or install webhook
94+
if ($repoInfo && $user->hasGitHubToken()) {
95+
$githubService = GitHubUserService::for($user);
96+
$webhookUrl = $this->plugin->getWebhookUrl();
97+
98+
// Check if our webhook already exists on the repo
99+
if ($webhookUrl && $githubService->webhookExists($repoInfo['owner'], $repoInfo['repo'], $webhookUrl)) {
100+
if (! $this->plugin->webhook_installed) {
101+
$this->plugin->update(['webhook_installed' => true]);
102+
}
103+
} else {
104+
// Webhook not found on GitHub — try to create it
105+
$webhookSecret = $this->plugin->webhook_secret ?? $this->plugin->generateWebhookSecret();
106+
$webhookResult = $githubService->createWebhook(
107+
$repoInfo['owner'],
108+
$repoInfo['repo'],
109+
$this->plugin->getWebhookUrl(),
110+
$webhookSecret
111+
);
112+
$this->plugin->update(['webhook_installed' => $webhookResult['success']]);
113+
}
114+
}
115+
116+
// Run review checks
117+
(new ReviewPluginRepository($this->plugin))->handle();
118+
119+
$this->plugin->refresh();
120+
}
121+
77122
public function submitForReview(): void
78123
{
79124
if (! $this->plugin->isDraft()) {
80-
session()->flash('error', 'Only draft plugins can be submitted for review.');
125+
Flux::toast(variant: 'danger', text: 'Only draft plugins can be submitted for review.');
126+
127+
return;
128+
}
129+
130+
if (! $this->plugin->description) {
131+
Flux::toast(variant: 'danger', text: 'Please add a description before submitting for review.');
81132

82133
return;
83134
}
84135

85136
if (! $this->plugin->support_channel) {
86-
session()->flash('error', 'Please set a support channel before submitting for review.');
137+
Flux::toast(variant: 'danger', text: 'Please set a support channel before submitting for review.');
87138

88139
return;
89140
}
90141

91142
if ($this->plugin->isPaid() && ! $this->plugin->tier) {
92-
session()->flash('error', 'Please select a pricing tier for your paid plugin.');
143+
Flux::toast(variant: 'danger', text: 'Please select a pricing tier for your paid plugin.');
93144

94145
return;
95146
}
96147

97-
$user = auth()->user();
148+
// Run preflight checks
149+
$this->runPreflightChecks();
98150

99-
// Install webhook
100-
$repoInfo = $this->plugin->getRepositoryOwnerAndName();
151+
// Only submit if required checks pass
152+
if (! $this->plugin->passesRequiredReviewChecks()) {
153+
Flux::toast(variant: 'danger', text: 'Your plugin doesn\'t pass all required checks yet. Please resolve the failing checks and try again.');
101154

102-
if ($repoInfo && $user->hasGitHubToken()) {
103-
$webhookSecret = $this->plugin->webhook_secret ?? $this->plugin->generateWebhookSecret();
104-
$githubService = GitHubUserService::for($user);
105-
$webhookResult = $githubService->createWebhook(
106-
$repoInfo['owner'],
107-
$repoInfo['repo'],
108-
$this->plugin->getWebhookUrl(),
109-
$webhookSecret
110-
);
111-
$this->plugin->update(['webhook_installed' => $webhookResult['success']]);
155+
return;
112156
}
113157

114-
// Run review checks
115-
(new ReviewPluginRepository($this->plugin))->handle();
158+
$user = auth()->user();
116159

117160
// Submit
118161
$this->plugin->submit();
@@ -121,41 +164,89 @@ public function submitForReview(): void
121164
// Notify
122165
$user->notify(new PluginSubmitted($this->plugin));
123166

124-
session()->flash('success', 'Your plugin has been submitted for review!');
167+
Flux::toast(variant: 'success', text: 'Your plugin has been submitted for review!');
168+
}
169+
170+
public function certifyWebhook(): void
171+
{
172+
if ($this->plugin->webhook_installed) {
173+
return;
174+
}
175+
176+
if (! $this->plugin->webhook_secret) {
177+
$this->plugin->generateWebhookSecret();
178+
}
179+
180+
$this->plugin->update(['webhook_installed' => true]);
181+
$this->plugin->refresh();
182+
183+
$this->modal('certify-webhook')->close();
184+
185+
Flux::toast(variant: 'success', text: 'Webhook marked as installed.');
186+
}
187+
188+
public function retryWebhook(): void
189+
{
190+
$user = auth()->user();
191+
$repoInfo = $this->plugin->getRepositoryOwnerAndName();
192+
193+
if (! $repoInfo || ! $user->hasGitHubToken()) {
194+
Flux::toast(variant: 'danger', text: 'Unable to register webhook automatically. Please ensure your GitHub account is connected and the repository URL is valid.');
195+
196+
return;
197+
}
198+
199+
$webhookSecret = $this->plugin->webhook_secret ?? $this->plugin->generateWebhookSecret();
200+
$githubService = GitHubUserService::for($user);
201+
$webhookResult = $githubService->createWebhook(
202+
$repoInfo['owner'],
203+
$repoInfo['repo'],
204+
$this->plugin->getWebhookUrl(),
205+
$webhookSecret
206+
);
207+
208+
$this->plugin->update(['webhook_installed' => $webhookResult['success']]);
209+
$this->plugin->refresh();
210+
211+
if ($webhookResult['success']) {
212+
Flux::toast(variant: 'success', text: 'Webhook installed successfully.');
213+
} else {
214+
Flux::toast(variant: 'danger', text: 'Failed to install webhook: '.($webhookResult['error'] ?? 'Unknown error'));
215+
}
125216
}
126217

127218
public function withdrawFromReview(): void
128219
{
129220
if (! $this->plugin->isPending()) {
130-
session()->flash('error', 'Only pending plugins can be withdrawn.');
221+
Flux::toast(variant: 'danger', text: 'Only pending plugins can be withdrawn.');
131222

132223
return;
133224
}
134225

135226
$this->plugin->withdraw();
136227
$this->plugin->refresh();
137228

138-
session()->flash('success', 'Your plugin has been withdrawn from review and returned to draft.');
229+
Flux::toast(variant: 'success', text: 'Your plugin has been withdrawn from review and returned to draft.');
139230
}
140231

141232
public function returnToDraft(): void
142233
{
143234
if (! $this->plugin->isRejected()) {
144-
session()->flash('error', 'Only rejected plugins can be returned to draft.');
235+
Flux::toast(variant: 'danger', text: 'Only rejected plugins can be returned to draft.');
145236

146237
return;
147238
}
148239

149240
$this->plugin->returnToDraft();
150241
$this->plugin->refresh();
151242

152-
session()->flash('success', 'Your plugin has been returned to draft. You can make changes and resubmit.');
243+
Flux::toast(variant: 'success', text: 'Your plugin has been returned to draft. You can make changes and resubmit.');
153244
}
154245

155246
public function save(): void
156247
{
157248
if (! $this->plugin->isDraft() && ! $this->plugin->isApproved()) {
158-
session()->flash('error', 'You can only edit draft or approved plugins.');
249+
Flux::toast(variant: 'danger', text: 'You can only edit draft or approved plugins.');
159250

160251
return;
161252
}
@@ -189,7 +280,7 @@ function (string $attribute, mixed $value, \Closure $fail) {
189280
]);
190281

191282
if ($this->plugin->isDraft() && $this->pluginType === 'paid' && ! $this->hasCompletedDeveloperOnboarding) {
192-
session()->flash('error', 'You must complete developer onboarding before setting a plugin as paid.');
283+
Flux::toast(variant: 'danger', text: 'You must complete developer onboarding before setting a plugin as paid.');
193284

194285
return;
195286
}
@@ -211,13 +302,13 @@ function (string $attribute, mixed $value, \Closure $fail) {
211302
$this->plugin->updateDescription($this->description, auth()->id());
212303
$this->plugin->refresh();
213304

214-
session()->flash('success', 'Plugin details saved successfully!');
305+
Flux::toast(variant: 'success', text: 'Plugin details saved successfully!');
215306
}
216307

217308
public function updateIcon(): void
218309
{
219310
if (! $this->plugin->isDraft() && ! $this->plugin->isApproved()) {
220-
session()->flash('error', 'You can only edit the icon for draft or approved plugins.');
311+
Flux::toast(variant: 'danger', text: 'You can only edit the icon for draft or approved plugins.');
221312

222313
return;
223314
}
@@ -239,13 +330,13 @@ public function updateIcon(): void
239330

240331
$this->plugin->refresh();
241332

242-
session()->flash('success', 'Plugin icon updated successfully!');
333+
Flux::toast(variant: 'success', text: 'Plugin icon updated successfully!');
243334
}
244335

245336
public function uploadLogo(): void
246337
{
247338
if (! $this->plugin->isDraft() && ! $this->plugin->isApproved()) {
248-
session()->flash('error', 'You can only upload a logo for draft or approved plugins.');
339+
Flux::toast(variant: 'danger', text: 'You can only upload a logo for draft or approved plugins.');
249340

250341
return;
251342
}
@@ -270,13 +361,13 @@ public function uploadLogo(): void
270361
$this->logo = null;
271362
$this->iconMode = 'upload';
272363

273-
session()->flash('success', 'Plugin logo updated successfully!');
364+
Flux::toast(variant: 'success', text: 'Plugin logo updated successfully!');
274365
}
275366

276367
public function deleteIcon(): void
277368
{
278369
if (! $this->plugin->isDraft() && ! $this->plugin->isApproved()) {
279-
session()->flash('error', 'You can only remove the icon for draft or approved plugins.');
370+
Flux::toast(variant: 'danger', text: 'You can only remove the icon for draft or approved plugins.');
280371

281372
return;
282373
}
@@ -294,13 +385,13 @@ public function deleteIcon(): void
294385
$this->plugin->refresh();
295386
$this->iconMode = 'gradient';
296387

297-
session()->flash('success', 'Plugin icon removed successfully!');
388+
Flux::toast(variant: 'success', text: 'Plugin icon removed successfully!');
298389
}
299390

300391
public function toggleListing(): void
301392
{
302393
if (! $this->plugin->isApproved()) {
303-
session()->flash('error', 'Only approved plugins can be listed or de-listed.');
394+
Flux::toast(variant: 'danger', text: 'Only approved plugins can be listed or de-listed.');
304395

305396
return;
306397
}
@@ -312,7 +403,18 @@ public function toggleListing(): void
312403
$this->plugin->refresh();
313404

314405
$action = $this->plugin->is_active ? 'listed' : 'de-listed';
315-
session()->flash('success', "Your plugin has been {$action}.");
406+
Flux::toast(variant: 'success', text: "Your plugin has been {$action}.");
407+
}
408+
409+
public function validate($rules = [], $messages = [], $attributes = []): array
410+
{
411+
try {
412+
return parent::validate($rules, $messages, $attributes);
413+
} catch (ValidationException $e) {
414+
$this->dispatch('scroll-to-first-error');
415+
416+
throw $e;
417+
}
316418
}
317419

318420
public function render()

app/Services/GitHubUserService.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,33 @@ public function getComposerJson(string $owner, string $repo, string $branch = 'm
144144
return json_decode($content, true);
145145
}
146146

147+
/**
148+
* Check if a webhook with the given URL exists on a GitHub repository.
149+
*/
150+
public function webhookExists(string $owner, string $repo, string $webhookUrl): bool
151+
{
152+
$token = $this->user->getGitHubToken();
153+
154+
if (! $token) {
155+
return false;
156+
}
157+
158+
$response = Http::withToken($token)
159+
->get("https://api.github.com/repos/{$owner}/{$repo}/hooks");
160+
161+
if ($response->failed()) {
162+
return false;
163+
}
164+
165+
foreach ($response->json() as $hook) {
166+
if (($hook['config']['url'] ?? null) === $webhookUrl) {
167+
return true;
168+
}
169+
}
170+
171+
return false;
172+
}
173+
147174
/**
148175
* Create a webhook on a GitHub repository.
149176
*

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

resources/views/components/layouts/dashboard.blade.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ class="min-h-screen bg-white font-poppins antialiased dark:bg-zinc-900 dark:text
250250
{{ $slot }}
251251
</flux:main>
252252

253+
<flux:toast />
254+
253255
<x-impersonate::banner/>
254256

255257
@livewireScriptConfig

0 commit comments

Comments
 (0)