import ManaCore import SwiftUI /// Wird angezeigt, wenn nach erfolgreichem Email/PW-`signIn` der /// `AuthClient.status` auf ``AuthClient/Status/twoFactorRequired(token:methods:email:)`` /// gewechselt ist. Bietet TOTP-Code-Eingabe (6-stellig) plus einen /// Fallback auf Backup-Codes. /// /// Apps müssen das selbst nicht einbauen — ``ManaLoginView`` schaltet /// automatisch um. Nur direkt nötig wenn die App eine eigene Login- /// UI-Maschine hat (z.B. Memoros AccountView). public struct ManaTwoFactorChallengeView: View { @Environment(\.manaBrand) private var brand @State private var model: TwoFactorChallengeViewModel private let onCancel: () -> Void /// - Parameters: /// - auth: gemeinsamer `AuthClient` der App (Status muss bereits /// `.twoFactorRequired` sein). /// - onCancel: Callback wenn der User "Abbrechen" drückt. Apps /// setzen den AuthClient typischerweise auf `.signedOut` /// zurück und zeigen wieder die Login-View. public init( auth: AuthClient, onCancel: @escaping () -> Void ) { _model = State(initialValue: TwoFactorChallengeViewModel(auth: auth)) self.onCancel = onCancel } public var body: some View { ManaAuthScaffold(showsHeader: false) { VStack(spacing: 20) { Image(systemName: "lock.shield.fill") .font(.system(size: 56, weight: .light)) .foregroundStyle(brand.primary) Text(model.mode == .totp ? "Zwei-Faktor-Code" : "Backup-Code") .font(.title2) .fontWeight(.semibold) .foregroundStyle(brand.foreground) Text(promptText) .font(.subheadline) .foregroundStyle(brand.mutedForeground) .multilineTextAlignment(.center) ManaTextField(placeholderText, text: $model.code) .autocorrectionDisabled() .font(.system(.title3, design: .monospaced)) #if os(iOS) .keyboardType(model.mode == .totp ? .numberPad : .asciiCapable) .textInputAutocapitalization(model.mode == .totp ? .never : .characters) #endif ManaPrimaryButton( "Bestätigen", isLoading: model.isVerifying, isEnabled: model.canSubmit ) { Task { await model.submit() } } if case let .error(message) = model.status { Text(message) .font(.footnote) .foregroundStyle(brand.error) .multilineTextAlignment(.center) } Button(action: { model.toggleMode() }) { Text(model.mode == .totp ? "Stattdessen Backup-Code verwenden" : "Stattdessen 6-stelligen Code verwenden" ) .font(.footnote) .foregroundStyle(brand.primary) } .padding(.top, 12) Button("Abbrechen", action: onCancel) .font(.subheadline) .foregroundStyle(brand.mutedForeground) .padding(.top, 8) } } } private var promptText: String { switch model.mode { case .totp: "Öffne deine Authenticator-App und gib den 6-stelligen Code für deinen Account ein." case .backupCode: "Gib einen deiner einmal-nutzbaren Backup-Codes ein. Jeder Code lässt sich nur einmal verwenden." } } private var placeholderText: String { model.mode == .totp ? "123 456" : "xxxx-xxxx" } }