|
| 1 | +// |
| 2 | +// SignUpNavigationStackFeature.swift |
| 3 | +// Feature |
| 4 | +// |
| 5 | +// Created by 김도형 on 7/7/24. |
| 6 | + |
| 7 | +import ComposableArchitecture |
| 8 | +import CoreKit |
| 9 | +import Util |
| 10 | + |
| 11 | +@Reducer |
| 12 | +public struct LoginFeature { |
| 13 | + /// - Dependency |
| 14 | + @Dependency(\.dismiss) var dismiss |
| 15 | + @Dependency(\.socialLogin) var socialLogin |
| 16 | + @Dependency(\.authClient) var authClient |
| 17 | + @Dependency(\.userClient) var userClient |
| 18 | + @Dependency(\.userDefaults) var userDefaults |
| 19 | + @Dependency(\.keychain) var keychain |
| 20 | + /// - State |
| 21 | + @ObservableState |
| 22 | + public struct State { |
| 23 | + var path = StackState<Path.State>() |
| 24 | + |
| 25 | + var nickName: String? |
| 26 | + var interests: [String]? |
| 27 | + |
| 28 | + public init() {} |
| 29 | + } |
| 30 | + /// - Action |
| 31 | + public enum Action: FeatureAction, ViewAction { |
| 32 | + case view(View) |
| 33 | + case inner(InnerAction) |
| 34 | + case async(AsyncAction) |
| 35 | + case scope(ScopeAction) |
| 36 | + case delegate(DelegateAction) |
| 37 | + case path(StackActionOf<Path>) |
| 38 | + |
| 39 | + @CasePathable |
| 40 | + public enum View: Equatable { |
| 41 | + /// - Button Tapped |
| 42 | + case appleLoginButtonTapped |
| 43 | + case googleLoginButtonTapped |
| 44 | + } |
| 45 | + public enum InnerAction: Equatable { |
| 46 | + case pushAgreeToTermsView |
| 47 | + case pushRegisterNicknameView |
| 48 | + case pushSelectFieldView(nickname: String) |
| 49 | + case pushSignUpDoneView |
| 50 | + case 애플로그인(SocialLoginInfo) |
| 51 | + case 구글로그인(SocialLoginInfo) |
| 52 | + case 로그인_이후_화면이동(isRegistered: Bool) |
| 53 | + } |
| 54 | + public enum AsyncAction: Equatable { |
| 55 | + case 회원가입 |
| 56 | + case 로그인(SocialLoginInfo) |
| 57 | + } |
| 58 | + public enum ScopeAction { |
| 59 | + case agreeToTerms(AgreeToTermsFeature.Action.DelegateAction) |
| 60 | + case registerNickname(RegisterNicknameFeature.Action.DelegateAction) |
| 61 | + case selectField(SelectFieldFeature.Action.DelegateAction) |
| 62 | + } |
| 63 | + public enum DelegateAction: Equatable { |
| 64 | + case dismissLoginRootView |
| 65 | + case 회원가입_완료_화면_이동 |
| 66 | + } |
| 67 | + } |
| 68 | + /// initiallizer |
| 69 | + public init() {} |
| 70 | + /// - Reducer Core |
| 71 | + private func core(into state: inout State, action: Action) -> Effect<Action> { |
| 72 | + switch action { |
| 73 | + /// - View |
| 74 | + case .view(let viewAction): |
| 75 | + return handleViewAction(viewAction, state: &state) |
| 76 | + /// - Inner |
| 77 | + case .inner(let innerAction): |
| 78 | + return handleInnerAction(innerAction, state: &state) |
| 79 | + /// - Async |
| 80 | + case .async(let asyncAction): |
| 81 | + return handleAsyncAction(asyncAction, state: &state) |
| 82 | + /// - Scope |
| 83 | + case .scope(let scopeAction): |
| 84 | + return handleScopeAction(scopeAction, state: &state) |
| 85 | + /// - Delegate |
| 86 | + case .delegate(let delegateAction): |
| 87 | + return handleDelegateAction(delegateAction, state: &state) |
| 88 | + case .path(let pathAction): |
| 89 | + return handlePathAction(pathAction, state: &state) |
| 90 | + } |
| 91 | + } |
| 92 | + /// - Reducer body |
| 93 | + public var body: some ReducerOf<Self> { |
| 94 | + Reduce(self.core) |
| 95 | + .forEach(\.path, action: \.path) |
| 96 | + } |
| 97 | +} |
| 98 | +//MARK: - FeatureAction Effect |
| 99 | +private extension LoginFeature { |
| 100 | + /// - View Effect |
| 101 | + func handleViewAction(_ action: Action.View, state: inout State) -> Effect<Action> { |
| 102 | + switch action { |
| 103 | + case .appleLoginButtonTapped: |
| 104 | + return .run { send in |
| 105 | + let response = try await socialLogin.appleLogin() |
| 106 | + await send(.async(.로그인(response))) |
| 107 | + } |
| 108 | + |
| 109 | + case .googleLoginButtonTapped: |
| 110 | + return .run { send in |
| 111 | + let response = try await socialLogin.googleLogin() |
| 112 | + await send(.async(.로그인(response))) |
| 113 | + } |
| 114 | + } |
| 115 | + } |
| 116 | + /// - Inner Effect |
| 117 | + func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect<Action> { |
| 118 | + switch action { |
| 119 | + case .pushAgreeToTermsView: |
| 120 | + state.path.append(.agreeToTerms(AgreeToTermsFeature.State())) |
| 121 | + return .none |
| 122 | + case .pushRegisterNicknameView: |
| 123 | + state.path.append(.registerNickname(RegisterNicknameFeature.State())) |
| 124 | + return .none |
| 125 | + case .pushSelectFieldView(let nickname): |
| 126 | + state.path.append(.selecteField(SelectFieldFeature.State(nickname: nickname))) |
| 127 | + return .none |
| 128 | + case .pushSignUpDoneView: |
| 129 | + return .send(.delegate(.회원가입_완료_화면_이동)) |
| 130 | + case let .애플로그인(response): |
| 131 | + return .run { send in |
| 132 | + guard let idToken = response.idToken else { return } |
| 133 | + guard let authCode = response.authCode else { return } |
| 134 | + guard let jwt = response.jwt else { return } |
| 135 | + |
| 136 | + let platform = response.provider.description |
| 137 | + let request = SignInRequest(authPlatform: platform, idToken: idToken) |
| 138 | + let tokenResponse = try await authClient.로그인(request) |
| 139 | + |
| 140 | + /// [1]. UserDefaults에 최근 로그인한 애플로그인 `정보`저장 |
| 141 | + await userDefaults.setString(platform, .authPlatform) |
| 142 | + await userDefaults.setString(authCode, .authCode) |
| 143 | + await userDefaults.setString(jwt, .jwt) |
| 144 | + /// [2]. Keychain에 `access`, `refresh` 저장 |
| 145 | + keychain.save(.accessToken, tokenResponse.accessToken) |
| 146 | + keychain.save(.refreshToken, tokenResponse.refreshToken) |
| 147 | + |
| 148 | + let appleTokenRequest = AppleTokenRequest(authCode: authCode, jwt: jwt) |
| 149 | + let appleTokenResponse = try await authClient.apple(appleTokenRequest) |
| 150 | + keychain.save(.serverRefresh, appleTokenResponse.refresh_token) |
| 151 | + |
| 152 | + await send(.inner(.로그인_이후_화면이동(isRegistered: tokenResponse.isRegistered))) |
| 153 | + } |
| 154 | + case let .구글로그인(response): |
| 155 | + return .run { send in |
| 156 | + guard let idToken = response.idToken else { return } |
| 157 | + let platform = response.provider.description |
| 158 | + let request = SignInRequest(authPlatform: platform, idToken: idToken) |
| 159 | + let tokenResponse = try await authClient.로그인(request) |
| 160 | + |
| 161 | + /// [1]. UserDefaults에 최근 로그인한 소셜로그인 `타입`저장 |
| 162 | + await userDefaults.setString(platform, .authPlatform) |
| 163 | + /// [2]. Keychain에 `access`, `refresh` 저장 |
| 164 | + keychain.save(.accessToken, tokenResponse.accessToken) |
| 165 | + keychain.save(.refreshToken, tokenResponse.refreshToken) |
| 166 | + keychain.save(.serverRefresh, response.serverRefreshToken) |
| 167 | + |
| 168 | + await send(.inner(.로그인_이후_화면이동(isRegistered: tokenResponse.isRegistered))) |
| 169 | + } |
| 170 | + case let .로그인_이후_화면이동(isRegistered): |
| 171 | + /// [3]. 이미 회원가입했던 유저라면 `메인`이동 |
| 172 | + if isRegistered { |
| 173 | + return .run { send in await send(.delegate(.dismissLoginRootView)) } |
| 174 | + } else { |
| 175 | + return .run { send in await send(.inner(.pushAgreeToTermsView)) } |
| 176 | + } |
| 177 | + } |
| 178 | + } |
| 179 | + /// - Async Effect |
| 180 | + func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect<Action> { |
| 181 | + switch action { |
| 182 | + case .회원가입: |
| 183 | + return .run { [nickName = state.nickName, interests = state.interests] send in |
| 184 | + guard let nickName else { return } |
| 185 | + guard let interests else { return } |
| 186 | + let signUpRequest = SignupRequest(nickName: nickName, interests: interests) |
| 187 | + let _ = try await userClient.회원등록(signUpRequest) |
| 188 | + |
| 189 | + await send(.inner(.pushSignUpDoneView)) |
| 190 | + } |
| 191 | + |
| 192 | + case .로그인(let response): |
| 193 | + switch response.provider { |
| 194 | + case .apple: |
| 195 | + return .run { send in await send(.inner(.애플로그인(response))) } |
| 196 | + case .google: |
| 197 | + return .run { send in await send(.inner(.구글로그인(response))) } |
| 198 | + } |
| 199 | + } |
| 200 | + } |
| 201 | + /// - Scope Effect |
| 202 | + func handleScopeAction(_ action: Action.ScopeAction, state: inout State) -> Effect<Action> { |
| 203 | + switch action { |
| 204 | + case .agreeToTerms(let delegate): |
| 205 | + switch delegate { |
| 206 | + case .pushRegisterNicknameView: |
| 207 | + return .send(.inner(.pushRegisterNicknameView)) |
| 208 | + } |
| 209 | + case .registerNickname(let delegate): |
| 210 | + switch delegate { |
| 211 | + case .pushSelectFieldView(let nickname): |
| 212 | + state.nickName = nickname |
| 213 | + return .send(.inner(.pushSelectFieldView(nickname: nickname))) |
| 214 | + } |
| 215 | + case .selectField(let delegate): |
| 216 | + switch delegate { |
| 217 | + case let .pushSignUpDoneView(interests): |
| 218 | + state.interests = interests |
| 219 | + return .send(.async(.회원가입)) |
| 220 | + } |
| 221 | + } |
| 222 | + } |
| 223 | + /// - Delegate Effect |
| 224 | + func handleDelegateAction(_ action: Action.DelegateAction, state: inout State) -> Effect<Action> { |
| 225 | + return .none |
| 226 | + } |
| 227 | + |
| 228 | + func handlePathAction(_ action: StackActionOf<Path>, state: inout State) -> Effect<Action> { |
| 229 | + switch action { |
| 230 | + case .element(id: _, action: .agreeToTerms(.delegate(let delegate))): |
| 231 | + return .send(.scope(.agreeToTerms(delegate))) |
| 232 | + case .element(id: _, action: .registerNickname(.delegate(let delegate))): |
| 233 | + return .send(.scope(.registerNickname(delegate))) |
| 234 | + case .element(id: _, action: .selecteField(.delegate(let delegate))): |
| 235 | + return .send(.scope(.selectField(delegate))) |
| 236 | + case .element, .popFrom, .push: |
| 237 | + return .none |
| 238 | + } |
| 239 | + } |
| 240 | +} |
| 241 | + |
| 242 | +//MARK: - Path |
| 243 | +extension LoginFeature { |
| 244 | + @Reducer |
| 245 | + public enum Path { |
| 246 | + case agreeToTerms(AgreeToTermsFeature) |
| 247 | + case registerNickname(RegisterNicknameFeature) |
| 248 | + case selecteField(SelectFieldFeature) |
| 249 | + } |
| 250 | +} |
0 commit comments