# Notifications in iOS Home Screen apps

> Reproduce permission, push-manager exposure, and notificationclick handling measured in one iOS 18.6.2 Home Screen app test.

Notifications in an iOS Home Screen web app combine a user-granted permission, a push subscription surface, a service worker, and notification events. Use these field notes to reproduce the behaviour measured on iOS 18.6.2, investigate a permission or click path, or compare a later build without turning one device result into a general support claim.

The recorded run used one iPhone 16 Pro on iOS 18.6.2 (22G100) on 2026-10-20. Prepare an HTTPS app with a manifest set to `display: standalone`, a service worker, a permission button, an IndexedDB event log, and a way to create a local notification. Add the app to the Home Screen before testing. WebKit documents Web Push for Home Screen web apps from iOS and iPadOS 16.4 and requires notification permission to be requested from direct user interaction ([Web Push for Web Apps on iOS and iPadOS](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/), webkit.org).

The test did not send a real remote push. It measured permission, subscription objects, locally displayed notifications, notification taps, and worker state. The [device study](/news/testing-home-screen-web-apps-on-ios-18-6-2/) and [public data package](/research/2026-10-ios18-home-screen-web-apps/data/) preserve the run.

## Support scope

WebKit documents Web Push for Home Screen web apps from iOS and iPadOS 16.4. The recorded behaviour below is limited to one installed Home Screen app on iOS 18.6.2; it does not establish notification support in Safari tabs, other releases, or remote-push delivery, which this run did not test.

## Detect the installed notification surface

Open the app from its Home Screen icon and test the interfaces that the flow will use. Keep a fallback for a Safari tab or another context where they are absent:

```js
function notificationSurface() {
  const installed = matchMedia('(display-mode: standalone)').matches
    || navigator.standalone === true;
  const supported = 'Notification' in window
    && 'serviceWorker' in navigator
    && 'PushManager' in window;

  if (!installed || !supported) {
    return { enabled: false, action: 'Open the Home Screen app' };
  }
  return { enabled: true, action: 'Show permission control' };
}
```

The fallback is part of the flow. On the recorded build, `Notification` and the `PushManager` constructor were absent in a Safari tab and present in the installed app. iOS 18.4 also introduced `window.pushManager` for Declarative Web Push; the run found that property in both contexts, so testing that property alone did not identify the installed notification surface.

## Request permission from a tap

Start with an app that has not asked before. Put `Notification.requestPermission()` directly inside a button handler, record `performance.now()` before the call, and record the settled result afterward. Do not trigger the request at page load. WebKit's documentation states that the request must come from direct user interaction.

```js
permissionButton.addEventListener('click', async () => {
  const started = performance.now();
  permissionButton.disabled = true;
  const permission = await Notification.requestPermission();
  const elapsed = Math.round(performance.now() - started);
  eventLog.textContent = JSON.stringify({ permission, elapsed });
  if (permission !== 'granted') {
    permissionButton.disabled = false;
    fallbackPanel.hidden = false;
    return;
  }
  subscribeButton.hidden = false;
});
```

In the archived run, the system prompt appeared after the tap, and the promise remained pending until the operator answered. One recorded grant settled after 2068 ms. That number describes the recorded interaction; it is not a timeout value for application code ([Notification permission prompt](/lab/behaviour/notification-permission-prompt/)).

If the result is `denied` or `default`, keep the in-app fallback visible and do not immediately ask again. If the promise never settles during a test, preserve a screen recording, the app's prior permission state, and the system build before changing the code.

## Compare the two push managers

From iOS and iPadOS 18.4, WebKit exposes Declarative Web Push through `window.pushManager`. WebKit says that a root-scoped service worker shares the window object's subscription; the 18.6.2 test used workers scoped to subpaths, so that sharing condition was not met ([Meet Declarative Web Push](https://webkit.org/blog/16535/meet-declarative-web-push/), webkit.org).

After permission is granted, read both surfaces without creating another subscription:

