Notificaciones de app de sitio web progresivo con Laravel, Vue.js y Twilio Notify

April 21, 2020
Redactado por
Michael Okoko
Colaborador
Las opiniones expresadas por los colaboradores de Twilio son propias.
Revisado por

Notificaciones de app de sitio web progresivo con Laravel, Vue.js y Twilio Notify

As of October 24, 2022, the Twilio Notify API is no longer for sale. Please refer to our Help Center Article for more information. 

Las app web progresivas (PWA, por sus siglas en inglés) son sitios web instalables que ofrecen una experiencia similar a una app para sus usuarios. Se logran a través de tecnologías como Service Workers y diseños con capacidad de respuesta, que les permiten brindar casi la misma experiencia de usuario que las aplicaciones nativas.

La API Twilio Notify le permite enviar notificaciones a sus usuarios a través de distintos canales: Web, SMS, Android e iOS, mediante una única API.

En este artículo, vamos a crear una receta de PWA con Laravel y Vue.js, en la que se puede notificar a nuestros usuarios cuando haya una nueva publicación disponible mediante la API de Twilio Notify.

Requisitos previos

Para comenzar con este tutorial, necesitará las siguientes dependencias:

Introducción a la aplicación de ejemplo

Para comenzar, ejecute el siguiente comando para crear una nueva aplicación de Laravel y cámbielo en el directorio:

$ laravel new recipe-pwa && cd recipe-pwa

Modelo de usuario y migración

Nuestra receta para la app requiere dos modelos: User (que nos ayudará a adaptar las notificaciones al canal preferido del usuario) y Recipe. Dado que Laravel ya generó el modelo User para nosotros, solo necesitamos modificarlo para que se ajuste a nuestras necesidades de aplicación. Abra el archivo de migración de User en el directorio database/migrations y actualice el método up como se muestra a continuación.

public function up()
{
   Schema::create('users', function (Blueprint $table) {
           $table->bigIncrements('id');
          $table->string('name');
          $table->string('email')->unique();
             $table->string('password');
             // Twilio Notify identity for this user
             $table->string('notification_id')->nullable();
             $table->rememberToken();
             $table->timestamps();
   });
}

Además, actualice el atributo $fillable de la clase de modelo User en el directorio de app como se muestra.

protected $fillable = [
'name', 'email', 'password', 'notification_id'
];

Modelo de receta y migración

Cree el modelo Recipe con su archivo de migración correspondiente mediante el indicador -m.

$ php artisan make:model Recipe -m

A continuación, abra el archivo de clase de modelo Recipe creado recientemente y reemplace el contenido con lo siguiente.

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use League\CommonMark\CommonMarkConverter;

class Recipe extends Model
{
        use Notifiable;

        protected $fillable = ['title', 'body'];

        public static function getBodyAttribute($value) {
            $converter = new CommonMarkConverter([
                'html_input' => 'strip',
                'allow_unsafe_links' => false
            ]);
            return $converter->convertToHtml($value);
        }

        protected $dispatchesEvents = [
            'saved' => \App\Events\RecipeEvent::class
        ];
}

Nuestras recetas deben contener textos con formato, como listas y encabezados. Logramos esto utilizando Mutators en el método getBodyAttribute que aparece arriba. Básicamente, la cadena del cuerpo se almacena en anotaciones y se transforma en HTML mediante el paquete de CommonMarkConverter que viene con Laravel.

A continuación, actualice el archivo de migración de recetas ubicado en la carpeta database/migrations.

<?php

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

class CreateRecipesTable extends Migration
{
        public function up()
        {
            Schema::create('recipes', function (Blueprint $table) {
                $table->bigIncrements('id');
                $table->string('title');
                $table->text('body');
                $table->timestamps();
            });
        }

        public function down()
        {
            Schema::dropIfExists('recipes');
        }
}

Bases de datos y ejecución de nuestras migraciones

A continuación, vamos a configurar la base de datos de la aplicación. Abra el archivo .env y actualice las credenciales de la base de datos según se indica a continuación.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=recipe_pwa
DB_USERNAME=YOUR_MYSQL_USERNAME
DB_PASSWORD=YOUR_MYSQL_PASSWORD

