Issues (62)

src/Filament/Pages/NotificationSettings.php (4 issues)

1
<?php
2
3
namespace Usamamuneerchaudhary\Notifier\Filament\Pages;
4
5
use Filament\Forms\Components\Select;
6
use Filament\Forms\Components\TextInput;
7
use Filament\Forms\Components\Toggle;
8
use Filament\Forms\Concerns\InteractsWithForms;
9
use Filament\Notifications\Notification;
10
use Filament\Pages\Page;
11
use Filament\Schemas\Components\Section;
12
use Filament\Schemas\Components\Tabs;
13
use Filament\Schemas\Components\Tabs\Tab;
14
use Filament\Schemas\Components\Utilities\Get;
15
use Filament\Schemas\Schema;
16
use Usamamuneerchaudhary\Notifier\Filament\Widgets\NotificationStatsOverview;
17
use Usamamuneerchaudhary\Notifier\Models\NotificationSetting;
18
use Usamamuneerchaudhary\Notifier\Models\NotificationChannel;
19
20
class NotificationSettings extends Page
21
{
22
    use InteractsWithForms;
0 ignored issues
show
The trait Filament\Forms\Concerns\InteractsWithForms requires some properties which are not provided by Usamamuneerchaudhary\Not...es\NotificationSettings: $timestamp, $map
Loading history...
23
24
    protected static string|null|\BackedEnum $navigationIcon = 'heroicon-o-cog-6-tooth';
0 ignored issues
show
The type BackedEnum was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
25
    protected static string|null|\UnitEnum $navigationGroup = 'Notifier';
0 ignored issues
show
The type UnitEnum was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
26
    protected static ?string $title = 'Notification Settings';
27
    protected static ?string $navigationLabel = 'Settings';
28
    protected static ?int $navigationSort = 1;
29
    protected string $view = 'notifier::pages.settings';
30
31
    public array $data;
32
33
    public function mount(): void
34
    {
35
        $channels = NotificationChannel::all();
36
        $channelsData = [];
37
38
        foreach ($channels as $channel) {
39
            $settings = $channel->settings ?? [];
40
            $channelsData[$channel->type] = array_merge([
41
                'enabled' => $channel->is_active,
42
            ], $settings);
43
        }
44
45
        // Load preferences settings
46
        $preferences = NotificationSetting::get('preferences', config('notifier.settings.preferences', []));
47
        // Load analytics settings
48
        $analytics = NotificationSetting::get('analytics', config('notifier.settings.analytics', []));
49
        // Load rate limiting settings
50
        $rateLimiting = NotificationSetting::get('rate_limiting', config('notifier.settings.rate_limiting', []));
51
52
        $this->form->fill([
0 ignored issues
show
Bug Best Practice introduced by
The property form does not exist on Usamamuneerchaudhary\Not...es\NotificationSettings. Since you implemented __get, consider adding a @property annotation.
Loading history...
53
            'enabled' => NotificationSetting::get('enabled', true),
54
            'queue_name' => NotificationSetting::get('queue_name', 'default'),
55
            'default_channel' => NotificationSetting::get('default_channel', 'email'),
56
            'channels' => $channelsData,
57
            'preferences' => [
58
                'enabled' => $preferences['enabled'] ?? true,
59
                'default_channels' => $preferences['default_channels'] ?? ['email'],
60
                'allow_override' => $preferences['allow_override'] ?? true,
61
            ],
62
            'analytics' => [
63
                'enabled' => $analytics['enabled'] ?? true,
64
                'track_opens' => $analytics['track_opens'] ?? true,
65
                'track_clicks' => $analytics['track_clicks'] ?? true,
66
                'retention_days' => $analytics['retention_days'] ?? 90,
67
            ],
68
            'rate_limiting' => [
69
                'enabled' => $rateLimiting['enabled'] ?? true,
70
                'max_per_minute' => $rateLimiting['max_per_minute'] ?? 60,
71
                'max_per_hour' => $rateLimiting['max_per_hour'] ?? 1000,
72
                'max_per_day' => $rateLimiting['max_per_day'] ?? 10000,
73
            ],
74
        ]);
75
    }
76
77
    public function form(Schema $schema): Schema
78
    {
79
        $channels = NotificationChannel::all();
80
        $tabs = [
81
            Tab::make('General')
82
                ->icon('heroicon-o-cog')
83
                ->schema([
84
                    Toggle::make('enabled')
85
                        ->label('Enable Notifications')
86
                        ->default(true),
87
                    TextInput::make('queue_name')
88
                        ->label('Queue Name')
89
                        ->default('default')
90
                        ->required(),
91
                    Select::make('default_channel')
92
                        ->label('Default Channel')
93
                        ->options(NotificationChannel::pluck('title', 'type')->toArray())
94
                        ->required(),
95
                ]),
96
            Tab::make('Preferences')
97
                ->icon('heroicon-o-user-circle')
98
                ->schema([
99
                    Section::make('User Preferences')
100
                        ->description('Configure default user notification preferences')
101
                        ->schema([
102
                            Toggle::make('preferences.enabled')
103
                                ->label('Enable User Preferences')
104
                                ->default(true),
105
                            Select::make('preferences.default_channels')
106
                                ->label('Default Channels')
107
                                ->multiple()
108
                                ->options(NotificationChannel::pluck('title', 'type')->toArray())
109
                                ->default(['email'])
110
                                ->required(),
111
                            Toggle::make('preferences.allow_override')
112
                                ->label('Allow Users to Override Preferences')
113
                                ->default(true)
114
                                ->helperText('If enabled, users can customize their notification preferences'),
115
                        ]),
116
                ]),
117
            Tab::make('Analytics')
118
                ->icon('heroicon-o-chart-bar')
119
                ->schema([
120
                    Section::make('Analytics Settings')
121
                        ->description('Configure notification analytics and tracking')
122
                        ->schema([
123
                            Toggle::make('analytics.enabled')
124
                                ->label('Enable Analytics')
125
                                ->default(true),
126
                            Toggle::make('analytics.track_opens')
127
                                ->label('Track Email Opens')
128
                                ->default(true)
129
                                ->visible(fn(Get $get): bool => $get('analytics.enabled')),
130
                            Toggle::make('analytics.track_clicks')
131
                                ->label('Track Link Clicks')
132
                                ->default(true)
133
                                ->visible(fn(Get $get): bool => $get('analytics.enabled')),
134
                            TextInput::make('analytics.retention_days')
135
                                ->label('Data Retention (Days)')
136
                                ->numeric()
137
                                ->default(90)
138
                                ->required()
139
                                ->visible(fn(Get $get): bool => $get('analytics.enabled')),
140
                        ]),
141
                ]),
142
            Tab::make('Rate Limiting')
143
                ->icon('heroicon-o-clock')
144
                ->schema([
145
                    Section::make('Rate Limiting Settings')
146
                        ->description('Configure rate limits for notifications to prevent abuse')
147
                        ->schema([
148
                            Toggle::make('rate_limiting.enabled')
149
                                ->label('Enable Rate Limiting')
150
                                ->default(true),
151
                            TextInput::make('rate_limiting.max_per_minute')
152
                                ->label('Max Per Minute')
153
                                ->numeric()
154
                                ->default(60)
155
                                ->required()
156
                                ->visible(fn(Get $get): bool => $get('rate_limiting.enabled')),
157
                            TextInput::make('rate_limiting.max_per_hour')
158
                                ->label('Max Per Hour')
159
                                ->numeric()
160
                                ->default(1000)
161
                                ->required()
162
                                ->visible(fn(Get $get): bool => $get('rate_limiting.enabled')),
163
                            TextInput::make('rate_limiting.max_per_day')
164
                                ->label('Max Per Day')
165
                                ->numeric()
166
                                ->default(10000)
167
                                ->required()
168
                                ->visible(fn(Get $get): bool => $get('rate_limiting.enabled')),
169
                        ]),
170
                ]),
171
        ];
172
173
        // Dynamically create tabs for each channel
174
        foreach ($channels as $channel) {
175
            $tabs[] = Tab::make($channel->title)
176
                ->icon($channel->icon ?? 'heroicon-o-bell')
177
                ->schema(self::getChannelSchema($channel));
178
        }
179
180
        return $schema
181
            ->schema([
182
                Tabs::make('Settings')
183
                    ->tabs($tabs)
184
                    ->columnSpanFull()
185
            ])
186
            ->statePath('data');
187
    }
188
189
    protected static function getChannelSchema(NotificationChannel $channel): array
190
    {
191
        $schema = [
192
            Toggle::make("channels.{$channel->type}.enabled")
193
                ->label("Enable {$channel->title}"),
194
        ];
195
196
        $fields = match ($channel->type) {
197
            'email' => [
198
                TextInput::make("channels.{$channel->type}.from_address")
199
                    ->label('From Address')
200
                    ->email()
201
                    ->required()
202
                    ->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")),
203
                TextInput::make("channels.{$channel->type}.from_name")
204
                    ->label('From Name')
205
                    ->required()
206
                    ->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")),
207
            ],
208
            'slack' => [
209
                TextInput::make("channels.{$channel->type}.webhook_url")
210
                    ->label('Webhook URL')
211
                    ->url()
212
                    ->required()
213
                    ->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")),
214
                TextInput::make("channels.{$channel->type}.channel")
215
                    ->label('Slack Channel')
216
                    ->default('#notifications')
217
                    ->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")),
218
                TextInput::make("channels.{$channel->type}.username")
219
                    ->label('Bot Username')
220
                    ->default('Notification Bot')
221
                    ->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")),
222
            ],
223
            'sms' => [
224
                Select::make("channels.{$channel->type}.provider")
225
                    ->label('SMS Provider')
226
                    ->options([
227
                        'twilio' => 'Twilio',
228
                        'vonage' => 'Vonage (Nexmo)',
229
                        'generic' => 'Generic API',
230
                    ])
231
                    ->required()
232
                    ->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")),
233
                TextInput::make("channels.{$channel->type}.twilio_account_sid")
234
                    ->label('Twilio Account SID')
235
                    ->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled") && $get("channels.{$channel->type}.provider") === 'twilio'),
236
                TextInput::make("channels.{$channel->type}.twilio_auth_token")
237
                    ->label('Twilio Auth Token')
238
                    ->password()
239
                    ->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled") && $get("channels.{$channel->type}.provider") === 'twilio'),
240
                TextInput::make("channels.{$channel->type}.twilio_phone_number")
241
                    ->label('Twilio Phone Number')
242
                    ->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled") && $get("channels.{$channel->type}.provider") === 'twilio'),
243
                TextInput::make("channels.{$channel->type}.api_url")
244
                    ->label('API URL')
245
                    ->url()
246
                    ->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled") && $get("channels.{$channel->type}.provider") === 'generic'),
247
                TextInput::make("channels.{$channel->type}.api_key")
248
                    ->label('API Key')
249
                    ->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled") && ($get("channels.{$channel->type}.provider") === 'generic' || $get("channels.{$channel->type}.provider") === 'vonage')),
250
                TextInput::make("channels.{$channel->type}.api_secret")
251
                    ->label('API Secret')
252
                    ->password()
253
                    ->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled") && ($get("channels.{$channel->type}.provider") === 'generic' || $get("channels.{$channel->type}.provider") === 'vonage')),
254
            ],
255
            'push' => [
256
                TextInput::make("channels.{$channel->type}.firebase_server_key")
257
                    ->label('Firebase Server Key')
258
                    ->password()
259
                    ->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")),
260
                TextInput::make("channels.{$channel->type}.firebase_project_id")
261
                    ->label('Firebase Project ID')
262
                    ->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")),
263
            ],
264
            'discord' => [
265
                TextInput::make("channels.{$channel->type}.webhook_url")
266
                    ->label('Discord Webhook URL')
267
                    ->url()
268
                    ->required()
269
                    ->helperText('Get this from your Discord server settings > Integrations > Webhooks')
270
                    ->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")),
271
                TextInput::make("channels.{$channel->type}.username")
272
                    ->label('Bot Username')
273
                    ->helperText('Optional: Custom username for the webhook')
274
                    ->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")),
275
                TextInput::make("channels.{$channel->type}.avatar_url")
276
                    ->label('Avatar URL')
277
                    ->url()
278
                    ->helperText('Optional: URL for the webhook avatar')
279
                    ->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")),
280
                TextInput::make("channels.{$channel->type}.color")
281
                    ->label('Embed Color')
282
                    ->helperText('Optional: Decimal color code for embed (e.g., 3447003 for blue)')
283
                    ->numeric()
284
                    ->visible(fn(Get $get): bool => $get("channels.{$channel->type}.enabled")),
285
            ],
286
            default => [],
287
        };
288
289
        return array_merge($schema, $fields);
290
    }
291
292
    public function save(): void
293
    {
294
        $settings = $this->data;
295
296
        NotificationSetting::set('enabled', $settings['enabled'] ?? true, 'general');
297
        NotificationSetting::set('queue_name', $settings['queue_name'] ?? 'default', 'general');
298
        NotificationSetting::set('default_channel', $settings['default_channel'] ?? 'email', 'general');
299
300
        // Save preferences settings
301
        if (isset($settings['preferences'])) {
302
            NotificationSetting::set('preferences', $settings['preferences'], 'preferences');
303
        }
304
305
        // Save analytics settings
306
        if (isset($settings['analytics'])) {
307
            NotificationSetting::set('analytics', $settings['analytics'], 'analytics');
308
        }
309
310
        // Save rate limiting settings
311
        if (isset($settings['rate_limiting'])) {
312
            NotificationSetting::set('rate_limiting', $settings['rate_limiting'], 'rate_limiting');
313
        }
314
315
        // Save channel settings to channel records
316
        if (isset($settings['channels']) && is_array($settings['channels'])) {
317
            foreach ($settings['channels'] as $channelType => $channelData) {
318
                $channel = NotificationChannel::where('type', $channelType)->first();
319
320
                if ($channel) {
321
                    $isActive = $channelData['enabled'] ?? false;
322
                    unset($channelData['enabled']);
323
324
                    $channel->is_active = $isActive;
325
326
                    $existingSettings = $channel->settings ?? [];
327
                    $channel->settings = array_merge($existingSettings, $channelData);
328
329
                    $channel->save();
330
                }
331
            }
332
        }
333
334
        Notification::make()
335
            ->title('Settings saved successfully')
336
            ->success()
337
            ->send();
338
    }
339
340
    protected function getHeaderWidgets(): array
341
    {
342
        return [
343
            NotificationStatsOverview::class,
344
        ];
345
    }
346
}
347