forked from shadowfacts/Tusker
parent
bbfb3b0a7a
commit
35d21fb725
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
class LocalData: ObservableObject {
|
class LocalData: ObservableObject {
|
||||||
|
|
||||||
|
@ -22,7 +23,6 @@ class LocalData: ObservableObject {
|
||||||
if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING_LOGIN") {
|
if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING_LOGIN") {
|
||||||
accounts = [
|
accounts = [
|
||||||
UserAccountInfo(
|
UserAccountInfo(
|
||||||
id: UUID().uuidString,
|
|
||||||
instanceURL: URL(string: "http://localhost:8080")!,
|
instanceURL: URL(string: "http://localhost:8080")!,
|
||||||
clientID: "client_id",
|
clientID: "client_id",
|
||||||
clientSecret: "client_secret",
|
clientSecret: "client_secret",
|
||||||
|
@ -33,23 +33,15 @@ class LocalData: ObservableObject {
|
||||||
} else {
|
} else {
|
||||||
defaults = UserDefaults(suiteName: "group.space.vaccor.Tusker")!
|
defaults = UserDefaults(suiteName: "group.space.vaccor.Tusker")!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
migrateAccountIDsIfNecessary()
|
||||||
}
|
}
|
||||||
|
|
||||||
private let accountsKey = "accounts"
|
private let accountsKey = "accounts"
|
||||||
var accounts: [UserAccountInfo] {
|
private(set) var accounts: [UserAccountInfo] {
|
||||||
get {
|
get {
|
||||||
if let array = defaults.array(forKey: accountsKey) as? [[String: String]] {
|
if let array = defaults.array(forKey: accountsKey) as? [[String: String]] {
|
||||||
return array.compactMap { (info) in
|
return array.compactMap(UserAccountInfo.init(userDefaultsDict:))
|
||||||
guard let id = info["id"],
|
|
||||||
let instanceURL = info["instanceURL"],
|
|
||||||
let url = URL(string: instanceURL),
|
|
||||||
let clientId = info["clientID"],
|
|
||||||
let secret = info["clientSecret"],
|
|
||||||
let accessToken = info["accessToken"] else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return UserAccountInfo(id: id, instanceURL: url, clientID: clientId, clientSecret: secret, username: info["username"], accessToken: accessToken)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -84,6 +76,41 @@ class LocalData: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let usesAccountIDHashesKey = "usesAccountIDHashes"
|
||||||
|
private var usesAccountIDHashes: Bool {
|
||||||
|
get {
|
||||||
|
return defaults.bool(forKey: usesAccountIDHashesKey)
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
return defaults.set(newValue, forKey: usesAccountIDHashesKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func migrateAccountIDsIfNecessary() {
|
||||||
|
if usesAccountIDHashes {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let mostRecentAccount = getMostRecentAccount() {
|
||||||
|
let hashedMostRecentID = UserAccountInfo.id(instanceURL: mostRecentAccount.instanceURL, username: mostRecentAccount.username)
|
||||||
|
mostRecentAccountID = hashedMostRecentID
|
||||||
|
}
|
||||||
|
if let array = defaults.array(forKey: accountsKey) as? [[String: String]] {
|
||||||
|
accounts = array.compactMap {
|
||||||
|
guard let urlString = $0["instanceURL"],
|
||||||
|
let url = URL(string: urlString),
|
||||||
|
let username = $0["username"] else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var dict = $0
|
||||||
|
dict["id"] = UserAccountInfo.id(instanceURL: url, username: username)
|
||||||
|
return UserAccountInfo(userDefaultsDict: dict)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usesAccountIDHashes = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Account Management
|
||||||
|
|
||||||
var onboardingComplete: Bool {
|
var onboardingComplete: Bool {
|
||||||
return !accounts.isEmpty
|
return !accounts.isEmpty
|
||||||
}
|
}
|
||||||
|
@ -93,20 +120,12 @@ class LocalData: ObservableObject {
|
||||||
if let index = accounts.firstIndex(where: { $0.instanceURL == url && $0.username == username }) {
|
if let index = accounts.firstIndex(where: { $0.instanceURL == url && $0.username == username }) {
|
||||||
accounts.remove(at: index)
|
accounts.remove(at: index)
|
||||||
}
|
}
|
||||||
let id = UUID().uuidString
|
let info = UserAccountInfo(instanceURL: url, clientID: clientID, clientSecret: secret, username: username, accessToken: accessToken)
|
||||||
let info = UserAccountInfo(id: id, instanceURL: url, clientID: clientID, clientSecret: secret, username: username, accessToken: accessToken)
|
|
||||||
accounts.append(info)
|
accounts.append(info)
|
||||||
self.accounts = accounts
|
self.accounts = accounts
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
func setUsername(for info: UserAccountInfo, username: String) {
|
|
||||||
var info = info
|
|
||||||
info.username = username
|
|
||||||
removeAccount(info)
|
|
||||||
accounts.append(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeAccount(_ info: UserAccountInfo) {
|
func removeAccount(_ info: UserAccountInfo) {
|
||||||
accounts.removeAll(where: { $0.id == info.id })
|
accounts.removeAll(where: { $0.id == info.id })
|
||||||
}
|
}
|
||||||
|
@ -138,9 +157,57 @@ extension LocalData {
|
||||||
let instanceURL: URL
|
let instanceURL: URL
|
||||||
let clientID: String
|
let clientID: String
|
||||||
let clientSecret: String
|
let clientSecret: String
|
||||||
fileprivate(set) var username: String!
|
private(set) var username: String!
|
||||||
let accessToken: String
|
let accessToken: String
|
||||||
|
|
||||||
|
fileprivate static let tempAccountID = "temp"
|
||||||
|
|
||||||
|
fileprivate static func id(instanceURL: URL, username: String?) -> String {
|
||||||
|
// We hash the instance host and username to form the account ID
|
||||||
|
// so that account IDs will match across devices, allowing for data syncing and handoff.
|
||||||
|
var hasher = SHA256()
|
||||||
|
hasher.update(data: instanceURL.host!.data(using: .utf8)!)
|
||||||
|
if let username {
|
||||||
|
hasher.update(data: username.data(using: .utf8)!)
|
||||||
|
}
|
||||||
|
return Data(hasher.finalize()).base64EncodedString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only to be used for temporary MastodonController needed to fetch own account info and create final UserAccountInfo with real username
|
||||||
|
init(tempInstanceURL instanceURL: URL, clientID: String, clientSecret: String, accessToken: String) {
|
||||||
|
self.id = UserAccountInfo.tempAccountID
|
||||||
|
self.instanceURL = instanceURL
|
||||||
|
self.clientID = clientID
|
||||||
|
self.clientSecret = clientSecret
|
||||||
|
self.accessToken = accessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate init(instanceURL: URL, clientID: String, clientSecret: String, username: String? = nil, accessToken: String) {
|
||||||
|
self.id = UserAccountInfo.id(instanceURL: instanceURL, username: username)
|
||||||
|
self.instanceURL = instanceURL
|
||||||
|
self.clientID = clientID
|
||||||
|
self.clientSecret = clientSecret
|
||||||
|
self.username = username
|
||||||
|
self.accessToken = accessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate init?(userDefaultsDict dict: [String: String]) {
|
||||||
|
guard let id = dict["id"],
|
||||||
|
let instanceURL = dict["instanceURL"],
|
||||||
|
let url = URL(string: instanceURL),
|
||||||
|
let clientID = dict["clientID"],
|
||||||
|
let secret = dict["clientSecret"],
|
||||||
|
let accessToken = dict["accessToken"] else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.id = id
|
||||||
|
self.instanceURL = url
|
||||||
|
self.clientID = clientID
|
||||||
|
self.clientSecret = secret
|
||||||
|
self.username = dict["username"]
|
||||||
|
self.accessToken = accessToken
|
||||||
|
}
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ class OnboardingViewController: UINavigationController {
|
||||||
}
|
}
|
||||||
|
|
||||||
// construct a temporary UserAccountInfo instance for the MastodonController to use to fetch its own account
|
// construct a temporary UserAccountInfo instance for the MastodonController to use to fetch its own account
|
||||||
let tempAccountInfo = LocalData.UserAccountInfo(id: "temp", instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: nil, accessToken: accessToken)
|
let tempAccountInfo = LocalData.UserAccountInfo(tempInstanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, accessToken: accessToken)
|
||||||
mastodonController.accountInfo = tempAccountInfo
|
mastodonController.accountInfo = tempAccountInfo
|
||||||
|
|
||||||
let ownAccount: Account
|
let ownAccount: Account
|
||||||
|
|
|
@ -48,7 +48,7 @@ struct PreferencesView: View {
|
||||||
indices.remove(index)
|
indices.remove(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
localData.accounts.remove(atOffsets: indices)
|
indices.forEach { localData.removeAccount(localData.accounts[$0]) }
|
||||||
|
|
||||||
if logoutFromCurrent {
|
if logoutFromCurrent {
|
||||||
self.logoutPressed()
|
self.logoutPressed()
|
||||||
|
|
Loading…
Reference in New Issue