# Notifications on the device - JavaScript bridge API

> Posting and scheduling without a server, the POST_NOTIFICATIONS grant, exact alarms, and custom sounds.

- **Applies to:** AppMint
- **Source:** extracted from the app runtime and its per-method example files; this page is generated from them.
- **HTML:** https://freewebtoapk.com/docs/api/notifications

### `navigator.setAppBadge`

```js
navigator.setAppBadge(count) / clearAppBadge()
```

**Example**

The standard Badging API: a number on the app's launcher icon, like an unread count.

**Returns:** a Promise that resolves when the badge is set; `TypeError` for a negative or non-numeric count. **Needs:** nothing to switch on. It exists only on phones whose launcher shows counts (Samsung, Huawei, Xiaomi, OPPO, Sony, …); on Pixel-style launchers `'setAppBadge' in navigator` is `false`.

```js
function updateBadge(unread) {
  if (!('setAppBadge' in navigator)) return;      // this launcher cannot show a count
  if (unread > 0) navigator.setAppBadge(unread);
  else navigator.clearAppBadge();
}

updateBadge(messages.filter((m) => !m.read).length);
```

**Notes:** `setAppBadge()` with no number is the spec's "flag" badge, shown as 1 on a count launcher. The count survives an app restart. The same store backs `badge.set()` in `@/lib/appmintNative`, so both always agree.

### `Notification`

```js
new Notification(title, options) / Notification.requestPermission()
```

**Example**

The standard Web Notifications API works inside the app: `new Notification()` shows a real Android notification, `close()` removes it, and `Notification.requestPermission()` shows the Android permission dialog and waits for the user's answer.

**Returns:** `Notification.permission` is `'granted'`, `'denied'` or `'default'`, read live from the phone (a change in Settings shows without a reload). `requestPermission()` returns a Promise of `'granted'` or `'denied'` (the callback form also works). A new notification fires `show` when posted, or `error` when permission is missing. **Needs:** turn on **Notifications** in the Access step (Step 4) when you build.

```js
async function notifyWhenSaved() {
  if (!('Notification' in window)) return;
  let perm = Notification.permission;
  if (perm === 'default') perm = await Notification.requestPermission();
  if (perm !== 'granted') {
    showMessage('Turn on notifications for this app in Settings to get alerts.');
    return;
  }
  const n = new Notification('Saved', {
    body: 'Your note was saved',
    icon: 'https://example.com/avatar.png',   // shown as the large icon
    image: 'https://example.com/note.jpg',    // big picture
    tag: 'note'                               // same tag replaces the old one
  });
  n.onshow = function () { console.log('posted'); };
  n.onerror = function () { console.log('not posted'); };
  setTimeout(function () { n.close(); }, 60000);   // removes it from the shade
}

document.getElementById('save').addEventListener('click', notifyWhenSaved);
```

**Callback form** (older code):

```js
Notification.requestPermission(function (result) {
  if (result === 'granted') new Notification('Welcome!', { body: 'Alerts are on', silent: true });
});
```

**Service-worker style** (PWA code) also posts through the app, but only where the WebView itself has no `navigator.serviceWorker` (for example `file://` or `http://` pages):

```js
if ('serviceWorker' in navigator && Notification.permission === 'granted') {
  navigator.serviceWorker.ready.then(function (reg) {
    return reg.showNotification('Backup done', { body: '12 files', tag: 'backup' });
  });
}
```

**A tap** fires `click` on the notification - the app comes to the front and the page gets it. If the app was closed (the tap started it), the object is gone, so the window gets a `notificationclick` event instead, shaped like a service worker's - `event.notification.tag / title / body / data`:

```js
const n = new Notification('Order shipped', { body: '#7', tag: 'order-7', data: { orderId: 7 } });
n.onclick = () => openOrder(7);                         // the app was running
window.addEventListener('notificationclick', (e) => {   // the tap started the app
  if (e.notification.data && e.notification.data.orderId) openOrder(e.notification.data.orderId);
});
```

**Notes:** used options: `body`, `icon`, `image`, `tag`, `silent`, `data` (JSON, under 16 KB). The tap reaches the page through the splash and PIN screens (event behind it: `appmint:notification-click`). On Android 12 and older there is no runtime permission, so it is `'granted'` unless the user switched notifications off. Web Push (`pushManager.subscribe`) is not supported; use AppMint Push. For buttons, progress or a sticky notification use `WebToApk.notify(...)`. The same code works in a normal browser.

