add network manager

This commit is contained in:
cheykrym 2025-10-06 03:43:00 +03:00
parent 24ca5643eb
commit 200b0172eb
4 changed files with 486 additions and 219 deletions

View File

@ -0,0 +1,27 @@
import Foundation
struct APIResponse<T: Decodable>: Decodable {
let status: String
let data: T
let detail: String?
}
struct TokenPairPayload: Decodable {
let access_token: String
let refresh_token: String
let user_id: String?
}
struct RegisterPayload: Decodable {
let message: String
}
struct ErrorPayload: Decodable {
let message: String?
}
struct ErrorResponse: Decodable {
let status: String?
let data: ErrorPayload?
let detail: String?
}

View File

@ -7,296 +7,245 @@
import Foundation import Foundation
class AuthService { final class AuthService {
func autoLogin(completion: @escaping (Bool, String?) -> Void) { func autoLogin(completion: @escaping (Bool, String?) -> Void) {
// 1 Проверяем наличие текущего пользователя
if let currentUser = UserDefaults.standard.string(forKey: "currentUser"), if let currentUser = UserDefaults.standard.string(forKey: "currentUser"),
let _ = KeychainService.shared.get(forKey: "access_token", service: currentUser), let _ = KeychainService.shared.get(forKey: "access_token", service: currentUser),
let _ = KeychainService.shared.get(forKey: "refresh_token", service: currentUser) { let _ = KeychainService.shared.get(forKey: "refresh_token", service: currentUser) {
if AppConfig.DEBUG{ print("AutoLogin: найден текущий пользователь — \(currentUser)")} if AppConfig.DEBUG { print("AutoLogin: найден текущий пользователь — \(currentUser)") }
completion(true, nil) completion(true, nil)
return return
} }
// 2 Текущий пользователь не найден или токены отсутствуют if AppConfig.DEBUG { print("AutoLogin: текущий пользователь не найден или токены отсутствуют. Пробуем найти другого пользователя...") }
if AppConfig.DEBUG{ print("AutoLogin: текущий пользователь не найден или токены отсутствуют. Пробуем найти другого пользователя...")}
let allUsers = KeychainService.shared.getAllServices() let allUsers = KeychainService.shared.getAllServices()
for user in allUsers { for user in allUsers {
let hasAccessToken = KeychainService.shared.get(forKey: "access_token", service: user) != nil let hasAccessToken = KeychainService.shared.get(forKey: "access_token", service: user) != nil
let hasRefreshToken = KeychainService.shared.get(forKey: "refresh_token", service: user) != nil let hasRefreshToken = KeychainService.shared.get(forKey: "refresh_token", service: user) != nil
if hasAccessToken && hasRefreshToken { if hasAccessToken && hasRefreshToken {
// Нашли пользователя с токенами назначаем как currentUser
UserDefaults.standard.set(user, forKey: "currentUser") UserDefaults.standard.set(user, forKey: "currentUser")
if AppConfig.DEBUG{ print("AutoLogin: переключились на пользователя \(user)")} if AppConfig.DEBUG { print("AutoLogin: переключились на пользователя \(user)") }
completion(true, nil) completion(true, nil)
return return
} }
} }
// 3 Если никто не найден
// completion(false, "Не найден авторизованный пользователь. Пожалуйста, войдите снова.")
completion(false, nil) completion(false, nil)
} }
func login(username: String, password: String, completion: @escaping (Bool, String?) -> Void) { func login(username: String, password: String, completion: @escaping (Bool, String?) -> Void) {
let url = URL(string: "\(AppConfig.API_SERVER)/v1/auth/login")! let payload = LoginRequest(login: username, password: password)
var request = URLRequest(url: url) guard let body = try? JSONEncoder().encode(payload) else {
request.httpMethod = "POST" completion(false, NSLocalizedString("Не удалось сериализовать данные запроса.", comment: ""))
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("Не удалось сериализовать данные запроса.", comment: ""))
}
return return
} }
let task = URLSession.shared.dataTask(with: request) { data, response, error in NetworkClient.shared.request(
DispatchQueue.main.async { path: "/v1/auth/login",
if let error = error { method: .post,
let errorMessage = String(format: NSLocalizedString("Ошибка сети: %@", comment: ""), error.localizedDescription) body: body,
completion(false, errorMessage) requiresAuth: false
return ) { result in
} switch result {
case .success(let response):
guard let httpResponse = response as? HTTPURLResponse else {
completion(false, NSLocalizedString("Некорректный ответ от сервера.", comment: ""))
return
}
guard (200...299).contains(httpResponse.statusCode) else {
if httpResponse.statusCode == 401{
completion(false, NSLocalizedString("Неверный логин или пароль.", comment: ""))
} else if httpResponse.statusCode == 502{
completion(false, NSLocalizedString("Сервер не отвечает. Попробуйте позже.", comment: ""))
} else if httpResponse.statusCode == 429 {
completion(false, NSLocalizedString("Слишком много запросов.", comment: ""))
} else {
let errorMessage = String(format: NSLocalizedString("Ошибка сервера: %@", comment: ""), "\(httpResponse.statusCode)")
completion(false, errorMessage)
}
return
}
guard let data = data else {
completion(false, NSLocalizedString("Пустой ответ от сервера.", comment: ""))
return
}
do { do {
let decoder = JSONDecoder() let decoder = JSONDecoder()
let loginResponse = try decoder.decode(APIResponse<LoginPayload>.self, from: data).data let apiResponse = try decoder.decode(APIResponse<TokenPairPayload>.self, from: response.data)
// Сохраняем токены в Keychain guard apiResponse.status == "fine" else {
KeychainService.shared.save(loginResponse.access_token, forKey: "access_token", service: username) let message = apiResponse.detail ?? NSLocalizedString("Неизвестная ошибка", comment: "")
KeychainService.shared.save(loginResponse.refresh_token, forKey: "refresh_token", service: username) completion(false, message)
KeychainService.shared.save(loginResponse.user_id, forKey: "userId", service: username) return
}
let tokens = apiResponse.data
KeychainService.shared.save(tokens.access_token, forKey: "access_token", service: username)
KeychainService.shared.save(tokens.refresh_token, forKey: "refresh_token", service: username)
if let userId = tokens.user_id {
KeychainService.shared.save(userId, forKey: "userId", service: username)
}
UserDefaults.standard.set(username, forKey: "currentUser") UserDefaults.standard.set(username, forKey: "currentUser")
completion(true, nil) completion(true, nil)
} catch { } catch {
completion(false, NSLocalizedString("Не удалось обработать ответ сервера.", comment: "")) completion(false, NSLocalizedString("Не удалось обработать ответ сервера.", comment: ""))
} }
case .failure(let error):
let message = self.loginErrorMessage(for: error)
completion(false, message)
} }
} }
task.resume()
} }
func register(username: String, password: String, invite: String?, completion: @escaping (Bool, String?) -> Void) { func register(username: String, password: String, invite: String?, completion: @escaping (Bool, String?) -> Void) {
let url = URL(string: "\(AppConfig.API_SERVER)/v1/auth/register")! let payload = RegisterRequest(login: username, password: password, invite: invite)
var request = URLRequest(url: url) guard let body = try? JSONEncoder().encode(payload) else {
request.httpMethod = "POST" completion(false, NSLocalizedString("Не удалось сериализовать данные запроса.", comment: ""))
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("Не удалось сериализовать данные запроса.", comment: ""))
}
return return
} }
let task = URLSession.shared.dataTask(with: request) { data, response, error in NetworkClient.shared.request(
DispatchQueue.main.async { path: "/v1/auth/register",
if let error = error { method: .post,
let errorMessage = String(format: NSLocalizedString("Ошибка сети: %@", comment: ""), error.localizedDescription) body: body,
completion(false, errorMessage) requiresAuth: false
return ) { result in
} switch result {
case .success(let response):
guard let httpResponse = response as? HTTPURLResponse else { do {
completion(false, NSLocalizedString("Некорректный ответ от сервера.", comment: "")) let decoder = JSONDecoder()
return let apiResponse = try decoder.decode(APIResponse<RegisterPayload>.self, from: response.data)
}
guard let data = data else {
completion(false, NSLocalizedString("Пустой ответ от сервера.", comment: ""))
return
}
let decoder = JSONDecoder()
if (200...299).contains(httpResponse.statusCode) {
do {
let _ = try decoder.decode(APIResponse<RegisterPayload>.self, from: data)
if AppConfig.DEBUG{ print("Регистрация успешна. Пытаемся сразу войти...")}
// Сразу логинимся guard apiResponse.status == "fine" else {
self.login(username: username, password: password) { loginSuccess, loginMessage in let message = apiResponse.detail ?? NSLocalizedString("Неизвестная ошибка", comment: "")
if loginSuccess { completion(false, message)
completion(true, "Регистрация и вход выполнены успешно.") return
} else {
// Регистрация успешна, но логин не удался покажем сообщение
completion(false, loginMessage ?? NSLocalizedString("Регистрация выполнена, но вход не удался.", comment: ""))
}
}
} catch {
completion(false, NSLocalizedString("Не удалось обработать ответ сервера.", comment: ""))
} }
} else {
// Ошибка сервера пробуем распарсить message if AppConfig.DEBUG { print("Регистрация успешна. Пытаемся сразу войти...") }
if let errorResponseMessage = try? decoder.decode(ErrorResponse.self, from: data),
let message = errorResponseMessage.data?.message { self.login(username: username, password: password) { loginSuccess, loginMessage in
if loginSuccess {
if let jsonString = String(data: data, encoding: .utf8) { completion(true, NSLocalizedString("Регистрация и вход выполнены успешно.", comment: ""))
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("Неверный код приглашения.", comment: ""))
} else if message.contains("This invitation is not active") {
completion(false, NSLocalizedString("Приглашение не активно.", comment: ""))
} else if message.contains("This invitation has reached its usage limit") {
completion(false, NSLocalizedString("Приглашение достигло лимита использования.", comment: ""))
} else if message.contains("This invitation has expired") {
completion(false, NSLocalizedString("Приглашение истекло.", comment: ""))
} else if message.contains("Login already registered") {
completion(false, NSLocalizedString("Логин уже занят.", comment: ""))
} else {
completion(false, message)
}
} else if httpResponse.statusCode == 403 {
if message.contains("Registration is currently disabled") {
completion(false, NSLocalizedString("Регистрация временно недоступна.", comment: ""))
} else {
completion(false, message)
}
} else if httpResponse.statusCode == 429 {
completion(false, NSLocalizedString("Слишком много запросов.", comment: ""))
} else if httpResponse.statusCode == 502{
completion(false, NSLocalizedString("Сервер не отвечает. Попробуйте позже.", comment: ""))
} else { } else {
let errorMessage = String(format: NSLocalizedString("Ошибка сервера: %@", comment: ""), "\(httpResponse.statusCode)") completion(false, loginMessage ?? NSLocalizedString("Регистрация выполнена, но вход не удался.", comment: ""))
completion(false, errorMessage)
}
} else {
// Не удалось распарсить JSON fallback
if httpResponse.statusCode == 400 {
completion(false, NSLocalizedString("Неверный запрос (400).", comment: ""))
} else if httpResponse.statusCode == 403 {
completion(false, NSLocalizedString("Регистрация запрещена.", comment: ""))
} else if httpResponse.statusCode == 429 {
completion(false, NSLocalizedString("Слишком много запросов.", comment: ""))
} else if httpResponse.statusCode == 502{
completion(false, NSLocalizedString("Сервер не отвечает. Попробуйте позже.", comment: ""))
} else {
let errorMessage = String(format: NSLocalizedString("Ошибка сервера: %@", comment: ""), "\(httpResponse.statusCode)")
completion(false, errorMessage)
} }
} }
} catch {
completion(false, NSLocalizedString("Не удалось обработать ответ сервера.", comment: ""))
} }
case .failure(let error):
let message = self.registerErrorMessage(for: error)
completion(false, message)
} }
} }
task.resume()
} }
func logoutCurrentUser(completion: @escaping (Bool, String?) -> Void) { func logoutCurrentUser(completion: @escaping (Bool, String?) -> Void) {
guard let currentUser = UserDefaults.standard.string(forKey: "currentUser") else { guard let currentUser = UserDefaults.standard.string(forKey: "currentUser") else {
completion(false, "Не найден текущий пользователь.") completion(false, "Не найден текущий пользователь.")
return return
} }
// Удаляем токены текущего пользователя
KeychainService.shared.delete(forKey: "access_token", service: currentUser) KeychainService.shared.delete(forKey: "access_token", service: currentUser)
KeychainService.shared.delete(forKey: "refresh_token", service: currentUser) KeychainService.shared.delete(forKey: "refresh_token", service: currentUser)
KeychainService.shared.delete(forKey: "userId", service: currentUser) KeychainService.shared.delete(forKey: "userId", service: currentUser)
// Сбрасываем текущего пользователя
UserDefaults.standard.removeObject(forKey: "currentUser") UserDefaults.standard.removeObject(forKey: "currentUser")
// Пробуем переключиться на другого пользователя
let allUsers = KeychainService.shared.getAllServices() let allUsers = KeychainService.shared.getAllServices()
for user in allUsers { for user in allUsers {
let hasAccessToken = KeychainService.shared.get(forKey: "access_token", service: user) != nil let hasAccessToken = KeychainService.shared.get(forKey: "access_token", service: user) != nil
let hasRefreshToken = KeychainService.shared.get(forKey: "refresh_token", service: user) != nil let hasRefreshToken = KeychainService.shared.get(forKey: "refresh_token", service: user) != nil
if hasAccessToken && hasRefreshToken { if hasAccessToken && hasRefreshToken {
UserDefaults.standard.set(user, forKey: "currentUser") UserDefaults.standard.set(user, forKey: "currentUser")
if AppConfig.DEBUG{ print("Logout: переключились на пользователя \(user)")} if AppConfig.DEBUG { print("Logout: переключились на пользователя \(user)") }
completion(true, nil) completion(true, nil)
return return
} }
} }
// Если пользователей больше нет
// completion(false, "Нет доступных пользователей. Пожалуйста, войдите снова.")
completion(false, nil) completion(false, nil)
} }
private func loginErrorMessage(for error: NetworkError) -> String {
switch error {
case .network(let err):
return String(format: NSLocalizedString("Ошибка сети: %@", comment: ""), err.localizedDescription)
case .server(let statusCode, _):
switch statusCode {
case 401:
return NSLocalizedString("Неверный логин или пароль.", comment: "")
case 429:
return NSLocalizedString("Слишком много запросов.", comment: "")
case 502:
return NSLocalizedString("Сервер не отвечает. Попробуйте позже.", comment: "")
default:
return String(format: NSLocalizedString("Ошибка сервера: %@", comment: ""), "\(statusCode)")
}
case .unauthorized:
return NSLocalizedString("Неверный логин или пароль.", comment: "")
case .invalidURL, .noResponse:
return NSLocalizedString("Некорректный ответ от сервера.", comment: "")
}
}
private func registerErrorMessage(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 data,
let response = try? JSONDecoder().decode(ErrorResponse.self, from: data),
let message = response.data?.message {
return mappedRegistrationMessage(for: message, statusCode: statusCode)
}
switch statusCode {
case 400:
return NSLocalizedString("Неверный запрос (400).", comment: "")
case 403:
return NSLocalizedString("Регистрация запрещена.", comment: "")
case 429:
return NSLocalizedString("Слишком много запросов.", comment: "")
case 502:
return NSLocalizedString("Сервер не отвечает. Попробуйте позже.", comment: "")
default:
return String(format: NSLocalizedString("Ошибка сервера: %@", comment: ""), "\(statusCode)")
}
case .unauthorized:
return NSLocalizedString("Регистрация запрещена.", comment: "")
case .invalidURL, .noResponse:
return NSLocalizedString("Некорректный ответ от сервера.", comment: "")
}
}
private func mappedRegistrationMessage(for message: String, statusCode: Int) -> String {
if statusCode == 400 {
if message.contains("Invalid invitation code") {
return NSLocalizedString("Неверный код приглашения.", comment: "")
} else if message.contains("This invitation is not active") {
return NSLocalizedString("Приглашение не активно.", comment: "")
} else if message.contains("This invitation has reached its usage limit") {
return NSLocalizedString("Приглашение достигло лимита использования.", comment: "")
} else if message.contains("This invitation has expired") {
return NSLocalizedString("Приглашение истекло.", comment: "")
} else if message.contains("Login already registered") {
return NSLocalizedString("Логин уже занят.", comment: "")
}
}
if statusCode == 403 {
if message.contains("Registration is currently disabled") {
return NSLocalizedString("Регистрация временно недоступна.", comment: "")
}
}
if statusCode == 429 {
return NSLocalizedString("Слишком много запросов.", comment: "")
}
if statusCode == 502 {
return NSLocalizedString("Сервер не отвечает. Попробуйте позже.", comment: "")
}
return message
}
} }
struct APIResponse<T: Decodable>: Decodable { private struct LoginRequest: Encodable {
let status: String let login: String
let data: T let password: String
} }
struct LoginPayload: Decodable { private struct RegisterRequest: Encodable {
let access_token: String let login: String
let refresh_token: String let password: String
let user_id: String let invite: String?
}
struct TokenRefreshPayload: Decodable {
let access_token: String
let token_type: String
}
struct RegisterPayload: Decodable {
let message: String
}
struct ErrorPayload: Decodable {
let message: String?
}
struct ErrorResponse: Decodable {
let status: String?
let data: ErrorPayload?
} }

