From 7f8ba0ae98ecf19d78b8b4523168ae2d7d3fb2d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 11 Nov 2026 20:46:58 +0100 Subject: [PATCH] fix(push): tell iPhone Safari users to open the Home Screen app iOS offers Web Push only to a web app opened from the Home Screen. In a Safari tab the push card said the browser does not support push notifications, and the step that fixes it was only in the docs. On an iPhone or iPad outside the installed app the card now says to add HealthLog to the Home Screen, open it from there and turn push on. WebPushCard grows by two lines for the second sentence; its baseline goes from 201 to 203, still below the 215 it had before the previous change. --- messages/de.json | 1 + messages/en.json | 1 + messages/es.json | 1 + messages/fr.json | 1 + messages/it.json | 1 + messages/ko.json | 1 + messages/nl.json | 1 + messages/pl.json | 1 + messages/pt-BR.json | 1 + messages/ru.json | 1 + .../baselines/source-function-lines.json | 4 +- .../web-push-card-home-screen.test.ts | 62 +++++++++++++++++++ src/components/settings/web-push-card.tsx | 29 +++++++-- 13 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 src/components/settings/__tests__/web-push-card-home-screen.test.ts diff --git a/messages/de.json b/messages/de.json index 737f6dce7..146f8a994 100644 --- a/messages/de.json +++ b/messages/de.json @@ -6377,6 +6377,7 @@ "webPush": "Browser-Push", "webPushDescription": "Erhalte Benachrichtigungen direkt im Browser, auch wenn HealthLog nicht geöffnet ist.", "webPushNotSupported": "Dein Browser unterstützt keine Push-Benachrichtigungen.", + "webPushInstallFirst": "Auf iPhone und iPad funktionieren Push-Benachrichtigungen nur in der installierten App. Tippe in Safari auf Teilen und dann auf „Zum Home-Bildschirm“, öffne HealthLog vom Home-Bildschirm aus und schalte Push hier ein.", "webPushDenied": "Push-Benachrichtigungen wurden blockiert. Erlaube sie in den Browser-Einstellungen.", "webPushNotConfigured": "Web Push ist serverseitig nicht konfiguriert.", "webPushSubscribe": "Push aktivieren", diff --git a/messages/en.json b/messages/en.json index 8877c35d5..7341a1e66 100644 --- a/messages/en.json +++ b/messages/en.json @@ -6377,6 +6377,7 @@ "webPush": "Browser Push", "webPushDescription": "Receive notifications directly in your browser, even when HealthLog is not open.", "webPushNotSupported": "Your browser does not support push notifications.", + "webPushInstallFirst": "On iPhone and iPad, push notifications only work in the installed app. In Safari, tap Share, then Add to Home Screen, open HealthLog from the Home Screen and turn on push here.", "webPushDenied": "Push notifications are blocked. Allow them in your browser settings.", "webPushNotConfigured": "Web Push is not configured on the server.", "webPushSubscribe": "Enable push", diff --git a/messages/es.json b/messages/es.json index ad30b5020..b3f5203f9 100644 --- a/messages/es.json +++ b/messages/es.json @@ -6377,6 +6377,7 @@ "webPush": "Push del navegador", "webPushDescription": "Recibe notificaciones directamente en tu navegador, incluso cuando HealthLog no está abierto.", "webPushNotSupported": "Tu navegador no admite notificaciones push.", + "webPushInstallFirst": "En iPhone y iPad, las notificaciones push solo funcionan en la app instalada. En Safari, toca Compartir y luego «Añadir a pantalla de inicio», abre HealthLog desde la pantalla de inicio y activa las notificaciones push aquí.", "webPushDenied": "Las notificaciones push están bloqueadas. Permítelas en los ajustes de tu navegador.", "webPushNotConfigured": "Web Push no está configurado en el servidor.", "webPushSubscribe": "Activar push", diff --git a/messages/fr.json b/messages/fr.json index ed15c1527..ef5ea9d9c 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -6377,6 +6377,7 @@ "webPush": "Push navigateur", "webPushDescription": "Recevez des notifications directement dans votre navigateur, même lorsque HealthLog n’est pas ouvert.", "webPushNotSupported": "Votre navigateur ne prend pas en charge les notifications push.", + "webPushInstallFirst": "Sur iPhone et iPad, les notifications push ne fonctionnent que dans l'app installée. Dans Safari, touchez Partager puis « Sur l'écran d'accueil », ouvrez HealthLog depuis l'écran d'accueil et activez les notifications push ici.", "webPushDenied": "Les notifications push sont bloquées. Autorisez-les dans les réglages de votre navigateur.", "webPushNotConfigured": "Web Push n’est pas configuré sur le serveur.", "webPushSubscribe": "Activer le push", diff --git a/messages/it.json b/messages/it.json index bb602f60a..9c695a0e4 100644 --- a/messages/it.json +++ b/messages/it.json @@ -6377,6 +6377,7 @@ "webPush": "Push del browser", "webPushDescription": "Ricevi notifiche direttamente nel browser, anche quando HealthLog non è aperto.", "webPushNotSupported": "Il tuo browser non supporta le notifiche push.", + "webPushInstallFirst": "Su iPhone e iPad le notifiche push funzionano solo nell'app installata. In Safari tocca Condividi, poi «Aggiungi alla schermata Home», apri HealthLog dalla schermata Home e attiva qui le notifiche push.", "webPushDenied": "Le notifiche push sono bloccate. Consentile nelle impostazioni del browser.", "webPushNotConfigured": "Web Push non è configurato sul server.", "webPushSubscribe": "Attiva push", diff --git a/messages/ko.json b/messages/ko.json index a7baf05aa..c5314d117 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -6377,6 +6377,7 @@ "webPush": "브라우저 푸시", "webPushDescription": "HealthLog를 열어 두지 않아도 브라우저로 바로 알림을 받아요.", "webPushNotSupported": "이 브라우저는 푸시 알림을 지원하지 않아요.", + "webPushInstallFirst": "iPhone과 iPad에서는 설치한 앱에서만 푸시 알림을 받을 수 있어요. Safari에서 공유를 누른 다음 '홈 화면에 추가'를 선택하고, 홈 화면에서 HealthLog를 연 뒤 여기서 푸시 알림을 켜 주세요.", "webPushDenied": "푸시 알림이 차단돼 있어요. 브라우저 설정에서 허용해 주세요.", "webPushNotConfigured": "서버에 Web Push가 설정돼 있지 않아요.", "webPushSubscribe": "푸시 켜기", diff --git a/messages/nl.json b/messages/nl.json index 67807173c..9398345ba 100644 --- a/messages/nl.json +++ b/messages/nl.json @@ -6377,6 +6377,7 @@ "webPush": "Browserpush", "webPushDescription": "Ontvang meldingen rechtstreeks in je browser, ook als HealthLog niet geopend is.", "webPushNotSupported": "Je browser ondersteunt geen pushmeldingen.", + "webPushInstallFirst": "Op iPhone en iPad werken pushmeldingen alleen in de geïnstalleerde app. Tik in Safari op Deel en daarna op 'Zet op beginscherm', open HealthLog vanaf het beginscherm en zet pushmeldingen hier aan.", "webPushDenied": "Pushmeldingen zijn geblokkeerd. Sta ze toe in de instellingen van je browser.", "webPushNotConfigured": "Web Push is niet geconfigureerd op de server.", "webPushSubscribe": "Push inschakelen", diff --git a/messages/pl.json b/messages/pl.json index 6bc8109e9..655e74b46 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -6377,6 +6377,7 @@ "webPush": "Push przeglądarki", "webPushDescription": "Otrzymuj powiadomienia bezpośrednio w przeglądarce, nawet gdy HealthLog nie jest otwarty.", "webPushNotSupported": "Twoja przeglądarka nie obsługuje powiadomień push.", + "webPushInstallFirst": "Na iPhonie i iPadzie powiadomienia push działają tylko w zainstalowanej aplikacji. W Safari stuknij Udostępnij, potem „Do ekranu początkowego”, otwórz HealthLog z ekranu początkowego i włącz tutaj powiadomienia push.", "webPushDenied": "Powiadomienia push są zablokowane. Zezwól na nie w ustawieniach przeglądarki.", "webPushNotConfigured": "Web Push nie jest skonfigurowany na serwerze.", "webPushSubscribe": "Włącz push", diff --git a/messages/pt-BR.json b/messages/pt-BR.json index 5efe0f404..941c24df1 100644 --- a/messages/pt-BR.json +++ b/messages/pt-BR.json @@ -6377,6 +6377,7 @@ "webPush": "Push do navegador", "webPushDescription": "Receba notificações diretamente no navegador, mesmo quando o HealthLog não estiver aberto.", "webPushNotSupported": "Seu navegador não oferece suporte a notificações push.", + "webPushInstallFirst": "No iPhone e no iPad, as notificações push só funcionam no app instalado. No Safari, toque em Compartilhar e depois em “Adicionar à Tela de Início”, abra o HealthLog pela Tela de Início e ative as notificações push aqui.", "webPushDenied": "As notificações push estão bloqueadas. Permita-as nas configurações do navegador.", "webPushNotConfigured": "O Web Push não está configurado no servidor.", "webPushSubscribe": "Ativar push", diff --git a/messages/ru.json b/messages/ru.json index 412c87fa9..ce87f9f0d 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -6377,6 +6377,7 @@ "webPush": "Push в браузере", "webPushDescription": "Получайте уведомления прямо в браузере, даже когда HealthLog не открыт.", "webPushNotSupported": "Ваш браузер не поддерживает push-уведомления.", + "webPushInstallFirst": "На iPhone и iPad push-уведомления работают только в установленном приложении. В Safari нажмите «Поделиться», затем «На экран „Домой“», откройте HealthLog с экрана «Домой» и включите push-уведомления здесь.", "webPushDenied": "Push-уведомления заблокированы. Разрешите их в настройках браузера.", "webPushNotConfigured": "Web Push не настроен на сервере.", "webPushSubscribe": "Включить push", diff --git a/src/__tests__/baselines/source-function-lines.json b/src/__tests__/baselines/source-function-lines.json index ad2d1a644..e9e3df96f 100644 --- a/src/__tests__/baselines/source-function-lines.json +++ b/src/__tests__/baselines/source-function-lines.json @@ -1,7 +1,7 @@ { "$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.", "count": 344, - "total": 111493, + "total": 111495, "entries": { "app/achievements/page.tsx :: AchievementsPage": 206, "app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 275, @@ -224,7 +224,7 @@ "components/settings/sharing-section.tsx :: ShareLinksCard": 260, "components/settings/sources-section.tsx :: SourcesSection": 601, "components/settings/thresholds-editor-section.tsx :: MetricRow": 241, - "components/settings/web-push-card.tsx :: WebPushCard": 201, + "components/settings/web-push-card.tsx :: WebPushCard": 203, "components/symptoms/symptom-entry-form.tsx :: SymptomEntryForm": 317, "components/symptoms/symptoms-section.tsx :: SymptomsSection": 205, "components/targets/range-bar.tsx :: RangeBar": 220, diff --git a/src/components/settings/__tests__/web-push-card-home-screen.test.ts b/src/components/settings/__tests__/web-push-card-home-screen.test.ts new file mode 100644 index 000000000..fefe8e260 --- /dev/null +++ b/src/components/settings/__tests__/web-push-card-home-screen.test.ts @@ -0,0 +1,62 @@ +/** + * Which "push is unavailable" sentence the Web Push card shows. + * + * An iPhone in a Safari tab has no PushManager, the same as a browser that + * cannot do push at all, and the card used to tell both that push is not + * supported. On iOS the fix is to open HealthLog from the Home Screen, so + * that case gets its own sentence. + */ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { needsHomeScreen } from "../web-push-card"; + +const IPHONE = + "Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Mobile/15E148 Safari/604.1"; +const IPAD_AS_MAC = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Safari/605.1.15"; +const ANDROID_FIREFOX = + "Mozilla/5.0 (Android 14; Mobile; rv:132.0) Gecko/132.0 Firefox/132.0"; + +function device( + userAgent: string, + maxTouchPoints: number, + standalone: boolean, +) { + vi.stubGlobal("navigator", { userAgent, maxTouchPoints }); + vi.stubGlobal("window", { + matchMedia: (query: string) => ({ + matches: standalone && query === "(display-mode: standalone)", + }), + }); +} + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("needsHomeScreen", () => { + it("asks an iPhone in a Safari tab to open the Home Screen app", () => { + device(IPHONE, 5, false); + expect(needsHomeScreen()).toBe(true); + }); + + it("asks an iPad, which reports itself as a Mac, the same", () => { + device(IPAD_AS_MAC, 5, false); + expect(needsHomeScreen()).toBe(true); + }); + + it("does not ask the installed app, where push is missing for real", () => { + device(IPHONE, 5, true); + expect(needsHomeScreen()).toBe(false); + }); + + it("does not ask a Mac without a touch screen", () => { + device(IPAD_AS_MAC, 0, false); + expect(needsHomeScreen()).toBe(false); + }); + + it("does not ask a browser that has no push at all", () => { + device(ANDROID_FIREFOX, 5, false); + expect(needsHomeScreen()).toBe(false); + }); +}); diff --git a/src/components/settings/web-push-card.tsx b/src/components/settings/web-push-card.tsx index c9515fc88..f25ae2795 100644 --- a/src/components/settings/web-push-card.tsx +++ b/src/components/settings/web-push-card.tsx @@ -25,10 +25,25 @@ function urlBase64ToArrayBuffer(base64String: string): ArrayBuffer { return buffer; } +/** + * iPhone and iPad offer Web Push only to HealthLog opened from the Home + * Screen; in a Safari tab the API is missing. iPadOS Safari reports itself + * as a Mac, so a Mac with a touch screen counts too. + */ +export function needsHomeScreen(): boolean { + const ua = navigator.userAgent; + const appleTouch = + /iPhone|iPad|iPod/.test(ua) || + (/Macintosh/.test(ua) && navigator.maxTouchPoints > 1); + return appleTouch && !window.matchMedia("(display-mode: standalone)").matches; +} + +type Unsupported = "browser" | "install" | null; + export function WebPushCard() { const { t } = useTranslations(); const [isSubscribed, setIsSubscribed] = useState(false); - const [isSupported, setIsSupported] = useState(true); + const [unsupported, setUnsupported] = useState(null); const [isDenied, setIsDenied] = useState(false); const [loading, setLoading] = useState(true); const [actionLoading, setActionLoading] = useState(false); @@ -41,7 +56,7 @@ export function WebPushCard() { // the effect body is rejected. async function checkSubscription() { if (!("serviceWorker" in navigator) || !("PushManager" in window)) { - setIsSupported(false); + setUnsupported(needsHomeScreen() ? "install" : "browser"); setLoading(false); return; } @@ -156,7 +171,7 @@ export function WebPushCard() { // carries a status badge. const cardAction = loading ? ( - ) : !isSupported || isDenied ? null : ( + ) : unsupported || isDenied ? null : ( {isSubscribed && ( @@ -199,11 +214,13 @@ export function WebPushCard() { description={t("settings.webPushDescription")} /> - {(!loading && !isSupported) || (!loading && isDenied) || msg ? ( + {(!loading && unsupported) || (!loading && isDenied) || msg ? (
- {!loading && !isSupported ? ( + {!loading && unsupported ? (

- {t("settings.webPushNotSupported")} + {unsupported === "install" + ? t("settings.webPushInstallFirst") + : t("settings.webPushNotSupported")}

) : !loading && isDenied ? (