Skip to content

Commit 61d436a

Browse files
simonhampclaude
andcommitted
Add email notifications and grant UI for all Filament resources
- Add PluginGranted and BundleGranted notifications with markdown templates - Add "Grant to User" action to PluginResource index page - Send email notifications from all Filament grant actions - Use Blade markdown templates for proper button and link rendering Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4c8aa46 commit 61d436a

9 files changed

Lines changed: 211 additions & 6 deletions

File tree

app/Filament/Resources/PluginBundleResource.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use App\Models\PluginBundle;
1010
use App\Models\PluginLicense;
1111
use App\Models\User;
12+
use App\Notifications\BundleGranted;
1213
use Filament\Forms;
1314
use Filament\Forms\Form;
1415
use Filament\Notifications\Notification;
@@ -190,7 +191,7 @@ public static function table(Table $table): Table
190191
$user = User::findOrFail($data['user_id']);
191192
$record->loadMissing('plugins');
192193

193-
$grantedCount = 0;
194+
$grantedPlugins = collect();
194195

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

214-
$grantedCount++;
215+
$grantedPlugins->push($plugin);
215216
}
216217

217218
$user->getPluginLicenseKey();
218219

220+
if ($grantedPlugins->isNotEmpty()) {
221+
$user->notify(new BundleGranted($record, $grantedPlugins));
222+
}
223+
219224
Notification::make()
220-
->title("Granted {$grantedCount} plugin license(s) to {$user->name}")
225+
->title("Granted {$grantedPlugins->count()} plugin license(s) to {$user->name}")
221226
->success()
222227
->send();
223228
})

app/Filament/Resources/PluginResource.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
use App\Jobs\ReviewPluginRepository;
1111
use App\Jobs\SyncPlugin;
1212
use App\Models\Plugin;
13+
use App\Models\PluginLicense;
14+
use App\Models\User;
15+
use App\Notifications\PluginGranted;
1316
use App\Notifications\PluginReviewChecksIncomplete;
1417
use Filament\Forms;
1518
use Filament\Forms\Form;
@@ -272,6 +275,62 @@ public static function table(Table $table): Table
272275
->send();
273276
}),
274277

278+
Tables\Actions\Action::make('grantToUser')
279+
->label('Grant to User')
280+
->icon('heroicon-o-gift')
281+
->color('success')
282+
->form([
283+
Forms\Components\Select::make('user_id')
284+
->label('User')
285+
->searchable()
286+
->getSearchResultsUsing(function (string $search): array {
287+
return User::query()
288+
->where('name', 'like', "%{$search}%")
289+
->orWhere('email', 'like', "%{$search}%")
290+
->limit(50)
291+
->get()
292+
->mapWithKeys(fn (User $user) => [$user->id => "{$user->name} ({$user->email})"])
293+
->toArray();
294+
})
295+
->required(),
296+
])
297+
->action(function (Plugin $record, array $data): void {
298+
$user = User::findOrFail($data['user_id']);
299+
300+
$existingLicense = $user->pluginLicenses()
301+
->where('plugin_id', $record->id)
302+
->exists();
303+
304+
if ($existingLicense) {
305+
Notification::make()
306+
->title('User already has a license for this plugin')
307+
->warning()
308+
->send();
309+
310+
return;
311+
}
312+
313+
PluginLicense::create([
314+
'user_id' => $user->id,
315+
'plugin_id' => $record->id,
316+
'price_paid' => 0,
317+
'currency' => 'USD',
318+
'is_grandfathered' => true,
319+
'purchased_at' => now(),
320+
]);
321+
322+
$user->getPluginLicenseKey();
323+
$user->notify(new PluginGranted($record));
324+
325+
Notification::make()
326+
->title("Granted '{$record->name}' license to {$user->name}")
327+
->success()
328+
->send();
329+
})
330+
->modalHeading('Grant Plugin to User')
331+
->modalDescription(fn (Plugin $record): string => "Grant '{$record->name}' to a user for free.")
332+
->modalSubmitActionLabel('Grant'),
333+
275334
Tables\Actions\Action::make('runReviewChecks')
276335
->label('Run Review Checks')
277336
->icon('heroicon-o-clipboard-document-check')

app/Filament/Resources/PluginResource/Pages/EditPlugin.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use App\Jobs\SyncPluginReleases;
1111
use App\Models\PluginLicense;
1212
use App\Models\User;
13+
use App\Notifications\PluginGranted;
1314
use Filament\Actions;
1415
use Filament\Forms;
1516
use Filament\Notifications\Notification;
@@ -143,6 +144,7 @@ protected function getHeaderActions(): array
143144
]);
144145

145146
$user->getPluginLicenseKey();
147+
$user->notify(new PluginGranted($this->record));
146148

147149
Notification::make()
148150
->title("Granted '{$this->record->name}' license to {$user->name}")

app/Filament/Resources/ProductResource.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use App\Models\Product;
88
use App\Models\ProductLicense;
99
use App\Models\User;
10+
use App\Notifications\ProductGranted;
1011
use Filament\Forms;
1112
use Filament\Forms\Form;
1213
use Filament\Notifications\Notification;
@@ -197,6 +198,8 @@ public static function table(Table $table): Table
197198
'purchased_at' => now(),
198199
]);
199200

