Refreshing bearer token and using AccessTokenPlugin - swift

For many of my endpoints, I require a bearer token to be passed with the request to authenticate the user. Because of that, I also need to refresh the user's token if it expires.
I found this question to start me along, but I'm still unsure about some things and how to implement them.
As the answer to the question above suggests, I've created an extension for MoyaProvider:
extension MoyaProvider {
convenience init(handleRefreshToken: Bool) {
if handleRefreshToken {
self.init(requestClosure: MoyaProvider.endpointResolver())
} else {
self.init()
}
}
static func endpointResolver() -> MoyaProvider<Target>.RequestClosure {
return { (endpoint, closure) in
//Getting the original request
let request = try! endpoint.urlRequest()
//assume you have saved the existing token somewhere
if (#tokenIsNotExpired#) {
// Token is valid, so just resume the original request
closure(.success(request))
return
}
//Do a request to refresh the authtoken based on refreshToken
authenticationProvider.request(.refreshToken(params)) { result in
switch result {
case .success(let response):
let token = response.mapJSON()["token"]
let newRefreshToken = response.mapJSON()["refreshToken"]
//overwrite your old token with the new token
//overwrite your old refreshToken with the new refresh token
closure(.success(request)) // This line will "resume" the actual request, and then you can use AccessTokenPlugin to set the Authentication header
case .failure(let error):
closure(.failure(error)) //something went terrible wrong! Request will not be performed
}
}
}
}
I've also created an extension for TargetType, which looks like this:
import Moya
public protocol TargetTypeExtension: TargetType, AccessTokenAuthorizable {}
public extension TargetTypeExtension {
var baseURL: URL { return URL(string: Constants.apiUrl)! }
var headers: [String: String]? { return nil }
var method: Moya.Method { return .get }
var authorizationType: AuthorizationType { return .bearer }
var sampleData: Data { return Data() }
}
Here is one such implementation of it:
import Moya
public enum Posts {
case likePost(postId: Int)
case getComments(postId: Int)
}
extension Posts: TargetTypeExtension {
public var path: String {
switch self {
case .likePost(_): return "posts/like"
case .getComments(_): return "posts/comments"
}
}
public var authorizationType: AuthorizationType {
switch self {
case .likePost(_): return .bearer
case .getComments(_): return .none
}
}
public var method: Moya.Method {
switch self {
case .likePost, .getComments:
return .post
}
}
public var task: Task {
switch self {
case let .likePost(postId):
return .requestParameters(parameters: ["postId": postId], encoding: JSONEncoding.default)
case let .getComments(postId):
return .requestParameters(parameters: ["postId": postId], encoding: JSONEncoding.default)
}
}
}
As you can see, my likePost request requires a bearer token, while my getComments request does not.
So I have a few questions:
How would I modify my MoyaProvider extension to utilize Moya's AccessTokenPlugin, found here?
How would I modify my MoyaProvider extension to NOT require a bearer token if the AuthorizationType is .none for a specific request?
Thanks!

I did with custom moya provider and RxSwift if you are using RxSwift you can take reference, it will not directly fit into yor code!!
extension Reactive where Base: MyMoyaProvider {
func request(_ token: Base.Target, callbackQueue: DispatchQueue? = nil) -> Single<Response> {
return Single.create { [weak base] single in
var pandingCancelable:Cancellable?
/// reschedule original request
func reschedulCurrentRequest() {
base?.saveRequest({
pandingCancelable = base?.request(token, callbackQueue: callbackQueue, progress: nil) { result in
switch result {
case.success(let response):
single(.success(response))
case .failure(let error):
single(.error(error))
}
}
})
}
func refreshAccessToken(refreshToken:String) -> Cancellable? {
base?.isRefreshing = true /// start retrieveing refresh token
return base?.request(.retriveAccessToken(refreshToken: refreshToken), completion: { _ in
base?.isRefreshing = false /// end retrieveing refresh token
base?.executeAllSavedRequests()
})
}
if (base?.isRefreshing ?? false) {
reschedulCurrentRequest()
return Disposables.create { pandingCancelable?.cancel() }
}
let cancellableToken = base?.request(token, callbackQueue: callbackQueue, progress: nil) { result in
switch result {
case let .success(response):
if !(base?.isRefreshing ?? false), !token.isOAuth2TokenRefreshType, response.statusCode == TokenPlugin.CODE_UNAUTHORIZED, let refreshToken = token.getRefreshToken {
reschedulCurrentRequest()
_ = refreshAccessToken(refreshToken: refreshToken)
} else if (base?.isRefreshing ?? false) {
reschedulCurrentRequest()
} else {
single(.success(response))
}
case let .failure(error):
single(.error(error))
}
}
return Disposables.create {
cancellableToken?.cancel()
}
}
}
}
CustomProvider:
class MyMoyaProvider: MoyaProvider<MyServices> {
var isRefreshing = false
private var pandingRequests: [DispatchWorkItem] = []
func saveRequest(_ block: #escaping () -> Void) {
// Save request to DispatchWorkItem array
pandingRequests.append( DispatchWorkItem {
block()
})
}
func executeAllSavedRequests() {
pandingRequests.forEach({ DispatchQueue.global().async(execute: $0); })
pandingRequests.removeAll()
}
}
extension TargetType {
var isOAuth2TokenRefreshType: Bool { return false }
var getRefreshToken:String? { return nil }
}

Related

How add page for pagination in protocol oriented networking

I just learn how to create a protocol oriented networking, but I just don't get how to add page for pagination in protocol. my setup is like this
protocol Endpoint {
var base: String { get }
var path: String { get }
}
extension Endpoint {
var apiKey: String {
return "api_key=SOME_API_KEY"
}
var urlComponents: URLComponents {
var components = URLComponents(string: base)!
components.path = path
components.query = apiKey
return components
}
var request: URLRequest {
let url = urlComponents.url!
return URLRequest(url: url)
}
}
enum MovieDBResource {
case popular
case topRated
case nowPlaying
case reviews(id: Int)
}
extension MovieDBResource: Endpoint {
var base: String {
return "https://api.themoviedb.org"
}
var path: String {
switch self {
case .popular: return "/3/movie/popular"
case .topRated: return "/3/movie/top_rated"
case .nowPlaying: return "/3/movie/now_playing"
case .reviews(let id): return "/3/movie/\(id)/videos"
}
}
}
and this is my network service class method
func getReview(movie resource: MovieDBResource, completion: #escaping (Result<MovieItem, MDBError>) -> Void) {
print(resource.request)
fetch(with: resource.request, decode: { (json) -> MovieItem? in
guard let movieResults = json as? MovieItem else { return nil }
return movieResults
}, completion: completion)
}
How I add page in protocol so I can called and add parameter in viewController, for now my service in my viewController is like this. I need parameter to page
service.getReview(movie: .reviews(id: movie.id)) { [weak self] results in
guard let self = self else { return }
switch results {
case .success(let movies):
print(movies)
case .failure(let error):
print(error)
}
}
Thank you
You can add it as a parameter to your enum case like:
case popular(queryParameters: [String: Any])
You can create Router class that has buildRequest(_:Endpoint), and here you can get the queryParameters and add it to the url witch will contain page and limit or any other query parameters.
you can also add another parameter for bodyParameters if the request HTTPMethod is POST and you need to send data in the body.

How to get all page url of a paginated page with RxSwift

I am starting my first RxSwift project.
I want all the pagination url of this page : http://mangafox.me/directory/
So far, I can get the next page url.
Here is the code used so far:
func getNextPageUrl(currentStringUrl: String) -> Observable<String> {
return Observable.create { observer -> Disposable in
let request = Alamofire.request(currentStringUrl)
.validate()
.responseString { response in
if response.result.isSuccess {
if let doc = HTML(html: response.result.value!, encoding: .utf8) {
if let nextPage = doc.css("a > span.next").first?.parent {
observer.onNext("\(currentStringUrl)\(nextPage["href"]!)")
}
}
observer.onCompleted()
}else{
observer.onError(response.result.error!)
}
}
return Disposables.create {
request.cancel()
}
}
}
Now I want to make a string list of other next pagination link. How can I make that?
I have found myself so here my method :
func getAllMangaPage() -> Observable<String>{
return Observable.create { observer in
var urls :[String] = []
let subs = self.repeatEvery(second: 0.2)
.subscribe { event in
if urls.isEmpty {
urls.append(self.popularMangaInitialUrl())
}else{
self.getNextPageUrl(currentStringUrl: urls.last!)
.subscribe(
onNext: { url in
var nextUrl = url as String
if !nextUrl.contains("http://") {
let completeUrl = "\(self.popularMangaInitialUrl())\(nextUrl)"
if !urls.contains(completeUrl){
urls.append("\(self.popularMangaInitialUrl())\(nextUrl)")
observer.onNext(urls.last!)
}
}else{
if !urls.contains(nextUrl){
urls.append(nextUrl)
observer.onNext(urls.last!)
}
}
},
onCompleted: {
},
onDisposed: {
}
)
}
}
return Disposables.create {
print("Disposed")
subs.dispose()
}
}
}

unable to upload file RXSwift Moya multipart

I'm using Moya for handling communication between my swift application and api, I'm able to post and get data but unable to post file to api server, following is my code
enum TestApi {
...
case PostTest(obj: [String: AnyObject])
...
}
extension TestApi: TargetType {
var baseURL: NSURL {
switch self {
case .PostTest:
return NSURL(string: "http://192.168.9.121:3000")!
}
}
var path: String {
switch self {
case .PostTest:
return "/api/file"
}
}
var method: Moya.Method {
switch self {
case .PostTest:
return .POST
}
}
var parameters: [String: AnyObject]? {
switch self {
case .PostTest(let obj):
return ["image": obj["image"]!]
}
}
var sampleData: NSData {
return "".dataUsingEncoding(NSUTF8StringEncoding)!
}
var multipartBody: [MultipartFormData]? {
switch self {
case .PostTest(let multipartData):
guard let image = multipartData["image"] as? [NSData] else { return[] }
let formData: [MultipartFormData] = image.map{MultipartFormData(provider: .Data($0), name: "images", mimeType: "image/jpeg", fileName: "photo.jpg")}
return formData
default:
return []
}
}
}
and following is the way I called
return testApiProvider.request(.PostTest(obj: _file)).debug().mapJSON().map({ JSON -> EKResponse? in
return Mapper<EKResponse>().map(JSON)
})
I dont receive no response and no hit was sent to the api server
Multipart body is deprecated in Moya 8.0.0. Instead of that use Task for uploading.
Check this issue:
Moya multipart upload target
the subscription is missing in the calling code. This is not really a Moya problem, but problem with Reactive Extensions. the following .subscribeNext { _ in } fixed my issue
return testApiProvider
.request(.PostTest(obj: _file))
.debug()
.mapJSON()
.map({ JSON -> EKResponse? in
return Mapper<EKResponse>().map(JSON)
})
.subscribeNext { _ in }

swift, alamofire cancel previous request

I have an NetworkRequest class, where all my alamofire requests made:
class NetworkRequest {
static let request = NetworkRequest()
var currentRequest: Alamofire.Request?
let dataManager = DataManager()
let networkManager = NetworkReachabilityManager()
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
func downloadData<T: Film>(slug: String, provider: String, section: String, dynamic: String, anyClass: T, completion: ([T]?) -> Void ) {
var token: String = ""
if LOGGED_IN == true {
token = "\(NSUserDefaults.standardUserDefaults().valueForKey(TOKEN)!)"
}
let headers = [
"Access": "application/json",
"Authorization": "Bearer \(token)"
]
let dataUrl = "\(BASE_URL)\(slug)\(provider)\(section)\(dynamic)"
print(headers)
print(dataUrl)
if networkManager!.isReachable {
currentRequest?.cancel()
dispatch_async(dispatch_get_global_queue(priority, 0)) {
if let url = NSURL(string: dataUrl) {
let request = Alamofire.request(.GET, url, headers: headers)
request.validate().responseJSON { response in
switch response.result {
case .Success:
if let data = response.result.value as! [String: AnyObject]! {
let receivedData = self.dataManager.parseDataToFilms(data, someClass: anyClass)
completion(receivedData)
}
case .Failure(let error):
print("Alamofire error: \(error)")
if error.code == 1001 {
self.goToNoConnectionVC()
}
print("canceled")
}
}
}
}
} else {
goToNoConnectionVC()
}
}
}
And I need to cancel previous downloadData request, when the new one starts, tried to cancel using currentRequest?.cancel(), but it doesn't help.
Already tried to cancelOperations using NSOperationsBlock, but it doesn't cancels current operation.
I block UI now, so that user can't send another request. But this is not correct, causes some errors later...
Pls, help
Now on Alamofire 4 the Alamofire.Manager.sharedInstance.session is not available you should use this solution:
let sessionManager = Alamofire.SessionManager.default
sessionManager.session.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in
dataTasks.forEach { $0.cancel() }
uploadTasks.forEach { $0.cancel() }
downloadTasks.forEach { $0.cancel() }
}
and if you want to cancel (suspend, resume) a particular request you can check the request url in your .forEach block like this:
dataTasks.forEach {
if ($0.originalRequest?.url?.absoluteString == url) {
$0.cancel()
}
}
Found needed solution:
func stopAllSessions() {
Alamofire.Manager.sharedInstance.session.getAllTasksWithCompletionHandler { tasks in
tasks.forEach { $0.cancel() }
}
}
Update for Alamofire 5
func stopAllSessions() {
AF.session.getTasksWithCompletionHandler { (sessionDataTask, uploadData, downloadData) in
sessionDataTask.forEach { $0.cancel() }
uploadData.forEach { $0.cancel() }
downloadData.forEach { $0.cancel() }
}
}
If you want to cancel the request, you need to trace the requests made and try to cancel it. You can store it in an array and cancel every previous request stored.
In your code you create a request:
let request = Alamofire.request(.GET, url, headers: headers)
but you try to cancel the currentRequest?.cancel() that is never valued.
Swift 5
To cancel all requests use
Alamofire.Session.default.session.getTasksWithCompletionHandler({ dataTasks, uploadTasks, downloadTasks in
dataTasks.forEach { $0.cancel() }
uploadTasks.forEach { $0.cancel() }
downloadTasks.forEach { $0.cancel() }
})
To cancel a request with a particular url use
Alamofire.Session.default.session.getTasksWithCompletionHandler({ dataTasks, uploadTasks, downloadTasks in
dataTasks.forEach {
if ($0.originalRequest?.url?.absoluteString == "www.google.com") {
$0.cancel()
}
}
uploadTasks.forEach {
if ($0.originalRequest?.url?.absoluteString == "www.google.com") {
$0.cancel()
}
}
downloadTasks.forEach {
if ($0.originalRequest?.url?.absoluteString == "www.google.com") {
$0.cancel()
}
}
})