### `__badgeSupported`

```js
window.WebToApk.__badgeSupported(): Boolean
```

Whether this launcher can show a badge count (navigator.setAppBadge exists only then).

**Example**

Internal transport behind the presence of `navigator.setAppBadge`. Pages use the standard API; never call this directly - the `__` methods may change without notice.

**Returns:** `true` when this phone's launcher shows badge counts. **Needs:** nothing to switch on.

**Use the public API:**

```js
if ('setAppBadge' in navigator) navigator.setAppBadge(unreadCount);
```

**Notes:** The Badging API is installed only where the launcher can show a count (Samsung, Huawei, Xiaomi, OPPO, Sony, …); on Pixel-style launchers it is absent, like in a browser without badging.

### `__closeNotification`

```js
window.WebToApk.__closeNotification(tag: String): Boolean
```

Notification.close(): removes a notification the page posted, by its tag.

**Example**

Internal plumbing behind `Notification.prototype.close()` - pages use `new Notification(...)` and call `close()` on it. It removes a notification from the shade by its tag.

**Returns:** `true` when the remove request was made (also when nothing had that tag); `false` on an internal error. **Needs:** nothing.

**Use the public API:**

```js
let uploadNote = null;

function showUploading() {
  if (!('Notification' in window) || Notification.permission !== 'granted') return;
  uploadNote = new Notification('Uploading…', { body: 'Keep the app open', tag: 'upload', silent: true });
  uploadNote.onclose = function () { uploadNote = null; };
}

function uploadFinished() {
  if (uploadNote) uploadNote.close();   // removes it from the notification shade
}
```

**Notes:** do not call `WebToApk.__closeNotification` directly; the `__` methods may change without notice. `close()` fires the notification's `close` event.

### `__setAppBadge`

```js
window.WebToApk.__setAppBadge(contents: Long): Boolean
```

navigator.setAppBadge(n): n >= 0 is a count (0 clears), -1 the spec's flag badge.

**Example**

Internal transport behind `navigator.setAppBadge(count)` / `clearAppBadge()`. Pages use the standard API; never call this directly - the `__` methods may change without notice.

**Returns:** `true` when the count was applied. **Needs:** nothing to switch on.

**Use the public API:**

```js
if ('setAppBadge' in navigator) navigator.setAppBadge(unreadCount);
```

**Notes:** `-1` is the spec's flag badge (no number), which a count launcher shows as 1.

### `cancelAllNotifications`

```js
window.WebToApk.cancelAllNotifications(): Boolean
```

Reading and requesting the POST_NOTIFICATIONS grant, and managing notifications the page already scheduled. Posting and scheduling are documented on their own methods.

_Described by its group, Notification permission, cancelling and listing, rather than on its own._

**Example**

Cancels every reminder this app has scheduled.

**Returns:** `true` when done; `false` only on an internal error. **Needs:** nothing extra.

```js
document.getElementById('reset-reminders').addEventListener('click', function () {
  if (!(window.WebToApk && window.WebToApk.cancelAllNotifications)) return;
  if (!confirm('Remove all your reminders?')) return;
  const ok = WebToApk.cancelAllNotifications();
  showMessage(ok ? 'All reminders removed' : 'Could not remove reminders');
});
```

**Notes:** it clears the scheduled list, so nothing fires later and nothing is set again after a restart. Notifications already showing in the tray stay there.

### `cancelNotification`

```js
window.WebToApk.cancelNotification(id: String): Boolean
```

Reading and requesting the POST_NOTIFICATIONS grant, and managing notifications the page already scheduled. Posting and scheduling are documented on their own methods.

_Described by its group, Notification permission, cancelling and listing, rather than on its own._

**Example**

Cancels one scheduled reminder by its id, so it will not fire again.

**Returns:** `true` (also when no reminder had that id); `false` only on an internal error. **Needs:** nothing extra (reminders need **⏰ Reminders** at build time).

```js
function stopWaterReminder() {
  if (!(window.WebToApk && window.WebToApk.cancelNotification)) return;
  WebToApk.cancelNotification('water');   // the id you gave scheduleNotification / scheduleNotificationEx

  // Check it is gone:
  const left = JSON.parse(WebToApk.getScheduledNotifications() || '[]');
  const stillThere = left.some(function (r) { return r.id === 'water'; });
  console.log('water reminder still scheduled?', stillThere);   // false
}
```

