Notificaciones push con JavaScript y Cloud Messaging

Hoy aprenderás a implementar la funcionalidad de envío y escucha de notificaciones push con JavaScript y Cloud Messaging de Firebase.

Además, lo haremos dando soporte tanto a aplicaciones web alojadas en un servidor, como a apps desarrolladas con tecnología híbrida HTML5.

Notificaciones push con JavaScript y Cloud Messaging
Notificaciones push con JavaScript y Cloud Messaging

Eso significa una cosa, se viene un artículo un poco largo, pero sin duda, uno de los más interesantes que he escrito nunca.

Así que, prepárate un buen café, ponte cómodo y empecemos.

¿Vas con prisa y quieres saltarte la introducción? Haz click aquí para ir directamente a la práctica.

¿Qué son las notificaciones push?

Antes de entrar en materia, es necesario definir qué son y cómo funcionan las notificaciones push.

La principal característica de este tipo de notificaciones, es que se originan en el lado del servidor.

Del término en inglés «push» (empujar), es el entorno «backend» quien, proactivamente envía un mensaje al cliente.

A diferencia de la clásica metodología «pull», el programa del lado del usuario, no realiza una petición a una API, ni utiliza ningún «endpoint» habilitado para ello. En su lugar, queda a la espera de la llamada del servidor, despertando, y notificando al usuario cuando eso sucede.

Pero, ¿cómo se habilita ese estado de escucha latente? La respuesta se encuentra en las funciones nativas del software cliente.

Los sistemas operativos de los dispositivos, sean móviles o de sobremesa, tienen incorporada la funcionalidad de atender a llamadas “push” emitidas desde un servidor.

Entonces, si esa característica se encuentra ubicada en una capa de software más baja, ¿Cómo se conecta con una aplicación HTML5?

Pues dependerá de la tipología de aplicación que vayamos a desarrollar.

Me explico, en el caso de una aplicación web, el navegador provee de una API que permite conectar nuestro script JS con el sistema de alertas del sistema operativo.

Sin embargo, para conectar esa funcionalidad en una app híbrida creada con tecnología web, hay que hacerlo mediante un plugin específicamente creado para ello

Mediante esa extensión, el webviewer que renderiza nuestra webapp, se conectará con la función de notificaciones del dispositivo móvil.

Soy consciente de que todo esto suena un tanto enrevesado, pero no te preocupes, en seguida lo pondremos en práctica, y verás que es más sencillo de lo que parece.

Cloud Messaging de Google Firebase para el envío de notificaciones PUSH.

Para escuchar llamadas “push”, es necesario disponer de un servidor que las envíe.

No obstante, habilitar un servidor propio, que realice este tipo de llamadas, puede resultar muy complejo. 

Por ese motivo, para el ejercicio de hoy, vamos a utilizar la plataforma Google Firebase. En concreto, haremos uso del servicio Cloud Messaging.

Firebase, es una solución BaaS (Backend as a Service) que simplifica enormemente la creación de un “backend”.

De hecho, hace unas semanas ya vimos cómo darse de alta gratuitamente, y cómo conectar los recursos que ofrece, a nuestro desarrollo frontend.

Crear aplicaciones fullstack con JavaScript y Firebase

Si no has trabajado nunca antes con Firebase, te animo a que leas primero ese otro artículo. Allí se cubren en detalle muchos de los puntos, que hoy veremos solo por encima.

El tutorial que sigue a esta introducción, se centrará en crear una aplicación que sea capaz de recibir notificaciones “push” enviadas desde Cloud Messaging.

Y como dije al inicio, no solo lo implementaremos sobre una “webapp” de navegador, además, crearemos un app híbrida que cumpla con la misma función.

Todo ello aprovechando gran parte del código fuente.

Existe otro post donde estudiamos detenidamente, cómo crear apps multiplataforma con JavaScript y la librería Capacitor.

Desarrollo de apps híbridas con Capacitor

De nuevo, te recomiendo leerlo si no tienes ninguna experiencia desarrollando con este tipo de tecnologías. 

En cualquier caso, si no te apetece leerlo, siempre tienes la posibilidad de implementar sólo las notificaciones de navegador, y dejar la app híbrida para otro día.

Soy consciente de que todo esto suena un tanto enrevesado, pero no te preocupes, en seguida lo pondremos en práctica, y verás que es más sencillo de lo que parece.

Notifica a los usuarios sobre cualquier novedad

Imagina el siguiente escenario. Has creado un sitio web de contenidos, conseguiste crear una pequeña comunidad, y ahora deseas dar un paso más allá, añadiendo un factor de fidelización.

Una buena forma de hacerlo, sería  ofrecer a tus usuarios la posibilidad de suscribirse a un sistema de notificaciones.

De este modo, cada vez que publicaras contenido nuevo, podrías avisarlos de forma proactiva.

