Создать APK бесплатно

JavaScript bridge API

Уведомления на устройстве

Показ и планирование без сервера, разрешение POST_NOTIFICATIONS, точные будильники и пользовательские звуки.

AppMint.enableNotifications помощник#

AppMint.enableNotifications()

Пример

«Включить уведомления» одним вызовом для пользователей, которые отклонили или пропустили запрос уведомлений Android: диалог разрешения, пока Android может его показать, иначе — настройки уведомлений этого приложения, а затем новое состояние, когда пользователь вернётся.

Возвращает: AppMint.enableNotifications() — Promise, который всегда завершается {state, changed, openedSettings}; state равно 'granted' | 'prompt' | 'blocked' | 'unavailable'. AppMint.getNotificationState() — Promise с этим состоянием. AppMint.openNotificationSettings() — Promise со значением true, когда экран настроек открылся. Требуется: вызывайте enableNotifications() по кнопке, которую нажал пользователь; для уведомлений в сборке нужны Уведомления (или Push / Напоминания).

// A banner that only shows while notifications are off, and turns them on in one tap.
async function setupNotifBanner() {
  if (!(window.AppMint && AppMint.enableNotifications)) return;   // a browser: no banner
  var banner = document.getElementById('notif-banner');
  var state = await AppMint.getNotificationState();
  banner.hidden = !(state === 'prompt' || state === 'blocked');
  document.getElementById('notif-turn-on').onclick = async function () {
    var r = await AppMint.enableNotifications();
    banner.hidden = r.state === 'granted';
    if (r.state === 'blocked') showToast('Switch on "Allow notifications" to get updates.');
  };
}
setupNotifBanner();

Простая ссылка «Настройки уведомлений» на вашей странице настроек:

document.getElementById('notif-settings').onclick = async function () {
  var opened = window.AppMint && AppMint.openNotificationSettings && await AppMint.openNotificationSettings();
  if (!opened) showToast('Open Settings > Apps > this app > Notifications.');
};

Примечания: кнопка push-сообщения или сообщения в приложении со ссылкой appmint://notifications/enable запускает тот же процесс без какого-либо кода на странице (страница услышит appmint:notifications). Push-аудитория приложения («Уведомления вкл / выкл») обновляется, как только пользователь их включает.

AppMint.getNotificationState помощник#

window.AppMint.getNotificationState

Пример

Описано вместе с AppMint.enableNotifications — пример там показывает и этот.

«Включить уведомления» одним вызовом для пользователей, которые отклонили или пропустили запрос уведомлений Android: диалог разрешения, пока Android может его показать, иначе — настройки уведомлений этого приложения, а затем новое состояние, когда пользователь вернётся.

Возвращает: AppMint.enableNotifications() — Promise, который всегда завершается {state, changed, openedSettings}; state равно 'granted' | 'prompt' | 'blocked' | 'unavailable'. AppMint.getNotificationState() — Promise с этим состоянием. AppMint.openNotificationSettings() — Promise со значением true, когда экран настроек открылся. Требуется: вызывайте enableNotifications() по кнопке, которую нажал пользователь; для уведомлений в сборке нужны Уведомления (или Push / Напоминания).

// A banner that only shows while notifications are off, and turns them on in one tap.
async function setupNotifBanner() {
  if (!(window.AppMint && AppMint.enableNotifications)) return;   // a browser: no banner
  var banner = document.getElementById('notif-banner');
  var state = await AppMint.getNotificationState();
  banner.hidden = !(state === 'prompt' || state === 'blocked');
  document.getElementById('notif-turn-on').onclick = async function () {
    var r = await AppMint.enableNotifications();
    banner.hidden = r.state === 'granted';
    if (r.state === 'blocked') showToast('Switch on "Allow notifications" to get updates.');
  };
}
setupNotifBanner();

Простая ссылка «Настройки уведомлений» на вашей странице настроек:

document.getElementById('notif-settings').onclick = async function () {
  var opened = window.AppMint && AppMint.openNotificationSettings && await AppMint.openNotificationSettings();
  if (!opened) showToast('Open Settings > Apps > this app > Notifications.');
};

Примечания: кнопка push-сообщения или сообщения в приложении со ссылкой appmint://notifications/enable запускает тот же процесс без какого-либо кода на странице (страница услышит appmint:notifications). Push-аудитория приложения («Уведомления вкл / выкл») обновляется, как только пользователь их включает.

AppMint.openNotificationSettings помощник#

window.AppMint.openNotificationSettings

Пример

Описано вместе с AppMint.enableNotifications — пример там показывает и этот.

«Включить уведомления» одним вызовом для пользователей, которые отклонили или пропустили запрос уведомлений Android: диалог разрешения, пока Android может его показать, иначе — настройки уведомлений этого приложения, а затем новое состояние, когда пользователь вернётся.

Возвращает: AppMint.enableNotifications() — Promise, который всегда завершается {state, changed, openedSettings}; state равно 'granted' | 'prompt' | 'blocked' | 'unavailable'. AppMint.getNotificationState() — Promise с этим состоянием. AppMint.openNotificationSettings() — Promise со значением true, когда экран настроек открылся. Требуется: вызывайте enableNotifications() по кнопке, которую нажал пользователь; для уведомлений в сборке нужны Уведомления (или Push / Напоминания).

// A banner that only shows while notifications are off, and turns them on in one tap.
async function setupNotifBanner() {
  if (!(window.AppMint && AppMint.enableNotifications)) return;   // a browser: no banner
  var banner = document.getElementById('notif-banner');
  var state = await AppMint.getNotificationState();
  banner.hidden = !(state === 'prompt' || state === 'blocked');
  document.getElementById('notif-turn-on').onclick = async function () {
    var r = await AppMint.enableNotifications();
    banner.hidden = r.state === 'granted';
    if (r.state === 'blocked') showToast('Switch on "Allow notifications" to get updates.');
  };
}
setupNotifBanner();

Простая ссылка «Настройки уведомлений» на вашей странице настроек:

document.getElementById('notif-settings').onclick = async function () {
  var opened = window.AppMint && AppMint.openNotificationSettings && await AppMint.openNotificationSettings();
  if (!opened) showToast('Open Settings > Apps > this app > Notifications.');
};

Примечания: кнопка push-сообщения или сообщения в приложении со ссылкой appmint://notifications/enable запускает тот же процесс без какого-либо кода на странице (страница услышит appmint:notifications). Push-аудитория приложения («Уведомления вкл / выкл») обновляется, как только пользователь их включает.

navigator.setAppBadge веб-стандарт#

navigator.setAppBadge(count) / clearAppBadge()

Пример

Стандартный Badging API: число на значке приложения в лаунчере, например счётчик непрочитанных.

Возвращает: Promise, который завершается, когда значок установлен; TypeError — для отрицательного или нечислового значения. Требуется: ничего включать не нужно. Существует только на телефонах, лаунчер которых показывает числа (Samsung, Huawei, Xiaomi, OPPO, Sony, …); на лаунчерах в стиле Pixel 'setAppBadge' in navigator равно false.

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);

Сбрасывайте значок, пока пользователь смотрит на приложение, и обрабатывайте отклонённое обновление:

function setUnread(count) {
  if (!('setAppBadge' in navigator)) return;
  navigator.setAppBadge(count).catch(function (err) {
    console.warn('badge not updated:', err.name);   // NotAllowedError or TypeError
  });
}

document.addEventListener('visibilitychange', function () {
  if (!document.hidden && 'clearAppBadge' in navigator) {
    navigator.clearAppBadge().catch(function () {});
  }
});

Покажите простой флаг «есть что-то новое», когда синхронизация находит изменения, но точного числа нет:

async function afterSync(result) {
  if (!('setAppBadge' in navigator)) return;
  try {
    if (result.newCount > 0) await navigator.setAppBadge(result.newCount);
    else if (result.hasChanges) await navigator.setAppBadge();   // flag badge, shown as 1
    else await navigator.clearAppBadge();
  } catch (e) {
    console.warn('badge update failed', e);
  }
}