```js
const registration = await navigator.serviceWorker.ready;
const workerSubscription = await registration.pushManager.getSubscription();
const windowSubscription = window.pushManager
  ? await window.pushManager.getSubscription()
  : null;

console.table({
  worker: workerSubscription?.endpoint ? 'present' : 'missing',
  window: windowSubscription?.endpoint ? 'present' : 'missing',
  permission: Notification.permission,
});
```

The recorded Home Screen app had a service-worker subscription whose endpoint host was `web.push.apple.com`; `window.pushManager.getSubscription()` returned `null`. One permission grant made both managers' permission-state checks return `granted`. The result is consistent with WebKit's root-scope condition because the tested worker used non-root demo or Device Lab scopes, not `/` (see the [subscription-scope record](/lab/behaviour/declarative-web-push-subscription-scope/)). A root-scoped worker was not tested, and the test did not deliver a remote push.

## Log both notification click handlers

Register the event with `addEventListener` and assign `self.onnotificationclick` as separate probes. Write each invocation to IndexedDB before focusing a client so the next page load can display the result.

```js
// sw.js
self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  event.waitUntil(recordClick('listener').then(() => focusClient()));
});

self.onnotificationclick = (event) => {
  event.notification.close();
  event.waitUntil(recordClick('property').then(() => focusClient()));
};

async function focusClient() {
  const clients = await self.clients.matchAll({ type: 'window' });
  return clients[0]?.focus();
}
```

Tap a displayed notification from its banner and from Notification Centre, then repeat after relaunching and after swiping the app away. In the study, both handlers ran for each of six recorded taps, and `clients.matchAll()` found the open window so `focus()` succeeded. The `clients.openWindow()` path was not reached ([Notification clicks in Home Screen apps](/lab/behaviour/notificationclick-in-home-screen-apps/)).

This device result and BCD describe different contexts. BCD records `api.ServiceWorkerGlobalScope.notificationclick_event` as unsupported for Safari on iOS. Issue #22959 explains that its collector checks for the handler property in a service worker; the 18.6.2 study found `'onnotificationclick' in self` false when the worker was controlled by a Safari tab and true when controlled by the Home Screen app ([worker handler exposure](/lab/behaviour/worker-event-handler-exposure-tab-vs-app/); [BCD issue #22959](https://github.com/mdn/browser-compat-data/issues/22959), github.com).

## Preserve a useful failure record

If a tap does not reach the handler, first confirm that the notification belongs to the installed app, the worker containing the handler is active, and the event log survives a page relaunch. Record whether an app window was already alive. WebKit bug reports include failures when no page is alive, while this run reached an existing client and did not exercise `openWindow()`.

Do not infer that a missing log proves the event did not fire until the write path has also been checked. The study found an observer error during the run and retained corrected notebook entries. Save the notification source, handler registration method, app lifecycle state, worker script URL, system build, and raw IndexedDB log.

:::observed
OpenPWA device test, 2026-10-20, iPhone 16 Pro, iOS 18.6.2 (22G100): a permission request made from a tap showed the system prompt and settled after the answer; both `notificationclick` registrations ran on six recorded taps in the Home Screen app; the same worker exposed `onnotificationclick` in the installed context and not in a Safari tab. See N35 to N40 and N53 to N59 in the [study data package](https://openpwa.net/research/2026-10-ios18-home-screen-web-apps/data/) (openpwa.net).
:::

## See also

- [Web Push for Web Apps on iOS and iPadOS](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/) (webkit.org)
- [Meet Declarative Web Push](https://webkit.org/blog/16535/meet-declarative-web-push/) (webkit.org)
- [Testing Home Screen web apps on iOS 18.6.2](/news/testing-home-screen-web-apps-on-ios-18-6-2/)
- [Notification permission prompt on iOS 18.6.2](/lab/behaviour/notification-permission-prompt/)
- [`notificationclick` in iOS Home Screen apps](/lab/behaviour/notificationclick-in-home-screen-apps/)
- [iOS and Safari Web Push](/reference/notifications/ios-safari-push/)