add 2fa
This commit is contained in:
parent
58c841b5c7
commit
6eed966fc9
@ -61,6 +61,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"2FA включена" : {
|
||||
"comment" : "Заголовок уведомления об успешной активации 2FA"
|
||||
},
|
||||
"2FA отключена" : {
|
||||
"comment" : "Заголовок уведомления об отключении 2FA"
|
||||
},
|
||||
"Companion ID" : {
|
||||
"comment" : "Search placeholder companion title"
|
||||
},
|
||||
@ -247,6 +253,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Введите код из приложения" : {
|
||||
"comment" : "Поле ввода кода 2FA"
|
||||
},
|
||||
"Веб" : {
|
||||
"comment" : "Тип сессии — веб"
|
||||
},
|
||||
@ -285,6 +294,12 @@
|
||||
},
|
||||
"Включено" : {
|
||||
|
||||
},
|
||||
"Включить" : {
|
||||
"comment" : "Кнопка подтверждения включения 2FA"
|
||||
},
|
||||
"Включить 2FA" : {
|
||||
"comment" : "Тоггл активации 2FA"
|
||||
},
|
||||
"Включить автоудаление аккаунта" : {
|
||||
"localizations" : {
|
||||
@ -296,6 +311,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Включить двухфакторную аутентификацию?" : {
|
||||
"comment" : "Заголовок подтверждения включения 2FA"
|
||||
},
|
||||
"Вложение" : {
|
||||
|
||||
},
|
||||
@ -345,12 +363,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Вы всегда можете отключить двухфакторную защиту, но мы рекомендуем оставлять её включённой для безопасности." : {
|
||||
"comment" : "Рекомендация оставить 2FA включенной"
|
||||
},
|
||||
"Вы выйдете из выбранной сессии." : {
|
||||
"comment" : "Описание подтверждения завершения конкретной сессии"
|
||||
},
|
||||
"Вы выйдете со всех устройств, кроме текущего." : {
|
||||
"comment" : "Описание подтверждения завершения сессий"
|
||||
},
|
||||
"Вы можете включить защиту снова в любой момент." : {
|
||||
"comment" : "Сообщение после отключения 2FA"
|
||||
},
|
||||
"Выберите оценку — это поможет нам понять настроение." : {
|
||||
"comment" : "feedback: rating hint",
|
||||
"localizations" : {
|
||||
@ -403,6 +427,7 @@
|
||||
}
|
||||
},
|
||||
"Двухфакторная аутентификация" : {
|
||||
"comment" : "Заголовок экрана 2FA",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@ -412,6 +437,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Двухфакторная аутентификация настроена." : {
|
||||
"comment" : "Сообщение после успешного подтверждения кода 2FA"
|
||||
},
|
||||
"Десктоп" : {
|
||||
"comment" : "Тип сессии — десктоп"
|
||||
},
|
||||
@ -432,6 +460,9 @@
|
||||
"Добавьте контакты, чтобы быстрее выходить на связь." : {
|
||||
"comment" : "Contacts empty state subtitle"
|
||||
},
|
||||
"Добавьте новый аккаунт в приложении аутентификации и введите следующий ключ:" : {
|
||||
"comment" : "Инструкция по добавлению ключа 2FA"
|
||||
},
|
||||
"Другие устройства (%d)" : {
|
||||
"comment" : "Заголовок секции других устройств с количеством"
|
||||
},
|
||||
@ -474,9 +505,6 @@
|
||||
},
|
||||
"Заглушка: Push-уведомления" : {
|
||||
|
||||
},
|
||||
"Заглушка: Двухфакторная аутентификация" : {
|
||||
|
||||
},
|
||||
"Заглушка: Другие настройки" : {
|
||||
"extractionState" : "stale",
|
||||
@ -583,6 +611,9 @@
|
||||
"Здесь появится информация о собеседнике и существующих чатах." : {
|
||||
"comment" : "Search placeholder description"
|
||||
},
|
||||
"Значение сохранено в буфере обмена." : {
|
||||
"comment" : "Сообщение после копирования"
|
||||
},
|
||||
"Идея" : {
|
||||
"comment" : "feedback category: idea",
|
||||
"localizations" : {
|
||||
@ -663,6 +694,12 @@
|
||||
"Код дружбы" : {
|
||||
"comment" : "Friend code badge"
|
||||
},
|
||||
"Код принят" : {
|
||||
"comment" : "Заголовок успешного подтверждения кода 2FA"
|
||||
},
|
||||
"Коды восстановления" : {
|
||||
"comment" : "Раздел кодов восстановления 2FA"
|
||||
},
|
||||
"Контактов пока нет" : {
|
||||
"comment" : "Contacts empty state title"
|
||||
},
|
||||
@ -816,6 +853,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Мессенджер-режим сейчас проработан примерно на 50%." : {
|
||||
|
||||
},
|
||||
"Мессенджер-режим сейчас проработан примерно на 60%." : {
|
||||
|
||||
@ -958,6 +998,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Настройка приложения" : {
|
||||
"comment" : "Раздел инструкций подключения"
|
||||
},
|
||||
"Настройки" : {
|
||||
"comment" : "Settings",
|
||||
"localizations" : {
|
||||
@ -1112,6 +1155,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Неверный код" : {
|
||||
"comment" : "Заголовок ошибки неправильного кода 2FA"
|
||||
},
|
||||
"Неверный код приглашения." : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -1363,6 +1409,12 @@
|
||||
},
|
||||
"Основной режим находится в ранней разработке (около 10%)." : {
|
||||
|
||||
},
|
||||
"Отключить" : {
|
||||
"comment" : "Кнопка подтверждения отключения 2FA"
|
||||
},
|
||||
"Отключить двухфакторную аутентификацию?" : {
|
||||
"comment" : "Заголовок подтверждения отключения 2FA"
|
||||
},
|
||||
"Открыть правила" : {
|
||||
|
||||
@ -1630,6 +1682,9 @@
|
||||
},
|
||||
"Подключение" : {
|
||||
|
||||
},
|
||||
"Подтвердить" : {
|
||||
"comment" : "Кнопка подтверждения кода 2FA"
|
||||
},
|
||||
"Подтверждение пароля" : {
|
||||
"comment" : "Подтверждение пароля",
|
||||
@ -1885,6 +1940,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Проверочный код" : {
|
||||
"comment" : "Раздел верификации 2FA"
|
||||
},
|
||||
"Проверьте данные и повторите попытку." : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -1895,6 +1953,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Проверьте цифры и попробуйте снова." : {
|
||||
"comment" : "Описание ошибки неверного кода 2FA"
|
||||
},
|
||||
"Произошла неизвестная ошибка." : {
|
||||
"comment" : "Search unknown error"
|
||||
},
|
||||
@ -2092,6 +2153,9 @@
|
||||
"Связаться с разработчиками" : {
|
||||
"comment" : "FAQ: contact developers link"
|
||||
},
|
||||
"Сгенерируйте резервные коды и сохраните их в надежном месте." : {
|
||||
"comment" : "Подсказка о необходимости генерации кодов"
|
||||
},
|
||||
"Сервер вернул ошибку (%d)." : {
|
||||
"comment" : "Search error server status"
|
||||
},
|
||||
@ -2140,9 +2204,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Скопировано" : {
|
||||
"comment" : "Заголовок уведомления о копировании"
|
||||
},
|
||||
"Скопировать" : {
|
||||
"comment" : "Search placeholder copy"
|
||||
},
|
||||
"Скопировать ключ" : {
|
||||
"comment" : "Кнопка копирования секретного ключа"
|
||||
},
|
||||
"Скопировать код" : {
|
||||
"comment" : "Кнопка копирования кода восстановления"
|
||||
},
|
||||
"Скоро" : {
|
||||
"comment" : "Add blocked user placeholder title\nContacts placeholder title"
|
||||
},
|
||||
@ -2187,6 +2260,9 @@
|
||||
},
|
||||
"Согласиться с правилами" : {
|
||||
|
||||
},
|
||||
"Создать новые коды" : {
|
||||
"comment" : "Кнопка генерации резервных кодов"
|
||||
},
|
||||
"Сообщение" : {
|
||||
|
||||
@ -2205,6 +2281,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Сохраните секретный ключ и введите код из приложения, чтобы завершить настройку." : {
|
||||
"comment" : "Сообщение после активации 2FA"
|
||||
},
|
||||
"Сохранить изменения" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -2258,6 +2337,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Статус защиты" : {
|
||||
"comment" : "Раздел состояния 2FA"
|
||||
},
|
||||
"Стикеры" : {
|
||||
|
||||
},
|
||||
|
||||
@ -22,6 +22,7 @@ class LoginViewModel: ObservableObject {
|
||||
@Published var isLoadingTerms: Bool = false
|
||||
@Published var termsContent: String = ""
|
||||
@Published var termsErrorMessage: String?
|
||||
@Published var onboardingDestination: OnboardingDestination?
|
||||
|
||||
private let authService = AuthService()
|
||||
private let socketService = SocketService.shared
|
||||
@ -32,6 +33,10 @@ class LoginViewModel: ObservableObject {
|
||||
case loading
|
||||
}
|
||||
|
||||
enum OnboardingDestination: Equatable {
|
||||
case twoFactor
|
||||
}
|
||||
|
||||
private enum DefaultsKeys {
|
||||
static let currentUser = "currentUser"
|
||||
static let userId = "userId"
|
||||
@ -127,6 +132,7 @@ class LoginViewModel: ObservableObject {
|
||||
self?.isLoggedIn = true // 👈 переключаем на главный экран после автологина
|
||||
self?.loadStoredUser()
|
||||
self?.socketService.connectForCurrentUser()
|
||||
self?.onboardingDestination = .twoFactor
|
||||
} else {
|
||||
self?.socketService.disconnect()
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ struct MainView: View {
|
||||
@State private var isQrPresented = false
|
||||
@State private var deepLinkChatItem: PrivateChatListItem?
|
||||
@State private var isDeepLinkChatActive = false
|
||||
@State private var hasTriggeredTwoFactorOnboarding = false
|
||||
|
||||
private var tabTitle: String {
|
||||
switch selectedTab {
|
||||
@ -172,9 +173,14 @@ struct MainView: View {
|
||||
}
|
||||
.onAppear {
|
||||
enforceTabSelectionForMessengerMode()
|
||||
handleTwoFactorOnboardingIfNeeded()
|
||||
}
|
||||
.onChange(of: isMessengerModeEnabled) { _ in
|
||||
enforceTabSelectionForMessengerMode()
|
||||
handleTwoFactorOnboardingIfNeeded()
|
||||
}
|
||||
.onChange(of: viewModel.onboardingDestination) { _ in
|
||||
handleTwoFactorOnboardingIfNeeded()
|
||||
}
|
||||
.onChange(of: messageCenter.pendingNavigation?.id) { _ in
|
||||
guard !AppConfig.PRESENT_CHAT_AS_SHEET,
|
||||
@ -217,6 +223,29 @@ private extension MainView {
|
||||
}
|
||||
}
|
||||
|
||||
func handleTwoFactorOnboardingIfNeeded() {
|
||||
guard viewModel.onboardingDestination == .twoFactor else {
|
||||
hasTriggeredTwoFactorOnboarding = false
|
||||
return
|
||||
}
|
||||
|
||||
guard !hasTriggeredTwoFactorOnboarding else { return }
|
||||
hasTriggeredTwoFactorOnboarding = true
|
||||
|
||||
if isMessengerModeEnabled {
|
||||
if selectedTab != 5 {
|
||||
selectedTab = 5
|
||||
}
|
||||
} else {
|
||||
if selectedTab != 3 {
|
||||
selectedTab = 3
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
isSettingsPresented = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var deepLinkNavigationLink: some View {
|
||||
NavigationLink(
|
||||
destination: deepLinkChatDestination,
|
||||
|
||||
@ -4,6 +4,7 @@ struct SettingsView: View {
|
||||
@ObservedObject var viewModel: LoginViewModel
|
||||
@EnvironmentObject private var themeManager: ThemeManager
|
||||
@State private var isThemeExpanded = false
|
||||
@State private var isTwoFactorActive = false
|
||||
private let themeOptions = ThemeOption.ordered
|
||||
|
||||
private var selectedThemeOption: ThemeOption {
|
||||
@ -32,8 +33,10 @@ struct SettingsView: View {
|
||||
NavigationLink(destination: ChangePasswordView()) {
|
||||
Label(NSLocalizedString("Сменить пароль", comment: ""), systemImage: "key")
|
||||
}
|
||||
NavigationLink(destination: Text("Заглушка: Двухфакторная аутентификация")) {
|
||||
Label("Двухфакторная аутентификация", systemImage: "lock.shield")
|
||||
NavigationLink(isActive: $isTwoFactorActive) {
|
||||
TwoFactorAuthView()
|
||||
} label: {
|
||||
Label(NSLocalizedString("Двухфакторная аутентификация", comment: ""), systemImage: "lock.shield")
|
||||
}
|
||||
NavigationLink(destination: ActiveSessionsView()) {
|
||||
Label(NSLocalizedString("Активные сессии", comment: ""), systemImage: "iphone")
|
||||
@ -125,6 +128,12 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Настройки")
|
||||
.onAppear {
|
||||
handleTwoFactorOnboardingIfNeeded()
|
||||
}
|
||||
.onChange(of: viewModel.onboardingDestination) { _ in
|
||||
handleTwoFactorOnboardingIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
private func openLanguageSettings() {
|
||||
@ -165,3 +174,11 @@ struct SettingsView: View {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension SettingsView {
|
||||
func handleTwoFactorOnboardingIfNeeded() {
|
||||
guard viewModel.onboardingDestination == .twoFactor else { return }
|
||||
isTwoFactorActive = true
|
||||
viewModel.onboardingDestination = nil
|
||||
}
|
||||
}
|
||||
|
||||
226
yobble/Views/Tab/Settings/TwoFactorAuthView.swift
Normal file
226
yobble/Views/Tab/Settings/TwoFactorAuthView.swift
Normal file
@ -0,0 +1,226 @@
|
||||
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
|
||||
Loading…
x
Reference in New Issue
Block a user