JavaScript bridge API
Vibration and sound
Haptic feedback, the app's own click sound fired on your semantic events rather than every stray tap, and the background music track the creator chose at build time - getMusicVolume returns -1 when the app ships none, so a settings screen can hide the slider instead of showing a dead one.
cancelVibrate bridge#
window.WebToApk.cancelVibrate()
Simple haptics for the page (navigator.vibrate and a pattern form). Both are ignored unless the build enabled the Vibrate permission.
Described by its group, Vibration, rather than on its own.
Example
Stops a vibration that is still running. Most pages use the standard navigator.vibrate(0), which calls this for you.
Returns: nothing. Needs: turn on Vibrate in Step 4 (Access) when you build.
The standard way:
if ('vibrate' in navigator) navigator.vibrate(0); // or navigator.vibrate([])The raw call, for example to stop a long alarm pattern when the user taps "Dismiss":
if (window.WebToApk && window.WebToApk.vibratePattern) {
window.WebToApk.vibratePattern(JSON.stringify([1000, 500, 1000, 500, 1000]));
}
document.getElementById('dismiss').addEventListener('click', function () {
if (window.WebToApk && typeof window.WebToApk.cancelVibrate === 'function') {
window.WebToApk.cancelVibrate();
}
});Stop a running vibration when the user leaves the screen, so the phone does not keep buzzing in their pocket:
document.addEventListener('visibilitychange', function () {
if (!document.hidden) return;
if (window.WebToApk && typeof window.WebToApk.cancelVibrate === 'function') {
window.WebToApk.cancelVibrate();
}
});Notes: Safe to call when nothing is vibrating.
getMusicVolume bridge#
window.WebToApk.getMusicVolume(): Int
Current level 0..100, or -1 when this app ships no background music.
Example
Reads the level of the app's looping background music, 0 to 100.
Returns: a number, synchronously: 0-100, or -1 when this app was built without a music file. Needs: a file chosen under Background Music in Step 3 (Integrate), under Display Options, when you build.
Show a music slider only when there is music:
var slider = document.getElementById('music-slider');
var level = -1;
if (window.WebToApk && typeof window.WebToApk.getMusicVolume === 'function') {
level = window.WebToApk.getMusicVolume();
}
if (level < 0) {
slider.parentNode.hidden = true; // no music in this app: hide the setting
} else {
slider.value = level;
slider.addEventListener('input', function () {
window.WebToApk.setMusicVolume(parseInt(slider.value, 10));
});
}A mute button that returns to the user's own level on unmute (the app remembers 0 too, so keep the old level yourself):
function musicLevel() {
return window.WebToApk && typeof window.WebToApk.getMusicVolume === 'function'
? window.WebToApk.getMusicVolume() : -1;
}
var muteBtn = document.getElementById('mute');
function paint() {
var v = musicLevel();
muteBtn.hidden = v < 0;
muteBtn.textContent = v === 0 ? 'Unmute' : 'Mute';
}
muteBtn.addEventListener('click', function () {
var v = musicLevel();
if (v < 0) return;
if (v > 0) {
try { localStorage.setItem('musicBefore', String(v)); } catch (e) {}
window.WebToApk.setMusicVolume(0);
} else {
var back = 40;
try { back = parseInt(localStorage.getItem('musicBefore') || '40', 10); } catch (e) {}
window.WebToApk.setMusicVolume(back);
}
paint();
});
paint();Fade the music from its current level to a new one, for example under a dialogue scene:
function fadeMusicTo(target, ms) {
if (!window.WebToApk || typeof window.WebToApk.getMusicVolume !== 'function') return;
var start = window.WebToApk.getMusicVolume();
if (start < 0 || start === target) return;
var steps = 10, i = 0;
var t = setInterval(function () {
i++;
window.WebToApk.setMusicVolume(Math.round(start + (target - start) * i / steps));
if (i === steps) clearInterval(t);
}, ms / steps);
}
fadeMusicTo(10, 800); // dim under the dialogue
// later: fadeMusicTo(60, 800); the last level set is the one kept for the next launchNotes: Returns the level set by the build slider or by your last setMusicVolume call. When another app or a call takes the audio, the music ducks or pauses by itself; the number you read does not change.
getVolume bridge#
window.WebToApk.getVolume(stream: String): String
One stream's level: `{stream, current, max, percent, muted}` as a JSON string. stream: 'media' | 'alarm' | 'ring' | 'notification' | 'call'.
Example
Reads one of the phone's own volume levels.
Returns: a JSON string, synchronously: {"stream":"alarm","current":5,"max":7,"percent":71,"muted":false} - current/max are the phone's own steps (they differ between phones), percent is 0-100. stream is 'media', 'alarm', 'ring', 'notification' or 'call'; any other name gives {"error":"unknown_stream"}. Needs: nothing.
function warnIfAlarmSilent() {
if (!(window.WebToApk && window.WebToApk.getVolume)) return;
const v = JSON.parse(WebToApk.getVolume('alarm'));
if (!v.error && v.percent === 0) {
showMessage('Your alarm volume is off, so the alarm will not be heard.');
}
}
warnIfAlarmSilent();Show all levels in a settings screen:
['media', 'alarm', 'ring', 'notification'].forEach(function (s) {
const v = JSON.parse(WebToApk.getVolume(s));
document.getElementById('vol-' + s).value = v.percent;
});Set a volume and read the result. setVolume(stream, percent, showUi) answers with a JSON string.
function setAlarmVolume(percent) {
if (!(window.WebToApk && WebToApk.setVolume)) return;
const r = JSON.parse(WebToApk.setVolume('alarm', percent, true));
if (!r.ok) showMessage('Could not change the volume: ' + r.error); // fixed_volume, muted, ...
else document.getElementById('vol-alarm').value = r.percent;
}playClick bridge#
window.WebToApk.playClick()
Plays the app's tap sound on demand, so a page can fire it on its OWN semantic events (a button press) instead of on every stray tap - which is what a native app actually does, and the reason clickSoundMode can be "off" while this still works. A no-op only when the app was built WITHOUT tap sound (enableClickSound off). In clickSoundMode "off" - no automatic every-tap sound - an explicit call still plays: that mode exists for apps that sound on their own events. Otherwise it plays the creator's configured feedback.
Example
Plays the app's tap sound once, so your own buttons can click like a native app.
Returns: nothing. Needs: the Tap Sound switch in Step 3 (Integrate), under Display Options, when you build (on by default). With the switch off, this does nothing.
function clickSound() {
if (window.WebToApk && typeof window.WebToApk.playClick === 'function') {
window.WebToApk.playClick();
}
}
document.querySelectorAll('button, .tab').forEach(function (el) {
el.addEventListener('click', clickSound);
});One listener for a whole game board, clicking only on a tap that does something:
document.getElementById('board').addEventListener('click', function (e) {
var cell = e.target.closest('.cell');
if (!cell || cell.classList.contains('taken')) return; // a dead tap stays silent
if (window.WebToApk && typeof window.WebToApk.playClick === 'function') {
window.WebToApk.playClick();
}
placeMark(cell);
});Give your users their own "Sounds" switch on top of the build setting:
var toggle = document.getElementById('sounds-toggle');
var soundsOn = true;
try { soundsOn = localStorage.getItem('sounds') !== 'off'; } catch (e) {}
toggle.checked = soundsOn;
toggle.addEventListener('change', function () {
soundsOn = toggle.checked;
try { localStorage.setItem('sounds', soundsOn ? 'on' : 'off'); } catch (e) {}
});
function tapSound() {
if (soundsOn && window.WebToApk && typeof window.WebToApk.playClick === 'function') {
window.WebToApk.playClick();
}
}Notes: What you hear depends on the Tap sound choice:
- Follow phone setting (recommended): the system click. It is silent when the user turned off "Touch sounds" in the phone's settings. That is on purpose.
- Always play on every tap: the app's own click, always audible. Every tap on the page clicks too, so you usually do not need
playClick()in this mode.
Call it from a real button press, not from a timer.
requestDndAccess bridge#
window.WebToApk.requestDndAccess(): String
Do Not Disturb access, needed to change the ring / notification level while DND is on. "granted" | "opened" (the settings screen was opened) | "not_declared".
Example
Asks for Do Not Disturb access, which Android requires before an app may change the ring or notification volume while Do Not Disturb is on (setVolume answers dnd_access_required then).
Returns: a string: 'granted' (already allowed), 'opened' (the system screen where the user allows it was opened - re-check when they come back), or 'not_declared' (this build does not carry the permission, so Android would not list the app there). Needs: calling requestDndAccess in your app's code: the build then declares ACCESS_NOTIFICATION_POLICY automatically.
function allowRingVolume() {
if (!(window.WebToApk && window.WebToApk.requestDndAccess)) return;
const r = WebToApk.requestDndAccess();
if (r === 'opened') showMessage('Allow Do Not Disturb access for this app, then come back.');
if (r === 'not_declared') showMessage('Turn off Do Not Disturb to change the ring volume.');
}Retry the volume change once the user has allowed access:
function setRing(percent) {
const r = JSON.parse(WebToApk.setVolume('ring', percent, true));
if (r.ok || r.error !== 'dnd_access_required') return r;
const a = WebToApk.requestDndAccess(); // 'granted' | 'opened' | 'not_declared'
if (a === 'granted') return JSON.parse(WebToApk.setVolume('ring', percent, true));
showMessage(a === 'opened' ? 'Allow access, then press the button again.' : 'Turn off Do Not Disturb first.');
return r;
}Silence the ring from a "meeting mode" switch, and say why when it cannot work:
function meetingMode(on) {
if (!(window.WebToApk && WebToApk.setVolume)) return;
const r = JSON.parse(WebToApk.setVolume('ring', on ? 0 : 70, false));
if (!r.ok && r.error === 'dnd_access_required') {
const a = WebToApk.requestDndAccess();
if (a === 'not_declared') showMessage('This build cannot change the ring volume while Do Not Disturb is on.');
}
}setMusicVolume bridge#
window.WebToApk.setMusicVolume(percent: Int)
Set the background-music level from the page, 0..100. The wizard slider decides how the app STARTS; this is how an app gives its own users a music control, which any game with a settings screen is expected to have. Clamped in [BackgroundMusic.setVolume], so a page passing 500 or -1 cannot hand MediaPlayer a gain outside the range it accepts. The level is REMEMBERED: the next launch starts at it, not at the wizard's level. A no-op when the app was built without a track - there is nothing to set, and the honest answer is [getMusicVolume] returning -1 so the page can hide its slider rather than show one that does nothing.
Example
Changes the level of the app's looping background music, 0 to 100, for example from your game's settings screen.
Returns: nothing. Needs: a file chosen under Background Music in Step 3 (Integrate), under Display Options, when you build. Without music this does nothing (check getMusicVolume() for -1).
A mute button - the app remembers the level for the next launch:
function hasMusic() {
return !!(window.WebToApk && window.WebToApk.getMusicVolume) && window.WebToApk.getMusicVolume() >= 0;
}
if (!hasMusic()) document.getElementById('mute').hidden = true;
document.getElementById('mute').addEventListener('click', function () {
if (!hasMusic()) return;
// clamped to 0..100 by the app, and kept for the next launch
window.WebToApk.setMusicVolume(window.WebToApk.getMusicVolume() > 0 ? 0 : 40);
});Off / Low / High buttons in a settings screen:
var LEVELS = { off: 0, low: 25, high: 70 };
document.querySelectorAll('[data-music]').forEach(function (btn) {
btn.addEventListener('click', function () {
if (!window.WebToApk || typeof window.WebToApk.setMusicVolume !== 'function') return;
if (window.WebToApk.getMusicVolume() < 0) return; // built without music
window.WebToApk.setMusicVolume(LEVELS[btn.dataset.music]);
markSelected(btn);
});
});Turn the music down while your own video plays, then put it back (the level is remembered, so always restore it):
var video = document.getElementById('intro-video');
var before = -1;
video.addEventListener('play', function () {
if (!window.WebToApk || typeof window.WebToApk.getMusicVolume !== 'function') return;
before = window.WebToApk.getMusicVolume();
if (before > 0) window.WebToApk.setMusicVolume(Math.round(before / 4));
});
function restoreMusic() {
if (before > 0) window.WebToApk.setMusicVolume(before);
before = -1;
}
video.addEventListener('pause', restoreMusic);
video.addEventListener('ended', restoreMusic);Notes: The level you set is saved: the next launch starts at it instead of the level from the build slider (until the user clears the app's data). 0 makes the music silent but it keeps playing.
setVolume bridge#
window.WebToApk.setVolume(stream: String, percent: Double, showUi: Boolean): String
Sets a stream to [percent] (0-100); [showUi] shows the phone's volume slider. `{ok:true, stream, current, max, percent}` or `{ok:false, error}` - error is unknown_stream | bad_percent | fixed_volume | dnd_access_required | muted | not_applied | failed | foreign_origin (a page outside the app's own site).
Example
Sets one of the phone's own volume levels, 0-100 - for example turning the alarm stream up for a wake-up alarm, or a media slider in a music app.
Returns: a JSON string: {"ok":true,"stream":"alarm","current":6,"max":7,"percent":86}, or {"ok":false,"error":"…"} where error is unknown_stream, bad_percent, fixed_volume (this device has no adjustable volume), dnd_access_required (Do Not Disturb is on and blocks the ring / notification level - see requestDndAccess()), foreign_origin (a page outside your app's own site) or failed. showUi: true shows the phone's own volume slider as it moves. Needs: nothing for media, alarm and call; ring and notification may need Do Not Disturb access while DND is on.
function setAlarmLoud() {
if (!(window.WebToApk && window.WebToApk.setVolume)) return;
const r = JSON.parse(WebToApk.setVolume('alarm', 80, true));
if (!r.ok) showMessage('Could not change the alarm volume: ' + r.error);
}
document.getElementById('ring-slider').oninput = function (e) {
const r = JSON.parse(WebToApk.setVolume('ring', Number(e.target.value), false));
if (r.error === 'dnd_access_required' && WebToApk.requestDndAccess() === 'not_declared') {
showMessage('Turn off Do Not Disturb to change the ring volume.');
}
};Read a level, then set a media slider:
if (window.WebToApk && WebToApk.setVolume) {
const now = JSON.parse(WebToApk.getVolume('media')); // {stream, current, max, percent, muted}
slider.value = now.percent;
slider.oninput = () => {
const r = JSON.parse(WebToApk.setVolume('media', Number(slider.value), false));
if (r.ok) label.textContent = r.percent + '%';
else if (r.error === 'fixed_volume') slider.disabled = true; // this phone has no adjustable volume
};
}Show the current ring level and react when it changes:
if (window.WebToApk && WebToApk.getVolume) {
const ring = JSON.parse(WebToApk.getVolume('ring')); // {stream, current, max, percent, muted}
document.getElementById('ring-slider').value = ring.percent;
WebToApk.watchVolume(true);
window.addEventListener('appmint:volume', (e) => {
if (e.detail.stream === 'ring') document.getElementById('ring-slider').value = e.detail.percent;
});
}Notes: the value is rounded to the phone's nearest step, and the answer carries the level actually set. The call stream never goes below its minimum (usually 1).
vibrate bridge#
window.WebToApk.vibrate(durationMs: Long)
Simple haptics for the page (navigator.vibrate and a pattern form). Both are ignored unless the build enabled the Vibrate permission.
Described by its group, Vibration, rather than on its own.
Example
Vibrates the phone once for the given number of milliseconds. Most pages use the standard navigator.vibrate(ms), which calls this for you.
Returns: nothing. Needs: turn on Vibrate in Step 4 (Access) when you build. Without it the call does nothing.
The standard way (works the same in a browser):
if ('vibrate' in navigator) navigator.vibrate(50);The raw call:
document.getElementById('like').addEventListener('click', function () {
if (window.WebToApk && typeof window.WebToApk.vibrate === 'function') {
window.WebToApk.vibrate(40); // 40 ms
}
});
// 0 (or any value <= 0) stops a running vibration
if (window.WebToApk && window.WebToApk.vibrate) window.WebToApk.vibrate(0);A tiny tick each time a slider snaps to a new step, like a native picker:
var slider = document.getElementById('size');
var last = slider.value;
slider.addEventListener('input', function () {
if (slider.value === last) return; // same step, no tick
last = slider.value;
if (window.WebToApk && typeof window.WebToApk.vibrate === 'function') {
window.WebToApk.vibrate(10); // 10 ms: a light tick
}
});A longer buzz when a form cannot be sent, so the user feels the error:
document.getElementById('signup').addEventListener('submit', function (e) {
if (this.checkValidity()) return;
e.preventDefault();
if (window.WebToApk && typeof window.WebToApk.vibrate === 'function') {
window.WebToApk.vibrate(250);
}
this.reportValidity();
});Notes: Pass a whole number. The strength is the phone's default; for stronger/weaker taps use Capacitor.Plugins.Haptics.impact or Capacitor.Plugins.AppwrightHaptics.play. For a buzz-pause-buzz pattern use vibratePattern.
vibratePattern bridge#
window.WebToApk.vibratePattern(patternJson: String)
Vibrates an on/off pattern, e.g. "[100,50,100]" = buzz 100ms, pause 50, buzz 100. Backs navigator.vibrate(array); prefer the standard API. No-op unless the creator enabled the Vibrate permission.
Example
Vibrates an on/off pattern, given as a JSON string. Most pages use the standard navigator.vibrate([...]), which calls this for you.
Returns: nothing. Needs: turn on Vibrate in Step 4 (Access) when you build. Without it the call does nothing.
The standard way:
if ('vibrate' in navigator) navigator.vibrate([100, 50, 100]); // buzz, pause, buzzThe raw call takes the same web-style pattern as a JSON string:
function pattern(list) {
if (window.WebToApk && typeof window.WebToApk.vibratePattern === 'function') {
window.WebToApk.vibratePattern(JSON.stringify(list));
}
}
pattern([200]); // one 200 ms buzz
pattern([100, 50, 100, 50, 300]); // short, short, long
pattern([]); // empty list stops a running vibrationThe pattern plays once, so an alarm repeats it itself until the user stops it:
var alarm = null;
var ALARM = JSON.stringify([500, 300, 500, 300, 500]); // about 2.1 s
function startAlarm() {
if (!window.WebToApk || typeof window.WebToApk.vibratePattern !== 'function') return;
window.WebToApk.vibratePattern(ALARM);
alarm = setInterval(function () { window.WebToApk.vibratePattern(ALARM); }, 3000);
}
function stopAlarm() {
if (alarm === null) return;
clearInterval(alarm);
alarm = null;
window.WebToApk.vibratePattern('[]'); // stop the buzz that is still running
}Notes: The first number is a buzz, then pause, buzz, pause ... (web order). The app adds Android's leading "wait" for you, so do not put a 0 first yourself: [0, 100] means "buzz 0 ms, pause 100 ms" and you feel nothing. Negative numbers count as 0. The pattern plays once.
watchVolume bridge#
window.WebToApk.watchVolume(on: Boolean): Boolean
Turns `appmint:volume` events on (true) or off (false).
Example
Turns live volume-change events on or off: while on, every change to a volume level (the hardware keys, the system slider, another app) reaches the page as appmint:volume.
Returns: true when the change took effect. Needs: nothing.
if (window.WebToApk && window.WebToApk.watchVolume) {
window.addEventListener('appmint:volume', function (e) {
// e.detail = { stream: 'media', current: 9, max: 15, percent: 60, muted: false }
document.getElementById('vol-' + e.detail.stream).value = e.detail.percent;
});
WebToApk.watchVolume(true);
window.addEventListener('pagehide', function () { WebToApk.watchVolume(false); });
}The same object also reaches a page hook, if you prefer one:
window.onAppMintVolume = function (level) {
if (level.stream === 'alarm' && level.percent === 0) showMessage('Alarm volume is now off.');
};
WebToApk.watchVolume(true);Show the level once with getVolume, then keep it live only while the screen is visible:
if (window.WebToApk && WebToApk.watchVolume && WebToApk.getVolume) {
const meter = document.getElementById('media-meter');
meter.value = JSON.parse(WebToApk.getVolume('media')).percent;
const onVol = function (e) { if (e.detail.stream === 'media') meter.value = e.detail.percent; };
document.addEventListener('visibilitychange', function () {
if (document.hidden) { WebToApk.watchVolume(false); window.removeEventListener('appmint:volume', onVol); }
else { window.addEventListener('appmint:volume', onVol); WebToApk.watchVolume(true); }
});
}Notes: one event per stream that changed (media, alarm, ring, notification, call). Turn it off when the screen that shows levels closes.