Bien, pues precisamente vamos a reproducir este caso, programando una sencilla web capaz de escuchar notificaciones PUSH, enviadas desde el servicio Cloud Messaging de Firebase.

Para que puedas seguir cómodamente ésta guía, te dejo el enlace al código completo en el repositorio de Github.

Código del ejercicio completo

A partir de aquí, iremos implementando los siguientes puntos, uno tras otro:

Requisitos previos

Para poder avanzar de forma ágil, es necesario que primero prepares una serie de recursos básicos para el proyecto.

En primer lugar, tienes que disponer de una cuenta activa en Firebase, y un proyecto nuevo dado de alta.

Como ya dije, no voy a detallar cómo hacerlo, puedes recurrir al enlace del anterior post, si en algún momento te hace falta.

¿Cómo preparar una cuenta de Google Firebase?

Por otro lado, también es necesario que prepares un entorno de desarrollo frontend.

Puedes generarlo de cero, o aprovechar el que ya generamos hace un tiempo. En cualquier caso, te aconsejo que incluyas un «web bundle» como Vite o Webpack.

En el siguiente enlace, puedes descargar el entorno sobre el que montaré el ejercicio de hoy.

Enlace a repositorio del entorno de desarrollo con Vite

Adicionalmente, debes añadir un archivo manifest.json a tu directorio público, es necesario para que las notificaciones funcionen correctamente en ciertos navegadores. No es necesario que lo rellenes con ningún dato específico, de hecho puedes copiar el que te dejo a continuación:

{
  "//_comment1": "Some browsers will use this to enable push notifications.",
  "//_comment2": "It is the same for all projects, this is not your project's sender ID",
  "gcm_sender_id": "103953800507"
}

Con estos puntos resueltos, ya estás en disposición de seguir con el tutorial.

Alta de la aplicación en Firebase

Accede a tu panel de control de Firebase. Selecciona el proyecto que has creado, y da de alta una nueva aplicación web.

De momento nos centraremos en la versión web para navegador. Así que de momento no actives la opción para Android.

Crear app web en Firebase
Crear app web en Firebase

Dale un nombre y obtén los datos de configuración.

Dale un nombre a tu app web
Dale un nombre a tu app web
Datos de configuración web
Datos de configuración web

Crea un archivo a partir de los datos de configuración, y guárdalo en tu proyecto bajo el nombre «./firebaseConfig.js».

const firebaseConfig = {
  apiKey: "AIzaSyB0de3v63rcTp6MB_q5r4IiGFhLkVYm6io",
  authDomain: "libreriasjs-pnotifications.firebaseapp.com",
  projectId: "libreriasjs-pnotifications",
  storageBucket: "libreriasjs-pnotifications.appspot.com",
  messagingSenderId: "892795652792",
  appId: "1:892795652792:web:e01b4ff96316b6bb7c024d",
};
export default firebaseConfig;

Más tarde lo utilizaremos para configurar la conexión con el “backend”.

Generar un certificado PUSH web

Una vez tengas el proyecto web activado, deberás generar un par de claves necesarias para el correcto funcionamiento de las notificaciones web.

Para ello, debes acceder a las opciones que se encuentran en la rueda dentada ubicada en la parte superior del menú principal.

Opciones de proyecto Firebase
Opciones de proyecto Firebase

Dentro de la pestaña Cloud Messaging, haz click en el botón “Generate key pair” en la parte inferior.

Generar claves push web
Generar claves push web

Copia y guarda la cadena de texto que te acaba de proveer la plataforma, más adelante te va a hacer falta. 

Por el momento, puedes dejar de lado Firebase, pero no cierres la pestaña, volveremos en un rato.

Maquetación responsive de la interfaz gráfica.

Llegados a este punto, toca montar una estructura HTML básica para nuestra app.

Simularemos la interfaz propia de un «blog». De modo que crearemos un listado de publicaciones. 

Por supuesto, serán meramente presenciales, de modo que no hará falta programar sus apartados detalle.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <base href="/" />
    <meta
      name="viewport"
      content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
    />
    <meta name="format-detection" content="telephone=no" />
    <meta name="msapplication-tap-highlight" content="no" />
    <!-- add to homescreen for ios -->
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta name="apple-mobile-web-app-title" content="Ionic App" />
    <meta name="apple-mobile-web-app-status-bar-style" content="black" />
    <title>Push notifications</title>
  </head>
  <body>
    <div class="main-container">
      <div class="request-permission-container">
        <button class="request-permission-btn">
          <span class="loader hidden"></span>
          <span class="label-btn">🔔Activar notificaciones</span>
        </button>
      </div>
      <div id="token-container"></div>
      <div class="header">
        <h1>Trotamundos</h1>
        <span>🗺️ blog de viaje 🗺️</span>
      </div>
      <div class="content">
        <a href="#" class="blog-post">
          <img src="images/clay-banks-hwLAI5lRhdM-unsplash.jpg" alt="" />
          <div class="post-content">
            <div class="title-wrapper">
              <h2>Japón - Dia 0</h2>
              <h3>Kyoto</h3>
            </div>
            <p class="content-excerpt">
              La llegada fue maravillosa, no teníamos ni idea de lo que nos
              aguardaba al cruzar las puertas del aeorpuerto...
            </p>
          </div>
        </a>
      </div>
      <div class="footer">
        El Trotamundos - blog - 2022 - libreriasjs.com - v.2.0.4
      </div>
    </div>
    <script type="module" src="./src/main.js"></script>
  </body>
