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 app/Console/Commands/GrantPluginToBundleOwners.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public function handle(): int
'plugin_bundle_id' => $bundle->id,
'price_paid' => 0,
'currency' => 'USD',
'is_grandfathered' => false,
'is_grandfathered' => true,
'purchased_at' => now(),
]);

Expand Down
85 changes: 85 additions & 0 deletions app/Console/Commands/GrantProduct.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

namespace App\Console\Commands;

use App\Models\Product;
use App\Models\ProductLicense;
use App\Models\User;
use App\Notifications\ProductGranted;
use Illuminate\Console\Command;

class GrantProduct extends Command
{
protected $signature = 'products:grant
{product : The product slug}
{user : The user email}
{--dry-run : Preview what would happen without making changes}
{--no-email : Grant the product without sending a notification email}';

protected $description = 'Grant a product to a user by email';

public function handle(): int
{
$product = Product::where('slug', $this->argument('product'))->first();

if (! $product) {
$this->error("Product not found: {$this->argument('product')}");

return Command::FAILURE;
}

$user = User::where('email', $this->argument('user'))->first();

if (! $user) {
$this->error("User not found: {$this->argument('user')}");

return Command::FAILURE;
}

$dryRun = $this->option('dry-run');
$noEmail = $this->option('no-email');

$this->info("Product: {$product->name} (slug: {$product->slug})");
$this->info("User: {$user->email}");

if ($dryRun) {
$this->warn('[DRY RUN] No changes will be made.');
}

$this->newLine();

// Check if user already has a license for this product
$existingLicense = ProductLicense::where('user_id', $user->id)
->where('product_id', $product->id)
->exists();

if ($existingLicense) {
$this->warn("User {$user->email} already has a license for this product.");

return Command::SUCCESS;
}

if (! $dryRun) {
ProductLicense::create([
'user_id' => $user->id,
'product_id' => $product->id,
'price_paid' => 0,
'currency' => 'USD',
'is_comped' => true,
'purchased_at' => now(),
]);

if (! $noEmail) {
$user->notify(new ProductGranted($product));
}
}

$this->info("Granted to {$user->email}");

if ($dryRun) {
$this->warn('This was a dry run. Run again without --dry-run to apply changes.');
}

return Command::SUCCESS;
}
}
11 changes: 8 additions & 3 deletions app/Filament/Resources/PluginBundleResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use App\Models\PluginBundle;
use App\Models\PluginLicense;
use App\Models\User;
use App\Notifications\BundleGranted;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
Expand Down Expand Up @@ -190,7 +191,7 @@ public static function table(Table $table): Table
$user = User::findOrFail($data['user_id']);
$record->loadMissing('plugins');

$grantedCount = 0;
$grantedPlugins = collect();

foreach ($record->plugins as $plugin) {
$existingLicense = $user->pluginLicenses()
Expand All @@ -211,13 +212,17 @@ public static function table(Table $table): Table
'purchased_at' => now(),
]);

$grantedCount++;
$grantedPlugins->push($plugin);
}

$user->getPluginLicenseKey();

if ($grantedPlugins->isNotEmpty()) {
$user->notify(new BundleGranted($record, $grantedPlugins));
}

Notification::make()
->title("Granted {$grantedCount} plugin license(s) to {$user->name}")
->title("Granted {$grantedPlugins->count()} plugin license(s) to {$user->name}")
->success()
->send();
})
Expand Down
59 changes: 59 additions & 0 deletions app/Filament/Resources/PluginResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
use App\Jobs\ReviewPluginRepository;
use App\Jobs\SyncPlugin;
use App\Models\Plugin;
use App\Models\PluginLicense;
use App\Models\User;
use App\Notifications\PluginGranted;
use App\Notifications\PluginReviewChecksIncomplete;
use Filament\Forms;
use Filament\Forms\Form;
Expand Down Expand Up @@ -272,6 +275,62 @@ public static function table(Table $table): Table
->send();
}),

Tables\Actions\Action::make('grantToUser')
->label('Grant to User')
->icon('heroicon-o-gift')
->color('success')
->form([
Forms\Components\Select::make('user_id')
->label('User')
->searchable()
->getSearchResultsUsing(function (string $search): array {
return User::query()
->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%")
->limit(50)
->get()
->mapWithKeys(fn (User $user) => [$user->id => "{$user->name} ({$user->email})"])
->toArray();
})
->required(),
])
->action(function (Plugin $record, array $data): void {
$user = User::findOrFail($data['user_id']);

$existingLicense = $user->pluginLicenses()
->where('plugin_id', $record->id)
->exists();

if ($existingLicense) {
Notification::make()
->title('User already has a license for this plugin')
->warning()
->send();

return;
}

PluginLicense::create([
'user_id' => $user->id,
'plugin_id' => $record->id,
'price_paid' => 0,
'currency' => 'USD',
'is_grandfathered' => true,
'purchased_at' => now(),
]);

$user->getPluginLicenseKey();
$user->notify(new PluginGranted($record));

Notification::make()
->title("Granted '{$record->name}' license to {$user->name}")
->success()
->send();
})
->modalHeading('Grant Plugin to User')
->modalDescription(fn (Plugin $record): string => "Grant '{$record->name}' to a user for free.")
->modalSubmitActionLabel('Grant'),

