diff --git a/frontend/dashboard.css b/frontend/dashboard.css index f1e88d5..9972189 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -18,6 +18,16 @@ button { background: linear-gradient(180deg,#1f3a5f,#15324d); border:1px solid # .active-devices-header h2, .active-devices-header p { margin-top:0; } .active-devices-header button, .active-device button, .enrolled-passkey button { min-height:44px; } .active-devices-list { display:grid; gap:10px; margin-top:16px; } +.device-setup-sheet { position:fixed; inset:0; z-index:95; display:flex; align-items:flex-end; justify-content:center; background:rgba(5,12,21,.82); backdrop-filter:blur(4px); } +.device-setup-sheet[hidden] { display:none; } +.device-setup-panel { box-sizing:border-box; width:min(620px,100%); max-height:100dvh; overflow:auto; overflow-x:hidden; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); border:1px solid #31577f; border-radius:18px 18px 0 0; background:#0b1526; } +.device-setup-header { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; } +.device-setup-header h2, .device-setup-header p { margin-top:0; } +.device-setup-header button, .device-setup-action { min-height:44px; } +.device-setup-list { display:grid; gap:10px; margin:14px 0; padding:0; list-style:none; } +.device-setup-step { min-width:0; display:grid; grid-template-columns:minmax(0,1fr) auto; gap:12px; align-items:center; padding:12px; border:1px solid #2a496e; border-radius:12px; background:#10233a; overflow-wrap:anywhere; } +.device-setup-step p { margin:5px 0 0; } +.device-setup-ready { margin:0; padding:12px; border-radius:10px; background:#0f2237; color:#bfdbfe; font-weight:700; } .active-device { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:8px; align-items:center; padding:12px; border:1px solid #2a496e; border-radius:12px; background:#0f2237; } .active-device strong, .active-device span { display:block; overflow-wrap:anywhere; } .active-device-current { color:#55d6be; font-weight:700; } @@ -463,6 +473,9 @@ textarea { resize: vertical; min-height: 120px; } .app-menu:not([open]) > .app-menu-panel { display:none; } .app-menu-panel { position:absolute; top:calc(100% + 6px); right:0; width:min(280px,calc(100vw - 20px)); display:grid; gap:8px; padding:10px; border:1px solid #2a496e; border-radius:12px; background:#0b1526; box-shadow:0 18px 45px rgba(0,0,0,.5); } .app-menu-panel button { min-height:44px; width:100%; } + .device-setup-panel { width:100%; padding:14px; padding-bottom:calc(14px + env(safe-area-inset-bottom)); } + .device-setup-step { grid-template-columns:1fr; } + .device-setup-action { width:100%; } .my-work { margin:0; } .my-work-header { align-items:flex-start; } .my-work-actions { width:100%; flex-wrap:nowrap; } diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 53c70b9..4bc6592 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -418,6 +418,7 @@ urls: { 'issue-capture': document.querySelector('meta[name="stackchain-feature-issue-capture"]')?.content || '', 'push-notifications': document.querySelector('meta[name="stackchain-feature-push-notifications"]')?.content || '', + 'device-setup': document.querySelector('meta[name="stackchain-feature-device-setup"]')?.content || '', }, }); let sharedLaunchState = null; @@ -5066,20 +5067,25 @@ if (selectedReview && offlineReview) openReviewSheet(selectedReview, reviewTrigger); }); } - keepWorkOffline.addEventListener('change', async () => { - offlineWorkStore.setEnabled(keepWorkOffline.checked); - if (keepWorkOffline.checked && liveMode && lastContextSnapshot) { + async function setOfflineWorkEnabled(enabled) { + keepWorkOffline.checked = enabled; + offlineWorkStore.setEnabled(enabled); + if (enabled && liveMode && lastContextSnapshot) { const admitted = await offlineWorkStore.save({ ...lastContextSnapshot, notifications:lastNotifications, notification_pagination:notificationPagination }); if (admitted === false) { await updateOfflineWorkControls('Offline saving unavailable ยท retry after reconnect.'); - return; + return false; } } - if (!keepWorkOffline.checked) await offlineWorkStore.clear(); - await updateOfflineWorkControls(keepWorkOffline.checked ? 'Offline saving enabled.' : 'Offline work data cleared.'); - if (keepWorkOffline.checked) warmTodayOffline(); + if (!enabled) await offlineWorkStore.clear(); + await updateOfflineWorkControls(enabled ? 'Offline saving enabled.' : 'Offline work data cleared.'); + if (enabled) warmTodayOffline(); else offlineToday.cancel(); + return offlineWorkStore.enabled(); + } + keepWorkOffline.addEventListener('change', async () => { + await setOfflineWorkEnabled(keepWorkOffline.checked); }); deliveryReceipts.addEventListener('change', async () => { let enabled = deliveryReceipts.checked; @@ -5270,40 +5276,63 @@ renderMyWork(); if (workSession.active()) workSession.reconcile(); }); + let pushControllerReady = Promise.resolve(null); if ('serviceWorker' in navigator) { - navigator.serviceWorker.register('service-worker.js').then(async () => { + pushControllerReady = navigator.serviceWorker.register('service-worker.js').then(async () => { await issueCaptureFeatures.load('push-notifications'); - return createPushNotifications({ + const controller = createPushNotifications({ control:qs('#push-updates'), status:qs('#push-update-status'), notification:window.Notification, serviceWorker:navigator.serviceWorker, fetchJson:fetchReviewJson, - }).init(); + }); + await controller.init(); + return controller; }).catch(error => { qs('#push-update-status').textContent = 'Update notification settings unavailable.'; console.warn('Push notifications unavailable', error); + return null; }); } else { qs('#push-updates').disabled = true; qs('#push-update-status').textContent = 'This browser does not support update notifications.'; } - const isIosDevice = /iPad|iPhone|iPod/.test(navigator.userAgent) || - (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); - const isIosSafari = isIosDevice && /Safari/.test(navigator.userAgent) && - !/CriOS|FxiOS|EdgiOS|OPiOS/.test(navigator.userAgent); - createInstallApp({ - window, - card: qs('#install-app-card'), - installButton: qs('#install-app'), - dismissButton: qs('#dismiss-install-app'), - guidance: qs('#install-app-guidance'), - status: qs('#install-app-status'), - storage: localStorage, - isStandalone: () => window.matchMedia('(display-mode: standalone)').matches || - navigator.standalone === true, - isIosSafari: () => isIosSafari, - }).start(); + let deferredInstallPrompt = null; + window.addEventListener('beforeinstallprompt', event => { + event.preventDefault(); + deferredInstallPrompt = event; + }); + let deviceSetup = null; + qs('#open-device-setup').addEventListener('click', async event => { + if (deviceSetup) return; + await issueCaptureFeatures.load('device-setup'); + const isIosDevice = /iPad|iPhone|iPod/.test(navigator.userAgent) || + (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); + const installApp = createInstallApp({ + window, initialPrompt:deferredInstallPrompt, + card:qs('#install-app-card'), installButton:qs('#install-app'), + dismissButton:qs('#dismiss-install-app'), guidance:qs('#install-app-guidance'), + status:qs('#install-app-status'), storage:localStorage, + isStandalone:() => window.matchMedia('(display-mode: standalone)').matches || navigator.standalone === true, + isIosSafari:() => isIosDevice && /Safari/.test(navigator.userAgent) && !/CriOS|FxiOS|EdgiOS|OPiOS/.test(navigator.userAgent), + }); + installApp.start(); + deviceSetup = createMobileDeviceSetup.mount({ + document, installApp, + offlineAvailable:() => offlineStorageReady, + offlineEnabled:() => offlineWorkStore.enabled(), + enableOffline:() => setOfflineWorkEnabled(true), + enablePush:async () => { + const controller = await pushControllerReady; + if (!controller) return; + qs('#push-updates').checked = true; + await controller.change(); + }, + }); + deviceSetup.start(); + await deviceSetup.open(event); + }); contextPoller.start(); document.addEventListener('visibilitychange', () => { contextPoller.setVisible(!document.hidden); diff --git a/frontend/index.html b/frontend/index.html index 848d3ec..c0f22e3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -18,6 +18,7 @@
+Prepare this phone for fast launch, outages, and new update alerts. Nothing asks for permission until you choose it.
+