</html>

Como ves, se compone de una área de activación de notificaciones, una cabecera, el listado de entradas y un pié de página. Nada del otro mundo. En esta muestra he eliminiado algunos elementos a.blog-post dentro de .content para recortar, tienes el código completo en el repositorio

Con todo, es importante que sea 100% responsive, ya que el mismo «layout» debe servir para distintos formatos web, y para la app móvil.

La maqueta por sí sola deja bastante que desear, de modo que vamos a darle estilos.

@import url('https://fonts.googleapis.com/css2?family=Raleway:wght@400;700&display=swap')
body
  margin: 0
  padding: 0
  background-color: #111111
  background-image: url('./assets/topography.webp')
  font-family: 'Raleway', sans-serif
.loader
  width: 30px
  height: 30px
  border: 5px solid #e4e4e4
  border-bottom-color: #0077ff
  border-radius: 50%
  display: inline-block
  box-sizing: border-box
  animation: rotation 1s linear infinite
  &.hidden
    display: none
  @keyframes rotation
    0%
      transform: rotate(0deg)
    100%
      transform: rotate(360deg)
.main-container
  max-width: 1000px
  margin: 10px auto
  border-radius: 5px
  display: flex
  flex-direction: column
  padding: 40px 5% 40px 5%
  .header
    color: #ffffff
    border-radius: 5px
    padding: 40px
    background-color: #232323
    background-image: url('./assets/double-bubble-dark.webp')
    margin: 0 0 20px 0
    h1
      margin: 0
  .request-permission-container
    text-align: right
    margin: 0 0 20px 0
    display: flex
    justify-content: flex-end
    &.hidden
      display: none
    button.request-permission-btn
      cursor: pointer
      padding: 10px 30px
      background-color: #0c62a9
      font-weight: 700
      font-family: 'Raleway', sans-serif
      border: none
      border-radius: 3px
      box-shadow: 0 3px 4px rgba(0,0,0,.2)
      color: #fff
      display: flex
      justify-content: center
      align-items: center
      font-size: 1.2rem
      .label-btn
        &.hidden
          display: none
  #token-container
    margin-bottom: 20px
    padding: 10px
    border-radius: 5px
    background-color: #bababa
    text-align: center
    display: none
    &.active
      display: block
    &.ready
      color: #087e00
      background-color: #a5ff81
      word-break: break-all
    &.error
      color: #7e0000
      background-color: #ff8181
  .blog-post
    display: block
    background-color: #fff
    color: #000
    text-decoration: none
    box-shadow: 0 2px 3px rgba(0, 0, 0, .2)
    color: #fff
    position: relative
    padding: 20% 20px 20px 20px
    overflow: hidden
    border-radius: 4px
    margin-bottom: 20px
    transition: transform .1s ease, opacity .5s ease
    transform: scale(1)
    opacity: 1
    &.appear
      opacity: 0
    &::after
      content: ''
      position: absolute
      z-index: 1
      top: 0
      left: 0
      width: 100%
      height: 100%
      transform: translateY(30%)
      transition: transform .2s ease
      background: rgb(0,0,0)
      background: -moz-linear-gradient(0deg, rgba(0,0,0,0.8) 50%, rgba(0,2,9,0.5) 70%, rgba(3,28,135,0) 100%)
      background: -webkit-linear-gradient(0deg, rgba(0,0,0,0.8) 50%, rgba(0,2,9,0.5) 70%, rgba(3,28,135,0) 100%)
      background: linear-gradient(0deg, rgba(0,0,0,0.8) 50%, rgba(0,2,9,0.5) 70%, rgba(3,28,135,0) 100%)
      filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#000000",endColorstr="#031c87",GradientType=1)
    &:hover
      transform: scale(1.02)
      &::after
        transform: translateY(0%)
      .post-content
        transform: translate(0%)
        .content-excerpt
          opacity: 1
    img
      width: 100%
      height: 100%
      object-fit: cover
      display: block
      position: absolute
      top: 0
      left: 0
      z-index: 1
      &.bottom
        object-position: center bottom
      &.top
        object-position: center top
    .post-content
      position: relative
      z-index: 2
      transform: translateY(50%)
      transition: transform .2s ease .2s
      .title-wrapper
        border-left: solid 2px #fff
        padding: 0 0 0 20px
        h2
          margin: 0 0 8px 0
          font-weight: 700
        h3
          margin: 0
          font-weight: 300
      .content-excerpt
        opacity: 0
        transition: opacity .2s ease .3s
        max-width: 60%
  .footer
    font-size: .8rem
    text-align: right

