diff --git a/Projects/Core/Network/Sources/MoyaPulgins/MoyaLoggerPlugin.swift b/Projects/Core/Network/Sources/MoyaPulgins/MoyaLoggerPlugin.swift index ddec0549..ae54590f 100644 --- a/Projects/Core/Network/Sources/MoyaPulgins/MoyaLoggerPlugin.swift +++ b/Projects/Core/Network/Sources/MoyaPulgins/MoyaLoggerPlugin.swift @@ -33,7 +33,7 @@ final class MoyaLoggerPlugin: PluginType { log += "header: \(headers)\n" } if let body = httpRequest.httpBody, let bodyString = String(bytes: body, encoding: String.Encoding.utf8) { - log += "bodyString: \(bodyString)" + log += "bodyString: \(bodyString)\n" } log += "---------------------------------------------" @@ -79,7 +79,7 @@ final class MoyaLoggerPlugin: PluginType { if let data = error.response?.data { log += "Data - \(data)\n" } else { - log += "Data - empty" + log += "Data - empty\n" } log += "---------------------------------------------" diff --git a/Projects/Domain/User/Interface/Sources/API/UserAPI.swift b/Projects/Domain/User/Interface/Sources/API/UserAPI.swift index 59a2206d..30999f43 100644 --- a/Projects/Domain/User/Interface/Sources/API/UserAPI.swift +++ b/Projects/Domain/User/Interface/Sources/API/UserAPI.swift @@ -15,6 +15,7 @@ public enum UserAPI { case fetchAlertState case updateAlertState(reqeustData: AlertStateRequestDTO) case updateBlockContacts(blockContactRequestDTO: BlockContactRequestDTO) + case updatePushNotificationAllowStatus(requestDTO: UpdatePushNotificationAllowStatusRequestDTO) } extension UserAPI: BaseTargetType { @@ -26,6 +27,8 @@ extension UserAPI: BaseTargetType { return "api/v1/user/alimy" case .updateBlockContacts: return "api/v1/user/block/contact-list" + case .updatePushNotificationAllowStatus: + return "api/v1/user/native-setting" } } @@ -37,6 +40,8 @@ extension UserAPI: BaseTargetType { return .post case .updateBlockContacts: return .post + case .updatePushNotificationAllowStatus: + return .post } } @@ -48,6 +53,8 @@ extension UserAPI: BaseTargetType { return .requestJSONEncodable(requestData) case let .updateBlockContacts(blockContactRequestDTO): return .requestJSONEncodable(blockContactRequestDTO) + case let .updatePushNotificationAllowStatus(requestDTO): + return .requestJSONEncodable(requestDTO) } } } diff --git a/Projects/Domain/User/Interface/Sources/DTO/Request/UpdatePushNotificationAllowStatusRequestDTO.swift b/Projects/Domain/User/Interface/Sources/DTO/Request/UpdatePushNotificationAllowStatusRequestDTO.swift new file mode 100644 index 00000000..bc5071a5 --- /dev/null +++ b/Projects/Domain/User/Interface/Sources/DTO/Request/UpdatePushNotificationAllowStatusRequestDTO.swift @@ -0,0 +1,27 @@ +// +// UpdatePushNotificationAllowStatusRequestDTO.swift +// DomainUserInterface +// +// Created by JongHoon on 11/3/24. +// + +import Foundation + +public struct UpdatePushNotificationAllowStatusRequestDTO: Encodable { + public let alimyTurnedOn: Bool + public let deviceName: String + public let appVersion: String + public let deviceId: String + + public init( + turnOn: Bool, + deviceName: String, + appVersion: String, + deviceId: String + ) { + self.alimyTurnedOn = turnOn + self.deviceName = deviceName + self.appVersion = appVersion + self.deviceId = deviceId + } +} diff --git a/Projects/Domain/User/Interface/Sources/NeedUpdatePushNotificationAllowStatusRemotelyType.swift b/Projects/Domain/User/Interface/Sources/NeedUpdatePushNotificationAllowStatusRemotelyType.swift new file mode 100644 index 00000000..27acfeec --- /dev/null +++ b/Projects/Domain/User/Interface/Sources/NeedUpdatePushNotificationAllowStatusRemotelyType.swift @@ -0,0 +1,13 @@ +// +// NeedUpdatePushNotificationAllowStatusRemotelyType.swift +// DomainUserInterface +// +// Created by JongHoon on 11/2/24. +// + +import Foundation + +public enum NeedUpdatePushNotificationAllowStatusRemotelyType { + case notNeed + case need(isAllow: Bool) +} diff --git a/Projects/Domain/User/Interface/Sources/UserClient.swift b/Projects/Domain/User/Interface/Sources/UserClient.swift index 34fd1927..768ce0a8 100644 --- a/Projects/Domain/User/Interface/Sources/UserClient.swift +++ b/Projects/Domain/User/Interface/Sources/UserClient.swift @@ -13,12 +13,16 @@ public struct UserClient { private let _isLoggedIn: () -> Bool private let _isAppDeleted: () -> Bool private let _fetchFcmToken: () -> String? + var _remotelyUploadedPushNotificationAllowStatus: () -> Bool? private let updateLoginState: (Bool) -> Void private let updateDeleteState: (Bool) -> Void private let updateFcmToken: (String) -> Void - private let updatePushNotificationAllowStatus: (Bool) -> Void + private let updatePushNotificationAllowStatusLocally: (Bool) -> Void + private let updatePushNotificationAllowStatusRemotely: (Bool) async throws -> Void + private let updateRemotelyUploadedPushNotificationAllowStatus: (Bool) -> Void + private let _isNeedUpdatePushNotificationRemotely: () async -> NeedUpdatePushNotificationAllowStatusRemotelyType private let _fetchAlertState: () async throws -> [UserAlertState] - private let _fetchPushNotificationAllowStatus: () -> Bool + private let _fetchPushNotificationAllowStatusLocally: () -> Bool private let updateAlertState: (UserAlertState) async throws -> Void private let fetchContacts: () async throws -> [String] private let updateBlockContacts: ([String]) async throws -> Void @@ -32,12 +36,16 @@ public struct UserClient { isLoggedIn: @escaping () -> Bool, isAppDeleted: @escaping () -> Bool, fetchFcmToken: @escaping () -> String?, + remotelyUploadedPushNotificationAllowStatus: @escaping () -> Bool?, updateLoginState: @escaping (Bool) -> Void, updateDeleteState: @escaping (Bool) -> Void, updateFcmToken: @escaping (String) -> Void, - updatePushNotificationAllowStatus: @escaping (Bool) -> Void, + updatePushNotificationAllowStatusLocally: @escaping (Bool) -> Void, + updatePushNotificationAllowStatusRemotely: @escaping (Bool) async throws -> Void, + updateRemotelyUploadedPushNotificationAllowStatus: @escaping (Bool) -> Void, + isNeedUpdatePushNotificationRemotely: @escaping () async -> NeedUpdatePushNotificationAllowStatusRemotelyType, fetchAlertState: @escaping () async throws -> [UserAlertState], - fetchPushNotificationAllowStatus: @escaping () -> Bool, + fetchPushNotificationAllowStatusLocally: @escaping () -> Bool, updateAlertState: @escaping (UserAlertState) async throws -> Void, fetchContacts: @escaping () async throws -> [String], updateBlockContacts: @escaping ([String]) async throws -> Void @@ -45,12 +53,16 @@ public struct UserClient { self._isLoggedIn = isLoggedIn self._isAppDeleted = isAppDeleted self._fetchFcmToken = fetchFcmToken + self._remotelyUploadedPushNotificationAllowStatus = remotelyUploadedPushNotificationAllowStatus self.updateLoginState = updateLoginState self.updateDeleteState = updateDeleteState self.updateFcmToken = updateFcmToken - self.updatePushNotificationAllowStatus = updatePushNotificationAllowStatus + self.updatePushNotificationAllowStatusLocally = updatePushNotificationAllowStatusLocally + self.updatePushNotificationAllowStatusRemotely = updatePushNotificationAllowStatusRemotely + self.updateRemotelyUploadedPushNotificationAllowStatus = updateRemotelyUploadedPushNotificationAllowStatus + self._isNeedUpdatePushNotificationRemotely = isNeedUpdatePushNotificationRemotely self._fetchAlertState = fetchAlertState - self._fetchPushNotificationAllowStatus = fetchPushNotificationAllowStatus + self._fetchPushNotificationAllowStatusLocally = fetchPushNotificationAllowStatusLocally self.updateAlertState = updateAlertState self.fetchContacts = fetchContacts self.updateBlockContacts = updateBlockContacts @@ -68,6 +80,10 @@ public struct UserClient { _fetchFcmToken() } + public func remotelyUploadedPushNotificationAllowStatus() -> Bool? { + _remotelyUploadedPushNotificationAllowStatus() + } + public func updateLoginState(isLoggedIn: Bool) { updateLoginState(isLoggedIn) } @@ -80,17 +96,29 @@ public struct UserClient { updateFcmToken(fcmToken) } - public func updatePushNotificationAllowStatus(isAllow: Bool) { + public func updatePushNotificationAllowStatusLocally(isAllow: Bool) { pushNotificationAllowStatusSubject.send(isAllow) - updatePushNotificationAllowStatus(isAllow) + updatePushNotificationAllowStatusLocally(isAllow) + } + + public func updatePushNotificationAllowStatusRemotely(isAllow: Bool) async throws { + try await updatePushNotificationAllowStatusRemotely(isAllow) + } + + public func updateRemotelyUploadedPushNotificationAllowStatus(isAllow: Bool) { + updateRemotelyUploadedPushNotificationAllowStatus(isAllow) + } + + public func isNeedUpdatePushNotificationRemotely() async -> NeedUpdatePushNotificationAllowStatusRemotelyType { + await _isNeedUpdatePushNotificationRemotely() } public func fetchAlertState() async throws -> [UserAlertState] { try await _fetchAlertState() } - public func fetchPushNotificationAllowStatus() -> Bool { - _fetchPushNotificationAllowStatus() + public func fetchPushNotificationAllowStatusLocally() -> Bool { + _fetchPushNotificationAllowStatusLocally() } public func updateAlertState(alertState: UserAlertState) async throws { diff --git a/Projects/Domain/User/Sources/UserClient.swift b/Projects/Domain/User/Sources/UserClient.swift index c46472f1..65786371 100644 --- a/Projects/Domain/User/Sources/UserClient.swift +++ b/Projects/Domain/User/Sources/UserClient.swift @@ -5,13 +5,17 @@ // Created by 임현규 on 8/22/24. // +import UIKit import Foundation +import UserNotifications import Contacts import DomainUserInterface import CoreKeyChainStore import CoreNetwork +import CoreLoggerInterface +import SharedUtilInterface import ComposableArchitecture import Moya @@ -22,6 +26,7 @@ extension UserClient: DependencyKey { case deleteState case fcmToken case alertAllowState + case remotelyUploadedPushNotificationAllowStatus } static public var liveValue: UserClient = .live() @@ -42,6 +47,16 @@ extension UserClient: DependencyKey { return UserDefaults.standard.string(forKey: UserDefaultsKeys.fcmToken.rawValue) }, + remotelyUploadedPushNotificationAllowStatus: { + let status = UserDefaults.standard.object(forKey: UserDefaultsKeys.remotelyUploadedPushNotificationAllowStatus.rawValue) + guard let status = status as? Bool + else { + return nil + } + + return status + }, + updateLoginState: { isLoggedIn in UserDefaults.standard.set(isLoggedIn, forKey: UserDefaultsKeys.loginState.rawValue) }, @@ -54,16 +69,94 @@ extension UserClient: DependencyKey { UserDefaults.standard.set(fcmToken, forKey: UserDefaultsKeys.fcmToken.rawValue) }, - updatePushNotificationAllowStatus: { isAllow in + updatePushNotificationAllowStatusLocally: { isAllow in UserDefaults.standard.set(isAllow, forKey: UserDefaultsKeys.alertAllowState.rawValue) }, + updatePushNotificationAllowStatusRemotely: { isAllow in + @Dependency(\.userClient) var userClient + + var deviceName: String? { + if let simulatorModelIdentifier = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] { + return simulatorModelIdentifier + } else { + var systemInfo = utsname() + uname(&systemInfo) + let modelIdentifier = withUnsafePointer(to: &systemInfo.machine) { + $0.withMemoryRebound(to: CChar.self, capacity: 1) { ptr in + String(validatingUTF8: ptr) + } + } + return modelIdentifier + } + } + + let requestDTO = await UpdatePushNotificationAllowStatusRequestDTO( + turnOn: isAllow, + deviceName: deviceName ?? "", + appVersion: (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "", + deviceId: UIDevice.current.identifierForVendor?.uuidString ?? "" + ) + + try await networkManager.reqeust(api: .apiType(UserAPI.updatePushNotificationAllowStatus(requestDTO: requestDTO))) + userClient.updateRemotelyUploadedPushNotificationAllowStatus(isAllow: isAllow) + }, + + updateRemotelyUploadedPushNotificationAllowStatus: { isAllow in + UserDefaults.standard.set(isAllow, forKey: UserDefaultsKeys.remotelyUploadedPushNotificationAllowStatus.rawValue) + }, + + isNeedUpdatePushNotificationRemotely: { + @Dependency(\.userClient) var userClient + + guard userClient.isLoggedIn() + else { + return .notNeed + } + + let remotelyUploadedStatus = userClient.remotelyUploadedPushNotificationAllowStatus() + let isAuthorized = await withCheckedContinuation { continuation in + UNUserNotificationCenter.current().getNotificationSettings { settings in + switch settings.authorizationStatus { + case .notDetermined: + continuation.resume(returning: false) + + case .denied: + continuation.resume(returning: false) + + case .authorized: + continuation.resume(returning: true) + + case .provisional: + continuation.resume(returning: false) + + case .ephemeral: + continuation.resume(returning: false) + + @unknown default: + continuation.resume(returning: false) + Log.assertion(message: "not handled status") + } + } + } + + let isNeedType: NeedUpdatePushNotificationAllowStatusRemotelyType = switch remotelyUploadedStatus { + case .none: + .need(isAllow: isAuthorized) + + case let .some(localAllowStatus): + (localAllowStatus == isAuthorized) ? .notNeed : .need(isAllow: isAuthorized) + } + + return isNeedType + }, + fetchAlertState: { let responseData = try await networkManager.reqeust(api: .apiType(UserAPI.fetchAlertState), dto: [AlertStateResponseDTO].self) return responseData.map { $0.toDomain() } }, - fetchPushNotificationAllowStatus: { + fetchPushNotificationAllowStatusLocally: { return UserDefaults.standard.bool(forKey: UserDefaultsKeys.alertAllowState.rawValue) }, diff --git a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift index 4a719871..ca869d12 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift @@ -153,7 +153,7 @@ extension AlertSettingFeature { } func updatePushNotificationAllowStatus(state: inout State) { - let isAllow = userClient.fetchPushNotificationAllowStatus() + let isAllow = userClient.fetchPushNotificationAllowStatusLocally() state.isAllowPushNotification = isAllow } } diff --git a/Projects/Feature/Sources/App/AppDelegateFeature.swift b/Projects/Feature/Sources/App/AppDelegateFeature.swift index fb7b9659..0cbd733c 100644 --- a/Projects/Feature/Sources/App/AppDelegateFeature.swift +++ b/Projects/Feature/Sources/App/AppDelegateFeature.swift @@ -57,7 +57,7 @@ public struct AppDelegateFeature { } case let .pushNotificationAllowStatusDidChanged(isAllow): - userClient.updatePushNotificationAllowStatus(isAllow: isAllow) + userClient.updatePushNotificationAllowStatusLocally(isAllow: isAllow) return .none default: diff --git a/Projects/Feature/Sources/SplashView/SplashFeature.swift b/Projects/Feature/Sources/SplashView/SplashFeature.swift index 6f009b86..c2463401 100644 --- a/Projects/Feature/Sources/SplashView/SplashFeature.swift +++ b/Projects/Feature/Sources/SplashView/SplashFeature.swift @@ -18,6 +18,7 @@ import ComposableArchitecture @Reducer public struct SplashFeature { @Dependency(\.authClient) private var authClient + @Dependency(\.userClient) private var userClient @ObservableState public struct State: Equatable { @@ -59,7 +60,11 @@ public struct SplashFeature { switch action { case .onAppear: return .run { send in - try await authClient.checkUpdateVersion() + async let checkUpdateVersionTask: () = try await authClient.checkUpdateVersion() + async let updatePushNotificationAllowStatusTask: () = try await updatePushNotificationAllowStatusRemotely() + + let _ = try await (checkUpdateVersionTask, updatePushNotificationAllowStatusTask) + await send(.delegate(.initialCheckCompleted)) } catch: { error, send in Log.error(error) @@ -100,6 +105,17 @@ public struct SplashFeature { case .alert, .delegate, .destination, .binding: return .none } + + @Sendable func updatePushNotificationAllowStatusRemotely() async throws { + let isNeed = await userClient.isNeedUpdatePushNotificationRemotely() + switch isNeed { + case let .need(isAllow): + try await userClient.updatePushNotificationAllowStatusRemotely(isAllow: isAllow) + + case .notNeed: + return + } + } } } diff --git a/Projects/Shared/Util/Interface/Sources/UtilInterface.swift b/Projects/Shared/Util/Interface/Sources/UtilInterface.swift deleted file mode 100644 index b5477d2b..00000000 --- a/Projects/Shared/Util/Interface/Sources/UtilInterface.swift +++ /dev/null @@ -1,5 +0,0 @@ -// This is for Tuist - -public protocol UtilInterface { - -}