WIP PWA for Grain
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add Install App option to settings

- Add PWA service to capture beforeinstallprompt event
- Show Install App in settings when browser supports installation
- Add download icon

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+78 -1
+2 -1
src/components/atoms/grain-icon.js
··· 14 14 logout: 'fa-solid fa-right-from-bracket', 15 15 plus: 'fa-solid fa-plus', 16 16 ellipsis: 'fa-solid fa-ellipsis', 17 - ellipsisVertical: 'fa-solid fa-ellipsis-vertical' 17 + ellipsisVertical: 'fa-solid fa-ellipsis-vertical', 18 + download: 'fa-solid fa-download' 18 19 }; 19 20 20 21 export class GrainIcon extends LitElement {
+29
src/components/pages/grain-settings.js
··· 1 1 import { LitElement, html, css } from 'lit'; 2 2 import { router } from '../../router.js'; 3 3 import { auth } from '../../services/auth.js'; 4 + import { pwa } from '../../services/pwa.js'; 4 5 import '../atoms/grain-icon.js'; 5 6 6 7 export class GrainSettings extends LitElement { 8 + static properties = { 9 + _canInstall: { state: true } 10 + }; 7 11 static styles = css` 8 12 :host { 9 13 display: block; ··· 68 72 } 69 73 `; 70 74 75 + #unsubscribe = null; 76 + 77 + connectedCallback() { 78 + super.connectedCallback(); 79 + this._canInstall = pwa.canInstall; 80 + this.#unsubscribe = pwa.subscribe((canInstall) => { 81 + this._canInstall = canInstall; 82 + }); 83 + } 84 + 85 + disconnectedCallback() { 86 + super.disconnectedCallback(); 87 + this.#unsubscribe?.(); 88 + } 89 + 71 90 #goBack() { 72 91 history.back(); 92 + } 93 + 94 + #installApp() { 95 + pwa.install(); 73 96 } 74 97 75 98 #signOut() { ··· 86 109 <h1>Settings</h1> 87 110 </div> 88 111 <div class="settings-list"> 112 + ${this._canInstall ? html` 113 + <button class="settings-row" @click=${this.#installApp}> 114 + <grain-icon name="download" size="18"></grain-icon> 115 + Install App 116 + </button> 117 + ` : ''} 89 118 <button class="settings-row" @click=${this.#signOut}> 90 119 <grain-icon name="logout" size="18"></grain-icon> 91 120 Sign Out
+47
src/services/pwa.js
··· 1 + // PWA install prompt service 2 + class PWAService { 3 + #deferredPrompt = null; 4 + #listeners = new Set(); 5 + 6 + constructor() { 7 + window.addEventListener('beforeinstallprompt', (e) => { 8 + e.preventDefault(); 9 + this.#deferredPrompt = e; 10 + this.#notify(); 11 + }); 12 + 13 + window.addEventListener('appinstalled', () => { 14 + this.#deferredPrompt = null; 15 + this.#notify(); 16 + }); 17 + } 18 + 19 + get canInstall() { 20 + return this.#deferredPrompt !== null; 21 + } 22 + 23 + async install() { 24 + if (!this.#deferredPrompt) return false; 25 + 26 + this.#deferredPrompt.prompt(); 27 + const { outcome } = await this.#deferredPrompt.userChoice; 28 + 29 + if (outcome === 'accepted') { 30 + this.#deferredPrompt = null; 31 + this.#notify(); 32 + } 33 + 34 + return outcome === 'accepted'; 35 + } 36 + 37 + subscribe(callback) { 38 + this.#listeners.add(callback); 39 + return () => this.#listeners.delete(callback); 40 + } 41 + 42 + #notify() { 43 + this.#listeners.forEach(cb => cb(this.canInstall)); 44 + } 45 + } 46 + 47 + export const pwa = new PWAService();