Last active
November 5, 2019 11:42
-
-
Save arpitjain03/d08906da589db6f7d26a1ca6d3af463f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// APIManager.swift | |
// APIManager.swift | |
// | |
// Created by Arpit Jain on 22/01/18. | |
// Copyright © 2018 Arpit Jain. All rights reserved. | |
// | |
// | |
import Foundation | |
import UIKit | |
import Alamofire | |
import SwiftyJSON | |
import SystemConfiguration | |
/// This is the Server End URL | |
let strServerBasicURL = AppConstant.ServerURL.live_development // live // development | |
private struct FNAPIStruct { | |
static let VIDEOTYPES: [String] = ["mov","m4v","mp4"] | |
static let AUDIOTYPES: [String] = ["mp3","caf","m4v","aac"] | |
static let IMAGETYPES: [String] = ["jpg","jpeg","m4a","aac"] | |
static let FILE_DATA: String = "file_data" | |
static let FILE_KEY: String = "file_key" | |
static let FILE_MIME: String = "file_mime" | |
static let FILE_EXT: String = "file_ext" | |
static let FILE_NAME: String = "file_name" | |
static let KEY_PREVIEW_APP = "is_app_preview" | |
} | |
class FNAPIManager: NSObject { | |
/// Structure for the Default SharedInstance of Sesstion Manager which will be used to call webservice | |
struct APIManager { | |
static let shared: SessionManager = { | |
let configuration = URLSessionConfiguration.default | |
configuration.timeoutIntervalForRequest = 60.0 // Seconds | |
configuration.timeoutIntervalForResource = 60.0 // Seconds | |
return Alamofire.SessionManager(configuration: configuration) | |
}() | |
} | |
//MARK: - Network Reachability | |
/** | |
Network Reachability | |
- parameter reachableBlock: reachableBlock description | |
*/ | |
class func isConnectedToNetwork() -> Bool { | |
var zeroAddress = sockaddr_in() | |
zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress)) | |
zeroAddress.sin_family = sa_family_t(AF_INET) | |
let defaultRouteReachability = withUnsafePointer(to: &zeroAddress) { | |
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {zeroSockAddress in | |
SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress) | |
} | |
} | |
var flags = SCNetworkReachabilityFlags() | |
if !SCNetworkReachabilityGetFlags(defaultRouteReachability!, &flags) { | |
return false | |
} | |
let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0 | |
let needsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0 | |
return (isReachable && !needsConnection) | |
} | |
/// This method is used to Cancel all API call | |
class func stopAllAPICall() -> Void { | |
APIManager.shared.session.getTasksWithCompletionHandler { (dataTast, uploadTask, downloadTask) in | |
dataTast.forEach { $0.cancel() } | |
uploadTask.forEach { $0.cancel() } | |
downloadTask.forEach { $0.cancel() } | |
} | |
} | |
//MARK: - GET Service | |
/** | |
GET Web Service Method | |
- parameter url: API URL String | |
- parameter param: Parameter description | |
- parameter controller: Object of UIViewController | |
- parameter completionSuccessBlock: completionSuccessBlock description | |
- parameter completionFailureBlock: completionFailureBlock description | |
### Usage Example: ### | |
```` | |
FNAPIManager.GET(strPostURL, param: nil, controller: self, successBlock: { (jsonResponse) in | |
print("success response is received") | |
}) { (error, isTimeOut) in | |
if isTimeOut { | |
print("Request Timeout") | |
} else { | |
print(error?.localizedDescription) | |
} | |
} | |
```` | |
*/ | |
class func GET(_ url: String, | |
param: [String: Any]?, | |
controller: UIViewController, | |
successBlock: @escaping (_ response: JSON) -> Void, | |
failureBlock: @escaping (_ error: Error? , _ isTimeOut: Bool) -> Void) { | |
if FNAPIManager.isConnectedToNetwork() { | |
// Internet is connected | |
var headers = [ | |
"Accept": "application/json" | |
] | |
if let strToken:String = UserManager.getAccessToken() | |
{ | |
headers["accesstoken"] = strToken | |
} | |
let aStrURl = strServerBasicURL + url | |
print("---- GET REQUEST URL : \(aStrURl)") | |
print("---- GET REQUEST PARAM : \(param ?? ["":""])") | |
APIManager.shared.request(aStrURl, method: .get, parameters: param, encoding: JSONEncoding.default, headers: headers).responseJSON(completionHandler: { (response) in | |
print("---- GET REQUEST URL RESPONSE : \(url)\n\(response.result)") | |
print(response.timeline) | |
switch response.result { | |
case .success: | |
// print(response.request) // original URL request | |
// print(response.response) // HTTP URL response | |
// print(response.data) // server data | |
if let aJSON = response.result.value { | |
let json = JSON(aJSON) | |
print("---- GET SUCCESS RESPONSE : \(json)") | |
successBlock(json) | |
} | |
case .failure(let error): | |
print(error) | |
if (error as NSError).code == -999 { | |
// Request has been cancelled by the User | |
} else { | |
MessageBar.show(.error, strMessage: "REQUEST_TIMED_OUT") | |
} | |
failureBlock(error, false) | |
// if (error as NSError).code == -1001 { | |
// // The request timed out error occured. // Code=-1001 "REQUEST_TIMED_OUT" | |
// MessageBar.show(.error, strMessage: "REQUEST_TIMED_OUT") | |
// // UIAlertController.showAlertWithOkButton(controller: controller, aStrMessage: "REQUEST_TIMED_OUT", completion: nil) | |
// failureBlock(error, true) | |
// } else if (error as NSError).code == 3840 { | |
// MessageBar.show(.error, strMessage: "RESPONSE_INVALID") | |
// failureBlock(error, false) | |
// } else { | |
// MessageBar.show(.error, strMessage: error.localizedDescription) | |
// // UIAlertController.showAlertWithOkButton(controller: controller, aStrMessage: error.localizedDescription, completion: nil) | |
// failureBlock(error, false) | |
// } | |
} | |
}) | |
} else { | |
// Internet is not connected | |
MessageBar.show(.error, strMessage: "INTERNET_NOT_AVAILABLE") | |
let aErrorConnection = NSError(domain: "InternetNotAvailable", code: 0456, userInfo: nil) | |
failureBlock(aErrorConnection as Error , false) | |
} | |
} | |
//MARK: - POST Service | |
/** | |
POST Web Service Method | |
- parameter url: API URL String | |
- parameter param: Parameter description | |
- parameter controller: Object of UIViewController | |
- parameter completionSuccessBlock: completionSuccessBlock description | |
- parameter completionFailureBlock: completionFailureBlock description | |
### Usage Example: ### | |
```` | |
FNAPIManager.POST(strPostURL, param: aDictParam, controller: self, successBlock: { (jsonResponse) in | |
print("success response is received") | |
}) { (error, isTimeOut) in | |
if isTimeOut { | |
print("Request Timeout") | |
} else { | |
print(error?.localizedDescription) | |
} | |
} | |
```` | |
*/ | |
class func POST(_ url: String, | |
param: [String: Any], | |
controller: UIViewController, | |
successBlock: @escaping (_ response: JSON) -> Void, | |
failureBlock: @escaping (_ error: Error? , _ isTimeOut: Bool) -> Void) { | |
if FNAPIManager.isConnectedToNetwork() { | |
// Internet is connected | |
var headers = [ | |
"content-type": "application/json" | |
] | |
if let strToken:String = UserManager.getAccessToken() | |
{ | |
headers["accesstoken"] = strToken | |
} | |
let aStrURl = strServerBasicURL + url | |
print("---- POST REQUEST URL : \(aStrURl)") | |
print("---- POST REQUEST PARAM : \(param)") | |
APIManager.shared.request(aStrURl, method: .post, parameters: param, encoding: JSONEncoding.default, headers: headers).responseJSON(completionHandler: { (response) in | |
print("---- POST RESPONSE URL : \(aStrURl)\n\(response.result)") | |
print(response.timeline) | |
switch response.result { | |
case .success: | |
// print(response.request) // original URL request | |
// print(response.response) // HTTP URL response | |
// print(response.data) // server data | |
if let aJSON = response.result.value { | |
let json = JSON(aJSON) | |
print("---- POST SUCCESS RESPONSE : \(json)") | |
successBlock(json) | |
} | |
case .failure(let error): | |
print(error) | |
if (error as NSError).code == -999 { | |
// Request has been cancelled by the User | |
} else { | |
MessageBar.show(.error, strMessage: "REQUEST_TIMED_OUT") | |
} | |
failureBlock(error, false) | |
// if (error as NSError).code == -1001 { | |
// // The request timed out error occured. // Code=-1001 "REQUEST_TIMED_OUT" | |
// MessageBar.show(.error, strMessage: "REQUEST_TIMED_OUT") | |
// failureBlock(error, true) | |
// } else if (error as NSError).code == 3840 { | |
// MessageBar.show(.error, strMessage: "RESPONSE_INVALID") | |
// failureBlock(error, false) | |
// } else if (error as NSError).code == -1005 { | |
// MessageBar.show(.error, strMessage: "REQUEST_TIMED_OUT") | |
// failureBlock(error, false) | |
// } else if (error as NSError).code == -999 { | |
// // Request has been cancelled by the User | |
// failureBlock(error, false) | |
// } else { | |
// MessageBar.show(.error, strMessage: error.localizedDescription) | |
// failureBlock(error, false) | |
// } | |
} | |
}) | |
} else { | |
// Internet is not connected | |
MessageBar.show(.error, strMessage: "INTERNET_NOT_AVAILABLE") | |
let aErrorConnection = NSError(domain: "InternetNotAvailable", code: 0456, userInfo: nil) | |
failureBlock(aErrorConnection as Error , false) | |
} | |
} | |
//MARK: - UPLOAD Service | |
/** | |
UPLOAD Web Service | |
- parameter url: url description | |
- parameter param: param description | |
- parameter completionSuccessBlock: completionSuccessBlock description | |
- parameter completionFailureBlock: completionFailureBlock description | |
### Usage Example: ### | |
```` | |
let aDictParameter: [String: Any] = [ | |
"email": "[email protected]" as Any, | |
"relationship_status": "Single" as Any, | |
"doc_url": <Pass URL of file here> as Any, | |
"profile_img": UIImage.init(named: "img-sharewith-bg@3x")!, | |
"ParamName": "profile_img,doc_url" as Any | |
] | |
FNAPIManager.UPLOAD(strPostURL, param: aDictParameter, controller: self, successBlock: { (jsonResponse) in | |
print("success response is received") | |
}) { (error, isTimeOut) in | |
if isTimeOut { | |
print("Request Timeout") | |
} else { | |
print(error?.localizedDescription) | |
} | |
```` | |
- Remark: | |
You have to pass all the "keys" seperated by comma(,) for "ParamName" key which is having Image or File | |
*/ | |
class func UPLOAD(_ url: String, | |
param: [String: Any], | |
controller: UIViewController, | |
progressBlock: @escaping (_ progress: Progress) -> Void, | |
successBlock: @escaping (_ response: JSON) -> Void, | |
failureBlock: @escaping (_ error: Error? , _ isTimeOut: Bool) -> Void) { | |
let aStrURl = strServerBasicURL + url | |
/* | |
// Sample Request has to be like this : | |
let aDictTem: [String: Any] = [ | |
"email":"[email protected]" as Any, | |
"relationship_status" : "Single" as Any, | |
"doc_url" : <Pass URL of file here> as Any, | |
"profile_img":UIImage.init(named: "img-sharewith-bg@3x")!, | |
"ParamName":"profile_img,doc_url" as Any] | |
*/ | |
if FNAPIManager.isConnectedToNetwork() { | |
// MMSwiftSpinner.show("Uploading...") | |
var aParam = param | |
var arrKeys: [String] = [String]() | |
if let paramNameKey = aParam["ParamName"] as? String { | |
arrKeys = paramNameKey.components(separatedBy: ",") | |
} | |
var arrMutData = [ [String: Any] ]() | |
for strKey in arrKeys { | |
var dictRequestPatamData = [String: Any]() | |
print(aParam[strKey]!) | |
if aParam[strKey]! is UIImage { | |
let image = aParam[strKey] as! UIImage | |
// let imageData: Data = UIImageJPEGRepresentation(image, 0.5)! | |
let imageData: Data = (image.mediumQualityJPEGNSData) | |
dictRequestPatamData[FNAPIStruct.FILE_DATA] = imageData | |
dictRequestPatamData[FNAPIStruct.FILE_KEY] = strKey | |
dictRequestPatamData[FNAPIStruct.FILE_NAME] = "\(strKey).png" | |
dictRequestPatamData[FNAPIStruct.FILE_MIME] = "image/jpeg" | |
dictRequestPatamData[FNAPIStruct.FILE_EXT] = "png" | |
} else if aParam[strKey]! is NSURL || aParam[strKey]! is URL { | |
let aURL = aParam[strKey] as! URL | |
do { | |
if try aURL.checkResourceIsReachable() { | |
let strFileName: String = aURL.absoluteURL.lastPathComponent | |
let strFileType = strFileName.components(separatedBy: ".").last | |
if let fileData = try? Data(contentsOf: aURL) { | |
// Data is received from URL | |
dictRequestPatamData[FNAPIStruct.FILE_DATA] = fileData | |
dictRequestPatamData[FNAPIStruct.FILE_KEY] = strKey | |
dictRequestPatamData[FNAPIStruct.FILE_NAME] = strFileName | |
if strFileType?.lowercased() == "pdf" { | |
dictRequestPatamData[FNAPIStruct.FILE_MIME] = "application/pdf" | |
dictRequestPatamData[FNAPIStruct.FILE_EXT] = "pdf" | |
} else if strFileType?.lowercased() == "doc" { | |
dictRequestPatamData[FNAPIStruct.FILE_MIME] = "application/msword" | |
dictRequestPatamData[FNAPIStruct.FILE_EXT] = "doc" | |
} else if strFileType?.lowercased() == "docx" { | |
dictRequestPatamData[FNAPIStruct.FILE_MIME] = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" | |
dictRequestPatamData[FNAPIStruct.FILE_EXT] = "docx" | |
} else if strFileType?.lowercased() == "txt" { | |
dictRequestPatamData[FNAPIStruct.FILE_MIME] = "text/plain" | |
dictRequestPatamData[FNAPIStruct.FILE_EXT] = "txt" | |
} else if (FNAPIStruct.IMAGETYPES.contains((strFileType?.lowercased())!)) { | |
let img : UIImage = UIImage(data: fileData)! | |
// let imageData: Data = UIImageJPEGRepresentation(img, 0.5)! | |
let imageData: Data = (img.mediumQualityJPEGNSData) | |
dictRequestPatamData[FNAPIStruct.FILE_DATA] = imageData | |
dictRequestPatamData[FNAPIStruct.FILE_MIME] = "image/jpeg" | |
dictRequestPatamData[FNAPIStruct.FILE_EXT] = "png" | |
} else if (FNAPIStruct.AUDIOTYPES.contains((strFileType?.lowercased())!)) { | |
dictRequestPatamData[FNAPIStruct.FILE_MIME] = "Audio/mp3" | |
dictRequestPatamData[FNAPIStruct.FILE_EXT] = "mp3" | |
} else if (FNAPIStruct.VIDEOTYPES.contains((strFileType?.lowercased())!)) { | |
dictRequestPatamData[FNAPIStruct.FILE_MIME] = "video/mov" | |
dictRequestPatamData[FNAPIStruct.FILE_EXT] = "mov" | |
} | |
} else { | |
// Data is not received from URL | |
print("Something went wrong. Unable to Get Data from the URL : \(aURL)") | |
} | |
} | |
} | |
catch { | |
//Handle error | |
print("Exception is occurred. Unable to Get Data from the URL") | |
} | |
} | |
arrMutData.append(dictRequestPatamData) | |
aParam.removeValue(forKey: strKey) | |
} | |
aParam.removeValue(forKey: "ParamName") | |
// let headers = [ | |
// "Content-Type": "application/json" | |
// ] | |
print("---- POST REQUEST URL : \(aStrURl)") | |
print("---- POST REQUEST PARAM : \(aParam)") | |
APIManager.shared.upload(multipartFormData: { (multipartFormData) in | |
for dict in arrMutData { | |
let aData = dict[FNAPIStruct.FILE_DATA] as! Data | |
let strKey = dict[FNAPIStruct.FILE_KEY] as! String | |
let strName = dict[FNAPIStruct.FILE_NAME] as! String | |
let strMime = dict[FNAPIStruct.FILE_MIME] as! String | |
multipartFormData.append(aData, withName: strKey, fileName: strName, mimeType: strMime) | |
} | |
for (key, value) in aParam { | |
if value is String { | |
let aStrValue = value as! String | |
multipartFormData.append(aStrValue.data(using: .utf8)!, withName: key) | |
} else if value is Dictionary<String, Any> { | |
let a1dict = value as! [String: Any] | |
for (key1, value1) in a1dict { | |
if value1 is String { | |
let aStrValue1 = value1 as! String | |
multipartFormData.append(aStrValue1.data(using: .utf8)!, withName: key1) | |
} | |
} | |
} | |
} | |
}, to: aStrURl, encodingCompletion: { (encodingResult) in | |
switch encodingResult { | |
case .success(let upload, _, _): | |
upload.uploadProgress(closure: { (progress) in | |
// Update progress indicator | |
// print(progress.fractionCompleted) | |
progressBlock(progress) | |
}) | |
upload.responseJSON { response in | |
print(response.timeline) | |
if let aJSON = response.result.value { | |
let json = JSON(aJSON) | |
print("---- UPLOAD SUCCESS RESPONSE : \(json)") | |
successBlock(json) | |
} | |
} | |
case .failure(let error): | |
print(error) | |
if (error as NSError).code == -999 { | |
// Request has been cancelled by the User | |
} else { | |
MessageBar.show(.error, strMessage: "REQUEST_TIMED_OUT") | |
} | |
failureBlock(error, false) | |
// if (error as NSError).code == -1001 { | |
// // The request timed out error occured. // Code=-1001 "REQUEST_TIMED_OUT" | |
// MessageBar.show(.error, strMessage: "REQUEST_TIMED_OUT") | |
// UIAlertController.showAlertWithOkButton(controller: controller, aStrMessage: "REQUEST_TIMED_OUT", completion: nil) | |
// failureBlock(error, true) | |
// } else { | |
// MessageBar.show(.error, strMessage: error.localizedDescription) | |
// UIAlertController.showAlertWithOkButton(controller: controller, aStrMessage: error.localizedDescription, completion: nil) | |
// failureBlock(error, false) | |
// } | |
} | |
}) | |
} else { | |
// Internet is not connected | |
MessageBar.show(.error, strMessage: "INTERNET_NOT_AVAILABLE") | |
let aErrorConnection = NSError(domain: "InternetNotAvailable", code: 0456, userInfo: nil) | |
failureBlock(aErrorConnection as Error , false) | |
} | |
} | |
/* | |
class func POST(_ url: String, | |
param: [String: Any], | |
controller: UIViewController, | |
successBlock: @escaping (_ response: JSON) -> Void, | |
failureBlock: @escaping (_ error: Error? , _ isTimeOut: Bool) -> Void) {*/ | |
class func WSPostAPIMultiPart(url : String, | |
params : [String:Any], | |
controller: UIViewController, | |
progressBlock: @escaping (_ progress: Progress) -> Void, | |
successBlock: @escaping (_ response: JSON) -> Void, | |
failureBlock: @escaping (_ error: Error? , _ isTimeOut: Bool) -> Void) { | |
let aStrURl = strServerBasicURL + url | |
if isConnectedToNetwork() { | |
// let headers = [ | |
// "Accept": "application/json" | |
// ] | |
var headers:[String:String] = [ | |
"content-type":"multipart/form-data" | |
] | |
if let strToken:String = UserManager.getAccessToken() | |
{ | |
headers["accesstoken"] = strToken | |
} | |
// SVProgressHUD.show(withStatus: "Preparing files...") | |
print("---- POST REQUEST URL : \(aStrURl)") | |
print("---- POST REQUEST PARAM : \(params)") | |
Alamofire.upload(multipartFormData: { (formData) in | |
for (key, value) in params { | |
if let url = value as? URL{ | |
formData.append(url, withName: key) | |
}else if let image = value as? UIImage{ | |
let data = UIImageJPEGRepresentation(image, 1.0) | |
formData.append(data!, withName: key, fileName: "profile_image.jpeg", mimeType: "image/jpeg") | |
}else{ | |
formData.append((value as AnyObject).data(using: String.Encoding.utf8.rawValue)!, withName: key) | |
} | |
} | |
}, to: aStrURl , method : .post , headers : headers) { (response) in | |
switch response { | |
case .success(let upload, _, _): | |
// SVProgressHUD.dismiss() | |
// SVProgressHUD.show(withStatus: "Uploading...") | |
upload.uploadProgress(closure: { (progress) in | |
progressBlock(progress) | |
}) | |
upload.responseJSON { response in | |
// SVProgressHUD.dismiss() | |
print(response.timeline) | |
if let aJSON = response.result.value { | |
let json = JSON(aJSON) | |
print("---- UPLOAD SUCCESS RESPONSE : \(json)") | |
successBlock(json) | |
} | |
} | |
case .failure(let error): | |
print(error) | |
if (error as NSError).code == -1001 { | |
MessageBar.show(.error, strMessage: "REQUEST_TIMED_OUT") | |
} else { | |
MessageBar.show(.error, strMessage: "SOMETHING_WENT_WRONG") | |
} | |
} | |
} | |
} | |
else { | |
MessageBar.show(.error, strMessage: "INTERNET_NOT_AVAILABLE") | |
// failureBlock(aErrorConnection as Error , false) | |
} | |
} | |
class func WSGetAPIMultiPart(url : String, | |
params : [String:Any], | |
progressBlock: @escaping (_ progress: Progress) -> Void, | |
successBlock: @escaping (_ response: JSON) -> Void, | |
failureBlock: @escaping (_ error: Error? , _ isTimeOut: Bool) -> Void) { | |
let aStrURl = strServerBasicURL + url | |
if isConnectedToNetwork() { | |
// let headers = [ | |
// "Accept": "application/json" | |
// ] | |
var headers:[String:String] = [ | |
"content-type":"multipart/form-data" | |
] | |
if let strToken:String = UserManager.getAccessToken() | |
{ | |
headers["accesstoken"] = strToken | |
} | |
// SVProgressHUD.show(withStatus: "Preparing files...") | |
print("---- Get REQUEST URL : \(aStrURl)") | |
print("---- Get REQUEST PARAM : \(params)") | |
Alamofire.upload(multipartFormData: { (formData) in | |
for (key, value) in params { | |
if let url = value as? URL{ | |
formData.append(url, withName: key) | |
}else if let image = value as? UIImage{ | |
let data = UIImageJPEGRepresentation(image, 1.0) | |
formData.append(data!, withName: key, fileName: "profile_image.jpeg", mimeType: "image/jpeg") | |
}else{ | |
formData.append((value as AnyObject).data(using: String.Encoding.utf8.rawValue)!, withName: key) | |
} | |
} | |
}, to: aStrURl , method : .get , headers : headers) { (response) in | |
switch response { | |
case .success(let upload, _, _): | |
// SVProgressHUD.dismiss() | |
// SVProgressHUD.show(withStatus: "Uploading...") | |
upload.uploadProgress(closure: { (progress) in | |
progressBlock(progress) | |
}) | |
upload.responseJSON { response in | |
// SVProgressHUD.dismiss() | |
print(response.timeline) | |
if let aJSON = response.result.value { | |
let json = JSON(aJSON) | |
print("---- UPLOAD SUCCESS RESPONSE : \(json)") | |
successBlock(json) | |
} | |
} | |
case .failure(let error): | |
print(error) | |
if (error as NSError).code == -1001 { | |
MessageBar.show(.error, strMessage: "REQUEST_TIMED_OUT") | |
} else { | |
MessageBar.show(.error, strMessage: "SOMETHING_WENT_WRONG") | |
} | |
} | |
} | |
} | |
else { | |
MessageBar.show(.error, strMessage: "INTERNET_NOT_AVAILABLE") | |
// failureBlock(aErrorConnection as Error , false) | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment