Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/components/pwa-install-prompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import { useEffect, useState, useCallback } from "react";

const DISMISSED_KEY = "openproof-install-dismissed";
const RE_SHOW_AFTER_DAYS = 7;

interface BeforeInstallPromptEvent extends Event {
readonly platforms: string[];
readonly userChoice: Promise<{ outcome: "accepted" | "dismissed"; platform: string }>;
Expand All @@ -14,14 +17,39 @@
}
}

function isDismissedRecently(): boolean {
if (typeof window === "undefined") return false;
try {
const stored = localStorage.getItem(DISMISSED_KEY);
if (!stored) return false;
const dismissedAt = parseInt(stored, 10);
if (isNaN(dismissedAt)) return false;
return Date.now() - dismissedAt < RE_SHOW_AFTER_DAYS * 24 * 60 * 60 * 1000;
} catch {
return false;
}
}

function markDismissed() {
try {
localStorage.setItem(DISMISSED_KEY, String(Date.now()));
} catch {
// localStorage unavailable
}
}

export function PwaInstallPrompt() {
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
const [showPrompt, setShowPrompt] = useState(false);
const [updateAvailable, setUpdateAvailable] = useState(false);

// Listen for beforeinstallprompt
useEffect(() => {
// If user dismissed recently, don't intercept — let Chrome handle it
if (isDismissedRecently()) return;

const handler = (e: BeforeInstallPromptEvent) => {
// Only preventDefault if we intend to show our own prompt
e.preventDefault();
setDeferredPrompt(e);
// Show prompt after a short delay — don't interrupt initial load
Expand Down Expand Up @@ -88,6 +116,7 @@
const handleDismiss = useCallback(() => {
setShowPrompt(false);
setDeferredPrompt(null);
markDismissed();
}, []);

const handleUpdate = useCallback(() => {
Expand All @@ -104,7 +133,7 @@
{showPrompt && deferredPrompt && (
<div className="fixed bottom-6 left-1/2 z-[100] -translate-x-1/2 animate-in slide-in-from-bottom-4 fade-in">
<div className="flex items-center gap-3 rounded-full border border-border-default bg-bg-surface px-5 py-3 shadow-xl backdrop-blur-md">
<img alt="" className="size-6 shrink-0" src="/icon.svg" />

Check warning on line 136 in src/components/pwa-install-prompt.tsx

View workflow job for this annotation

GitHub Actions / Validate

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span className="text-sm font-semibold text-text-primary">Install OpenProof</span>
<button
className="rounded-full bg-accent px-4 py-1.5 text-xs font-bold text-white transition hover:bg-accent/90 active:scale-[0.97]"
Expand Down
Loading