Примечания: setAppBadge() без числа — это «флаговый» значок из спецификации, который на лаунчере со счётчиком показывается как 1. Счётчик сохраняется после перезапуска приложения. То же хранилище используется для badge.set() в @/lib/appmintNative, поэтому они всегда согласованы.

Notification веб-стандарт#

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

Пример

Стандартный Web Notifications API работает внутри приложения: new Notification() показывает настоящее уведомление Android, close() убирает его, а Notification.requestPermission() показывает диалог разрешения Android и ждёт ответа пользователя.

Возвращает: Notification.permission — это 'granted', 'denied' или 'default', считывается прямо с телефона (изменение в настройках видно без перезагрузки). requestPermission() возвращает Promise со значением 'granted' или 'denied' (форма с callback тоже работает). Новое уведомление вызывает show при показе или error, если разрешения нет. Требуется: включите Уведомления на шаге «Доступ» (шаг 4) при сборке.

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 (старый код):

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

Стиль service worker (код PWA) тоже показывает уведомления через приложение, но только там, где в самом WebView нет navigator.serviceWorker (например, на страницах file:// или http://):

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

Нажатие вызывает click на уведомлении — приложение выходит на передний план, и страница получает событие. Если приложение было закрыто (его запустило нажатие), объекта уже нет, поэтому окно получает событие notificationclick, устроенное как у service worker, — event.notification.tag / title / body / data:

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);
});

Примечания: используемые параметры: body, icon, image, tag, silent, data (JSON, до 16 КБ). Нажатие доходит до страницы через заставку и экран PIN (событие за ним: appmint:notification-click). На Android 12 и старше разрешения во время работы нет, поэтому значение 'granted', если только пользователь не отключил уведомления. Web Push (pushManager.subscribe) не поддерживается; используйте AppMint Push. Для кнопок, прогресса или закреплённого уведомления используйте WebToApk.notify(...). Тот же код работает в обычном браузере.

__badgeSupported мост#

window.WebToApk.__badgeSupported(): Boolean

Может ли этот лаунчер показывать число на значке (navigator.setAppBadge существует только тогда).

Пример

Внутренний транспорт за наличием navigator.setAppBadge. Страницы используют стандартный API; никогда не вызывайте это напрямую — методы __ могут меняться без предупреждения.

Возвращает: true, когда лаунчер этого телефона показывает числа на значках. Требуется: ничего включать не нужно.

Используйте публичный API:

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

Показывайте настройку «значок на иконке приложения» только на телефонах, лаунчер которых умеет показывать число:

function renderBadgeSetting() {
  const row = document.getElementById('badge-setting');
  // setAppBadge exists only when this launcher shows counts
  row.hidden = !('setAppBadge' in navigator);
}
renderBadgeSetting();

Сбросьте число, когда пользователь открывает входящие, на любом лаунчере, где оно есть:

function openInbox() {
  showInbox();
  if ('clearAppBadge' in navigator) {
    navigator.clearAppBadge().catch(function (e) {
      console.warn('badge not cleared', e.name);   // NotAllowedError
    });
  }
}

Примечания: Badging API устанавливается только там, где лаунчер умеет показывать число (Samsung, Huawei, Xiaomi, OPPO, Sony, …); на лаунчерах в стиле Pixel его нет, как в браузере без поддержки значков.

__closeNotification мост#

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

Notification.close(): убирает уведомление, показанное страницей, по его метке.

Пример

Внутренняя часть за Notification.prototype.close() — страницы используют new Notification(...) и вызывают на нём close(). Убирает уведомление из шторки по его метке.

Возвращает: true, когда запрос на удаление выполнен (в том числе когда ничего с такой меткой не было); false — при внутренней ошибке. Требуется: ничего.

Используйте публичный API:

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
}

Откройте нужный чат при нажатии на уведомление, затем уберите его из шторки:

function notifyNewMessage(msg) {
  if (!('Notification' in window) || Notification.permission !== 'granted') return;
  const n = new Notification(msg.from, {
    body: msg.text,
    tag: 'chat-' + msg.threadId,          // one notification per chat
    data: { threadId: msg.threadId }
  });
  n.onclick = function () {
    openThread(n.data.threadId);
    n.close();
  };
}

Покажите короткое подтверждение, которое исчезает само через пять секунд:

function confirmCopied() {
  if (!('Notification' in window) || Notification.permission !== 'granted') return;
  const n = new Notification('Link copied', { body: 'Paste it anywhere', tag: 'copied', silent: true });
  setTimeout(function () { n.close(); }, 5000);
}

Примечания: не вызывайте WebToApk.__closeNotification напрямую; методы __ могут меняться без предупреждения. close() вызывает событие close уведомления.

__setAppBadge мост#

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

navigator.setAppBadge(n): n >= 0 — это число (0 сбрасывает), -1 — флаговый значок из спецификации.

Пример

Внутренний транспорт за navigator.setAppBadge(count) / clearAppBadge(). Страницы используют стандартный API; никогда не вызывайте это напрямую — методы __ могут меняться без предупреждения.

Возвращает: true, когда число было применено. Требуется: ничего включать не нужно.

Используйте публичный API:

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

Держите число на значке в соответствии с числом непрочитанных и сбрасывайте его на нуле:

async function syncBadge(unread) {
  if (!('setAppBadge' in navigator)) return;
  try {
    if (unread > 0) await navigator.setAppBadge(unread);
    else await navigator.clearAppBadge();
  } catch (e) {
    console.warn('badge not applied', e.name);   // NotAllowedError when the launcher refused it
  }
}
syncBadge(messages.filter(function (m) { return !m.read; }).length);

Поставьте на значок флаговый индикатор (без числа), пока есть несинхронизированная работа:

function markPendingSync(hasPending) {
  if (!('setAppBadge' in navigator)) return;
  const p = hasPending
    ? navigator.setAppBadge()      // no argument = flag badge; a count launcher shows 1
    : navigator.clearAppBadge();
  p.catch(function () {});
}

Примечания: -1 — это флаговый значок из спецификации (без числа), который лаунчер со счётчиком показывает как 1.

cancelAllNotifications мост#

window.WebToApk.cancelAllNotifications(): Boolean

Чтение и запрос разрешения POST_NOTIFICATIONS, а также управление уведомлениями, которые страница уже запланировала. Показ и планирование описаны в их собственных методах.

Описано в своей группе, Разрешение на уведомления, отмена и список, а не отдельно.

Пример

Отменяет все напоминания, которые запланировало это приложение.

Возвращает: true, когда готово; false — только при внутренней ошибке. Требуется: ничего дополнительного.

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');
});

При выходе из аккаунта остановите напоминания предыдущего пользователя и сбросьте число на значке:

async function signOut() {
  if (window.WebToApk && window.WebToApk.cancelAllNotifications) {
    WebToApk.cancelAllNotifications();
  }
  if ('clearAppBadge' in navigator) navigator.clearAppBadge().catch(function () {});
  await auth.signOut();
  showLoginScreen();
}

Пересоберите всё ежедневное расписание из сохранённых времён (сначала отмените всё, затем добавьте каждое):

function applyMedicineTimes(times) {        // e.g. ['08:00', '20:00']
  const b = window.WebToApk;
  if (!(b && b.cancelAllNotifications)) return;
  b.cancelAllNotifications();
  times.forEach(function (hhmm, i) {
    const parts = hhmm.split(':');
    const at = new Date();
    at.setHours(Number(parts[0]), Number(parts[1]), 0, 0);
    if (at.getTime() <= Date.now()) at.setDate(at.getDate() + 1);   // next occurrence
    b.scheduleNotification('med-' + i, 'Medicine', 'Time for your ' + hhmm + ' dose',
      String(at.getTime()), 'daily');
  });
}

Примечания: очищает список запланированных, поэтому ничего не сработает позже и ничего не будет установлено заново после перезапуска. Уведомления, уже показанные в шторке, остаются.

cancelNotification мост#

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

Чтение и запрос разрешения POST_NOTIFICATIONS, а также управление уведомлениями, которые страница уже запланировала. Показ и планирование описаны в их собственных методах.

Описано в своей группе, Разрешение на уведомления, отмена и список, а не отдельно.

