add terms
This commit is contained in:
parent
726a6983b2
commit
140e82e122
@ -257,6 +257,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Включено" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Включить автоудаление аккаунта" : {
|
"Включить автоудаление аккаунта" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -331,6 +334,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Выключено" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Где найти сохранённые черновики?" : {
|
"Где найти сохранённые черновики?" : {
|
||||||
"comment" : "FAQ question: drafts"
|
"comment" : "FAQ question: drafts"
|
||||||
@ -913,6 +919,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Не удалось загрузить текст правил." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Не удалось загрузить чаты." : {
|
"Не удалось загрузить чаты." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -1243,6 +1252,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Открыть правила" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Отображаемое имя" : {
|
"Отображаемое имя" : {
|
||||||
|
|
||||||
@ -1623,6 +1635,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Правила сервиса" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Предложите, что добавить" : {
|
"Предложите, что добавить" : {
|
||||||
"comment" : "feedback category subtitle: idea",
|
"comment" : "feedback category subtitle: idea",
|
||||||
@ -2031,6 +2046,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Согласиться с правилами" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Сообщение" : {
|
"Сообщение" : {
|
||||||
|
|
||||||
@ -2298,6 +2316,9 @@
|
|||||||
},
|
},
|
||||||
"Экспериментальная поддержка iOS 15" : {
|
"Экспериментальная поддержка iOS 15" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Я ознакомился и принимаю правила сервиса" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Язык" : {
|
"Язык" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
@ -18,6 +18,14 @@ 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 {
|
||||||
|
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 authService = AuthService()
|
||||||
private let socketService = SocketService.shared
|
private let socketService = SocketService.shared
|
||||||
@ -31,10 +39,12 @@ 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()
|
||||||
@ -169,4 +179,53 @@ class LoginViewModel: ObservableObject {
|
|||||||
|
|
||||||
if AppConfig.DEBUG{ print("username: \(username) | userId: \(userId)")}
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,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
|
||||||
@FocusState private var focusedField: Field?
|
@FocusState private var focusedField: Field?
|
||||||
|
|
||||||
private enum Field: Hashable {
|
private enum Field: Hashable {
|
||||||
@ -30,6 +31,10 @@ struct LoginView: View {
|
|||||||
private var isPasswordValid: Bool {
|
private var isPasswordValid: Bool {
|
||||||
return viewModel.password.count >= 8 && viewModel.password.count <= 128
|
return viewModel.password.count >= 8 && viewModel.password.count <= 128
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var isLoginButtonEnabled: Bool {
|
||||||
|
!viewModel.isLoading && isUsernameValid && isPasswordValid && viewModel.hasAcceptedTerms
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
@ -99,7 +104,7 @@ struct LoginView: View {
|
|||||||
viewModel.password = String(newValue.prefix(32))
|
viewModel.password = String(newValue.prefix(32))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Показываем ошибку для пароля
|
// Показываем ошибку для пароля
|
||||||
if !isPasswordValid && !viewModel.password.isEmpty {
|
if !isPasswordValid && !viewModel.password.isEmpty {
|
||||||
Text(NSLocalizedString("Неверный пароль", comment: "Неверный пароль"))
|
Text(NSLocalizedString("Неверный пароль", comment: "Неверный пароль"))
|
||||||
@ -107,10 +112,15 @@ struct LoginView: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
}
|
}
|
||||||
|
|
||||||
var isButtonEnabled: Bool {
|
TermsAgreementView(
|
||||||
!viewModel.isLoading && isUsernameValid && isPasswordValid
|
isAccepted: $viewModel.hasAcceptedTerms,
|
||||||
}
|
openTerms: {
|
||||||
|
viewModel.loadTermsIfNeeded()
|
||||||
|
isShowingTerms = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
viewModel.login()
|
viewModel.login()
|
||||||
}) {
|
}) {
|
||||||
@ -126,11 +136,11 @@ struct LoginView: View {
|
|||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.padding()
|
.padding()
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.background(isButtonEnabled ? Color.blue : Color.gray)
|
.background(isLoginButtonEnabled ? Color.blue : Color.gray)
|
||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabled(!isButtonEnabled)
|
.disabled(!isLoginButtonEnabled)
|
||||||
|
|
||||||
// Spacer()
|
// Spacer()
|
||||||
|
|
||||||
@ -171,6 +181,23 @@ struct LoginView: View {
|
|||||||
.zIndex(1)
|
.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 {
|
private var themeIconName: String {
|
||||||
switch themeManager.theme {
|
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 {
|
private var selectedThemeOption: ThemeOption {
|
||||||
ThemeOption.option(for: themeManager.theme)
|
ThemeOption.option(for: themeManager.theme)
|
||||||
}
|
}
|
||||||
@ -277,6 +404,7 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user