add terms

This commit is contained in:
cheykrym 2025-10-23 19:04:25 +03:00
parent 726a6983b2
commit 140e82e122
3 changed files with 215 additions and 7 deletions

View File

@ -257,6 +257,9 @@
}
}
}
},
"Включено" : {
},
"Включить автоудаление аккаунта" : {
"localizations" : {
@ -331,6 +334,9 @@
}
}
}
},
"Выключено" : {
},
"Где найти сохранённые черновики?" : {
"comment" : "FAQ question: drafts"
@ -913,6 +919,9 @@
}
}
}
},
"Не удалось загрузить текст правил." : {
},
"Не удалось загрузить чаты." : {
"localizations" : {
@ -1243,6 +1252,9 @@
}
}
}
},
"Открыть правила" : {
},
"Отображаемое имя" : {
@ -1623,6 +1635,9 @@
}
}
}
},
"Правила сервиса" : {
},
"Предложите, что добавить" : {
"comment" : "feedback category subtitle: idea",
@ -2031,6 +2046,9 @@
}
}
}
},
"Согласиться с правилами" : {
},
"Сообщение" : {
@ -2298,6 +2316,9 @@
},
"Экспериментальная поддержка iOS 15" : {
},
"Я ознакомился и принимаю правила сервиса" : {
},
"Язык" : {
"localizations" : {

View File

@ -18,6 +18,14 @@ class LoginViewModel: ObservableObject {
@Published var isLoggedIn: Bool = false
@Published var socketState: SocketService.ConnectionState
@Published var chatLoadingState: ChatLoadingState = .idle
@Published var hasAcceptedTerms: Bool {
didSet {
UserDefaults.standard.set(hasAcceptedTerms, forKey: DefaultsKeys.termsAccepted)
}
}
@Published var isLoadingTerms: Bool = false
@Published var termsContent: String = ""
@Published var termsErrorMessage: String?
private let authService = AuthService()
private let socketService = SocketService.shared
@ -31,10 +39,12 @@ class LoginViewModel: ObservableObject {
private enum DefaultsKeys {
static let currentUser = "currentUser"
static let userId = "userId"
static let termsAccepted = "termsAccepted"
}
init() {
socketState = socketService.currentConnectionState
hasAcceptedTerms = UserDefaults.standard.bool(forKey: DefaultsKeys.termsAccepted)
observeSocketState()
observeChatsReload()
// loadStoredUser()
@ -169,4 +179,53 @@ class LoginViewModel: ObservableObject {
if AppConfig.DEBUG{ print("username: \(username) | userId: \(userId)")}
}
func loadTermsIfNeeded() {
guard !isLoadingTerms else { return }
if !termsContent.isEmpty {
termsErrorMessage = nil
return
}
isLoadingTerms = true
termsErrorMessage = nil
NetworkClient.shared.request(
path: "/legal/terms",
headers: ["Accept": "text/plain"],
requiresAuth: false,
callbackQueue: .main
) { [weak self] result in
guard let self else { return }
self.isLoadingTerms = false
switch result {
case .success(let response):
if let content = String(data: response.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),
!content.isEmpty {
self.termsContent = content
return
}
if let jsonObject = try? JSONSerialization.jsonObject(with: response.data, options: []),
let json = jsonObject as? [String: Any],
let content = (json["content"] as? String) ?? (json["text"] as? String),
!content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
self.termsContent = content
} else {
self.termsErrorMessage = NSLocalizedString("Не удалось загрузить текст правил.", comment: "")
}
case .failure:
self.termsErrorMessage = NSLocalizedString("Не удалось загрузить текст правил.", comment: "")
}
}
}
func reloadTerms() {
termsContent = ""
termsErrorMessage = nil
loadTermsIfNeeded()
}
}

View File

@ -15,6 +15,7 @@ struct LoginView: View {
@State private var isShowingRegistration = false
@State private var showLegacySupportNotice = false
@State private var isShowingTerms = false
@FocusState private var focusedField: Field?
private enum Field: Hashable {
@ -30,6 +31,10 @@ struct LoginView: View {
private var isPasswordValid: Bool {
return viewModel.password.count >= 8 && viewModel.password.count <= 128
}
private var isLoginButtonEnabled: Bool {
!viewModel.isLoading && isUsernameValid && isPasswordValid && viewModel.hasAcceptedTerms
}
var body: some View {
@ -99,7 +104,7 @@ struct LoginView: View {
viewModel.password = String(newValue.prefix(32))
}
}
// Показываем ошибку для пароля
if !isPasswordValid && !viewModel.password.isEmpty {
Text(NSLocalizedString("Неверный пароль", comment: "Неверный пароль"))
@ -107,10 +112,15 @@ struct LoginView: View {
.font(.caption)
}
var isButtonEnabled: Bool {
!viewModel.isLoading && isUsernameValid && isPasswordValid
}
TermsAgreementView(
isAccepted: $viewModel.hasAcceptedTerms,
openTerms: {
viewModel.loadTermsIfNeeded()
isShowingTerms = true
}
)
.padding(.vertical, 12)
Button(action: {
viewModel.login()
}) {
@ -126,11 +136,11 @@ struct LoginView: View {
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(isButtonEnabled ? Color.blue : Color.gray)
.background(isLoginButtonEnabled ? Color.blue : Color.gray)
.cornerRadius(8)
}
}
.disabled(!isButtonEnabled)
.disabled(!isLoginButtonEnabled)
// Spacer()
@ -171,6 +181,23 @@ struct LoginView: View {
.zIndex(1)
}
}
.fullScreenCover(isPresented: $isShowingTerms) {
TermsFullScreenView(
isPresented: $isShowingTerms,
title: NSLocalizedString("Правила сервиса", comment: ""),
content: viewModel.termsContent,
isLoading: viewModel.isLoadingTerms,
errorMessage: viewModel.termsErrorMessage,
onRetry: {
viewModel.reloadTerms()
}
)
.onAppear {
if viewModel.termsContent.isEmpty {
viewModel.loadTermsIfNeeded()
}
}
}
}
private var themeIconName: String {
switch themeManager.theme {
@ -245,6 +272,106 @@ struct LoginView: View {
}
}
private struct TermsAgreementView: View {
@Binding var isAccepted: Bool
var openTerms: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(alignment: .top, spacing: 12) {
Button {
isAccepted.toggle()
} label: {
Image(systemName: isAccepted ? "checkmark.square.fill" : "square")
.font(.system(size: 24, weight: .semibold))
.foregroundColor(isAccepted ? .blue : .secondary)
}
.buttonStyle(.plain)
.accessibilityLabel(NSLocalizedString("Согласиться с правилами", comment: ""))
.accessibilityValue(isAccepted ? NSLocalizedString("Включено", comment: "") : NSLocalizedString("Выключено", comment: ""))
VStack(alignment: .leading, spacing: 6) {
Text(NSLocalizedString("Я ознакомился и принимаю правила сервиса", comment: ""))
.font(.subheadline)
.foregroundColor(.primary)
Button(action: openTerms) {
HStack(spacing: 4) {
Text(NSLocalizedString("Открыть правила", comment: ""))
Image(systemName: "arrow.up.right")
.font(.caption)
}
}
.font(.footnote)
.foregroundColor(.blue)
.buttonStyle(.plain)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(16)
.background(Color(.secondarySystemBackground))
.cornerRadius(14)
}
}
private struct TermsFullScreenView: View {
@Binding var isPresented: Bool
var title: String
var content: String
var isLoading: Bool
var errorMessage: String?
var onRetry: () -> Void
var body: some View {
NavigationView {
Group {
if isLoading {
ProgressView()
} else if let errorMessage {
VStack(spacing: 16) {
Text(errorMessage)
.multilineTextAlignment(.center)
.padding(.horizontal)
Button(action: onRetry) {
Text(NSLocalizedString("Повторить", comment: ""))
.padding(.horizontal, 24)
.padding(.vertical, 12)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(12)
}
}
} else {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
if let attributed = try? AttributedString(markdown: content) {
Text(attributed)
} else {
Text(content)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemBackground))
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(action: { isPresented = false }) {
Text(NSLocalizedString("Закрыть", comment: ""))
}
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
private var selectedThemeOption: ThemeOption {
ThemeOption.option(for: themeManager.theme)
}
@ -277,6 +404,7 @@ struct LoginView_Previews: PreviewProvider {
static var previews: some View {
let viewModel = LoginViewModel()
viewModel.isLoading = false // чтобы убрать спиннер
viewModel.hasAcceptedTerms = true
return LoginView(viewModel: viewModel)
}
}