import { useCallback, useEffect, useMemo, useState } from "react" import { bilderVorladen } from "./screens/bilder" import { App as CapacitorApp } from "@capacitor/app" import { DataMetricApi, zugangLesen } from "./api" import { DatenAnbieter, useDaten } from "./daten/DatenKontext" import { geteiltenBelegLesen, geteiltenBelegSetzen } from "./daten/geteilterBeleg" import { startklarMelden } from "./daten/ota" import { Shell } from "./Shell" import type { Route, SeitenName } from "./navigation" import { useTabBeschriftung, useTheme } from "./theme" import { Bestaetigungsblatt } from "./screens/bestaetigung" import { Einrichtung } from "./screens/Einrichtung" import { Hinweisleiste } from "./screens/Hinweisleiste" import { Versionshinweis } from "./screens/Versionshinweis" import { SeiteFuer } from "./screens/register" import { zugangMerken } from "./screens/zugang" import "./stile/rahmen.css" import "./stile/screens.css" type Startzustand = "pruefen" | "einrichten" | "bereit" /** * Wurzel der App. Entscheidet zuerst, ob Zugangsdaten vorliegen — ohne sie * gibt es nichts anzuzeigen außer der Ersteinrichtung. Danach hängt alles * unter dem Datenkontext. */ export function App() { const [theme] = useTheme() const [start, setzeStart] = useState("pruefen") // Genau eine Instanz für die Lebensdauer der App: sie hält die // WebSocket-Verbindung und die Warteschlange. const api = useMemo(() => new DataMetricApi(), []) useEffect(() => { void zugangLesen().then(async (zugang) => { // Adresse für Bild-URLs merken, bevor der erste Screen rendert. if (zugang) await zugangMerken() setzeStart(zugang ? "bereit" : "einrichten") }) }, []) return (
{/* Einmal fuer die ganze App: die Rueckfrage vor loeschenden Aktionen (siehe screens/bestaetigung.tsx). Rendert nichts, solange nichts gefragt ist. */} {start === "pruefen" &&
Einen Moment …
} {start === "einrichten" && setzeStart("bereit")} />} {start === "bereit" && ( setzeStart("einrichten")} /> )}
) } function AngemeldeteApp({ beiAbmeldung }: { beiAbmeldung: () => void }) { const [route, setzeRoute] = useState({ name: "home" }) const [tabBeschriftung, setzeTabBeschriftung] = useTabBeschriftung() const daten = useDaten() const geheZu = useCallback((name: SeitenName, id?: string) => { setzeRoute(id === undefined ? { name } : { name, id }) document.querySelector(".dm-inhalt")?.scrollTo({ top: 0 }) }, []) // Ein abgelaufener oder zurückgezogener Token führt zurück in die // Ersteinrichtung statt in eine endlose Fehlerschleife. useEffect(() => { if (daten.verbindung === "nicht_angemeldet") beiAbmeldung() }, [daten.verbindung, beiAbmeldung]) // Meldet dem OTA-Plugin, dass diese Fassung hochgekommen ist (siehe // daten/ota.ts). Bewusst hier und nicht in main.tsx: erst wenn diese // Komponente rendert, hat React tatsächlich etwas auf den Schirm gebracht. // Ein Absturz vor diesem Punkt bleibt unbestätigt, und das Plugin rollt // nach appReadyTimeout von selbst auf das vorherige Bündel zurück. useEffect(() => { void startklarMelden() }, []) /* Fahrzeugfotos einmal vorab holen und dekodieren, sobald Daten da sind - siehe bilderVorladen(). Damit erscheint beim Umschalten der Seiten sofort das Bild statt kurz des Platzhalters. */ useEffect(() => { if (daten.verbindung === "verbunden") bilderVorladen() }, [daten.verbindung]) /* Belege aus dem Teilen-Blatt. Die Erweiterung legt das PDF ab und oeffnet die App - beim ersten Start kommt die App dadurch frisch hoch, bei einer schon laufenden App nur nach vorn. Deshalb beide Faelle: einmal beim Aufbau, und danach bei jeder Rueckkehr in den Vordergrund. Der Beleg wandert in den geteilten Speicher und die App springt auf Tanken - dort nimmt ihn Tanken.tsx auf und laedt ihn hoch, ueber genau denselben Weg wie eine von Hand gewaehlte Datei. */ useEffect(() => { let abgemeldet = false /* Der Beleg kommt im Aufruf mit: die Teilen-Erweiterung schreibt ihn in den Container der App-Gruppe und öffnet die App mit dem Pfad (ShareViewController.swift). Zwei Wege hierher, weil iOS beide benutzt: war die App zu, steht der Aufruf in getLaunchUrl(); lief sie schon, kommt er als appUrlOpen. Bis zum 02.09.2026 stand hier "resume" plus ein Griff in die UserDefaults - der konnte nie etwas finden, die Begründung steht in geteilterBeleg.ts. */ const holen = async (url: string | null | undefined) => { const datei = await geteiltenBelegLesen(url) if (abgemeldet || !datei) return geteiltenBelegSetzen(datei) geheZu("fuel") } void CapacitorApp.getLaunchUrl().then((start) => holen(start?.url)) const horcher = CapacitorApp.addListener("appUrlOpen", (ereignis) => { void holen(ereignis.url) }) return () => { abgemeldet = true void horcher.then((h) => h.remove()) } }, [geheZu]) return ( } > {/* Erscheint von selbst, sobald der erste Datenabruf eine neuere Fassung meldet - siehe screens/Versionshinweis.tsx. */} ) }