Реалізація Web Push повідомлень для сайту

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.
Розробка та обслуговування будь-яких видів сайтів:
Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація Web Push повідомлень для сайту
Середня
~2-3 робочих дні
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація Web Push уведомлень

Web Push дозволяє відправляти push-уведомлення користувачам браузера — як мобільні приложення, але без App Store. Працює через браузерний Service Worker навіть коли сайт закритий.

Архітектура Web Push

Приложення → Push Service (Google FCM / Mozilla) → Browser → Service Worker → Уведомлення

VAPID (Voluntary Application Server Identification) — стандарт аутентифікації сервера уведомлень.

Генерація VAPID-ключів

npx web-push generate-vapid-keys
# VAPID_PUBLIC_KEY=BEl62...
# VAPID_PRIVATE_KEY=Uf..._...
// .env
VAPID_PUBLIC_KEY=BEl62iUYagI8ghDIpicv9...
VAPID_PRIVATE_KEY=Uf7yFBAs-jWnM...
VAPID_SUBJECT=mailto:[email protected]

Підписка в браузері

// push-subscription.ts
const VAPID_PUBLIC_KEY = import.meta.env.VITE_VAPID_PUBLIC_KEY;

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

export async function subscribeToPush(): Promise<boolean> {
    if (!('PushManager' in window)) {
        console.warn('Push notifications not supported');
        return false;
    }

    const permission = await Notification.requestPermission();
    if (permission !== 'granted') return false;

    const registration = await navigator.serviceWorker.ready;
    const subscription = await registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
    });

    // Відправити підписку на сервер
    await fetch('/api/push/subscribe', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(subscription),
    });

    return true;
}

export async function unsubscribeFromPush(): Promise<void> {
    const registration = await navigator.serviceWorker.ready;
    const subscription = await registration.pushManager.getSubscription();
    if (subscription) {
        await subscription.unsubscribe();
        await fetch('/api/push/unsubscribe', {
            method: 'DELETE',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ endpoint: subscription.endpoint }),
        });
    }
}

Service Worker: обробка push-сповіщень

// sw.js
self.addEventListener('push', event => {
    const data = event.data?.json() ?? {};

    const options = {
        body:    data.body ?? 'Нове сповіщення',
        icon:    data.icon ?? '/icons/icon-192.png',
        badge:   '/icons/badge-72.png',
        image:   data.image,
        tag:     data.tag ?? 'default',
        renotify: data.renotify ?? false,
        data:    { url: data.url ?? '/' },
        actions: data.actions ?? [],
        requireInteraction: data.requireInteraction ?? false,
    };

    event.waitUntil(
        self.registration.showNotification(data.title ?? 'Сповіщення', options)
    );
});

self.addEventListener('notificationclick', event => {
    event.notification.close();

    const url = event.notification.data?.url ?? '/';

    event.waitUntil(
        clients.matchAll({ type: 'window', includeUncontrolled: true })
            .then(windowClients => {
                const existing = windowClients.find(c => c.url === url && 'focus' in c);
                if (existing) return existing.focus();
                return clients.openWindow(url);
            })
    );
});

Laravel бекенд

// Установка пакета
// composer require minishlink/web-push

// Міграція
Schema::create('push_subscriptions', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
    $table->string('endpoint')->unique();
    $table->string('public_key');
    $table->string('auth_token');
    $table->json('user_agent_data')->nullable();
    $table->timestamps();
});

// Контроллер підписки
class PushSubscriptionController extends Controller
{
    public function subscribe(Request $request): JsonResponse
    {
        $data = $request->validate([
            'endpoint'                  => 'required|url',
            'keys.p256dh'               => 'required|string',
            'keys.auth'                 => 'required|string',
        ]);

        PushSubscription::updateOrCreate(
            ['endpoint' => $data['endpoint']],
            [
                'user_id'    => auth()->id(),
                'public_key' => $data['keys']['p256dh'],
                'auth_token' => $data['keys']['auth'],
            ]
        );

        return response()->json(['status' => 'ok']);
    }
}

// Відправка сповіщення
use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;

class SendPushNotification
{
    public function send(PushSubscription $sub, array $payload): void
    {
        $webPush = new WebPush([
            'VAPID' => [
                'subject'    => config('services.vapid.subject'),
                'publicKey'  => config('services.vapid.public_key'),
                'privateKey' => config('services.vapid.private_key'),
            ],
        ]);

        $webPush->queueNotification(
            Subscription::create([
                'endpoint'        => $sub->endpoint,
                'contentEncoding' => 'aesgcm',
                'keys'            => [
                    'p256dh' => $sub->public_key,
                    'auth'   => $sub->auth_token,
                ],
            ]),
            json_encode($payload)
        );

        foreach ($webPush->flush() as $report) {
            if (!$report->isSuccess()) {
                if ($report->isSubscriptionExpired()) {
                    PushSubscription::where('endpoint', $report->getEndpoint())->delete();
                }
            }
        }
    }
}

Сценарії сповіщень

// Замовлення змінило статус
class OrderStatusChanged
{
    public function handle(Order $order): void
    {
        $user = $order->user;
        $subscriptions = PushSubscription::where('user_id', $user->id)->get();

        foreach ($subscriptions as $sub) {
            $this->sender->send($sub, [
                'title'   => 'Статус замовлення змінився',
                'body'    => "Замовлення #{$order->number}: {$order->status_label}",
                'icon'    => '/icons/order-icon.png',
                'url'     => "/account/orders/{$order->id}",
                'tag'     => "order-{$order->id}",
                'renotify' => true,
                'actions' => [
                    ['action' => 'view',   'title' => 'Переглянути замовлення'],
                    ['action' => 'dismiss', 'title' => 'Закрити'],
                ],
            ]);
        }
    }
}

Час реалізації: 1–2 дні для підписки, SW-обробника та серверної відправки.