import SwiftUI /// Den zweiten Faktor einrichten. /// /// Bis Stufe 5 verwies die App dafür in den Browser. Drei Schritte: /// Geheimnis anlegen, in die Authenticator-App übernehmen, mit einem Code /// scharfschalten. /// /// **Abschalten geht hier nicht**, und das ist Absicht: Es verlangt das /// Kontopasswort, und die App speichert keines. Wer es abschalten will, /// macht das am Rechner – ein Vollzugang auf diesem Gerät soll den Schutz /// des Kontos nicht aufheben können. struct ZweiterFaktorAnsicht: View { @Environment(AppZustand.self) private var zustand @State private var lader: Lader? @State private var vorgang = Speichervorgang() @State private var einrichtung: ZweiterFaktorEinrichtung? @State private var code = "" @State private var rettungscodes: [String]? var body: some View { Group { if let lader { Inhalt(zustand: lader.zustand, nochmal: { Task { await lader.lade() } }) { stand in inhalt(stand) } } else { ProgressView() } } .navigationTitle(String(localized: "zweifaktor.titel")) .navigationBarTitleDisplayMode(.inline) .task { guard lader == nil, let client = zustand.client else { return } let neuer = Lader(schluessel: "auth/2fa/status") { try await client.zweiterFaktorStand() } lader = neuer await neuer.lade() } .sheet(item: Binding(get: { rettungscodes.map(Codeliste.init) }, set: { if $0 == nil { rettungscodes = nil } })) { liste in RettungscodeBlatt(codes: liste.codes) { rettungscodes = nil Task { await lader?.aktualisiere() } } } } /// Nur da, um die Codes an `.sheet(item:)` zu geben. private struct Codeliste: Identifiable { let codes: [String] var id: String { codes.joined() } } @ViewBuilder private func inhalt(_ stand: ZweiterFaktorStand) -> some View { Form { Section { LabeledContent(String(localized: "zweifaktor.stand")) { Text(stand.enabled ? String(localized: "zweifaktor.aktiv") : String(localized: "zweifaktor.inaktiv")) .foregroundStyle(stand.enabled ? .green : .secondary) } if stand.enabled { LabeledContent(String(localized: "zweifaktor.rettungscodes"), value: "\(stand.recoveryCodesLeft)") } } footer: { Text(stand.enabled ? String(localized: "zweifaktor.aktivErklaerung") : String(localized: "zweifaktor.inaktivErklaerung")) } if !stand.enabled { if let einrichtung { Section { // Auf dem Telefon selbst ist ein QR-Code sinnlos – die // Authenticator-App liegt daneben. Der Link führt // direkt hinein, das Geheimnis steht zum Abtippen da. Link(destination: URL(string: einrichtung.otpauthUri) ?? URL(string: "https://example.invalid")!) { Label(String(localized: "zweifaktor.inAppOeffnen"), systemImage: "arrow.up.forward.app") } LabeledContent(String(localized: "zweifaktor.geheimnis")) { Text(einrichtung.secret) .font(.footnote.monospaced()) .textSelection(.enabled) } } header: { Text(String(localized: "zweifaktor.schrittEins")) } footer: { Text(String(localized: "zweifaktor.geheimnisErklaerung")) } Section { TextField(String(localized: "zweifaktor.code"), text: $code) .keyboardType(.numberPad) .textContentType(.oneTimeCode) .monospaced() Button(String(localized: "zweifaktor.aktivieren")) { Task { await aktiviere() } } .disabled(code.count < 6 || vorgang.laeuft) } header: { Text(String(localized: "zweifaktor.schrittZwei")) } } else { Section { Button(String(localized: "zweifaktor.einrichten")) { Task { await starte() } } .disabled(vorgang.laeuft) } } } if let fehler = vorgang.fehler { Section { FehlerZeile(text: fehler) } } } } private func starte() async { guard let client = zustand.client else { return } var neu: ZweiterFaktorEinrichtung? if await vorgang.fuehreAus({ neu = try await client.richteZweitenFaktorEin() }) { einrichtung = neu } } private func aktiviere() async { guard let client = zustand.client else { return } var codes: Rettungscodes? let geglueckt = await vorgang.fuehreAus { codes = try await client.aktiviereZweitenFaktor(code: code) } if geglueckt { code = "" einrichtung = nil rettungscodes = codes?.codes } } } /// Die Rettungscodes – einmal zu sehen, danach nie wieder. /// /// Deshalb lässt sich dieses Blatt nicht wegwischen: Wer es versehentlich /// schliesst, bekommt neue nur mit dem Kontopasswort am Rechner. private struct RettungscodeBlatt: View { let codes: [String] let fertig: () -> Void @State private var abgehakt = false var body: some View { NavigationStack { List { Section { ForEach(codes, id: \.self) { code in Text(code) .font(.body.monospaced()) .textSelection(.enabled) } } header: { Text(String(localized: "zweifaktor.rettungscodes")) } footer: { Text(String(localized: "zweifaktor.rettungscodesErklaerung")) } Section { ShareLink(item: codes.joined(separator: "\n")) { Label(String(localized: "zweifaktor.codesSichern"), systemImage: "square.and.arrow.up") } Toggle(String(localized: "zweifaktor.codesNotiert"), isOn: $abgehakt) } } .navigationTitle(String(localized: "zweifaktor.aktiv")) .navigationBarTitleDisplayMode(.inline) .interactiveDismissDisabled() .toolbar { ToolbarItem(placement: .confirmationAction) { Button(String(localized: "allgemein.fertig"), action: fertig) .disabled(!abgehakt) } } } } }