Notificações em Progressive Web App com Laravel, Vue.js e Twilio Notify

April 21, 2020
Escrito por
Michael Okoko
Contribuidor
As opiniões expressas pelos colaboradores da Twilio são de sua autoria
Revisado por

Notificações em Progressive Web App com Laravel, Vue.js e 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. 

Progressive Web Applications (PWA, aplicativos da web progressivos) são sites que podem ser instalados e fornecem uma experiência semelhante a um aplicativo para seus usuários. Somente são possíveis por meio de tecnologias como Service Workers e designs responsivos, que permitem que eles forneçam praticamente a mesma experiência do usuário que os aplicativos nativos.

API do Twilio Notify permite que você envie notificações para seus usuários em diferentes canais, como web, SMS, Android e iOS, usando uma única API.

Neste artigo, criaremos uma receita PWA com Laravel e Vue.js, com a capacidade de notificar nossos usuários quando uma nova publicação estiver disponível usando a API do Twilio Notify.

Pré-requisitos

Para começar este tutorial, você vai precisar das seguintes dependências:

Introdução ao aplicativo Sample

Para começar, execute o seguinte comando para criar um novo aplicativo Laravel e mude para o diretório:

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

Modelo User e migração

Nosso app de receitas requer dois modelos - User, que nos ajuda a personalizar as notificações para o canal preferido do usuário, e Recipe. Como o Laravel já gerou o modelo User, precisamos apenas modificá-lo para atender às nossas necessidades do aplicativo. Abra o arquivo de migração User no diretório database/migrations e atualize o método up conforme mostrado abaixo.

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();
   });
}

Além disso, atualize o atributo $fillable da classe de modelo User no diretório app conforme mostrado.

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

Modelo Recipe e migração

Crie o modelo Recipe com seu arquivo de migração correspondente usando o sinalizador -m.

$ php artisan make:model Recipe -m

Em seguida, abra o arquivo da classe de modelo recém-criado Recipe e substitua o conteúdo conforme abaixo.

<?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
        ];
}

Nossas receitas precisam conter textos formatados, como listas e cabeçalhos. Para isso, deve-se usar o Mutators no método getBodyAttribute acima. Essencialmente, a string do corpo é armazenada em markdown e transformada em HTML usando o pacote CommonMarkConverter que acompanha o Laravel.

Em seguida, atualize o arquivo de migração (migration) de receitas localizado na pasta 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');
        }
}

Bancos de dados e execução de nossas migrações (migrations)

Em seguida, configuramos o banco de dados do aplicativo. Abra o arquivo .env e atualize as credenciais do banco de dados conforme abaixo.

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

Continue as alterações em nosso banco de dados pela execução das migrations (migrações).

$ php artisan migrate

Como propagar o banco de dados de aplicativos

É preciso adicionar alguns dados fictícios às tabelas que acabamos de criar para ajudar a focar na criação das outras partes do nosso aplicativo. Gere os seeders (propagadores) necessários com o comando abaixo:

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

Abra o arquivo UsersTableSeeder (database/seeds/UsersTableSeeder.php) e substitua o conteúdo por:

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

Em seguida, substitua o conteúdo do arquivo RecipesTableSeeder pelo seguinte código:

<?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 tornar os seeders (propagadores) detectáveis para o Laravel, abra o arquivo database/seeds/DatabaseSeeder.php e atualize seu método de execução no código abaixo:

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

Em seguida, aplique os seeders (propagadores) ao executar o comando abaixo em seu terminal.

$ php artisan db:seed

Como criar nossos Controllers (controladores)

Nosso aplicativo precisa de três controllers (controladores):

  • RecipeController: responsável pelo gerenciamento das rotas relacionadas à receita
  • SPAController: carregar e renderizar nosso shell de app antes de entregar para Vue Router e
  • NotificationController: trata de nossas ações relacionadas a notificações.

Crie os controllers (controladores) com os seguintes comandos:

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

Em seguida, abra o recém-criado RecipeController (app/Http/Controllers/RecipeController.php) e adicione:

<?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));
        }
}

Em seguida, registramos os métodos no RecipeController com nossa rota de API. Abra routes/api.php e adicione as seguintes rotas:

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

OBSERVAÇÃO: você pode ler mais sobre grupos de rotas e a chave de prefixo acima aqui na documentação do Laravel.

Como testar endpoints com cURL

Testaremos nossos endpoints de API com cURL, embora as solicitações possam ser facilmente replicadas com o Postman. Esses testes ajudam a garantir que as solicitações no nosso aplicativo sejam tratadas corretamente e que tenhamos as respostas esperadas antes de usá-las no front-end voltado para o usuário. Confirme se seu servidor Laravel já está em execução (você pode iniciá-lo com php artisan serve) antes de executar os comandos abaixo.

Criar uma nova receita

Execute o comando abaixo para criar uma nova receita via 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

Você deve obter uma resposta semelhante à abaixo. O corpo da receita também é transformado em HTML pelos Eloquent mutators implementados 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}}

Como buscar todas as receitas

Para recuperar uma matriz JSON de todas as nossas receitas, que corresponde à nossa rota de índice, execute o seguinte comando no seu terminal:

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

O resultado da sua resposta deve ser semelhante a:

{"id":1

Como buscar uma única receita

Em seguida, recuperamos uma única instância de receita pelo seu ID, como abaixo:

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

E devemos obter um resultado semelhante ao abaixo

{"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"}

Como impulsionar a integração Vue-Laravel

Agora que temos uma API em funcionamento, vamos desenvolver nosso front-end de aplicativos usando o Vue.js. O Laravel facilita o trabalho com o Vue.js e todas as ferramentas de criação fornecidas prontas para uso. Para instalar o suporte Vue, execute o Artisan preset e instale as dependências atualizadas com:

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

Como nosso app Vue é acoplado ao back-end Laravel, usaremos uma rota da web do Laravel para carregar o shell do aplicativo e o roteador Vue cuida das outras partes. Abra routes/web.php e substitua o conteúdo por:

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

Depois, abra o arquivo de classe SPAController criado anteriormente e adicione a implementação do método index mencionado na rota acima.

<?php
namespace App\Http\Controllers;

class SPAController extends Controller
{

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

Observe que estamos reutilizando o modelo blade welcome padrão do Laravel aqui. Abra o arquivo de modelo em resources/views/welcome.blade.php e substitua-o pelo código abaixo:

<!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>

Também é preciso configurar o roteador Vue para lidar com nossa navegação no app no front-end. Instale o pacote vue-router com npm install vue-router e atualize o arquivo resources/js/app.js com o código abaixo:

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

Em seguida, vamos criar os componentes de que precisaremos. O App.vue serve como um contêiner para todos os outros componentes, Home.vue será responsável por exibir a lista de receitas e Recipe.vue processa uma única receita.

Crie o arquivo resources/js/components/App.vue e adicione:

<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>

Da mesma forma, crie o arquivo de componente Home em resources/js/components/Home.vue e adicione o código abaixo.

<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>

A seguir, crie o componente Recipe para mostrar uma receita única em resources/js/components/Recipe.vue e adicione o bloco de código abaixo nele.

<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>

Execute npm run watch na pasta raiz do projeto para compilar os ativos de front-end e ver as modificações nesses arquivos. Em um terminal separado, inicie o servidor PHP com o php artisan serve. Agora é possível acessar o aplicativo em http://localhost:8000 para ver o que conseguimos até agora.

Como converter o app Vue.js em um PWA

Os progressive web applications (ou PWAs) são acionados por três componentes principais:

  • Um arquivo de manifesto de aplicativo da web estruturado em JSON (geralmente denominado manifest.json ou manifest.webmanifest) que informa ao navegador que nosso site é, de fato, um PWA e pode ser instalado. Ele também especifica metadados e configurações de aplicativos, como display, que informa ao nosso aplicativo como se comportar após a instalação.
  • Service Workers que são, na verdade, scripts em segundo plano responsáveis pela execução de serviços que não exigem interação de nossos usuários. Alguns desses serviços são: a sincronização em segundo plano e a escuta de notificações por push.
  • Uma estratégia de armazenamento em cache que ajuda a especificar como queremos que nossos Service Workers lidem com os recursos depois de buscá-los. Essas estratégias incluem Cache-first, Cache-only, Network-first etc. É possível encontrar mais detalhes sobre as diferentes estratégias em The Offline Cookbook (O livro de receitas offline) da Google.

Para informar os navegadores de nosso PWA, crie o arquivo manifest.json no diretório do Laravel public e adicione o seguinte comando.

{
        "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"
}

OBSERVAÇÃO: lembre-se de baixar o ícone do app aqui ou atualizar o nome do ícone se estiver usando um personalizado.

Com nosso manifesto de aplicativo da web em execução, vamos fazer referência a partir de welcome.blade.php. Adicione o seguinte à seção head do arquivo de modelo.

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

Como configurar o Service Worker

Usaremos Workbox para nos ajudar a automatizar o processo de gerenciamento de nosso service worker. Instale a interface de linha de comando workbox-cli globalmente com:

$ npm install -g workbox-cli

Uma vez instalado, inicie o assistente Workbox na raiz do projeto com o comando abaixo e selecione a pasta public como sua raiz da web, pois ela é a pasta exposta pelo Laravel.

$ workbox wizard

Configurando o Workbox com o assistente do Workbox

O comando gera um arquivo de configuração que contém o local de seu arquivo de Service Worker preferido e os arquivos que devem ser armazenados em cache com base nas suas respostas ao prompt.

Gere o arquivo final do Service Worker em public/sw.js com o comando abaixo:

$ workbox generateSW

Em seguida, registramos o Service Worker no arquivo welcome.blade.php. Adicione o código abaixo no arquivo de modelo logo após a tag </div> de fechamento e logo antes da tag de script que carrega app.js

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

Nosso aplicativo deve registrar o Service Worker quando acessar http://localhost:8000 no navegador, embora talvez seja necessário forçar a atualização para buscar os arquivos de ativos atualizados.

Exemplo do Worker

Como configurar o Firebase e Twilio Notify

O Twilio Notify usa Firebase Cloud Messaging (FCM) como a base para suas notificações por push na web, portanto, precisamos criar um novo projeto Firebase no console do Firebase (ou usar um existente). Copie as credenciais do app (Firebase Sender ID (ID do remetente do Firebase) e Server Key (Chave do servidor)) do seu console (ou seja, Project Overview (Visão geral do projeto) > Project Settings (Configurações do projeto) > Cloud Messaging (Mensagens na nuvem)) e adicione Server Key (Chave do servidor) como uma Push Credential (Credencial por push) no console da Twilio Adicionar uma página de credencial. Defina o Credential Type (Tipo de credencial) como FCM e cole a chave do servidor na caixa de texto "FCM Secret" (Segredo FCM).

Além disso, crie um novo serviço de notificação no console da Twilio e selecione a credencial de envio que você acabou de criar como FCM Credential SID (SID da credencial de FCM). Anote o Service SID (SID de serviço) gerado, pois o usaremos em breve.

Em seguida, atualize seu arquivo .env do projeto como no bloco de código abaixo:

# 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"

OBSERVAÇÃO: a configuração de exemplo do Firebase está acessível na seção "Your apps" (Seus aplicativos) em Project Overview > Settings > General (Visão geral do projeto > Configurações > Geral), pois inclui as credenciais que usaremos para usar a API.

O Firebase fornece um pacote NPM que facilita o uso de todos os recursos em seu código. Instale o pacote com npm install firebase@^7.8.2 para garantir que a versão corresponda à que especificaremos no arquivo do service worker (ou seja, firebase-messaging-sw.js). Em seguida, atualizaremos o componente App para:

  • Solicite permissão de notificação (depois de clicar no botão "Enable Notifications" (Ativar notificações))
  • Salve a permissão no armazenamento local para que não pergunte novamente.
  • Configure as mensagens do firebase para lidar com notificações quando nosso aplicativo da web estiver em primeiro plano.

Conseguimos isso anexando o bloco de código abaixo ao App.vue, logo após o fechamento da tag </template>.

<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>

Na inicialização, o Firebase procura um arquivo firebase-messaging-sw.js acessível ao público que hospeda o service worker, bem como trata com notificações quando nosso aplicativo está em segundo plano. Crie este arquivo na pasta de Laravel public e adicione o seguinte:

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

Como criar ligações do dispositivo

SDK da Twilio PHP fornece um wrapper que ajuda a nos comunicar com a API da Twilio. Neste artigo, essa comunicação inclui o registro do navegador do usuário com a Twilio Notify (isto é criação de uma ligação de dispositivo), bem como informar ao Notify quando for a hora de enviar notificações.

Instale o SDK como uma dependência do composer com:

$ composer require twilio/sdk

Em seguida, adicionamos um método público ao NotificationsController que aceita o token do dispositivo do usuário gerado pelo Firebase e registramos em nosso serviço de notificação na Twilio usando o SDK. Veja como abaixo:

<?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);
            }
        }
}