Con unas pocas líneas, el acabado queda mucho más creíble.

Escucha de notificaciones en el navegador.

Ha llegado el momento de implementar la funcionalidad con JavaScript.

Empezamos instalando las librerías de Firebase en nuestro proyecto.

npm install firebase

No trabajaremos la librería directamente en el script principal. En su lugar, crearemos una clase controladora para mantener el código organizado.

Por eso, generamos una clase llamada «FirebaseCtrl«, que importa la librería como dependencia.

import firebaseConfig from "../firebaseConfig";
import { initializeApp } from "firebase/app";
import { getMessaging, getToken, isSupported } from "firebase/messaging";
class FirebaseCtrl {
  constructor() {
    this.token = undefined;
    this.onRecieveNotificationCb = undefined;
    this.onErrorCb = undefined;
    this.onGetTokenCb = undefined;
  }
}
export default FirebaseCtrl;

En el método constructor habilitamos las siguientes propiedades:

  • token: Esta variable guardará una referencia al token asignado por Cloud Messaging. Es único para cada dispositivo y lo utilizaremos para mandar notificaciones a usuarios específicos.
  • onRecieveNotificationCb: En ésta propiedad almacenaremos una función de retorno que se ejecutará al recibir una notificación.
  • onErrorCb: Guardará una función de retorno para manejar los errores.
  • onGetTokenCb: Expondrá una función cada vez que reciba el token desde Firebase

Más adelante les asignaremos el valor correspondiente, de momento, las declaramos como indefinidas.

Seguidamente, definimos el método “initApp()”

  async initApp() {
    const savedToken = window.localStorage.getItem(
      "libreriasjs-notification-token"
    );
    if (savedToken) {
      this.enableWebNotifications();
    }
  }

Cuando se ejecute “initApp”, comprobamos si ya existe un token guardado en la memoria, mediante la API “localstorage. En caso de que exista, llamaremos inmediatamente al método “enableWebNotifications”.

Más adelante, ampliaremos initApp para dar soporte a la versión app híbrida. Por ahora preparamos el terreno para una mejor escalabilidad.

La función “enableWebNotifications()”, sin duda es la más importante del controlador.

async enableWebNotifications() {
    const supported = await isSupported();
    if (!supported && typeof this.onErrorCb === "function") {
      this.onErrorCb(
        "This browser does not support the API's required to use the Firebase SDK"
      );
      return;
    }
    const app = initializeApp(firebaseConfig);
    const messaging = getMessaging(app);
    try {
      this.token = await getToken(messaging, {
        vapidKey:
          "BCXvt8U7Y4HBlhyFVA9WemdDEhatVQFJagusaEon-0ypK2FeJTXf2TVpOO5fbIUSB-fN2YeQEudUvPQ-fe16lI8",
      });
    } catch (err) {
      console.log("An error occurred while retrieving token. ", err);
      if (typeof this.onErrorCb === "function") {
        this.onErrorCb(err.message);
      }
      return;
    }
    if (!this.token) {
      const error =
        "No registration token available. Request permission to generate one.";
      console.log(error);
      if (typeof this.onErrorCb === "function") {
        this.onErrorCb(error);
      }
      return;
    }
    console.log(this.token);
    if (typeof this.onGetTokenCb === "function") {
      window.localStorage.setItem("libreriasjs-notification-token", this.token);
      this.onGetTokenCb(this.token);
    }
    navigator.serviceWorker.addEventListener("message", (event) => {
      console.log("FROM ON SERVICEWORKER MESSAGE", event);
      if (typeof this.onRecieveNotificationCb === "function") {
        this.onRecieveNotificationCb(event.data);
      }
    });
  }

La primera instrucción comprueba si el navegador soporta la funcionalidad que vamos a implementar, mediante la función “isSupported” de la librería. En caso de que no dé soporte, llamaremos al callback “onErrorCb”, si existe.

En caso de que sí esté soportado, tratamos de generar un objeto llamado “app”. Ésta variable es una referencia al proyecto web que hemos dado de alta con panel de gestión de Firebase. 

Para obtener “app”, ejecutamos la instrucción “initializeApp(firebaseConfig)”, pasándole como argumento el objeto de configuración del archivo “firebaseConfig.js”.

El objeto “app” nos servirá para conectar el frontend con cualquier otro servicio de Firebase. En este caso en concreto, obtendremos una referencia al servicio messaging de Cloud Messaging

 const app = initializeApp(firebaseConfig);
 const messaging = getMessaging(app);

Acto seguido, tratamos de obtener un token asociado al dispositivo con «getToken«, la variable «messaging«, y un objeto con la clave que obtuvimos anteriormente, tal como aparece en el código de muestra.

