Skip to content
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

[IDLE-393] FCM SDK를 설치하고 해당 기능을 담당하는 객체를 만든다. #75

Merged
merged 5 commits into from
Sep 26, 2024
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
Original file line number Diff line number Diff line change
@@ -1,5 +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/>
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,6 @@ public enum IdleInfoPlist {
],

"NMFClientId": "$(NAVER_API_CLIENT_ID)",

// 앱추적 허용 메세지
"NSUserTrackingUsageDescription": "사용자 맞춤 서비스 제공을 위해 권한을 허용해 주세요. 권한을 허용하지 않을 경우, 앱 사용에 제약이 있을 수 있습니다.",

// 네트워크 사용 메세지
"NSLocalNetworkUsageDescription": "이 앱은 로컬 네트워크를 통해 서버에 연결하여 데이터를 주고받기 위해 로컬 네트워크 접근 권한이 필요합니다."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}

4 changes: 4 additions & 0 deletions project/Projects/App/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
],
Expand All @@ -43,6 +44,9 @@ let project = Project(

// Logger
D.App.ConcreteLogger,

// ThirdParty
D.ThirdParty.FirebaseMessaging,
],
settings: .settings(
configurations: IdleConfiguration.appConfigurations
Expand Down
40 changes: 10 additions & 30 deletions project/Projects/App/Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

푸시알림 권한 요청은 앱 실행시 바로하는것보다
앱 사용 중 알림과 관련된 페이지에서 하는게 사용자 경험에 좋습니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

푸시알림 권한 요청은 앱 실행시 바로하는것보다 앱 사용 중 알림과 관련된 페이지에서 하는게 사용자 경험에 좋습니다.

넵 해당부분 팀원들과 논의해보겠습니다!


application.registerForRemoteNotifications()

return true
}

Expand All @@ -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")
}
})
}

}
4 changes: 4 additions & 0 deletions project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,9 @@ public struct DomainAssembly: Assembly {
userInfoLocalRepository: userInfoLocalRepository
)
}

container.register(NotificationUseCase.self) { resolver in
DefaultNotificationUseCase()
}
}
}
91 changes: 91 additions & 0 deletions project/Projects/App/Sources/RemoteNotification/FCMService.swift
Original file line number Diff line number Diff line change
@@ -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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로그아웃, 회원탈퇴 이후 호출되는 코드일 것 같습니다.
여기에서 FCM토큰을 없애는 작업을 진행하도록 함수 호출을 하고 있는데(내부 구현체는 없지만) 아마 내부 구현이 추가되면 정상동작 하지 않을거에요.
그 이유를 고민해봅시다~!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이유를 잘 모르겠습니다 멘토님🥲 힌트 주실 수 있으실까요?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

먼저 구현체에 어떤 내용을 작성할지 생각해봅시다 😎

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

구현체에는 서버로부터 FCM을 전송하기위한 객체와 유저정보를 가져오는 객체가 필요할 것 같습니다! 먼저 유저정보를 가져온 후 토큰을 서버로 전송하는 방법으로 개발할 것 같습니다

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)
}
}
12 changes: 10 additions & 2 deletions project/Projects/App/Sources/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand All @@ -28,8 +32,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
LoggerAssembly(),
DataAssembly(),
DomainAssembly(),
])
])

// FCMService
fcmService = FCMService()

// RootCoordinator
rootCoordinator = RootCoordinator(
dependency: .init(
navigationController: rootNavigationController,
Expand Down
2 changes: 2 additions & 0 deletions project/Projects/Data/ConcretesTests/ImageCachingTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class ImageCachingTest: XCTestCase {
}
}

return

// 디스크 캐싱 내역 삭제
_ = cacheRepository.clearImageCacheDirectory()

Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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) -> ())
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ 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

var screenName: String {
switch self {
case .registerFinished:
""
case .info:
"input|personalInfo"
case .phoneNumber:
"input|phoneNumber"
case .info:
"input|personalInfo"
case .address:
"input|address"
case .finish:
Expand Down Expand Up @@ -94,7 +94,7 @@ public class WorkerRegisterCoordinator: ChildCoordinator {

navigationController.pushViewController(vc, animated: true)

excuteStage(.info, moveTo: .next)
excuteStage(.phoneNumber, moveTo: .next)

// MARK: 시작 로깅
logger.startWorkerRegister()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading