import SwiftUI #if canImport(UIKit) import UIKit #endif struct TwoFactorAuthView: View { @State private var isTwoFactorEnabled = false @State private var showEnableConfirmation = false @State private var showDisableConfirmation = false @State private var secretKey: String = TwoFactorAuthView.generateSecret() @State private var verificationCode: String = "" @State private var backupCodes: [String] = [] @State private var activeAlert: TwoFactorAlert? @FocusState private var isCodeFieldFocused: Bool var body: some View { List { Section(header: Text(NSLocalizedString("Статус защиты", comment: "Раздел состояния 2FA"))) { Toggle(isOn: Binding( get: { isTwoFactorEnabled }, set: { handleToggleChange($0) } )) { Label(NSLocalizedString("Включить 2FA", comment: "Тоггл активации 2FA"), systemImage: "lock.shield") } } if isTwoFactorEnabled { Section(header: Text(NSLocalizedString("Настройка приложения", comment: "Раздел инструкций подключения"))) { Text(NSLocalizedString("Добавьте новый аккаунт в приложении аутентификации и введите следующий ключ:", comment: "Инструкция по добавлению ключа 2FA")) .font(.callout) keyRow } Section(header: Text(NSLocalizedString("Проверочный код", comment: "Раздел верификации 2FA"))) { VStack(alignment: .leading, spacing: 12) { TextField(NSLocalizedString("Введите код из приложения", comment: "Поле ввода кода 2FA"), text: $verificationCode) .keyboardType(.numberPad) .focused($isCodeFieldFocused) .onChange(of: verificationCode) { newValue in verificationCode = newValue.trimmingCharacters(in: .whitespacesAndNewlines) } Button(action: verifyCode) { Text(NSLocalizedString("Подтвердить", comment: "Кнопка подтверждения кода 2FA")) .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .disabled(verificationCode.isEmpty) } .padding(.vertical, 4) } Section(header: Text(NSLocalizedString("Коды восстановления", comment: "Раздел кодов восстановления 2FA"))) { if backupCodes.isEmpty { Text(NSLocalizedString("Сгенерируйте резервные коды и сохраните их в надежном месте.", comment: "Подсказка о необходимости генерации кодов")) .font(.callout) .foregroundColor(.secondary) } else { ForEach(backupCodes, id: \.self) { code in HStack { Text(code) .font(.system(.body, design: .monospaced)) Spacer() Button(action: { copyToPasteboard(code) }) { Image(systemName: "doc.on.doc") } .buttonStyle(.plain) .accessibilityLabel(NSLocalizedString("Скопировать код", comment: "Кнопка копирования кода восстановления")) } } } Button(action: generateBackupCodes) { Label(NSLocalizedString("Создать новые коды", comment: "Кнопка генерации резервных кодов"), systemImage: "arrow.clockwise") } } Section(footer: Text(NSLocalizedString("Вы всегда можете отключить двухфакторную защиту, но мы рекомендуем оставлять её включённой для безопасности.", comment: "Рекомендация оставить 2FA включенной"))) { EmptyView() } } } .listStyle(.insetGrouped) .navigationTitle(NSLocalizedString("Двухфакторная аутентификация", comment: "Заголовок экрана 2FA")) .navigationBarTitleDisplayMode(.inline) .alert(item: $activeAlert) { alert in Alert( title: Text(alert.title), message: Text(alert.message), dismissButton: .default(Text(NSLocalizedString("OK", comment: "Общий текст кнопки OK"))) ) } .confirmationDialog( NSLocalizedString("Включить двухфакторную аутентификацию?", comment: "Заголовок подтверждения включения 2FA"), isPresented: $showEnableConfirmation, titleVisibility: .visible ) { Button(NSLocalizedString("Включить", comment: "Кнопка подтверждения включения 2FA"), role: .destructive) { enableTwoFactor() } Button(NSLocalizedString("Отмена", comment: "Общий текст кнопки отмены"), role: .cancel) {} } .confirmationDialog( NSLocalizedString("Отключить двухфакторную аутентификацию?", comment: "Заголовок подтверждения отключения 2FA"), isPresented: $showDisableConfirmation, titleVisibility: .visible ) { Button(NSLocalizedString("Отключить", comment: "Кнопка подтверждения отключения 2FA"), role: .destructive) { disableTwoFactor() } Button(NSLocalizedString("Отмена", comment: "Общий текст кнопки отмены"), role: .cancel) {} } } } private extension TwoFactorAuthView { var keyRow: some View { HStack(alignment: .center, spacing: 12) { Text(secretKey) .font(.system(.body, design: .monospaced)) .textSelection(.enabled) Spacer() Button(action: { copyToPasteboard(secretKey) }) { Image(systemName: "doc.on.doc") } .buttonStyle(.plain) .accessibilityLabel(NSLocalizedString("Скопировать ключ", comment: "Кнопка копирования секретного ключа")) } .padding(8) .background(Color(UIColor.secondarySystemBackground)) .cornerRadius(10) } func handleToggleChange(_ newValue: Bool) { if newValue { showEnableConfirmation = true } else { showDisableConfirmation = true } } func enableTwoFactor() { isTwoFactorEnabled = true showEnableConfirmation = false secretKey = Self.generateSecret() verificationCode = "" generateBackupCodes() activeAlert = TwoFactorAlert( title: NSLocalizedString("2FA включена", comment: "Заголовок уведомления об успешной активации 2FA"), message: NSLocalizedString("Сохраните секретный ключ и введите код из приложения, чтобы завершить настройку.", comment: "Сообщение после активации 2FA") ) DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { isCodeFieldFocused = true } } func disableTwoFactor() { isTwoFactorEnabled = false showDisableConfirmation = false verificationCode = "" backupCodes.removeAll() activeAlert = TwoFactorAlert( title: NSLocalizedString("2FA отключена", comment: "Заголовок уведомления об отключении 2FA"), message: NSLocalizedString("Вы можете включить защиту снова в любой момент.", comment: "Сообщение после отключения 2FA") ) } func verifyCode() { let normalized = verificationCode.trimmingCharacters(in: .whitespacesAndNewlines) guard normalized.count == 6, normalized.allSatisfy(\.isNumber) else { activeAlert = TwoFactorAlert( title: NSLocalizedString("Неверный код", comment: "Заголовок ошибки неправильного кода 2FA"), message: NSLocalizedString("Проверьте цифры и попробуйте снова.", comment: "Описание ошибки неверного кода 2FA") ) return } verificationCode = "" activeAlert = TwoFactorAlert( title: NSLocalizedString("Код принят", comment: "Заголовок успешного подтверждения кода 2FA"), message: NSLocalizedString("Двухфакторная аутентификация настроена.", comment: "Сообщение после успешного подтверждения кода 2FA") ) } func generateBackupCodes() { backupCodes = Self.generateBackupCodes() } func copyToPasteboard(_ value: String) { #if canImport(UIKit) UIPasteboard.general.string = value #endif activeAlert = TwoFactorAlert( title: NSLocalizedString("Скопировано", comment: "Заголовок уведомления о копировании"), message: NSLocalizedString("Значение сохранено в буфере обмена.", comment: "Сообщение после копирования") ) } static func generateSecret() -> String { let alphabet = Array("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567") return String((0..<16).compactMap { _ in alphabet.randomElement() }) } static func generateBackupCodes(count: Int = 8) -> [String] { let alphabet = Array("ABCDEFGHJKLMNPQRSTUVWXYZ23456789") return (0..