90 lines
2.8 KiB
TypeScript
90 lines
2.8 KiB
TypeScript
/**
|
|
* PWA Install Prompt — shows install button when PWA is installable (Task 5.24).
|
|
*/
|
|
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Download, X } from 'lucide-react';
|
|
|
|
interface BeforeInstallPromptEvent extends Event {
|
|
prompt: () => Promise<void>;
|
|
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
|
|
}
|
|
|
|
const DISMISS_KEY = 'leocrm_pwa_install_dismissed';
|
|
|
|
export function PWAInstallPrompt() {
|
|
const { t } = useTranslation();
|
|
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
|
|
const [visible, setVisible] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const dismissed = localStorage.getItem(DISMISS_KEY);
|
|
if (dismissed) return;
|
|
|
|
const handler = (e: Event) => {
|
|
e.preventDefault();
|
|
setDeferredPrompt(e as BeforeInstallPromptEvent);
|
|
setVisible(true);
|
|
};
|
|
|
|
window.addEventListener('beforeinstallprompt', handler);
|
|
return () => window.removeEventListener('beforeinstallprompt', handler);
|
|
}, []);
|
|
|
|
const handleInstall = useCallback(async () => {
|
|
if (!deferredPrompt) return;
|
|
await deferredPrompt.prompt();
|
|
const choice = await deferredPrompt.userChoice;
|
|
if (choice.outcome === 'accepted') {
|
|
setVisible(false);
|
|
}
|
|
setDeferredPrompt(null);
|
|
}, [deferredPrompt]);
|
|
|
|
const handleDismiss = useCallback(() => {
|
|
localStorage.setItem(DISMISS_KEY, '1');
|
|
setVisible(false);
|
|
}, []);
|
|
|
|
if (!visible || !deferredPrompt) return null;
|
|
|
|
return (
|
|
<div
|
|
className="fixed bottom-4 right-4 z-50 bg-white rounded-lg shadow-lg border border-secondary-200 p-4 max-w-sm"
|
|
data-testid="pwa-install-prompt"
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<Download className="w-5 h-5 text-primary-600 mt-0.5" />
|
|
<div className="flex-1">
|
|
<p className="font-medium text-secondary-900">{t('pwa.installTitle')}</p>
|
|
<p className="text-sm text-secondary-600 mt-1">{t('pwa.installDescription')}</p>
|
|
<div className="flex gap-2 mt-3">
|
|
<button
|
|
className="px-3 py-1.5 bg-primary-600 text-white rounded-md text-sm font-medium hover:bg-primary-700"
|
|
onClick={handleInstall}
|
|
data-testid="pwa-install-btn"
|
|
>
|
|
{t('pwa.install')}
|
|
</button>
|
|
<button
|
|
className="px-3 py-1.5 text-secondary-600 text-sm hover:bg-secondary-100 rounded-md"
|
|
onClick={handleDismiss}
|
|
data-testid="pwa-dismiss-btn"
|
|
>
|
|
{t('pwa.dismiss')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<button
|
|
className="text-secondary-400 hover:text-secondary-600"
|
|
onClick={handleDismiss}
|
|
aria-label="Close"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|