Compare commits

...

3 Commits

Author SHA1 Message Date
52cf7e3b1c patch terms flag 2025-10-23 19:22:40 +03:00
85fb780c96 patch terms 2025-10-23 19:20:04 +03:00
a28402136d patch terms 2025-10-23 19:12:21 +03:00
4 changed files with 138 additions and 110 deletions

View File

@ -18,11 +18,7 @@ class LoginViewModel: ObservableObject {
@Published var isLoggedIn: Bool = false @Published var isLoggedIn: Bool = false
@Published var socketState: SocketService.ConnectionState @Published var socketState: SocketService.ConnectionState
@Published var chatLoadingState: ChatLoadingState = .idle @Published var chatLoadingState: ChatLoadingState = .idle
@Published var hasAcceptedTerms: Bool { @Published var hasAcceptedTerms: Bool = false
didSet {
UserDefaults.standard.set(hasAcceptedTerms, forKey: DefaultsKeys.termsAccepted)
}
}
@Published var isLoadingTerms: Bool = false @Published var isLoadingTerms: Bool = false
@Published var termsContent: String = "" @Published var termsContent: String = ""
@Published var termsErrorMessage: String? @Published var termsErrorMessage: String?
@ -39,12 +35,10 @@ class LoginViewModel: ObservableObject {
private enum DefaultsKeys { private enum DefaultsKeys {
static let currentUser = "currentUser" static let currentUser = "currentUser"
static let userId = "userId" static let userId = "userId"
static let termsAccepted = "termsAccepted"
} }
init() { init() {
socketState = socketService.currentConnectionState socketState = socketService.currentConnectionState
hasAcceptedTerms = UserDefaults.standard.bool(forKey: DefaultsKeys.termsAccepted)
observeSocketState() observeSocketState()
observeChatsReload() observeChatsReload()
// loadStoredUser() // loadStoredUser()

View File

@ -16,6 +16,7 @@ struct LoginView: View {
@State private var isShowingRegistration = false @State private var isShowingRegistration = false
@State private var showLegacySupportNotice = false @State private var showLegacySupportNotice = false
@State private var isShowingTerms = false @State private var isShowingTerms = false
@State private var hasResetTermsOnAppear = false
@FocusState private var focusedField: Field? @FocusState private var focusedField: Field?
private enum Field: Hashable { private enum Field: Hashable {
@ -112,7 +113,7 @@ struct LoginView: View {
.font(.caption) .font(.caption)
} }
TermsAgreementView( TermsAgreementCard(
isAccepted: $viewModel.hasAcceptedTerms, isAccepted: $viewModel.hasAcceptedTerms,
openTerms: { openTerms: {
viewModel.loadTermsIfNeeded() viewModel.loadTermsIfNeeded()
@ -147,6 +148,7 @@ struct LoginView: View {
// Кнопка регистрации // Кнопка регистрации
Button(action: { Button(action: {
isShowingRegistration = true isShowingRegistration = true
viewModel.hasAcceptedTerms = false
}) { }) {
Text(NSLocalizedString("Нет аккаунта? Регистрация", comment: "Регистрация")) Text(NSLocalizedString("Нет аккаунта? Регистрация", comment: "Регистрация"))
.foregroundColor(.blue) .foregroundColor(.blue)
@ -168,6 +170,10 @@ struct LoginView: View {
) )
} }
.onAppear { .onAppear {
if !hasResetTermsOnAppear {
viewModel.hasAcceptedTerms = false
hasResetTermsOnAppear = true
}
if shouldShowLegacySupportNotice { if shouldShowLegacySupportNotice {
showLegacySupportNotice = true showLegacySupportNotice = true
} }
@ -272,106 +278,6 @@ 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 { private var selectedThemeOption: ThemeOption {
ThemeOption.option(for: themeManager.theme) ThemeOption.option(for: themeManager.theme)
} }
@ -404,7 +310,6 @@ struct LoginView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let viewModel = LoginViewModel() let viewModel = LoginViewModel()
viewModel.isLoading = false // чтобы убрать спиннер viewModel.isLoading = false // чтобы убрать спиннер
viewModel.hasAcceptedTerms = true
return LoginView(viewModel: viewModel) return LoginView(viewModel: viewModel)
} }
} }

View File

@ -20,6 +20,7 @@ struct RegistrationView: View {
@State private var isLoading: Bool = false @State private var isLoading: Bool = false
@State private var showError: Bool = false @State private var showError: Bool = false
@State private var errorMessage: String = "" @State private var errorMessage: String = ""
@State private var isShowingTerms: Bool = false
@FocusState private var focusedField: Field? @FocusState private var focusedField: Field?
@ -44,7 +45,7 @@ struct RegistrationView: View {
} }
private var isFormValid: Bool { private var isFormValid: Bool {
isUsernameValid && isPasswordValid && isConfirmPasswordValid isUsernameValid && isPasswordValid && isConfirmPasswordValid && viewModel.hasAcceptedTerms
} }
var body: some View { var body: some View {
@ -146,6 +147,14 @@ struct RegistrationView: View {
.focused($focusedField, equals: .invite) .focused($focusedField, equals: .invite)
} }
TermsAgreementCard(
isAccepted: $viewModel.hasAcceptedTerms,
openTerms: {
viewModel.loadTermsIfNeeded()
isShowingTerms = true
}
)
Button(action: registerUser) { Button(action: registerUser) {
if isLoading { if isLoading {
ProgressView() ProgressView()
@ -184,6 +193,23 @@ struct RegistrationView: View {
) )
} }
} }
.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 func registerUser() { private func registerUser() {
@ -202,6 +228,7 @@ struct RegistrationView: View {
private func dismissSheet() { private func dismissSheet() {
focusedField = nil focusedField = nil
viewModel.hasAcceptedTerms = false
isPresented = false isPresented = false
presentationMode.wrappedValue.dismiss() presentationMode.wrappedValue.dismiss()
} }

View File

@ -0,0 +1,102 @@
import SwiftUI
struct TermsAgreementCard: 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)
}
}
.buttonStyle(.plain)
.font(.footnote)
.foregroundColor(.blue)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(16)
.background(Color(.secondarySystemBackground))
.cornerRadius(14)
}
}
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())
}
}