Cargando…

Esto está tardando más de lo esperado.

Volver al centro de ayuda

Web push y la PWA

Haz el sitio instalable, genera sus íconos y deja que un navegador reciba notificaciones push.

Esta guía cubre la capa de Progressive Web App: el manifiesto, el service worker, los íconos, las llaves VAPID detrás de web push, cómo se suscribe un navegador y cómo se instala el resultado en un teléfono. La necesitas cuando las notificaciones tienen que llegarle a un usuario con la pestaña cerrada, o cuando el producto tiene que vivir en una pantalla de inicio.

Enciéndela

APP_PWA=true

config/app.php la lee en app.pwa, apagada por omisión. Cuando está encendida:

  • resources/views/layouts/base.blade.php renderiza la directiva @laravelPWA en el <head>: el enlace al manifiesto, el color del tema, las etiquetas meta de pantalla de inicio de Android e iOS, las imágenes splash y un script que registra /sw.js como service worker con alcance /. Una vista que no debe llevarla declara una sección avoidPwa vacía.
  • La página de perfil muestra el interruptor de push, siempre que la llave pública VAPID también esté definida.
  • App\Http\Middleware\IsPWABuilder, en la ruta de inicio, manda a un visitante que llega desde la aplicación móvil instalada directo al tablero en vez de a la página de inicio.
  • La cookie de sesión cambia de forma cuando APP_DEBUG es falso: SESSION_SECURE_COOKIE toma por omisión el valor de APP_PWA y SESSION_SAME_SITE toma por omisión none, para que la sesión sobreviva dentro de la aplicación instalada. Ambas necesitan HTTPS, que la PWA necesita de todos modos. Defínelas explícitamente si quieres otra cosa.

El paquete detrás es silviolleite/laravelpwa. Sirve /manifest.json (ruta laravelpwa.manifest) a partir de config/laravelpwa.php, y /offline, que renderiza resources/views/vendor/laravelpwa/offline.blade.php, una página dentro del layout de la aplicación que dice que el dispositivo no tiene conexión.

El manifiesto

config/laravelpwa.php es el manifiesto. El paquete copia las claves de primer nivel tal cual y cada clave bajo custom literalmente a la raíz del JSON, así que cualquier cosa que la especificación del manifiesto permita puede ir bajo custom. Lo que cambias:

Clave Valor por omisión Qué hace
manifest.name APP_NAME Nombre completo, mostrado en el aviso de instalación y en la pantalla splash
manifest.short_name APP_NAME Nombre bajo el ícono de la pantalla de inicio; mantenlo por debajo de doce caracteres
manifest.start_url / Página con la que abre la aplicación instalada
manifest.display standalone standalone oculta la interfaz del navegador; minimal-ui conserva atrás y recargar; browser no instala nada
manifest.background_color #ffffff Color detrás de la pantalla splash mientras la aplicación carga
manifest.theme_color #000000 Color de la barra de estado de Android y de la barra de título de la ventana
manifest.orientation any Fíjala con portrait o landscape
manifest.status_bar black Estilo de la barra de estado de iOS: default, black o black-translucent
manifest.icons ocho tamaños de 72 a 512 Una entrada por tamaño, ruta bajo public/; purpose es any o maskable
manifest.splash diez tamaños de iOS Una imagen por clase de dispositivo, emparejada con los dispositivos por las etiquetas meta
manifest.shortcuts dos marcadores Menú de pulsación larga del ícono en Android. Reemplaza las entradas /shortcutlink1 con rutas reales o pon la clave en []
custom.id mx.weblabor.base Identidad estable de la aplicación aunque cambie la URL; también el nombre de paquete Android que PWABuilder propone. Cámbialo a tu dominio invertido
custom.description vacío Texto bajo el nombre en las tiendas y los diálogos de instalación
custom.lang APP_LOCALE, por omisión es Idioma del manifiesto
custom.dir ltr Dirección del texto
custom.scope APP_URL URLs que la aplicación instalada posee; lo que quede fuera se abre en el navegador
custom.scope_extensions *. más el host de APP_URL Subdominios que la aplicación también posee
custom.handle_links preferred Abrir los enlaces a tu dominio en la aplicación instalada
custom.launch_handler.client_mode auto Si un nuevo lanzamiento enfoca la ventana existente
custom.display_override window-controls-overlay, minimal-ui Modos de presentación que se intentan antes de display, en orden
custom.categories [] Categorías de tienda, por ejemplo ['business', 'productivity']
custom.prefer_related_applications false Déjalo en false o Android ofrece la aplicación de la tienda en vez de instalar la PWA
custom.related_applications el propio manifiesto Aplicaciones nativas del mismo producto; agrega tu entrada de Play Store cuando exista
custom.edge_side_panel.preferred_width 400 Ancho al abrirse en el panel lateral de Edge
custom.screenshots un marcador Imágenes del diálogo de instalación en Chrome de escritorio y Android. El marcador apunta a /images/screenshots/wide/1.jpeg, que no existe: agrega tus propios archivos o el diálogo muestra una imagen rota. form_factor es wide para escritorio o narrow para teléfonos

