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
2 changes: 1 addition & 1 deletion resources/views/components/plugin-card.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class="size-12 shrink-0 rounded-xl object-cover"
</span>
@elseif ($plugin->isPaid())
<span class="inline-flex items-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">
Paid
Premium
</span>
@else
<span class="inline-flex items-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">
Expand Down
17 changes: 17 additions & 0 deletions resources/views/components/plugin-command.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@props(['command'])

<div class="flex items-center gap-2 rounded-lg bg-zinc-900 dark:bg-zinc-800">
<div class="min-w-0 flex-1 overflow-x-auto p-3">
<code class="block whitespace-pre font-mono text-xs text-zinc-100">{{ $slot->isEmpty() ? $command : $slot }}</code>
</div>
<button
type="button"
x-data="{ copied: false }"
x-on:click="navigator.clipboard.writeText(@js($command)).then(() => { copied = true; setTimeout(() => copied = false, 2000) })"
class="flex shrink-0 items-center self-stretch px-3 text-zinc-400 hover:text-zinc-200"
title="Copy command"
>
<x-heroicon-o-clipboard x-show="!copied" class="size-4" />
<x-heroicon-o-check-circle x-show="copied" x-cloak class="size-4 text-green-400" />
</button>
</div>
161 changes: 96 additions & 65 deletions resources/views/plugin-show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,77 +114,108 @@ class="text-2xl font-bold sm:text-3xl"
@endif

@if ($plugin->isPaid())
<aside class="mb-6 rounded-2xl border border-indigo-200 bg-indigo-50 p-5 dark:border-indigo-800 dark:bg-indigo-950/30">
<h3 class="text-sm font-semibold text-indigo-900 dark:text-indigo-200">Installing this plugin</h3>
<p class="mt-1 text-sm text-indigo-800 dark:text-indigo-300">
Premium plugins require Composer to be configured with the NativePHP plugin repository and your credentials.
</p>
<div class="mt-3 space-y-2">
<div class="flex items-center gap-2 rounded-lg bg-zinc-900 dark:bg-zinc-800">
<div class="min-w-0 flex-1 overflow-x-auto p-3">
<code class="block whitespace-pre font-mono text-xs text-zinc-100">composer config repositories.nativephp-plugins composer https://plugins.nativephp.com</code>
</div>
<aside
x-data="{ open: false }"
class="mb-6 rounded-2xl border border-indigo-200 bg-indigo-50 p-5 dark:border-indigo-800 dark:bg-indigo-950/30"
>
<h3 class="text-sm font-semibold text-indigo-900 dark:text-indigo-200">
<button
type="button"
x-on:click="open = !open"
:aria-expanded="open"
class="flex w-full items-center justify-between gap-2 text-left"
>
Installation Instructions
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-4 shrink-0 text-indigo-500 transition-transform duration-200 dark:text-indigo-400"
:class="{ 'rotate-180': open }"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
</button>
</h3>

<div x-show="open" x-collapse x-cloak>
<p class="mt-3 text-sm text-indigo-800 dark:text-indigo-300">
Premium plugins are served from the NativePHP plugins repository. Configure Composer with your credentials, then install and register the plugin.
</p>

<div
x-data="{ open: false }"
class="mt-4"
>
<button
type="button"
x-data="{ copied: false }"
x-on:click="navigator.clipboard.writeText('composer config repositories.nativephp-plugins composer https://plugins.nativephp.com').then(() => { copied = true; setTimeout(() => copied = false, 2000) })"
class="shrink-0 self-stretch px-3 text-zinc-400 hover:text-zinc-200"
title="Copy command"
x-on:click="open = !open"
:aria-expanded="open"
class="flex w-full items-center justify-between gap-2 text-left"
>
<x-heroicon-o-clipboard x-show="!copied" class="size-4" />
<x-heroicon-o-check-circle x-show="copied" x-cloak class="size-4 text-green-400" />
</button>
</div>
@auth
@php
$pluginCredentialUser = auth()->user();
$pluginCredentialEmail = $pluginCredentialUser->email;
$pluginCredentialKey = $pluginCredentialUser->getPluginLicenseKey();
[$pluginCredentialEmailLocal, $pluginCredentialEmailDomain] = array_pad(explode('@', $pluginCredentialEmail, 2), 2, '');
$maskedPluginCredentialEmail = mb_substr($pluginCredentialEmailLocal, 0, 1).str_repeat('•', 6).($pluginCredentialEmailDomain !== '' ? '@'.$pluginCredentialEmailDomain : '');
$maskedPluginCredentialKey = str_repeat('•', 16);
@endphp
<div class="flex items-center gap-2 rounded-lg bg-zinc-900 dark:bg-zinc-800">
<div class="min-w-0 flex-1 overflow-x-auto p-3">
<code class="block whitespace-pre font-mono text-xs text-zinc-100">composer config http-basic.plugins.nativephp.com {{ $maskedPluginCredentialEmail }} {{ $maskedPluginCredentialKey }}</code>
</div>
<button
type="button"
x-data="{ copied: false }"
x-on:click="navigator.clipboard.writeText('composer config http-basic.plugins.nativephp.com {{ $pluginCredentialEmail }} {{ $pluginCredentialKey }}').then(() => { copied = true; setTimeout(() => copied = false, 2000) })"
class="shrink-0 self-stretch px-3 text-zinc-400 hover:text-zinc-200"
title="Copy command"
>
<x-heroicon-o-clipboard x-show="!copied" class="size-4" />
<x-heroicon-o-check-circle x-show="copied" x-cloak class="size-4 text-green-400" />
</button>
</div>
@else
<div class="flex items-center gap-2 rounded-lg bg-zinc-900 dark:bg-zinc-800">
<div class="min-w-0 flex-1 overflow-x-auto p-3">
<code class="block whitespace-pre font-mono text-xs text-zinc-100">composer config http-basic.plugins.nativephp.com <span class="text-zinc-400">your-email@example.com</span> <span class="text-zinc-400">your-license-key</span></code>
</div>
<button
type="button"
x-data="{ copied: false }"
x-on:click="navigator.clipboard.writeText('composer config http-basic.plugins.nativephp.com your-email@example.com your-license-key').then(() => { copied = true; setTimeout(() => copied = false, 2000) })"
class="shrink-0 self-stretch px-3 text-zinc-400 hover:text-zinc-200"
title="Copy command"
<span class="text-xs font-semibold text-indigo-900 dark:text-indigo-200">Configure Composer</span>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-4 shrink-0 text-indigo-500 transition-transform duration-200 dark:text-indigo-400"
:class="{ 'rotate-180': open }"
aria-hidden="true"
>
<x-heroicon-o-clipboard x-show="!copied" class="size-4" />
<x-heroicon-o-check-circle x-show="copied" x-cloak class="size-4 text-green-400" />
</button>
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
</button>

<div x-show="open" x-collapse x-cloak class="mt-3 space-y-2">
<p class="text-xs text-indigo-800 dark:text-indigo-300">
A one-time Composer setup so you can install premium plugins.
</p>
@auth
@php
$pluginCredentialUser = auth()->user();
$pluginCredentialEmail = $pluginCredentialUser->email;
$pluginCredentialKey = $pluginCredentialUser->getPluginLicenseKey();
[$pluginCredentialEmailLocal, $pluginCredentialEmailDomain] = array_pad(explode('@', $pluginCredentialEmail, 2), 2, '');
$maskedPluginCredentialEmail = mb_substr($pluginCredentialEmailLocal, 0, 1).str_repeat('•', 6).($pluginCredentialEmailDomain !== '' ? '@'.$pluginCredentialEmailDomain : '');
$maskedPluginCredentialKey = str_repeat('•', 16);
$pluginRepositoryCommand = 'composer config repositories.nativephp-plugins composer https://plugins.nativephp.com';
$pluginConfigCommand = $pluginRepositoryCommand."\n".'composer config http-basic.plugins.nativephp.com '.$pluginCredentialEmail.' '.$pluginCredentialKey;
$pluginConfigDisplay = e($pluginRepositoryCommand)."\n".'composer config http-basic.plugins.nativephp.com '.e($maskedPluginCredentialEmail).' '.e($maskedPluginCredentialKey);
@endphp
<x-plugin-command :command="$pluginConfigCommand">{!! $pluginConfigDisplay !!}</x-plugin-command>
@else
@php
$pluginRepositoryCommand = 'composer config repositories.nativephp-plugins composer https://plugins.nativephp.com';
$pluginConfigCommand = $pluginRepositoryCommand."\n".'composer config http-basic.plugins.nativephp.com your-email@example.com your-license-key';
$pluginConfigDisplay = e($pluginRepositoryCommand)."\n".'composer config http-basic.plugins.nativephp.com <span class="text-zinc-400">your-email@example.com</span> <span class="text-zinc-400">your-license-key</span>';
@endphp
<x-plugin-command :command="$pluginConfigCommand">{!! $pluginConfigDisplay !!}</x-plugin-command>
@endauth
<p class="text-xs text-indigo-700 dark:text-indigo-400">
@auth
Manage your credentials on your <a href="{{ route('customer.purchased-plugins.index') }}" class="font-medium underline hover:no-underline">Purchased Plugins</a> dashboard.
@else
<a href="{{ route('customer.login') }}" class="font-medium underline hover:no-underline">Log in</a> to see your credentials, or find them on your <a href="{{ route('customer.purchased-plugins.index') }}" class="font-medium underline hover:no-underline">Purchased Plugins</a> dashboard.
@endauth
</p>
</div>
@endauth
</div>

