Files
hedena_gold/docs/superpowers/specs/2026-10-15-pwa-install-prompt-design.md
T

177 lines
7.0 KiB
Markdown
Raw Normal View History

# PWA install prompt + platform guides
Date: 2026-10-15
App: `apps/user-app`
## Goal
Show a brand-focused centered install modal automatically after first useful visit, with separate Android-web and iOS install guides. Do not nag: if the user dismisses, wait 7 days before offering again (only while not installed). Once installed, never show again.
## Decisions (locked)
| Topic | Choice |
|---|---|
| Entry | Auto prompt after short delay (not a permanent settings entry in v1) |
| UI | Centered modal, brand-first, one primary CTA |
| Re-offer | 7 days after dismiss, only if not installed |
| Installed | Never show again |
| iOS / no native prompt | Same modal shell → visual step guide for that platform |
| Guides | Separate copy/steps for Android web vs iOS |
| Style | Follow existing `hg-dialog` / `DialogVant` island pattern |
## Out of scope
- Admin panel PWA install UX
- Push-notification permission prompts
- Changing `vite-plugin-pwa` manifest / SW strategy (already present)
- Persistent “Install app” row in profile/menu (can add later)
- Desktop-specific illustrated guide (desktop Chrome may still get native prompt or Android-style fallback text)
## Architecture
Three pieces in `user-app`:
1. **`usePwaInstall`** (composable)
- Listen for `beforeinstallprompt`, stash the event, expose `promptInstall()`
- Detect installed / standalone (`display-mode: standalone`, `navigator.standalone` on iOS)
- Detect platform for guide content: `ios` | `android` (default non-iOS mobile/desktop to android-style guide)
- Read/write persistence; expose `shouldShowPrompt`, `open()`, `dismiss()`, `markInstalled()`
- After ~4s on eligible routes, set `visible` if `shouldShowPrompt`
2. **`InstallPromptModal.vue`**
- Centered `van-dialog` with `hg-dialog` styling (or thin wrapper consistent with `DialogVant`)
- Modes: `offer` | `guide`
- Offer: brand logo, title, one line, primary CTA, subtle close
- Guide: platform-specific steps; secondary “متوجه شدم” dismisses
3. **Mount in `App.vue`**
- Sibling of `AppUpdateGate`
- Skip while route uses `FullPage` layout (login/register and similar)
Existing SW registration via `useRegisterSW` stays as-is; this feature only adds install UX.
## Show / hide rules
Show only when **all** are true:
1. Not already installed (standalone / `navigator.standalone` / persisted `installed`)
2. `dismissedAt` is missing, or older than **7 days**
3. Current route is not `meta.layout === 'FullPage'`
4. Initial delay of **~4 seconds** after app shell ready has elapsed
5. Document is visible (`document.visibilityState === 'visible'`) — optional soft guard
On dismiss (×, overlay, or “متوجه شدم”):
- Set `pwaInstall.dismissedAt = Date.now()`
- Close modal; reset mode to `offer` for next open
On successful native install (`appinstalled` event or user accepted `prompt()`):
- Set `pwaInstall.installed = true`
- Clear need to re-offer; never show again
On later visit already in standalone:
- Treat as installed (set `installed` if not set); never show
## Persistence
`localStorage` keys (single JSON object preferred, matching cart/appearance patterns):
```json
{
"dismissedAt": null,
"installed": false
}
```
Key name: `pwaInstall` (or `hg.pwaInstall` if namespacing is preferred in implementation).
Constants:
- `REOFFER_MS = 7 * 24 * 60 * 60 * 1000`
- `INITIAL_DELAY_MS = 4000`
## Modal content
### Offer mode (both platforms)
| Element | Copy / asset |
|---|---|
| Media | `brand-logo` (existing public/assets path) |
| Title | نصب اپلیکیشن هدنا گلد |
| Body | دسترسی سریع‌تر و تجربهٔ شبیه اپ واقعی |
| Primary CTA | If deferred prompt ready: نصب اپ — else: مشاهده راهنما |
| Close | Small ×; no loud secondary cancel |
CTA behavior:
- Deferred prompt available → `event.prompt()`; on accept → `markInstalled()`; on dismiss of browser UI only → keep our `dismissedAt` unset unless user also closed our modal (browser decline alone does not start the 7-day clock; closing our modal does)
- No deferred prompt → switch modal to `guide` for detected platform (do not dismiss yet)
### Guide — Android web
Title: راهنمای نصب
1. منوی مرورگر (⋮) را باز کنید
2. گزینهٔ «نصب برنامه» یا «Add to Home screen» را بزنید
3. نصب را تأیید کنید
Footer CTA: متوجه شدم → dismiss + 7-day clock
### Guide — iOS
Title: راهنمای نصب
1. دکمهٔ Share (مربع با فلش رو به بالا) را بزنید
2. «Add to Home Screen» / افزودن به صفحهٔ اصلی را انتخاب کنید
3. Add را تأیید کنید
Footer CTA: متوجه شدم → dismiss + 7-day clock
Guides use short icon+text rows (no long paragraphs). Illustrations may be simple inline SVG icons (share / menu / home); no new marketing photography required for v1.
## Platform detection (guide only)
- **iOS:** iPhone/iPad/iPod UA, or iPadOS desktop UA with touch — use iOS guide
- **Else:** Android-web guide (covers Android Chrome without prompt, and as fallback elsewhere)
Native `beforeinstallprompt` path is independent of guide platform: if the event exists, offer CTA installs directly regardless of guide branch.
## Error / edge cases
| Case | Behavior |
|---|---|
| `prompt()` rejects / user cancels browser sheet | Stay closed or return to offer only if still open; do **not** set `installed`; if our modal was closed by user earlier, 7-day rule already applies |
| Already installed mid-session (`appinstalled`) | Close modal; set `installed` |
| Private mode / `localStorage` throws | Fail soft: skip auto-show (no crash); optional in-memory session-only suppress |
| User opens app from home screen | Never show |
| Update gate (`AppUpdateGate`) visible | Do not stack: keep install modal closed while the update gate is open; schedule or re-check after it closes |
| HTTPS / installability not met | No deferred prompt → auto path still can show offer → guide |
## Testing (manual)
1. Fresh profile, Android Chrome (installable): after ~4s modal → نصب اپ → browser prompt → accept → never again
2. Same, dismiss modal → gone; advance clock / spoof `dismissedAt` −8 days → shows again
3. iOS Safari: after ~4s → مشاهده راهنما → iOS steps → متوجه شدم → 7-day silence
4. Standalone launch: no modal
5. Login (`FullPage`): no modal until leaving that layout and delay elapses on an eligible view
## File sketch
```
apps/user-app/src/composables/usePwaInstall.ts
apps/user-app/src/components/pwa/InstallPromptModal.vue
apps/user-app/src/App.vue (mount modal)
```
Optional tiny helpers for UA / display-mode can live next to the composable.
## Success criteria
- Brand-first centered modal, one primary CTA
- Android and iOS guides are distinct
- Auto once per window of eligibility; 7-day re-offer only if not installed
- Installed / standalone users never see it
- No change to existing SW/update-gate behavior beyond coexistence