feat(push): wire Capacitor native push into the web UI
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 <noreply@anthropic.com>
This commit is contained in:
+5
-2
@@ -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.
|
||||
|
||||
|
||||
@@ -721,7 +721,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<script src="/icons.js?v=4"></script>
|
||||
<script>window.__BUILD='2026-06-30-batch14';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);</script>
|
||||
<script>window.__BUILD='2026-06-30-batch15';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);</script>
|
||||
<div class="loading" id="loading">Loading…</div>
|
||||
|
||||
<header>
|
||||
@@ -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<raw.length;i++) arr[i]=raw.charCodeAt(i); return arr; }
|
||||
// ----- Native push (Capacitor app: FCM on Android, APNs on iOS) -----
|
||||
// In a native shell, Capacitor injects window.Capacitor and exposes the PushNotifications
|
||||
// plugin. We register the OS device token with /api/v1/devices so the server can deliver via
|
||||
// FCM/APNs (see server/push.js). No bundler needed — plugins live on Capacitor.Plugins.
|
||||
function capPlugin(name){ const C=window.Capacitor; return (C && C.isNativePlatform && C.isNativePlatform() && C.Plugins && C.Plugins[name]) ? C.Plugins[name] : null; }
|
||||
function nativePlatform(){ const C=window.Capacitor; if(C && C.getPlatform){ const p=C.getPlatform(); if(p==='ios'||p==='android') return p; } if(window.__NATIVE__==='desktop') return 'desktop'; return ''; }
|
||||
let _nativeTok=null;
|
||||
async function setupNativePush(){
|
||||
const PN=capPlugin('PushNotifications'); const plat=nativePlatform();
|
||||
if(!PN || (plat!=='ios' && plat!=='android')) return false; // not a mobile native app
|
||||
try{
|
||||
PN.addListener('registration', async (t)=>{ 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;
|
||||
|
||||
Viittaa uudesa ongelmassa
Block a user