Update oauth scopes when enabling push notifications

Closes #467
This commit is contained in:
Shadowfacts 2024-04-09 19:05:31 -04:00
parent 9353bbb56c
commit ff11835333
9 changed files with 172 additions and 59 deletions

View File

@ -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
}
} }

View File

@ -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
}
} }

View File

@ -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 */,

View File

@ -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
}
}

View File

@ -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

View File

@ -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()
})
} }
} }

View File

@ -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)

View File

@ -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")
}

View File

@ -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 {