Пример

Отменяет одно запланированное напоминание по его id, чтобы оно больше не срабатывало.

Возвращает: true (в том числе когда напоминания с таким id не было); false — только при внутренней ошибке. Требуется: ничего дополнительного (для напоминаний при сборке нужна опция ⏰ Напоминания).

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
}

Переключатель напоминания в настройках, который планирует или отменяет по одному и тому же id:

document.getElementById('water-toggle').addEventListener('change', function (e) {
  const b = window.WebToApk;
  if (!(b && b.cancelNotification && b.scheduleNotification)) return;
  if (e.target.checked) {
    b.scheduleNotification('water', 'Drink water', 'Time for a glass of water',
      String(Date.now() + 60 * 60 * 1000), 'hourly');
  } else {
    b.cancelNotification('water');
  }
});

При удалении задачи отмените все связанные с ней напоминания:

function cancelTaskReminders(taskId) {
  const b = window.WebToApk;
  if (!(b && b.cancelNotification)) return;
  let list = [];
  try { list = JSON.parse(b.getScheduledNotifications() || '[]'); } catch (e) { list = []; }
  list
    .filter(function (r) { return r.id.indexOf('task-' + taskId + '-') === 0; })
    .forEach(function (r) { b.cancelNotification(r.id); });
}

Примечания: отменяет напоминание, которое ещё не сработало (разовое или повторяющееся). Не убирает уведомление, уже показанное в шторке, и не влияет на уведомления, показанные через notify() / showNotification().

canOpenAppAtTime мост#

window.WebToApk.canOpenAppAtTime(): String

Может ли запланированное уведомление вывести на экран САМО ПРИЛОЖЕНИЕ (`openApp`). "granted" — полноэкранное оповещение будет показано, в том числе поверх экрана блокировки. "denied" — Android 14+ не разрешил; вызовите requestOpenAppPermission(). Третьего варианта, когда приложение молча запускается само в фоне, нет: Android блокирует запуск активностей из фона с Android 10, а полноэкранный intent — это всё, что остаётся.

Пример

Сообщает, может ли напоминание с openApp: true само вывести приложение на экран (полноэкранное оповещение, в том числе поверх экрана блокировки).

Возвращает: строку, синхронно: 'granted' или 'denied'. Ниже Android 14 всегда 'granted'. Требуется: включите ⏰ Напоминания на шаге «Доступ» (шаг 4) при сборке.

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);
});

Экран настроек с обоими переключателями будильника и их текущим состоянием:

function renderAlarmStatus() {
  const b = window.WebToApk;
  if (!(b && b.canOpenAppAtTime && b.getAlarmPrecision)) return;
  const openApp = b.canOpenAppAtTime();       // 'granted' | 'denied'
  const precision = b.getAlarmPrecision();    // 'exact' | 'inexact' | 'unavailable'
  document.getElementById('open-app-state').textContent =
    openApp === 'granted' ? 'Opens over the lock screen' : 'Notification only';
  document.getElementById('open-app-fix').hidden = openApp === 'granted';
  document.getElementById('timing-state').textContent =
    precision === 'exact' ? 'On the minute' : 'May be late';
}

renderAlarmStatus();
document.addEventListener('visibilitychange', function () {
  if (!document.hidden) renderAlarmStatus();  // back from Settings
});

Предупредите, когда сохранённые будильники были настроены открывать приложение, но Android сейчас этого не разрешает:

function checkSavedAlarms() {
  const b = window.WebToApk;
  if (!(b && b.canOpenAppAtTime && b.getScheduledNotifications)) return;
  if (b.canOpenAppAtTime() === 'granted') return;
  // [{ id, title, body, trigger, repeat, options }] - options is the JSON string you scheduled
  const items = JSON.parse(b.getScheduledNotifications());
  const affected = items.filter(function (r) {
    try { return JSON.parse(r.options || '{}').openApp === true; } catch (e) { return false; }
  });
  if (affected.length) {
    showMessage(affected.length + ' alarm(s) will only show a notification. Tap "Allow" to let them open the app.');
  }
}

Примечания: когда значение 'denied', напоминание с openApp всё равно показывает всплывающее уведомление, но приложение само не открывается. Android не позволяет приложению запускать себя в фоне иным способом.

clearNotificationSounds мост#

window.WebToApk.clearNotificationSounds(): Int

Удаляет скопированные звуковые файлы и возвращает, сколько удалено. Файл копируется снова, когда уведомление (или запланированное напоминание при срабатывании) в следующий раз использует этот звук — в том числе на уже существующем канале, который продолжает указывать на то же имя файла (NotificationBridge.restoreBundleSound).

Пример

Удаляет копии ваших пользовательских звуков уведомлений, которые приложение создало из вашего пакета.

Возвращает: число, синхронно: сколько файлов удалено. Требуется: ничего.

Очистите, затем верните звуки, которые вы всё ещё используете:

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');
});

Кнопка на экране хранилища, которая освобождает скопированные звуковые файлы и сообщает, сколько удалено:

document.getElementById('free-space').addEventListener('click', function () {
  const b = window.WebToApk;
  if (!(b && b.clearNotificationSounds)) return;
  const removed = b.clearNotificationSounds();
  showMessage(removed === 0 ? 'No sound files to remove.' : 'Removed ' + removed + ' sound file(s).');
});

При выходе из аккаунта отмените все напоминания и удалите копии звуков, принадлежавшие этому пользователю:

function signOutAndReset() {
  const b = window.WebToApk;
  if (b && b.cancelAllNotifications) b.cancelAllNotifications();
  if (b && b.clearNotificationSounds) b.clearNotificationSounds();
  localStorage.removeItem('alarmSound');
  goToLoginScreen();
}

Примечания: нужно это редко; копии небольшие. Звук копируется заново при следующем использовании — notify(), напоминанием при срабатывании, scheduleNotificationEx или prepareNotificationSound(path) — в том числе на уже существующем канале уведомлений, поэтому пользовательский звук продолжает играть.

enableNotifications мост#

window.WebToApk.enableNotifications(callbackId: String)

Возвращает пользователя к уведомлениям: системный диалог, пока его ещё можно показать, иначе — настройки уведомлений приложения. Завершает `window.__appmintReplies[callbackId]` значением {state, changed, openedSettings} (AppMint.enableNotifications() — обёртка с Promise).

Пример

Возвращает пользователя, который отклонил или пропустил уведомления: показывает диалог разрешения Android, пока его ещё можно показать, иначе открывает настройки уведомлений приложения и отвечает, когда пользователь вернётся.

Возвращает: напрямую ничего. Ответ {state, changed, openedSettings} доставляется в window.__appmintReplies[callbackId]; state равно 'granted' | 'prompt' | 'blocked' | 'unavailable', changed равно true, если оно отличается от значения до вызова, openedSettings равно true, если использовался экран настроек. Если уже 'granted' (или 'unavailable'), отвечает сразу. Требуется: вызывайте по кнопке, которую нажал пользователь; для уведомлений в сборке нужны Уведомления (или Push / Напоминания).

Предпочитайте обёртку с Promise AppMint.enableNotifications(). Необработанный вызов — для кода, который сам управляет ответами:

function enableNotificationsRaw() {
  return new Promise(function (resolve) {
    if (!(window.WebToApk && WebToApk.enableNotifications)) return resolve({ state: 'unavailable', changed: false, openedSettings: false });
    var replies = window.__appmintReplies = window.__appmintReplies || {};
    var id = 'my_notif_' + Date.now();
    replies[id] = resolve;
    WebToApk.enableNotifications(id);
  });
}

document.getElementById('turn-on-btn').onclick = async function () {
  var r = await enableNotificationsRaw();
  if (r.state === 'granted') document.getElementById('notif-banner').hidden = true;
};

