From 0d0818baf3ad32f481e7b89537c2bcd9a7a6a779 Mon Sep 17 00:00:00 2001 From: kusowl Date: Mon, 9 Feb 2026 12:13:27 +0530 Subject: [PATCH] feature(send push notification): send push notification to broker's followers --- .env.example | 3 ++ ...dDealCreatedNotificationCustomerAction.php | 11 +++-- app/Http/Controllers/Admin/DealController.php | 5 +- .../PushSubscriptionController.php | 5 +- app/Http/Requests/PushSubscriptionRequest.php | 2 +- app/Http/Resources/DealResource.php | 2 +- app/Models/Follow.php | 1 - app/Models/User.php | 3 +- app/Notifications/NewDealNotification.php | 8 ++-- config/webpush.php | 44 +++++++++++++++++ ...060528_create_push_subscriptions_table.php | 4 +- resources/js/interaction.js | 7 +-- resources/js/webpush/enable-push.js | 48 +++++++++++-------- routes/web.php | 14 ++++++ storage/logs/.gitignore | 0 15 files changed, 114 insertions(+), 43 deletions(-) create mode 100644 config/webpush.php mode change 100644 => 100755 storage/logs/.gitignore diff --git a/.env.example b/.env.example index e92826b..14ceede 100644 --- a/.env.example +++ b/.env.example @@ -73,3 +73,6 @@ OTP_LIFESPAN=10 VAPID_PUBLIC_KEY= VAPID_PRIVATE_KEY= + +# Same as the VAPID_PUBLIC_KEY +VITE_VAPID_PUBLIC_KEY=BOBjjU2E-h8pDCV13yPwvMDR_WZwEhFmQY90gr16oJ5L1mpJ5qc7-0WzXcD1Z9D0Ozz0cLZxTe0_7nnDK3VFMP4 diff --git a/app/Actions/SendDealCreatedNotificationCustomerAction.php b/app/Actions/SendDealCreatedNotificationCustomerAction.php index 2ed239c..318ae95 100644 --- a/app/Actions/SendDealCreatedNotificationCustomerAction.php +++ b/app/Actions/SendDealCreatedNotificationCustomerAction.php @@ -5,7 +5,6 @@ use App\Models\Broker; use App\Models\Customer; use App\Models\Deal; -use App\Models\Follow; use App\Notifications\NewDealNotification; final readonly class SendDealCreatedNotificationCustomerAction @@ -17,10 +16,12 @@ public function execute(Deal $deal): void */ $broker = $deal->broker->type; $followers = $broker->followers()->with('user')->get(); - $followers->map(function (Customer $customer) use ($deal) { - \Log::info('dump', [$customer]); - \Log::info("Sending notification to {$customer->user->name}"); - $customer->user->notify(new NewDealNotification($deal)); + $followers->map(function (Customer $follower) use ($deal) { + $user = $follower->user; + + \Log::info("Sending notification to {$follower->user->name}", [$deal, $follower->user]); + + $user->notifyNow(new NewDealNotification($deal)); }); } } diff --git a/app/Http/Controllers/Admin/DealController.php b/app/Http/Controllers/Admin/DealController.php index 6a719f9..005d81a 100644 --- a/app/Http/Controllers/Admin/DealController.php +++ b/app/Http/Controllers/Admin/DealController.php @@ -19,11 +19,12 @@ public function index() public function approve(Deal $deal, SendDealCreatedNotificationCustomerAction $notificationCustomerAction) { try { - \DB::transaction(function () use ($notificationCustomerAction, $deal) { + \DB::transaction(function () use ($deal) { $deal->active = true; $deal->save(); - $notificationCustomerAction->execute($deal); }); + $notificationCustomerAction->execute($deal); + return back()->with('success', 'Deal activated successfully.'); } catch (\Throwable $e) { Log::error('Deal activation Failed: ', [$e->getMessage(), $e->getTrace()]); diff --git a/app/Http/Controllers/PushSubscriptionController.php b/app/Http/Controllers/PushSubscriptionController.php index 4199c7c..6a5874e 100644 --- a/app/Http/Controllers/PushSubscriptionController.php +++ b/app/Http/Controllers/PushSubscriptionController.php @@ -3,14 +3,15 @@ namespace App\Http\Controllers; use App\Http\Requests\PushSubscriptionRequest; +use App\Models\User; +use Illuminate\Container\Attributes\CurrentUser; use Illuminate\Http\JsonResponse; class PushSubscriptionController extends Controller { - public function __invoke(PushSubscriptionRequest $request): JsonResponse + public function __invoke(#[CurrentUser] User $user, PushSubscriptionRequest $request): JsonResponse { $data = $request->validated(); - $user = $request->user(); $user->updatePushSubscription($data['endpoint'], $data['keys']['p256dh'], $data['keys']['auth']); return response()->json(['message' => 'Push subscription updated successfully.']); diff --git a/app/Http/Requests/PushSubscriptionRequest.php b/app/Http/Requests/PushSubscriptionRequest.php index f334f9e..db9d2c3 100644 --- a/app/Http/Requests/PushSubscriptionRequest.php +++ b/app/Http/Requests/PushSubscriptionRequest.php @@ -22,7 +22,7 @@ public function authorize(): bool public function rules(): array { return [ - 'endpoint' => 'required|string|max:255', + 'endpoint' => 'required|string|max:500', 'keys.auth' => 'required|string|max:255', 'keys.p256dh' => 'required|string|max:255', ]; diff --git a/app/Http/Resources/DealResource.php b/app/Http/Resources/DealResource.php index c7f63a3..220f4e7 100644 --- a/app/Http/Resources/DealResource.php +++ b/app/Http/Resources/DealResource.php @@ -27,7 +27,7 @@ public function toArray(Request $request): array 'totalRedirection' => $this->total_redirection, 'isLiked' => $this->is_liked, 'isFavorite' => $this->is_favorite, - 'isFollowed' => $this->is_followed + 'isFollowed' => $this->is_followed, ]; } } diff --git a/app/Models/Follow.php b/app/Models/Follow.php index 80e6c24..7370a73 100644 --- a/app/Models/Follow.php +++ b/app/Models/Follow.php @@ -2,7 +2,6 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\Pivot; diff --git a/app/Models/User.php b/app/Models/User.php index c9f955f..2bf223d 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -11,6 +11,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use NotificationChannels\WebPush\HasPushSubscriptions; + /** * @property int $id * @property string $name @@ -62,7 +63,7 @@ class User extends Authenticatable { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable, HasPushSubscriptions; + use HasFactory, HasPushSubscriptions, Notifiable; /** * The attributes that are mass assignable. diff --git a/app/Notifications/NewDealNotification.php b/app/Notifications/NewDealNotification.php index 7ad8b18..fa57354 100644 --- a/app/Notifications/NewDealNotification.php +++ b/app/Notifications/NewDealNotification.php @@ -3,15 +3,13 @@ namespace App\Notifications; use App\Models\Deal; -use Illuminate\Bus\Queueable; use Illuminate\Notifications\Notification; +use Illuminate\Support\Str; use NotificationChannels\WebPush\WebPushChannel; use NotificationChannels\WebPush\WebPushMessage; class NewDealNotification extends Notification { - use Queueable; - /** * Create a new notification instance. */ @@ -32,9 +30,11 @@ public function via(object $notifiable): array public function toWebPush($notifiable, $notification): WebPushMessage { + \Log::info('Building WebPush for user: '.$notifiable->id); + return (new WebPushMessage) ->title("New deal from {$this->deal->broker->name}") - ->body("Check out this deal: {$this->deal->title}") + ->body('Check out this deal:'.Str::limit($this->deal->title, 30, '...')) ->action('View deal', route('explore', ['show' => $this->deal->id])); } } diff --git a/config/webpush.php b/config/webpush.php new file mode 100644 index 0000000..db63ca5 --- /dev/null +++ b/config/webpush.php @@ -0,0 +1,44 @@ + [ + 'subject' => env('VAPID_SUBJECT'), + 'public_key' => env('VAPID_PUBLIC_KEY'), + 'private_key' => env('VAPID_PRIVATE_KEY'), + 'pem_file' => env('VAPID_PEM_FILE'), + ], + + /** + * This is model that will be used to for push subscriptions. + */ + 'model' => \NotificationChannels\WebPush\PushSubscription::class, + + /** + * This is the name of the table that will be created by the migration and + * used by the PushSubscription model shipped with this package. + */ + 'table_name' => env('WEBPUSH_DB_TABLE', 'push_subscriptions'), + + /** + * This is the database connection that will be used by the migration and + * the PushSubscription model shipped with this package. + */ + 'database_connection' => env('WEBPUSH_DB_CONNECTION', env('DB_CONNECTION', 'mysql')), + + /** + * The Guzzle client options used by Minishlink\WebPush. + */ + 'client_options' => [], + + /** + * The automatic padding in bytes used by Minishlink\WebPush. + * Set to false to support Firefox Android with v1 endpoint. + */ + 'automatic_padding' => env('WEBPUSH_AUTOMATIC_PADDING', true), + +]; diff --git a/database/migrations/2026_02_06_060528_create_push_subscriptions_table.php b/database/migrations/2026_02_06_060528_create_push_subscriptions_table.php index 567a743..e5b0cbb 100644 --- a/database/migrations/2026_02_06_060528_create_push_subscriptions_table.php +++ b/database/migrations/2026_02_06_060528_create_push_subscriptions_table.php @@ -1,8 +1,8 @@ { * Request permission and trigger subscription */ const initPush = async () => { - // Request permission + if (Notification.permission === 'denied') { + alert('You have blocked notifications. Please click the Lock icon in your address bar to enable them.'); + return; + } + + // 2. Request permission (this will only pop up if status is 'default') const permission = await Notification.requestPermission(); if (permission !== 'granted') { - console.warn('Notification permission denied.'); + console.warn('Notification permission denied or dismissed.'); return; } @@ -39,27 +44,28 @@ const subscribeUser = async () => { // Check if a subscription already exists let pushSubscription = await registration.pushManager.getSubscription(); - - if (!pushSubscription) { - const subscribeOptions = { - userVisibleOnly: true, - applicationServerKey: urlBase64ToUint8Array( - import.meta.env.VITE_VAPID_PUBLIC_KEY - ) - }; - - try { - pushSubscription = await registration.pushManager.subscribe(subscribeOptions); - console.log('New Subscription created.'); - } catch (err) { - console.error('Failed to subscribe user:', err); - return; - } - } else { - console.log('User is already subscribed.'); + if (pushSubscription) { + await pushSubscription.unsubscribe(); + console.log('Unsubscribed old record...'); } - // Send subscription to Laravel backend + const subscribeOptions = { + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array( + import.meta.env.VITE_VAPID_PUBLIC_KEY + ) + }; + + try { + pushSubscription = await registration.pushManager.subscribe(subscribeOptions); + console.log('New Subscription created.'); + } catch (err) { + console.error('Failed to subscribe user:', err); + return; + } + + + // Send a subscription to Laravel backend if (pushSubscription) { console.log('Received PushSubscription:', JSON.stringify(pushSubscription)); await storePushSubscription(pushSubscription); diff --git a/routes/web.php b/routes/web.php index 0fec1e6..76e227c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -14,6 +14,20 @@ Route::get('/', HomeController::class)->name('home'); Route::get('/explore', ExplorePageController::class)->name('explore'); Route::post('/contact', ContactController::class)->name('contact'); + +Route::get('/test-openssl', function () { + $res = openssl_pkey_new([ + 'curve_name' => 'prime256v1', + 'private_key_type' => OPENSSL_KEYTYPE_EC, + ]); + + if ($res === false) { + return 'ERROR: '.openssl_error_string(); // likely "error:02001003:system library:fopen:No such process" + } + + return 'SUCCESS: OpenSSL is configured correctly.'; +}); + /** * This routes are accessed by JS XHR requests, and is loaded here cause * we do not want to use sanctum for web requests diff --git a/storage/logs/.gitignore b/storage/logs/.gitignore old mode 100644 new mode 100755