forked from shadowfacts/Tusker
parent
9353bbb56c
commit
ff11835333
|
@ -13,8 +13,9 @@ public struct UserAccountInfo: Equatable, Hashable, Identifiable, Sendable {
|
||||||
public let instanceURL: URL
|
public let instanceURL: URL
|
||||||
public let clientID: String
|
public let clientID: String
|
||||||
public let clientSecret: String
|
public let clientSecret: String
|
||||||
public private(set) var username: String!
|
public let username: String!
|
||||||
public let accessToken: String
|
public internal(set) var accessToken: String
|
||||||
|
public internal(set) var scopes: [String]?
|
||||||
|
|
||||||
// Sort of hack to be able to access these from the share extension.
|
// Sort of hack to be able to access these from the share extension.
|
||||||
public internal(set) var serverDefaultLanguage: String?
|
public internal(set) var serverDefaultLanguage: String?
|
||||||
|
@ -40,16 +41,19 @@ public struct UserAccountInfo: Equatable, Hashable, Identifiable, Sendable {
|
||||||
self.instanceURL = instanceURL
|
self.instanceURL = instanceURL
|
||||||
self.clientID = clientID
|
self.clientID = clientID
|
||||||
self.clientSecret = clientSecret
|
self.clientSecret = clientSecret
|
||||||
|
self.username = nil
|
||||||
self.accessToken = accessToken
|
self.accessToken = accessToken
|
||||||
|
self.scopes = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
init(instanceURL: URL, clientID: String, clientSecret: String, username: String? = nil, accessToken: String) {
|
init(instanceURL: URL, clientID: String, clientSecret: String, username: String? = nil, accessToken: String, scopes: [String]) {
|
||||||
self.id = UserAccountInfo.id(instanceURL: instanceURL, username: username)
|
self.id = UserAccountInfo.id(instanceURL: instanceURL, username: username)
|
||||||
self.instanceURL = instanceURL
|
self.instanceURL = instanceURL
|
||||||
self.clientID = clientID
|
self.clientID = clientID
|
||||||
self.clientSecret = clientSecret
|
self.clientSecret = clientSecret
|
||||||
self.username = username
|
self.username = username
|
||||||
self.accessToken = accessToken
|
self.accessToken = accessToken
|
||||||
|
self.scopes = scopes
|
||||||
}
|
}
|
||||||
|
|
||||||
init?(userDefaultsDict dict: [String: Any]) {
|
init?(userDefaultsDict dict: [String: Any]) {
|
||||||
|
@ -67,6 +71,7 @@ public struct UserAccountInfo: Equatable, Hashable, Identifiable, Sendable {
|
||||||
self.clientSecret = secret
|
self.clientSecret = secret
|
||||||
self.username = dict["username"] as? String
|
self.username = dict["username"] as? String
|
||||||
self.accessToken = accessToken
|
self.accessToken = accessToken
|
||||||
|
self.scopes = dict["scopes"] as? [String]
|
||||||
self.serverDefaultLanguage = dict["serverDefaultLanguage"] as? String
|
self.serverDefaultLanguage = dict["serverDefaultLanguage"] as? String
|
||||||
self.serverDefaultVisibility = dict["serverDefaultVisibility"] as? String
|
self.serverDefaultVisibility = dict["serverDefaultVisibility"] as? String
|
||||||
self.serverDefaultFederation = dict["serverDefaultFederation"] as? Bool
|
self.serverDefaultFederation = dict["serverDefaultFederation"] as? Bool
|
||||||
|
@ -83,6 +88,9 @@ public struct UserAccountInfo: Equatable, Hashable, Identifiable, Sendable {
|
||||||
if let username {
|
if let username {
|
||||||
dict["username"] = username
|
dict["username"] = username
|
||||||
}
|
}
|
||||||
|
if let scopes {
|
||||||
|
dict["scopes"] = scopes
|
||||||
|
}
|
||||||
if let serverDefaultLanguage {
|
if let serverDefaultLanguage {
|
||||||
dict["serverDefaultLanguage"] = serverDefaultLanguage
|
dict["serverDefaultLanguage"] = serverDefaultLanguage
|
||||||
}
|
}
|
||||||
|
@ -100,12 +108,4 @@ public struct UserAccountInfo: Equatable, Hashable, Identifiable, Sendable {
|
||||||
// slashes are not allowed in the persistent store coordinator name
|
// slashes are not allowed in the persistent store coordinator name
|
||||||
id.replacingOccurrences(of: "/", with: "_")
|
id.replacingOccurrences(of: "/", with: "_")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
|
||||||
hasher.combine(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func ==(lhs: UserAccountInfo, rhs: UserAccountInfo) -> Bool {
|
|
||||||
return lhs.id == rhs.id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,9 @@ public class UserAccountsManager: ObservableObject {
|
||||||
clientID: "client_id",
|
clientID: "client_id",
|
||||||
clientSecret: "client_secret",
|
clientSecret: "client_secret",
|
||||||
username: "admin",
|
username: "admin",
|
||||||
accessToken: "access_token")
|
accessToken: "access_token",
|
||||||
|
scopes: []
|
||||||
|
)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -38,7 +40,7 @@ public class UserAccountsManager: ObservableObject {
|
||||||
private let accountsKey = "accounts"
|
private let accountsKey = "accounts"
|
||||||
public private(set) var accounts: [UserAccountInfo] {
|
public 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: Any]] {
|
||||||
return array.compactMap(UserAccountInfo.init(userDefaultsDict:))
|
return array.compactMap(UserAccountInfo.init(userDefaultsDict:))
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
|
@ -101,12 +103,12 @@ public class UserAccountsManager: ObservableObject {
|
||||||
return !accounts.isEmpty
|
return !accounts.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
public func addAccount(instanceURL url: URL, clientID: String, clientSecret secret: String, username: String?, accessToken: String) -> UserAccountInfo {
|
public func addAccount(instanceURL url: URL, clientID: String, clientSecret secret: String, username: String?, accessToken: String, scopes: [String]) -> UserAccountInfo {
|
||||||
var accounts = self.accounts
|
var accounts = self.accounts
|
||||||
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 info = UserAccountInfo(instanceURL: url, clientID: clientID, clientSecret: secret, username: username, accessToken: accessToken)
|
let info = UserAccountInfo(instanceURL: url, clientID: clientID, clientSecret: secret, username: username, accessToken: accessToken, scopes: scopes)
|
||||||
accounts.append(info)
|
accounts.append(info)
|
||||||
self.accounts = accounts
|
self.accounts = accounts
|
||||||
return info
|
return info
|
||||||
|
@ -145,6 +147,16 @@ public class UserAccountsManager: ObservableObject {
|
||||||
account.serverDefaultFederation = defaultFederation
|
account.serverDefaultFederation = defaultFederation
|
||||||
accounts[index] = account
|
accounts[index] = account
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func updateAccessToken(_ account: UserAccountInfo, token: String, scopes: [String]) {
|
||||||
|
guard let index = accounts.firstIndex(where: { $0.id == account.id }) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var account = account
|
||||||
|
account.accessToken = token
|
||||||
|
account.scopes = scopes
|
||||||
|
accounts[index] = account
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -94,6 +94,7 @@
|
||||||
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; };
|
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; };
|
||||||
D630C3C82BC43AFD00208903 /* PushNotifications in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3C72BC43AFD00208903 /* PushNotifications */; };
|
D630C3C82BC43AFD00208903 /* PushNotifications in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3C72BC43AFD00208903 /* PushNotifications */; };
|
||||||
D630C3CA2BC59FF500208903 /* MastodonController+Push.swift in Sources */ = {isa = PBXBuildFile; fileRef = D630C3C92BC59FF500208903 /* MastodonController+Push.swift */; };
|
D630C3CA2BC59FF500208903 /* MastodonController+Push.swift in Sources */ = {isa = PBXBuildFile; fileRef = D630C3C92BC59FF500208903 /* MastodonController+Push.swift */; };
|
||||||
|
D630C3CC2BC5FD4600208903 /* GetAuthorizationTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D630C3CB2BC5FD4600208903 /* GetAuthorizationTokenService.swift */; };
|
||||||
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6311C4F25B3765B00B27539 /* ImageDataCache.swift */; };
|
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6311C4F25B3765B00B27539 /* ImageDataCache.swift */; };
|
||||||
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; };
|
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; };
|
||||||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
|
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
|
||||||
|
@ -499,6 +500,7 @@
|
||||||
D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMetaIndicatorsView.swift; sourceTree = "<group>"; };
|
D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMetaIndicatorsView.swift; sourceTree = "<group>"; };
|
||||||
D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = "<group>"; };
|
D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = "<group>"; };
|
||||||
D630C3C92BC59FF500208903 /* MastodonController+Push.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonController+Push.swift"; sourceTree = "<group>"; };
|
D630C3C92BC59FF500208903 /* MastodonController+Push.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonController+Push.swift"; sourceTree = "<group>"; };
|
||||||
|
D630C3CB2BC5FD4600208903 /* GetAuthorizationTokenService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetAuthorizationTokenService.swift; sourceTree = "<group>"; };
|
||||||
D6311C4F25B3765B00B27539 /* ImageDataCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataCache.swift; sourceTree = "<group>"; };
|
D6311C4F25B3765B00B27539 /* ImageDataCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataCache.swift; sourceTree = "<group>"; };
|
||||||
D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = "<group>"; };
|
D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = "<group>"; };
|
||||||
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
|
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -1668,6 +1670,7 @@
|
||||||
D65B4B6529773AE600DABDFB /* DeleteStatusService.swift */,
|
D65B4B6529773AE600DABDFB /* DeleteStatusService.swift */,
|
||||||
D6DD8FFC298495A8002AD3FD /* LogoutService.swift */,
|
D6DD8FFC298495A8002AD3FD /* LogoutService.swift */,
|
||||||
D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */,
|
D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */,
|
||||||
|
D630C3CB2BC5FD4600208903 /* GetAuthorizationTokenService.swift */,
|
||||||
);
|
);
|
||||||
path = API;
|
path = API;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2064,6 +2067,7 @@
|
||||||
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
|
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
|
||||||
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameView.swift in Sources */,
|
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameView.swift in Sources */,
|
||||||
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */,
|
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */,
|
||||||
|
D630C3CC2BC5FD4600208903 /* GetAuthorizationTokenService.swift in Sources */,
|
||||||
D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */,
|
D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */,
|
||||||
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */,
|
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */,
|
||||||
D6934F3A2BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift in Sources */,
|
D6934F3A2BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift in Sources */,
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
//
|
||||||
|
// GetAuthorizationTokenService.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/9/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AuthenticationServices
|
||||||
|
|
||||||
|
class GetAuthorizationTokenService {
|
||||||
|
let instanceURL: URL
|
||||||
|
let clientID: String
|
||||||
|
let presentationContextProvider: ASWebAuthenticationPresentationContextProviding
|
||||||
|
|
||||||
|
init(instanceURL: URL, clientID: String, presentationContextProvider: ASWebAuthenticationPresentationContextProviding) {
|
||||||
|
self.instanceURL = instanceURL
|
||||||
|
self.clientID = clientID
|
||||||
|
self.presentationContextProvider = presentationContextProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func run() async throws -> String {
|
||||||
|
var components = URLComponents(url: instanceURL, resolvingAgainstBaseURL: false)!
|
||||||
|
components.path = "/oauth/authorize"
|
||||||
|
components.queryItems = [
|
||||||
|
URLQueryItem(name: "client_id", value: clientID),
|
||||||
|
URLQueryItem(name: "response_type", value: "code"),
|
||||||
|
URLQueryItem(name: "scope", value: MastodonController.oauthScopes.map(\.rawValue).joined(separator: " ")),
|
||||||
|
URLQueryItem(name: "redirect_uri", value: "tusker://oauth")
|
||||||
|
]
|
||||||
|
let authorizeURL = components.url!
|
||||||
|
|
||||||
|
return try await withCheckedThrowingContinuation({ continuation in
|
||||||
|
let authenticationSession = ASWebAuthenticationSession(url: authorizeURL, callbackURLScheme: "tusker", completionHandler: { url, error in
|
||||||
|
if let error = error {
|
||||||
|
if (error as? ASWebAuthenticationSessionError)?.code == .canceledLogin {
|
||||||
|
continuation.resume(throwing: Error.cancelled)
|
||||||
|
} else {
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
}
|
||||||
|
} else if let url = url,
|
||||||
|
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||||
|
let item = components.queryItems?.first(where: { $0.name == "code" }),
|
||||||
|
let code = item.value {
|
||||||
|
continuation.resume(returning: code)
|
||||||
|
} else {
|
||||||
|
continuation.resume(throwing: Error.noAuthorizationCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Prefer ephemeral sessions to make it easier to sign into multiple accounts on the same instance.
|
||||||
|
authenticationSession.prefersEphemeralWebBrowserSession = true
|
||||||
|
authenticationSession.presentationContextProvider = presentationContextProvider
|
||||||
|
authenticationSession.start()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Error: Swift.Error {
|
||||||
|
case cancelled
|
||||||
|
case noAuthorizationCode
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,22 +24,22 @@ final class MastodonController: ObservableObject, Sendable {
|
||||||
static let oauthScopes = [Scope.read, .write, .follow, .push]
|
static let oauthScopes = [Scope.read, .write, .follow, .push]
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
static private(set) var all = [UserAccountInfo: MastodonController]()
|
static private(set) var all = [String: MastodonController]()
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
static func getForAccount(_ account: UserAccountInfo) -> MastodonController {
|
static func getForAccount(_ account: UserAccountInfo) -> MastodonController {
|
||||||
if let controller = all[account] {
|
if let controller = all[account.id] {
|
||||||
return controller
|
return controller
|
||||||
} else {
|
} else {
|
||||||
let controller = MastodonController(instanceURL: account.instanceURL, accountInfo: account)
|
let controller = MastodonController(instanceURL: account.instanceURL, accountInfo: account)
|
||||||
all[account] = controller
|
all[account.id] = controller
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
static func removeForAccount(_ account: UserAccountInfo) {
|
static func removeForAccount(_ account: UserAccountInfo) {
|
||||||
all.removeValue(forKey: account)
|
all.removeValue(forKey: account.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|
|
@ -158,7 +158,14 @@ class OnboardingViewController: UINavigationController {
|
||||||
throw Error.gettingOwnAccount(error)
|
throw Error.gettingOwnAccount(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
let accountInfo = UserAccountsManager.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: ownAccount.username, accessToken: accessToken)
|
let accountInfo = UserAccountsManager.shared.addAccount(
|
||||||
|
instanceURL: instanceURL,
|
||||||
|
clientID: clientID,
|
||||||
|
clientSecret: clientSecret,
|
||||||
|
username: ownAccount.username,
|
||||||
|
accessToken: accessToken,
|
||||||
|
scopes: MastodonController.oauthScopes.map(\.rawValue)
|
||||||
|
)
|
||||||
self.onboardingDelegate?.didFinishOnboarding(account: accountInfo)
|
self.onboardingDelegate?.didFinishOnboarding(account: accountInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,38 +184,19 @@ class OnboardingViewController: UINavigationController {
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func getAuthorizationCode(instanceURL: URL, clientID: String) async throws -> String {
|
private func getAuthorizationCode(instanceURL: URL, clientID: String) async throws -> String {
|
||||||
var components = URLComponents(url: instanceURL, resolvingAgainstBaseURL: false)!
|
do {
|
||||||
components.path = "/oauth/authorize"
|
let service = GetAuthorizationTokenService(instanceURL: instanceURL, clientID: clientID, presentationContextProvider: self)
|
||||||
components.queryItems = [
|
return try await service.run()
|
||||||
URLQueryItem(name: "client_id", value: clientID),
|
} catch let error as GetAuthorizationTokenService.Error {
|
||||||
URLQueryItem(name: "response_type", value: "code"),
|
switch error {
|
||||||
URLQueryItem(name: "scope", value: MastodonController.oauthScopes.map(\.rawValue).joined(separator: " ")),
|
case .cancelled:
|
||||||
URLQueryItem(name: "redirect_uri", value: "tusker://oauth")
|
throw Error.cancelled
|
||||||
]
|
case .noAuthorizationCode:
|
||||||
let authorizeURL = components.url!
|
throw Error.noAuthorizationCode
|
||||||
|
}
|
||||||
return try await withCheckedThrowingContinuation({ continuation in
|
} catch {
|
||||||
self.authenticationSession = ASWebAuthenticationSession(url: authorizeURL, callbackURLScheme: "tusker", completionHandler: { url, error in
|
throw Error.authenticationSessionError(error)
|
||||||
if let error = error {
|
}
|
||||||
if (error as? ASWebAuthenticationSessionError)?.code == .canceledLogin {
|
|
||||||
continuation.resume(throwing: Error.cancelled)
|
|
||||||
} else {
|
|
||||||
continuation.resume(throwing: Error.authenticationSessionError(error))
|
|
||||||
}
|
|
||||||
} else if let url = url,
|
|
||||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
|
||||||
let item = components.queryItems?.first(where: { $0.name == "code" }),
|
|
||||||
let code = item.value {
|
|
||||||
continuation.resume(returning: code)
|
|
||||||
} else {
|
|
||||||
continuation.resume(throwing: Error.noAuthorizationCode)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// Prefer ephemeral sessions to make it easier to sign into multiple accounts on the same instance.
|
|
||||||
self.authenticationSession!.prefersEphemeralWebBrowserSession = true
|
|
||||||
self.authenticationSession!.presentationContextProvider = self
|
|
||||||
self.authenticationSession!.start()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ struct PushInstanceSettingsView: View {
|
||||||
@State private var mode: AsyncToggle.Mode
|
@State private var mode: AsyncToggle.Mode
|
||||||
@State private var error: Error?
|
@State private var error: Error?
|
||||||
@State private var subscription: PushNotifications.PushSubscription?
|
@State private var subscription: PushNotifications.PushSubscription?
|
||||||
|
@State private var showReLoginRequiredAlert = false
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
init(account: UserAccountInfo, pushProxyRegistration: PushProxyRegistration) {
|
init(account: UserAccountInfo, pushProxyRegistration: PushProxyRegistration) {
|
||||||
|
@ -43,12 +44,20 @@ struct PushInstanceSettingsView: View {
|
||||||
} message: { data in
|
} message: { data in
|
||||||
Text(data.localizedDescription)
|
Text(data.localizedDescription)
|
||||||
}
|
}
|
||||||
|
.alert("Re-Login Required", isPresented: $showReLoginRequiredAlert) {
|
||||||
|
Button("Cancel", role: .cancel) {}
|
||||||
|
Button("Login") {
|
||||||
|
NotificationCenter.default.post(name: .reLogInRequired, object: account)
|
||||||
|
}
|
||||||
|
} message: {
|
||||||
|
Text("You must grant permission on \(account.instanceURL.host!) to turn on push notifications.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateNotificationsEnabled(enabled: Bool) async -> Bool {
|
private func updateNotificationsEnabled(enabled: Bool) async -> Bool {
|
||||||
if enabled {
|
if enabled {
|
||||||
do {
|
do {
|
||||||
try await enableNotifications()
|
return try await enableNotifications()
|
||||||
} catch {
|
} catch {
|
||||||
PushManager.logger.error("Error creating instance subscription: \(String(describing: error))")
|
PushManager.logger.error("Error creating instance subscription: \(String(describing: error))")
|
||||||
self.error = .enabling(error)
|
self.error = .enabling(error)
|
||||||
|
@ -66,13 +75,19 @@ struct PushInstanceSettingsView: View {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func enableNotifications() async throws {
|
private func enableNotifications() async throws -> Bool {
|
||||||
|
guard account.scopes?.contains(Scope.push.rawValue) == true else {
|
||||||
|
showReLoginRequiredAlert = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
let subscription = try await PushManager.shared.createSubscription(account: account)
|
let subscription = try await PushManager.shared.createSubscription(account: account)
|
||||||
let mastodonController = await MastodonController.getForAccount(account)
|
let mastodonController = await MastodonController.getForAccount(account)
|
||||||
do {
|
do {
|
||||||
let result = try await mastodonController.createPushSubscription(subscription: subscription)
|
let result = try await mastodonController.createPushSubscription(subscription: subscription)
|
||||||
PushManager.logger.debug("Push subscription \(result.id) created on \(account.instanceURL)")
|
PushManager.logger.debug("Push subscription \(result.id) created on \(account.instanceURL)")
|
||||||
self.subscription = subscription
|
self.subscription = subscription
|
||||||
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
// if creation failed, remove the subscription locally as well
|
// if creation failed, remove the subscription locally as well
|
||||||
await PushManager.shared.removeSubscription(account: account)
|
await PushManager.shared.removeSubscription(account: account)
|
||||||
|
|
|
@ -10,6 +10,8 @@ import UIKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UserAccounts
|
import UserAccounts
|
||||||
import SafariServices
|
import SafariServices
|
||||||
|
import AuthenticationServices
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
class PreferencesNavigationController: UINavigationController {
|
class PreferencesNavigationController: UINavigationController {
|
||||||
|
|
||||||
|
@ -37,6 +39,7 @@ class PreferencesNavigationController: UINavigationController {
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(activateAccount(_:)), name: .activateAccount, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(activateAccount(_:)), name: .activateAccount, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(userLoggedOut), name: .userLoggedOut, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(userLoggedOut), name: .userLoggedOut, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(showMastodonSettings), name: .showMastodonSettings, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(showMastodonSettings), name: .showMastodonSettings, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(reLogInToAccount), name: .reLogInRequired, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
@ -63,7 +66,7 @@ class PreferencesNavigationController: UINavigationController {
|
||||||
dismiss(animated: true) // dismisses instance selector
|
dismiss(animated: true) // dismisses instance selector
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func activateAccount(_ notification: Notification) {
|
@objc func activateAccount(_ notification: Foundation.Notification) {
|
||||||
// TODO: this is a temporary measure
|
// TODO: this is a temporary measure
|
||||||
// when switching accounts shortly after adding a new one, there is an old instance of PreferncesNavigationController still around
|
// when switching accounts shortly after adding a new one, there is an old instance of PreferncesNavigationController still around
|
||||||
// which tries to handle the notification but is unable to because it no longer is in a window (and therefore doesn't have a scene delegate)
|
// which tries to handle the notification but is unable to because it no longer is in a window (and therefore doesn't have a scene delegate)
|
||||||
|
@ -105,6 +108,27 @@ class PreferencesNavigationController: UINavigationController {
|
||||||
let vc = SFSafariViewController(url: components.url!)
|
let vc = SFSafariViewController(url: components.url!)
|
||||||
present(vc, animated: true)
|
present(vc, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func reLogInToAccount(_ notification: Foundation.Notification) {
|
||||||
|
guard let account = notification.object as? UserAccountInfo else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let service = GetAuthorizationTokenService(instanceURL: account.instanceURL, clientID: account.clientID, presentationContextProvider: self)
|
||||||
|
let code = try await service.run()
|
||||||
|
let mastodonController = MastodonController.getForAccount(account)
|
||||||
|
let token = try await mastodonController.authorize(authorizationCode: code)
|
||||||
|
UserAccountsManager.shared.updateAccessToken(account, token: token, scopes: MastodonController.oauthScopes.map(\.rawValue))
|
||||||
|
// try to revoke the old token
|
||||||
|
try? await Client(baseURL: account.instanceURL, accessToken: account.accessToken, clientID: account.clientID, clientSecret: account.clientSecret).revokeAccessToken()
|
||||||
|
} catch {
|
||||||
|
let alert = UIAlertController(title: "Error Updating Permissions", message: error.localizedDescription, preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||||
|
self.present(alert, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,3 +148,14 @@ extension PreferencesNavigationController: OnboardingViewControllerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension PreferencesNavigationController: ASWebAuthenticationPresentationContextProviding {
|
||||||
|
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
||||||
|
view.window!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Foundation.Notification.Name {
|
||||||
|
static let showMastodonSettings = Notification.Name("Tusker.showMastodonSettings")
|
||||||
|
static let reLogInRequired = Notification.Name("Tusker.reLogInRequired")
|
||||||
|
}
|
||||||
|
|
|
@ -143,10 +143,6 @@ struct PreferencesView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static let showMastodonSettings = Notification.Name("Tusker.showMastodonSettings")
|
|
||||||
}
|
|
||||||
|
|
||||||
//#if DEBUG
|
//#if DEBUG
|
||||||
//struct PreferencesView_Previews : PreviewProvider {
|
//struct PreferencesView_Previews : PreviewProvider {
|
||||||
// static var previews: some View {
|
// static var previews: some View {
|
||||||
|
|
Loading…
Reference in New Issue