Каждый завершённый процесс также объявляется, включая начатый кнопкой push-сообщения (appmint://notifications/enable):

window.addEventListener('appmint:notifications', function (e) {
  // e.detail = { state, changed, openedSettings }
  if (e.detail.state === 'granted') document.getElementById('notif-banner').hidden = true;
});

Примечания: кнопка push-сообщения или сообщения в приложении со ссылкой appmint://notifications/enable запускает тот же процесс; appmint://notifications/settings открывает экран настроек. Push-аудитория приложения сразу видит новое состояние.

getAlarmPrecision мост#

window.WebToApk.getAlarmPrecision(): String

"exact" — напоминания срабатывают ровно в назначенную минуту. "inexact" — пользователь (или производитель устройства) отключил точные будильники для этого приложения, поэтому Doze может сдвигать их на несколько минут. Попросите [requestExactAlarms], чтобы это исправить. "unavailable" — Android 12+ и системная служба будильников недоступна, поэтому точность прочитать нельзя (напоминание, запланированное сейчас, тоже может не сработать). Страница, которой нужна точность по времени, должна проверять это, а не предполагать. @return `"exact"`, `"inexact"` или `"unavailable"`.

Пример

Сообщает, сработают ли напоминания ровно в назначенную минуту или могут опоздать, потому что пользователь (или производитель телефона) отключил точные будильники для этого приложения.

Возвращает: строку, синхронно: 'exact', 'inexact' (может сдвигаться экономией заряда, часто на минуты) или 'unavailable' (в телефоне нет службы будильников). Требуется: включите ⏰ Напоминания на шаге «Доступ» (шаг 4) при сборке.

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();
});

Проверьте перед установкой будильника для пробуждения и скажите пользователю, если он может опоздать:

function setWakeUp(atMs) {
  const b = window.WebToApk;
  if (!(b && b.getAlarmPrecision && b.scheduleNotificationEx)) return;
  const precision = b.getAlarmPrecision();
  if (precision === 'unavailable') { showMessage('Alarms cannot be set on this phone right now.'); return; }
  b.scheduleNotificationEx('wake', JSON.stringify({ at: atMs, title: 'Wake up', body: 'Good morning', openApp: true }));
  if (precision === 'inexact') {
    showMessage('Alarm set, but it may ring a few minutes late. Allow exact alarms for on-time alarms.');
  }
}

Покажите время напоминания строкой на экране настроек:

function renderTimingRow() {
  if (!(window.WebToApk && window.WebToApk.getAlarmPrecision)) return;
  const labels = {
    exact: 'Reminders arrive on time',
    inexact: 'Reminders may be a few minutes late',
    unavailable: 'Reminders are not available on this phone'
  };
  document.getElementById('timing-row').textContent = labels[WebToApk.getAlarmPrecision()];
}
renderTimingRow();

Примечания: Android 11 и старше всегда отвечают 'exact'. Напоминание 'inexact' всё равно срабатывает, только не ровно в минуту.

getAlarmSupport мост#

window.WebToApk.getAlarmSupport(): String

Можно ли сейчас запланировать `scheduleNotificationEx(id, {alarm: true, ...})`: "ready" | "not_enabled" (будильник отключён в этой сборке) | "needs_exact_alarm" (вызовите requestExactAlarms) | "needs_notification_permission".

Пример

Сообщает, можно ли сейчас установить на этом телефоне будильник (scheduleNotificationEx(id, { alarm: true, ... })) и, если нет, что пользователю нужно сначала разрешить.

Возвращает: строку, синхронно: 'ready', 'not_enabled' (приложение собрано без 🔔 Будильника), 'needs_exact_alarm' (Android 12+: точные будильники отключены для этого приложения — вызовите requestExactAlarms()) или 'needs_notification_permission' (Android 13+: нет разрешения на уведомления, поэтому не было бы кнопки «Отключить» — вызовите requestNotificationPermission()). Требуется: включите 🔔 Будильник на шаге «Доступ» (шаг 4) при сборке.

function prepareAlarms() {
  const b = window.WebToApk;
  if (!(b && b.getAlarmSupport)) { showMessage('Alarms work in the installed app only.'); return false; }
  switch (b.getAlarmSupport()) {
    case 'ready': return true;
    case 'needs_exact_alarm':
      showMessage('Allow "Alarms & reminders" for this app, then come back.');
      b.requestExactAlarms();
      return false;
    case 'needs_notification_permission':
      b.requestNotificationPermission();
      return false;
    default:
      showMessage('This app was built without alarm clocks.');
      return false;
  }
}

Проверьте снова, когда пользователь вернётся из настроек:

document.addEventListener('visibilitychange', function () {
  if (document.hidden || !(window.WebToApk && window.WebToApk.getAlarmSupport)) return;
  document.getElementById('alarm-setup').hidden = WebToApk.getAlarmSupport() === 'ready';
});

Создавайте будильник, только когда телефон готов:

function setAlarmIn(hours) {
  const b = window.WebToApk;
  if (!(b && b.getAlarmSupport)) return false;
  if (b.getAlarmSupport() !== 'ready') { prepareAlarms(); return false; }   // asks for what is missing
  return b.scheduleNotificationEx('wake-up', JSON.stringify({
    at: Date.now() + hours * 3600 * 1000,
    alarm: true,
    title: 'Wake up',
    body: 'Good morning'
  }));
}

Примечания: scheduleNotificationEx с alarm: true возвращает false, когда значение не 'ready' — будильник, который никогда не смог бы зазвонить, не сохраняется.

getNotificationPermission мост#

window.WebToApk.getNotificationPermission(): String

ТЕКУЩЕЕ разрешение на уведомления, считанное напрямую из ОС. Полифил раньше жёстко задавал `Notification.permission = 'default'` при каждой загрузке страницы, поэтому самый распространённый шаблон if (Notification.permission === 'granted') new Notification(...) никогда не срабатывал — даже для пользователя, который уже дал разрешение, и даже на Android 12 и ниже, где уведомлениям вообще не нужно разрешение во время работы.

Пример

Прямо сейчас сообщает, может ли это приложение показывать уведомления.

Возвращает: строку, синхронно: 'granted'; 'denied' (пользователь отказал окончательно или отключил уведомления этого приложения в настройках — показ не сработает); или 'default' (ещё не решено, приложение всё ещё может спросить). Требуется: для вызова ничего не нужно; для показа уведомлений нужна включённая опция Уведомления на шаге «Доступ» (шаг 4) при сборке.

Стандартный веб-API читает то же актуальное значение:

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

Необработанный вызов, например после возврата пользователя из настроек:

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();

Решите перед планированием: спросить, объяснить или действовать сразу:

async function ensureAlerts() {
  const b = window.WebToApk;
  if (!(b && b.getNotificationPermission)) return false;
  const perm = b.getNotificationPermission();
  if (perm === 'granted') return true;
  if (perm === 'denied') {
    showMessage('Notifications are off for this app. Turn them on in Settings.');
    return false;
  }
  // 'default': the app can still ask
  return (await Notification.requestPermission()) === 'granted';
}

Примечания: даёт тот же ответ, что и navigator.permissions.query({ name: 'notifications' }). Чтобы спросить пользователя, используйте Notification.requestPermission().

getNotificationState мост#

window.WebToApk.getNotificationState(): String

'granted' | 'prompt' (Android всё ещё может спросить) | 'blocked' (отказано окончательно или отключено в настройках — исправить может только настройка) | 'unavailable' (Android 13+ и в этой сборке не объявлено разрешение на уведомления). См. NotificationOptIn.

Пример

Прямо сейчас сообщает, включены ли уведомления приложения, а если нет — может ли приложение ещё спросить или включить их обратно можно только в настройках.

Возвращает: строку, синхронно: 'granted' (уведомления доходят до телефона); 'prompt' (Android всё ещё может показать диалог разрешения); 'blocked' (пользователь отказал окончательно или отключил уведомления этого приложения в системных настройках — исправить может только настройка); или 'unavailable' (Android 13+ и приложение собрано без Уведомлений / Напоминаний / Push, поэтому включить их нельзя). Требуется: для вызова ничего не нужно.

Необработанный вызов, чтобы решить, показывать ли баннер «Включите уведомления»:

