import ManaCore import Observation import SwiftUI /// Account-Sheet: Account vollständig löschen. /// /// **App-Store-Guideline 5.1.1(v):** jede App mit Account-Erstellung /// MUSS eine Account-Löschung anbieten, die nicht über das Web läuft. /// Dieser View deckt das Pflicht-Surface ab. /// /// **Server-Limitation (v0.1.0):** funktioniert erst nach Phase-3- /// Server-PR (`mana-auth` braucht Bearer-Plugin). Die UI ist fertig, /// der Wire ist fertig. /// /// **UX:** zweistufig — User muss ein Bestätigungs-Wort tippen /// (zusätzlich zur Passwort-Eingabe), bevor der destruktive Button /// klickbar wird. Verhindert Fehlklicks auf einem Setting-Screen. public struct ManaDeleteAccountView: View { @Environment(\.manaBrand) private var brand @State private var model: DeleteAccountViewModel private let onDone: () -> Void public init(auth: AuthClient, onDone: @escaping () -> Void) { _model = State(initialValue: DeleteAccountViewModel(auth: auth)) self.onDone = onDone } public var body: some View { switch model.status { case .done: doneView default: formView } } @ViewBuilder private var formView: some View { ManaAuthScaffold(showsHeader: false) { VStack(spacing: 16) { Image(systemName: "exclamationmark.triangle.fill") .font(.system(size: 48, weight: .medium)) .foregroundStyle(brand.error) .frame(maxWidth: .infinity) Text("Account löschen") .font(.title2) .fontWeight(.semibold) .foregroundStyle(brand.foreground) .frame(maxWidth: .infinity, alignment: .leading) Text( "Das ist endgültig. Alle deine Daten werden auf allen Servern gelöscht — " + "Decks, Notizen, Aufnahmen, Verläufe, alles. Kein Restore möglich." ) .font(.subheadline) .foregroundStyle(brand.mutedForeground) Text("Tippe **LÖSCHEN** zur Bestätigung:") .font(.subheadline) .foregroundStyle(brand.foreground) .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 4) ManaTextField("LÖSCHEN", text: $model.confirmationText) .autocorrectionDisabled() #if os(iOS) .textInputAutocapitalization(.characters) #endif ManaSecureField( "Passwort", text: $model.password, textContentType: .password ) ManaPrimaryButton( "Account endgültig löschen", role: .destructive, isLoading: model.isSubmitting, isEnabled: model.canSubmit ) { Task { await model.submit() } } if case let .error(message) = model.status { Text(message) .font(.footnote) .foregroundStyle(brand.error) .multilineTextAlignment(.center) .padding(.top, 4) } } .padding(.top, 16) Button("Abbrechen", action: onDone) .font(.subheadline) .foregroundStyle(brand.mutedForeground) .padding(.top, 12) } } @ViewBuilder private var doneView: some View { ManaAuthScaffold(showsHeader: false) { VStack(spacing: 16) { Image(systemName: "trash.fill") .font(.system(size: 56, weight: .light)) .foregroundStyle(brand.mutedForeground) Text("Account gelöscht") .font(.title2) .fontWeight(.semibold) .foregroundStyle(brand.foreground) Text("Schade dass du gehst. Auf Wiedersehen.") .font(.subheadline) .foregroundStyle(brand.mutedForeground) .multilineTextAlignment(.center) ManaPrimaryButton("Schließen") { onDone() } .padding(.top, 16) } } } } @MainActor @Observable final class DeleteAccountViewModel { enum Status: Equatable { case idle case submitting case done case error(String) } var confirmationText: String = "" var password: String = "" private(set) var status: Status = .idle private let auth: AuthClient init(auth: AuthClient) { self.auth = auth } var canSubmit: Bool { guard confirmationText.uppercased() == "LÖSCHEN" else { return false } guard !password.isEmpty else { return false } if case .submitting = status { return false } return true } var isSubmitting: Bool { if case .submitting = status { return true } return false } func submit() async { guard canSubmit else { return } status = .submitting do { try await auth.deleteAccount(password: password) password = "" confirmationText = "" status = .done } catch let error as AuthError { status = .error(error.errorDescription ?? "Löschen fehlgeschlagen") } catch { status = .error(String(describing: error)) } } }