@php
$pluginInstallCommands = "php artisan vendor:publish --tag=nativephp-plugins-provider\ncomposer require {$plugin->name}\nphp artisan native:plugin:register {$plugin->name}";
@endphp
<div class="mt-4 space-y-1.5 border-t border-indigo-200 pt-4 dark:border-indigo-800">
<x-plugin-command :command="$pluginInstallCommands" />
<p class="text-xs text-indigo-700 dark:text-indigo-400">
Full walkthrough in the <a href="{{ url('docs/mobile/3/plugins/using-plugins') }}" class="font-medium underline hover:no-underline">Using Plugins guide &rarr;</a>
</p>
</div>
</div>
<p class="mt-3 text-xs text-indigo-700 dark:text-indigo-400">
@auth
Manage your credentials on your <a href="{{ route('customer.purchased-plugins.index') }}" class="font-medium underline hover:no-underline">Purchased Plugins</a> dashboard.
@else
<a href="{{ route('customer.login') }}" class="font-medium underline hover:no-underline">Log in</a> to see your credentials, or find them on your <a href="{{ route('customer.purchased-plugins.index') }}" class="font-medium underline hover:no-underline">Purchased Plugins</a> dashboard.
@endauth
<a href="{{ url('docs/mobile/3/plugins/using-plugins') }}" class="font-medium underline hover:no-underline">Learn more &rarr;</a>
</p>
</aside>
@endif

Expand Down
9 changes: 9 additions & 0 deletions tests/Feature/PluginDirectoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,13 @@ public function test_plugin_directory_paginates_twelve_per_page(): void
&& $plugins->lastPage() === 2;
});
}

public function test_paid_plugin_card_shows_premium_badge(): void
{
Plugin::factory()->approved()->paid()->create();

Livewire::test(PluginDirectory::class)
->assertSee('Premium')
->assertDontSee('Paid');
}
}
34 changes: 34 additions & 0 deletions tests/Feature/PluginShowInstallCredentialsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,38 @@ public function test_copy_command_still_contains_the_real_credentials(): void
->assertSee('developer@example.test', false)
->assertSee($key, false);
}

public function test_install_commands_are_shown(): void
{
$plugin = $this->paidPlugin();

$this->get(route('plugins.show', $plugin->routeParams()))
->assertStatus(200)
->assertSee('php artisan vendor:publish --tag=nativephp-plugins-provider')
->assertSee('composer require '.$plugin->name)
->assertSee('php artisan native:plugin:register '.$plugin->name);
}

public function test_credentials_section_is_collapsible_and_still_renders_credentials(): void
{
$user = User::factory()->create(['email' => 'developer@example.test']);
$key = $user->getPluginLicenseKey();
$plugin = $this->paidPlugin();

$this->actingAs($user)
->get(route('plugins.show', $plugin->routeParams()))
->assertStatus(200)
->assertSee('Configure Composer')
->assertSee($key, false);
}

public function test_install_box_links_to_the_using_plugins_guide(): void
{
$plugin = $this->paidPlugin();

$this->get(route('plugins.show', $plugin->routeParams()))
->assertStatus(200)
->assertSee(url('docs/mobile/3/plugins/using-plugins'), false)
->assertSee('Using Plugins guide');
}
}
2 changes: 1 addition & 1 deletion tests/Feature/PluginShowPaidGuestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public function test_paid_plugin_show_page_renders_for_guest(): void

$this->get(route('plugins.show', $plugin->routeParams()))
->assertStatus(200)
->assertSee('Installing this plugin')
->assertSee('Installation Instructions')
->assertSee('Log in');
}
}
Loading