import { ActivityIndicator, Alert, Image, Platform, ScrollView, Text, TouchableOpacity, View, } from 'react-native'; import { Button } from '../components/Button'; import { useEffect, useState } from 'react'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Ionicons } from '@expo/vector-icons'; import { MenuView } from '@react-native-menu/menu'; import { useTranslation } from 'react-i18next'; import { useColors } from '../lib/theme'; import { useDevicesStore, type UserDevice } from '../stores/devices'; function formatCountdown(isoTarget: string): string { const ms = new Date(isoTarget).getTime() - Date.now(); if (ms <= 0) return '0min'; const totalMin = Math.floor(ms / 60_000); const h = Math.floor(totalMin / 60); const m = totalMin % 60; if (h > 0) return `${h}h ${m}min`; return `${m}min`; } import { useProtectedDevicesStore, type ProtectedDevice } from '../stores/protectedDevices'; import { useProtectedDevicesRealtime } from '../hooks/useProtectedDevicesRealtime'; import { useUserDevicesRealtime } from '../hooks/useUserDevicesRealtime'; import { useUserPlan } from '../hooks/useUserPlan'; import { AppHeader } from '../components/AppHeader'; import { MagicSheet } from '../components/devices/MagicSheet'; import { DeviceSlotDonut } from '../components/devices/DeviceSlotDonut'; import { DeviceStatusPill } from '../components/devices/DeviceStatusPill'; import { DeviceDetailSheet, type DeviceDetail } from '../components/devices/DeviceDetailSheet'; import { deviceImage } from '../components/devices/deviceIcon'; // ─── Helpers ───────────────────────────────────────────────────────────────── function formatLastSeen(iso: string, t: (k: string, o?: any) => string): string { const ms = Date.now() - new Date(iso).getTime(); const min = Math.floor(ms / 60_000); if (min < 1) return t('settings.devices_just_now'); if (min < 60) return t('settings.devices_mins_ago', { count: min }); const hr = Math.floor(min / 60); if (hr < 24) return t('settings.devices_hours_ago', { count: hr }); const day = Math.floor(hr / 24); if (day < 30) return t('settings.devices_days_ago', { count: day }); return new Date(iso).toLocaleDateString(Platform.OS === 'ios' ? undefined : 'de-DE', { day: '2-digit', month: 'short', year: 'numeric', }); } function formatSince(iso: string): string { return new Date(iso).toLocaleDateString(Platform.OS === 'ios' ? undefined : 'de-DE', { day: '2-digit', month: 'short', year: 'numeric', }); } // ─── Status Badge ───────────────────────────────────────────────────────────── function StatusBadge({ status }: { status: ProtectedDevice['status'] }) { const { t } = useTranslation(); const colors = useColors(); const config: Record = { pending: { label: t('devices.status_pending'), bg: 'rgba(245,158,11,0.12)', fg: '#f59e0b', }, active: { label: t('devices.status_active'), bg: colors.success + '1a', fg: colors.success, }, revoked: { label: t('devices.status_revoked'), bg: 'rgba(0,0,0,0.06)', fg: colors.textMuted, }, degraded: { label: t('plan_limit.device_degraded_title'), bg: 'rgba(220,38,38,0.1)', fg: colors.error, }, }; const resolved = config[status] ?? { label: status, bg: 'rgba(0,0,0,0.06)', fg: colors.textMuted }; return ( {resolved.label} ); } // ─── Mobile Device Row ──────────────────────────────────────────────────────── function MobileDeviceRow({ device, onRemove, onRequestRelease, onCancelRelease, onOpenDetail, }: { device: UserDevice; onRemove: (id: string) => void; onRequestRelease: (id: string) => void; onCancelRelease: (id: string) => void; onOpenDetail: (d: DeviceDetail) => void; }) { const { t } = useTranslation(); const colors = useColors(); const isBound = !!device.boundToPlan; const releaseAt = device.releaseRequestedAt ? new Date(new Date(device.releaseRequestedAt).getTime() + 24 * 60 * 60 * 1000).toISOString() : null; const releaseActive = !!releaseAt && new Date(releaseAt).getTime() > Date.now(); function confirmRequestRelease() { Alert.alert( t('devices.release_request_title'), t('devices.release_request_body'), [ { text: t('common.cancel'), style: 'cancel' }, { text: t('devices.release_request_confirm'), style: 'destructive', onPress: () => onRequestRelease(device.id), }, ] ); } function confirmCancelRelease() { Alert.alert( t('devices.release_cancel_confirm'), t('devices.release_cancel_body'), [ { text: t('common.cancel'), style: 'cancel' }, { text: t('devices.release_cancel_cta'), style: 'destructive', onPress: () => onCancelRelease(device.id), }, ] ); } const deviceName = device.model ?? device.name ?? device.platform; const footerText = `${formatLastSeen(device.lastSeenAt, t)} · ${t('settings.devices_since')} ${formatSince(device.createdAt)}`; function openDetail() { onOpenDetail({ name: deviceName, icon: deviceImage(device.platform, device.model), platform: device.platform, createdAt: device.createdAt, lastSeenAt: device.lastSeenAt, statusLabel: device.isCurrent ? t('settings.devices_this_device') : t('devices.status_active'), statusColor: colors.success, }); } return ( {deviceName} {device.isCurrent ? ( {t('settings.devices_this_device')} ) : null} {isBound && !releaseActive ? ( {t('devices.bound_badge')} ) : null} {releaseActive ? ( ) : isBound ? ( ) : ( // Entfernen passiert am Gerät selbst (Cooldown), nicht aus der Liste — // Pfeil öffnet nur das Info-/Detail-Sheet. )} ); } // ─── Protected Device Row ──────────────────────────────────────────────────── function ProtectedDeviceRow({ device, onRemove, onOpenDetail, }: { device: ProtectedDevice; onRemove: (id: string) => void; onOpenDetail: (d: DeviceDetail) => void; }) { const { t } = useTranslation(); const colors = useColors(); const statusMeta: Record = { pending: { label: t('devices.status_pending'), color: '#f59e0b' }, active: { label: t('devices.status_active'), color: colors.success }, revoked: { label: t('devices.status_revoked'), color: colors.textMuted }, degraded: { label: t('plan_limit.device_degraded_title'), color: colors.error }, }; const meta = statusMeta[device.status] ?? { label: device.status, color: colors.textMuted, }; function openDetail() { onOpenDetail({ name: device.label, icon: deviceImage(device.platform), platform: device.platform, createdAt: device.createdAt, lastSeenAt: null, statusLabel: meta.label, statusColor: meta.color, }); } return ( {device.label} {/* Kein Entfernen aus der Liste — Pfeil öffnet nur das Detail-Sheet. */} ); } // ─── Section Card wrapper ───────────────────────────────────────────────────── function SectionCard({ children }: { children: React.ReactNode }) { const colors = useColors(); return ( {children} ); } function SectionLabel({ title }: { title: string }) { const colors = useColors(); return ( {title} ); } // ─── Screen ────────────────────────────────────────────────────────────────── export default function DevicesScreen() { const insets = useSafeAreaInsets(); const { t } = useTranslation(); const colors = useColors(); const { plan } = useUserPlan(); const isLegend = plan === 'legend'; const { devices: mobileDevices, loading: mobileLoading, loadDevices, removeDevice: removeMobileDevice, requestRelease, cancelRelease, } = useDevicesStore(); const { devices: protectedDevices, loading: protectedLoading, load: loadProtected, remove: removeProtected, } = useProtectedDevicesStore(); const [magicVisible, setMagicVisible] = useState(false); const [magicPlatform, setMagicPlatform] = useState<'mac' | 'windows'>('mac'); const [detailDevice, setDetailDevice] = useState(null); function openMagic(platform: 'mac' | 'windows') { setMagicPlatform(platform); setMagicVisible(true); } useEffect(() => { loadDevices(); loadProtected(); }, []); useProtectedDevicesRealtime(); useUserDevicesRealtime(); // Geräte-Matrix (Mirror von backend plan-features): // Pro = 1 mobil + 1 stationär, Legend = 3 mobil + 2 stationär ("lückenlos auf 5") const mobileLimit = isLegend ? 3 : 1; const desktopLimit = isLegend ? 2 : 1; // Dedupe: wenn ein UserDevice (mobile/desktop) auf der gleichen Plattform // (mac/ios/android/win) bereits existiert, blende die entsprechende // ProtectedDevice-Row aus \u2014 sonst erscheint der MacBook doppelt // (1x als UserDevice via Magic, 1x als ProtectedDevice via altem DNS-Flow). const normalizePlatform = (p: string | null | undefined): string => { const n = (p ?? '').toLowerCase(); if (n.startsWith('mac') || n === 'darwin') return 'mac'; if (n.startsWith('ios') || n.startsWith('iphone') || n.startsWith('ipad')) return 'ios'; if (n.startsWith('android')) return 'android'; if (n.startsWith('win')) return 'win'; return n; }; const mobilePlatformKeys = new Set( mobileDevices.map((d) => normalizePlatform(d.platform || d.model || '')), ); const dedupedProtected = protectedDevices.filter( (d) => !mobilePlatformKeys.has(normalizePlatform(d.platform)), ); // Mobile vs Desktop getrennt zählen: UserDevices mit mac/win-Plattform sind // Magic-Desktops, der Rest (ios/android) zählt auf die Mobile-Slots. const isDesktopPlatform = (d: { platform?: string | null; model?: string | null }) => { const key = normalizePlatform(d.platform || d.model || ''); return key === 'mac' || key === 'win'; }; const mobileCount = mobileDevices.filter((d) => !isDesktopPlatform(d)).length; const desktopCount = mobileDevices.filter(isDesktopPlatform).length + dedupedProtected.filter((d) => d.status !== 'revoked').length; const atDesktopLimit = desktopCount >= desktopLimit; // Mobile zuerst (current oben), danach Desktop/Protected. const sortedMobile = [...mobileDevices].sort((a, b) => { if (a.isCurrent && !b.isCurrent) return -1; if (!a.isCurrent && b.isCurrent) return 1; return new Date(b.lastSeenAt).getTime() - new Date(a.lastSeenAt).getTime(); }); const isLoading = mobileLoading || protectedLoading; const isEmpty = !isLoading && sortedMobile.length === 0 && dedupedProtected.length === 0; const subtitle = isLegend ? t('devices.subtitle_legend') : t('devices.subtitle_pro'); async function handleRemoveProtected(id: string) { try { const { manualRemovalRequired } = await removeProtected(id); if (manualRemovalRequired) { Alert.alert(t('devices.remove_warning_title'), t('devices.remove_warning_body')); } } catch { Alert.alert(t('common.error'), t('common.unknown_error')); } } return ( {/* Subtitle + progress */} {subtitle} = mobileLimit} label={t('devices.progress_mobile')} /> {/* Unified devices section: Mobile zuerst, dann Desktop */} {isLoading && isEmpty ? ( ) : isEmpty ? ( {t('settings.devices_empty')} ) : ( <> {sortedMobile.map((device, i) => { const isLast = i === sortedMobile.length - 1 && dedupedProtected.length === 0; return ( ); })} {dedupedProtected.map((device, i) => ( ))} )} {/* CTA — Desktop-Gerät hinzufügen (Pro: 1 Slot, Legend: 2 Slots) */} {atDesktopLimit ? ( isLegend ? (