El manifiesto es un archivo de configuración, así que después de editarlo en producción corre php artisan config:cache otra vez.

Íconos y pantallas splash

Cada ruta de manifest.icons y manifest.splash debe existir bajo public/. El kit trae un juego completo en public/images/icons/ con el ícono de Weblabor Base. Reemplázalos con un comando:

php artisan pwa:generate-assets

Hace tres preguntas:

  1. De dónde cargar el ícono base. El valor por omisión es public/ más config('app.icon'), que es images/icon.svg. El generador lee el archivo con el driver GD, que no lee SVG: responde con la ruta a un PNG o JPG cuadrado de al menos 512×512 píxeles.
  2. El color de fondo primario de las pantallas splash, un valor hexadecimal. Por omisión #fff.
  3. El color de fondo secundario. Por omisión #f2f2f2.

Se detiene con un error, y no escribe nada, cuando el archivo no existe, no es cuadrado, mide menos de 512 píxeles, o un color no tiene tres o seis dígitos hexadecimales. Luego escribe, sobrescribiendo lo que haya:

  • public/images/icons/icon-{size}.png por cada clave de manifest.icons.
  • public/images/icons/splash-{width}x{height}.png por cada clave de manifest.splash: un degradado vertical del color primario al secundario, con el ícono centrado al treinta y cinco por ciento del lado más corto.

Lee los tamaños de la configuración, así que agregar un tamaño a la configuración y correr el comando otra vez basta. El servidor o la máquina que lo corre necesita la extensión gd de PHP. Committea los archivos generados; son assets estáticos.

También puedes dejar tus propios archivos en esas rutas. Nada los regenera solo.

El service worker

public/sw.js es un archivo estático, no generado. Al instalarse cachea /offline, el logo, los ocho íconos, la fuente de íconos y cada asset listado en public/build/manifest.json. El nombre del caché incluye el día y la hora, así que se construye un caché nuevo cada hora y los anteriores se borran al activarse. Las peticiones se responden desde el caché cuando está presente, y si no se descargan; un archivo descargado bajo /build/ se agrega al caché. Una navegación de página sin red cae a /offline; un archivo sin red recibe un 404.

El mismo archivo maneja push. En un evento push lee el JSON del mensaje y muestra una notificación del sistema con el título, el cuerpo, el ícono y el patrón de vibración, o "Notification" como título cuando el mensaje viene vacío. En notificationclick cierra la notificación y abre data.url, que es donde termina el url() de la notificación.

El navegador sólo registra un service worker en un origen seguro: HTTPS, o localhost. http://your-project.test no lo es.

Llaves VAPID

Web push autentica tu servidor ante los servicios push de los navegadores con un par de llaves VAPID. Génerálo una vez por proyecto:

php artisan webpush:vapid