Tables\Actions\Action::make('runReviewChecks')
->label('Run Review Checks')
->icon('heroicon-o-clipboard-document-check')
Expand Down
2 changes: 2 additions & 0 deletions app/Filament/Resources/PluginResource/Pages/EditPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use App\Jobs\SyncPluginReleases;
use App\Models\PluginLicense;
use App\Models\User;
use App\Notifications\PluginGranted;
use Filament\Actions;
use Filament\Forms;
use Filament\Notifications\Notification;
Expand Down Expand Up @@ -143,6 +144,7 @@ protected function getHeaderActions(): array
]);

$user->getPluginLicenseKey();
$user->notify(new PluginGranted($this->record));

Notification::make()
->title("Granted '{$this->record->name}' license to {$user->name}")
Expand Down
59 changes: 59 additions & 0 deletions app/Filament/Resources/ProductResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
use App\Filament\Resources\ProductResource\Pages;
use App\Filament\Resources\ProductResource\RelationManagers;
use App\Models\Product;
use App\Models\ProductLicense;
use App\Models\User;
use App\Notifications\ProductGranted;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
Expand Down Expand Up @@ -149,6 +153,61 @@ public static function table(Table $table): Table
->url(fn (Product $record) => route('products.show', $record))
->openUrlInNewTab()
->visible(fn (Product $record) => $record->is_active && $record->published_at?->isPast()),

Tables\Actions\Action::make('grantToUser')
->label('Grant to User')
->icon('heroicon-o-gift')
->color('success')
->form([
Forms\Components\Select::make('user_id')
->label('User')
->searchable()
->getSearchResultsUsing(function (string $search): array {
return User::query()
->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%")
->limit(50)
->get()
->mapWithKeys(fn (User $user) => [$user->id => "{$user->name} ({$user->email})"])
->toArray();
})
->required(),
])
->action(function (Product $record, array $data): void {
$user = User::findOrFail($data['user_id']);

$existingLicense = ProductLicense::where('user_id', $user->id)
->where('product_id', $record->id)
->exists();

if ($existingLicense) {
Notification::make()
->title("{$user->name} already has a license for this product")
->warning()
->send();

return;
}

ProductLicense::create([
'user_id' => $user->id,
'product_id' => $record->id,
'price_paid' => 0,
'currency' => 'USD',
'is_comped' => true,
'purchased_at' => now(),
]);

$user->notify(new ProductGranted($record));

Notification::make()
->title("Granted {$record->name} to {$user->name}")
->success()
->send();
})
->modalHeading('Grant Product to User')
->modalDescription(fn (Product $record) => "Grant '{$record->name}' to a user for free.")
->modalSubmitActionLabel('Grant'),
])
->label('More')
->icon('heroicon-m-ellipsis-vertical'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ public function form(Form $form): Form
])
->default('USD'),

Forms\Components\Toggle::make('is_comped')
->label('Comped')
->default(false),

Forms\Components\DateTimePicker::make('purchased_at')
->default(now()),
]);
Expand All @@ -61,6 +65,11 @@ public function table(Table $table): Table
->money('usd', divideBy: 100)
->sortable(),

Tables\Columns\IconColumn::make('is_comped')
->label('Comped')
->boolean()
->sortable(),

Tables\Columns\TextColumn::make('purchased_at')
->label('Purchased')
->dateTime()
Expand All @@ -72,7 +81,8 @@ public function table(Table $table): Table
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
Tables\Filters\TernaryFilter::make('is_comped')
->label('Comped'),
])
->headerActions([
Tables\Actions\CreateAction::make(),
Expand Down
1 change: 1 addition & 0 deletions app/Models/ProductLicense.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ protected function casts(): array
{
return [
'price_paid' => 'integer',
'is_comped' => 'boolean',
'purchased_at' => 'datetime',
];
}
Expand Down
60 changes: 60 additions & 0 deletions app/Notifications/BundleGranted.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace App\Notifications;

use App\Models\PluginBundle;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Collection;

class BundleGranted extends Notification implements ShouldQueue
{
use Queueable;

/**
* @param Collection<int, \App\Models\Plugin> $grantedPlugins
*/
public function __construct(
public PluginBundle $bundle,
public Collection $grantedPlugins,
) {}

/**
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['mail'];
}

public function toMail(object $notifiable): MailMessage
{
$pluginUrls = $this->grantedPlugins->mapWithKeys(function ($plugin) {
$params = $plugin->routeParams();

return [$plugin->id => "https://nativephp.com/plugins/{$params['vendor']}/{$params['package']}"];
})->toArray();

return (new MailMessage)
->subject("You've been granted the {$this->bundle->name} bundle!")
->markdown('mail.bundle-granted', [
'bundle' => $this->bundle,
'grantedPlugins' => $this->grantedPlugins,
'pluginUrls' => $pluginUrls,
]);
}

/**
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [
'bundle_id' => $this->bundle->id,
'bundle_name' => $this->bundle->name,
'granted_plugin_ids' => $this->grantedPlugins->pluck('id')->toArray(),
];
}
}
Loading
Loading