**Notes:** this cancels a reminder that has not fired yet (one-off or repeating). It does not remove a notification that is already showing in the tray, and it does not affect notifications posted with `notify()` / `showNotification()`.

### `canOpenAppAtTime`

```js
window.WebToApk.canOpenAppAtTime(): String
```

Whether a scheduled notification may put the APP ITSELF on screen (`openApp`). "granted"  - a full-screen alert will show, over the lock screen. "denied"   - Android 14+ withheld it; call requestOpenAppPermission(). There is no third answer where the app silently launches itself in the background: Android has blocked background activity starts since Android 10, and a full-screen intent is the whole of what remains.

**Example**

Tells you whether a reminder with `openApp: true` may put the app itself on screen (a full-screen alert, also over the lock screen).

**Returns:** a string, synchronously: `'granted'` or `'denied'`. Below Android 14 it is always `'granted'`. **Needs:** turn on **⏰ Reminders** in the Access step (Step 4) when you build.

```js
function prepareAlarm() {
  const b = window.WebToApk;
  if (!(b && b.canOpenAppAtTime)) return;
  if (b.canOpenAppAtTime() === 'denied') {
    showMessage('To open the app at alarm time, allow "full-screen notifications" for this app.');
    b.requestOpenAppPermission();
    return;
  }
  const t = new Date();
  t.setDate(t.getDate() + 1);
  t.setHours(7, 0, 0, 0);                    // tomorrow 07:00
  b.scheduleNotificationEx('wake-up', JSON.stringify({
    at: t.getTime(), repeat: 'daily',
    title: 'Wake up', body: 'Good morning!',
    openApp: true
  }));
}

window.addEventListener('appmint:reminder', function (e) {
  // e.detail.overLockScreen is true when the app was shown as a full-screen alert
  if (e.detail.overLockScreen) showBigAlarmScreen(e.detail.id);
});
```

**Notes:** when it is `'denied'`, an `openApp` reminder still posts a heads-up notification, but the app does not open by itself. Android does not allow any other way for an app to start itself in the background.

### `clearNotificationSounds`

```js
window.WebToApk.clearNotificationSounds(): Int
```

Drops the copied sound files and returns how many were deleted. A file is copied again when a notification (or a scheduled reminder, when it fires) next uses that sound - including on a channel that already exists, which keeps pointing at the same file name (NotificationBridge.restoreBundleSound).

**Example**

Deletes the copies of your custom notification sounds that the app made from your bundle.

**Returns:** a number, synchronously: how many files were deleted. **Needs:** nothing.

**Clear, then put back the sounds you still use:**

```js
document.getElementById('reset-sounds').addEventListener('click', function () {
  const b = window.WebToApk;
  if (!(b && b.clearNotificationSounds)) return;
  const removed = b.clearNotificationSounds();

  // Sounds still in use must be copied again, or they play the default sound:
  ['/audio/alarm.mp3', '/audio/chime.mp3'].forEach(function (path) {
    if (!b.prepareNotificationSound(path)) console.warn('missing sound', path);
  });
  showMessage(removed + ' sound file(s) refreshed');
});
```

**Notes:** you rarely need this; the copies are small. A sound is copied back the next time anything uses it - `notify()`, a reminder when it fires, `scheduleNotificationEx`, or `prepareNotificationSound(path)` - including on a notification channel that already exists, so the custom sound keeps playing.

### `getAlarmPrecision`

```js
window.WebToApk.getAlarmPrecision(): String
```

"exact" - reminders fire on the minute. "inexact" - the user (or the OEM) turned exact alarms off for this app, so Doze may batch them by minutes. Ask [requestExactAlarms] to fix it. "unavailable" - Android 12+ and the system alarm service could not be reached, so the precision cannot be read (a reminder scheduled now may not fire either). A page that must be on time should check this instead of assuming. @return `"exact"`, `"inexact"` or `"unavailable"`.

**Example**

Tells you whether reminders will fire on the exact minute, or may be late because the user (or the phone maker) turned exact alarms off for this app.

**Returns:** a string, synchronously: `'exact'`, `'inexact'` (may be batched by battery saving, often by minutes), or `'unavailable'` (the phone has no alarm service). **Needs:** turn on **⏰ Reminders** in the Access step (Step 4) when you build.

