91 lines
3.5 KiB
Swift
91 lines
3.5 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import UserNotifications
|
|
|
|
/// Manages APNs device token registration and the push.org-social.org subscription lifecycle.
|
|
///
|
|
/// Flow:
|
|
/// 1. App becomes ready → call `requestPermissionAndRegister()`.
|
|
/// 2. iOS calls `AppDelegate.didRegisterForRemoteNotificationsWithDeviceToken` →
|
|
/// that calls `subscribe(deviceToken:)` here.
|
|
/// 3. On relay disable or account deletion → call `unsubscribe()`.
|
|
@MainActor
|
|
final class PushRegistration {
|
|
|
|
static let shared = PushRegistration()
|
|
private init() {}
|
|
|
|
private let pushBaseURL = URL(string: "https://push.org-social.org")!
|
|
private let tokenKey = "pushDeviceToken"
|
|
|
|
// MARK: - Public API
|
|
|
|
func requestPermissionAndRegister() async {
|
|
guard isEligible else { return }
|
|
let center = UNUserNotificationCenter.current()
|
|
let settings = await center.notificationSettings()
|
|
switch settings.authorizationStatus {
|
|
case .notDetermined:
|
|
let granted = (try? await center.requestAuthorization(options: [.alert, .sound, .badge])) ?? false
|
|
if granted { await register() }
|
|
case .authorized, .provisional, .ephemeral:
|
|
await register()
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
func subscribe(deviceToken: Data) async {
|
|
let tokenString = deviceToken.map { String(format: "%02x", $0) }.joined()
|
|
UserDefaults.standard.set(tokenString, forKey: tokenKey)
|
|
guard let feedURL = ownFeedURL else { return }
|
|
await post(endpoint: "subscribe", body: ["feed": feedURL.absoluteString,
|
|
"device_token": tokenString])
|
|
}
|
|
|
|
func unsubscribe() async {
|
|
guard let token = UserDefaults.standard.string(forKey: tokenKey), !token.isEmpty else { return }
|
|
await delete(endpoint: "unsubscribe", body: ["device_token": token])
|
|
UserDefaults.standard.removeObject(forKey: tokenKey)
|
|
}
|
|
|
|
// MARK: - Private helpers
|
|
|
|
private var isEligible: Bool {
|
|
let useRelay = UserDefaults.standard.object(forKey: "useRelay") as? Bool ?? true
|
|
return useRelay && ownFeedURL != nil
|
|
}
|
|
|
|
private var ownFeedURL: URL? {
|
|
guard let raw = UserDefaults.standard.string(forKey: "publicFeedURL"),
|
|
let url = URL(string: raw), !raw.isEmpty else { return nil }
|
|
return url
|
|
}
|
|
|
|
@discardableResult
|
|
private func register() async -> Bool {
|
|
await MainActor.run { UIApplication.shared.registerForRemoteNotifications() }
|
|
return true
|
|
}
|
|
|
|
private func post(endpoint: String, body: [String: String]) async {
|
|
guard let url = URL(string: "\(pushBaseURL)/\(endpoint)/") else { return }
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "POST"
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
request.timeoutInterval = 10
|
|
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
|
|
_ = try? await URLSession.shared.data(for: request)
|
|
}
|
|
|
|
private func delete(endpoint: String, body: [String: String]) async {
|
|
guard let url = URL(string: "\(pushBaseURL)/\(endpoint)/") else { return }
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "DELETE"
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
request.timeoutInterval = 10
|
|
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
|
|
_ = try? await URLSession.shared.data(for: request)
|
|
}
|
|
}
|