Open bootuz opened 1 year ago
Here is the code, I wrote from the video if someone needs it.
import Foundation
import Combine
protocol HTTPClient {
func publisher(request: URLRequest) -> AnyPublisher<(Data, HTTPURLResponse), Error>
}
extension URLSession: HTTPClient {
struct InvalidHTTPResponseError: Error {}
func publisher(request: URLRequest) -> AnyPublisher<(Data, HTTPURLResponse), any Error> {
return dataTaskPublisher(for: request)
.tryMap({ result in
guard let httpResponse = result.response as? HTTPURLResponse else {
throw InvalidHTTPResponseError()
}
return (result.data, httpResponse)
})
.eraseToAnyPublisher()
}
}
protocol TokenProvider {
func tokenPublisher() -> AnyPublisher<AuthenticationJWTDTO, Error>
}
//enum LoginState {
// case loggedIn
// case loggedOut
//}
//class ViewState: ObservableObject {
// @Published var loginState: LoginState
//
// func logout() {
// loginState = .loggedOut
// }
//}
class CountryListService {
let httpClient: HTTPClient
init(httpClient: HTTPClient) {
self.httpClient = httpClient
}
func loadAllCountries() -> AnyPublisher<Root<LegalCase>, Error> {
return httpClient
.publisher(request: LanguageCountryProvider.getCountries.makeRequest)
.tryMap(GenericAPIHTTPRequestMapper.map)
.eraseToAnyPublisher()
}
}
private let customDateJSONDecoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom(customDateDecodingStrategy)
return decoder
}()
public func customDateDecodingStrategy(decoder: Decoder) throws -> Date {
let container = try decoder.singleValueContainer()
let str = try container.decode(String.self)
return try Date.dateFromString(str)
}
struct GenericAPIHTTPRequestMapper {
static func map<T>(data: Data, response: HTTPURLResponse) throws -> T where T: Decodable {
if(200..<300) ~= response.statusCode {
return try customDateJSONDecoder.decode(T.self, from: data)
} else if response.statusCode == 401 {
throw APIErrorHandler.tokenExpired
} else {
if let error = try? JSONDecoder().decode(ApiErrorDTO.self, from: data) {
throw APIErrorHandler.customApiError(error)
} else {
throw APIErrorHandler.emptyErrorWithStatusCode(response.statusCode.description)
}
}
}
}
class AuthenticatedHTTPClientDecorator: HTTPClient {
let client: HTTPClient
let tokenProvider: TokenProvider
var needsAuth: (() -> Void)?
init(client: HTTPClient, tokenProvider: TokenProvider) {
self.client = client
self.tokenProvider = tokenProvider
}
func publisher(request: URLRequest) -> AnyPublisher<(Data, HTTPURLResponse), any Error> {
return tokenProvider
.tokenPublisher()
.map{ token in
var signedRequest = request
signedRequest.allHTTPHeaderFields?.removeValue(forKey: "Authorization")
signedRequest.addValue("Bearer \(token.accessToken)", forHTTPHeaderField: "Authorization")
return signedRequest
}
.flatMap(client.publisher)
.handleEvents(receiveCompletion: { [needsAuth] completion in
if case let Subscribers.Completion<Error>.failure(error) = completion, case APIErrorHandler.tokenExpired? = error as? APIErrorHandler {
needsAuth?()
}
})
.eraseToAnyPublisher()
}
}
internal extension Date {
enum DateParserError: Error {
case failedToParseDateFromString(String)
case typeUnhandled(Any?)
}
// MARK: - Class
static func dateFromString(_ string: Any?) throws -> Date {
if let dateString = string as? String {
let count = dateString.count
if count <= 10 {
ISO8601DateFormatter.dateFormat = "yyyy-MM-dd"
} else if count == 23 {
ISO8601DateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss ZZZ"
} else if count == 19 {
ISO8601DateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
} else if count > 23 && dateString.contains("+") {
ISO8601DateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
} else {
ISO8601DateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS"
}
if let date = ISO8601DateFormatter.date(from: dateString) {
return date
} else {
throw DateParserError.failedToParseDateFromString("String to parse: \(dateString), date format: \(String(describing: ISO8601DateFormatter.dateFormat))")
}
} else if let date = string as? Date {
return date
} else {
throw DateParserError.typeUnhandled(string)
}
}
}
private let ISO8601DateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
let enUSPOSIXLocale = Locale(identifier: "en_US_POSIX")
dateFormatter.locale = enUSPOSIXLocale
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
return dateFormatter
}()
Hello there, did anyone complete 'refreshToken' logic? how to handle 401 from any request, refresh the access token, and resend the request again? In a mentoring session only 401 from refresh token was handled, as I understood
Hello there! By any chance do you have the code from the
Essential Developer
video?