-
Notifications
You must be signed in to change notification settings - Fork 85
/
Client.swift
395 lines (371 loc) · 17.1 KB
/
Client.swift
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
//
// Client.swift
// Lockdown
//
// Created by Johnny Lin on 7/31/19.
// Copyright © 2019 Confirmed Inc. All rights reserved.
//
import Foundation
import PromiseKit
import SwiftyStoreKit
import CocoaLumberjackSwift
let kApiCodeNoError = 0
let kApiCodeEmailNotConfirmed = 1
let kApiCodeIncorrectLogin = 2
let kApiCodeRequestFieldValidationError = 3
let kApiCodeNoActiveSubscription = 6
let kApiCodeNoSubscriptionInReceipt = 9
let kApiCodeMobileSubscriptionOnly = 38
let kApiCodeEmailAlreadyUsed = 40
let kApiCodeReceiptAlreadyUsed = 48
let kApiCodeInvalidAuth = 401
let kApiCodeTooManyRequests = 999
let kApiCodeSandboxReceiptNotAllowed = 9925
let kApiCodeUnknownError = 99999
let kApiCodeNegativeError = -1
class Client {
// MARK: - CLIENT CALLS
static func signIn(forceRefresh: Bool = false) throws -> Promise<SignIn> {
DDLogInfo("API CALL: signIn")
URLCache.shared.removeAllCachedResponses()
clearCookies()
return getReceipt(forceRefresh: forceRefresh)
.then { receipt -> Promise<(data: Data, response: URLResponse)> in
let parameters:[String : Any] = [
"authtype": "ios",
"authreceipt": receipt,
"lockdown": true
]
return URLSession.shared.dataTask(.promise,
with: try makePostRequest(urlString: mainURL + "/signin",
parameters: parameters))
}
.map { data, response -> SignIn in
try self.validateApiResponse(data: data, response: response)
let resp = response as! HTTPURLResponse // already validated the type in validateApiResponse
DDLogInfo("Got signin response with headers: \(resp.allHeaderFields)")
if (hasValidCookie()) {
return try JSONDecoder().decode(SignIn.self, from: data)
}
else {
throw "No valid cookie received and/or set when trying to sign in"
}
}
}
static func signInWithEmail(email: String, password: String) throws -> Promise<SignIn> {
DDLogInfo("API CALL: test signIn with email")
URLCache.shared.removeAllCachedResponses()
clearCookies()
return firstly { () -> Promise<(data: Data, response: URLResponse)> in
let parameters:[String : Any] = [
"email" : email,
"password" : password,
"lockdown": true
]
return URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/signin", parameters: parameters))
}
.map { data, response -> SignIn in
try self.validateApiResponse(data: data, response: response)
let resp = response as! HTTPURLResponse // already validated the type in validateApiResponse
DDLogInfo("Got signin (with email) response with headers: \(resp.allHeaderFields)")
return try JSONDecoder().decode(SignIn.self, from: data)
}
}
static func resendConfirmCode(email: String) throws -> Promise<Bool> {
DDLogInfo("API CALL: resendConfirmCode")
return firstly { () -> Promise<(data: Data, response: URLResponse)> in
let parameters:[String : Any] = [
"email" : email,
"lockdown": true
]
return URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/resend-confirm-code", parameters: parameters))
}
.map { data, response -> Bool in
if let httpResponse = response as? HTTPURLResponse {
DDLogInfo("API RESULT: resend-confirm-code: \(httpResponse.statusCode)")
if httpResponse.statusCode < 400 {
return true
}
return false
}
DDLogInfo("API RESULT: error - resend-confirm-code: not HTTPURLResponse")
return false
}
}
static func subscriptionEvent(forceRefresh: Bool = false) throws -> Promise<SubscriptionEvent> {
DDLogInfo("API CALL: subscription-event")
return getReceipt(forceRefresh: forceRefresh)
.then { receipt -> Promise<(data: Data, response: URLResponse)> in
let parameters:[String : Any] = [
"authtype": "ios",
"authreceipt": receipt,
"lockdown": true
]
return URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/subscription-event", parameters: parameters))
}
.map { data, response -> SubscriptionEvent in
try self.validateApiResponse(data: data, response: response)
let subscriptionEvent = try JSONDecoder().decode(SubscriptionEvent.self, from: data)
DDLogInfo("API RESULT: subscriptionEvent: \(subscriptionEvent)")
return subscriptionEvent
}
.recover { error -> Promise<SubscriptionEvent> in
DDLogInfo("Recovering from subscription-event error: \(error)")
return .value(SubscriptionEvent(message: "Recovery"))
}
}
static func activeSubscriptions() throws -> Promise<[Subscription]> {
DDLogInfo("API CALL: active-subscriptions")
return firstly {
URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/active-subscriptions", parameters: [:]))
}.map { data, response -> [Subscription] in
try self.validateApiResponse(data: data, response: response)
var subscriptions = try JSONDecoder().decode([Subscription].self, from: data)
DDLogInfo("API RESULT: active-subscriptions: \(subscriptions)")
// sort subscriptions with highest tier at the top
subscriptions.sort(by: { (sub1: Subscription, sub2: Subscription) -> Bool in
if (sub1.planType == .proAnnual) {
return true
}
else if (sub1.planType == .proMonthly) {
if (sub2.planType == .proAnnual) {
return false
}
else {
return true
}
}
else if (sub1.planType == .annual) {
if (sub2.planType == .proAnnual || sub2.planType == .proMonthly) {
return false
}
else {
return true
}
}
else {
if (sub2.planType == .proAnnual || sub2.planType == .proMonthly || sub2.planType == .annual) {
return false
}
else {
return true
}
}
})
DDLogInfo("API RESULT: sorted-active-subscriptions: \(subscriptions)")
return subscriptions
}
}
// For creating email account only - not signing up with IAP receipt
static func signup(email: String, password: String) throws -> Promise<Signup> {
DDLogInfo("API CALL: signup")
return firstly { () -> Promise<(data: Data, response: URLResponse)> in
let parameters:[String : Any] = [
"email" : email,
"password" : password,
"lockdown": true
]
return URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/signup", parameters: parameters))
}
.map { data, response -> Signup in
try self.validateApiResponse(data: data, response: response)
let signup = try JSONDecoder().decode(Signup.self, from: data)
DDLogInfo("API RESULT: signup: \(signup)")
return signup
}
}
static func forgotPassword(email: String) throws -> Promise<Bool> {
DDLogInfo("API CALL: forgot-password")
return firstly { () -> Promise<(data: Data, response: URLResponse)> in
let parameters:[String : Any] = [
"email" : email,
"lockdown": true
]
return URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/forgot-password", parameters: parameters))
}
.map { data, response -> Bool in
if let httpResponse = response as? HTTPURLResponse {
DDLogInfo("API RESULT: forgot-password: \(httpResponse.statusCode)")
if httpResponse.statusCode < 400 {
return true
}
return false
}
DDLogInfo("API RESULT: error - forgot-password: not HTTPURLResponse")
return false
}
}
static func getKey() throws -> Promise<GetKey> {
DDLogInfo("API CALL: getKey")
return firstly { () -> Promise<(data: Data, response: URLResponse)> in
let parameters:[String : Any] = [
"platform" : "ios",
"lockdown": true
]
return URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/get-key", parameters: parameters))
}
.map { data, response -> GetKey in
try self.validateApiResponse(data: data, response: response)
let getKey = try JSONDecoder().decode(GetKey.self, from: data)
DDLogInfo("API RESULT: getKey: \(getKey)")
return getKey
}
}
static func getSpeedTestBucket() -> Promise<SpeedTestBucket> {
DDLogInfo("API CALL: download speed test")
return firstly {
URLSession.shared.dataTask(.promise, with: try makeGetRequest(urlString: "\(mainURL)/download-speed-test"))
}
.map { data, response -> SpeedTestBucket in
try self.validateApiResponse(data: data, response: response)
let speedTestBucket = try JSONDecoder().decode(SpeedTestBucket.self, from: data)
DDLogInfo("API RESULT: speedTestBucket: \(speedTestBucket)")
return speedTestBucket
}
}
static func getIP() -> Promise<IP> {
DDLogInfo("API CALL: ip")
URLCache.shared.removeAllCachedResponses()
return firstly {
URLSession.shared.dataTask(.promise, with: try makeGetRequest(urlString: "https://ip.\(mainDomain)/ip"))
}
.map { data, response -> IP in
try self.validateApiResponse(data: data, response: response)
let ip = try JSONDecoder().decode(IP.self, from: data)
DDLogInfo("API RESULT: ip: \(ip)")
return ip
}
}
static func getBlockedDomainTest() -> Promise<Void> {
return firstly {
URLSession.shared.dataTask(.promise, with: try Client.makeGetRequest(urlString: "https://\(testFirewallDomain)"))
}.asVoid()
}
// MARK: - Request Makers
static func makeGetRequest(urlString: String) throws -> URLRequest {
DDLogInfo("makeGetRequest: \(urlString)")
if let url = URL(string: urlString) {
var rq = URLRequest(url: url)
rq.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
rq.httpMethod = "GET"
rq.addValue("application/json", forHTTPHeaderField: "Accept")
return rq
}
else {
throw "Invalid URL string: \(urlString)"
}
}
static func makePostRequest(urlString: String, parameters: [String: Any]) throws -> URLRequest {
DDLogInfo("makePostRequest: \(urlString)")//", parameters: \(parameters)")
if let url = URL(string: urlString) {
var rq = URLRequest(url: url)
rq.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
rq.httpMethod = "POST"
rq.addValue("application/json", forHTTPHeaderField: "Content-Type")
rq.addValue("application/json", forHTTPHeaderField: "Accept")
rq.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
return rq
}
else {
throw "Invalid URL string: \(urlString)"
}
}
// MARK: - Util
private static func getReceipt(forceRefresh: Bool) -> Promise<String> {
DDLogInfo("fetch and set latest receipt")
return Promise { seal in
SwiftyStoreKit.fetchReceipt(forceRefresh: forceRefresh) { result in
switch result {
case .success(let receiptData):
let receipt = receiptData.base64EncodedString(options: [])
DDLogInfo("fetch latest receipt success base64: \(receipt)")
seal.fulfill(receipt);
case .error(let error):
DDLogError("fetch latest receipt failure: \(error)")
do {
switch error {
case ReceiptError.noReceiptData:
throw "Error refreshing purchases with App Store: No Receipt Data"
case ReceiptError.networkError(let networkError):
throw "Error refreshing purchases with App Store: Network Error - \(networkError.localizedDescription)"
case ReceiptError.noRemoteData:
throw "Error refreshing purchases with App Store: No Remote Data"
case ReceiptError.receiptInvalid(_, let receiptStatus):
throw "Error refreshing purchases with App Store: Invalid Receipt - \(receiptStatus)"
case ReceiptError.requestBodyEncodeError(let error):
throw "Error refreshing purchases with App Store: Encoding Error - \(error.localizedDescription)"
case ReceiptError.jsonDecodeError(_):
throw "Error refreshing purchases with App Store: JSON Decode Error"
}
}
catch {
seal.reject(error)
}
}
}
}
}
static func hasValidCookie() -> Bool {
DDLogInfo("checking for valid cookie")
var hasValidCookie = false
if let cookies = HTTPCookieStorage.shared.cookies {
DDLogInfo("found cookies")
for cookie in cookies {
DDLogInfo("cookie: \(cookie)")
if let timeUntilExpire = cookie.expiresDate?.timeIntervalSinceNow {
DDLogInfo("time until expire: \(timeUntilExpire)")
if cookie.domain.contains(mainDomain) && timeUntilExpire > 120.0 {
DDLogInfo("cookie contains mainDomain and timeuntilexpires > 120")
hasValidCookie = true
}
}
}
}
return hasValidCookie
}
static func clearCookies() {
DDLogInfo("clearing cookies")
var cookiesToDelete:[HTTPCookie] = []
if let cookies = HTTPCookieStorage.shared.cookies {
DDLogInfo("found cookies")
for cookie in cookies {
DDLogInfo("cookie to delete: \(cookie)")
cookiesToDelete.append(cookie)
}
}
for cookie in cookiesToDelete {
DDLogInfo("deleting cookie: \(cookie)")
HTTPCookieStorage.shared.deleteCookie(cookie)
}
}
private static func validateApiResponse(data: Data, response: URLResponse) throws {
DDLogInfo("validating API response")
let dataString = String(data: data, encoding: String.Encoding.utf8)
DDLogInfo("RAW RESULT: \(String(describing: dataString))")
if let resp = response as? HTTPURLResponse {
DDLogInfo("response is HTTPURLResponse: \(resp)")
// see if there's a non-zero code returned
if let apiError = try? JSONDecoder().decode(ApiError.self, from: data) {
if apiError.code == kApiCodeNoError {
DDLogError("zero (non-error) API code received, validated OK: \(apiError)")
return;
}
else {
DDLogError("nonzero API code received, throwing: \(apiError)")
throw apiError;
}
}
// some 4xx/5xx error
else if (resp.statusCode >= 400 || resp.statusCode <= 0) {
DDLogError("response has bad status code \(resp.statusCode)")
throw "response has bad status code \(resp.statusCode)"
}
else {
DDLogInfo("response has good status code (2xx, 3xx) and no error code")
}
}
else {
throw "Invalid URL Response received"
}
}
}