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:
@@ -222,13 +222,63 @@ class TenantPanelProvider extends PanelProvider
|
||||
$reverbPort = config('broadcasting.connections.reverb.options.port');
|
||||
$reverbScheme = config('broadcasting.connections.reverb.options.scheme', 'https');
|
||||
$broadcastEnabled = config('broadcasting.default') === 'reverb' && $reverbKey && $reverbHost;
|
||||
$vapidPublic = config('webpush.vapid.public_key');
|
||||
$csrf = csrf_token();
|
||||
@endphp
|
||||
<button id="autocrm-install" type="button" style="display:none;position:fixed;bottom:16px;right:16px;z-index:60;background:#3b82f6;color:#fff;border:0;border-radius:24px;padding:10px 18px;font-size:13px;font-weight:600;box-shadow:0 4px 12px rgba(0,0,0,.2);cursor:pointer;">
|
||||
⤓ Instalează aplicația
|
||||
</button>
|
||||
<script>
|
||||
// Service worker + Web Push subscription.
|
||||
const AUTOCRM_VAPID = @json($vapidPublic);
|
||||
function urlBase64ToUint8Array(b64) {
|
||||
const pad = '='.repeat((4 - b64.length % 4) % 4);
|
||||
const base64 = (b64 + pad).replace(/-/g, '+').replace(/_/g, '/');
|
||||
const raw = atob(base64);
|
||||
return Uint8Array.from([...raw].map(c => c.charCodeAt(0)));
|
||||
}
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js').catch(() => {});
|
||||
window.addEventListener('load', async () => {
|
||||
try {
|
||||
const reg = await navigator.serviceWorker.register('/sw.js');
|
||||
if (AUTOCRM_VAPID && 'PushManager' in window) {
|
||||
const perm = await Notification.requestPermission().catch(() => 'default');
|
||||
if (perm === 'granted') {
|
||||
let sub = await reg.pushManager.getSubscription();
|
||||
if (!sub) {
|
||||
sub = await reg.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(AUTOCRM_VAPID),
|
||||
});
|
||||
}
|
||||
await fetch('/push/subscribe', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ $csrf }}' },
|
||||
body: JSON.stringify(sub.toJSON()),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) { /* push optional */ }
|
||||
});
|
||||
}
|
||||
// PWA install prompt.
|
||||
let deferredPrompt = null;
|
||||
const installBtn = document.getElementById('autocrm-install');
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
e.preventDefault();
|
||||
deferredPrompt = e;
|
||||
if (installBtn) installBtn.style.display = 'block';
|
||||
});
|
||||
if (installBtn) {
|
||||
installBtn.addEventListener('click', async () => {
|
||||
if (!deferredPrompt) return;
|
||||
deferredPrompt.prompt();
|
||||
await deferredPrompt.userChoice;
|
||||
deferredPrompt = null;
|
||||
installBtn.style.display = 'none';
|
||||
});
|
||||
}
|
||||
window.addEventListener('appinstalled', () => { if (installBtn) installBtn.style.display = 'none'; });
|
||||
</script>
|
||||
@if ($broadcastEnabled && $tenant)
|
||||
<script src="https://js.pusher.com/8.4/pusher.min.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user