From 9c249dfc60ac61cb316a62a796b6d4bd03f83ef1 Mon Sep 17 00:00:00 2001 From: stslex Date: Sun, 17 Aug 2025 15:43:02 +0300 Subject: [PATCH 1/4] app appError and refactor auth --- core/auth/build.gradle.kts | 1 + .../auth/controller/GoogleAuthController.kt | 1 - .../core/auth/repository/AuthRepository.kt | 10 ++ .../auth/repository/AuthRepositoryImpl.kt | 21 +++++ .../auth/repository/model/TokenDataMapper.kt | 16 ++++ .../auth/repository/model/TokenDataModel.kt | 8 ++ .../core/coroutine/scope/AppCoroutineScope.kt | 2 +- .../core/core/logger/AppLoggerCreator.kt | 7 ++ .../com/stslex/atten/core/core/logger/Log.kt | 16 ++++ .../stslex/atten/core/core/logger/Logger.kt | 2 + .../stslex/atten/core/core/model/AppError.kt | 2 +- .../atten/core/core/model/IgnoreError.kt | 9 +- .../atten/core/core/model/UnresolveError.kt | 9 ++ .../atten/core/core/result/AppResult.kt | 41 ++++++++ .../atten/core/core/result/AppResultMapper.kt | 6 ++ .../stslex/atten/core/core/result/Mapping.kt | 6 ++ .../atten/core/core/result/ResultFlow.kt | 45 +++++++++ .../atten/core/core/result/ResultUtils.kt | 94 +++++++++++++++++++ .../settings/mvi/handlers/ClickHandler.kt | 36 +++++-- 19 files changed, 322 insertions(+), 10 deletions(-) create mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/AuthRepository.kt create mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/AuthRepositoryImpl.kt create mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/model/TokenDataMapper.kt create mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/model/TokenDataModel.kt create mode 100644 core/core/src/commonMain/kotlin/com/stslex/atten/core/core/model/UnresolveError.kt create mode 100644 core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/AppResult.kt create mode 100644 core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/AppResultMapper.kt create mode 100644 core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/Mapping.kt create mode 100644 core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/ResultFlow.kt create mode 100644 core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/ResultUtils.kt diff --git a/core/auth/build.gradle.kts b/core/auth/build.gradle.kts index d12c79b..bf9040c 100644 --- a/core/auth/build.gradle.kts +++ b/core/auth/build.gradle.kts @@ -7,6 +7,7 @@ kotlin { commonMain.dependencies { implementation(project(":core:core")) implementation(project(":core:ui:kit")) + implementation(project(":core:network:api")) } androidMain.dependencies { implementation(libs.gms.auth) diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthController.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthController.kt index 696bde4..41ef2f9 100644 --- a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthController.kt +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthController.kt @@ -10,4 +10,3 @@ interface GoogleAuthController { fun auth(block: (Result) -> Unit) } - diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/AuthRepository.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/AuthRepository.kt new file mode 100644 index 0000000..0c97a41 --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/AuthRepository.kt @@ -0,0 +1,10 @@ +package com.stslex.atten.core.auth.repository + +import com.stslex.atten.core.auth.repository.model.TokenDataModel +import com.stslex.atten.core.core.result.AppResult +import kotlinx.coroutines.flow.Flow + +interface AuthRepository { + + fun auth(googleToken: String): Flow> +} diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/AuthRepositoryImpl.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/AuthRepositoryImpl.kt new file mode 100644 index 0000000..058105f --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/AuthRepositoryImpl.kt @@ -0,0 +1,21 @@ +package com.stslex.atten.core.auth.repository + +import com.stslex.atten.core.auth.repository.model.TokenDataMapper +import com.stslex.atten.core.auth.repository.model.TokenDataModel +import com.stslex.atten.core.core.result.AppResult +import com.stslex.atten.core.core.result.ResultUtils.flowRunCatching +import com.stslex.atten.core.network.api.AuthApiClient +import kotlinx.coroutines.flow.Flow +import org.koin.core.annotation.Single + +@Single +internal class AuthRepositoryImpl( + private val authClient: AuthApiClient +) : AuthRepository { + + override fun auth( + googleToken: String + ): Flow> = flowRunCatching(TokenDataMapper) { + authClient.auth(googleToken) + } +} \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/model/TokenDataMapper.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/model/TokenDataMapper.kt new file mode 100644 index 0000000..d8f1d32 --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/model/TokenDataMapper.kt @@ -0,0 +1,16 @@ +package com.stslex.atten.core.auth.repository.model + +import com.stslex.atten.core.core.result.Mapping +import com.stslex.atten.core.network.api.model.TokenResponseModel + +object TokenDataMapper : Mapping { + + override fun invoke(data: TokenResponseModel): TokenDataModel = with(data) { + TokenDataModel( + uuid = uuid, + email = email, + accessToken = accessToken, + refreshToken = refreshToken + ) + } +} \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/model/TokenDataModel.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/model/TokenDataModel.kt new file mode 100644 index 0000000..7743a41 --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/model/TokenDataModel.kt @@ -0,0 +1,8 @@ +package com.stslex.atten.core.auth.repository.model + +data class TokenDataModel( + val uuid: String, + val email: String, + val accessToken: String, + val refreshToken: String +) \ No newline at end of file diff --git a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/coroutine/scope/AppCoroutineScope.kt b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/coroutine/scope/AppCoroutineScope.kt index 474162e..e056544 100644 --- a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/coroutine/scope/AppCoroutineScope.kt +++ b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/coroutine/scope/AppCoroutineScope.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.withContext class AppCoroutineScope( private val scope: CoroutineScope, - private val appDispatcher: AppDispatcher, + val appDispatcher: AppDispatcher, ) { private fun exceptionHandler( diff --git a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/logger/AppLoggerCreator.kt b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/logger/AppLoggerCreator.kt index 7e0460d..23efbab 100644 --- a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/logger/AppLoggerCreator.kt +++ b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/logger/AppLoggerCreator.kt @@ -22,6 +22,13 @@ internal class AppLoggerCreator( ) } + override fun e(message: String) { + Log.e( + tag = tag, + message = message + ) + } + override fun i(message: String) { Log.i( tag = tag, diff --git a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/logger/Log.kt b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/logger/Log.kt index c4aefac..d76ab4f 100644 --- a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/logger/Log.kt +++ b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/logger/Log.kt @@ -24,6 +24,18 @@ object Log : AtTenLogger { ) } + fun e( + message: String, + tag: String? = null, + ) { + if (isDebug.not()) return + // todo firebase crashlytics + Logger.Companion.e( + tag = tag ?: DEFAULT_TAG, + messageString = message, + ) + } + fun d( message: String, tag: String? = null, @@ -61,6 +73,10 @@ object Log : AtTenLogger { e(throwable = throwable, tag = DEFAULT_TAG, message = message) } + override fun e(message: String) { + e(tag = DEFAULT_TAG, message = message) + } + override fun d(message: String) { d(message = message, tag = DEFAULT_TAG) } diff --git a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/logger/Logger.kt b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/logger/Logger.kt index 1f6ed0a..e150f31 100644 --- a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/logger/Logger.kt +++ b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/logger/Logger.kt @@ -7,6 +7,8 @@ interface Logger { message: String? = null ) + fun e(message: String) + fun d(message: String) fun i(message: String) diff --git a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/model/AppError.kt b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/model/AppError.kt index a270aea..7e3fd95 100644 --- a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/model/AppError.kt +++ b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/model/AppError.kt @@ -6,4 +6,4 @@ open class AppError( ) : Throwable( message = message, cause = cause, -) \ No newline at end of file +) diff --git a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/model/IgnoreError.kt b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/model/IgnoreError.kt index 6b86cf3..29bf513 100644 --- a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/model/IgnoreError.kt +++ b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/model/IgnoreError.kt @@ -1,2 +1,9 @@ -package com.stslex.atten.core.core.model +package com.stslex.atten.core.core.model +abstract class IgnoreError( + message: String? = null, + cause: Throwable? = null +) : AppError( + message = message, + cause = cause +) \ No newline at end of file diff --git a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/model/UnresolveError.kt b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/model/UnresolveError.kt new file mode 100644 index 0000000..d0e1c41 --- /dev/null +++ b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/model/UnresolveError.kt @@ -0,0 +1,9 @@ +package com.stslex.atten.core.core.model + +data class UnresolveError( + override val message: String? = null, + override val cause: Throwable? = null +) : AppError( + message = message, + cause = cause +) \ No newline at end of file diff --git a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/AppResult.kt b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/AppResult.kt new file mode 100644 index 0000000..cb56d72 --- /dev/null +++ b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/AppResult.kt @@ -0,0 +1,41 @@ +package com.stslex.atten.core.core.result + +import com.stslex.atten.core.core.model.AppError + +sealed interface AppResult : AppResultMapper { + + data class Success(val data: T) : AppResult { + + override fun map(mapper: Mapping): AppResult = Success(mapper(data)) + } + + data class Error(val error: AppError) : AppResult { + + override fun map(mapper: Mapping): AppResult = Error(error) + } + + data object Loading : AppResult { + + override fun map(mapper: Mapping): AppResult = Loading + } + + fun onSuccess(action: (T) -> Unit): AppResult = this.apply { + (this as? Success)?.let { action(it.data) } + } + + fun onError(action: (AppError) -> Unit): AppResult = + this.apply { (this as? Error)?.let { action(it.error) } } + + fun onLoading(action: () -> Unit): AppResult = this.apply { + if (this is Loading) action() + } + + companion object { + + fun success(value: T): AppResult = Success(value) + + fun error(error: AppError): AppResult<*> = Error(error) + + fun loading(): AppResult<*> = Loading + } +} diff --git a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/AppResultMapper.kt b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/AppResultMapper.kt new file mode 100644 index 0000000..72a7453 --- /dev/null +++ b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/AppResultMapper.kt @@ -0,0 +1,6 @@ +package com.stslex.atten.core.core.result + +interface AppResultMapper { + + fun map(mapper: Mapping): AppResult +} \ No newline at end of file diff --git a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/Mapping.kt b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/Mapping.kt new file mode 100644 index 0000000..f23f693 --- /dev/null +++ b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/Mapping.kt @@ -0,0 +1,6 @@ +package com.stslex.atten.core.core.result + +fun interface Mapping { + + operator fun invoke(data: T): R +} \ No newline at end of file diff --git a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/ResultFlow.kt b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/ResultFlow.kt new file mode 100644 index 0000000..a8f9a7f --- /dev/null +++ b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/ResultFlow.kt @@ -0,0 +1,45 @@ +package com.stslex.atten.core.core.result + +import com.stslex.atten.core.core.coroutine.scope.AppCoroutineScope +import com.stslex.atten.core.core.logger.Log +import com.stslex.atten.core.core.model.AppError +import com.stslex.atten.core.core.result.ResultUtils.fold +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow + +class ResultFlow( + private val flow: Flow> +) { + + private var onError: ((AppError) -> Unit)? = null + private var onLoading: (() -> Unit)? = null + private var onSuccess: ((T) -> Unit)? = null + + + fun onError(block: (AppError) -> Unit): ResultFlow { + onError = block + return this + } + + fun onLoading(block: () -> Unit): ResultFlow { + onLoading = block + return this + } + + fun onSuccess(block: (T) -> Unit): ResultFlow { + onSuccess = block + return this + } + + fun collect(scope: AppCoroutineScope): Job = flow.fold( + scope = scope, + onError = { onError?.invoke(it) ?: Log.tag(TAG).e(it, it.message) }, + onLoading = { onLoading?.invoke() ?: Log.tag(TAG).i("Loading...") }, + onSuccess = { onSuccess?.invoke(it) ?: Log.tag(TAG).i("Success: $it") } + ) + + companion object { + + private const val TAG = "ResultFlow" + } +} \ No newline at end of file diff --git a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/ResultUtils.kt b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/ResultUtils.kt new file mode 100644 index 0000000..7df1b66 --- /dev/null +++ b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/ResultUtils.kt @@ -0,0 +1,94 @@ +package com.stslex.atten.core.core.result + +import com.stslex.atten.core.core.coroutine.scope.AppCoroutineScope +import com.stslex.atten.core.core.logger.Log +import com.stslex.atten.core.core.model.AppError +import com.stslex.atten.core.core.model.UnresolveError +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +object ResultUtils { + + private const val TAG = "RESULT_CATCHER" + private val logger = Log.tag(TAG) + + fun Flow>.onError( + block: (AppError) -> Unit + ): ResultFlow = ResultFlow(this).onError(block) + + fun Flow>.onLoading( + block: () -> Unit + ): ResultFlow = ResultFlow(this).onLoading(block) + + fun Flow>.onSuccess( + block: (T) -> Unit + ): ResultFlow = ResultFlow(this).onSuccess(block) + + fun Flow>.fold( + scope: AppCoroutineScope, + onError: suspend (AppError) -> Unit = { logger.e(it, it.message) }, + onLoading: suspend () -> Unit = {}, + onSuccess: suspend (T) -> Unit, + ): Job = scope.launch(this) { result -> + when (result) { + is AppResult.Success -> onSuccess(result.data) + is AppResult.Error -> onError(result.error) + AppResult.Loading -> onLoading() + } + } + + fun flowRunCatching( + mapper: Mapping, + block: suspend () -> T + ): Flow> = flowRunCatching { mapper(block()) } + + fun flowRunCatching( + block: suspend () -> T + ): Flow> = flow { + val result = runCatching { + block() + }.fold( + onSuccess = { data -> + AppResult.Success(data) + }, + onFailure = { error -> + logger.e(error, error.message) + if (error is AppError) { + AppResult.Error(error) + } else { + AppResult.Error(UnresolveError(error.message, error)) + } + } + ) + emit(result) + } + + fun appRunCatching( + mapper: Mapping, + block: () -> T + ): AppResult = appRunCatching { mapper(block()) } + + fun appRunCatching( + block: () -> T + ): AppResult = runCatching { + block() + }.fold( + onSuccess = { data -> + AppResult.Success(data) + }, + onFailure = { error -> + logger.e(error, error.message) + if (error is AppError) { + AppResult.Error(error) + } else { + AppResult.Error( + UnresolveError( + message = error.message, + cause = error + ) + ) + } + } + ) +} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/ClickHandler.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/ClickHandler.kt index 51f5b49..21cc3be 100644 --- a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/ClickHandler.kt +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/ClickHandler.kt @@ -1,10 +1,14 @@ package com.stslex.atten.feature.settings.mvi.handlers import com.stslex.atten.core.auth.controller.GoogleAuthController +import com.stslex.atten.core.auth.model.GoogleAuthResult +import com.stslex.atten.core.auth.repository.AuthRepository +import com.stslex.atten.core.core.result.ResultUtils.onSuccess import com.stslex.atten.core.ui.mvi.handler.Handler import com.stslex.atten.feature.settings.di.SettingsScope import com.stslex.atten.feature.settings.mvi.SettingsHandlerStore import com.stslex.atten.feature.settings.mvi.SettingsStore.Action +import kotlinx.coroutines.Job import org.koin.core.annotation.Factory import org.koin.core.annotation.Scope import org.koin.core.annotation.Scoped @@ -13,8 +17,12 @@ import org.koin.core.annotation.Scoped @Scope(SettingsScope::class) @Scoped() class ClickHandler( - private val authController: GoogleAuthController -) : Handler { + private val authController: GoogleAuthController, + private val repository: AuthRepository, + private val store: SettingsHandlerStore +) : Handler, SettingsHandlerStore by store { + + private var loginJob: Job? = null override fun SettingsHandlerStore.invoke(action: Action.Click) { when (action) { @@ -23,15 +31,31 @@ class ClickHandler( } } - private fun SettingsHandlerStore.actionBack() { - consume(Action.Navigation.NavBack) + private fun actionBack() { + store.consume(Action.Navigation.NavBack) } - private fun SettingsHandlerStore.actionLogin() { + private fun actionLogin() { authController.auth { result -> result - .onSuccess { logger.i("success: $it") } + .onSuccess { consumeLogin(it) } .onFailure { logger.e(it, "auth error") } } } + + private fun consumeLogin(googleInfo: GoogleAuthResult) { + logger.i("consumeLogin: $googleInfo") + val token = googleInfo.accessToken + if (token.isNullOrEmpty()) { + logger.e(message = "Access token is null or empty") + // todo handle error, show message to user + return + } + loginJob?.cancel() + loginJob = repository.auth(token) + .onSuccess { logger.i("login success: $it") } + .onError { logger.e(it, "login error") } + .onLoading { logger.i("login loading...") } + .collect(store.scope) + } } \ No newline at end of file From ceadd669582aaa47ced3ff4eeeecfd0b6012c1e6 Mon Sep 17 00:00:00 2001 From: stslex Date: Sun, 17 Aug 2025 16:12:10 +0300 Subject: [PATCH 2/4] fix di issues --- .../kotlin/com/stslex/atten/di/AppModules.kt | 2 + core/auth/build.gradle.kts | 1 + .../GoogleAuthControllerImpl.android.kt | 40 ++++++++++++------- .../atten/core/auth/model/GoogleAuthData.kt | 10 +++++ .../atten/core/auth/model/GoogleAuthResult.kt | 11 +++-- .../atten/core/core/result/ResultUtils.kt | 15 ++++--- core/network/client/build.gradle.kts | 2 +- .../network/client/client/AppHttpApiImpl.kt | 2 +- .../client/client/AuthApiClientImpl.kt | 2 +- .../settings/mvi/handlers/ClickHandler.kt | 31 +++++++++----- 10 files changed, 78 insertions(+), 38 deletions(-) create mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/GoogleAuthData.kt diff --git a/commonApp/src/commonMain/kotlin/com/stslex/atten/di/AppModules.kt b/commonApp/src/commonMain/kotlin/com/stslex/atten/di/AppModules.kt index 4798884..e916a02 100644 --- a/commonApp/src/commonMain/kotlin/com/stslex/atten/di/AppModules.kt +++ b/commonApp/src/commonMain/kotlin/com/stslex/atten/di/AppModules.kt @@ -5,6 +5,7 @@ import com.stslex.atten.core.core.di.ModuleCore import com.stslex.atten.core.database.di.ModuleCoreDatabase import com.stslex.atten.core.network.client.di.ModuleCoreNetwork import com.stslex.atten.core.paging.di.ModuleCorePaging +import com.stslex.atten.core.store.di.ModuleCoreStore import com.stslex.atten.core.todo.di.ModuleCoreToDo import com.stslex.atten.core.ui.kit.utils.ModuleCoreUiUtils import com.stslex.atten.feature.details.di.ModuleFeatureDetails @@ -20,6 +21,7 @@ val appModules: List = listOf( ModuleCorePaging().module, ModuleCoreUiUtils().module, ModuleCoreAuth().module, + ModuleCoreStore().module, ModuleCoreNetwork().module, ModuleFeatureHome().module, ModuleFeatureDetails().module, diff --git a/core/auth/build.gradle.kts b/core/auth/build.gradle.kts index bf9040c..d2ed7d7 100644 --- a/core/auth/build.gradle.kts +++ b/core/auth/build.gradle.kts @@ -8,6 +8,7 @@ kotlin { implementation(project(":core:core")) implementation(project(":core:ui:kit")) implementation(project(":core:network:api")) + implementation(project(":core:network:client")) } androidMain.dependencies { implementation(libs.gms.auth) diff --git a/core/auth/src/androidMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.android.kt b/core/auth/src/androidMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.android.kt index 27e3413..bf63f3c 100644 --- a/core/auth/src/androidMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.android.kt +++ b/core/auth/src/androidMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.android.kt @@ -14,6 +14,7 @@ import com.google.android.gms.auth.api.identity.Identity import com.google.android.gms.common.Scopes import com.google.android.gms.common.api.Scope import com.stslex.atten.core.auth.callback.GoogleAuthCallback +import com.stslex.atten.core.auth.model.GoogleAuthData import com.stslex.atten.core.auth.model.GoogleAuthResult import com.stslex.atten.core.core.logger.Log import com.stslex.atten.core.ui.kit.utils.ActivityHolder @@ -34,18 +35,28 @@ internal actual class GoogleAuthControllerImpl actual constructor( ) { result -> logger.d("activity_result: $result, ${result.data}") val activity = checkNotNull(activityHolder.activity as? ComponentActivity) - val result = if (result.resultCode == Activity.RESULT_OK) { - val authorizationResult = Identity - .getAuthorizationClient(activity) - .getAuthorizationResultFromIntent(result.data) - val uiResult = GoogleAuthResult( - accessToken = authorizationResult.accessToken, - serverAuthCode = authorizationResult.serverAuthCode - ) - Result.success(uiResult) - } else { - val msg = "auth fail with ${result.resultCode} code" - Result.failure(IllegalStateException(msg)) + val result = when (result.resultCode) { + Activity.RESULT_OK -> { + val authorizationResult = Identity + .getAuthorizationClient(activity) + .getAuthorizationResultFromIntent(result.data) + val uiResult = GoogleAuthData( + accessToken = authorizationResult.accessToken, + serverAuthCode = authorizationResult.serverAuthCode + ) + Result.success(GoogleAuthResult.Success(uiResult)) + } + + Activity.RESULT_CANCELED -> { + logger.i("auth cancelled by user") + Result.success(GoogleAuthResult.Cancelled) + return@rememberLauncherForActivityResult + } + + else -> { + val msg = "auth fail with ${result.resultCode} code" + Result.failure(IllegalStateException(msg)) + } } callback.process(result) } @@ -73,11 +84,11 @@ internal actual class GoogleAuthControllerImpl actual constructor( logger.e(e, "Couldn't start Authorization UI: " + e.localizedMessage) } } else { - val uiResult = GoogleAuthResult( + val uiResult = GoogleAuthData( accessToken = result.accessToken, serverAuthCode = result.serverAuthCode ) - callback.process(Result.success(uiResult)) + callback.process(Result.success(GoogleAuthResult.Success(uiResult))) } } .addOnFailureListener { @@ -91,4 +102,5 @@ internal actual class GoogleAuthControllerImpl actual constructor( private val logger = Log.tag(TAG) private const val TAG = "GOOGLE_AUTH_CONTROLLER" } + } \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/GoogleAuthData.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/GoogleAuthData.kt new file mode 100644 index 0000000..701282d --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/GoogleAuthData.kt @@ -0,0 +1,10 @@ +package com.stslex.atten.core.auth.model + +import androidx.compose.runtime.Stable + +@Stable +data class GoogleAuthData( + val serverAuthCode: String?, + val accessToken: String?, +) + diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/GoogleAuthResult.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/GoogleAuthResult.kt index 4e50773..94b6653 100644 --- a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/GoogleAuthResult.kt +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/GoogleAuthResult.kt @@ -1,9 +1,8 @@ package com.stslex.atten.core.auth.model -import androidx.compose.runtime.Stable +sealed interface GoogleAuthResult { -@Stable -data class GoogleAuthResult( - val serverAuthCode: String?, - val accessToken: String?, -) + data class Success(val data: GoogleAuthData) : GoogleAuthResult + + data object Cancelled : GoogleAuthResult +} \ No newline at end of file diff --git a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/ResultUtils.kt b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/ResultUtils.kt index 7df1b66..fa07447 100644 --- a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/ResultUtils.kt +++ b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/ResultUtils.kt @@ -30,13 +30,16 @@ object ResultUtils { onError: suspend (AppError) -> Unit = { logger.e(it, it.message) }, onLoading: suspend () -> Unit = {}, onSuccess: suspend (T) -> Unit, - ): Job = scope.launch(this) { result -> - when (result) { - is AppResult.Success -> onSuccess(result.data) - is AppResult.Error -> onError(result.error) - AppResult.Loading -> onLoading() + ): Job = scope + .launch( + flow = this, + onError = { onError(UnresolveError(it.message, it)) }) { result -> + when (result) { + is AppResult.Success -> onSuccess(result.data) + is AppResult.Error -> onError(result.error) + AppResult.Loading -> onLoading() + } } - } fun flowRunCatching( mapper: Mapping, diff --git a/core/network/client/build.gradle.kts b/core/network/client/build.gradle.kts index 4a2f659..edcb949 100644 --- a/core/network/client/build.gradle.kts +++ b/core/network/client/build.gradle.kts @@ -12,7 +12,7 @@ kotlin { commonMain { dependencies { implementation(project(":core:core")) - implementation(project(":core:network:api")) + api(project(":core:network:api")) implementation(project(":core:store")) implementation(libs.bundles.ktor) } diff --git a/core/network/client/src/commonMain/kotlin/com/stslex/atten/core/network/client/client/AppHttpApiImpl.kt b/core/network/client/src/commonMain/kotlin/com/stslex/atten/core/network/client/client/AppHttpApiImpl.kt index c76f41f..5552e3a 100644 --- a/core/network/client/src/commonMain/kotlin/com/stslex/atten/core/network/client/client/AppHttpApiImpl.kt +++ b/core/network/client/src/commonMain/kotlin/com/stslex/atten/core/network/client/client/AppHttpApiImpl.kt @@ -10,7 +10,7 @@ import org.koin.core.annotation.Singleton @Single @Singleton -class AppHttpApiImpl( +internal class AppHttpApiImpl( private val appDispatcher: AppDispatcher, private val appHttpClient: AppHttpClient ) : AppHttpApi { diff --git a/core/network/client/src/commonMain/kotlin/com/stslex/atten/core/network/client/client/AuthApiClientImpl.kt b/core/network/client/src/commonMain/kotlin/com/stslex/atten/core/network/client/client/AuthApiClientImpl.kt index 25f073e..3e89b03 100644 --- a/core/network/client/src/commonMain/kotlin/com/stslex/atten/core/network/client/client/AuthApiClientImpl.kt +++ b/core/network/client/src/commonMain/kotlin/com/stslex/atten/core/network/client/client/AuthApiClientImpl.kt @@ -16,7 +16,7 @@ import org.koin.core.annotation.Singleton @Single @Singleton -class AuthApiClientImpl( +internal class AuthApiClientImpl( private val appHttpApi: AppHttpApi, private val userStore: UserStore, ) : AuthApiClient { diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/ClickHandler.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/ClickHandler.kt index 21cc3be..186b1d7 100644 --- a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/ClickHandler.kt +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/ClickHandler.kt @@ -19,8 +19,8 @@ import org.koin.core.annotation.Scoped class ClickHandler( private val authController: GoogleAuthController, private val repository: AuthRepository, - private val store: SettingsHandlerStore -) : Handler, SettingsHandlerStore by store { +// private val store: SettingsHandlerStore +) : Handler { private var loginJob: Job? = null @@ -31,11 +31,11 @@ class ClickHandler( } } - private fun actionBack() { - store.consume(Action.Navigation.NavBack) + private fun SettingsHandlerStore.actionBack() { + consume(Action.Navigation.NavBack) } - private fun actionLogin() { + private fun SettingsHandlerStore.actionLogin() { authController.auth { result -> result .onSuccess { consumeLogin(it) } @@ -43,9 +43,22 @@ class ClickHandler( } } - private fun consumeLogin(googleInfo: GoogleAuthResult) { - logger.i("consumeLogin: $googleInfo") - val token = googleInfo.accessToken + private fun SettingsHandlerStore.consumeLogin(result: GoogleAuthResult) { + logger.i("consumeLogin: $result") + val token = when (result) { + GoogleAuthResult.Cancelled -> { + logger.i("Google auth cancelled by user") + return + } + + is GoogleAuthResult.Success -> result.data.accessToken.also { + if (it.isNullOrBlank()) { + logger.e("Access token is null or empty") + return + } + } + } + if (token.isNullOrEmpty()) { logger.e(message = "Access token is null or empty") // todo handle error, show message to user @@ -56,6 +69,6 @@ class ClickHandler( .onSuccess { logger.i("login success: $it") } .onError { logger.e(it, "login error") } .onLoading { logger.i("login loading...") } - .collect(store.scope) + .collect(scope) } } \ No newline at end of file From ebbe5e594e12e2f5624f07f5ec26798b03541657 Mon Sep 17 00:00:00 2001 From: stslex Date: Sun, 24 Aug 2025 13:19:16 +0300 Subject: [PATCH 3/4] refactor gradle.properties --- gradle.properties | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/gradle.properties b/gradle.properties index 17df7ab..02bc10a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,10 +1,22 @@ -#Gradle -org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options=-Xmx2048M +## For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx1024m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +# +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects +# org.gradle.parallel=true +#Sat Aug 23 20:43:03 MSK 2025 +android.nonTransitiveRClass=true +android.useAndroidX=true +kotlin.code.style=official +ksp.useKSP2=true org.gradle.caching=true org.gradle.configuration-cache=true -#Kotlin -kotlin.code.style=official -#Android -android.useAndroidX=true -android.nonTransitiveRClass=true -ksp.useKSP2=true \ No newline at end of file +org.gradle.jvmargs=-Xms4g -Xmx18g -Dfile.encoding\=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx18g -XX:+UseG1GC -XX:MaxGCPauseMillis=400 -Dfile.encoding=UTF-8 +# Kotlin daemon (~18 ??) +kotlin.daemon.jvmargs=-Xms2g -Xmx18g From 7c81d2490a0b6f41ca9ae99d5df170ede4803df5 Mon Sep 17 00:00:00 2001 From: stslex Date: Mon, 25 Aug 2025 23:29:39 +0300 Subject: [PATCH 4/4] refactor auth endpoints --- .../core/auth/repository/AuthRepository.kt | 7 +++- .../auth/repository/AuthRepositoryImpl.kt | 19 +++++++--- .../auth/repository/model/TokenDataMapper.kt | 2 +- .../atten/core/core/model/UnresolveError.kt | 2 +- .../atten/core/core/result/AppResult.kt | 37 +++++++++++++------ .../atten/core/core/result/ResultUtils.kt | 33 ++++++++++++++++- .../atten/core/network/api/AppHttpApi.kt | 2 + .../atten/core/network/api/AppHttpClient.kt | 2 + .../atten/core/network/api/AuthApiClient.kt | 9 ++++- .../api/model/request/AuthGithubRequest.kt | 10 +++++ .../api/model/request/AuthGoogleRequest.kt | 2 +- .../model/response/GithubAuthResponseModel.kt | 14 +++++++ .../{ => response}/TokenResponseModel.kt | 2 +- .../network/client/client/AppHttpApiImpl.kt | 7 ++++ .../client/client/AppHttpClientImpl.kt | 9 +++++ .../client/client/AuthApiClientImpl.kt | 22 ++++++++++- .../settings/domain/AuthInteractorImpl.kt | 33 +++++++++++++++++ .../settings/domain/SettingsInteractor.kt | 12 ++++++ .../feature/settings/mvi/SettingsStore.kt | 5 ++- .../settings/mvi/handlers/ClickHandler.kt | 26 +++++++++---- .../feature/settings/ui/SettingsWidget.kt | 12 +++++- 21 files changed, 227 insertions(+), 40 deletions(-) create mode 100644 core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/model/request/AuthGithubRequest.kt create mode 100644 core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/model/response/GithubAuthResponseModel.kt rename core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/model/{ => response}/TokenResponseModel.kt (85%) create mode 100644 feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/domain/AuthInteractorImpl.kt create mode 100644 feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/domain/SettingsInteractor.kt diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/AuthRepository.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/AuthRepository.kt index 0c97a41..d012c9a 100644 --- a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/AuthRepository.kt +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/AuthRepository.kt @@ -2,9 +2,12 @@ package com.stslex.atten.core.auth.repository import com.stslex.atten.core.auth.repository.model.TokenDataModel import com.stslex.atten.core.core.result.AppResult -import kotlinx.coroutines.flow.Flow interface AuthRepository { - fun auth(googleToken: String): Flow> + suspend fun authGoogle(googleToken: String): AppResult + + suspend fun requestAuthGithub(): AppResult + + suspend fun authGithub(code: String): AppResult } diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/AuthRepositoryImpl.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/AuthRepositoryImpl.kt index 058105f..dc329be 100644 --- a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/AuthRepositoryImpl.kt +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/AuthRepositoryImpl.kt @@ -3,9 +3,8 @@ package com.stslex.atten.core.auth.repository import com.stslex.atten.core.auth.repository.model.TokenDataMapper import com.stslex.atten.core.auth.repository.model.TokenDataModel import com.stslex.atten.core.core.result.AppResult -import com.stslex.atten.core.core.result.ResultUtils.flowRunCatching +import com.stslex.atten.core.core.result.ResultUtils.suspendRunCatching import com.stslex.atten.core.network.api.AuthApiClient -import kotlinx.coroutines.flow.Flow import org.koin.core.annotation.Single @Single @@ -13,9 +12,19 @@ internal class AuthRepositoryImpl( private val authClient: AuthApiClient ) : AuthRepository { - override fun auth( + override suspend fun authGoogle( googleToken: String - ): Flow> = flowRunCatching(TokenDataMapper) { - authClient.auth(googleToken) + ): AppResult = suspendRunCatching(TokenDataMapper) { + authClient.googleAuth(googleToken) + } + + override suspend fun requestAuthGithub(): AppResult = suspendRunCatching { + authClient.githubRequestAuth().accessToken + } + + override suspend fun authGithub( + code: String + ): AppResult = suspendRunCatching(TokenDataMapper) { + authClient.githubAuth(code) } } \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/model/TokenDataMapper.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/model/TokenDataMapper.kt index d8f1d32..7cd07de 100644 --- a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/model/TokenDataMapper.kt +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/model/TokenDataMapper.kt @@ -1,7 +1,7 @@ package com.stslex.atten.core.auth.repository.model import com.stslex.atten.core.core.result.Mapping -import com.stslex.atten.core.network.api.model.TokenResponseModel +import com.stslex.atten.core.network.api.model.response.TokenResponseModel object TokenDataMapper : Mapping { diff --git a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/model/UnresolveError.kt b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/model/UnresolveError.kt index d0e1c41..2653c2d 100644 --- a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/model/UnresolveError.kt +++ b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/model/UnresolveError.kt @@ -1,8 +1,8 @@ package com.stslex.atten.core.core.model data class UnresolveError( + override val cause: Throwable? = null, override val message: String? = null, - override val cause: Throwable? = null ) : AppError( message = message, cause = cause diff --git a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/AppResult.kt b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/AppResult.kt index cb56d72..bf7509a 100644 --- a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/AppResult.kt +++ b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/AppResult.kt @@ -1,6 +1,7 @@ package com.stslex.atten.core.core.result import com.stslex.atten.core.core.model.AppError +import com.stslex.atten.core.core.model.UnresolveError sealed interface AppResult : AppResultMapper { @@ -19,23 +20,35 @@ sealed interface AppResult : AppResultMapper { override fun map(mapper: Mapping): AppResult = Loading } - fun onSuccess(action: (T) -> Unit): AppResult = this.apply { - (this as? Success)?.let { action(it.data) } - } + companion object { - fun onError(action: (AppError) -> Unit): AppResult = - this.apply { (this as? Error)?.let { action(it.error) } } + fun success(value: T): AppResult = Success(value) - fun onLoading(action: () -> Unit): AppResult = this.apply { - if (this is Loading) action() - } + fun error(error: AppError): AppResult = Error(error) - companion object { + fun error( + error: Throwable + ): AppResult = if (error is AppError) { + error(error) + } else { + error(UnresolveError(error)) + } - fun success(value: T): AppResult = Success(value) + fun loading(): AppResult = Loading + + inline fun AppResult.onError( + action: (AppError) -> Unit + ): AppResult = apply { (this as? Error)?.let { action(it.error) } } + + inline fun AppResult.onLoading(action: () -> Unit): AppResult = apply { + if (this is Loading) action() + } - fun error(error: AppError): AppResult<*> = Error(error) + inline fun AppResult.onSuccess( + action: (T) -> Unit + ): AppResult = this.apply { + (this as? Success)?.let { action(it.data) } + } - fun loading(): AppResult<*> = Loading } } diff --git a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/ResultUtils.kt b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/ResultUtils.kt index fa07447..9b9f2f5 100644 --- a/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/ResultUtils.kt +++ b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/ResultUtils.kt @@ -33,7 +33,7 @@ object ResultUtils { ): Job = scope .launch( flow = this, - onError = { onError(UnresolveError(it.message, it)) }) { result -> + onError = { onError(UnresolveError(it, it.message)) }) { result -> when (result) { is AppResult.Success -> onSuccess(result.data) is AppResult.Error -> onError(result.error) @@ -60,7 +60,7 @@ object ResultUtils { if (error is AppError) { AppResult.Error(error) } else { - AppResult.Error(UnresolveError(error.message, error)) + AppResult.Error(UnresolveError(error, error.message)) } } ) @@ -94,4 +94,33 @@ object ResultUtils { } } ) + + suspend fun suspendRunCatching( + mapper: Mapping, + block: suspend () -> T + ): AppResult = suspendRunCatching { mapper(block()) } + + suspend fun suspendRunCatching( + block: suspend () -> T + ): AppResult = runCatching { + block() + }.fold( + onSuccess = { data -> + AppResult.Success(data) + }, + onFailure = { error -> + logger.e(error, error.message) + if (error is AppError) { + AppResult.Error(error) + } else { + AppResult.Error( + UnresolveError( + message = error.message, + cause = error + ) + ) + } + } + ) + } \ No newline at end of file diff --git a/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/AppHttpApi.kt b/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/AppHttpApi.kt index f75338f..dd6efa1 100644 --- a/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/AppHttpApi.kt +++ b/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/AppHttpApi.kt @@ -5,4 +5,6 @@ import io.ktor.client.HttpClient interface AppHttpApi { suspend fun request(block: suspend HttpClient.() -> T): T + + suspend fun requestDefault(block: suspend HttpClient.() -> T): T } diff --git a/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/AppHttpClient.kt b/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/AppHttpClient.kt index 06c0337..0f80739 100644 --- a/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/AppHttpClient.kt +++ b/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/AppHttpClient.kt @@ -5,4 +5,6 @@ import io.ktor.client.HttpClient interface AppHttpClient { val client: HttpClient + + val defaultClient: HttpClient } \ No newline at end of file diff --git a/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/AuthApiClient.kt b/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/AuthApiClient.kt index 9ed83e0..f73be96 100644 --- a/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/AuthApiClient.kt +++ b/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/AuthApiClient.kt @@ -1,10 +1,15 @@ package com.stslex.atten.core.network.api -import com.stslex.atten.core.network.api.model.TokenResponseModel +import com.stslex.atten.core.network.api.model.response.GithubAuthResponseModel +import com.stslex.atten.core.network.api.model.response.TokenResponseModel interface AuthApiClient { - suspend fun auth(token: String): TokenResponseModel + suspend fun googleAuth(token: String): TokenResponseModel suspend fun refresh(): TokenResponseModel + + suspend fun githubRequestAuth(): GithubAuthResponseModel + + suspend fun githubAuth(code: String): TokenResponseModel } \ No newline at end of file diff --git a/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/model/request/AuthGithubRequest.kt b/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/model/request/AuthGithubRequest.kt new file mode 100644 index 0000000..b29bd30 --- /dev/null +++ b/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/model/request/AuthGithubRequest.kt @@ -0,0 +1,10 @@ +package com.stslex.atten.core.network.api.model.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class AuthGithubRequest( + @SerialName("code") + val code: String, +) \ No newline at end of file diff --git a/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/model/request/AuthGoogleRequest.kt b/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/model/request/AuthGoogleRequest.kt index a279a9f..6ade509 100644 --- a/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/model/request/AuthGoogleRequest.kt +++ b/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/model/request/AuthGoogleRequest.kt @@ -7,4 +7,4 @@ import kotlinx.serialization.Serializable data class AuthGoogleRequest( @SerialName("id_token") val idToken: String, -) \ No newline at end of file +) diff --git a/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/model/response/GithubAuthResponseModel.kt b/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/model/response/GithubAuthResponseModel.kt new file mode 100644 index 0000000..bdc611e --- /dev/null +++ b/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/model/response/GithubAuthResponseModel.kt @@ -0,0 +1,14 @@ +package com.stslex.atten.core.network.api.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +class GithubAuthResponseModel( + @SerialName("access_token") + val accessToken: String, + @SerialName("scope") + val scope: String, + @SerialName("token_type") + val tokenType: String +) \ No newline at end of file diff --git a/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/model/TokenResponseModel.kt b/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/model/response/TokenResponseModel.kt similarity index 85% rename from core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/model/TokenResponseModel.kt rename to core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/model/response/TokenResponseModel.kt index b4e0348..edb0eee 100644 --- a/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/model/TokenResponseModel.kt +++ b/core/network/api/src/commonMain/kotlin/com/stslex/atten/core/network/api/model/response/TokenResponseModel.kt @@ -1,4 +1,4 @@ -package com.stslex.atten.core.network.api.model +package com.stslex.atten.core.network.api.model.response import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/core/network/client/src/commonMain/kotlin/com/stslex/atten/core/network/client/client/AppHttpApiImpl.kt b/core/network/client/src/commonMain/kotlin/com/stslex/atten/core/network/client/client/AppHttpApiImpl.kt index 5552e3a..a16618e 100644 --- a/core/network/client/src/commonMain/kotlin/com/stslex/atten/core/network/client/client/AppHttpApiImpl.kt +++ b/core/network/client/src/commonMain/kotlin/com/stslex/atten/core/network/client/client/AppHttpApiImpl.kt @@ -20,4 +20,11 @@ internal class AppHttpApiImpl( ): T = withContext(appDispatcher.io) { block(appHttpClient.client) } + + override suspend fun requestDefault( + block: suspend HttpClient.() -> T + ): T = withContext(appDispatcher.io) { + block(appHttpClient.defaultClient) + } + } \ No newline at end of file diff --git a/core/network/client/src/commonMain/kotlin/com/stslex/atten/core/network/client/client/AppHttpClientImpl.kt b/core/network/client/src/commonMain/kotlin/com/stslex/atten/core/network/client/client/AppHttpClientImpl.kt index 66ba288..3c8f28b 100644 --- a/core/network/client/src/commonMain/kotlin/com/stslex/atten/core/network/client/client/AppHttpClientImpl.kt +++ b/core/network/client/src/commonMain/kotlin/com/stslex/atten/core/network/client/client/AppHttpClientImpl.kt @@ -44,6 +44,15 @@ internal class AppHttpClientImpl( installAuth() } + override val defaultClient: HttpClient = HttpClient(CIO) { + install(HttpCache.Companion) + expectSuccess = true + HttpResponseValidator { handleResponseExceptionWithRequest(errorHandler) } + setupNegotiation() + setupLogging() + installAuth() + } + private fun HttpClientConfig.setupNegotiation() { install(ContentNegotiation) { json( diff --git a/core/network/client/src/commonMain/kotlin/com/stslex/atten/core/network/client/client/AuthApiClientImpl.kt b/core/network/client/src/commonMain/kotlin/com/stslex/atten/core/network/client/client/AuthApiClientImpl.kt index 3e89b03..0df60cd 100644 --- a/core/network/client/src/commonMain/kotlin/com/stslex/atten/core/network/client/client/AuthApiClientImpl.kt +++ b/core/network/client/src/commonMain/kotlin/com/stslex/atten/core/network/client/client/AuthApiClientImpl.kt @@ -3,8 +3,9 @@ package com.stslex.atten.core.network.client.client import com.stslex.atten.core.network.api.AppHttpApi import com.stslex.atten.core.network.api.AuthApiClient import com.stslex.atten.core.network.api.model.request.AuthGoogleRequest +import com.stslex.atten.core.network.api.model.response.GithubAuthResponseModel +import com.stslex.atten.core.network.api.model.response.TokenResponseModel import com.stslex.atten.core.network.client.error.RefreshTokenValidator.setupResponseValidator -import com.stslex.atten.core.network.api.model.TokenResponseModel import com.stslex.atten.core.store.user.UserStore import io.ktor.client.call.body import io.ktor.client.request.bearerAuth @@ -21,7 +22,7 @@ internal class AuthApiClientImpl( private val userStore: UserStore, ) : AuthApiClient { - override suspend fun auth(token: String): TokenResponseModel = appHttpApi.request { + override suspend fun googleAuth(token: String): TokenResponseModel = appHttpApi.request { post("$AUTH_HOST/$GOOGLE_AUTH_HOST") { setBody(AuthGoogleRequest(token)) } @@ -38,6 +39,22 @@ internal class AuthApiClientImpl( .saveIntoUserStore() } + override suspend fun githubRequestAuth(): GithubAuthResponseModel = appHttpApi.requestDefault { + get("https://github.com/login/oauth/authorize") { // todo => change to PKCE with remote server + url { + parameters.append("client_id", "client_id_value") + } + }.body() + } + + override suspend fun githubAuth(code: String): TokenResponseModel = appHttpApi.request { + post("$AUTH_HOST/$GITHUB_AUTH_HOST") { + setBody(AuthGoogleRequest(code)) + } + .body() + .saveIntoUserStore() + } + private fun TokenResponseModel.saveIntoUserStore(): TokenResponseModel = apply { userStore.uuid.value = uuid userStore.refreshToken.value = refreshToken @@ -50,6 +67,7 @@ internal class AuthApiClientImpl( private const val AUTH_HOST = "auth" private const val REFRESH_HOST = "refresh" private const val GOOGLE_AUTH_HOST = "google" + private const val GITHUB_AUTH_HOST = "github" } } \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/domain/AuthInteractorImpl.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/domain/AuthInteractorImpl.kt new file mode 100644 index 0000000..e41b5c3 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/domain/AuthInteractorImpl.kt @@ -0,0 +1,33 @@ +package com.stslex.atten.feature.settings.domain + +import com.stslex.atten.core.auth.repository.AuthRepository +import com.stslex.atten.core.auth.repository.model.TokenDataModel +import com.stslex.atten.core.core.logger.Log +import com.stslex.atten.core.core.result.AppResult +import com.stslex.atten.core.core.result.AppResult.Companion.onError +import com.stslex.atten.core.core.result.AppResult.Companion.onSuccess +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import org.koin.core.annotation.Factory + +@Factory +class AuthInteractorImpl( + private val repository: AuthRepository, +) : SettingsInteractor { + + private val logger = Log.tag("AuthInteractor") + + // todo => add googleAuthController + override fun authGoogle(token: String): Flow> = flow { + emit(repository.authGoogle(token)) + } + + override fun authGithub(): Flow> = flow { + repository.requestAuthGithub() + .onError { emit(AppResult.error(it)) } + .onSuccess { + logger.v("github request: $it") + emit(repository.authGithub(it)) + } + } +} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/domain/SettingsInteractor.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/domain/SettingsInteractor.kt new file mode 100644 index 0000000..e69ce5c --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/domain/SettingsInteractor.kt @@ -0,0 +1,12 @@ +package com.stslex.atten.feature.settings.domain + +import com.stslex.atten.core.auth.repository.model.TokenDataModel +import com.stslex.atten.core.core.result.AppResult +import kotlinx.coroutines.flow.Flow + +interface SettingsInteractor { + + fun authGoogle(token: String): Flow> + + fun authGithub(): Flow> +} diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsStore.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsStore.kt index 01403cd..8f97948 100644 --- a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsStore.kt +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsStore.kt @@ -39,7 +39,10 @@ interface SettingsStore : Store { data object Back : Click @Stable - data object Login : Click + data object LoginGoogle : Click + + @Stable + data object LoginGithub : Click } @Stable diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/ClickHandler.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/ClickHandler.kt index 186b1d7..d74cabc 100644 --- a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/ClickHandler.kt +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/ClickHandler.kt @@ -2,10 +2,10 @@ package com.stslex.atten.feature.settings.mvi.handlers import com.stslex.atten.core.auth.controller.GoogleAuthController import com.stslex.atten.core.auth.model.GoogleAuthResult -import com.stslex.atten.core.auth.repository.AuthRepository import com.stslex.atten.core.core.result.ResultUtils.onSuccess import com.stslex.atten.core.ui.mvi.handler.Handler import com.stslex.atten.feature.settings.di.SettingsScope +import com.stslex.atten.feature.settings.domain.SettingsInteractor import com.stslex.atten.feature.settings.mvi.SettingsHandlerStore import com.stslex.atten.feature.settings.mvi.SettingsStore.Action import kotlinx.coroutines.Job @@ -17,8 +17,8 @@ import org.koin.core.annotation.Scoped @Scope(SettingsScope::class) @Scoped() class ClickHandler( - private val authController: GoogleAuthController, - private val repository: AuthRepository, + private val googleAuthController: GoogleAuthController, + private val interactor: SettingsInteractor, // private val store: SettingsHandlerStore ) : Handler { @@ -27,7 +27,8 @@ class ClickHandler( override fun SettingsHandlerStore.invoke(action: Action.Click) { when (action) { Action.Click.Back -> actionBack() - Action.Click.Login -> actionLogin() + Action.Click.LoginGoogle -> actionLoginGoole() + Action.Click.LoginGithub -> actionLoginGithub() } } @@ -35,11 +36,20 @@ class ClickHandler( consume(Action.Navigation.NavBack) } - private fun SettingsHandlerStore.actionLogin() { - authController.auth { result -> + private fun SettingsHandlerStore.actionLoginGithub() { + loginJob?.cancel() + loginJob = interactor.authGithub() + .onSuccess { logger.i("login github success: $it") } + .onError { logger.e(it, "login github error") } + .onLoading { logger.i("login github loading...") } + .collect(scope) + } + + private fun SettingsHandlerStore.actionLoginGoole() { + googleAuthController.auth { result -> result .onSuccess { consumeLogin(it) } - .onFailure { logger.e(it, "auth error") } + .onFailure { logger.e(it, "google auth error") } } } @@ -65,7 +75,7 @@ class ClickHandler( return } loginJob?.cancel() - loginJob = repository.auth(token) + loginJob = interactor.authGoogle(token) .onSuccess { logger.i("login success: $it") } .onError { logger.e(it, "login error") } .onLoading { logger.i("login loading...") } diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsWidget.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsWidget.kt index 7c0aeb2..e450bd3 100644 --- a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsWidget.kt +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsWidget.kt @@ -15,6 +15,7 @@ import com.stslex.atten.feature.settings.mvi.SettingsStore.Action import com.stslex.atten.feature.settings.mvi.SettingsStore.State import com.stslex.atten.feature.settings.ui.components.SettingsTopbar + @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun SettingsWidget( @@ -42,10 +43,17 @@ internal fun SettingsWidget( ) Button( - onClick = { consume(Action.Click.Login) }, + onClick = { consume(Action.Click.LoginGoogle) }, + modifier = Modifier.padding(top = 16.dp) + ) { + Text(text = "Login google") + } + + Button( + onClick = { consume(Action.Click.LoginGithub) }, modifier = Modifier.padding(top = 16.dp) ) { - Text(text = "Login") + Text(text = "Login github") } }