Files
lifeman/pwa-notes.md
2025-12-05 11:19:17 +00:00

9.7 KiB
Raw Blame History


Part 1 Turn your current app into a PWA

You already have:

  • index.html
  • style.css
  • script.js

Well add:

  • manifest.webmanifest
  • sw.js
  • A couple of small changes to index.html

Ill assume your app is served from the site root (e.g. https://mydomain.com/) and that your icon is a PNG. You can keep everything inside web/ in your repo; when you deploy, make sure these files end up in the same directory as index.html.


1. Create icons from your existing icon file

Chrome expects at least:

  • 192×192 PNG
  • 512×512 PNG

If your icon file is large enough (e.g. 1024×1024), generate:

  • icons/icon-192.png
  • icons/icon-512.png

(Use something like GIMP, Figma, or an online “PWA icon generator”.)

Folder structure (in web/):

web/
  index.html
  style.css
  script.js
  sw.js
  manifest.webmanifest
  icons/
    icon-192.png
    icon-512.png

2. Add manifest.webmanifest

Create web/manifest.webmanifest:

{
  "name": "Life Manager",
  "short_name": "LifeMgr",
  "description": "Offline-first life organizer with calendar, tags, and backups.",
  "start_url": "/",
  "scope": "/",
  "display": "standalone",
  "background_color": "#0f1014",
  "theme_color": "#0f1014",
  "orientation": "portrait",
  "icons": [
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ]
}

start_url and scope assume the app is at the root. If you serve from /life-manager/, change both to "/life-manager/".


3. Wire manifest + theme color into index.html

Modify your <head> in web/index.html to include the manifest and theme color.

Right now you have:

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>life-manager</title>
    <link rel="stylesheet" href="style.css">
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
</head>

Change to:

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>life-manager</title>

    <!-- PWA: theme color (matches manifest) -->
    <meta name="theme-color" content="#0f1014">

    <!-- PWA: manifest -->
    <link rel="manifest" href="manifest.webmanifest">

    <link rel="stylesheet" href="style.css">
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
</head>

4. Register a service worker from index.html

Just before </body> you already have:

    <script src="script.js"></script>
</body>
</html>

Change to:

    <script src="script.js"></script>

    <!-- PWA: Service worker registration -->
    <script>
      if ('serviceWorker' in navigator) {
        window.addEventListener('load', () => {
          navigator.serviceWorker.register('/sw.js')
            .catch(err => {
              console.error('SW registration failed', err);
            });
        });
      }
    </script>
</body>
</html>

If you host the app under a path (e.g. /life-manager/), change the registration path to '/life-manager/sw.js'.


5. Add sw.js (service worker)

Create web/sw.js:

// sw.js - simple cache-first service worker

const CACHE_NAME = 'lifemgr-cache-v1';

// List everything needed for offline
const ASSETS = [
  '/',
  '/index.html',
  '/style.css',
  '/script.js',
  '/manifest.webmanifest',
  '/icons/icon-192.png',
  '/icons/icon-512.png'
];

// Install: cache core assets
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(ASSETS);
    })
  );
  self.skipWaiting();
});

// Activate: cleanup old caches if you bump the version
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(
        keys.map((key) => {
          if (key !== CACHE_NAME) {
            return caches.delete(key);
          }
        })
      )
    )
  );
  self.clients.claim();
});

// Fetch: cache-first strategy
self.addEventListener('fetch', (event) => {
  const request = event.request;

  // Only handle GET
  if (request.method !== 'GET') return;

  event.respondWith(
    caches.match(request).then((cached) => {
      // Serve from cache if available, else network
      return (
        cached ||
        fetch(request).catch(() => {
          // Optional: custom offline response for HTML requests
          if (request.headers.get('accept')?.includes('text/html')) {
            return caches.match('/index.html');
          }
        })
      );
    })
  );
});

Again, if hosted under a sub-path, prepend /life-manager in ASSETS.

