-
Notifications
You must be signed in to change notification settings - Fork 0
[SOU-646] FCM 인프라·APNs 설정 #73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
793b7b6
fa5e12e
cc99b73
e719963
5f15292
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| <plist version="1.0"> | ||
| <dict> | ||
| <key>aps-environment</key> | ||
| <string>development</string> | ||
| <key>com.apple.developer.applesignin</key> | ||
| <array> | ||
| <string>Default</string> | ||
| </array> | ||
| </dict> | ||
| </plist> |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,58 @@ | ||||||||||||||||||||||||||||||||||||||
| import Logger | ||||||||||||||||||||||||||||||||||||||
| import UIKit | ||||||||||||||||||||||||||||||||||||||
| import UserNotifications | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /// 푸시 권한 요청·APNs 등록은 App 레이어 책임. Core FCM은 토큰 수신만 담당한다. | ||||||||||||||||||||||||||||||||||||||
| final class PushNotificationRegistrar { | ||||||||||||||||||||||||||||||||||||||
| private enum UserDefaultsKey { | ||||||||||||||||||||||||||||||||||||||
| static let hasPresentedPushPermissionPrompt = "push.hasPresentedPermissionPrompt" | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| func requestPermissionIfNeeded() { | ||||||||||||||||||||||||||||||||||||||
| if UserDefaults.standard.bool(forKey: UserDefaultsKey.hasPresentedPushPermissionPrompt) { | ||||||||||||||||||||||||||||||||||||||
| syncRegistrationIfAuthorized() | ||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| UNUserNotificationCenter.current().requestAuthorization(options: [ | ||||||||||||||||||||||||||||||||||||||
| .alert, | ||||||||||||||||||||||||||||||||||||||
| .badge, | ||||||||||||||||||||||||||||||||||||||
| .sound, | ||||||||||||||||||||||||||||||||||||||
| ]) { granted, error in | ||||||||||||||||||||||||||||||||||||||
| if let error { | ||||||||||||||||||||||||||||||||||||||
| Logger.shared.error( | ||||||||||||||||||||||||||||||||||||||
| "푸시 권한 요청 실패: \(error.localizedDescription)", | ||||||||||||||||||||||||||||||||||||||
| category: .general | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+22
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 권한 요청 실패 시에도 플래그를 설정해야 합니다. 현재 🛡️ 제안 수정 ]) { granted, error in
if let error {
+ UserDefaults.standard.set(
+ true,
+ forKey: UserDefaultsKey.hasPresentedPushPermissionPrompt
+ )
Logger.shared.error(
"푸시 권한 요청 실패: \(error.localizedDescription)",
category: .general
)
return
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| UserDefaults.standard.set( | ||||||||||||||||||||||||||||||||||||||
| true, | ||||||||||||||||||||||||||||||||||||||
| forKey: UserDefaultsKey.hasPresentedPushPermissionPrompt | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Logger.shared.info( | ||||||||||||||||||||||||||||||||||||||
| "푸시 권한 \(granted ? "허용" : "거부")", | ||||||||||||||||||||||||||||||||||||||
| category: .general | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| guard granted else { return } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| DispatchQueue.main.async { | ||||||||||||||||||||||||||||||||||||||
| UIApplication.shared.registerForRemoteNotifications() | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /// 설정 앱에서 권한을 켠 뒤 앱으로 돌아온 경우 등 APNs 재등록 | ||||||||||||||||||||||||||||||||||||||
| func syncRegistrationIfAuthorized() { | ||||||||||||||||||||||||||||||||||||||
| UNUserNotificationCenter.current().getNotificationSettings { settings in | ||||||||||||||||||||||||||||||||||||||
| guard settings.authorizationStatus == .authorized else { return } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| DispatchQueue.main.async { | ||||||||||||||||||||||||||||||||||||||
| UIApplication.shared.registerForRemoteNotifications() | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import Logger | ||
|
|
||
| protocol PushTokenSyncing: AnyObject { | ||
| func sync(fcmToken: String) async | ||
| } | ||
|
|
||
| /// 서버 API 스펙이 연결되기 전까지 토큰 수신 지점만 고정한다. | ||
| final class DeferredPushTokenSyncer: PushTokenSyncing { | ||
| private var lastObservedToken: String? | ||
|
|
||
| func sync(fcmToken: String) async { | ||
| guard lastObservedToken != fcmToken else { return } | ||
|
|
||
| lastObservedToken = fcmToken | ||
| Logger.shared.info( | ||
| "FCM 토큰 수신됨. 서버 등록 API 연결 대기", | ||
| category: .general | ||
| ) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import ProjectDescription | ||
| import ProjectDescriptionHelpers | ||
|
|
||
| let project = Project.framework( | ||
| .fcm, | ||
| additionalBaseSettings: [ | ||
| "PRODUCT_NAME": "FCM", | ||
| "ENABLE_MODULE_VERIFIER": "NO", | ||
| "CLANG_ENABLE_MODULE_VERIFIER": "NO", | ||
|
Comment on lines
+8
to
+9
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: You asked about “Firebase iOS SDK 11.14.0 Tuist SPM integration module verifier settings requirements”. Here’s what can be asserted from current sources about (a) Firebase 11.14.0 and (b) the Tuist/SPM settings patterns relevant to module verification/module-map problems. 1) Firebase iOS SDK 11.14.0 context Firebase Apple SDK 11.14.0 (June 3, 2025) is a released version of the “Firebase Apple SDK” distribution [1]. Release notes indicate it’s available as version 11.14.0 [1], and the SDK version is tracked in the repo release/compare tooling for 11.14.0 [2][3]. 2) What “module verifier settings” means (Xcode) Apple’s guidance for the Apple Clang Module Verifier is to enable the “Enable Module Verifier” build setting (Build Settings -> Apple Clang Module Verifier) and ensure “Supported Languages”/“Supported Language Dialects” match your requirements [4]. This is general Xcode guidance for identifying framework module issues before distribution [4]. 3) Tuist + SPM + Firebase: the practical “requirements” that map to module-verifier failures In Tuist, the most relevant build-setting category for Firebase SPM integration problems is the handling of module maps and DEFINES_MODULE. 3.1 Tuist generates/adjusts DEFINES_MODULE when a module map exists Tuist’s PackageInfoMapper logic sets “DEFINES_MODULE=NO” when it decides there is a module map coming from a directory/header/custom module-map setup (specifically when moduleMap is present and moduleMap kind is directory/header/custom) [5]. This is precisely the kind of setting that can affect module-map/module-verifier correctness. 3.2 Known Firebase-in-SPM issues in Tuist are usually solved by adjusting DEFINES_MODULE for specific targets There are multiple Tuist issue reports where developers ran into “DEFINE_MODULE was set, but no umbrella header could be found to generate the module map”, and one workaround was to set “DEFINES_MODULE”: false for Firebase-related targets (e.g., “Firebase”, “FirebaseAnalytics…”, “FirebaseCoreExtension”, etc.) via Tuist dependency targetSettings [6]. 3.3 Tuist had to change module-map generation behavior for a Firebase case A Tuist PR (“Fix integration of latest Firebase”) describes a Firebase Remote Config scenario where Tuist/module-map generation logic caused a compilation failure because a Swift-only target had publicHeadersSearchPath set but no headers; the fix was to “don’t generate a modulemap when a publicHeadersDir exists but it has no headers” and to “skip setting the DEFINES_MODULE=NO in those cases, so the objc target can consume the bridging header automatically generated by Xcode” [7]. So, if your question is “what Tuist module verifier settings requirements exist for Firebase 11.14.0 when integrating via SPM?”, the sourced answer is that Tuist’s SPM integration uses/affects module-map behavior through settings like DEFINES_MODULE, and Firebase-specific edge cases can require Tuist to avoid generating module maps (or avoid setting DEFINES_MODULE=NO) when there are no headers, so the objc targets can consume the bridging header automatically generated by Xcode [7]. 4) What you should actually configure in Tuist (actionable, grounded) A) Ensure your Xcode module verifier build settings are set correctly (general) - In Xcode, set “Enable Module Verifier” to Yes, and set “Supported Languages” and “Supported Language Dialects” to match what you need [4]. B) If you hit module-map/module-verifier failures specifically with Firebase SPM via Tuist, start by checking Tuist’s targetSettings for DEFINES_MODULE - Tuist issue workaround evidence shows setting DEFINES_MODULE false for many Firebase-related SPM products/targets can suppress “DEFINE_MODULE was set…” module-map generation warnings [6]. - Tuist’s own code shows it may set DEFINES_MODULE=NO in the presence of certain module-map configurations [5], and Tuist fixed a Firebase integration failure by skipping module-map generation and skipping DEFINES_MODULE=NO in the specific “publicHeadersDir exists but has no headers” case [7]. 5) Firebase SPM integration requirements that are not “module verifier” but are commonly required Firebase’s installation methods doc for Swift Package Manager states “Google Analytics requires adding the -ObjC linker flag to your target's build settings if included transitively.” [8]. Separately, Tuist’s dependencies guide states Firebase has a “Requires -ObjC linker flag” requirement (in its Firebase special configurations section) [9]. Limitations / what I could not find in the sources I did not find a Firebase 11.14.0–specific, single canonical “Tuist module verifier settings requirements” document that enumerates exact Tuist settings to configure for 11.14.0 specifically. Instead, the sources describe (1) general Xcode module verifier build settings [4], and (2) Tuist/Firebase integration failures and fixes centered on module-map + DEFINES_MODULE behavior [7][6][5], which is the most direct “requirement” relationship to module verifier/module map issues. If you share your Tuist snippet (how you declare Firebase as.remote/.external, and whether you’re using Tuist’s swiftPackageManager dependency integration or XcodeProj-based integration) and the exact module verifier warning/error text, I can map it to the correct Tuist targetSettings adjustments consistent with the above evidence. Citations:
Firebase 모듈 검증기 비활성화(
🤖 Prompt for AI Agents |
||
| ] | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| import FirebaseCore | ||
| import FirebaseMessaging | ||
| import Logger | ||
|
|
||
| public final class FCMManager: NSObject, FCMTokenProviding { | ||
| public static let shared = FCMManager() | ||
|
|
||
| private let tokenStore = FCMTokenStore() | ||
|
|
||
| public func currentToken() async -> String? { | ||
| await tokenStore.currentToken() | ||
| } | ||
|
|
||
| // swiftformat:disable:next modifierOrder | ||
| private override init() { | ||
| super.init() | ||
| } | ||
|
|
||
| // MARK: - Token | ||
|
|
||
| public func fcmTokenUpdates() -> AsyncStream<String> { | ||
| let tokenStore = tokenStore | ||
|
|
||
| return AsyncStream { continuation in | ||
| let streamID = UUID() | ||
|
|
||
| Task { | ||
| await tokenStore.registerStream( | ||
| id: streamID, | ||
| continuation: continuation | ||
| ) | ||
| } | ||
|
|
||
| continuation.onTermination = { @Sendable _ in | ||
| Task { | ||
| await tokenStore.unregisterStream(id: streamID) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Bootstrap | ||
|
|
||
| public func configure() { | ||
| guard FirebaseApp.app() == nil else { return } | ||
|
|
||
| FirebaseApp.configure() | ||
| Messaging.messaging().delegate = self | ||
|
|
||
| Logger.shared.info("Firebase 초기화 완료", category: .general) | ||
| } | ||
|
|
||
| public func setAPNSToken(_ deviceToken: Data) { | ||
| Messaging.messaging().apnsToken = deviceToken | ||
|
|
||
| Logger.shared.info( | ||
| "APNs device token 등록 완료", | ||
| category: .general | ||
| ) | ||
| } | ||
|
|
||
| private func deliverToken(_ token: String) { | ||
| Task { | ||
| let isNewToken = await tokenStore.updateToken(token) | ||
| guard isNewToken else { return } | ||
|
|
||
| #if DEBUG | ||
| Logger.shared.debug("FCM 토큰: \(token)", category: .general) | ||
| #else | ||
| Logger.shared.info("FCM 토큰 갱신됨", category: .general) | ||
| #endif | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // MARK: - MessagingDelegate | ||
|
|
||
| extension FCMManager: MessagingDelegate { | ||
| public func messaging( | ||
| _ messaging: Messaging, | ||
| didReceiveRegistrationToken fcmToken: String? | ||
| ) { | ||
| guard let fcmToken else { return } | ||
|
|
||
| deliverToken(fcmToken) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import Foundation | ||
|
|
||
| public protocol FCMTokenProviding: AnyObject { | ||
| func currentToken() async -> String? | ||
|
|
||
| /// 토큰 갱신 스트림. 구독 시점에 이미 토큰이 있으면 즉시 한 번 방출한다. | ||
| func fcmTokenUpdates() -> AsyncStream<String> | ||
|
|
||
| func configure() | ||
| func setAPNSToken(_ deviceToken: Data) | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.