Add web app install button and step-by-step PWA guide.

This commit is contained in:
2026-07-30 10:20:04 +02:00
parent e6dea15752
commit be896fe247
18 changed files with 1300 additions and 84 deletions
+129
View File
@@ -0,0 +1,129 @@
import { PWA_MANIFEST_READY_EVENT } from '@/modules/pwa/manifest'
import { isTWA } from '@/utils/config'
export const PWA_INSTALL_BANNER_DISMISSED_KEY = 'pwa-install-banner-dismissed'
type BeforeInstallPromptEvent = Event & {
prompt: () => Promise<void>
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>
}
let deferredInstallPrompt: BeforeInstallPromptEvent | null = null
let installListenerInitialized = false
export function isStandalonePwa() {
if (typeof window === 'undefined') return false
const nav = window.navigator as Navigator & { standalone?: boolean }
if (nav.standalone) return true
// installed / launched as app (not a normal browser tab)
const modes = ['standalone', 'fullscreen', 'minimal-ui', 'window-controls-overlay'] as const
return modes.some((mode) => window.matchMedia(`(display-mode: ${mode})`).matches)
}
/** دکمه/منوی نصب فقط وقتی هنوز داخل مرورگر عادی هستیم. */
export function shouldOfferPwaInstall() {
if (typeof window === 'undefined') return false
if (isStandalonePwa() || isTWA()) return false
return true
}
export function isAppleMobileDevice() {
if (typeof navigator === 'undefined') return false
const ua = navigator.userAgent || ''
if (/iPhone|iPod/i.test(ua)) return true
if (/iPad/i.test(ua)) return true
const isTouchMac = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1
if (!isTouchMac) return false
if (/CriOS|FxiOS|EdgiOS/i.test(ua)) return true
if (/Chrome|Edg|Firefox|OPR/i.test(ua) && !/Mobile/i.test(ua)) return false
return true
}
export function isAndroidDevice() {
if (typeof navigator === 'undefined') return false
return /Android/i.test(navigator.userAgent || '')
}
export function isMobileBrowser() {
if (typeof window === 'undefined') return false
const coarsePointer = window.matchMedia('(pointer: coarse)').matches
const narrowViewport = window.matchMedia('(max-width: 767.98px)').matches
return coarsePointer || narrowViewport || isAppleMobileDevice() || isAndroidDevice()
}
export function getDefaultPwaPlatform(): 'android' | 'ios' {
if (isAppleMobileDevice()) return 'ios'
return 'android'
}
export function hasNativeInstallPrompt() {
return Boolean(deferredInstallPrompt)
}
export function isPwaInstallBannerDismissed() {
if (typeof localStorage === 'undefined') return false
return localStorage.getItem(PWA_INSTALL_BANNER_DISMISSED_KEY) === '1'
}
export function dismissPwaInstallBanner() {
localStorage.setItem(PWA_INSTALL_BANNER_DISMISSED_KEY, '1')
}
/** بعد از اولین نمایش بنر / راهنما دیگر نشان داده نشود. */
export function markPwaInstallBannerAsShown() {
dismissPwaInstallBanner()
}
export function shouldShowPwaInstallPrompt() {
if (typeof window === 'undefined') return false
if (!window.isSecureContext) return false
if (!shouldOfferPwaInstall()) return false
if (isPwaInstallBannerDismissed()) return false
return true
}
export function initPwaInstallPromptListener() {
if (installListenerInitialized || typeof window === 'undefined') return
installListenerInitialized = true
window.addEventListener(PWA_MANIFEST_READY_EVENT, (event) => {
const installEvent = (event as CustomEvent<{ installEvent?: Event | null }>).detail?.installEvent
if (installEvent) {
deferredInstallPrompt = installEvent as BeforeInstallPromptEvent
}
})
}
export async function triggerNativePwaInstall() {
const promptEvent = deferredInstallPrompt
if (!promptEvent) {
return false
}
try {
await promptEvent.prompt()
const choice = await promptEvent.userChoice
if (choice.outcome === 'accepted') {
deferredInstallPrompt = null
dismissPwaInstallBanner()
return true
}
} catch (error) {
console.warn('Native PWA install prompt failed', error)
}
return false
}
+71
View File
@@ -0,0 +1,71 @@
export type PwaPlatform = 'android' | 'ios'
export type PwaGuideIllustration =
| 'android-chrome'
| 'android-menu'
| 'android-install'
| 'android-confirm'
| 'ios-safari'
| 'ios-share'
| 'ios-add-home'
| 'ios-confirm'
export type PwaGuideStep = {
illustration: PwaGuideIllustration
text: string
hint?: string
}
export type PwaGuidePlatformConfig = {
title: string
steps: PwaGuideStep[]
}
export const PWA_INSTALL_GUIDE: Record<PwaPlatform, PwaGuidePlatformConfig> = {
android: {
title: 'نصب روی Android',
steps: [
{
illustration: 'android-chrome',
text: 'سایت را در مرورگر Chrome باز کنید.',
},
{
illustration: 'android-menu',
text: 'روی منوی سه‌نقطه ⋮ در گوشه بالا بزنید.',
},
{
illustration: 'android-install',
text: 'گزینه «Install app» یا «Add to Home screen» / «افزودن به صفحه اصلی» را انتخاب کنید.',
},
{
illustration: 'android-confirm',
text: 'نصب را تأیید کنید تا میانبر روی صفحه اصلی ساخته شود.',
hint: 'اگر دکمه نصب را نمی‌بینید، یک‌بار صفحه را رفرش کنید و دوباره منو را باز کنید.',
},
],
},
ios: {
title: 'نصب روی iPhone / iPad',
steps: [
{
illustration: 'ios-safari',
text: 'سایت را در مرورگر Safari باز کنید (از Chrome روی iOS نصب نمی‌شود).',
},
{
illustration: 'ios-share',
text: 'روی دکمه Share در پایین صفحه بزنید (مربع با فلش رو به بالا).',
},
{
illustration: 'ios-add-home',
text: 'به پایین اسکرول کنید و «Add to Home Screen» یا «افزودن به صفحه اصلی» را انتخاب کنید.',
},
{
illustration: 'ios-confirm',
text: 'نام را تأیید کنید و روی Add / افزودن بزنید.',
hint: 'بعد از نصب، آیکون اپ مثل بقیه برنامه‌ها روی صفحه اصلی ظاهر می‌شود.',
},
],
},
}
export const PWA_GUIDE_STEP_LABELS = ['۱', '۲', '۳', '۴'] as const