From 42c28df102d0f7e915b4d4ad257d6b5aff9ca84c Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Mon, 20 Apr 2026 13:22:21 +0200 Subject: [PATCH 1/2] PWA: iOS-hjemskjermhint, kompass-feilmelding og zoom-fiks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fjern maximum-scale/user-scalable på viewport slik at siden kan zoomes (WCAG 1.4.4). Leaflet håndterer gestikk på kartet selv. - Legg til Apple-meta-tagger for standalone-modus og statuslinje. - Vis engangsbanner på iOS Safari om å legge til på hjemskjerm, siden iOS ikke støtter beforeinstallprompt. - Gi tydelig statusmelding når bruker avslår kompasstilgang i stedet for å stille reversere til kartmodus. Co-Authored-By: Claude Opus 4.7 (1M context) --- pwa/index.html | 9 +++++- pwa/src/app.ts | 6 +++- pwa/src/i18n/en.ts | 4 +++ pwa/src/i18n/nb.ts | 4 +++ pwa/src/i18n/nn.ts | 4 +++ pwa/src/main.ts | 5 ++++ pwa/src/styles/main.css | 35 ++++++++++++++++++++++ pwa/src/ui/install-hint.ts | 59 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 pwa/src/ui/install-hint.ts diff --git a/pwa/index.html b/pwa/index.html index 7440b65..8ce2047 100644 --- a/pwa/index.html +++ b/pwa/index.html @@ -2,9 +2,16 @@ - + + + + + + Tilfluktsrom diff --git a/pwa/src/app.ts b/pwa/src/app.ts index ceceb58..9e70099 100644 --- a/pwa/src/app.ts +++ b/pwa/src/app.ts @@ -101,10 +101,14 @@ function setupButtons(): void { const compassContainer = document.getElementById('compass-container')!; if (isCompassMode) { - // Request compass permission on first toggle (iOS requirement) + // Request compass permission on first toggle (iOS requirement). + // On denial we stay in map mode and surface a status message so the + // user understands why nothing happened — silent reverting is the + // failure mode the Android GPS-denied banner was designed to avoid. const granted = await compassProvider.requestPermission(); if (!granted) { isCompassMode = false; + statusBar.setStatus(t('compass_permission_denied')); return; } mapContainer.style.display = 'none'; diff --git a/pwa/src/i18n/en.ts b/pwa/src/i18n/en.ts index 6a77154..8d585eb 100644 --- a/pwa/src/i18n/en.ts +++ b/pwa/src/i18n/en.ts @@ -47,6 +47,10 @@ export const en: Record = { update_success: 'Shelter data updated', update_failed: 'Update failed \u2014 using cached data', error_shelter_not_found: 'Shelter not found', + compass_permission_denied: + 'Compass access denied. You can still use the map to find shelters.', + ios_install_hint: + 'Add Tilfluktsrom to your home screen for offline access: tap Share, then Add to Home Screen.', // Accessibility direction_arrow_description: 'Direction to shelter, %s away', diff --git a/pwa/src/i18n/nb.ts b/pwa/src/i18n/nb.ts index 8c285d8..2a00304 100644 --- a/pwa/src/i18n/nb.ts +++ b/pwa/src/i18n/nb.ts @@ -42,6 +42,10 @@ export const nb: Record = { update_success: 'Tilfluktsromdata oppdatert', update_failed: 'Oppdatering mislyktes — bruker lagrede data', error_shelter_not_found: 'Fant ikke tilfluktsrommet', + compass_permission_denied: + 'Kompasstilgang avslått. Du kan fortsatt bruke kartet til å finne tilfluktsrom.', + ios_install_hint: + 'Legg Tilfluktsrom til hjemskjermen for frakoblet bruk: trykk Del, deretter Legg til på hjem-skjerm.', // Tilgjengelighet direction_arrow_description: 'Retning til tilfluktsrom, %s unna', diff --git a/pwa/src/i18n/nn.ts b/pwa/src/i18n/nn.ts index eace6bd..6e3fa8f 100644 --- a/pwa/src/i18n/nn.ts +++ b/pwa/src/i18n/nn.ts @@ -42,6 +42,10 @@ export const nn: Record = { update_success: 'Tilfluktsromdata oppdatert', update_failed: 'Oppdatering mislukkast — brukar lagra data', error_shelter_not_found: 'Fann ikkje tilfluktsrommet', + compass_permission_denied: + 'Kompasstilgang avslått. Du kan framleis bruke kartet til å finne tilfluktsrom.', + ios_install_hint: + 'Legg Tilfluktsrom til heimeskjermen for fråkopla bruk: trykk Del, deretter Legg til på heimeskjerm.', // Tilgjenge direction_arrow_description: 'Retning til tilfluktsrom, %s unna', diff --git a/pwa/src/main.ts b/pwa/src/main.ts index 003d83f..6b1a920 100644 --- a/pwa/src/main.ts +++ b/pwa/src/main.ts @@ -13,6 +13,7 @@ import { initLocale } from './i18n/i18n'; import { init } from './app'; import { setStatus } from './ui/status-bar'; import { t } from './i18n/i18n'; +import { maybeShow as maybeShowIosInstallHint } from './ui/install-hint'; console.info(`[tilfluktsrom] build ${__BUILD_REVISION__}`); @@ -33,4 +34,8 @@ document.addEventListener('DOMContentLoaded', async () => { } await init(); + + // Shown only on first iOS Safari visit, once per device. Placed after init() + // so the banner doesn't compete with the loading overlay. + maybeShowIosInstallHint(); }); diff --git a/pwa/src/styles/main.css b/pwa/src/styles/main.css index a7e2dfb..353dd0f 100644 --- a/pwa/src/styles/main.css +++ b/pwa/src/styles/main.css @@ -199,6 +199,41 @@ html, body { font-size: 12px; } +/* --- iOS "Add to Home Screen" hint (shown once, dismissable) --- */ +#ios-install-hint { + position: fixed; + left: 12px; + right: 12px; + bottom: 12px; + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px; + background: #16213E; + color: #ECEFF1; + border: 1px solid #FF6B35; + border-radius: 8px; + font-size: 13px; + line-height: 1.3; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); + z-index: 1000; +} + +#ios-install-hint span { + flex: 1; +} + +#ios-install-hint button { + background: none; + border: 1px solid #ECEFF1; + color: #ECEFF1; + font-size: 12px; + padding: 4px 10px; + border-radius: 4px; + cursor: pointer; + flex-shrink: 0; +} + #cache-retry-btn { background: none; border: 1px solid #FFFFFF; diff --git a/pwa/src/ui/install-hint.ts b/pwa/src/ui/install-hint.ts new file mode 100644 index 0000000..abc17f2 --- /dev/null +++ b/pwa/src/ui/install-hint.ts @@ -0,0 +1,59 @@ +/** + * iOS-only "Add to Home Screen" hint. + * + * Chrome-based browsers fire `beforeinstallprompt` and we could show a native + * install UI there; iOS Safari does not. For iOS users the only way to install + * is Share → Add to Home Screen, so we show a dismissable textual hint the + * first time they visit in a non-standalone context. + * + * Shown once per device (localStorage). Harmless if the heuristic mis-fires + * on a new iOS version — the hint is dismissable. + */ + +import { t } from '../i18n/i18n'; + +const DISMISSED_KEY = 'tilfluktsrom:ios-install-hint:dismissed'; + +function isIOS(): boolean { + // Safari on iPadOS 13+ reports as MacIntel, so also check for touch + Safari. + const ua = navigator.userAgent; + if (/iPad|iPhone|iPod/.test(ua)) return true; + return ( + navigator.maxTouchPoints > 1 && + /Macintosh/.test(ua) && + /Safari/.test(ua) && + !/Chrome|CriOS|FxiOS/.test(ua) + ); +} + +function isStandalone(): boolean { + // iOS-specific property + const nav = navigator as Navigator & { standalone?: boolean }; + if (nav.standalone) return true; + // Standards-based check used by Chrome/Edge + return window.matchMedia?.('(display-mode: standalone)').matches ?? false; +} + +export function maybeShow(): void { + if (!isIOS() || isStandalone()) return; + if (localStorage.getItem(DISMISSED_KEY) === '1') return; + + const banner = document.createElement('div'); + banner.id = 'ios-install-hint'; + banner.setAttribute('role', 'status'); + + const text = document.createElement('span'); + text.textContent = t('ios_install_hint'); + + const dismiss = document.createElement('button'); + dismiss.type = 'button'; + dismiss.textContent = t('action_close'); + dismiss.setAttribute('aria-label', t('action_close')); + dismiss.addEventListener('click', () => { + localStorage.setItem(DISMISSED_KEY, '1'); + banner.remove(); + }); + + banner.append(text, dismiss); + document.body.appendChild(banner); +} From 262fafe9e0e423b1727123082ca1cca26f769e79 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Mon, 20 Apr 2026 13:27:35 +0200 Subject: [PATCH 2/2] Bump versjon til v1.10.0 (versionCode 15) Co-Authored-By: Claude Opus 4.7 (1M context) --- app/build.gradle.kts | 4 ++-- .../main/java/no/naiv/tilfluktsrom/data/ShelterRepository.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 23b23f3..ad7f5da 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,8 +14,8 @@ android { applicationId = "no.naiv.tilfluktsrom" minSdk = 26 targetSdk = 35 - versionCode = 14 - versionName = "1.9.1" + versionCode = 15 + versionName = "1.10.0" // Deep link domain — single source of truth for manifest + Kotlin code val deepLinkDomain = "tilfluktsrom.naiv.no" diff --git a/app/src/main/java/no/naiv/tilfluktsrom/data/ShelterRepository.kt b/app/src/main/java/no/naiv/tilfluktsrom/data/ShelterRepository.kt index a554375..5805368 100644 --- a/app/src/main/java/no/naiv/tilfluktsrom/data/ShelterRepository.kt +++ b/app/src/main/java/no/naiv/tilfluktsrom/data/ShelterRepository.kt @@ -46,7 +46,7 @@ class ShelterRepository(private val context: Context) { .readTimeout(60, TimeUnit.SECONDS) .addInterceptor(Interceptor { chain -> chain.proceed(chain.request().newBuilder() - .header("User-Agent", "Tilfluktsrom/1.9.1") + .header("User-Agent", "Tilfluktsrom/1.10.0") .build()) }) .build()