mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 09:41:00 +00:00
Refactoring
This commit is contained in:
parent
6dfda031a6
commit
54f8c82fcd
13 changed files with 310 additions and 227 deletions
|
@ -91,16 +91,24 @@ extension AppEnvironment {
|
||||||
webAuthSessionType: SuccessfulMockWebAuthSession.self)
|
webAuthSessionType: SuccessfulMockWebAuthSession.self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension IdentitiesService {
|
||||||
|
static let development = IdentitiesService(environment: .development)
|
||||||
|
}
|
||||||
|
|
||||||
extension IdentityService {
|
extension IdentityService {
|
||||||
static let development = try! IdentityService(identityID: devIdentityID, appEnvironment: .development)
|
static let development = try! IdentitiesService.development.identityService(id: devIdentityID)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RootViewModel {
|
extension RootViewModel {
|
||||||
static let development = RootViewModel(environment: .development)
|
static let development = RootViewModel(identitiesService: .development)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AddIdentityViewModel {
|
||||||
|
static let development = RootViewModel.development.addIdentityViewModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainNavigationViewModel {
|
extension MainNavigationViewModel {
|
||||||
static let development = RootViewModel.development.mainNavigationViewModel(identityID: devIdentityID)!
|
static let development = RootViewModel.development.mainNavigationViewModel!
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
|
|
|
@ -140,6 +140,10 @@
|
||||||
D0EC8DC624DF842700A08489 /* KeychainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DC424DF842700A08489 /* KeychainService.swift */; };
|
D0EC8DC624DF842700A08489 /* KeychainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DC424DF842700A08489 /* KeychainService.swift */; };
|
||||||
D0EC8DC824DF8B3C00A08489 /* SecretsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DC724DF8B3C00A08489 /* SecretsService.swift */; };
|
D0EC8DC824DF8B3C00A08489 /* SecretsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DC724DF8B3C00A08489 /* SecretsService.swift */; };
|
||||||
D0EC8DC924DF8B3C00A08489 /* SecretsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DC724DF8B3C00A08489 /* SecretsService.swift */; };
|
D0EC8DC924DF8B3C00A08489 /* SecretsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DC724DF8B3C00A08489 /* SecretsService.swift */; };
|
||||||
|
D0EC8DCB24DFA06700A08489 /* IdentitiesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DCA24DFA06700A08489 /* IdentitiesService.swift */; };
|
||||||
|
D0EC8DCC24DFA06700A08489 /* IdentitiesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DCA24DFA06700A08489 /* IdentitiesService.swift */; };
|
||||||
|
D0EC8DCE24DFB64200A08489 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DCD24DFB64200A08489 /* AuthenticationService.swift */; };
|
||||||
|
D0EC8DCF24DFB64200A08489 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DCD24DFB64200A08489 /* AuthenticationService.swift */; };
|
||||||
D0ED1B6E24CE100C00B4899C /* AddIdentityViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */; };
|
D0ED1B6E24CE100C00B4899C /* AddIdentityViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */; };
|
||||||
D0ED1BB724CE47F400B4899C /* WebAuthSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BB624CE47F400B4899C /* WebAuthSession.swift */; };
|
D0ED1BB724CE47F400B4899C /* WebAuthSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BB624CE47F400B4899C /* WebAuthSession.swift */; };
|
||||||
D0ED1BB824CE47F400B4899C /* WebAuthSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BB624CE47F400B4899C /* WebAuthSession.swift */; };
|
D0ED1BB824CE47F400B4899C /* WebAuthSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BB624CE47F400B4899C /* WebAuthSession.swift */; };
|
||||||
|
@ -245,6 +249,8 @@
|
||||||
D0EC8DC124DF7D9C00A08489 /* IdentityService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentityService.swift; sourceTree = "<group>"; };
|
D0EC8DC124DF7D9C00A08489 /* IdentityService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentityService.swift; sourceTree = "<group>"; };
|
||||||
D0EC8DC424DF842700A08489 /* KeychainService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainService.swift; sourceTree = "<group>"; };
|
D0EC8DC424DF842700A08489 /* KeychainService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainService.swift; sourceTree = "<group>"; };
|
||||||
D0EC8DC724DF8B3C00A08489 /* SecretsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecretsService.swift; sourceTree = "<group>"; };
|
D0EC8DC724DF8B3C00A08489 /* SecretsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecretsService.swift; sourceTree = "<group>"; };
|
||||||
|
D0EC8DCA24DFA06700A08489 /* IdentitiesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentitiesService.swift; sourceTree = "<group>"; };
|
||||||
|
D0EC8DCD24DFB64200A08489 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = "<group>"; };
|
||||||
D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddIdentityViewModelTests.swift; sourceTree = "<group>"; };
|
D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddIdentityViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
D0ED1BB624CE47F400B4899C /* WebAuthSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAuthSession.swift; sourceTree = "<group>"; };
|
D0ED1BB624CE47F400B4899C /* WebAuthSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAuthSession.swift; sourceTree = "<group>"; };
|
||||||
D0ED1BC024CED48800B4899C /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = "<group>"; };
|
D0ED1BC024CED48800B4899C /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = "<group>"; };
|
||||||
|
@ -349,6 +355,8 @@
|
||||||
D019E6F224DF7C9E00697C7D /* Services */ = {
|
D019E6F224DF7C9E00697C7D /* Services */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D0EC8DCD24DFB64200A08489 /* AuthenticationService.swift */,
|
||||||
|
D0EC8DCA24DFA06700A08489 /* IdentitiesService.swift */,
|
||||||
D0EC8DC124DF7D9C00A08489 /* IdentityService.swift */,
|
D0EC8DC124DF7D9C00A08489 /* IdentityService.swift */,
|
||||||
D0EC8DC424DF842700A08489 /* KeychainService.swift */,
|
D0EC8DC424DF842700A08489 /* KeychainService.swift */,
|
||||||
D0EC8DC724DF8B3C00A08489 /* SecretsService.swift */,
|
D0EC8DC724DF8B3C00A08489 /* SecretsService.swift */,
|
||||||
|
@ -796,6 +804,7 @@
|
||||||
D0DC174624CFEC2000A75C65 /* StubbingURLProtocol.swift in Sources */,
|
D0DC174624CFEC2000A75C65 /* StubbingURLProtocol.swift in Sources */,
|
||||||
D019E6F024DF7C2F00697C7D /* DatabaseError.swift in Sources */,
|
D019E6F024DF7C2F00697C7D /* DatabaseError.swift in Sources */,
|
||||||
D019E6D724DF728400697C7D /* MastodonEncoder.swift in Sources */,
|
D019E6D724DF728400697C7D /* MastodonEncoder.swift in Sources */,
|
||||||
|
D0EC8DCB24DFA06700A08489 /* IdentitiesService.swift in Sources */,
|
||||||
D0091B7124DD68220040E8D2 /* PreferencesViewModel.swift in Sources */,
|
D0091B7124DD68220040E8D2 /* PreferencesViewModel.swift in Sources */,
|
||||||
D0DC174D24CFF1F100A75C65 /* Stubbing.swift in Sources */,
|
D0DC174D24CFF1F100A75C65 /* Stubbing.swift in Sources */,
|
||||||
D0091B6B24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift in Sources */,
|
D0091B6B24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift in Sources */,
|
||||||
|
@ -814,6 +823,7 @@
|
||||||
D0EC8DC824DF8B3C00A08489 /* SecretsService.swift in Sources */,
|
D0EC8DC824DF8B3C00A08489 /* SecretsService.swift in Sources */,
|
||||||
D0159FA324DE955900E78478 /* CustomEmojiText.swift in Sources */,
|
D0159FA324DE955900E78478 /* CustomEmojiText.swift in Sources */,
|
||||||
D0C963FE24CC3812003BD330 /* Publisher+Extensions.swift in Sources */,
|
D0C963FE24CC3812003BD330 /* Publisher+Extensions.swift in Sources */,
|
||||||
|
D0EC8DCE24DFB64200A08489 /* AuthenticationService.swift in Sources */,
|
||||||
D04FD73C24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */,
|
D04FD73C24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */,
|
||||||
D0DC175B24D0154F00A75C65 /* MastodonAPI.swift in Sources */,
|
D0DC175B24D0154F00A75C65 /* MastodonAPI.swift in Sources */,
|
||||||
D0ED1BD124CF779B00B4899C /* MastodonTarget.swift in Sources */,
|
D0ED1BD124CF779B00B4899C /* MastodonTarget.swift in Sources */,
|
||||||
|
@ -849,6 +859,7 @@
|
||||||
D0BEC93924C9632800E864C4 /* RootViewModel.swift in Sources */,
|
D0BEC93924C9632800E864C4 /* RootViewModel.swift in Sources */,
|
||||||
D0ED1BC224CED48800B4899C /* HTTPClient.swift in Sources */,
|
D0ED1BC224CED48800B4899C /* HTTPClient.swift in Sources */,
|
||||||
D0666A4C24C6C37700F3F04B /* Identity.swift in Sources */,
|
D0666A4C24C6C37700F3F04B /* Identity.swift in Sources */,
|
||||||
|
D0EC8DCC24DFA06700A08489 /* IdentitiesService.swift in Sources */,
|
||||||
D0666A5524C6C3E500F3F04B /* Emoji.swift in Sources */,
|
D0666A5524C6C3E500F3F04B /* Emoji.swift in Sources */,
|
||||||
D019E6EE24DF7BF300697C7D /* IdentityDatabase.swift in Sources */,
|
D019E6EE24DF7BF300697C7D /* IdentityDatabase.swift in Sources */,
|
||||||
D0A652AE24DE3EB6002EA33F /* PreferencesEndpoint+Stubbing.swift in Sources */,
|
D0A652AE24DE3EB6002EA33F /* PreferencesEndpoint+Stubbing.swift in Sources */,
|
||||||
|
@ -875,6 +886,7 @@
|
||||||
D0EC8DC924DF8B3C00A08489 /* SecretsService.swift in Sources */,
|
D0EC8DC924DF8B3C00A08489 /* SecretsService.swift in Sources */,
|
||||||
D0DB6EF524C5233E00D965FE /* AddIdentityView.swift in Sources */,
|
D0DB6EF524C5233E00D965FE /* AddIdentityView.swift in Sources */,
|
||||||
D019E6EA24DF72E700697C7D /* InstanceEndpoint.swift in Sources */,
|
D019E6EA24DF72E700697C7D /* InstanceEndpoint.swift in Sources */,
|
||||||
|
D0EC8DCF24DFB64200A08489 /* AuthenticationService.swift in Sources */,
|
||||||
D0159F9C24DE748C00E78478 /* SidebarNavigationView.swift in Sources */,
|
D0159F9C24DE748C00E78478 /* SidebarNavigationView.swift in Sources */,
|
||||||
D019E6F124DF7C2F00697C7D /* DatabaseError.swift in Sources */,
|
D019E6F124DF7C2F00697C7D /* DatabaseError.swift in Sources */,
|
||||||
D074577824D29006004758DB /* MockWebAuthSession.swift in Sources */,
|
D074577824D29006004758DB /* MockWebAuthSession.swift in Sources */,
|
||||||
|
|
|
@ -138,8 +138,11 @@ extension IdentityDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
var mostRecentlyUsedIdentityID: UUID? {
|
func mostRecentlyUsedIdentityIDObservation() -> AnyPublisher<UUID?, Error> {
|
||||||
try? databaseQueue.read(StoredIdentity.select(Column("id")).order(Column("lastUsedAt").desc).fetchOne)
|
ValueObservation.tracking(StoredIdentity.select(Column("id")).order(Column("lastUsedAt").desc).fetchOne)
|
||||||
|
.removeDuplicates()
|
||||||
|
.publisher(in: databaseQueue, scheduling: .immediate)
|
||||||
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ struct MetatextApp: App {
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
RootView(viewModel: RootViewModel(environment: environment))
|
RootView(viewModel: RootViewModel(identitiesService: IdentitiesService(environment: environment)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
171
Shared/Services/AuthenticationService.swift
Normal file
171
Shared/Services/AuthenticationService.swift
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
struct AuthenticationService {
|
||||||
|
private let environment: AppEnvironment
|
||||||
|
private let networkClient: MastodonClient
|
||||||
|
private let webAuthSessionContextProvider = WebAuthSessionContextProvider()
|
||||||
|
|
||||||
|
init(environment: AppEnvironment) {
|
||||||
|
self.environment = environment
|
||||||
|
self.networkClient = MastodonClient(configuration: environment.URLSessionConfiguration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AuthenticationService {
|
||||||
|
func authenticate(instanceURL: URL) -> AnyPublisher<UUID, Error> {
|
||||||
|
let identityID = UUID()
|
||||||
|
let redirectURL: URL
|
||||||
|
|
||||||
|
do {
|
||||||
|
redirectURL = try identityID.uuidString.url(scheme: MastodonAPI.OAuth.callbackURLScheme)
|
||||||
|
} catch {
|
||||||
|
return Fail(error: error).eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorizeApp(
|
||||||
|
identityID: identityID,
|
||||||
|
instanceURL: instanceURL,
|
||||||
|
redirectURL: redirectURL,
|
||||||
|
keychainService: environment.keychainService)
|
||||||
|
.authenticationURL(instanceURL: instanceURL, redirectURL: redirectURL)
|
||||||
|
.authenticate(
|
||||||
|
webAuthSessionType: environment.webAuthSessionType,
|
||||||
|
contextProvider: webAuthSessionContextProvider,
|
||||||
|
callbackURLScheme: MastodonAPI.OAuth.callbackURLScheme)
|
||||||
|
.extractCode()
|
||||||
|
.requestAccessToken(
|
||||||
|
networkClient: networkClient,
|
||||||
|
identityID: identityID,
|
||||||
|
instanceURL: instanceURL,
|
||||||
|
redirectURL: redirectURL)
|
||||||
|
.createIdentity(id: identityID, instanceURL: instanceURL, environment: environment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AuthenticationService {
|
||||||
|
func authorizeApp(
|
||||||
|
identityID: UUID,
|
||||||
|
instanceURL: URL,
|
||||||
|
redirectURL: URL,
|
||||||
|
keychainService: KeychainServiceType) -> AnyPublisher<AppAuthorization, Error> {
|
||||||
|
let endpoint = AppAuthorizationEndpoint.apps(
|
||||||
|
clientName: MastodonAPI.OAuth.clientName,
|
||||||
|
redirectURI: redirectURL.absoluteString,
|
||||||
|
scopes: MastodonAPI.OAuth.scopes,
|
||||||
|
website: nil)
|
||||||
|
let target = MastodonTarget(baseURL: instanceURL, endpoint: endpoint, accessToken: nil)
|
||||||
|
|
||||||
|
return networkClient.request(target)
|
||||||
|
.tryMap {
|
||||||
|
let secretsService = SecretsService(identityID: identityID, keychainService: keychainService)
|
||||||
|
try secretsService.set($0.clientId, forItem: .clientID)
|
||||||
|
try secretsService.set($0.clientSecret, forItem: .clientSecret)
|
||||||
|
|
||||||
|
return $0
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Publisher where Output == AppAuthorization {
|
||||||
|
func authenticationURL(instanceURL: URL, redirectURL: URL) -> AnyPublisher<(AppAuthorization, URL), Error> {
|
||||||
|
tryMap { appAuthorization in
|
||||||
|
guard var authorizationURLComponents = URLComponents(url: instanceURL, resolvingAgainstBaseURL: true) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
authorizationURLComponents.path = "/oauth/authorize"
|
||||||
|
authorizationURLComponents.queryItems = [
|
||||||
|
"client_id": appAuthorization.clientId,
|
||||||
|
"scope": MastodonAPI.OAuth.scopes,
|
||||||
|
"response_type": "code",
|
||||||
|
"redirect_uri": redirectURL.absoluteString
|
||||||
|
].map { URLQueryItem(name: $0, value: $1) }
|
||||||
|
|
||||||
|
guard let authorizationURL = authorizationURLComponents.url else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (appAuthorization, authorizationURL)
|
||||||
|
}
|
||||||
|
.mapError { $0 as Error }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Publisher where Output == (AppAuthorization, URL), Failure == Error {
|
||||||
|
func authenticate(
|
||||||
|
webAuthSessionType: WebAuthSessionType.Type,
|
||||||
|
contextProvider: WebAuthSessionContextProvider,
|
||||||
|
callbackURLScheme: String) -> AnyPublisher<(AppAuthorization, URL), Error> {
|
||||||
|
flatMap { appAuthorization, url in
|
||||||
|
webAuthSessionType.publisher(
|
||||||
|
url: url,
|
||||||
|
callbackURLScheme: callbackURLScheme,
|
||||||
|
presentationContextProvider: contextProvider)
|
||||||
|
.tryCatch { error -> AnyPublisher<URL?, Error> in
|
||||||
|
if (error as? WebAuthSessionError)?.code == .canceledLogin {
|
||||||
|
return Just(nil).setFailureType(to: Error.self).eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
.compactMap { $0 }
|
||||||
|
.map { (appAuthorization, $0) }
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Publisher where Output == (AppAuthorization, URL) {
|
||||||
|
func extractCode() -> AnyPublisher<(AppAuthorization, String), Error> {
|
||||||
|
tryMap { appAuthorization, url -> (AppAuthorization, String) in
|
||||||
|
guard let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems,
|
||||||
|
let code = queryItems.first(where: { $0.name == MastodonAPI.OAuth.codeCallbackQueryItemName })?.value
|
||||||
|
else { throw MastodonAPI.OAuthError.codeNotFound }
|
||||||
|
|
||||||
|
return (appAuthorization, code)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Publisher where Output == (AppAuthorization, String), Failure == Error {
|
||||||
|
func requestAccessToken(
|
||||||
|
networkClient: HTTPClient,
|
||||||
|
identityID: UUID,
|
||||||
|
instanceURL: URL,
|
||||||
|
redirectURL: URL) -> AnyPublisher<AccessToken, Error> {
|
||||||
|
flatMap { appAuthorization, code -> AnyPublisher<AccessToken, Error> in
|
||||||
|
let endpoint = AccessTokenEndpoint.oauthToken(
|
||||||
|
clientID: appAuthorization.clientId,
|
||||||
|
clientSecret: appAuthorization.clientSecret,
|
||||||
|
code: code,
|
||||||
|
grantType: MastodonAPI.OAuth.grantType,
|
||||||
|
scopes: MastodonAPI.OAuth.scopes,
|
||||||
|
redirectURI: redirectURL.absoluteString)
|
||||||
|
let target = MastodonTarget(baseURL: instanceURL, endpoint: endpoint, accessToken: nil)
|
||||||
|
|
||||||
|
return networkClient.request(target)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Publisher where Output == AccessToken {
|
||||||
|
func createIdentity(id: UUID, instanceURL: URL, environment: AppEnvironment) -> AnyPublisher<UUID, Error> {
|
||||||
|
tryMap { accessToken -> (UUID, URL) in
|
||||||
|
let secretsService = SecretsService(identityID: id, keychainService: environment.keychainService)
|
||||||
|
|
||||||
|
try secretsService.set(accessToken.accessToken, forItem: .accessToken)
|
||||||
|
|
||||||
|
return (id, instanceURL)
|
||||||
|
}
|
||||||
|
.flatMap(environment.identityDatabase.createIdentity)
|
||||||
|
.map { id }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
43
Shared/Services/IdentitiesService.swift
Normal file
43
Shared/Services/IdentitiesService.swift
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
class IdentitiesService {
|
||||||
|
@Published var mostRecentlyUsedIdentityID: UUID?
|
||||||
|
|
||||||
|
private let environment: AppEnvironment
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
init(environment: AppEnvironment) {
|
||||||
|
self.environment = environment
|
||||||
|
|
||||||
|
environment.identityDatabase.mostRecentlyUsedIdentityIDObservation()
|
||||||
|
.replaceError(with: nil)
|
||||||
|
.assign(to: &$mostRecentlyUsedIdentityID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension IdentitiesService {
|
||||||
|
func identityService(id: UUID) throws -> IdentityService {
|
||||||
|
try IdentityService(identityID: id, environment: environment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func authenticationService() -> AuthenticationService {
|
||||||
|
AuthenticationService(environment: environment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteIdentity(id: UUID) -> AnyPublisher<Void, Error> {
|
||||||
|
environment.identityDatabase.deleteIdentity(id: id)
|
||||||
|
.continuingIfWeakReferenceIsStillAlive(to: self)
|
||||||
|
.tryMap { _, welf -> Void in
|
||||||
|
try SecretsService(
|
||||||
|
identityID: id,
|
||||||
|
keychainService: welf.environment.keychainService)
|
||||||
|
.deleteAllItems()
|
||||||
|
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,20 +8,20 @@ class IdentityService {
|
||||||
let observationErrors: AnyPublisher<Error, Never>
|
let observationErrors: AnyPublisher<Error, Never>
|
||||||
|
|
||||||
private let networkClient: MastodonClient
|
private let networkClient: MastodonClient
|
||||||
private let appEnvironment: AppEnvironment
|
private let environment: AppEnvironment
|
||||||
private let observationErrorsInput = PassthroughSubject<Error, Never>()
|
private let observationErrorsInput = PassthroughSubject<Error, Never>()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(identityID: UUID, appEnvironment: AppEnvironment) throws {
|
init(identityID: UUID, environment: AppEnvironment) throws {
|
||||||
self.appEnvironment = appEnvironment
|
self.environment = environment
|
||||||
observationErrors = observationErrorsInput.eraseToAnyPublisher()
|
observationErrors = observationErrorsInput.eraseToAnyPublisher()
|
||||||
networkClient = MastodonClient(configuration: appEnvironment.URLSessionConfiguration)
|
networkClient = MastodonClient(configuration: environment.URLSessionConfiguration)
|
||||||
networkClient.accessToken = try SecretsService(
|
networkClient.accessToken = try SecretsService(
|
||||||
identityID: identityID,
|
identityID: identityID,
|
||||||
keychainService: appEnvironment.keychainService)
|
keychainService: environment.keychainService)
|
||||||
.item(.accessToken)
|
.item(.accessToken)
|
||||||
|
|
||||||
let observation = appEnvironment.identityDatabase.identityObservation(id: identityID).share()
|
let observation = environment.identityDatabase.identityObservation(id: identityID).share()
|
||||||
|
|
||||||
var initialIdentity: Identity?
|
var initialIdentity: Identity?
|
||||||
|
|
||||||
|
@ -47,11 +47,15 @@ class IdentityService {
|
||||||
extension IdentityService {
|
extension IdentityService {
|
||||||
var isAuthorized: Bool { networkClient.accessToken != nil }
|
var isAuthorized: Bool { networkClient.accessToken != nil }
|
||||||
|
|
||||||
|
func updateLastUse() -> AnyPublisher<Void, Error> {
|
||||||
|
environment.identityDatabase.updateLastUsedAt(identityID: identity.id)
|
||||||
|
}
|
||||||
|
|
||||||
func verifyCredentials() -> AnyPublisher<Void, Error> {
|
func verifyCredentials() -> AnyPublisher<Void, Error> {
|
||||||
networkClient.request(AccountEndpoint.verifyCredentials)
|
networkClient.request(AccountEndpoint.verifyCredentials)
|
||||||
.continuingIfWeakReferenceIsStillAlive(to: self)
|
.continuingIfWeakReferenceIsStillAlive(to: self)
|
||||||
.map { ($0, $1.identity.id) }
|
.map { ($0, $1.identity.id) }
|
||||||
.flatMap(appEnvironment.identityDatabase.updateAccount)
|
.flatMap(environment.identityDatabase.updateAccount)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +63,7 @@ extension IdentityService {
|
||||||
networkClient.request(PreferencesEndpoint.preferences)
|
networkClient.request(PreferencesEndpoint.preferences)
|
||||||
.continuingIfWeakReferenceIsStillAlive(to: self)
|
.continuingIfWeakReferenceIsStillAlive(to: self)
|
||||||
.map { ($1.identity.preferences.updated(from: $0), $1.identity.id) }
|
.map { ($1.identity.preferences.updated(from: $0), $1.identity.id) }
|
||||||
.flatMap(appEnvironment.identityDatabase.updatePreferences)
|
.flatMap(environment.identityDatabase.updatePreferences)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,19 +71,19 @@ extension IdentityService {
|
||||||
networkClient.request(InstanceEndpoint.instance)
|
networkClient.request(InstanceEndpoint.instance)
|
||||||
.continuingIfWeakReferenceIsStillAlive(to: self)
|
.continuingIfWeakReferenceIsStillAlive(to: self)
|
||||||
.map { ($0, $1.identity.id) }
|
.map { ($0, $1.identity.id) }
|
||||||
.flatMap(appEnvironment.identityDatabase.updateInstance)
|
.flatMap(environment.identityDatabase.updateInstance)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func identitiesObservation() -> AnyPublisher<[Identity], Error> {
|
func identitiesObservation() -> AnyPublisher<[Identity], Error> {
|
||||||
appEnvironment.identityDatabase.identitiesObservation()
|
environment.identityDatabase.identitiesObservation()
|
||||||
}
|
}
|
||||||
|
|
||||||
func recentIdentitiesObservation() -> AnyPublisher<[Identity], Error> {
|
func recentIdentitiesObservation() -> AnyPublisher<[Identity], Error> {
|
||||||
appEnvironment.identityDatabase.recentIdentitiesObservation(excluding: identity.id)
|
environment.identityDatabase.recentIdentitiesObservation(excluding: identity.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatePreferences(_ preferences: Identity.Preferences) -> AnyPublisher<Void, Error> {
|
func updatePreferences(_ preferences: Identity.Preferences) -> AnyPublisher<Void, Error> {
|
||||||
appEnvironment.identityDatabase.updatePreferences(preferences, forIdentityID: identity.id)
|
environment.identityDatabase.updatePreferences(preferences, forIdentityID: identity.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,49 +9,19 @@ class AddIdentityViewModel: ObservableObject {
|
||||||
@Published private(set) var loading = false
|
@Published private(set) var loading = false
|
||||||
let addedIdentityID: AnyPublisher<UUID, Never>
|
let addedIdentityID: AnyPublisher<UUID, Never>
|
||||||
|
|
||||||
private let environment: AppEnvironment
|
private let authenticationService: AuthenticationService
|
||||||
private let networkClient: MastodonClient
|
|
||||||
private let webAuthSessionContextProvider = WebAuthSessionContextProvider()
|
|
||||||
private let addedIdentityIDInput = PassthroughSubject<UUID, Never>()
|
private let addedIdentityIDInput = PassthroughSubject<UUID, Never>()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(environment: AppEnvironment) {
|
init(authenticationService: AuthenticationService) {
|
||||||
self.environment = environment
|
self.authenticationService = authenticationService
|
||||||
self.networkClient = MastodonClient(configuration: environment.URLSessionConfiguration)
|
|
||||||
addedIdentityID = addedIdentityIDInput.eraseToAnyPublisher()
|
addedIdentityID = addedIdentityIDInput.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func goTapped() {
|
func goTapped() {
|
||||||
let identityID = UUID()
|
Just(urlFieldText)
|
||||||
let instanceURL: URL
|
.tryMap { try $0.url() }
|
||||||
let redirectURL: URL
|
.flatMap(authenticationService.authenticate(instanceURL:))
|
||||||
|
|
||||||
do {
|
|
||||||
instanceURL = try urlFieldText.url()
|
|
||||||
redirectURL = try identityID.uuidString.url(scheme: MastodonAPI.OAuth.callbackURLScheme)
|
|
||||||
} catch {
|
|
||||||
alertItem = AlertItem(error: error)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
authorizeApp(
|
|
||||||
identityID: identityID,
|
|
||||||
instanceURL: instanceURL,
|
|
||||||
redirectURL: redirectURL,
|
|
||||||
keychainService: environment.keychainService)
|
|
||||||
.authenticationURL(instanceURL: instanceURL, redirectURL: redirectURL)
|
|
||||||
.authenticate(
|
|
||||||
webAuthSessionType: environment.webAuthSessionType,
|
|
||||||
contextProvider: webAuthSessionContextProvider,
|
|
||||||
callbackURLScheme: MastodonAPI.OAuth.callbackURLScheme)
|
|
||||||
.extractCode()
|
|
||||||
.requestAccessToken(
|
|
||||||
networkClient: networkClient,
|
|
||||||
identityID: identityID,
|
|
||||||
instanceURL: instanceURL,
|
|
||||||
redirectURL: redirectURL)
|
|
||||||
.createIdentity(id: identityID, instanceURL: instanceURL, environment: environment)
|
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.receive(on: RunLoop.main)
|
.receive(on: RunLoop.main)
|
||||||
.handleEvents(
|
.handleEvents(
|
||||||
|
@ -61,128 +31,3 @@ class AddIdentityViewModel: ObservableObject {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension AddIdentityViewModel {
|
|
||||||
private func authorizeApp(
|
|
||||||
identityID: UUID,
|
|
||||||
instanceURL: URL,
|
|
||||||
redirectURL: URL,
|
|
||||||
keychainService: KeychainServiceType) -> AnyPublisher<AppAuthorization, Error> {
|
|
||||||
let endpoint = AppAuthorizationEndpoint.apps(
|
|
||||||
clientName: MastodonAPI.OAuth.clientName,
|
|
||||||
redirectURI: redirectURL.absoluteString,
|
|
||||||
scopes: MastodonAPI.OAuth.scopes,
|
|
||||||
website: nil)
|
|
||||||
let target = MastodonTarget(baseURL: instanceURL, endpoint: endpoint, accessToken: nil)
|
|
||||||
|
|
||||||
return networkClient.request(target)
|
|
||||||
.tryMap {
|
|
||||||
let secretsService = SecretsService(identityID: identityID, keychainService: keychainService)
|
|
||||||
try secretsService.set($0.clientId, forItem: .clientID)
|
|
||||||
try secretsService.set($0.clientSecret, forItem: .clientSecret)
|
|
||||||
|
|
||||||
return $0
|
|
||||||
}
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Publisher where Output == AppAuthorization {
|
|
||||||
func authenticationURL(instanceURL: URL, redirectURL: URL) -> AnyPublisher<(AppAuthorization, URL), Error> {
|
|
||||||
tryMap { appAuthorization in
|
|
||||||
guard var authorizationURLComponents = URLComponents(url: instanceURL, resolvingAgainstBaseURL: true) else {
|
|
||||||
throw URLError(.badURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
authorizationURLComponents.path = "/oauth/authorize"
|
|
||||||
authorizationURLComponents.queryItems = [
|
|
||||||
"client_id": appAuthorization.clientId,
|
|
||||||
"scope": MastodonAPI.OAuth.scopes,
|
|
||||||
"response_type": "code",
|
|
||||||
"redirect_uri": redirectURL.absoluteString
|
|
||||||
].map { URLQueryItem(name: $0, value: $1) }
|
|
||||||
|
|
||||||
guard let authorizationURL = authorizationURLComponents.url else {
|
|
||||||
throw URLError(.badURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (appAuthorization, authorizationURL)
|
|
||||||
}
|
|
||||||
.mapError { $0 as Error }
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Publisher where Output == (AppAuthorization, URL), Failure == Error {
|
|
||||||
func authenticate(
|
|
||||||
webAuthSessionType: WebAuthSessionType.Type,
|
|
||||||
contextProvider: WebAuthSessionContextProvider,
|
|
||||||
callbackURLScheme: String) -> AnyPublisher<(AppAuthorization, URL), Error> {
|
|
||||||
flatMap { appAuthorization, url in
|
|
||||||
webAuthSessionType.publisher(
|
|
||||||
url: url,
|
|
||||||
callbackURLScheme: callbackURLScheme,
|
|
||||||
presentationContextProvider: contextProvider)
|
|
||||||
.tryCatch { error -> AnyPublisher<URL?, Error> in
|
|
||||||
if (error as? WebAuthSessionError)?.code == .canceledLogin {
|
|
||||||
return Just(nil).setFailureType(to: Error.self).eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
.compactMap { $0 }
|
|
||||||
.map { (appAuthorization, $0) }
|
|
||||||
}
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Publisher where Output == (AppAuthorization, URL) {
|
|
||||||
func extractCode() -> AnyPublisher<(AppAuthorization, String), Error> {
|
|
||||||
tryMap { appAuthorization, url -> (AppAuthorization, String) in
|
|
||||||
guard let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems,
|
|
||||||
let code = queryItems.first(where: { $0.name == MastodonAPI.OAuth.codeCallbackQueryItemName })?.value
|
|
||||||
else { throw MastodonAPI.OAuthError.codeNotFound }
|
|
||||||
|
|
||||||
return (appAuthorization, code)
|
|
||||||
}
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Publisher where Output == (AppAuthorization, String), Failure == Error {
|
|
||||||
func requestAccessToken(
|
|
||||||
networkClient: HTTPClient,
|
|
||||||
identityID: UUID,
|
|
||||||
instanceURL: URL,
|
|
||||||
redirectURL: URL) -> AnyPublisher<AccessToken, Error> {
|
|
||||||
flatMap { appAuthorization, code -> AnyPublisher<AccessToken, Error> in
|
|
||||||
let endpoint = AccessTokenEndpoint.oauthToken(
|
|
||||||
clientID: appAuthorization.clientId,
|
|
||||||
clientSecret: appAuthorization.clientSecret,
|
|
||||||
code: code,
|
|
||||||
grantType: MastodonAPI.OAuth.grantType,
|
|
||||||
scopes: MastodonAPI.OAuth.scopes,
|
|
||||||
redirectURI: redirectURL.absoluteString)
|
|
||||||
let target = MastodonTarget(baseURL: instanceURL, endpoint: endpoint, accessToken: nil)
|
|
||||||
|
|
||||||
return networkClient.request(target)
|
|
||||||
}
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Publisher where Output == AccessToken {
|
|
||||||
func createIdentity(id: UUID, instanceURL: URL, environment: AppEnvironment) -> AnyPublisher<UUID, Error> {
|
|
||||||
tryMap { accessToken -> (UUID, URL) in
|
|
||||||
let secretsService = SecretsService(identityID: id, keychainService: environment.keychainService)
|
|
||||||
|
|
||||||
try secretsService.set(accessToken.accessToken, forItem: .accessToken)
|
|
||||||
|
|
||||||
return (id, instanceURL)
|
|
||||||
}
|
|
||||||
.flatMap(environment.identityDatabase.createIdentity)
|
|
||||||
.map { id }
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,57 +4,54 @@ import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
class RootViewModel: ObservableObject {
|
class RootViewModel: ObservableObject {
|
||||||
@Published private(set) var identityID: UUID?
|
@Published private(set) var mainNavigationViewModel: MainNavigationViewModel?
|
||||||
private let environment: AppEnvironment
|
|
||||||
|
private let identitiesService: IdentitiesService
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(environment: AppEnvironment) {
|
init(identitiesService: IdentitiesService) {
|
||||||
self.environment = environment
|
self.identitiesService = identitiesService
|
||||||
identityID = environment.identityDatabase.mostRecentlyUsedIdentityID
|
|
||||||
|
newIdentitySelected(id: identitiesService.mostRecentlyUsedIdentityID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RootViewModel {
|
extension RootViewModel {
|
||||||
func newIdentitySelected(id: UUID) {
|
func newIdentitySelected(id: UUID?) {
|
||||||
identityID = id
|
guard let id = id else {
|
||||||
|
mainNavigationViewModel = nil
|
||||||
|
|
||||||
environment.identityDatabase
|
return
|
||||||
.updateLastUsedAt(identityID: id)
|
}
|
||||||
|
|
||||||
|
let identityService: IdentityService
|
||||||
|
|
||||||
|
do {
|
||||||
|
identityService = try identitiesService.identityService(id: id)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
identityService.observationErrors
|
||||||
|
.receive(on: RunLoop.main)
|
||||||
|
.map { [weak self] _ in self?.identitiesService.mostRecentlyUsedIdentityID }
|
||||||
|
.sink(receiveValue: newIdentitySelected(id:))
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
identityService.updateLastUse()
|
||||||
.sink(receiveCompletion: { _ in }, receiveValue: {})
|
.sink(receiveCompletion: { _ in }, receiveValue: {})
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
mainNavigationViewModel = MainNavigationViewModel(identityService: identityService)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteIdentity(id: UUID) {
|
func deleteIdentity(id: UUID) {
|
||||||
environment.identityDatabase.deleteIdentity(id: id)
|
identitiesService.deleteIdentity(id: id)
|
||||||
.continuingIfWeakReferenceIsStillAlive(to: self)
|
|
||||||
.tryMap {
|
|
||||||
try SecretsService(
|
|
||||||
identityID: id,
|
|
||||||
keychainService: $1.environment.keychainService)
|
|
||||||
.deleteAllItems()
|
|
||||||
}
|
|
||||||
.sink(receiveCompletion: { _ in }, receiveValue: {})
|
.sink(receiveCompletion: { _ in }, receiveValue: {})
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addIdentityViewModel() -> AddIdentityViewModel {
|
func addIdentityViewModel() -> AddIdentityViewModel {
|
||||||
AddIdentityViewModel(environment: environment)
|
AddIdentityViewModel(authenticationService: identitiesService.authenticationService())
|
||||||
}
|
|
||||||
|
|
||||||
func mainNavigationViewModel(identityID: UUID) -> MainNavigationViewModel? {
|
|
||||||
let identityService: IdentityService
|
|
||||||
|
|
||||||
do {
|
|
||||||
identityService = try IdentityService(identityID: identityID, appEnvironment: environment)
|
|
||||||
} catch {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
identityService.observationErrors
|
|
||||||
.receive(on: RunLoop.main)
|
|
||||||
.map { [weak self] _ in self?.environment.identityDatabase.mostRecentlyUsedIdentityID }
|
|
||||||
.assign(to: &$identityID)
|
|
||||||
|
|
||||||
return MainNavigationViewModel(identityService: identityService)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ private extension View {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
struct AddAccountView_Previews: PreviewProvider {
|
struct AddAccountView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
AddIdentityView(viewModel: AddIdentityViewModel(environment: .development))
|
AddIdentityView(viewModel: .development)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -12,10 +12,9 @@ struct RootView: View {
|
||||||
@StateObject var viewModel: RootViewModel
|
@StateObject var viewModel: RootViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if let id = viewModel.identityID,
|
if let mainNavigationViewModel = viewModel.mainNavigationViewModel {
|
||||||
let mainNavigationViewModel = viewModel.mainNavigationViewModel(identityID: id) {
|
|
||||||
Self.mainNavigation(mainNavigationViewModel: mainNavigationViewModel)
|
Self.mainNavigation(mainNavigationViewModel: mainNavigationViewModel)
|
||||||
.id(id)
|
.id(UUID())
|
||||||
.environmentObject(viewModel)
|
.environmentObject(viewModel)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import CombineExpectations
|
||||||
class AddIdentityViewModelTests: XCTestCase {
|
class AddIdentityViewModelTests: XCTestCase {
|
||||||
func testAddIdentity() throws {
|
func testAddIdentity() throws {
|
||||||
let environment = AppEnvironment.fresh()
|
let environment = AppEnvironment.fresh()
|
||||||
let sut = AddIdentityViewModel(environment: environment)
|
let sut = AddIdentityViewModel(authenticationService: AuthenticationService(environment: environment))
|
||||||
let addedIDRecorder = sut.addedIdentityID.record()
|
let addedIDRecorder = sut.addedIdentityID.record()
|
||||||
|
|
||||||
sut.urlFieldText = "https://mastodon.social"
|
sut.urlFieldText = "https://mastodon.social"
|
||||||
|
@ -33,7 +33,7 @@ class AddIdentityViewModelTests: XCTestCase {
|
||||||
|
|
||||||
func testAddIdentityWithoutScheme() throws {
|
func testAddIdentityWithoutScheme() throws {
|
||||||
let environment = AppEnvironment.fresh()
|
let environment = AppEnvironment.fresh()
|
||||||
let sut = AddIdentityViewModel(environment: environment)
|
let sut = AddIdentityViewModel(authenticationService: AuthenticationService(environment: environment))
|
||||||
let addedIDRecorder = sut.addedIdentityID.record()
|
let addedIDRecorder = sut.addedIdentityID.record()
|
||||||
|
|
||||||
sut.urlFieldText = "mastodon.social"
|
sut.urlFieldText = "mastodon.social"
|
||||||
|
@ -47,7 +47,7 @@ class AddIdentityViewModelTests: XCTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
func testInvalidURL() throws {
|
func testInvalidURL() throws {
|
||||||
let sut = AddIdentityViewModel(environment: .fresh())
|
let sut = AddIdentityViewModel(authenticationService: AuthenticationService(environment: .fresh()))
|
||||||
let recorder = sut.$alertItem.record()
|
let recorder = sut.$alertItem.record()
|
||||||
|
|
||||||
XCTAssertNil(try wait(for: recorder.next(), timeout: 1))
|
XCTAssertNil(try wait(for: recorder.next(), timeout: 1))
|
||||||
|
@ -62,7 +62,7 @@ class AddIdentityViewModelTests: XCTestCase {
|
||||||
|
|
||||||
func testDoesNotAlertCanceledLogin() throws {
|
func testDoesNotAlertCanceledLogin() throws {
|
||||||
let environment = AppEnvironment.fresh(webAuthSessionType: CanceledLoginMockWebAuthSession.self)
|
let environment = AppEnvironment.fresh(webAuthSessionType: CanceledLoginMockWebAuthSession.self)
|
||||||
let sut = AddIdentityViewModel(environment: environment)
|
let sut = AddIdentityViewModel(authenticationService: AuthenticationService(environment: environment))
|
||||||
let recorder = sut.$alertItem.record()
|
let recorder = sut.$alertItem.record()
|
||||||
|
|
||||||
XCTAssertNil(try wait(for: recorder.next(), timeout: 1))
|
XCTAssertNil(try wait(for: recorder.next(), timeout: 1))
|
||||||
|
|
|
@ -9,8 +9,10 @@ class RootViewModelTests: XCTestCase {
|
||||||
var cancellables = Set<AnyCancellable>()
|
var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
func testAddIdentity() throws {
|
func testAddIdentity() throws {
|
||||||
let sut = RootViewModel(environment: .fresh())
|
let environment = AppEnvironment.fresh()
|
||||||
let recorder = sut.$identityID.record()
|
let sut = RootViewModel(
|
||||||
|
identitiesService: IdentitiesService(environment: environment))
|
||||||
|
let recorder = sut.$mainNavigationViewModel.record()
|
||||||
|
|
||||||
XCTAssertNil(try wait(for: recorder.next(), timeout: 1))
|
XCTAssertNil(try wait(for: recorder.next(), timeout: 1))
|
||||||
|
|
||||||
|
@ -23,9 +25,8 @@ class RootViewModelTests: XCTestCase {
|
||||||
addIdentityViewModel.urlFieldText = "https://mastodon.social"
|
addIdentityViewModel.urlFieldText = "https://mastodon.social"
|
||||||
addIdentityViewModel.goTapped()
|
addIdentityViewModel.goTapped()
|
||||||
|
|
||||||
let identityID = try wait(for: recorder.next(), timeout: 1)!
|
let mainNavigationViewModel = try wait(for: recorder.next(), timeout: 1)!
|
||||||
|
|
||||||
XCTAssertNotNil(identityID)
|
XCTAssertNotNil(mainNavigationViewModel)
|
||||||
XCTAssertNotNil(sut.mainNavigationViewModel(identityID: identityID))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue