In this codelab, you'll learn how to better handle permissions in your web application.

This will provide a better user experience and better understanding regarding permission requests to your users.

What you'll learn

What you'll need

How will you use this tutorial?

Read it through only Read it and complete the exercises

How would rate your experience with building Web apps?

Novice Intermediate Proficient

You can either download all the sample code to your computer:

Download Zip file

...or clone the GitHub repository from the command line:

$ git clone https://github.com/mounirlamouri/permissions-introduction.git
  1. Start up a web server on localhost

    To complete this codelab you will need to run a local web server. You may already have your own setup for this. Otherwise open a terminal window, navigate to the permissions-introduction directory you created when you downloaded the code (see above), and run the following Python command to start a server:
$ python -m SimpleHTTPServer 8080


This will start a server on the default HTTP port 8080. Navigate to http://localhost:8080 from your browser to see a listing for the top level permissions-introduction directory.

To view your own work in the app directory, navigate to http://localhost:8080/app. To view the examples of completed code for each step, navigate to the directories in http://localhost:8080/completed.

If you don't have Python, you can get it here. If there are problems starting the server, check that there is not another service using port 8080.

Alternatively, you may prefer to use web server stacks such as XAMPP or MAMP.

  1. Locate where to add the code

    In your app directory, locate the index.html; the end of the file should contain the following code:
<script async defer>
// Write code here...
</script>
</html>


Replace "// Write code here..." with your changes or add them below the line.

  1. Get user's location

    Add the following code to get the user's location:
navigator.geolocation.getCurrentPosition(function() {
  console.log('success');
}, function() {
  console.log('error');
});

This code requests the user's current position and adds two callbacks, one if the position is given (the actual positions isn't used in this codelab) and the other if the position can't be reported back. The latter situation usually happens if the user rejects the permission request.

  1. Try it out from localhost

Open index.html from localhost and open Chrome DevTools. On page load, you should see a permission prompt. Do not interact with it and open the console.

If you deny the permission prompt or dismiss it, you should see "error" in the console and "success" otherwise.

  1. Reset permission

    After interacting with the permission prompt, you can reset it to its default value. In order to do that, you can click on the document icon on the left of the URL bar. You should see a Permissions section with a Location entry which you can set back to "Use global default (Ask)".

  1. Show content when geolocation is known

    After you have tested the previous code, you can change it to this:
navigator.geolocation.getCurrentPosition(function() {
  showRestaurantsAround();
}, function() {
  console.log('Geolocation request failed!');
});

showRestaurantsAround() is defined in helpers.js and simulates finding restaurants around the user location.

Now, if you grant the geolocation permission, some content should appear on the screen.

If this page were a website, it would be a poor user experience because nothing is shown while the page waits for the user location. We might want to show something in case the user rejects or dismisses the request.

Before the call to navigator.geolocation.getCurrentPosition(), you can add the following code:

showTopRestaurants();

You script should now look like:

<script async defer>
showTopRestaurants();
navigator.geolocation.getCurrentPosition(function() {
  showRestaurantsAround();
}, function() {
  console.log('Geolocation request failed!');
});
</script>

Now, the page will always show three restaurants. They will stay the same if the user dismisses or rejects the permission request and they will be updated if the request is granted.

The experience is still sub-optimal because when the permission is granted, the page starts by showing some content then shows something different. Unfortunately, the Geolocation API doesn't expose whether the page has the permission to request geolocation before the call happens. However, it is possible to get that information by using the Permissions API.

  1. Querying permission status

The Permissions API provides a query() method that allows a website to know the status of a given permission. We can use this here to know whether the geolocation call will succeed. You can remove all the code that was previously in the script tag. We will instead base the logic on the permission status. We will start by getting it:

<script async defer>
navigator.permissions.query({name:'geolocation'}).then(function(p) {
  console.log(p);
});
</script>

You can have a look at the PermissionStatus object using the console. You will see that it contains a state property that should be set to "prompt". It might be "granted" or "denied" too.

  1. Show default content if permission is denied

We will start by handling the case where the permission was denied. If the geolocation permission is denied, it is a clear signal that the user isn't interested in getting location-based content. We should then fall back to the default content:

<script async defer>
navigator.permissions.query({name:'geolocation'}).then(function(p) {
  switch (p.state) {
    case 'denied':
      showTopRestaurants();
      break;
  }
});
</script>
  1. Request location if permission is granted

If the permission is already granted, geolocation calls should succeed and the user will not be prompted. We can then just make the geolocation call and update the content when the call succeeds.

    case 'granted':
      navigator.geolocation.getCurrentPosition(showRestaurantsAround);
      break;
  1. Show default content with a opt-in button for prompting

Finally, we need to handle the case where the user didn't yet make a choice regarding the geolocation permission. In this case, it is considered a good practice to make sure the user understands why the permission is needed. Furthermore, the request should follow a user gesture to create a clear relationship between the action and the prompt. Also, it's worth noting that some APIs require a user gesture to be used so it's a good practice to keep in mind.

We will start by adding a basic banner asking the user for geolocation permission and explaining why it is needed. You can insert this code inside the <body>, after <div id='restaurant-list'></div>:

