Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

All notable changes to FloRo Sign Controller are documented here. Version numbers match `APP_VERSION` in `js/ui.js`. Bump `CACHE_NAME` in `sw.js` when static assets change so installed PWAs pick up updates.

## [2.2.2] - 2026-09-05

### Fixed
- Match Health Hub's native installation contract: Android and desktop show **Install** only while Chromium supplies `beforeinstallprompt`, and clicking it calls `prompt()` directly.
- Remove the Android menu-help route that could lead users to **Create shortcut**, which is a Chrome bookmark and not a standalone PWA.
- Hide Settings > Install App when no native Chromium prompt exists; iOS guidance remains available.
- Remove the separate install-help control, self-referential `related_applications`, and installed-app detection for strict Health Hub parity.
- Restore the canonical production manifest ID used by Health Hub's identity pattern.

## [2.2.1] - 2026-09-05

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,11 @@ See [`specs/pwa.md`](specs/pwa.md). Short version: **Install** calls the browser

| Platform | Banner | Install button |
|----------|--------|----------------|
| **Android (Chrome/Edge)** | Native prompt when available; otherwise immediate honest guidance | Native **Install**, or clearly labeled **Install help** |
| **Android (Chrome/Edge)** | Native prompt when available; otherwise immediate status guidance | Native **Install** only—never a browser shortcut |
| **iOS (Safari)** | On first visit (Share → Add to Home Screen copy) | Hidden (Apple has no auto-install) |
| **Desktop** | Hidden until `beforeinstallprompt` | Native **Install** when Chrome/Edge offers it |

Dismiss **Not now** stores `floro_install_dismissed_v6` for **7 days**. Settings > **Install App** still works (native prompt, or a help sheet). On Android, an installed app appears in the app drawer as **FloRo Sign**; the launcher may not automatically pin it to the Home screen.
Dismiss **Not now** stores `floro_install_dismissed_v7` for **7 days**. On Android, **Install** appears only when Chrome offers the native standalone-app prompt; FloRo never routes users through **Create shortcut**. An installed app appears in the app drawer as **FloRo Sign**; the launcher may not automatically pin it to the Home screen.

## Assets

Expand Down
3 changes: 1 addition & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,6 @@
<p id="install-banner-subtitle" class="install-banner-subtitle">Install FloRo so it opens full screen, not as a browser tab.</p>
<div class="install-banner-actions">
<button id="btn-install" class="btn btn-primary install-banner-action install-banner-action-primary hidden" type="button">Install</button>
<button id="btn-install-help" class="btn btn-primary install-banner-action install-banner-action-primary hidden" type="button">Install help</button>
<button id="btn-dismiss-install" class="btn btn-secondary install-banner-action install-banner-action-dismiss" type="button">Not now</button>
</div>
</div>
Expand Down Expand Up @@ -298,7 +297,7 @@ <h3 class="settings-section-title">Activity Log</h3>

<section class="settings-section settings-about">
<p class="about-line">FloRo Sign Controller</p>
<p class="about-version" id="app-version">v2.2.1</p>
<p class="about-version" id="app-version">v2.2.2</p>
</section>

