Skip to content

Commit

Permalink
[Feature/#275] 알림 권한 미허용 시 alert 추가 (#276)
Browse files Browse the repository at this point in the history
* feat: AppDelegate 푸시 수신 상태 Notification 등록

* feat: UserClient 푸시 알림 허용 상태 로직 추가

* feat: 푸시 알림 허용 상태에 따른 알림설정 화면 로직 구현

* feat: UserClient 푸쉬알림허용상태 Publisher 구현

* feat: 푸쉬알림허용상태에 따른 로직 변경

* feat: 토글 버튼 binding 코드 개선

- 코드리뷰 반영

* feat: UserClient UserDefaultKeys enum 추가

* feat: 오탈자 수정

- pushNotificationSubject -> pushNotificationAllowStatusSubject
  • Loading branch information
leemhyungyu authored Sep 25, 2024
1 parent 9a14959 commit e130ab9
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 46 deletions.
42 changes: 40 additions & 2 deletions Projects/App/Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate {
UIApplication.shared.registerForRemoteNotifications()
UNUserNotificationCenter.current().delegate = self
Messaging.messaging().delegate = self

setNotification()
application.registerForRemoteNotifications()

store.send(.appDelegate(.didFinishLunching))
return true
}
}

// MARK: - UNUserNotificationCenterDelegate
extension AppDelegate: UNUserNotificationCenterDelegate {
func messaging(
_ messaging: Messaging,
Expand All @@ -62,3 +62,41 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
return [.badge, .sound, .banner, .list]
}
}

// MARK: - objc funcs
private extension AppDelegate {
@objc func checkPushNotificationStatus() {
UNUserNotificationCenter.current()
.getNotificationSettings { [weak self] permission in
guard let self = self else { return }
DispatchQueue.main.async {
switch permission.authorizationStatus {
case .notDetermined:
self.store.send(.appDelegate(.pushNotificationAllowStatusDidChanged(isAllow: true)))
case .denied:
self.store.send(.appDelegate(.pushNotificationAllowStatusDidChanged(isAllow: false)))
case .authorized:
self.store.send(.appDelegate(.pushNotificationAllowStatusDidChanged(isAllow: true)))
case .provisional:
self.store.send(.appDelegate(.pushNotificationAllowStatusDidChanged(isAllow: false)))
case .ephemeral:
self.store.send(.appDelegate(.pushNotificationAllowStatusDidChanged(isAllow: true)))
@unknown default:
Log.error("Unknow Notification Status")
}
}
}
}
}

// MARK: - Private Methods
private extension AppDelegate {
func setNotification() {
NotificationCenter.default.addObserver(
self,
selector: #selector(checkPushNotificationStatus),
name: UIApplication.willEnterForegroundNotification,
object: nil
)
}
}
22 changes: 22 additions & 0 deletions Projects/Domain/User/Interface/Sources/UserClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,26 @@

import Foundation

import Combine