El comando escribe VAPID_PUBLIC_KEY y VAPID_PRIVATE_KEY en .env, agregando las líneas cuando faltan. Cuando ya existen llaves y APP_ENV es production pregunta antes de sobrescribir (--force se salta la pregunta); en cualquier otro lugar sobrescribe sin preguntar, así que córrelo una sola vez. --show imprime el par en vez de tocar el archivo, que es lo que quieres en un servidor donde .env se administra en otro lado.

config/webpush.php lee:

Variable Valor por omisión Propósito
VAPID_PUBLIC_KEY ninguno Se envía al navegador cuando se suscribe. Sin ella el interruptor de push se oculta y el botón "Prueba" advierte que push no está configurado
VAPID_PRIVATE_KEY ninguno Firma cada petición push. Nunca la rotes a la ligera: cada suscripción existente se hizo contra la mitad pública
VAPID_SUBJECT APP_URL Un mailto: o URL con el que los servicios push pueden contactarte por abuso. El valor por omisión está bien
VAPID_PEM_FILE ninguno Ruta a un archivo PEM con la misma llave privada, para instalaciones que guardan las llaves en archivos. Sólo se lee cuando las dos llaves de arriba también están definidas; una ruta que empieza con storage se resuelve desde la raíz del proyecto
WEBPUSH_DB_TABLE push_subscriptions Tabla que guarda las suscripciones de los navegadores
WEBPUSH_DB_CONNECTION DB_CONNECTION Conexión de base de datos de esa tabla
WEBPUSH_AUTOMATIC_PADDING true Rellena cada mensaje a la misma longitud para que su tamaño no revele nada. Ponlo en false sólo si debes soportar Firefox en Android con un endpoint v1

Las llaves son las mismas en todos los entornos donde se suscriban los mismos usuarios: un servidor de staging con sus propias llaves no puede hacer push a un navegador que se suscribió en producción, ni al revés.

Cómo se suscribe un navegador

El usuario lo hace desde su perfil (/account), en la tarjeta "Notificaciones", con el interruptor "Notificación push". Es un componente Alpine definido en resources/views/livewire/auth/my-profile.blade.php y corre esta secuencia:

  1. Revisa window.isSecureContext. Si es falso, el interruptor se deshabilita con "Abre este sitio con HTTPS para activar las notificaciones push".
  2. Revisa que el navegador tenga serviceWorker, PushManager y Notification. Si no: "Este navegador no soporta notificaciones push".
  3. Si Notification.permission es denied, el usuario bloqueó el sitio antes y sólo la configuración de sitios del navegador puede deshacerlo: "Las notificaciones están bloqueadas en este navegador".
  4. Espera a que el service worker esté listo y lee pushManager.getSubscription() para saber si este navegador ya está suscrito.

Encender el interruptor llama a Notification.requestPermission(), luego a pushManager.subscribe({ userVisibleOnly: true, applicationServerKey }), donde la llave es la llave pública VAPID decodificada a bytes por el helper pwa_public_key(). La suscripción resultante se envía a POST /webpush/subscribe (ruta pwa.webpush, sólo usuarios con sesión). App\Http\Controllers\WebPushController::subscribe() valida endpoint, keys.auth y keys.p256dh y llama a $user->updatePushSubscription($endpoint, $key, $token).

Apagarlo cancela la suscripción en el navegador, luego envía DELETE /webpush/subscribe con el endpoint, y deletePushSubscription() elimina la fila.

La fila vive en push_subscriptions: el modelo dueño (subscribable_type, subscribable_id), un endpoint único de hasta 500 caracteres, public_key, auth_token, content_encoding y marcas de tiempo. El modelo es NotificationChannels\WebPush\PushSubscription, y App\Models\User obtiene pushSubscriptions(), updatePushSubscription() y deletePushSubscription() del trait HasPushSubscriptions. Un usuario tiene una fila por navegador, así que un teléfono y una laptop reciben ambos el push.