OBSERVAÇÃO: no código acima, estamos usando uma instância de usuário fictícia. O ideal é vincular o token a um usuário autenticado. Se você deseja oferecer suporte à autenticação na API do Laravel/Lumen, Documentação do Laravel Passport e o API do Twilio Authy são bons lugares para começar.

Como transmitir novas receitas com eventos no Laravel

Os eventos do Laravel oferecem uma forma de ouvir eventos que ocorrem em nosso aplicativo. Esses eventos incluem a criação, as atualizações e as exclusões de modelos. Neste artigo, usamos Eventos para enviar notificações aos nossos usuários quando uma nova receita for publicada. Crie o novo evento com php artisan make:event RecipeEvent e adicione o seguinte 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 RecipeEvent na classe EventServiceProvider fornecida pelo Laravel, ao abrir o arquivo app/Providers/EventServiceProvider e ao alterar o atributo $listen para o código abaixo:

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

Em seguida, gere o ouvinte especificado acima pela execução:

$ php artisan event:generate

O comando deve criar um arquivo RecipeEventListener.php no diretório app/Listeners. Abra o arquivo e implemente nossa lógica de ouvinte de eventos, conforme mostrado abaixo.

<?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);
            }
        }
}

No método handle acima, enviamos a notificação a todos os usuários que têm um notification_id definido, ou seja, usuários que concederam permissões de notificação ao aplicativo e, como tal, já possuem uma identidade vinculada a eles em nosso serviço Twilio Notify.

Crie os arquivos JS com npm run prod e inicie o servidor PHP se ainda não estiver sendo executado com php artisan serve. Continue criando uma nova receita via cURL com o comando abaixo:

$ 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

Receba a notificação no dispositivo registrado e a notificação deve ser registrada no Console de serviço da Twilio Notify.

OBSERVAÇÃO: Como os PWAs precisam de https para funcionar corretamente, é preciso iniciar um túnel ngrok na porta 8000 para poder acessar o app a partir de um dispositivo separado da sua máquina de desenvolvimento (ou seja, para torná-lo acessível a partir de outro lugar que não o localhost (host local)).

Conclusão

A API da Twilio Notify ajuda a enviar notificações aos nossos usuários em diferentes plataformas e, neste artigo, vimos como pode ser usada com PWAs. É possível encontrar o código-fonte completo para este tutorial em Github e se tiver um problema ou uma questão, fique à vontade para criar um novo problema no repositório.

Este artigo foi traduzido do original "Progressive Website App Notifications with Laravel, Vue.js, and Twilio Notify". Enquanto melhoramos nossos processos de tradução, adoraríamos receber seus comentários em help@twilio.com - contribuições valiosas podem render brindes da Twilio.