Siga con los cambios a nuestra base de datos mediante la ejecución de las migraciones.

$ php artisan migrate

Sincronización de la base de datos de la aplicación

Necesitamos agregar algunos datos de ejemplo a las tablas que acabamos de crear para que nos ayuden a concentrarnos en desarrollar las otras partes de nuestra aplicación. Genere los sincronizadores necesarios con el siguiente comando:

$ php artisan make:seeder UsersTableSeeder
$ php artisan make:seeder RecipesTableSeeder

Abra el archivo UsersTableSeeder (database/seeds/UsersTableSeeder.php) y reemplace el contenido con lo siguiente:

<?php

use Illuminate\Database\Seeder;

class UsersTableSeeder extends Seeder
{
        public function run()
        {
            \App\User::insert([
                [
                    'name' => "Akande Salami",
                    'email' => "test@example.com",
                    'password' => "super_secret",
                    'created_at' => \Carbon\Carbon::now(),
                    'updated_at' => \Carbon\Carbon::now()
                ],
                [
                    'name' => "Jeff Doe",
                    'email' => "jdoe@example.com",
                    'password' => "super_secret",
                    'created_at' => \Carbon\Carbon::now(),
                    'updated_at' => \Carbon\Carbon::now()
                ]
            ]);
        }
}

A continuación, reemplace el contenido del archivo RecipesTableSeeder con lo siguiente:

<?php

use Illuminate\Database\Seeder;

class RecipesTableSeeder extends Seeder
{
        public function run()
        {
            \App\Recipe::insert([
                [
                    'title' => "Nigerian Jollof Rice with Chicken",
                    'body' => "Test Recipe. Chicken is first sauteed on the stove top to produce a wonderful aromatic base for the rice.",
                    'created_at' => \Carbon\Carbon::now(),
                    'updated_at' => \Carbon\Carbon::now()
                ],
                [
                    'title' => "Chicken Suya Salad",
                    'body' => "- Cut the yam tuber into 1 inch slices. - Peel and cut the slices into half moons.
                            - Wash the slices, place in a pot and pour water to cover the contents.
                            - Boil till the yam is soft.",
                    'created_at' => \Carbon\Carbon::now(),
                    'updated_at' => \Carbon\Carbon::now()
                ]
            ]);
        }
}

Para que nuestros sincronizadores sean detectables en Laravel, abra el archivo database/seeds/DatabaseSeeder.php y actualice su método de ejecución al código que aparece a continuación:

public function run()
{
            $this->call(UsersTableSeeder::class);
            $this->call(RecipesTableSeeder::class);
}

Luego, aplique los sincronizadores ejecutando los siguientes comandos en su terminal.

$ php artisan db:seed

Creación de nuestros controladores

Nuestra aplicación necesita tres controladores:

  • RecipeController: es responsable de administrar las rutas relacionadas con las recetas
  • SPAController: para cargar y representar nuestro shell de app antes de entregarlo al Vue Router, y
  • NotificationController: que gestionará nuestras acciones relacionadas con notificaciones.

Cree los controladores con los siguientes comandos:

$ php artisan make:controller RecipeController
$ php artisan make:controller NotificationController
$ php artisan make:controller SPAController

A continuación, abra el RecipeController recién generado (app/Http/Controllers/RecipeController.php) y agregue lo siguiente:

<?php

namespace App\Http\Controllers;

use App\Recipe;
use Illuminate\Http\Request;

class RecipeController extends Controller
{
        public function index() {
            return response()->json(Recipe::all());
        }

        public function store(Request $request)  {
            $recipe = Recipe::create([
                'title' => $request->get('title'),
                'body' => $request->get('body')
            ]);
            $data = [
                'status' => (bool) $recipe,
                'message' => $recipe ? "Recipe created" : "Error creating recipe",
                'data' => $recipe
            ];
            return response()->json($data);
        }

        public function getRecipe($id) {
            return response()->json(Recipe::find($id));
        }
}

