diff --git a/app/layout.tsx b/app/layout.tsx
index d1cc180f..8e2dae5e 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -46,6 +46,14 @@ export const metadata: Metadata = {
title: "Prabhupāda Says",
description:
"Browse the books and teachings of His Divine Grace A.C. Bhaktivedanta Swami Prabhupāda",
+ // Makes the site a standalone Home Screen app on iOS — the prerequisite
+ // for Web Push on iPhone/iPad (iOS 16.4+). Emits
+ // etc.
+ appleWebApp: {
+ capable: true,
+ title: "Prabhupāda Says",
+ statusBarStyle: "default",
+ },
...ogDefault(),
};
diff --git a/components/push/push-toggle.tsx b/components/push/push-toggle.tsx
index 85e069e4..e2cf8f62 100644
--- a/components/push/push-toggle.tsx
+++ b/components/push/push-toggle.tsx
@@ -13,6 +13,7 @@ import {
disablePush,
enablePush,
isPushEnabled,
+ needsIOSInstall,
pushConfigured,
pushPermission,
pushSupported,
@@ -21,6 +22,7 @@ import {
type State =
| "loading"
| "unsupported"
+ | "ios-install" // iOS: must add to Home Screen first
| "unavailable" // server has no VAPID keys
| "denied" // browser permission blocked
| "on"
@@ -33,7 +35,9 @@ export function PushToggle() {
useEffect(() => {
let cancelled = false;
(async () => {
- if (!pushSupported()) return finish("unsupported");
+ if (!pushSupported()) {
+ return finish(needsIOSInstall() ? "ios-install" : "unsupported");
+ }
if (pushPermission() === "denied") return finish("denied");
if (!(await pushConfigured())) return finish("unavailable");
finish((await isPushEnabled()) ? "on" : "off");
@@ -101,7 +105,11 @@ export function PushToggle() {
) : (
- {state === "loading" ? "…" : "Unavailable"}
+ {state === "loading"
+ ? "…"
+ : state === "ios-install"
+ ? null
+ : "Unavailable"}
)}
@@ -118,6 +126,8 @@ function describe(state: State): string {
return "Get notified about follow requests and more, even when the app is closed.";
case "denied":
return "Blocked in your browser. Enable notifications for this site in your browser settings, then reload.";
+ case "ios-install":
+ return "On iPhone & iPad, notifications work only from the Home Screen app. Tap the Share icon in Safari, choose “Add to Home Screen”, then open Prabhupāda Says from the new icon and turn this on.";
case "unsupported":
return "This browser doesn't support push notifications.";
case "unavailable":
diff --git a/lib/push.ts b/lib/push.ts
index d3a937f4..466fae70 100644
--- a/lib/push.ts
+++ b/lib/push.ts
@@ -22,6 +22,37 @@ export function pushSupported(): boolean {
);
}
+/** iPhone/iPad/iPod (incl. iPadOS, which reports as "Macintosh" + touch). */
+export function isIOS(): boolean {
+ if (typeof navigator === "undefined") return false;
+ const ua = navigator.userAgent;
+ return (
+ /iPad|iPhone|iPod/.test(ua) ||
+ (navigator.maxTouchPoints > 1 && /Macintosh/.test(ua))
+ );
+}
+
+/** Running as an installed (Home Screen) app, not a browser tab. */
+export function isStandalone(): boolean {
+ if (typeof window === "undefined") return false;
+ return (
+ // iOS Safari's non-standard flag…
+ (navigator as Navigator & { standalone?: boolean }).standalone === true ||
+ // …and the standard display-mode for everyone else.
+ window.matchMedia("(display-mode: standalone)").matches
+ );
+}
+
+/**
+ * iOS only allows Web Push when the site has been added to the Home Screen
+ * (iOS 16.4+). In a plain Safari tab the push APIs don't exist, so this is
+ * the case where we should guide the user to "Add to Home Screen" rather
+ * than show a dead "unsupported".
+ */
+export function needsIOSInstall(): boolean {
+ return isIOS() && !isStandalone() && !pushSupported();
+}
+
/** The browser's current notification permission, or "unsupported". */
export function pushPermission(): NotificationPermission | "unsupported" {
if (!pushSupported()) return "unsupported";