try {
  this.token = await getToken(messaging, {
    vapidKey:
      "BCXvt8U7Y4HBlhyFVA9WemdDEhatVQFJagusaEon-0ypK2FeJTXf2TVpOO5fbIUSB-fN2YeQEudUvPQ-fe16lI8",
  });
} catch (err) {
  console.log("An error occurred while retrieving token. ", err);
  if (typeof this.onErrorCb === "function") {
    this.onErrorCb(err.message);
  }
  return;
}

Al tratarse de una función asíncrona recuerda añadir «await» y manejar posibles errores con «try» / «catch». 

En caso de que el token no se haya podido generar, deberás gestionar el posible error con el callback habilitado para ello.

Por otra parte, si se ha generado correctamente, llamamos al “callback” onGetTokenCb. De este modo, lo expondremos en el script principal.

Por último, pero no por ello menos importante, debemos escuchar cuando llega un nuevo mensaje

Realmente quien se encargará de eso será un “websocket” habilitado específicamente para ello.

En el controlador, sencillamente escucharemos la llegada del evento “message” enviado desde ese “worker”, y expondremos la información del mensaje a través de “onRecieveNotificationCb”.

En seguida veremos cómo implementar el service worker. Pero antes, vamos a preparar el resto de métodos, para guardar los “callbacks” correspondientes como propiedades de la instancia.

onGetToken(cb) {
  if (typeof cb === "function") {
    this.onGetTokenCb = cb;
  }
}
onRecieveNotification(cb) {
  if (typeof cb === "function") {
    this.onRecieveNotificationCb = cb;
  }
}
onError(cb) {
  if (typeof cb === "function") {
    this.onErrorCb = (err) => {
      window.localStorage.removeItem("libreriasjs-notification-token");
      cb(err);
    };
  }
}

Fíjate que “OnError” se encargará de eliminar el token guardado en memoria, justo antes de ejecutar la función de retorno. 

Ahora sí, pasamos a crear el «service worker» encargado de escuchar las notificaciones de “Cloud Messaging”. Lo guardaremos en el directorio público bajo el nombre “firebase-messaging-sw.js”.

Es importante mantener este nombre, ya que la librería Firebase se encargará de buscarlo y cargarlo por nosotros.

El archivo se compone de estas pocas líneas

importScripts(
  "https://www.gstatic.com/firebasejs/9.2.0/firebase-app-compat.js"
);
importScripts(
  "https://www.gstatic.com/firebasejs/9.2.0/firebase-messaging-compat.js"
);
const firebaseApp = firebase.initializeApp({
  apiKey: "AIzaSyB0de3v63rcTp6MB_q5r4IiGFhLkVYm6io",
  authDomain: "libreriasjs-pnotifications.firebaseapp.com",
  projectId: "libreriasjs-pnotifications",
  storageBucket: "libreriasjs-pnotifications.appspot.com",
  messagingSenderId: "892795652792",
  appId: "1:892795652792:web:e01b4ff96316b6bb7c024d",
});
const messaging = firebase.messaging();

Importamos las dependencias necesarias, generamos un objeto firebaseApp con las mismas opciones de configuración de “firebaseConfig.js”, e instanciamos el servicio “messaging”.

Parece mentira, pero solo con eso será suficiente para que el sistema funcione. Casi parece magia.

Ahora que ya disponemos del controlador, y del “serviceWorker”, ha llegado el momento de programar el script principal. En él actualizaremos la interfaz de usuario, en cada momento.

Comenzamos importando los estilos y la clase FirebaseCtrl.

import "./style.sass";
import FirebaseCtrl from "./services/FirebaseCtrl";

Generamos una instancia de nuestro controlador, y guardamos un conjunto de referencias a elementos del DOM en variables.

const fireBaseCtrl = new FirebaseCtrl();
const cardsContainer = document.querySelector(".content");
const tokenContainer = document.querySelector("#token-container");
const requestPermissionContainer = document.querySelector(
  ".request-permission-container"
);

Lanzamos el método “initApp”, para comprobar si el cliente ya dispone de un token.

fireBaseCtrl.initApp();

A continuación, prepararemos todas las funciones de respuesta para cada una de las posibles situaciones.

fireBaseCtrl.onError((errorMessage) => {
  requestPermissionContainer.classList.remove("hidden");
  tokenContainer.classList.remove("ready");
  tokenContainer.classList.add("active", "error");
  tokenContainer.innerHTML = errorMessage;
});

Cuando el controlador detecte un error, actualizaremos la UI con el mensaje, y mostrando el botón de activar las notificaciones (por si el usuario quiere volver a intentar habilitar las notificaciones)

fireBaseCtrl.onGetToken((token) => {
  requestPermissionContainer.classList.add("hidden");
  tokenContainer.classList.remove("error");
  tokenContainer.classList.add("active", "ready");
  tokenContainer.innerHTML = token;
});

