Laravel 12 web push với minishlink/web-push

Laravel 12 web push với minishlink/web-push

Mình dùng gói https://github.com/web-push-libs/web-push-php để cài đặt 

Tiếp theo mình code :  public/service-worker.js

self.addEventListener('push', function(event) {
    try {
        const payload = event.data ? event.data.json() : {};
        console.log('📩 Push nhận được:', payload);

        const options = {
            body: payload.body || 'Bạn có một thông báo mới!',
            icon: payload.icon || '/icon.png',
            badge: payload.badge || '/icon.png',
            data: payload.data || {}
        };
        if (payload.actions) {
            options.actions = payload.actions;
        }
        console.log('🚀 Trạng thái quyền thông báo:', Notification.permission);

        // self.registration.showNotification(payload.title || '🔥 Thông báo mới!', options)
        //     .catch(error => console.error('❌ Lỗi hiển thị thông báo:', error));
        event.waitUntil(
            self.registration.showNotification(payload.title || '🔥 Thông báo mới!', options)
                .then(() => console.log('✅ Thông báo hiển thị thành công!'))
                .catch(error => console.error('❌ Lỗi hiển thị thông báo:', error))
        );
    } catch (error) {
        console.error('Lỗi trong sự kiện push:', error);
    }
});

self.addEventListener('notificationclick', function(event) {
    event.notification.close(); // Đóng thông báo khi bấm vào

    if (event.action === 'open') {
        event.waitUntil(clients.openWindow(event.notification.data.url || '/'));
    } else if (event.action === 'like') {
        console.log('👍 Người dùng đã thích thông báo.');
    } else if (event.action === 'dislike') {
        console.log('👎 Người dùng không thích thông báo.');
    } else if (event.action === 'share') {
        console.log('📤 Người dùng muốn chia sẻ.');
        // Nếu cần tích hợp chia sẻ, có thể gọi API Web Share
    } else {
        console.log('🖱 Người dùng đã bấm vào thông báo nhưng không chọn action.');
        event.waitUntil(clients.openWindow(event.notification.data.url || '/'));
    }
});

self.addEventListener('install', function(event) {
    console.log('✅ Service Worker Installed!');
    self.skipWaiting();
});

self.addEventListener('activate', function(event) {
    console.log('✅ Service Worker Activated!');
    return self.clients.claim();
});

Ở trang layouts/app.blade :

  <script>
            if ('serviceWorker' in navigator && 'PushManager' in window) {
                // Kiểm tra quyền trước khi đăng ký Push
                if (Notification.permission === "denied") {
                    alert("🚫 Bạn đã chặn thông báo! Hãy vào cài đặt trình duyệt để bật lại.");
                } else if (Notification.permission === "default") {
                    Notification.requestPermission().then(permission => {
                        if (permission === "granted") {
                            registerPush();
                        } else {
                            alert("⚠️ Bạn cần bật thông báo để nhận cập nhật mới.");
                        }
                    });
                } else {
                    registerPush();
                }

                function registerPush() {
                    navigator.serviceWorker.register('/service-worker.js').then(registration => {
                        console.log('✅ Service Worker đăng ký thành công:', registration);

                        return registration.pushManager.subscribe({
                            userVisibleOnly: true,
                            applicationServerKey: '{{ env('WEBPUSH_PUBLIC_KEY') }}'
                        });
                    }).then(subscription => {
                        console.log('✅ Push đăng ký thành công:', subscription);

                        // Gửi subscription về server để lưu
                        fetch('/save-subscription', {
                            method: 'POST',
                            body: JSON.stringify(subscription),
                            headers: {
                                'Content-Type': 'application/json'
                            }
                        });
                    }).catch(error => {
                        console.error('❌ Push đăng ký thất bại:', error);

                        if (error.name === "NotAllowedError") {
                            alert("🚨 Bạn đã chặn thông báo! Hãy vào cài đặt trình duyệt để bật lại.");
                        }
                    });
                }
            }
        </script>

Ở routes/web.php mình thêm 1 url có nhiệm vụ thêm data vào trong database

Route::post('/save-subscription', [\App\Http\Controllers\PushSubscriptionController::class, 'saveSubscription']);

Ta tạo migrate để tạo database Ở trong database/migrations/2025_03_21_070512_create_push_subscriptions_table.php 

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('push_subscriptions', function (Blueprint $table) {
            $table->id();
            $table->string('endpoint')->unique();
            $table->unsignedBigInteger('user_id')->nullable();
            $table->text('public_key');
            $table->text('auth_token');
            $table->index('user_id');
            $table->index('endpoint');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('push_subscriptions');
    }
};

Trong controller : app/Http/Controllers/PushSubscriptionController.php

<?php

namespace App\Http\Controllers;

use Ably\Auth;
use App\Models\PushSubscription;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

class PushSubscriptionController extends Controller
{
    public function saveSubscription(Request $request)
    {
        Log::info('Lấy dữ liệu subscription:', $request->all());

        $data = $request->all();

        $userId = auth()->id() ?? null;

        if (!Cache::has('pushsub.' . $data['endpoint'])) {
            $subscription = Cache::remember('pushsub.' . $data['endpoint'], Carbon::now()->addDays(4), function () use ($data, $userId) {
                return PushSubscription::updateOrCreate(
                    ['endpoint' => $data['endpoint']],
                    [
                        'user_id' => $userId,
                        'public_key' => $data['keys']['p256dh'] ?? null,
                        'auth_token' => $data['keys']['auth'] ?? null,
                    ]
                );
            });

            return response()->json(['success' => true, 'data' => $subscription]);
        }

        return response()->json(['success' => true, 'message' => 'Subscription already exists']);
    }

}