function refreshNotifBanner() {
  if (!(window.WebToApk && WebToApk.getNotificationState)) return;
  var state = WebToApk.getNotificationState();   // 'granted' | 'prompt' | 'blocked' | 'unavailable'
  var banner = document.getElementById('notif-banner');
  banner.hidden = !(state === 'prompt' || state === 'blocked');
  banner.querySelector('.hint').textContent = state === 'blocked'
    ? 'Notifications are off. Tap to open Settings and switch them on.'
    : 'Get order updates. Tap to turn on notifications.';
}
document.addEventListener('visibilitychange', function () {
  if (!document.hidden) refreshNotifBanner();
});
refreshNotifBanner();

Примечания: getNotificationPermission() сохраняет свои слова ('granted' / 'default' / 'denied') для старых страниц. Чтобы вернуть пользователя, вызовите AppMint.enableNotifications() (он сам выберет диалог или настройки).

getScheduledNotifications мост#

window.WebToApk.getScheduledNotifications(): String

Чтение и запрос разрешения POST_NOTIFICATIONS, а также управление уведомлениями, которые страница уже запланировала. Показ и планирование описаны в их собственных методах.

Описано в своей группе, Разрешение на уведомления, отмена и список, а не отдельно.

Пример

Перечисляет напоминания, которые всё ещё запланированы.

Возвращает: JSON-строку, синхронно. Разберите её. Каждый элемент: { id, title, body, trigger, repeat, options? } — trigger — это время следующего срабатывания в epoch мс, repeat равно none|minutely|hourly|daily|weekly, а options (только для scheduleNotificationEx) — параметры в виде строки JSON. Живые поля показывают, как оно будет доставлено на самом деле, считываются прямо сейчас: precision ("exact"; "inexact" = «Будильники и напоминания» отключены для приложения, поэтому Android доставляет его в окне 10 минут, пока телефон бодрствует, и может задержать дольше, пока он спит; "alarm_clock" для alarm: true), windowMs + latestAt (окно, которое получил Android: 0 / trigger при точном, 600000 / trigger + 600000 при неточном) и notificationsEnabled (false = оно срабатывает, но Android ничего не показывает, потому что уведомления приложения отключены). '[]', когда их нет. Требуется: ничего дополнительного.

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);
  });
}

Покажите, включено ли одно напоминание и когда оно сработает в следующий раз:

function describeReminder(id) {
  if (!(window.WebToApk && window.WebToApk.getScheduledNotifications)) return 'Not available';
  let list = [];
  try { list = JSON.parse(WebToApk.getScheduledNotifications() || '[]'); } catch (e) { list = []; }
  const r = list.find(function (x) { return x.id === id; });
  return r ? 'Next: ' + new Date(r.trigger).toLocaleString() : 'Off';
}
document.getElementById('water-status').textContent = describeReminder('water');

Читайте дополнительные параметры напоминаний, созданных с помощью scheduleNotificationEx:

function listAlarmStyleReminders() {
  if (!(window.WebToApk && window.WebToApk.getScheduledNotifications)) return [];
  let list = [];
  try { list = JSON.parse(WebToApk.getScheduledNotifications() || '[]'); } catch (e) { list = []; }
  return list.filter(function (r) {
    if (!r.options) return false;                  // made with scheduleNotification
    let o = {};
    try { o = JSON.parse(r.options); } catch (e) { return false; }
    return o.openApp === true;                     // puts the app on screen when it fires
  }).map(function (r) { return r.id; });
}

Примечания: разовое напоминание исчезает из списка, когда срабатывает. Повторяющееся остаётся, а trigger переносится на следующее время. Напоминание, время которого прошло, пока телефон был выключен или приложение принудительно остановлено, показывается один раз, с опозданием, при следующем запуске приложения или телефона; опоздавший alarm: true сообщается как missed, а не звонит. Ежедневное, еженедельное, ежемесячное или ежегодное напоминание сохраняет местное время при смене часового пояса.

Предупредите, когда напоминания не будут приходить вовремя:

function reminderHealth() {
  if (!(window.WebToApk && window.WebToApk.getScheduledNotifications)) return '';
  let list = [];
  try { list = JSON.parse(WebToApk.getScheduledNotifications() || '[]'); } catch (e) { list = []; }
  if (!list.length) return '';
  if (list.some(function (r) { return r.notificationsEnabled === false; })) return 'Notifications are off for this app: reminders will not appear.';
  if (list.some(function (r) { return r.precision === 'inexact'; })) return 'Reminders may arrive late. Turn on Alarms & reminders.';
  return '';
}

isAlarmRinging мост#

window.WebToApk.isAlarmRinging(): Boolean

True, пока звонит будильник.

Пример

Сообщает, звонит ли сейчас один из будильников приложения, чтобы страница могла показать собственный экран остановки / отложить.

Возвращает: true, пока звонит будильник, иначе false (синхронно). Требуется: 🔔 Будильник на шаге «Доступ» (шаг 4) при сборке.

function showRingScreenIfNeeded() {
  const b = window.WebToApk;
  const ringing = !!(b && b.isAlarmRinging && b.isAlarmRinging());
  document.getElementById('ring-screen').hidden = !ringing;
}
showRingScreenIfNeeded();
document.addEventListener('visibilitychange', showRingScreenIfNeeded);

Остановите звонящий будильник со своего экрана соответствующими вызовами.

document.getElementById('stop').addEventListener('click', function () {
  const b = window.WebToApk;
  if (b && b.isAlarmRinging && b.isAlarmRinging()) b.stopAlarm('');   // '' = whichever is ringing
});
document.getElementById('snooze').addEventListener('click', function () {
  const b = window.WebToApk;
  if (b && b.snoozeAlarm && !b.snoozeAlarm('', 10)) showMessage('Nothing is ringing');
});

Реагируйте на открытие приложения звонящим будильником.

window.addEventListener('appmint:reminder', function (e) {
  if (e.detail.action === 'open' && e.detail.ringing) {
    document.getElementById('ring-screen').hidden = false;
  }
});

Примечания: когда звонящий будильник открывает приложение, appmint:reminder также приходит с { action: 'open', ringing: true }.

notify мост#

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

Расширенное уведомление. Полный список параметров см. в NotificationBridge.show: channel (urgent|default|quiet|ongoing), image, largeIcon, bigText, ongoing, actions, progress, sound, silent, group, color. Загрузка и декодирование изображений выполняются вне основного потока, поэтому удалённое изображение никогда не заблокирует страницу.

Пример

Показывает уведомление прямо сейчас с любым набором параметров: картинка, длинный текст, кнопки, индикатор прогресса, закреплённое (постоянное), канал, звук, цвет и метка.

Возвращает: true, когда параметры были корректным JSON и уведомление передано системе (оно строится в фоновом потоке, поэтому неверный URL изображения не блокирует страницу); false — при некорректном JSON. Требуется: включите Уведомления на шаге «Доступ» (шаг 4) при сборке и разрешение пользователя на Android 13+ (запрашивается с помощью Notification.requestPermission()).

Простое уведомление. tag — это ID: повторный показ с той же меткой заменяет старое.

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'
  }));
}

Картинка, большой значок и длинный текст. image имеет приоритет над bigText, если указаны оба.

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'
}));

Кнопки (не более 3). Нажатие приходит как событие appmint:notification-action.

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);
});

Закреплённый прогресс (постоянное). Чтобы обновить, показывайте заново с той же меткой. Когда работа закончена, покажите итоговое состояние с ongoing: false, чтобы пользователь мог его смахнуть.

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' }));

Другие параметры: sound (файл из вашего пакета, например '/audio/ding.mp3', URL data:audio/... или 'silent'), smallIcon (имя drawable), when (epoch в мс, показывается как время), openApp: true (полноэкранное оповещение; см. scheduleNotificationEx).

Примечания: у кнопок нет встроенного поля ответа, а нажатие кнопки доходит до страницы, только пока приложение запущено (приложение оно не открывает); событие также вызывает window.onAppMintNotificationAction(detail), если вы её определили. image и largeIcon принимают URL https:// или data: либо изображение из пакета приложения, названное так, как его называет ваша страница ('img/logo.png', '/img/logo.png' или полный адрес https://appassets.androidplatform.net/assets/…). Неудачное изображение пропускается, уведомление всё равно показывается. ongoing: true всегда использует тихий канал «Ongoing». У notify() нет публичной парной функции «удалить»: cancelNotification отменяет только запланированные напоминания. Уведомление, которое позже нужно убрать со страницы, следует показывать через new Notification(...) и убирать его close(); закреплённую карточку прогресса — через Capacitor.AppwrightLiveProgress (end). Android фиксирует звук канала при его создании, поэтому новое значение sound создаёт новый канал.

