update push

This commit is contained in:
cheykrym 2025-12-03 22:40:44 +03:00
parent bc9f82b8fb
commit 71fb0551fe
4 changed files with 210 additions and 1 deletions

View File

@ -6,6 +6,7 @@ import UserNotifications
import UIKit import UIKit
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate, MessagingDelegate { class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate, MessagingDelegate {
private let pushTokenManager = PushTokenManager.shared
func application(_ application: UIApplication, func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
@ -52,6 +53,12 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
} }
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
print("🔥 FCM token:", fcmToken ?? "NO TOKEN") guard let fcmToken else {
if AppConfig.DEBUG { print("🔥 FCM token: NO TOKEN") }
return
}
if AppConfig.DEBUG { print("🔥 FCM token:", fcmToken) }
pushTokenManager.registerFCMToken(fcmToken)
} }
} }

View File

@ -171,6 +171,50 @@ final class SessionsService {
} }
} }
func updatePushToken(_ token: String, completion: @escaping (Result<String, Error>) -> Void) {
client.request(
path: "/v1/auth/sessions/update_push_token",
method: .post,
query: ["fcm_token": token],
requiresAuth: true
) { [decoder] result in
switch result {
case .success(let response):
do {
let apiResponse = try decoder.decode(APIResponse<MessagePayload>.self, from: response.data)
guard apiResponse.status == "fine" else {
let message = apiResponse.detail ?? NSLocalizedString("Не удалось обновить push-токен.", comment: "Sessions service update push unexpected status")
completion(.failure(SessionsServiceError.unexpectedStatus(message)))
return
}
completion(.success(apiResponse.data.message))
} catch {
let debugMessage = Self.describeDecodingError(error: error, data: response.data)
if AppConfig.DEBUG {
print("[SessionsService] decode update-push failed: \(debugMessage)")
}
completion(.failure(SessionsServiceError.decoding(debugDescription: debugMessage)))
}
case .failure(let error):
if case let NetworkError.server(_, data) = error,
let data,
let message = Self.errorMessage(from: data) {
completion(.failure(SessionsServiceError.unexpectedStatus(message)))
return
}
completion(.failure(error))
}
}
}
func updatePushToken(_ token: String) async throws -> String {
try await withCheckedThrowingContinuation { continuation in
updatePushToken(token) { result in
continuation.resume(with: result)
}
}
}
private static func decodeDate(from decoder: Decoder) throws -> Date { private static func decodeDate(from decoder: Decoder) throws -> Date {
let container = try decoder.singleValueContainer() let container = try decoder.singleValueContainer()
let string = try container.decode(String.self) let string = try container.decode(String.self)

View File

@ -1198,6 +1198,9 @@
"Не удалось найти результаты." : { "Не удалось найти результаты." : {
"comment" : "Search unexpected status" "comment" : "Search unexpected status"
}, },
"Не удалось обновить push-токен." : {
"comment" : "Sessions service update push unexpected status"
},
"Не удалось обновить пароль." : { "Не удалось обновить пароль." : {
"localizations" : { "localizations" : {
"en" : { "en" : {

View File

@ -0,0 +1,155 @@
import Foundation
import UIKit
final class PushTokenManager {
static let shared = PushTokenManager()
private let queue = DispatchQueue(label: "org.yobble.push-token", qos: .utility)
private let sessionsService: SessionsService
private var currentFCMToken: String?
private var lastSentTokens: [String: String]
private var loginsRequiringSync: Set<String> = []
private var isUpdating = false
private var pendingUpdate = false
private var retryWorkItem: DispatchWorkItem?
private var notificationTokens: [NSObjectProtocol] = []
private enum Keys {
static let storedToken = "push.current_fcm_token"
static let sentTokens = "push.last_sent_tokens"
static let currentUser = "currentUser"
}
private enum Constants {
static let retryDelay: TimeInterval = 20
}
private init(sessionsService: SessionsService = SessionsService()) {
self.sessionsService = sessionsService
let defaults = UserDefaults.standard
self.currentFCMToken = defaults.string(forKey: Keys.storedToken)
self.lastSentTokens = defaults.dictionary(forKey: Keys.sentTokens) as? [String: String] ?? [:]
observeNotifications()
queue.async { [weak self] in
guard let self else { return }
if let login = self.currentLogin() {
self.loginsRequiringSync.insert(login)
}
self.pendingUpdate = true
self.tryUpdateTokenIfNeeded()
}
}
deinit {
notificationTokens.forEach { NotificationCenter.default.removeObserver($0) }
notificationTokens.removeAll()
}
func registerFCMToken(_ token: String) {
queue.async { [weak self] in
guard let self else { return }
guard self.currentFCMToken != token else { return }
self.currentFCMToken = token
UserDefaults.standard.set(token, forKey: Keys.storedToken)
if let login = self.currentLogin() {
self.loginsRequiringSync.insert(login)
}
self.pendingUpdate = true
self.tryUpdateTokenIfNeeded()
}
}
private func observeNotifications() {
let center = NotificationCenter.default
let accessTokenObserver = center.addObserver(forName: .accessTokenDidChange, object: nil, queue: nil) { [weak self] _ in
guard let self else { return }
self.queue.async {
if let login = self.currentLogin() {
self.loginsRequiringSync.insert(login)
} else {
self.loginsRequiringSync.removeAll()
}
self.pendingUpdate = true
self.tryUpdateTokenIfNeeded()
}
}
notificationTokens.append(accessTokenObserver)
let didBecomeActiveObserver = center.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in
guard let self else { return }
self.queue.async {
self.pendingUpdate = true
self.tryUpdateTokenIfNeeded()
}
}
notificationTokens.append(didBecomeActiveObserver)
}
private func tryUpdateTokenIfNeeded() {
guard pendingUpdate else { return }
guard !isUpdating else { return }
guard let login = currentLogin() else { return }
guard let token = currentFCMToken, !token.isEmpty else { return }
let needsForcedSync = loginsRequiringSync.contains(login)
if !needsForcedSync, let lastToken = lastSentTokens[login], lastToken == token {
pendingUpdate = false
return
}
pendingUpdate = false
isUpdating = true
retryWorkItem?.cancel()
retryWorkItem = nil
sessionsService.updatePushToken(token) { [weak self] result in
guard let self else { return }
self.queue.async {
self.isUpdating = false
switch result {
case .success:
self.loginsRequiringSync.remove(login)
self.lastSentTokens[login] = token
UserDefaults.standard.set(self.lastSentTokens, forKey: Keys.sentTokens)
if AppConfig.DEBUG {
print("[PushTokenManager] Push token updated for @\(login)")
}
case .failure(let error):
if AppConfig.DEBUG {
print("[PushTokenManager] Failed to update push token: \(error.localizedDescription)")
}
self.loginsRequiringSync.insert(login)
self.pendingUpdate = true
self.scheduleRetry()
}
self.tryUpdateTokenIfNeeded()
}
}
}
private func scheduleRetry() {
guard retryWorkItem == nil else { return }
let workItem = DispatchWorkItem { [weak self] in
guard let self else { return }
self.retryWorkItem = nil
self.pendingUpdate = true
self.tryUpdateTokenIfNeeded()
}
retryWorkItem = workItem
queue.asyncAfter(deadline: .now() + Constants.retryDelay, execute: workItem)
}
private func currentLogin() -> String? {
guard let login = UserDefaults.standard.string(forKey: Keys.currentUser), !login.isEmpty else {
return nil
}
return login
}
}