227 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			227 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
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..<count).map { _ in
 | 
						||
            String((0..<8).compactMap { _ in alphabet.randomElement() })
 | 
						||
        }
 | 
						||
    }
 | 
						||
}
 | 
						||
 | 
						||
private struct TwoFactorAlert: Identifiable {
 | 
						||
    let id = UUID()
 | 
						||
    let title: String
 | 
						||
    let message: String
 | 
						||
}
 | 
						||
 | 
						||
#if DEBUG
 | 
						||
struct TwoFactorAuthView_Previews: PreviewProvider {
 | 
						||
    static var previews: some View {
 | 
						||
        NavigationView {
 | 
						||
            TwoFactorAuthView()
 | 
						||
        }
 | 
						||
    }
 | 
						||
}
 | 
						||
#endif
 |