import { useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, Linking, Pressable, Share, Text, TouchableOpacity, View, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import * as Clipboard from 'expo-clipboard'; import type { ColorScheme } from '../../lib/theme'; import { apiFetch } from '../../lib/api'; import { FormSheet } from '../FormSheet'; type PairResponse = { code: string; expiresAt: string; expiresInSeconds: number; }; type MagicDevice = { deviceId: string; hostname: string; model: string | null; osVersion: string | null; magicEnrolledAt: string; }; type MagicInfo = { latestVersion: string; downloadUrl: string; dmgUrl: string; minMacosVersion: string; }; /** * MagicSheet — Rebreak-Magic-Pairing-Flow als geteiltes FormSheet * (Bottom-Sheet mit Titel-Header). Wird aus settings.tsx getriggert. */ export function MagicSheet({ visible, onClose, colors, }: { visible: boolean; onClose: () => void; colors: ColorScheme; }) { const [info, setInfo] = useState(null); const [pair, setPair] = useState(null); const [pairLoading, setPairLoading] = useState(false); const [pairError, setPairError] = useState(null); const [now, setNow] = useState(Date.now()); const [devices, setDevices] = useState(null); const tickRef = useRef | null>(null); useEffect(() => { (async () => { try { const i = await apiFetch('/api/magic/info'); setInfo(i); } catch { setInfo({ latestVersion: '0.1.0', downloadUrl: 'https://staging.rebreak.org/download/rebreakmagic', dmgUrl: 'https://staging.rebreak.org/downloads/RebreakMagic-latest.dmg', minMacosVersion: '13.0', }); } loadDevices(); })(); return () => { if (tickRef.current) clearInterval(tickRef.current); }; }, []); useEffect(() => { if (pair) { if (tickRef.current) clearInterval(tickRef.current); tickRef.current = setInterval(() => setNow(Date.now()), 1000); return () => { if (tickRef.current) clearInterval(tickRef.current); }; } }, [pair]); async function loadDevices() { try { const d = await apiFetch('/api/magic/devices'); setDevices(d); } catch { setDevices([]); } } async function handleGenerateCode() { setPairLoading(true); setPairError(null); try { const res = await apiFetch('/api/magic/pair/create', { method: 'POST', body: {}, }); setPair(res); setNow(Date.now()); } catch (e: any) { setPairError(e?.message ?? 'Fehler beim Generieren'); } finally { setPairLoading(false); } } async function handleCopyCode() { if (!pair) return; await Clipboard.setStringAsync(pair.code); } const remaining = useMemo(() => { if (!pair) return 0; const exp = new Date(pair.expiresAt).getTime(); return Math.max(0, Math.floor((exp - now) / 1000)); }, [pair, now]); const codeExpired = pair !== null && remaining <= 0; return ( {/* Sub-Header (Tagline) */} iPhone in 30 Sek. binden — ohne Werks-Reset. {/* Step 1 — Download */} Auf deinem Mac öffnen (min. macOS {info?.minMacosVersion ?? '13.0'}). info && Linking.openURL(info.downloadUrl)} /> info && Share.share({ message: info.downloadUrl })} style={{ marginTop: 10, alignSelf: 'flex-start' }} > Link an meinen Mac senden {/* Step 2 — Pairing-Code */} {!pair || codeExpired ? ( <> Erzeuge einen 6-stelligen Code und gib ihn in der Mac-App ein. Gültig 10 Minuten, nur einmal verwendbar. {pairError && ( {pairError} )} ) : ( <> In Mac-App eingeben: {pair.code.split('').map((d, i) => ( {d} ))} Läuft ab in {formatRemaining(remaining)} Kopieren { setPair(null); setPairError(null); }} style={{ marginTop: 14, alignSelf: 'center' }} > Code verwerfen )} {/* Verbundene Ger\u00e4te */} {devices === null ? ( ) : devices.length === 0 ? ( Noch keine Macs verbunden. Sobald du einen Pairing-Code einlöst und ein iPhone bindest, erscheint es hier. ) : ( devices.map((d, i) => ( {d.hostname} {d.model && ( {d.model} )} )) )} ); } // ─── Helpers ─────────────────────────────────────────────────────────────── function cardStyle(colors: ColorScheme) { return { backgroundColor: colors.card, borderRadius: 14, padding: 16, marginBottom: 16, } as const; } function SectionTitle({ text, colors }: { text: string; colors: ColorScheme }) { return ( {text} ); } function PrimaryButton({ icon, label, onPress, loading, }: { icon: React.ComponentProps['name']; label: string; onPress: () => void; loading?: boolean; }) { return ( {loading ? ( ) : ( )} {label} ); } function formatRemaining(seconds: number): string { const m = Math.floor(seconds / 60); const s = seconds % 60; return `${m}:${String(s).padStart(2, '0')}`; }