import React from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Shield, Bell, User, Smartphone, Sparkles } from 'lucide-react'; import { YStack, XStack } from '@tamagui/stacks'; import { YGroup } from '@tamagui/group'; import { ListItem } from '@tamagui/list-item'; import { SizableText as Text } from '@tamagui/text'; import { Switch } from '@tamagui/switch'; import { MobileShell } from './components/MobileShell'; import { MobileCard, CTAButton, PillBadge } from './components/Primitives'; import { useAuth } from '../auth/context'; import { getNotificationPreferences, updateNotificationPreferences, NotificationPreferences, } from '../api'; import { getApiErrorMessage } from '../lib/apiError'; import { adminPath, ADMIN_HOME_PATH, ADMIN_PROFILE_ACCOUNT_PATH } from '../constants'; import { useAdminPushSubscription } from './hooks/useAdminPushSubscription'; import { useDevicePermissions } from './hooks/useDevicePermissions'; import { type PermissionStatus, type StorageStatus } from './lib/devicePermissions'; import { useInstallPrompt } from './hooks/useInstallPrompt'; import { getInstallBannerDismissed, setInstallBannerDismissed, shouldShowInstallBanner } from './lib/installBanner'; import { MobileInstallBanner } from './components/MobileInstallBanner'; import { setTourSeen } from './lib/mobileTour'; import { useBackNavigation } from './hooks/useBackNavigation'; import { useOnlineStatus } from './hooks/useOnlineStatus'; import { useAdminTheme } from './theme'; type PreferenceKey = keyof NotificationPreferences; const AVAILABLE_PREFS: PreferenceKey[] = [ 'photo_thresholds', 'photo_limits', 'guest_thresholds', 'guest_limits', 'gallery_warnings', 'gallery_expired', 'event_thresholds', 'event_limits', 'package_expiring', 'package_expired', ]; export default function MobileSettingsPage() { const { t } = useTranslation('management'); const navigate = useNavigate(); const { user, logout } = useAuth(); const { text, muted, border, danger } = useAdminTheme(); const [preferences, setPreferences] = React.useState({}); const [defaults, setDefaults] = React.useState({}); const [loading, setLoading] = React.useState(true); const [saving, setSaving] = React.useState(false); const [error, setError] = React.useState(null); const [storageSaving, setStorageSaving] = React.useState(false); const [storageError, setStorageError] = React.useState(null); const pushState = useAdminPushSubscription(); const devicePermissions = useDevicePermissions(); const online = useOnlineStatus(); const installPrompt = useInstallPrompt(); const back = useBackNavigation(adminPath('/mobile/profile')); const [installBannerDismissed, setInstallBannerDismissedState] = React.useState(() => getInstallBannerDismissed()); const installBanner = shouldShowInstallBanner( { isInstalled: installPrompt.isInstalled, isStandalone: installPrompt.isStandalone, canInstall: installPrompt.canInstall, isIos: installPrompt.isIos, }, installBannerDismissed, ); const pushDescription = React.useMemo(() => { if (!pushState.supported) { return t('mobileSettings.pushUnsupported', 'Push wird auf diesem Gerät nicht unterstützt.'); } if (pushState.permission === 'denied') { return t('mobileSettings.pushDenied', 'Benachrichtigungen sind im Browser blockiert.'); } if (pushState.subscribed) { return t('mobileSettings.pushActive', 'Push aktiv'); } return t('mobileSettings.pushInactive', 'Push deaktiviert'); }, [pushState.permission, pushState.subscribed, pushState.supported, t]); const permissionTone = (status: PermissionStatus) => { if (status === 'granted') { return 'success'; } if (status === 'denied' || status === 'prompt') { return 'warning'; } return 'muted'; }; const storageTone = (status: StorageStatus) => { if (status === 'persisted') { return 'success'; } if (status === 'available') { return 'warning'; } return 'muted'; }; const permissionLabel = (status: PermissionStatus) => t(`mobileSettings.deviceStatusValues.${status}`, status); const storageLabel = (status: StorageStatus) => t(`mobileSettings.deviceStatusValues.${status}`, status); const connectionLabel = online ? t('mobileSettings.deviceStatusValues.online', 'Online') : t('mobileSettings.deviceStatusValues.offline', 'Offline'); React.useEffect(() => { (async () => { setLoading(true); try { const result = await getNotificationPreferences(); const defaultsMerged: NotificationPreferences = result.defaults ?? {}; const prefs = { ...defaultsMerged, ...(result.preferences ?? {}) }; AVAILABLE_PREFS.forEach((key) => { if (prefs[key] === undefined) { prefs[key] = defaultsMerged[key] ?? false; } }); setPreferences(prefs); setDefaults(defaultsMerged); setError(null); } catch (err) { setError(getApiErrorMessage(err, t('settings.notifications.errorLoad', 'Benachrichtigungen konnten nicht geladen werden.'))); } finally { setLoading(false); } })(); }, [t]); React.useEffect(() => { if (devicePermissions.storage === 'persisted') { setStorageError(null); } }, [devicePermissions.storage]); const handleReplayTour = () => { setTourSeen(false); navigate(`${ADMIN_HOME_PATH}?tour=1`); }; const handleResetInstallBanner = () => { setInstallBannerDismissed(false); setInstallBannerDismissedState(false); }; const togglePref = (key: PreferenceKey) => { setPreferences((prev) => ({ ...prev, [key]: !prev[key], })); }; const handleSave = async () => { setSaving(true); try { const payload: NotificationPreferences = {}; AVAILABLE_PREFS.forEach((key) => { payload[key] = Boolean(preferences[key]); }); await updateNotificationPreferences(payload); setError(null); } catch (err) { setError(getApiErrorMessage(err, t('settings.notifications.errorSave', 'Speichern fehlgeschlagen'))); } finally { setSaving(false); } }; const handleReset = () => { setPreferences(defaults); }; const handleStoragePersist = async () => { setStorageSaving(true); const granted = await devicePermissions.requestPersistentStorage(); setStorageSaving(false); if (granted) { setStorageError(null); void devicePermissions.refresh(); } else { setStorageError( t('mobileSettings.deviceStorageError', 'Offline-Schutz konnte nicht aktiviert werden.') ); } }; return ( {error ? ( {error} ) : null} void installPrompt.promptInstall() : undefined} onDismiss={() => { setInstallBannerDismissed(true); setInstallBannerDismissedState(true); }} /> {t('mobileSettings.accountTitle', 'Account')} {user?.name ?? user?.email ?? t('settings.session.unknown', 'Benutzer')} {user?.tenant_id ? ( {t('mobileSettings.tenantBadge', 'Account #{{id}}', { id: user.tenant_id })} ) : null} navigate(ADMIN_PROFILE_ACCOUNT_PATH)} /> logout({ redirect: adminPath('/logout') })} /> {t('mobileSettings.notificationsTitle', 'Notifications')} {loading ? ( {t('mobileSettings.notificationsLoading', 'Loading settings ...')} ) : ( {t('mobileSettings.pushTitle', 'App Push')} } subTitle={ {pushState.loading ? t('mobileSettings.pushLoading', 'Lädt ...') : pushDescription} } iconAfter={ { if (value) { void pushState.enable(); } else { void pushState.disable(); } }} disabled={!pushState.supported || pushState.permission === 'denied' || pushState.loading} aria-label={t('mobileSettings.pushTitle', 'App Push')} > } /> {AVAILABLE_PREFS.map((key, index) => ( {t(`settings.notifications.keys.${key}.label`, key)} } subTitle={ {t(`settings.notifications.keys.${key}.description`, '')} } iconAfter={ togglePref(key)} aria-label={t(`settings.notifications.keys.${key}.label`, key)} > } /> ))} )} {pushState.error ? ( {pushState.error} ) : null} handleSave()} /> handleReset()} /> {t('mobileSettings.deviceTitle', 'Device & permissions')} {t('mobileSettings.deviceDescription', 'Check permissions so the admin app stays fast and offline-ready.')} {devicePermissions.loading ? ( {t('mobileSettings.deviceLoading', 'Checking device status ...')} ) : ( {t('mobileSettings.deviceStatus.notifications.label', 'Notifications')} {t('mobileSettings.deviceStatus.notifications.description', 'Allow alerts and admin updates.')} {permissionLabel(devicePermissions.notifications)} {t('mobileSettings.deviceStatus.camera.label', 'Camera')} {t('mobileSettings.deviceStatus.camera.description', 'Needed for QR scans and quick capture.')} {permissionLabel(devicePermissions.camera)} {t('mobileSettings.deviceStatus.storage.label', 'Offline storage')} {t('mobileSettings.deviceStatus.storage.description', 'Protect cached data from eviction.')} {storageLabel(devicePermissions.storage)} {t('mobileSettings.deviceStatus.connection.label', 'Connection')} {t('mobileSettings.deviceStatus.connection.description', 'Shows if the app is online or offline.')} {connectionLabel} )} {devicePermissions.storage === 'available' ? ( handleStoragePersist()} disabled={storageSaving} /> ) : null} {storageError ? ( {storageError} ) : null} {t('mobileSettings.experienceTitle', 'Experience')} {t('mobileSettings.experienceBody', 'Replay the quick tour or re-enable the install banner.')} {t('settings.appearance.title', 'Darstellung')} {t('settings.appearance.description', 'Schalte Dark-Mode oder passe Branding im Admin an.')} navigate(adminPath('/settings'))} /> ); }