<button id="btn-close-settings" class="btn btn-secondary btn-full" type="button">Done</button>
Expand Down
1 change: 0 additions & 1 deletion js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1190,7 +1190,6 @@ setupPwaInstall({
bannerTitle: document.getElementById('install-banner-title'),
bannerSubtitle: document.getElementById('install-banner-subtitle'),
btnInstall: document.getElementById('btn-install'),
btnInstallHelp: document.getElementById('btn-install-help'),
btnDismiss: document.getElementById('btn-dismiss-install'),
settingsBtn: document.getElementById('btn-settings-install'),
modal: document.getElementById('install-modal'),
Expand Down
93 changes: 25 additions & 68 deletions js/pwa-install.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** Native PWA install with honest manual and already-installed fallbacks. */

export const DISMISS_KEY = 'floro_install_dismissed_v6';
export const DISMISS_KEY = 'floro_install_dismissed_v7';
const DISMISS_TTL_MS = 7 * 24 * 60 * 60 * 1000;

export function isStandalone() {
Expand Down Expand Up @@ -43,27 +43,12 @@ function shouldSuppressBanner() {
return isStandalone() || isDismissed();
}

async function detectInstalledPwa() {
if (isStandalone()) return true;
if (!('getInstalledRelatedApps' in navigator)) return false;

try {
const apps = await navigator.getInstalledRelatedApps();
return apps.some((app) => app.platform === 'webapp');
} catch {
return false;
}
}

function installHelpText(hasNativePrompt, installedPwa) {
if (installedPwa) {
return 'FloRo Sign is installed. Find it in your Android app drawer; Chrome may not add a Home screen icon.';
}
function installHelpText(hasNativePrompt) {
if (isIOS() && !hasNativePrompt) {
return 'Open this page in Safari, tap Share, then Add to Home Screen.';
}
if (isAndroid() && !hasNativePrompt) {
return 'Chrome has not offered one-tap install yet. You can still install from the Chrome menu.';
return 'Waiting for Chrome’s app installer. Do not create a shortcut—it opens as a browser tab.';
}
return 'Install FloRo so it opens full screen, not as a browser tab.';
}
Expand All @@ -73,7 +58,6 @@ export function setupPwaInstall({
bannerTitle,
bannerSubtitle,
btnInstall,
btnInstallHelp,
btnDismiss,
settingsBtn,
modal,
Expand All @@ -85,7 +69,6 @@ export function setupPwaInstall({
log,
}) {
let deferredPrompt = window.__floroDeferredInstall || null;
let installedPwa = isStandalone();

function showBanner() {
if (!bannerEl || shouldSuppressBanner()) return;
Expand All @@ -102,23 +85,25 @@ export function setupPwaInstall({
function syncBanner() {
const hasNativePrompt = Boolean(deferredPrompt);
if (bannerTitle) {
bannerTitle.textContent = installedPwa ? 'FloRo Sign is installed' : 'Install FloRo Sign';
bannerTitle.textContent = 'Install FloRo Sign';
}
if (bannerSubtitle) {
bannerSubtitle.textContent = installHelpText(hasNativePrompt, installedPwa);
bannerSubtitle.textContent = installHelpText(hasNativePrompt);
}
if (btnInstall) {
btnInstall.textContent = 'Install';
btnInstall.classList.toggle('hidden', !hasNativePrompt || installedPwa);
}
if (btnInstallHelp) {
btnInstallHelp.textContent = installedPwa ? 'Find app' : 'Install help';
btnInstallHelp.classList.toggle(
'hidden',
hasNativePrompt || (!installedPwa && !isAndroid() && !isIOS())
);
btnInstall.classList.toggle('hidden', !hasNativePrompt);
}
btnDismiss?.classList.remove('hidden');
syncSettingsInstall();
}

function syncSettingsInstall() {
const section = settingsBtn?.closest('.settings-section');
if (!section) return;
const canInstallOrHelp =
!isStandalone() && (Boolean(deferredPrompt) || isIOS());
section.classList.toggle('hidden', !canInstallOrHelp);
}

function renderModalSteps(items) {
Expand All @@ -137,18 +122,6 @@ export function setupPwaInstall({
modalSteps?.classList.remove('hidden');
modalAction?.classList.add('hidden');

if (installedPwa) {
modalTitle.textContent = 'Find FloRo Sign';
modalDesc.textContent =
'Android installs the app in your app drawer, but your launcher may not add it to the Home screen.';
renderModalSteps([
'Swipe up from your Home screen to open the app drawer',
'Search for FloRo Sign (older installs may appear as FloRo)',
'Long-press the app icon and drag it to your Home screen',
]);
return;
}

if (isIOS()) {
modalTitle.textContent = 'Add to Home Screen';
modalDesc.textContent = 'iPhone and iPad cannot auto-install. In Safari:';
Expand All @@ -160,14 +133,14 @@ export function setupPwaInstall({
return;
}

modalTitle.textContent = 'Install FloRo Sign';
modalTitle.textContent = 'App install not ready';
modalDesc.textContent =
'Chrome can delay or temporarily suppress its one-tap prompt. Manual installation is always available:';
'Chrome has not made native app installation available yet. A shortcut is only a bookmark and will keep the browser bar.';
renderModalSteps([
'Open this page directly in Chrome or Edge, not an in-app browser',
'Tap the three-dot Chrome menu',
'Tap Add to Home screen (or Install and create shortcut), then choose Install app',
'After installation, swipe up and search your app drawer for FloRo Sign',
'Keep the page open briefly and interact with it so Chrome can finish checking the app',
'Reload the page and use FloRo’s Install button when it appears',
'Do not choose Create shortcut from Chrome’s menu',
]);
}

Expand All @@ -177,13 +150,10 @@ export function setupPwaInstall({

async function triggerNativeInstall() {
if (!deferredPrompt) return false;
let accepted = false;
try {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
accepted = true;
installedPwa = true;
log?.('User installed FloRo Sign.', 'success');
}
} catch (err) {
Expand All @@ -193,15 +163,14 @@ export function setupPwaInstall({
deferredPrompt = null;
window.__floroDeferredInstall = null;
closeHelpSheet();
if (accepted) showBanner();
else hideBanner();
hideBanner();
syncSettingsInstall();
return true;
}

function capturePrompt(event) {
event.preventDefault();
deferredPrompt = event;
installedPwa = false;
window.__floroDeferredInstall = event;
showBanner();
}
Expand All @@ -217,21 +186,20 @@ export function setupPwaInstall({
} else {
hideBanner();
}
syncSettingsInstall();

window.addEventListener('appinstalled', () => {
installedPwa = true;
log?.('FloRo Sign installed.', 'success');
deferredPrompt = null;
window.__floroDeferredInstall = null;
showBanner();
hideBanner();
syncSettingsInstall();
});

btnInstall?.addEventListener('click', async () => {
await triggerNativeInstall();
});

btnInstallHelp?.addEventListener('click', openHelpSheet);

btnDismiss?.addEventListener('click', () => {
try {
localStorage.setItem(DISMISS_KEY, String(Date.now()));
Expand All @@ -256,15 +224,4 @@ export function setupPwaInstall({
if (e.target === modal) closeHelpSheet();
});

if (settingsBtn && isStandalone()) {
settingsBtn.closest('.settings-section')?.classList.add('hidden');
}

detectInstalledPwa().then((installed) => {
if (!installed || isStandalone()) return;
installedPwa = true;
deferredPrompt = null;
window.__floroDeferredInstall = null;
showBanner();
});
}
2 changes: 1 addition & 1 deletion js/ui.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { formatModeLabel, getModeName, MODE_COUNT } from './mode-names.js';

export const APP_VERSION = '2.2.1';
export const APP_VERSION = '2.2.2';

export function createLogger(consoleBody) {
return function log(message, type = 'info') {
Expand Down
8 changes: 1 addition & 7 deletions manifest.webmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,13 @@
"description": "Control your FloRo neon sign over Bluetooth",
"start_url": "./",
"scope": "./",
"id": "./",
"id": "https://rawshn.com/sign-controller/",
"display": "standalone",
"display_override": ["standalone"],
"orientation": "portrait",
"background_color": "#020D0A",
"theme_color": "#020D0A",
"prefer_related_applications": false,
"related_applications": [
{
"platform": "webapp",
"url": "./manifest.webmanifest"
}
],
"launch_handler": {
"client_mode": ["navigate-existing", "auto"]
},
Expand Down
26 changes: 17 additions & 9 deletions scripts/check-pwa.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ if (!html.includes('beforeinstallprompt')) {
if (!html.includes('id="btn-install"') || !html.includes('install-banner-action-primary hidden')) {
fail('Install button must start hidden until a native prompt exists');
}
if (!html.includes('id="btn-install-help"')) {
fail('Android needs an honest manual-install fallback when Chrome withholds prompt()');
if (html.includes('id="btn-install-help"')) {
fail('do not expose a separate install-help action; match Health Hub native prompt flow');
}
if (!html.includes("updateViaCache: 'none'")) {
fail('service worker registration must bypass stale HTTP caches');
Expand All @@ -68,8 +68,8 @@ if (manifest) {
if (manifest.start_url !== './' || manifest.scope !== './') {
fail('manifest start_url and scope must be "./" (portable across localhost and /sign-controller/)');
}
if (manifest.id !== './') {
fail('manifest id must be "./" so it resolves consistently on production and localhost');
if (manifest.id !== 'https://rawshn.com/sign-controller/') {
fail('manifest id must be the canonical production identity');
}
if (manifest.display !== 'standalone') {
fail('manifest display must be standalone');
Expand All @@ -89,10 +89,8 @@ if (manifest) {
if (!icons.some((i) => i.sizes === '192x192') || !icons.some((i) => i.sizes === '512x512')) {
fail('manifest needs 192x192 and 512x512 icons');
}
if (!manifest.related_applications?.some(
(app) => app.platform === 'webapp' && app.url === './manifest.webmanifest'
)) {
fail('manifest must self-reference so Android can detect an existing installation');
if (manifest.related_applications) {
fail('do not self-reference related applications; it is not part of Health Hub installation');
}
}

Expand Down Expand Up @@ -135,9 +133,19 @@ if (!nativeInstallHandler.includes('triggerNativeInstall') ||
nativeInstallHandler.includes('openHelpSheet')) {
fail('Install button click must call prompt() only; do not open the help sheet');
}
if (!pwaSrc.includes("btnInstall.classList.toggle('hidden', !hasNativePrompt || installedPwa)")) {
if (!pwaSrc.includes("btnInstall.classList.toggle('hidden', !hasNativePrompt)")) {
fail('Install button visibility must follow hasNativePrompt');
}
if (!pwaSrc.includes('syncSettingsInstall')) {
fail('Settings install action must be hidden until native install or valid platform help exists');
}
if (pwaSrc.includes('getInstalledRelatedApps')) {
fail('do not let installed-app detection clear a valid deferred native prompt');
}
if (pwaSrc.includes('Install and create shortcut') ||
pwaSrc.includes('Tap the three-dot Chrome menu')) {
fail('Android install guidance must never route users into Chrome shortcut creation');
}

if (!process.exitCode) {
console.log('check-pwa: ok');
Expand Down
16 changes: 7 additions & 9 deletions specs/pwa.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,12 @@ No `<base>` tag. Relative URLs resolve against the page URL (`/` locally, `/sign
|-------|--------|
| `name` / `short_name` | FloRo Sign Controller / FloRo Sign |
| `start_url` / `scope` | `./` |
| `id` | `./` (resolves to the canonical scope in production and localhost in development) |
| `id` | `https://rawshn.com/sign-controller/` (canonical production identity) |
| `display` | `standalone` |
| `display_override` | `["standalone"]` |
| `launch_handler.client_mode` | `["navigate-existing", "auto"]` (array, not a string) |
| `icons` | `./icons/icon-192.png` (`any`), `./icons/icon-512.png` (`any` + `maskable`) |
| `theme_color` / `background_color` | `#020D0A` |
| `related_applications` | Self-reference used to detect an existing Android installation |

`prefer_related_applications` stays `false` so Chrome does not skip the install UI looking for a Play Store app.

## Service worker
Expand All @@ -61,15 +59,14 @@ Module: `js/pwa-install.js`. Early capture in `index.html` (before other scripts
| Situation | Banner | Install button | Click |
|-----------|--------|----------------|-------|
| `beforeinstallprompt` fired | Show | Show | `event.prompt()` only |
| Android, no native event | Show immediately (honest copy) | **Install help** | Chrome menu instructions |
| Android, PWA already installed | Show find-app copy | **Find app** | App-drawer instructions |
| iOS, not standalone | Show (Share → A2HS copy) | **Install help** | Share → Add to Home Screen instructions |
| Desktop, no event | Hidden | Hidden | Settings help sheet only |
| Android, no native event | Show immediately (honest copy) | Hidden | Wait for native eligibility; never create a shortcut |
| iOS, not standalone | Show (Share → A2HS copy) | Hidden | Settings retains Share → Add to Home Screen help |
| Desktop, no event | Hidden | Hidden | Settings install row hidden |
| Already standalone | Hidden | Hidden | Settings install row hidden |

Dismiss **Not now** writes `localStorage` `floro_install_dismissed_v6` with a 7-day TTL. Settings → **Install App** ignores dismiss: native `prompt()` if held, otherwise the help sheet. A self-reference in `related_applications` lets supporting Chrome versions distinguish “prompt unavailable” from “already installed.” Android launchers may install into the app drawer without pinning a Home screen icon, so the installed help names both **FloRo Sign** and the old **FloRo** short name.
Dismiss **Not now** writes `localStorage` `floro_install_dismissed_v7` with a 7-day TTL. On Android and desktop, Settings → **Install App** is visible only while a native prompt is held. It never directs users into Chrome’s **Create shortcut** flow, which creates a browser bookmark instead of a standalone app. As in Health Hub, FloRo does not use `related_applications` or installed-app detection as part of installation.

`preventDefault` on `beforeinstallprompt` is required to call `prompt()` later. The in-page **Install** button must call that event directly. If Chrome does not emit the event (for example, after a recent dismissal), the separately labeled **Install help** action may explain the manual menu route; it must never pretend to invoke native installation.
`preventDefault` on `beforeinstallprompt` is required to call `prompt()` later. The in-page **Install** button must call that event directly. If Chrome does not emit the event, FloRo must not offer an Android install action. **Create shortcut** is not an installation and must never be presented as a fallback.

## Verification

Expand All @@ -83,5 +80,6 @@ Manual: Chrome or Edge on Android, https://rawshn.com/sign-controller/, tap **In

| Date | Change |
|------|--------|
| 2026-09-05 | Removed Android shortcut flow; only native `beforeinstallprompt` can expose Install |
| 2026-09-05 | Register SW immediately, detect existing installs, add honest manual fallback, and make cached launches fast |
| 2026-09-05 | Reimplemented to match Health Hub: native prompt only, static manifest, relative scope |
2 changes: 1 addition & 1 deletion sw.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const CACHE_NAME = 'floro-controller-v62';
const CACHE_NAME = 'floro-controller-v64';
const ASSETS = [
'./',
'./index.html',
Expand Down
Loading