From 416787043353e687e576d545d41e1cce0c9b6e94 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Mon, 23 Sep 2024 01:04:51 +0900 Subject: [PATCH 01/11] =?UTF-8?q?chore:=20=EB=B9=8C=EB=93=9C=20=EB=84=98?= =?UTF-8?q?=EB=B2=84=201.0.8=20(29)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift index ab33dae3..d077af94 100644 --- a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift @@ -10,8 +10,8 @@ import ProjectDescription public extension InfoPlist { static var app: InfoPlist { return .extendingDefault(with: [ - "CFBundleShortVersionString": "1.0.7", - "CFBundleVersion": "27", + "CFBundleShortVersionString": "1.0.8", + "CFBundleVersion": "29", "UIUserInterfaceStyle": "Light", "CFBundleName": "보틀", "UILaunchScreen": [ @@ -43,8 +43,8 @@ public extension InfoPlist { static var example: InfoPlist { return .extendingDefault(with: [ - "CFBundleShortVersionString": "1.0.7", - "CFBundleVersion": "27", + "CFBundleShortVersionString": "1.0.8", + "CFBundleVersion": "29", "UIUserInterfaceStyle": "Light", "UILaunchScreen": [:], "UISupportedInterfaceOrientations": [ From 840916990c54dbaf2765d26af11d5193fccbd67a Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Mon, 23 Sep 2024 01:11:13 +0900 Subject: [PATCH 02/11] =?UTF-8?q?chore:=20DomainAuth=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Domain/Auth/Project.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Projects/Domain/Auth/Project.swift b/Projects/Domain/Auth/Project.swift index ff0338f1..6d3339ba 100644 --- a/Projects/Domain/Auth/Project.swift +++ b/Projects/Domain/Auth/Project.swift @@ -18,7 +18,8 @@ let project = Project.makeModule( factory: .init( dependencies: [ .domain(interface: .Auth), - .domain(interface: .Error) + .domain(interface: .Error), + .domain(implements: .User) ] ) ), From bff8b3f521f3b5d1feb9040137dcd6d739f886ea Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Mon, 23 Sep 2024 17:01:29 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20ProfileEditView=20bottom=20ignore?= =?UTF-8?q?SafeArea=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MyPage/Interface/Sources/EditProfile/ProfileEditView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Projects/Feature/MyPage/Interface/Sources/EditProfile/ProfileEditView.swift b/Projects/Feature/MyPage/Interface/Sources/EditProfile/ProfileEditView.swift index af132360..1c0a40b0 100644 --- a/Projects/Feature/MyPage/Interface/Sources/EditProfile/ProfileEditView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/EditProfile/ProfileEditView.swift @@ -48,5 +48,6 @@ public struct ProfileEditView: View { } } } + .ignoresSafeArea(.all, edges: [.bottom]) } } From a1a4c0b5d6e2f2072572b9d51a408160ba76f1ce Mon Sep 17 00:00:00 2001 From: JongHoon Date: Wed, 25 Sep 2024 19:17:28 +0900 Subject: [PATCH 04/11] =?UTF-8?q?[Feature/#277]=20=EC=9B=B9=EB=B7=B0=20os?= =?UTF-8?q?=20type,=20=EB=B2=84=EC=A0=84=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80=20(#279)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interface/Sources/BaseWebViewType.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift b/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift index 60136dbf..11d53307 100644 --- a/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift +++ b/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift @@ -7,10 +7,15 @@ import Foundation +import DomainApplicationInterface +import DomainApplication + import CoreWebViewInterface import CoreKeyChainStoreInterface import CoreKeyChainStore +import Dependencies + public enum BottleWebViewType { private var baseURL: String { (Bundle.main.infoDictionary?["WEB_VIEW_BASE_URL"] as? String) ?? "" @@ -67,11 +72,15 @@ public enum BottleWebViewType { // MARK: - private methods private extension BottleWebViewType { func makeUrlWithToken(_ path: String) -> URL { + @Dependency(\.applicationClient) var applicationClient + var components = URLComponents(string: baseURL) components?.path = "/\(path)" components?.queryItems = [ URLQueryItem(name: "accessToken", value: KeyChainTokenStore.shared.load(property: .accessToken)), - URLQueryItem(name: "refreshToken", value: KeyChainTokenStore.shared.load(property: .refreshToken)) + URLQueryItem(name: "refreshToken", value: KeyChainTokenStore.shared.load(property: .refreshToken)), + URLQueryItem(name: "device", value: "ios"), + URLQueryItem(name: "version", value: applicationClient.fetchCurrentAppVersion()) ] return (components?.url)! From c667047f1f921915be7f097380689aa9fc4c805a Mon Sep 17 00:00:00 2001 From: JongHoon Date: Wed, 25 Sep 2024 19:19:39 +0900 Subject: [PATCH 05/11] =?UTF-8?q?[Feature/#278]=20=EC=9B=B9=EB=B7=B0=20?= =?UTF-8?q?=EC=83=81=EB=8B=A8=20safe=20area=20=EB=AC=B4=EC=8B=9C=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#284)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BottleArrival/Interface/Sources/BottleArrivalView.swift | 2 +- .../GeneralSignUp/Interface/Sources/GeneralSignUpView.swift | 2 +- .../Login/Interface/Sources/GeneralLogIn/GeneralLogInView.swift | 2 +- .../MyPage/Interface/Sources/EditProfile/ProfileEditView.swift | 2 +- .../Interface/Sources/Onboarding/OnboardingView.swift | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalView.swift b/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalView.swift index 18e02dc9..1b002cf9 100644 --- a/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalView.swift +++ b/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalView.swift @@ -43,7 +43,7 @@ public struct BottleArrivalView: View { LoadingIndicator() } } - .ignoresSafeArea(.all, edges: .bottom) + .ignoresSafeArea(.all, edges: [.top, .bottom]) .toolbar(.hidden, for: .navigationBar) .toolbar(.hidden, for: .bottomBar) } diff --git a/Projects/Feature/GeneralSignUp/Interface/Sources/GeneralSignUpView.swift b/Projects/Feature/GeneralSignUp/Interface/Sources/GeneralSignUpView.swift index 6cb6f38c..a396f737 100644 --- a/Projects/Feature/GeneralSignUp/Interface/Sources/GeneralSignUpView.swift +++ b/Projects/Feature/GeneralSignUp/Interface/Sources/GeneralSignUpView.swift @@ -54,7 +54,7 @@ public struct GeneralSignUpView: View { } } .toolbar(.hidden, for: .navigationBar) - .ignoresSafeArea(.all, edges: .bottom) + .ignoresSafeArea(.all, edges: [.top, .bottom]) .sheet(isPresented: $store.isPresentTerms) { TermsWebView(url: store.termsURL ?? "") } diff --git a/Projects/Feature/Login/Interface/Sources/GeneralLogIn/GeneralLogInView.swift b/Projects/Feature/Login/Interface/Sources/GeneralLogIn/GeneralLogInView.swift index 7b50c6ac..04af2723 100644 --- a/Projects/Feature/Login/Interface/Sources/GeneralLogIn/GeneralLogInView.swift +++ b/Projects/Feature/Login/Interface/Sources/GeneralLogIn/GeneralLogInView.swift @@ -51,7 +51,7 @@ public struct GeneralLogInView: View { LoadingIndicator() } } - .ignoresSafeArea(.all, edges: .bottom) + .ignoresSafeArea(.all, edges: [.top, .bottom]) .toolbar(.hidden, for: .navigationBar) } } diff --git a/Projects/Feature/MyPage/Interface/Sources/EditProfile/ProfileEditView.swift b/Projects/Feature/MyPage/Interface/Sources/EditProfile/ProfileEditView.swift index 1c0a40b0..7addfb6b 100644 --- a/Projects/Feature/MyPage/Interface/Sources/EditProfile/ProfileEditView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/EditProfile/ProfileEditView.swift @@ -48,6 +48,6 @@ public struct ProfileEditView: View { } } } - .ignoresSafeArea(.all, edges: [.bottom]) + .ignoresSafeArea(.all, edges: [.bottom, .top]) } } diff --git a/Projects/Feature/Onboarding/Interface/Sources/Onboarding/OnboardingView.swift b/Projects/Feature/Onboarding/Interface/Sources/Onboarding/OnboardingView.swift index fb869298..6191dcde 100644 --- a/Projects/Feature/Onboarding/Interface/Sources/Onboarding/OnboardingView.swift +++ b/Projects/Feature/Onboarding/Interface/Sources/Onboarding/OnboardingView.swift @@ -43,7 +43,7 @@ public struct OnboardingView: View { } } ) - .ignoresSafeArea(.all, edges: .bottom) + .ignoresSafeArea(.all, edges: [.top, .bottom]) .toolbar(.hidden, for: .navigationBar) .overlay { if store.isShowLoadingProgressView { From eeba2239cebc9cbf159cde9693b24d116be70c59 Mon Sep 17 00:00:00 2001 From: JongHoon Date: Wed, 25 Sep 2024 19:21:07 +0900 Subject: [PATCH 06/11] =?UTF-8?q?[Fix/#280]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=B0=B1=EA=B7=B8=EB=9D=BC=EC=9A=B4?= =?UTF-8?q?=EB=93=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=B9=84=EC=9C=A8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#281)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Feature/Login/Interface/Sources/Login/LoginView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Projects/Feature/Login/Interface/Sources/Login/LoginView.swift b/Projects/Feature/Login/Interface/Sources/Login/LoginView.swift index 3b585f70..db3d380a 100644 --- a/Projects/Feature/Login/Interface/Sources/Login/LoginView.swift +++ b/Projects/Feature/Login/Interface/Sources/Login/LoginView.swift @@ -49,6 +49,7 @@ public struct LoginView: View { BottleImageView( type: .local(bottleImageSystem: .illustraition(.loginBackground)) ) + .scaledToFill() } .edgesIgnoringSafeArea([.top, .bottom]) .sheet( From 85925f64ddf6d7b0a74493343a1a4e0eff930c65 Mon Sep 17 00:00:00 2001 From: JongHoon Date: Wed, 25 Sep 2024 19:22:12 +0900 Subject: [PATCH 07/11] =?UTF-8?q?[Fix/#282]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20vstack=20=ED=95=98=EB=8B=A8=20=EB=A7=88=EC=A7=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#283)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Login/AppleLoginView.swift | 34 ++++++++-------- .../Interface/Sources/Login/LoginView.swift | 39 ++++++++++--------- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/Projects/Feature/Login/Interface/Sources/Login/AppleLoginView.swift b/Projects/Feature/Login/Interface/Sources/Login/AppleLoginView.swift index e1ace40d..d79754b4 100644 --- a/Projects/Feature/Login/Interface/Sources/Login/AppleLoginView.swift +++ b/Projects/Feature/Login/Interface/Sources/Login/AppleLoginView.swift @@ -19,31 +19,33 @@ public struct AppleLoginView: View { } public var body: some View { - VStack(spacing: 0) { - Spacer() - .frame(height: 52) - whiteLogo - .padding(.top, 52) - .padding(.bottom, .xl) - mainText + ZStack(alignment: .bottom) { + VStack(spacing: 0) { + Spacer() + .frame(height: 52) + whiteLogo + .padding(.top, 52) + .padding(.bottom, .xl) + mainText - Spacer() + Spacer() + } + .frame(maxWidth: .infinity) + .background { + BottleImageView( + type: .local(bottleImageSystem: .illustraition(.loginBackground)) + ) + } + .edgesIgnoringSafeArea([.top, .bottom]) signInWithAppleButton - .padding(.bottom, 30.0) - - } - .background { - BottleImageView( - type: .local(bottleImageSystem: .illustraition(.loginBackground)) - ) + .padding(.bottom, 16.0) } .setNavigationBar { makeNaivgationleftButton() { store.send(.backButtonDidTapped) } } - .edgesIgnoringSafeArea([.top, .bottom]) } } diff --git a/Projects/Feature/Login/Interface/Sources/Login/LoginView.swift b/Projects/Feature/Login/Interface/Sources/Login/LoginView.swift index db3d380a..56d322f6 100644 --- a/Projects/Feature/Login/Interface/Sources/Login/LoginView.swift +++ b/Projects/Feature/Login/Interface/Sources/Login/LoginView.swift @@ -28,30 +28,33 @@ public struct LoginView: View { public var body: some View { WithPerceptionTracking { NavigationStack(path: $store.scope(state: \.path, action: \.path)) { - VStack(spacing: 0) { - Spacer() - .frame(height: 52) - whiteLogo - .padding(.top, 52) - .padding(.bottom, .xl) - - mainText - - Spacer() + ZStack(alignment: .bottom) { + VStack(spacing: 0) { + Spacer() + .frame(height: 52) + whiteLogo + .padding(.top, 52) + .padding(.bottom, .xl) + + mainText + + Spacer() + } + .frame(maxWidth: .infinity) + .background { + BottleImageView( + type: .local(bottleImageSystem: .illustraition(.loginBackground)) + ) + .scaledToFill() + } + .edgesIgnoringSafeArea([.top, .bottom]) VStack(spacing: 30.0) { signInWithKakaoButton snsLoginButton } - .padding(.bottom, 30.0) - } - .background { - BottleImageView( - type: .local(bottleImageSystem: .illustraition(.loginBackground)) - ) - .scaledToFill() + .padding(.bottom, 16.0) } - .edgesIgnoringSafeArea([.top, .bottom]) .sheet( isPresented: $store.isPresentTermView, content: { From dc507b3a8799aff273261226d684a81401ee062a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=ED=98=84=EA=B7=9C?= <48830320+leemhyungyu@users.noreply.github.com> Date: Wed, 25 Sep 2024 21:13:39 +0900 Subject: [PATCH 08/11] =?UTF-8?q?[Feature/#232]=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20alert=20=EC=A0=81=EC=9A=A9=20(#272)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: SplashView bottleAlert 적용 * feat: PingPongDetailView bottleAlert 적용 * feat: ReportUserView bottleAlert 적용 * feat: SandBeachView bottleAlert 적용 --- .../PingPongDetailFeature.swift | 48 +++++++++++++++++-- .../PingPongDetailFeatureInterface.swift | 20 +++++++- .../PingPongDetail/PingPongDetailView.swift | 1 + .../Introduction/IntroductionFeature.swift | 23 +-------- .../IntroductionFeatureInterface.swift | 24 +--------- .../Introduction/IntroductionView.swift | 2 - .../QuestionAndAnswerFeature.swift | 24 +--------- .../QuestionAndAnswerFeatureInterface.swift | 21 +------- .../QuestionAndAnswerView.swift | 1 - .../Interface/Sources/ReportUserFeature.swift | 32 +++++++++---- .../Sources/ReportUserFeatureInterface.swift | 1 + .../Interface/Sources/ReportUserView.swift | 2 +- .../Sources/SandBeach/SandBeachView.swift | 2 +- .../Sources/SplashView/SplashView.swift | 2 +- 14 files changed, 99 insertions(+), 104 deletions(-) diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeature.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeature.swift index b24ca8dd..9ee14df5 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeature.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeature.swift @@ -7,10 +7,11 @@ import Foundation -import CoreLoggerInterface import FeatureReportInterface import DomainBottle +import CoreLoggerInterface + import ComposableArchitecture extension PingPongDetailFeature { @@ -42,12 +43,46 @@ extension PingPongDetailFeature { imageURL: imageURL ?? "", userID: userId ?? -1, userName: userName, userAge: userAge ?? -1) return .send(.delegate(.reportButtonDidTapped(userReportProfile))) + case .stopTalkAlertDidRequired: + state.destination = .alert(.init( + title: { TextState("중단하기") }, + actions: { + ButtonState( + role: .cancel, + action: .dismiss, + label: { TextState("계속하기")}) + + ButtonState( + role: .destructive, + action: .confirmStopTalk, + label: { TextState("중단하기") }) + }, + message: { TextState("중단 시 모든 핑퐁 내용이 사라져요. 정말 중단하시겠어요?") } + )) + return .none + + // Destination + case let .destination(.presented(.alert(alert))): + switch alert { + case .confirmStopTalk: + return .run { [bottleID = state.bottleID] send in + try await bottleClient.stopTalk(bottleID: bottleID) + await send(.delegate(.popToRootDidRequired)) + } + + case .dismiss: + state.destination = nil + return .none + } + + // Introduction Delegate case let .introduction(.delegate(delegate)): switch delegate { - case .popToRootDidRequired: - return .send(.delegate(.popToRootDidRequired)) + case .stopTaskButtonTapped: + return .send(.stopTalkAlertDidRequired) } - + + // QuestionAndAnswer Delegate case let .questionAndAnswer(.delegate(delegate)): switch delegate { case .reloadPingPongRequired: @@ -56,8 +91,11 @@ extension PingPongDetailFeature { return .send(.delegate(.popToRootDidRequired)) case .refreshPingPong: return fetchPingPong(state: &state) + case .stopTaskButtonDidTapped: + return .send(.stopTalkAlertDidRequired) } - + + // Matching Delegate case let .matching(.delegate(delegate)): switch delegate { case .otherBottleButtonDidTapped: diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeatureInterface.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeatureInterface.swift index 45dfb790..5938b3e1 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeatureInterface.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeatureInterface.swift @@ -43,6 +43,8 @@ public struct PingPongDetailFeature { var matching: MatchingFeature.State var selectedTab: PingPongDetailViewTabType + @Presents var destination: Destination.State? + public init( bottleID: Int, isRead: Bool, @@ -67,7 +69,7 @@ public struct PingPongDetailFeature { case pingPongDidFetched(_: BottlePingPong) case backButtonDidTapped case reportButtonDidTapped - + case stopTalkAlertDidRequired // Delegate case delegate(Delegate) @@ -83,6 +85,13 @@ public struct PingPongDetailFeature { case questionAndAnswer(QuestionAndAnswerFeature.Action) case matching(MatchingFeature.Action) case binding(BindingAction) + case destination(PresentationAction) + // Alert + case alert(Alert) + public enum Alert: Equatable { + case confirmStopTalk + case dismiss + } } public var body: some ReducerOf { @@ -97,6 +106,15 @@ public struct PingPongDetailFeature { MatchingFeature() } reducer + .ifLet(\.$destination, action: \.destination) } } +// MARK: - Destination + +extension PingPongDetailFeature { + @Reducer(state: .equatable) + public enum Destination { + case alert(AlertState) + } +} diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailView.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailView.swift index 55cce1f8..9da0026a 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailView.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailView.swift @@ -55,6 +55,7 @@ public struct PingPongDetailView: View { } ) .ignoresSafeArea(.all, edges: .bottom) + .bottleAlert($store.scope(state: \.destination?.alert, action: \.destination.alert)) } } diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionFeature.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionFeature.swift index 3a88ad0d..1ba3a024 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionFeature.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionFeature.swift @@ -33,26 +33,7 @@ extension IntroductionFeature { return .none case .stopTaskButtonTapped: - state.destination = .alert(.init( - title: { TextState("중단하기") }, - actions: { - ButtonState( - role: .destructive, - action: .confirmStopTalk, - label: { TextState("중단하기") }) - }, - message: { TextState("중단 시 모든 핑퐁 내용이 사라져요. 정말 중단하시겠어요?") } - )) - return .none - - case let .destination(.presented(.alert(alert))): - switch alert { - case .confirmStopTalk: - return .run { [bottleID = state.bottleID] send in - try await bottleClient.stopTalk(bottleID: bottleID) - await send(.delegate(.popToRootDidRequired)) - } - } + return .send(.delegate(.stopTaskButtonTapped)) case .refreshPingPongDidRequired: return .run { [bottleID = state.bottleID] send in @@ -62,7 +43,7 @@ extension IntroductionFeature { await send(.introductionFetched(pingPong.introduction ?? [])) } - case .binding, .alert: + case .binding: return .none default: diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionFeatureInterface.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionFeatureInterface.swift index 52be5fb3..17e4d8df 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionFeatureInterface.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionFeatureInterface.swift @@ -68,9 +68,7 @@ public struct IntroductionFeature { + (interest?.etc ?? []) + (interest?.sports ?? []) } - - @Presents var destination: Destination.State? - + public init (bottleID: Int) { self.bottleID = bottleID } @@ -88,32 +86,14 @@ public struct IntroductionFeature { // ETC. case binding(BindingAction) - case destination(PresentationAction) - - case alert(Alert) - public enum Alert: Equatable { - case confirmStopTalk - } - case delegate(Delegate) public enum Delegate { - case popToRootDidRequired + case stopTaskButtonTapped } } public var body: some ReducerOf { reducer - .ifLet(\.$destination, action: \.destination) } } - -// MARK: - Destination - -extension IntroductionFeature { - @Reducer(state: .equatable) - public enum Destination { - case alert(AlertState) - } -} - diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionView.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionView.swift index 16d1f144..9744ee5a 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionView.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionView.swift @@ -81,9 +81,7 @@ public struct IntroductionView: View { .padding(.top, 32.0) } .scrollIndicators(.hidden) - .alert($store.scope(state: \.destination?.alert, action: \.destination.alert)) .background(to: ColorToken.background(.primary)) - } } } diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeature.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeature.swift index 5a4dc213..a03ed829 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeature.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeature.swift @@ -83,30 +83,10 @@ extension QuestionAndAnswerFeature { } case .stopTalkButtonDidTapped: - state.destination = .alert(.init( - title: { TextState("중단하기") }, - actions: { - ButtonState( - role: .destructive, - action: .confirmStopTalk, - label: { TextState("중단하기") }) - }, - message: { TextState("중단 시 모든 핑퐁 내용이 사라져요. 정말 중단하시겠어요?") } - )) - return .none + return .send(.delegate(.stopTaskButtonDidTapped)) case .refreshDidPulled: return .send(.delegate(.refreshPingPong)) - - case let .destination(.presented(.alert(alert))): - switch alert { - case .confirmStopTalk: - state.isShowLoadingIndicator = true - return .run { [bottleID = state.bottleID] send in - try await bottleClient.stopTalk(bottleID: bottleID) - await send(.delegate(.popToRootDidRequired)) - } - } case .binding(\.firstLetterTextFieldContent): if state.firstLetterTextFieldContent.count >= 50 { @@ -132,7 +112,7 @@ extension QuestionAndAnswerFeature { } return .none - case .binding, .destination, .alert: + case .binding: return .none default: diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeatureInterface.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeatureInterface.swift index f4144743..770ba42a 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeatureInterface.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeatureInterface.swift @@ -114,9 +114,7 @@ public struct QuestionAndAnswerFeature { } var finalSelectIsSelctedYesButton: Bool var finalSelectIsSelctedNoButton: Bool - - @Presents var destination: Destination.State? - + public init(bottleID: Int) { self.bottleID = bottleID self.isShowLoadingIndicator = false @@ -181,12 +179,6 @@ public struct QuestionAndAnswerFeature { // ETC. case binding(BindingAction) - case destination(PresentationAction) - - case alert(Alert) - public enum Alert: Equatable { - case confirmStopTalk - } case delegate(Delegate) @@ -194,21 +186,12 @@ public struct QuestionAndAnswerFeature { case reloadPingPongRequired case popToRootDidRequired case refreshPingPong + case stopTaskButtonDidTapped } } public var body: some ReducerOf { BindingReducer() reducer - .ifLet(\.$destination, action: \.destination) - } -} - -// MARK: - Destination - -extension QuestionAndAnswerFeature { - @Reducer(state: .equatable) - public enum Destination { - case alert(AlertState) } } diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerView.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerView.swift index 22c45bfe..24270272 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerView.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerView.swift @@ -134,7 +134,6 @@ public struct QuestionAndAnswerView: View { LoadingIndicator() } } - .alert($store.scope(state: \.destination?.alert, action: \.destination.alert)) .background(to: ColorToken.background(.primary)) .toolbar(.hidden, for: .bottomBar) diff --git a/Projects/Feature/Report/Interface/Sources/ReportUserFeature.swift b/Projects/Feature/Report/Interface/Sources/ReportUserFeature.swift index 9a008b48..b854751c 100644 --- a/Projects/Feature/Report/Interface/Sources/ReportUserFeature.swift +++ b/Projects/Feature/Report/Interface/Sources/ReportUserFeature.swift @@ -28,10 +28,19 @@ extension ReportUserFeature { print("tapped") state.destination = .alert(.init( title: { TextState("신고하기")}, - actions: { ButtonState( - role: .destructive, - action: .confirmReport, - label: { TextState("신고하기") }) }, + actions: { + ButtonState( + role: .cancel, + action: .confirmReport, + label: { TextState("계속하기") } + ) + + ButtonState( + role: .destructive, + action: .dismiss, + label: { TextState("중단하기") } + ) + }, message: { TextState("접수 후 취소할 수 없으며 해당 사용자는 차단되요.\n정말 신고하시겠어요?")})) return .none @@ -43,10 +52,17 @@ extension ReportUserFeature { } return .none - case .destination(.presented(.alert(.confirmReport))): - return .run { [userProfile = state.userProfile, reportText = state.reportText] send in - try await reportClient.reportUser(userReportInfo: .init(reason: reportText, userId: userProfile.userID)) - await send(.delegate(.reportDidCompleted)) + case let .destination(.presented(.alert(alert))): + switch alert { + case .confirmReport: + return .run { [userProfile = state.userProfile, reportText = state.reportText] send in + try await reportClient.reportUser(userReportInfo: .init(reason: reportText, userId: userProfile.userID)) + await send(.delegate(.reportDidCompleted)) + } + + case .dismiss: + state.destination = nil + return .none } case .binding(\.reportText): diff --git a/Projects/Feature/Report/Interface/Sources/ReportUserFeatureInterface.swift b/Projects/Feature/Report/Interface/Sources/ReportUserFeatureInterface.swift index 9c8b487f..6b1fe780 100644 --- a/Projects/Feature/Report/Interface/Sources/ReportUserFeatureInterface.swift +++ b/Projects/Feature/Report/Interface/Sources/ReportUserFeatureInterface.swift @@ -70,6 +70,7 @@ public struct ReportUserFeature { case alert(Alert) public enum Alert: Equatable { case confirmReport + case dismiss } // ETC diff --git a/Projects/Feature/Report/Interface/Sources/ReportUserView.swift b/Projects/Feature/Report/Interface/Sources/ReportUserView.swift index 3f626967..bef7669e 100644 --- a/Projects/Feature/Report/Interface/Sources/ReportUserView.swift +++ b/Projects/Feature/Report/Interface/Sources/ReportUserView.swift @@ -35,7 +35,7 @@ public struct ReportUserView: View { } } .padding(.horizontal, .md) - .alert($store.scope(state: \.destination?.alert, action: \.destination.alert)) + .bottleAlert($store.scope(state: \.destination?.alert, action: \.destination.alert)) .toolbar(.hidden, for: .bottomBar) } } diff --git a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachView.swift b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachView.swift index bcfea92b..cb523178 100644 --- a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachView.swift +++ b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachView.swift @@ -64,7 +64,7 @@ public struct SandBeachView: View { } } } - .alert($store.scope(state: \.destination?.alert, action: \.destination.alert)) + .bottleAlert($store.scope(state: \.destination?.alert, action: \.destination.alert)) .onAppear { store.send(.onAppear) } diff --git a/Projects/Feature/Sources/SplashView/SplashView.swift b/Projects/Feature/Sources/SplashView/SplashView.swift index 52d19d52..7329ec31 100644 --- a/Projects/Feature/Sources/SplashView/SplashView.swift +++ b/Projects/Feature/Sources/SplashView/SplashView.swift @@ -26,7 +26,7 @@ public struct SplashView: View { Image.BottleImageSystem.illustraition(.splash).image } - .alert($store.scope(state: \.destination?.alert, action: \.destination.alert)) + .bottleAlert($store.scope(state: \.destination?.alert, action: \.destination.alert)) .ignoresSafeArea() .task { store.send(.onAppear) From 9a14959c09c87a6634a017ede1f990362a460ad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=ED=98=84=EA=B7=9C?= <48830320+leemhyungyu@users.noreply.github.com> Date: Wed, 25 Sep 2024 21:17:01 +0900 Subject: [PATCH 09/11] =?UTF-8?q?feat:=20=ED=83=88=ED=87=B4=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20Alert=20=20message=20=EC=88=98=EC=A0=95=20(#274)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/AccountSetting/AccountSettingFeature.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift index b3ad4753..bc4aed2e 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift @@ -55,7 +55,7 @@ extension AccountSettingFeature { ButtonState(role: .cancel, action: .confirmWithdrawal, label: { TextState("탈퇴하기") }) ButtonState(role: .destructive, action: .dismiss, label: { TextState("계속 이용하기") }) }, - message: { TextState("탈퇴 시 계정 복구가 어려워요.\n정말 탈퇴하시겠어요?") } + message: { TextState("탈퇴 시 48시간 동안 재가입이 불가능하며 계정 복구가 어려워요.\n정말 탈퇴하시겠어요?") } )) return .none From e130ab925421cddac817b1b886ae488961b2dcea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=ED=98=84=EA=B7=9C?= <48830320+leemhyungyu@users.noreply.github.com> Date: Wed, 25 Sep 2024 21:18:03 +0900 Subject: [PATCH 10/11] =?UTF-8?q?[Feature/#275]=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EB=AF=B8=ED=97=88=EC=9A=A9=20=EC=8B=9C=20?= =?UTF-8?q?alert=20=EC=B6=94=EA=B0=80=20(#276)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: AppDelegate 푸시 수신 상태 Notification 등록 * feat: UserClient 푸시 알림 허용 상태 로직 추가 * feat: 푸시 알림 허용 상태에 따른 알림설정 화면 로직 구현 * feat: UserClient 푸쉬알림허용상태 Publisher 구현 * feat: 푸쉬알림허용상태에 따른 로직 변경 * feat: 토글 버튼 binding 코드 개선 - 코드리뷰 반영 * feat: UserClient UserDefaultKeys enum 추가 * feat: 오탈자 수정 - pushNotificationSubject -> pushNotificationAllowStatusSubject --- Projects/App/Sources/AppDelegate.swift | 42 +++++- .../User/Interface/Sources/UserClient.swift | 22 ++++ Projects/Domain/User/Sources/UserClient.swift | 29 ++++- .../AlertSetting/AlertSettingFeature.swift | 121 +++++++++++++----- .../AlertSettingFeatureInterface.swift | 27 +++- .../AlertSetting/AlertSettingView.swift | 1 + .../Sources/App/AppDelegateFeature.swift | 9 ++ 7 files changed, 205 insertions(+), 46 deletions(-) diff --git a/Projects/App/Sources/AppDelegate.swift b/Projects/App/Sources/AppDelegate.swift index 1b193461..870f4114 100644 --- a/Projects/App/Sources/AppDelegate.swift +++ b/Projects/App/Sources/AppDelegate.swift @@ -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, @@ -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 + ) + } +} diff --git a/Projects/Domain/User/Interface/Sources/UserClient.swift b/Projects/Domain/User/Interface/Sources/UserClient.swift index e80d44c8..34fd1927 100644 --- a/Projects/Domain/User/Interface/Sources/UserClient.swift +++ b/Projects/Domain/User/Interface/Sources/UserClient.swift @@ -7,6 +7,8 @@ import Foundation +import Combine + public struct UserClient { private let _isLoggedIn: () -> Bool private let _isAppDeleted: () -> Bool @@ -14,10 +16,17 @@ public struct UserClient { 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(true) + + public var pushNotificationAllowStatusPublisher: AnyPublisher { + return pushNotificationAllowStatusSubject.eraseToAnyPublisher() + } public init( isLoggedIn: @escaping () -> Bool, @@ -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 @@ -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 @@ -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) } diff --git a/Projects/Domain/User/Sources/UserClient.swift b/Projects/Domain/User/Sources/UserClient.swift index 303f1feb..c46472f1 100644 --- a/Projects/Domain/User/Sources/UserClient.swift +++ b/Projects/Domain/User/Sources/UserClient.swift @@ -17,6 +17,13 @@ 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 { @@ -24,34 +31,42 @@ extension UserClient: DependencyKey { 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))) diff --git a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift index 71617f7c..4a719871 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift @@ -6,10 +6,14 @@ // import Foundation +import Combine import DomainUser import DomainUserInterface +import CoreURLHandlerInterface +import CoreLoggerInterface + import ComposableArchitecture extension AlertSettingFeature { @@ -20,11 +24,24 @@ extension AlertSettingFeature { let reducer = Reduce { 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)) @@ -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 @@ -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) diff --git a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift index 3f4ae4c0..0f554493 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift @@ -6,6 +6,7 @@ // import Foundation +import Combine import DomainUserInterface @@ -18,20 +19,30 @@ public struct AlertSettingFeature { public init(reducer: Reduce) { self.reducer = reducer } + // Desination + @Reducer(state: .equatable) + public enum Destination { + case alert(AlertState) + } @ObservableState public struct State: Equatable { + var isAllowPushNotification: Bool public var isOnRandomBottleToggle: Bool public var isOnArrivalBottleToggle: Bool public var isOnPingPongToggle: Bool public var isOnMarketingToggle: Bool + @Presents var destination: Destination.State? + public init( + isAllowPushNotification: Bool = false, isOnRandomBottleToggle: Bool = false, isOnArrivalBottleToggle: Bool = false, isOnPingPongToggle: Bool = false, isOnMarketingToggle: Bool = false ) { + self.isAllowPushNotification = isAllowPushNotification self.isOnRandomBottleToggle = isOnRandomBottleToggle self.isOnArrivalBottleToggle = isOnArrivalBottleToggle self.isOnPingPongToggle = isOnPingPongToggle @@ -46,15 +57,26 @@ public struct AlertSettingFeature { case arrivalBottleToggleDidFetched(isOn: Bool) case pingpongToggleDidFetched(isOn: Bool) case marketingToggleDidFetched(isOn: Bool) + case pushNotificationAlertDidRequired + case pushNotificationAllowed(isAllow: Bool) + case alertStateFetchDidRequest // UserAction - case toggleDidChanged(alertState: UserAlertState) + case toggleDidChanged(alertState: UserAlertState, id: ID) case backButtonDidTapped + // ETC case binding(BindingAction) + case destination(PresentationAction) + + // Alert + case alert(Alert) + public enum Alert: Equatable { + case confirmPushNotification + } } - enum ID: Hashable { + public enum ID: Hashable { case randomBottle case arrivalBottle case pingping @@ -64,5 +86,6 @@ public struct AlertSettingFeature { public var body: some ReducerOf { BindingReducer() reducer + .ifLet(\.$destination, action: \.destination) } } diff --git a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift index d9c9840b..d0428bea 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift @@ -39,6 +39,7 @@ public struct AlertSettingView: View { .setNavigationBar { makeNaivgationleftButton { store.send(.backButtonDidTapped) } } + .bottleAlert($store.scope(state: \.destination?.alert, action: \.destination.alert)) .onLoad { store.send(.onLoad) } } } diff --git a/Projects/Feature/Sources/App/AppDelegateFeature.swift b/Projects/Feature/Sources/App/AppDelegateFeature.swift index 4233bede..fb7b9659 100644 --- a/Projects/Feature/Sources/App/AppDelegateFeature.swift +++ b/Projects/Feature/Sources/App/AppDelegateFeature.swift @@ -7,6 +7,8 @@ import Foundation +import DomainUserInterface + import ComposableArchitecture import KakaoSDKCommon @@ -20,6 +22,7 @@ public struct AppDelegateFeature { public enum Action { case didFinishLunching case didReceivedFcmToken(fcmToken: String) + case pushNotificationAllowStatusDidChanged(isAllow: Bool) // Delegate case delegate(Delegate) @@ -37,6 +40,8 @@ public struct AppDelegateFeature { state: inout State, action: Action ) -> EffectOf { + @Dependency(\.userClient) var userClient + switch action { case .didFinishLunching: guard let kakaoAppKey = Bundle.main.infoDictionary?["KAKAO_APP_KEY"] as? String else { @@ -51,6 +56,10 @@ public struct AppDelegateFeature { await send(.delegate(.fcmTokenDidRecevied(fcmToken: fcmToken))) } + case let .pushNotificationAllowStatusDidChanged(isAllow): + userClient.updatePushNotificationAllowStatus(isAllow: isAllow) + return .none + default: return .none } From 7053585dfbfcc52ed35d2b14e3f1e6ef99504da5 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Wed, 25 Sep 2024 21:19:22 +0900 Subject: [PATCH 11/11] =?UTF-8?q?chore:=20=EB=B9=8C=EB=93=9C=20=EB=84=98?= =?UTF-8?q?=EB=B2=84=201.0.8=20(30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift index d077af94..3e24b704 100644 --- a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift @@ -11,7 +11,7 @@ public extension InfoPlist { static var app: InfoPlist { return .extendingDefault(with: [ "CFBundleShortVersionString": "1.0.8", - "CFBundleVersion": "29", + "CFBundleVersion": "30", "UIUserInterfaceStyle": "Light", "CFBundleName": "보틀", "UILaunchScreen": [ @@ -44,7 +44,7 @@ public extension InfoPlist { static var example: InfoPlist { return .extendingDefault(with: [ "CFBundleShortVersionString": "1.0.8", - "CFBundleVersion": "29", + "CFBundleVersion": "30", "UIUserInterfaceStyle": "Light", "UILaunchScreen": [:], "UISupportedInterfaceOrientations": [