-
Notifications
You must be signed in to change notification settings - Fork 0
feat: #42 알림 설정 화면 + 도리 등록 알림 권한 안내 #46
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
d00d32c
bc3ee6e
42ebf70
e902565
d179a15
d3f9f6f
d61b92c
c6ef65e
f19adce
692da2b
31a6f1d
469c4e2
fb86203
3ecf539
353ea2a
c5c4a6a
b27b3df
e1bb3ac
3809163
5938e0d
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 |
|---|---|---|
|
|
@@ -39,3 +39,4 @@ Derived/ | |
| *.xcodeproj | ||
| *.xcworkspace | ||
| .claude/worktrees | ||
| GoogleService-Info.plist | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| <?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> | ||
| </dict> | ||
| </plist> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| <?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> | ||
| </dict> | ||
| </plist> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| // | ||
| // AppDelegate.swift | ||
| // Dori-iOS | ||
| // | ||
| // Created by 강동영 on 3/25/26. | ||
| // | ||
|
|
||
| import UIKit | ||
| import PlatformFCM | ||
|
|
||
| final class AppDelegate: NSObject, UIApplicationDelegate { | ||
|
|
||
| func application( | ||
| _ application: UIApplication, | ||
| didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil | ||
| ) -> Bool { | ||
| FCMService.shared.configure() | ||
|
Collaborator
Author
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.
|
||
| return true | ||
| } | ||
|
|
||
| // MARK: - APNs Token | ||
|
|
||
| func application( | ||
| _ application: UIApplication, | ||
| didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data | ||
| ) { | ||
| let token = deviceToken.map { String(format: "%02x", $0) }.joined() | ||
| print("APNS Token: \"\(token)\"") | ||
| FCMService.shared.setAPNSToken(deviceToken) | ||
| } | ||
|
|
||
| func application( | ||
| _ application: UIApplication, | ||
| didFailToRegisterForRemoteNotificationsWithError error: Error | ||
| ) { | ||
| print("APNs 등록 실패: \(error.localizedDescription)") | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,9 +17,11 @@ import FeatureHistory | |
| import FeatureCalendar | ||
| import PlatformKakaoAuth | ||
| import PlatformKeychain | ||
| import PlatformFCM | ||
|
|
||
| @main | ||
| struct DoriApp: App { | ||
| @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate | ||
| let store: StoreOf<AppFeature> | ||
| #if DEBUG | ||
| private let debugLaunchRoute: DebugLaunchRoute? | ||
|
|
@@ -75,13 +77,24 @@ struct DoriApp: App { | |
| networkService: networkService, | ||
| tokenStore: tokenStore | ||
| ) | ||
| $0.fcmPushTestAPIClient = .live(networkService: networkService) | ||
| $0.notificationSettingsAPIClient = .live(networkService: networkService) | ||
| } | ||
|
|
||
| storeBox.store = store | ||
| self.store = store | ||
|
|
||
| FontManager.registerAllFonts() | ||
| KakaoSDKHandler.initializeFromMainBundle() | ||
|
|
||
| FCMService.shared.tokenRefreshHandler = { token in | ||
| try? tokenStore.saveFCMToken(token) | ||
| let endpoint = RegisterFCMTokenEndpoint(token: token) | ||
| _ = try? await networkService.request( | ||
|
Collaborator
Author
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. FCM 토큰 등록 실패를 |
||
| endpoint, | ||
| responseType: SuccessResponse<EmptyResponse>.self | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| var body: some Scene { | ||
|
|
@@ -109,16 +122,21 @@ struct DoriApp: App { | |
| .onOpenURL { url in | ||
| _ = KakaoSDKHandler.handleOpenURL(url) | ||
| } | ||
| .task { | ||
| await FCMService.shared.requestAuthorization() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #if DEBUG | ||
| private struct DebugLaunchRoute { | ||
| private let environment: [String: String] | ||
| private let route: String | ||
| private let memo: String | ||
|
|
||
| init?(environment: [String: String]) { | ||
| guard let route = environment["DORI_DEBUG_ROUTE"] else { return nil } | ||
| self.environment = environment | ||
| self.route = route | ||
| self.memo = environment["DORI_DEBUG_MEMO"] ?? "" | ||
| } | ||
|
|
@@ -132,6 +150,11 @@ private struct DebugLaunchRoute { | |
| AddDoriView( | ||
| store: Store(initialState: configuredState) { | ||
| AddDoriFeature() | ||
| } withDependencies: { | ||
| $0.addDoriAPIClient = .testValue | ||
| $0.userNotificationSettingsClient.isNotificationEnabled = { | ||
| environment["DORI_DEBUG_NOTIFICATION_ENABLED"] == "true" | ||
| } | ||
| } | ||
| ) | ||
| } | ||
|
|
@@ -144,6 +167,8 @@ private struct DebugLaunchRoute { | |
| private var configuredState: AddDoriFeature.State { | ||
| var state = AddDoriFeature.State() | ||
| state.currentPage = 2 | ||
| state.searchQuery = environment["DORI_DEBUG_PARTNER_NAME"] ?? "김철수" | ||
| state.amountInput.text = environment["DORI_DEBUG_AMOUNT"] ?? "100000" | ||
| state.memo = memo | ||
| return state | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| // | ||
| // TestUIBase.swift | ||
| // DoriAppUITests | ||
| // | ||
| // Created by Codex on 4/27/26. | ||
| // | ||
|
|
||
| import XCTest | ||
|
|
||
| @MainActor | ||
| class TestUIBase: XCTestCase { | ||
| let app = XCUIApplication() | ||
|
|
||
| override func setUpWithError() throws { | ||
| continueAfterFailure = false | ||
| } | ||
|
|
||
| override func tearDownWithError() throws { | ||
| app.terminate() | ||
| } | ||
|
|
||
| func waitForExistence( | ||
| _ element: XCUIElement, | ||
| timeout: TimeInterval = 5, | ||
| file: StaticString = #filePath, | ||
| line: UInt = #line | ||
| ) { | ||
| XCTAssertTrue(element.waitForExistence(timeout: timeout), file: file, line: line) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| // | ||
| // XCTestExpectation+.swift | ||
| // DoriAppUITests | ||
| // | ||
| // Created by Codex on 4/27/26. | ||
| // | ||
|
|
||
| import XCTest | ||
|
|
||
| extension XCTestCase { | ||
| func waitBriefly(seconds: TimeInterval = 0.3) { | ||
| let expectation = expectation(description: "brief wait") | ||
| DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { | ||
| expectation.fulfill() | ||
| } | ||
| wait(for: [expectation], timeout: seconds + 1) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| // | ||
| // UITestProtocols.swift | ||
| // DoriAppUITests | ||
| // | ||
| // Created by Codex on 4/27/26. | ||
| // | ||
|
|
||
| import XCTest | ||
|
|
||
| @MainActor | ||
| protocol UITestPage { | ||
| var app: XCUIApplication { get } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| // | ||
| // TestIdentifier.swift | ||
| // DoriAppUITests | ||
| // | ||
| // Created by Codex on 4/27/26. | ||
| // | ||
|
|
||
| enum TestIdentifier { | ||
| enum LaunchEnvironment { | ||
| static let debugRoute = "DORI_DEBUG_ROUTE" | ||
| static let notificationEnabled = "DORI_DEBUG_NOTIFICATION_ENABLED" | ||
| static let partnerName = "DORI_DEBUG_PARTNER_NAME" | ||
| static let amount = "DORI_DEBUG_AMOUNT" | ||
| } | ||
|
|
||
| enum DebugRoute { | ||
| static let addDoriPage3 = "addDoriPage3" | ||
| } | ||
|
|
||
| enum AddDori { | ||
| static let submitButton = "완료" | ||
| static let notificationAlertTitle = "알림을 켜주세요" | ||
| static let notificationAlertDescription = "도리 일정을 놓치지 않도록\n시스템 알림 설정을 켜주세요" | ||
| static let openSettingsButton = "설정하기" | ||
| static let laterButton = "나중에" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| // | ||
| // TestUIScenarioAddDoriNotificationSettings.swift | ||
| // DoriAppUITests | ||
| // | ||
| // Created by Codex on 4/27/26. | ||
| // | ||
|
|
||
| import XCTest | ||
|
|
||
| final class TestUIScenarioAddDoriNotificationSettings: TestUIBase { | ||
| func testShowNotificationSettingsAlertWhenSystemNotificationIsOffAfterAddingDori() { | ||
| app.launchEnvironment[TestIdentifier.LaunchEnvironment.debugRoute] = TestIdentifier.DebugRoute.addDoriPage3 | ||
| app.launchEnvironment[TestIdentifier.LaunchEnvironment.notificationEnabled] = "false" | ||
| app.launchEnvironment[TestIdentifier.LaunchEnvironment.partnerName] = "김철수" | ||
| app.launchEnvironment[TestIdentifier.LaunchEnvironment.amount] = "100000" | ||
| app.launch() | ||
|
|
||
| let addDoriView = UIBaseAddDoriView(app: app) | ||
|
|
||
| waitForExistence(addDoriView.submitButton) | ||
| addDoriView.submit() | ||
|
|
||
| waitForExistence(addDoriView.notificationAlertTitle) | ||
| waitForExistence(addDoriView.notificationAlertDescription) | ||
| waitForExistence(addDoriView.openSettingsButton) | ||
| waitForExistence(addDoriView.laterButton) | ||
|
|
||
| addDoriView.laterButton.tap() | ||
| waitBriefly() | ||
|
|
||
| XCTAssertFalse(addDoriView.notificationAlertTitle.exists) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| // | ||
| // UIBaseAddDoriView.swift | ||
| // DoriAppUITests | ||
| // | ||
| // Created by Codex on 4/27/26. | ||
| // | ||
|
|
||
| import XCTest | ||
|
|
||
| @MainActor | ||
| final class UIBaseAddDoriView: UITestPage { | ||
| let app: XCUIApplication | ||
|
|
||
| init(app: XCUIApplication) { | ||
| self.app = app | ||
| } | ||
|
|
||
| var submitButton: XCUIElement { | ||
| app.buttons[TestIdentifier.AddDori.submitButton] | ||
| } | ||
|
|
||
| var notificationAlertTitle: XCUIElement { | ||
| app.staticTexts[TestIdentifier.AddDori.notificationAlertTitle] | ||
| } | ||
|
|
||
| var notificationAlertDescription: XCUIElement { | ||
| app.staticTexts[TestIdentifier.AddDori.notificationAlertDescription] | ||
| } | ||
|
|
||
| var openSettingsButton: XCUIElement { | ||
| app.buttons[TestIdentifier.AddDori.openSettingsButton] | ||
| } | ||
|
|
||
| var laterButton: XCUIElement { | ||
| app.buttons[TestIdentifier.AddDori.laterButton] | ||
| } | ||
|
|
||
| func submit() { | ||
| submitButton.tap() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| // | ||
| // BuildEnvironment.swift | ||
| // Dori-iOS | ||
| // | ||
| // Created by 강동영 on 4/18/26. | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| public enum BuildEnvironment: String, Sendable { | ||
| case debug | ||
| case qa | ||
| case release | ||
|
|
||
| public static let current: BuildEnvironment = { | ||
| #if DEBUG | ||
| return .debug | ||
| #elseif QA | ||
| return .qa | ||
| #else | ||
| return .release | ||
| #endif | ||
| }() | ||
|
|
||
| public var isTestingEnabled: Bool { | ||
| switch self { | ||
| case .debug, .qa: return true | ||
| case .release: return false | ||
| } | ||
| } | ||
|
|
||
| public var displayName: String { | ||
| switch self { | ||
| case .debug: return "DEBUG" | ||
| case .qa: return "QA" | ||
| case .release: return "RELEASE" | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
앱 타겟이 항상
Resources/DoriApp.entitlements를 사용하고, 해당 파일의aps-environment가development로 고정되어 있습니다. Release/App Store 배포에서는 production APNs entitlement가 필요하므로 배포 서명 실패나 푸시 환경 불일치가 날 수 있습니다. Debug/Release 별 entitlements를 분리하거나 빌드 설정으로aps-environment를 맞춰주세요.