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
12 changes: 12 additions & 0 deletions app/Livewire/Customer/Plugins/Create.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ class Create extends Component

public bool $reposLoaded = false;

#[Computed]
public function hasCompletedDeveloperOnboarding(): bool
{
return auth()->user()->developerAccount?->hasCompletedOnboarding() ?? false;
}

#[Computed]
public function owners(): array
{
Expand Down Expand Up @@ -142,6 +148,12 @@ function ($attribute, $value, $fail): void {
return;
}

if ($this->pluginType === 'paid' && ! $this->hasCompletedDeveloperOnboarding) {
session()->flash('error', 'You must complete developer onboarding before creating a paid plugin.');

return;
}

$repository = trim($this->repository, '/');
$repositoryUrl = 'https://github.com/'.$repository;
[$owner, $repo] = explode('/', $repository);
Expand Down
13 changes: 13 additions & 0 deletions app/Livewire/Customer/Plugins/Show.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use App\Notifications\PluginSubmitted;
use App\Services\GitHubUserService;
use Illuminate\Support\Facades\Storage;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Validate;
Expand Down Expand Up @@ -48,6 +49,12 @@ class Show extends Component

public ?string $tier = null;

#[Computed]
public function hasCompletedDeveloperOnboarding(): bool
{
return auth()->user()->developerAccount?->hasCompletedOnboarding() ?? false;
}

public function mount(string $vendor, string $package): void
{
$this->plugin = Plugin::findByVendorPackageOrFail($vendor, $package);
Expand Down Expand Up @@ -181,6 +188,12 @@ function (string $attribute, mixed $value, \Closure $fail) {
'tier.required' => 'Please select a pricing tier for your paid plugin.',
]);

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

return;
}

