Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions app/Jobs/ReviewPluginRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,29 @@ public function handle(): array
{
$repo = $this->plugin->getRepositoryOwnerAndName();

$failedChecks = [
'has_license_file' => false,
'has_release_version' => false,
'release_version' => null,
'supports_ios' => false,
'supports_android' => false,
'supports_js' => false,
'requires_mobile_sdk' => false,
'mobile_sdk_constraint' => null,
'has_ios_min_version' => false,
'ios_min_version' => null,
'has_android_min_version' => false,
'android_min_version' => null,
];

if (! $repo) {
Log::warning('[ReviewPluginRepository] No valid repository URL', [
'plugin_id' => $this->plugin->id,
]);

return [];
$this->plugin->update(['review_checks' => $failedChecks, 'reviewed_at' => now()]);

return $failedChecks;
}

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

return [];
$this->plugin->update(['review_checks' => $failedChecks, 'reviewed_at' => now()]);

return $failedChecks;
}

$tree = $this->fetchRepoTree($owner, $repoName, $defaultBranch, $token);
Expand Down
170 changes: 136 additions & 34 deletions app/Livewire/Customer/Plugins/Show.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
use App\Models\Plugin;
use App\Notifications\PluginSubmitted;
use App\Services\GitHubUserService;
use Flux\Flux;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
Expand Down Expand Up @@ -74,45 +76,86 @@ public function mount(string $vendor, string $package): void
$this->tier = $this->plugin->tier?->value;
}

public function runPreflightChecks(): void
{
if (! $this->plugin->isDraft()) {
return;
}

$user = auth()->user();
$repoInfo = $this->plugin->getRepositoryOwnerAndName();

// Ensure a webhook secret exists so we can always show setup instructions
if (! $this->plugin->webhook_secret) {
$this->plugin->generateWebhookSecret();
}

// Verify or install webhook
if ($repoInfo && $user->hasGitHubToken()) {
$githubService = GitHubUserService::for($user);
$webhookUrl = $this->plugin->getWebhookUrl();

// Check if our webhook already exists on the repo
if ($webhookUrl && $githubService->webhookExists($repoInfo['owner'], $repoInfo['repo'], $webhookUrl)) {
if (! $this->plugin->webhook_installed) {
$this->plugin->update(['webhook_installed' => true]);
}
} else {
// Webhook not found on GitHub — try to create it
$webhookSecret = $this->plugin->webhook_secret ?? $this->plugin->generateWebhookSecret();
$webhookResult = $githubService->createWebhook(
$repoInfo['owner'],
$repoInfo['repo'],
$this->plugin->getWebhookUrl(),
$webhookSecret
);
$this->plugin->update(['webhook_installed' => $webhookResult['success']]);
}
}

// Run review checks
(new ReviewPluginRepository($this->plugin))->handle();

$this->plugin->refresh();
}

public function submitForReview(): void
{
if (! $this->plugin->isDraft()) {
session()->flash('error', 'Only draft plugins can be submitted for review.');
Flux::toast(variant: 'danger', text: 'Only draft plugins can be submitted for review.');

return;
}

if (! $this->plugin->description) {
Flux::toast(variant: 'danger', text: 'Please add a description before submitting for review.');

return;
}

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

return;
}

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

return;
}

$user = auth()->user();
// Run preflight checks
$this->runPreflightChecks();

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

if ($repoInfo && $user->hasGitHubToken()) {
$webhookSecret = $this->plugin->webhook_secret ?? $this->plugin->generateWebhookSecret();
$githubService = GitHubUserService::for($user);
$webhookResult = $githubService->createWebhook(
$repoInfo['owner'],
$repoInfo['repo'],
$this->plugin->getWebhookUrl(),
$webhookSecret
);
$this->plugin->update(['webhook_installed' => $webhookResult['success']]);
return;
}

// Run review checks
(new ReviewPluginRepository($this->plugin))->handle();
$user = auth()->user();

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

session()->flash('success', 'Your plugin has been submitted for review!');
Flux::toast(variant: 'success', text: 'Your plugin has been submitted for review!');
}