Por contra, al recibir el token lo mostraremos en pantalla y ocultamos el botón.

Por supuesto también debemos reaccionar a la llegada de notificaciones, y lo haremos mediante el método “onRecieveNotification”.

const createCard = (notificationData) => {
  const dataCard = {
    title: "Título",
    snap: "https://picsum.photos/1000/350",
    subtitle: "Subtítulo",
    excerpt: "Lorem ipsum dolor sit amet",
    ...notificationData,
  };
  const a = document.createElement("a");
  a.classList.add("blog-post", "appear");
  a.setAttribute("href", "#");
  a.innerHTML = `
  <img src="${dataCard.snap}" alt="" />
  <div class="post-content">
    <div class="title-wrapper">
      <h2>${dataCard.title}</h2>
      <h3>${dataCard.subtitle}</h3>
    </div>
    <p class="content-excerpt">
      ${dataCard.excerpt}
    </p>
  </div>`;
  return a;
};
fireBaseCtrl.onRecieveNotification((notificationData) => {
  const element = createCard(notificationData.data);
  cardsContainer.prepend(element);
  window.setTimeout(() => {
    element.classList.remove("appear");
  }, 500);
});

Cada notificación puede llegar con una propiedad “data”, a partir de la cual construiremos una nueva tarjeta, con la función “createCard”. Date cuenta de que si una notificación llega sin “data”, la función “createCard” mostrará unos valores por defecto.

Finalmente, capturamos el evento “click” en el botón “request-permission-btn”. Dentro del “callback”, solicitamos permiso al usuario para habilitar las notificaciones con “Notification.requestPermission()”, la API própia de navegador.

requestPermissionContainer
  .querySelector(".request-permission-btn")
  .addEventListener("click", async (event) => {
    const loader = requestPermissionContainer.querySelector(".loader");
    const label = requestPermissionContainer.querySelector(".label-btn");
    label.classList.add("hidden");
    loader.classList.remove("hidden");
    try {
      const permission = await Notification.requestPermission();
      if (permission !== "granted") {
        console.log("No se ha aceptado el registro de notificaciones");
        return;
      }
      await fireBaseCtrl.enableWebNotifications();
    } catch (err) {
      console.log("Hubo un error", err);
    } finally {
      label.classList.remove("hidden");
      loader.classList.add("hidden");
    }
  });

Si se garantiza el permiso, entonces llamaremos al método “enableWebNotifications”, actualizando, en todo momento, la interfaz, e informando al usuario.

Envío de una notificación web

Ha llegado la hora de la verdad, vas a mandar tu primera notificación push a través de la plataforma Cloud Messaging de Firebase, y si todo ha ido bien, deberías recibirla en tu frontend.

Asegúrate de que tienes habilitadas las notificaciones, a nivel de navegador y a nivel de sistema operativo. Compruébalo accediendo a las preferencias del sistema de tu dispositivo, y en la configuración de tu navegador (preferiblemente Chrome o Firefox).

Si aún no lo habías hecho, arranca tu entorno local, con el comando adecuado, en mi caso:

npm run dev

Es importante que, para realizar esta prueba, accedas con el navegador a tu aplicación web, a través del dominio “localhost” (seguido del puerto que sea)

Si accedes con una IP (como por ejemplo 192.168.1.106), y sin protocolo https, el navegador considerará el sitio como inseguro, y no permitirá activar las notificaciones.

Si ya visualizas una interfaz parecida a esta:

Interfaz del proyecto PNotifications
Interfaz del proyecto PNotifications

Entonces, haz click en el botón de “activar notificaciones”, y concede permiso al navegador. Deberías recibir un token y visualizarlo en pantalla, en caso de que no sea así, un mensaje de error te informará de qué ha sucedido.

Ejemplo de token
Ejemplo de token

Copia el token y vuelve al panel de control de Firebase.

Selecciona Cloud Messaging de entre los productos de la plataforma, haz click en “Crear la primera campaña”, y elige “Mensajes de Firebase Notifications”.

Crear campaña de Messaging
Crear campaña de Messaging
Tipo de campaña Messaging
Tipo de campaña Messaging

En la vista que sigue puedes crear una campaña para notificar a todos los usuarios suscritos.

Nueva campaña de notificaciones
Nueva campaña de notificaciones

Añade un título y un texto para la notificación. Al hacerlo, se activará el botón de “Enviar mensaje de prueba”, si haces click en él verás una modal parecida a esta.

Hacer un envio de notificación de prueba

Hacer un envio de notificación de prueba

Añade el token que aparece en tu aplicación, seleccionalo, y haz clic en el botón “probar”.

Deberías recibir una notificación si todo ha funcionado correctamente, que tras hacer click en ella, abra la pestaña de tu navegador con tu webapp. Si tenías la pestaña del navegador en primer plano, directamente debería aparecer una nueva entrada en la parte superior

