Skip to content

Commit 36723b3

Browse files
authored
[#600] 맥에서 소셜 로그인이 안되는 현상을 해결한다 (#603)
* chore: 배포를 편하게 하기 위한 1.2 으로 설정 * fix: fcmToken이 없어도 로그인을 성공하도록 수정 * fix: AppDelegate에서 FCM 동기화 트리거를 분리하도록 수정 * fix: FCMTokenSyncHandler에서 토큰 backfill 흐름을 처리하도록 수정 * chore: FCMTokenSyncHandler 동작 테스트를 추가 * refactor: 불필요 파라미터 제거 * fix: MainActor 내에서 동작하도록 보장 * refactor: FCMTokenSyncHandler의 중첩 Task를 정리 * refactor: 불필요 nil 반환 제거 * chore: 1.2.5 원복 * chore: 마이너 버전 + 1
1 parent 4fc460c commit 36723b3

14 files changed

Lines changed: 316 additions & 27 deletions

File tree

Application/DevLogApp/Sources/App/Assembler/AppLayerAssembler.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ final class AppLayerAssembler: Assembler {
1212
func assemble(_ container: any DIContainer) {
1313
container.register(FCMTokenSyncHandler.self) {
1414
FCMTokenSyncHandler(
15+
messagingService: container.resolve(PushMessagingService.self),
1516
userService: container.resolve(UserService.self)
1617
)
1718
}

Application/DevLogApp/Sources/App/Delegate/AppDelegate.swift

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
3232
_ = container.resolve(UserTimeZoneSyncHandler.self)
3333
_ = container.resolve(WidgetSyncEventHandler.self)
3434
_ = container.resolve(WidgetSessionSyncHandler.self)
35+
NotificationCenter.default.addObserver(
36+
self,
37+
selector: #selector(handleSceneDidActivate),
38+
name: UIScene.didActivateNotification,
39+
object: nil
40+
)
41+
NotificationCenter.default.addObserver(
42+
self,
43+
selector: #selector(handleRemoteNotificationRegistrationRequest),
44+
name: .didRequestRemoteNotificationRegistration,
45+
object: nil
46+
)
3547

3648
// 알림 권한 요청
3749
UNUserNotificationCenter.current().delegate = self
@@ -40,9 +52,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
4052
self.logger.error("Notification authorization error", error: error)
4153
} else {
4254
self.logger.info("Notification permission granted: \(granted)")
43-
DispatchQueue.main.async {
44-
UIApplication.shared.registerForRemoteNotifications()
45-
}
55+
NotificationCenter.default.post(name: .didRequestFCMTokenSync, object: nil)
4656
}
4757
}
4858

@@ -63,13 +73,27 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
6373
return true
6474
}
6575

76+
@objc private func handleSceneDidActivate() {
77+
NotificationCenter.default.post(name: .didRequestFCMTokenSync, object: nil)
78+
}
79+
80+
@objc private func handleRemoteNotificationRegistrationRequest() {
81+
Task { @MainActor in
82+
UIApplication.shared.registerForRemoteNotifications()
83+
}
84+
}
85+
6686
// APNs 등록 성공
6787
func application(
6888
_ application: UIApplication,
6989
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
7090
) {
7191
logger.info("APNs token: \(deviceToken.map { String(format: "%02.2hhx", $0) }.joined())")
72-
container.resolve(PushMessagingService.self).setAPNSToken(deviceToken)
92+
NotificationCenter.default.post(
93+
name: .didReceiveAPNSToken,
94+
object: nil,
95+
userInfo: ["deviceToken": deviceToken]
96+
)
7397
}
7498

7599
// APNs 등록 실패

Application/DevLogApp/Sources/App/Handler/FCMTokenSyncHandler.swift

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,80 @@ import DevLogData
1111
import Foundation
1212