public struct UserClient {
private let _isLoggedIn: () -> Bool
private let _isAppDeleted: () -> Bool
private let _fetchFcmToken: () -> String?
private let updateLoginState: (Bool) -> Void
private let updateDeleteState: (Bool) -> Void
private let updateFcmToken: (String) -> Void
private let updatePushNotificationAllowStatus: (Bool) -> Void
private let _fetchAlertState: () async throws -> [UserAlertState]
private let _fetchPushNotificationAllowStatus: () -> Bool
private let updateAlertState: (UserAlertState) async throws -> Void
private let fetchContacts: () async throws -> [String]
private let updateBlockContacts: ([String]) async throws -> Void
private let pushNotificationAllowStatusSubject = CurrentValueSubject<Bool, Never>(true)

public var pushNotificationAllowStatusPublisher: AnyPublisher<Bool, Never> {
return pushNotificationAllowStatusSubject.eraseToAnyPublisher()
}

public init(
isLoggedIn: @escaping () -> Bool,
Expand All @@ -26,7 +35,9 @@ public struct UserClient {
updateLoginState: @escaping (Bool) -> Void,
updateDeleteState: @escaping (Bool) -> Void,
updateFcmToken: @escaping (String) -> Void,
updatePushNotificationAllowStatus: @escaping (Bool) -> Void,
fetchAlertState: @escaping () async throws -> [UserAlertState],
fetchPushNotificationAllowStatus: @escaping () -> Bool,
updateAlertState: @escaping (UserAlertState) async throws -> Void,
fetchContacts: @escaping () async throws -> [String],
updateBlockContacts: @escaping ([String]) async throws -> Void
Expand All @@ -37,7 +48,9 @@ public struct UserClient {
self.updateLoginState = updateLoginState
self.updateDeleteState = updateDeleteState
self.updateFcmToken = updateFcmToken
self.updatePushNotificationAllowStatus = updatePushNotificationAllowStatus
self._fetchAlertState = fetchAlertState
self._fetchPushNotificationAllowStatus = fetchPushNotificationAllowStatus
self.updateAlertState = updateAlertState
self.fetchContacts = fetchContacts
self.updateBlockContacts = updateBlockContacts
Expand Down Expand Up @@ -67,10 +80,19 @@ public struct UserClient {
updateFcmToken(fcmToken)
}

public func updatePushNotificationAllowStatus(isAllow: Bool) {
pushNotificationAllowStatusSubject.send(isAllow)
updatePushNotificationAllowStatus(isAllow)
}

public func fetchAlertState() async throws -> [UserAlertState] {
try await _fetchAlertState()
}

public func fetchPushNotificationAllowStatus() -> Bool {
_fetchPushNotificationAllowStatus()
}

public func updateAlertState(alertState: UserAlertState) async throws {
try await updateAlertState(alertState)
}
Expand Down
29 changes: 22 additions & 7 deletions Projects/Domain/User/Sources/UserClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,41 +17,56 @@ import ComposableArchitecture
import Moya

extension UserClient: DependencyKey {
private enum UserDefaultsKeys: String {
case loginState
case deleteState
case fcmToken
case alertAllowState
}

static public var liveValue: UserClient = .live()

static func live() -> UserClient {
@Dependency(\.network) var networkManager

return .init(
isLoggedIn: {
return UserDefaults.standard.bool(forKey: "loginState")
return UserDefaults.standard.bool(forKey: UserDefaultsKeys.loginState.rawValue)
},

isAppDeleted: {
return !UserDefaults.standard.bool(forKey: "deleteState")
return !UserDefaults.standard.bool(forKey: UserDefaultsKeys.deleteState.rawValue)
},

fetchFcmToken: {
return UserDefaults.standard.string(forKey: "fcmToken")
return UserDefaults.standard.string(forKey: UserDefaultsKeys.fcmToken.rawValue)
},

updateLoginState: { isLoggedIn in
UserDefaults.standard.set(isLoggedIn, forKey: "loginState")
UserDefaults.standard.set(isLoggedIn, forKey: UserDefaultsKeys.loginState.rawValue)
},

updateDeleteState: { isDelete in
UserDefaults.standard.set(!isDelete, forKey: "deleteState")
UserDefaults.standard.set(!isDelete, forKey: UserDefaultsKeys.deleteState.rawValue)
},

updateFcmToken: { fcmToken in
UserDefaults.standard.set(fcmToken, forKey: "fcmToken")
UserDefaults.standard.set(fcmToken, forKey: UserDefaultsKeys.fcmToken.rawValue)
},

updatePushNotificationAllowStatus: { isAllow in
UserDefaults.standard.set(isAllow, forKey: UserDefaultsKeys.alertAllowState.rawValue)
},

fetchAlertState: {
let responseData = try await networkManager.reqeust(api: .apiType(UserAPI.fetchAlertState), dto: [AlertStateResponseDTO].self)
return responseData.map { $0.toDomain() }

},

fetchPushNotificationAllowStatus: {
return UserDefaults.standard.bool(forKey: UserDefaultsKeys.alertAllowState.rawValue)
},

updateAlertState: { alertState in
let requestData = AlertStateRequestDTO(alertType: alertState.alertType, enabled: alertState.enabled)
try await networkManager.reqeust(api: .apiType(UserAPI.updateAlertState(reqeustData: requestData)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@
//

import Foundation
import Combine

import DomainUser
import DomainUserInterface

import CoreURLHandlerInterface
import CoreLoggerInterface

import ComposableArchitecture

extension AlertSettingFeature {
Expand All @@ -20,11 +24,24 @@ extension AlertSettingFeature {
let reducer = Reduce<State, Action> { state, action in
switch action {
case .onLoad:
return .run { send in
return Effect.publisher {
userClient.pushNotificationAllowStatusPublisher
.receive(on: DispatchQueue.main)
.map { isAllow in
.pushNotificationAllowed(isAllow: isAllow)
}
}
.cancellable(id: "PushNotificationPublisher", cancelInFlight: true)

case .alertStateFetchDidRequest:
updatePushNotificationAllowStatus(state: &state)

return .run { [state = state] send in
let isAllow = state.isAllowPushNotification
let alertStateList = try await userClient.fetchAlertState()

for alertState in alertStateList {
let isOn = alertState.enabled
let isOn = isAllow ? alertState.enabled : false
switch alertState.alertType {
case .randomBottle:
await send(.randomBottleToggleDidFetched(isOn: isOn))
Expand All @@ -39,7 +56,20 @@ extension AlertSettingFeature {
}
}
}


case let .pushNotificationAllowed(isAllow):
state.isAllowPushNotification = isAllow
if isAllow {
return .send(.alertStateFetchDidRequest)
} else {
return .merge(
.send(.randomBottleToggleDidFetched(isOn: false)),
.send(.pingpongToggleDidFetched(isOn: false)),
.send(.arrivalBottleToggleDidFetched(isOn: false)),
.send(.marketingToggleDidFetched(isOn: false))
)
}

case let .randomBottleToggleDidFetched(isOn):
state.isOnRandomBottleToggle = isOn
return .none
Expand All @@ -62,49 +92,70 @@ extension AlertSettingFeature {
}

case .binding(\.isOnRandomBottleToggle):
return .run { [isOn = state.isOnRandomBottleToggle] send in
await send(.toggleDidChanged(alertState: .init(alertType: .randomBottle, enabled: isOn)))
}
.debounce(
id: ID.randomBottle,
for: 1.0,
scheduler: DispatchQueue.main)
let isOn = state.isOnRandomBottleToggle
return .send(.toggleDidChanged(
alertState: .init(alertType: .randomBottle, enabled: isOn),
id: .randomBottle))

case .binding(\.isOnArrivalBottleToggle):
return .run { [isOn = state.isOnArrivalBottleToggle] send in
await send(.toggleDidChanged(alertState: .init(alertType: .arrivalBottle, enabled: isOn)))
}
.debounce(
id: ID.arrivalBottle,
for: 1.0,
scheduler: DispatchQueue.main)
let isOn = state.isOnArrivalBottleToggle
return .send(.toggleDidChanged(
alertState: .init(alertType: .arrivalBottle, enabled: isOn),
id: .arrivalBottle))

case .binding(\.isOnPingPongToggle):
return .run { [isOn = state.isOnPingPongToggle] send in
await send(.toggleDidChanged(alertState: .init(alertType: .pingpong, enabled: isOn)))
}
.debounce(
id: ID.pingping,
for: 1.0,
scheduler: DispatchQueue.main)
let isOn = state.isOnPingPongToggle
return .send(.toggleDidChanged(
alertState: .init(alertType: .pingpong, enabled: isOn),
id: .pingping))

case .binding(\.isOnMarketingToggle):
return .run { [isOn = state.isOnMarketingToggle] send in
await send(.toggleDidChanged(alertState: .init(alertType: .marketing, enabled: isOn)))
}
.debounce(
id: ID.marketing,
for: 1.0,
scheduler: DispatchQueue.main)

case let .toggleDidChanged(alertState):
return .run { send in
try await userClient.updateAlertState(alertState: alertState)
let isOn = state.isOnMarketingToggle
return .send(.toggleDidChanged(
alertState: .init(alertType: .marketing, enabled: isOn),
id: .marketing))

case let .toggleDidChanged(alertState, id):
updatePushNotificationAllowStatus(state: &state)

if state.isAllowPushNotification {
return .run { send in
try await userClient.updateAlertState(alertState: alertState)
}
.debounce(
id: id,
for: 0.5,
scheduler: DispatchQueue.main)
} else {
return .send(.pushNotificationAlertDidRequired)
}

case .pushNotificationAlertDidRequired:
state.destination = .alert(.init(
title: { TextState("알림 권한 안내")},
actions: { ButtonState(
role: .destructive,
action: .confirmPushNotification,
label: { TextState("설정하러 가기") }) },
message: { TextState("설정 > '보틀' > 알림에서 알림을 허용해주세요.")}))

return .none

case let .destination(.presented(.alert(alert))):
switch alert {
case .confirmPushNotification:
URLHandler.shared.openURL(urlType: .setting)
return .none
}

default:
return .none
}

func updatePushNotificationAllowStatus(state: inout State) {
let isAllow = userClient.fetchPushNotificationAllowStatus()
state.isAllowPushNotification = isAllow
}
}

self.init(reducer: reducer)
Expand Down
Loading

0 comments on commit e130ab9

Please sign in to comment.