// // RegistrationView.swift // VolnahubApp // // Created by cheykrym on 09/06/2025. // import SwiftUI struct RegistrationView: View { @ObservedObject var viewModel: LoginViewModel @Binding var isPresented: Bool @Environment(\.presentationMode) private var presentationMode @State private var username: String = "" @State private var password: String = "" @State private var confirmPassword: String = "" @State private var inviteCode: String = "" @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? private enum Field: Hashable { case username case password case confirmPassword case invite } private var isUsernameValid: Bool { let pattern = "^[A-Za-z0-9_]{3,32}$" return username.range(of: pattern, options: .regularExpression) != nil } private var isPasswordValid: Bool { password.count >= 8 && password.count <= 128 } private var isConfirmPasswordValid: Bool { confirmPassword == password && !confirmPassword.isEmpty } private var isFormValid: Bool { isUsernameValid && isPasswordValid && isConfirmPasswordValid && viewModel.hasAcceptedTerms } var body: some View { NavigationView { ScrollView { ZStack(alignment: .top) { Color.clear .contentShape(Rectangle()) .onTapGesture { focusedField = nil } VStack(alignment: .leading, spacing: 16) { Group { HStack { TextField(NSLocalizedString("Логин", comment: "Логин"), text: $username) .autocapitalization(.none) .disableAutocorrection(true) .focused($focusedField, equals: .username) Spacer() if !username.isEmpty { Image(systemName: isUsernameValid ? "checkmark.circle" : "xmark.circle") .foregroundColor(isUsernameValid ? .green : .red) } } .padding() .background(Color(.secondarySystemBackground)) .cornerRadius(8) .autocapitalization(.none) .disableAutocorrection(true) .onChange(of: username) { newValue in if newValue.count > 32 { username = String(newValue.prefix(32)) } } if !isUsernameValid && !username.isEmpty { Text(NSLocalizedString("Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)", comment: "Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)")) .foregroundColor(.red) .font(.caption) } HStack { SecureField(NSLocalizedString("Пароль", comment: "Пароль"), text: $password) .autocapitalization(.none) .focused($focusedField, equals: .password) Spacer() if !password.isEmpty { Image(systemName: isPasswordValid ? "checkmark.circle" : "xmark.circle") .foregroundColor(isPasswordValid ? .green : .red) } } .padding() .background(Color(.secondarySystemBackground)) .cornerRadius(8) .autocapitalization(.none) .onChange(of: password) { newValue in if newValue.count > 128 { password = String(newValue.prefix(128)) } } if !isPasswordValid && !password.isEmpty { Text(NSLocalizedString("Пароль должен быть от 8 до 128 символов", comment: "Пароль должен быть от 6 до 32 символов")) .foregroundColor(.red) .font(.caption) } HStack { SecureField(NSLocalizedString("Подтверждение пароля", comment: "Подтверждение пароля"), text: $confirmPassword) .autocapitalization(.none) .focused($focusedField, equals: .confirmPassword) Spacer() if !confirmPassword.isEmpty { Image(systemName: isConfirmPasswordValid ? "checkmark.circle" : "xmark.circle") .foregroundColor(isConfirmPasswordValid ? .green : .red) } } .padding() .background(Color(.secondarySystemBackground)) .cornerRadius(8) .autocapitalization(.none) .onChange(of: confirmPassword) { newValue in if newValue.count > 32 { confirmPassword = String(newValue.prefix(32)) } } if !isConfirmPasswordValid && !confirmPassword.isEmpty { Text(NSLocalizedString("Пароли не совпадают", comment: "Пароли не совпадают")) .foregroundColor(.red) .font(.caption) } TextField(NSLocalizedString("Инвайт-код (необязательно)", comment: "Инвайт-код"), text: $inviteCode) .padding() .background(Color(.secondarySystemBackground)) .cornerRadius(8) .autocapitalization(.none) .disableAutocorrection(true) .focused($focusedField, equals: .invite) } TermsAgreementCard( isAccepted: $viewModel.hasAcceptedTerms, openTerms: { viewModel.loadTermsIfNeeded() isShowingTerms = true } ) Button(action: registerUser) { if isLoading { ProgressView() .padding() .frame(maxWidth: .infinity) .background(Color.gray.opacity(0.6)) .cornerRadius(8) } else { Text(NSLocalizedString("Зарегистрироваться", comment: "Зарегистрироваться")) .foregroundColor(.white) .padding() .frame(maxWidth: .infinity) .background(isFormValid ? Color.blue : Color.gray.opacity(0.6)) .cornerRadius(8) } } .disabled(!isFormValid) .padding(.bottom) } .padding() } } .navigationTitle(Text(NSLocalizedString("Регистрация", comment: "Регистрация"))) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button(action: dismissSheet) { Text(NSLocalizedString("Закрыть", comment: "Закрыть")) } } } .alert(isPresented: $showError) { Alert( title: Text(NSLocalizedString("Ошибка регистрация", comment: "Ошибка")), message: Text(errorMessage), dismissButton: .default(Text(NSLocalizedString("OK", comment: ""))) ) } } .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() { isLoading = true errorMessage = "" viewModel.registerUser(username: username, password: password, invite: inviteCode.isEmpty ? nil : inviteCode) { success, message in isLoading = false if success { dismissSheet() } else { errorMessage = message ?? NSLocalizedString("Неизвестная ошибка.", comment: "") showError = true } } } private func dismissSheet() { focusedField = nil viewModel.hasAcceptedTerms = false isPresented = false presentationMode.wrappedValue.dismiss() } } struct RegistrationView_Previews: PreviewProvider { static var previews: some View { let viewModel = LoginViewModel() viewModel.isLoading = false // чтобы убрать спиннер return RegistrationView(viewModel: viewModel, isPresented: .constant(true)) } }