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