diff --git a/src/App.vue b/src/App.vue index 119f4a6..25c4def 100644 --- a/src/App.vue +++ b/src/App.vue @@ -381,14 +381,14 @@ const pwaInstallCopy = computed(() => { ? 'Install the app when prompted, then continue signup inside L484.' : 'Open Chrome menu, choose Install app or Add to Home screen, then launch L484 from the app icon.' return deferredInstallPrompt.value - ? 'Install L484 as a desktop app when prompted, then continue signup.' - : 'Use your browser menu or address bar install icon to install L484, then open the installed app.' + ? 'Use the browser install prompt to install L484, then continue signup.' + : 'Install L484 using the browser install icon in the address bar. The install prompt appears here when the browser makes it available.' }) const pwaInstallPrimaryLabel = computed(() => { if (isPwaStandalone.value) return 'Continue signup' if (deferredInstallPrompt.value) return 'Install app' if (installPlatform.value === 'ios') return 'I installed it' - return 'Continue after install' + return 'Install app' }) const membershipBtcAmount = computed(() => bitcoinUsdPrice.value ? MEMBERSHIP_MONTHLY_USD / bitcoinUsdPrice.value : 0, @@ -774,6 +774,11 @@ const handlePwaInstallPrimary = async () => { return } + if (installPlatform.value === 'desktop') { + formError.value = 'The browser install prompt is not available yet. Use the install icon in the address bar, or refresh once and press Install app again.' + return + } + formError.value = installPlatform.value === 'ios' ? 'Install L484 from Safari using Share, Add to Home Screen, then reopen it from the app icon.' : 'Install L484 from your browser menu or address bar, then reopen the installed app to continue.' diff --git a/src/services/notifications.js b/src/services/notifications.js index 2721d1e..196f219 100644 --- a/src/services/notifications.js +++ b/src/services/notifications.js @@ -2,14 +2,14 @@ import { ref } from 'vue' const permission = ref(typeof Notification === 'undefined' ? 'unsupported' : Notification.permission) -const urlBase64ToUint8Array = (base64String) => { +const urlBase64ToArrayBuffer = (base64String) => { const clean = String(base64String || '').trim().replace(/^["']|["']$/g, '') const padding = '='.repeat((4 - (clean.length % 4)) % 4) const base64 = (clean + padding).replace(/-/g, '+').replace(/_/g, '/') const rawData = window.atob(base64) const key = new Uint8Array([...rawData].map((char) => char.charCodeAt(0))) if (key.length !== 65 || key[0] !== 4) throw new Error('VAPID public key is not valid.') - return key + return key.buffer } export const notificationPermission = permission @@ -34,13 +34,16 @@ const subscribeWithRetry = async (registration, applicationServerKey) => { try { return await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey }) } catch (error) { - await registration.update().catch(() => {}) - await new Promise((resolve) => window.setTimeout(resolve, 350)) + await registration.unregister().catch(() => {}) + await new Promise((resolve) => window.setTimeout(resolve, 500)) + const freshRegistration = await navigator.serviceWorker.register('/sw.js', { scope: '/' }) + const readyRegistration = await navigator.serviceWorker.ready + const activeRegistration = readyRegistration || freshRegistration try { - return await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey }) + return await activeRegistration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey }) } catch { const message = error instanceof Error ? error.message : 'Push service error.' - throw new Error(`Push registration failed: ${message}`) + throw new Error(`Push registration failed: ${message}. Close and reopen the installed app, then try again.`) } } } @@ -54,14 +57,14 @@ export const subscribeToNotifications = async () => { const keyData = await keyResponse.json().catch(() => ({})) if (!keyResponse.ok || !keyData.publicKey) throw new Error('VAPID public key is not configured.') if (!keyData.configured) throw new Error('VAPID private key is not configured on the server.') - const applicationServerKey = urlBase64ToUint8Array(keyData.publicKey) + const applicationServerKey = urlBase64ToArrayBuffer(keyData.publicKey) const requested = await Notification.requestPermission() permission.value = requested if (requested !== 'granted') throw new Error('Notification permission was not granted.') - const registration = await navigator.serviceWorker.register('/sw.js') - await navigator.serviceWorker.ready + await navigator.serviceWorker.register('/sw.js', { scope: '/' }) + const registration = await navigator.serviceWorker.ready const existing = await registration.pushManager.getSubscription() if (existing) return saveSubscription(existing) diff --git a/src/style.css b/src/style.css index 6efa255..92d121d 100644 --- a/src/style.css +++ b/src/style.css @@ -1476,7 +1476,7 @@ body.menu-open { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; - gap: 1rem; + gap: clamp(0.45rem, 1.2vw, 1rem); border-bottom: 1px solid rgba(255, 255, 255, 0.1); background: rgba(5, 5, 5, 0.88); padding: 0.65rem var(--admin-page-x); @@ -1490,19 +1490,19 @@ body.menu-open { .admin-profile-button { display: flex; align-items: center; - gap: 0.75rem; + gap: clamp(0.45rem, 0.8vw, 0.75rem); border: 1px solid rgba(255, 255, 255, 0.13); border-radius: 8px; background: rgba(255, 255, 255, 0.055); - padding: 0.55rem 0.7rem; + padding: 0.55rem clamp(0.5rem, 0.8vw, 0.7rem); color: white; text-align: left; } .admin-avatar { display: grid; - width: 2.25rem; - height: 2.25rem; + width: clamp(1.9rem, 2.4vw, 2.25rem); + height: clamp(1.9rem, 2.4vw, 2.25rem); place-items: center; border: 1px solid rgba(255, 255, 255, 0.16); border-radius: 999px; @@ -1517,14 +1517,14 @@ body.menu-open { } .admin-profile-text strong { - font-size: 0.82rem; + font-size: clamp(0.68rem, 0.9vw, 0.82rem); line-height: 1; } .admin-profile-text small { color: rgba(255, 255, 255, 0.48); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; - font-size: 0.68rem; + font-size: clamp(0.58rem, 0.75vw, 0.68rem); } .admin-profile-menu { @@ -1586,8 +1586,9 @@ body.menu-open { .admin-tabbar { display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); - gap: 0.5rem; + grid-auto-flow: column; + grid-auto-columns: minmax(0, 1fr); + gap: clamp(0.22rem, 0.7vw, 0.5rem); min-width: 0; } @@ -1616,10 +1617,10 @@ body.menu-open { } .desktop-admin-tabbar .admin-tab { - gap: 0.42rem; - padding: 0.64rem 0.5rem; - font-size: 0.62rem; - letter-spacing: 0.1em; + gap: clamp(0.22rem, 0.5vw, 0.42rem); + padding: 0.62rem clamp(0.28rem, 0.7vw, 0.5rem); + font-size: clamp(0.5rem, 0.75vw, 0.62rem); + letter-spacing: clamp(0.055em, 0.15vw, 0.1em); } .admin-tab svg {