mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 17:50:59 +00:00
Refactoring
This commit is contained in:
parent
24dd407caa
commit
827c3cfc77
16 changed files with 311 additions and 195 deletions
|
@ -6,22 +6,47 @@ import Combine
|
||||||
// swiftlint:disable force_try
|
// swiftlint:disable force_try
|
||||||
private let decoder = MastodonDecoder()
|
private let decoder = MastodonDecoder()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
private let devInstanceURL = URL(string: "https://mastodon.social")!
|
||||||
private let devIdentityID = "DEVELOPMENT_IDENTITY_ID"
|
private let devIdentityID = "DEVELOPMENT_IDENTITY_ID"
|
||||||
|
private let devAccessToken = "DEVELOPMENT_ACCESS_TOKEN"
|
||||||
|
|
||||||
extension Secrets {
|
extension Secrets {
|
||||||
|
static func fresh() -> Secrets { Secrets(keychain: FakeKeychain()) }
|
||||||
|
|
||||||
static let development: Secrets = {
|
static let development: Secrets = {
|
||||||
let secrets = Secrets(keychain: FakeKeychain())
|
let secrets = Secrets.fresh()
|
||||||
|
|
||||||
try! secrets.set("DEVELOPMENT_CLIENT_ID", forItem: .clientID, forIdentityID: devIdentityID)
|
try! secrets.set("DEVELOPMENT_CLIENT_ID", forItem: .clientID, forIdentityID: devIdentityID)
|
||||||
try! secrets.set("DEVELOPMENT_CLIENT_SECRET", forItem: .clientSecret, forIdentityID: devIdentityID)
|
try! secrets.set("DEVELOPMENT_CLIENT_SECRET", forItem: .clientSecret, forIdentityID: devIdentityID)
|
||||||
try! secrets.set("DEVELOPMENT_ACCESS_TOKEN", forItem: .accessToken, forIdentityID: devIdentityID)
|
try! secrets.set(devAccessToken, forItem: .accessToken, forIdentityID: devIdentityID)
|
||||||
|
|
||||||
return secrets
|
return secrets
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Preferences {
|
||||||
|
static func fresh() -> Preferences { Preferences(userDefaults: FakeUserDefaults()) }
|
||||||
|
|
||||||
|
static let development: Preferences = {
|
||||||
|
let preferences = Preferences.fresh()
|
||||||
|
|
||||||
|
preferences[.recentIdentityID] = devIdentityID
|
||||||
|
|
||||||
|
return preferences
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
extension MastodonClient {
|
extension MastodonClient {
|
||||||
static let development = MastodonClient(configuration: .stubbing)
|
static func fresh() -> MastodonClient { MastodonClient(configuration: .stubbing) }
|
||||||
|
|
||||||
|
static let development: MastodonClient = {
|
||||||
|
let client = MastodonClient.fresh()
|
||||||
|
|
||||||
|
client.instanceURL = devInstanceURL
|
||||||
|
client.accessToken = devAccessToken
|
||||||
|
|
||||||
|
return client
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Account {
|
extension Account {
|
||||||
|
@ -33,10 +58,12 @@ extension Instance {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension IdentityDatabase {
|
extension IdentityDatabase {
|
||||||
static var development: IdentityDatabase = {
|
static func fresh() -> IdentityDatabase { try! IdentityDatabase(inMemory: true) }
|
||||||
let db = try! IdentityDatabase(inMemory: true)
|
|
||||||
|
|
||||||
db.createIdentity(id: devIdentityID, url: URL(string: "https://mastodon.social")!)
|
static var development: IdentityDatabase = {
|
||||||
|
let db = IdentityDatabase.fresh()
|
||||||
|
|
||||||
|
db.createIdentity(id: devIdentityID, url: devInstanceURL)
|
||||||
.receive(on: ImmediateScheduler.shared)
|
.receive(on: ImmediateScheduler.shared)
|
||||||
.sink(receiveCompletion: { _ in }, receiveValue: { _ in })
|
.sink(receiveCompletion: { _ in }, receiveValue: { _ in })
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
@ -68,11 +95,28 @@ extension Identity {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SceneViewModel {
|
extension AppEnvironment {
|
||||||
static let development = SceneViewModel(
|
static func fresh(
|
||||||
networkClient: .development,
|
identityDatabase: IdentityDatabase = .fresh(),
|
||||||
|
preferences: Preferences = .fresh(),
|
||||||
|
secrets: Secrets = .fresh(),
|
||||||
|
webAuthSessionType: WebAuthSession.Type = SuccessfulStubbingWebAuthSession.self) -> AppEnvironment {
|
||||||
|
AppEnvironment(
|
||||||
|
identityDatabase: identityDatabase,
|
||||||
|
preferences: preferences,
|
||||||
|
secrets: secrets,
|
||||||
|
webAuthSessionType: webAuthSessionType)
|
||||||
|
}
|
||||||
|
|
||||||
|
static let development = AppEnvironment(
|
||||||
identityDatabase: .development,
|
identityDatabase: .development,
|
||||||
secrets: .development)
|
preferences: .development,
|
||||||
|
secrets: .development,
|
||||||
|
webAuthSessionType: SuccessfulStubbingWebAuthSession.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SceneViewModel {
|
||||||
|
static let development = SceneViewModel(networkClient: .development, environment: .development)
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:enable force_try
|
// swiftlint:enable force_try
|
||||||
|
|
21
Development Assets/FakeUserDefaults.swift
Normal file
21
Development Assets/FakeUserDefaults.swift
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class FakeUserDefaults: UserDefaults {
|
||||||
|
convenience init() {
|
||||||
|
self.init(suiteName: Self.suiteName)!
|
||||||
|
}
|
||||||
|
|
||||||
|
override init?(suiteName suitename: String?) {
|
||||||
|
guard let suitename = suitename else { return nil }
|
||||||
|
|
||||||
|
UserDefaults().removePersistentDomain(forName: suitename)
|
||||||
|
|
||||||
|
super.init(suiteName: suitename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension FakeUserDefaults {
|
||||||
|
private static let suiteName = "com.metatext.metabolist.fake-user-defaults"
|
||||||
|
}
|
|
@ -1,18 +1,17 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import AuthenticationServices
|
|
||||||
|
|
||||||
class StubbingWebAuthenticationSession: WebAuthenticationSessionType {
|
class StubbingWebAuthSession: WebAuthSession {
|
||||||
let completionHandler: ASWebAuthenticationSession.CompletionHandler
|
let completionHandler: WebAuthSessionCompletionHandler
|
||||||
let url: URL
|
let url: URL
|
||||||
let callbackURLScheme: String?
|
let callbackURLScheme: String?
|
||||||
var presentationContextProvider: ASWebAuthenticationPresentationContextProviding?
|
var presentationContextProvider: WebAuthPresentationContextProviding?
|
||||||
|
|
||||||
required init(
|
required init(
|
||||||
url URL: URL,
|
url URL: URL,
|
||||||
callbackURLScheme: String?,
|
callbackURLScheme: String?,
|
||||||
completionHandler: @escaping ASWebAuthenticationSession.CompletionHandler) {
|
completionHandler: @escaping WebAuthSessionCompletionHandler) {
|
||||||
self.url = URL
|
self.url = URL
|
||||||
self.callbackURLScheme = callbackURLScheme
|
self.callbackURLScheme = callbackURLScheme
|
||||||
self.completionHandler = completionHandler
|
self.completionHandler = completionHandler
|
||||||
|
@ -33,15 +32,13 @@ class StubbingWebAuthenticationSession: WebAuthenticationSessionType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable type_name
|
class SuccessfulStubbingWebAuthSession: StubbingWebAuthSession {
|
||||||
class SuccessfulStubbingWebAuthenticationSession: StubbingWebAuthenticationSession {
|
|
||||||
// swiftlint:enable type_name
|
|
||||||
private let redirectURL: URL
|
private let redirectURL: URL
|
||||||
|
|
||||||
required init(
|
required init(
|
||||||
url URL: URL,
|
url URL: URL,
|
||||||
callbackURLScheme: String?,
|
callbackURLScheme: String?,
|
||||||
completionHandler: @escaping ASWebAuthenticationSession.CompletionHandler) {
|
completionHandler: @escaping WebAuthSessionCompletionHandler) {
|
||||||
redirectURL = Foundation.URL(
|
redirectURL = Foundation.URL(
|
||||||
string: URLComponents(url: URL, resolvingAgainstBaseURL: true)!
|
string: URLComponents(url: URL, resolvingAgainstBaseURL: true)!
|
||||||
.queryItems!.first(where: { $0.name == "redirect_uri" })!.value!)!
|
.queryItems!.first(where: { $0.name == "redirect_uri" })!.value!)!
|
||||||
|
@ -62,10 +59,8 @@ class SuccessfulStubbingWebAuthenticationSession: StubbingWebAuthenticationSessi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable type_name
|
class CanceledLoginStubbingWebAuthSession: StubbingWebAuthSession {
|
||||||
class CanceledLoginStubbingWebAuthenticationSession: StubbingWebAuthenticationSession {
|
|
||||||
// swiftlint:enable type_name
|
|
||||||
override var completionHandlerError: Error? {
|
override var completionHandlerError: Error? {
|
||||||
ASWebAuthenticationSessionError(.canceledLogin)
|
WebAuthSessionError(.canceledLogin)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -21,6 +21,13 @@
|
||||||
D04FD73D24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04FD73B24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift */; };
|
D04FD73D24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04FD73B24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift */; };
|
||||||
D04FD74224D4AA34007D572D /* DevelopmentModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04FD74124D4AA34007D572D /* DevelopmentModels.swift */; };
|
D04FD74224D4AA34007D572D /* DevelopmentModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04FD74124D4AA34007D572D /* DevelopmentModels.swift */; };
|
||||||
D04FD74324D4AA34007D572D /* DevelopmentModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04FD74124D4AA34007D572D /* DevelopmentModels.swift */; };
|
D04FD74324D4AA34007D572D /* DevelopmentModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04FD74124D4AA34007D572D /* DevelopmentModels.swift */; };
|
||||||
|
D052BBC724D749C800A80A7A /* SceneViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBC624D749C800A80A7A /* SceneViewModelTests.swift */; };
|
||||||
|
D052BBCA24D74C9200A80A7A /* FakeUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBC824D74B6400A80A7A /* FakeUserDefaults.swift */; };
|
||||||
|
D052BBCB24D74C9300A80A7A /* FakeUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBC824D74B6400A80A7A /* FakeUserDefaults.swift */; };
|
||||||
|
D052BBCF24D750C000A80A7A /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBCE24D750C000A80A7A /* Preferences.swift */; };
|
||||||
|
D052BBD024D750C000A80A7A /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBCE24D750C000A80A7A /* Preferences.swift */; };
|
||||||
|
D052BBD124D750CA00A80A7A /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBCC24D750A100A80A7A /* AppEnvironment.swift */; };
|
||||||
|
D052BBD224D750CB00A80A7A /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBCC24D750A100A80A7A /* AppEnvironment.swift */; };
|
||||||
D065F53924D37E5100741304 /* CombineExpectations in Frameworks */ = {isa = PBXBuildFile; productRef = D065F53824D37E5100741304 /* CombineExpectations */; };
|
D065F53924D37E5100741304 /* CombineExpectations in Frameworks */ = {isa = PBXBuildFile; productRef = D065F53824D37E5100741304 /* CombineExpectations */; };
|
||||||
D065F53B24D3B33A00741304 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065F53A24D3B33A00741304 /* View+Extensions.swift */; };
|
D065F53B24D3B33A00741304 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065F53A24D3B33A00741304 /* View+Extensions.swift */; };
|
||||||
D065F53C24D3B33A00741304 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065F53A24D3B33A00741304 /* View+Extensions.swift */; };
|
D065F53C24D3B33A00741304 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065F53A24D3B33A00741304 /* View+Extensions.swift */; };
|
||||||
|
@ -54,8 +61,8 @@
|
||||||
D06B492024D3FB8000642749 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D06B491E24D3F7FE00642749 /* Localizable.strings */; };
|
D06B492024D3FB8000642749 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D06B491E24D3F7FE00642749 /* Localizable.strings */; };
|
||||||
D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492224D4611300642749 /* KingfisherSwiftUI */; };
|
D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492224D4611300642749 /* KingfisherSwiftUI */; };
|
||||||
D06B492524D4612400642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492424D4612400642749 /* KingfisherSwiftUI */; };
|
D06B492524D4612400642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492424D4612400642749 /* KingfisherSwiftUI */; };
|
||||||
D074577724D29006004758DB /* StubbingWebAuthenticationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577624D29006004758DB /* StubbingWebAuthenticationSession.swift */; };
|
D074577724D29006004758DB /* StubbingWebAuthSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577624D29006004758DB /* StubbingWebAuthSession.swift */; };
|
||||||
D074577824D29006004758DB /* StubbingWebAuthenticationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577624D29006004758DB /* StubbingWebAuthenticationSession.swift */; };
|
D074577824D29006004758DB /* StubbingWebAuthSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577624D29006004758DB /* StubbingWebAuthSession.swift */; };
|
||||||
D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */; };
|
D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */; };
|
||||||
D074577B24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */; };
|
D074577B24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */; };
|
||||||
D081A40524D0F1A8001B016E /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D081A40424D0F1A8001B016E /* String+Extensions.swift */; };
|
D081A40524D0F1A8001B016E /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D081A40424D0F1A8001B016E /* String+Extensions.swift */; };
|
||||||
|
@ -103,8 +110,8 @@
|
||||||
D0DC177724D0CF2600A75C65 /* FakeKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC177624D0CF2600A75C65 /* FakeKeychain.swift */; };
|
D0DC177724D0CF2600A75C65 /* FakeKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC177624D0CF2600A75C65 /* FakeKeychain.swift */; };
|
||||||
D0DC177824D0CF2600A75C65 /* FakeKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC177624D0CF2600A75C65 /* FakeKeychain.swift */; };
|
D0DC177824D0CF2600A75C65 /* FakeKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC177624D0CF2600A75C65 /* FakeKeychain.swift */; };
|
||||||
D0ED1B6E24CE100C00B4899C /* AddIdentityViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */; };
|
D0ED1B6E24CE100C00B4899C /* AddIdentityViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */; };
|
||||||
D0ED1BB724CE47F400B4899C /* WebAuthenticationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BB624CE47F400B4899C /* WebAuthenticationSession.swift */; };
|
D0ED1BB724CE47F400B4899C /* WebAuthSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BB624CE47F400B4899C /* WebAuthSession.swift */; };
|
||||||
D0ED1BB824CE47F400B4899C /* WebAuthenticationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BB624CE47F400B4899C /* WebAuthenticationSession.swift */; };
|
D0ED1BB824CE47F400B4899C /* WebAuthSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BB624CE47F400B4899C /* WebAuthSession.swift */; };
|
||||||
D0ED1BC124CED48800B4899C /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BC024CED48800B4899C /* HTTPClient.swift */; };
|
D0ED1BC124CED48800B4899C /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BC024CED48800B4899C /* HTTPClient.swift */; };
|
||||||
D0ED1BC224CED48800B4899C /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BC024CED48800B4899C /* HTTPClient.swift */; };
|
D0ED1BC224CED48800B4899C /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BC024CED48800B4899C /* HTTPClient.swift */; };
|
||||||
D0ED1BC424CED54D00B4899C /* HTTPTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BC324CED54D00B4899C /* HTTPTarget.swift */; };
|
D0ED1BC424CED54D00B4899C /* HTTPTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BC324CED54D00B4899C /* HTTPTarget.swift */; };
|
||||||
|
@ -150,6 +157,10 @@
|
||||||
D04FD73824D4A7B4007D572D /* AccountEndpoint+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountEndpoint+Stubbing.swift"; sourceTree = "<group>"; };
|
D04FD73824D4A7B4007D572D /* AccountEndpoint+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountEndpoint+Stubbing.swift"; sourceTree = "<group>"; };
|
||||||
D04FD73B24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InstanceEndpoint+Stubbing.swift"; sourceTree = "<group>"; };
|
D04FD73B24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InstanceEndpoint+Stubbing.swift"; sourceTree = "<group>"; };
|
||||||
D04FD74124D4AA34007D572D /* DevelopmentModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevelopmentModels.swift; sourceTree = "<group>"; };
|
D04FD74124D4AA34007D572D /* DevelopmentModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevelopmentModels.swift; sourceTree = "<group>"; };
|
||||||
|
D052BBC624D749C800A80A7A /* SceneViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
|
D052BBC824D74B6400A80A7A /* FakeUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeUserDefaults.swift; sourceTree = "<group>"; };
|
||||||
|
D052BBCC24D750A100A80A7A /* AppEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppEnvironment.swift; sourceTree = "<group>"; };
|
||||||
|
D052BBCE24D750C000A80A7A /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||||
D065F53A24D3B33A00741304 /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
|
D065F53A24D3B33A00741304 /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D065F53D24D3D20300741304 /* InstanceEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceEndpoint.swift; sourceTree = "<group>"; };
|
D065F53D24D3D20300741304 /* InstanceEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceEndpoint.swift; sourceTree = "<group>"; };
|
||||||
D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -166,7 +177,7 @@
|
||||||
D0666A6E24C6DFB300F3F04B /* AccessToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessToken.swift; sourceTree = "<group>"; };
|
D0666A6E24C6DFB300F3F04B /* AccessToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessToken.swift; sourceTree = "<group>"; };
|
||||||
D0666A7124C6E0D300F3F04B /* Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = "<group>"; };
|
D0666A7124C6E0D300F3F04B /* Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = "<group>"; };
|
||||||
D06B491E24D3F7FE00642749 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
|
D06B491E24D3F7FE00642749 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
|
||||||
D074577624D29006004758DB /* StubbingWebAuthenticationSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StubbingWebAuthenticationSession.swift; sourceTree = "<group>"; };
|
D074577624D29006004758DB /* StubbingWebAuthSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StubbingWebAuthSession.swift; sourceTree = "<group>"; };
|
||||||
D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionConfiguration+Extensions.swift"; sourceTree = "<group>"; };
|
D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionConfiguration+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D081A40424D0F1A8001B016E /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
|
D081A40424D0F1A8001B016E /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+Extensions.swift"; sourceTree = "<group>"; };
|
D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -191,7 +202,7 @@
|
||||||
D0DC177324D0B58800A75C65 /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
|
D0DC177324D0B58800A75C65 /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
|
||||||
D0DC177624D0CF2600A75C65 /* FakeKeychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeKeychain.swift; sourceTree = "<group>"; };
|
D0DC177624D0CF2600A75C65 /* FakeKeychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeKeychain.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 /* WebAuthenticationSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAuthenticationSession.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>"; };
|
||||||
D0ED1BC324CED54D00B4899C /* HTTPTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPTarget.swift; sourceTree = "<group>"; };
|
D0ED1BC324CED54D00B4899C /* HTTPTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPTarget.swift; sourceTree = "<group>"; };
|
||||||
D0ED1BCA24CF744200B4899C /* MastodonClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonClient.swift; sourceTree = "<group>"; };
|
D0ED1BCA24CF744200B4899C /* MastodonClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonClient.swift; sourceTree = "<group>"; };
|
||||||
|
@ -309,6 +320,7 @@
|
||||||
D0666A5024C6C3BC00F3F04B /* Account.swift */,
|
D0666A5024C6C3BC00F3F04B /* Account.swift */,
|
||||||
D0C963FA24CC359D003BD330 /* AlertItem.swift */,
|
D0C963FA24CC359D003BD330 /* AlertItem.swift */,
|
||||||
D0666A6224C6DC6C00F3F04B /* AppAuthorization.swift */,
|
D0666A6224C6DC6C00F3F04B /* AppAuthorization.swift */,
|
||||||
|
D052BBCC24D750A100A80A7A /* AppEnvironment.swift */,
|
||||||
D0ED1BD624CF94B200B4899C /* Application.swift */,
|
D0ED1BD624CF94B200B4899C /* Application.swift */,
|
||||||
D0666A4424C6BC0A00F3F04B /* DatabaseError.swift */,
|
D0666A4424C6BC0A00F3F04B /* DatabaseError.swift */,
|
||||||
D0666A5324C6C3E500F3F04B /* Emoji.swift */,
|
D0666A5324C6C3E500F3F04B /* Emoji.swift */,
|
||||||
|
@ -317,6 +329,7 @@
|
||||||
D0666A4D24C6C39600F3F04B /* Instance.swift */,
|
D0666A4D24C6C39600F3F04B /* Instance.swift */,
|
||||||
D0DC177324D0B58800A75C65 /* Keychain.swift */,
|
D0DC177324D0B58800A75C65 /* Keychain.swift */,
|
||||||
D0ED1BE224CFA84400B4899C /* MastodonError.swift */,
|
D0ED1BE224CFA84400B4899C /* MastodonError.swift */,
|
||||||
|
D052BBCE24D750C000A80A7A /* Preferences.swift */,
|
||||||
D0666A7124C6E0D300F3F04B /* Secrets.swift */,
|
D0666A7124C6E0D300F3F04B /* Secrets.swift */,
|
||||||
);
|
);
|
||||||
path = Model;
|
path = Model;
|
||||||
|
@ -357,7 +370,7 @@
|
||||||
D0ED1BC024CED48800B4899C /* HTTPClient.swift */,
|
D0ED1BC024CED48800B4899C /* HTTPClient.swift */,
|
||||||
D0666A5624C6C63400F3F04B /* MastodonDecoder.swift */,
|
D0666A5624C6C63400F3F04B /* MastodonDecoder.swift */,
|
||||||
D0666A5924C6C64100F3F04B /* MastodonEncoder.swift */,
|
D0666A5924C6C64100F3F04B /* MastodonEncoder.swift */,
|
||||||
D0ED1BB624CE47F400B4899C /* WebAuthenticationSession.swift */,
|
D0ED1BB624CE47F400B4899C /* WebAuthSession.swift */,
|
||||||
);
|
);
|
||||||
path = Networking;
|
path = Networking;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -400,6 +413,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */,
|
D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */,
|
||||||
|
D052BBC624D749C800A80A7A /* SceneViewModelTests.swift */,
|
||||||
);
|
);
|
||||||
path = "View Models";
|
path = "View Models";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -409,11 +423,12 @@
|
||||||
children = (
|
children = (
|
||||||
D04FD74124D4AA34007D572D /* DevelopmentModels.swift */,
|
D04FD74124D4AA34007D572D /* DevelopmentModels.swift */,
|
||||||
D0DC177624D0CF2600A75C65 /* FakeKeychain.swift */,
|
D0DC177624D0CF2600A75C65 /* FakeKeychain.swift */,
|
||||||
|
D052BBC824D74B6400A80A7A /* FakeUserDefaults.swift */,
|
||||||
D0DC175724D0130800A75C65 /* HTTPStubs.swift */,
|
D0DC175724D0130800A75C65 /* HTTPStubs.swift */,
|
||||||
D0DC174824CFF13700A75C65 /* Mastodon API Stubs */,
|
D0DC174824CFF13700A75C65 /* Mastodon API Stubs */,
|
||||||
D0DC174C24CFF1F100A75C65 /* Stubbing.swift */,
|
D0DC174C24CFF1F100A75C65 /* Stubbing.swift */,
|
||||||
D0DC174524CFEC2000A75C65 /* StubbingURLProtocol.swift */,
|
D0DC174524CFEC2000A75C65 /* StubbingURLProtocol.swift */,
|
||||||
D074577624D29006004758DB /* StubbingWebAuthenticationSession.swift */,
|
D074577624D29006004758DB /* StubbingWebAuthSession.swift */,
|
||||||
D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */,
|
D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */,
|
||||||
);
|
);
|
||||||
path = "Development Assets";
|
path = "Development Assets";
|
||||||
|
@ -630,6 +645,7 @@
|
||||||
D0BEC94724CA22C400E864C4 /* TimelineViewModel.swift in Sources */,
|
D0BEC94724CA22C400E864C4 /* TimelineViewModel.swift in Sources */,
|
||||||
D0666A4E24C6C39600F3F04B /* Instance.swift in Sources */,
|
D0666A4E24C6C39600F3F04B /* Instance.swift in Sources */,
|
||||||
D0ED1BDA24CF963E00B4899C /* AppAuthorizationEndpoint.swift in Sources */,
|
D0ED1BDA24CF963E00B4899C /* AppAuthorizationEndpoint.swift in Sources */,
|
||||||
|
D052BBCF24D750C000A80A7A /* Preferences.swift in Sources */,
|
||||||
D0ED1BE324CFA84400B4899C /* MastodonError.swift in Sources */,
|
D0ED1BE324CFA84400B4899C /* MastodonError.swift in Sources */,
|
||||||
D0666A6324C6DC6C00F3F04B /* AppAuthorization.swift in Sources */,
|
D0666A6324C6DC6C00F3F04B /* AppAuthorization.swift in Sources */,
|
||||||
D065F53B24D3B33A00741304 /* View+Extensions.swift in Sources */,
|
D065F53B24D3B33A00741304 /* View+Extensions.swift in Sources */,
|
||||||
|
@ -639,6 +655,7 @@
|
||||||
D0666A5124C6C3BC00F3F04B /* Account.swift in Sources */,
|
D0666A5124C6C3BC00F3F04B /* Account.swift in Sources */,
|
||||||
D0ED1BE024CF98FB00B4899C /* AccountEndpoint.swift in Sources */,
|
D0ED1BE024CF98FB00B4899C /* AccountEndpoint.swift in Sources */,
|
||||||
D0B93B3024D55098007AF646 /* Screen.swift in Sources */,
|
D0B93B3024D55098007AF646 /* Screen.swift in Sources */,
|
||||||
|
D052BBD224D750CB00A80A7A /* AppEnvironment.swift in Sources */,
|
||||||
D081A40524D0F1A8001B016E /* String+Extensions.swift in Sources */,
|
D081A40524D0F1A8001B016E /* String+Extensions.swift in Sources */,
|
||||||
D0BEC93824C9632800E864C4 /* SceneViewModel.swift in Sources */,
|
D0BEC93824C9632800E864C4 /* SceneViewModel.swift in Sources */,
|
||||||
D0ED1BC124CED48800B4899C /* HTTPClient.swift in Sources */,
|
D0ED1BC124CED48800B4899C /* HTTPClient.swift in Sources */,
|
||||||
|
@ -648,6 +665,7 @@
|
||||||
D0666A5424C6C3E500F3F04B /* Emoji.swift in Sources */,
|
D0666A5424C6C3E500F3F04B /* Emoji.swift in Sources */,
|
||||||
D0DC175524D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift in Sources */,
|
D0DC175524D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift in Sources */,
|
||||||
D0B23F0D24D210E90066F411 /* NSError+Extensions.swift in Sources */,
|
D0B23F0D24D210E90066F411 /* NSError+Extensions.swift in Sources */,
|
||||||
|
D052BBCA24D74C9200A80A7A /* FakeUserDefaults.swift in Sources */,
|
||||||
D0DC175224D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */,
|
D0DC175224D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */,
|
||||||
D0666A4224C6BB7B00F3F04B /* IdentityDatabase.swift in Sources */,
|
D0666A4224C6BB7B00F3F04B /* IdentityDatabase.swift in Sources */,
|
||||||
D0BEC94A24CA231200E864C4 /* TimelineView.swift in Sources */,
|
D0BEC94A24CA231200E864C4 /* TimelineView.swift in Sources */,
|
||||||
|
@ -662,10 +680,10 @@
|
||||||
D0666A5724C6C63400F3F04B /* MastodonDecoder.swift in Sources */,
|
D0666A5724C6C63400F3F04B /* MastodonDecoder.swift in Sources */,
|
||||||
D0DB6EF424C5228A00D965FE /* AddIdentityView.swift in Sources */,
|
D0DB6EF424C5228A00D965FE /* AddIdentityView.swift in Sources */,
|
||||||
D0DC177424D0B58800A75C65 /* Keychain.swift in Sources */,
|
D0DC177424D0B58800A75C65 /* Keychain.swift in Sources */,
|
||||||
D074577724D29006004758DB /* StubbingWebAuthenticationSession.swift in Sources */,
|
D074577724D29006004758DB /* StubbingWebAuthSession.swift in Sources */,
|
||||||
D0ED1BCE24CF768200B4899C /* MastodonEndpoint.swift in Sources */,
|
D0ED1BCE24CF768200B4899C /* MastodonEndpoint.swift in Sources */,
|
||||||
D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */,
|
D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */,
|
||||||
D0ED1BB724CE47F400B4899C /* WebAuthenticationSession.swift in Sources */,
|
D0ED1BB724CE47F400B4899C /* WebAuthSession.swift in Sources */,
|
||||||
D0666A7224C6E0D300F3F04B /* Secrets.swift in Sources */,
|
D0666A7224C6E0D300F3F04B /* Secrets.swift in Sources */,
|
||||||
D0BEC95124CA2B7E00E864C4 /* TabNavigation.swift in Sources */,
|
D0BEC95124CA2B7E00E864C4 /* TabNavigation.swift in Sources */,
|
||||||
D0ED1BC424CED54D00B4899C /* HTTPTarget.swift in Sources */,
|
D0ED1BC424CED54D00B4899C /* HTTPTarget.swift in Sources */,
|
||||||
|
@ -690,6 +708,7 @@
|
||||||
D0BEC94824CA22C400E864C4 /* TimelineViewModel.swift in Sources */,
|
D0BEC94824CA22C400E864C4 /* TimelineViewModel.swift in Sources */,
|
||||||
D0666A4F24C6C39600F3F04B /* Instance.swift in Sources */,
|
D0666A4F24C6C39600F3F04B /* Instance.swift in Sources */,
|
||||||
D0ED1BDB24CF963E00B4899C /* AppAuthorizationEndpoint.swift in Sources */,
|
D0ED1BDB24CF963E00B4899C /* AppAuthorizationEndpoint.swift in Sources */,
|
||||||
|
D052BBD024D750C000A80A7A /* Preferences.swift in Sources */,
|
||||||
D0ED1BE424CFA84400B4899C /* MastodonError.swift in Sources */,
|
D0ED1BE424CFA84400B4899C /* MastodonError.swift in Sources */,
|
||||||
D0666A6424C6DC6C00F3F04B /* AppAuthorization.swift in Sources */,
|
D0666A6424C6DC6C00F3F04B /* AppAuthorization.swift in Sources */,
|
||||||
D065F53C24D3B33A00741304 /* View+Extensions.swift in Sources */,
|
D065F53C24D3B33A00741304 /* View+Extensions.swift in Sources */,
|
||||||
|
@ -699,6 +718,7 @@
|
||||||
D0666A5224C6C3BC00F3F04B /* Account.swift in Sources */,
|
D0666A5224C6C3BC00F3F04B /* Account.swift in Sources */,
|
||||||
D0ED1BE124CF98FB00B4899C /* AccountEndpoint.swift in Sources */,
|
D0ED1BE124CF98FB00B4899C /* AccountEndpoint.swift in Sources */,
|
||||||
D0B93B3124D55098007AF646 /* Screen.swift in Sources */,
|
D0B93B3124D55098007AF646 /* Screen.swift in Sources */,
|
||||||
|
D052BBD124D750CA00A80A7A /* AppEnvironment.swift in Sources */,
|
||||||
D081A40624D0F1A8001B016E /* String+Extensions.swift in Sources */,
|
D081A40624D0F1A8001B016E /* String+Extensions.swift in Sources */,
|
||||||
D0BEC93924C9632800E864C4 /* SceneViewModel.swift in Sources */,
|
D0BEC93924C9632800E864C4 /* SceneViewModel.swift in Sources */,
|
||||||
D0ED1BC224CED48800B4899C /* HTTPClient.swift in Sources */,
|
D0ED1BC224CED48800B4899C /* HTTPClient.swift in Sources */,
|
||||||
|
@ -708,6 +728,7 @@
|
||||||
D0666A5524C6C3E500F3F04B /* Emoji.swift in Sources */,
|
D0666A5524C6C3E500F3F04B /* Emoji.swift in Sources */,
|
||||||
D0DC175624D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift in Sources */,
|
D0DC175624D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift in Sources */,
|
||||||
D0B23F0E24D210E90066F411 /* NSError+Extensions.swift in Sources */,
|
D0B23F0E24D210E90066F411 /* NSError+Extensions.swift in Sources */,
|
||||||
|
D052BBCB24D74C9300A80A7A /* FakeUserDefaults.swift in Sources */,
|
||||||
D0DC175324D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */,
|
D0DC175324D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */,
|
||||||
D0666A4324C6BB7B00F3F04B /* IdentityDatabase.swift in Sources */,
|
D0666A4324C6BB7B00F3F04B /* IdentityDatabase.swift in Sources */,
|
||||||
D0BEC94B24CA231200E864C4 /* TimelineView.swift in Sources */,
|
D0BEC94B24CA231200E864C4 /* TimelineView.swift in Sources */,
|
||||||
|
@ -722,10 +743,10 @@
|
||||||
D0666A5824C6C63400F3F04B /* MastodonDecoder.swift in Sources */,
|
D0666A5824C6C63400F3F04B /* MastodonDecoder.swift in Sources */,
|
||||||
D0DB6EF524C5233E00D965FE /* AddIdentityView.swift in Sources */,
|
D0DB6EF524C5233E00D965FE /* AddIdentityView.swift in Sources */,
|
||||||
D0DC177524D0B58800A75C65 /* Keychain.swift in Sources */,
|
D0DC177524D0B58800A75C65 /* Keychain.swift in Sources */,
|
||||||
D074577824D29006004758DB /* StubbingWebAuthenticationSession.swift in Sources */,
|
D074577824D29006004758DB /* StubbingWebAuthSession.swift in Sources */,
|
||||||
D0ED1BCF24CF768200B4899C /* MastodonEndpoint.swift in Sources */,
|
D0ED1BCF24CF768200B4899C /* MastodonEndpoint.swift in Sources */,
|
||||||
D074577B24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */,
|
D074577B24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */,
|
||||||
D0ED1BB824CE47F400B4899C /* WebAuthenticationSession.swift in Sources */,
|
D0ED1BB824CE47F400B4899C /* WebAuthSession.swift in Sources */,
|
||||||
D0BEC94F24CA2B5300E864C4 /* SidebarNavigation.swift in Sources */,
|
D0BEC94F24CA2B5300E864C4 /* SidebarNavigation.swift in Sources */,
|
||||||
D0666A7324C6E0D300F3F04B /* Secrets.swift in Sources */,
|
D0666A7324C6E0D300F3F04B /* Secrets.swift in Sources */,
|
||||||
D0ED1BC524CED54D00B4899C /* HTTPTarget.swift in Sources */,
|
D0ED1BC524CED54D00B4899C /* HTTPTarget.swift in Sources */,
|
||||||
|
@ -744,6 +765,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D0ED1B6E24CE100C00B4899C /* AddIdentityViewModelTests.swift in Sources */,
|
D0ED1B6E24CE100C00B4899C /* AddIdentityViewModelTests.swift in Sources */,
|
||||||
|
D052BBC724D749C800A80A7A /* SceneViewModelTests.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,15 +4,22 @@ import SwiftUI
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct MetatextApp: App {
|
struct MetatextApp: App {
|
||||||
private let identityDatabase: IdentityDatabase
|
private let environment: AppEnvironment
|
||||||
private let secrets = Secrets(keychain: Keychain(service: "com.metabolist.metatext"))
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
let identityDatabase: IdentityDatabase
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try identityDatabase = IdentityDatabase()
|
try identityDatabase = IdentityDatabase()
|
||||||
} catch {
|
} catch {
|
||||||
fatalError("Failed to initialize identity database")
|
fatalError("Failed to initialize identity database")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
environment = AppEnvironment(
|
||||||
|
identityDatabase: identityDatabase,
|
||||||
|
preferences: Preferences(userDefaults: .standard),
|
||||||
|
secrets: Secrets(keychain: Keychain(service: "com.metabolist.metatext")),
|
||||||
|
webAuthSessionType: RealWebAuthSession.self)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
|
@ -21,8 +28,7 @@ struct MetatextApp: App {
|
||||||
.environmentObject(
|
.environmentObject(
|
||||||
SceneViewModel(
|
SceneViewModel(
|
||||||
networkClient: MastodonClient(),
|
networkClient: MastodonClient(),
|
||||||
identityDatabase: identityDatabase,
|
environment: environment))
|
||||||
secrets: secrets))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
10
Shared/Model/AppEnvironment.swift
Normal file
10
Shared/Model/AppEnvironment.swift
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct AppEnvironment {
|
||||||
|
let identityDatabase: IdentityDatabase
|
||||||
|
let preferences: Preferences
|
||||||
|
let secrets: Secrets
|
||||||
|
let webAuthSessionType: WebAuthSession.Type
|
||||||
|
}
|
24
Shared/Model/Preferences.swift
Normal file
24
Shared/Model/Preferences.swift
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class Preferences {
|
||||||
|
private let userDefaults: UserDefaults
|
||||||
|
|
||||||
|
init(userDefaults: UserDefaults) {
|
||||||
|
self.userDefaults = userDefaults
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Preferences {
|
||||||
|
enum Item: String {
|
||||||
|
case recentIdentityID = "recent-identity-id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Preferences {
|
||||||
|
subscript<T>(index: Preferences.Item) -> T? {
|
||||||
|
get { userDefaults.value(forKey: index.rawValue) as? T }
|
||||||
|
set { userDefaults.set(newValue, forKey: index.rawValue) }
|
||||||
|
}
|
||||||
|
}
|
49
Shared/Networking/WebAuthSession.swift
Normal file
49
Shared/Networking/WebAuthSession.swift
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AuthenticationServices
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
protocol WebAuthSession: AnyObject {
|
||||||
|
init(url URL: URL,
|
||||||
|
callbackURLScheme: String?,
|
||||||
|
completionHandler: @escaping WebAuthSessionCompletionHandler)
|
||||||
|
var presentationContextProvider: WebAuthPresentationContextProviding? { get set }
|
||||||
|
@discardableResult func start() -> Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
extension WebAuthSession {
|
||||||
|
static func publisher(
|
||||||
|
url: URL,
|
||||||
|
callbackURLScheme: String?,
|
||||||
|
presentationContextProvider: WebAuthPresentationContextProviding) -> AnyPublisher<URL?, Error> {
|
||||||
|
Future<URL?, Error> { promise in
|
||||||
|
let webAuthSession = Self(
|
||||||
|
url: url,
|
||||||
|
callbackURLScheme: callbackURLScheme) { oauthCallbackURL, error in
|
||||||
|
if let error = error {
|
||||||
|
return promise(.failure(error))
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise(.success(oauthCallbackURL))
|
||||||
|
}
|
||||||
|
|
||||||
|
webAuthSession.presentationContextProvider = presentationContextProvider
|
||||||
|
webAuthSession.start()
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebAuthSessionContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding {
|
||||||
|
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
||||||
|
ASPresentationAnchor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias WebAuthSessionCompletionHandler = ASWebAuthenticationSession.CompletionHandler
|
||||||
|
typealias WebAuthSessionError = ASWebAuthenticationSessionError
|
||||||
|
typealias WebAuthPresentationContextProviding = ASWebAuthenticationPresentationContextProviding
|
||||||
|
typealias RealWebAuthSession = ASWebAuthenticationSession
|
||||||
|
|
||||||
|
extension RealWebAuthSession: WebAuthSession {}
|
|
@ -1,38 +0,0 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import AuthenticationServices
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
protocol WebAuthenticationSessionType: AnyObject {
|
|
||||||
init(url URL: URL,
|
|
||||||
callbackURLScheme: String?,
|
|
||||||
completionHandler: @escaping ASWebAuthenticationSession.CompletionHandler)
|
|
||||||
var presentationContextProvider: ASWebAuthenticationPresentationContextProviding? { get set }
|
|
||||||
@discardableResult func start() -> Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ASWebAuthenticationSession: WebAuthenticationSessionType {}
|
|
||||||
|
|
||||||
extension WebAuthenticationSessionType {
|
|
||||||
static func publisher(
|
|
||||||
url: URL,
|
|
||||||
callbackURLScheme: String?,
|
|
||||||
presentationContextProvider: ASWebAuthenticationPresentationContextProviding) -> AnyPublisher<URL?, Error> {
|
|
||||||
Future<URL?, Error> { promise in
|
|
||||||
let webAuthenticationSession = Self(
|
|
||||||
url: url,
|
|
||||||
callbackURLScheme: callbackURLScheme) { oauthCallbackURL, error in
|
|
||||||
if let error = error {
|
|
||||||
return promise(.failure(error))
|
|
||||||
}
|
|
||||||
|
|
||||||
return promise(.success(oauthCallbackURL))
|
|
||||||
}
|
|
||||||
|
|
||||||
webAuthenticationSession.presentationContextProvider = presentationContextProvider
|
|
||||||
webAuthenticationSession.start()
|
|
||||||
}
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
import AuthenticationServices
|
|
||||||
|
|
||||||
class AddIdentityViewModel: ObservableObject {
|
class AddIdentityViewModel: ObservableObject {
|
||||||
@Published var urlFieldText = ""
|
@Published var urlFieldText = ""
|
||||||
|
@ -11,20 +10,12 @@ class AddIdentityViewModel: ObservableObject {
|
||||||
@Published private(set) var addedIdentityID: String?
|
@Published private(set) var addedIdentityID: String?
|
||||||
|
|
||||||
private let networkClient: HTTPClient
|
private let networkClient: HTTPClient
|
||||||
private let identityDatabase: IdentityDatabase
|
private let environment: AppEnvironment
|
||||||
private let secrets: Secrets
|
private let webAuthSessionContextProvider = WebAuthSessionContextProvider()
|
||||||
private let webAuthenticationSessionType: WebAuthenticationSessionType.Type
|
|
||||||
private let webAuthenticationSessionContextProvider = WebAuthenticationSessionContextProvider()
|
|
||||||
|
|
||||||
init(
|
init(networkClient: HTTPClient, environment: AppEnvironment) {
|
||||||
networkClient: HTTPClient,
|
|
||||||
identityDatabase: IdentityDatabase,
|
|
||||||
secrets: Secrets,
|
|
||||||
webAuthenticationSessionType: WebAuthenticationSessionType.Type = ASWebAuthenticationSession.self) {
|
|
||||||
self.networkClient = networkClient
|
self.networkClient = networkClient
|
||||||
self.identityDatabase = identityDatabase
|
self.environment = environment
|
||||||
self.secrets = secrets
|
|
||||||
self.webAuthenticationSessionType = webAuthenticationSessionType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func goTapped() {
|
func goTapped() {
|
||||||
|
@ -45,22 +36,19 @@ class AddIdentityViewModel: ObservableObject {
|
||||||
identityID: identityID,
|
identityID: identityID,
|
||||||
instanceURL: instanceURL,
|
instanceURL: instanceURL,
|
||||||
redirectURL: redirectURL,
|
redirectURL: redirectURL,
|
||||||
secrets: secrets)
|
secrets: environment.secrets)
|
||||||
.authenticationURL(instanceURL: instanceURL, redirectURL: redirectURL)
|
.authenticationURL(instanceURL: instanceURL, redirectURL: redirectURL)
|
||||||
.authenticate(
|
.authenticate(
|
||||||
webAuthenticationSessionType: webAuthenticationSessionType,
|
webAuthSessionType: environment.webAuthSessionType,
|
||||||
contextProvider: webAuthenticationSessionContextProvider,
|
contextProvider: webAuthSessionContextProvider,
|
||||||
callbackURLScheme: MastodonAPI.OAuth.callbackURLScheme)
|
callbackURLScheme: MastodonAPI.OAuth.callbackURLScheme)
|
||||||
.extractCode()
|
.extractCode()
|
||||||
.requestAccessToken(
|
.requestAccessToken(
|
||||||
networkClient: networkClient,
|
networkClient: networkClient,
|
||||||
identityID: identityID,
|
identityID: identityID,
|
||||||
instanceURL: instanceURL)
|
|
||||||
.createIdentity(
|
|
||||||
id: identityID,
|
|
||||||
instanceURL: instanceURL,
|
instanceURL: instanceURL,
|
||||||
identityDatabase: identityDatabase,
|
redirectURL: redirectURL)
|
||||||
secrets: secrets)
|
.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(
|
||||||
|
@ -72,12 +60,6 @@ class AddIdentityViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension AddIdentityViewModel {
|
private extension AddIdentityViewModel {
|
||||||
private class WebAuthenticationSessionContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding {
|
|
||||||
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
|
||||||
ASPresentationAnchor()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func authorizeApp(
|
private func authorizeApp(
|
||||||
identityID: String,
|
identityID: String,
|
||||||
instanceURL: URL,
|
instanceURL: URL,
|
||||||
|
@ -102,9 +84,7 @@ private extension AddIdentityViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension Publisher where Output == AppAuthorization {
|
private extension Publisher where Output == AppAuthorization {
|
||||||
func authenticationURL(
|
func authenticationURL(instanceURL: URL, redirectURL: URL) -> AnyPublisher<(AppAuthorization, URL), Error> {
|
||||||
instanceURL: URL,
|
|
||||||
redirectURL: URL) -> AnyPublisher<(AppAuthorization, URL), Error> {
|
|
||||||
tryMap { appAuthorization in
|
tryMap { appAuthorization in
|
||||||
guard var authorizationURLComponents = URLComponents(url: instanceURL, resolvingAgainstBaseURL: true) else {
|
guard var authorizationURLComponents = URLComponents(url: instanceURL, resolvingAgainstBaseURL: true) else {
|
||||||
throw URLError(.badURL)
|
throw URLError(.badURL)
|
||||||
|
@ -131,16 +111,16 @@ private extension Publisher where Output == AppAuthorization {
|
||||||
|
|
||||||
private extension Publisher where Output == (AppAuthorization, URL), Failure == Error {
|
private extension Publisher where Output == (AppAuthorization, URL), Failure == Error {
|
||||||
func authenticate(
|
func authenticate(
|
||||||
webAuthenticationSessionType: WebAuthenticationSessionType.Type,
|
webAuthSessionType: WebAuthSession.Type,
|
||||||
contextProvider: ASWebAuthenticationPresentationContextProviding,
|
contextProvider: WebAuthSessionContextProvider,
|
||||||
callbackURLScheme: String) -> AnyPublisher<(AppAuthorization, URL), Error> {
|
callbackURLScheme: String) -> AnyPublisher<(AppAuthorization, URL), Error> {
|
||||||
flatMap { appAuthorization, url in
|
flatMap { appAuthorization, url in
|
||||||
webAuthenticationSessionType.publisher(
|
webAuthSessionType.publisher(
|
||||||
url: url,
|
url: url,
|
||||||
callbackURLScheme: callbackURLScheme,
|
callbackURLScheme: callbackURLScheme,
|
||||||
presentationContextProvider: contextProvider)
|
presentationContextProvider: contextProvider)
|
||||||
.tryCatch { error -> AnyPublisher<URL?, Error> in
|
.tryCatch { error -> AnyPublisher<URL?, Error> in
|
||||||
if (error as? ASWebAuthenticationSessionError)?.code == .canceledLogin {
|
if (error as? WebAuthSessionError)?.code == .canceledLogin {
|
||||||
return Just(nil).setFailureType(to: Error.self).eraseToAnyPublisher()
|
return Just(nil).setFailureType(to: Error.self).eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,33 +134,32 @@ private extension Publisher where Output == (AppAuthorization, URL), Failure ==
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension Publisher where Output == (AppAuthorization, URL) {
|
private extension Publisher where Output == (AppAuthorization, URL) {
|
||||||
// swiftlint:disable large_tuple
|
func extractCode() -> AnyPublisher<(AppAuthorization, String), Error> {
|
||||||
func extractCode() -> AnyPublisher<(AppAuthorization, URL, String), Error> {
|
tryMap { appAuthorization, url -> (AppAuthorization, String) in
|
||||||
tryMap { appAuthorization, url -> (AppAuthorization, URL, String) in
|
|
||||||
guard let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems,
|
guard let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems,
|
||||||
let code = queryItems.first(where: { $0.name == MastodonAPI.OAuth.codeCallbackQueryItemName })?.value
|
let code = queryItems.first(where: { $0.name == MastodonAPI.OAuth.codeCallbackQueryItemName })?.value
|
||||||
else { throw MastodonAPI.OAuthError.codeNotFound }
|
else { throw MastodonAPI.OAuthError.codeNotFound }
|
||||||
|
|
||||||
return (appAuthorization, url, code)
|
return (appAuthorization, code)
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
// swiftlint:enable large_tuple
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension Publisher where Output == (AppAuthorization, URL, String), Failure == Error {
|
private extension Publisher where Output == (AppAuthorization, String), Failure == Error {
|
||||||
func requestAccessToken(
|
func requestAccessToken(
|
||||||
networkClient: HTTPClient,
|
networkClient: HTTPClient,
|
||||||
identityID: String,
|
identityID: String,
|
||||||
instanceURL: URL) -> AnyPublisher<AccessToken, Error> {
|
instanceURL: URL,
|
||||||
flatMap { appAuthorization, url, code -> AnyPublisher<AccessToken, Error> in
|
redirectURL: URL) -> AnyPublisher<AccessToken, Error> {
|
||||||
|
flatMap { appAuthorization, code -> AnyPublisher<AccessToken, Error> in
|
||||||
let endpoint = AccessTokenEndpoint.oauthToken(
|
let endpoint = AccessTokenEndpoint.oauthToken(
|
||||||
clientID: appAuthorization.clientId,
|
clientID: appAuthorization.clientId,
|
||||||
clientSecret: appAuthorization.clientSecret,
|
clientSecret: appAuthorization.clientSecret,
|
||||||
code: code,
|
code: code,
|
||||||
grantType: MastodonAPI.OAuth.grantType,
|
grantType: MastodonAPI.OAuth.grantType,
|
||||||
scopes: MastodonAPI.OAuth.scopes,
|
scopes: MastodonAPI.OAuth.scopes,
|
||||||
redirectURI: url.absoluteString)
|
redirectURI: redirectURL.absoluteString)
|
||||||
let target = MastodonTarget(baseURL: instanceURL, endpoint: endpoint, accessToken: nil)
|
let target = MastodonTarget(baseURL: instanceURL, endpoint: endpoint, accessToken: nil)
|
||||||
|
|
||||||
return networkClient.request(target)
|
return networkClient.request(target)
|
||||||
|
@ -190,18 +169,18 @@ private extension Publisher where Output == (AppAuthorization, URL, String), Fai
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension Publisher where Output == AccessToken {
|
private extension Publisher where Output == AccessToken {
|
||||||
func createIdentity(
|
func createIdentity(id: String, instanceURL: URL, environment: AppEnvironment) -> AnyPublisher<String, Error> {
|
||||||
id: String,
|
|
||||||
instanceURL: URL,
|
|
||||||
identityDatabase: IdentityDatabase,
|
|
||||||
secrets: Secrets) -> AnyPublisher<String, Error> {
|
|
||||||
tryMap { accessToken -> (String, URL) in
|
tryMap { accessToken -> (String, URL) in
|
||||||
try secrets.set(accessToken.accessToken, forItem: .accessToken, forIdentityID: id)
|
try environment.secrets.set(accessToken.accessToken, forItem: .accessToken, forIdentityID: id)
|
||||||
|
|
||||||
return (id, instanceURL)
|
return (id, instanceURL)
|
||||||
}
|
}
|
||||||
.flatMap(identityDatabase.createIdentity)
|
.flatMap(environment.identityDatabase.createIdentity)
|
||||||
.map { id }
|
.map {
|
||||||
|
environment.preferences[.recentIdentityID] = id
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,21 +10,14 @@ class SceneViewModel: ObservableObject {
|
||||||
var selectedTopLevelNavigation: TopLevelNavigation? = .timelines
|
var selectedTopLevelNavigation: TopLevelNavigation? = .timelines
|
||||||
|
|
||||||
private let networkClient: MastodonClient
|
private let networkClient: MastodonClient
|
||||||
private let identityDatabase: IdentityDatabase
|
private let environment: AppEnvironment
|
||||||
private let secrets: Secrets
|
|
||||||
private let userDefaults: UserDefaults
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(networkClient: MastodonClient,
|
init(networkClient: MastodonClient, environment: AppEnvironment) {
|
||||||
identityDatabase: IdentityDatabase,
|
|
||||||
secrets: Secrets,
|
|
||||||
userDefaults: UserDefaults = .standard) {
|
|
||||||
self.networkClient = networkClient
|
self.networkClient = networkClient
|
||||||
self.identityDatabase = identityDatabase
|
self.environment = environment
|
||||||
self.secrets = secrets
|
|
||||||
self.userDefaults = userDefaults
|
|
||||||
|
|
||||||
if let recentIdentityID = recentIdentityID {
|
if let recentIdentityID = environment.preferences[.recentIdentityID] as String? {
|
||||||
changeIdentity(id: recentIdentityID)
|
changeIdentity(id: recentIdentityID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +30,7 @@ extension SceneViewModel {
|
||||||
if networkClient.accessToken != nil {
|
if networkClient.accessToken != nil {
|
||||||
networkClient.request(AccountEndpoint.verifyCredentials)
|
networkClient.request(AccountEndpoint.verifyCredentials)
|
||||||
.map { ($0, identity.id) }
|
.map { ($0, identity.id) }
|
||||||
.flatMap(identityDatabase.updateAccount)
|
.flatMap(environment.identityDatabase.updateAccount)
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.sink(receiveValue: {})
|
.sink(receiveValue: {})
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
@ -45,17 +38,14 @@ extension SceneViewModel {
|
||||||
|
|
||||||
networkClient.request(InstanceEndpoint.instance)
|
networkClient.request(InstanceEndpoint.instance)
|
||||||
.map { ($0, identity.id) }
|
.map { ($0, identity.id) }
|
||||||
.flatMap(identityDatabase.updateInstance)
|
.flatMap(environment.identityDatabase.updateInstance)
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.sink(receiveValue: {})
|
.sink(receiveValue: {})
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addIdentityViewModel() -> AddIdentityViewModel {
|
func addIdentityViewModel() -> AddIdentityViewModel {
|
||||||
let addAccountViewModel = AddIdentityViewModel(
|
let addAccountViewModel = AddIdentityViewModel(networkClient: networkClient, environment: environment)
|
||||||
networkClient: networkClient,
|
|
||||||
identityDatabase: identityDatabase,
|
|
||||||
secrets: secrets)
|
|
||||||
|
|
||||||
addAccountViewModel.$addedIdentityID
|
addAccountViewModel.$addedIdentityID
|
||||||
.compactMap { $0 }
|
.compactMap { $0 }
|
||||||
|
@ -67,24 +57,17 @@ extension SceneViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension SceneViewModel {
|
private extension SceneViewModel {
|
||||||
private static let recentIdentityIDKey = "recentIdentityID"
|
|
||||||
|
|
||||||
private var recentIdentityID: String? {
|
|
||||||
get { userDefaults.value(forKey: Self.recentIdentityIDKey) as? String }
|
|
||||||
set { userDefaults.set(newValue, forKey: Self.recentIdentityIDKey) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func changeIdentity(id: String) {
|
private func changeIdentity(id: String) {
|
||||||
identityDatabase.identityObservation(id: id)
|
environment.identityDatabase.identityObservation(id: id)
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.handleEvents(receiveOutput: { [weak self] in
|
.handleEvents(receiveOutput: { [weak self] in
|
||||||
guard let self = self, let identity = $0 else { return }
|
guard let self = self, let identity = $0 else { return }
|
||||||
|
|
||||||
self.recentIdentityID = identity.id
|
|
||||||
self.networkClient.instanceURL = identity.url
|
self.networkClient.instanceURL = identity.url
|
||||||
|
|
||||||
do {
|
do {
|
||||||
self.networkClient.accessToken = try self.secrets.item(.accessToken, forIdentityID: identity.id)
|
self.networkClient.accessToken =
|
||||||
|
try self.environment.secrets.item(.accessToken, forIdentityID: identity.id)
|
||||||
} catch {
|
} catch {
|
||||||
self.alertItem = AlertItem(error: error)
|
self.alertItem = AlertItem(error: error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,10 +41,7 @@ struct AddAccountView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
AddIdentityView(viewModel: AddIdentityViewModel(
|
AddIdentityView(viewModel: AddIdentityViewModel(
|
||||||
networkClient: MastodonClient.development,
|
networkClient: MastodonClient.development,
|
||||||
// swiftlint:disable force_try
|
environment: .development))
|
||||||
identityDatabase: try! IdentityDatabase(inMemory: true),
|
|
||||||
// swiftlint:enable force_try
|
|
||||||
secrets: Secrets(keychain: FakeKeychain())))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -24,7 +24,8 @@ struct ContentView: View {
|
||||||
private extension ContentView {
|
private extension ContentView {
|
||||||
private func mainNavigation(identity: Identity) -> some View {
|
private func mainNavigation(identity: Identity) -> some View {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
return SidebarNavigation().frame(minWidth: 900, maxWidth: .infinity, minHeight: 500, maxHeight: .infinity)
|
return SidebarNavigation(identity: identity)
|
||||||
|
.frame(minWidth: 900, maxWidth: .infinity, minHeight: 500, maxHeight: .infinity)
|
||||||
#else
|
#else
|
||||||
return TabNavigation(identity: identity)
|
return TabNavigation(identity: identity)
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -6,72 +6,54 @@ import CombineExpectations
|
||||||
@testable import Metatext
|
@testable import Metatext
|
||||||
|
|
||||||
class AddIdentityViewModelTests: XCTestCase {
|
class AddIdentityViewModelTests: XCTestCase {
|
||||||
var networkClient: MastodonClient!
|
|
||||||
var identityDatabase: IdentityDatabase!
|
|
||||||
var secrets: Secrets!
|
|
||||||
var cancellables = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
|
||||||
networkClient = MastodonClient(configuration: .stubbing)
|
|
||||||
identityDatabase = try IdentityDatabase(inMemory: true)
|
|
||||||
secrets = Secrets(keychain: FakeKeychain())
|
|
||||||
}
|
|
||||||
|
|
||||||
func testAddIdentity() throws {
|
func testAddIdentity() throws {
|
||||||
let sut = AddIdentityViewModel(
|
let environment = AppEnvironment.fresh()
|
||||||
networkClient: networkClient,
|
let sut = AddIdentityViewModel(networkClient: MastodonClient.fresh(), environment: environment)
|
||||||
identityDatabase: identityDatabase,
|
|
||||||
secrets: secrets,
|
|
||||||
webAuthenticationSessionType: SuccessfulStubbingWebAuthenticationSession.self)
|
|
||||||
let addedIDRecorder = sut.$addedIdentityID.record()
|
let addedIDRecorder = sut.$addedIdentityID.record()
|
||||||
_ = try wait(for: addedIDRecorder.next(), timeout: 1)
|
XCTAssertNil(try wait(for: addedIDRecorder.next(), timeout: 1))
|
||||||
|
|
||||||
sut.urlFieldText = "https://mastodon.social"
|
sut.urlFieldText = "https://mastodon.social"
|
||||||
sut.goTapped()
|
sut.goTapped()
|
||||||
|
|
||||||
let addedIdentityID = try wait(for: addedIDRecorder.next(), timeout: 1)!
|
let addedIdentityID = try wait(for: addedIDRecorder.next(), timeout: 1)!
|
||||||
let identityRecorder = identityDatabase.identityObservation(id: addedIdentityID).record()
|
let identityRecorder = environment.identityDatabase.identityObservation(id: addedIdentityID).record()
|
||||||
let addedIdentity = try wait(for: identityRecorder.next(), timeout: 1)!
|
let addedIdentity = try wait(for: identityRecorder.next(), timeout: 1)!
|
||||||
|
|
||||||
XCTAssertEqual(addedIdentity.id, addedIdentityID)
|
XCTAssertEqual(addedIdentity.id, addedIdentityID)
|
||||||
XCTAssertEqual(addedIdentity.url, URL(string: "https://mastodon.social")!)
|
XCTAssertEqual(addedIdentity.url, URL(string: "https://mastodon.social")!)
|
||||||
|
XCTAssertEqual(environment.preferences[.recentIdentityID], addedIdentity.id)
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
try secrets.item(.clientID, forIdentityID: addedIdentityID) as String?,
|
try environment.secrets.item(.clientID, forIdentityID: addedIdentityID) as String?,
|
||||||
"AUTHORIZATION_CLIENT_ID_STUB_VALUE")
|
"AUTHORIZATION_CLIENT_ID_STUB_VALUE")
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
try secrets.item(.clientSecret, forIdentityID: addedIdentityID) as String?,
|
try environment.secrets.item(.clientSecret, forIdentityID: addedIdentityID) as String?,
|
||||||
"AUTHORIZATION_CLIENT_SECRET_STUB_VALUE")
|
"AUTHORIZATION_CLIENT_SECRET_STUB_VALUE")
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
try secrets.item(.accessToken, forIdentityID: addedIdentityID) as String?,
|
try environment.secrets.item(.accessToken, forIdentityID: addedIdentityID) as String?,
|
||||||
"ACCESS_TOKEN_STUB_VALUE")
|
"ACCESS_TOKEN_STUB_VALUE")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAddIdentityWithoutScheme() throws {
|
func testAddIdentityWithoutScheme() throws {
|
||||||
let sut = AddIdentityViewModel(
|
let environment = AppEnvironment.fresh()
|
||||||
networkClient: networkClient,
|
let sut = AddIdentityViewModel(networkClient: MastodonClient.fresh(), environment: environment)
|
||||||
identityDatabase: identityDatabase,
|
|
||||||
secrets: secrets,
|
|
||||||
webAuthenticationSessionType: SuccessfulStubbingWebAuthenticationSession.self)
|
|
||||||
let addedIDRecorder = sut.$addedIdentityID.record()
|
let addedIDRecorder = sut.$addedIdentityID.record()
|
||||||
_ = try wait(for: addedIDRecorder.next(), timeout: 1)
|
XCTAssertNil(try wait(for: addedIDRecorder.next(), timeout: 1))
|
||||||
|
|
||||||
sut.urlFieldText = "mastodon.social"
|
sut.urlFieldText = "mastodon.social"
|
||||||
sut.goTapped()
|
sut.goTapped()
|
||||||
|
|
||||||
let addedIdentityID = try wait(for: addedIDRecorder.next(), timeout: 1)!
|
let addedIdentityID = try wait(for: addedIDRecorder.next(), timeout: 1)!
|
||||||
let identityRecorder = identityDatabase.identityObservation(id: addedIdentityID).record()
|
let identityRecorder = environment.identityDatabase.identityObservation(id: addedIdentityID).record()
|
||||||
let addedIdentity = try wait(for: identityRecorder.next(), timeout: 1)!
|
let addedIdentity = try wait(for: identityRecorder.next(), timeout: 1)!
|
||||||
|
|
||||||
XCTAssertEqual(addedIdentity.url, URL(string: "https://mastodon.social")!)
|
XCTAssertEqual(addedIdentity.url, URL(string: "https://mastodon.social")!)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testInvalidURL() throws {
|
func testInvalidURL() throws {
|
||||||
let sut = AddIdentityViewModel(
|
let sut = AddIdentityViewModel(networkClient: MastodonClient.fresh(), environment: .fresh())
|
||||||
networkClient: networkClient,
|
let recorder = sut.$alertItem.record()
|
||||||
identityDatabase: identityDatabase,
|
|
||||||
secrets: secrets,
|
XCTAssertNil(try wait(for: recorder.next(), timeout: 1))
|
||||||
webAuthenticationSessionType: SuccessfulStubbingWebAuthenticationSession.self)
|
|
||||||
let recorder = sut.$alertItem.dropFirst().record()
|
|
||||||
|
|
||||||
sut.urlFieldText = "🐘.social"
|
sut.urlFieldText = "🐘.social"
|
||||||
sut.goTapped()
|
sut.goTapped()
|
||||||
|
@ -80,4 +62,17 @@ class AddIdentityViewModelTests: XCTestCase {
|
||||||
|
|
||||||
XCTAssertEqual((alertItem?.error as? URLError)?.code, URLError.badURL)
|
XCTAssertEqual((alertItem?.error as? URLError)?.code, URLError.badURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testDoesNotAlertCanceledLogin() throws {
|
||||||
|
let environment = AppEnvironment.fresh(webAuthSessionType: CanceledLoginStubbingWebAuthSession.self)
|
||||||
|
let sut = AddIdentityViewModel(networkClient: MastodonClient.fresh(), environment: environment)
|
||||||
|
let recorder = sut.$alertItem.record()
|
||||||
|
|
||||||
|
XCTAssertNil(try wait(for: recorder.next(), timeout: 1))
|
||||||
|
|
||||||
|
sut.urlFieldText = "https://mastodon.social"
|
||||||
|
sut.goTapped()
|
||||||
|
|
||||||
|
try wait(for: recorder.next().inverted, timeout: 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
24
Tests/View Models/SceneViewModelTests.swift
Normal file
24
Tests/View Models/SceneViewModelTests.swift
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
import Combine
|
||||||
|
import CombineExpectations
|
||||||
|
@testable import Metatext
|
||||||
|
|
||||||
|
class SceneViewModelTests: XCTestCase {
|
||||||
|
func testAddIdentity() throws {
|
||||||
|
let sut = SceneViewModel(networkClient: .fresh(), environment: .fresh())
|
||||||
|
let identityRecorder = sut.$identity.record()
|
||||||
|
|
||||||
|
XCTAssertNil(try wait(for: identityRecorder.next(), timeout: 1))
|
||||||
|
|
||||||
|
let addIdentityViewModel = sut.addIdentityViewModel()
|
||||||
|
|
||||||
|
addIdentityViewModel.urlFieldText = "https://mastodon.social"
|
||||||
|
addIdentityViewModel.goTapped()
|
||||||
|
|
||||||
|
let identity = try wait(for: identityRecorder.next(), timeout: 1)!
|
||||||
|
|
||||||
|
XCTAssertEqual(identity.id, addIdentityViewModel.addedIdentityID)
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SidebarNavigation: View {
|
struct SidebarNavigation: View {
|
||||||
|
let identity: Identity
|
||||||
@EnvironmentObject var sceneViewModel: SceneViewModel
|
@EnvironmentObject var sceneViewModel: SceneViewModel
|
||||||
|
|
||||||
var sidebar: some View {
|
var sidebar: some View {
|
||||||
|
@ -40,8 +41,11 @@ private extension SidebarNavigation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
struct SidebarNavigation_Previews: PreviewProvider {
|
struct SidebarNavigation_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
SidebarNavigation()
|
SidebarNavigation(identity: .development)
|
||||||
|
.environmentObject(SceneViewModel.development)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
Loading…
Reference in a new issue