¡Enhorabuena!, acabas de enviar tu primera notificación push.

El sistema de Cloud messaging también permite programar campañas para enviar notificaciones a múltiples dispositivos.

Sigues rellenando los campos de todos los pasos de la modal, para programar tu primera campaña. Suele tardar unos pocos minutos en enviarse.

Las campañas son geniales para no tener que mandar notificaciones de una en una.

Con esto doy por cerrado el tutorial para escuchar notificaciones desde el navegador. Seguidamente ampliaremos el core de nuestra aplicación para crear una app híbrida, que contenga la misma funcionalidad.

Crear una aplicación multiplataforma con Capacitor

Pasamos a ver cómo implementar esta misma funcionalidad en forma de app híbrida

De nuevo, no entraré en detalle a explicar cómo preparar un entorno de desarrollo con Capacitor. Como dije, si tienes dudas, puedes recurrir al artículo que dediqué enteramente a ello.

https://libreriasjs.com/libreria-javascript-apps-multiplataforma-capacitor/#tutorial

Sin embargo, sí he listado a modo de resumen de los pasos a seguir.

  • Instala las librerías @capacitor/core y @capacitor/cli con NPM.
  • Ejecuta el comando “npx cap init” y configura el nombre y ID de tu proyecto Capacitor.
  • Edita el archivo capacitor.config.ts acorde a tu entorno de desarrollo.
  • Instala Android Studio si aún no lo tienes en tu ordenador.
  • Instala los recursos necesarios para compilar para Android con el comando “npm install @capacitor/android
  • Si aún no lo has hecho, compila tu proyecto con “npm run build” (o equivalente)
  • Crea el directorio nativo de Android con lanzando el comando “npx cap add android
  • Comprueba que todo está correcto llamando a la instrucción “npx cap open android
  • Si se abre el programa Android Studio, entonces haz click en el botón play para instalar tu app híbrida en un emulador o en tu dispositivo.

Asegúrate de que tu app híbrida funciona en un dispositivo Android, y sigue con el resto de ésta guía.

Instala el plugin que conecta el sistemas de notificaciones nativo con el “webviewer” con el siguiente comando:

npm install @capacitor/push-notifications

Configurar la app híbrida con el servicio Cloud Messaging

Regresa de nuevo al panel de administración de Firebase, y agrega una nueva aplicación de tipo Android.

Agrega una app de tipo Android
Agrega una app de tipo Android

Escribe el nombre del paquete y sobrenombre de modo que coincidan con tu AppID y AppName. Puedes encontrar esos parámetros dentro del archivo capacitor.config.js

En el paso dos, descarga el archivo google-services.json, e inclúyelo en el nivel indicado dentro de tu directorio Android creado por Capacitor 

Paso dos en la configuración
Paso dos en la configuración

En el paso 3 Firebase te pedirá que amplies dos archivos llamados “build.gradle”, para incluir una serie de líneas. Hazlo del modo que te indica.

Agregar el SDK en el directorio Android
Agregar el SDK en el directorio Android
Editar los archivos nativos dentro del directorio ./android
Editar los archivos nativos dentro del directorio ./android

Acepta y regresa de nuevo a la consola. Con eso será suficiente para conectar tu app híbrida con el servicio de mensajería Cloud Messaging.

Escalar el “core” de aplicación web para dar soporte a la app híbrida

Con esto hemos dejado lista la infraestructura, pero ahora toca ampliar el software para controlar el comportamiento de la app híbrida.

Abre el archivo “./src/services/FirebaseCtrl.js” con tu editor de código, y añade las siguentes dependencias en el inicio

import { Capacitor } from "@capacitor/core";
import { PushNotifications } from "@capacitor/push-notifications";

Amplía el método “initApp” de modo que, si detecta que se trata de una plataforma nativa, ejecute el comando “enableMobileNotifications” en vez de “enableWebNotifications”.

async initApp() {
  const savedToken = window.localStorage.getItem(
    "libreriasjs-notification-token"
  );
  if (savedToken) {
    if (Capacitor.isNativePlatform()) {
      return this.enableMobileNotifications();
    }
    this.enableWebNotifications();
  }
}

Acto seguido, declaramos el método “enableMobileNotifications” de este modo.

