From c7eeefc320fc8ab1d32db5a965c871b772bc027e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Lopes?= Date: Mon, 2 Feb 2026 08:51:15 -0300 Subject: [PATCH] feat(notification): ignore to send notification globally --- README.md | 23 +++++++++++++++++++++- src/DeviceCreator.php | 19 ++++++++++++++++++ src/Listeners/SaveUserDevice.php | 21 ++++++++++++++++---- tests/Feature/UserDevicesTest.php | 32 +++++++++++++++++++++++++++++++ tests/Unit/DeviceCreatorTest.php | 10 ++++++++++ 5 files changed, 100 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index eebf39a..2ae37d6 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,11 @@ class AppServiceProvider extends ServiceProvider // Customize user agent generation DeviceCreator::userAgentUsing(fn ($userAgent) => substr($userAgent, 0, 255)); + // Control when to send new login notifications (e.g. disable in local/staging) + DeviceCreator::shouldSendNotificationUsing(function ($user, $device) { + return ! app()->environment('local'); + }); + // Customize the notification email NewLoginDeviceNotification::toMailUsing(function ($notifiable, $device) { $expire = Config::get('auth.verification.expire', 60); @@ -130,11 +135,26 @@ use UserDevices\DeviceCreator; DeviceCreator::ignoreNotification(); ``` +To control notifications globally (e.g. disable in local/staging, or custom logic per user/device): + +```php +use UserDevices\DeviceCreator; + +// Disable notifications entirely +DeviceCreator::shouldSendNotificationUsing(fn () => false); + +// Custom logic (e.g. skip for admins) +DeviceCreator::shouldSendNotificationUsing(fn ($user, $device) => ! $user->isAdmin()); + +// Only send in production +DeviceCreator::shouldSendNotificationUsing(fn ($user, $device) => app()->environment('production')); +``` + This will: - Create or update the device record (IP + user agent) - Update last activity timestamp -- Send a notification email on **first login** from that device (unless `ignoreNotification()` was called) +- Send a notification email on **first login** from that device (unless `ignoreNotification()` was called or `shouldSendNotificationUsing()` returns false) #### 3. Block Device Route @@ -204,6 +224,7 @@ $user->sendNewLoginDeviceNotification($device); DeviceCreator::useUserModel(string $model): void DeviceCreator::useUserDeviceModel(string $model): void DeviceCreator::userAgentUsing(Closure $callback): void +DeviceCreator::shouldSendNotificationUsing(Closure $callback): void // (user, device) => bool // Methods DeviceCreator::ignoreNotification(): void // Add to Context to skip notification for current request diff --git a/src/DeviceCreator.php b/src/DeviceCreator.php index c407dff..952ee88 100644 --- a/src/DeviceCreator.php +++ b/src/DeviceCreator.php @@ -24,6 +24,15 @@ class DeviceCreator */ public static string $userModel = 'App\\Models\\User'; + /** + * A custom callback to determine whether to send the new login device notification. + * + * When set, this closure receives the user and device and returns a boolean. + * If it returns false, the notification is not sent. When null, notifications + * are sent by default (unless ignoreNotification() was called for the request). + */ + public static ?Closure $shouldSendNotification = null; + /** * The fully qualified class name of the user device model. * @@ -66,4 +75,14 @@ public static function useUserDeviceModel(string $model): void { static::$userDeviceModel = $model; } + + /** + * Set a callback to determine whether to send the new login device notification. + * + * The callback receives the user and device as arguments and should return a boolean. + */ + public static function shouldSendNotificationUsing(Closure $callback): void + { + static::$shouldSendNotification = $callback; + } } diff --git a/src/Listeners/SaveUserDevice.php b/src/Listeners/SaveUserDevice.php index a2000fa..ad1fcfe 100644 --- a/src/Listeners/SaveUserDevice.php +++ b/src/Listeners/SaveUserDevice.php @@ -7,6 +7,7 @@ use Illuminate\Support\Facades\Context; use Illuminate\Support\Facades\Request; use UserDevices\DeviceCreator; +use UserDevices\Models\UserDevice; use UserDevices\Traits\HasUserDevices; class SaveUserDevice @@ -23,21 +24,33 @@ public function handle(Authenticated $event): void return; } - $ignore = Context::get('user_devices.ignore_notification', false); - $device = $user->userDevices()->firstOrNew([ 'ip_address' => Request::ip(), 'user_agent' => with(Request::userAgent(), DeviceCreator::$userAgent), ]); - tap($device->exists, function ($exists) use ($user, $device, $ignore) { + tap($device->exists, function ($exists) use ($user, $device) { $device->fill([ 'last_activity' => Carbon::now()->timestamp, ])->save(); - if (! $exists && ! $ignore) { + $shouldSend = $this->shouldSendNotification($user, $device); + + if (! $exists && $shouldSend) { $user->sendNewLoginDeviceNotification($device); } }); } + + /** + * Determine whether to send the new login device notification. + */ + private function shouldSendNotification(mixed $user, UserDevice $device): bool + { + if (is_callable(DeviceCreator::$shouldSendNotification)) { + return (DeviceCreator::$shouldSendNotification)($user, $device); + } + + return ! Context::get('user_devices.ignore_notification', false); + } } diff --git a/tests/Feature/UserDevicesTest.php b/tests/Feature/UserDevicesTest.php index 65d683c..27245a9 100644 --- a/tests/Feature/UserDevicesTest.php +++ b/tests/Feature/UserDevicesTest.php @@ -45,6 +45,38 @@ Notification::assertSentTo($user, NewLoginDeviceNotification::class); }); +test('it should not send notification when shouldSendNotificationUsing returns false', function () { + Notification::fake(); + + $user = User::factory()->create(); + + DeviceCreator::shouldSendNotificationUsing(fn () => false); + + $this->actingAs($user) + ->withHeader('User-Agent', 'Mozilla/5.0 Yet Another New Device') + ->get('/dashboard'); + + Notification::assertNothingSent(); + + DeviceCreator::$shouldSendNotification = null; +}); + +test('it should send notification when shouldSendNotificationUsing returns true', function () { + Notification::fake(); + + $user = User::factory()->create(); + + DeviceCreator::shouldSendNotificationUsing(fn () => true); + + $this->actingAs($user) + ->withHeader('User-Agent', 'Mozilla/5.0 Custom Callback Device') + ->get('/dashboard'); + + Notification::assertSentTo($user, NewLoginDeviceNotification::class); + + DeviceCreator::$shouldSendNotification = null; +}); + test('it should block device when accessing signed URL', function () { $user = User::factory()->create(); diff --git a/tests/Unit/DeviceCreatorTest.php b/tests/Unit/DeviceCreatorTest.php index 19f9ba2..0d27981 100644 --- a/tests/Unit/DeviceCreatorTest.php +++ b/tests/Unit/DeviceCreatorTest.php @@ -46,3 +46,13 @@ expect(Context::get('user_devices.ignore_notification'))->toBeTrue(); }); + +test('it should set should send notification callback correctly', function () { + $callback = fn () => true; + + DeviceCreator::shouldSendNotificationUsing($callback); + + expect(DeviceCreator::$shouldSendNotification)->toBe($callback); + + DeviceCreator::$shouldSendNotification = null; +});