Cuando un servicio push responde que una suscripción expiró, el canal borra su fila. Un navegador que se reinstaló o revocó el permiso se limpia solo en el siguiente envío.

Enviar

Nada cambia de tu lado. Una notificación que extiende App\Notifications\Notification llega a cada navegador suscrito del destinatario porque channelNotifications() agrega el canal de web push cuando pushSubscriptions() tiene filas (/help/send-notifications). El push se envía en la misma petición que el resto de la notificación a menos que tu notificación implemente ShouldQueue.

Prueba un push en local

  1. Pon APP_PWA=true en .env y corre php artisan webpush:vapid.
  2. Abre el sitio en un origen seguro. php artisan serve en http://localhost:8000 cuenta; un dominio .test no, a menos que lo sirvas por HTTPS. Pon APP_URL en el origen que uses para que las rutas y el manifiesto coincidan.
  3. Inicia sesión, abre /account y enciende "Notificación push". Acepta el aviso del navegador. La línea de estado dice "Las notificaciones push están activadas en este navegador".
  4. Pulsa el botón "Prueba" que aparece junto al interruptor. Envía App\Notifications\TestWebPushNotification, sólo push, con el título "Las notificaciones están funcionando". El botón advierte en su lugar cuando falta la llave pública o este usuario no tiene suscripción.

Los administradores tienen la misma prueba en la DevZone (/help/the-devzone) como "Test WebPush Notification", junto a los botones "Test as iOS" y "Test as Android" (sus etiquetas no están traducidas en pantalla): esos ponen un platform_override en la sesión para que is_ios() e is_android() devuelvan verdadero y puedas ver lo que ve la aplicación instalada sin un teléfono.

Instalar en iOS

Safari no pregunta. El usuario abre el sitio, toca Compartir y luego "Agregar a pantalla de inicio". El ícono es la entrada más grande de manifest.icons (la etiqueta meta apple-touch-icon), el título es short_name, la imagen splash se elige de manifest.splash según el tamaño del dispositivo, y la barra de estado sigue a status_bar.

Los límites son de Apple. Push funciona sólo para la copia en la pantalla de inicio, no en una pestaña de Safari, y sólo en iOS 16.4 o posterior. El usuario tiene que suscribirse desde dentro de esa copia instalada: una suscripción hecha en Safari no se traslada.

Cuando el producto llega a la App Store a través de PWABuilder, el envoltorio pone una cookie app-platform, e is_ios() la lee. El kit la usa para ocultar los enlaces de precios de la página de inicio dentro de la aplicación de iOS, ya que Apple no permite vender ahí una suscripción fuera de su propio sistema de pago.

Instalar en Android

Chrome ofrece "Instalar aplicación" por su cuenta una vez que el sitio se sirve por HTTPS, tiene un service worker registrado, y el manifiesto tiene un nombre, un ícono de 192 y uno de 512, un start_url y un display distinto de browser. Todo eso está en su lugar una vez que APP_PWA=true y los íconos existen.

Para un paquete de Play Store:

  1. Despliega a producción con los íconos, pantallas splash y capturas finales.
  2. Entra a pwabuilder.com y escribe tu URL de producción. Lee /manifest.json y reporta lo que falta.
  3. Genera el paquete de Android. El id del paquete toma por omisión custom.id. En sus opciones activa la delegación de notificaciones, para que el envoltorio le entregue el permiso de push a la capa web.
  4. La descarga incluye un assetlinks.json. Cópialo a public/.well-known/assetlinks.json y despliega otra vez. public/.well-known/ ya existe con un .htaccess que hace que Apache sirva ahí los archivos .json con el tipo correcto. Sin este archivo Android no confía en la aplicación para tu dominio y muestra la barra del navegador dentro de ella.

El envoltorio de Android abre tu sitio con un referer android-app://, que es lo que is_android() revisa.

Para iOS a través de PWABuilder, descarga el proyecto de Xcode, edita los textos de permisos en su Info.plist, y quita la capacidad de audio en segundo plano antes de enviarlo.