diff --git a/Account/Sources/Account/AccountDelegates/FeedlyAccountDelegate+OAuth.swift b/Account/Sources/Account/AccountDelegates/FeedlyAccountDelegate+OAuth.swift index 9c1ef6717..5ee8dd3e7 100644 --- a/Account/Sources/Account/AccountDelegates/FeedlyAccountDelegate+OAuth.swift +++ b/Account/Sources/Account/AccountDelegates/FeedlyAccountDelegate+OAuth.swift @@ -12,7 +12,7 @@ import Secrets /// Models the access token response from Feedly. /// https://developer.feedly.com/v3/auth/#exchanging-an-auth-code-for-a-refresh-token-and-an-access-token -public struct FeedlyOAuthAccessTokenResponse: Decodable, OAuthAccessTokenResponse { +public struct FeedlyOAuthAccessTokenResponse: Decodable, OAuthAccessTokenResponse, Sendable { /// The ID of the Feedly user. public var id: String @@ -45,55 +45,38 @@ extension FeedlyAccountDelegate: OAuthAuthorizationGranting { scope: oauthAuthorizationGrantScope, client: client) let caller = FeedlyAPICaller(transport: transport, api: environment, secretsProvider: secretsProvider) + let response = try await caller.requestAccessToken(request) - return try await withCheckedThrowingContinuation { continuation in - caller.requestAccessToken(request) { result in - switch result { - case .success(let response): - let accessToken = Credentials(type: .oauthAccessToken, username: response.id, secret: response.accessToken) - - let refreshToken: Credentials? = { - guard let token = response.refreshToken else { - return nil - } - return Credentials(type: .oauthRefreshToken, username: response.id, secret: token) - }() - - let grant = OAuthAuthorizationGrant(accessToken: accessToken, refreshToken: refreshToken) - - continuation.resume(returning: grant) - - case .failure(let error): - continuation.resume(throwing: error) - } + let accessToken = Credentials(type: .oauthAccessToken, username: response.id, secret: response.accessToken) + let refreshToken: Credentials? = { + guard let token = response.refreshToken else { + return nil } - } + return Credentials(type: .oauthRefreshToken, username: response.id, secret: token) + }() + + let grant = OAuthAuthorizationGrant(accessToken: accessToken, refreshToken: refreshToken) + + return grant } } extension FeedlyAccountDelegate: OAuthAccessTokenRefreshing { - func refreshAccessToken(with refreshToken: String, client: OAuthAuthorizationClient, completion: @escaping (Result) -> ()) { + func refreshAccessToken(with refreshToken: String, client: OAuthAuthorizationClient) async throws -> OAuthAuthorizationGrant { + let request = OAuthRefreshAccessTokenRequest(refreshToken: refreshToken, scope: nil, client: client) - - caller.refreshAccessToken(request) { result in - switch result { - case .success(let response): - let accessToken = Credentials(type: .oauthAccessToken, username: response.id, secret: response.accessToken) - - let refreshToken: Credentials? = { - guard let token = response.refreshToken else { - return nil - } - return Credentials(type: .oauthRefreshToken, username: response.id, secret: token) - }() - - let grant = OAuthAuthorizationGrant(accessToken: accessToken, refreshToken: refreshToken) - - completion(.success(grant)) - - case .failure(let error): - completion(.failure(error)) + let response = try await caller.refreshAccessToken(request) + + let accessToken = Credentials(type: .oauthAccessToken, username: response.id, secret: response.accessToken) + let refreshToken: Credentials? = { + guard let token = response.refreshToken else { + return nil } - } + return Credentials(type: .oauthRefreshToken, username: response.id, secret: token) + }() + + let grant = OAuthAuthorizationGrant(accessToken: accessToken, refreshToken: refreshToken) + + return grant } } diff --git a/Account/Sources/Account/Feedly/FeedlyAPICaller.swift b/Account/Sources/Account/Feedly/FeedlyAPICaller.swift index bceda7ee4..857ef3fd1 100644 --- a/Account/Sources/Account/Feedly/FeedlyAPICaller.swift +++ b/Account/Sources/Account/Feedly/FeedlyAPICaller.swift @@ -155,7 +155,7 @@ protocol FeedlyAPICallerDelegate: AnyObject { guard !isSuspended else { throw TransportError.suspended } - var request = try urlRequest(path: "/v3/opml", method: HTTPMethod.post, includeJSONHeaders: false, includeOauthToken: true) + var request = try urlRequest(path: "/v3/opml", method: HTTPMethod.post, includeJSONHeaders: false, includeOAuthToken: true) request.addValue("text/xml", forHTTPHeaderField: HTTPRequestHeader.contentType) request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.acceptType) request.httpBody = opmlData @@ -170,14 +170,12 @@ protocol FeedlyAPICallerDelegate: AnyObject { guard !isSuspended else { throw TransportError.suspended } - var request = try urlRequest(path: "/v3/collections", method: HTTPMethod.post, includeJSONHeaders: true, includeOauthToken: true) + var request = try urlRequest(path: "/v3/collections", method: HTTPMethod.post, includeJSONHeaders: true, includeOAuthToken: true) struct CreateCollectionBody: Encodable { var label: String } - let encoder = JSONEncoder() - let data = try encoder.encode(CreateCollectionBody(label: label)) - request.httpBody = data + try addObject(CreateCollectionBody(label: label), to: &request) let (httpResponse, collections) = try await send(request: request, resultType: [FeedlyCollection].self) @@ -191,15 +189,13 @@ protocol FeedlyAPICallerDelegate: AnyObject { guard !isSuspended else { throw TransportError.suspended } - var request = try urlRequest(path: "/v3/collections", method: HTTPMethod.post, includeJSONHeaders: true, includeOauthToken: true) + var request = try urlRequest(path: "/v3/collections", method: HTTPMethod.post, includeJSONHeaders: true, includeOAuthToken: true) struct RenameCollectionBody: Encodable { var id: String var label: String } - let encoder = JSONEncoder() - let data = try encoder.encode(RenameCollectionBody(id: id, label: name)) - request.httpBody = data + try addObject(RenameCollectionBody(id: id, label: name), to: &request) let (httpResponse, collections) = try await send(request: request, resultType: [FeedlyCollection].self) @@ -220,7 +216,7 @@ protocol FeedlyAPICallerDelegate: AnyObject { guard let encodedID = encodeForURLPath(id) else { throw FeedlyAccountDelegateError.unexpectedResourceID(id) } - let request = try urlRequest(path: "/v3/collections/\(encodedID)", method: HTTPMethod.delete, includeJSONHeaders: true, includeOauthToken: true) + let request = try urlRequest(path: "/v3/collections/\(encodedID)", method: HTTPMethod.delete, includeJSONHeaders: true, includeOAuthToken: true) let (httpResponse, _) = try await send(request: request, resultType: Optional.self) @@ -229,7 +225,7 @@ protocol FeedlyAPICallerDelegate: AnyObject { } } - func removeFeed(_ feedId: String, fromCollectionWith collectionID: String) async throws { + func removeFeed(_ feedID: String, fromCollectionWith collectionID: String) async throws { guard !isSuspended else { throw TransportError.suspended } @@ -246,14 +242,12 @@ protocol FeedlyAPICallerDelegate: AnyObject { var request = URLRequest(url: url) request.httpMethod = HTTPMethod.delete addJSONHeaders(&request) - try addOauthAccessToken(&request) + try addOAuthAccessToken(&request) struct RemovableFeed: Encodable { let id: String } - let encoder = JSONEncoder() - let data = try encoder.encode([RemovableFeed(id: feedId)]) - request.httpBody = data + try addObject([RemovableFeed(id: feedID)], to: &request) // `resultType` is optional because the Feedly API has gone from returning an array of removed feeds to returning `null`. // https://developer.feedly.com/v3/collections/#remove-multiple-feeds-from-a-personal-collection @@ -283,15 +277,13 @@ extension FeedlyAPICaller: FeedlyAddFeedToCollectionService { var request = URLRequest(url: url) request.httpMethod = HTTPMethod.put addJSONHeaders(&request) - try addOauthAccessToken(&request) + try addOAuthAccessToken(&request) struct AddFeedBody: Encodable { var id: String var title: String? } - let encoder = JSONEncoder() - let data = try encoder.encode(AddFeedBody(id: feedID.id, title: title)) - request.httpBody = data + try addObject(AddFeedBody(id: feedID.id, title: title), to: &request) let (_, collectionFeeds) = try await send(request: request, resultType: [FeedlyFeed].self) guard let collectionFeeds else { @@ -305,6 +297,7 @@ extension FeedlyAPICaller: FeedlyAddFeedToCollectionService { extension FeedlyAPICaller: OAuthAuthorizationCodeGrantRequesting { static func authorizationCodeURLRequest(for request: OAuthAuthorizationRequest, baseUrlComponents: URLComponents) -> URLRequest { + var components = baseUrlComponents components.path = "/v3/auth/auth" components.queryItems = request.queryItems @@ -323,154 +316,63 @@ extension FeedlyAPICaller: OAuthAuthorizationCodeGrantRequesting { typealias AccessTokenResponse = FeedlyOAuthAccessTokenResponse - func requestAccessToken(_ authorizationRequest: OAuthAccessTokenRequest, completion: @escaping (Result) -> ()) { - guard !isSuspended else { - return DispatchQueue.main.async { - completion(.failure(TransportError.suspended)) - } - } - - var components = baseURLComponents - components.path = "/v3/auth/token" - - guard let url = components.url else { - fatalError("\(components) does not produce a valid URL.") - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - request.addValue("application/json", forHTTPHeaderField: "Accept-Type") - - do { - let encoder = JSONEncoder() - encoder.keyEncodingStrategy = .convertToSnakeCase - request.httpBody = try encoder.encode(authorizationRequest) - } catch { - DispatchQueue.main.async { - completion(.failure(error)) - } - return - } - - send(request: request, resultType: AccessTokenResponse.self, keyDecoding: .convertFromSnakeCase) { result in - switch result { - case .success(let (_, tokenResponse)): - if let response = tokenResponse { - completion(.success(response)) - } else { - completion(.failure(URLError(.cannotDecodeContentData))) - } - case .failure(let error): - completion(.failure(error)) - } + func requestAccessToken(_ authorizationRequest: OAuthAccessTokenRequest) async throws -> FeedlyOAuthAccessTokenResponse { + + guard !isSuspended else { throw TransportError.suspended } + + var request = try urlRequest(path: "/v3/auth/token", method: HTTPMethod.post, includeJSONHeaders: true, includeOAuthToken: false) + try addObject(authorizationRequest, keyEncodingStrategy: .convertToSnakeCase, to: &request) + + let (_, tokenResponse) = try await send(request: request, resultType: AccessTokenResponse.self) + guard let tokenResponse else { + throw URLError(.cannotDecodeContentData) } + + return tokenResponse } } extension FeedlyAPICaller: OAuthAcessTokenRefreshRequesting { - func refreshAccessToken(_ refreshRequest: OAuthRefreshAccessTokenRequest, completion: @escaping (Result) -> ()) { - guard !isSuspended else { - return DispatchQueue.main.async { - completion(.failure(TransportError.suspended)) - } - } - - var components = baseURLComponents - components.path = "/v3/auth/token" - - guard let url = components.url else { - fatalError("\(components) does not produce a valid URL.") - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - request.addValue("application/json", forHTTPHeaderField: "Accept-Type") - - do { - let encoder = JSONEncoder() - encoder.keyEncodingStrategy = .convertToSnakeCase - request.httpBody = try encoder.encode(refreshRequest) - } catch { - DispatchQueue.main.async { - completion(.failure(error)) - } - return - } - - send(request: request, resultType: AccessTokenResponse.self, keyDecoding: .convertFromSnakeCase) { result in - switch result { - case .success(let (_, tokenResponse)): - if let response = tokenResponse { - completion(.success(response)) - } else { - completion(.failure(URLError(.cannotDecodeContentData))) - } - case .failure(let error): - completion(.failure(error)) - } + func refreshAccessToken(_ refreshRequest: OAuthRefreshAccessTokenRequest) async throws -> FeedlyOAuthAccessTokenResponse { + + guard !isSuspended else { throw TransportError.suspended } + + var request = try urlRequest(path: "/v3/auth/token", method: HTTPMethod.post, includeJSONHeaders: true, includeOAuthToken: false) + try addObject(refreshRequest, keyEncodingStrategy: .convertToSnakeCase, to: &request) + + let (_, tokenResponse) = try await send(request: request, resultType: AccessTokenResponse.self) + guard let tokenResponse else { + throw URLError(.cannotDecodeContentData) } + + return tokenResponse } } extension FeedlyAPICaller: FeedlyGetCollectionsService { - func getCollections(completion: @escaping @Sendable (Result<[FeedlyCollection], Error>) -> ()) { - guard !isSuspended else { - return DispatchQueue.main.async { - completion(.failure(TransportError.suspended)) - } + func getCollections() async throws -> [FeedlyCollection] { + + guard !isSuspended else { throw TransportError.suspended } + + let request = try urlRequest(path: "/v3/collections", method: HTTPMethod.get, includeJSONHeaders: true, includeOAuthToken: true) + + let (_, collections) = try await send(request: request, resultType: [FeedlyCollection].self) + guard let collections else { + throw URLError(.cannotDecodeContentData) } - guard let accessToken = credentials?.secret else { - return DispatchQueue.main.async { - completion(.failure(CredentialsError.incompleteCredentials)) - } - } - var components = baseURLComponents - components.path = "/v3/collections" - - guard let url = components.url else { - fatalError("\(components) does not produce a valid URL.") - } - - var request = URLRequest(url: url) - request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) - request.addValue("application/json", forHTTPHeaderField: "Accept-Type") - request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) - - send(request: request, resultType: [FeedlyCollection].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in - switch result { - case .success(let (_, collections)): - if let response = collections { - completion(.success(response)) - } else { - completion(.failure(URLError(.cannotDecodeContentData))) - } - case .failure(let error): - completion(.failure(error)) - } - } + return collections } } extension FeedlyAPICaller: FeedlyGetStreamContentsService { - @MainActor func getStreamContents(for resource: FeedlyResourceID, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool?, completion: @escaping (Result) -> ()) { - guard !isSuspended else { - return DispatchQueue.main.async { - completion(.failure(TransportError.suspended)) - } - } - - guard let accessToken = credentials?.secret else { - return DispatchQueue.main.async { - completion(.failure(CredentialsError.incompleteCredentials)) - } - } - + @MainActor func getStreamContents(for resource: FeedlyResourceID, continuation: String?, newerThan: Date?, unreadOnly: Bool?) async throws -> FeedlyStream { + + guard !isSuspended else { throw TransportError.suspended } + var components = baseURLComponents components.path = "/v3/streams/contents" @@ -505,22 +407,16 @@ extension FeedlyAPICaller: FeedlyGetStreamContentsService { } var request = URLRequest(url: url) - request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) - request.addValue("application/json", forHTTPHeaderField: "Accept-Type") - request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) - - send(request: request, resultType: FeedlyStream.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in - switch result { - case .success(let (_, collections)): - if let response = collections { - completion(.success(response)) - } else { - completion(.failure(URLError(.cannotDecodeContentData))) - } - case .failure(let error): - completion(.failure(error)) - } + addJSONHeaders(&request) + try addOAuthAccessToken(&request) + + let (_, collections) = try await send(request: request, resultType: FeedlyStream.self) + + guard let collections else { + throw URLError(.cannotDecodeContentData) } + + return collections } } @@ -809,7 +705,7 @@ extension FeedlyAPICaller: FeedlyLogoutService { private extension FeedlyAPICaller { - func urlRequest(path: String, method: String, includeJSONHeaders: Bool, includeOauthToken: Bool) throws -> URLRequest { + func urlRequest(path: String, method: String, includeJSONHeaders: Bool, includeOAuthToken: Bool) throws -> URLRequest { let url = apiURL(path) var request = URLRequest(url: url) @@ -819,8 +715,8 @@ private extension FeedlyAPICaller { if includeJSONHeaders { addJSONHeaders(&request) } - if includeOauthToken { - try addOauthAccessToken(&request) + if includeOAuthToken { + try addOAuthAccessToken(&request) } return request @@ -832,7 +728,7 @@ private extension FeedlyAPICaller { request.addValue("application/json", forHTTPHeaderField: "Accept-Type") } - func addOauthAccessToken(_ request: inout URLRequest) throws { + func addOAuthAccessToken(_ request: inout URLRequest) throws { guard let accessToken = credentials?.secret else { throw CredentialsError.incompleteCredentials @@ -852,4 +748,10 @@ private extension FeedlyAPICaller { return url } + + func addObject(_ object: T, keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy = .useDefaultKeys, to request: inout URLRequest) throws { + + let data = try JSONEncoder().encode(object) + request.httpBody = data + } } diff --git a/Account/Sources/Account/Feedly/OAuthAcessTokenRefreshing.swift b/Account/Sources/Account/Feedly/OAuthAcessTokenRefreshing.swift index de7d21fbc..47de9e279 100644 --- a/Account/Sources/Account/Feedly/OAuthAcessTokenRefreshing.swift +++ b/Account/Sources/Account/Feedly/OAuthAcessTokenRefreshing.swift @@ -12,7 +12,7 @@ import Feedly /// Models section 6 of the OAuth 2.0 Authorization Framework /// https://tools.ietf.org/html/rfc6749#section-6 -public struct OAuthRefreshAccessTokenRequest: Encodable { +public struct OAuthRefreshAccessTokenRequest: Encodable, Sendable { public let grantType = "refresh_token" public var refreshToken: String public var scope: String? @@ -37,11 +37,11 @@ public protocol OAuthAcessTokenRefreshRequesting { /// Access tokens expire. Perform a request for a fresh access token given the long life refresh token received when authorization was granted. /// - Parameter refreshRequest: The refresh token and other information the authorization server requires to grant the client fresh access tokens on the user's behalf. /// - Parameter completion: On success, the access token response appropriate for concrete type's service. Both the access and refresh token should be stored, preferably on the Keychain. On failure, possibly a `URLError` or `OAuthAuthorizationErrorResponse` value. - func refreshAccessToken(_ refreshRequest: OAuthRefreshAccessTokenRequest, completion: @escaping (Result) -> ()) + func refreshAccessToken(_ refreshRequest: OAuthRefreshAccessTokenRequest) async throws -> AccessTokenResponse } /// Implemented by concrete types to perform the actual request. protocol OAuthAccessTokenRefreshing: AnyObject { - - @MainActor func refreshAccessToken(with refreshToken: String, client: OAuthAuthorizationClient, completion: @escaping (Result) -> ()) + + @MainActor func refreshAccessToken(with refreshToken: String, client: OAuthAuthorizationClient) async throws -> OAuthAuthorizationGrant } diff --git a/Account/Sources/Account/Feedly/OAuthAuthorizationCodeGranting.swift b/Account/Sources/Account/Feedly/OAuthAuthorizationCodeGranting.swift index 24e907534..2ffcbb1af 100644 --- a/Account/Sources/Account/Feedly/OAuthAuthorizationCodeGranting.swift +++ b/Account/Sources/Account/Feedly/OAuthAuthorizationCodeGranting.swift @@ -110,7 +110,7 @@ public enum OAuthAuthorizationError: String, Sendable { /// Models section 4.1.3 of the OAuth 2.0 Authorization Framework /// https://tools.ietf.org/html/rfc6749#section-4.1.3 -public struct OAuthAccessTokenRequest: Encodable { +public struct OAuthAccessTokenRequest: Encodable, Sendable { public let grantType = "authorization_code" public var code: String public var redirectUri: String @@ -157,13 +157,13 @@ public protocol OAuthAuthorizationCodeGrantRequesting { /// Provides the URL request that allows users to consent to the client having access to their information. Typically loaded by a web view. /// - Parameter request: The information about the client requesting authorization to be granted access tokens. /// - Parameter baseUrlComponents: The scheme and host of the url except for the path. - static func authorizationCodeURLRequest(for request: OAuthAuthorizationRequest, baseUrlComponents: URLComponents) -> URLRequest + @MainActor static func authorizationCodeURLRequest(for request: OAuthAuthorizationRequest, baseUrlComponents: URLComponents) -> URLRequest /// Performs the request for the access token given an authorization code. /// - Parameter authorizationRequest: The authorization code and other information the authorization server requires to grant the client access tokens on the user's behalf. - /// - Parameter completion: On success, the access token response appropriate for concrete type's service. On failure, possibly a `URLError` or `OAuthAuthorizationErrorResponse` value. - func requestAccessToken(_ authorizationRequest: OAuthAccessTokenRequest, completion: @escaping (Result) -> ()) + /// - Returns: On success, the access token response appropriate for concrete type's service. On failure, throws possibly a `URLError` or `OAuthAuthorizationErrorResponse` value. + func requestAccessToken(_ authorizationRequest: OAuthAccessTokenRequest) async throws -> FeedlyOAuthAccessTokenResponse } protocol OAuthAuthorizationGranting: AccountDelegate { diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyRefreshAccessTokenOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyRefreshAccessTokenOperation.swift index 7486099c2..4bcd8061d 100644 --- a/Account/Sources/Account/Feedly/Operations/FeedlyRefreshAccessTokenOperation.swift +++ b/Account/Sources/Account/Feedly/Operations/FeedlyRefreshAccessTokenOperation.swift @@ -27,52 +27,34 @@ final class FeedlyRefreshAccessTokenOperation: FeedlyOperation { } override func run() { - let refreshToken: Credentials - - do { - guard let credentials = try account.retrieveCredentials(type: .oauthRefreshToken) else { - os_log(.debug, log: log, "Could not find a refresh token in the keychain. Check the refresh token is added to the Keychain, remove the account and add it again.") - throw TransportError.httpError(status: 403) - } - - refreshToken = credentials - - } catch { - didFinish(with: error) - return - } - - os_log(.debug, log: log, "Refreshing access token.") - - // Ignore cancellation after the request is resumed otherwise we may continue storing a potentially invalid token! - service.refreshAccessToken(with: refreshToken.secret, client: oauthClient) { result in - self.didRefreshAccessToken(result) - } - } - - private func didRefreshAccessToken(_ result: Result) { - assert(Thread.isMainThread) - - switch result { - case .success(let grant): + + Task { @MainActor in + do { - os_log(.debug, log: log, "Storing refresh token.") - // Store the refresh token first because it sends this token to the account delegate. - if let token = grant.refreshToken { - try account.storeCredentials(token) + guard let credentials = try account.retrieveCredentials(type: .oauthRefreshToken) else { + os_log(.debug, log: log, "Could not find a refresh token in the keychain. Check the refresh token is added to the Keychain, remove the account and add it again.") + throw TransportError.httpError(status: 403) } - - os_log(.debug, log: log, "Storing access token.") + + // Ignore cancellation after the request is resumed otherwise we may continue storing a potentially invalid token! + os_log(.debug, log: log, "Refreshing access token.") + let grant = try await service.refreshAccessToken(with: credentials.secret, client: oauthClient) + + // Store the refresh token first because it sends this token to the account delegate. + os_log(.debug, log: log, "Storing refresh token.") + if let refreshToken = grant.refreshToken { + try account.storeCredentials(refreshToken) + } + // Now store the access token because we want the account delegate to use it. + os_log(.debug, log: log, "Storing access token.") try account.storeCredentials(grant.accessToken) - + didFinish() + } catch { didFinish(with: error) } - - case .failure(let error): - didFinish(with: error) } } } diff --git a/Feedly/Sources/Feedly/Operations/FeedlyGetCollectionsOperation.swift b/Feedly/Sources/Feedly/Operations/FeedlyGetCollectionsOperation.swift index f9f0c1a7d..5abbde159 100644 --- a/Feedly/Sources/Feedly/Operations/FeedlyGetCollectionsOperation.swift +++ b/Feedly/Sources/Feedly/Operations/FeedlyGetCollectionsOperation.swift @@ -28,21 +28,19 @@ public final class FeedlyGetCollectionsOperation: FeedlyOperation, FeedlyCollect } public override func run() { - os_log(.debug, log: log, "Requesting collections.") - - service.getCollections { result in - MainActor.assumeIsolated { - switch result { - case .success(let collections): - os_log(.debug, log: self.log, "Received collections: %{public}@", collections.map { $0.id }) - self.collections = collections - self.didFinish() - - case .failure(let error): - os_log(.debug, log: self.log, "Unable to request collections: %{public}@.", error as NSError) - self.didFinish(with: error) - } + Task { @MainActor in + os_log(.debug, log: log, "Requesting collections.") + + do { + let collections = try await service.getCollections() + os_log(.debug, log: self.log, "Received collections: %{public}@", collections.map { $0.id }) + self.collections = collections + self.didFinish() + + } catch { + os_log(.debug, log: self.log, "Unable to request collections: %{public}@.", error as NSError) + self.didFinish(with: error) } } } diff --git a/Feedly/Sources/Feedly/Operations/FeedlyGetStreamContentsOperation.swift b/Feedly/Sources/Feedly/Operations/FeedlyGetStreamContentsOperation.swift index 86f82731b..c3a1f3854 100644 --- a/Feedly/Sources/Feedly/Operations/FeedlyGetStreamContentsOperation.swift +++ b/Feedly/Sources/Feedly/Operations/FeedlyGetStreamContentsOperation.swift @@ -98,16 +98,17 @@ public final class FeedlyGetStreamContentsOperation: FeedlyOperation, FeedlyEntr } public override func run() { - service.getStreamContents(for: resourceProvider.resource, continuation: continuation, newerThan: newerThan, unreadOnly: unreadOnly) { result in - switch result { - case .success(let stream): + + Task { @MainActor in + + do { + let stream = try await service.getStreamContents(for: resourceProvider.resource, continuation: continuation, newerThan: newerThan, unreadOnly: unreadOnly) + self.stream = stream - self.streamDelegate?.feedlyGetStreamContentsOperation(self, didGetContentsOf: stream) - self.didFinish() - - case .failure(let error): + + } catch { os_log(.debug, log: self.log, "Unable to get stream contents: %{public}@.", error as NSError) self.didFinish(with: error) } diff --git a/Feedly/Sources/Feedly/Services/FeedlyGetCollectionsService.swift b/Feedly/Sources/Feedly/Services/FeedlyGetCollectionsService.swift index b1c47abfd..17d69395b 100644 --- a/Feedly/Sources/Feedly/Services/FeedlyGetCollectionsService.swift +++ b/Feedly/Sources/Feedly/Services/FeedlyGetCollectionsService.swift @@ -9,5 +9,6 @@ import Foundation public protocol FeedlyGetCollectionsService: AnyObject { - func getCollections(completion: @escaping @Sendable (Result<[FeedlyCollection], Error>) -> ()) + + @MainActor func getCollections() async throws -> [FeedlyCollection] } diff --git a/Feedly/Sources/Feedly/Services/FeedlyGetStreamContentsService.swift b/Feedly/Sources/Feedly/Services/FeedlyGetStreamContentsService.swift index ceac9d931..4b1260acd 100644 --- a/Feedly/Sources/Feedly/Services/FeedlyGetStreamContentsService.swift +++ b/Feedly/Sources/Feedly/Services/FeedlyGetStreamContentsService.swift @@ -9,5 +9,6 @@ import Foundation public protocol FeedlyGetStreamContentsService: AnyObject { - func getStreamContents(for resource: FeedlyResourceID, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completion: @escaping (Result) -> ()) + + @MainActor func getStreamContents(for resource: FeedlyResourceID, continuation: String?, newerThan: Date?, unreadOnly: Bool?) async throws -> FeedlyStream }