InstallPrompt
A PWA install dialog with a hook for the browser's install event and an iPhone walkthrough, plus a per-device push opt-in that reuses the notification permission prompt and lists subscribed devices.
Code
import { InstallPrompt, PushOptIn, useInstallPrompt, useNotificationPermission } from "@fadymondy/nasaq/web";import { useState } from "react";export function Onboarding() { const [open, setOpen] = useState(true); const { platform, install } = useInstallPrompt(); const { permission, request } = useNotificationPermission(); const [subscribed, setSubscribed] = useState(false); return ( <> <InstallPrompt open={open} onOpenChange={setOpen} platform={platform} appName="Nasaq" onInstall={install} /> <PushOptIn permission={permission} onRequestPermission={request} subscribed={subscribed} onSubscribedChange={async (on) => { await savePushSubscription(on); setSubscribed(on); }} /> </> );}Apps & Platforms · beta
Live examples and controls: InstallPrompt in the lab.
Install
npx shadcn@latest add https://docs.nasaqui.com/r/install-prompt.jsonTwo pieces for a web app that wants to live on a device. InstallPrompt is the dialog that asks for the install and
useInstallPrompt reads the browser's state. PushOptIn is the per-device notification switch. It reuses the soft ask
from NotificationPermissionPrompt so people hear why before the browser asks,
and once allowed it shows one switch for this device and a list of the other subscribed devices.
When to use
- After a useful moment (a second visit, a finished task), offer to install the app.
- In settings, let each device choose to receive push.
When not to use
- A store-style Install button for an app in a catalogue: use
InstallButton. - Updating an already installed app: use
AppUpdate.
Import
import { InstallPrompt, PushOptIn, useInstallPrompt } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"Quick start
import { InstallPrompt, PushOptIn, useInstallPrompt, useNotificationPermission } from "@fadymondy/nasaq/web";
import { useState } from "react";
export function Onboarding() {
const [open, setOpen] = useState(true);
const { platform, install } = useInstallPrompt();
const { permission, request } = useNotificationPermission();
const [subscribed, setSubscribed] = useState(false);
return (
<>
<InstallPrompt open={open} onOpenChange={setOpen} platform={platform} appName="Nasaq" onInstall={install} />
<PushOptIn
permission={permission}
onRequestPermission={request}
subscribed={subscribed}
onSubscribedChange={async (on) => {
await savePushSubscription(on);
setSubscribed(on);
}}
/>
</>
);
}Anatomy
InstallPrompt data-slot="install-prompt" (data-platform), a Dialog
├─ icon + title your icon, or the provider brand mark
├─ benefits (prompt) three lines, or your own
├─ steps (ios) Share, then Add to Home Screen
└─ Not now / Install
PushOptIn data-slot="push-opt-in"
├─ NotificationPermissionPrompt until the browser allows notifications
├─ this-device switch, test button
└─ devices list name, last seen, removeAPI
useInstallPrompt() returns { platform, install }. platform is "prompt" | "ios" | "installed" | "unsupported"; install() opens the browser's dialog and resolves "accepted" | "dismissed" | "unavailable". The pure detectInstallPlatform(userAgent, standalone, hasPromptEvent), nextAskAt(now, days) and shouldAsk(now, nextAsk) are exported from install-prompt-platform.ts.
InstallPrompt: open, onOpenChange, platform, appName, appIcon, benefits, onInstall, onDismiss, labels.
PushOptIn: section props except children, plus permission, onRequestPermission, subscribed, onSubscribedChange(on), devices (PushDevice[]: id, name, kind, lastSeen, current), onRemoveDevice(id), onTest, requiresInstall, onOpenSettings, labels, permissionLabels. Async callbacks return void or { error }.
Examples
Snooze "Not now" for two weeks
import { nextAskAt, shouldAsk } from "@fadymondy/nasaq/web";
const next = Number(localStorage.getItem("install-next-ask"));
if (shouldAsk(Date.now(), next || null)) setOpen(true);
// onDismiss: localStorage.setItem("install-next-ask", String(nextAskAt(Date.now())));Accessibility
- The dialog traps focus and closes with Escape. Steps are an ordered list; the switch is named and its state is announced.
- Install state is words, not colour alone.
RTL & i18n
- English and Arabic follow the locale. Device names use
dir="auto". The Share icon is not mirrored: it is the system's own symbol. - iPhone steps say Add to Home Screen: keep your own
labelsif the OS language differs from the page.
Styling & tokens
- Uses
Dialog,Switch,Badge,Alertand card tokens. Target[data-slot="install-prompt"],[data-slot="push-opt-in"].
Do / Don't
- Do ask after a useful moment and remember "Not now".
- Do check
requiresInstallon iPhone: push only works from the installed app. - Don't open the browser's permission dialog on first load: use the soft ask.
- Don't show the install dialog when
platformisunsupportedunless you want the "use another browser" note.