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.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 20:46:58 +01:00
1 parent 32911fea37
commit 7f8ba0ae98
13 files changed
+97 -8

No files matched your search

+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -6377,6 +6377,7 @@
"webPush": "브라우저 푸시",
"webPushDescription": "HealthLog를 열어 두지 않아도 브라우저로 바로 알림을 받아요.",
"webPushNotSupported": "이 브라우저는 푸시 알림을 지원하지 않아요.",
"webPushInstallFirst": "iPhone과 iPad에서는 설치한 앱에서만 푸시 알림을 받을 수 있어요. Safari에서 공유를 누른 다음 '홈 화면에 추가'를 선택하고, 홈 화면에서 HealthLog를 연 뒤 여기서 푸시 알림을 켜 주세요.",
"webPushDenied": "푸시 알림이 차단돼 있어요. 브라우저 설정에서 허용해 주세요.",
"webPushNotConfigured": "서버에 Web Push가 설정돼 있지 않아요.",
"webPushSubscribe": "푸시 켜기",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -6377,6 +6377,7 @@
"webPush": "Push в браузере",
"webPushDescription": "Получайте уведомления прямо в браузере, даже когда HealthLog не открыт.",
"webPushNotSupported": "Ваш браузер не поддерживает push-уведомления.",
"webPushInstallFirst": "На iPhone и iPad push-уведомления работают только в установленном приложении. В Safari нажмите «Поделиться», затем «На экран „Домой“», откройте HealthLog с экрана «Домой» и включите push-уведомления здесь.",
"webPushDenied": "Push-уведомления заблокированы. Разрешите их в настройках браузера.",
"webPushNotConfigured": "Web Push не настроен на сервере.",
"webPushSubscribe": "Включить push",
@@ -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,
@@ -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);
});
});
+23 -6
View File
@@ -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<Unsupported>(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 ? (
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin motion-reduce:animate-none" />
) : !isSupported || isDenied ? null : (
) : unsupported || isDenied ? null : (
<SettingsCardActions>
{isSubscribed && (
<TestConnectionButton endpoint="/api/notifications/web-push/test" />
@@ -199,11 +214,13 @@ export function WebPushCard() {
description={t("settings.webPushDescription")}
/>
{(!loading && !isSupported) || (!loading && isDenied) || msg ? (
{(!loading && unsupported) || (!loading && isDenied) || msg ? (
<div className="space-y-4">
{!loading && !isSupported ? (
{!loading && unsupported ? (
<p className="text-muted-foreground text-sm">
{t("settings.webPushNotSupported")}
{unsupported === "install"
? t("settings.webPushInstallFirst")
: t("settings.webPushNotSupported")}
</p>
) : !loading && isDenied ? (
<p className="text-destructive text-sm">