```js
function checkReminderTiming() {
  if (!(window.WebToApk && window.WebToApk.getAlarmPrecision)) return;
  const precision = WebToApk.getAlarmPrecision();
  const warn = document.getElementById('timing-warning');
  warn.hidden = precision === 'exact';
  if (precision === 'inexact') {
    warn.textContent = 'Reminders may be a few minutes late. Tap to allow exact alarms.';
    warn.onclick = function () { WebToApk.requestExactAlarms(); };
  }
}
checkReminderTiming();
// Re-check when the user returns from Settings:
document.addEventListener('visibilitychange', function () {
  if (!document.hidden) checkReminderTiming();
});
```

**Notes:** Android 11 and older always answer `'exact'`. An `'inexact'` reminder still fires, just not on the minute.

### `getNotificationPermission`

```js
window.WebToApk.getNotificationPermission(): String
```

The CURRENT notification permission, read live from the OS. The polyfill used to hardcode `Notification.permission = 'default'` on every page load, so the overwhelmingly common page pattern if (Notification.permission === 'granted') new Notification(...) never fired - not even for a user who had already granted it, and not on Android 12 and below where notifications need no runtime grant at all.

**Example**

Reads, right now, whether this app may post notifications.

**Returns:** a string, synchronously: `'granted'`; `'denied'` (the user refused for good, or switched this app's notifications off in Settings - a post would not show); or `'default'` (not decided yet, the app can still ask). **Needs:** nothing to call it; posting needs **Notifications** turned on in the Access step (Step 4) when you build.

**The standard web API** reads the same live value:

```js
if ('Notification' in window && Notification.permission === 'granted') {
  document.getElementById('alerts-on').hidden = false;
}
```

**The raw call**, for example after the user comes back from Settings:

```js
function refreshAlertSetting() {
  if (!(window.WebToApk && window.WebToApk.getNotificationPermission)) return;
  const perm = WebToApk.getNotificationPermission();   // 'granted' | 'denied' | 'default'
  document.getElementById('enable-alerts').hidden = perm !== 'default';   // can still ask
  document.getElementById('open-settings-hint').hidden = perm !== 'denied';
}
document.addEventListener('visibilitychange', function () {
  if (!document.hidden) refreshAlertSetting();
});
refreshAlertSetting();
```

**Notes:** it gives the same answer as `navigator.permissions.query({ name: 'notifications' })`. To ask the user, use `Notification.requestPermission()`.

### `getScheduledNotifications`

```js
window.WebToApk.getScheduledNotifications(): String
```

Reading and requesting the POST_NOTIFICATIONS grant, and managing notifications the page already scheduled. Posting and scheduling are documented on their own methods.

_Described by its group, Notification permission, cancelling and listing, rather than on its own._

**Example**

Lists the reminders that are still scheduled.

**Returns:** a JSON **string**, synchronously. Parse it. Each item: `{ id, title, body, trigger, repeat, options? }` - `trigger` is the next fire time in epoch ms, `repeat` is `none|minutely|hourly|daily|weekly`, and `options` (only for `scheduleNotificationEx`) is the options as a JSON string. `'[]'` when there are none. **Needs:** nothing extra.

```js
function renderReminders() {
  if (!(window.WebToApk && window.WebToApk.getScheduledNotifications)) return;
  let list = [];
  try { list = JSON.parse(WebToApk.getScheduledNotifications() || '[]'); } catch (e) { list = []; }

  const ul = document.getElementById('reminders');
  ul.innerHTML = '';
  list.sort(function (a, b) { return a.trigger - b.trigger; }).forEach(function (r) {
    const li = document.createElement('li');
    li.textContent = r.title + ' — ' + new Date(r.trigger).toLocaleString() +
      (r.repeat !== 'none' ? ' (' + r.repeat + ')' : '');
    ul.appendChild(li);
  });
}
```

**Notes:** a one-off reminder leaves the list when it fires. A repeating one stays, with `trigger` moved to the next time.

### `notify`

```js
window.WebToApk.notify(optionsJson: String): Boolean
```

Rich notification. See NotificationBridge.show for the full option list: channel (urgent|default|quiet|ongoing), image, largeIcon, bigText, ongoing, actions, progress, sound, silent, group, color. Image fetching and decoding happen off the main thread, so a remote image can never block the page.

**Example**

Posts a notification right now, with any mix of options: picture, big text, buttons, progress bar, sticky (ongoing), channel, sound, colour and tag.

**Returns:** `true` when the options were valid JSON and the notification was handed to the system (it is built on a background thread, so a bad image URL does not block the page); `false` for invalid JSON. **Needs:** turn on **Notifications** in the Access step (Step 4) when you build, and the user's permission on Android 13+ (ask with `Notification.requestPermission()`).

**Simple notification.** `tag` is an ID: posting again with the same tag replaces the old one.

```js
async function notifyOrderReady() {
  if (!(window.WebToApk && window.WebToApk.notify)) return;
  if (window.Notification && Notification.permission !== 'granted') {
    const p = await Notification.requestPermission();
    if (p !== 'granted') { showMessage('Notifications are off for this app.'); return; }
  }
  WebToApk.notify(JSON.stringify({
    title: 'Order ready',
    body: 'Your order is ready to collect',
    channel: 'default',            // urgent | default | quiet | ongoing
    tag: 'order-42'
  }));
}
```

**Picture, large icon and long text.** `image` wins over `bigText` when both are given.

```js
WebToApk.notify(JSON.stringify({
  title: 'New photo',
  body: 'Tap to see it',
  image: 'https://example.com/photo.jpg',     // expanded big picture
  largeIcon: 'https://example.com/avatar.png', // round icon on the right
  color: '#00C6FF',
  tag: 'photo-7'
}));

WebToApk.notify(JSON.stringify({
  title: 'Weekly summary',
  body: '3 tasks done, 2 left',
  bigText: 'Done: pay rent, call bank, water plants. Left: tax form, dentist.',
  channel: 'quiet',                            // no sound, low priority
  group: 'summaries'
}));
```

**Buttons** (at most 3). The tap comes back as the `appmint:notification-action` event.

```js
WebToApk.notify(JSON.stringify({
  title: 'Ali sent you a message',
  body: 'Are we still on for 6pm?',
  channel: 'urgent',                           // heads-up banner
  tag: 'chat-ali',
  actions: [
    { id: 'open', label: 'Open chat' },
    { id: 'done', label: 'Mark read' }
  ]
}));

window.addEventListener('appmint:notification-action', function (e) {
  // e.detail = { actionId: 'done', tag: 'chat-ali' }
  if (e.detail.actionId === 'open') openChat(e.detail.tag);
  if (e.detail.actionId === 'done') markRead(e.detail.tag);
});
```

**Sticky progress (ongoing).** Re-post with the same tag to update it. When the job ends, post the final state with `ongoing: false` so the user can swipe it away.

```js
function showUpload(percent) {
  WebToApk.notify(JSON.stringify({
    title: 'Uploading', body: percent + '%',
    ongoing: true,                             // cannot be swiped away
    progress: { current: percent, max: 100 },  // or { indeterminate: true }
    silent: true,
    tag: 'upload'
  }));
}
showUpload(40);
// finished:
WebToApk.notify(JSON.stringify({ title: 'Upload complete', body: '3 photos saved', tag: 'upload' }));
```

**Other options:** `sound` (a file in your bundle like `'/audio/ding.mp3'`, a `data:audio/...` URL, or `'silent'`), `smallIcon` (a drawable name), `when` (epoch ms shown as the time), `openApp: true` (full-screen alert; see `scheduleNotificationEx`).

**Notes:** buttons have no inline reply box, and a button tap reaches the page only while the app is running (it does not open the app); the event also calls `window.onAppMintNotificationAction(detail)` if you define it. `image` and `largeIcon` take an `https://` or `data:` URL, or an image packaged with your app named as your page names it (`'img/logo.png'`, `'/img/logo.png'`, or its full `https://appassets.androidplatform.net/assets/…` address). A bad image is skipped, the notification still shows. `ongoing: true` always uses the quiet "Ongoing" channel. `notify()` has no public "remove" partner: `cancelNotification` only cancels scheduled reminders. A notification you want to remove later from the page should be posted with `new Notification(...)` and removed with its `close()`; a sticky progress card with `Capacitor.AppwrightLiveProgress` (`end`). Android fixes a channel's sound when the channel is created, so a new `sound` value makes a new channel.

### `prepareNotificationSound`

```js
window.WebToApk.prepareNotificationSound(spec: String): Boolean
```

Copies a bundled audio file into place ahead of time and reports whether it is usable as a notification sound. Optional - `notify()` and `scheduleNotificationEx()` do it themselves - but it lets a settings screen verify a sound the moment the user picks it.

**Example**

Checks that an audio file in your app can be used as a notification sound, and copies it into place now. Optional: `notify()` and `scheduleNotificationEx()` do the same by themselves; this lets a settings screen check a sound the moment the user picks it.

**Returns:** `true` if the sound is usable; `false` if the file does not exist or is not a supported audio type. **Needs:** nothing.

```js
function chooseAlarmSound(path) {             // e.g. '/audio/chime.mp3'
  if (!(window.WebToApk && window.WebToApk.prepareNotificationSound)) return;
  if (!WebToApk.prepareNotificationSound(path)) {
    showMessage('This sound file is missing from the app: ' + path);
    return;
  }
  localStorage.setItem('alarmSound', path);
  // Use it:
  WebToApk.notify(JSON.stringify({ title: 'Sound test', body: 'This is your alarm sound', sound: path, tag: 'sound-test' }));
}
```

**Accepted values:** a path in your web bundle (`'/audio/chime.mp3'`, `'audio/chime.mp3'`) or a `data:audio/...;base64,...` URL your page made. Types: mp3, wav, ogg, oga, m4a, aac, flac, opus, mid, midi.

**Notes:** a bare name without a folder or extension (a `res/raw` sound) is not checked here and returns `false`, even though `notify({sound: 'name'})` can use it. Android keeps one sound per channel, so changing the sound creates a new channel and resets the user's settings for that channel.

### `requestExactAlarms`

```js
window.WebToApk.requestExactAlarms(): Boolean
```

Opens the system screen where exact alarms are granted. Android 12+.

**Example**

Opens the Android 12+ system screen where the user allows this app to use exact alarms, so reminders fire on the minute.

**Returns:** `true` when the settings screen was opened (and on Android 11 and older, where nothing is needed); `false` if the screen could not be opened. It does not tell you the user's choice. **Needs:** turn on **⏰ Reminders** in the Access step (Step 4) when you build.

```js
document.getElementById('allow-exact').addEventListener('click', function () {
  if (!(window.WebToApk && window.WebToApk.requestExactAlarms)) return;
  if (WebToApk.getAlarmPrecision() === 'exact') return;   // already fine
  WebToApk.requestExactAlarms();
});

// The user comes back from Settings: read the real answer.
document.addEventListener('visibilitychange', function () {
  if (document.hidden || !window.WebToApk) return;
  const exact = WebToApk.getAlarmPrecision() === 'exact';
  document.getElementById('allow-exact').hidden = exact;
});
```

**Notes:** call it from a button tap and explain why first - it leaves your app. Reminders already scheduled keep working either way; only their timing changes.

### `requestNotificationPermission`

```js
window.WebToApk.requestNotificationPermission(): String
```

Reading and requesting the POST_NOTIFICATIONS grant, and managing notifications the page already scheduled. Posting and scheduling are documented on their own methods.

_Described by its group, Notification permission, cancelling and listing, rather than on its own._

**Example**

Shows the Android 13+ "Allow notifications?" dialog. Pages normally use `Notification.requestPermission()`, which calls this and waits for the user's answer.

**Returns:** a string, synchronously: `'granted'` if already allowed (or on Android 12 and older), otherwise `'default'` while the dialog opens. It cannot return the user's answer. **Needs:** turn on **Notifications** in the Access step (Step 4) when you build.

**Use the Promise API** - it resolves with the real answer (`'granted'` or `'denied'`):

```js
document.getElementById('enable-alerts').addEventListener('click', async function () {
  if (!('Notification' in window)) return;
  const perm = await Notification.requestPermission();
  if (perm === 'granted') {
    showMessage('Alerts are on');
  } else {
    showMessage('Alerts are off. You can turn them on in Settings.');
  }
});
```

**The raw call**, only to open the dialog without waiting:

```js
if (window.WebToApk && window.WebToApk.requestNotificationPermission) {
  const now = WebToApk.requestNotificationPermission();  // 'granted' or 'default'
  if (now === 'granted') enableAlertFeatures();
  // otherwise re-check later with WebToApk.getNotificationPermission()
  // ('granted' | 'denied' | 'default')
}
```

**Notes:** ask from a button tap with a clear reason. After the user refuses twice, Android stops showing the dialog and the answer is `'denied'` at once; the user must then allow it in the app's Settings.

### `requestOpenAppPermission`

```js
window.WebToApk.requestOpenAppPermission(): Boolean
```

Opens the system screen where the full-screen alert is granted. Android 14+.

**Example**

Opens the Android 14+ system screen where the user allows full-screen alerts for this app, so an `openApp: true` reminder can put the app on screen.

**Returns:** `true` when the settings screen was opened (and below Android 14, where nothing is needed); `false` if it could not be opened. It does not tell you the user's choice. **Needs:** turn on **⏰ Reminders** in the Access step (Step 4) when you build.

```js
document.getElementById('allow-open-app').addEventListener('click', function () {
  const b = window.WebToApk;
  if (!(b && b.requestOpenAppPermission)) return;
  if (b.canOpenAppAtTime() === 'granted') return;
  b.requestOpenAppPermission();
});

// Read the real answer when the user comes back:
document.addEventListener('visibilitychange', function () {
  if (document.hidden || !window.WebToApk) return;
  const ok = WebToApk.canOpenAppAtTime() === 'granted';
  document.getElementById('allow-open-app').hidden = ok;
});
```

**Notes:** call it from a button tap after explaining why - it leaves your app.

### `scheduleNotification`

```js
window.WebToApk.scheduleNotification(id: String, title: String, body: String, triggerAtMillis: String, repeat: String): Boolean
```

===== Tier-2: SCHEDULED local notifications (reminders) ===== Fire a notification at a future time even when the app is closed - backed by AlarmManager + persistence so it survives reboot. Dormant unless called. JS contract (all args strings; triggerAtMillis = epoch millis): window.WebToApk.scheduleNotification(id, title, body, triggerAtMillis, repeat) repeat ∈ "none" | "minutely" | "hourly" | "daily" | "weekly" window.WebToApk.cancelNotification(id) window.WebToApk.cancelAllNotifications() window.WebToApk.getScheduledNotifications()  -> JSON array string

**Example**

Schedules a simple reminder (title and text) for a future time. It fires even when the app is closed and is set again after the phone restarts.

**Returns:** `true` when it was scheduled; `false` when `id` is empty or the time is not a number. **Needs:** turn on **⏰ Reminders** in the Access step (Step 4) when you build (it also turns on Notifications), and the user's notification permission on Android 13+.

```js
function remindToDrinkWater() {
  if (!(window.WebToApk && window.WebToApk.scheduleNotification)) {
    showMessage('Reminders work in the installed app only.');
    return;
  }
  const ok = WebToApk.scheduleNotification(
    'water',                                  // your own id; same id replaces the old reminder
    'Drink water',                            // title
    'Time for a glass of water',              // body
    String(Date.now() + 60 * 60 * 1000),      // epoch milliseconds, AS A STRING
    'daily'                                   // none | minutely | hourly | daily | weekly
  );
  if (!ok) showMessage('Could not schedule the reminder');
}
```

**Know when a reminder opened the app** (the user tapped it):

```js
window.addEventListener('appmint:reminder', function (e) {
  // e.detail = { id: 'water', overLockScreen: false }
  if (e.detail.id === 'water') openWaterLog();
});
```

**Notes:** the reminder uses the "urgent" (heads-up) style and its tag is the id. A repeating reminder that was missed while the phone was off fires at the next step. For a picture, buttons, your own sound or opening the app over the lock screen, use `scheduleNotificationEx`. If exact alarms are off for the app, the time can drift by minutes; check `getAlarmPrecision()`.

### `scheduleNotificationEx`

```js
window.WebToApk.scheduleNotificationEx(id: String, optionsJson: String): Boolean
```

The full form of [scheduleNotification]: everything `notify()` accepts, fired at a future time with the app closed. WebToApk.scheduleNotificationEx('dose-1', JSON.stringify({ at: Date.now() + 3600000,   // epoch millis (alias: triggerAt) repeat: 'daily',            // none|minutely|hourly|daily|weekly title: 'Time for your dose', body: 'Vitamin D - 1 tablet', sound: '/audio/chime.mp3',  // a file in YOUR bundle; 'silent' for none openApp: true               // open the app itself, over the lock screen })); A bundled sound is copied into place during this call, so a wrong file name is reported now (false) rather than turning into the default sound days later.

**Example**

Schedules a reminder with every option `notify()` has (picture, buttons, sound, channel, open the app), for a future time. It fires when the app is closed and survives a restart.

**Returns:** `true` when scheduled. `false` when no time is given (`at`, `triggerAt`, `triggerAtMillis` or `inSeconds`), the id is empty, the JSON is bad, or `sound` names a file that is not in your app. **Needs:** turn on **⏰ Reminders** in the Access step (Step 4) when you build, and the user's notification permission on Android 13+.

**A daily reminder with your own sound that opens the app** (over the lock screen):

```js
function scheduleStandup(tomorrowAt9am) {
  const b = window.WebToApk;
  if (!(b && b.scheduleNotificationEx)) { showMessage('Reminders work in the installed app only.'); return; }

  // Both are user settings: check, and ask if needed.
  if (b.getAlarmPrecision() === 'inexact') b.requestExactAlarms();
  if (b.canOpenAppAtTime() === 'denied') b.requestOpenAppPermission();

  const ok = b.scheduleNotificationEx('standup', JSON.stringify({
    at: tomorrowAt9am,          // epoch ms (or triggerAt, or inSeconds: 3600)
    repeat: 'daily',            // none | minutely | hourly | daily | weekly
    title: 'Stand-up',
    body: 'Daily sync starts now',
    sound: '/audio/alarm.mp3',  // a file in YOUR app; 'silent' for none
    openApp: true               // full-screen alert that puts the app on screen
  }));
  if (!ok) showMessage('Check the sound file path — nothing was scheduled.');
}

window.addEventListener('appmint:reminder', function (e) {
  // e.detail = { id: 'standup', overLockScreen: true }
  openMeetingScreen(e.detail.id);
});
```

**A reminder with buttons, in 30 minutes:**

```js
WebToApk.scheduleNotificationEx('dose-1', JSON.stringify({
  inSeconds: 30 * 60,
  title: 'Time for your dose',
  body: 'Vitamin D — 1 tablet',
  actions: [{ id: 'taken', label: 'Taken' }, { id: 'snooze', label: 'Snooze' }]
}));

window.addEventListener('appmint:notification-action', function (e) {
  // e.detail = { actionId: 'taken', tag: 'dose-1' }
  if (e.detail.actionId === 'snooze') {
    WebToApk.scheduleNotificationEx(e.detail.tag, JSON.stringify({ inSeconds: 600, title: 'Time for your dose', body: 'Snoozed reminder' }));
  }
});
```

**Notes:** the same id replaces an older reminder. Defaults: tag = the id, channel = `urgent`, `bigText` = the body. A button tap reaches the page only while the app is running. The sound file is copied when you schedule, so a wrong path fails now and not days later; mp3, wav, ogg, m4a, aac, flac and opus work. `openApp` is the only way an app can come on screen by itself at a set time; on Android 14+ the user must allow it (`canOpenAppAtTime`).

### `showNotification`

```js
window.WebToApk.showNotification(title: String, body: String, icon: String, tag: String): Boolean
```

The ORIGINAL four-argument form, kept verbatim as an adapter so every app already in the wild and every guide snippet keeps working. It now forwards to the rich builder - which means the `icon` argument, silently discarded by the old implementation, finally does something.

**Example**

The original four-argument notification call. It shows a notification with a title, text, an optional large icon and a tag. New code should use `notify(optionsJson)` or `new Notification(...)`, which do more.

**Returns:** `true` when the notification was handed to the system. **Needs:** turn on **Notifications** in the Access step (Step 4) when you build, and the user's permission on Android 13+.

**The standard web API** (it calls this method for you):

```js
async function tellUser() {
  if (!('Notification' in window)) return;
  const p = Notification.permission === 'granted' ? 'granted' : await Notification.requestPermission();
  if (p === 'granted') new Notification('Saved', { body: 'Your note was saved', tag: 'note' });
}
```

**The raw call.** Arguments: title, body, icon, tag. Pass `''` for no icon or no tag.

```js
if (window.WebToApk && window.WebToApk.showNotification) {
  WebToApk.showNotification(
    'Saved',                                  // title
    'Your note was saved',                    // body
    'https://example.com/avatar.png',         // large icon (https://, data:, or 'img/x.png' in your app), or ''
    'note'                                    // tag: same tag replaces the old notification
  );
}
```

**Notes:** it forwards to `notify({title, body, largeIcon: icon, tag})` on the default channel. Without a tag every call adds a new notification. For pictures, buttons, progress or a sticky notification use `notify`.