async enableMobileNotifications() {
  const result = await PushNotifications.requestPermissions();
  if (result.receive === "granted") {
    PushNotifications.register();
  } else {
    console.log("an error ocurred");
    if (typeof this.onErrorCb === "function") {
      this.onErrorCb("an error ocurred");
    }
  }
  PushNotifications.addListener("registration", (token) => {
    window.localStorage.setItem(
      "libreriasjs-notification-token",
      token.value
    );
    if (typeof this.onGetTokenCb === "function") {
      this.onGetTokenCb(token.value);
    }
  });
  PushNotifications.addListener("registrationError", (error) => {
    if (typeof this.onErrorCb === "function") {
      this.onErrorCb(JSON.stringify(error));
    }
  });
  PushNotifications.addListener(
    "pushNotificationReceived",
    (notification) => {
      console.log("Push received: ", notification);
      if (typeof this.onRecieveNotificationCb === "function") {
        this.onRecieveNotificationCb(notification);
      }
    }
  );
  PushNotifications.addListener(
    "pushNotificationActionPerformed",
    (notificationAction) => {
      console.log("pushNotificationActionPerformed: ", notificationAction);
      if (typeof this.onRecieveNotificationCb === "function") {
        this.onRecieveNotificationCb(notificationAction.notification);
      }
    }
  );
}

En el cuerpo del método, vemos como el primer paso consistirá en pedir permiso al usuario. Si se conceden los permisos, llamaremos al método “register” de “PushNotifications”.

Por supuesto, en caso contrario, notificaremos del error mediante el “callback” onErrorCb.

const result = await PushNotifications.requestPermissions();
  if (result.receive === "granted") {
    PushNotifications.register();
  } else {
    console.log("an error ocurred");
    if (typeof this.onErrorCb === "function") {
      this.onErrorCb("an error ocurred");
    }
  }

En los siguientes pasos escucharemos un conjunto de eventos capturados por la librería “PushNotifications”. Comenzando por cuando se obtiene el token.

PushNotifications.addListener("registration", (token) => {
  window.localStorage.setItem(
    "libreriasjs-notification-token",
    token.value
  );
  if (typeof this.onGetTokenCb === "function") {
    this.onGetTokenCb(token.value);
  }
});

Tal y como se aprecia en el código de ejemplo, guardamos el toque en la memoria del dispositivo y ejecutamos nuestra función de retorno “onGetTokenCb”.

También capturamos el error en el momento del registro con el evento “registrationError”.

PushNotifications.addListener("registrationError", (error) => {
  if (typeof this.onErrorCb === "function") {
    this.onErrorCb(JSON.stringify(error));
  }
});

En este caso, la llegada de nuevas notificaciones se capturará con el evento “pushNotificationReceived”. Evidentemente expondremos su contenido gracias a nuestro método “onRecieveNotificationCb”.

PushNotifications.addListener(
  "pushNotificationReceived",
  (notification) => {
    console.log("Push received: ", notification);
    if (typeof this.onRecieveNotificationCb === "function") {
      this.onRecieveNotificationCb(notification);
    }
  }
);

Por último, también capturaremos la acción que se debe desencadenar cuando el usuario  presiona sobre la notificación, fuera de la app.

PushNotifications.addListener(
  "pushNotificationActionPerformed",
  (notificationAction) => {
    console.log("pushNotificationActionPerformed: ", notificationAction);
    if (typeof this.onRecieveNotificationCb === "function") {
      this.onRecieveNotificationCb(notificationAction.notification);
    }
  }
);

Nuestro controlador ya está preparado para la plataforma nativa. Terminaremos el ejercicio ampliando el script principal.

Al inicio de “main.js” importa nuevamente la librería Capacitor.

import { Capacitor } from "@capacitor/core";

Aquí sólo añadiremos un condicional en el evento “click” del botón “.request-permission-btn», tal que así

if (Capacitor.isNativePlatform()) {
  await fireBaseCtrl.enableMobileNotifications();
  return;
}

Compila de nuevo la aplicación Android ejecutando todas las instrucciones que siguen:

npm run build
npx cap sync
npx cap open android

Instala la aplicación en tu dispositivo móvil y comprueba que aparece el token en pantalla.

Envío de otra notificación para la app

Regresa al dashboard de Firebase, y crea una nueva campaña de Cloud Messaging.

Selecciona notificaciones, y trata de enviar un mensaje de prueba usando el token de la app híbrida.

Si recibes la notificación en tu dispositivo móvil, ¡enhorabuena! Has conseguido implementar las notificaciones en una app creada con Capacitor.

Para terminar, crea una campaña que envíe un mensaje tanto a la versión web, como a la app.

Selecciona múltiples plataformas para la campaña
Selecciona múltiples plataformas para la campaña

Esto ha sido todo, espero, de verdad, que haya servido. 

Puede que algunos puntos no estén del todo claros, por eso iré revisando periódicamente su contenido, para mejorarlo y ampliarlo si hace falta.

Tras escribir este artículo, me he planteado incorporar la opción de activar notificaciones en este mismo blog.

De este modo podría informar cuando publico una nueva entrada, a los usuarios que lo deseen.

¿Qué te parece? ¿Consideras que es una buena idea?, ¿o te parece un recurso molesto? 

Puedes dejar un comentario al respecto, o enviármelo directamente a través de mis redes sociales.

De nuevo muchas gracias por leer esta publicación, y ¡hasta la próxima!

2 comentarios en «Notificaciones push con JavaScript y Cloud Messaging»

Deja un comentario