201+
$user->notify(new ProductGranted($record));
202+
200203
Notification::make()
201204
->title("Granted {$record->name} to {$user->name}")
202205
->success()
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace App\Notifications;
4+
5+
use App\Models\PluginBundle;
6+
use Illuminate\Bus\Queueable;
7+
use Illuminate\Contracts\Queue\ShouldQueue;
8+
use Illuminate\Notifications\Messages\MailMessage;
9+
use Illuminate\Notifications\Notification;
10+
use Illuminate\Support\Collection;
11+
12+
class BundleGranted extends Notification implements ShouldQueue
13+
{
14+
use Queueable;
15+
16+
/**
17+
* @param Collection<int, \App\Models\Plugin> $grantedPlugins
18+
*/
19+
public function __construct(
20+
public PluginBundle $bundle,
21+
public Collection $grantedPlugins,
22+
) {}
23+
24+
/**
25+
* @return array<int, string>
26+
*/
27+
public function via(object $notifiable): array
28+
{
29+
return ['mail'];
30+
}
31+
32+
public function toMail(object $notifiable): MailMessage
33+
{
34+
$pluginUrls = $this->grantedPlugins->mapWithKeys(function ($plugin) {
35+
$params = $plugin->routeParams();
36+
37+
return [$plugin->id => "https://nativephp.com/plugins/{$params['vendor']}/{$params['package']}"];
38+
})->toArray();
39+
40+
return (new MailMessage)
41+
->subject("You've been granted the {$this->bundle->name} bundle!")
42+
->markdown('mail.bundle-granted', [
43+
'bundle' => $this->bundle,
44+
'grantedPlugins' => $this->grantedPlugins,
45+
'pluginUrls' => $pluginUrls,
46+
]);
47+
}
48+
49+
/**
50+
* @return array<string, mixed>
51+
*/
52+
public function toArray(object $notifiable): array
53+
{
54+
return [
55+
'bundle_id' => $this->bundle->id,
56+
'bundle_name' => $this->bundle->name,
57+
'granted_plugin_ids' => $this->grantedPlugins->pluck('id')->toArray(),
58+
];
59+
}
60+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace App\Notifications;
4+
5+
use App\Models\Plugin;
6+
use Illuminate\Bus\Queueable;
7+
use Illuminate\Contracts\Queue\ShouldQueue;
8+
use Illuminate\Notifications\Messages\MailMessage;
9+
use Illuminate\Notifications\Notification;
10+
11+
class PluginGranted extends Notification implements ShouldQueue
12+
{
13+
use Queueable;
14+
15+
public function __construct(
16+
public Plugin $plugin,
17+
) {}
18+
19+
/**
20+
* @return array<int, string>
21+
*/
22+
public function via(object $notifiable): array
23+
{
24+
return ['mail'];
25+
}
26+
27+
public function toMail(object $notifiable): MailMessage
28+
{
29+
$parts = explode('/', $this->plugin->name ?? '');
30+
$vendor = $parts[0] ?? '';
31+
$package = $parts[1] ?? '';
32+
33+
$pluginUrl = "https://nativephp.com/plugins/{$vendor}/{$package}";
34+
35+
return (new MailMessage)
36+
->subject("You've been granted access to {$this->plugin->name}!")
37+
->greeting('Great news!')
38+
->line("You've been granted access to **{$this->plugin->name}**.")
39+
->action('Check it out', $pluginUrl)
40+
->line('Thank you for being a NativePHP customer!');
41+
}
42+
43+
/**
44+
* @return array<string, mixed>
45+
*/
46+
public function toArray(object $notifiable): array
47+
{
48+
return [
49+
'plugin_id' => $this->plugin->id,
50+
'plugin_name' => $this->plugin->name,
51+
];
52+
}
53+
}

app/Notifications/ProductGranted.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ public function toMail(object $notifiable): MailMessage
2828
{
2929
return (new MailMessage)
3030
->subject("You've been granted access to {$this->product->name}!")
31-
->greeting('Great news!')
32-
->line("You've been granted access to **{$this->product->name}**.")
33-
->line('Thank you for being a NativePHP customer!');
31+
->markdown('mail.product-granted', [
32+
'product' => $this->product,
33+
'url' => route('products.show', $this->product),
34+
]);
3435
}
3536

3637
/**
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<x-mail::message>
2+
# Great news!
3+
4+
You've been granted access to the **{{ $bundle->name }}** bundle, which includes the following plugins:
5+
6+
@foreach ($grantedPlugins as $plugin)
7+
- [{{ $plugin->name }}]({{ $pluginUrls[$plugin->id] }})
8+
@endforeach
9+
10+
Thank you for being a NativePHP customer!
11+
</x-mail::message>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<x-mail::message>
2+
# Great news!
3+
4+
You've been granted access to **{{ $product->name }}**.
5+
6+
<x-mail::button :url="$url">
7+
Check it out
8+
</x-mail::button>
9+
10+
Thank you for being a NativePHP customer!
11+
</x-mail::message>

0 commit comments

Comments
 (0)