← Public packages
@kentcdodds/package-app-kit
Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.
import { useCallback, useEffect, useState } from 'react'
import { IconDownload } from '../icons/download.tsx'
type DeferredPrompt = {
prompt: () => Promise<void>
userChoice?: Promise<unknown>
}
/**
* Module-level capture so remounts cannot drop a preventDefault'd BIP
* event (Chrome warns if preventDefault runs and prompt() never does).
*/
let capturedPrompt: DeferredPrompt | null = null
let bipListenerBound = false
const listeners = new Set<() => void>()
function bindBeforeInstallPrompt() {
if (typeof window === 'undefined' || bipListenerBound) return
bipListenerBound = true
window.addEventListener('beforeinstallprompt', (event) => {
const promptFn = (event as unknown as DeferredPrompt).prompt
if (typeof promptFn !== 'function' || !document.querySelector('[data-install]')) {
return
}
event.preventDefault()
capturedPrompt = event as unknown as DeferredPrompt
for (const listener of listeners) listener()
})
window.addEventListener('appinstalled', () => {
capturedPrompt = null
for (const listener of listeners) listener()
})
}
async function promptCapturedInstall() {
const event = capturedPrompt
if (typeof event?.prompt !== 'function') return false
capturedPrompt = null
try {
await event.prompt()
if (event.userChoice) await event.userChoice
return true
} catch {
return false
}
}
/**
* Header install control (reserved slot) + optional iOS A2HS overlay.
*/
export function InstallCta({ appName }: { appName: string }) {
const [standalone, setStandalone] = useState(false)
const [ios, setIos] = useState(false)
const [hintDismissed, setHintDismissed] = useState(false)
const [iosHintOpen, setIosHintOpen] = useState(false)
const [canNative, setCanNative] = useState(false)
const [clientReady, setClientReady] = useState(false)
const hintKey = '__PACKAGE_ID__-install-hint-dismissed'
const detect = useCallback(() => {
try {
setStandalone(
(navigator as { standalone?: boolean }).standalone === true ||
window.matchMedia(
'(display-mode: standalone), (display-mode: fullscreen), (display-mode: minimal-ui)',
).matches,
)
} catch {
setStandalone(false)
}
setIos(
/iphone|ipad|ipod/i.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && (navigator.maxTouchPoints || 0) > 1),
)
try {
setHintDismissed(localStorage.getItem(hintKey) !== null)
} catch {
/* ignore */
}
setCanNative(Boolean(capturedPrompt?.prompt))
}, [])
useEffect(() => {
setClientReady(true)
detect()
bindBeforeInstallPrompt()
const onChange = () => detect()
listeners.add(onChange)
return () => {
listeners.delete(onChange)
}
}, [detect])
const showIos = clientReady && !standalone && ios && !hintDismissed && !canNative
const installAvailable = canNative || showIos
const tip = `Install ${appName}`
return (
<span className="pak-install-root">
<div
className="pak-install-slot"
data-install-wrap
data-available={installAvailable ? 'true' : 'false'}
>
<button
className="pak-btn pak-btn-icon"
type="button"
data-install
data-tip={tip}
aria-label={tip}
aria-hidden={installAvailable ? undefined : true}
tabIndex={installAvailable ? undefined : -1}
onClick={async () => {
if (capturedPrompt?.prompt) {
await promptCapturedInstall()
detect()
return
}
if (showIos) setIosHintOpen((open) => !open)
}}
>
<IconDownload size={20} />
</button>
</div>
{showIos && iosHintOpen ? (
<div
className="pak-install-hint-overlay"
data-install-hint
role="dialog"
aria-label="Add to Home Screen"
>
<div className="pak-card pak-stack pak-install-hint-card">
<p className="pak-muted">
Add <strong>{appName}</strong> to your Home Screen: tap <strong>Share</strong>, then{' '}
<strong>Add to Home Screen</strong>.
</p>
<button
className="pak-btn"
type="button"
data-install-hint-dismiss
onClick={() => {
try {
localStorage.setItem(hintKey, String(Date.now()))
} catch {
/* ignore */
}
setHintDismissed(true)
setIosHintOpen(false)
}}
>
Dismiss
</button>
</div>
</div>
) : null}
</span>
)
}