I'm doing login using SwiftUI and Combine. Could you please give me some idea how can I decode and show json error when user types incorrect email or password? I can only get token.
When I'm doing the same login request with incorrect email or password, server returns me this error message:
{
"code": "[jwt_auth] incorrect_password",
"message": "Incorrect password!",
"data": {
"status": 403
}
}
The problem is that I can't understand how can I decode two different json responses when doing one request in Combine? I can only get token.
Here's model for login request:
struct LoginResponse: Decodable {
let token: String }
struct ErrorResponse: Decodable {
let message: String
}
struct Login: Codable {
let username: String
let password: String
}
static func login(email: String, password: String) -> AnyPublisher<LoginResponse, Error> {
let url = MarketplaceAPI.jwtAuth!
var request = URLRequest(url: url)
let encoder = JSONEncoder()
let login = Login(username: email, password: password)
let jsonData = try? encoder.encode(login)
request.httpBody = jsonData
request.httpMethod = HTTPMethod.POST.rawValue
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
return URLSession.shared
.dataTaskPublisher(for: request)
.print()
.receive(on: DispatchQueue.main)
.map(\.data)
.decode(
type: LoginResponse.self,
decoder: JSONDecoder())
.eraseToAnyPublisher()
}
And in viewModel:
MarketplaceAPI.login(email: email, password: password)
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("finished")
case .failure(let error):
print("Failure error:", error.localizedDescription) // This's returning token error
}
},
receiveValue: { value in
print("Token:", value.token)
}
})
.store(in: &subscriptions)
}
CodePudding user response:
I would make ErrorResponse
conform to the Error
protocol:
struct ErrorResponse: Decodable, Error {
let message: String
}
Then, use tryMap
instead of decode
(which is sort of a special case of tryMap
).
.tryMap({ data -> LoginResponse in
let decoder = JSONDecoder()
guard let loginResponse = try? decoder.decode(LoginResponse.self, from: data) else {
throw try decoder.decode(ErrorResponse.self, from: data)
}
return loginResponse
})
First, try to decode the data as a LoginResponse
. Note the use of try?
here. This is so that we can check whether this has failed or not. If this fails, we throw an error. The error we throw is the data decoded as an ErrorResponse
, or whatever error is thrown during the decoding of that.
In your view model, you can check the error like this:
.sink { completion in
switch completion {
case .failure(let error as ErrorResponse):
// wrong password/username
// you can access error.message here
case .failure(let error):
// some other sort of error:
default:
break
}
} receiveValue: { loginResponse in
...
}
CodePudding user response:
You can make use of tryMap
with combine to figure out where the function should return. I'd suggest you take a read of the documentation on it but here is a snippet that should be able to get you moving with it.
Hopefully this is what you mean by the question - I've changed a few things but feel free to take the code as a building block and adapt as needed!
enum LoginError: Error, Equatable {
case noConnection
case invalidResponse
}
static func login(email: String, password: String) -> AnyPublisher<Void, LoginError> {
return URLSession.shared
.dataTaskPublisher(for: request)
.print()
.receive(on: DispatchQueue.main)
.mapError { _ in LoginError.noConnection }
.tryMap { (data, response) in
guard let response = response as? HTTPURLResponse else {
throw LoginError.invalidResponse
}
if response.statusCode == 200 {
return data
} else {
throw LoginError.invalidResponse
}
}
.decode(type: LoginResponse.self, decoder: JSONDecoder())
.tryMap { [unowned self] in
// Update your session with the token here using $0 then return.
// e.g session.token = $0.token
return
}
.mapError { $0 as? LoginError ?? .invalidResponse }
.eraseToAnyPublisher()
}