diff --git a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Modules.swift b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Modules.swift index ad0ef009..9f4730b8 100644 --- a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Modules.swift +++ b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Modules.swift @@ -47,6 +47,7 @@ public extension ModulePath { public extension ModulePath { enum Domain: String, CaseIterable { + case User case Report case WebView case Bottle diff --git a/Projects/Domain/Auth/Interface/Sources/AuthClient.swift b/Projects/Domain/Auth/Interface/Sources/AuthClient.swift index 816bd13a..98831064 100644 --- a/Projects/Domain/Auth/Interface/Sources/AuthClient.swift +++ b/Projects/Domain/Auth/Interface/Sources/AuthClient.swift @@ -18,6 +18,7 @@ public struct AuthClient { private let _revokeAppleLogin: () async throws -> Void private let fetchAppleClientSecret: () async throws -> String private let registerUserProfile: (String) async throws -> Void + private let _removeAllToken: () -> Void public init( signInWithKakao: @escaping () async throws -> UserInfo, @@ -29,7 +30,8 @@ public struct AuthClient { refreshAppleToken: @escaping () async throws -> AppleToken, revokeAppleLogin: @escaping () async throws -> Void, fetchAppleClientSecret: @escaping () async throws -> String, - registerUserProfile: @escaping (String) async throws -> Void + registerUserProfile: @escaping (String) async throws -> Void, + removeAllToken: @escaping () -> Void ) { self.signInWithKakao = signInWithKakao self.signInWithApple = signInWithApple @@ -41,6 +43,7 @@ public struct AuthClient { self._revokeAppleLogin = revokeAppleLogin self.fetchAppleClientSecret = fetchAppleClientSecret self.registerUserProfile = registerUserProfile + self._removeAllToken = removeAllToken } public func signInWithKakao() async throws -> UserInfo { @@ -81,5 +84,9 @@ public struct AuthClient { public func registerUserProfile(userName: String) async throws { try await registerUserProfile(userName) } + + public func removeAllToken() { + _removeAllToken() + } } diff --git a/Projects/Domain/Auth/Sources/AuthClient.swift b/Projects/Domain/Auth/Sources/AuthClient.swift index 2d0293b1..581a331e 100644 --- a/Projects/Domain/Auth/Sources/AuthClient.swift +++ b/Projects/Domain/Auth/Sources/AuthClient.swift @@ -84,6 +84,9 @@ extension AuthClient: DependencyKey { registerUserProfile: { userName in let requestDTO = ProfileRequestDTO(name: userName) try await networkManager.reqeust(api: .apiType(AuthAPI.profile(requestDTO))) + }, + removeAllToken: { + LocalAuthDataSourceImpl.removeAllToken() } ) } diff --git a/Projects/Domain/Auth/Sources/DataSource/LocalAuthDataSourceImpl.swift b/Projects/Domain/Auth/Sources/DataSource/LocalAuthDataSourceImpl.swift index 2d089f4c..ae70d8ef 100644 --- a/Projects/Domain/Auth/Sources/DataSource/LocalAuthDataSourceImpl.swift +++ b/Projects/Domain/Auth/Sources/DataSource/LocalAuthDataSourceImpl.swift @@ -25,4 +25,8 @@ struct LocalAuthDataSourceImpl: LocalAuthDataSource { static func checkTokeinIsExist() -> Bool { return !KeyChainTokenStore.shared.load(property: .accessToken).isEmpty } + + static func removeAllToken() { + KeyChainTokenStore.shared.deleteAll() + } } diff --git a/Projects/Domain/Bottle/Interface/Sources/API/BottleAPI.swift b/Projects/Domain/Bottle/Interface/Sources/API/BottleAPI.swift index 06e4e9ba..868ebb7b 100644 --- a/Projects/Domain/Bottle/Interface/Sources/API/BottleAPI.swift +++ b/Projects/Domain/Bottle/Interface/Sources/API/BottleAPI.swift @@ -17,6 +17,7 @@ public enum BottleAPI { bottleID: Int, registerLetterAnswerRequestDTO: RegisterLetterAnswerRequestDTO ) + case readBottle(bottleID: Int) case imageShare( bottleID: Int, imageShareRequestDTO: BottleImageShareRequestDTO @@ -37,6 +38,8 @@ extension BottleAPI: BaseTargetType { return "api/v1/bottles/ping-pong" case let .fetchBottlePingPong(bottleID): return "api/v1/bottles/ping-pong/\(bottleID)" + case let .readBottle(bottleID): + return "api/v1/bottles/ping-pong/\(bottleID)/read" case let .registerLetterAnswer(bottleID, _): return "api/v1/bottles/ping-pong/\(bottleID)/letters" case let .imageShare(bottleID, _): @@ -56,6 +59,8 @@ extension BottleAPI: BaseTargetType { return .get case .fetchBottlePingPong: return .get + case .readBottle: + return .post case .registerLetterAnswer: return .post case .imageShare: @@ -75,6 +80,8 @@ extension BottleAPI: BaseTargetType { return .requestPlain case .fetchBottlePingPong: return .requestPlain + case .readBottle: + return .requestPlain case let .registerLetterAnswer(_, registerLetterAnswerRequestDTO): return .requestJSONEncodable(registerLetterAnswerRequestDTO) case let .imageShare(_, imageShareRequestDTO): diff --git a/Projects/Domain/Bottle/Interface/Sources/BottleClient.swift b/Projects/Domain/Bottle/Interface/Sources/BottleClient.swift index 72e53039..371c70f2 100644 --- a/Projects/Domain/Bottle/Interface/Sources/BottleClient.swift +++ b/Projects/Domain/Bottle/Interface/Sources/BottleClient.swift @@ -12,6 +12,7 @@ public struct BottleClient { private let fetchUserBottleInfo: () async throws -> UserBottleInfo private let fetchBottleStorageList: () async throws -> BottleStorageList private let fetchBottlePingPong: (_ bottleID: Int) async throws -> BottlePingPong + private let readBottle: (_ bottleID: Int) async throws -> Void private let registerLetterAnswer: (_ bottleID: Int, _ order: Int, _ answer: String) async throws -> Void private let shareImage: (_ bottleID: Int, _ willShare: Bool) async throws -> Void private let finalSelect: (_ bottleID: Int, _ willMatch: Bool) async throws -> Void @@ -21,6 +22,7 @@ public struct BottleClient { fetchUserBottleInfo: @escaping () async throws -> UserBottleInfo, fetchBottleStorageList: @escaping () async throws -> BottleStorageList, fetchBottlePingPong: @escaping (_ bottleID: Int) async throws -> BottlePingPong, + readBottle: @escaping (_ bottleID: Int) async throws -> Void, registerLetterAnswer: @escaping (_ bottleID: Int, _ order: Int, _ answer: String) async throws -> Void, shareImage: @escaping (_ bottleID: Int, _ willShare: Bool) async throws -> Void, finalSelect: @escaping (_ bottleID: Int, _ willMatch: Bool) async throws -> Void, @@ -29,6 +31,7 @@ public struct BottleClient { self.fetchUserBottleInfo = fetchUserBottleInfo self.fetchBottleStorageList = fetchBottleStorageList self.fetchBottlePingPong = fetchBottlePingPong + self.readBottle = readBottle self.registerLetterAnswer = registerLetterAnswer self.shareImage = shareImage self.finalSelect = finalSelect @@ -47,6 +50,10 @@ public struct BottleClient { try await fetchBottlePingPong(bottleID) } + public func readBottle(bottleID: Int) async throws { + try await readBottle(bottleID) + } + public func registerLetterAnswer(bottleID: Int, order: Int, answer: String) async throws { try await registerLetterAnswer(bottleID, order, answer) } diff --git a/Projects/Domain/Bottle/Interface/Sources/Entity/BottleStorageList.swift b/Projects/Domain/Bottle/Interface/Sources/Entity/BottleStorageList.swift index 61b083e8..a8d52ae5 100644 --- a/Projects/Domain/Bottle/Interface/Sources/Entity/BottleStorageList.swift +++ b/Projects/Domain/Bottle/Interface/Sources/Entity/BottleStorageList.swift @@ -22,7 +22,7 @@ public struct BottleStorageList: Decodable { public struct BottleStorageItem: Decodable, Equatable { public let age: Int? public let id: Int - public let isRead: Bool? + public let isRead: Bool public let keyword: [String] public let mbti: String public let userImageUrl: String @@ -31,7 +31,7 @@ public struct BottleStorageItem: Decodable, Equatable { public init( age: Int?, id: Int, - isRead: Bool?, + isRead: Bool, keyword: [String], mbti: String, userImageUrl: String, diff --git a/Projects/Domain/Bottle/Sources/BottleClient.swift b/Projects/Domain/Bottle/Sources/BottleClient.swift index e6656134..b5e372b1 100644 --- a/Projects/Domain/Bottle/Sources/BottleClient.swift +++ b/Projects/Domain/Bottle/Sources/BottleClient.swift @@ -36,6 +36,9 @@ extension BottleClient: DependencyKey { ).toDomain() return bottlePingPong }, + readBottle: { id in + try await networkManager.reqeust(api: .apiType(BottleAPI.readBottle(bottleID: id))) + }, registerLetterAnswer: { bottleID, order, answer in try await networkManager.reqeust(api: .apiType(BottleAPI.registerLetterAnswer( bottleID: bottleID, diff --git a/Projects/Domain/User/Interface/Sources/UserClient.swift b/Projects/Domain/User/Interface/Sources/UserClient.swift new file mode 100644 index 00000000..c140a255 --- /dev/null +++ b/Projects/Domain/User/Interface/Sources/UserClient.swift @@ -0,0 +1,43 @@ +// +// UserClient.swift +// DomainUserInterface +// +// Created by 임현규 on 8/22/24. +// + +import Foundation + +public struct UserClient { + private let _isLoggedIn: () -> Bool + private let _isAppDeleted: () -> Bool + private let updateLoginState: (Bool) -> Void + private let updateDeleteState: (Bool) -> Void + + public init( + isLoggedIn: @escaping () -> Bool, + isAppDeleted: @escaping () -> Bool, + updateLoginState: @escaping (Bool) -> Void, + updateDeleteState: @escaping (Bool) -> Void + ) { + self._isLoggedIn = isLoggedIn + self._isAppDeleted = isAppDeleted + self.updateLoginState = updateLoginState + self.updateDeleteState = updateDeleteState + } + + public func isLoggedIn() -> Bool { + _isLoggedIn() + } + + public func isAppDeleted() -> Bool { + _isAppDeleted() + } + + public func updateLoginState(isLoggedIn: Bool) { + updateLoginState(isLoggedIn) + } + + public func updateDeleteState(isDelete: Bool) { + updateDeleteState(isDelete) + } +} diff --git a/Projects/Domain/User/Project.swift b/Projects/Domain/User/Project.swift new file mode 100644 index 00000000..e071d174 --- /dev/null +++ b/Projects/Domain/User/Project.swift @@ -0,0 +1,42 @@ +import ProjectDescription +import ProjectDescriptionHelpers +import DependencyPlugin + +let project = Project.makeModule( + name: ModulePath.Domain.name+ModulePath.Domain.User.rawValue, + targets: [ + .domain( + interface: .User, + factory: .init(dependencies: [ + .core + ]) + ), + .domain( + implements: .User, + factory: .init( + dependencies: [ + .domain(interface: .User) + ] + ) + ), + + .domain( + testing: .User, + factory: .init( + dependencies: [ + .domain(interface: .User) + ] + ) + ), + .domain( + tests: .User, + factory: .init( + dependencies: [ + .domain(testing: .User), + .domain(implements: .User) + ] + ) + ), + + ] +) diff --git a/Projects/Domain/User/Sources/UserClient.swift b/Projects/Domain/User/Sources/UserClient.swift new file mode 100644 index 00000000..96f0107f --- /dev/null +++ b/Projects/Domain/User/Sources/UserClient.swift @@ -0,0 +1,45 @@ +// +// UserClient.swift +// DomainUser +// +// Created by 임현규 on 8/22/24. +// + +import Foundation + +import DomainUserInterface + +import CoreKeyChainStore + +import ComposableArchitecture + +extension UserClient: DependencyKey { + static public var liveValue: UserClient = .live() + + static func live() -> UserClient { + return .init( + isLoggedIn: { + return UserDefaults.standard.bool(forKey: "loginState") + }, + + isAppDeleted: { + return !UserDefaults.standard.bool(forKey: "deleteState") + }, + + updateLoginState: { isLoggedIn in + UserDefaults.standard.set(isLoggedIn, forKey: "loginState") + }, + + updateDeleteState: { isDelete in + UserDefaults.standard.set(!isDelete, forKey: "deleteState") + } + ) + } +} + +extension DependencyValues { + public var userClient: UserClient { + get { self[UserClient.self] } + set { self[UserClient.self] = newValue } + } +} diff --git a/Projects/Domain/User/Testing/Sources/UserTesting.swift b/Projects/Domain/User/Testing/Sources/UserTesting.swift new file mode 100644 index 00000000..b1853ce6 --- /dev/null +++ b/Projects/Domain/User/Testing/Sources/UserTesting.swift @@ -0,0 +1 @@ +// This is for Tuist diff --git a/Projects/Domain/User/Tests/Sources/UserTest.swift b/Projects/Domain/User/Tests/Sources/UserTest.swift new file mode 100644 index 00000000..b89cc7d1 --- /dev/null +++ b/Projects/Domain/User/Tests/Sources/UserTest.swift @@ -0,0 +1,11 @@ +import XCTest + +final class UserTests: XCTestCase { + override func setUpWithError() throws {} + + override func tearDownWithError() throws {} + + func testExample() { + XCTAssertEqual(1, 1) + } +} diff --git a/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeature.swift b/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeature.swift index 3b8388cc..5b46a6a8 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeature.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeature.swift @@ -25,8 +25,12 @@ extension BottleStorageFeature { state.doneBottlsList = bottleStorageList.doneBottles return .none - case let .bottleStorageItemDidTapped(bottleID, userName): - state.path.append(.pingPongDetail(.init(bottleID: bottleID, userName: userName))) + case let .bottleStorageItemDidTapped(bottleID, isRead, userName): + state.path.append(.pingPongDetail(.init( + bottleID: bottleID, + isRead: isRead, + userName: userName + ))) return .none case let .bottleActiveStateTabButtonTapped(activeState): diff --git a/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeatureInterface.swift b/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeatureInterface.swift index 28831cb4..96f821ef 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeatureInterface.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeatureInterface.swift @@ -55,7 +55,11 @@ public struct BottleStorageFeature { // 보틀 리스트 case bottleStorageListFetched(BottleStorageList) - case bottleStorageItemDidTapped(bottleID: Int, userName: String) + case bottleStorageItemDidTapped( + bottleID: Int, + isRead: Bool, + userName: String + ) case selectedTabDidChanged(selectedTab: TabType) case delegate(Delegate) // ETC. diff --git a/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageView.swift b/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageView.swift index 6e36bc6f..748c78d9 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageView.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageView.swift @@ -124,10 +124,14 @@ private extension BottleStorageView { mbti: bottle.mbti, keywords: bottle.keyword, imageURL: bottle.userImageUrl, - isRead: bottle.isRead ?? false + isRead: bottle.isRead ) .asButton { - store.send(.bottleStorageItemDidTapped(bottleID: bottle.id, userName: bottle.userName ?? "")) + store.send(.bottleStorageItemDidTapped( + bottleID: bottle.id, + isRead: bottle.isRead, + userName: bottle.userName ?? "" + )) } } } diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PhotoSharePingPongView.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PhotoSharePingPongView.swift index 4bc3a73b..65010224 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PhotoSharePingPongView.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PhotoSharePingPongView.swift @@ -6,6 +6,7 @@ // import SwiftUI + import SharedDesignSystem public struct PhotoSharePingPongView: View { @@ -139,20 +140,17 @@ private extension PhotoSharePingPongView { @ViewBuilder var peerProfileImage: some View { - if let peerProfileImageURL = photoShareState.peerProfileImageURL { - GeometryReader { geo in - RemoteImageView( - imageURL: peerProfileImageURL, - downsamplingWidth: 150, - downsamplingHeight: 150 - ) - .cornerRadius(.md, corenrs: [.topRight, .bottomLeft, .bottomRight]) - .frame(height: geo.size.width) - } - .aspectRatio(1, contentMode: .fit) - } else { - EmptyView() + GeometryReader { geo in + RemoteImageView( + imageURL: photoShareState.peerProfileImageURL, + downsamplingWidth: 150, + downsamplingHeight: 150 + ) + .cornerRadius(.md, corenrs: [.topRight, .bottomLeft, .bottomRight]) + .frame(height: geo.size.width) } + .aspectRatio(1, contentMode: .fit) + .preventScreenshot() } @ViewBuilder @@ -167,6 +165,7 @@ private extension PhotoSharePingPongView { ) .cornerRadius(.md, corenrs: [.topRight, .topLeft, .bottomLeft]) .frame(height: geo.size.width) + .preventScreenshot() } .aspectRatio(1, contentMode: .fit) } else { 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 f886e760..cc01fbd4 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeature.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeature.swift @@ -70,23 +70,26 @@ extension PingPongDetailFeature { self.init(reducer: reducer) func fetchPingPong(state: inout State) -> Effect { - return .run { [bottleID = state.bottleID, userName = state.userName] send in - let pingPong = try await bottleClient.fetchBottlePingPong(bottleID: bottleID) + return .run { [bottleID = state.bottleID, isRead = state.isRead , userName = state.userName] send in + async let pingPong = await bottleClient.fetchBottlePingPong(bottleID: bottleID) + if !isRead { + async let _ = await bottleClient.readBottle(bottleID: bottleID) + } - await send(.pingPongDidFetched(pingPong)) + try await send(.pingPongDidFetched(pingPong)) - await send(.introduction(.profileFetched(pingPong.userProfile))) - await send(.introduction(.isStoppedFetched(pingPong.isStopped))) - await send(.introduction(.introductionFetched(pingPong.introduction ?? []))) + try await send(.introduction(.profileFetched(pingPong.userProfile))) + try await send(.introduction(.isStoppedFetched(pingPong.isStopped))) + try await send(.introduction(.introductionFetched(pingPong.introduction ?? []))) - await send(.questionAndAnswer(.pingPongDidFetched(pingPong))) - await send(.matching(.matchingStateDidFetched( + try await send(.questionAndAnswer(.pingPongDidFetched(pingPong))) + try await send(.matching(.matchingStateDidFetched( matchResult: pingPong.matchResult, userName: userName, matchingPlace: pingPong.matchResult.meetingPlace, matchingPlaceImageURL: pingPong.matchResult.meetingPlaceImageURL ))) - await send(.pingPongDidFetched(pingPong)) + try await send(.pingPongDidFetched(pingPong)) } catch: { error, send in Log.error(error) } 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 8233870c..886d2cce 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeatureInterface.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeatureInterface.swift @@ -23,6 +23,7 @@ public struct PingPongDetailFeature { @ObservableState public struct State: Equatable { let bottleID: Int + let isRead: Bool var userName: String var pingPong: BottlePingPong? var isStopped: Bool { @@ -35,8 +36,13 @@ public struct PingPongDetailFeature { var matching: MatchingFeature.State var selectedTab: PingPongDetailViewTabType - public init(bottleID: Int, userName: String) { + public init( + bottleID: Int, + isRead: Bool, + userName: String + ) { self.introduction = .init(bottleID: bottleID) + self.isRead = isRead self.questionAndAnswer = .init(bottleID: bottleID) self.matching = .init() self.selectedTab = .introduction 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 8ee0e5b0..16d1f144 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 @@ -27,6 +27,7 @@ public struct IntroductionView: View { } else if store.isStopped == false { UserProfileView( imageURL: store.userImageURL, + isBlurred: true, userName: store.userName, userAge: store.age ) diff --git a/Projects/Feature/Login/Interface/Sources/Login/LoginFeature.swift b/Projects/Feature/Login/Interface/Sources/Login/LoginFeature.swift index c82c41dc..4fd0dcf8 100644 --- a/Projects/Feature/Login/Interface/Sources/Login/LoginFeature.swift +++ b/Projects/Feature/Login/Interface/Sources/Login/LoginFeature.swift @@ -7,18 +7,22 @@ import Foundation +import FeatureOnboardingInterface +import FeatureGeneralSignUpInterface + import DomainAuth import DomainAuthInterface +import DomainUser + import CoreLoggerInterface import CoreKeyChainStore -import FeatureOnboardingInterface -import FeatureGeneralSignUpInterface import ComposableArchitecture extension LoginFeature { public init() { @Dependency(\.authClient) var authClient + @Dependency(\.userClient) var userClient let reducer = Reduce { state, action in switch action { @@ -125,6 +129,8 @@ extension LoginFeature { let token = userInfo.token, isSignUp = userInfo.isSignUp let isCompletedOnboardingIntroduction = userInfo.isCompletedOnboardingIntroduction authClient.saveToken(token: token) + userClient.updateLoginState(isLoggedIn: true) + Log.error(token) if let userName = userInfo.userName, !isSignUp { diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift index 1a24112f..430bfb83 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift @@ -38,7 +38,6 @@ extension MyPageFeature { return .none case .logOutDidCompleted: - KeyChainTokenStore.shared.deleteAll() return .send(.delegate(.logoutDidCompleted)) case .withdrawalButtonDidTapped: @@ -50,7 +49,6 @@ extension MyPageFeature { return .none case .withdrawalDidCompleted: - KeyChainTokenStore.shared.deleteAll() return .send(.delegate(.withdrawalDidCompleted)) case let .destination(.presented(.alert(alert))): diff --git a/Projects/Feature/ProfileSetup/Interface/Sources/ProfileImageUpload/ProfileImageUploadView.swift b/Projects/Feature/ProfileSetup/Interface/Sources/ProfileImageUpload/ProfileImageUploadView.swift index 2c2cd105..b38bd763 100644 --- a/Projects/Feature/ProfileSetup/Interface/Sources/ProfileImageUpload/ProfileImageUploadView.swift +++ b/Projects/Feature/ProfileSetup/Interface/Sources/ProfileImageUpload/ProfileImageUploadView.swift @@ -104,6 +104,8 @@ private extension ProfileImageUploadView { } } } + + selectedItems.removeAll() } } diff --git a/Projects/Feature/Report/Interface/Sources/ReportUserView.swift b/Projects/Feature/Report/Interface/Sources/ReportUserView.swift index f94a02b2..3f626967 100644 --- a/Projects/Feature/Report/Interface/Sources/ReportUserView.swift +++ b/Projects/Feature/Report/Interface/Sources/ReportUserView.swift @@ -50,6 +50,7 @@ private extension ReportUserView { var userProfile: some View { UserProfileView( imageURL: store.userProfile.imageURL, + isBlurred: true, userName: store.userProfile.userName, userAge: store.userProfile.userAge ) diff --git a/Projects/Feature/Sources/App/AppFeature.swift b/Projects/Feature/Sources/App/AppFeature.swift index 9831a230..a20dcc19 100644 --- a/Projects/Feature/Sources/App/AppFeature.swift +++ b/Projects/Feature/Sources/App/AppFeature.swift @@ -12,6 +12,8 @@ import FeatureLoginInterface import FeatureOnboardingInterface import DomainAuth +import DomainAuthInterface + import DomainProfile import CoreKeyChainStore import CoreLoggerInterface @@ -22,6 +24,7 @@ import ComposableArchitecture public struct AppFeature { @Dependency(\.authClient) var authClient @Dependency(\.profileClient) var profileClient + @Dependency(\.userClient) var userClient enum Root { case Login @@ -53,6 +56,9 @@ public struct AppFeature { case appleUserIdDidRevoked case loginCheckCompleted(isLoggedIn: Bool) case profileSelectExistCheckCompleted(isExist: Bool) + + // Error + case requiredInvalidToken } public init() {} @@ -80,8 +86,15 @@ public struct AppFeature { ) -> EffectOf { switch action { case .onAppear: + let isAppDeleted = userClient.isAppDeleted() let isLoggedIn = authClient.checkTokenIsExist() - return .send(.loginCheckCompleted(isLoggedIn: isLoggedIn)) + + if isAppDeleted { + userClient.updateDeleteState(isDelete: false) + return .send(.loginCheckCompleted(isLoggedIn: false)) + } else { + return .send(.loginCheckCompleted(isLoggedIn: isLoggedIn)) + } case .login(.goToMainTab): return changeRoot(.MainTab, state: &state) @@ -93,7 +106,8 @@ public struct AppFeature { let isExistProfileSelect = userProfileStatus == .empty ? false : true return await send(.profileSelectExistCheckCompleted(isExist: isExistProfileSelect)) } catch: { error, send in - // TODO: - 네트워킹 에러 처리 + // TODO: - Domain Error 처리. + await send(.requiredInvalidToken) } } else { return changeRoot(.Login, state: &state) @@ -153,9 +167,10 @@ public struct AppFeature { } case .appleUserIdDidRevoked: - KeyChainTokenStore.shared.deleteAll() return changeRoot(.Login, state: &state) + case .requiredInvalidToken: + return changeRoot(.Login, state: &state) default: return .none } @@ -167,6 +182,8 @@ public struct AppFeature { state.mainTab = nil state.onboarding = nil state.login = LoginFeature.State() + userClient.updateLoginState(isLoggedIn: false) + authClient.removeAllToken() case .MainTab: state.login = nil state.onboarding = nil diff --git a/Projects/Shared/DesignSystem/Example/Sources/DesignSystemExampleView/DesignSystemExampleView.swift b/Projects/Shared/DesignSystem/Example/Sources/DesignSystemExampleView/DesignSystemExampleView.swift index d0b504db..5b83ef92 100644 --- a/Projects/Shared/DesignSystem/Example/Sources/DesignSystemExampleView/DesignSystemExampleView.swift +++ b/Projects/Shared/DesignSystem/Example/Sources/DesignSystemExampleView/DesignSystemExampleView.swift @@ -196,6 +196,16 @@ struct EtcSection: View { ) .frame(width: 300.0, height: 300.0) } + + NavigationLink("Capture Prevent View") { + RemoteImageView( + imageURL: "https://static.wikia.nocookie.net/wallaceandgromit/images/3/38/Gromit-3.png/revision/latest/scale-to-width/360?cb=20191228190308", + downsamplingWidth: 300.0, + downsamplingHeight: 300.0 + ) + .frame(width: 300.0, height: 300.0) + .preventScreenshot() + } }, header: { Text("ETC") diff --git a/Projects/Shared/DesignSystem/Example/Sources/SubViews/UserProfileTest/UserProfileTestView.swift b/Projects/Shared/DesignSystem/Example/Sources/SubViews/UserProfileTest/UserProfileTestView.swift index 86ef0c45..17d2d093 100644 --- a/Projects/Shared/DesignSystem/Example/Sources/SubViews/UserProfileTest/UserProfileTestView.swift +++ b/Projects/Shared/DesignSystem/Example/Sources/SubViews/UserProfileTest/UserProfileTestView.swift @@ -15,6 +15,7 @@ public struct UserProfileTestView: View { VStack(spacing: .xl) { UserProfileView( imageURL: "https://static.wikia.nocookie.net/wallaceandgromit/images/3/38/Gromit-3.png/revision/latest/scale-to-width/360?cb=20191228190308", + isBlurred: true, userName: "임현규", userAge: 26 ) @@ -22,6 +23,7 @@ public struct UserProfileTestView: View { UserProfileView( imageURL: "", + isBlurred: false, userName: "임현규", userAge: 26 ) diff --git a/Projects/Shared/DesignSystem/Resources/Images.xcassets/image_splash.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Images.xcassets/image/image_splash.imageset/Contents.json similarity index 100% rename from Projects/Shared/DesignSystem/Resources/Images.xcassets/image_splash.imageset/Contents.json rename to Projects/Shared/DesignSystem/Resources/Images.xcassets/image/image_splash.imageset/Contents.json diff --git a/Projects/Shared/DesignSystem/Resources/Images.xcassets/image_splash.imageset/icon_splash.svg b/Projects/Shared/DesignSystem/Resources/Images.xcassets/image/image_splash.imageset/icon_splash.svg similarity index 100% rename from Projects/Shared/DesignSystem/Resources/Images.xcassets/image_splash.imageset/icon_splash.svg rename to Projects/Shared/DesignSystem/Resources/Images.xcassets/image/image_splash.imageset/icon_splash.svg diff --git a/Projects/Shared/DesignSystem/Sources/Components/Card/UserProfile/UserProfileView.swift b/Projects/Shared/DesignSystem/Sources/Components/Card/UserProfile/UserProfileView.swift index 999eb710..6ae44506 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Card/UserProfile/UserProfileView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Card/UserProfile/UserProfileView.swift @@ -9,15 +9,18 @@ import SwiftUI public struct UserProfileView: View { private let imageURL: String + private let isBlurred: Bool private let userName: String private let userAge: Int public init( imageURL: String, + isBlurred: Bool, userName: String, userAge: Int ) { self.imageURL = imageURL + self.isBlurred = isBlurred self.userName = userName self.userAge = userAge } @@ -35,14 +38,28 @@ public struct UserProfileView: View { } private extension UserProfileView { + + @ViewBuilder var profileImage: some View { - RemoteImageView( - imageURL: imageURL, - downsamplingWidth: 80.0, - downsamplingHeight: 80.0 - ) - .clipShape(Circle()) - .frame(width: 80, height: 80) + switch isBlurred { + case true: + BlurImageView( + imageURL: imageURL, + downsamplingWidth: 80.0, + downsamplingHeight: 80.0 + ) + .clipShape(Circle()) + .frame(width: 80.0, height: 80.0) + + case false: + RemoteImageView( + imageURL: imageURL, + downsamplingWidth: 80.0, + downsamplingHeight: 80.0 + ) + .clipShape(Circle()) + .frame(width: 80, height: 80) + } } var userNameText: some View { diff --git a/Projects/Shared/DesignSystem/Sources/Components/ImageView/RemoteImageView.swift b/Projects/Shared/DesignSystem/Sources/Components/ImageView/RemoteImageView.swift index b1ecc2ed..9fd628a7 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/ImageView/RemoteImageView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/ImageView/RemoteImageView.swift @@ -10,11 +10,11 @@ import SwiftUI import Kingfisher public struct RemoteImageView: View { - private let imageURL: String + private let imageURL: String? private let downsamplingSize: CGSize public init( - imageURL: String, + imageURL: String?, downsamplingWidth: Double, downsamplingHeight: Double ) { @@ -26,7 +26,7 @@ public struct RemoteImageView: View { } public var body: some View { - KFImage(URL(string: imageURL)) + KFImage(URL(string: imageURL ?? "")) .cancelOnDisappear(true) .placeholder { ColorToken.icon(.secondary).color diff --git a/Projects/Shared/DesignSystem/Sources/ScreenShotPrevent/ScreenShotPreventView.swift b/Projects/Shared/DesignSystem/Sources/ScreenShotPrevent/ScreenShotPreventView.swift new file mode 100644 index 00000000..d6eda934 --- /dev/null +++ b/Projects/Shared/DesignSystem/Sources/ScreenShotPrevent/ScreenShotPreventView.swift @@ -0,0 +1,58 @@ +// +// ScreenShotPreventView.swift +// DesignSystemExample +// +// Created by JongHoon on 8/22/24. +// + +import SwiftUI + +struct ScreenshotPreventView: UIViewRepresentable { + + private let content: () -> Content + + public init(@ViewBuilder content: @escaping () -> Content) { + self.content = content + } + + public func makeUIView(context: Context) -> UIView { + let secureTextField = UITextField() + secureTextField.isSecureTextEntry = true + secureTextField.isUserInteractionEnabled = false + + guard let secureView = secureTextField.layer.sublayers?.first?.delegate as? UIView + else { + return UIView() + } + + secureView.subviews.forEach { subview in + subview.removeFromSuperview() + } + + let hController = UIHostingController(rootView: content()) + hController.view.backgroundColor = .clear + hController.view.translatesAutoresizingMaskIntoConstraints = false + secureView.addSubview(hController.view) + NSLayoutConstraint.activate([ + hController.view.topAnchor.constraint(equalTo: secureView.topAnchor), + hController.view.bottomAnchor.constraint(equalTo: secureView.bottomAnchor), + hController.view.leadingAnchor.constraint(equalTo: secureView.leadingAnchor), + hController.view.trailingAnchor.constraint(equalTo: secureView.trailingAnchor) + ]) + + return secureView + } + + public func updateUIView(_ uiView: UIView, context: Context) { } +} + +public extension View { + @ViewBuilder + func preventScreenshot(_ shouldPrevent: Bool = true) -> some View { + if shouldPrevent { + ScreenshotPreventView { self } + } else { + self + } + } +} diff --git a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift index 8663c1d0..0e81bb24 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.0", - "CFBundleVersion": "17", + "CFBundleShortVersionString": "1.0.1", + "CFBundleVersion": "20", "UIUserInterfaceStyle": "Light", "CFBundleName": "보틀", "UILaunchScreen": [ @@ -39,8 +39,8 @@ public extension InfoPlist { static var example: InfoPlist { return .extendingDefault(with: [ - "CFBundleShortVersionString": "1.0.0", - "CFBundleVersion": "17", + "CFBundleShortVersionString": "1.0.1", + "CFBundleVersion": "20", "UIUserInterfaceStyle": "Light", "UILaunchScreen": [:], "UISupportedInterfaceOrientations": [