Luego, registramos los métodos en el RecipeController con nuestra ruta de API. Abra routes/api.php y agregue las siguientes rutas:

Route::group(['prefix' => 'recipes'], function() {
    Route::get('/{id}', 'RecipeController@getRecipe');
    Route::get('/', 'RecipeController@index');
    Route::post('/', 'RecipeController@store');
});

NOTA: Puede leer más acerca de los grupos de rutas y la clave de prefijo anterior en la documentación de Laravel.

Prueba de endpoints con cURL

Probaremos nuestros puntos finales de API con cURL, incluso si las solicitudes se pueden replicar fácilmente con Postman. Estas pruebas ayudan a garantizar que las solicitudes a nuestra aplicación se manejen de la manera correcta, y obtenemos las respuestas esperadas antes de usarlas en el front-end orientado al usuario. Asegúrese de que el servidor Laravel ya esté en ejecución (puede iniciarlo con php artisan serve) antes de ejecutar los comandos que se indican a continuación.

Creación de una nueva receta

Ejecute el siguiente comando para crear una nueva receta mediante cURL.

$ curl -X POST -d '{"title": "Demerara Brownies", "body": "Test Recipe. Preheat oven to 325 degrees F (165 degrees C)"}' -H 'Content-type: application/json' -H 'Accept: application/json' http://localhost:8000/api/recipes

Debe obtener una respuesta similar a la que se muestra a continuación. El cuerpo de la receta también se transforma en HTML debido a los mutators de Eloquent que implementamos anteriormente.

{"status":true,"message":"Recipe created","data":{"title":"Demerara Brownies","body":"<p>Test Recipe. Preheat oven to 325 degrees F (165 degrees C)<\/p>\n","updated_at":"2020-02-25 06:14:33","created_at":"2020-02-25 06:14:33","id":3}}

Obtención de todas las recetas

Para recuperar una matriz de JSON de todas nuestras recetas (que corresponde a nuestra ruta de índice), ejecute el siguiente comando en su terminal:

$ curl http://localhost:8000/api/recipes

El resultado de respuesta debe ser similar al siguiente:

