Permissions in JavaScript: WebInto.app `permissions` Bridge
When your website runs inside WebInto.app, you may need to know whether the user has allowed notifications, location, microphone, or camera—or to open system settings if they previously denied access. The permissions bridge lets your page ask the same permission flow the app uses, without leaving the WebView.
This guide documents the permissions bridge: window.Native.call(method, payload, callback) — the third argument is a function the app invokes with the result (JSON string or already-parsed object, depending on the host). The payload you pass is the usual { action, payload } object described below.
If your template defines waitForNative, run it before the first call (same idea as the custom HTML no-internet screen guide). Other bridge tutorials: OneSignal, screen brightness.
When to use the permissions bridge
| Goal | Suggested action |
|---|---|
| Read current access without a prompt | CHECK_PERMISSION |
| Show the system permission sheet (when allowed) | ASK_FOR_PERMISSION |
| User should fix things in system settings | OPEN_APP_SETTINGS |
| Ping that the bridge exists | NONE |
Native method name: permissions. The app uses one permission pipeline for both the WebView and this bridge, so CHECK / ASK stay aligned with what the shell already uses for geo, mic, camera, and notifications.
Payload (by action)
action |
payload passed inside { action, payload } |
|---|---|
NONE |
null |
CHECK_PERMISSION |
{ "permission": "<TYPE>" } |
ASK_FOR_PERMISSION |
{ "permission": "<TYPE>" } |
OPEN_APP_SETTINGS |
null |
<TYPE> (case-insensitive in the app; normalize in JS if you like): NOTIFICATION | LOCATION | AUDIO | VIDEO — maps to remote notifications, location, microphone, and camera.
Unknown or missing permission on CHECK / ASK returns status: false with a message asking for one of the four types above.
Response shape
Parsed JSON: status (boolean), optional message, optional params.
| Field | Type | Description |
|---|---|---|
status |
boolean | true when the call completed as expected (including many “user denied” outcomes for ASK). |
message |
string or omitted | Human-readable detail when status is false. |
params |
object or omitted | After CHECK or ASK, when status is true and params is present, expect permissionStatus. |
params.permissionStatus
GRANTED | DENIED | DENIED_ALWAYS | NOT_DETERMINED | NOT_GRANTED
| Value | Meaning (plain language) |
|---|---|
GRANTED |
Access is allowed. |
DENIED |
User declined (or access not granted) this time; they may be asked again later depending on the OS. |
DENIED_ALWAYS |
ASK_FOR_PERMISSION will not fix this—send the user to system settings. |
NOT_DETERMINED |
No final choice yet. On check, often “not granted / not asked yet”. On ask, can mean the user dismissed the system sheet without choosing. |
NOT_GRANTED |
Android-focused: the OS may not distinguish “not asked yet” vs “blocked like always-deny”. Treat as “not usable until the user acts” — often follow with ASK_FOR_PERMISSION or guide to settings; do not assume which case it is. On iOS, CHECK more often surfaces NOT_DETERMINED, DENIED, DENIED_ALWAYS, or GRANTED. |
Bad JSON or unknown action → status: false and message explains.
How CHECK_PERMISSION maps into these strings
- Granted →
GRANTED - Denied →
DENIED - DeniedAlways →
DENIED_ALWAYS - NotDetermined →
NOT_DETERMINED - NotGranted →
NOT_GRANTED(see table)
How ASK_FOR_PERMISSION maps
You still usually get status: true with permissionStatus set (denial is not a “transport error”):
- User allows →
GRANTED - Permanent-style denial →
DENIED_ALWAYS - Ordinary denial →
DENIED - User dismissed the sheet with no choice →
NOT_DETERMINED - Unexpected failures →
status: falseandmessage
Simple flow
CHECK_PERMISSIONfirst.GRANTED→ done.NOT_GRANTED,NOT_DETERMINED,DENIED→ASK_FOR_PERMISSION(samepermissiontype).DENIED_ALWAYS(from check or after ask) → short UI text, thenOPEN_APP_SETTINGS(usually on a button, not auto-spam).- After the user returns from settings,
CHECK_PERMISSIONagain if you need a fresh state.
Example code
Uses window.Native.call(method, payload, callback) — the callback receives the JSON string (or object, depending on the host). The helpers below parse either shape.
const PERMISSIONS_METHOD = "permissions";
function permissionsParse(raw) {
if (raw == null) return { _raw: raw };
if (typeof raw === "object") return raw;
try {
return JSON.parse(raw);
} catch (e) {
return { _parseError: String(e), _raw: raw };
}
}
function hasNative() {
return !!(window.Native && typeof window.Native.call === "function");
}
function permissionsCall(payload) {
return new Promise((resolve, reject) => {
if (!hasNative()) {
reject(new Error("window.Native.call missing"));
return;
}
console.log("[permissions] calling:", payload);
window.Native.call(PERMISSIONS_METHOD, payload, function (raw) {
const res = permissionsParse(raw);
console.log("[permissions] response:", res);
resolve(res);
});
});
}
window.permissionsNone = async function () {
return permissionsCall({ action: "NONE", payload: null });
};
window.permissionsCheck = async function (permission = "LOCATION") {
const perm = String(permission || "LOCATION").toUpperCase();
return permissionsCall({
action: "CHECK_PERMISSION",
payload: { permission: perm },
});
};
window.permissionsAsk = async function (permission = "LOCATION") {
const perm = String(permission || "LOCATION").toUpperCase();
return permissionsCall({
action: "ASK_FOR_PERMISSION",
payload: { permission: perm },
});
};
window.permissionsOpenSettings = async function () {
return permissionsCall({ action: "OPEN_APP_SETTINGS", payload: null });
};
/** CHECK → ASK if needed; DENIED_ALWAYS → open settings. Uses helpers above only. */
window.ensurePermission = async function (permission = "LOCATION") {
const perm = String(permission || "LOCATION").toUpperCase();
const check = await window.permissionsCheck(perm);
if (!check || typeof check.status !== "boolean") {
return { ok: false, reason: "no_response", detail: check };
}
if (!check.status) return { ok: false, step: "check", response: check };
const s1 = check.params && check.params.permissionStatus;
if (s1 === "GRANTED") return { ok: true, permissionStatus: s1 };
if (s1 === "DENIED_ALWAYS") {
await window.permissionsOpenSettings();
return { ok: false, permissionStatus: s1, openedSettings: true };
}
if (s1 === "NOT_GRANTED" || s1 === "NOT_DETERMINED" || s1 === "DENIED") {
const ask = await window.permissionsAsk(perm);
if (!ask || typeof ask.status !== "boolean") {
return { ok: false, reason: "no_response", detail: ask };
}
if (!ask.status) return { ok: false, step: "ask", response: ask };
const s2 = ask.params && ask.params.permissionStatus;
if (s2 === "DENIED_ALWAYS") {
await window.permissionsOpenSettings();
return { ok: false, permissionStatus: s2, openedSettings: true };
}
return { ok: s2 === "GRANTED", permissionStatus: s2 };
}
return { ok: false, permissionStatus: s1 };
};
console.log("Permissions bridge ready");
Try it
Run after the block above (so permissionsCall / permissionsCheck exist).
await window.permissionsCheck("LOCATION");
await window.ensurePermission("LOCATION");
// same as ensurePermission("location") — normalized to uppercase
If you see no_response or a missing status: the host did not invoke the third-argument callback, or the permissions bridge is not registered. Your WebInto build must support Native.call(method, payload, callback) (same shape as in-app templates).
Actions
NONE
Confirms the permissions bridge is reachable.
Request
{ "action": "NONE", "payload": null }
Response
{ "status": true }
CHECK_PERMISSION
Reads the current decision without showing a system dialog. On Android, the result may include NOT_GRANTED—see Response shape.
Request (example)
{ "action": "CHECK_PERMISSION", "payload": { "permission": "LOCATION" } }
Response (example)
{
"status": true,
"params": {
"permissionStatus": "GRANTED"
}
}
Missing or unknown permission
{
"status": false,
"message": "please send permission in payload: NOTIFICATION, LOCATION, AUDIO, or VIDEO."
}
ASK_FOR_PERMISSION
Asks the system to show the permission prompt (when the OS allows it). Same payload shape as CHECK_PERMISSION.
After the user responds (or dismisses the sheet), you still usually get status: true with params.permissionStatus. See “How ASK_FOR_PERMISSION maps” under Response shape for the full list (GRANTED, DENIED, DENIED_ALWAYS, NOT_DETERMINED, and when status: false applies).
OPEN_APP_SETTINGS
Opens the app’s system settings screen so the user can toggle permissions manually.
Request
{ "action": "OPEN_APP_SETTINGS", "payload": null }
Response
{ "status": true }
Pair OPEN_APP_SETTINGS with clear UI (“Notifications are blocked—open Settings to enable”) when permissionStatus is DENIED_ALWAYS. Prefer opening settings after the user taps a button, not automatically on every load. For DENIED (without “always”), you may still show in-app guidance before sending them to Settings.
Invalid body
If the JSON cannot be parsed or action is not one of the supported values:
{
"status": false,
"message": "please send valid parameters see docs."
}
Tip: only run this inside WebInto.app
If the same URL opens in a normal browser, window.Native.call will be missing. Combine permissionsCall with User-Agent or feature detection (see the OneSignal article) so you do not treat “no bridge” as a permission denial.
Summary
Use Native.call('permissions', { action, payload }, callback) and parse the callback argument (string or object). Follow CHECK → ASK → settings; honor NOT_GRANTED on Android as “needs user action” before assuming a final state. Use window.ensurePermission (or the smaller helpers) from the example code block; gate OPEN_APP_SETTINGS on a button when DENIED_ALWAYS.
For reload, error screens, or connectivity flows, use the web bridge as in the custom HTML no-internet screen guide. For brightness, see the screen brightness tutorial.