diff --git a/yobble/Network/APIModels.swift b/yobble/Network/APIModels.swift index f784726..7c9a0ae 100644 --- a/yobble/Network/APIModels.swift +++ b/yobble/Network/APIModels.swift @@ -25,3 +25,7 @@ struct ErrorResponse: Decodable { let data: ErrorPayload? let detail: String? } + +struct MessagePayload: Decodable { + let message: String +} diff --git a/yobble/Network/AuthService.swift b/yobble/Network/AuthService.swift index 2e9e30d..8054deb 100644 --- a/yobble/Network/AuthService.swift +++ b/yobble/Network/AuthService.swift @@ -126,6 +126,43 @@ final class AuthService { } } + func changePassword(oldPassword: String, newPassword: String, completion: @escaping (Bool, String?) -> Void) { + let payload = ChangePasswordRequestPayload(old_password: oldPassword, new_password: newPassword) + guard let body = try? JSONEncoder().encode(payload) else { + completion(false, NSLocalizedString("Не удалось сериализовать данные запроса.", comment: "")) + return + } + + NetworkClient.shared.request( + path: "/v1/auth/password/change", + method: .post, + body: body, + requiresAuth: true + ) { result in + switch result { + case .success(let response): + do { + let decoder = JSONDecoder() + let apiResponse = try decoder.decode(APIResponse.self, from: response.data) + + guard apiResponse.status == "fine" else { + let message = apiResponse.detail ?? NSLocalizedString("Не удалось обновить пароль.", comment: "") + completion(false, message) + return + } + + completion(true, apiResponse.data.message) + } catch { + completion(false, NSLocalizedString("Не удалось обработать ответ сервера.", comment: "")) + } + + case .failure(let error): + let message = self.changePasswordErrorMessage(for: error) + completion(false, message) + } + } + } + func logoutCurrentUser(completion: @escaping (Bool, String?) -> Void) { guard let currentUser = UserDefaults.standard.string(forKey: "currentUser") else { completion(false, "Не найден текущий пользователь.") @@ -237,6 +274,47 @@ final class AuthService { return message } + + private func changePasswordErrorMessage(for error: NetworkError) -> String { + switch error { + case .network(let err): + return String(format: NSLocalizedString("Ошибка сети: %@", comment: ""), err.localizedDescription) + case .server(let statusCode, let data): + if let message = extractMessage(from: data) { + return message + } + + switch statusCode { + case 401: + return NSLocalizedString("Необходимо авторизоваться заново.", comment: "") + case 403: + return NSLocalizedString("Старый пароль указан неверно или совпадает с новым.", comment: "") + case 422: + return NSLocalizedString("Проверьте данные и повторите попытку.", comment: "") + case 429: + return NSLocalizedString("Слишком много попыток. Попробуйте позже.", comment: "") + default: + return String(format: NSLocalizedString("Ошибка сервера: %@", comment: ""), "\(statusCode)") + } + case .unauthorized: + return NSLocalizedString("Необходимо авторизоваться заново.", comment: "") + case .invalidURL, .noResponse: + return NSLocalizedString("Некорректный ответ от сервера.", comment: "") + } + } + + private func extractMessage(from data: Data?) -> String? { + guard let data else { return nil } + if let response = try? JSONDecoder().decode(ErrorResponse.self, from: data) { + if let message = response.data?.message, !message.isEmpty { + return message + } + if let detail = response.detail, !detail.isEmpty { + return detail + } + } + return nil + } } private struct LoginRequest: Encodable { @@ -249,3 +327,8 @@ private struct RegisterRequest: Encodable { let password: String let invite: String? } + +private struct ChangePasswordRequestPayload: Encodable { + let old_password: String + let new_password: String +} diff --git a/yobble/Resources/Localizable.xcstrings b/yobble/Resources/Localizable.xcstrings index 0d21227..7a37d2a 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -380,6 +380,9 @@ }, "Не удалось загрузить чаты." : { + }, + "Не удалось обновить пароль." : { + }, "Не удалось обработать данные чатов." : { @@ -422,6 +425,9 @@ }, "Некорректный ответ от сервера." : { + }, + "Необходимо авторизоваться заново." : { + }, "Нет аккаунта? Регистрация" : { "comment" : "Регистрация" @@ -449,6 +455,9 @@ }, "Отправляем..." : { + }, + "Ошибка" : { + }, "Ошибка авторизации" : { @@ -473,12 +482,21 @@ }, "Пароли не совпадают" : { "comment" : "Пароли не совпадают" + }, + "Пароли не совпадают." : { + }, "Пароль" : { "comment" : "Пароль" }, "Пароль должен быть от 8 до 128 символов" : { "comment" : "Пароль должен быть от 6 до 32 символов" + }, + "Пароль обновлен" : { + + }, + "Пароль успешно обновлен." : { + }, "Перейдите в раздел \"Настройки > Сменить пароль\" и следуйте инструкциям." : { "comment" : "FAQ answer: reset password" @@ -520,6 +538,9 @@ }, "Применить" : { + }, + "Проверьте данные и повторите попытку." : { + }, "Произошла ошибка." : { @@ -564,6 +585,9 @@ }, "Слишком много запросов." : { + }, + "Слишком много попыток. Попробуйте позже." : { + }, "Сменить пароль" : { @@ -576,9 +600,15 @@ }, "Старый пароль" : { "comment" : "Старый пароль" + }, + "Старый пароль указан неверно или совпадает с новым." : { + }, "Темы" : { + }, + "Ты шо ебанутый? А ниче тот факт что новый пароль должен отличаться от старого." : { + }, "Уведомления" : { diff --git a/yobble/ViewModels/ChangePasswordViewModel.swift b/yobble/ViewModels/ChangePasswordViewModel.swift new file mode 100644 index 0000000..b898f93 --- /dev/null +++ b/yobble/ViewModels/ChangePasswordViewModel.swift @@ -0,0 +1,34 @@ +import Foundation +import Combine + +final class ChangePasswordViewModel: ObservableObject { + @Published private(set) var isLoading: Bool = false + @Published var successMessage: String? + @Published var errorMessage: String? + + private let authService: AuthService + + init(authService: AuthService = AuthService()) { + self.authService = authService + } + + func changePassword(oldPassword: String, newPassword: String) { + guard !isLoading else { return } + + isLoading = true + successMessage = nil + errorMessage = nil + + authService.changePassword(oldPassword: oldPassword, newPassword: newPassword) { [weak self] success, message in + guard let self else { return } + DispatchQueue.main.async { + self.isLoading = false + if success { + self.successMessage = message ?? NSLocalizedString("Пароль успешно обновлен.", comment: "") + } else { + self.errorMessage = message ?? NSLocalizedString("Не удалось обновить пароль.", comment: "") + } + } + } + } +} diff --git a/yobble/Views/Tab/Settings/ChangePasswordView.swift b/yobble/Views/Tab/Settings/ChangePasswordView.swift index 600fbbf..9e0a4e5 100644 --- a/yobble/Views/Tab/Settings/ChangePasswordView.swift +++ b/yobble/Views/Tab/Settings/ChangePasswordView.swift @@ -1,31 +1,35 @@ import SwiftUI - - struct ChangePasswordView: View { + @StateObject private var viewModel = ChangePasswordViewModel() @State private var oldPassword = "" @State private var newPassword = "" @State private var confirmPassword = "" @State private var isOldPasswordVisible = false @State private var isNewPasswordVisible = false @State private var isConfirmPasswordVisible = false + @State private var alertData: AlertData? private var isOldPasswordValid: Bool { - return oldPassword.count >= 8 && oldPassword.count <= 128 + oldPassword.count >= 8 && oldPassword.count <= 128 } private var isOldPasswordSame: Bool { - return oldPassword == newPassword + oldPassword == newPassword } - + private var isNewPasswordValid: Bool { - return newPassword.count >= 8 && newPassword.count <= 128 + newPassword.count >= 8 && newPassword.count <= 128 } - + private var isPasswordConfirmValid: Bool { - return newPassword == confirmPassword + newPassword == confirmPassword } - + + private var isButtonEnabled: Bool { + isPasswordConfirmValid && !isOldPasswordSame && isNewPasswordValid && isOldPasswordValid && !viewModel.isLoading + } + var body: some View { Form { Section { @@ -86,12 +90,17 @@ struct ChangePasswordView: View { if !newPassword.isEmpty { let isAllValid = isNewPasswordValid && !isOldPasswordSame - + Image(systemName: isAllValid ? "checkmark.circle" : "xmark.circle") .foregroundColor(isAllValid ? .green : .red) } } - + if isOldPasswordSame && !newPassword.isEmpty { + Text(NSLocalizedString("Ты шо ебанутый? А ниче тот факт что новый пароль должен отличаться от старого.", comment: "")) + .font(.caption) + .foregroundColor(.red) + } + HStack { if isConfirmPasswordVisible { TextField(NSLocalizedString("Подтверждение пароля", comment: "Подтверждение пароля"), text: $confirmPassword) @@ -121,27 +130,34 @@ struct ChangePasswordView: View { .foregroundColor(isPasswordConfirmValid ? .green : .red) } } - - - + if !confirmPassword.isEmpty && !isPasswordConfirmValid { + Text(NSLocalizedString("Пароли не совпадают.", comment: "")) + .font(.caption) + .foregroundColor(.red) + } + + + } - var isButtonEnabled: Bool { - isPasswordConfirmValid && !isOldPasswordSame && isNewPasswordValid && isOldPasswordValid - } - Button(action: { - // Действие для сохранения профиля - print("oldPassword: \(oldPassword)") - print("newPassword: \(newPassword)") - print("confirmPassword: \(confirmPassword)") + viewModel.changePassword(oldPassword: oldPassword, newPassword: newPassword) }) { - Text(NSLocalizedString("Применить", comment: "")) - .foregroundColor(.white) - .padding() - .frame(maxWidth: .infinity) - .background(isButtonEnabled ? Color.blue : Color.gray) - .cornerRadius(8) + if viewModel.isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .padding() + .frame(maxWidth: .infinity) + .background(Color.gray.opacity(0.6)) + .cornerRadius(8) + } else { + Text(NSLocalizedString("Применить", comment: "")) + .foregroundColor(.white) + .padding() + .frame(maxWidth: .infinity) + .background(isButtonEnabled ? Color.blue : Color.gray) + .cornerRadius(8) + } } .disabled(!isButtonEnabled) .buttonStyle(PlainButtonStyle()) @@ -149,5 +165,44 @@ struct ChangePasswordView: View { .listRowBackground(Color.clear) } .navigationTitle(NSLocalizedString("Изменение пароля", comment: "")) + .onChange(of: viewModel.successMessage) { message in + guard let message else { return } + alertData = AlertData(kind: .success, message: message) + } + .onChange(of: viewModel.errorMessage) { message in + guard let message else { return } + alertData = AlertData(kind: .error, message: message) + } + .alert(item: $alertData) { data in + Alert( + title: Text(data.kind == .success + ? NSLocalizedString("Пароль обновлен", comment: "") + : NSLocalizedString("Ошибка", comment: "")), + message: Text(data.message), + dismissButton: .default(Text(NSLocalizedString("OK", comment: ""))) { + switch data.kind { + case .success: + oldPassword = "" + newPassword = "" + confirmPassword = "" + viewModel.successMessage = nil + case .error: + viewModel.errorMessage = nil + } + alertData = nil + } + ) + } } } + +private struct AlertData: Identifiable { + enum Kind { + case success + case error + } + + let id = UUID() + let kind: Kind + let message: String +}