-
Notifications
You must be signed in to change notification settings - Fork 32
feat: implement custom mobile PWA install prompt with dual-lock devic… #85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
46fde56
ea9f205
03670a7
ac37cdc
725bc2c
ad86582
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,19 +1,24 @@ | ||||||
| <!doctype html> | ||||||
| <html lang="en"> | ||||||
| <head> | ||||||
| <meta charset="UTF-8" /> | ||||||
| <link rel="icon" type="image/gif" href="/fish.gif" /> | ||||||
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> | ||||||
| <meta name="description" content="FreshScan AI — Edge-AI Progressive Web App for real-time fish freshness assessment using biomarker analysis." /> | ||||||
| <meta name="theme-color" content="#131313" /> | ||||||
| <link rel="manifest" href="/manifest.json" /> | ||||||
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | ||||||
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> | ||||||
| <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Inter:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet" /> | ||||||
| <title>FreshScan AI | Edge-AI Fish Freshness Assessment</title> | ||||||
| </head> | ||||||
| <body> | ||||||
| <div id="root"></div> | ||||||
| <script type="module" src="/src/main.tsx"></script> | ||||||
| </body> | ||||||
| </html> | ||||||
|
|
||||||
| <head> | ||||||
| <meta charset="UTF-8" /> | ||||||
| <link rel="icon" type="image/gif" href="/fish.gif" /> | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify the new PNG icon files exist in the public directory
fd -t f '^(image_192\.png|image_512\.png|fish\.gif)$' public/Repository: jpdevhub/FreshScanAi Length of output: 122 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Find manifest file referenced by index.html
echo "== index.html manifest references =="
rg -n --hidden --no-ignore -S '<link[^>]+rel=["'\'']manifest["'\'']' -g'index.html' . || true
# Locate manifest file(s)
echo
echo "== candidate manifest files =="
fd -t f -e json -e webmanifest -e manifest -d 4 'manifest*.{json,webmanifest}' . || true
fd -t f -d 4 -e json -e webmanifest '*manifest*.{json,webmanifest}' . || true
# Print the manifest icon entries (paths only)
echo
echo "== manifest icon references (paths) =="
for f in $(fd -t f 'manifest*.json' . -0 2>/dev/null | tr '\0' '\n' || true); do
echo "--- $f ---"
# extract icon src fields and sizes
rg -n '"icons"\s*:' "$f" || true
rg -n '"src"\s*:' "$f" || true
done
for f in $(fd -t f 'manifest*.webmanifest' . -0 2>/dev/null | tr '\0' '\n' || true); do
echo "--- $f ---"
rg -n '"src"\s*:' "$f" || true
done
# Show the favicon line from index.html
echo
echo "== favicon line in index.html =="
rg -n --hidden --no-ignore -S '<link[^>]+rel=["'\'']icon["'\'']' index.html || trueRepository: jpdevhub/FreshScanAi Length of output: 1097 Fix favicon to match PWA manifest icon assets.
🔧 Recommended fix to align favicon with manifest- <link rel="icon" type="image/gif" href="/fish.gif" />
+ <link rel="icon" type="image/png" href="/image_192.png" />📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> | ||||||
| <meta name="description" | ||||||
| content="FreshScan AI — Edge-AI Progressive Web App for real-time fish freshness assessment using biomarker analysis." /> | ||||||
| <meta name="theme-color" content="#131313" /> | ||||||
| <link rel="manifest" href="/manifest.json" /> | ||||||
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | ||||||
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> | ||||||
| <link | ||||||
| href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Inter:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" | ||||||
| rel="stylesheet" /> | ||||||
| <title>FreshScan AI | Edge-AI Fish Freshness Assessment</title> | ||||||
| </head> | ||||||
|
|
||||||
| <body> | ||||||
| <div id="root"></div> | ||||||
| <script type="module" src="/src/main.tsx"></script> | ||||||
| </body> | ||||||
|
|
||||||
| </html> | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| import { useState, useEffect } from 'react'; | ||
|
|
||
| type PromptType = "ios" | "android" | null; | ||
|
|
||
| const IOSMsg = "Add FreshScan AI to your Home Screen for one-tap fish freshness analysis. Tap Share and select Add to Home Screen." | ||
| const AndroidMsg = "Add FreshScan AI to your home screen for instant fish freshness analysis in one tap." | ||
|
|
||
| interface BeforeInstallPromptEvent extends Event { | ||
| readonly platforms: string[]; | ||
| readonly userChoice: Promise<{ | ||
| outcome: 'accepted' | 'dismissed'; | ||
| platform: string; | ||
| }>; | ||
| prompt(): Promise<void>; | ||
| } | ||
|
|
||
| interface NavigatorWithStandalone extends Navigator { | ||
| standalone?: boolean; | ||
| } | ||
|
|
||
| export default function InstallPrompt() { | ||
| const [promptType, setpromptType] = useState<PromptType>(() => { | ||
| if (typeof window === 'undefined') return null; | ||
|
|
||
| const userAgent = navigator.userAgent || navigator.vendor || ''; | ||
| const ios = /iphone|ipad|ipod/i.test(userAgent); | ||
| const standalone = window.matchMedia('(display-mode: standalone)').matches || (navigator as NavigatorWithStandalone).standalone; | ||
|
|
||
| return (ios && !standalone) ? "ios" : null; | ||
| }); | ||
|
|
||
| const [showInstallPrompt, setShowInstallPrompt] = useState(() => { | ||
| return promptType === "ios"; | ||
| }); | ||
| const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| const userAgent = navigator.userAgent || navigator.vendor || ''; | ||
| const handler = (e: Event) => { | ||
| e.preventDefault(); | ||
|
|
||
| const installEvent = e as BeforeInstallPromptEvent; | ||
|
|
||
| const isMobileOrTablet = /android|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent); | ||
| const hasTouchScreen = window.matchMedia('(pointer: coarse)').matches; | ||
|
|
||
| if (isMobileOrTablet && hasTouchScreen) { | ||
| setpromptType("android"); | ||
| setDeferredPrompt(installEvent); | ||
| setShowInstallPrompt(true); | ||
| } | ||
| }; | ||
|
|
||
| window.addEventListener('beforeinstallprompt', handler as EventListener); | ||
|
|
||
| return () => { | ||
| window.removeEventListener('beforeinstallprompt', handler as EventListener); | ||
| }; | ||
| }, []); | ||
|
|
||
| const handleInstallClick = async () => { | ||
| if (!deferredPrompt) return; | ||
|
|
||
| try { | ||
| await deferredPrompt.prompt(); | ||
|
|
||
| const choiceResult = await deferredPrompt.userChoice; | ||
|
|
||
| if (choiceResult.outcome === 'accepted') { | ||
| console.log('User installed the PWA!'); | ||
| } else { | ||
| console.log('User dismissed the install dialog.'); | ||
| } | ||
| } catch (err) { | ||
| console.error("Error triggering the install prompt:", err); | ||
| } | ||
|
|
||
| setDeferredPrompt(null); | ||
| setShowInstallPrompt(false); | ||
| }; | ||
|
|
||
| const handleNotNow = () => { | ||
| setShowInstallPrompt(false); | ||
| }; | ||
|
|
||
| const renderFunction = (Message: string) => { | ||
| return ( | ||
| <div | ||
| className="fixed bottom-5 left-5 right-5 max-w-sm z-[9999] border-4 border-black bg-gray-800 p-5 shadow-[8px_8px_0px_0px_black]" | ||
| > | ||
| <div className="flex items-center gap-4 border-b-4 border-black pb-4"> | ||
| <img | ||
| src="/fish.gif" | ||
| alt="FreshScan AI" | ||
| width={56} | ||
| height={56} | ||
| /> | ||
|
|
||
| <div> | ||
| <p className="text-xs font-black tracking-widest uppercase"> | ||
| FreshScan AI | ||
| </p> | ||
|
|
||
| <h2 className="text-xl font-black uppercase"> | ||
| Install App | ||
| </h2> | ||
| </div> | ||
| </div> | ||
|
|
||
| <p className="mt-4 text-base font-bold leading-relaxed"> | ||
| {Message} | ||
| </p> | ||
|
|
||
| <div className="mt-5 flex gap-3"> | ||
| {(promptType == 'android') && ( | ||
| <button | ||
| onClick={handleInstallClick} | ||
| className="flex-1 border-4 border-black bg-lime-300 px-4 py-3 text-black uppercase shadow-[4px_4px_0px_0px_black] active:translate-x-1 active:translate-y-1 active:shadow-none" | ||
| > | ||
| Install | ||
| </button> | ||
| )} | ||
|
|
||
| <button | ||
| onClick={handleNotNow} | ||
| className="px-4 py-3 font-black uppercase" | ||
| > | ||
| Not Now | ||
| </button> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| const message = promptType === 'ios' ? IOSMsg : AndroidMsg; | ||
| return ( | ||
| <> | ||
| {showInstallPrompt && renderFunction(message)} | ||
| </> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add the missing DOCTYPE declaration.
HTML5 documents should begin with
<!DOCTYPE html>. Without it, browsers may enter quirks mode, leading to inconsistent rendering behavior across platforms.🔧 Proposed fix
+<!DOCTYPE html> <html lang="en">📝 Committable suggestion
🧰 Tools
🪛 HTMLHint (1.9.2)
[error] 1-1: Doctype must be declared before any non-comment content.
(doctype-first)
🤖 Prompt for AI Agents