If you have ever created a web application which requires different features (like push notifications, webcam access, midi access), you probably noticed that their APIs look very different.
// for the geolocation API you need to call getCurrentPosition to check if geolocation is accessible navigator.geolocation.getCurrentPosition(gotLocation, didNotGetLocation); // for notifications we can directly check on the Notification object if (Notification.permission == 'granted') // do notification stuff if (Notification.permission == 'denied') // ask for notification access ....
This is not very handy.
The Permissions API allows us to have overview of the permissions available on our pages. What we mean by “permission” is whether we can access a specific feature with our code. Features that require permission to access them with code are called powerful features. Camera, midi, notifications, geolocation are all powerful features.
All powerful feature’s APIs are a bit different. Thus, it can be a pain to figure out what is the state of each feature’s permissions. With the Permissions API, we manage all the permission’s statuses with a single interface.
↓ Here's a great JavaScript course we recommend. Plus, this affiliate banner helps support the site 🙏
Permissions API Basics
The permission API is very experimental at this stage and should be used carefully. You should only use it if it is mission critical and you can keep up with future breaking changes. For instance, a few browsers used to support navigator.permissions.revoke
but it is now deprecated.
At the time of the writing, query
is the only property we can access from then permissions
interface. query
takes an object as an argument called a PermissionDescriptor. The permission descriptor has one field called name
, which is the name of the permission you want to access.
// This query will give us information about the permissions attached to the camera navigator.permissions.query({name: 'camera'})
The query returns a promise which resolves to a PermissionStatus
. PermissionStatus has two fields: state
and onchange
.
navigator.permissions.query({name: 'camera'}).then( permissionStatus => { console.log(permissionStatus) // in my browser on this page it logs: //{ // status: "prompt", // onchange: null, // } })
state
has 3 possible states: “granted”, “denied” and “prompt”. “granted” means that we have access to the feature. “denied” means that we won’t be able to access the feature. “prompt” means that the User-Agent (i.e. the browser) will ask the user for permission if we try to access that feature.
Some PermissionDescriptor
have additional fields and you can read more about them here. For example, camera
’s PermissionDescriptor has an additional field called deviceId
if you want to target a specific camera. Your query might look like this: .query({name: 'camera', deviceId: "my-device-id"})
.
onchange
is an event listener which activates whenever the permissions of the queried feature changes.
navigator.permissions.query({name:'camera'}).then(res => { res.onchange = ((e)=>{ // detecting if the event is a change if (e.type === 'change'){ // checking what the new permissionStatus state is const newState = e.target.state if (newState === 'denied') { console.log('why did you decide to block us?') } else if (newState === 'granted') { console.log('We will be together forever!') } else { console.log('Thanks for reverting things back to normal') } } }) })
All Permissions API
There are a lot of different powerful permissions and browser support is very uneven. In the following script, you can see all the permissions described by W3C’s editor’s draft in the permissionsName
variable. The getAllPermissions
function returns an array with the different permissions available and their state. Please note that the result will change depending on your browser, the user’s preference and of course the website’s setup.
const permissionsNames = [ "geolocation", "notifications", "push", "midi", "camera", "microphone", "speaker", "device-info", "background-fetch", "background-sync", "bluetooth", "persistent-storage", "ambient-light-sensor", "accelerometer", "gyroscope", "magnetometer", "clipboard", "display-capture", "nfc" ] const getAllPermissions = async () => { const allPermissions = [] // We use Promise.all to wait until all the permission queries are resolved await Promise.all( permissionsNames.map(async permissionName => { try { let permission switch (permissionName) { case 'push': // Not necessary but right now Chrome only supports push messages with notifications permission = await navigator.permissions.query({name: permissionName, userVisibleOnly: true}) break default: permission = await navigator.permissions.query({name: permissionName}) } console.log(permission) allPermissions.push({permissionName, state: permission.state}) } catch(e){ allPermissions.push({permissionName, state: 'error', errorMessage: e.toString()}) } }) ) return allPermissions }
If I then run the following code in my developer console on Alligator.io:
(async function () { const allPermissions = await getAllPermissions() console.log(allPermissions) })()
Here’s a screenshot of what I get at the console:
Permissions in Workers
So far we only used the navigator.permissions
API because it is much easier to write concise examples. The Permissions API is also available inside of workers. WorkerNavigator.permissions
allows us to check permissions inside our workers.
Hopefully you now have a better idea on how to use the Permissions API. It's not very complicated, nor is it essential but it does make it much easier for us to manage permissions in our JavaScript-based apps. There will probably be some new features and changes to the API and we'll keep you updated!