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
|