Stage 15 — PWA complete: install prompt + Web Push notifications

Dependency:
- minishlink/web-push v10 (VAPID JWT + aes128gcm payload encryption)
- Dockerfile: add curl, mbstring, gmp extensions (web-push needs ext-curl)

VAPID:
- config/webpush.php from env; `php artisan push:vapid` generates keypair
- Shared platform keypair; .env.example has empty placeholders

Schema:
- push_subscriptions (user/company, endpoint unique, p256dh, auth, encoding)

WebPushService:
- send / sendToUser / dispatch via WebPush::flush
- Auto-prunes subscriptions reported expired (404/410)

Subscribe flow:
- POST /push/subscribe + /push/unsubscribe (auth, tenant)
- Tenant panel JS subscribes after SW registration with VAPID public key

Service worker (/sw.js):
- Cache v2, push listener → showNotification, notificationclick → focus/open

Install prompt:
- Floating "Instalează aplicația" button wired to beforeinstallprompt

Staff push:
- WorkOrder master_id change → push to assigned mechanic
- Settings "Test notificare push" action

Tests (6 new):
- subscribe stores + upserts; requires auth (401); validation (422);
  service configured; sendToUser with no subs returns zero

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 05:11:18 +00:00
parent e48ef1b755
commit c413004930
16 changed files with 840 additions and 8 deletions
@@ -0,0 +1,28 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Minishlink\WebPush\VAPID;
class GenerateVapidKeysCommand extends Command
{
protected $signature = 'push:vapid';
protected $description = 'Generate a VAPID keypair for Web Push and print the .env lines.';
public function handle(): int
{
$keys = VAPID::createVapidKeys();
$this->info('VAPID keys generated. Add these to your .env:');
$this->newLine();
$this->line('VAPID_SUBJECT=mailto:admin@service.mir.md');
$this->line('VAPID_PUBLIC_KEY=' . $keys['publicKey']);
$this->line('VAPID_PRIVATE_KEY=' . $keys['privateKey']);
$this->newLine();
$this->warn('Keep the private key secret. Re-generating invalidates existing subscriptions.');
return self::SUCCESS;
}
}