Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Application/DevLogApp/Sources/App/DevLogApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ struct DevLogApp: App {
networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self),
systemThemeUseCase: container.resolve(ObserveSystemThemeUseCase.self),
trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self),
signInUseCase: container.resolve(SignInUseCase.self),
widgetURLTab: { MainTab(widgetURL: $0) },
Comment thread
opficdev marked this conversation as resolved.
windowEvent: windowEvent,
pushNotificationTodoIdPublisher: PushNotificationRoute.shared.observe(),
Expand Down
50 changes: 25 additions & 25 deletions Application/DevLogPresentation/Sources/Login/LoginFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,13 @@ import Foundation
struct LoginFeature {
@ObservableState
struct State: Equatable {
@Presents var alert: AlertState<Never>?
var isLoading = false
var showAlert = false
var alertType: AlertType?
var alertTitle = ""
var alertMessage = ""
}

enum Action {
case setAlert(Bool, AlertType? = nil)
case alert(PresentationAction<Never>)
case tapSignInButton(AuthProvider)
case signInSucceeded
case signInFailed(AlertType)
case signInCancelled
}
Expand All @@ -38,14 +34,13 @@ struct LoginFeature {
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .setAlert(let isPresented, let alertType):
setAlert(&state, isPresented: isPresented, alertType: alertType)
case .alert:
break
case .tapSignInButton(let provider):
state.isLoading = true
return .run { [signInUseCase] send in
do {
try await signInUseCase.execute(provider)
await send(.signInSucceeded)
} catch {
if error.isSocialLoginCancelled {
await send(.signInCancelled)
Expand All @@ -54,14 +49,15 @@ struct LoginFeature {
await send(.signInFailed(alertType(for: error)))
}
}
case .signInSucceeded, .signInCancelled:
case .signInCancelled:
state.isLoading = false
case .signInFailed(let alertType):
state.isLoading = false
setAlert(&state, isPresented: true, alertType: alertType)
state.alert = alertState(for: alertType)
}
return .none
}
.ifLet(\.$alert, action: \.alert)
}
}

Expand Down Expand Up @@ -95,24 +91,28 @@ extension DependencyValues {
}

private extension LoginFeature {
func setAlert(
_ state: inout State,
isPresented: Bool,
alertType: AlertType?
) {
func alertState(for alertType: AlertType) -> AlertState<Never> {
let title: String
let message: String

switch alertType {
case .emailUnavailable:
state.alertTitle = String(localized: "login_alert_email_unavailable_title")
state.alertMessage = String(localized: "login_alert_email_unavailable_message")
title = String(localized: "login_alert_email_unavailable_title")
message = String(localized: "login_alert_email_unavailable_message")
case .error:
state.alertTitle = String(localized: "common_error_title")
state.alertMessage = String(localized: "common_error_message")
case .none:
state.alertTitle = ""
state.alertMessage = ""
title = String(localized: "common_error_title")
message = String(localized: "common_error_message")
}

return AlertState {
TextState(title)
} actions: {
ButtonState(role: .cancel) {
TextState(String(localized: "common_close"))
}
} message: {
TextState(message)
}
state.showAlert = isPresented
state.alertType = alertType
}

func alertType(for error: Error) -> AlertType {
Expand Down
22 changes: 13 additions & 9 deletions Application/DevLogPresentation/Sources/Login/LoginView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,22 @@

import SwiftUI
import ComposableArchitecture
import DevLogDomain

struct LoginView: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.sceneWidth) var sceneWidth
let store: StoreOf<LoginFeature>
@State private var store: StoreOf<LoginFeature>

init(signInUseCase: SignInUseCase) {
self._store = State(initialValue: Store(
initialState: LoginFeature.State()
) {
LoginFeature()
} withDependencies: {
$0.signInUseCase = .live(signInUseCase)
})
}

var body: some View {
ZStack {
Expand Down Expand Up @@ -46,13 +57,6 @@ struct LoginView: View {
LoadingView()
}
}
.alert(store.alertTitle, isPresented: Binding(
get: { store.showAlert },
set: { store.send(.setAlert($0)) }
)) {
Button(String(localized: "common_close"), role: .cancel) { }
} message: {
Text(store.alertMessage)
}
.alert($store.scope(state: \.alert, action: \.alert))
}
}
11 changes: 1 addition & 10 deletions Application/DevLogPresentation/Sources/Root/RootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ public struct RootView: View {
@State var viewModel: RootViewModel
@State private var selectedRoute: Route?
@State private var selectedMainTab = MainTab.home
private let loginStore: StoreOf<LoginFeature>
private let widgetURLTab: (URL) -> MainTab?
Comment thread
opficdev marked this conversation as resolved.
private let windowEvent: TodoEditorWindowEvent
private let pushNotificationTodoIdPublisher: AnyPublisher<String, Never>
Expand All @@ -27,7 +26,6 @@ public struct RootView: View {
networkConnectivityUseCase: ObserveNetworkConnectivityUseCase,
systemThemeUseCase: ObserveSystemThemeUseCase,
trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase,
signInUseCase: SignInUseCase,
widgetURLTab: @escaping (URL) -> MainTab?,
Comment thread
opficdev marked this conversation as resolved.
windowEvent: TodoEditorWindowEvent,
pushNotificationTodoIdPublisher: AnyPublisher<String, Never>,
Expand All @@ -39,13 +37,6 @@ public struct RootView: View {
systemThemeUseCase: systemThemeUseCase,
trackAnalyticsEventUseCase: trackAnalyticsEventUseCase
))
self.loginStore = Store(
initialState: LoginFeature.State()
) {
LoginFeature()
} withDependencies: {
$0.signInUseCase = .live(signInUseCase)
}
self.widgetURLTab = widgetURLTab
Comment thread
opficdev marked this conversation as resolved.
self.windowEvent = windowEvent
self.pushNotificationTodoIdPublisher = pushNotificationTodoIdPublisher
Expand All @@ -63,7 +54,7 @@ public struct RootView: View {
selectedTab: $selectedMainTab
)
} else {
LoginView(store: loginStore)
LoginView(signInUseCase: container.resolve(SignInUseCase.self))
}
Comment thread
opficdev marked this conversation as resolved.
}
}
Expand Down
85 changes: 41 additions & 44 deletions Application/DevLogPresentation/Tests/Login/LoginFeatureTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ struct LoginFeatureTests {
#expect(spy.calledProviders == [.github])
}

