Files
xenia-pos-local/waiter_pwa/src/components/InstallAppBanner.jsx
T
bonaminandClaude Opus 5.5 a486e3ff63 feat(waiter): native pairing, venue switcher, QR scanner, Android back handling
- First run in the native app shows only the pairing screen (VenuesPage):
  scan the manager's pairing QR or type the server address; the server must
  answer /api/system/identity as xenia-pos with a supported api_version, and
  a QR's ?pair=<site_id> must match - a code from another venue is refused
- /venues (native only): list of paired venues, switch (reloads into that
  venue's own token/IndexedDB), remove (data kept - may hold unsynced
  orders). Reachable from the login screen, the user menu and the offline page
- QrScannerModal: WebView camera via qr-scanner (no Google Play Services
  dependency, carries over to iOS); requests the camera once up front so the
  Android prompt appears a single time and "denied" gets its own message
- Hardware back: @capacitor/app listener - no-op on root screens (/, /tables,
  /login), otherwise history back. Path-based because Chrome's history
  intervention makes the WebView report canGoBack=false for the sentinel
  entry AndroidBackGuard pushes
- Before pairing no IndexedDB is opened, so nothing is created under the
  un-namespaced name
- InstallAppBanner now shows in plain-HTTP browser mode only when the venue
  server actually serves the APK (HEAD /downloads/xenia-waiter.apk)

Verified on an Android 15 emulator (API 35) with real taps via adb and state
via WebView DevTools: wrong address -> error; other venue's QR -> refused;
pairing -> venue-namespaced storage, live WebSocket; back on /tables keeps the
app open; restart keeps venue + session; second venue pairs logged-out and
switching back keeps venue 1's session; camera deny -> one prompt + message;
allow -> live preview. Signed release APK installed and paired. Web modes
(same-origin, plain-HTTP LAN IP, VITE_SERVER_URL) re-verified in Edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 13:31:23 +03:00

98 lines
2.8 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react'
import { deliveryMode } from '../config/server'
// Shown only in plain-HTTP browser mode (http://<LAN IP>), where the page can't
// work offline after a reload and can't be installed as a real PWA.
//
// The APK is served by the venue's own server (scripts/build-apk.mjs bundles the
// release APK into the web build), so the banner appears only when that file is
// actually there. VITE_APP_DOWNLOAD_URL overrides the location if ever needed.
const DOWNLOAD_URL = import.meta.env.VITE_APP_DOWNLOAD_URL || '/downloads/xenia-waiter.apk'
const DISMISS_KEY = 'install-banner-dismissed-until'
const DISMISS_DAYS = 7
function isDismissed() {
try {
return Number(localStorage.getItem(DISMISS_KEY) || 0) > Date.now()
} catch {
return false
}
}
export default function InstallAppBanner() {
const eligible = deliveryMode() === 'plain-web' && !isDismissed()
const [available, setAvailable] = useState(false)
const [hidden, setHidden] = useState(false)
useEffect(() => {
if (!eligible) return
fetch(DOWNLOAD_URL, { method: 'HEAD' })
.then(r => setAvailable(r.ok && !(r.headers.get('content-type') || '').includes('text/html')))
.catch(() => {})
}, [eligible])
if (!eligible || !available || hidden) return null
function dismiss() {
try {
localStorage.setItem(DISMISS_KEY, String(Date.now() + DISMISS_DAYS * 86_400_000))
} catch {
// storage unavailable — banner just reappears next load
}
setHidden(true)
}
return (
<div style={{
position: 'fixed',
bottom: 80,
left: 12,
right: 12,
zIndex: 9998,
background: 'var(--bg2)',
border: '1px solid var(--accent)',
borderRadius: 16,
padding: '12px 14px',
display: 'flex',
alignItems: 'center',
gap: 12,
boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
}}>
<span style={{ flex: 1, fontSize: 13, color: 'var(--text)', lineHeight: 1.35 }}>
Για γρηγορότερη και πιο σταθερή λειτουργία, εγκαταστήστε την εφαρμογή.
</span>
<a
href={DOWNLOAD_URL}
download
style={{
background: 'var(--accent)',
color: '#0f172a',
borderRadius: 10,
padding: '8px 14px',
fontSize: 14,
fontWeight: 700,
textDecoration: 'none',
whiteSpace: 'nowrap',
}}
>
Εγκατάσταση
</a>
<button
onClick={dismiss}
aria-label="Κλείσιμο"
style={{
background: 'none',
border: 'none',
color: 'var(--muted)',
fontSize: 20,
lineHeight: 1,
padding: 4,
cursor: 'pointer',
}}
>
×
</button>
</div>
)
}