View File

@ -0,0 +1,291 @@
import Foundation
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
}
struct NetworkResponse {
let statusCode: Int
let headers: [AnyHashable: Any]
let data: Data
}
enum NetworkError: Error {
case invalidURL
case unauthorized
case network(Error)
case noResponse
case server(statusCode: Int, data: Data?)
}
final class NetworkClient {
static let shared = NetworkClient()
private let session: URLSession
private let refreshQueue = DispatchQueue(label: "org.yobble.network.refresh")
private var isRefreshing = false
private var refreshCompletions: [(Bool) -> Void] = []
init(session: URLSession = .shared) {
self.session = session
}
func request(
path: String,
method: HTTPMethod = .get,
query: [String: String?]? = nil,
headers: [String: String] = [:],
body: Data? = nil,
contentType: String? = nil,
requiresAuth: Bool = false,
callbackQueue: DispatchQueue = .main,
completion: @escaping (Result<NetworkResponse, NetworkError>) -> Void
) {
performRequest(
path: path,
method: method,
query: query,
headers: headers,
body: body,
contentType: contentType,
requiresAuth: requiresAuth,
callbackQueue: callbackQueue,
allowRefreshRetry: true,
isRetry: false,
completion: completion
)
}
private func performRequest(
path: String,
method: HTTPMethod,
query: [String: String?]?,
headers: [String: String],
body: Data?,
contentType: String?,
requiresAuth: Bool,
callbackQueue: DispatchQueue,
allowRefreshRetry: Bool,
isRetry: Bool,
completion: @escaping (Result<NetworkResponse, NetworkError>) -> Void
) {
guard let url = buildURL(path: path, query: query) else {
callbackQueue.async {
completion(.failure(.invalidURL))
}
return
}
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
request.httpBody = body
var allHeaders: [String: String] = [
"User-Agent": AppConfig.USER_AGENT,
"Accept": "application/json"
]
if let contentType = contentType ?? (body != nil ? "application/json" : nil),
headers["Content-Type"] == nil {
allHeaders["Content-Type"] = contentType
}
headers.forEach { allHeaders[$0.key] = $0.value }
if requiresAuth {
guard let tokenInfo = currentTokenInfo() else {
callbackQueue.async {
completion(.failure(.unauthorized))
}
return
}
allHeaders["Authorization"] = "Bearer \(tokenInfo.access)"
}
for (key, value) in allHeaders {
request.setValue(value, forHTTPHeaderField: key)
}
let task = session.dataTask(with: request) { [weak self] data, response, error in
guard let self else { return }
if let error = error {
callbackQueue.async {
completion(.failure(.network(error)))
}
return
}
guard let httpResponse = response as? HTTPURLResponse else {
callbackQueue.async {
completion(.failure(.noResponse))
}
return
}
let responseData = data ?? Data()
if (200...299).contains(httpResponse.statusCode) {
let payload = NetworkResponse(
statusCode: httpResponse.statusCode,
headers: httpResponse.allHeaderFields,
data: responseData
)
callbackQueue.async {
completion(.success(payload))
}
return
}
if httpResponse.statusCode == 401,
requiresAuth,
allowRefreshRetry,
!isRetry {
self.refreshAccessToken { success in
if success {
self.performRequest(
path: path,
method: method,
query: query,
headers: headers,
body: body,
contentType: contentType,
requiresAuth: requiresAuth,
callbackQueue: callbackQueue,
allowRefreshRetry: false,
isRetry: true,
completion: completion
)
} else {
callbackQueue.async {
completion(.failure(.unauthorized))
}
}
}
return
}
callbackQueue.async {
completion(.failure(.server(statusCode: httpResponse.statusCode, data: responseData)))
}
}
task.resume()
}
private func buildURL(path: String, query: [String: String?]?) -> URL? {
guard let baseURL = URL(string: AppConfig.API_SERVER) else {
return nil
}
var cleanedPath = path
if cleanedPath.hasPrefix("/") {
cleanedPath.removeFirst()
}
let url = cleanedPath.isEmpty ? baseURL : baseURL.appendingPathComponent(cleanedPath)
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return nil
}
if let query {
let items = query.compactMap { key, value -> URLQueryItem? in
guard let value else { return nil }
return URLQueryItem(name: key, value: value)
}
if !items.isEmpty {
components.queryItems = items
}
}
return components.url
}
private func currentTokenInfo() -> (login: String, access: String, refresh: String)? {
guard
let login = UserDefaults.standard.string(forKey: "currentUser"),
let access = KeychainService.shared.get(forKey: "access_token", service: login),
let refresh = KeychainService.shared.get(forKey: "refresh_token", service: login)
else {
return nil
}
return (login, access, refresh)
}
private func refreshAccessToken(completion: @escaping (Bool) -> Void) {
refreshQueue.async {
if self.isRefreshing {
self.refreshCompletions.append(completion)
return
}
guard let tokenInfo = self.currentTokenInfo() else {
completion(false)
return
}
self.isRefreshing = true
self.refreshCompletions.append(completion)
let payload = RefreshRequest(access_token: tokenInfo.access, refresh_token: tokenInfo.refresh)
guard let body = try? JSONEncoder().encode(payload) else {
self.completeRefresh(success: false)
return
}
self.performRequest(
path: "/v1/auth/token/refresh",
method: .post,
query: nil,
headers: [:],
body: body,
contentType: "application/json",
requiresAuth: false,
callbackQueue: self.refreshQueue,
allowRefreshRetry: false,
isRetry: false
) { result in
switch result {
case .success(let response):
do {
let decoder = JSONDecoder()
let apiResponse = try decoder.decode(APIResponse<TokenPairPayload>.self, from: response.data)
guard apiResponse.status == "fine" else {
self.completeRefresh(success: false)
return
}
let data = apiResponse.data
KeychainService.shared.save(data.access_token, forKey: "access_token", service: tokenInfo.login)
KeychainService.shared.save(data.refresh_token, forKey: "refresh_token", service: tokenInfo.login)
if let userId = data.user_id {
KeychainService.shared.save(userId, forKey: "userId", service: tokenInfo.login)
}
self.completeRefresh(success: true)
} catch {
self.completeRefresh(success: false)
}
case .failure:
self.completeRefresh(success: false)
}
}
}
}
private func completeRefresh(success: Bool) {
let completions = refreshCompletions
refreshCompletions.removeAll()
isRefreshing = false
completions.forEach { $0(success) }
}
}
private struct RefreshRequest: Encodable {
let access_token: String
let refresh_token: String
}

View File

@ -432,9 +432,6 @@
}, },
"Публичная информация" : { "Публичная информация" : {
},
"Пустой ответ от сервера." : {
}, },
"Регистрация" : { "Регистрация" : {
"comment" : "Регистрация" "comment" : "Регистрация"
@ -447,6 +444,9 @@
}, },
"Регистрация запрещена." : { "Регистрация запрещена." : {
},
"Регистрация и вход выполнены успешно." : {
}, },
"Редактировать профиль" : { "Редактировать профиль" : {