Enviando Push Notification com Node.js para o Browser
Tags: webdev, pushnotification, serviceworker, node
Published: 2023-01-28 23:15:49

## Introdução
Neste tutorial vamos aprender a enviar Push Notification para o Browser usando Node.js, irei utilizar também o React com Vite, mas poderia ser qualquer outra coisa, pois vamos utilizar de API's nativas da WEB para isso.

Será abordado o uso da [Notification API](https://developer.mozilla.org/en-US/docs/Web/API/notification) para solicitarmos a permissão para o usuário, [PushManager API](https://developer.mozilla.org/en-US/docs/Web/API/PushManager) para receber notificações de servidores de terceiros, [web-push](https://www.npmjs.com/package/web-push) para fazer o envio da notificação no lado servidor, e por fim o Service Worker, para lidar com a notificação em Background.

---

## Criando Back-end
Antes de tudo, precisamos criar nosso servidor em Node.js para fazer o envio dessas notificações, vou utilizar o [Express](https://www.npmjs.com/package/express) para isso, mas poderia ser com qualquer outro, como o [Fastify](https://www.npmjs.com/package/fastify) etc.

### Instalando pacotes necessários e configurando script
```sh
$ mkdir server
$ cd server
$ npm init -y
$ npm i express cors web-push
$ npm i typescript tsx @types/express @types/cors @types/node @types/web-push -D
$ npx tsc --init
```

Adicione no seu `package.json` o script de `dev`:

```json
"scripts": {
  "dev": "tsx watch src/server.ts"
}
```

### Criando servidor
Crie um arquivo dentro da pasta `src` chamado `server.ts` e adicione o código abaixo:

```ts
import express from 'express';
import cors from 'cors';
import WebPush from 'web-push';

const app = express();

app.use(express.json());
app.use(cors());

// WebPush

// console.log(WebPush.generateVAPIDKeys());
const publicKey = 'DASdasdjk1hdhajshdashdgadaskdhaj...';
const privateKey = 'dsaduiw18eudasd1d9sdd3wddasdasa...';

WebPush.setVapidDetails('http://localhost:3333', publicKey, privateKey);

// Routes

interface ISubscription {
  subscription: {
    endpoint: string;
    keys: {
      p256dh: string;
      auth: string;
    };
  };
}

app.get('/notification/push/public_key', (request, response) => {
  return response.json({ publicKey });
});

app.post('/notification/push/register', (request, response) => {
  console.log(request.body);

  return response.sendStatus(201);
});

app.post('/notification/push/send', async (request, response) => {
  const { subscription } = request.body as ISubscription;

  WebPush.sendNotification(
    subscription,
    JSON.stringify({
      icon: 'your-icon-link.png',
      title: 'Your title',
      body: 'Content of your message',
      imageUrl: 'your-image-link.png'
    }),
  );

  return response.sendStatus(201);
});

app.listen(3333, () => console.log('SERVER IS RUNNING AT :3333'));
```

Bom de Back-end é só isso, para você gerar sua `publicKey` e `privateKey` é necessário rodar o código `console.log(WebPush.generateVAPIDKeys());` somente uma vez, com isso ele vai te retornar suas chaves, ai basta você substituir essas fictícias que coloquei pelas as suas.

Sobre as rotas do servidor:

- **/notification/push/public_key**: Essa rota é responsável por enviar nossa `publicKey` para o Front-end.

- **/notification/push/register**: Essa rota serviria para você salvar no Banco de Dados por exemplo a `subscription` que o Front-end iria enviar, e com essa informação você poderia enviar a notificação depois, mas para não estender muito não irei abordar essa parte de salvar em algum Banco de Dados.

- **/notification/push/send**: Essa rota serve para fazer o envio da notificação, nela estou recebendo a `subscription` no body, pois é necessário para o envio da notificação, mas caso você tenha persistido essas subscriptions no Banco de Dados, você poderia substituir o recebimento dela nessa rota, e realizar a consulta dessas subscriptions existentes.

---

## Preparando nosso Front-end

### Criando o Service Worker
Vamos criar um arquivo chamado `service-worker.ts` dentro da pasta `public` pois esse arquivo precisa estar disponível publicamente.

> Estou utilizando `.ts` pois TypeScript é vida ❤️, mas se você quiser pode usar `.js` e remover as tipagens que irei adicionar, de qualquer forma se você usar com TypeScript, vai precisar converter para JavaScript no final, mas fique tranquilo irei mostrar como posteriormente.

```ts
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" />

interface INotificationPayload {
  icon: string;
  title: string;
  body: string;
  imageUrl?: string;
}

const sw = self as unknown as ServiceWorkerGlobalScope & typeof globalThis;

sw.addEventListener('push', (event) => {
  const notificationPayload = event.data?.json() as INotificationPayload;

  event.waitUntil(
    sw.registration.showNotification(notificationPayload.title, {
      icon: notificationPayload.icon,
      body: notificationPayload.body,
      image: notificationPayload.imageUrl
    }),
  );
});

```
> O código acima é bem simples, basicamente adicionamos um `listener` chamado `push`, com isso, quando for enviado alguma notificação vamos ficar sabendo, e teremos acesso ao evento que foi disparado, com esse evento conseguimos obter o `payload` que é os dados que foram enviados, e com esses dados conseguimos exibir de fato a notificação para o usuário.

> Você pode explorar mais atributos, que podem ser passados dentro desse objeto de configuração da função `showNotification`, por meio da [documentação oficial](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification).

---

### Solicitando permissão
Agora precisamos solicitar as permissões para o usuário, ele precisa de fato autorizar o recebimento dessas notificações para estar chegando para ele.

Você pode implementar o código abaixo no arquivo principal do seu projeto, para que quando o mesmo for iniciado chame a função abaixo.

```ts
function notifyMe() {
  if (!('Notification' in window)) {
    alert('This browser does not support desktop notification');
  } else if (Notification.permission === 'granted') {
    handleServiceWorker();
  } else if (Notification.permission !== 'denied') {
    Notification.requestPermission().then((permission) => {
      if (permission === 'granted') {
        handleServiceWorker();
      }
    });
  }
}

notifyMe();
```

> Basicamente na função acima, realizamos uma verificação para checar se temos acesso a `Notification API`, em seguida verificamos se já temos acesso garantido a notificação, se já tivermos só executamos a função `handleServiceWorker` que criaremos depois, por fim se ainda não tivermos concedido permissão para a notificação, solicitamos ao usuário para que garanta o acesso.

---

### Criando função handleServiceWorker

Essa função vai realizar o registro do nosso Service Worker, e após ser realizado o registro com sucesso, chamamos a função `getSubscription` do `pushManager`, pois ela recupera uma assinatura push existente e retorna seus detalhes, caso não encontrar retorna `null`.

Quando não temos uma assinatura existente, chamamos nosso endpoint `/notification/push/public_key` para retornar nossa `publicKey`, e com o valor dessa chave realizamos o `subscribe`, assim da próxima vez que tentarmos acessar a aplicação já vamos ter nossa inscrição feita.

Fiz também para fim de exemplo a chamada para o endpoint de registro da `subscription` e de envio da notificação, mas cabe a você definir quando e onde realizar o envio dessas notificações da melhor maneira.

```ts
async function handleServiceWorker() {
  navigator.serviceWorker
    .register('service-worker.js')
    .then(async (serviceWorker) => {
      serviceWorker.update();

      let subscription = await serviceWorker.pushManager.getSubscription();

      if (!subscription) {
        const publicKeyResponse = await api.get<{ publicKey: string }>(
          '/notification/push/public_key',
        );

        await serviceWorker.pushManager.subscribe({
          userVisibleOnly: true,
          applicationServerKey: publicKeyResponse.data.publicKey,
        });

        subscription = await serviceWorker.pushManager.getSubscription();
      }

      await axios.post('http://localhost:3333/notification/push/register', {
        subscription,
      });

      await axios.post('http://localhost:3333/notification/push/send', {
        subscription,
      });
    });
}
```

> obs: usei o `axios` como lib para realizar a chamada HTTP, mas poderia ser o `fetch` normalmente.

---

### Convertendo Service Worker para JavaScript
Essa etapa não é necessária se você criou o arquivo `service-worker.js`, mas caso tenha criado ele em TypeScript execute os passos abaixo:

Instale a dependência `esbuild` como dependência de desenvolvimento:

```sh
$ npm i esbuild -D
```

Adicione o script abaixo no seu arquivo `package.json`:

```json
"scripts": {
  "build-sw": "esbuild public/service-worker.ts --outdir=public --bundle --sourcemap --minify --format=esm --legal-comments=external"
}
```

Agora é só executar o script `build-sw`:

```sh
$ npm run build-sw
```

Pronto, agora o `service-worker.ts` vai ser convertido para JavaScript.