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 d12c79b..d2ed7d7 100644 --- a/core/auth/build.gradle.kts +++ b/core/auth/build.gradle.kts @@ -7,6 +7,8 @@ kotlin { commonMain.dependencies { 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/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/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/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..d012c9a --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/AuthRepository.kt @@ -0,0 +1,13 @@ +package com.stslex.atten.core.auth.repository + +import com.stslex.atten.core.auth.repository.model.TokenDataModel +import com.stslex.atten.core.core.result.AppResult + +interface AuthRepository { + + 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 new file mode 100644 index 0000000..dc329be --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/repository/AuthRepositoryImpl.kt @@ -0,0 +1,30 @@ +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.suspendRunCatching +import com.stslex.atten.core.network.api.AuthApiClient +import org.koin.core.annotation.Single + +@Single +internal class AuthRepositoryImpl( + private val authClient: AuthApiClient +) : AuthRepository { + + override suspend fun authGoogle( + googleToken: String + ): 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 new file mode 100644 index 0000000..7cd07de --- /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.response.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..2653c2d --- /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 cause: Throwable? = null, + override val message: String? = 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..bf7509a --- /dev/null +++ b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/AppResult.kt @@ -0,0 +1,54 @@ +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 { + + 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 + } + + companion object { + + fun success(value: T): AppResult = Success(value) + + fun error(error: AppError): AppResult = Error(error) + + fun error( + error: Throwable + ): AppResult = if (error is AppError) { + error(error) + } else { + error(UnresolveError(error)) + } + + 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() + } + + inline fun AppResult.onSuccess( + action: (T) -> Unit + ): AppResult = this.apply { + (this as? Success)?.let { action(it.data) } + } + + } +} 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..9b9f2f5 --- /dev/null +++ b/core/core/src/commonMain/kotlin/com/stslex/atten/core/core/result/ResultUtils.kt @@ -0,0 +1,126 @@ +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( + flow = this, + onError = { onError(UnresolveError(it, it.message)) }) { 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, error.message)) + } + } + ) + 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 + ) + ) + } + } + ) + + 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/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..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 @@ -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 { @@ -20,4 +20,11 @@ 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 25f073e..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 @@ -16,12 +17,12 @@ import org.koin.core.annotation.Singleton @Single @Singleton -class AuthApiClientImpl( +internal class AuthApiClientImpl( private val appHttpApi: AppHttpApi, 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 @@ 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 @@ 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 51f5b49..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 @@ -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.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 import org.koin.core.annotation.Factory import org.koin.core.annotation.Scope import org.koin.core.annotation.Scoped @@ -13,13 +17,18 @@ import org.koin.core.annotation.Scoped @Scope(SettingsScope::class) @Scoped() class ClickHandler( - private val authController: GoogleAuthController + private val googleAuthController: GoogleAuthController, + private val interactor: SettingsInteractor, +// private val store: SettingsHandlerStore ) : Handler { + private var loginJob: Job? = null + 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() } } @@ -27,11 +36,49 @@ 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 { logger.i("success: $it") } - .onFailure { logger.e(it, "auth error") } + .onSuccess { consumeLogin(it) } + .onFailure { logger.e(it, "google auth error") } + } + } + + 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 + return } + loginJob?.cancel() + loginJob = interactor.authGoogle(token) + .onSuccess { logger.i("login success: $it") } + .onError { logger.e(it, "login error") } + .onLoading { logger.i("login loading...") } + .collect(scope) } } \ No newline at end of file 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") } } 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