From 7ae0cacf74df45e3ee107356d57d2f9d2ad748f3 Mon Sep 17 00:00:00 2001 From: sravan Date: Tue, 30 Jun 2026 19:56:59 +0530 Subject: [PATCH] feat(push): wire Capacitor native push into the web UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit home.html: in a Capacitor app shell, setupPush() now uses the native FCM/APNs path instead of Web Push — requests permission, registers, POSTs the OS device token to /api/v1/devices, deep-links on notification tap (selectChat), and unregisters the token on logout. Web Notification prompts are suppressed on native. Fully inert in a normal browser (Web Push unchanged). build batch15. CLIENTS.md Phase B push items checked off. Co-Authored-By: Claude Opus 4.8 --- CLIENTS.md | 7 +++++-- server/public/home.html | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/CLIENTS.md b/CLIENTS.md index db389c6..f98e99e 100644 --- a/CLIENTS.md +++ b/CLIENTS.md @@ -60,8 +60,11 @@ absolute API base if offline-launch or store policy requires it. tokens are pruned on 404/410/UNREGISTERED. (db `device_tokens`, repo `deviceTokens`, `push.js`.) *Mobile app still needs the Capacitor push plugin wired + FCM/APNs creds to deliver end-to-end.* -- [ ] **Wire the Capacitor push plugin** in the mobile app → register the token via - `POST /api/v1/devices` on launch; handle taps (deep link to the chat/session). +- [x] **Capacitor push plugin wired** in the web UI (`setupNativePush` in home.html): + inside the app it requests permission, registers, and `POST`s the FCM/APNs token to + `/api/v1/devices`; notification taps deep-link via `selectChat`; logout unregisters the + token. Web Push is skipped when running natively. Inert in a normal browser. *Activates + once the Capacitor Android/iOS app is built and FCM/APNs creds are set.* - [ ] **Mobile screen capture** for "Share Screen" from a phone (ReplayKit / MediaProjection plugin). - [ ] **Deep links / universal links** so a session/meeting link opens the app. diff --git a/server/public/home.html b/server/public/home.html index 8137bfa..75ff167 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -721,7 +721,7 @@ - +
Loading…
@@ -1766,7 +1766,7 @@ async function sendMessage(){ // Request permission from a user gesture (e.g. opening a chat) AND subscribe on grant — the // subscribe-on-grant is essential on iOS, where permission is granted in-session and push // won't work until a subscription exists. -function ensureNotifyPermission(){ try{ if('Notification' in window && Notification.permission==='default') Notification.requestPermission().then(r=>{ if(r==='granted'){ try{ subscribePush(); }catch(_){} } }).catch(()=>{}); }catch(_){} } +function ensureNotifyPermission(){ try{ const p=nativePlatform(); if(p==='ios'||p==='android') return; if('Notification' in window && Notification.permission==='default') Notification.requestPermission().then(r=>{ if(r==='granted'){ try{ subscribePush(); }catch(_){} } }).catch(()=>{}); }catch(_){} } // Notification preferences (set in Settings; stored per browser). Default ON. function notifOn(kind){ try{ return localStorage.getItem('notif_'+kind)!=='off'; }catch(_){ return true; } } // Mobile has no top bar (#7): keep bell+profile in the chat-list header; desktop keeps them in @@ -1782,6 +1782,7 @@ function placeHdrRight(){ // iOS (permission must be requested from a tap inside the installed PWA). function maybeNotifPrompt(){ try{ + const p=nativePlatform(); if(p==='ios'||p==='android') return; // native app handles its own permission flow if(!('Notification' in window) || Notification.permission!=='default') return; if(sessionStorage.getItem('notifPromptDismissed')==='1' || document.getElementById('notifPrompt')) return; const b=document.createElement('div'); b.id='notifPrompt'; b.className='notif-prompt'; @@ -1796,7 +1797,33 @@ function maybeNotifPrompt(){ // pushActive => the page can leave hidden-tab popups to push (avoids double-notifying). let pushActive=false, _swReg=null; function urlB64ToUint8(base64){ const pad='='.repeat((4-base64.length%4)%4); const b=(base64+pad).replace(/-/g,'+').replace(/_/g,'/'); const raw=atob(b); const arr=new Uint8Array(raw.length); for(let i=0;i{ const token=t&&t.value; if(!token) return; _nativeTok=token; + try{ await postJSON('/api/v1/devices',{ platform:plat, token }); pushActive=true; console.log('[push] native device registered'); } + catch(e){ console.warn('[push] device register failed:', e); } }); + PN.addListener('registrationError', (e)=>console.warn('[push] native registration error:', e)); + // Tapping an OS notification opens the relevant chat (payload carries kind+id). + PN.addListener('pushNotificationActionPerformed', (a)=>{ try{ const d=(a&&a.notification&&a.notification.data)||{}; if(d.id) selectChat(d.kind||'dm', d.id); }catch(_){} }); + let perm=await PN.checkPermissions(); + if(perm.receive!=='granted') perm=await PN.requestPermissions(); + if(perm.receive!=='granted'){ console.log('[push] native permission not granted'); return true; } + await PN.register(); + console.log('[push] native push registered'); + }catch(e){ console.warn('[push] native push setup failed:', e); } + return true; // handled the native path (skip Web Push regardless of outcome) +} async function setupPush(){ + if(await setupNativePush()) return; // native app → FCM/APNs, not Web Push if(!('serviceWorker' in navigator) || !('PushManager' in window)){ console.warn('[push] not supported by this browser'); return; } try{ await navigator.serviceWorker.register('/sw.js'); }catch(e){ console.warn('[push] SW register failed:', e); return; } try{ _swReg=await navigator.serviceWorker.ready; }catch(e){ console.warn('[push] SW never became ready:', e); return; } // ensure an ACTIVE worker before subscribe() @@ -1819,6 +1846,8 @@ async function subscribePush(){ } // On logout, remove this device's push subscription so notifications STOP for the logged-out user. async function unsubscribePush(){ + // Native app: drop this device's FCM/APNs token so push stops for the logged-out user. + try{ if(_nativeTok){ await postJSON('/api/v1/devices/remove',{ token:_nativeTok }); _nativeTok=null; pushActive=false; return; } }catch(_){} try{ if(!_swReg){ try{ _swReg=await navigator.serviceWorker.getRegistration(); }catch(_){} } if(!_swReg) return;