{"id":1

Obtención de una sola receta

A continuación, recuperamos una sola instancia de receta por su ID, como es la siguiente:

$ curl http://localhost:8000/api/recipes/1

Y deberíamos obtener un resultado similar al que se muestra a continuación

{"id":1,"title":"Nigerian Jollof Rice with Chicken","body":"<p>Test Recipe. Chicken is first sauteed on the stove top to produce a wonderful aromatic base for the rice.<\/p>\n","created_at":"2020-02-25 05:59:56","updated_at":"2020-02-25 05:59:56"}

Cómo aprovechar la integración de Vue y Laravel

Ahora que tenemos una API que funciona, creemos nuestro front-end de la aplicación con Vue.js. Laravel facilita la tarea de trabajar con Vue.js y todas las herramientas de creación complementarias listas para usar. Para instalar el andamiaje de Vue, ejecute el preset de Artisan e instale las dependencias actualizadas con lo siguiente:

$ composer require laravel/ui --dev
$ php artisan ui vue && npm install

Debido a que nuestra app Vue está acoplada con la actividad en segundo plano de Laravel, usaremos una ruta web de Laravel para cargar el shell de la app y Vue Router gestionará las piezas restantes. Abra routes/web.php y reemplace el contenido con lo siguiente:

<?php
Route::get('/{any}', 'SPAController@index')->where('any', '.*');

A continuación, abra el archivo de clase SPAController que creamos anteriormente y agregue la implementación del método index al que se hace referencia en la ruta anterior.

<?php
namespace App\Http\Controllers;

class SPAController extends Controller
{

        public function index() {
            return view('welcome');
        }
}

Tenga en cuenta que aquí estamos reutilizando la plantilla predeterminada welcome de Laravel aquí. Abra el archivo de plantilla (en resources/views/welcome.blade.php) y reemplácelo con el siguiente código:

<!DOCTYPE html>
<html>
<head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Recipe PWA</title>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
        <link href="https://fonts.googleapis.com/css?family=Nunito:200,600" rel="stylesheet">
</head>
<body>
<div id="app">
        <app></app>
</div>
<script src="{{mix('js/app.js')}}"></script>
</body>
</html>

También debemos configurar Vue Router para gestionar nuestra navegación dentro de la app en el front-end. Instale el paquete vue-router con npm install vue-router y actualice el archivo resources/js/app.js al siguiente código:

require('./bootstrap');

window.Vue = require('vue');

import VueRouter from 'vue-router';

import App from './components/App'
import Home from "./components/Home";
import Recipe from './components/Recipe'

Vue.use(VueRouter);

const router = new VueRouter({
        mode: 'history',
        routes: [
            {
                path: '/',
                name: 'home',
                component: Home
            },
            {
                path: '/recipes/:recipeId',
                name: 'recipe',
                component: Recipe
            }
        ]
});

const app = new Vue({
        el: '#app',
        components: {App},
        router
});

A continuación, vamos a crear los componentes que necesitaremos. App.vue servirá como contenedor para todos los demás componentes, Home.vue será responsable de mostrar la lista de recetas y Recipe.vue hará una sola receta.

Cree el archivo resources/js/components/App.vue y agregue lo siguiente.

<template>
        <div>
            <div class="sticky-top alert alert-primary" v-if="requestPermission"
                 v-on:click="enableNotifications">
                Want to know when we publish a new recipe?
                <button class="btn btn-sm btn-dark">Enable Notifications</button>
            </div>
            <nav class="navbar navbar-expand-md navbar-light navbar-laravel">
                <div class="container">
                    <router-link :to="{name: 'home'}" class="navbar-brand">Recipe PWA</router-link>
                    <button
                        class="navbar-toggler"
                        type="button"
                        data-toggle="collapse"
                        data-target="#navbarSupportedContent"
                        aria-controls="navbarSupportedContent"
                        aria-expanded="false"
                    >
                        <span class="navbar-toggler-icon"></span>
                    </button>
                    <div class="collapse navbar-collapse" id="navbarSupportedContent">
                        <ul class="navbar-nav ml-auto">
                            <li>
                                <router-link :to="{name: 'home'}" class="nav-link">Recipes</router-link>
                            </li>
                        </ul>
                    </div>
                </div>
            </nav>

            <div class="py-4">
                <router-view></router-view>
            </div>
        </div>
</template>

Del mismo modo, cree el archivo de componente Home en resources/js/components/Home.vue y agregue el código que aparece a continuación.

<template>
        <div class="container">
            <div class="row justify-content-center">
                <div class="col-md-8" v-for="recipe,index in recipes" :key="recipe.id">
                    <div class="my-4">
                        <h5>
                            <router-link :to="{ name: 'recipe', params: {recipeId: recipe.id}}">
                                {{recipe.title}}
                            </router-link>
                        </h5>
                        <small class="text-muted">Posted on: {{recipe.created_at}}</small>
                    </div>
                </div>
            </div>
        </div>
</template>

<script>
        import axios from 'axios';
        export default {
            data(){
                return {
                  recipes: []
                }
            },

            mounted() {
                axios.defaults.headers.common['Content-type'] = "application/json";
                axios.defaults.headers.common['Accept'] = "application/json";

                axios.get('api/recipes').then(response => {
                    response.data.forEach((data) => {
                        this.recipes.push({
                            id: data.id,
                            body: data.body.slice(0, 100) + '...',
                            created_at: data.created_at,
                            title: data.title
                        })
                    });
                })
            }
        }
</script>

Luego, cree el componente Recipe para mostrar una sola receta en resources/js/components/Recipe.vue y agregue el bloque de código que aparece a continuación.

<template>
        <div class="container">
            <div class="row justify-content-center">
                <div class="col-md-8">
                    <div class="my-4">
                        <h4>{{ recipe.title }}</h4>
                        <div v-html="recipe.body"></div>
                    </div>
                </div>
            </div>
        </div>
</template>
<script>
import axios from 'axios';

export default {
        data() {
            return {
                recipe: {}
            }
        },

        mounted() {
            axios.defaults.headers.common['Content-type'] = "application/json";

            axios.defaults.headers.common['Accept'] = "application/json";
            let recipeUrl = `/api/recipes/${this.$route.params.recipeId}`;
            axios.get(recipeUrl).then(response => {
                this.recipe = response.data;
            })
        }
}
</script>

Ejecute npm run watch en la carpeta raíz del proyecto para compilar los activos de front-end y escuchar las modificaciones de esos archivos. En una terminal separada, inicie el servidor PHP con php artisan serve. Ahora puede acceder a la app en http://localhost:8000 para ver lo que hemos logrado hasta ahora.

Convertir nuestra app Vue.js a una PWA

Las aplicaciones web progresivas (o PWA para abreviar) se alimentan con tres componentes principales:

  • Un archivo JSON declarativo de una app web estructurada (generalmente denominado manifest.json o manifest.webmanifest) que le indica al navegador que nuestro sitio web es realmente una PWA y que se puede instalar. También especifica los metadatos y las configuraciones de la aplicación, como display, que indica cómo se comporta nuestra app después de la instalación.
  • Los Service Workers, en realidad, son scripts en segundo plano responsables de ejecutar servicios que no requieren la interacción de nuestros usuarios. Algunos de estos servicios son la sincronización en segundo plano y la escucha de las notificaciones push.
  • Una estrategia de almacenamiento en caché que nos ayuda a especificar cómo queremos que nuestros service workers gestionen los recursos después de obtenerlos. Dichas estrategias incluyen primero en caché, solo en caché, primero en la red, etc. Puede encontrar más detalles sobre las diferentes estrategias en Offline Cookbook de Google.

Para informar a los navegadores de nuestra PWA, cree un archivo manifest.json en el directorio public de Laravel y agregue lo siguiente.

{
        "name": "Recipe PWA",
        "short_name": "Recipe PWA",
        "icons": [
            {
                  "src": "/recipe-book.png",
                  "sizes": "192x192",
                  "type": "image/png"
                }
        ],
        "start_url": "/",
        "scope": ".",
        "display": "standalone",
        "background_color": "#fff",
        "theme_color": "#000",
        "description": "You favorite recipe app",
        "dir": "ltr",
        "lang": "en-US"
}

NOTA: Recuerde descargar el ícono de la app desde aquí o actualizar el nombre del ícono si está usando uno personalizado.

Con nuestra declaración de app web en su lugar, vamos a consultarlo desde el welcome.blade.php. Agregue lo siguiente a la sección head del archivo de plantilla.

...
<link rel="manifest" href="manifest.json" />
...

Configuración del Service Worker

Utilizaremos Workbox para que nos ayude a automatizar el proceso de administración de nuestro service worker. Instale la workbox-cli de manera global con lo siguiente:

$ npm install -g workbox-cli

Una vez instalado, inicie el asistente de Workbox en la raíz del proyecto con el siguiente comando y seleccione la carpeta public como la raíz web, ya que es la carpeta expuesta por Laravel.

$ workbox wizard

Configuración de Workbox con el asistente de Workbox

El comando generará un archivo de configuración que contiene la ubicación del archivo del service worker de su preferencia y los archivos que se deben almacenar en caché según sus respuestas a la indicación.

Genere el archivo final del service worker en public/sw.js con el siguiente comando:

$ workbox generateSW

A continuación, registramos el service worker en el archivo welcome.blade.php. Agregue el código que aparece a continuación en el archivo de plantilla justo después de cerrar la etiqueta </div> y justo antes de la etiqueta de script que carga app.js

...
if ('serviceWorker' in navigator) {
            window.addEventListener('load', () => {
                navigator.serviceWorker.register('/sw.js');
            });
        }

Nuestra aplicación debe registrar el service worker cuando visitamos http://localhost:8000 en el navegador, aunque es posible que deba forzar la actualización para obtener los archivos de activos actualizados.

Ejemplo de service worker

Configuración de Firebase y Twilio Notify

Twilio Notify utiliza Firebase Cloud Messaging (FCM) como la base para las notificaciones push web, por lo tanto, debemos crear un nuevo proyecto de Firebase en la consola de Firebase (o utilizar uno existente). Copie las credenciales de la app (Firebase Sender ID y clave del servidor) desde su consola (es decir, Project Overview [Descripción general del proyecto] > Project Settings [Configuración del proyecto] > Cloud Messaging [Mensajería en la nube]) y agregue la clave del servidor como una credencial push en Add a Credential Page (Agregar una página de credencial) en la consola de Twilio. Establezca el Credential Type (Tipo de credencial) en FCM y pegue la clave del servidor en el cuadro de texto “FCM Secret” (Secreto de FCM).

Además, cree un nuevo servicio Notify (servicio de notificación) en la consola de Twilio y seleccione la credencial push que creó como FCM Credential SID (SID de credencial de FCM). Tome nota del Service SID (SID de servicio) generado, ya que lo utilizaremos en breve.

A continuación, actualice su archivo .env del proyecto como en el bloque de código que aparece a continuación:

# Your Twilio API Key
TWILIO_API_KEY="SKXXXXXXXXXXXXXXXXXXXXXXX"
# Your Twilio API Secret
TWILIO_API_SECRET="XXXXXXXXXXXXXXXXXXXXXXXXX"
# Your Twilio account SID
TWILIO_ACCOUNT_SID="ACXXXXXXXXXXXXXXXXXXXXXXX"
# The service SID for the Notify service we just created
TWILIO_NOTIFY_SERVICE_SID="ISXXXXXXXXXXXXXXXXXX"

NOTA: Se puede acceder al ejemplo de la configuración de Firebase desde la sección “Your apps” (Sus app) en Project Overview (Descripción general del proyecto) > Settings (Configuración) > General (General), ya que incluye las credenciales que utilizaremos para usar la API.

Firebase ofrece un paquete NPM que facilita el uso de todas sus funciones en el código. Instale el paquete con npm install firebase@^7.8.2 para asegurarse de que la versión coincida con la que especificaremos en el archivo del service worker (es decir firebase-messaging-sw.js). A continuación, actualizaremos el componente App a lo siguiente:

  • Solicite el permiso de notificación (después de hacer clic en el botón “Enable Notifications” [Habilitar notificaciones]).
  • Guarde el permiso en el almacenamiento local para que no se le vuelva a preguntar.
  • Configure la mensajería de Firebase para gestionar las notificaciones cuando nuestra app web esté en primer plano.

Logramos eso anexando el bloque de código que aparece a continuación a App.vue, justo después de la etiqueta </template> de cierre.

<script>
        import firebase from "firebase/app";
        import axios from "axios";
        import "firebase/messaging";

        export default {
            data() {
                return {
                    // use a getter and setter to watch the user's notification preference in local storage
                    get requestPermission() {
                        return (localStorage.getItem("notificationPref") === null)
                    },
                    set requestPermission(value) {
                        localStorage.setItem("notificationPref", value)
                    }
                }
            }
            ,
            methods: {
                registerToken(token) {
                    axios.post(
                        "/api/register-token",
                        {
                            token: token
                        },
                        {
                            headers: {
                                "Content-type": "application/json",
                                Accept: "application/json"
                            }
                        }
                    ).then(response => {
                        console.log(response)
                    });
                },

                enableNotifications() {
                    if (!("Notification" in window)) {
                        alert("Notifications are not supported");
                    } else if (Notification.permission === "granted") {
                        this.initializeFirebase();
                    } else if (Notification.permission !== "denied") {
                        Notification.requestPermission().then((permission) => {
                            if (permission === "granted") {
                                this.initializeFirebase();
                            }
                        })
                    } else {
                        alert("No permission to send notification")
                    }
                    this.requestPermission = Notification.permission;
                },

                initializeFirebase() {
                    if (firebase.messaging.isSupported()) {
                        let config = {
                            apiKey: "FIREBASE_API_KEY",
                            authDomain: "FIREBASE_AUTH_DOMAIN",
                            projectId: "FIREBASE_PROJECT_ID",
                            messagingSenderId: "FIREBASE_MESSENGER_ID",
                            appId: "FIREBASE_APP_ID",
                        };
                        firebase.initializeApp(config);
                        const messaging = firebase.messaging();

                        messaging.getToken()
                            .then((token) => {
                                console.log(token);
                                this.registerToken(token)
                            })
                            .catch((err) => {
                                console.log('An error occurred while retrieving token. ', err);
                            });

                        messaging.onMessage(function (payload) {
                            console.log("Message received", payload);
                            let n = new Notification("New Recipe alert!")
                        });
                    }
                }
            }
        };
</script>

Al inicializarse, Firebase busca un archivo firebase-messaging-sw.js de acceso público que alberga el service worker, además de gestionar las notificaciones cuando nuestra app está en segundo plano. Cree este archivo en la carpeta public de Laravel y agregue lo siguiente:

importScripts("https://www.gstatic.com/firebasejs/7.8.2/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/7.8.2/firebase-messaging.js");

let config = {
        apiKey: "FIREBASE_API_KEY",
        authDomain: "FIREBASE_AUTH_DOMAIN",
        projectId: "FIREBASE_PROJECT_ID",
        messagingSenderId: "FIREBASE_MESSENGER_ID",
        appId: "FIREBASE_APP_ID",
};
firebase.initializeApp(config);
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(function (payload) {
        console.log(' Received background message ', payload);
        let title = 'Recipe PWA',
            options = {
                body: "New Recipe Alert",
                icon: "https://raw.githubusercontent.com/idoqo/laravel-vue-recipe-pwa/master/public/recipe-book.png"
            };
        return self.registration.showNotification(
            title,
            options
        );
});

Creación de vinculaciones de dispositivos

El SDK PHP de Twilio ofrece un contenedor que nos ayuda a comunicarnos con la API de Twilio. En este artículo, esa comunicación incluye el registro del navegador del usuario con Twilio Notify (es decir, la creación de una vinculación de dispositivos), además de informar cuándo es el momento de enviar notificaciones.

Instale el SDK como una dependencia de Composer con lo siguiente:

$ composer require twilio/sdk

A continuación, agregaremos un método público al NotificationsController que acepta el token de dispositivo del usuario generado por Firebase y lo registraremos en nuestro servicio de notificación en Twilio mediante el SDK. Vea cómo se hace a continuación:

<?php
namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use PHPUnit\Exception;
use Twilio\Rest\Client;

class NotificationsController extends Controller
{
        public function createBinding(Request $request) {
            $client = new Client(getenv('TWILIO_API_KEY'), getenv('TWILIO_API_SECRET'),
                getenv('TWILIO_ACCOUNT_SID'));
            $service = $client->notify->v1->services(getenv('TWILIO_NOTIFY_SERVICE_SID'));

            $request->validate([
                'token' => 'string|required'
            ]);
            $address = $request->get('token');

            // we are just picking the user with id = 1,
            // ideally, it should be the authenticated user's id e.g $userId = auth()->user()->id
            $user = User::find(1);
            $identity = sprintf("%05d", $user->id);
            // attach the identity to this user's record
            $user->update(['notification_id' => $identity]);
            try {
// the fcm type is for firebase messaging, view other binding types at https://www.twilio.com/docs/notify/api/notification-resource
                $binding = $service->bindings->create(
                    $identity,
                    'fcm',
                    $address
                );
                Log::info($binding);
                return response()->json(['message' => 'binding created']);
            } catch (Exception $e) {
                Log::error($e);
                return response()->json(['message' => 'could not create binding'], 500);
            }
        }
}

NOTA: En el código anterior, estamos utilizando una instancia de usuario de ejemplo. Lo ideal sería enlazar el token a un usuario autenticado en su lugar. Si desea admitir la autenticación en su API de Laravel/Lumen, los Documentos de Laravel Passport y la API de Twilio Authy son buenos lugares para comenzar.

Emita nuevas recetas con Laravel Events

Laravel Events nos ofrece una manera de buscar los eventos que se producen dentro de nuestra aplicación. Estos eventos incluyen la creación, las actualizaciones y las eliminaciones de modelos. En este artículo, usaremos Events para enviar notificaciones a nuestros usuarios cuando se publique una nueva receta. Cree el nuevo evento con php artisan make:event RecipeEvent y agregue el siguiente código:

<?php

namespace App\Events;

use App\Recipe;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class RecipeEvent
{
        use Dispatchable, InteractsWithSockets, SerializesModels;

        public $recipe;

        public function __construct(Recipe $recipe) {
                    $this->recipe = $recipe;
        }

        public function broadcastOn() {
                    return [];
        }
}

Registre el RecipeEvent en la clase EventServiceProvider que ofrece Laravel mediante la apertura del archivo app/Providers/EventServiceProvider y cambie su atributo $listen al código que aparece a continuación:

...
protected $listen = [
    'App\Events\RecipeEvent' => [
             'App\Listeners\RecipeEventListener'
   ],
];
...

Luego, genere el oyente especificado anteriormente ejecutando lo siguiente:

$ php artisan event:generate

El comando debe crear un archivo RecipeEventListener.php en el directorio app/Listeners. Abra el archivo e implemente nuestra lógica de escucha de eventos, como se muestra a continuación.

<?php

namespace App\Listeners;

use App\Events\RecipeEvent;
use App\User;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Client;

class RecipeEventListener
{
        public function __construct()
        {
        }

        public function handle(RecipeEvent $event)
        {
            $recipe = $event->recipe;
            $users = User::where('notification_id', '!=', null);
            $identities = $users->pluck('notification_id')->toArray();
            $client = new Client(getenv('TWILIO_API_KEY'), getenv('TWILIO_API_SECRET'),
                getenv('TWILIO_ACCOUNT_SID'));
            try {
                $n = $client->notify->v1->services(getenv('TWILIO_NOTIFY_SERVICE_SID'))
                    ->notifications
                    ->create([
                        'title' => "New recipe alert",
                        'body' => $recipe->title,
                        'identity' => $identities
                    ]);
                Log::info($n->sid);
            } catch (TwilioException $e) {
                Log::error($e);
            }
        }
}

En el método handle anterior, estamos enviando la notificación a todos los usuarios que tienen un notification_id configurado, es decir, los usuarios que han otorgado permisos de notificación a la app y, como tales, ya tienen una identidad vinculada a ellos en nuestro servicio de Twilio Notify.

Cree los archivos JS con npm run prod e inicie el servidor PHP en caso de que aún no se esté ejecutando con php artisan serve. Siga con el comando cURL a continuación para crear una nueva receta:

$ curl -X POST -d '{"title": "Creamy Chicken Masala", "body": "Yet another test recipe. -In a shallow bowl, season flour with salt and pepper. Dredge chicken in flour."}' -H 'Content-type: application/json' -H 'Accept: application/json' http://localhost:8000/api/recipes

Debe recibir una notificación en el dispositivo registrado, y la notificación debe iniciar sesión en su consola de servicio de Twilio Notify.

NOTA: Debido a que las PWA requieren https para funcionar de la forma correcta, tendrá que iniciar un túnel ngrok en el puerto 8000 para poder acceder a la app desde un dispositivo distinto de su máquina de desarrollo (es decir, para que sea accesible desde otro lugar que no sea el host local).

Conclusión

La API de Twilio Notify nos ayuda a enviar notificaciones a nuestros usuarios en diferentes plataformas y, en este artículo, hemos visto cómo se puede utilizar con PWA. Puede encontrar el código fuente completo para este tutorial en Github y, si tiene una inquietud o una pregunta, puede crear una inquietud nueva en el repositorio.

Este artículo fue traducido del original "Progressive Website App Notifications with Laravel, Vue.js, and Twilio Notify". Mientras estamos en nuestros procesos de traducción, nos encantaría recibir sus comentarios en help@twilio.com - las contribuciones valiosas pueden generar regalos de Twilio.