JavaScript bridge API
Notifications on the device
Posting and scheduling without a server, the POST_NOTIFICATIONS grant, exact alarms, and custom sounds.
AppMint.enableNotifications helper#
AppMint.enableNotifications()
Example
"Turn on notifications" in one call, for users who denied or skipped Android's notification prompt: the permission dialog while Android can still show it, otherwise this app's notification settings - and the new state when the user comes back.
Returns: AppMint.enableNotifications() - a Promise that always resolves {state, changed, openedSettings}; state is 'granted' | 'prompt' | 'blocked' | 'unavailable'. AppMint.getNotificationState() - a Promise of that state. AppMint.openNotificationSettings() - a Promise of true when the settings screen opened. Needs: call enableNotifications() from a button the user tapped; notifications need Notifications (or Push / Reminders) in the build.
// 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();A plain "Notification settings" link in your own settings page:
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.');
};Notes: a push or in-app message button whose link is appmint://notifications/enable runs the same flow without any page code (the page hears appmint:notifications). The app's push audience ("Notifications on / off") updates as soon as the user turns them on.
AppMint.getNotificationState helper#
window.AppMint.getNotificationState
Example
Documented together with AppMint.enableNotifications - the example there shows this one too.
"Turn on notifications" in one call, for users who denied or skipped Android's notification prompt: the permission dialog while Android can still show it, otherwise this app's notification settings - and the new state when the user comes back.
Returns: AppMint.enableNotifications() - a Promise that always resolves {state, changed, openedSettings}; state is 'granted' | 'prompt' | 'blocked' | 'unavailable'. AppMint.getNotificationState() - a Promise of that state. AppMint.openNotificationSettings() - a Promise of true when the settings screen opened. Needs: call enableNotifications() from a button the user tapped; notifications need Notifications (or Push / Reminders) in the build.
// 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();A plain "Notification settings" link in your own settings page:
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.');
};Notes: a push or in-app message button whose link is appmint://notifications/enable runs the same flow without any page code (the page hears appmint:notifications). The app's push audience ("Notifications on / off") updates as soon as the user turns them on.
AppMint.openNotificationSettings helper#
window.AppMint.openNotificationSettings
Example
Documented together with AppMint.enableNotifications - the example there shows this one too.
"Turn on notifications" in one call, for users who denied or skipped Android's notification prompt: the permission dialog while Android can still show it, otherwise this app's notification settings - and the new state when the user comes back.
Returns: AppMint.enableNotifications() - a Promise that always resolves {state, changed, openedSettings}; state is 'granted' | 'prompt' | 'blocked' | 'unavailable'. AppMint.getNotificationState() - a Promise of that state. AppMint.openNotificationSettings() - a Promise of true when the settings screen opened. Needs: call enableNotifications() from a button the user tapped; notifications need Notifications (or Push / Reminders) in the build.
// 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();A plain "Notification settings" link in your own settings page:
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.');
};Notes: a push or in-app message button whose link is appmint://notifications/enable runs the same flow without any page code (the page hears appmint:notifications). The app's push audience ("Notifications on / off") updates as soon as the user turns them on.
Notification web standard#
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.
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):
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):
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:
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 bridge#
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:
if ('setAppBadge' in navigator) navigator.setAppBadge(unreadCount);Show the "badge on app icon" setting only on phones whose launcher can show a count:
function renderBadgeSetting() {
const row = document.getElementById('badge-setting');
// setAppBadge exists only when this launcher shows counts
row.hidden = !('setAppBadge' in navigator);
}
renderBadgeSetting();Clear the count when the user opens the inbox, on any launcher that has one:
function openInbox() {
showInbox();
if ('clearAppBadge' in navigator) {
navigator.clearAppBadge().catch(function (e) {
console.warn('badge not cleared', e.name); // NotAllowedError
});
}
}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 bridge#
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:
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
}Open the right chat when the notification is tapped, then remove it from the 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();
};
}Show a short confirmation that removes itself after five seconds:
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);
}Notes: do not call WebToApk.__closeNotification directly; the __ methods may change without notice. close() fires the notification's close event.
__setAppBadge bridge#
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:
if ('setAppBadge' in navigator) navigator.setAppBadge(unreadCount);Keep the icon count in step with the unread count, and clear it at zero:
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);Put a flag badge (no number) on the icon while there is unsynced work:
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 () {});
}Notes: -1 is the spec's flag badge (no number), which a count launcher shows as 1.
cancelAllNotifications bridge#
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.
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');
});On sign-out, stop the previous user's reminders and clear the icon count:
async function signOut() {
if (window.WebToApk && window.WebToApk.cancelAllNotifications) {
WebToApk.cancelAllNotifications();
}
if ('clearAppBadge' in navigator) navigator.clearAppBadge().catch(function () {});
await auth.signOut();
showLoginScreen();
}Rebuild the whole daily schedule from the saved times (cancel all, then add each one):
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');
});
}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 bridge#
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).
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
}A reminder switch in settings that schedules or cancels by the same 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');
}
});When a task is deleted, cancel every reminder that belongs to it:
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); });
}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 bridge#
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.
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);
});A settings screen that shows both alarm switches with their current state:
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
});Warn when saved alarms were set to open the app but Android does not allow it right now:
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.');
}
}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 bridge#
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:
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');
});A storage screen button that frees the copied sound files and says how many were removed:
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).');
});On sign-out, cancel every reminder and delete the sound copies that belonged to this user:
function signOutAndReset() {
const b = window.WebToApk;
if (b && b.cancelAllNotifications) b.cancelAllNotifications();
if (b && b.clearNotificationSounds) b.clearNotificationSounds();
localStorage.removeItem('alarmSound');
goToLoginScreen();
}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.
enableNotifications bridge#
window.WebToApk.enableNotifications(callbackId: String)
Brings a user back to notifications: the system dialog while it can still be shown, otherwise the app's notification settings. Resolves `window.__appmintReplies[callbackId]` with {state, changed, openedSettings} (AppMint.enableNotifications() is the promise wrapper).
Example
Brings a user who denied or skipped notifications back: shows Android's permission dialog while it can still be shown, otherwise opens this app's notification settings and answers when the user comes back.
Returns: nothing directly. The answer {state, changed, openedSettings} is delivered to window.__appmintReplies[callbackId]; state is 'granted' | 'prompt' | 'blocked' | 'unavailable', changed is true when it differs from before the call, openedSettings is true when the settings screen was used. Already 'granted' (or 'unavailable') answers at once. Needs: call it from a button the user tapped; notifications need Notifications (or Push / Reminders) in the build.
Prefer the promise wrapper AppMint.enableNotifications(). The raw call, for code that manages its own replies:
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;
};Every finished flow is also announced, including one started by a push button (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;
});Notes: a push or in-app message button whose link is appmint://notifications/enable runs this same flow; appmint://notifications/settings opens the settings screen. The app's push audience sees the new state at once.
getAlarmPrecision bridge#
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.
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();
});Check before setting a wake-up alarm, and tell the user if it may be late:
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.');
}
}Show the reminder timing as a line in a settings screen:
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();Notes: Android 11 and older always answer 'exact'. An 'inexact' reminder still fires, just not on the minute.
getAlarmSupport bridge#
window.WebToApk.getAlarmSupport(): String
Whether `scheduleNotificationEx(id, {alarm: true, ...})` can be scheduled now: "ready" | "not_enabled" (Alarm clock is off in this build) | "needs_exact_alarm" (call requestExactAlarms) | "needs_notification_permission".
Example
Tells you whether an alarm clock (scheduleNotificationEx(id, { alarm: true, ... })) can be set on this phone right now, and if not, what the user has to allow first.
Returns: a string, synchronously: 'ready', 'not_enabled' (the app was built without 🔔 Alarm clock), 'needs_exact_alarm' (Android 12+: exact alarms are off for this app - call requestExactAlarms()), or 'needs_notification_permission' (Android 13+: no notification permission, so there would be no Dismiss button - call requestNotificationPermission()). Needs: turn on 🔔 Alarm clock in the Access step (Step 4) when you build.
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;
}
}Re-check when the user comes back from Settings:
document.addEventListener('visibilitychange', function () {
if (document.hidden || !(window.WebToApk && window.WebToApk.getAlarmSupport)) return;
document.getElementById('alarm-setup').hidden = WebToApk.getAlarmSupport() === 'ready';
});Create an alarm only when the phone is 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'
}));
}Notes: scheduleNotificationEx with alarm: true returns false whenever this is not 'ready' - an alarm that could never ring is never stored.
getNotificationPermission bridge#
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:
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:
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();Decide before scheduling whether to ask, explain, or go ahead:
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';
}Notes: it gives the same answer as navigator.permissions.query({ name: 'notifications' }). To ask the user, use Notification.requestPermission().
getNotificationState bridge#
window.WebToApk.getNotificationState(): String
'granted' | 'prompt' (Android can still ask) | 'blocked' (refused for good or switched off in Settings - only Settings fixes it) | 'unavailable' (Android 13+ and this build does not declare the notification permission). See NotificationOptIn.
Example
Reads, right now, whether this app's notifications are on - and if not, whether the app can still ask or only Settings can turn them back on.
Returns: a string, synchronously: 'granted' (notifications reach the phone); 'prompt' (Android can still show its permission dialog); 'blocked' (the user refused for good, or switched this app's notifications off in system Settings - only Settings can fix it); or 'unavailable' (Android 13+ and the app was built without Notifications / Reminders / Push, so nothing can turn them on). Needs: nothing to call it.
The raw call, to decide whether to show a "Turn on notifications" banner:
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();Notes: getNotificationPermission() keeps its own words ('granted' / 'default' / 'denied') for older pages. To bring the user back, call AppMint.enableNotifications() (it picks the dialog or Settings for you).
getScheduledNotifications bridge#
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. Live fields say how it will really be delivered, read live: precision ("exact"; "inexact" = "Alarms & reminders" is off for the app, so Android delivers it within a 10-minute window while the phone is awake, and may hold it longer while it sleeps; "alarm_clock" for alarm: true), windowMs + latestAt (the window Android was given: 0 / trigger when exact, 600000 / trigger + 600000 when inexact) and notificationsEnabled (false = it fires but Android shows nothing, because the app's notifications are off). '[]' when there are none. Needs: nothing extra.
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);
});
}Show whether one reminder is on, and when it fires next:
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');Read the extra options of reminders made with 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; });
}Notes: a one-off reminder leaves the list when it fires. A repeating one stays, with trigger moved to the next time. A reminder whose time passed while the phone was off or the app was force-stopped is shown once, late, the next time the app or phone starts; a late alarm: true is reported as missed instead of ringing. A daily, weekly, monthly or yearly reminder keeps its local time when the phone changes time zone.
Warn when reminders will not show up on time:
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 bridge#
window.WebToApk.isAlarmRinging(): Boolean
True while an alarm clock is ringing.
Example
Tells you whether one of the app's alarm clocks is ringing right now, so the page can show its own stop / snooze screen.
Returns: true while an alarm rings, otherwise false (synchronously). Needs: 🔔 Alarm clock in the Access step (Step 4) when you build.
function showRingScreenIfNeeded() {
const b = window.WebToApk;
const ringing = !!(b && b.isAlarmRinging && b.isAlarmRinging());
document.getElementById('ring-screen').hidden = !ringing;
}
showRingScreenIfNeeded();
document.addEventListener('visibilitychange', showRingScreenIfNeeded);Stop the ringing alarm from your own screen with the matching calls.
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');
});React to the app being opened by a ringing alarm.
window.addEventListener('appmint:reminder', function (e) {
if (e.detail.action === 'open' && e.detail.ringing) {
document.getElementById('ring-screen').hidden = false;
}
});Notes: when the ringing alarm opens the app, appmint:reminder also arrives with { action: 'open', ringing: true }.
notify bridge#
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.
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.
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.
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.
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.
openNotificationSettings bridge#
window.WebToApk.openNotificationSettings(): Boolean
Opens this app's notification settings; true when the screen could be opened.
Example
Opens this app's own notification settings screen, where the user can switch notifications (and each channel) on or off.
Returns: true when the screen was opened, synchronously; false when it cannot be (Android 13+ and the build has no notifications). On Android 8+ it is the app's notification page; on older Android, the App info screen. Needs: nothing to switch on.
document.getElementById('notif-settings-link').onclick = function () {
if (!(window.WebToApk && WebToApk.openNotificationSettings && WebToApk.openNotificationSettings())) {
showMessage('Open Settings > Apps > this app > Notifications.');
}
};Notes: to bring a user back in one tap, use AppMint.enableNotifications() instead: it shows the permission dialog when Android still allows it and opens this screen only when it does not.
prepareNotificationSound bridge#
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.
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' }));
}Fill a sound picker with only the sounds that are really usable:
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);
});
}Let the user pick their own audio file and use it for a daily reminder, passed as a data: URL:
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);
});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 bridge#
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.
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;
});Before saving a time-critical reminder, offer the settings screen only when timing is actually off:
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.');
}
}When the settings screen cannot be opened (false), tell the user where to find the switch:
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().
});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 bridge#
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'):
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:
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')
}Older code that passes a callback to Notification.requestPermission gets the same final answer:
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
});
});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 bridge#
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.
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;
});Remember the alarm the user was setting, send them to Settings, and finish it when they come back:
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;
});When the settings screen cannot be opened (false), say where the switch is:
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.');
}
});Notes: call it from a button tap after explaining why - it leaves your app.
scheduleNotification bridge#
window.WebToApk.scheduleNotification(id: String, title: String, body: String, triggerAtMillis: String, repeat: 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
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+.
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):
window.addEventListener('appmint:reminder', function (e) {
// e.detail = { id: 'water', overLockScreen: false }
if (e.detail.id === 'water') openWaterLog();
});A one-off reminder at 8:00 on the day a task is due:
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'
);
}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 bridge#
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|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 })); 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):
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);
});Gym days only - Monday, Wednesday and Friday at 18:00 (days are ISO: Monday = 1 … Sunday = 7; without days, 'weekdays' means Monday-Friday):
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' }));A real alarm clock (alarm: true): rings on the phone's ALARM volume - through silent mode, and through Do Not Disturb when the user allows alarms - and keeps ringing until Dismiss / Snooze or ringSeconds. It shows the alarm icon in the status bar and comes on screen over the lock screen:
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);
});A reminder with buttons, in 30 minutes:
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' }));
}
});A one-off reminder with a picture when a sale starts, on the normal channel:
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
}));
}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). repeat: 'weekdays' with days outside 1-7 returns false. alarm: true needs 🔔 Alarm clock in the Access step (Step 4) and returns false unless getAlarmSupport() is 'ready'; only one alarm rings at a time (a new one ends the previous as missed). If an alarm's own sound file has disappeared by the time it rings (a Remote Update removed it), it rings with the phone's alarm tone and the event carries soundError: true.
showNotification bridge#
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):
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.
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
);
}Update one status notification in place by reusing its tag:
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
);
}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.
snoozeAlarm bridge#
window.WebToApk.snoozeAlarm(id: String, minutes: Int): Boolean
Stops the ringing alarm [id] ("" = whichever is ringing) and rings it again in [minutes] (1-1440; 0 = the alarm's own snoozeMinutes). False when it is not ringing.
Example
Stops a ringing alarm clock and rings it again after a few minutes, like its Snooze button. Only this ring moves: a daily alarm still rings at its usual time tomorrow.
Returns: true when the alarm was ringing and is snoozed; false when it was not ringing. id '' = whichever rings; minutes 1-1440, or 0 for the alarm's own snoozeMinutes (default 10). Needs: 🔔 Alarm clock in the Access step (Step 4) when you build.
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);
});Schedule an alarm clock, then wire a dismiss button:
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('');
};Snooze by the alarm's own length, and handle "nothing is ringing":
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.');
};Notes: the snoozed ring is listed by getScheduledNotifications() as <id>#snooze; cancelNotification(id) removes it too.
stopAlarm bridge#
window.WebToApk.stopAlarm(id: String): Boolean
Stops the ringing alarm [id] ("" = whichever is ringing), like its Dismiss button. A repeating alarm stays scheduled for its next time. False when it is not ringing.
Example
Stops a ringing alarm clock, exactly like its Dismiss button. A repeating alarm stays set for its next time; use cancelNotification(id) to remove it.
Returns: true when that alarm was ringing and is now stopped; false when it was not ringing. Pass '' to stop whichever alarm is ringing. Needs: 🔔 Alarm clock in the Access step (Step 4) when you build.
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();
};Offer the stop button only where the bridge exists, and read the answer:
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
}Pair it with isAlarmRinging() so the page can show the ring screen for a ring that started while it was away:
document.addEventListener('visibilitychange', function () {
if (document.hidden || !window.WebToApk) return;
if (WebToApk.isAlarmRinging && WebToApk.isAlarmRinging()) showRingScreen();
else hideRingScreen();
});Notes: the page then also receives appmint:reminder with { id, action: 'dismiss', alarm: true, source: 'page' }.