Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Application/App/Sources/App/Delegate/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
name: UIApplication.willEnterForegroundNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(requestAPNsRegistration),
name: UIApplication.willEnterForegroundNotification,
object: nil
)

// 알림 권한 요청
UNUserNotificationCenter.current().delegate = self
Expand Down Expand Up @@ -79,7 +85,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}

func applicationWillEnterForeground(_ application: UIApplication) {
@objc private func requestAPNsRegistration() {
NotificationCenter.default.post(name: .didRequestAPNsRegistration, object: nil)
}

Expand Down
20 changes: 18 additions & 2 deletions Application/App/Sources/App/Handler/FCMTokenSyncHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ import Data
import Foundation

final class FCMTokenSyncHandler {
private struct SyncKey: Equatable {
let uid: String
let fcmToken: String
}

private let authService: AuthService
private let messagingService: PushMessagingService
private let userService: UserService
private let notificationCenter: NotificationCenter
private let logger = Logger(category: "FCMTokenSyncHandler")
private var cancellables = Set<AnyCancellable>()
private var lastSyncedKey: SyncKey?

init(
authService: AuthService,
Expand Down Expand Up @@ -68,7 +74,10 @@ final class FCMTokenSyncHandler {

private extension FCMTokenSyncHandler {
func handleSessionUpdate(isSignedIn: Bool) {
guard isSignedIn else { return }
guard isSignedIn else {
lastSyncedKey = nil
return
}

requestFCMTokenSync()
}
Expand Down Expand Up @@ -114,11 +123,18 @@ private extension FCMTokenSyncHandler {
}

func syncFCMTokenIfNeeded(_ fcmToken: String) async throws {
guard authService.uid != nil else {
guard let uid = authService.uid else {
logger.info("Skipping FCM token update because no authenticated user exists")
return
}

let key = SyncKey(uid: uid, fcmToken: fcmToken)
guard lastSyncedKey != key else {
logger.info("Skipping FCM token update because the token was already synced")
return
}

try await userService.updateFCMToken(fcmToken)
lastSyncedKey = key
Comment thread
opficdev marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ struct FCMTokenSyncHandlerTests {
_ = handler
}

@Test("같은 사용자와 같은 FCM token도 매번 저장한다")
func 같은_사용자와_같은_FCM_token도_매번_저장한다() async throws {
@Test("같은 사용자와 같은 FCM token은 한 번만 저장한다")
func 같은_사용자와_같은_FCM_token은_한_번만_저장한다() async throws {
let notificationCenter = NotificationCenter()
let messagingService = PushMessagingServiceSpy(currentFCMToken: "current-token")
let userService = UserServiceSpy()
Expand All @@ -156,9 +156,8 @@ struct FCMTokenSyncHandlerTests {
}

notificationCenter.post(name: .didRequestFCMTokenSync, object: nil)
try await waitUntil {
await userService.updatedFCMTokens == ["current-token", "current-token"]
}
try await Task.sleep(for: .milliseconds(100))
#expect(await userService.updatedFCMTokens == ["current-token"])

_ = handler
}
Expand Down Expand Up @@ -189,6 +188,72 @@ struct FCMTokenSyncHandlerTests {
_ = handler
}

@Test("FCM token이 바뀌면 같은 사용자도 다시 저장한다")
func FCM_token이_바뀌면_같은_사용자도_다시_저장한다() async throws {
let notificationCenter = NotificationCenter()
let messagingService = PushMessagingServiceSpy(currentFCMToken: "first-token")
let userService = UserServiceSpy()
let authService = AuthServiceSpy()
let handler = FCMTokenSyncHandler(
authService: authService,
messagingService: messagingService,
userService: userService,
notificationCenter: notificationCenter
)

notificationCenter.post(name: .didRequestFCMTokenSync, object: nil)
try await waitUntil { await userService.updatedFCMTokens == ["first-token"] }

messagingService.currentFCMToken = "second-token"
notificationCenter.post(name: .didRequestFCMTokenSync, object: nil)
try await waitUntil { await userService.updatedFCMTokens == ["first-token", "second-token"] }
_ = handler
}

@Test("로그아웃 후 같은 사용자로 다시 로그인하면 같은 FCM token도 다시 저장한다")
func 로그아웃_후_같은_사용자로_다시_로그인하면_같은_FCM_token도_다시_저장한다() async throws {
let notificationCenter = NotificationCenter()
let messagingService = PushMessagingServiceSpy(currentFCMToken: "current-token")
let userService = UserServiceSpy()
let authService = AuthServiceSpy(uid: "user-id")
let handler = FCMTokenSyncHandler(
authService: authService,
messagingService: messagingService,
userService: userService,
notificationCenter: notificationCenter
)

notificationCenter.post(name: .didRequestFCMTokenSync, object: nil)
try await waitUntil { await userService.updatedFCMTokens == ["current-token"] }

authService.updateSession(uid: nil)
authService.updateSession(uid: "user-id")
try await waitUntil { await userService.updatedFCMTokens == ["current-token", "current-token"] }
_ = handler
}

@Test("FCM token 저장에 실패하면 같은 요청을 다시 저장한다")
func FCM_token_저장에_실패하면_같은_요청을_다시_저장한다() async throws {
let notificationCenter = NotificationCenter()
let messagingService = PushMessagingServiceSpy(currentFCMToken: "current-token")
let userService = UserServiceSpy(updateError: FCMTokenSyncTestError())
let authService = AuthServiceSpy()
let handler = FCMTokenSyncHandler(
authService: authService,
messagingService: messagingService,
userService: userService,
notificationCenter: notificationCenter
)

notificationCenter.post(name: .didRequestFCMTokenSync, object: nil)
try await waitUntil { await userService.updatedFCMTokens == ["current-token"] }

await userService.setUpdateError(nil)
notificationCenter.post(name: .didRequestFCMTokenSync, object: nil)
try await waitUntil { await userService.updatedFCMTokens == ["current-token", "current-token"] }
_ = handler
}

@Test("로그인 세션 전이 시 현재 FCM token을 저장한다")
func 로그인_세션_전이_시_현재_FCM_token을_저장한다() async throws {
let notificationCenter = NotificationCenter()
Expand All @@ -214,16 +279,28 @@ struct FCMTokenSyncHandlerTests {

private actor UserServiceSpy: UserService {
private(set) var updatedFCMTokens = [String]()
private var updateError: Error?

init(updateError: Error? = nil) {
self.updateError = updateError
}

func upsertUser(_ response: AuthDataResponse) async throws { }
func fetchUserProfile() async throws -> UserProfileResponse { fatalError() }
func upsertStatusMessage(_ message: String) async throws { }

func updateFCMToken(_ fcmToken: String) async throws {
updatedFCMTokens.append(fcmToken)
if let updateError {
throw updateError
}
}

func updateUserTimeZone() async throws { }

func setUpdateError(_ error: Error?) {
updateError = error
}
}

private final class AuthServiceSpy: AuthService {
Expand Down Expand Up @@ -255,7 +332,7 @@ private final class AuthServiceSpy: AuthService {
}

private final class PushMessagingServiceSpy: PushMessagingService {
private let currentFCMToken: String?
var currentFCMToken: String?
private(set) var apnsTokens = [Data]()

init(currentFCMToken: String?) {
Expand All @@ -273,6 +350,8 @@ private final class PushMessagingServiceSpy: PushMessagingService {
}
}

private struct FCMTokenSyncTestError: Error { }

private final class NotificationObserver {
private(set) var didReceiveNotification = false
private var token: NSObjectProtocol?
Expand Down
Loading