1313
final class FCMTokenSyncHandler {
14+
private let messagingService: PushMessagingService
1415
private let userService: UserService
16+
private let notificationCenter: NotificationCenter
1517
private let logger = Logger(category: "FCMTokenSyncHandler")
1618
private var cancellables = Set<AnyCancellable>()
1719

1820
init(
21+
messagingService: PushMessagingService,
1922
userService: UserService,
2023
notificationCenter: NotificationCenter = .default
2124
) {
25+
self.messagingService = messagingService
2226
self.userService = userService
27+
self.notificationCenter = notificationCenter
2328

2429
notificationCenter.publisher(for: .didRefreshFCMToken)
2530
.compactMap { $0.userInfo?["fcmToken"] as? String }
2631
.sink { [weak self] fcmToken in
27-
Task {
28-
do {
29-
try await self?.userService.updateFCMToken(fcmToken)
30-
} catch {
31-
self?.logger.error("Failed to sync refreshed FCM token", error: error)
32-
}
33-
}
32+
self?.syncFCMToken(fcmToken)
3433
}
3534
.store(in: &cancellables)
35+
36+
notificationCenter.publisher(for: .didRequestFCMTokenSync)
37+
.sink { [weak self] _ in
38+
self?.requestFCMTokenSync()
39+
}
40+
.store(in: &cancellables)
41+
42+
notificationCenter.publisher(for: .didReceiveAPNSToken)
43+
.compactMap { $0.userInfo?["deviceToken"] as? Data }
44+
.sink { [weak self] deviceToken in
45+
self?.handleAPNSToken(deviceToken)
46+
}
47+
.store(in: &cancellables)
48+
}
49+
}
50+
51+
private extension FCMTokenSyncHandler {
52+
func requestFCMTokenSync() {
53+
Task { [weak self] in
54+
guard let self else { return }
55+
guard await messagingService.isNotificationAuthorized() else {
56+
return
57+
}
58+
notificationCenter.post(name: .didRequestRemoteNotificationRegistration, object: nil)
59+
await syncCurrentFCMToken()
60+
}
61+
}
62+
63+
func handleAPNSToken(_ deviceToken: Data) {
64+
messagingService.setAPNSToken(deviceToken)
65+
Task { [weak self] in
66+
await self?.syncCurrentFCMToken()
67+
}
68+
}
69+
70+
func syncCurrentFCMToken() async {
71+
do {
72+
guard let fcmToken = try await messagingService.fetchFCMToken() else {
73+
return
74+
}
75+
try await userService.updateFCMToken(fcmToken)
76+
} catch {
77+
logger.error("Failed to sync current FCM token", error: error)
78+
}
79+
}
80+
81+
func syncFCMToken(_ fcmToken: String) {
82+
Task { [weak self] in
83+
do {
84+
try await self?.userService.updateFCMToken(fcmToken)
85+
} catch {
86+
self?.logger.error("Failed to sync refreshed FCM token", error: error)
87+
}
88+
}
3689
}
3790
}

Application/DevLogApp/Sources/App/Notification/NotificationName+.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,8 @@ import Foundation
99