@Test("로그인 요청 중에는 로딩 상태가 켜지고 요청이 끝나면 꺼진다")
func 로그인_요청_중에는_로딩_상태가_켜지고_요청이_끝나면_꺼진다() async {
@Test("로그인 성공 후에도 메인 화면 전환 전까지 로딩 상태를 유지한다")
func 로그인_성공_후에도_메인_화면_전환_전까지_로딩_상태를_유지한다() async {
let spy = SignInUseCaseSpy()
spy.shouldSuspend = true
let driver = LoginTestDriver(useCase: spy)
Expand All @@ -44,10 +44,10 @@ struct LoginFeatureTests {
spy.resume()

await waitUntil {
!driver.isLoading
spy.successfulProviders == [.google]
}

#expect(!driver.isLoading)
#expect(driver.isLoading)
}

@Test("로그인 실패 후에도 로딩 상태가 꺼진다")
Expand All @@ -68,7 +68,7 @@ struct LoginFeatureTests {
spy.resume()

await waitUntil {
!driver.isLoading && driver.showAlert
!driver.isLoading && driver.hasAlert
}

#expect(!driver.isLoading)
Expand All @@ -83,12 +83,13 @@ struct LoginFeatureTests {
driver.tapSignInButton(.google)

await waitUntil {
driver.showAlert
driver.hasAlert
}

#expect(driver.alertKind == .emailUnavailable)
#expect(driver.alertTitle == String(localized: "login_alert_email_unavailable_title"))
#expect(driver.alertMessage == String(localized: "login_alert_email_unavailable_message"))
#expect(driver.alert == expectedAlert(
title: String(localized: "login_alert_email_unavailable_title"),
message: String(localized: "login_alert_email_unavailable_message")
))
}

@Test("일반 로그인 에러가 발생하면 공통 에러 알림을 표시한다")
Expand All @@ -100,12 +101,13 @@ struct LoginFeatureTests {
driver.tapSignInButton(.apple)

await waitUntil {
driver.showAlert
driver.hasAlert
}

#expect(driver.alertKind == .error)
#expect(driver.alertTitle == String(localized: "common_error_title"))
#expect(driver.alertMessage == String(localized: "common_error_message"))
#expect(driver.alert == expectedAlert(
title: String(localized: "common_error_title"),
message: String(localized: "common_error_message")
))
}

@Test("소셜 로그인 취소 에러가 발생하면 알림을 표시하지 않는다")
Expand All @@ -121,9 +123,7 @@ struct LoginFeatureTests {
}

#expect(!driver.showAlert)
#expect(driver.alertKind == nil)
#expect(driver.alertTitle.isEmpty)
#expect(driver.alertMessage.isEmpty)
#expect(driver.alert == nil)
}

@Test("알림을 닫으면 알림 상태와 문구가 초기화된다")
Expand All @@ -135,23 +135,16 @@ struct LoginFeatureTests {
driver.tapSignInButton(.google)

await waitUntil {
driver.showAlert
driver.hasAlert
}

driver.setAlert(false)
driver.dismissAlert()

#expect(!driver.showAlert)
#expect(driver.alertKind == nil)
#expect(driver.alertTitle.isEmpty)
#expect(driver.alertMessage.isEmpty)
#expect(driver.alert == nil)
}
}

private enum LoginAlertKind: Equatable {
case emailUnavailable
case error
}

@MainActor
private struct LoginTestDriver {
private let feature: StoreOf<LoginFeature>
Expand All @@ -161,26 +154,15 @@ private struct LoginTestDriver {
}

var showAlert: Bool {
feature.state.showAlert
hasAlert
}

var alertKind: LoginAlertKind? {
switch feature.state.alertType {
case .emailUnavailable:
return .emailUnavailable
case .error:
return .error
case .none:
return nil
}
var hasAlert: Bool {
alert != nil
}

var alertTitle: String {
feature.state.alertTitle
}

var alertMessage: String {
feature.state.alertMessage
var alert: AlertState<Never>? {
feature.state.alert
}

init(useCase: SignInUseCase) {
Expand All @@ -197,7 +179,22 @@ private struct LoginTestDriver {
feature.send(.tapSignInButton(provider))
}

func setAlert(_ isPresented: Bool) {
feature.send(.setAlert(isPresented))
func dismissAlert() {
feature.send(.alert(.dismiss))
}
}

private func expectedAlert(
title: String,
message: String
) -> AlertState<Never> {
AlertState {
TextState(title)
} actions: {
ButtonState(role: .cancel) {
TextState(String(localized: "common_close"))
}
} message: {
TextState(message)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ final class SignInUseCaseSpy: SignInUseCase {
var error: Error?
var shouldSuspend = false
private(set) var calledProviders: [AuthProvider] = []
private(set) var successfulProviders = [AuthProvider]()
private var continuation: CheckedContinuation<Void, Never>?
private var shouldResume = false

Expand All @@ -74,6 +75,8 @@ final class SignInUseCaseSpy: SignInUseCase {
if let error {
throw error
}

successfulProviders.append(provider)
}

func resume() {
Expand Down
Loading