Installable to the home screen

Hi,

I try to make my web app “installable” to the home screen.
So i follow this: https://developers.google.com/web/fundamentals/web-app-manifest/?utm_source=devtools

I create the manifest.json and i add <link rel="manifest" href="/manifest.json"> to the first page.

But in the inspector of the web browser, there is the message “No manifest detected”.
And localhost:3000/manifest.json is accessible, and show me the correct json file.

How to make it working?

Thanks in advance.

1 Like

I put the <link rel="manifest" href="/manifest.json"> in index.html and in the inspector i can see the manifest.

But now how to show the add to home screen banner?
There is some code here https://developers.google.com/web/fundamentals/app-install-banners/#pwa-criteria
How to adapt the code to meteor?

Shouldn’t have to do anything special to adapt to Meteor.
Is there a particular part that doesn’t work for you?

You need to have a service worker set up and running.

The meta tag looks right. Where did you created the manifest? Must be at the root of the /public folder.

The part that doesnt work is to show the add to home screen banner.

let deferredPrompt;

    window.addEventListener('beforeinstallprompt', (e) => {
    e.preventDefault();
    deferredPrompt = e;
    // Update UI notify the user they can add to home screen
    btnAdd.style.display = 'block';
});

btnAdd.addEventListener('click', (e) => {
    // hide our user interface that shows our A2HS button
    btnAdd.style.display = 'none';
    // Show the prompt
    deferredPrompt.prompt();
    // Wait for the user to respond to the prompt
    deferredPrompt.userChoice
        .then((choiceResult) => {
            if (choiceResult.outcome === 'accepted') {
                console.log('User accepted the A2HS prompt');
            } else {
                console.log('User dismissed the A2HS prompt');
            }
            deferredPrompt = null;
        });
});

There is a lot of error in my editor and when i run meteor:
Uncaught ReferenceError: btnAdd is not defined

I do not have a service worker, is it possible to add this in a meteor web app?
The manisfet.json is in the public folder and it load now.

Possible, but not as easy as everything else in Meteor.

You need a service worker indeed. And it’s as easy as adding it to any other app.
You don’t need a full blown service worker with caching mechanisms. So start with only what you need. Place it into the root of the /public folder and load it from there.

For my app I currently only need push notifications, so I just implemented handling for those. Additionally I just implemented an empty fetch handler. That meets already the minimum requirement to be able to show the banner.

1 Like

@vuhrmeister Do you have an example of code for the service worker, the minimum requirement to be able to show the banner please?

I think the only requirement is to have a fetch handler. In my case I just added an empty handler:

self.addEventListener('fetch', function (event) {})

Thanks,
I tried, with a sw.js file in /public folder.
But it doesn’t load.

So, i follow these tutorial: https://developers.google.com/web/fundamentals/codelabs/offline/

I added in index.html

<script>
if('serviceWorker' in navigator) {
  navigator.serviceWorker
           .register('/sw.js')
           .then(function() { console.log("Service Worker Registered"); });
}
</script>

And added

importScripts('/cache-polyfill.js');

self.addEventListener('install', function(e) {
    e.waitUntil(
        caches.open('airhorner').then(function(cache) {
            return cache.addAll([
                '/',
                '/index.html',
                '/styles/main.css',
                '/scripts/main.min.js',
            ]);
        })
    );
});

But it doesn’t load the service loader.

I finally resolve with this: https://github.com/NitroBAY/meteor-service-worker
I’m using the sw.js
And this code:

Meteor.startup(() => { navigator.serviceWorker.register('/sw.js') .then() .catch(error => console.log('ServiceWorker registration failed: ', err)); });

It work, but there is a little error message:

client/main.ts (13, 146): Cannot find name 'err'.

In Chrome, i can install the web app.
But the adress bar doesnt hide, here the manifest.json

{
  "short_name": "Test",
  "name": "Test",
  "icons": [
    {
      "src": "/icons/96.png", // this links to /public/icons/96.png
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "/icons/144.png", // this links to /public/icons/144.png
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "/icons/192.png", // this links to /public/icons/192.png
      "sizes": "192x192",
      "type": "image/png"
    }
  ],
  "start_url": "/",
  // to remove the address bar at the top
  "display": "standalone",
  // to fix the orientation. `portrait`, `landscape` or `null`
  "orientation": "portrait",
  "background_color": "#3367D6",
  "scope": "/",
  "theme_color": "#3367D6"
}

Display in fullscreen or standalone never hide the adress bar.

How to remove the adresse bar?

did you finally solve this?

can share how did you code your push notification?

Sure. That’s basically the content of my service worker. Might be improvable, but I didn’t change anything for a long time.

self.addEventListener('fetch', function (event) {})

const defaultNotificationOptions = {
  renotify: true,
  vibrate: [200, 100, 200],
  icon: '/static/img/app-icons/android-chrome-192x192.png',
  badge: '/static/img/push-badge.png',
}

const titles = {
  newMessage: 'You have a new message',
  requestClosed: 'request closed',
}

const tags = {
  newMessage: ({ channelId }) => channelId,
  default: () => null,
}

const paths = {
  newMessage: ({ channelId }) => `/chat/${channelId}`,
  default: () => `/`,
}

const getTitle = data => titles[data.type] || ''
const getTag = data => (tags[data.type] || tags.default)(data)
const getPath = data => (paths[data.type] || paths.default)(data)

self.addEventListener('install', function (event) {
  event.waitUntil(self.skipWaiting())
})

self.addEventListener('activate', function (event) {
  event.waitUntil(self.clients.claim())
})

self.addEventListener('push', function (event) {
  const { body, ...data } = event.data.json()
  const title = getTitle(data)
  const options = {
    ...defaultNotificationOptions,
    body,
    data,
    tag: getTag(data),
  }

  event.waitUntil(
    showNotificationIfNotFocused(title, options)
  )
})

self.addEventListener('notificationclick', function (event) {
  const { notification } = event
  const path = getPath(notification.data)

  event.waitUntil(
    openApp(path)
  )
  notification.close()
})

self.addEventListener('pushsubscriptionchange', async function (event) {
  const clientList = await getClientList()
  if (clientList.length > 0) {
    const client = clientList[0]
    client.postMessage({ action: 'resubscribe' })
  }
})

async function showNotificationIfNotFocused (title, options) {
  return hasFocusedClient().then(hasFocused => {
    if (hasFocused) {
      return
    }
    return self.registration.showNotification(title, options)
  })
}

function hasFocusedClient () {
  return getClientList().then(clientList => {
    return clientList.some(client => client.focused)
  })
}

async function openApp (path) {
  const clientList = await getClientList()
  if (clientList.length > 0) {
    const client = clientList[0]
    client.postMessage({ action: 'goto', path })
    client.focus()
  } else {
    self.clients.openWindow(path)
  }
}

function getClientList () {
  return self.clients.matchAll({
    type: 'window',
  })
}