$data = [
'display_name' => $this->displayName ?: null,
'support_channel' => $this->supportChannel,
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.

11 changes: 8 additions & 3 deletions resources/views/livewire/customer/plugins/create.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,19 @@
</span>
</label>

<label class="relative flex cursor-pointer rounded-lg border p-4 transition focus:outline-none"
:class="$wire.pluginType === 'paid' ? 'border-indigo-500 bg-indigo-50 dark:border-indigo-400 dark:bg-indigo-950/30' : 'border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50'">
<input type="radio" wire:model="pluginType" value="paid" class="sr-only" />
<label class="relative flex rounded-lg border p-4 transition focus:outline-none {{ $this->hasCompletedDeveloperOnboarding ? 'cursor-pointer' : 'cursor-not-allowed opacity-60' }}"
:class="$wire.pluginType === 'paid' ? 'border-indigo-500 bg-indigo-50 dark:border-indigo-400 dark:bg-indigo-950/30' : 'border-gray-200 dark:border-gray-700 {{ $this->hasCompletedDeveloperOnboarding ? 'hover:bg-gray-50 dark:hover:bg-gray-700/50' : '' }}'">
<input type="radio" wire:model="pluginType" value="paid" class="sr-only" {{ $this->hasCompletedDeveloperOnboarding ? '' : 'disabled' }} />
<span class="flex flex-1 flex-col">
<span class="text-sm font-medium text-gray-900 dark:text-white">Paid Plugin</span>
<span class="mt-1 text-sm text-gray-500 dark:text-gray-400">Commercial plugin, hosted on plugins.nativephp.com</span>
</span>
</label>
@if (! $this->hasCompletedDeveloperOnboarding)
<flux:text class="text-sm text-gray-500 dark:text-gray-400">
To create paid plugins, you need to <a href="{{ route('customer.developer.onboarding') }}" class="font-medium text-indigo-600 hover:text-indigo-500 dark:text-indigo-400" wire:navigate>complete developer onboarding</a>.
</flux:text>
@endif
</div>

@error('pluginType')
Expand Down
11 changes: 8 additions & 3 deletions resources/views/livewire/customer/plugins/show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -217,14 +217,19 @@
</span>
</label>

<label class="relative flex cursor-pointer rounded-lg border p-4 transition focus:outline-none"
:class="$wire.pluginType === 'paid' ? 'border-indigo-500 bg-indigo-50 dark:border-indigo-400 dark:bg-indigo-950/30' : 'border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50'">
<input type="radio" wire:model.live="pluginType" value="paid" class="sr-only" />
<label class="relative flex rounded-lg border p-4 transition focus:outline-none {{ $this->hasCompletedDeveloperOnboarding ? 'cursor-pointer' : 'cursor-not-allowed opacity-60' }}"
:class="$wire.pluginType === 'paid' ? 'border-indigo-500 bg-indigo-50 dark:border-indigo-400 dark:bg-indigo-950/30' : 'border-gray-200 dark:border-gray-700 {{ $this->hasCompletedDeveloperOnboarding ? 'hover:bg-gray-50 dark:hover:bg-gray-700/50' : '' }}'">
<input type="radio" wire:model.live="pluginType" value="paid" class="sr-only" {{ $this->hasCompletedDeveloperOnboarding ? '' : 'disabled' }} />
<span class="flex flex-1 flex-col">
<span class="text-sm font-medium text-gray-900 dark:text-white">Paid Plugin</span>
<span class="mt-1 text-sm text-gray-500 dark:text-gray-400">Commercial plugin, hosted on plugins.nativephp.com</span>
</span>
</label>
@if (! $this->hasCompletedDeveloperOnboarding)
<flux:text class="text-sm text-gray-500 dark:text-gray-400">
To create paid plugins, you need to <a href="{{ route('customer.developer.onboarding') }}" class="font-medium text-indigo-600 hover:text-indigo-500 dark:text-indigo-400" wire:navigate>complete developer onboarding</a>.
</flux:text>
@endif
</div>
</flux:card>

Expand Down
240 changes: 240 additions & 0 deletions tests/Feature/Livewire/Customer/PluginPaidOnboardingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
<?php

namespace Tests\Feature\Livewire\Customer;

use App\Enums\PluginType;
use App\Features\AllowPaidPlugins;
use App\Features\ShowAuthButtons;
use App\Features\ShowPlugins;
use App\Livewire\Customer\Plugins\Create;
use App\Livewire\Customer\Plugins\Show;
use App\Models\DeveloperAccount;
use App\Models\Plugin;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Laravel\Pennant\Feature;
use Livewire\Livewire;
use Tests\TestCase;

class PluginPaidOnboardingTest extends TestCase
{
use RefreshDatabase;

protected function setUp(): void
{
parent::setUp();

Feature::define(ShowAuthButtons::class, true);
Feature::define(ShowPlugins::class, true);
Feature::define(AllowPaidPlugins::class, true);
}

private function createGitHubUser(): User
{
return User::factory()->create([
'github_id' => '12345',
'github_username' => 'testuser',
'github_token' => encrypt('fake-token'),
]);
}

private function fakeComposerJson(string $owner, string $repo, string $packageName): void
{
$composerJson = base64_encode(json_encode(['name' => $packageName]));

Http::fake([
"api.github.com/repos/{$owner}/{$repo}/contents/composer.json*" => Http::response([
'content' => $composerJson,
]),
'api.github.com/*' => Http::response([], 404),
]);
}

// ========================================
// Create: Paid option disabled without onboarding
// ========================================

public function test_create_page_shows_paid_option_disabled_without_onboarding(): void
{
$user = $this->createGitHubUser();

Livewire::actingAs($user)->test(Create::class)
->assertSee('complete developer onboarding')
->assertSeeHtml('disabled');
}

public function test_create_page_shows_paid_option_enabled_with_onboarding(): void
{
$user = $this->createGitHubUser();
DeveloperAccount::factory()->for($user)->create();

Livewire::actingAs($user)->test(Create::class)
->assertDontSee('complete developer onboarding');
}

public function test_create_paid_plugin_blocked_without_onboarding(): void
{
$user = $this->createGitHubUser();

$this->fakeComposerJson('testuser', 'my-plugin', 'testuser/my-plugin');

Livewire::actingAs($user)->test(Create::class)
->set('repository', 'testuser/my-plugin')
->set('pluginType', 'paid')
->call('createPlugin')
->assertNoRedirect();

$this->assertDatabaseMissing('plugins', [
'repository_url' => 'https://github.com/testuser/my-plugin',
]);
}

public function test_create_paid_plugin_allowed_with_onboarding(): void
{
$user = $this->createGitHubUser();
DeveloperAccount::factory()->for($user)->create();

$this->fakeComposerJson('testuser', 'paid-plugin', 'testuser/paid-plugin');

Livewire::actingAs($user)->test(Create::class)
->set('repository', 'testuser/paid-plugin')
->set('pluginType', 'paid')
->call('createPlugin');

$this->assertDatabaseHas('plugins', [
'repository_url' => 'https://github.com/testuser/paid-plugin',
'type' => 'paid',
'status' => 'draft',
]);
}

public function test_create_free_plugin_allowed_without_onboarding(): void
{
$user = $this->createGitHubUser();

$this->fakeComposerJson('testuser', 'free-plugin', 'testuser/free-plugin');

Livewire::actingAs($user)->test(Create::class)
->set('repository', 'testuser/free-plugin')
->set('pluginType', 'free')
->call('createPlugin');

$this->assertDatabaseHas('plugins', [
'repository_url' => 'https://github.com/testuser/free-plugin',
'type' => 'free',
'status' => 'draft',
]);
}

// ========================================
// Edit Draft: Paid option disabled without onboarding
// ========================================

public function test_edit_draft_shows_paid_option_disabled_without_onboarding(): void
{
$user = $this->createGitHubUser();
$plugin = Plugin::factory()->draft()->for($user)->create([
'name' => 'testuser/onboard-test',
]);

[$vendor, $package] = explode('/', $plugin->name);

Livewire::actingAs($user)->test(Show::class, [
'vendor' => $vendor,
'package' => $package,
])
->assertSee('complete developer onboarding')
->assertSeeHtml('disabled');
}

public function test_edit_draft_shows_paid_option_enabled_with_onboarding(): void
{
$user = $this->createGitHubUser();
DeveloperAccount::factory()->for($user)->create();
$plugin = Plugin::factory()->draft()->for($user)->create([
'name' => 'testuser/onboard-enabled',
]);

[$vendor, $package] = explode('/', $plugin->name);

Livewire::actingAs($user)->test(Show::class, [
'vendor' => $vendor,
'package' => $package,
])
->assertDontSee('complete developer onboarding');
}

public function test_save_draft_as_paid_blocked_without_onboarding(): void
{
$user = $this->createGitHubUser();
$plugin = Plugin::factory()->draft()->for($user)->create([
'name' => 'testuser/save-paid-test',
'support_channel' => 'support@test.io',
]);

[$vendor, $package] = explode('/', $plugin->name);

Livewire::actingAs($user)->test(Show::class, [
'vendor' => $vendor,
'package' => $package,
])
->set('description', 'A test plugin')
->set('supportChannel', 'support@test.io')
->set('pluginType', 'paid')
->set('tier', 'gold')
->call('save');

$plugin->refresh();
$this->assertNotEquals(PluginType::Paid, $plugin->type);
}

public function test_save_draft_as_paid_allowed_with_onboarding(): void
{
$user = $this->createGitHubUser();
DeveloperAccount::factory()->for($user)->create();
$plugin = Plugin::factory()->draft()->for($user)->create([
'name' => 'testuser/save-paid-ok',
'support_channel' => 'support@test.io',
]);

[$vendor, $package] = explode('/', $plugin->name);

Livewire::actingAs($user)->test(Show::class, [
'vendor' => $vendor,
'package' => $package,
])
->set('description', 'A test plugin')
->set('supportChannel', 'support@test.io')
->set('pluginType', 'paid')
->set('tier', 'gold')
->call('save');

$plugin->refresh();
$this->assertEquals(PluginType::Paid, $plugin->type);
}

public function test_create_page_shows_onboarding_link_without_onboarding(): void
{
$user = $this->createGitHubUser();

Livewire::actingAs($user)->test(Create::class)
->assertSeeHtml(route('customer.developer.onboarding'));
}

public function test_edit_draft_shows_onboarding_link_without_onboarding(): void
{
$user = $this->createGitHubUser();
$plugin = Plugin::factory()->draft()->for($user)->create([
'name' => 'testuser/link-test',
]);

[$vendor, $package] = explode('/', $plugin->name);

Livewire::actingAs($user)->test(Show::class, [
'vendor' => $vendor,
'package' => $package,
])
->assertSeeHtml(route('customer.developer.onboarding'));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Enums\PluginType;
use App\Features\AllowPaidPlugins;
use App\Livewire\Customer\Plugins\Show;
use App\Models\DeveloperAccount;
use App\Models\Plugin;
use App\Models\User;
use App\Notifications\PluginSubmitted;
Expand Down Expand Up @@ -374,6 +375,7 @@ public function test_save_paid_plugin_requires_tier(): void
Feature::define(AllowPaidPlugins::class, true);

$user = $this->createGitHubUser();
DeveloperAccount::factory()->for($user)->create();
$plugin = $this->createDraftPlugin($user);

$this->mountShowComponent($user, $plugin)
Expand All @@ -393,6 +395,7 @@ public function test_submit_paid_plugin_with_tier_saves_type_and_tier(): void
Feature::define(AllowPaidPlugins::class, true);

$user = $this->createGitHubUser();
DeveloperAccount::factory()->for($user)->create();
$plugin = $this->createDraftPlugin($user);
$this->fakeGitHubForSubmission($plugin);

Expand Down
Loading