77
88import Combine
99import Core
10+ import CryptoKit
1011import Data
1112import Foundation
13+ import Infra
1214
1315final class FCMTokenSyncHandler {
16+ private enum Key {
17+ static let fcmTokenHash = " FCMTokenSyncHandler.fcmTokenHash "
18+ }
19+
20+ private let authService : AuthService
1421 private let messagingService : PushMessagingService
1522 private let userService : UserService
23+ private let store : UserDefaultsStore
1624 private let notificationCenter : NotificationCenter
1725 private let logger = Logger ( category: " FCMTokenSyncHandler " )
1826 private var cancellables = Set < AnyCancellable > ( )
1927
2028 init (
29+ authService: AuthService ,
2130 messagingService: PushMessagingService ,
2231 userService: UserService ,
32+ store: UserDefaultsStore ,
2333 notificationCenter: NotificationCenter = . default
2434 ) {
35+ self . authService = authService
2536 self . messagingService = messagingService
2637 self . userService = userService
38+ self . store = store
2739 self . notificationCenter = notificationCenter
2840
41+ authService. observeSignedIn ( )
42+ . removeDuplicates ( )
43+ . sink { [ weak self] isSignedIn in
44+ self ? . handleSessionUpdate ( isSignedIn: isSignedIn)
45+ }
46+ . store ( in: & cancellables)
47+
2948 notificationCenter. publisher ( for: . didRefreshFCMToken)
3049 . compactMap { $0. userInfo ? [ " fcmToken " ] as? String }
3150 . sink { [ weak self] fcmToken in
@@ -49,12 +68,19 @@ final class FCMTokenSyncHandler {
4968}
5069
5170private extension FCMTokenSyncHandler {
71+ func handleSessionUpdate( isSignedIn: Bool ) {
72+ guard isSignedIn else {
73+ store. setString ( nil , forKey: Key . fcmTokenHash)
74+ return
75+ }
76+
77+ requestFCMTokenSync ( )
78+ }
79+
5280 func requestFCMTokenSync( ) {
5381 Task { [ weak self] in
5482 guard let self else { return }
55- guard await messagingService. isNotificationAuthorized ( ) else {
56- return
57- }
83+ guard await messagingService. isNotificationAuthorized ( ) else { return }
5884 notificationCenter. post ( name: . didRequestRemoteNotificationRegistration, object: nil )
5985 await syncCurrentFCMToken ( )
6086 }
@@ -69,10 +95,8 @@ private extension FCMTokenSyncHandler {
6995
7096 func syncCurrentFCMToken( ) async {
7197 do {
72- guard let fcmToken = try await messagingService. fetchFCMToken ( ) else {
73- return
74- }
75- try await userService. updateFCMToken ( fcmToken)
98+ guard let fcmToken = try await messagingService. fetchFCMToken ( ) else { return }
99+ try await syncFCMTokenIfNeeded ( fcmToken)
76100 } catch {
77101 logger. error ( " Failed to sync current FCM token " , error: error)
78102 }
@@ -81,10 +105,33 @@ private extension FCMTokenSyncHandler {
81105 func syncFCMToken( _ fcmToken: String ) {
82106 Task { [ weak self] in
83107 do {
84- try await self ? . userService . updateFCMToken ( fcmToken)
108+ try await self ? . syncFCMTokenIfNeeded ( fcmToken)
85109 } catch {
86110 self ? . logger. error ( " Failed to sync refreshed FCM token " , error: error)
87111 }
88112 }
89113 }
114+
115+ func syncFCMTokenIfNeeded( _ fcmToken: String ) async throws {
116+ guard let uid = authService. uid else {
117+ store. setString ( nil , forKey: Key . fcmTokenHash)
118+ logger. info ( " Skipping FCM token update because no authenticated user exists " )
119+ return
120+ }
121+
122+ let tokenHash = makeTokenHash ( uid: uid, fcmToken: fcmToken)
123+ guard store. string ( forKey: Key . fcmTokenHash) != tokenHash else {
124+ logger. info ( " Skipping FCM token update because token hash is unchanged " )
125+ return
126+ }
127+
128+ try await userService. updateFCMToken ( fcmToken)
129+ store. setString ( tokenHash, forKey: Key . fcmTokenHash)
130+ }
131+
132+ func makeTokenHash( uid: String , fcmToken: String ) -> String {
133+ let value = " \( uid) | \( FirebaseConfiguration . databaseID) | \( fcmToken) "
134+ let digest = SHA256 . hash ( data: Data ( value. utf8) )
135+ return digest. map { String ( format: " %02x " , $0) } . joined ( )
136+ }
90137}
0 commit comments