Some simplification to login
Creating async await alternatives to Combine publishers in cases where that makes sense. Near term goal is to centralize responsibility for caching of user info. contributes to iOS-319
This commit is contained in:
parent
8ee189e3cf
commit
1e472c8232
@ -169,12 +169,14 @@ class MastodonLoginViewController: UIViewController, NeedsDependency {
|
||||
|
||||
self.mastodonAuthenticationController = authenticationController
|
||||
authenticationController.authenticationSession?.presentationContextProvider = self
|
||||
authenticationController.authenticationSession?.start()
|
||||
|
||||
self.authenticationViewModel.authenticate(
|
||||
info: info,
|
||||
pinCodePublisher: authenticationController.pinCodePublisher
|
||||
pinCodePublisher: authenticationController.resultStream
|
||||
)
|
||||
|
||||
authenticationController.authenticationSession?.start()
|
||||
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
@ -24,12 +24,12 @@ extension MastodonRegisterViewController {
|
||||
|
||||
let instanceResponse = try await APIService.shared.instance(domain: domain, authenticationBox: nil).singleOutput()
|
||||
let applicationResponse = try await APIService.shared.createApplication(domain: domain).singleOutput()
|
||||
let accessTokenResponse = try await APIService.shared.applicationAccessToken(
|
||||
let accessToken = try await APIService.shared.applicationAccessToken(
|
||||
domain: domain,
|
||||
clientID: applicationResponse.value.clientID!,
|
||||
clientSecret: applicationResponse.value.clientSecret!,
|
||||
redirectURI: applicationResponse.value.redirectURI!
|
||||
).singleOutput()
|
||||
)
|
||||
|
||||
viewController.viewModel = MastodonRegisterViewModel(
|
||||
context: context,
|
||||
@ -39,7 +39,7 @@ extension MastodonRegisterViewController {
|
||||
application: applicationResponse.value
|
||||
)!,
|
||||
instance: instanceResponse.value,
|
||||
applicationToken: accessTokenResponse.value
|
||||
applicationToken: accessToken
|
||||
)
|
||||
|
||||
return viewController
|
||||
|
@ -11,6 +11,7 @@ import CoreDataStack
|
||||
import Combine
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
import AuthenticationServices
|
||||
|
||||
@MainActor
|
||||
final class AuthenticationViewModel {
|
||||
@ -136,50 +137,38 @@ extension AuthenticationViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
func authenticate(info: AuthenticateInfo, pinCodePublisher: PassthroughSubject<String, Never>) {
|
||||
pinCodePublisher
|
||||
.handleEvents(receiveOutput: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.isAuthenticating.value = true
|
||||
})
|
||||
.compactMap { [weak self] code -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error>? in
|
||||
guard let self = self else { return nil }
|
||||
return APIService.shared
|
||||
.userAccessToken(
|
||||
domain: info.domain,
|
||||
clientID: info.clientID,
|
||||
clientSecret: info.clientSecret,
|
||||
redirectURI: info.redirectURI,
|
||||
code: code
|
||||
)
|
||||
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in
|
||||
let token = response.value
|
||||
return AuthenticationViewModel.verifyAndSaveAuthentication(
|
||||
context: self.context,
|
||||
info: info,
|
||||
userToken: token
|
||||
func authenticate(info: AuthenticateInfo, pinCodePublisher: AsyncThrowingStream<String, Error>) {
|
||||
Task {
|
||||
do {
|
||||
for try await code in pinCodePublisher {
|
||||
self.isAuthenticating.value = true
|
||||
let token = try await APIService.shared
|
||||
.userAccessToken(
|
||||
domain: info.domain,
|
||||
clientID: info.clientID,
|
||||
clientSecret: info.clientSecret,
|
||||
redirectURI: info.redirectURI,
|
||||
code: code
|
||||
)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.switchToLatest()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
self.isAuthenticating.value = false
|
||||
self.error.value = error
|
||||
case .finished:
|
||||
break
|
||||
let account = try await AuthenticationViewModel.verifyAndSaveAuthentication(
|
||||
context: self.context,
|
||||
info: info,
|
||||
userToken: token
|
||||
)
|
||||
self.authenticated.send((domain: info.domain, account: account))
|
||||
}
|
||||
} catch let error {
|
||||
self.isAuthenticating.value = false
|
||||
if let error = error as? ASWebAuthenticationSessionError {
|
||||
if error.errorCode == ASWebAuthenticationSessionError.canceledLogin.rawValue {
|
||||
//cancelled
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// error
|
||||
}
|
||||
} receiveValue: { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
let account = response.value
|
||||
|
||||
self.authenticated.send((domain: info.domain, account: account))
|
||||
}
|
||||
.store(in: &self.disposeBag)
|
||||
}
|
||||
}
|
||||
|
||||
static func verifyAndSaveAuthentication(
|
||||
@ -214,4 +203,33 @@ extension AuthenticationViewModel {
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
static func verifyAndSaveAuthentication(
|
||||
context: AppContext,
|
||||
info: AuthenticateInfo,
|
||||
userToken: Mastodon.Entity.Token
|
||||
) async throws -> Mastodon.Entity.Account {
|
||||
let authorization = Mastodon.API.OAuth.Authorization(accessToken: userToken.accessToken)
|
||||
|
||||
let account = try await APIService.shared.accountVerifyCredentials(
|
||||
domain: info.domain,
|
||||
authorization: authorization
|
||||
)
|
||||
|
||||
let authentication = MastodonAuthentication
|
||||
.createFrom(domain: info.domain,
|
||||
userID: account.id,
|
||||
username: account.username,
|
||||
appAccessToken: userToken.accessToken, // TODO: swap app token
|
||||
userAccessToken: userToken.accessToken,
|
||||
clientID: info.clientID,
|
||||
clientSecret: info.clientSecret,
|
||||
accountCreatedAt: account.createdAt)
|
||||
|
||||
AuthenticationServiceProvider.shared
|
||||
.authentications
|
||||
.insert(authentication, at: 0) // TODO: this should not be happening. authentications should be readonly.
|
||||
|
||||
return account
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import Combine
|
||||
import AuthenticationServices
|
||||
import MastodonCore
|
||||
|
||||
@MainActor
|
||||
final class MastodonAuthenticationController {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
@ -20,9 +21,8 @@ final class MastodonAuthenticationController {
|
||||
var authenticationSession: ASWebAuthenticationSession?
|
||||
|
||||
// output
|
||||
let isAuthenticating = CurrentValueSubject<Bool, Never>(false)
|
||||
let error = CurrentValueSubject<Error?, Never>(nil)
|
||||
let pinCodePublisher = PassthroughSubject<String, Never>()
|
||||
public let resultStream: AsyncThrowingStream<String, Error>
|
||||
private let resultStreamContinuation: AsyncThrowingStream<String, Error>.Continuation
|
||||
|
||||
init(
|
||||
context: AppContext,
|
||||
@ -31,9 +31,10 @@ final class MastodonAuthenticationController {
|
||||
self.context = context
|
||||
self.authenticateURL = authenticateURL
|
||||
|
||||
(resultStream, resultStreamContinuation) = AsyncThrowingStream<String, Error>.makeStream()
|
||||
|
||||
authentication()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonAuthenticationController {
|
||||
@ -45,15 +46,7 @@ extension MastodonAuthenticationController {
|
||||
guard let self = self else { return }
|
||||
|
||||
if let error = error {
|
||||
if let error = error as? ASWebAuthenticationSessionError {
|
||||
if error.errorCode == ASWebAuthenticationSessionError.canceledLogin.rawValue {
|
||||
self.isAuthenticating.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
self.isAuthenticating.value = false
|
||||
self.error.value = error
|
||||
self.resultStreamContinuation.finish(throwing: error)
|
||||
return
|
||||
}
|
||||
|
||||
@ -61,10 +54,12 @@ extension MastodonAuthenticationController {
|
||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
||||
let codeQueryItem = components.queryItems?.first(where: { $0.name == "code" }),
|
||||
let code = codeQueryItem.value else {
|
||||
self.resultStreamContinuation.finish()
|
||||
return
|
||||
}
|
||||
|
||||
self.pinCodePublisher.send(code)
|
||||
self.resultStreamContinuation.yield(code)
|
||||
self.resultStreamContinuation.finish()
|
||||
}
|
||||
authenticationSession?.prefersEphemeralWebBrowserSession = true
|
||||
}
|
||||
|
@ -55,7 +55,11 @@ public class PersistenceManager {
|
||||
}
|
||||
|
||||
public func cacheAccount(_ account: Mastodon.Entity.Account, for authenticationBox: MastodonAuthenticationBox) {
|
||||
FileManager.default.store(account: account, forUserID: authenticationBox.authentication.userIdentifier())
|
||||
cacheAccount(account, forUserID: authenticationBox.authentication.userIdentifier())
|
||||
}
|
||||
|
||||
public func cacheAccount(_ account: Mastodon.Entity.Account, forUserID userID: MastodonUserIdentifier) {
|
||||
FileManager.default.store(account: account, forUserID: userID)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,11 +45,13 @@ extension APIService {
|
||||
domain: String,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) async throws -> Mastodon.Entity.Account {
|
||||
return try await Mastodon.API.Account.verifyCredentials(
|
||||
let account = try await Mastodon.API.Account.verifyCredentials(
|
||||
session: session,
|
||||
domain: domain,
|
||||
authorization: authorization
|
||||
)
|
||||
PersistenceManager.shared.cacheAccount(account, forUserID: MastodonUserIdentifier(domain: domain, userID: account.id))
|
||||
return account
|
||||
}
|
||||
|
||||
public func accountUpdateCredentials(
|
||||
|
@ -54,4 +54,45 @@ extension APIService {
|
||||
)
|
||||
}
|
||||
|
||||
public func userAccessToken(
|
||||
domain: String,
|
||||
clientID: String,
|
||||
clientSecret: String,
|
||||
redirectURI: String,
|
||||
code: String
|
||||
) async throws -> Mastodon.Entity.Token {
|
||||
let query = Mastodon.API.OAuth.AccessTokenQuery(
|
||||
clientID: clientID,
|
||||
clientSecret: clientSecret,
|
||||
redirectURI: redirectURI,
|
||||
code: code,
|
||||
grantType: "authorization_code"
|
||||
)
|
||||
return try await Mastodon.API.OAuth.accessToken(
|
||||
session: session,
|
||||
domain: domain,
|
||||
query: query
|
||||
)
|
||||
}
|
||||
|
||||
public func applicationAccessToken(
|
||||
domain: String,
|
||||
clientID: String,
|
||||
clientSecret: String,
|
||||
redirectURI: String
|
||||
) async throws -> Mastodon.Entity.Token {
|
||||
let query = Mastodon.API.OAuth.AccessTokenQuery(
|
||||
clientID: clientID,
|
||||
clientSecret: clientSecret,
|
||||
redirectURI: redirectURI,
|
||||
code: nil,
|
||||
grantType: "client_credentials"
|
||||
)
|
||||
return try await Mastodon.API.OAuth.accessToken(
|
||||
session: session,
|
||||
domain: domain,
|
||||
query: query
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -20,6 +20,13 @@ extension APIService {
|
||||
return Mastodon.API.Instance.instance(session: session, authorization: authenticationBox?.userAuthorization, domain: domain)
|
||||
}
|
||||
|
||||
public func instance(
|
||||
domain: String,
|
||||
authenticationBox: MastodonAuthenticationBox?
|
||||
) async throws -> Mastodon.Entity.Instance {
|
||||
return try await Mastodon.API.Instance.instance(session: session, authorization: authenticationBox?.userAuthorization, domain: domain)
|
||||
}
|
||||
|
||||
public func instanceV2(
|
||||
domain: String,
|
||||
authenticationBox: MastodonAuthenticationBox?
|
||||
|
@ -20,7 +20,7 @@ public final class APIService {
|
||||
public static let shared = { APIService(backgroundContext: PersistenceManager.shared.backgroundManagedObjectContext) }()
|
||||
|
||||
public static let callbackURLScheme = "mastodon"
|
||||
public static let oauthCallbackURL = "mastodon://joinmastodon.org/oauth"
|
||||
nonisolated public static let oauthCallbackURL = "mastodon://joinmastodon.org/oauth"
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
|
@ -52,6 +52,29 @@ extension Mastodon.API.Instance {
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public static func instance(
|
||||
session: URLSession,
|
||||
authorization: Mastodon.API.OAuth.Authorization?,
|
||||
domain: String
|
||||
) async throws -> Mastodon.Entity.Instance {
|
||||
let request = Mastodon.API.get(url: instanceEndpointURL(domain: domain), authorization: authorization)
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
let value: Mastodon.Entity.Instance
|
||||
|
||||
do {
|
||||
value = try Mastodon.API.decode(type: Mastodon.Entity.Instance.self, from: data, response: response)
|
||||
} catch {
|
||||
if let response = response as? HTTPURLResponse, 400 ..< 500 ~= response.statusCode {
|
||||
// For example, AUTHORIZED_FETCH may result in authentication errors
|
||||
value = Mastodon.Entity.Instance(domain: domain)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
static func extendedDescriptionEndpointURL(domain: String) -> URL {
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("instance").appendingPathComponent("extended_description")
|
||||
|
@ -95,6 +95,22 @@ extension Mastodon.API.OAuth {
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public static func accessToken(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
query: AccessTokenQuery
|
||||
) async throws -> Mastodon.Entity.Token {
|
||||
let request = Mastodon.API.post(
|
||||
url: accessTokenEndpointURL(domain: domain),
|
||||
query: query,
|
||||
authorization: nil
|
||||
)
|
||||
let (data, response) = try await session.data(for: request)
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Token.self, from: data, response: response)
|
||||
return value
|
||||
}
|
||||
|
||||
|
||||
/// Revoke User Access Token
|
||||
///
|
||||
|
Loading…
x
Reference in New Issue
Block a user