Seven Awesome PWA Features

Progressive Web Apps (PWA) represent a collection of capabilities that put web apps on a level playing field with native iOS, Android, and desktops apps. The following tutorial implements a 7 lesser-known web features.

1. App Shortcuts

App Shortcuts

file_type_config manifest.json
{
    "name": "Fireship",
    //...
    "shortcuts": [
      {
        "name": "Activity Feed",
        "short_name": "Feed",
        "description": "View your activity feed",
        "url": "/feed?utm_source=homescreen",
        "icons": [{ "src": "/icons/feed.png", "sizes": "192x192" }]
      },
      {
        "name": "Recent Comments",
        "short_name": "Comments",
        "description": "View recent comments",
        "url": "/comments?utm_source=homescreen",
        "icons": [{ "src": "/icons/comments.png", "sizes": "192x192" }]
      }
    ]
}

2. Contact Picker

The Contact Picker requires an HTTPS connection. If developing locally, use Ngrok to setup an SSH tunnel that can broadcast localhost to a secure URL.

file_type_js app.js
const btn = document.getElementById('contacts');
btn.addEventListener('click', (event) => getContacts());

const props = ['name', 'email', 'tel', 'address', 'icon'];
const opts = {multiple: true};
const supported = ('contacts' in navigator && 'ContactsManager' in window);

async function getContacts() {

    if (supported) {
        const contacts = await navigator.contacts.select(props, opts);
        console.log(contacts);
    } else {
        alert('contact picker not supported!')
    }

}

3. Device Motion

Device Motion

file_type_js app.js
window.addEventListener('devicemotion', function(event) {
    const el = document.getElementById('motion');
    console.log(event);
    el.innerText = (Math.round((event.acceleration.x + Number.EPSILON) * 100) / 100) + ' m/s2';
    // el.innerText = event.rotationRate.beta;
  });

  window.navigator.geolocation.getCurrentPosition(console.log)

4. Bluetooth & External Devices

Bluetooth

file_type_js app.js
const button = document.getElementById('ble');
button.addEventListener('click', (event) => connectBluetooth());

async function connectBluetooth() {

    // Connect Device
    const device = await navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] });
    const server = await device.gatt.connect();

    // Get heart rate data
    const hr = await server.getPrimaryService('heart_rate');
    const hrMeasurement = await hr.getCharacteristic('heart_rate_measurement');

    // Listen to changes on device
    await hrMeasurement.startNotifications(); 

    hrMeasurement.addEventListener('characteristicvaluechanged', (e) => {
        console.log(parseHeartRate(e.target.value));
    });

}

5. Idle Detection

Idle Detection

file_type_js app.js
if ('IdleDetector' in window) {

    const idleBtn = document.getElementById('idle');
    idleBtn.addEventListener('click', (event) => runIdleDetection());


    async function runIdleDetection() {
        const state = await IdleDetector.requestPermission();
        console.log(state);

        const idleDetector = new IdleDetector();

        idleDetector.addEventListener('change', () => {
            const { userState, screenState } = idleDetector;
            console.log(idleDetector)
        
            if (userState == 'idle') {
                // update database with status
            }
        });

        await idleDetector.start({
            threshold: 120000,
        });
    }
}

6. File System

File System

file_type_js app.js
const getFileBtn = document.getElementById('fs-get')

getFileBtn.onclick = async () => {
    const [handle] = await window.showOpenFilePicker();
    const file = await handle.getFile();
    console.log(file)
};


const saveFileBtn = document.getElementById('fs-save')

saveFileBtn.onclick = async () => {
  const textFile = new File(["hello world"], "hello.txt", {
    type: "text/plain",
  });
  const handle = await window.showSaveFilePicker();
  const writable = await handle.createWritable();
  await writable.write(textFile);
  await writable.close();
};

7. Web Share

Use the Share Target API by updating the manifest with a share target:

file_type_config manifest.json
{
    "name": "Fireship",
    //...
    "share_target": {
      "action": "/share-photo",
      "method": "POST",
      "enctype": "multipart/form-data",
      "params": {
        "title": "name",
        "text": "description",
        "url": "link",
        "files": [
          {
            "name": "photos",
            "accept": "image/png"
          }
        ]
      }
    },
}

Use the Web Share API to open a share dialog on the native device:

file_type_js app.js


const shareBtn = document.getElementById('share');

shareBtn.onclick = async (filesArray) => {
    if (navigator.canShare) {
        navigator.share({
            url: 'https://fireship.io',
            title: 'PWAs are awesome!',
            text: 'I learned how to build a PWA today',
        })
    }
}

Questions? Let's chat

Open Discord