diff --git a/project/Projects/App/Idle-iOS.entitlements b/project/Entitlements/App/Idle-iOS.entitlements similarity index 70% rename from project/Projects/App/Idle-iOS.entitlements rename to project/Entitlements/App/Idle-iOS.entitlements index 0c67376e..903def2a 100644 --- a/project/Projects/App/Idle-iOS.entitlements +++ b/project/Entitlements/App/Idle-iOS.entitlements @@ -1,5 +1,8 @@ - + + aps-environment + development + diff --git a/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/InfoPlist.swift b/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/InfoPlist.swift index 6fb1f096..36a607b6 100644 --- a/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/InfoPlist.swift +++ b/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/InfoPlist.swift @@ -46,9 +46,6 @@ public enum IdleInfoPlist { ], "NMFClientId": "$(NAVER_API_CLIENT_ID)", - - // 앱추적 허용 메세지 - "NSUserTrackingUsageDescription": "사용자 맞춤 서비스 제공을 위해 권한을 허용해 주세요. 권한을 허용하지 않을 경우, 앱 사용에 제약이 있을 수 있습니다.", // 네트워크 사용 메세지 "NSLocalNetworkUsageDescription": "이 앱은 로컬 네트워크를 통해 서버에 연결하여 데이터를 주고받기 위해 로컬 네트워크 접근 권한이 필요합니다." diff --git a/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency.swift b/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency.swift index 85e41042..38d20d45 100644 --- a/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency.swift +++ b/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency.swift @@ -48,11 +48,14 @@ public extension ModuleDependency { public static let RxMoya: TargetDependency = .external(name: "RxMoya") public static let FSCalendar: TargetDependency = .external(name: "FSCalendar") public static let NaverMapSDKForSPM: TargetDependency = .external(name: "Junios.NMapSDKForSPM") + public static let Amplitude: TargetDependency = .external(name: "AmplitudeSwift") + public static let SDWebImageWebPCoder: TargetDependency = .external(name: "SDWebImageWebPCoder") + + // FireBase public static let FirebaseRemoteConfig: TargetDependency = .external(name: "FirebaseRemoteConfig") public static let FirebaseCrashlytics: TargetDependency = .external(name: "FirebaseCrashlytics") public static let FirebaseAnalytics: TargetDependency = .external(name: "FirebaseAnalytics") - public static let Amplitude: TargetDependency = .external(name: "AmplitudeSwift") - public static let SDWebImageWebPCoder: TargetDependency = .external(name: "SDWebImageWebPCoder") + public static let FirebaseMessaging: TargetDependency = .external(name: "FirebaseMessaging") } } diff --git a/project/Projects/App/Project.swift b/project/Projects/App/Project.swift index 1d467150..e26c09f2 100644 --- a/project/Projects/App/Project.swift +++ b/project/Projects/App/Project.swift @@ -27,6 +27,7 @@ let project = Project( infoPlist: IdleInfoPlist.mainApp, sources: ["Sources/**"], resources: ["Resources/**"], + entitlements: .file(path: .relativeToRoot("Entitlements/App/Idle-iOS.entitlements")), scripts: [ .crashlyticsScript ], @@ -43,6 +44,9 @@ let project = Project( // Logger D.App.ConcreteLogger, + + // ThirdParty + D.ThirdParty.FirebaseMessaging, ], settings: .settings( configurations: IdleConfiguration.appConfigurations diff --git a/project/Projects/App/Sources/AppDelegate.swift b/project/Projects/App/Sources/AppDelegate.swift index 69eba4b1..10f8f43a 100644 --- a/project/Projects/App/Sources/AppDelegate.swift +++ b/project/Projects/App/Sources/AppDelegate.swift @@ -10,19 +10,23 @@ import AppTrackingTransparency import AdSupport import PresentationCore import FirebaseCore +import UserNotifications + @main -class AppDelegate: UIResponder, UIApplicationDelegate { +class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: { [weak self] in - self?.requestTrackingAuthorization() - }) // FireBase setting FirebaseApp.configure() + // 앱실행시 알람수신 동의를 받음 + UNUserNotificationCenter.current().delegate = self + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound], completionHandler: { _, _ in }) + + application.registerForRemoteNotifications() + return true } @@ -39,29 +43,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } - - private func requestTrackingAuthorization() { - ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in - switch status { - case .authorized: - // Tracking authorization dialog was shown - // and we are authorized - printIfDebug("앱추적권한: Authorized") - - // 추적을 허용한 사용자 식별자 - printIfDebug(ASIdentifierManager.shared().advertisingIdentifier) - case .denied: - // Tracking authorization dialog was - // shown and permission is denied - printIfDebug("앱추적권한: Denied") - case .notDetermined: - // Tracking authorization dialog has not been shown - printIfDebug("앱추적권한: Not Determined") - case .restricted: - printIfDebug("앱추적권한: Restricted") - @unknown default: - printIfDebug("앱추적권한: Unknown") - } - }) - } + } diff --git a/project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift b/project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift index b0ad4a5e..3b24b323 100644 --- a/project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift +++ b/project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift @@ -82,5 +82,9 @@ public struct DomainAssembly: Assembly { userInfoLocalRepository: userInfoLocalRepository ) } + + container.register(NotificationUseCase.self) { resolver in + DefaultNotificationUseCase() + } } } diff --git a/project/Projects/App/Sources/RemoteNotification/FCMService.swift b/project/Projects/App/Sources/RemoteNotification/FCMService.swift new file mode 100644 index 00000000..2df4dd9f --- /dev/null +++ b/project/Projects/App/Sources/RemoteNotification/FCMService.swift @@ -0,0 +1,91 @@ +// +// FCMService.swift +// Idle-iOS +// +// Created by choijunios on 9/24/24. +// + +import Foundation +import BaseFeature +import UseCaseInterface +import PresentationCore + + +import FirebaseMessaging + +class FCMService: NSObject { + + @Injected var notificationUseCase: NotificationUseCase + + override public init() { + super.init() + Messaging.messaging().delegate = self + + + // Notification설정 + subscribeNotification() + } + + func subscribeNotification() { + + NotificationCenter.default.addObserver( + forName: .requestTransportTokenToServer, + object: nil, + queue: nil) { [weak self] _ in + + guard let self else { return } + + if let token = Messaging.messaging().fcmToken { + + notificationUseCase.setNotificationToken( + token: token) { result in + + print("FCMService 토큰 전송 \(result ? "완료" : "실패")") + } + } + } + + NotificationCenter.default.addObserver( + forName: .requestDeleteTokenFromServer, + object: nil, + queue: nil) { [weak self] _ in + + guard let self else { return } + + notificationUseCase.deleteNotificationToken(completion: { result in + print("FCMService 토큰 삭제 \(result ? "완료" : "실패")") + }) + } + } +} + +extension FCMService: MessagingDelegate { + + func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { + + if let fcmToken { + + print("FCM토큰: \(fcmToken)") + + notificationUseCase.setNotificationToken(token: fcmToken) { isSuccess in + + print(isSuccess ? "토큰 전송 성공" : "토큰 전송 실패") + } + } + } +} + + +extension FCMService: UNUserNotificationCenterDelegate { + + /// 앱이 포그라운드에 있는 경우, 노티페이케이션이 도착하기만 하면 호출된다. + public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + + } + + /// 앱이 백그라운드에 있는 경우, 유저가 노티피케이션을 통해 액션을 선택한 경우 호출 + public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + + print(response.notification.request.content.userInfo) + } +} diff --git a/project/Projects/App/Sources/SceneDelegate.swift b/project/Projects/App/Sources/SceneDelegate.swift index 102b573f..95bd3753 100644 --- a/project/Projects/App/Sources/SceneDelegate.swift +++ b/project/Projects/App/Sources/SceneDelegate.swift @@ -12,7 +12,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - var rootCoordinator: RootCoordinator? + // RootCoordinator + var rootCoordinator: RootCoordinator! + + // FCMService + var fcmService: FCMService! func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { @@ -28,8 +32,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { LoggerAssembly(), DataAssembly(), DomainAssembly(), - ]) + ]) + + // FCMService + fcmService = FCMService() + // RootCoordinator rootCoordinator = RootCoordinator( dependency: .init( navigationController: rootNavigationController, diff --git a/project/Projects/Data/ConcretesTests/ImageCachingTest.swift b/project/Projects/Data/ConcretesTests/ImageCachingTest.swift index f8369bfd..69d00a7e 100644 --- a/project/Projects/Data/ConcretesTests/ImageCachingTest.swift +++ b/project/Projects/Data/ConcretesTests/ImageCachingTest.swift @@ -33,6 +33,8 @@ class ImageCachingTest: XCTestCase { } } + return + // 디스크 캐싱 내역 삭제 _ = cacheRepository.clearImageCacheDirectory() diff --git a/project/Projects/Domain/ConcreteUseCase/Notification/DefaultNotificationUseCase.swift b/project/Projects/Domain/ConcreteUseCase/Notification/DefaultNotificationUseCase.swift new file mode 100644 index 00000000..a01da62a --- /dev/null +++ b/project/Projects/Domain/ConcreteUseCase/Notification/DefaultNotificationUseCase.swift @@ -0,0 +1,26 @@ +// +// DefaultNotificationUseCase.swift +// ConcreteUseCase +// +// Created by choijunios on 9/26/24. +// + +import Foundation +import UseCaseInterface + +public class DefaultNotificationUseCase: NotificationUseCase { + + public init() { } + + public func setNotificationToken(token: String, completion: @escaping (Bool) -> ()) { + + //TODO: 구체적 스팩 산정 후 구현 + completion(true) + } + + public func deleteNotificationToken(completion: @escaping (Bool) -> ()) { + + //TODO: 구체적 스팩 산정 후 구현 + completion(true) + } +} diff --git a/project/Projects/Domain/UseCaseInterface/RemoteNotification/NotificationUseCase.swift b/project/Projects/Domain/UseCaseInterface/RemoteNotification/NotificationUseCase.swift new file mode 100644 index 00000000..71793b82 --- /dev/null +++ b/project/Projects/Domain/UseCaseInterface/RemoteNotification/NotificationUseCase.swift @@ -0,0 +1,17 @@ +// +// NotificationUseCase.swift +// UseCaseInterface +// +// Created by choijunios on 9/26/24. +// + +import Foundation + +public protocol NotificationUseCase { + + /// 유저와 매치되는 노티피케이션 토큰을 서버로 전송합니다. + func setNotificationToken(token: String, completion: @escaping (Bool) -> ()) + + /// 유저와 매치되는 노티피케이션 토큰을 서버로부터 제거합니다. + func deleteNotificationToken(completion: @escaping (Bool) -> ()) +} diff --git a/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Worker/WorkerRegisterCoordinator.swift b/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Worker/WorkerRegisterCoordinator.swift index ec0ccb09..731974af 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Worker/WorkerRegisterCoordinator.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Worker/WorkerRegisterCoordinator.swift @@ -12,8 +12,8 @@ import PresentationCore enum WorkerRegisterStage: Int { case registerFinished=0 - case info=1 - case phoneNumber=2 + case phoneNumber=1 + case info=2 case address=3 case finish=4 @@ -21,10 +21,10 @@ enum WorkerRegisterStage: Int { switch self { case .registerFinished: "" - case .info: - "input|personalInfo" case .phoneNumber: "input|phoneNumber" + case .info: + "input|personalInfo" case .address: "input|address" case .finish: @@ -94,7 +94,7 @@ public class WorkerRegisterCoordinator: ChildCoordinator { navigationController.pushViewController(vc, animated: true) - excuteStage(.info, moveTo: .next) + excuteStage(.phoneNumber, moveTo: .next) // MARK: 시작 로깅 logger.startWorkerRegister() diff --git a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+PhoneNumber.swift b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+PhoneNumber.swift index 323c8d85..f23ccfcb 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+PhoneNumber.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+PhoneNumber.swift @@ -95,6 +95,10 @@ public extension AuthInOutStreamManager { output.loginSuccess = loginResult .compactMap { $0.value } .map { phoneNumber in + + // 원격 알림 토큰 전송 + NotificationCenter.default.post(name: .requestTransportTokenToServer, object: nil) + printIfDebug("✅ 요양보호사 로그인 성공") return () } diff --git a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Login/CenterLoginViewModel.swift b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Login/CenterLoginViewModel.swift index 1b56836f..1bd598ac 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Login/CenterLoginViewModel.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Login/CenterLoginViewModel.swift @@ -5,14 +5,17 @@ // Created by choijunios on 7/10/24. // -import RxSwift +import Foundation import BaseFeature -import RxCocoa import UseCaseInterface import RepositoryInterface import Entity import PresentationCore + +import RxSwift +import RxCocoa + public class CenterLoginViewModel: BaseViewModel, ViewModelType { // Init @@ -60,6 +63,9 @@ public class CenterLoginViewModel: BaseViewModel, ViewModelType { .subscribe(onNext: { [weak self] _ in guard let self else { return } + // 원격 알림 토큰 저장요청 + NotificationCenter.default.post(name: .requestTransportTokenToServer, object: nil) + self.coordinator?.authFinished() }) .disposed(by: disposeBag) diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Setting/CenterSettingVM.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Setting/CenterSettingVM.swift index c0777fad..53c9a415 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Setting/CenterSettingVM.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Setting/CenterSettingVM.swift @@ -161,6 +161,10 @@ public class CenterSettingVM: BaseViewModel, CenterSettingVMable { signOutSuccess .subscribe(onNext: { [weak self] _ in + + // 로그이아웃 성공 -> 원격알림 토큰 제거 + NotificationCenter.default.post(name: .requestDeleteTokenFromServer, object: nil) + self?.coordinator?.popToRoot() }) .disposed(by: disposeBag) diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Setting/PasswordForDeregisterVM.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Setting/PasswordForDeregisterVM.swift index 70cafdbd..d3f1a7fe 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Setting/PasswordForDeregisterVM.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Setting/PasswordForDeregisterVM.swift @@ -5,11 +5,14 @@ // Created by choijunios on 8/21/24. // +import Foundation import BaseFeature import UseCaseInterface +import Entity + + import RxCocoa import RxSwift -import Entity public class PasswordForDeregisterVM: BaseViewModel { @@ -44,6 +47,9 @@ public class PasswordForDeregisterVM: BaseViewModel { .observe(on: MainScheduler.asyncInstance) .subscribe(onNext: { [weak self] _ in + // 회원탈퇴 성공 -> 원격알림 토큰 제거 + NotificationCenter.default.post(name: .requestDeleteTokenFromServer, object: nil) + // RootCoordinator로 이동 self?.coordinator?.popToRoot() }) diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/PhoneNumberValidationForDeregisterVM.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/PhoneNumberValidationForDeregisterVM.swift index df0bb6ad..148744f0 100644 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/PhoneNumberValidationForDeregisterVM.swift +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/PhoneNumberValidationForDeregisterVM.swift @@ -80,6 +80,9 @@ class PhoneNumberValidationForDeregisterVM: BaseViewModel, PhoneNumberValidation .observe(on: MainScheduler.asyncInstance) .subscribe(onNext: { [weak self] _ in + // 회원탈퇴 성공 -> 원격알림 토큰 제거 + NotificationCenter.default.post(name: .requestDeleteTokenFromServer, object: nil) + // RootCoordinator로 이동 self?.coordinator?.popToRoot() }) diff --git a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Seting/WorkerSettingVM.swift b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Seting/WorkerSettingVM.swift index 49593ca9..00a7a347 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Seting/WorkerSettingVM.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Seting/WorkerSettingVM.swift @@ -153,6 +153,10 @@ public class WorkerSettingVM: BaseViewModel, WorkerSettingVMable { signOutSuccess .subscribe(onNext: { [weak self] _ in + + // 로그이아웃 성공 -> 원격알림 토큰 제거 + NotificationCenter.default.post(name: .requestDeleteTokenFromServer, object: nil) + self?.coordinator?.popToRoot() }) .disposed(by: disposeBag) diff --git a/project/Projects/Presentation/PresentationCore/Sources/NotificationName+Extension/RemoteNotification.swift b/project/Projects/Presentation/PresentationCore/Sources/NotificationName+Extension/RemoteNotification.swift new file mode 100644 index 00000000..a4f64697 --- /dev/null +++ b/project/Projects/Presentation/PresentationCore/Sources/NotificationName+Extension/RemoteNotification.swift @@ -0,0 +1,14 @@ +// +// RemoteNotification.swift +// PresentationCore +// +// Created by choijunios on 9/26/24. +// + +import Foundation + +public extension Notification.Name { + + static let requestTransportTokenToServer: Self = .init("requestTransportTokenToServer") + static let requestDeleteTokenFromServer: Self = .init("requestDeleteTokenFromServer") +}