openNotificationSettings мост#

window.WebToApk.openNotificationSettings(): Boolean

Открывает настройки уведомлений этого приложения; true, если экран удалось открыть.

Пример

Открывает собственный экран настроек уведомлений этого приложения, где пользователь может включить или выключить уведомления (и каждый канал).

Возвращает: true, когда экран был открыт, синхронно; false, когда это невозможно (Android 13+ и в сборке нет уведомлений). На Android 8+ это страница уведомлений приложения, на более старых — экран «О приложении». Требуется: ничего включать не нужно.

document.getElementById('notif-settings-link').onclick = function () {
  if (!(window.WebToApk && WebToApk.openNotificationSettings && WebToApk.openNotificationSettings())) {
    showMessage('Open Settings > Apps > this app > Notifications.');
  }
};

Примечания: чтобы вернуть пользователя одним нажатием, используйте вместо этого AppMint.enableNotifications(): он показывает диалог разрешения, пока Android это позволяет, и открывает этот экран, только если нет.

prepareNotificationSound мост#

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

Заранее копирует встроенный аудиофайл на место и сообщает, пригоден ли он как звук уведомления. Необязательно — `notify()` и `scheduleNotificationEx()` делают это сами, — но позволяет экрану настроек проверить звук сразу после выбора.

Пример

Проверяет, что аудиофайл из вашего приложения можно использовать как звук уведомления, и сразу копирует его на место. Необязательно: notify() и scheduleNotificationEx() делают то же сами; это позволяет экрану настроек проверить звук сразу после выбора.

Возвращает: true, если звук пригоден; false, если файла нет или это неподдерживаемый тип аудио. Требуется: ничего.

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' }));
}

Заполните выбор звука только теми звуками, которые действительно можно использовать:

const SOUNDS = [
  { label: 'Chime', path: '/audio/chime.mp3' },
  { label: 'Bell',  path: '/audio/bell.ogg' },
  { label: 'Beep',  path: '/audio/beep.wav' }
];

function renderSoundPicker(select) {
  const b = window.WebToApk;
  if (!(b && b.prepareNotificationSound)) return;
  select.innerHTML = '';
  SOUNDS.forEach(function (s) {
    if (!b.prepareNotificationSound(s.path)) return;   // missing or unsupported file
    const opt = document.createElement('option');
    opt.value = s.path; opt.textContent = s.label;
    select.appendChild(opt);
  });
}

Позвольте пользователю выбрать свой аудиофайл и использовать его для ежедневного напоминания, передав как URL data::

document.getElementById('own-sound').addEventListener('change', function (e) {
  const file = e.target.files[0];
  const b = window.WebToApk;
  if (!file || !(b && b.prepareNotificationSound)) return;
  const reader = new FileReader();
  reader.onload = function () {
    const dataUrl = reader.result;                 // 'data:audio/mpeg;base64,...'
    if (!b.prepareNotificationSound(dataUrl)) {
      showMessage('That file cannot be used as a notification sound.');
      return;
    }
    b.scheduleNotificationEx('daily-walk', JSON.stringify({
      at: nextEvening(), repeat: 'daily',
      title: 'Walk time', body: 'Time for your evening walk', sound: dataUrl
    }));
  };
  reader.readAsDataURL(file);
});

Допустимые значения: путь в вашем веб-пакете ('/audio/chime.mp3', 'audio/chime.mp3') или URL data:audio/...;base64,..., созданный вашей страницей. Типы: mp3, wav, ogg, oga, m4a, aac, flac, opus, mid, midi.

Примечания: простое имя без папки и расширения (звук res/raw) здесь не проверяется и возвращает false, хотя notify({sound: 'name'}) может его использовать. Android хранит один звук на канал, поэтому смена звука создаёт новый канал и сбрасывает настройки пользователя для этого канала.

requestExactAlarms мост#

window.WebToApk.requestExactAlarms(): Boolean

Открывает системный экран, где выдаются разрешения на точные будильники. Android 12+.

Пример

Открывает системный экран Android 12+, где пользователь разрешает этому приложению использовать точные будильники, чтобы напоминания срабатывали ровно в назначенную минуту.

Возвращает: true, когда экран настроек был открыт (и на Android 11 и старше, где ничего не нужно); false, если экран не удалось открыть. Выбор пользователя не сообщается. Требуется: включите ⏰ Напоминания на шаге «Доступ» (шаг 4) при сборке.

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;
});

Перед сохранением критичного по времени напоминания предлагайте экран настроек, только если точность времени действительно отключена:

function saveMedicineReminder(at) {
  const b = window.WebToApk;
  if (!(b && b.scheduleNotificationEx && b.getAlarmPrecision)) return;
  b.scheduleNotificationEx('medicine', JSON.stringify({
    at: at, repeat: 'daily', title: 'Medicine', body: 'Time for your tablet'
  }));
  const precision = b.getAlarmPrecision();   // 'exact' | 'inexact' | 'unavailable'
  if (precision === 'inexact') {
    showBanner('Reminders may arrive a few minutes late.', 'Fix timing', function () {
      b.requestExactAlarms();                // runs from the banner button tap
    });
  } else if (precision === 'unavailable') {
    showBanner('The phone\'s alarm service is not answering; this reminder may not fire.');
  }
}

Когда экран настроек не удаётся открыть (false), подскажите пользователю, где найти переключатель:

document.getElementById('fix-timing').addEventListener('click', function () {
  const b = window.WebToApk;
  if (!(b && b.requestExactAlarms)) return;
  const opened = b.requestExactAlarms();
  if (!opened) {
    showMessage('Open Settings > Apps > this app > "Alarms & reminders" and turn it on.');
  }
  // When it did open, read the answer on return with getAlarmPrecision().
});

Примечания: вызывайте по нажатию кнопки и сначала объясните причину — пользователь покидает ваше приложение. Уже запланированные напоминания продолжают работать в любом случае; меняется только их точность по времени.

requestNotificationPermission мост#

window.WebToApk.requestNotificationPermission(): String

Чтение и запрос разрешения POST_NOTIFICATIONS, а также управление уведомлениями, которые страница уже запланировала. Показ и планирование описаны в их собственных методах.

Описано в своей группе, Разрешение на уведомления, отмена и список, а не отдельно.

Пример

Показывает диалог Android 13+ «Разрешить уведомления?». Страницы обычно используют Notification.requestPermission(), который вызывает это и ждёт ответа пользователя.

Возвращает: строку, синхронно: 'granted', если уже разрешено (или на Android 12 и старше), иначе 'default', пока открывается диалог. Ответ пользователя вернуть она не может. Требуется: включите Уведомления на шаге «Доступ» (шаг 4) при сборке.

Используйте Promise API — он завершается с настоящим ответом ('granted' или 'denied'):

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.');
  }
});

Необработанный вызов, только чтобы открыть диалог, не дожидаясь результата:

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')
}

Старый код, передающий callback в Notification.requestPermission, получает тот же итоговый ответ:

document.getElementById('turn-on-reminders').addEventListener('click', function () {
  if (!('Notification' in window)) return;
  if (Notification.permission !== 'default') { updateReminderUi(Notification.permission); return; }
  Notification.requestPermission(function (perm) {
    updateReminderUi(perm);   // 'granted' or 'denied', once the user answers
  });
});

Примечания: спрашивайте по нажатию кнопки с понятной причиной. После двух отказов Android перестаёт показывать диалог, и ответом сразу становится 'denied'; тогда пользователь должен разрешить это в настройках приложения.

requestOpenAppPermission мост#

window.WebToApk.requestOpenAppPermission(): Boolean

Открывает системный экран, где выдаётся разрешение на полноэкранное оповещение. Android 14+.

