From be1513e67aabf62bb14a29ad3d64327aea71c2c0 Mon Sep 17 00:00:00 2001 From: Ryo Takeuchi Date: Sat, 2 Dec 2023 23:11:33 +0900 Subject: [PATCH] =?UTF-8?q?:+1:=20=E3=82=BB=E3=83=83=E3=82=B7=E3=83=A7?= =?UTF-8?q?=E3=83=B3=E3=81=AE=E3=83=AA=E3=83=95=E3=83=AC=E3=83=83=E3=82=B7?= =?UTF-8?q?=E3=83=A5=E5=87=A6=E7=90=86=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../club/nito/ios/combined/EntryPointTest.kt | 8 ++-- .../Sources/Auth/ComposeLoginScreen.swift | 2 +- .../Sources/Auth/LoginStateMachine.swift | 4 +- .../AuthStatusStreamUseCaseProvider.swift | 27 +++++++++++ .../ObserveAuthStatusUseCaseProvider.swift | 27 ----------- .../Sources/Navigation/RootStateMachine.swift | 9 ++-- .../Settings/ComposeSettingsScreen.swift | 2 +- .../nito/app/shared/NitoAppStateMachine.kt | 23 ++++++---- .../club/nito/app/shared/NitoNavHost.kt | 2 + .../club/nito/app/shared/di/AppModule.kt | 2 +- .../club/nito/core/data/AuthRepository.kt | 3 +- .../nito/core/data/DefaultAuthRepository.kt | 3 +- .../core/domain/AuthStatusStreamUseCase.kt | 18 ++++++++ .../core/domain/ObserveAuthStatusUseCase.kt | 19 -------- .../club/nito/core/domain/di/UseCaseModule.kt | 6 +-- .../kotlin/club/nito/core/model/AuthState.kt | 4 ++ .../club/nito/core/network/NetworkService.kt | 38 +++++++++++++++ .../core/network/auth/AuthRemoteDataSource.kt | 4 +- .../network/auth/FakeAuthRemoteDataSource.kt | 21 ++++----- .../auth/SupabaseAuthRemoteDataSource.kt | 43 +++++++++-------- .../core/network/di/RemoteDataSourceModule.kt | 9 ++++ .../SupabaseParticipantRemoteDataSource.kt | 46 +++++++++++-------- .../SupabaseScheduleRemoteDataSource.kt | 30 ++++++------ .../user/SupabaseUserRemoteDataSource.kt | 8 ++-- .../feature/auth/LoginScreenStateMachine.kt | 13 +++--- .../nito/feature/auth/di/AuthFeatureModule.kt | 2 +- .../settings/SettingsScreenStateMachine.kt | 13 +++--- .../settings/di/SettingsFeatureModule.kt | 2 +- 28 files changed, 229 insertions(+), 159 deletions(-) create mode 100644 app/ios/Modules/Sources/KmpContainer/AuthStatusStreamUseCaseProvider.swift delete mode 100644 app/ios/Modules/Sources/KmpContainer/ObserveAuthStatusUseCaseProvider.swift create mode 100644 core/domain/src/commonMain/kotlin/club/nito/core/domain/AuthStatusStreamUseCase.kt delete mode 100644 core/domain/src/commonMain/kotlin/club/nito/core/domain/ObserveAuthStatusUseCase.kt create mode 100644 core/network/src/commonMain/kotlin/club/nito/core/network/NetworkService.kt diff --git a/app/ios-combined/src/iosTest/kotlin/club/nito/ios/combined/EntryPointTest.kt b/app/ios-combined/src/iosTest/kotlin/club/nito/ios/combined/EntryPointTest.kt index 2b9fc4d8..e65c1291 100644 --- a/app/ios-combined/src/iosTest/kotlin/club/nito/ios/combined/EntryPointTest.kt +++ b/app/ios-combined/src/iosTest/kotlin/club/nito/ios/combined/EntryPointTest.kt @@ -5,7 +5,7 @@ import club.nito.core.data.ScheduleRepository import club.nito.core.domain.GetParticipantScheduleListUseCase import club.nito.core.domain.GetRecentScheduleUseCase import club.nito.core.domain.ModifyPasswordUseCase -import club.nito.core.domain.ObserveAuthStatusUseCase +import club.nito.core.domain.AuthStatusStreamUseCase import club.nito.core.domain.ParticipateUseCase import club.nito.core.domain.LoginUseCase import club.nito.core.domain.LogoutUseCase @@ -13,7 +13,7 @@ import club.nito.core.network.auth.AuthRemoteDataSource import club.nito.core.network.participation.ParticipantRemoteDataSource import club.nito.core.network.schedule.ScheduleRemoteDataSource import io.github.jan.supabase.SupabaseClient -import io.github.jan.supabase.gotrue.GoTrue +import io.github.jan.supabase.gotrue.Auth import kotlinx.serialization.json.Json import kotlin.test.Test import kotlin.test.assertNotNull @@ -26,7 +26,7 @@ class EntryPointTest { // Check finer dependencies first to debug easily assertNotNull(kmpEntryPoint.get()) assertNotNull(kmpEntryPoint.get()) - assertNotNull(kmpEntryPoint.get()) + assertNotNull(kmpEntryPoint.get()) assertNotNull(kmpEntryPoint.get()) assertNotNull(kmpEntryPoint.get()) @@ -35,7 +35,7 @@ class EntryPointTest { assertNotNull(kmpEntryPoint.get()) assertNotNull(kmpEntryPoint.get()) - assertNotNull(kmpEntryPoint.get()) + assertNotNull(kmpEntryPoint.get()) assertNotNull(kmpEntryPoint.get()) assertNotNull(kmpEntryPoint.get()) assertNotNull(kmpEntryPoint.get()) diff --git a/app/ios/Modules/Sources/Auth/ComposeLoginScreen.swift b/app/ios/Modules/Sources/Auth/ComposeLoginScreen.swift index 29bdd17e..fc5d57cd 100644 --- a/app/ios/Modules/Sources/Auth/ComposeLoginScreen.swift +++ b/app/ios/Modules/Sources/Auth/ComposeLoginScreen.swift @@ -15,7 +15,7 @@ public struct ComposeLoginScreen: UIViewControllerRepresentable { public func makeUIViewController(context: Context) -> UIViewController { return LoginScreen_iosKt.LoginRouteViewController( viewModel: LoginScreenStateMachine( - observeAuthStatusUseCase: Container.shared.get(type: ObserveAuthStatusUseCase.self), + authStatusStream: Container.shared.get(type: AuthStatusStreamUseCase.self), login: Container.shared.get(type: LoginUseCase.self), userMessageStateHolder: Container.shared.get(type: UserMessageStateHolder.self) ), diff --git a/app/ios/Modules/Sources/Auth/LoginStateMachine.swift b/app/ios/Modules/Sources/Auth/LoginStateMachine.swift index fc609987..4c540666 100644 --- a/app/ios/Modules/Sources/Auth/LoginStateMachine.swift +++ b/app/ios/Modules/Sources/Auth/LoginStateMachine.swift @@ -18,7 +18,7 @@ enum LoginScreenEvent { @MainActor final class LoginStateMachine: ObservableObject { @Dependency(\.loginUseCase) var loginUseCase - @Dependency(\.observeAuthStatusUseCase) var observeAuthStatusUseCase + @Dependency(\.authStatusStreamUseCase) var authStatusStream @Published var state: LoginViewUIState = .init() @Published var event: LoginScreenEvent? = nil @@ -40,7 +40,7 @@ final class LoginStateMachine: ObservableObject { loadTask = Task.detached { @MainActor in do { - for try await authStatus in self.observeAuthStatusUseCase.execute() { + for try await authStatus in self.authStatusStream.execute() { if case let status as FetchSingleResultSuccess = authStatus { self.cachedAuthStatus = status.data } diff --git a/app/ios/Modules/Sources/KmpContainer/AuthStatusStreamUseCaseProvider.swift b/app/ios/Modules/Sources/KmpContainer/AuthStatusStreamUseCaseProvider.swift new file mode 100644 index 00000000..0944f6fd --- /dev/null +++ b/app/ios/Modules/Sources/KmpContainer/AuthStatusStreamUseCaseProvider.swift @@ -0,0 +1,27 @@ +import Dependencies +import NitoKmp + +public struct AuthStatusStreamUseCaseProvider { + private static var observeAuthStatusUseCase: AuthStatusStreamUseCase { + Container.shared.get(type: AuthStatusStreamUseCase.self) + } + + public let execute: () -> AsyncThrowingStream +} + +extension AuthStatusStreamUseCaseProvider: DependencyKey { + @MainActor + static public var liveValue: AuthStatusStreamUseCaseProvider = + AuthStatusStreamUseCaseProvider( + execute: { + observeAuthStatusUseCase.invoke().stream() + } + ) +} + +extension DependencyValues { + public var authStatusStreamUseCase: AuthStatusStreamUseCaseProvider { + get { self[AuthStatusStreamUseCaseProvider.self] } + set { self[AuthStatusStreamUseCaseProvider.self] = newValue } + } +} diff --git a/app/ios/Modules/Sources/KmpContainer/ObserveAuthStatusUseCaseProvider.swift b/app/ios/Modules/Sources/KmpContainer/ObserveAuthStatusUseCaseProvider.swift deleted file mode 100644 index a232701c..00000000 --- a/app/ios/Modules/Sources/KmpContainer/ObserveAuthStatusUseCaseProvider.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Dependencies -import NitoKmp - -public struct ObserveAuthStatusUseCaseProvider { - private static var observeAuthStatusUseCase: ObserveAuthStatusUseCase { - Container.shared.get(type: ObserveAuthStatusUseCase.self) - } - - public let execute: () -> AsyncThrowingStream -} - -extension ObserveAuthStatusUseCaseProvider: DependencyKey { - @MainActor - static public var liveValue: ObserveAuthStatusUseCaseProvider = - ObserveAuthStatusUseCaseProvider( - execute: { - observeAuthStatusUseCase.invoke().stream() - } - ) -} - -extension DependencyValues { - public var observeAuthStatusUseCase: ObserveAuthStatusUseCaseProvider { - get { self[ObserveAuthStatusUseCaseProvider.self] } - set { self[ObserveAuthStatusUseCaseProvider.self] = newValue } - } -} diff --git a/app/ios/Modules/Sources/Navigation/RootStateMachine.swift b/app/ios/Modules/Sources/Navigation/RootStateMachine.swift index 5d4a3527..70240de5 100644 --- a/app/ios/Modules/Sources/Navigation/RootStateMachine.swift +++ b/app/ios/Modules/Sources/Navigation/RootStateMachine.swift @@ -24,7 +24,7 @@ enum Routing: Hashable { class RootStateMachine: ObservableObject { @Published var state: RootViewUIState = .init() @Published var path: NavigationPath = .init() - @Dependency(\.observeAuthStatusUseCase) var observeAuthStatusUseCase + @Dependency(\.authStatusStreamUseCase) var authStatusStream private var cachedAuthStatus: AuthStatus? { didSet { @@ -49,10 +49,9 @@ class RootStateMachine: ObservableObject { loadTask = Task.detached { @MainActor in do { - for try await authStatus in self.observeAuthStatusUseCase.execute() { - if case let status as FetchSingleResultSuccess = authStatus { - self.cachedAuthStatus = status.data - } + for try await authStatus in self.authStatusStream.execute() { + self.cachedAuthStatus = authStatus + print(authStatus) } } catch let error { self.state.authStatus = .failed(error) diff --git a/app/ios/Modules/Sources/Settings/ComposeSettingsScreen.swift b/app/ios/Modules/Sources/Settings/ComposeSettingsScreen.swift index 8b4802ea..7d7c04be 100644 --- a/app/ios/Modules/Sources/Settings/ComposeSettingsScreen.swift +++ b/app/ios/Modules/Sources/Settings/ComposeSettingsScreen.swift @@ -11,7 +11,7 @@ public struct ComposeSettingsScreen: UIViewControllerRepresentable { public func makeUIViewController(context: Context) -> UIViewController { return SettingsScreen_iosKt.SettingsScreenUIViewController( stateMachine: SettingsScreenStateMachine( - observeAuthStatus: Container.shared.get(type: ObserveAuthStatusUseCase.self), + authStatusStream: Container.shared.get(type: AuthStatusStreamUseCase.self), modifyPassword: Container.shared.get(type: ModifyPasswordUseCase.self), logout: Container.shared.get(type: LogoutUseCase.self), userMessageStateHolder: Container.shared.get(type: UserMessageStateHolder.self) diff --git a/app/shared/src/commonMain/kotlin/club/nito/app/shared/NitoAppStateMachine.kt b/app/shared/src/commonMain/kotlin/club/nito/app/shared/NitoAppStateMachine.kt index be482c6d..114dd6af 100644 --- a/app/shared/src/commonMain/kotlin/club/nito/app/shared/NitoAppStateMachine.kt +++ b/app/shared/src/commonMain/kotlin/club/nito/app/shared/NitoAppStateMachine.kt @@ -1,7 +1,7 @@ package club.nito.app.shared -import club.nito.core.domain.ObserveAuthStatusUseCase -import club.nito.core.model.FetchSingleResult +import club.nito.core.domain.AuthStatusStreamUseCase +import club.nito.core.model.AuthStatus import club.nito.core.ui.StateMachine import club.nito.core.ui.buildUiState import club.nito.core.ui.stateMachineScope @@ -10,19 +10,24 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn class NitoAppStateMachine( - observeAuthStatus: ObserveAuthStatusUseCase, + authStatusStream: AuthStatusStreamUseCase, ) : StateMachine() { - private val authStatus = observeAuthStatus().stateIn( + private val authStatus = authStatusStream().stateIn( scope = stateMachineScope, started = SharingStarted.WhileSubscribed(5_000), - initialValue = FetchSingleResult.Loading, + initialValue = AuthStatus.Loading, ) val uiState: StateFlow = buildUiState(authStatus) { - if (it !is FetchSingleResult.Success) { - return@buildUiState NitoAppUiState.Loading - } + when (it) { + AuthStatus.Loading -> NitoAppUiState.Loading + + AuthStatus.NotAuthenticated, + is AuthStatus.Authenticated, + -> NitoAppUiState.Success(it) - NitoAppUiState.Success(it.data) + // TODO: 適切なハンドリングを行う + AuthStatus.NetworkError -> NitoAppUiState.Loading + } } } diff --git a/app/shared/src/commonMain/kotlin/club/nito/app/shared/NitoNavHost.kt b/app/shared/src/commonMain/kotlin/club/nito/app/shared/NitoNavHost.kt index 0aa544e3..71f5a9a5 100644 --- a/app/shared/src/commonMain/kotlin/club/nito/app/shared/NitoNavHost.kt +++ b/app/shared/src/commonMain/kotlin/club/nito/app/shared/NitoNavHost.kt @@ -95,6 +95,8 @@ private fun RouteBuilder.root( ), ), ) + + else -> {} } } } diff --git a/app/shared/src/commonMain/kotlin/club/nito/app/shared/di/AppModule.kt b/app/shared/src/commonMain/kotlin/club/nito/app/shared/di/AppModule.kt index a3a80b06..53ff8eb3 100644 --- a/app/shared/src/commonMain/kotlin/club/nito/app/shared/di/AppModule.kt +++ b/app/shared/src/commonMain/kotlin/club/nito/app/shared/di/AppModule.kt @@ -6,7 +6,7 @@ import org.koin.dsl.module val appModule = module { factory { NitoAppStateMachine( - observeAuthStatus = get(), + authStatusStream = get(), ) } } diff --git a/core/data/src/commonMain/kotlin/club/nito/core/data/AuthRepository.kt b/core/data/src/commonMain/kotlin/club/nito/core/data/AuthRepository.kt index aebc7409..613398a3 100644 --- a/core/data/src/commonMain/kotlin/club/nito/core/data/AuthRepository.kt +++ b/core/data/src/commonMain/kotlin/club/nito/core/data/AuthRepository.kt @@ -1,7 +1,6 @@ package club.nito.core.data import club.nito.core.model.AuthStatus -import club.nito.core.model.FetchSingleResult import club.nito.core.model.UserInfo import kotlinx.coroutines.flow.Flow @@ -12,7 +11,7 @@ public sealed interface AuthRepository { /** * 認証情報の状態 */ - public val authStatus: Flow> + public val authStatus: Flow /** * ログインする diff --git a/core/data/src/commonMain/kotlin/club/nito/core/data/DefaultAuthRepository.kt b/core/data/src/commonMain/kotlin/club/nito/core/data/DefaultAuthRepository.kt index 847bc8b8..977065d5 100644 --- a/core/data/src/commonMain/kotlin/club/nito/core/data/DefaultAuthRepository.kt +++ b/core/data/src/commonMain/kotlin/club/nito/core/data/DefaultAuthRepository.kt @@ -1,7 +1,6 @@ package club.nito.core.data import club.nito.core.model.AuthStatus -import club.nito.core.model.FetchSingleResult import club.nito.core.model.UserInfo import club.nito.core.network.auth.AuthRemoteDataSource import kotlinx.coroutines.flow.Flow @@ -9,7 +8,7 @@ import kotlinx.coroutines.flow.Flow public class DefaultAuthRepository( private val remoteDataSource: AuthRemoteDataSource, ) : AuthRepository { - override val authStatus: Flow> = remoteDataSource.authStatus + override val authStatus: Flow = remoteDataSource.authStatus override suspend fun login(email: String, password: String): Unit = remoteDataSource.login( email = email, diff --git a/core/domain/src/commonMain/kotlin/club/nito/core/domain/AuthStatusStreamUseCase.kt b/core/domain/src/commonMain/kotlin/club/nito/core/domain/AuthStatusStreamUseCase.kt new file mode 100644 index 00000000..0d2fa0ee --- /dev/null +++ b/core/domain/src/commonMain/kotlin/club/nito/core/domain/AuthStatusStreamUseCase.kt @@ -0,0 +1,18 @@ +package club.nito.core.domain + +import club.nito.core.data.AuthRepository +import club.nito.core.model.AuthStatus +import kotlinx.coroutines.flow.Flow + +/** + * 認証状態を購読するユースケース + */ +public sealed interface AuthStatusStreamUseCase { + public operator fun invoke(): Flow +} + +public class AuthStatusStreamExecutor( + private val authRepository: AuthRepository, +) : AuthStatusStreamUseCase { + override fun invoke(): Flow = authRepository.authStatus +} diff --git a/core/domain/src/commonMain/kotlin/club/nito/core/domain/ObserveAuthStatusUseCase.kt b/core/domain/src/commonMain/kotlin/club/nito/core/domain/ObserveAuthStatusUseCase.kt deleted file mode 100644 index 8685ff40..00000000 --- a/core/domain/src/commonMain/kotlin/club/nito/core/domain/ObserveAuthStatusUseCase.kt +++ /dev/null @@ -1,19 +0,0 @@ -package club.nito.core.domain - -import club.nito.core.data.AuthRepository -import club.nito.core.model.AuthStatus -import club.nito.core.model.FetchSingleResult -import kotlinx.coroutines.flow.Flow - -/** - * 認証状態を購読するユースケース - */ -public sealed interface ObserveAuthStatusUseCase { - public operator fun invoke(): Flow> -} - -public class ObserveAuthStatusExecutor( - private val authRepository: AuthRepository, -) : ObserveAuthStatusUseCase { - override fun invoke(): Flow> = authRepository.authStatus -} diff --git a/core/domain/src/commonMain/kotlin/club/nito/core/domain/di/UseCaseModule.kt b/core/domain/src/commonMain/kotlin/club/nito/core/domain/di/UseCaseModule.kt index 3dc5c9b6..8a9b744b 100644 --- a/core/domain/src/commonMain/kotlin/club/nito/core/domain/di/UseCaseModule.kt +++ b/core/domain/src/commonMain/kotlin/club/nito/core/domain/di/UseCaseModule.kt @@ -1,5 +1,7 @@ package club.nito.core.domain.di +import club.nito.core.domain.AuthStatusStreamExecutor +import club.nito.core.domain.AuthStatusStreamUseCase import club.nito.core.domain.GetParticipantScheduleListExecutor import club.nito.core.domain.GetParticipantScheduleListUseCase import club.nito.core.domain.GetRecentScheduleExecutor @@ -10,8 +12,6 @@ import club.nito.core.domain.LogoutExecutor import club.nito.core.domain.LogoutUseCase import club.nito.core.domain.ModifyPasswordExecutor import club.nito.core.domain.ModifyPasswordUseCase -import club.nito.core.domain.ObserveAuthStatusExecutor -import club.nito.core.domain.ObserveAuthStatusUseCase import club.nito.core.domain.ParticipateExecutor import club.nito.core.domain.ParticipateUseCase import org.koin.core.module.Module @@ -20,7 +20,7 @@ import org.koin.dsl.bind import org.koin.dsl.module public val useCaseModule: Module = module { - singleOf(::ObserveAuthStatusExecutor) bind ObserveAuthStatusUseCase::class + singleOf(::AuthStatusStreamExecutor) bind AuthStatusStreamUseCase::class singleOf(::LoginExecutor) bind LoginUseCase::class singleOf(::ModifyPasswordExecutor) bind ModifyPasswordUseCase::class singleOf(::LogoutExecutor) bind LogoutUseCase::class diff --git a/core/model/src/commonMain/kotlin/club/nito/core/model/AuthState.kt b/core/model/src/commonMain/kotlin/club/nito/core/model/AuthState.kt index e8b47b89..94673124 100644 --- a/core/model/src/commonMain/kotlin/club/nito/core/model/AuthState.kt +++ b/core/model/src/commonMain/kotlin/club/nito/core/model/AuthState.kt @@ -1,7 +1,11 @@ package club.nito.core.model public sealed interface AuthStatus { + public data object Loading : AuthStatus + public data object NotAuthenticated : AuthStatus public data class Authenticated(val session: UserSession) : AuthStatus + + public data object NetworkError : AuthStatus } diff --git a/core/network/src/commonMain/kotlin/club/nito/core/network/NetworkService.kt b/core/network/src/commonMain/kotlin/club/nito/core/network/NetworkService.kt new file mode 100644 index 00000000..7f3fd6b1 --- /dev/null +++ b/core/network/src/commonMain/kotlin/club/nito/core/network/NetworkService.kt @@ -0,0 +1,38 @@ +package club.nito.core.network + +import club.nito.core.model.ApiException +import club.nito.core.model.NitoError +import club.nito.core.network.auth.AuthRemoteDataSource +import io.ktor.client.network.sockets.SocketTimeoutException +import io.ktor.client.plugins.HttpRequestTimeoutException +import io.ktor.client.plugins.ResponseException +import io.ktor.util.cio.ChannelReadException +import kotlinx.coroutines.TimeoutCancellationException + +public class NetworkService( + public val authRemoteDataSource: AuthRemoteDataSource, +) { + public suspend inline operator fun invoke( + block: () -> T, + ): T = try { + authRemoteDataSource.authIfNeeded() + block() + } catch (e: Throwable) { + throw e.toNitoError() + } +} + +public fun Throwable.toNitoError(): NitoError = when (this) { + is NitoError -> this + + is ResponseException -> ApiException.ServerException(this) + + is ChannelReadException -> ApiException.NetworkException(this) + + is TimeoutCancellationException, + is HttpRequestTimeoutException, + is SocketTimeoutException, + -> ApiException.TimeoutException(this) + + else -> ApiException.UnknownException(this) +} diff --git a/core/network/src/commonMain/kotlin/club/nito/core/network/auth/AuthRemoteDataSource.kt b/core/network/src/commonMain/kotlin/club/nito/core/network/auth/AuthRemoteDataSource.kt index c9a9b222..a1e83121 100644 --- a/core/network/src/commonMain/kotlin/club/nito/core/network/auth/AuthRemoteDataSource.kt +++ b/core/network/src/commonMain/kotlin/club/nito/core/network/auth/AuthRemoteDataSource.kt @@ -1,14 +1,14 @@ package club.nito.core.network.auth import club.nito.core.model.AuthStatus -import club.nito.core.model.FetchSingleResult import club.nito.core.model.UserInfo import kotlinx.coroutines.flow.Flow public sealed interface AuthRemoteDataSource { - public val authStatus: Flow> + public val authStatus: Flow public suspend fun login(email: String, password: String) public suspend fun logout() public suspend fun modifyAuthUser(email: String?, password: String?): UserInfo + public suspend fun authIfNeeded() } diff --git a/core/network/src/commonMain/kotlin/club/nito/core/network/auth/FakeAuthRemoteDataSource.kt b/core/network/src/commonMain/kotlin/club/nito/core/network/auth/FakeAuthRemoteDataSource.kt index 67dd3edb..f85fdf21 100644 --- a/core/network/src/commonMain/kotlin/club/nito/core/network/auth/FakeAuthRemoteDataSource.kt +++ b/core/network/src/commonMain/kotlin/club/nito/core/network/auth/FakeAuthRemoteDataSource.kt @@ -1,7 +1,6 @@ package club.nito.core.network.auth import club.nito.core.model.AuthStatus -import club.nito.core.model.FetchSingleResult import club.nito.core.model.UserInfo import club.nito.core.model.UserMfaFactor import club.nito.core.model.UserSession @@ -16,17 +15,15 @@ public class FakeAuthRemoteDataSource( coroutineScope: CoroutineScope, ) : AuthRemoteDataSource { - private val _authStatus = MutableStateFlow>(FetchSingleResult.Loading) - override val authStatus: Flow> = _authStatus + private val _authStatus = MutableStateFlow(AuthStatus.Loading) + override val authStatus: Flow = _authStatus init { coroutineScope.launch { delay(1000) - _authStatus.value = FetchSingleResult.Success( - AuthStatus.Authenticated( - session = authenticatedUserSession, - ), + _authStatus.value = AuthStatus.Authenticated( + session = authenticatedUserSession, ) } } @@ -62,13 +59,11 @@ public class FakeAuthRemoteDataSource( ) override suspend fun login(email: String, password: String): Unit = _authStatus.emit( - FetchSingleResult.Success( - AuthStatus.Authenticated(session = authenticatedUserSession), - ), + AuthStatus.Authenticated(session = authenticatedUserSession), ) override suspend fun logout(): Unit = _authStatus.emit( - FetchSingleResult.Success(AuthStatus.NotAuthenticated), + AuthStatus.NotAuthenticated, ) override suspend fun modifyAuthUser(email: String?, password: String?): UserInfo { @@ -77,6 +72,10 @@ public class FakeAuthRemoteDataSource( ) } + override suspend fun authIfNeeded() { + // Do nothing + } + private fun createFakeUserInfo( aud: String = "aud", confirmationSentAt: Instant? = null, diff --git a/core/network/src/commonMain/kotlin/club/nito/core/network/auth/SupabaseAuthRemoteDataSource.kt b/core/network/src/commonMain/kotlin/club/nito/core/network/auth/SupabaseAuthRemoteDataSource.kt index f0a7eb83..54587b14 100644 --- a/core/network/src/commonMain/kotlin/club/nito/core/network/auth/SupabaseAuthRemoteDataSource.kt +++ b/core/network/src/commonMain/kotlin/club/nito/core/network/auth/SupabaseAuthRemoteDataSource.kt @@ -1,7 +1,7 @@ package club.nito.core.network.auth +import club.nito.core.model.ApiException import club.nito.core.model.AuthStatus -import club.nito.core.model.FetchSingleResult import club.nito.core.model.UserInfo import club.nito.core.model.UserSession import io.github.jan.supabase.gotrue.Auth @@ -9,31 +9,30 @@ import io.github.jan.supabase.gotrue.SessionStatus import io.github.jan.supabase.gotrue.providers.builtin.Email import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.datetime.Clock public class SupabaseAuthRemoteDataSource( private val goTrue: Auth, ) : AuthRemoteDataSource { - override val authStatus: Flow> = goTrue.sessionStatus.map { + override val authStatus: Flow = goTrue.sessionStatus.map { when (it) { - is SessionStatus.Authenticated -> FetchSingleResult.Success( - AuthStatus.Authenticated( - session = UserSession( - accessToken = it.session.accessToken, - refreshToken = it.session.refreshToken, - providerRefreshToken = it.session.providerRefreshToken, - providerToken = it.session.providerToken, - expiresIn = it.session.expiresIn, - tokenType = it.session.tokenType, - user = it.session.user?.let(SupabaseAuthRemoteDataSourceMapper::transformToUserInfo), - type = it.session.type, - expiresAt = it.session.expiresAt, - ), + is SessionStatus.Authenticated -> AuthStatus.Authenticated( + session = UserSession( + accessToken = it.session.accessToken, + refreshToken = it.session.refreshToken, + providerRefreshToken = it.session.providerRefreshToken, + providerToken = it.session.providerToken, + expiresIn = it.session.expiresIn, + tokenType = it.session.tokenType, + user = it.session.user?.let(SupabaseAuthRemoteDataSourceMapper::transformToUserInfo), + type = it.session.type, + expiresAt = it.session.expiresAt, ), ) - is SessionStatus.NotAuthenticated -> FetchSingleResult.Success(AuthStatus.NotAuthenticated) - is SessionStatus.LoadingFromStorage -> FetchSingleResult.Loading - is SessionStatus.NetworkError -> FetchSingleResult.Failure(null) + is SessionStatus.NotAuthenticated -> AuthStatus.NotAuthenticated + is SessionStatus.LoadingFromStorage -> AuthStatus.Loading + is SessionStatus.NetworkError -> AuthStatus.NetworkError } } @@ -48,4 +47,12 @@ public class SupabaseAuthRemoteDataSource( this.email = email this.password = password }.let(SupabaseAuthRemoteDataSourceMapper::transformToUserInfo) + + override suspend fun authIfNeeded() { + val session = goTrue.currentSessionOrNull() ?: throw ApiException.SessionNotFoundException(cause = null) + + if (session.expiresAt <= Clock.System.now()) { + goTrue.refreshCurrentSession() + } + } } diff --git a/core/network/src/commonMain/kotlin/club/nito/core/network/di/RemoteDataSourceModule.kt b/core/network/src/commonMain/kotlin/club/nito/core/network/di/RemoteDataSourceModule.kt index 02dea31a..12b89e87 100644 --- a/core/network/src/commonMain/kotlin/club/nito/core/network/di/RemoteDataSourceModule.kt +++ b/core/network/src/commonMain/kotlin/club/nito/core/network/di/RemoteDataSourceModule.kt @@ -1,5 +1,6 @@ package club.nito.core.network.di +import club.nito.core.network.NetworkService import club.nito.core.network.auth.AuthRemoteDataSource import club.nito.core.network.auth.SupabaseAuthRemoteDataSource import club.nito.core.network.participation.ParticipantRemoteDataSource @@ -17,18 +18,26 @@ public val remoteDataSourceModule: Module = module { goTrue = get(), ) } + single { + NetworkService( + authRemoteDataSource = get(), + ) + } single { SupabaseScheduleRemoteDataSource( + networkService = get(), client = get(), ) } single { SupabaseParticipantRemoteDataSource( + networkService = get(), client = get(), ) } single { SupabaseUserRemoteDataSource( + networkService = get(), client = get(), ) } diff --git a/core/network/src/commonMain/kotlin/club/nito/core/network/participation/SupabaseParticipantRemoteDataSource.kt b/core/network/src/commonMain/kotlin/club/nito/core/network/participation/SupabaseParticipantRemoteDataSource.kt index 1bc690d8..0e9851ae 100644 --- a/core/network/src/commonMain/kotlin/club/nito/core/network/participation/SupabaseParticipantRemoteDataSource.kt +++ b/core/network/src/commonMain/kotlin/club/nito/core/network/participation/SupabaseParticipantRemoteDataSource.kt @@ -2,6 +2,7 @@ package club.nito.core.network.participation import club.nito.core.model.participant.Participant import club.nito.core.model.participant.ParticipantDeclaration +import club.nito.core.network.NetworkService import club.nito.core.network.participation.model.NetworkParticipant import club.nito.core.network.participation.model.toNetworkModel import io.github.jan.supabase.SupabaseClient @@ -9,40 +10,45 @@ import io.github.jan.supabase.postgrest.postgrest import io.github.jan.supabase.postgrest.query.Count public class SupabaseParticipantRemoteDataSource( + private val networkService: NetworkService, private val client: SupabaseClient, ) : ParticipantRemoteDataSource { private val postgrest = client.postgrest["participants"] - override suspend fun getParticipants(scheduleId: String): List = postgrest - .select { - filter { - and { - eq("schedule_id", scheduleId) - exact("deleted_at", null) + override suspend fun getParticipants(scheduleId: String): List = networkService { + postgrest + .select { + filter { + and { + eq("schedule_id", scheduleId) + exact("deleted_at", null) + } } } - } - .decodeList() - .map(NetworkParticipant::toParticipant) + .decodeList() + .map(NetworkParticipant::toParticipant) + } - override suspend fun getParticipants(scheduleIds: List): List = postgrest - .select { - filter { - and { - isIn("schedule_id", scheduleIds) - exact("deleted_at", null) + override suspend fun getParticipants(scheduleIds: List): List = networkService { + postgrest + .select { + filter { + and { + isIn("schedule_id", scheduleIds) + exact("deleted_at", null) + } } } - } - .decodeList() - .map(NetworkParticipant::toParticipant) + .decodeList() + .map(NetworkParticipant::toParticipant) + } - override suspend fun participate(declaration: ParticipantDeclaration): Long { + override suspend fun participate(declaration: ParticipantDeclaration): Long = networkService { val result = postgrest.insert( value = declaration.toNetworkModel(), ) { count(Count.EXACT) } - return result.countOrNull() ?: 0 + result.countOrNull() ?: 0 } } diff --git a/core/network/src/commonMain/kotlin/club/nito/core/network/schedule/SupabaseScheduleRemoteDataSource.kt b/core/network/src/commonMain/kotlin/club/nito/core/network/schedule/SupabaseScheduleRemoteDataSource.kt index bc48ee54..25215468 100644 --- a/core/network/src/commonMain/kotlin/club/nito/core/network/schedule/SupabaseScheduleRemoteDataSource.kt +++ b/core/network/src/commonMain/kotlin/club/nito/core/network/schedule/SupabaseScheduleRemoteDataSource.kt @@ -2,6 +2,7 @@ package club.nito.core.network.schedule import club.nito.core.model.Order import club.nito.core.model.schedule.Schedule +import club.nito.core.network.NetworkService import club.nito.core.network.schedule.model.NetworkSchedule import club.nito.core.network.toSupabaseOrder import co.touchlab.kermit.Logger @@ -15,6 +16,7 @@ private enum class Column(val columnName: String) { } public class SupabaseScheduleRemoteDataSource( + private val networkService: NetworkService, private val client: SupabaseClient, ) : ScheduleRemoteDataSource { private val log = Logger.withTag("SupabaseScheduleRemoteDataSource") @@ -24,21 +26,23 @@ public class SupabaseScheduleRemoteDataSource( limit: Int, order: Order, after: Instant?, - ): List = postgrest - .select { - filter { - exact(Column.DELETED_AT.columnName, null) - after?.let { gte(Column.SCHEDULED_AT.columnName, it) } + ): List = networkService { + postgrest + .select { + filter { + exact(Column.DELETED_AT.columnName, null) + after?.let { gte(Column.SCHEDULED_AT.columnName, it) } + } + order(Column.SCHEDULED_AT.columnName, order = order.toSupabaseOrder()) + limit(count = limit.toLong()) } - order(Column.SCHEDULED_AT.columnName, order = order.toSupabaseOrder()) - limit(count = limit.toLong()) - } - .decodeList() - .map(NetworkSchedule::toSchedule) - .also { log.d { "getScheduleList: $it" } } + .decodeList() + .map(NetworkSchedule::toSchedule) + .also { log.d { "getScheduleList: $it" } } + } - override suspend fun getSchedule(id: String): Schedule { - return postgrest + override suspend fun getSchedule(id: String): Schedule = networkService { + postgrest .select { single() filter { diff --git a/core/network/src/commonMain/kotlin/club/nito/core/network/user/SupabaseUserRemoteDataSource.kt b/core/network/src/commonMain/kotlin/club/nito/core/network/user/SupabaseUserRemoteDataSource.kt index 8661d8f6..e3eeb691 100644 --- a/core/network/src/commonMain/kotlin/club/nito/core/network/user/SupabaseUserRemoteDataSource.kt +++ b/core/network/src/commonMain/kotlin/club/nito/core/network/user/SupabaseUserRemoteDataSource.kt @@ -1,16 +1,18 @@ package club.nito.core.network.user import club.nito.core.model.UserProfile +import club.nito.core.network.NetworkService import club.nito.core.network.user.model.NetworkUserProfile import io.github.jan.supabase.SupabaseClient import io.github.jan.supabase.postgrest.postgrest public class SupabaseUserRemoteDataSource( + private val networkService: NetworkService, private val client: SupabaseClient, ) : UserRemoteDataSource { private val postgrest = client.postgrest["profiles"] - override suspend fun getProfile(userId: String): UserProfile? { + override suspend fun getProfile(userId: String): UserProfile? = networkService { return postgrest .select { single() @@ -22,8 +24,8 @@ public class SupabaseUserRemoteDataSource( ?.let(NetworkUserProfile::toUserProfile) } - override suspend fun getProfiles(userIds: List): List { - return postgrest + override suspend fun getProfiles(userIds: List): List = networkService { + postgrest .select { filter { isIn("id", userIds) diff --git a/feature/auth/src/commonMain/kotlin/club/nito/feature/auth/LoginScreenStateMachine.kt b/feature/auth/src/commonMain/kotlin/club/nito/feature/auth/LoginScreenStateMachine.kt index 1fe8e865..519245bc 100644 --- a/feature/auth/src/commonMain/kotlin/club/nito/feature/auth/LoginScreenStateMachine.kt +++ b/feature/auth/src/commonMain/kotlin/club/nito/feature/auth/LoginScreenStateMachine.kt @@ -1,10 +1,9 @@ package club.nito.feature.auth +import club.nito.core.domain.AuthStatusStreamUseCase import club.nito.core.domain.LoginUseCase -import club.nito.core.domain.ObserveAuthStatusUseCase import club.nito.core.model.AuthStatus import club.nito.core.model.ExecuteResult -import club.nito.core.model.FetchSingleResult import club.nito.core.ui.StateMachine import club.nito.core.ui.buildUiState import club.nito.core.ui.message.UserMessageStateHolder @@ -19,7 +18,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch public class LoginScreenStateMachine( - observeAuthStatusUseCase: ObserveAuthStatusUseCase, + authStatusStream: AuthStatusStreamUseCase, private val login: LoginUseCase, public val userMessageStateHolder: UserMessageStateHolder, ) : StateMachine(), @@ -27,10 +26,10 @@ public class LoginScreenStateMachine( private val email = MutableStateFlow("") private val password = MutableStateFlow("") - private val authStatus = observeAuthStatusUseCase().stateIn( + private val authStatus = authStatusStream().stateIn( scope = stateMachineScope, started = SharingStarted.WhileSubscribed(5_000), - initialValue = FetchSingleResult.Loading, + initialValue = AuthStatus.Loading, ) public val uiState: StateFlow = buildUiState( @@ -41,7 +40,7 @@ public class LoginScreenStateMachine( LoginScreenUiState( email = email, password = password, - isSignInning = authStatus is FetchSingleResult.Loading, + isSignInning = authStatus is AuthStatus.Loading, ) } @@ -51,7 +50,7 @@ public class LoginScreenStateMachine( init { stateMachineScope.launch { authStatus.collectLatest { - if (it is FetchSingleResult.Success && it.data is AuthStatus.Authenticated) { + if (it is AuthStatus.Authenticated) { _events.emit(listOf(LoginScreenEvent.LoggedIn)) } } diff --git a/feature/auth/src/commonMain/kotlin/club/nito/feature/auth/di/AuthFeatureModule.kt b/feature/auth/src/commonMain/kotlin/club/nito/feature/auth/di/AuthFeatureModule.kt index c13063cb..a3fcbc0a 100644 --- a/feature/auth/src/commonMain/kotlin/club/nito/feature/auth/di/AuthFeatureModule.kt +++ b/feature/auth/src/commonMain/kotlin/club/nito/feature/auth/di/AuthFeatureModule.kt @@ -7,7 +7,7 @@ import org.koin.dsl.module public val authFeatureModule: Module = module { factory { LoginScreenStateMachine( - observeAuthStatusUseCase = get(), + authStatusStream = get(), login = get(), userMessageStateHolder = get(), ) diff --git a/feature/settings/src/commonMain/kotlin/club/nito/feature/settings/SettingsScreenStateMachine.kt b/feature/settings/src/commonMain/kotlin/club/nito/feature/settings/SettingsScreenStateMachine.kt index d13cd1c0..286de152 100644 --- a/feature/settings/src/commonMain/kotlin/club/nito/feature/settings/SettingsScreenStateMachine.kt +++ b/feature/settings/src/commonMain/kotlin/club/nito/feature/settings/SettingsScreenStateMachine.kt @@ -1,11 +1,10 @@ package club.nito.feature.settings +import club.nito.core.domain.AuthStatusStreamUseCase import club.nito.core.domain.LogoutUseCase import club.nito.core.domain.ModifyPasswordUseCase -import club.nito.core.domain.ObserveAuthStatusUseCase import club.nito.core.model.AuthStatus import club.nito.core.model.ExecuteResult -import club.nito.core.model.FetchSingleResult import club.nito.core.ui.StateMachine import club.nito.core.ui.buildUiState import club.nito.core.ui.message.UserMessageStateHolder @@ -21,17 +20,17 @@ import kotlinx.coroutines.launch import moe.tlaster.precompose.viewmodel.viewModelScope public class SettingsScreenStateMachine( - observeAuthStatus: ObserveAuthStatusUseCase, + authStatusStream: AuthStatusStreamUseCase, private val modifyPassword: ModifyPasswordUseCase, private val logout: LogoutUseCase, public val userMessageStateHolder: UserMessageStateHolder, ) : StateMachine(), UserMessageStateHolder by userMessageStateHolder { - private val authStatus = observeAuthStatus().stateIn( + private val authStatus = authStatusStream().stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), - initialValue = FetchSingleResult.Loading, + initialValue = AuthStatus.Loading, ) private val showModifyPasswordDialog = MutableStateFlow(false) @@ -51,7 +50,7 @@ public class SettingsScreenStateMachine( } SettingsScreenUiState( - isSignOuting = authStatus is FetchSingleResult.Loading, + isSignOuting = authStatus is AuthStatus.Loading, modifyPassword = modifyPassword, ) } @@ -62,7 +61,7 @@ public class SettingsScreenStateMachine( init { viewModelScope.launch { authStatus.collectLatest { - if (it is FetchSingleResult.Success && it.data is AuthStatus.NotAuthenticated) { + if (it is AuthStatus.NotAuthenticated) { _events.emit(listOf(SettingsScreenEvent.SignedOut)) } } diff --git a/feature/settings/src/commonMain/kotlin/club/nito/feature/settings/di/SettingsFeatureModule.kt b/feature/settings/src/commonMain/kotlin/club/nito/feature/settings/di/SettingsFeatureModule.kt index 913f9da9..1b30c6ee 100644 --- a/feature/settings/src/commonMain/kotlin/club/nito/feature/settings/di/SettingsFeatureModule.kt +++ b/feature/settings/src/commonMain/kotlin/club/nito/feature/settings/di/SettingsFeatureModule.kt @@ -7,7 +7,7 @@ import org.koin.dsl.module public val settingsFeatureModule: Module = module { factory { SettingsScreenStateMachine( - observeAuthStatus = get(), + authStatusStream = get(), modifyPassword = get(), logout = get(), userMessageStateHolder = get(),