public function certifyWebhook(): void
{
if ($this->plugin->webhook_installed) {
return;
}

if (! $this->plugin->webhook_secret) {
$this->plugin->generateWebhookSecret();
}

$this->plugin->update(['webhook_installed' => true]);
$this->plugin->refresh();

$this->modal('certify-webhook')->close();

Flux::toast(variant: 'success', text: 'Webhook marked as installed.');
}

public function retryWebhook(): void
{
$user = auth()->user();
$repoInfo = $this->plugin->getRepositoryOwnerAndName();

if (! $repoInfo || ! $user->hasGitHubToken()) {
Flux::toast(variant: 'danger', text: 'Unable to register webhook automatically. Please ensure your GitHub account is connected and the repository URL is valid.');

return;
}

$webhookSecret = $this->plugin->webhook_secret ?? $this->plugin->generateWebhookSecret();
$githubService = GitHubUserService::for($user);
$webhookResult = $githubService->createWebhook(
$repoInfo['owner'],
$repoInfo['repo'],
$this->plugin->getWebhookUrl(),
$webhookSecret
);

$this->plugin->update(['webhook_installed' => $webhookResult['success']]);
$this->plugin->refresh();

if ($webhookResult['success']) {
Flux::toast(variant: 'success', text: 'Webhook installed successfully.');
} else {
Flux::toast(variant: 'danger', text: 'Failed to install webhook: '.($webhookResult['error'] ?? 'Unknown error'));
}
}

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

return;
}

$this->plugin->withdraw();
$this->plugin->refresh();

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

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

return;
}

$this->plugin->returnToDraft();
$this->plugin->refresh();

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

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

return;
}
Expand Down Expand Up @@ -189,7 +280,7 @@ function (string $attribute, mixed $value, \Closure $fail) {
]);

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

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

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

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

return;
}
Expand All @@ -239,13 +330,13 @@ public function updateIcon(): void

$this->plugin->refresh();

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

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

return;
}
Expand All @@ -270,13 +361,13 @@ public function uploadLogo(): void
$this->logo = null;
$this->iconMode = 'upload';

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

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

return;
}
Expand All @@ -294,13 +385,13 @@ public function deleteIcon(): void
$this->plugin->refresh();
$this->iconMode = 'gradient';

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

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

return;
}
Expand All @@ -312,7 +403,18 @@ public function toggleListing(): void
$this->plugin->refresh();

$action = $this->plugin->is_active ? 'listed' : 'de-listed';
session()->flash('success', "Your plugin has been {$action}.");
Flux::toast(variant: 'success', text: "Your plugin has been {$action}.");
}

public function validate($rules = [], $messages = [], $attributes = []): array
{
try {
return parent::validate($rules, $messages, $attributes);
} catch (ValidationException $e) {
$this->dispatch('scroll-to-first-error');

throw $e;
}
}

public function render()
Expand Down
27 changes: 27 additions & 0 deletions app/Services/GitHubUserService.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,33 @@ public function getComposerJson(string $owner, string $repo, string $branch = 'm
return json_decode($content, true);
}

/**
* Check if a webhook with the given URL exists on a GitHub repository.
*/
public function webhookExists(string $owner, string $repo, string $webhookUrl): bool
{
$token = $this->user->getGitHubToken();

if (! $token) {
return false;
}

$response = Http::withToken($token)
->get("https://api.github.com/repos/{$owner}/{$repo}/hooks");

if ($response->failed()) {
return false;
}

foreach ($response->json() as $hook) {
if (($hook['config']['url'] ?? null) === $webhookUrl) {
return true;
}
}

return false;
}

/**
* Create a webhook on a GitHub repository.
*
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions resources/views/components/layouts/dashboard.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ class="min-h-screen bg-white font-poppins antialiased dark:bg-zinc-900 dark:text
{{ $slot }}
</flux:main>

<flux:toast />

<x-impersonate::banner/>

@livewireScriptConfig
Expand Down
Loading
Loading