-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathAPIService.swift
More file actions
169 lines (152 loc) · 6.38 KB
/
APIService.swift
File metadata and controls
169 lines (152 loc) · 6.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import Foundation
import Combine
enum APIError: Error {
case invalidURL
case invalidResponse
case unauthorized
case refreshTokenExpired
case unknown
}
final class API {
static let service = API()
private static let airdropBaseUrl = "https://airdrop.tari.com/api"
private static let rwaBaseUrl = "https://rwa.y.at"
private init() {}
func minerStats() async -> MinerStats? {
await request(endpoint: "/miner/stats")
}
func minerStatus(appId: String) async -> MiningStatus? {
await request(endpoint: "/miner/status/\(appId)")
}
func requiredAppVersion() async -> AppVersion? {
await request(endpoint: "/mobile/version", baseUrl: Self.rwaBaseUrl)
}
func request<T: Decodable>(endpoint: String, method: String = "GET", body: Data? = nil, baseUrl: String = API.airdropBaseUrl) async -> T? {
do {
let request = try urlRequest(endpoint: endpoint, method: method, body: body, baseUrl: baseUrl)
let (data, response) = try await URLSession.shared.data(for: request)
try process(data: data, response: response, endpoint: endpoint)
return try JSONDecoder().decode(T.self, from: data)
} catch {
if case APIError.unauthorized = error {
do {
try await refreshToken()
return await request(endpoint: endpoint, method: method, body: body, baseUrl: baseUrl)
} catch {
log(error, endpoint)
}
}
log(error, endpoint)
return nil
}
}
// TODO: Remove Publisher once async implementation is finalised
func request<T: Decodable>(endpoint: String, method: String = "GET", body: Data? = nil) -> AnyPublisher<T, Error> {
do {
let request = try urlRequest(endpoint: endpoint, method: method, body: body)
return URLSession.shared.dataTaskPublisher(for: request).tryMap { data, response in
try self.process(data: data, response: response, endpoint: endpoint)
return data
}
.decode(type: T.self, decoder: JSONDecoder())
.catch { error -> AnyPublisher<T, Error> in
if case APIError.unauthorized = error {
return self.refreshTokenPublisher()
.flatMap { _ in
self.request(endpoint: endpoint, method: method, body: body)
}
.eraseToAnyPublisher()
}
return Fail(error: error).eraseToAnyPublisher()
}
.eraseToAnyPublisher()
} catch {
return Fail(error: error).eraseToAnyPublisher()
}
}
}
private extension API {
struct TokenResponse: Decodable {
let accessToken: String
let refreshToken: String?
}
func urlRequest(endpoint: String, method: String, body: Data?, baseUrl: String = API.airdropBaseUrl) throws -> URLRequest {
guard let url = URL(string: "\(baseUrl)\(endpoint)") else {
throw APIError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = method
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
if let accessToken = UserManager.shared.accessToken {
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
}
if let body = body {
request.httpBody = body
}
return request
}
func process(data: Data, response: URLResponse, endpoint: String) throws {
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.invalidResponse
}
if httpResponse.statusCode == 401 {
throw APIError.unauthorized
}
}
func refreshToken() async throws {
guard let refreshToken = UserManager.shared.refreshToken else {
UserManager.shared.clearTokens()
throw APIError.refreshTokenExpired
}
guard let request = refreshTokenRequest(token: refreshToken) else {
throw APIError.invalidURL
}
let (data, response) = try await URLSession.shared.data(for: request)
try processRefreshToken(data: data, response: response)
}
// TODO: Remove Publisher once async implementation is finalised
func refreshTokenPublisher() -> AnyPublisher<Void, Error> {
guard let refreshToken = UserManager.shared.refreshToken else {
UserManager.shared.clearTokens()
return Fail(error: APIError.refreshTokenExpired).eraseToAnyPublisher()
}
guard let request = refreshTokenRequest(token: refreshToken) else {
return Fail(error: APIError.invalidURL).eraseToAnyPublisher()
}
return URLSession.shared.dataTaskPublisher(for: request)
.tryMap { data, response in
try self.processRefreshToken(data: data, response: response)
}
.eraseToAnyPublisher()
}
func refreshTokenRequest(token: String, baseUrl: String = API.airdropBaseUrl) -> URLRequest? {
guard let url = URL(string: "\(baseUrl)/auth/local/refresh") else {
return nil
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
return request
}
func processRefreshToken(data: Data, response: URLResponse) throws {
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.invalidResponse
}
if httpResponse.statusCode == 401 {
UserManager.shared.clearTokens()
throw APIError.refreshTokenExpired
}
guard httpResponse.statusCode == 200 else {
throw APIError.unknown
}
let tokenResponse = try JSONDecoder().decode(TokenResponse.self, from: data)
UserManager.shared.accessToken = tokenResponse.accessToken
if let newRefreshToken = tokenResponse.refreshToken {
UserManager.shared.refreshToken = newRefreshToken
}
}
func log(_ error: Error, _ endpoint: String) {
print("Network error: \(endpoint) - " + error.localizedDescription)
}
}