Neuer Ordner ios/ mit dem Geruest: Netzwerkschicht, Modelle, Offline-Ablage, Texte in Deutsch und Englisch, Navigation, der komplette Anmeldeweg (Server waehlen, anmelden, zweiter Faktor, Pflicht-Passwortwechsel) und als erste Fachansicht die Kontenliste. Die Anmeldung laeuft einmal ueber das Sitzungs-Cookie, holt sich dabei eine App-Marke und legt sie in den Schluesselbund. Danach traegt nur noch der Header. Kein Passwort auf dem Geraet, kein Ablauf nach 30 Tagen. Der Code ist nie uebersetzt worden - hier gibt es kein Xcode. Geprueft ist dafuer jedes Modellfeld gegen echte Antworten des laufenden Backends; dabei kam heraus, dass created_at ohne Zeitzone und mit Mikrosekunden kommt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
80 lines
3.0 KiB
Swift
80 lines
3.0 KiB
Swift
import SwiftUI
|
||
|
||
/// Benutzername und Passwort – einmal. Danach trägt die Gerätemarke, und
|
||
/// das Passwort wird nirgends gespeichert.
|
||
struct AnmeldeAnsicht: View {
|
||
@Environment(AppZustand.self) private var zustand
|
||
@State private var benutzer = Ablage.benutzername ?? ""
|
||
@State private var passwort = ""
|
||
@FocusState private var feld: Feld?
|
||
|
||
private enum Feld { case benutzer, passwort }
|
||
|
||
var body: some View {
|
||
NavigationStack {
|
||
Form {
|
||
Section {
|
||
TextField(String(localized: "anmeldung.benutzer"), text: $benutzer)
|
||
.textContentType(.username)
|
||
.textInputAutocapitalization(.never)
|
||
.autocorrectionDisabled()
|
||
.focused($feld, equals: .benutzer)
|
||
.submitLabel(.next)
|
||
.onSubmit { feld = .passwort }
|
||
|
||
SecureField(String(localized: "anmeldung.passwort"), text: $passwort)
|
||
.textContentType(.password)
|
||
.focused($feld, equals: .passwort)
|
||
.submitLabel(.go)
|
||
.onSubmit { melde() }
|
||
} footer: {
|
||
Text(String(localized: "anmeldung.erklaerung"))
|
||
}
|
||
|
||
if let meldung = zustand.meldung {
|
||
Section {
|
||
Label(meldung, systemImage: "exclamationmark.triangle")
|
||
.foregroundStyle(.red)
|
||
}
|
||
}
|
||
|
||
Section {
|
||
Button(action: melde) {
|
||
if zustand.beschaeftigt {
|
||
ProgressView()
|
||
} else {
|
||
Text(String(localized: "anmeldung.anmelden"))
|
||
}
|
||
}
|
||
.disabled(bereit == false || zustand.beschaeftigt)
|
||
}
|
||
|
||
Section {
|
||
Button(String(localized: "anmeldung.andererServer"), role: .destructive) {
|
||
Task { await zustand.meldeAb(adresseBehalten: false) }
|
||
}
|
||
} footer: {
|
||
if let adresse = zustand.serverAdresse {
|
||
Text(String(format: String(localized: "anmeldung.aktuellerServer"),
|
||
adresse.host() ?? adresse.absoluteString))
|
||
}
|
||
}
|
||
}
|
||
.navigationTitle(String(localized: "anmeldung.titel"))
|
||
.onAppear { feld = benutzer.isEmpty ? .benutzer : .passwort }
|
||
}
|
||
}
|
||
|
||
private var bereit: Bool {
|
||
!benutzer.trimmingCharacters(in: .whitespaces).isEmpty && !passwort.isEmpty
|
||
}
|
||
|
||
private func melde() {
|
||
guard bereit else { return }
|
||
let name = benutzer.trimmingCharacters(in: .whitespaces)
|
||
let geheim = passwort
|
||
passwort = ""
|
||
Task { await zustand.melde(benutzer: name, passwort: geheim) }
|
||
}
|
||
}
|