feature(send push notification): send push notification to broker's followers

This commit is contained in:
kusowl 2026-02-09 12:13:27 +05:30
parent 82715973dc
commit 0d0818baf3
15 changed files with 114 additions and 43 deletions

View File

@ -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

View File

@ -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));
});
}
}

View File

@ -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()]);

View File

@ -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.']);

View File

@ -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',
];

View File

@ -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,
];
}
}

View File

@ -2,7 +2,6 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\Pivot;

View File

@ -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.

View File

@ -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]));
}
}

44
config/webpush.php Normal file
View File

@ -0,0 +1,44 @@
<?php
return [
/**
* These are the keys for authentication (VAPID).
* These keys must be safely stored and should not change.
*/
'vapid' => [
'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),
];

View File

@ -1,8 +1,8 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{

View File

@ -71,6 +71,9 @@ export async function favorite(button) {
*/
export async function follow(button, brokerId) {
let isFollowed = button.dataset.followed === 'true';
if (!isFollowed) {
await initSw();
}
try {
button.dataset.followed = isFollowed ? 'false' : 'true';
// Update other buttons
@ -83,9 +86,7 @@ export async function follow(button, brokerId) {
let response = await axios.post(`/api/follow/${brokerId}`);
showToast(response.data.message);
if (!isFollowed) {
initSw();
}
} catch (e) {
button.dataset.followed = isFollowed ? 'true' : 'false';
showToast('Something went wrong!')

View File

@ -20,11 +20,16 @@ export const initSw = async () => {
* 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);

View File

@ -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

0
storage/logs/.gitignore vendored Normal file → Executable file
View File