Có nhiệm vụ lưu vào database 

Mình tạo command để gửi web push :

<?php

namespace App\Console\Commands;

use App\Models\PushSubscription;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;

class SendPushNotification extends Command
{
    protected $signature = 'push:send';
    protected $description = 'Gửi thông báo đến tất cả người dùng đã đăng ký Web Push';

    public function handle()
    {
        $auth = [
            'VAPID' => [
                'subject' => 'mailto: <[email protected]>',
                'publicKey' => env('VAPID_PUBLIC_KEY'),
                'privateKey' => env('VAPID_PRIVATE_KEY'),
            ],
        ];

        $webPush = new WebPush($auth);

        $subscriptions = PushSubscription::all();
        foreach ($subscriptions as $sub) {
            $subscription = Subscription::create([
                'endpoint' => $sub->endpoint,
                'publicKey' => $sub->public_key,
                'authToken' => $sub->auth_token,
            ]);

            $actions = [];
            $actionUrl = 1;
            $actionLike = 1;
            // Kiểm tra nếu actionUrl tồn tại và không rỗng, thêm action 'open'
            if (isset($actionUrl) && !empty($actionUrl)) {
                $actions[] = ['action' => 'open', 'title' => '🔗 Xem chi tiết'];
            }

            // Kiểm tra nếu actionLike tồn tại và không rỗng, thêm action 'like'
            if (isset($actionLike) && !empty($actionLike)) {
                $actions[] = ['action' => 'like', 'title' => '👍 Thích'];
            }
            $webPush->queueNotification($subscription, json_encode([
                'title' => '🔥 Thông báo mới!',
                'body' => 'Nhấp vào để xem chi tiết.',
                'icon' => '/logo.png',
                'image' => '123.png',
                'data' => ['url' => 'https://dev.truyenvideo.com'],
            ]));
        }

        foreach ($webPush->flush() as $report) {
            $endpoint = $report->getRequest()->getUri()->__toString();
            $response = $report->getResponse(); // Lấy response từ FCM
            if ($report->isSuccess()) {
                $this->info("✅ Đã gửi: {$endpoint}");
//                $this->info("✅ Đã gửi: {$endpoint}");
            } else {
                PushSubscription::where('endpoint', $endpoint)->delete();
                $this->error("❌ Lỗi gửi: {$endpoint}, lý do: " . $report->getReason());
            }
            // Kiểm tra response chi tiết
//            if ($response) {
//                $statusCode = $response->getStatusCode();
//                $headers = $response->getHeaders();
//                $body = (string) $response->getBody();
//
//                $this->info("📩 FCM Response:");
//                $this->info("🔹 Status: {$statusCode}");
//                $this->info("🔹 Headers: " . json_encode($headers));
//                $this->info("🔹 Body: {$body}");
//            }

        }
    }
}

Còn 1 cách là thêm 

<script src="{{ asset('push-notifications.js?data=1234') }}" />

File push-notifications.js ở trong public :

document.addEventListener("DOMContentLoaded", () => {
    const notificationButton = Object.assign(document.createElement("button"), {
        textContent: "🔔 Bạn cần Bật Thông Báo",
        style: "display:none;position:fixed;bottom:20px;right:20px;padding:10px 15px;background:#FF9800;color:#fff;border:none;border-radius:5px;cursor:pointer;"
    });
    document.body.appendChild(notificationButton);

    const scriptElement = document.querySelector('script[src*="push-notifications.js"]');
    const dataValue = scriptElement ? new URL(scriptElement.src).searchParams.get('data') : null;
    // console.log("Giá trị của data:", dataValue);
    if (!('serviceWorker' in navigator && 'PushManager' in window)) return;
    function registerPush() {
        fetch('/vapid-public-key')
            .then(res => res.json())
            .then(({ key }) => navigator.serviceWorker.register('/service-worker.js')
                .then(reg => reg.pushManager.subscribe({
                    userVisibleOnly: true,
                    applicationServerKey: urlBase64ToUint8Array(key)
                })))
            .then(subscription => {
                console.log('✅ Push đăng ký thành công:', subscription);
                return fetch('/save-subscription', {
                    method: 'POST',
                    body: JSON.stringify({ ...subscription.toJSON(), dataValue }),
                    headers: { 'Content-Type': 'application/json' }
                });
            })
            .catch(error => console.error('❌ Push đăng ký thất bại:', error));
    }

    function checkNotificationPermission() {
        if (Notification.permission === "denied") {
            alert("🚫 Bạn đã chặn thông báo! Hãy vào cài đặt trình duyệt để bật lại.");
        } else if (Notification.permission === "default") {
            notificationButton.style.display = "block";
        } else {
            registerPush();
        }
    }

    notificationButton.addEventListener("click", () => {
        Notification.requestPermission().then(permission => {
            if (permission === "granted") {
                notificationButton.style.display = "none";
                registerPush();
            }
        });
    });

    checkNotificationPermission();

    function urlBase64ToUint8Array(base64String) {
        const padding = '='.repeat((4 - base64String.length % 4) % 4);
        const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
        return new Uint8Array([...atob(base64)].map(c => c.charCodeAt(0)));
    }
});

để fetch : /vapid-public-key

trong route web.php :

Route::get('/vapid-public-key', function () {
    return response()->json(['key' => env('WEBPUSH_PUBLIC_KEY')]);
});