← back to Watches
src/components/InstallPrompt.jsx
233 lines
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
setupInstallPrompt,
showInstallPrompt,
isAppInstalled,
isIOS,
isAndroid,
getInstallStatusMessage
} from '../utils/pwaUtils';
/**
* Install Prompt Component
* Shows a banner or button to install the PWA
*/
function InstallPrompt({ variant = 'banner' }) {
const [installable, setInstallable] = useState(false);
const [showBanner, setShowBanner] = useState(false);
const [installed, setInstalled] = useState(false);
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
// Check if already installed
if (isAppInstalled()) {
setInstalled(true);
return;
}
// Check if banner was previously dismissed
const wasDismissed = localStorage.getItem('installBannerDismissed');
if (wasDismissed) {
const dismissedAt = parseInt(wasDismissed);
const daysSince = (Date.now() - dismissedAt) / (1000 * 60 * 60 * 24);
// Show again after 7 days
if (daysSince < 7) {
setDismissed(true);
return;
}
}
// Setup install prompt
setupInstallPrompt(
(isInstallable) => {
setInstallable(isInstallable);
setShowBanner(true);
},
() => {
setInstalled(true);
setShowBanner(false);
}
);
// Show banner after 10 seconds
const timer = setTimeout(() => {
if (!installed && !dismissed) {
setShowBanner(true);
}
}, 10000);
return () => clearTimeout(timer);
}, [installed, dismissed]);
const handleInstall = async () => {
const accepted = await showInstallPrompt();
if (accepted) {
setShowBanner(false);
}
};
const handleDismiss = () => {
setShowBanner(false);
setDismissed(true);
localStorage.setItem('installBannerDismissed', Date.now().toString());
};
// Don't show if already installed or dismissed
if (installed || dismissed) {
return null;
}
// Button variant (for header or settings)
if (variant === 'button') {
return (
<button
onClick={handleInstall}
className="flex items-center gap-2 px-4 py-2 bg-omega-red text-white rounded-lg hover:bg-red-700 transition-colors"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
<span className="hidden sm:inline">Install App</span>
</button>
);
}
// iOS banner (different UI since no install prompt API)
if (isIOS() && showBanner) {
return (
<AnimatePresence>
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
className="fixed bottom-0 left-0 right-0 z-50 p-4 bg-gradient-to-r from-omega-navy to-omega-red text-white shadow-2xl"
>
<div className="container mx-auto max-w-2xl">
<div className="flex items-start gap-4">
<div className="flex-shrink-0">
<svg
className="w-12 h-12"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12 2L2 7v10c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-10-5z" />
</svg>
</div>
<div className="flex-1">
<h3 className="font-bold text-lg mb-1">Install Omega Watch App</h3>
<p className="text-sm text-gray-200 mb-2">
Add to your home screen for quick access and offline browsing
</p>
<div className="flex items-center gap-2 text-sm">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"
/>
</svg>
<span>Tap Share, then "Add to Home Screen"</span>
</div>
</div>
<button
onClick={handleDismiss}
className="flex-shrink-0 p-2 hover:bg-white/20 rounded-lg transition-colors"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
</motion.div>
</AnimatePresence>
);
}
// Android/Chrome banner (with install button)
if (installable && showBanner) {
return (
<AnimatePresence>
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
className="fixed bottom-4 left-4 right-4 z-50 mx-auto max-w-2xl"
>
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl border-2 border-omega-red overflow-hidden">
<div className="p-6">
<div className="flex items-start gap-4">
<div className="flex-shrink-0 w-16 h-16 bg-gradient-to-br from-omega-red to-red-700 rounded-xl flex items-center justify-center">
<svg
className="w-8 h-8 text-white"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
</svg>
</div>
<div className="flex-1">
<h3 className="font-bold text-lg text-gray-800 dark:text-white mb-1">
Install Omega Watch App
</h3>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-4">
Get quick access and browse watches offline
</p>
<div className="flex gap-3">
<button
onClick={handleInstall}
className="flex-1 bg-omega-red hover:bg-red-700 text-white font-semibold py-3 px-6 rounded-lg transition-colors"
>
Install Now
</button>
<button
onClick={handleDismiss}
className="px-4 py-3 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
Not Now
</button>
</div>
</div>
</div>
</div>
<div className="bg-gradient-to-r from-omega-red to-red-700 h-1"></div>
</div>
</motion.div>
</AnimatePresence>
);
}
return null;
}
export default InstallPrompt;