import ManaCore import Observation import SwiftUI /// Account-Sheet: Passwort ändern. Erfordert aktuelles Passwort (Re-Auth). /// /// **Server-Limitation (v0.1.0):** funktioniert erst nach Phase-3- /// Server-PR (`mana-auth` braucht Bearer-Plugin). public struct ManaChangePasswordView: View { @Environment(\.manaBrand) private var brand @State private var model: ChangePasswordViewModel private let onDone: () -> Void public init(auth: AuthClient, onDone: @escaping () -> Void) { _model = State(initialValue: ChangePasswordViewModel(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) { Text("Passwort ändern") .font(.title2) .fontWeight(.semibold) .foregroundStyle(brand.foreground) .frame(maxWidth: .infinity, alignment: .leading) ManaSecureField( "Aktuelles Passwort", text: $model.currentPassword, textContentType: .password ) ManaSecureField( "Neues Passwort", text: $model.newPassword, textContentType: .newPassword ) ManaSecureField( "Neues Passwort bestätigen", text: $model.confirmPassword, textContentType: .newPassword ) if let hint = model.validationHint { Text(hint) .font(.footnote) .foregroundStyle(brand.mutedForeground) .frame(maxWidth: .infinity, alignment: .leading) } ManaPrimaryButton( "Passwort ändern", 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: "lock.rotation") .font(.system(size: 56, weight: .light)) .foregroundStyle(brand.success) Text("Passwort geändert") .font(.title2) .fontWeight(.semibold) .foregroundStyle(brand.foreground) ManaPrimaryButton("Fertig") { onDone() } .padding(.top, 16) } } } } @MainActor @Observable final class ChangePasswordViewModel { enum Status: Equatable { case idle case submitting case done case error(String) } var currentPassword: String = "" var newPassword: String = "" var confirmPassword: String = "" private(set) var status: Status = .idle private let auth: AuthClient init(auth: AuthClient) { self.auth = auth } var canSubmit: Bool { guard !currentPassword.isEmpty, !newPassword.isEmpty, !confirmPassword.isEmpty else { return false } guard newPassword == confirmPassword else { return false } guard newPassword.count >= 8 else { return false } if case .submitting = status { return false } return true } var isSubmitting: Bool { if case .submitting = status { return true } return false } var validationHint: String? { if !newPassword.isEmpty, newPassword.count < 8 { return "Neues Passwort muss mindestens 8 Zeichen lang sein." } if !confirmPassword.isEmpty, newPassword != confirmPassword { return "Die neuen Passwörter stimmen nicht überein." } return nil } func submit() async { guard canSubmit else { return } status = .submitting do { try await auth.changePassword(currentPassword: currentPassword, newPassword: newPassword) currentPassword = "" newPassword = "" confirmPassword = "" status = .done } catch let error as AuthError { status = .error(error.errorDescription ?? "Änderung fehlgeschlagen") } catch { status = .error(String(describing: error)) } } }