Refactoring Code in Swift

I have a pretty standard problem here that I am a little confused how to make more elegant in the swift tradition of things.
I have a base URL string in my APIClient, a path for different methods to use, and methods might pass in a optional string.
How do I write this such that it is much cleaner? I feel like I am writing swift code but not using any of the new constructs (like guard, let), etc.
// call the API
ApiClient.sharedInstance.getUser("56cfffce227a6c2c9b000001", successCompletion: successCompletion, failureCompletion: failureCompletion)
// setting the current URL
let currentURL = String("http://localhost:3000")
// class definition
class ApiClient {
var baseURL:String!;
// have to init the string class for the
init (base:String){
self.baseURL = base
}
class var sharedInstance: ApiClient {
struct Static{
static let instance = ApiClient(base: currentURL)
}
return Static.instance
}
func getUser(user_id: String?, successCompletion: ResultHandler<User>, failureCompletion:ResultHandler<FailureReason>){
// this is awkward
var url: URLStringConvertible!
// this is even more awkward
if user_id != nil {
url = "\(currentURL)/users/\(user_id!).json"
} else {
url = "\(currentURL)/users/me"
}
}
}
Cleaned some things up and removed some unnecessary forced-unwraps.
class ApiClient {
var baseURL: String;
// have to init the string class for the
init (base:String){
self.baseURL = base
}
class var sharedInstance: ApiClient {
struct Static{
static let instance = ApiClient(base: currentURL)
}
return Static.instance
}
func getUser(user_id: String?, successCompletion: ResultHandler<User>, failureCompletion:ResultHandler<FailureReason>){
var url: URLStringConvertible
if let user_id = user_id {
url = "\(currentURL)/users/\(user_id!).json"
}
else{
url = "\(currentURL)/users/me"
}
}
Depending on what you're doing in getUser, things could be done differently.
FYI, what I tend to do for singletons: (arguably not the best):
private static var realSharedInstance: Class?
static var sharedInstance: Class {
get {
if let realSharedInstance = realSharedInstance {
return realSharedInstance
}
else{
realSharedInstance = Class()
return realSharedInstance!
}
}
}
For me, I like to create an Endpoint for every endpoint that the API supports. In this case, a UsersEndpoint which can make requests via APIClient.
This is easier to scale (for me) when there more endpoints and each endpoint has multiple resources.
Use in code:
MyAPI.sharedInstance.users.getUser(id, parameters: params, success: {}, failure: {})
MyAPI.sharedInstance.anotherEndpoint.doSomething(parameters: params, success: {}, failure: {})
The classes:
class MyAPI {
static let sharedInstance = MyAPI()
let users: UsersEndpoint
// more endpoints here
init() {
users = UsersEndpoint()
// ....
}
}
class APIClient {
static let sharedClient = APIClient()
var manager : Alamofire.Manager!
var baseURL: String
init() {
baseURL = String(format: "https://%#%#", self.instanceUrl, API.Version)
}
func get(endpoint endpoint: String,
parameters: [String:AnyObject],
success: SuccessCallback,
failure: FailureCallback) {
let requestUrl = NSURL(string: "\(self.baseURL)\(endpoint)")!
manager.request(.GET, requestUrl, parameters: parameters, headers: headers)
.validate()
.responseData { response in
switch response.result {
case .Success(let value):
if let data: NSData = value {
success(data: data)
log.verbose("Success - GET requestUrl=\(requestUrl)")
}
break;
case .Failure(let error):
failure(error: error)
log.error("Failure - GET requestUrl=\(requestUrl)\nError = \(error.localizedDescription)")
break;
}
}
}
}
class APIFacade {
var endpoint: String
init(endpoint: String) {
self.endpoint = endpoint;
}
func get(endpoint endpoint: String,
parameters: [String:AnyObject],
success: SuccessCallback,
failure: FailureCallback) {
APIClient.sharedClient.get(endpoint: endpoint, parameters: parameters, success: success, failure: failure)
}
}
class UsersEndpoint: APIFacade {
init() {
super.init(endpoint: "/users")
}
func getUser(id: String, parameters: [String:AnyObject], success: SuccessCallback, failure: FailureCallback) {
super.get(endpoint: "\(self.endpoint)/\(id)", parameters: parameters, success: success, failure: failure)
}
}