add login and register
This commit is contained in:
		
							parent
							
								
									ea4952bafe
								
							
						
					
					
						commit
						8ebaecf902
					
				@ -8,22 +8,287 @@
 | 
				
			|||||||
import Foundation
 | 
					import Foundation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AuthService {
 | 
					class AuthService {
 | 
				
			||||||
    func autoLogin(completion: @escaping (Bool) -> Void) {
 | 
					 | 
				
			||||||
        // Симуляция проверки токена
 | 
					 | 
				
			||||||
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
 | 
					 | 
				
			||||||
            let success = false  // Пока всегда неуспешно, для теста
 | 
					 | 
				
			||||||
            completion(success)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    func login(username: String, password: String, completion: @escaping (Bool, String?) -> Void) {
 | 
					    func autoLogin(completion: @escaping (Bool, String?) -> Void) {
 | 
				
			||||||
        // Симуляция запроса
 | 
					        // 1️⃣ Проверяем наличие текущего пользователя
 | 
				
			||||||
        DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
 | 
					        if let currentUser = UserDefaults.standard.string(forKey: "currentUser"),
 | 
				
			||||||
            if username == "test" && password == "123123" {
 | 
					           let accessToken = KeychainService.shared.get(forKey: "access_token", service: currentUser),
 | 
				
			||||||
 | 
					           let refreshToken = KeychainService.shared.get(forKey: "refresh_token", service: currentUser) {
 | 
				
			||||||
 | 
					            if AppConfig.DEBUG{ print("AutoLogin: найден текущий пользователь — \(currentUser)")}
 | 
				
			||||||
 | 
					            completion(true, nil)
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 2️⃣ Текущий пользователь не найден или токены отсутствуют
 | 
				
			||||||
 | 
					        if AppConfig.DEBUG{ print("AutoLogin: текущий пользователь не найден или токены отсутствуют. Пробуем найти другого пользователя...")}
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        let allUsers = KeychainService.shared.getAllServices()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        for user in allUsers {
 | 
				
			||||||
 | 
					            let hasAccessToken = KeychainService.shared.get(forKey: "access_token", service: user) != nil
 | 
				
			||||||
 | 
					            let hasRefreshToken = KeychainService.shared.get(forKey: "refresh_token", service: user) != nil
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if hasAccessToken && hasRefreshToken {
 | 
				
			||||||
 | 
					                // Нашли пользователя с токенами — назначаем как currentUser
 | 
				
			||||||
 | 
					                UserDefaults.standard.set(user, forKey: "currentUser")
 | 
				
			||||||
 | 
					                if AppConfig.DEBUG{ print("AutoLogin: переключились на пользователя \(user)")}
 | 
				
			||||||
                completion(true, nil)
 | 
					                completion(true, nil)
 | 
				
			||||||
            } else {
 | 
					                return
 | 
				
			||||||
                completion(false, "Неверные учетные данные.")
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 3️⃣ Если никто не найден
 | 
				
			||||||
 | 
					//        completion(false, "Не найден авторизованный пользователь. Пожалуйста, войдите снова.")
 | 
				
			||||||
 | 
					        completion(false, nil)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    func login(username: String, password: String, completion: @escaping (Bool, String?) -> Void) {
 | 
				
			||||||
 | 
					        let url = URL(string: "\(AppConfig.API_SERVER)/auth/login")!
 | 
				
			||||||
 | 
					        var request = URLRequest(url: url)
 | 
				
			||||||
 | 
					        request.httpMethod = "POST"
 | 
				
			||||||
 | 
					        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
 | 
				
			||||||
 | 
					        request.setValue("\(AppConfig.USER_AGENT)", forHTTPHeaderField: "User-Agent")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let payload: [String: String] = [
 | 
				
			||||||
 | 
					            "login": username,
 | 
				
			||||||
 | 
					            "password": password
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        do {
 | 
				
			||||||
 | 
					            let jsonData = try JSONEncoder().encode(payload)
 | 
				
			||||||
 | 
					            request.httpBody = jsonData
 | 
				
			||||||
 | 
					        } catch {
 | 
				
			||||||
 | 
					            DispatchQueue.main.async {
 | 
				
			||||||
 | 
					                completion(false, NSLocalizedString("AuthService_error_serialization", comment: ""))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let task = URLSession.shared.dataTask(with: request) { data, response, error in
 | 
				
			||||||
 | 
					            DispatchQueue.main.async {
 | 
				
			||||||
 | 
					                if let error = error {
 | 
				
			||||||
 | 
					                    let errorMessage = String(format: NSLocalizedString("AuthService_error_network", comment: ""), error.localizedDescription)
 | 
				
			||||||
 | 
					                    completion(false, errorMessage)
 | 
				
			||||||
 | 
					                    return
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                guard let httpResponse = response as? HTTPURLResponse else {
 | 
				
			||||||
 | 
					                    completion(false, NSLocalizedString("AuthService_error_invalid_response", comment: ""))
 | 
				
			||||||
 | 
					                    return
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                guard (200...299).contains(httpResponse.statusCode) else {
 | 
				
			||||||
 | 
					                    if httpResponse.statusCode == 401{
 | 
				
			||||||
 | 
					                        completion(false, NSLocalizedString("AuthService_error_invalid_credentials", comment: ""))
 | 
				
			||||||
 | 
					                    } else if httpResponse.statusCode == 502{
 | 
				
			||||||
 | 
					                        completion(false, NSLocalizedString("AuthService_error_server_unavailable", comment: ""))
 | 
				
			||||||
 | 
					                    } else if httpResponse.statusCode == 429 {
 | 
				
			||||||
 | 
					                        completion(false, NSLocalizedString("AuthService_error_too_many_requests", comment: ""))
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        let errorMessage = String(format: NSLocalizedString("AuthService_error_server_error", comment: ""), "\(httpResponse.statusCode)")
 | 
				
			||||||
 | 
					                        completion(false, errorMessage)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    return
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                guard let data = data else {
 | 
				
			||||||
 | 
					                    completion(false, NSLocalizedString("AuthService_error_empty_response", comment: ""))
 | 
				
			||||||
 | 
					                    return
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                do {
 | 
				
			||||||
 | 
					                    let decoder = JSONDecoder()
 | 
				
			||||||
 | 
					                    let loginResponse = try decoder.decode(LoginResponse.self, from: data)
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Сохраняем токены в Keychain
 | 
				
			||||||
 | 
					                    KeychainService.shared.save(loginResponse.access_token, forKey: "access_token", service: username)
 | 
				
			||||||
 | 
					                    KeychainService.shared.save(loginResponse.refresh_token, forKey: "refresh_token", service: username)
 | 
				
			||||||
 | 
					                    UserDefaults.standard.set(username, forKey: "currentUser")
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    completion(true, nil)
 | 
				
			||||||
 | 
					                } catch {
 | 
				
			||||||
 | 
					                    completion(false, NSLocalizedString("AuthService_error_parsing_response", comment: ""))
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        task.resume()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    func register(username: String, password: String, invite: String?, completion: @escaping (Bool, String?) -> Void) {
 | 
				
			||||||
 | 
					        let url = URL(string: "\(AppConfig.API_SERVER)/auth/register")!
 | 
				
			||||||
 | 
					        var request = URLRequest(url: url)
 | 
				
			||||||
 | 
					        request.httpMethod = "POST"
 | 
				
			||||||
 | 
					        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
 | 
				
			||||||
 | 
					        request.setValue(AppConfig.USER_AGENT, forHTTPHeaderField: "User-Agent")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let payload: [String: Any] = [
 | 
				
			||||||
 | 
					            "login": username,
 | 
				
			||||||
 | 
					            "password": password,
 | 
				
			||||||
 | 
					            "invite": invite ?? NSNull()
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        do {
 | 
				
			||||||
 | 
					            let jsonData = try JSONSerialization.data(withJSONObject: payload)
 | 
				
			||||||
 | 
					            request.httpBody = jsonData
 | 
				
			||||||
 | 
					        } catch {
 | 
				
			||||||
 | 
					            DispatchQueue.main.async {
 | 
				
			||||||
 | 
					                completion(false, NSLocalizedString("AuthService_error_serialization", comment: ""))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let task = URLSession.shared.dataTask(with: request) { data, response, error in
 | 
				
			||||||
 | 
					            DispatchQueue.main.async {
 | 
				
			||||||
 | 
					                if let error = error {
 | 
				
			||||||
 | 
					                    let errorMessage = String(format: NSLocalizedString("AuthService_error_network", comment: ""), error.localizedDescription)
 | 
				
			||||||
 | 
					                    completion(false, errorMessage)
 | 
				
			||||||
 | 
					                    return
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                guard let httpResponse = response as? HTTPURLResponse else {
 | 
				
			||||||
 | 
					                    completion(false, NSLocalizedString("AuthService_error_invalid_response", comment: ""))
 | 
				
			||||||
 | 
					                    return
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                guard let data = data else {
 | 
				
			||||||
 | 
					                    completion(false, NSLocalizedString("AuthService_error_empty_response", comment: ""))
 | 
				
			||||||
 | 
					                    return
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                let decoder = JSONDecoder()
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (200...299).contains(httpResponse.statusCode) {
 | 
				
			||||||
 | 
					                    do {
 | 
				
			||||||
 | 
					                        let _ = try decoder.decode(RegisterResponse.self, from: data)
 | 
				
			||||||
 | 
					                        if AppConfig.DEBUG{ print("Регистрация успешна. Пытаемся сразу войти...")}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        // Сразу логинимся
 | 
				
			||||||
 | 
					                        self.login(username: username, password: password) { loginSuccess, loginMessage in
 | 
				
			||||||
 | 
					                            if loginSuccess {
 | 
				
			||||||
 | 
					                                completion(true, "Регистрация и вход выполнены успешно.")
 | 
				
			||||||
 | 
					                            } else {
 | 
				
			||||||
 | 
					                                // Регистрация успешна, но логин не удался — покажем сообщение
 | 
				
			||||||
 | 
					                                completion(false, loginMessage ?? NSLocalizedString("AuthService_login_success_but_failed", comment: ""))
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } catch {
 | 
				
			||||||
 | 
					                        completion(false, NSLocalizedString("AuthService_error_parsing_response", comment: ""))
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    // Ошибка сервера — пробуем распарсить message
 | 
				
			||||||
 | 
					                    if let errorResponseMessage = try? decoder.decode(ErrorResponseMessage.self, from: data),
 | 
				
			||||||
 | 
					                       let message = errorResponseMessage.message {
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        if let jsonString = String(data: data, encoding: .utf8) {
 | 
				
			||||||
 | 
					                            if AppConfig.DEBUG{ print("Raw JSON:", jsonString)}
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        if AppConfig.DEBUG{ print("message:", message)}
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        if httpResponse.statusCode == 400 {
 | 
				
			||||||
 | 
					                            if message.contains("Invalid invitation code") {
 | 
				
			||||||
 | 
					                                completion(false, NSLocalizedString("AuthService_error_invalid_invitation_code", comment: ""))
 | 
				
			||||||
 | 
					                            } else if message.contains("This invitation is not active") {
 | 
				
			||||||
 | 
					                                completion(false, NSLocalizedString("AuthService_error_invitation_not_active", comment: ""))
 | 
				
			||||||
 | 
					                            } else if message.contains("This invitation has reached its usage limit") {
 | 
				
			||||||
 | 
					                                completion(false, NSLocalizedString("AuthService_error_invitation_usage_limit", comment: ""))
 | 
				
			||||||
 | 
					                            } else if message.contains("This invitation has expired") {
 | 
				
			||||||
 | 
					                                completion(false, NSLocalizedString("AuthService_error_invitation_expired", comment: ""))
 | 
				
			||||||
 | 
					                            } else if message.contains("Login already registered") {
 | 
				
			||||||
 | 
					                                completion(false, NSLocalizedString("AuthService_error_login_already_registered", comment: ""))
 | 
				
			||||||
 | 
					                            } else {
 | 
				
			||||||
 | 
					                                completion(false, message)
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        } else if httpResponse.statusCode == 403 {
 | 
				
			||||||
 | 
					                            if message.contains("Registration is currently disabled") {
 | 
				
			||||||
 | 
					                                completion(false, NSLocalizedString("AuthService_error_registration_disabled", comment: ""))
 | 
				
			||||||
 | 
					                            } else {
 | 
				
			||||||
 | 
					                                completion(false, message)
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        } else if httpResponse.statusCode == 429 {
 | 
				
			||||||
 | 
					                            completion(false, NSLocalizedString("AuthService_error_too_many_requests", comment: ""))
 | 
				
			||||||
 | 
					                        } else if httpResponse.statusCode == 502{
 | 
				
			||||||
 | 
					                            completion(false, NSLocalizedString("AuthService_error_server_unavailable", comment: ""))
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            let errorMessage = String(format: NSLocalizedString("AuthService_error_server_error", comment: ""), "\(httpResponse.statusCode)")
 | 
				
			||||||
 | 
					                            completion(false, errorMessage)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        // Не удалось распарсить JSON — fallback
 | 
				
			||||||
 | 
					                        if httpResponse.statusCode == 400 {
 | 
				
			||||||
 | 
					                            completion(false, NSLocalizedString("AuthService_error_invalid_request", comment: ""))
 | 
				
			||||||
 | 
					                        } else if httpResponse.statusCode == 403 {
 | 
				
			||||||
 | 
					                            completion(false, NSLocalizedString("AuthService_error_registration_forbidden", comment: ""))
 | 
				
			||||||
 | 
					                        } else if httpResponse.statusCode == 429 {
 | 
				
			||||||
 | 
					                            completion(false, NSLocalizedString("AuthService_error_too_many_requests", comment: ""))
 | 
				
			||||||
 | 
					                        } else if httpResponse.statusCode == 502{
 | 
				
			||||||
 | 
					                            completion(false, NSLocalizedString("AuthService_error_server_unavailable", comment: ""))
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            let errorMessage = String(format: NSLocalizedString("AuthService_error_server_error", comment: ""), "\(httpResponse.statusCode)")
 | 
				
			||||||
 | 
					                            completion(false, errorMessage)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        task.resume()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    func logoutCurrentUser(completion: @escaping (Bool, String?) -> Void) {
 | 
				
			||||||
 | 
					        guard let currentUser = UserDefaults.standard.string(forKey: "currentUser") else {
 | 
				
			||||||
 | 
					            completion(false, "Не найден текущий пользователь.")
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Удаляем токены текущего пользователя
 | 
				
			||||||
 | 
					        KeychainService.shared.delete(forKey: "access_token", service: currentUser)
 | 
				
			||||||
 | 
					        KeychainService.shared.delete(forKey: "refresh_token", service: currentUser)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Сбрасываем текущего пользователя
 | 
				
			||||||
 | 
					        UserDefaults.standard.removeObject(forKey: "currentUser")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Пробуем переключиться на другого пользователя
 | 
				
			||||||
 | 
					        let allUsers = KeychainService.shared.getAllServices()
 | 
				
			||||||
 | 
					        for user in allUsers {
 | 
				
			||||||
 | 
					            let hasAccessToken = KeychainService.shared.get(forKey: "access_token", service: user) != nil
 | 
				
			||||||
 | 
					            let hasRefreshToken = KeychainService.shared.get(forKey: "refresh_token", service: user) != nil
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if hasAccessToken && hasRefreshToken {
 | 
				
			||||||
 | 
					                UserDefaults.standard.set(user, forKey: "currentUser")
 | 
				
			||||||
 | 
					                if AppConfig.DEBUG{ print("Logout: переключились на пользователя \(user)")}
 | 
				
			||||||
 | 
					                completion(true, nil)
 | 
				
			||||||
 | 
					                return
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Если пользователей больше нет
 | 
				
			||||||
 | 
					//        completion(false, "Нет доступных пользователей. Пожалуйста, войдите снова.")
 | 
				
			||||||
 | 
					        completion(false, nil)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct LoginResponse: Decodable {
 | 
				
			||||||
 | 
					    let status: String
 | 
				
			||||||
 | 
					    let access_token: String
 | 
				
			||||||
 | 
					    let refresh_token: String
 | 
				
			||||||
 | 
					    let token_type: String
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct TokenRefreshResponse: Decodable {
 | 
				
			||||||
 | 
					    let status: String
 | 
				
			||||||
 | 
					    let access_token: String
 | 
				
			||||||
 | 
					    let token_type: String
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct RegisterResponse: Decodable {
 | 
				
			||||||
 | 
					    let status: String
 | 
				
			||||||
 | 
					    let message: String
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct ErrorResponseMessage: Decodable {
 | 
				
			||||||
 | 
					    let status: String?
 | 
				
			||||||
 | 
					    let message: String?
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										89
									
								
								Shared/Network/refreshtokenex.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								Shared/Network/refreshtokenex.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,89 @@
 | 
				
			|||||||
 | 
					////
 | 
				
			||||||
 | 
					////  refreshtokenex.swift
 | 
				
			||||||
 | 
					////  volnahub (iOS)
 | 
				
			||||||
 | 
					////
 | 
				
			||||||
 | 
					////  Created by cheykrym on 11/06/2025.
 | 
				
			||||||
 | 
					////
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//import Foundation
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//func autoLogin(completion: @escaping (Bool, String?) -> Void) {
 | 
				
			||||||
 | 
					//    guard let username = UserDefaults.standard.string(forKey: "currentUser") else {
 | 
				
			||||||
 | 
					//        completion(false, "Не найден текущий пользователь")
 | 
				
			||||||
 | 
					//        return
 | 
				
			||||||
 | 
					//    }
 | 
				
			||||||
 | 
					//    
 | 
				
			||||||
 | 
					//    guard let accessToken = KeychainService.shared.get(forKey: "access_token", service: username),
 | 
				
			||||||
 | 
					//          let refreshToken = KeychainService.shared.get(forKey: "refresh_token", service: username) else {
 | 
				
			||||||
 | 
					//        completion(false, "Токены отсутствуют. Пожалуйста, войдите снова.")
 | 
				
			||||||
 | 
					//        return
 | 
				
			||||||
 | 
					//    }
 | 
				
			||||||
 | 
					//    
 | 
				
			||||||
 | 
					////        let url = URL(string: "\(AppConfig.API_SERVER)/auth/refresh")!
 | 
				
			||||||
 | 
					////        var request = URLRequest(url: url)
 | 
				
			||||||
 | 
					////        request.httpMethod = "POST"
 | 
				
			||||||
 | 
					////        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
 | 
				
			||||||
 | 
					//////        request.setValue("VolnahubApp", forHTTPHeaderField: "User-Agent")
 | 
				
			||||||
 | 
					////        request.setValue("VolnahubApp", forHTTPHeaderField: "\(AppConfig.USER_AGENT)")
 | 
				
			||||||
 | 
					////
 | 
				
			||||||
 | 
					////        let payload: [String: String] = [
 | 
				
			||||||
 | 
					////            "access_token": accessToken,
 | 
				
			||||||
 | 
					////            "refresh_token": refreshToken
 | 
				
			||||||
 | 
					////        ]
 | 
				
			||||||
 | 
					////
 | 
				
			||||||
 | 
					////        do {
 | 
				
			||||||
 | 
					////            let jsonData = try JSONEncoder().encode(payload)
 | 
				
			||||||
 | 
					////            request.httpBody = jsonData
 | 
				
			||||||
 | 
					////        } catch {
 | 
				
			||||||
 | 
					////            DispatchQueue.main.async {
 | 
				
			||||||
 | 
					////                completion(false, "Не удалось подготовить запрос.")
 | 
				
			||||||
 | 
					////            }
 | 
				
			||||||
 | 
					////            return
 | 
				
			||||||
 | 
					////        }
 | 
				
			||||||
 | 
					////
 | 
				
			||||||
 | 
					////        let task = URLSession.shared.dataTask(with: request) { data, response, error in
 | 
				
			||||||
 | 
					////            DispatchQueue.main.async {
 | 
				
			||||||
 | 
					////                if let error = error {
 | 
				
			||||||
 | 
					////                    print("AutoLogin error: \(error.localizedDescription)")
 | 
				
			||||||
 | 
					////                    completion(false, "Ошибка сети: \(error.localizedDescription)")
 | 
				
			||||||
 | 
					////                    return
 | 
				
			||||||
 | 
					////                }
 | 
				
			||||||
 | 
					////
 | 
				
			||||||
 | 
					////                guard let httpResponse = response as? HTTPURLResponse else {
 | 
				
			||||||
 | 
					////                    completion(false, "Некорректный ответ от сервера.")
 | 
				
			||||||
 | 
					////                    return
 | 
				
			||||||
 | 
					////                }
 | 
				
			||||||
 | 
					////
 | 
				
			||||||
 | 
					////                guard (200...299).contains(httpResponse.statusCode) else {
 | 
				
			||||||
 | 
					////                    if httpResponse.statusCode == 401 {
 | 
				
			||||||
 | 
					////                        print("деавторизация") //TODO
 | 
				
			||||||
 | 
					////                        completion(false, "Сессия недействительна. Пожалуйста, войдите снова.")
 | 
				
			||||||
 | 
					////                    } else if httpResponse.statusCode == 502 {
 | 
				
			||||||
 | 
					////                        completion(false, "Сервер не отвечает. Попробуйте позже.")
 | 
				
			||||||
 | 
					////                    } else {
 | 
				
			||||||
 | 
					////                        completion(false, "Ошибка сервера: \(httpResponse.statusCode)")
 | 
				
			||||||
 | 
					////                    }
 | 
				
			||||||
 | 
					////                    return
 | 
				
			||||||
 | 
					////                }
 | 
				
			||||||
 | 
					////
 | 
				
			||||||
 | 
					////                guard let data = data else {
 | 
				
			||||||
 | 
					////                    completion(false, "Пустой ответ от сервера.")
 | 
				
			||||||
 | 
					////                    return
 | 
				
			||||||
 | 
					////                }
 | 
				
			||||||
 | 
					////
 | 
				
			||||||
 | 
					////                do {
 | 
				
			||||||
 | 
					////                    let decoder = JSONDecoder()
 | 
				
			||||||
 | 
					////                    let refreshResponse = try decoder.decode(TokenRefreshResponse.self, from: data)
 | 
				
			||||||
 | 
					////
 | 
				
			||||||
 | 
					////                    KeychainService.shared.save(refreshResponse.access_token, forKey: "access_token", service: username)
 | 
				
			||||||
 | 
					////
 | 
				
			||||||
 | 
					////                    print("AutoLogin: токен обновлён.")
 | 
				
			||||||
 | 
					////                    completion(true, nil)
 | 
				
			||||||
 | 
					////                } catch {
 | 
				
			||||||
 | 
					////                    print("AutoLogin decode error: \(error.localizedDescription)")
 | 
				
			||||||
 | 
					////                    completion(false, "Ошибка обработки ответа сервера.")
 | 
				
			||||||
 | 
					////                }
 | 
				
			||||||
 | 
					////            }
 | 
				
			||||||
 | 
					////        }
 | 
				
			||||||
 | 
					////        task.resume()
 | 
				
			||||||
 | 
					//}
 | 
				
			||||||
@ -1,7 +1,54 @@
 | 
				
			|||||||
/* 
 | 
					/*
 | 
				
			||||||
  Localizable.strings
 | 
					  Localizable.strings
 | 
				
			||||||
  volnahub
 | 
					  volnahub
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Created by cheykrym on 10/06/2025.
 | 
					  Created by cheykrym on 10/06/2025.
 | 
				
			||||||
  
 | 
					 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* General */
 | 
				
			||||||
 | 
					"ok" = "OK";
 | 
				
			||||||
 | 
					"loading_placeholder" = "Loading...";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* LoginView */
 | 
				
			||||||
 | 
					"LoginView_change_language" = "Language";
 | 
				
			||||||
 | 
					"LoginView_login" = "Login";
 | 
				
			||||||
 | 
					"LoginView_password" = "Password";
 | 
				
			||||||
 | 
					"LoginView_button_login" = "Log in";
 | 
				
			||||||
 | 
					"LoginView_error" = "Login error";
 | 
				
			||||||
 | 
					"LoginView_button_register" = "Register";
 | 
				
			||||||
 | 
					"LoginView_error_username_invalid" = "Username must be 3 to 32 characters (letters, digits, or _)";
 | 
				
			||||||
 | 
					"LoginView_error_password_invalid" = "Password must be 6 to 32 characters long";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* RegistrationView */
 | 
				
			||||||
 | 
					"RegistrationView_title" = "Registration";
 | 
				
			||||||
 | 
					"RegistrationView_fullname" = "Full name";
 | 
				
			||||||
 | 
					"RegistrationView_login" = "Login";
 | 
				
			||||||
 | 
					"RegistrationView_error_username_invalid" = "Username must be 3 to 32 characters (letters, digits, or _)";
 | 
				
			||||||
 | 
					"RegistrationView_password" = "Password";
 | 
				
			||||||
 | 
					"RegistrationView_error_password_invalid" = "Password must be 6 to 32 characters long";
 | 
				
			||||||
 | 
					"RegistrationView_confirm_password" = "Confirm password";
 | 
				
			||||||
 | 
					"RegistrationView_error_confirm_password_invalid" = "Passwords do not match";
 | 
				
			||||||
 | 
					"RegistrationView_invite" = "Invite code (optional)";
 | 
				
			||||||
 | 
					"RegistrationView_button_register" = "Register";
 | 
				
			||||||
 | 
					"RegistrationView_close" = "Close";
 | 
				
			||||||
 | 
					"RegistrationView_error" = "Registration error";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* AuthService */
 | 
				
			||||||
 | 
					"AuthService_error_invalid_invitation_code" = "Invalid invitation code.";
 | 
				
			||||||
 | 
					"AuthService_error_invitation_not_active" = "The invitation is not active.";
 | 
				
			||||||
 | 
					"AuthService_error_invitation_usage_limit" = "The invitation has reached its usage limit.";
 | 
				
			||||||
 | 
					"AuthService_error_invitation_expired" = "The invitation has expired.";
 | 
				
			||||||
 | 
					"AuthService_error_login_already_registered" = "This login is already registered.";
 | 
				
			||||||
 | 
					"AuthService_error_registration_disabled" = "Registration is temporarily unavailable.";
 | 
				
			||||||
 | 
					"AuthService_error_server_unavailable" = "Server is unavailable. Please try again later.";
 | 
				
			||||||
 | 
					"AuthService_error_too_many_requests" = "Too many requests.";
 | 
				
			||||||
 | 
					"AuthService_error_invalid_request" = "Invalid request (400).";
 | 
				
			||||||
 | 
					"AuthService_error_registration_forbidden" = "Registration is forbidden.";
 | 
				
			||||||
 | 
					"AuthService_error_server_error" = "Server error: %@";
 | 
				
			||||||
 | 
					"AuthService_error_network" = "Network error: %@";
 | 
				
			||||||
 | 
					"AuthService_error_invalid_response" = "Invalid server response.";
 | 
				
			||||||
 | 
					"AuthService_error_invalid_credentials" = "Invalid username or password.";
 | 
				
			||||||
 | 
					"AuthService_error_empty_response" = "Empty server response.";
 | 
				
			||||||
 | 
					"AuthService_error_parsing_response" = "Failed to parse server response.";
 | 
				
			||||||
 | 
					"AuthService_error_serialization" = "Failed to serialize request data.";
 | 
				
			||||||
 | 
					"AuthService_login_success_but_failed" = "Registration succeeded, but login failed.";
 | 
				
			||||||
 | 
				
			|||||||
@ -5,10 +5,54 @@
 | 
				
			|||||||
  Created by cheykrym on 10/06/2025.
 | 
					  Created by cheykrym on 10/06/2025.
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					"ok" = "OK";
 | 
				
			||||||
"loading_placeholder" = "Загрузка...";
 | 
					"loading_placeholder" = "Загрузка...";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* LoginView */
 | 
				
			||||||
"LoginView_change_language" = "Язык";
 | 
					"LoginView_change_language" = "Язык";
 | 
				
			||||||
"LoginView_title" = "Вход";
 | 
					 | 
				
			||||||
"LoginView_login" = "Логин";
 | 
					"LoginView_login" = "Логин";
 | 
				
			||||||
"LoginView_password" = "Пароль";
 | 
					"LoginView_password" = "Пароль";
 | 
				
			||||||
"LoginView_button_login" = "Войти";
 | 
					"LoginView_button_login" = "Войти";
 | 
				
			||||||
 | 
					"LoginView_error" = "Ошибка авторизации";
 | 
				
			||||||
"LoginView_button_register" = "Регистрация";
 | 
					"LoginView_button_register" = "Регистрация";
 | 
				
			||||||
 | 
					"LoginView_error_username_invalid" = "Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)";
 | 
				
			||||||
 | 
					"LoginView_error_password_invalid" = "Пароль должен быть от 6 до 32 символов";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* RegistrationView */
 | 
				
			||||||
 | 
					"RegistrationView_title" = "Регистрация";
 | 
				
			||||||
 | 
					"RegistrationView_fullname" = "Имя";
 | 
				
			||||||
 | 
					"RegistrationView_login" = "Логин";
 | 
				
			||||||
 | 
					"RegistrationView_error_username_invalid" = "Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)";
 | 
				
			||||||
 | 
					"RegistrationView_password" = "Пароль";
 | 
				
			||||||
 | 
					"RegistrationView_error_password_invalid" = "Пароль должен быть от 6 до 32 символов";
 | 
				
			||||||
 | 
					"RegistrationView_confirm_password" = "Подтверждение пароля";
 | 
				
			||||||
 | 
					"RegistrationView_error_confirm_password_invalid" = "Пароли не совпадают";
 | 
				
			||||||
 | 
					"RegistrationView_invite" = "Инвайт-код (необязательно)";
 | 
				
			||||||
 | 
					"RegistrationView_button_register" = "Зарегистрироваться";
 | 
				
			||||||
 | 
					"RegistrationView_close" = "Закрыть";
 | 
				
			||||||
 | 
					"RegistrationView_error" = "Ошибка регистрация";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* AuthService */
 | 
				
			||||||
 | 
					"AuthService_error_invalid_invitation_code" = "Неверный код приглашения.";
 | 
				
			||||||
 | 
					"AuthService_error_invitation_not_active" = "Приглашение не активно.";
 | 
				
			||||||
 | 
					"AuthService_error_invitation_usage_limit" = "Приглашение достигло лимита использования.";
 | 
				
			||||||
 | 
					"AuthService_error_invitation_expired" = "Приглашение истекло.";
 | 
				
			||||||
 | 
					"AuthService_error_login_already_registered" = "Логин уже занят.";
 | 
				
			||||||
 | 
					"AuthService_error_registration_disabled" = "Регистрация временно недоступна.";
 | 
				
			||||||
 | 
					"AuthService_error_server_unavailable" = "Сервер не отвечает. Попробуйте позже.";
 | 
				
			||||||
 | 
					"AuthService_error_too_many_requests" = "Слишком много запросов.";
 | 
				
			||||||
 | 
					"AuthService_error_invalid_request" = "Неверный запрос (400).";
 | 
				
			||||||
 | 
					"AuthService_error_registration_forbidden" = "Регистрация запрещена.";
 | 
				
			||||||
 | 
					"AuthService_error_server_error" = "Ошибка сервера: %@";
 | 
				
			||||||
 | 
					"AuthService_error_network" = "Ошибка сети: %@";
 | 
				
			||||||
 | 
					"AuthService_error_invalid_response" = "Некорректный ответ от сервера.";
 | 
				
			||||||
 | 
					"AuthService_error_invalid_credentials" = "Неверный логин или пароль.";
 | 
				
			||||||
 | 
					"AuthService_error_empty_response" = "Пустой ответ от сервера.";
 | 
				
			||||||
 | 
					"AuthService_error_parsing_response" = "Не удалось обработать ответ сервера.";
 | 
				
			||||||
 | 
					"AuthService_error_serialization" = "Не удалось сериализовать данные запроса.";
 | 
				
			||||||
 | 
					"AuthService_login_success_but_failed" = "Регистрация выполнена, но вход не удался.";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* MainView */
 | 
				
			||||||
 | 
					"MainView_contacts" = "Контакты";
 | 
				
			||||||
 | 
					"MainView_chats" = "Чаты";
 | 
				
			||||||
 | 
					"MainView_settings" = "Настройки";
 | 
				
			||||||
 | 
				
			|||||||
@ -11,28 +11,39 @@ import Combine
 | 
				
			|||||||
class LoginViewModel: ObservableObject {
 | 
					class LoginViewModel: ObservableObject {
 | 
				
			||||||
    @Published var username: String = ""
 | 
					    @Published var username: String = ""
 | 
				
			||||||
    @Published var password: String = ""
 | 
					    @Published var password: String = ""
 | 
				
			||||||
    @Published var isLoading: Bool = true    // сразу true, чтобы вызывался автологин
 | 
					    @Published var isLoading: Bool = true    // сразу true, чтобы показать спиннер при автологине
 | 
				
			||||||
    @Published var showError: Bool = false
 | 
					    @Published var showError: Bool = false
 | 
				
			||||||
    @Published var errorMessage: String = ""
 | 
					    @Published var errorMessage: String = ""
 | 
				
			||||||
    @Published var isLoggedIn: Bool = false
 | 
					    @Published var isLoggedIn: Bool = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private let authService = AuthService()
 | 
					    private let authService = AuthService()
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    init() {
 | 
					    init() {
 | 
				
			||||||
 | 
					        // Если username сохранён, подставим его сразу
 | 
				
			||||||
 | 
					        if let savedUsername = UserDefaults.standard.string(forKey: "currentUser") {
 | 
				
			||||||
 | 
					            username = savedUsername
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Запускаем автологин
 | 
				
			||||||
        autoLogin()
 | 
					        autoLogin()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    func autoLogin() {
 | 
					    func autoLogin() {
 | 
				
			||||||
        authService.autoLogin { [weak self] success in
 | 
					        authService.autoLogin { [weak self] success, error in
 | 
				
			||||||
            DispatchQueue.main.async {
 | 
					            DispatchQueue.main.async {
 | 
				
			||||||
                self?.isLoading = false
 | 
					                self?.isLoading = false
 | 
				
			||||||
                if success {
 | 
					                if success {
 | 
				
			||||||
                    self?.isLoggedIn = true
 | 
					                    self?.isLoggedIn = true
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    self?.isLoggedIn = false
 | 
				
			||||||
 | 
					                    self?.errorMessage = error ?? "Произошла ошибка."
 | 
				
			||||||
 | 
					                    self?.showError = false
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    func login() {
 | 
					    func login() {
 | 
				
			||||||
        isLoading = true
 | 
					        isLoading = true
 | 
				
			||||||
        showError = false
 | 
					        showError = false
 | 
				
			||||||
@ -50,10 +61,42 @@ class LoginViewModel: ObservableObject {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    func logout() {
 | 
					    func registerUser(username: String, password: String, invite: String?, completion: @escaping (Bool, String?) -> Void) {
 | 
				
			||||||
        username = ""
 | 
					        authService.register(username: username, password: password, invite: invite) { [weak self] success, message in
 | 
				
			||||||
        password = ""
 | 
					            DispatchQueue.main.async {
 | 
				
			||||||
        isLoggedIn = false
 | 
					                if success {
 | 
				
			||||||
 | 
					                    self?.isLoggedIn = true // 👈 переключаем на главный экран после автологина
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                completion(success, message)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    func logoutCurrentUser() {
 | 
				
			||||||
 | 
					        authService.logoutCurrentUser { [weak self] success, error in
 | 
				
			||||||
 | 
					            DispatchQueue.main.async {
 | 
				
			||||||
 | 
					                if success {
 | 
				
			||||||
 | 
					                    self?.username = UserDefaults.standard.string(forKey: "currentUser") ?? ""
 | 
				
			||||||
 | 
					                    self?.password = ""
 | 
				
			||||||
 | 
					                    self?.isLoggedIn = true
 | 
				
			||||||
 | 
					                    self?.showError = false
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    self?.username = ""
 | 
				
			||||||
 | 
					                    self?.password = ""
 | 
				
			||||||
 | 
					                    self?.isLoggedIn = false
 | 
				
			||||||
 | 
					                    self?.errorMessage = error ?? "Ошибка при деавторизации."
 | 
				
			||||||
 | 
					                    self?.showError = false
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					//    func logout() {
 | 
				
			||||||
 | 
					//        username = ""
 | 
				
			||||||
 | 
					//        password = ""
 | 
				
			||||||
 | 
					//        isLoggedIn = false
 | 
				
			||||||
 | 
					//        showError = false
 | 
				
			||||||
 | 
					//        errorMessage = ""
 | 
				
			||||||
 | 
					//    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										55
									
								
								Shared/Views/CustomTextField.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								Shared/Views/CustomTextField.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					//  CustomTextField.swift
 | 
				
			||||||
 | 
					//  VolnahubApp
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//  Created by cheykrym on 11/06/2025.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import SwiftUI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct CustomTextField: UIViewRepresentable {
 | 
				
			||||||
 | 
					    class Coordinator: NSObject, UITextFieldDelegate {
 | 
				
			||||||
 | 
					        var parent: CustomTextField
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        init(_ parent: CustomTextField) {
 | 
				
			||||||
 | 
					            self.parent = parent
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
 | 
				
			||||||
 | 
					            parent.onReturn()
 | 
				
			||||||
 | 
					            return false // предотвращаем автоматический переход на новую строку
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @objc func textFieldDidChange(_ textField: UITextField) {
 | 
				
			||||||
 | 
					            parent.text = textField.text ?? ""
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var placeholder: String
 | 
				
			||||||
 | 
					    @Binding var text: String
 | 
				
			||||||
 | 
					    var isSecure: Bool = false
 | 
				
			||||||
 | 
					    var onReturn: () -> Void
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    func makeUIView(context: Context) -> UITextField {
 | 
				
			||||||
 | 
					        let textField = UITextField()
 | 
				
			||||||
 | 
					        textField.placeholder = placeholder
 | 
				
			||||||
 | 
					        textField.delegate = context.coordinator
 | 
				
			||||||
 | 
					        textField.borderStyle = .roundedRect
 | 
				
			||||||
 | 
					        textField.autocapitalizationType = .none
 | 
				
			||||||
 | 
					        textField.autocorrectionType = .no
 | 
				
			||||||
 | 
					        textField.isSecureTextEntry = isSecure
 | 
				
			||||||
 | 
					        textField.returnKeyType = .next
 | 
				
			||||||
 | 
					        textField.addTarget(context.coordinator, action: #selector(Coordinator.textFieldDidChange(_:)), for: .editingChanged)
 | 
				
			||||||
 | 
					        return textField
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    func updateUIView(_ uiView: UITextField, context: Context) {
 | 
				
			||||||
 | 
					        if uiView.text != text {
 | 
				
			||||||
 | 
					            uiView.text = text
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    func makeCoordinator() -> Coordinator {
 | 
				
			||||||
 | 
					        Coordinator(self)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -24,115 +24,128 @@ struct LoginView: View {
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
    var body: some View {
 | 
					    var body: some View {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        VStack {
 | 
					        ZStack {
 | 
				
			||||||
            HStack {
 | 
					            Color.clear // чтобы поймать тап
 | 
				
			||||||
 | 
					                .contentShape(Rectangle())
 | 
				
			||||||
                Button(action: openLanguageSettings) {
 | 
					                .onTapGesture {
 | 
				
			||||||
                    Text("🌍 " + NSLocalizedString("LoginView_change_language", comment: ""))
 | 
					                    hideKeyboard()
 | 
				
			||||||
                        .padding()
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                Spacer()
 | 
					 | 
				
			||||||
                Button(action: toggleTheme) {
 | 
					 | 
				
			||||||
                    Image(systemName: isDarkMode ? "moon.fill" : "sun.max.fill")
 | 
					 | 
				
			||||||
                        .padding()
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Spacer()
 | 
					            VStack {
 | 
				
			||||||
 | 
					                HStack {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            
 | 
					                    Button(action: openLanguageSettings) {
 | 
				
			||||||
//            Text(NSLocalizedString("LoginView_title", comment: ""))
 | 
					                        Text("🌍")
 | 
				
			||||||
//                .font(.largeTitle)
 | 
					                            .padding()
 | 
				
			||||||
//                .bold()
 | 
					                    }
 | 
				
			||||||
 | 
					                    Spacer()
 | 
				
			||||||
            TextField(NSLocalizedString("LoginView_login", comment: ""), text: $viewModel.username)
 | 
					                    Button(action: toggleTheme) {
 | 
				
			||||||
                .padding()
 | 
					                        Image(systemName: isDarkMode ? "moon.fill" : "sun.max.fill")
 | 
				
			||||||
                .background(Color(.secondarySystemBackground))
 | 
					                            .padding()
 | 
				
			||||||
                .cornerRadius(8)
 | 
					 | 
				
			||||||
                .autocapitalization(.none)
 | 
					 | 
				
			||||||
                .disableAutocorrection(true)
 | 
					 | 
				
			||||||
                .onChange(of: viewModel.username) { newValue in
 | 
					 | 
				
			||||||
                    if newValue.count > 32 {
 | 
					 | 
				
			||||||
                        viewModel.username = String(newValue.prefix(32))
 | 
					 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					                .onTapGesture {
 | 
				
			||||||
            // Показываем ошибку для логина
 | 
					                hideKeyboard()
 | 
				
			||||||
            if !isUsernameValid && !viewModel.username.isEmpty {
 | 
					 | 
				
			||||||
                Text(NSLocalizedString("LoginView_error_username_invalid", comment: "Неверный логин"))
 | 
					 | 
				
			||||||
                    .foregroundColor(.red)
 | 
					 | 
				
			||||||
                    .font(.caption)
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Показываем поле пароля (даже если оно невалидное) только если логин корректен
 | 
					                Spacer()
 | 
				
			||||||
            if isUsernameValid {
 | 
					
 | 
				
			||||||
                SecureField(NSLocalizedString("LoginView_password", comment: ""), text: $viewModel.password)
 | 
					                TextField(NSLocalizedString("LoginView_login", comment: ""), text: $viewModel.username)
 | 
				
			||||||
                    .padding()
 | 
					                    .padding()
 | 
				
			||||||
                    .background(Color(.secondarySystemBackground))
 | 
					                    .background(Color(.secondarySystemBackground))
 | 
				
			||||||
                    .cornerRadius(8)
 | 
					                    .cornerRadius(8)
 | 
				
			||||||
                    .autocapitalization(.none)
 | 
					                    .autocapitalization(.none)
 | 
				
			||||||
                    .onChange(of: viewModel.password) { newValue in
 | 
					                    .disableAutocorrection(true)
 | 
				
			||||||
 | 
					                    .onChange(of: viewModel.username) { newValue in
 | 
				
			||||||
                        if newValue.count > 32 {
 | 
					                        if newValue.count > 32 {
 | 
				
			||||||
                            viewModel.password = String(newValue.prefix(32))
 | 
					                            viewModel.username = String(newValue.prefix(32))
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // Показываем ошибку для пароля
 | 
					                // Показываем ошибку для логина
 | 
				
			||||||
                if !isPasswordValid && !viewModel.password.isEmpty {
 | 
					                if !isUsernameValid && !viewModel.username.isEmpty {
 | 
				
			||||||
                    Text(NSLocalizedString("LoginView_error_password_invalid", comment: "Неверный пароль"))
 | 
					                    Text(NSLocalizedString("LoginView_error_username_invalid", comment: "Неверный логин"))
 | 
				
			||||||
                        .foregroundColor(.red)
 | 
					                        .foregroundColor(.red)
 | 
				
			||||||
                        .font(.caption)
 | 
					                        .font(.caption)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					
 | 
				
			||||||
            
 | 
					                // Показываем поле пароля (даже если оно невалидное) только если логин корректен
 | 
				
			||||||
            if isUsernameValid && isPasswordValid {
 | 
					                if isUsernameValid || !viewModel.password.isEmpty {
 | 
				
			||||||
                Button(action: {
 | 
					                    SecureField(NSLocalizedString("LoginView_password", comment: ""), text: $viewModel.password)
 | 
				
			||||||
                    viewModel.login()
 | 
					                        .padding()
 | 
				
			||||||
                }) {
 | 
					                        .background(Color(.secondarySystemBackground))
 | 
				
			||||||
                    if viewModel.isLoading {
 | 
					                        .cornerRadius(8)
 | 
				
			||||||
                        ProgressView()
 | 
					                        .autocapitalization(.none)
 | 
				
			||||||
                            .progressViewStyle(CircularProgressViewStyle())
 | 
					                        .onChange(of: viewModel.password) { newValue in
 | 
				
			||||||
                            .padding()
 | 
					                            if newValue.count > 32 {
 | 
				
			||||||
                            .frame(maxWidth: .infinity)
 | 
					                                viewModel.password = String(newValue.prefix(32))
 | 
				
			||||||
                            .background(Color.gray.opacity(0.6))
 | 
					                            }
 | 
				
			||||||
                            .cornerRadius(8)
 | 
					                        }
 | 
				
			||||||
                    } else {
 | 
					
 | 
				
			||||||
                        Text(NSLocalizedString("LoginView_button_login", comment: ""))
 | 
					                    // Показываем ошибку для пароля
 | 
				
			||||||
                            .foregroundColor(.white)
 | 
					                    if !isPasswordValid && !viewModel.password.isEmpty {
 | 
				
			||||||
                            .padding()
 | 
					                        Text(NSLocalizedString("LoginView_error_password_invalid", comment: "Неверный пароль"))
 | 
				
			||||||
                            .frame(maxWidth: .infinity)
 | 
					                            .foregroundColor(.red)
 | 
				
			||||||
                            .background(Color.blue)
 | 
					                            .font(.caption)
 | 
				
			||||||
                            .cornerRadius(8)
 | 
					 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                .disabled(viewModel.isLoading)
 | 
					                
 | 
				
			||||||
            }
 | 
					                if isUsernameValid && isPasswordValid {
 | 
				
			||||||
 | 
					                    Button(action: {
 | 
				
			||||||
 | 
					                        viewModel.login()
 | 
				
			||||||
 | 
					                    }) {
 | 
				
			||||||
 | 
					                        if viewModel.isLoading {
 | 
				
			||||||
 | 
					                            ProgressView()
 | 
				
			||||||
 | 
					                                .progressViewStyle(CircularProgressViewStyle())
 | 
				
			||||||
 | 
					                                .padding()
 | 
				
			||||||
 | 
					                                .frame(maxWidth: .infinity)
 | 
				
			||||||
 | 
					                                .background(Color.gray.opacity(0.6))
 | 
				
			||||||
 | 
					                                .cornerRadius(8)
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            Text(NSLocalizedString("LoginView_button_login", comment: ""))
 | 
				
			||||||
 | 
					                                .foregroundColor(.white)
 | 
				
			||||||
 | 
					                                .padding()
 | 
				
			||||||
 | 
					                                .frame(maxWidth: .infinity)
 | 
				
			||||||
 | 
					                                .background(Color.blue)
 | 
				
			||||||
 | 
					                                .cornerRadius(8)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    .disabled(viewModel.isLoading || !isUsernameValid || !isPasswordValid)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Spacer()
 | 
					                Spacer()
 | 
				
			||||||
            
 | 
					                
 | 
				
			||||||
            // Кнопка регистрации
 | 
					                // Кнопка регистрации
 | 
				
			||||||
            Button(action: {
 | 
					                Button(action: {
 | 
				
			||||||
                isShowingRegistration = true
 | 
					                    isShowingRegistration = true
 | 
				
			||||||
            }) {
 | 
					                }) {
 | 
				
			||||||
                Text(NSLocalizedString("LoginView_button_register", comment: "Регистрация"))
 | 
					                    Text(NSLocalizedString("LoginView_button_register", comment: "Регистрация"))
 | 
				
			||||||
                    .foregroundColor(.blue)
 | 
					                        .foregroundColor(.blue)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                .padding(.top, 10)
 | 
				
			||||||
 | 
					                .sheet(isPresented: $isShowingRegistration) {
 | 
				
			||||||
 | 
					                    RegistrationView(viewModel: viewModel)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            .padding(.top, 10)
 | 
					            .padding()
 | 
				
			||||||
            .sheet(isPresented: $isShowingRegistration) {
 | 
					            .alert(isPresented: $viewModel.showError) {
 | 
				
			||||||
                RegistrationView()
 | 
					                Alert(
 | 
				
			||||||
 | 
					                    title: Text(NSLocalizedString("LoginView_error", comment: "")),
 | 
				
			||||||
 | 
					                    message: Text(viewModel.errorMessage),
 | 
				
			||||||
 | 
					                    dismissButton: .default(Text(NSLocalizedString("ok", comment: "")))
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            
 | 
					            .onTapGesture {
 | 
				
			||||||
 | 
					            hideKeyboard()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        .padding()
 | 
					 | 
				
			||||||
        .alert(isPresented: $viewModel.showError) {
 | 
					 | 
				
			||||||
            Alert(
 | 
					 | 
				
			||||||
                title: Text(NSLocalizedString("LoginView_error", comment: "")),
 | 
					 | 
				
			||||||
                message: Text(viewModel.errorMessage),
 | 
					 | 
				
			||||||
                dismissButton: .default(Text("ОК"))
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    private func toggleTheme() {
 | 
					    private func toggleTheme() {
 | 
				
			||||||
        isDarkMode.toggle()
 | 
					        isDarkMode.toggle()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -141,6 +154,10 @@ struct LoginView: View {
 | 
				
			|||||||
        guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
 | 
					        guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
 | 
				
			||||||
        UIApplication.shared.open(url)
 | 
					        UIApplication.shared.open(url)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private func hideKeyboard() {
 | 
				
			||||||
 | 
					        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -16,21 +16,21 @@ struct MainView: View {
 | 
				
			|||||||
            ContactsTab()
 | 
					            ContactsTab()
 | 
				
			||||||
                .tabItem {
 | 
					                .tabItem {
 | 
				
			||||||
                    Image(systemName: "person.2.fill")
 | 
					                    Image(systemName: "person.2.fill")
 | 
				
			||||||
                    Text("Контакты")
 | 
					                    Text(NSLocalizedString("MainView_contacts", comment: ""))
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                .tag(0)
 | 
					                .tag(0)
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            ChatsTab()
 | 
					            ChatsTab()
 | 
				
			||||||
                .tabItem {
 | 
					                .tabItem {
 | 
				
			||||||
                    Image(systemName: "bubble.left.and.bubble.right.fill")
 | 
					                    Image(systemName: "bubble.left.and.bubble.right.fill")
 | 
				
			||||||
                    Text("Чаты")
 | 
					                    Text(NSLocalizedString("MainView_chats", comment: ""))
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                .tag(1)
 | 
					                .tag(1)
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            SettingsTab()
 | 
					            SettingsTab(viewModel: viewModel)
 | 
				
			||||||
                .tabItem {
 | 
					                .tabItem {
 | 
				
			||||||
                    Image(systemName: "gearshape.fill")
 | 
					                    Image(systemName: "gearshape.fill")
 | 
				
			||||||
                    Text("Настройки")
 | 
					                    Text(NSLocalizedString("MainView_settings", comment: ""))
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                .tag(2)
 | 
					                .tag(2)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -8,15 +8,19 @@
 | 
				
			|||||||
import SwiftUI
 | 
					import SwiftUI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct RegistrationView: View {
 | 
					struct RegistrationView: View {
 | 
				
			||||||
 | 
					    @ObservedObject var viewModel: LoginViewModel
 | 
				
			||||||
    @Environment(\.presentationMode) private var presentationMode
 | 
					    @Environment(\.presentationMode) private var presentationMode
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    @State private var fullName: String = ""
 | 
					 | 
				
			||||||
    @State private var username: String = ""
 | 
					    @State private var username: String = ""
 | 
				
			||||||
    @State private var password: String = ""
 | 
					    @State private var password: String = ""
 | 
				
			||||||
    @State private var confirmPassword: String = ""
 | 
					    @State private var confirmPassword: String = ""
 | 
				
			||||||
    @State private var inviteCode: String = ""
 | 
					    @State private var inviteCode: String = ""
 | 
				
			||||||
    @AppStorage("isDarkMode") private var isDarkMode: Bool = true
 | 
					    @AppStorage("isDarkMode") private var isDarkMode: Bool = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @State private var isLoading: Bool = false
 | 
				
			||||||
 | 
					    @State private var showError: Bool = false
 | 
				
			||||||
 | 
					    @State private var errorMessage: String = ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private var isUsernameValid: Bool {
 | 
					    private var isUsernameValid: Bool {
 | 
				
			||||||
        let pattern = "^[A-Za-z0-9_]{3,32}$"
 | 
					        let pattern = "^[A-Za-z0-9_]{3,32}$"
 | 
				
			||||||
        return username.range(of: pattern, options: .regularExpression) != nil
 | 
					        return username.range(of: pattern, options: .regularExpression) != nil
 | 
				
			||||||
@ -31,104 +35,179 @@ struct RegistrationView: View {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private var isFormValid: Bool {
 | 
					    private var isFormValid: Bool {
 | 
				
			||||||
        !fullName.isEmpty && isUsernameValid && isPasswordValid && isConfirmPasswordValid
 | 
					        isUsernameValid && isPasswordValid && isConfirmPasswordValid
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var body: some View {
 | 
					    var body: some View {
 | 
				
			||||||
        NavigationView {
 | 
					        NavigationView {
 | 
				
			||||||
            VStack {
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                Text(NSLocalizedString("RegistrationView_title", comment: "Регистрация"))
 | 
					            ScrollView {
 | 
				
			||||||
                    .font(.largeTitle)
 | 
					                ZStack {
 | 
				
			||||||
                    .bold()
 | 
					                    Color.clear
 | 
				
			||||||
 | 
					                        .contentShape(Rectangle())
 | 
				
			||||||
 | 
					                        .onTapGesture {
 | 
				
			||||||
 | 
					                            hideKeyboard()
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//                Spacer()
 | 
					                VStack(alignment: .leading, spacing: 16) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // Полное имя
 | 
					                    Group {
 | 
				
			||||||
                TextField(NSLocalizedString("RegistrationView_fullname", comment: "Полное имя"), text: $fullName)
 | 
					 | 
				
			||||||
                    .padding()
 | 
					 | 
				
			||||||
                    .background(Color(.secondarySystemBackground))
 | 
					 | 
				
			||||||
                    .cornerRadius(8)
 | 
					 | 
				
			||||||
                    .autocapitalization(.words)
 | 
					 | 
				
			||||||
                    .disableAutocorrection(true)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // Логин
 | 
					                        HStack {
 | 
				
			||||||
                TextField(NSLocalizedString("RegistrationView_login", comment: "Логин"), text: $username)
 | 
					                            TextField(NSLocalizedString("RegistrationView_login", comment: "Логин"), text: $username)
 | 
				
			||||||
                    .padding()
 | 
					                                .autocapitalization(.none)
 | 
				
			||||||
                    .background(Color(.secondarySystemBackground))
 | 
					                                .disableAutocorrection(true)
 | 
				
			||||||
                    .cornerRadius(8)
 | 
					                            Spacer()
 | 
				
			||||||
                    .autocapitalization(.none)
 | 
					                            if !username.isEmpty {
 | 
				
			||||||
                    .disableAutocorrection(true)
 | 
					                                Image(systemName: isUsernameValid ? "checkmark.circle" : "xmark.circle")
 | 
				
			||||||
 | 
					                                    .foregroundColor(isUsernameValid ? .green : .red)
 | 
				
			||||||
                if !isUsernameValid && !username.isEmpty {
 | 
					                            }
 | 
				
			||||||
                    Text(NSLocalizedString("RegistrationView_error_username_invalid", comment: "Неверный логин"))
 | 
					                        }
 | 
				
			||||||
                        .foregroundColor(.red)
 | 
					 | 
				
			||||||
                        .font(.caption)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                // Пароль
 | 
					 | 
				
			||||||
                SecureField(NSLocalizedString("RegistrationView_password", comment: "Пароль"), text: $password)
 | 
					 | 
				
			||||||
                    .padding()
 | 
					 | 
				
			||||||
                    .background(Color(.secondarySystemBackground))
 | 
					 | 
				
			||||||
                    .cornerRadius(8)
 | 
					 | 
				
			||||||
                    .autocapitalization(.none)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if !isPasswordValid && !password.isEmpty {
 | 
					 | 
				
			||||||
                    Text(NSLocalizedString("RegistrationView_error_password_invalid", comment: "Пароль должен быть от 6 до 32 символов"))
 | 
					 | 
				
			||||||
                        .foregroundColor(.red)
 | 
					 | 
				
			||||||
                        .font(.caption)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                // Подтверждение пароля
 | 
					 | 
				
			||||||
                SecureField(NSLocalizedString("RegistrationView_confirm_password", comment: "Подтверждение пароля"), text: $confirmPassword)
 | 
					 | 
				
			||||||
                    .padding()
 | 
					 | 
				
			||||||
                    .background(Color(.secondarySystemBackground))
 | 
					 | 
				
			||||||
                    .cornerRadius(8)
 | 
					 | 
				
			||||||
                    .autocapitalization(.none)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if !isConfirmPasswordValid && !confirmPassword.isEmpty {
 | 
					 | 
				
			||||||
                    Text(NSLocalizedString("RegistrationView_error_confirm_password_invalid", comment: "Пароли не совпадают"))
 | 
					 | 
				
			||||||
                        .foregroundColor(.red)
 | 
					 | 
				
			||||||
                        .font(.caption)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                // Инвайт-код
 | 
					 | 
				
			||||||
                TextField(NSLocalizedString("RegistrationView_invite", comment: "Инвайт-код"), text: $inviteCode)
 | 
					 | 
				
			||||||
                    .padding()
 | 
					 | 
				
			||||||
                    .background(Color(.secondarySystemBackground))
 | 
					 | 
				
			||||||
                    .cornerRadius(8)
 | 
					 | 
				
			||||||
                    .autocapitalization(.none)
 | 
					 | 
				
			||||||
                    .disableAutocorrection(true)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                // Кнопка регистрации
 | 
					 | 
				
			||||||
                Button(action: {
 | 
					 | 
				
			||||||
                    print("Регистрация отправлена")
 | 
					 | 
				
			||||||
                }) {
 | 
					 | 
				
			||||||
                    Text(NSLocalizedString("RegistrationView_button_register", comment: "Зарегистрироваться"))
 | 
					 | 
				
			||||||
                        .foregroundColor(.white)
 | 
					 | 
				
			||||||
                        .padding()
 | 
					                        .padding()
 | 
				
			||||||
                        .frame(maxWidth: .infinity)
 | 
					                        .background(Color(.secondarySystemBackground))
 | 
				
			||||||
                        .background(isFormValid ? Color.blue : Color.gray.opacity(0.6))
 | 
					 | 
				
			||||||
                        .cornerRadius(8)
 | 
					                        .cornerRadius(8)
 | 
				
			||||||
                }
 | 
					                        .autocapitalization(.none)
 | 
				
			||||||
                .disabled(!isFormValid)
 | 
					                        .disableAutocorrection(true)
 | 
				
			||||||
 | 
					                        .onChange(of: username) { newValue in
 | 
				
			||||||
 | 
					                            if newValue.count > 32 {
 | 
				
			||||||
 | 
					                                username = String(newValue.prefix(32))
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            }
 | 
					                        if !isUsernameValid && !username.isEmpty {
 | 
				
			||||||
            .padding()
 | 
					                            Text(NSLocalizedString("RegistrationView_error_username_invalid", comment: "Неверный логин"))
 | 
				
			||||||
            .navigationBarItems(trailing:
 | 
					                                .foregroundColor(.red)
 | 
				
			||||||
                Button(action: {
 | 
					                                .font(.caption)
 | 
				
			||||||
                    presentationMode.wrappedValue.dismiss()
 | 
					                        }
 | 
				
			||||||
                }) {
 | 
					
 | 
				
			||||||
                    Text(NSLocalizedString("RegistrationView_close", comment: "Закрыть"))
 | 
					                        HStack {
 | 
				
			||||||
 | 
					                            SecureField(NSLocalizedString("RegistrationView_password", comment: "Пароль"), text: $password)
 | 
				
			||||||
 | 
					                                .autocapitalization(.none)
 | 
				
			||||||
 | 
					                            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 > 32 {
 | 
				
			||||||
 | 
					                                password = String(newValue.prefix(32))
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if !isPasswordValid && !password.isEmpty {
 | 
				
			||||||
 | 
					                            Text(NSLocalizedString("RegistrationView_error_password_invalid", comment: "Пароль должен быть от 6 до 32 символов"))
 | 
				
			||||||
 | 
					                                .foregroundColor(.red)
 | 
				
			||||||
 | 
					                                .font(.caption)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        HStack {
 | 
				
			||||||
 | 
					                            SecureField(NSLocalizedString("RegistrationView_confirm_password", comment: "Подтверждение пароля"), text: $confirmPassword)
 | 
				
			||||||
 | 
					                                .autocapitalization(.none)
 | 
				
			||||||
 | 
					                            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("RegistrationView_error_confirm_password_invalid", comment: "Пароли не совпадают"))
 | 
				
			||||||
 | 
					                                .foregroundColor(.red)
 | 
				
			||||||
 | 
					                                .font(.caption)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        TextField(NSLocalizedString("RegistrationView_invite", comment: "Инвайт-код"), text: $inviteCode)
 | 
				
			||||||
 | 
					                            .padding()
 | 
				
			||||||
 | 
					                            .background(Color(.secondarySystemBackground))
 | 
				
			||||||
 | 
					                            .cornerRadius(8)
 | 
				
			||||||
 | 
					                            .autocapitalization(.none)
 | 
				
			||||||
 | 
					                            .disableAutocorrection(true)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    Button(action: registerUser) {
 | 
				
			||||||
 | 
					                        if isLoading {
 | 
				
			||||||
 | 
					                            ProgressView()
 | 
				
			||||||
 | 
					                                .padding()
 | 
				
			||||||
 | 
					                                .frame(maxWidth: .infinity)
 | 
				
			||||||
 | 
					                                .background(Color.gray.opacity(0.6))
 | 
				
			||||||
 | 
					                                .cornerRadius(8)
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            Text(NSLocalizedString("RegistrationView_button_register", comment: "Зарегистрироваться"))
 | 
				
			||||||
 | 
					                                .foregroundColor(.white)
 | 
				
			||||||
 | 
					                                .padding()
 | 
				
			||||||
 | 
					                                .frame(maxWidth: .infinity)
 | 
				
			||||||
 | 
					                                .background(isFormValid ? Color.blue : Color.gray.opacity(0.6))
 | 
				
			||||||
 | 
					                                .cornerRadius(8)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    .disabled(!isFormValid)
 | 
				
			||||||
 | 
					                    .padding(.bottom)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            )
 | 
					                .padding()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                .navigationBarItems(trailing:
 | 
				
			||||||
 | 
					                        Button(action: {
 | 
				
			||||||
 | 
					                            presentationMode.wrappedValue.dismiss()
 | 
				
			||||||
 | 
					                        }) {
 | 
				
			||||||
 | 
					                            Text(NSLocalizedString("RegistrationView_close", comment: "Закрыть"))
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                .navigationTitle(Text(NSLocalizedString("RegistrationView_title", comment: "Регистрация")))
 | 
				
			||||||
 | 
					                .alert(isPresented: $showError) {
 | 
				
			||||||
 | 
					                    Alert(
 | 
				
			||||||
 | 
					                        title: Text(NSLocalizedString("RegistrationView_error", comment: "Ошибка")),
 | 
				
			||||||
 | 
					                        message: Text(errorMessage),
 | 
				
			||||||
 | 
					                        dismissButton: .default(Text(NSLocalizedString("ok", comment: "")))
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            .onTapGesture {
 | 
				
			||||||
 | 
					            hideKeyboard()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .onTapGesture {
 | 
				
			||||||
 | 
					        hideKeyboard()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private func registerUser() {
 | 
				
			||||||
 | 
					        isLoading = true
 | 
				
			||||||
 | 
					        errorMessage = ""
 | 
				
			||||||
 | 
					        viewModel.registerUser(username: username, password: password, invite: inviteCode.isEmpty ? nil : inviteCode) { success, message in
 | 
				
			||||||
 | 
					            isLoading = false
 | 
				
			||||||
 | 
					            if success {
 | 
				
			||||||
 | 
					                presentationMode.wrappedValue.dismiss()
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                errorMessage = message ?? "Неизвестная ошибка."
 | 
				
			||||||
 | 
					                showError = true
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private func hideKeyboard() {
 | 
				
			||||||
 | 
					        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct RegistrationView_Previews: PreviewProvider {
 | 
					struct RegistrationView_Previews: PreviewProvider {
 | 
				
			||||||
    static var previews: some View {
 | 
					    static var previews: some View {
 | 
				
			||||||
        RegistrationView()
 | 
					        let viewModel = LoginViewModel()
 | 
				
			||||||
 | 
					        viewModel.isLoading = false // чтобы убрать спиннер
 | 
				
			||||||
 | 
					        return RegistrationView(viewModel: viewModel)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,8 @@
 | 
				
			|||||||
import SwiftUI
 | 
					import SwiftUI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct SettingsTab: View {
 | 
					struct SettingsTab: View {
 | 
				
			||||||
 | 
					    @ObservedObject var viewModel: LoginViewModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var body: some View {
 | 
					    var body: some View {
 | 
				
			||||||
        NavigationView {
 | 
					        NavigationView {
 | 
				
			||||||
            VStack {
 | 
					            VStack {
 | 
				
			||||||
@ -17,6 +19,15 @@ struct SettingsTab: View {
 | 
				
			|||||||
                    .padding()
 | 
					                    .padding()
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                Spacer()
 | 
					                Spacer()
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                Button(action: {
 | 
				
			||||||
 | 
					                    viewModel.logoutCurrentUser()
 | 
				
			||||||
 | 
					                }) {
 | 
				
			||||||
 | 
					                    Text("Выйти из текущего пользователя")
 | 
				
			||||||
 | 
					                        .foregroundColor(.red)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                .padding()
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            .navigationTitle("Настройки")
 | 
					            .navigationTitle("Настройки")
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,12 @@
 | 
				
			|||||||
import SwiftUI
 | 
					import SwiftUI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct AppConfig {
 | 
					struct AppConfig {
 | 
				
			||||||
    static var DEBUG: Bool = false
 | 
					    static var DEBUG: Bool = true
 | 
				
			||||||
    static let SERVICE = Bundle.main.bundleIdentifier ?? "default.service"
 | 
					    static let SERVICE = Bundle.main.bundleIdentifier ?? "default.service"
 | 
				
			||||||
    static let PROTOCOL = "https"
 | 
					    static let PROTOCOL = "https"
 | 
				
			||||||
    static let API_SERVER = "\(PROTOCOL)://api.volnahub.ru"
 | 
					    static let API_SERVER = "\(PROTOCOL)://api.volnahub.ru"
 | 
				
			||||||
    static let SERVER_TIMEZONE = "GMT+3"
 | 
					    static let SERVER_TIMEZONE = "GMT+3"
 | 
				
			||||||
 | 
					    static let USER_AGENT = "volnahub ios"
 | 
				
			||||||
 | 
					    static let APP_BUILD = "freestore"
 | 
				
			||||||
 | 
					    static let APP_VERSION = "0.1"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@ import SwiftUI
 | 
				
			|||||||
struct volnahubApp: App {
 | 
					struct volnahubApp: App {
 | 
				
			||||||
    @AppStorage("isDarkMode") private var isDarkMode: Bool = true
 | 
					    @AppStorage("isDarkMode") private var isDarkMode: Bool = true
 | 
				
			||||||
    @StateObject private var viewModel = LoginViewModel()
 | 
					    @StateObject private var viewModel = LoginViewModel()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    var body: some Scene {
 | 
					    var body: some Scene {
 | 
				
			||||||
        WindowGroup {
 | 
					        WindowGroup {
 | 
				
			||||||
            ZStack {
 | 
					            ZStack {
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@
 | 
				
			|||||||
	<key>CFBundlePackageType</key>
 | 
						<key>CFBundlePackageType</key>
 | 
				
			||||||
	<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
 | 
						<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
 | 
				
			||||||
	<key>CFBundleShortVersionString</key>
 | 
						<key>CFBundleShortVersionString</key>
 | 
				
			||||||
	<string>1.0</string>
 | 
						<string>$(MARKETING_VERSION)</string>
 | 
				
			||||||
	<key>CFBundleVersion</key>
 | 
						<key>CFBundleVersion</key>
 | 
				
			||||||
	<string>$(CURRENT_PROJECT_VERSION)</string>
 | 
						<string>$(CURRENT_PROJECT_VERSION)</string>
 | 
				
			||||||
	<key>LSRequiresIPhoneOS</key>
 | 
						<key>LSRequiresIPhoneOS</key>
 | 
				
			||||||
 | 
				
			|||||||
@ -7,6 +7,8 @@
 | 
				
			|||||||
	objects = {
 | 
						objects = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* Begin PBXBuildFile section */
 | 
					/* Begin PBXBuildFile section */
 | 
				
			||||||
 | 
							1A0276032DF909F900D8BC53 /* refreshtokenex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0276022DF909F900D8BC53 /* refreshtokenex.swift */; };
 | 
				
			||||||
 | 
							1A0276112DF9247000D8BC53 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0276102DF9247000D8BC53 /* CustomTextField.swift */; };
 | 
				
			||||||
		1A79408D2DF77BC3002569DA /* volnahubApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A79407A2DF77BC2002569DA /* volnahubApp.swift */; };
 | 
							1A79408D2DF77BC3002569DA /* volnahubApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A79407A2DF77BC2002569DA /* volnahubApp.swift */; };
 | 
				
			||||||
		1A79408F2DF77BC3002569DA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A79407B2DF77BC2002569DA /* ContentView.swift */; };
 | 
							1A79408F2DF77BC3002569DA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A79407B2DF77BC2002569DA /* ContentView.swift */; };
 | 
				
			||||||
		1A7940902DF77BC3002569DA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A79407B2DF77BC2002569DA /* ContentView.swift */; };
 | 
							1A7940902DF77BC3002569DA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A79407B2DF77BC2002569DA /* ContentView.swift */; };
 | 
				
			||||||
@ -28,6 +30,8 @@
 | 
				
			|||||||
/* End PBXBuildFile section */
 | 
					/* End PBXBuildFile section */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* Begin PBXFileReference section */
 | 
					/* Begin PBXFileReference section */
 | 
				
			||||||
 | 
							1A0276022DF909F900D8BC53 /* refreshtokenex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = refreshtokenex.swift; sourceTree = "<group>"; };
 | 
				
			||||||
 | 
							1A0276102DF9247000D8BC53 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = "<group>"; };
 | 
				
			||||||
		1A79407A2DF77BC2002569DA /* volnahubApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = volnahubApp.swift; sourceTree = "<group>"; };
 | 
							1A79407A2DF77BC2002569DA /* volnahubApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = volnahubApp.swift; sourceTree = "<group>"; };
 | 
				
			||||||
		1A79407B2DF77BC2002569DA /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
 | 
							1A79407B2DF77BC2002569DA /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
 | 
				
			||||||
		1A79407C2DF77BC3002569DA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 | 
							1A79407C2DF77BC3002569DA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 | 
				
			||||||
@ -133,6 +137,7 @@
 | 
				
			|||||||
				1A7940CD2DF7A9AA002569DA /* SettingsTab.swift */,
 | 
									1A7940CD2DF7A9AA002569DA /* SettingsTab.swift */,
 | 
				
			||||||
				1A7940E62DF7B5E5002569DA /* SplashScreenView.swift */,
 | 
									1A7940E62DF7B5E5002569DA /* SplashScreenView.swift */,
 | 
				
			||||||
				1A79410B2DF7C81D002569DA /* RegistrationView.swift */,
 | 
									1A79410B2DF7C81D002569DA /* RegistrationView.swift */,
 | 
				
			||||||
 | 
									1A0276102DF9247000D8BC53 /* CustomTextField.swift */,
 | 
				
			||||||
			);
 | 
								);
 | 
				
			||||||
			path = Views;
 | 
								path = Views;
 | 
				
			||||||
			sourceTree = "<group>";
 | 
								sourceTree = "<group>";
 | 
				
			||||||
@ -157,6 +162,7 @@
 | 
				
			|||||||
			isa = PBXGroup;
 | 
								isa = PBXGroup;
 | 
				
			||||||
			children = (
 | 
								children = (
 | 
				
			||||||
				1A7940A12DF77DE9002569DA /* AuthService.swift */,
 | 
									1A7940A12DF77DE9002569DA /* AuthService.swift */,
 | 
				
			||||||
 | 
									1A0276022DF909F900D8BC53 /* refreshtokenex.swift */,
 | 
				
			||||||
			);
 | 
								);
 | 
				
			||||||
			path = Network;
 | 
								path = Network;
 | 
				
			||||||
			sourceTree = "<group>";
 | 
								sourceTree = "<group>";
 | 
				
			||||||
@ -280,8 +286,10 @@
 | 
				
			|||||||
				1A7940E72DF7B5E5002569DA /* SplashScreenView.swift in Sources */,
 | 
									1A7940E72DF7B5E5002569DA /* SplashScreenView.swift in Sources */,
 | 
				
			||||||
				1A7940B02DF77E26002569DA /* LoginView.swift in Sources */,
 | 
									1A7940B02DF77E26002569DA /* LoginView.swift in Sources */,
 | 
				
			||||||
				1A79410C2DF7C81D002569DA /* RegistrationView.swift in Sources */,
 | 
									1A79410C2DF7C81D002569DA /* RegistrationView.swift in Sources */,
 | 
				
			||||||
 | 
									1A0276112DF9247000D8BC53 /* CustomTextField.swift in Sources */,
 | 
				
			||||||
				1A7940CE2DF7A9AA002569DA /* SettingsTab.swift in Sources */,
 | 
									1A7940CE2DF7A9AA002569DA /* SettingsTab.swift in Sources */,
 | 
				
			||||||
				1A7940DE2DF7B0D7002569DA /* config.swift in Sources */,
 | 
									1A7940DE2DF7B0D7002569DA /* config.swift in Sources */,
 | 
				
			||||||
 | 
									1A0276032DF909F900D8BC53 /* refreshtokenex.swift in Sources */,
 | 
				
			||||||
				1A7940B62DF77F21002569DA /* MainView.swift in Sources */,
 | 
									1A7940B62DF77F21002569DA /* MainView.swift in Sources */,
 | 
				
			||||||
				1A7940E22DF7B1C5002569DA /* KeychainService.swift in Sources */,
 | 
									1A7940E22DF7B1C5002569DA /* KeychainService.swift in Sources */,
 | 
				
			||||||
				1A7940A62DF77DF5002569DA /* User.swift in Sources */,
 | 
									1A7940A62DF77DF5002569DA /* User.swift in Sources */,
 | 
				
			||||||
@ -434,8 +442,10 @@
 | 
				
			|||||||
			buildSettings = {
 | 
								buildSettings = {
 | 
				
			||||||
				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 | 
									ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 | 
				
			||||||
				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 | 
									ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 | 
				
			||||||
 | 
									CODE_SIGN_IDENTITY = "Apple Development";
 | 
				
			||||||
				CODE_SIGN_STYLE = Automatic;
 | 
									CODE_SIGN_STYLE = Automatic;
 | 
				
			||||||
				CURRENT_PROJECT_VERSION = 1337;
 | 
									CURRENT_PROJECT_VERSION = 1337;
 | 
				
			||||||
 | 
									DEVELOPMENT_TEAM = V22H44W47J;
 | 
				
			||||||
				ENABLE_PREVIEWS = YES;
 | 
									ENABLE_PREVIEWS = YES;
 | 
				
			||||||
				INFOPLIST_FILE = iOS/Info.plist;
 | 
									INFOPLIST_FILE = iOS/Info.plist;
 | 
				
			||||||
				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 | 
									IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 | 
				
			||||||
@ -443,8 +453,10 @@
 | 
				
			|||||||
					"$(inherited)",
 | 
										"$(inherited)",
 | 
				
			||||||
					"@executable_path/Frameworks",
 | 
										"@executable_path/Frameworks",
 | 
				
			||||||
				);
 | 
									);
 | 
				
			||||||
				PRODUCT_BUNDLE_IDENTIFIER = ckp.volnahub;
 | 
									MARKETING_VERSION = 0.1;
 | 
				
			||||||
 | 
									PRODUCT_BUNDLE_IDENTIFIER = ckp2.volnahub;
 | 
				
			||||||
				PRODUCT_NAME = volnahub;
 | 
									PRODUCT_NAME = volnahub;
 | 
				
			||||||
 | 
									PROVISIONING_PROFILE_SPECIFIER = "";
 | 
				
			||||||
				SDKROOT = iphoneos;
 | 
									SDKROOT = iphoneos;
 | 
				
			||||||
				SWIFT_VERSION = 5.0;
 | 
									SWIFT_VERSION = 5.0;
 | 
				
			||||||
				TARGETED_DEVICE_FAMILY = "1,2";
 | 
									TARGETED_DEVICE_FAMILY = "1,2";
 | 
				
			||||||
@ -456,8 +468,10 @@
 | 
				
			|||||||
			buildSettings = {
 | 
								buildSettings = {
 | 
				
			||||||
				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 | 
									ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 | 
				
			||||||
				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 | 
									ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 | 
				
			||||||
 | 
									CODE_SIGN_IDENTITY = "Apple Development";
 | 
				
			||||||
				CODE_SIGN_STYLE = Automatic;
 | 
									CODE_SIGN_STYLE = Automatic;
 | 
				
			||||||
				CURRENT_PROJECT_VERSION = 1337;
 | 
									CURRENT_PROJECT_VERSION = 1337;
 | 
				
			||||||
 | 
									DEVELOPMENT_TEAM = V22H44W47J;
 | 
				
			||||||
				ENABLE_PREVIEWS = YES;
 | 
									ENABLE_PREVIEWS = YES;
 | 
				
			||||||
				INFOPLIST_FILE = iOS/Info.plist;
 | 
									INFOPLIST_FILE = iOS/Info.plist;
 | 
				
			||||||
				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 | 
									IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 | 
				
			||||||
@ -465,8 +479,10 @@
 | 
				
			|||||||
					"$(inherited)",
 | 
										"$(inherited)",
 | 
				
			||||||
					"@executable_path/Frameworks",
 | 
										"@executable_path/Frameworks",
 | 
				
			||||||
				);
 | 
									);
 | 
				
			||||||
				PRODUCT_BUNDLE_IDENTIFIER = ckp.volnahub;
 | 
									MARKETING_VERSION = 0.1;
 | 
				
			||||||
 | 
									PRODUCT_BUNDLE_IDENTIFIER = ckp2.volnahub;
 | 
				
			||||||
				PRODUCT_NAME = volnahub;
 | 
									PRODUCT_NAME = volnahub;
 | 
				
			||||||
 | 
									PROVISIONING_PROFILE_SPECIFIER = "";
 | 
				
			||||||
				SDKROOT = iphoneos;
 | 
									SDKROOT = iphoneos;
 | 
				
			||||||
				SWIFT_VERSION = 5.0;
 | 
									SWIFT_VERSION = 5.0;
 | 
				
			||||||
				TARGETED_DEVICE_FAMILY = "1,2";
 | 
									TARGETED_DEVICE_FAMILY = "1,2";
 | 
				
			||||||
 | 
				
			|||||||
@ -7,12 +7,12 @@
 | 
				
			|||||||
		<key>volnahub (iOS).xcscheme_^#shared#^_</key>
 | 
							<key>volnahub (iOS).xcscheme_^#shared#^_</key>
 | 
				
			||||||
		<dict>
 | 
							<dict>
 | 
				
			||||||
			<key>orderHint</key>
 | 
								<key>orderHint</key>
 | 
				
			||||||
			<integer>1</integer>
 | 
								<integer>0</integer>
 | 
				
			||||||
		</dict>
 | 
							</dict>
 | 
				
			||||||
		<key>volnahub (macOS).xcscheme_^#shared#^_</key>
 | 
							<key>volnahub (macOS).xcscheme_^#shared#^_</key>
 | 
				
			||||||
		<dict>
 | 
							<dict>
 | 
				
			||||||
			<key>orderHint</key>
 | 
								<key>orderHint</key>
 | 
				
			||||||
			<integer>0</integer>
 | 
								<integer>1</integer>
 | 
				
			||||||
		</dict>
 | 
							</dict>
 | 
				
			||||||
	</dict>
 | 
						</dict>
 | 
				
			||||||
</dict>
 | 
					</dict>
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user