Add web app install button and step-by-step PWA guide.
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user