461151cfec
Gemeldet als "Verbindung trennen tut nichts". Kein Fehler der App: in eingebetteten Ansichten sind Dialoge abgeschaltet, window.confirm kehrt sofort mit false zurueck (gemessen: 1 ms), und abmelden() beginnt damit. Die Suche danach brachte den eigentlichen Befund: elf Stellen hingen an window.confirm - acht direkt, drei im Zeilenmenue (Wischgeste in Fahrten, Tankvorgaengen, Messwerten). Jede davon eine loeschende Aktion, jede in eingebetteten Ansichten still wirkungslos. Das Panel zeigt an denselben Stellen seit jeher sein eigenes Blatt. screens/bestaetigung.tsx: ein kleiner Speicher plus ein ActionSheet, das einmal in App.tsx haengt. Kein Hook je Aufrufstelle - der haette jede der elf Stellen gezwungen, das Blatt auch selbst zu rendern, elf Gelegenheiten es zu vergessen, ohne dass etwas auffaellt. Ohne gehaengtes Blatt (Testlauf, Fremdeinbettung) gilt weiter window.confirm. LOESCH_TEXT und LOESCH_HINWEIS wortgleich aus dem Panel, samt der Trennung in Frage, erklaerenden Satz und Knopfbeschriftung. 225 Tests (vier neue), live an der gemeldeten Stelle geprueft: das Blatt erscheint, Abbrechen schliesst es, die Sitzung bleibt. Bestaetigt wurde bewusst nicht - das haette den Token vom Geraet geloescht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
169 lines
6.2 KiB
TypeScript
169 lines
6.2 KiB
TypeScript
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<Startzustand>("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 (
|
|
<div className="ads-root dm-wurzel" data-theme={theme}>
|
|
{/* Einmal fuer die ganze App: die Rueckfrage vor loeschenden Aktionen
|
|
(siehe screens/bestaetigung.tsx). Rendert nichts, solange nichts
|
|
gefragt ist. */}
|
|
<Bestaetigungsblatt />
|
|
{start === "pruefen" && <div className="dm-laden">Einen Moment …</div>}
|
|
{start === "einrichten" && <Einrichtung fertig={() => setzeStart("bereit")} />}
|
|
{start === "bereit" && (
|
|
<DatenAnbieter api={api}>
|
|
<AngemeldeteApp beiAbmeldung={() => setzeStart("einrichten")} />
|
|
</DatenAnbieter>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function AngemeldeteApp({ beiAbmeldung }: { beiAbmeldung: () => void }) {
|
|
const [route, setzeRoute] = useState<Route>({ 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 (
|
|
<Shell
|
|
route={route}
|
|
geheZu={geheZu}
|
|
tabBeschriftung={tabBeschriftung}
|
|
statusStand={daten.statusStand}
|
|
// Ziehen zum Aktualisieren gibt es im Panel nur auf der Übersicht.
|
|
beiAktualisieren={route.name === "home" ? daten.jetztAktualisieren : undefined}
|
|
hinweis={
|
|
<Hinweisleiste
|
|
verbindung={daten.verbindung}
|
|
warteschlange={daten.warteschlange}
|
|
versionsstand={daten.versionsstand}
|
|
/>
|
|
}
|
|
>
|
|
<SeiteFuer
|
|
route={route}
|
|
geheZu={geheZu}
|
|
tabBeschriftung={tabBeschriftung}
|
|
setzeTabBeschriftung={setzeTabBeschriftung}
|
|
beiAbmeldung={beiAbmeldung}
|
|
/>
|
|
{/* Erscheint von selbst, sobald der erste Datenabruf eine neuere
|
|
Fassung meldet - siehe screens/Versionshinweis.tsx. */}
|
|
<Versionshinweis
|
|
versionsstand={daten.versionsstand}
|
|
serverVersion={daten.serverVersion}
|
|
otaBuendel={daten.otaBuendel}
|
|
/>
|
|
</Shell>
|
|
)
|
|
}
|