177 lines
7.0 KiB
Markdown
177 lines
7.0 KiB
Markdown
# 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
|