Now when you deploy over HTTPS, Chrome will see:

  • a valid manifest,
  • a service worker controlling the scope,
  • and will treat it as a PWA with install capability.

6. Deploy and sanity-check

  1. Deploy web/ to somewhere HTTPS (Netlify, Vercel, etc.).

  2. Open it in Chrome (Android or desktop).

  3. In DevTools → Application → Manifest/Service Workers:

    • Confirm manifest is found,
    • service worker is active,
    • and pages are controlled.
  4. On Android Chrome, you should see “Install app” / “Add to Home screen” in the menu.

Once this works, youre ready for the Trusted Web Activity step.


Part 2 Wrap your PWA in a Trusted Web Activity (TWA) with Bubblewrap

Now you have a real PWA live at (for example):

https://your-domain.com/

Well use Bubblewrap, which takes your hosted manifest.webmanifest and generates an Android project that starts your PWA as a TWA.

1. Prereqs checklist

You need:

  • Node.js (v14+)

  • Java JDK

  • Android SDK

  • Your PWA served via HTTPS with:

    • manifest,
    • service worker,
    • and working offline (which we just did).

And of course, a Google Play developer account for publishing later.


2. Install Bubblewrap CLI

npm install -g @bubblewrap/cli
bubblewrap --version

(Just to confirm its installed.)


3. Initialize a Bubblewrap project from your manifest

Pick a new folder for the Android project:

mkdir life-manager-twa
cd life-manager-twa

Run the init command, pointing to your hosted manifest:

bubblewrap init --manifest https://your-domain.com/manifest.webmanifest

Bubblewrap will:

  • Download the web manifest.

  • Ask you about:

    • Package ID (e.g. com.yourdomain.lifemanager)
    • App name (can reuse "Life Manager")
    • Colors, icons, orientation (it can reuse values from the manifest)
    • Signing key (create a new keystore or use an existing one)

It will generate:

  • An Android project (Gradle, Java/Kotlin code that opens your URL as a TWA).
  • A twa-manifest.json with config about your app.
  • An assetlinks.json file template for your website.

4. Build the APK/App Bundle

Inside that new project folder:

bubblewrap build

This compiles the Android project and outputs a release build (APK/App Bundle).

You can also install directly on a connected device:

bubblewrap install

At this stage, when you open the app on the device:

  • It will usually open as a Custom Tab (with a little browser UI) until we set up the trust link (Digital Asset Links).

To get true Trusted Web Activity (full screen, no browser bar), Android needs to verify that:

  • The app signing key
  • And your websites domain

belong to the same owner, via Digital Asset Links.

Bubblewrap will give you an assetlinks.json snippet. If you need a template, it looks like this:

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.yourdomain.lifemanager",
      "sha256_cert_fingerprints": [
        "AA:BB:CC:DD:EE:FF:..."
      ]
    }
  }
]
  • Replace com.yourdomain.lifemanager with your actual package ID.
  • Replace the fingerprint with the SHA-256 of the keystore you actually sign the Play build with (Bubblewrap/Android Studio docs show how to get this).

Host this file at:

https://your-domain.com/.well-known/assetlinks.json

Once this is accessible and valid:

  • Opening your app on device will show your PWA full-screen as a Trusted Web Activity, not a normal Chrome tab.

You can open the generated Android project in Android Studio for:

  • Changing app name, icon, version code, etc.
  • Building signed App Bundles for the Play Store.
  • Running on emulators.

Bubblewraps build and install commands already use Gradle under the hood, so this is optional, but very handy for Play Store upload.


7. Upload to Google Play

Once:

  • PWA runs well over HTTPS.
  • Asset Links are configured and verified.
  • The app works as full-screen TWA on a device.

Then:

  1. In the Google Play Console, create a new app.
  2. Upload the signed App Bundle/APK from your Bubblewrap/Android Studio build.
  3. Fill in listing details (screenshots, description, etc.).
  4. Roll out a test track or production release.