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.phprenderiza la directiva@laravelPWAen 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.jscomo service worker con alcance/. Una vista que no debe llevarla declara una secciónavoidPwavací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_DEBUGes falso:SESSION_SECURE_COOKIEtoma por omisión el valor deAPP_PWAySESSION_SAME_SITEtoma por omisiónnone, 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:
- De dónde cargar el ícono base. El valor por omisión es
public/másconfig('app.icon'), que esimages/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. - El color de fondo primario de las pantallas splash, un valor hexadecimal. Por omisión
#fff. - 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}.pngpor cada clave demanifest.icons.public/images/icons/splash-{width}x{height}.pngpor cada clave demanifest.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:
- Revisa
window.isSecureContext. Si es falso, el interruptor se deshabilita con "Abre este sitio con HTTPS para activar las notificaciones push". - Revisa que el navegador tenga
serviceWorker,PushManageryNotification. Si no: "Este navegador no soporta notificaciones push". - Si
Notification.permissionesdenied, 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". - 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
- Pon
APP_PWA=trueen.envy correphp artisan webpush:vapid. - Abre el sitio en un origen seguro.
php artisan serveenhttp://localhost:8000cuenta; un dominio.testno, a menos que lo sirvas por HTTPS. PonAPP_URLen el origen que uses para que las rutas y el manifiesto coincidan. - Inicia sesión, abre
/accounty enciende "Notificación push". Acepta el aviso del navegador. La línea de estado dice "Las notificaciones push están activadas en este navegador". - 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:
- Despliega a producción con los íconos, pantallas splash y capturas finales.
- Entra a pwabuilder.com y escribe tu URL de producción. Lee
/manifest.jsony reporta lo que falta. - 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. - La descarga incluye un
assetlinks.json. Cópialo apublic/.well-known/assetlinks.jsony despliega otra vez.public/.well-known/ya existe con un.htaccessque hace que Apache sirva ahí los archivos.jsoncon 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.