1010
extension Notification.Name {
1111
static let didRefreshFCMToken = Notification.Name("didRefreshFCMToken")
12+
static let didReceiveAPNSToken = Notification.Name("didReceiveAPNSToken")
13+
static let didRequestFCMTokenSync = Notification.Name("didRequestFCMTokenSync")
14+
static let didRequestRemoteNotificationRegistration = Notification.Name("didRequestRemoteNotificationRegistration")
1215
static let didRequestUserTimeZoneSync = Notification.Name("didRequestUserTimeZoneSync")
1316
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
//
2+
// FCMTokenSyncHandlerTests.swift
3+
// DevLogAppTests
4+
//
5+
// Created by opfic on 6/13/26.
6+
//
7+
8+
import Foundation
9+
import Testing
10+
import DevLogData
11+
@testable import DevLogApp
12+
13+
struct FCMTokenSyncHandlerTests {
14+
@Test("현재 FCM token 동기화 요청 시 token이 있으면 저장한다")
15+
func 현재_FCM_token_동기화_요청_시_token이_있으면_저장한다() async throws {
16+
let notificationCenter = NotificationCenter()
17+
let registrationObserver = NotificationObserver(
18+
notificationCenter: notificationCenter,
19+
name: .didRequestRemoteNotificationRegistration
20+
)
21+
let messagingService = PushMessagingServiceSpy(currentFCMToken: "current-token")
22+
let userService = UserServiceSpy()
23+
let handler = FCMTokenSyncHandler(
24+
messagingService: messagingService,
25+
userService: userService,
26+
notificationCenter: notificationCenter
27+
)
28+
29+
notificationCenter.post(name: .didRequestFCMTokenSync, object: nil)
30+
31+
try await waitUntil {
32+
await userService.updatedFCMTokens == ["current-token"]
33+
}
34+
#expect(registrationObserver.didReceiveNotification)
35+
_ = handler
36+
}
37+
38+
@Test("현재 FCM token 동기화 요청 시 token이 없으면 저장하지 않는다")
39+
func 현재_FCM_token_동기화_요청_시_token이_없으면_저장하지_않는다() async throws {
40+
let notificationCenter = NotificationCenter()
41+
let messagingService = PushMessagingServiceSpy(currentFCMToken: nil)
42+
let userService = UserServiceSpy()
43+
let handler = FCMTokenSyncHandler(
44+
messagingService: messagingService,
45+
userService: userService,
46+
notificationCenter: notificationCenter
47+
)
48+
49+
notificationCenter.post(name: .didRequestFCMTokenSync, object: nil)
50+
51+
try await Task.sleep(for: .milliseconds(100))
52+
#expect(await userService.updatedFCMTokens.isEmpty)
53+
_ = handler
54+
}
55+
56+
@Test("갱신된 FCM token 이벤트 수신 시 저장한다")
57+
func 갱신된_FCM_token_이벤트_수신_시_저장한다() async throws {
58+
let notificationCenter = NotificationCenter()
59+
let messagingService = PushMessagingServiceSpy(currentFCMToken: nil)
60+
let userService = UserServiceSpy()
61+
let handler = FCMTokenSyncHandler(
62+
messagingService: messagingService,
63+
userService: userService,
64+
notificationCenter: notificationCenter
65+
)
66+
67+
notificationCenter.post(
68+
name: .didRefreshFCMToken,
69+
object: nil,
70+
userInfo: ["fcmToken": "refreshed-token"]
71+
)
72+
73+
try await waitUntil {
74+
await userService.updatedFCMTokens == ["refreshed-token"]
75+
}
76+
_ = handler
77+
}
78+
79+
@Test("APNs token 이벤트 수신 시 APNs token을 적용하고 현재 FCM token을 저장한다")
80+
func APNs_token_이벤트_수신_시_APNs_token을_적용하고_현재_FCM_token을_저장한다() async throws {
81+
let notificationCenter = NotificationCenter()
82+
let messagingService = PushMessagingServiceSpy(currentFCMToken: "current-token")
83+
let userService = UserServiceSpy()
84+
let handler = FCMTokenSyncHandler(
85+
messagingService: messagingService,
86+
userService: userService,
87+
notificationCenter: notificationCenter
88+
)
89+
let deviceToken = Data([0x01, 0x02, 0x03])
90+
91+
notificationCenter.post(
92+
name: .didReceiveAPNSToken,
93+
object: nil,
94+
userInfo: ["deviceToken": deviceToken]
95+
)
96+
97+
try await waitUntil {
98+
await userService.updatedFCMTokens == ["current-token"]
99+
}
100+
#expect(messagingService.apnsTokens == [deviceToken])
101+
_ = handler
102+
}
103+
}
104+
105+
private actor UserServiceSpy: UserService {
106+
private(set) var updatedFCMTokens = [String]()
107+
108+
func upsertUser(_ response: AuthDataResponse) async throws { }
109+
func fetchUserProfile() async throws -> UserProfileResponse { fatalError() }
110+
func upsertStatusMessage(_ message: String) async throws { }
111+
112+
func updateFCMToken(_ fcmToken: String) async throws {
113+
updatedFCMTokens.append(fcmToken)
114+
}
115+
116+
func updateUserTimeZone() async throws { }
117+
}
118+
119+
private final class PushMessagingServiceSpy: PushMessagingService {
120+
private let currentFCMToken: String?
121+
private(set) var apnsTokens = [Data]()
122+
123+
init(currentFCMToken: String?) {
124+
self.currentFCMToken = currentFCMToken
125+
}
126+
127+
func setDelegate(_ delegate: PushMessagingServiceDelegate?) { }
128+
func setAPNSToken(_ deviceToken: Data) {
129+
apnsTokens.append(deviceToken)
130+
}
131+
func isNotificationAuthorized() async -> Bool { true }
132+
133+
func fetchFCMToken() async throws -> String? {
134+
currentFCMToken
135+
}
136+
}
137+
138+
private final class NotificationObserver {
139+
private(set) var didReceiveNotification = false
140+
private var token: NSObjectProtocol?
141+
private let notificationCenter: NotificationCenter
142+
143+
init(notificationCenter: NotificationCenter, name: Notification.Name) {
144+
self.notificationCenter = notificationCenter
145+
self.token = notificationCenter.addObserver(
146+
forName: name,
147+
object: nil,
148+
queue: nil
149+
) { [weak self] _ in
150+
self?.didReceiveNotification = true
151+
}
152+
}
153+
154+
deinit {
155+
if let token {
156+
notificationCenter.removeObserver(token)
157+
}
158+
}
159+
}
160+
161+
private func waitUntil(
162+
timeout: Duration = .seconds(1),
163+
pollInterval: Duration = .milliseconds(10),
164+
condition: @escaping @Sendable () async -> Bool
165+
) async throws {
166+
let deadline = ContinuousClock.now + timeout
167+
168+
while ContinuousClock.now < deadline {
169+
if await condition() {
170+
return
171+
}
172+
try await Task.sleep(for: pollInterval)
173+
}
174+
175+
Issue.record("조건을 만족하지 못함")
176+
}

Application/DevLogData/Sources/DTO/AuthDataResponse.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public struct AuthDataResponse {
1414
public let email: String?
1515
public let providers: [String]
1616
public let providerID: String
17-
public let fcmToken: String
17+
public let fcmToken: String?
1818
public let accessToken: String?
1919

2020
public init(
@@ -23,7 +23,7 @@ public struct AuthDataResponse {
2323
email: String?,
2424
providers: [String],
2525
providerID: String,
26-
fcmToken: String,
26+
fcmToken: String? = nil,
2727
accessToken: String? = nil
2828
) {
2929
self.uid = uid

Application/DevLogData/Sources/Protocol/PushMessagingService.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import Foundation
1010
public protocol PushMessagingService: AnyObject {
1111
func setDelegate(_ delegate: PushMessagingServiceDelegate?)
1212
func setAPNSToken(_ deviceToken: Data)
13+
func isNotificationAuthorized() async -> Bool
14+
func fetchFCMToken() async throws -> String?
1315
}
1416

1517
public protocol PushMessagingServiceDelegate: AnyObject {

Application/DevLogInfra/Sources/Extension/FirebaseAuthUser+.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import DevLogData
1212
extension FirebaseAuth.User {
1313
func makeResponse(
1414
providerID: AuthProviderID,
15-
fcmToken: String,
1615
accessToken: String? = nil
1716
) -> AuthDataResponse {
1817
return AuthDataResponse(
@@ -21,7 +20,6 @@ extension FirebaseAuth.User {
2120
email: self.email,
2221
providers: self.providerData.map { $0.providerID },
2322
providerID: providerID.rawValue,
24-
fcmToken: fcmToken,
2523
accessToken: accessToken
2624
)
2725
}

0 commit comments

Comments
 (0)