JavaScript bridge API
Notifications on the device
Posting and scheduling without a server, the POST_NOTIFICATIONS grant, exact alarms, and custom sounds.
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);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
}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);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');
});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
}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);
});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');
});Notes: you rarely need this; the copies are small. A sound is copied back the next time anything uses it - notify(), a reminder when it fires, scheduleNotificationEx, or prepareNotificationSound(path) - including on a notification channel that already exists, so the custom sound keeps playing.
getAlarmPrecision 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();
});Notes: Android 11 and older always answer 'exact'. An 'inexact' reminder still fires, just not on the minute.
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();Notes: it gives the same answer as navigator.permissions.query({ name: 'notifications' }). To ask the user, use Notification.requestPermission().
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. '[]' 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);
});
}Notes: a one-off reminder leaves the list when it fires. A repeating one stays, with trigger moved to the next time.
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.
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' }));
}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;
});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')
}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;
});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
===== Tier-2: SCHEDULED local notifications (reminders) ===== Fire a notification at a future time even when the app is closed - backed by AlarmManager + persistence so it survives reboot. Dormant unless called. JS contract (all args strings; triggerAtMillis = epoch millis): window.WebToApk.scheduleNotification(id, title, body, triggerAtMillis, repeat) repeat ∈ "none" | "minutely" | "hourly" | "daily" | "weekly" window.WebToApk.cancelNotification(id) window.WebToApk.cancelAllNotifications() window.WebToApk.getScheduledNotifications() -> JSON array string
Example
Schedules a simple reminder (title and text) for a future time. It fires even when the app is closed and is set again after the phone restarts.
Returns: true when it was scheduled; false when id is empty or the time is not a number. Needs: turn on ⏰ Reminders in the Access step (Step 4) when you build (it also turns on Notifications), and the user's notification permission on Android 13+.
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();
});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 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
title: 'Stand-up',
body: 'Daily sync starts now',
sound: '/audio/alarm.mp3', // a file in YOUR app; 'silent' for none
openApp: true // full-screen alert that puts the app on screen
}));
if (!ok) showMessage('Check the sound file path — nothing was scheduled.');
}
window.addEventListener('appmint:reminder', function (e) {
// e.detail = { id: 'standup', overLockScreen: true }
openMeetingScreen(e.detail.id);
});A reminder with buttons, in 30 minutes:
WebToApk.scheduleNotificationEx('dose-1', JSON.stringify({
inSeconds: 30 * 60,
title: 'Time for your dose',
body: 'Vitamin D — 1 tablet',
actions: [{ id: 'taken', label: 'Taken' }, { id: 'snooze', label: 'Snooze' }]
}));
window.addEventListener('appmint:notification-action', function (e) {
// e.detail = { actionId: 'taken', tag: 'dose-1' }
if (e.detail.actionId === 'snooze') {
WebToApk.scheduleNotificationEx(e.detail.tag, JSON.stringify({ inSeconds: 600, title: 'Time for your dose', body: 'Snoozed reminder' }));
}
});Notes: the same id replaces an older reminder. Defaults: tag = the id, channel = urgent, bigText = the body. A button tap reaches the page only while the app is running. The sound file is copied when you schedule, so a wrong path fails now and not days later; mp3, wav, ogg, m4a, aac, flac and opus work. openApp is the only way an app can come on screen by itself at a set time; on Android 14+ the user must allow it (canOpenAppAtTime).
showNotification 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
);
}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.