Пример

Открывает системный экран Android 14+, где пользователь разрешает этому приложению полноэкранные оповещения, чтобы напоминание с openApp: true могло вывести приложение на экран.

Возвращает: true, когда экран настроек был открыт (и ниже Android 14, где ничего не нужно); false, если его не удалось открыть. Выбор пользователя не сообщается. Требуется: включите ⏰ Напоминания на шаге «Доступ» (шаг 4) при сборке.

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;
});

Запомните будильник, который настраивал пользователь, отправьте его в настройки и завершите, когда он вернётся:

let pendingAlarm = null;

function setWakeAlarm(at) {
  const b = window.WebToApk;
  if (!(b && b.requestOpenAppPermission)) return;
  if (b.canOpenAppAtTime() === 'denied') {
    pendingAlarm = at;
    b.requestOpenAppPermission();
    return;
  }
  b.scheduleNotificationEx('wake', JSON.stringify({ at: at, title: 'Alarm', body: 'Wake up', openApp: true }));
}

document.addEventListener('visibilitychange', function () {
  if (document.hidden || pendingAlarm === null || !window.WebToApk) return;
  if (WebToApk.canOpenAppAtTime() === 'granted') setWakeAlarm(pendingAlarm);
  else showMessage('Without that setting the alarm cannot open the app by itself.');
  pendingAlarm = null;
});

Когда экран настроек не удаётся открыть (false), подскажите, где находится переключатель:

document.getElementById('allow-fullscreen').addEventListener('click', function () {
  const b = window.WebToApk;
  if (!(b && b.requestOpenAppPermission)) return;
  if (!b.requestOpenAppPermission()) {
    showMessage('Open Settings > Apps > this app > Notifications and allow full-screen notifications.');
  }
});

Примечания: вызывайте по нажатию кнопки после объяснения причины — пользователь покидает ваше приложение.

scheduleNotification мост#

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

Чтение и запрос разрешения POST_NOTIFICATIONS, а также управление уведомлениями, которые страница уже запланировала. Показ и планирование описаны в их собственных методах.

Описано в своей группе, Разрешение на уведомления, отмена и список, а не отдельно.

Пример

Планирует простое напоминание (заголовок и текст) на будущее время. Срабатывает даже при закрытом приложении и устанавливается заново после перезагрузки телефона.

Возвращает: true, когда запланировано; false, когда id пуст или время не является числом. Требуется: включите ⏰ Напоминания на шаге «Доступ» (шаг 4) при сборке (это также включает Уведомления) и разрешение пользователя на уведомления на Android 13+.

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');
}

Узнайте, что напоминание открыло приложение (пользователь нажал на него):

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

Разовое напоминание в 8:00 в день срока задачи:

function remindTask(task) {
  if (!(window.WebToApk && window.WebToApk.scheduleNotification)) return false;
  const at = new Date(task.dueDate);
  at.setHours(8, 0, 0, 0);
  if (at.getTime() <= Date.now()) return false;     // that morning has passed
  return WebToApk.scheduleNotification(
    'task-' + task.id,
    'Due today',
    task.title,
    String(at.getTime()),                           // epoch ms as a string
    'none'
  );
}

Примечания: напоминание использует «срочный» стиль (всплывающее), а его метка равна id. Повторяющееся напоминание, пропущенное, пока телефон был выключен, срабатывает на следующем шаге. Для картинки, кнопок, собственного звука или открытия приложения поверх экрана блокировки используйте scheduleNotificationEx. Если точные будильники для приложения отключены, время может сдвигаться на минуты; проверьте getAlarmPrecision().

scheduleNotificationEx мост#

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

Полная форма [scheduleNotification]: всё, что принимает `notify()`, срабатывает в будущем при закрытом приложении. WebToApk.scheduleNotificationEx('dose-1', JSON.stringify({ at: Date.now() + 3600000, // epoch millis (alias: triggerAt) repeat: 'daily', // none|minutely|hourly|daily|weekly|weekdays|monthly|yearly // days: [1, 3, 5], // with 'weekdays': ISO Mon=1..Sun=7 (default Mon-Fri) // alarm: true, // ring like an alarm clock (getAlarmSupport()) 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 })); Встроенный звук копируется на место во время этого вызова, поэтому неверное имя файла выявляется сразу (false), а не превращается в звук по умолчанию через несколько дней.

Пример

Планирует напоминание со всеми параметрами notify() (картинка, кнопки, звук, канал, открытие приложения) на будущее время. Срабатывает, когда приложение закрыто, и переживает перезапуск.

Возвращает: true, когда запланировано. false, когда время не задано (at, triggerAt, triggerAtMillis или inSeconds), id пуст, JSON некорректен или sound указывает на файл, которого нет в вашем приложении. Требуется: включите ⏰ Напоминания на шаге «Доступ» (шаг 4) при сборке и разрешение пользователя на уведомления на Android 13+.

Ежедневное напоминание с вашим звуком, открывающее приложение (поверх экрана блокировки):

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 | weekdays | monthly | yearly
    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, action: 'open', ringing: false }
  openMeetingScreen(e.detail.id);
});

Только дни тренировок — понедельник, среда и пятница в 18:00 (days в формате ISO: понедельник = 1 … воскресенье = 7; без days 'weekdays' означает понедельник–пятницу):

WebToApk.scheduleNotificationEx('gym', JSON.stringify({
  at: nextAt(18, 0),          // epoch ms of the first time; moved to the next listed day if needed
  repeat: 'weekdays',
  days: [1, 3, 5],
  title: 'Gym time',
  body: 'Leg day'
}));

// Rent on the 31st: 'monthly' rings on the last day of shorter months (28/29/30) and on the 31st again when there is one.
WebToApk.scheduleNotificationEx('rent', JSON.stringify({ at: rentDueAt, repeat: 'monthly', title: 'Rent is due' }));
// A birthday: same date every year (29 February rings on the 28th in other years).
WebToApk.scheduleNotificationEx('bday-ana', JSON.stringify({ at: anaBirthdayAt9am, repeat: 'yearly', title: 'Ana\'s birthday' }));

Настоящий будильник (alarm: true): звонит на громкости БУДИЛЬНИКА телефона — в беззвучном режиме и в режиме «Не беспокоить», если пользователь разрешает будильники, — и продолжает звонить, пока не нажмут Отключить / Отложить или ringSeconds. Показывает значок будильника в строке состояния и выходит на экран поверх экрана блокировки:

function setWakeAlarm(atMs) {
  const b = window.WebToApk;
  if (!(b && b.getAlarmSupport)) { showMessage('Alarms work in the installed app only.'); return; }
  const support = b.getAlarmSupport();   // 'ready' | 'not_enabled' | 'needs_exact_alarm' | 'needs_notification_permission'
  if (support === 'needs_exact_alarm') { b.requestExactAlarms(); return; }
  if (support !== 'ready') { showMessage('Alarms are not available: ' + support); return; }

  const ok = b.scheduleNotificationEx('wake', JSON.stringify({
    at: atMs,
    repeat: 'weekdays',          // daily/weekly/weekdays/monthly/yearly keep 07:00 across clock changes
    title: 'Wake up',
    body: 'Good morning',
    alarm: true,
    sound: '',                   // '' = the phone's alarm tone; or '/audio/rooster.mp3'; 'silent' = vibrate only
    vibrate: true,               // needs Vibration on in the build
    ringSeconds: 300,            // stop and count as missed after 5 minutes (default 600, max 3600)
    snoozeMinutes: 9,            // 0 hides the Snooze button (default 10)
    snoozeLabel: 'Snooze', dismissLabel: 'Stop'
  }));
  if (!ok) showMessage('The alarm was not set.');
}

window.addEventListener('appmint:reminder', function (e) {
  const d = e.detail;
  // Opened by the ringing alarm:  { id: 'wake', action: 'open', ringing: true, overLockScreen: true }
  // How a ring ended (also delivered later if the app was closed):
  //   { id: 'wake', action: 'dismiss' | 'snooze' | 'missed' | 'error', alarm: true,
  //     source: 'notification' | 'page' | 'timeout' | 'replaced' | 'system',
  //     snoozeMinutes?, snoozedUntil?, error?, soundError? }
  if (d.action === 'open' && d.ringing) showRingScreen(d.id);   // your Stop / Snooze buttons call stopAlarm / snoozeAlarm
  if (d.action === 'missed') logMissedAlarm(d.id);
});

