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()
|
||||||
|
//}
|
||||||
@ -3,5 +3,52 @@
|
|||||||
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,7 +11,7 @@ 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
|
||||||
@ -19,20 +19,31 @@ class LoginViewModel: ObservableObject {
|
|||||||
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 {
|
// Показываем поле пароля (даже если оно невалидное) только если логин корректен
|
||||||
Button(action: {
|
if isUsernameValid || !viewModel.password.isEmpty {
|
||||||
viewModel.login()
|
SecureField(NSLocalizedString("LoginView_password", comment: ""), text: $viewModel.password)
|
||||||
}) {
|
.padding()
|
||||||
if viewModel.isLoading {
|
.background(Color(.secondarySystemBackground))
|
||||||
ProgressView()
|
.cornerRadius(8)
|
||||||
.progressViewStyle(CircularProgressViewStyle())
|
.autocapitalization(.none)
|
||||||
.padding()
|
.onChange(of: viewModel.password) { newValue in
|
||||||
.frame(maxWidth: .infinity)
|
if newValue.count > 32 {
|
||||||
.background(Color.gray.opacity(0.6))
|
viewModel.password = String(newValue.prefix(32))
|
||||||
.cornerRadius(8)
|
}
|
||||||
} else {
|
}
|
||||||
Text(NSLocalizedString("LoginView_button_login", comment: ""))
|
|
||||||
.foregroundColor(.white)
|
// Показываем ошибку для пароля
|
||||||
.padding()
|
if !isPasswordValid && !viewModel.password.isEmpty {
|
||||||
.frame(maxWidth: .infinity)
|
Text(NSLocalizedString("LoginView_error_password_invalid", comment: "Неверный пароль"))
|
||||||
.background(Color.blue)
|
.foregroundColor(.red)
|
||||||
.cornerRadius(8)
|
.font(.caption)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabled(viewModel.isLoading)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
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()
|
||||||
Button(action: {
|
|
||||||
isShowingRegistration = true
|
|
||||||
}) {
|
|
||||||
Text(NSLocalizedString("LoginView_button_register", comment: "Регистрация"))
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
}
|
|
||||||
.padding(.top, 10)
|
|
||||||
.sheet(isPresented: $isShowingRegistration) {
|
|
||||||
RegistrationView()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Кнопка регистрации
|
||||||
|
Button(action: {
|
||||||
|
isShowingRegistration = true
|
||||||
|
}) {
|
||||||
|
Text(NSLocalizedString("LoginView_button_register", comment: "Регистрация"))
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
}
|
||||||
|
.padding(.top, 10)
|
||||||
|
.sheet(isPresented: $isShowingRegistration) {
|
||||||
|
RegistrationView(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.alert(isPresented: $viewModel.showError) {
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
@ -142,6 +155,10 @@ struct LoginView: View {
|
|||||||
UIApplication.shared.open(url)
|
UIApplication.shared.open(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func hideKeyboard() {
|
||||||
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LoginView_Previews: PreviewProvider {
|
struct LoginView_Previews: PreviewProvider {
|
||||||
|
|||||||
@ -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 @@
|
|||||||
<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