forked from shadowfacts/Tusker
Remove the need to register with the push proxy
This commit is contained in:
parent
2e31d34e9d
commit
19ca930ee8
|
@ -32,7 +32,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||||
}
|
}
|
||||||
|
|
||||||
guard request.content.userInfo["v"] as? Int == 1,
|
guard request.content.userInfo["v"] as? Int == 1,
|
||||||
let accountID = request.content.userInfo["ctx"] as? String,
|
let accountID = (request.content.userInfo["ctx"] as? String).flatMap(\.removingPercentEncoding),
|
||||||
let account = UserAccountsManager.shared.getAccount(id: accountID),
|
let account = UserAccountsManager.shared.getAccount(id: accountID),
|
||||||
let subscription = getSubscription(account: account),
|
let subscription = getSubscription(account: account),
|
||||||
let encryptedBody = (request.content.userInfo["data"] as? String).flatMap({ Data(base64Encoded: $0) }),
|
let encryptedBody = (request.content.userInfo["data"] as? String).flatMap({ Data(base64Encoded: $0) }),
|
||||||
|
|
|
@ -13,15 +13,11 @@ class DisabledPushManager: _PushManager {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
var proxyRegistration: PushProxyRegistration? {
|
|
||||||
nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var subscriptions: [PushSubscription] {
|
var subscriptions: [PushSubscription] {
|
||||||
[]
|
[]
|
||||||
}
|
}
|
||||||
|
|
||||||
func createSubscription(account: UserAccountInfo) throws -> PushSubscription {
|
func createSubscription(account: UserAccountInfo) async throws -> PushSubscription {
|
||||||
throw Disabled()
|
throw Disabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,14 +31,6 @@ class DisabledPushManager: _PushManager {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func register() async throws -> PushProxyRegistration {
|
|
||||||
throw Disabled()
|
|
||||||
}
|
|
||||||
|
|
||||||
func unregister() async throws {
|
|
||||||
throw Disabled()
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateIfNecessary(updateSubscription: @escaping (PushSubscription) async -> Bool) async {
|
func updateIfNecessary(updateSubscription: @escaping (PushSubscription) async -> Bool) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,16 +41,13 @@ public struct PushManager {
|
||||||
@MainActor
|
@MainActor
|
||||||
public protocol _PushManager {
|
public protocol _PushManager {
|
||||||
var enabled: Bool { get }
|
var enabled: Bool { get }
|
||||||
var proxyRegistration: PushProxyRegistration? { get }
|
|
||||||
|
|
||||||
var subscriptions: [PushSubscription] { get }
|
var subscriptions: [PushSubscription] { get }
|
||||||
func createSubscription(account: UserAccountInfo) throws -> PushSubscription
|
func createSubscription(account: UserAccountInfo) async throws -> PushSubscription
|
||||||
func removeSubscription(account: UserAccountInfo)
|
func removeSubscription(account: UserAccountInfo)
|
||||||
func updateSubscription(account: UserAccountInfo, alerts: PushSubscription.Alerts, policy: PushSubscription.Policy)
|
func updateSubscription(account: UserAccountInfo, alerts: PushSubscription.Alerts, policy: PushSubscription.Policy)
|
||||||
func pushSubscription(account: UserAccountInfo) -> PushSubscription?
|
func pushSubscription(account: UserAccountInfo) -> PushSubscription?
|
||||||
|
|
||||||
func register() async throws -> PushProxyRegistration
|
|
||||||
func unregister() async throws
|
|
||||||
func updateIfNecessary(updateSubscription: @escaping (PushSubscription) async -> Bool) async
|
func updateIfNecessary(updateSubscription: @escaping (PushSubscription) async -> Bool) async
|
||||||
|
|
||||||
func didRegisterForRemoteNotifications(deviceToken: Data)
|
func didRegisterForRemoteNotifications(deviceToken: Data)
|
||||||
|
|
|
@ -20,26 +20,13 @@ class PushManagerImpl: _PushManager {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
"development"
|
"development"
|
||||||
#else
|
#else
|
||||||
"release"
|
"production"
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var remoteNotificationsRegistrationContinuation: CheckedContinuation<Data, any Error>?
|
private var remoteNotificationsRegistrationContinuation: CheckedContinuation<Data, any Error>?
|
||||||
|
|
||||||
private let defaults = UserDefaults(suiteName: "group.space.vaccor.Tusker")!
|
private let defaults = UserDefaults(suiteName: "group.space.vaccor.Tusker")!
|
||||||
private(set) var proxyRegistration: PushProxyRegistration? {
|
|
||||||
get {
|
|
||||||
if let dict = defaults.dictionary(forKey: "PushProxyRegistration") as? [String: String],
|
|
||||||
let registration = PushProxyRegistration(defaultsDict: dict) {
|
|
||||||
return registration
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
defaults.setValue(newValue?.defaultsDict, forKey: "PushProxyRegistration")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public private(set) var subscriptions: [PushSubscription] {
|
public private(set) var subscriptions: [PushSubscription] {
|
||||||
get {
|
get {
|
||||||
if let array = defaults.array(forKey: "PushSubscriptions") as? [[String: Any]] {
|
if let array = defaults.array(forKey: "PushSubscriptions") as? [[String: Any]] {
|
||||||
|
@ -57,10 +44,7 @@ class PushManagerImpl: _PushManager {
|
||||||
self.endpoint = endpoint
|
self.endpoint = endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
func createSubscription(account: UserAccountInfo) throws -> PushSubscription {
|
func createSubscription(account: UserAccountInfo) async throws -> PushSubscription {
|
||||||
guard let proxyRegistration else {
|
|
||||||
throw CreateSubscriptionError.notRegisteredWithProxy
|
|
||||||
}
|
|
||||||
if let existing = pushSubscription(account: account) {
|
if let existing = pushSubscription(account: account) {
|
||||||
return existing
|
return existing
|
||||||
}
|
}
|
||||||
|
@ -72,9 +56,10 @@ class PushManagerImpl: _PushManager {
|
||||||
guard res == errSecSuccess else {
|
guard res == errSecSuccess else {
|
||||||
throw CreateSubscriptionError.generatingAuthSecret(res)
|
throw CreateSubscriptionError.generatingAuthSecret(res)
|
||||||
}
|
}
|
||||||
|
let token = try await getDeviceToken()
|
||||||
let subscription = PushSubscription(
|
let subscription = PushSubscription(
|
||||||
accountID: account.id,
|
accountID: account.id,
|
||||||
endpoint: endpointURL(registration: proxyRegistration, accountID: account.id),
|
endpoint: endpointURL(deviceToken: token, accountID: account.id),
|
||||||
secretKey: key,
|
secretKey: key,
|
||||||
authSecret: authSecret,
|
authSecret: authSecret,
|
||||||
alerts: [],
|
alerts: [],
|
||||||
|
@ -84,10 +69,10 @@ class PushManagerImpl: _PushManager {
|
||||||
return subscription
|
return subscription
|
||||||
}
|
}
|
||||||
|
|
||||||
private func endpointURL(registration: PushProxyRegistration, accountID: String) -> URL {
|
private func endpointURL(deviceToken: Data, accountID: String) -> URL {
|
||||||
var endpoint = URLComponents(url: registration.endpoint, resolvingAgainstBaseURL: false)!
|
var endpoint = URLComponents(url: endpoint, resolvingAgainstBaseURL: false)!
|
||||||
endpoint.queryItems = endpoint.queryItems ?? []
|
let accountID = accountID.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!
|
||||||
endpoint.queryItems!.append(URLQueryItem(name: "ctx", value: accountID))
|
endpoint.path = "/push/v1/\(apnsEnvironment)/\(deviceToken.hexEncodedString())/\(accountID)"
|
||||||
return endpoint.url!
|
return endpoint.url!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,70 +94,28 @@ class PushManagerImpl: _PushManager {
|
||||||
subscriptions.first { $0.accountID == account.id }
|
subscriptions.first { $0.accountID == account.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
func register() async throws -> PushProxyRegistration {
|
|
||||||
guard remoteNotificationsRegistrationContinuation == nil else {
|
|
||||||
throw PushRegistrationError.alreadyRegistering
|
|
||||||
}
|
|
||||||
let deviceToken = try await getDeviceToken().hexEncodedString()
|
|
||||||
PushManager.logger.debug("Got device token: \(deviceToken)")
|
|
||||||
let registration: PushProxyRegistration
|
|
||||||
do {
|
|
||||||
registration = try await register(deviceToken: deviceToken)
|
|
||||||
PushManager.logger.debug("Got endpoint: \(registration.endpoint)")
|
|
||||||
} catch {
|
|
||||||
PushManager.logger.error("Proxy registration failed: \(String(describing: error))")
|
|
||||||
throw PushRegistrationError.registeringWithProxy(error)
|
|
||||||
}
|
|
||||||
proxyRegistration = registration
|
|
||||||
return registration
|
|
||||||
}
|
|
||||||
|
|
||||||
func unregister() async throws {
|
|
||||||
guard let proxyRegistration else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var url = URLComponents(url: endpoint, resolvingAgainstBaseURL: false)!
|
|
||||||
url.path = "/app/v1/registrations/\(proxyRegistration.id)"
|
|
||||||
var request = URLRequest(url: url.url!)
|
|
||||||
request.httpMethod = "DELETE"
|
|
||||||
let (data, resp) = try await URLSession.shared.data(for: request)
|
|
||||||
let status = (resp as! HTTPURLResponse).statusCode
|
|
||||||
if (200...299).contains(status) {
|
|
||||||
self.proxyRegistration = nil
|
|
||||||
PushManager.logger.debug("Unregistered from proxy")
|
|
||||||
} else {
|
|
||||||
PushManager.logger.error("Unregistering: unexpected status \(status)")
|
|
||||||
let error = (try? JSONDecoder().decode(ProxyRegistrationError.self, from: data)) ?? ProxyRegistrationError(error: "Unknown error", fields: nil)
|
|
||||||
throw PushRegistrationError.unregistering(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateIfNecessary(updateSubscription: @escaping (PushSubscription) async -> Bool) async {
|
func updateIfNecessary(updateSubscription: @escaping (PushSubscription) async -> Bool) async {
|
||||||
guard let proxyRegistration else {
|
let subscriptions = self.subscriptions
|
||||||
|
guard !subscriptions.isEmpty else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
PushManager.logger.debug("Push proxy registration: \(proxyRegistration.id, privacy: .public)")
|
|
||||||
do {
|
do {
|
||||||
let token = try await getDeviceToken().hexEncodedString()
|
let token = try await getDeviceToken()
|
||||||
guard token != proxyRegistration.deviceToken else {
|
self.subscriptions = await AsyncSequenceAdaptor(wrapping: subscriptions).map {
|
||||||
// already up-to-date, nothing to do
|
let newEndpoint = await self.endpointURL(deviceToken: token, accountID: $0.accountID)
|
||||||
return
|
guard newEndpoint != $0.endpoint else {
|
||||||
}
|
return $0
|
||||||
let newRegistration = try await update(registration: proxyRegistration, deviceToken: token)
|
}
|
||||||
self.proxyRegistration = newRegistration
|
var copy = $0
|
||||||
if proxyRegistration.endpoint != newRegistration.endpoint {
|
copy.endpoint = newEndpoint
|
||||||
self.subscriptions = await AsyncSequenceAdaptor(wrapping: self.subscriptions).map {
|
if await updateSubscription(copy) {
|
||||||
var copy = $0
|
return copy
|
||||||
copy.endpoint = await self.endpointURL(registration: newRegistration, accountID: $0.accountID)
|
} else {
|
||||||
if await updateSubscription(copy) {
|
return $0
|
||||||
return copy
|
}
|
||||||
} else {
|
}.reduce(into: [], { partialResult, el in
|
||||||
return $0
|
partialResult.append(el)
|
||||||
}
|
})
|
||||||
}.reduce(into: [], { partialResult, el in
|
|
||||||
partialResult.append(el)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
PushManager.logger.error("Failed to update push registration: \(String(describing: error), privacy: .public)")
|
PushManager.logger.error("Failed to update push registration: \(String(describing: error), privacy: .public)")
|
||||||
PushManager.captureError?(error)
|
PushManager.captureError?(error)
|
||||||
|
@ -195,47 +138,11 @@ class PushManagerImpl: _PushManager {
|
||||||
remoteNotificationsRegistrationContinuation?.resume(throwing: PushRegistrationError.registeringForRemoteNotifications(error))
|
remoteNotificationsRegistrationContinuation?.resume(throwing: PushRegistrationError.registeringForRemoteNotifications(error))
|
||||||
remoteNotificationsRegistrationContinuation = nil
|
remoteNotificationsRegistrationContinuation = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func register(deviceToken: String) async throws -> PushProxyRegistration {
|
|
||||||
var url = URLComponents(url: endpoint, resolvingAgainstBaseURL: false)!
|
|
||||||
url.path = "/app/v1/registrations"
|
|
||||||
var request = URLRequest(url: url.url!)
|
|
||||||
request.httpMethod = "POST"
|
|
||||||
request.setValue("application/json", forHTTPHeaderField: "content-type")
|
|
||||||
request.httpBody = try! JSONEncoder().encode(PushRegistrationParams(environment: apnsEnvironment, deviceToken: deviceToken, pushVersion: 1))
|
|
||||||
let (data, resp) = try await URLSession.shared.data(for: request)
|
|
||||||
let status = (resp as! HTTPURLResponse).statusCode
|
|
||||||
guard (200...299).contains(status) else {
|
|
||||||
PushManager.logger.error("Registering: unexpected status \(status)")
|
|
||||||
let error = (try? JSONDecoder().decode(ProxyRegistrationError.self, from: data)) ?? ProxyRegistrationError(error: "Unknown error", fields: [])
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
return try JSONDecoder().decode(PushProxyRegistration.self, from: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func update(registration: PushProxyRegistration, deviceToken: String) async throws -> PushProxyRegistration {
|
|
||||||
var url = URLComponents(url: endpoint, resolvingAgainstBaseURL: false)!
|
|
||||||
url.path = "/app/v1/registrations/\(registration.id)"
|
|
||||||
var request = URLRequest(url: url.url!)
|
|
||||||
request.httpMethod = "PUT"
|
|
||||||
request.setValue("application/json", forHTTPHeaderField: "content-type")
|
|
||||||
request.httpBody = try! JSONEncoder().encode(PushUpdateParams(environment: apnsEnvironment, deviceToken: deviceToken, pushVersion: 1))
|
|
||||||
let (data, resp) = try await URLSession.shared.data(for: request)
|
|
||||||
let status = (resp as! HTTPURLResponse).statusCode
|
|
||||||
guard (200...299).contains(status) else {
|
|
||||||
PushManager.logger.error("Updating: unexpected status \(status)")
|
|
||||||
let error = (try? JSONDecoder().decode(ProxyRegistrationError.self, from: data)) ?? ProxyRegistrationError(error: "Unknown error", fields: [])
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
return try JSONDecoder().decode(PushProxyRegistration.self, from: data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PushRegistrationError: LocalizedError {
|
enum PushRegistrationError: LocalizedError {
|
||||||
case alreadyRegistering
|
case alreadyRegistering
|
||||||
case registeringForRemoteNotifications(any Error)
|
case registeringForRemoteNotifications(any Error)
|
||||||
case registeringWithProxy(any Error)
|
|
||||||
case unregistering(any Error)
|
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -243,71 +150,21 @@ enum PushRegistrationError: LocalizedError {
|
||||||
"Already registering"
|
"Already registering"
|
||||||
case .registeringForRemoteNotifications(let error):
|
case .registeringForRemoteNotifications(let error):
|
||||||
"Remote notifications: \(error.localizedDescription)"
|
"Remote notifications: \(error.localizedDescription)"
|
||||||
case .registeringWithProxy(let error):
|
|
||||||
"Proxy: \(error.localizedDescription)"
|
|
||||||
case .unregistering(let error):
|
|
||||||
"Unregistering: \(error.localizedDescription)"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ProxyRegistrationError: LocalizedError, Decodable {
|
|
||||||
let error: String
|
|
||||||
let fields: [Field]?
|
|
||||||
|
|
||||||
var errorDescription: String? {
|
|
||||||
if let fields,
|
|
||||||
!fields.isEmpty {
|
|
||||||
error + ": " + fields.map { "\($0.key): \($0.reason)" }.joined(separator: ", ")
|
|
||||||
} else {
|
|
||||||
error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Field: Decodable {
|
|
||||||
let key: String
|
|
||||||
let reason: String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum CreateSubscriptionError: LocalizedError {
|
enum CreateSubscriptionError: LocalizedError {
|
||||||
case notRegisteredWithProxy
|
|
||||||
case generatingAuthSecret(OSStatus)
|
case generatingAuthSecret(OSStatus)
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .notRegisteredWithProxy:
|
|
||||||
"Not registered with proxy"
|
|
||||||
case .generatingAuthSecret(let code):
|
case .generatingAuthSecret(let code):
|
||||||
"Generating auth secret: \(code)"
|
"Generating auth secret: \(code)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct PushRegistrationParams: Encodable {
|
|
||||||
let environment: String
|
|
||||||
let deviceToken: String
|
|
||||||
let pushVersion: Int
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case environment
|
|
||||||
case deviceToken = "device_token"
|
|
||||||
case pushVersion = "push_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct PushUpdateParams: Encodable {
|
|
||||||
let environment: String
|
|
||||||
let deviceToken: String
|
|
||||||
let pushVersion: Int
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case environment
|
|
||||||
case deviceToken = "device_token"
|
|
||||||
case pushVersion = "push_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Data {
|
private extension Data {
|
||||||
func hexEncodedString() -> String {
|
func hexEncodedString() -> String {
|
||||||
String(unsafeUninitializedCapacity: count * 2) { buffer in
|
String(unsafeUninitializedCapacity: count * 2) { buffer in
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
//
|
|
||||||
// PushProxyRegistration.swift
|
|
||||||
// PushNotifications
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 4/7/24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
public struct PushProxyRegistration: Decodable {
|
|
||||||
let id: String
|
|
||||||
public let endpoint: URL
|
|
||||||
let deviceToken: String
|
|
||||||
|
|
||||||
var defaultsDict: [String: String] {
|
|
||||||
[
|
|
||||||
"id": id,
|
|
||||||
"endpoint": endpoint.absoluteString,
|
|
||||||
"deviceToken": deviceToken,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
init?(defaultsDict: [String: String]) {
|
|
||||||
guard let id = defaultsDict["id"],
|
|
||||||
let endpoint = defaultsDict["endpoint"].flatMap(URL.init(string:)),
|
|
||||||
let deviceToken = defaultsDict["deviceToken"] else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
self.id = id
|
|
||||||
self.endpoint = endpoint
|
|
||||||
self.deviceToken = deviceToken
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case id
|
|
||||||
case endpoint
|
|
||||||
case deviceToken = "device_token"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -14,127 +14,30 @@ import TuskerComponents
|
||||||
|
|
||||||
struct NotificationsPrefsView: View {
|
struct NotificationsPrefsView: View {
|
||||||
@State private var error: NotificationsSetupError?
|
@State private var error: NotificationsSetupError?
|
||||||
@State private var isSetup = AsyncToggle.Mode.off
|
|
||||||
@State private var pushProxyRegistration: PushProxyRegistration?
|
|
||||||
@ObservedObject private var userAccounts = UserAccountsManager.shared
|
@ObservedObject private var userAccounts = UserAccountsManager.shared
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
enableSection
|
Section {
|
||||||
if isSetup == .on,
|
ForEach(userAccounts.accounts) { account in
|
||||||
let pushProxyRegistration {
|
PushInstanceSettingsView(account: account)
|
||||||
accountsSection(pushProxyRegistration: pushProxyRegistration)
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
.appGroupedListBackground(container: PreferencesNavigationController.self)
|
.appGroupedListBackground(container: PreferencesNavigationController.self)
|
||||||
.navigationTitle("Notifications")
|
.navigationTitle("Notifications")
|
||||||
}
|
}
|
||||||
|
|
||||||
private var enableSection: some View {
|
|
||||||
Section {
|
|
||||||
AsyncToggle("Push Notifications", mode: $isSetup, onChange: isSetupChanged(newValue:))
|
|
||||||
}
|
|
||||||
.appGroupedListRowBackground()
|
|
||||||
.alertWithData("An Error Occurred", data: $error) { error in
|
|
||||||
Button("OK") {}
|
|
||||||
} message: { error in
|
|
||||||
Text(error.localizedDescription)
|
|
||||||
}
|
|
||||||
.task { @MainActor in
|
|
||||||
pushProxyRegistration = PushManager.shared.proxyRegistration
|
|
||||||
isSetup = pushProxyRegistration != nil ? .on : .off
|
|
||||||
if !UIApplication.shared.isRegisteredForRemoteNotifications {
|
|
||||||
_ = await registerForRemoteNotifications()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func accountsSection(pushProxyRegistration: PushProxyRegistration) -> some View {
|
|
||||||
Section {
|
|
||||||
ForEach(userAccounts.accounts) { account in
|
|
||||||
PushInstanceSettingsView(account: account, pushProxyRegistration: pushProxyRegistration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.appGroupedListRowBackground()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func isSetupChanged(newValue: Bool) async -> Bool {
|
|
||||||
if newValue {
|
|
||||||
return await startRegistration()
|
|
||||||
} else {
|
|
||||||
return await unregister()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func startRegistration() async -> Bool {
|
|
||||||
let authorized: Bool
|
|
||||||
do {
|
|
||||||
authorized = try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .providesAppNotificationSettings])
|
|
||||||
} catch {
|
|
||||||
self.error = .requestingAuthorization(error)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
guard authorized else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return await registerForRemoteNotifications()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func registerForRemoteNotifications() async -> Bool {
|
|
||||||
do {
|
|
||||||
pushProxyRegistration = try await PushManager.shared.register()
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
self.error = .registering(error)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func unregister() async -> Bool {
|
|
||||||
do {
|
|
||||||
try await PushManager.shared.unregister()
|
|
||||||
pushProxyRegistration = nil
|
|
||||||
for subscription in PushManager.shared.subscriptions {
|
|
||||||
if let account = UserAccountsManager.shared.getAccount(id: subscription.accountID) {
|
|
||||||
let mastodonController = MastodonController.getForAccount(account)
|
|
||||||
do {
|
|
||||||
try await mastodonController.deletePushSubscription()
|
|
||||||
PushManager.shared.removeSubscription(account: account)
|
|
||||||
PushManager.logger.debug("Push subscription removed on \(account.instanceURL)")
|
|
||||||
// this is a bit of a hack. the PushInstanceSettingsViews need to know to update
|
|
||||||
// their @State variables after we remove the subscription
|
|
||||||
NotificationCenter.default.post(name: .pushSubscriptionRemoved, object: account.id)
|
|
||||||
} catch {
|
|
||||||
PushManager.logger.error("Erroring removing push subscription: \(String(describing: error))")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
self.error = .unregistering(error)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum NotificationsSetupError: LocalizedError {
|
private enum NotificationsSetupError: LocalizedError {
|
||||||
case requestingAuthorization(any Error)
|
case requestingAuthorization(any Error)
|
||||||
case registering(any Error)
|
|
||||||
case unregistering(any Error)
|
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .requestingAuthorization(let error):
|
case .requestingAuthorization(let error):
|
||||||
"Notifications authorization request failed: \(error.localizedDescription)"
|
"Notifications authorization request failed: \(error.localizedDescription)"
|
||||||
case .registering(let error):
|
|
||||||
"Registration failed: \(error.localizedDescription)"
|
|
||||||
case .unregistering(let error):
|
|
||||||
"Deactivation failed: \(error.localizedDescription)"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static let pushSubscriptionRemoved = Notification.Name("Tusker.pushSubscriptionRemoved")
|
|
||||||
}
|
|
||||||
|
|
|
@ -14,16 +14,14 @@ import TuskerComponents
|
||||||
|
|
||||||
struct PushInstanceSettingsView: View {
|
struct PushInstanceSettingsView: View {
|
||||||
let account: UserAccountInfo
|
let account: UserAccountInfo
|
||||||
let pushProxyRegistration: PushProxyRegistration
|
|
||||||
@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
|
@State private var showReLoginRequiredAlert = false
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
init(account: UserAccountInfo, pushProxyRegistration: PushProxyRegistration) {
|
init(account: UserAccountInfo) {
|
||||||
self.account = account
|
self.account = account
|
||||||
self.pushProxyRegistration = pushProxyRegistration
|
|
||||||
let subscription = PushManager.shared.pushSubscription(account: account)
|
let subscription = PushManager.shared.pushSubscription(account: account)
|
||||||
self.subscription = subscription
|
self.subscription = subscription
|
||||||
self.mode = subscription == nil ? .off : .on
|
self.mode = subscription == nil ? .off : .on
|
||||||
|
@ -52,13 +50,6 @@ struct PushInstanceSettingsView: View {
|
||||||
} message: {
|
} message: {
|
||||||
Text("You must grant permission on \(account.instanceURL.host!) to turn on push notifications.")
|
Text("You must grant permission on \(account.instanceURL.host!) to turn on push notifications.")
|
||||||
}
|
}
|
||||||
.onReceive(NotificationCenter.default
|
|
||||||
.publisher(for: .pushSubscriptionRemoved)
|
|
||||||
.filter { ($0.object as? String) == account.id }
|
|
||||||
) { _ in
|
|
||||||
mode = .off
|
|
||||||
subscription = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateNotificationsEnabled(enabled: Bool) async -> Bool {
|
private func updateNotificationsEnabled(enabled: Bool) async -> Bool {
|
||||||
|
@ -88,6 +79,11 @@ struct PushInstanceSettingsView: View {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let authorized = try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .providesAppNotificationSettings])
|
||||||
|
guard authorized else {
|
||||||
|
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 {
|
||||||
|
|
Loading…
Reference in New Issue