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 socketState: SocketService.ConnectionState
@Published var chatLoadingState: ChatLoadingState = .idle
@Published var hasAcceptedTerms: Bool {
didSet {
UserDefaults.standard.set(hasAcceptedTerms, forKey: DefaultsKeys.termsAccepted)
}
}
@Published var hasAcceptedTerms: Bool = false
@Published var isLoadingTerms: Bool = false
@Published var termsContent: String = ""
@Published var termsErrorMessage: String?
@ -39,12 +35,10 @@ 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()

View File

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

View File

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