Напоминание с кнопками через 30 минут:

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' }));
  }
});

Разовое напоминание с картинкой при начале распродажи, на обычном канале:

function remindSale(sale) {                        // sale.startsAt = epoch ms
  if (!(window.WebToApk && window.WebToApk.scheduleNotificationEx)) return false;
  return WebToApk.scheduleNotificationEx('sale-' + sale.id, JSON.stringify({
    at: sale.startsAt,
    title: 'The sale starts now',
    body: sale.name,
    image: 'https://example.com/banners/' + sale.id + '.jpg',   // expanded picture
    channel: 'default'                             // not the heads-up style reminders use by default
  }));
}

Примечания: тот же id заменяет более старое напоминание. Значения по умолчанию: метка = id, канал = urgent, bigText = текст. Нажатие кнопки доходит до страницы, только пока приложение запущено. Звуковой файл копируется при планировании, поэтому неверный путь выявляется сразу, а не через несколько дней; работают mp3, wav, ogg, m4a, aac, flac и opus. openApp — единственный способ, которым приложение может само появиться на экране в заданное время; на Android 14+ пользователь должен это разрешить (canOpenAppAtTime). repeat: 'weekdays' с days вне диапазона 1–7 возвращает false. alarm: true требует 🔔 Будильник на шаге «Доступ» (шаг 4) и возвращает false, если getAlarmSupport() не равно 'ready'; одновременно звонит только один будильник (новый завершает предыдущий как missed). Если собственный звуковой файл будильника исчез к моменту звонка (его удалило Remote Update), он звонит стандартным сигналом будильника телефона, а событие содержит soundError: true.

showNotification мост#

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

ИСХОДНАЯ форма с четырьмя аргументами, сохранённая как есть в виде адаптера, чтобы продолжали работать все уже выпущенные приложения и все примеры из руководств. Теперь она передаёт вызов расширенному конструктору — а значит, аргумент `icon`, который старая реализация молча отбрасывала, наконец что-то делает.

Пример

Исходный вызов уведомления с четырьмя аргументами. Показывает уведомление с заголовком, текстом, необязательным большим значком и меткой. Новому коду следует использовать notify(optionsJson) или new Notification(...), которые умеют больше.

Возвращает: true, когда уведомление передано системе. Требуется: включите Уведомления на шаге «Доступ» (шаг 4) при сборке и разрешение пользователя на Android 13+.

Стандартный веб-API (он сам вызывает этот метод):

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' });
}

Необработанный вызов. Аргументы: заголовок, текст, значок, метка. Передайте '', если значок или метка не нужны.

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
  );
}

Обновляйте одно уведомление о статусе на месте, используя ту же метку:

function showSyncStatus(done, total) {
  const b = window.WebToApk;
  if (!(b && b.showNotification)) return;
  if (b.getNotificationPermission() !== 'granted') return;
  b.showNotification(
    'Syncing photos',
    done + ' of ' + total + ' uploaded',
    '',          // no large icon
    'sync'       // same tag every time: the text updates, no new notification
  );
}

Примечания: передаёт вызов в notify({title, body, largeIcon: icon, tag}) на канале по умолчанию. Без метки каждый вызов добавляет новое уведомление. Для картинок, кнопок, прогресса или закреплённого уведомления используйте notify.

snoozeAlarm мост#

window.WebToApk.snoozeAlarm(id: String, minutes: Int): Boolean

Останавливает звонящий будильник [id] ("" = тот, который звонит) и снова звонит через [minutes] (1–1440; 0 = собственный snoozeMinutes будильника). False, если он не звонит.

Пример

Останавливает звонящий будильник и снова звонит через несколько минут, как его кнопка Отложить. Сдвигается только этот звонок: ежедневный будильник всё равно прозвонит в обычное время завтра.

Возвращает: true, когда будильник звонил и отложен; false, когда он не звонил. id '' = тот, который звонит; minutes 1–1440 или 0 для собственного snoozeMinutes будильника (по умолчанию 10). Требуется: 🔔 Будильник на шаге «Доступ» (шаг 4) при сборке.

document.getElementById('snooze').onclick = function () {
  if (!(window.WebToApk && window.WebToApk.snoozeAlarm)) return;
  if (WebToApk.snoozeAlarm('', 5)) showMessage('Ringing again in 5 minutes');
  hideRingScreen();
};

window.addEventListener('appmint:reminder', function (e) {
  // { id: 'wake', action: 'snooze', alarm: true, source: 'page', snoozeMinutes: 5, snoozedUntil: 1767340800000 }
  if (e.detail.action === 'snooze') updateNextAlarmLabel(e.detail.snoozedUntil);
});

Запланируйте будильник, затем подключите кнопку отключения:

const b = window.WebToApk;
if (b && b.snoozeAlarm && b.getAlarmSupport() === 'ready') {
  b.scheduleNotificationEx('wake', JSON.stringify({
    at: Date.now() + 8 * 3600000, repeat: 'daily', alarm: true,
    title: 'Wake up', body: 'Good morning'
  }));
}
document.getElementById('dismiss').onclick = () => {
  if (WebToApk.isAlarmRinging()) WebToApk.stopAlarm('');
};

Отложите на длительность самого будильника и обработайте случай «ничего не звонит»:

document.getElementById('snooze').onclick = () => {
  if (!(window.WebToApk && WebToApk.snoozeAlarm)) return;
  const ok = WebToApk.snoozeAlarm('', 0);   // 0 = the alarm's own snoozeMinutes (default 10)
  if (!ok) showMessage('No alarm is ringing right now.');
};

Примечания: отложенный звонок отображается в getScheduledNotifications() как <id>#snooze; cancelNotification(id) удаляет и его.

stopAlarm мост#

window.WebToApk.stopAlarm(id: String): Boolean

Останавливает звонящий будильник [id] ("" = тот, который звонит), как его кнопка «Отключить». Повторяющийся будильник остаётся запланированным на следующее время. False, если он не звонит.

Пример

Останавливает звонящий будильник точно так же, как его кнопка Отключить. Повторяющийся будильник остаётся установленным на следующее время; чтобы удалить его, используйте cancelNotification(id).

Возвращает: true, когда этот будильник звонил и теперь остановлен; false, когда он не звонил. Передайте '', чтобы остановить тот, который звонит. Требуется: 🔔 Будильник на шаге «Доступ» (шаг 4) при сборке.

window.addEventListener('appmint:reminder', function (e) {
  const d = e.detail;                 // { id, action: 'open', ringing: true, overLockScreen }
  if (d.action === 'open' && d.ringing) showRingScreen(d.id);
});

document.getElementById('stop').onclick = function () {
  if (window.WebToApk && window.WebToApk.stopAlarm) WebToApk.stopAlarm(currentAlarmId);
  hideRingScreen();
};

Предлагайте кнопку остановки только там, где есть мост, и читайте ответ:

const stopBtn = document.getElementById('stop');
if (window.WebToApk && typeof WebToApk.stopAlarm === 'function') {
  stopBtn.onclick = function () {
    const wasRinging = WebToApk.stopAlarm('');      // '' = whichever alarm is ringing
    if (!wasRinging) showMessage('Nothing was ringing.');
  };
} else {
  stopBtn.hidden = true;                             // plain browser: no alarm clock
}

Используйте вместе с isAlarmRinging(), чтобы страница могла показать экран звонка для звонка, который начался, пока её не было:

document.addEventListener('visibilitychange', function () {
  if (document.hidden || !window.WebToApk) return;
  if (WebToApk.isAlarmRinging && WebToApk.isAlarmRinging()) showRingScreen();
  else hideRingScreen();
});

Примечания: тогда страница также получает appmint:reminder с { id, action: 'dismiss', alarm: true, source: 'page' }.