<div id='geolocation-banner' hidden>
  <span>You can have better location-based result if you grant us access to
  your location. Are you interested?</span>
  <button>I'm in</button>
</div>

This HTML adds a hidden text with a button. When the permission state is prompt, we will show this message and make the button do a geolocation request. Because the user might never interact with the prompt or might still block it, we want to make sure we show some content beforehand. If the request is granted, the content will be replaced.

The following snippet does that. It is the last state to handle in the switch statement we added earlier:

    case 'prompt':
      showTopRestaurants();

      var banner = document.querySelector('#geolocation-banner');
      banner.hidden = false;
      var button = banner.querySelector('button');
      button.onclick = function() {
        banner.hidden = true;
        navigator.geolocation.getCurrentPosition(showRestaurantsAround);
      };
      break;

5. Overview

The script should now look like this:

<script async defer>
navigator.permissions.query({name:'geolocation'}).then(function(p) {
  switch (p.state) {
    case 'denied':
      showTopRestaurants();
      break;
    case 'granted':
      navigator.geolocation.getCurrentPosition(showRestaurantsAround);
      break;
    case 'prompt':
      showTopRestaurants();

      var banner = document.querySelector('#geolocation-banner');
      banner.hidden = false;
      var button = banner.querySelector('button');
      button.onclick = function() {
        banner.hidden = true;
        navigator.geolocation.getCurrentPosition(showRestaurantsAround);
      };
      break;
  }
});
</script>

The page now properly handles all the possible permission states at page load and offers the best possible experience to the user without aggressively prompting them.

The Permissions API allows a website to get notified when a permission state changes. In this step, we will add a checkbox that will be used to enable notifications for the website and we will make the checkbox state match the notification permission state. This step will not update the permission state if the user interacts with the checkbox; that will be done later.

  1. Create checkbox for notifications permission

We need to add the checkbox to the HTML. We can add it just after the geolocation-banner, like the following:

  <div id='notification-checkbox' hidden>
    <span>Do you want to be notified when new results are available?</span>
    <input type='checkbox'>
  </div>
  1. Query for notifications permission status

In order to get notified for permission state, we first need to query for the current state; we can add this the line after the permission query for geolocation:

navigator.permissions.query({name:'notifications'}).then(function(p) {
  console.log(p);
});
  1. Listen for the change event

The PermissionStatus instance p will receive a change event when the permission changes. The instance's state property will be updated, and reading p again will show the new permission state. The following snippets should now show the PermissionStatus object in the console every time the permission is updated:

navigator.permissions.query({name:'notifications'}).then(function(p) {
  console.log(p);
  p.onchange = function() {
    console.log(p);
  };
});
  1. Dynamic update of the notifications checkbox

We need to create a method that will update the notification div and the checkbox depending on the permission state. If the permission is enabled, the checkbox will be checked. If the permission is set to prompt, the checkbox will be unchecked. If the permission is denied, the entire block will be hidden:

function updateNotificationCheckbox(state) {
  var notificationBlock = document.querySelector('#notification-checkbox');
  var checkbox = notificationBlock.querySelector('input');
  switch (state) {
    case 'granted':
      notificationBlock.hidden = false;
      checkbox.checked = true;
      break;
    case 'prompt':
      notificationBlock.hidden = false;
      checkbox.checked = false;
      break;
    case 'denied':
      notificationBlock.hidden = true;
      checkbox.checked = false;
      break;
  }
}
navigator.permissions.query({name:'notifications'}).then(function(p) {
  updateNotificationCheckbox(p.state);
  p.onchange = function() {
    updateNotificationCheckbox(this.state);
  };
});

The Permissions API in Chrome currently has only one method, query(), that allows websites to check a permission status. Chrome 47 has new experimental features for the Permissions API that allow websites to have a better control of their permissions.

The next and last step of this codelab will use these experimental features. To enable them, you have to go to chrome://flags in a new tab and search for Enable experimental Web Platform features. Then, you will have to click "Enable" and restart Chrome by clicking the "Relaunch Now" button at the bottom of the Window.

Earlier, we added a checkbox that reflects the notification permission status, but changing the checkbox state has no effect. We will allow the website to control the permission via the checkbox. To do so, we will listen to the change event on the checkbox and call request() or revoke() depending on the new checkbox state.

navigator.permissions.request() allows a website to request a permission before using it. It can be used in a settings page for example, instead of requesting a permission out of the blue, without enough context offered to the user.

navigator.permissions.revoke() allows a website to reset a permission to its default state if the permission was already granted. This way, a website can clean up its state.

In order to implement this behavior, we will have to add this snippet at the end of the current Javascript code:

document.querySelector('#notification-checkbox > input').onchange = function() {
  if (this.checked) {
    navigator.permissions.request({name:'notifications'});
  } else {
    navigator.permissions.revoke({name:'notifications'});
  }
};

Note that we don't need to check for the result of the calls because we automatically update the UI when the permission status changes. Otherwise, these methods return a similar promise to the one returned by query() which would allow us to know the new permission state and get notified when it is updated.