Knowledge Base

Preserving for the future: Shell scripts, AoC, and more

Progressive web app with share target for Android

I have known for a while that "progressive web apps" (apparently only supported in one browser) can be "installed" as an "app" on a mobile device, and they can be the target for receiving shared items! I finally bothered to learn how to do this with my flask app stackbin!

There's a large number of steps required, and I ripped pretty much all of this (minus flask-specific troubleshooting) from reference 1. The main parts include:

  1. Add file manifest.json on the top level virtual path of your application. I had to add some jinja2 templating to handle the static prefix for my app.
{
  "name": "Stackbin Paste Service",
  "description": "A small pastebin service",
  "short_name": "Stackbin",
  "start_url": "{{ prefix }}/",
  "scope": "{{ prefix }}/",
  "display": "standalone",
  "theme_color": "#317EFB",
  "background_color": "#317EFB",
  "icons": [
    {
      "src": "/static/icons/stackbin-144.png",
      "sizes": "144x144",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/static/icons/stackbin-192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/static/icons/stackbin-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ],
  "share_target": {
    "action": "{{ prefix }}/",
    "method": "POST",
    "enctype": "multipart/form-data",
    "params": {
      "title": "title",
      "text": "text",
      "url": "url",
      "files": [
         { "name": "plaintext", "accept": ["text/plain", ".txt"] },
         { "name": "csv", "accept": ["text/csv",".csv"] }
      ]
    }
  }
}

Almost every single attribute here is required for an Android browser to recognize this page as an "installable app."

For sending with the correct mimetype, use this:

@app.route("/manifest.json")
def serve_manifest():
   return render_template("manifest.json",mimetype="application/manifest+json")
  1. Set up a "service worker," whatever that means. Apparently it means a file named sw.js.

    //sw.js self.addEventListener('install', function(event) { console.log('[Service Worker] Installing Service Worker ...', event); }); self.addEventListener('activate', function(event) { console.log('[Service Worker] Activating Service Worker ...', event); }); / self.addEventListener('fetch', function(event) { console.log('[Service Worker] Fetching something ...', event); /

I had to comment out the fetch listener because I didn't want to implement a page inside the "installed app" to sit between the web app and the upload process. I think this is where I will need to adjust code in the future to let the user choose is_private and title for the content before sending the http POST request.

This one was just static, so in flask:

@app.route("/sw.js")
def serve_sw():
   return send_file("static/sw.js", mimetype="application/javascript")

And then I had to add to my layout.html which is included in [most?] pages of my app, including the front page ("/").

<head>
<meta name="theme-color" content="#317efb"/>
<title>{% block title %}{% endblock %}{% if appname %} | {{ appname }}{% endif %}</title>
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="format-detection" content="telephone=no">
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
{#<link rel="stylesheet" media="screen and (max-width: 800px)" href="{{ url_for('static', filename='small.css') }}" />#}
<link rel="icon" href="{{url_for('static',filename='icons/stackbin-144.png')}}" type="image/png">
<link rel="icon" href="{{url_for('static',filename='icons/stackbin-144.png')}}" type="image/png">
<link rel="icon" href="{{url_for('static',filename='icons/stackbin-144.png')}}" type="image/png">
<link rel="apple-touch-icon" href="{{url_for('static',filename='icons/stackbin-144.png')}}" type="image/png">
<link rel="apple-touch-icon" href="{{url_for('static',filename='icons/stackbin-144.png')}}" type="image/png">
<link rel="apple-touch-icon" href="{{url_for('static',filename='icons/stackbin-144.png')}}" type="image/png">
<link rel="manifest" href="{{prefix}}/manifest.json">
<script>
   if ('serviceWorker' in navigator) {
      window.addEventListener('load', function() {
         navigator.serviceWorker.register("{{prefix}}/sw.js").then(function(registration) {
            console.log('ServiceWorker registration successful with scope: ', registration.scope);
         }, function(err) {
            console.log('ServiceWorker registration failed: ', err);
         });
      });
   }
</script>
</head>

Upon getting all this loaded, and reloading the flask uwsgi server, I was then able to visit the app and choose to install it from the dot-dot-dot menu, aka "Add to home screen."

Screenshot of android browser showing "Add to Home Screen" for this web app

If you only see the option to install as shortcut, you don't have all the correct parts set up. I found the icons a little tricky. Some of my environments, including my production environment, would not serve static content from a subdirectory of static/. It did not make any sense. I had to flatten it and put the icon files directly in static/.

Upon installing the app, it is now a sharing target!

Screenshot of android screen showing "Share note to..."

You'll notice my manifest sharing target includes csv! That is what I had to add to make sharing work from the F-droid app store when exporting the list of installed apps.

Screenshot of the list of installed apps from f-droid in stackbin

References

Weblinks

  1. How to create a progressive web app (PWA) using Flask | by Tris Tan | Medium
  2. javascript - "share_target" does not work in my PWA app - Stack Overflow

Comments